mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
Enable strict TypeScript in the Bootstrap port and add the bootstrap-component skill (#3044)
This commit is contained in:
1 parent
107e493275
commit
1ffbeeca42
32 files changed
+643
-189
No files matched your search
@@ -30,6 +30,7 @@ Task-specific instructions live in `.agents/skills/<name>/SKILL.md` — read the
|
|||||||
| `shared-lib` | moving logic into `shared/lib/` and testing it |
|
| `shared-lib` | moving logic into `shared/lib/` and testing it |
|
||||||
| `core-scss` | writing or changing styles in `core/scss/` |
|
| `core-scss` | writing or changing styles in `core/scss/` |
|
||||||
| `core-js` | working on the framework JavaScript in `core/js/` |
|
| `core-js` | working on the framework JavaScript in `core/js/` |
|
||||||
|
| `bootstrap-component` | writing a new component class in `core/js/src/bootstrap/` (typed config, events, Data API) |
|
||||||
| `demo-pages` | adding or updating a page in `preview/pages/` |
|
| `demo-pages` | adding or updating a page in `preview/pages/` |
|
||||||
| `write-docs` | writing or updating a page in `docs/content/` |
|
| `write-docs` | writing or updating a page in `docs/content/` |
|
||||||
| `class-reference` | a docs page needs its `classnames` table added or refreshed |
|
| `class-reference` | a docs page needs its `classnames` table added or refreshed |
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
---
|
||||||
|
name: bootstrap-component
|
||||||
|
description: >-
|
||||||
|
Write a new Bootstrap-style JavaScript component class in `core/js/src/bootstrap/` — a `BaseComponent` subclass with typed config, `EVENT_*`/`CLASS_NAME_*` constants and a `data-bs-toggle` Data API. Use whenever the user asks for a new component in the Bootstrap port (a toggler, a dialog, a chip input, a stepper) or
|
||||||
|
wants an existing one brought up to this baseline. Covers file structure, TypeScript typing of `Default` and `DefaultType`, constructor and config merging, events, the Data API handler and the minimal public API. Not for the build, the tests or the size budget — that is the `core-js` skill.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Rules for writing new components
|
||||||
|
|
||||||
|
You are a master of JavaScript and TypeScript.
|
||||||
|
|
||||||
|
The points below are the baseline for every new component class in `core/js/src/bootstrap/`. Read the `core-js` skill first for what that directory is (a vendored port of Bootstrap's JavaScript), how it is built, tested and size-budgeted.
|
||||||
|
|
||||||
|
## 1. File structure
|
||||||
|
|
||||||
|
In this order:
|
||||||
|
|
||||||
|
1. Header with the MIT license comment and the file name (`Bootstrap toggler.ts`).
|
||||||
|
2. Imports: `BaseComponent`, `EventHandler`, optionally `Manipulator`, `SelectorEngine`, helpers from `./util/*`.
|
||||||
|
3. Types and interfaces at the top (`ComponentConfig`, `ComponentConfigInput`).
|
||||||
|
4. Constants: `NAME`, `DATA_KEY`, `EVENT_KEY`, selectors, classes, event names.
|
||||||
|
5. `Default` and `DefaultType` with TS types.
|
||||||
|
6. The component class.
|
||||||
|
7. "Data API implementation" section (when applicable).
|
||||||
|
8. Default export of the class.
|
||||||
|
|
||||||
|
## 2. Typing
|
||||||
|
|
||||||
|
- Define `type ComponentConfig` and `type ComponentConfigInput`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type ComponentConfig = {
|
||||||
|
attribute: string
|
||||||
|
value: string | number | boolean | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComponentConfigInput = Partial<ComponentConfig> & Record<string, unknown>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Type `Default` as `ComponentConfig`.
|
||||||
|
- Type `DefaultType` as `Record<keyof ComponentConfig, string>`.
|
||||||
|
- In the class, declare:
|
||||||
|
- `declare _element: HTMLElement` (or a more specific element type).
|
||||||
|
- `declare _config: ComponentConfig`.
|
||||||
|
- `declare _isTransitioning: boolean` (when needed).
|
||||||
|
- Type method signatures explicitly (`show(): void`, `hide(): void`).
|
||||||
|
- `EventHandler.trigger(...)` can return `null` — use `?.defaultPrevented`.
|
||||||
|
- Use `Event` for handlers, and cast when you need specifics (`const keyboardEvent = event as KeyboardEvent`). `EventHandler.on` is generic, so `(event: KeyboardEvent) => …` type-checks without a cast; for `event.delegateTarget` cast to `DelegatedEvent` from `./dom/event-handler`.
|
||||||
|
- `core/tsconfig.json` is `strict` and `core/js/src` has no `any` at all, so a missing null check or an implicit `any` fails `pnpm run type-check`. Reach for `unknown` plus a type guard, or a small named type, instead of `any`, `[key: string]: any` or `as any`.
|
||||||
|
- Prefer modern syntax like `#private` class fields over the `private` keyword.
|
||||||
|
|
||||||
|
## 3. Constructor and config
|
||||||
|
|
||||||
|
- Call `super(element, config as Record<string, unknown>)`.
|
||||||
|
- Read config from `Manipulator.getDataAttributes(...)` and merge with `Default`. `BaseComponent._getConfig` already does this through `_mergeConfigObj` → `_configAfterMerge` → `_typeCheckConfig`.
|
||||||
|
- If you need custom config processing, override `_getConfig` (or `_configAfterMerge`).
|
||||||
|
- After constructing, only set required fields and attach listeners.
|
||||||
|
|
||||||
|
## 4. Events and CSS classes
|
||||||
|
|
||||||
|
- Use constants in the format `EVENT_${NAME}` and `CLASS_NAME_*`.
|
||||||
|
- Build events with `EVENT_KEY` and `DATA_KEY` (as in `alert.ts`):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const NAME = 'toggler'
|
||||||
|
const DATA_KEY = 'bs.toggler'
|
||||||
|
const EVENT_KEY = `.${DATA_KEY}`
|
||||||
|
|
||||||
|
const EVENT_TOGGLE = `toggle${EVENT_KEY}`
|
||||||
|
const EVENT_TOGGLED = `toggled${EVENT_KEY}`
|
||||||
|
const CLASS_NAME_SHOW = 'show'
|
||||||
|
```
|
||||||
|
|
||||||
|
- Fire a cancellable `before` event, do the work, then fire the `after` event.
|
||||||
|
- Use only `EventHandler.on/off/one/trigger`.
|
||||||
|
|
||||||
|
## 5. Data API
|
||||||
|
|
||||||
|
- Selector in the form `[data-bs-toggle="name"]`. The attribute API stays `data-bs-*`; `data-tblr-*` is a 2.0 change (see `core-js`).
|
||||||
|
- Data API handler:
|
||||||
|
- check `A/AREA` and `event.preventDefault()`.
|
||||||
|
- use `SelectorEngine.getElementFromSelector`.
|
||||||
|
- `Name.getOrCreateInstance(target, config)` for data attributes.
|
||||||
|
- Use the helpers from `./util/component-functions` instead of writing the handler by hand:
|
||||||
|
- `eventActionOnPlugin(Component, 'click', SELECTOR_DATA_TOGGLE, 'toggle')` for a `data-bs-toggle` — resolves the targets (`data-bs-target` / `href`, or the trigger itself), skips `.disabled` / `:disabled`, prevents the default on `A/AREA`, and calls the method on `getOrCreateInstance` of every target. An optional
|
||||||
|
fifth argument receives `{ targets, event, instances }`.
|
||||||
|
- `enableDismissTrigger(Component, 'close')` for a `data-bs-dismiss`.
|
||||||
|
- Write the `EventHandler.on(document, ...)` handler yourself only when the trigger needs config from data attributes or other custom logic.
|
||||||
|
|
||||||
|
## 6. Initialization and public API
|
||||||
|
|
||||||
|
- Minimal public API: `show`, `hide`, `toggle`, `dispose`.
|
||||||
|
- If the component has animation state, use `_queueCallback` and `_isAnimated`.
|
||||||
|
- Expose `static get NAME`, `static get Default`, `static get DefaultType`.
|
||||||
|
- Export the class from `core/js/src/bootstrap.ts` (named export and the `bootstrap` namespace object), otherwise it never ships.
|
||||||
|
|
||||||
|
## 7. Style and consistency
|
||||||
|
|
||||||
|
- Keep style consistent with existing components (indentation, naming, comments, the `// Getters` / `// Public` / `// Private` section comments).
|
||||||
|
- Avoid direct DOM operations outside `EventHandler` and `SelectorEngine`, unless the component requires otherwise.
|
||||||
|
|
||||||
|
## 8. Template
|
||||||
|
|
||||||
|
```ts
|
||||||
|
/**
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
* Bootstrap toggler.ts
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
import BaseComponent from './base-component'
|
||||||
|
import EventHandler from './dom/event-handler'
|
||||||
|
import { eventActionOnPlugin } from './util/component-functions'
|
||||||
|
|
||||||
|
type ComponentConfig = {
|
||||||
|
attribute: string
|
||||||
|
value: string | number | boolean | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComponentConfigInput = Partial<ComponentConfig> & Record<string, unknown>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants
|
||||||
|
*/
|
||||||
|
|
||||||
|
const NAME = 'toggler'
|
||||||
|
const DATA_KEY = 'bs.toggler'
|
||||||
|
const EVENT_KEY = `.${DATA_KEY}`
|
||||||
|
|
||||||
|
const EVENT_TOGGLE = `toggle${EVENT_KEY}`
|
||||||
|
const EVENT_TOGGLED = `toggled${EVENT_KEY}`
|
||||||
|
const EVENT_CLICK = 'click'
|
||||||
|
|
||||||
|
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="toggler"]'
|
||||||
|
|
||||||
|
const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||||
|
attribute: 'string',
|
||||||
|
value: '(string|number|boolean)',
|
||||||
|
}
|
||||||
|
|
||||||
|
const Default: ComponentConfig = {
|
||||||
|
attribute: 'class',
|
||||||
|
value: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class definition
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Toggler extends BaseComponent {
|
||||||
|
declare _element: HTMLElement
|
||||||
|
declare _config: ComponentConfig
|
||||||
|
|
||||||
|
constructor(element?: Element | string | null, config?: ComponentConfigInput | null) {
|
||||||
|
super(element as string | HTMLElement, (config ?? undefined) as Record<string, unknown>)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
static get Default(): ComponentConfig {
|
||||||
|
return Default
|
||||||
|
}
|
||||||
|
|
||||||
|
static get DefaultType(): Record<keyof ComponentConfig, string> {
|
||||||
|
return DefaultType
|
||||||
|
}
|
||||||
|
|
||||||
|
static get NAME(): string {
|
||||||
|
return NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public
|
||||||
|
toggle(): void {
|
||||||
|
const toggleEvent = EventHandler.trigger(this._element, EVENT_TOGGLE)
|
||||||
|
|
||||||
|
if (toggleEvent?.defaultPrevented) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this._execute()
|
||||||
|
|
||||||
|
EventHandler.trigger(this._element, EVENT_TOGGLED)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private
|
||||||
|
_execute(): void {
|
||||||
|
const { attribute, value } = this._config
|
||||||
|
|
||||||
|
if (attribute === 'class') {
|
||||||
|
if (value) {
|
||||||
|
this._element.classList.toggle(String(value))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare as strings since getAttribute() always returns a string
|
||||||
|
if (this._element.getAttribute(attribute) === String(value)) {
|
||||||
|
this._element.removeAttribute(attribute)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this._element.setAttribute(attribute, String(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data API implementation
|
||||||
|
*/
|
||||||
|
|
||||||
|
eventActionOnPlugin(Toggler, EVENT_CLICK, SELECTOR_DATA_TOGGLE, 'toggle')
|
||||||
|
|
||||||
|
export default Toggler
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Checklist
|
||||||
|
|
||||||
|
- [ ] File order as in §1, MIT header with the file name
|
||||||
|
- [ ] `ComponentConfig` / `ComponentConfigInput` defined; `Default` and `DefaultType` typed
|
||||||
|
- [ ] `declare _element` / `declare _config` in the class, explicit return types
|
||||||
|
- [ ] `EVENT_*` and `CLASS_NAME_*` constants, `?.defaultPrevented` on cancellable events
|
||||||
|
- [ ] Data API on `[data-bs-toggle="name"]` through `eventActionOnPlugin` (or a hand-written handler with the `A/AREA` guard and `getOrCreateInstance`)
|
||||||
|
- [ ] Public API limited to `show` / `hide` / `toggle` / `dispose`
|
||||||
|
- [ ] Exported from `core/js/src/bootstrap.ts`
|
||||||
|
- [ ] Spec in `js/tests/unit/`, and the `core-js` checklist (type-check, prettier, bundlewatch)
|
||||||
@@ -1,13 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: core-js
|
name: core-js
|
||||||
description: >-
|
description: >-
|
||||||
Work on the framework's own JavaScript in `core/js/` — the `tabler.js` and
|
Work on the framework's own JavaScript in `core/js/` — the `tabler.js` and `tabler-theme.js` bundles, the vendored Bootstrap component port, and their browser tests. Use when a Tabler behaviour needs adding or fixing (a `data-bs-toggle` handler, the theme switcher, a plugin initialiser), when a Bootstrap component's
|
||||||
`tabler-theme.js` bundles, the vendored Bootstrap component port, and their
|
JS misbehaves, and before changing anything under `core/js/src/`. Not for demo-page scripts — that is the `astro-scripts` skill.
|
||||||
browser tests. Use when a Tabler behaviour needs adding or fixing (a
|
|
||||||
`data-bs-toggle` handler, the theme switcher, a plugin initialiser), when a
|
|
||||||
Bootstrap component's JS misbehaves, and before changing anything under
|
|
||||||
`core/js/src/`. Not for demo-page scripts — that is the `astro-scripts`
|
|
||||||
skill.
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# The framework JavaScript
|
# The framework JavaScript
|
||||||
@@ -17,7 +12,7 @@ description: >-
|
|||||||
## 1. Two entry points
|
## 1. Two entry points
|
||||||
|
|
||||||
| Entry | Ships as | Holds |
|
| Entry | Ships as | Holds |
|
||||||
| --- | --- | --- |
|
| -------------------- | ---------------------------------- | ---------------------------------------------------------------------------- |
|
||||||
| `js/tabler.ts` | `tabler.js` / `.esm.js` (+ `.min`) | plugin initialisers, the Bootstrap components, the `tabler` helper namespace |
|
| `js/tabler.ts` | `tabler.js` / `.esm.js` (+ `.min`) | plugin initialisers, the Bootstrap components, the `tabler` helper namespace |
|
||||||
| `js/tabler-theme.ts` | `tabler-theme.js` (+ variants) | the colour-mode/theme switcher only |
|
| `js/tabler-theme.ts` | `tabler-theme.js` (+ variants) | the colour-mode/theme switcher only |
|
||||||
|
|
||||||
@@ -29,6 +24,7 @@ That directory is Bootstrap's JavaScript rewritten in TypeScript, MIT headers ke
|
|||||||
|
|
||||||
- Fix bugs the way upstream did, and keep the file's structure recognisable against `twbs/bootstrap`. Gratuitous restructuring makes the next upstream sync expensive.
|
- Fix bugs the way upstream did, and keep the file's structure recognisable against `twbs/bootstrap`. Gratuitous restructuring makes the next upstream sync expensive.
|
||||||
- `js/src/bootstrap.ts` is the single source of truth for what is exported and for the `bootstrap` namespace object.
|
- `js/src/bootstrap.ts` is the single source of truth for what is exported and for the `bootstrap` namespace object.
|
||||||
|
- A new or reworked component class follows the `bootstrap-component` skill (file order, typed config, Data API).
|
||||||
- Coverage is configured to measure exactly this directory (`js/src/bootstrap/**`), and `js/tests/unit/*.spec.ts` mirrors upstream's suite — a change here is expected to come with its test.
|
- Coverage is configured to measure exactly this directory (`js/src/bootstrap/**`), and `js/tests/unit/*.spec.ts` mirrors upstream's suite — a change here is expected to come with its test.
|
||||||
|
|
||||||
## 3. Tabler's own modules
|
## 3. Tabler's own modules
|
||||||
@@ -75,7 +71,7 @@ pnpm --filter @tabler/core test:js:coverage
|
|||||||
`vite` builds each entry as a library in `es` + `umd` (`BASE_NAME` selects the entry), then terser produces the `.min` variants with source maps. Sizes are enforced:
|
`vite` builds each entry as a library in `es` + `umd` (`BASE_NAME` selects the entry), then terser produces the `.min` variants with source maps. Sizes are enforced:
|
||||||
|
|
||||||
| File | Limit |
|
| File | Limit |
|
||||||
| --- | --- |
|
| ------------------------- | ----- |
|
||||||
| `dist/js/tabler.js` | 64 kB |
|
| `dist/js/tabler.js` | 64 kB |
|
||||||
| `dist/js/tabler.min.js` | 48 kB |
|
| `dist/js/tabler.min.js` | 48 kB |
|
||||||
| `dist/js/tabler-theme.js` | 1 kB |
|
| `dist/js/tabler-theme.js` | 1 kB |
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@tabler/core": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Updated the Bootstrap component port to strict TypeScript with typed `ComponentConfig` types and added `eventActionOnPlugin`.
|
||||||
@@ -49,6 +49,7 @@ sri.json
|
|||||||
# Test coverage
|
# Test coverage
|
||||||
coverage/
|
coverage/
|
||||||
__screenshots__/
|
__screenshots__/
|
||||||
|
.vitest-attachments/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
.claude/launch.json
|
.claude/launch.json
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
@@ -58,7 +58,7 @@ class BaseComponent extends Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static getOrCreateInstance(element: ElementSelector, config: ComponentConfig = {}): BaseComponent {
|
static getOrCreateInstance(element: ElementSelector, config: ComponentConfig = {}): BaseComponent {
|
||||||
return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null)
|
return this.getInstance(element) || new this(element, typeof config === 'object' ? config : undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
static get VERSION(): string {
|
static get VERSION(): string {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="button"], [data-tblr-toggle="butt
|
|||||||
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
|
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
|
||||||
|
|
||||||
class Button extends BaseComponent {
|
class Button extends BaseComponent {
|
||||||
|
declare _element: HTMLElement
|
||||||
|
|
||||||
static get NAME(): string {
|
static get NAME(): string {
|
||||||
return NAME
|
return NAME
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,18 @@ import Manipulator from './dom/manipulator'
|
|||||||
import SelectorEngine from './dom/selector-engine'
|
import SelectorEngine from './dom/selector-engine'
|
||||||
import { getNextActiveElement, isRTL, isVisible, reflow, triggerTransitionEnd } from './util/index'
|
import { getNextActiveElement, isRTL, isVisible, reflow, triggerTransitionEnd } from './util/index'
|
||||||
import Swipe from './util/swipe'
|
import Swipe from './util/swipe'
|
||||||
import type { ComponentConfig, ComponentConfigType } from './types'
|
|
||||||
|
type ComponentConfig = {
|
||||||
|
interval: number | boolean
|
||||||
|
keyboard: boolean
|
||||||
|
pause: 'hover' | boolean
|
||||||
|
ride: boolean | 'carousel'
|
||||||
|
touch: boolean
|
||||||
|
wrap: boolean
|
||||||
|
defaultInterval?: number | boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComponentConfigInput = Partial<Omit<ComponentConfig, 'defaultInterval'>> & Record<string, unknown>
|
||||||
|
|
||||||
const NAME = 'carousel'
|
const NAME = 'carousel'
|
||||||
const DATA_KEY = 'bs.carousel'
|
const DATA_KEY = 'bs.carousel'
|
||||||
@@ -66,7 +77,7 @@ const Default: ComponentConfig = {
|
|||||||
wrap: true,
|
wrap: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultType: ComponentConfigType = {
|
const DefaultType: Record<Exclude<keyof ComponentConfig, 'defaultInterval'>, string> = {
|
||||||
interval: '(number|boolean)',
|
interval: '(number|boolean)',
|
||||||
keyboard: 'boolean',
|
keyboard: 'boolean',
|
||||||
pause: '(string|boolean)',
|
pause: '(string|boolean)',
|
||||||
@@ -76,6 +87,8 @@ const DefaultType: ComponentConfigType = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Carousel extends BaseComponent {
|
class Carousel extends BaseComponent {
|
||||||
|
declare _element: HTMLElement
|
||||||
|
declare _config: ComponentConfig
|
||||||
_interval: ReturnType<typeof setInterval> | null
|
_interval: ReturnType<typeof setInterval> | null
|
||||||
_activeElement: HTMLElement | null
|
_activeElement: HTMLElement | null
|
||||||
_isSliding: boolean
|
_isSliding: boolean
|
||||||
@@ -83,7 +96,7 @@ class Carousel extends BaseComponent {
|
|||||||
_swipeHelper: Swipe | null
|
_swipeHelper: Swipe | null
|
||||||
_indicatorsElement: HTMLElement | null
|
_indicatorsElement: HTMLElement | null
|
||||||
|
|
||||||
constructor(element: HTMLElement | string, config?: Partial<ComponentConfig>) {
|
constructor(element: HTMLElement | string, config?: ComponentConfigInput) {
|
||||||
super(element, config)
|
super(element, config)
|
||||||
|
|
||||||
this._interval = null
|
this._interval = null
|
||||||
@@ -104,7 +117,7 @@ class Carousel extends BaseComponent {
|
|||||||
return Default
|
return Default
|
||||||
}
|
}
|
||||||
|
|
||||||
static get DefaultType(): ComponentConfigType {
|
static get DefaultType(): Record<Exclude<keyof ComponentConfig, 'defaultInterval'>, string> {
|
||||||
return DefaultType
|
return DefaultType
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,7 +286,7 @@ class Carousel extends BaseComponent {
|
|||||||
|
|
||||||
const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval') || element.getAttribute('data-tblr-interval') || '', 10)
|
const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval') || element.getAttribute('data-tblr-interval') || '', 10)
|
||||||
|
|
||||||
this._config.interval = elementInterval || this._config.defaultInterval
|
this._config.interval = elementInterval || (this._config.defaultInterval ?? this._config.interval)
|
||||||
}
|
}
|
||||||
|
|
||||||
_slide(order: string, element: HTMLElement | null = null): void {
|
_slide(order: string, element: HTMLElement | null = null): void {
|
||||||
@@ -302,7 +315,7 @@ class Carousel extends BaseComponent {
|
|||||||
|
|
||||||
const slideEvent = triggerEvent(EVENT_SLIDE)
|
const slideEvent = triggerEvent(EVENT_SLIDE)
|
||||||
|
|
||||||
if (slideEvent.defaultPrevented) {
|
if (slideEvent?.defaultPrevented) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,15 @@ import BaseComponent from './base-component'
|
|||||||
import EventHandler from './dom/event-handler'
|
import EventHandler from './dom/event-handler'
|
||||||
import SelectorEngine from './dom/selector-engine'
|
import SelectorEngine from './dom/selector-engine'
|
||||||
import { getElement, reflow } from './util/index'
|
import { getElement, reflow } from './util/index'
|
||||||
import type { ComponentConfig, ComponentConfigType, ElementSelector } from './types'
|
import type { DelegatedEvent } from './dom/event-handler'
|
||||||
|
import type { ElementSelector } from './types'
|
||||||
|
|
||||||
|
type ComponentConfig = {
|
||||||
|
parent: HTMLElement | null
|
||||||
|
toggle: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComponentConfigInput = Partial<Omit<ComponentConfig, 'parent'>> & { parent?: HTMLElement | string | null } & Record<string, unknown>
|
||||||
|
|
||||||
const NAME = 'collapse'
|
const NAME = 'collapse'
|
||||||
const DATA_KEY = 'bs.collapse'
|
const DATA_KEY = 'bs.collapse'
|
||||||
@@ -40,16 +48,18 @@ const Default: ComponentConfig = {
|
|||||||
toggle: true,
|
toggle: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultType: ComponentConfigType = {
|
const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||||
parent: '(null|element)',
|
parent: '(null|element)',
|
||||||
toggle: 'boolean',
|
toggle: 'boolean',
|
||||||
}
|
}
|
||||||
|
|
||||||
class Collapse extends BaseComponent {
|
class Collapse extends BaseComponent {
|
||||||
|
declare _element: HTMLElement
|
||||||
|
declare _config: ComponentConfig
|
||||||
_isTransitioning: boolean
|
_isTransitioning: boolean
|
||||||
_triggerArray: HTMLElement[]
|
_triggerArray: HTMLElement[]
|
||||||
|
|
||||||
constructor(element: ElementSelector, config?: ComponentConfig) {
|
constructor(element: ElementSelector, config?: ComponentConfigInput) {
|
||||||
super(element, config)
|
super(element, config)
|
||||||
|
|
||||||
this._isTransitioning = false
|
this._isTransitioning = false
|
||||||
@@ -81,7 +91,7 @@ class Collapse extends BaseComponent {
|
|||||||
return Default
|
return Default
|
||||||
}
|
}
|
||||||
|
|
||||||
static get DefaultType(): ComponentConfigType {
|
static get DefaultType(): Record<keyof ComponentConfig, string> {
|
||||||
return DefaultType
|
return DefaultType
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +250,8 @@ class Collapse extends BaseComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (this: HTMLElement, event: Event) {
|
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (this: HTMLElement, event: Event) {
|
||||||
if ((event.target as HTMLElement).tagName === 'A' || ((event as any).delegateTarget && (event as any).delegateTarget.tagName === 'A')) {
|
const { delegateTarget } = event as DelegatedEvent
|
||||||
|
if ((event.target as HTMLElement).tagName === 'A' || (delegateTarget && delegateTarget.tagName === 'A')) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
* --------------------------------------------------------------------------
|
* --------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type EventCallback = (this: EventTarget, ...args: unknown[]) => void
|
// `this` is the element the handler was bound to, or the delegate target
|
||||||
|
export type EventCallback<E extends Event = Event> = (this: HTMLElement, event: E) => void
|
||||||
|
|
||||||
|
export type DelegatedEvent<E extends Event = Event> = E & { delegateTarget: HTMLElement }
|
||||||
|
|
||||||
interface BootstrapHandler {
|
interface BootstrapHandler {
|
||||||
(event: Event): void
|
(event: Event): void
|
||||||
@@ -99,7 +102,7 @@ function bootstrapHandler(element: EventTarget, fn: EventCallback): BootstrapHan
|
|||||||
EventHandler.off(element, event.type, fn)
|
EventHandler.off(element, event.type, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fn.apply(element, [event])
|
return fn.apply(element as HTMLElement, [event])
|
||||||
} as BootstrapHandler
|
} as BootstrapHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +122,7 @@ function bootstrapDelegationHandler(element: EventTarget, selector: string, fn:
|
|||||||
EventHandler.off(element, event.type, selector, fn)
|
EventHandler.off(element, event.type, selector, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fn.apply(target, [event])
|
return fn.apply(target as HTMLElement, [event])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} as BootstrapHandler
|
} as BootstrapHandler
|
||||||
@@ -129,7 +132,7 @@ function findHandler(events: Record<string | number, BootstrapHandler>, callable
|
|||||||
return Object.values(events).find((event) => event.callable === callable && event.delegationSelector === delegationSelector)
|
return Object.values(events).find((event) => event.callable === callable && event.delegationSelector === delegationSelector)
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeParameters(originalTypeEvent: string, handler: string | EventCallback | undefined, delegationFunction: EventCallback | undefined): [boolean, EventCallback, string] {
|
function normalizeParameters(originalTypeEvent: string, handler: string | false | EventCallback | undefined, delegationFunction: EventCallback | undefined): [boolean, EventCallback, string] {
|
||||||
const isDelegated = typeof handler === 'string'
|
const isDelegated = typeof handler === 'string'
|
||||||
const callable = isDelegated ? delegationFunction! : (handler || delegationFunction)!
|
const callable = isDelegated ? delegationFunction! : (handler || delegationFunction)!
|
||||||
let typeEvent = getTypeEvent(originalTypeEvent)
|
let typeEvent = getTypeEvent(originalTypeEvent)
|
||||||
@@ -141,7 +144,7 @@ function normalizeParameters(originalTypeEvent: string, handler: string | EventC
|
|||||||
return [isDelegated, callable, typeEvent]
|
return [isDelegated, callable, typeEvent]
|
||||||
}
|
}
|
||||||
|
|
||||||
function addHandler(element: EventTarget | null, originalTypeEvent: string, handler: string | EventCallback | undefined, delegationFunction: EventCallback | undefined, oneOff: boolean): void {
|
function addHandler(element: EventTarget | null, originalTypeEvent: string, handler: string | false | EventCallback | undefined, delegationFunction: EventCallback | undefined, oneOff: boolean): void {
|
||||||
if (typeof originalTypeEvent !== 'string' || !element) {
|
if (typeof originalTypeEvent !== 'string' || !element) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -150,10 +153,10 @@ function addHandler(element: EventTarget | null, originalTypeEvent: string, hand
|
|||||||
|
|
||||||
if (originalTypeEvent in customEvents) {
|
if (originalTypeEvent in customEvents) {
|
||||||
const wrapFunction = (fn: EventCallback): EventCallback => {
|
const wrapFunction = (fn: EventCallback): EventCallback => {
|
||||||
return function (this: EventTarget, event: unknown) {
|
return function (this: EventTarget, event: Event) {
|
||||||
const evt = event as MouseEvent & { delegateTarget: HTMLElement }
|
const evt = event as DelegatedEvent<MouseEvent>
|
||||||
if (!evt.relatedTarget || (evt.relatedTarget !== evt.delegateTarget && !evt.delegateTarget.contains(evt.relatedTarget as Node))) {
|
if (!evt.relatedTarget || (evt.relatedTarget !== evt.delegateTarget && !evt.delegateTarget.contains(evt.relatedTarget as Node))) {
|
||||||
return fn.call(this, event)
|
return fn.call(this as HTMLElement, event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,20 +213,20 @@ function getTypeEvent(event: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EventHandler = {
|
const EventHandler = {
|
||||||
on(element: EventTarget | null, event: string, handler: string | EventCallback, delegationFunction?: EventCallback): void {
|
on<E extends Event = Event>(element: EventTarget | null, event: string, handler: string | false | EventCallback<E>, delegationFunction?: EventCallback<E>): void {
|
||||||
addHandler(element, event, handler, delegationFunction, false)
|
addHandler(element, event, handler as string | false | EventCallback, delegationFunction as EventCallback | undefined, false)
|
||||||
},
|
},
|
||||||
|
|
||||||
one(element: EventTarget | null, event: string, handler: string | EventCallback, delegationFunction?: EventCallback): void {
|
one<E extends Event = Event>(element: EventTarget | null, event: string, handler: string | false | EventCallback<E>, delegationFunction?: EventCallback<E>): void {
|
||||||
addHandler(element, event, handler, delegationFunction, true)
|
addHandler(element, event, handler as string | false | EventCallback, delegationFunction as EventCallback | undefined, true)
|
||||||
},
|
},
|
||||||
|
|
||||||
off(element: EventTarget | null, originalTypeEvent: string, handler?: string | EventCallback, delegationFunction?: EventCallback): void {
|
off<E extends Event = Event>(element: EventTarget | null, originalTypeEvent: string, handler?: string | false | EventCallback<E>, delegationFunction?: EventCallback<E>): void {
|
||||||
if (typeof originalTypeEvent !== 'string' || !element) {
|
if (typeof originalTypeEvent !== 'string' || !element) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)
|
const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler as string | false | EventCallback | undefined, delegationFunction as EventCallback | undefined)
|
||||||
const inNamespace = typeEvent !== originalTypeEvent
|
const inNamespace = typeEvent !== originalTypeEvent
|
||||||
const events = getElementEvents(element)
|
const events = getElementEvents(element)
|
||||||
const storeElementEvent = events[typeEvent] || {}
|
const storeElementEvent = events[typeEvent] || {}
|
||||||
|
|||||||
@@ -11,7 +11,24 @@ import EventHandler from './dom/event-handler'
|
|||||||
import Manipulator from './dom/manipulator'
|
import Manipulator from './dom/manipulator'
|
||||||
import SelectorEngine from './dom/selector-engine'
|
import SelectorEngine from './dom/selector-engine'
|
||||||
import { execute, getElement, getNextActiveElement, isDisabled, isElement, isRTL, isVisible, noop } from './util/index'
|
import { execute, getElement, getNextActiveElement, isDisabled, isElement, isRTL, isVisible, noop } from './util/index'
|
||||||
import type { ComponentConfig, ComponentConfigType } from './types'
|
import type { DelegatedEvent } from './dom/event-handler'
|
||||||
|
|
||||||
|
type PopperOffsetData = { placement: Popper.Placement; reference: Popper.Rect; popper: Popper.Rect }
|
||||||
|
type PopperOffsetFunction = (popperData: PopperOffsetData) => number[]
|
||||||
|
type PopperConfigFunction = (defaultConfig: Partial<Popper.Options>) => Partial<Popper.Options>
|
||||||
|
|
||||||
|
type ComponentConfig = {
|
||||||
|
autoClose: boolean | 'inside' | 'outside'
|
||||||
|
boundary: Popper.Boundary
|
||||||
|
display: 'dynamic' | 'static'
|
||||||
|
offset: number[] | string | ((popperData: PopperOffsetData, element: HTMLElement) => number[])
|
||||||
|
popperConfig: Partial<Popper.Options> | PopperConfigFunction | null
|
||||||
|
reference: 'toggle' | 'parent' | HTMLElement | Popper.VirtualElement
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComponentConfigInput = Partial<ComponentConfig> & Record<string, unknown>
|
||||||
|
|
||||||
|
type RelatedTarget = { relatedTarget: HTMLElement; clickEvent?: Event }
|
||||||
|
|
||||||
const NAME = 'dropdown'
|
const NAME = 'dropdown'
|
||||||
const DATA_KEY = 'bs.dropdown'
|
const DATA_KEY = 'bs.dropdown'
|
||||||
@@ -64,7 +81,7 @@ const Default: ComponentConfig = {
|
|||||||
reference: 'toggle',
|
reference: 'toggle',
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultType: ComponentConfigType = {
|
const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||||
autoClose: '(boolean|string)',
|
autoClose: '(boolean|string)',
|
||||||
boundary: '(string|element)',
|
boundary: '(string|element)',
|
||||||
display: 'string',
|
display: 'string',
|
||||||
@@ -74,12 +91,14 @@ const DefaultType: ComponentConfigType = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Dropdown extends BaseComponent {
|
class Dropdown extends BaseComponent {
|
||||||
|
declare _element: HTMLElement
|
||||||
|
declare _config: ComponentConfig
|
||||||
_popper: Popper.Instance | null
|
_popper: Popper.Instance | null
|
||||||
_parent: HTMLElement
|
_parent: HTMLElement
|
||||||
_menu: HTMLElement
|
_menu: HTMLElement
|
||||||
_inNavbar: boolean
|
_inNavbar: boolean
|
||||||
|
|
||||||
constructor(element: HTMLElement | string, config?: Partial<ComponentConfig>) {
|
constructor(element: HTMLElement | string, config?: ComponentConfigInput) {
|
||||||
super(element, config)
|
super(element, config)
|
||||||
|
|
||||||
this._popper = null
|
this._popper = null
|
||||||
@@ -92,7 +111,7 @@ class Dropdown extends BaseComponent {
|
|||||||
return Default
|
return Default
|
||||||
}
|
}
|
||||||
|
|
||||||
static get DefaultType(): ComponentConfigType {
|
static get DefaultType(): Record<keyof ComponentConfig, string> {
|
||||||
return DefaultType
|
return DefaultType
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,14 +134,14 @@ class Dropdown extends BaseComponent {
|
|||||||
|
|
||||||
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, relatedTarget)
|
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, relatedTarget)
|
||||||
|
|
||||||
if (showEvent.defaultPrevented) {
|
if (showEvent?.defaultPrevented) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this._createPopper()
|
this._createPopper()
|
||||||
|
|
||||||
if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {
|
if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {
|
||||||
for (const element of [].concat(...(document.body.children as any))) {
|
for (const element of Array.from(document.body.children)) {
|
||||||
EventHandler.on(element, 'mouseover', noop)
|
EventHandler.on(element, 'mouseover', noop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,14 +181,14 @@ class Dropdown extends BaseComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_completeHide(relatedTarget: Record<string, any>): void {
|
_completeHide(relatedTarget: RelatedTarget): void {
|
||||||
const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, relatedTarget)
|
const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, relatedTarget)
|
||||||
if (hideEvent.defaultPrevented) {
|
if (hideEvent?.defaultPrevented) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('ontouchstart' in document.documentElement) {
|
if ('ontouchstart' in document.documentElement) {
|
||||||
for (const element of [].concat(...(document.body.children as any))) {
|
for (const element of Array.from(document.body.children)) {
|
||||||
EventHandler.off(element, 'mouseover', noop)
|
EventHandler.off(element, 'mouseover', noop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,14 +204,14 @@ class Dropdown extends BaseComponent {
|
|||||||
EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)
|
EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
_getConfig(config: Partial<ComponentConfig>): ComponentConfig {
|
_getConfig(config?: ComponentConfigInput): ComponentConfig {
|
||||||
config = super._getConfig(config)
|
const merged = super._getConfig(config) as ComponentConfig
|
||||||
|
|
||||||
if (typeof config.reference === 'object' && !isElement(config.reference) && typeof (config.reference as any).getBoundingClientRect !== 'function') {
|
if (typeof merged.reference === 'object' && !isElement(merged.reference) && typeof (merged.reference as Popper.VirtualElement).getBoundingClientRect !== 'function') {
|
||||||
throw new TypeError(`${NAME.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`)
|
throw new TypeError(`${NAME.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
_createPopper(): void {
|
_createPopper(): void {
|
||||||
@@ -250,7 +269,7 @@ class Dropdown extends BaseComponent {
|
|||||||
return this._element.closest(SELECTOR_NAVBAR) !== null
|
return this._element.closest(SELECTOR_NAVBAR) !== null
|
||||||
}
|
}
|
||||||
|
|
||||||
_getOffset(): number[] | ((popperData: any) => number[]) {
|
_getOffset(): number[] | PopperOffsetFunction {
|
||||||
const { offset } = this._config
|
const { offset } = this._config
|
||||||
|
|
||||||
if (typeof offset === 'string') {
|
if (typeof offset === 'string') {
|
||||||
@@ -258,10 +277,10 @@ class Dropdown extends BaseComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof offset === 'function') {
|
if (typeof offset === 'function') {
|
||||||
return (popperData: any) => (offset as Function)(popperData, this._element)
|
return (popperData: PopperOffsetData) => offset(popperData, this._element)
|
||||||
}
|
}
|
||||||
|
|
||||||
return offset as number[]
|
return offset
|
||||||
}
|
}
|
||||||
|
|
||||||
_getPopperConfig(): Partial<Popper.Options> {
|
_getPopperConfig(): Partial<Popper.Options> {
|
||||||
@@ -300,14 +319,15 @@ class Dropdown extends BaseComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_selectMenuItem({ key, target }: { key: string; target: HTMLElement }): void {
|
_selectMenuItem({ key, target }: { key: string; target: EventTarget | null }): void {
|
||||||
const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter((element) => isVisible(element))
|
const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter((element) => isVisible(element))
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus()
|
const activeElement = target as HTMLElement
|
||||||
|
getNextActiveElement(items, activeElement, key === ARROW_DOWN_KEY, !items.includes(activeElement)).focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
static clearMenus(event: Event & { button?: number; key?: string; composedPath?: () => EventTarget[] }): void {
|
static clearMenus(event: Event & { button?: number; key?: string; composedPath?: () => EventTarget[] }): void {
|
||||||
@@ -333,7 +353,7 @@ class Dropdown extends BaseComponent {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedTarget: Record<string, any> = { relatedTarget: context._element }
|
const relatedTarget: RelatedTarget = { relatedTarget: context._element }
|
||||||
|
|
||||||
if (event.type === 'click') {
|
if (event.type === 'click') {
|
||||||
relatedTarget.clickEvent = event
|
relatedTarget.clickEvent = event
|
||||||
@@ -358,14 +378,14 @@ class Dropdown extends BaseComponent {
|
|||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE, (event as any).delegateTarget.parentNode)
|
const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE, (event as DelegatedEvent<KeyboardEvent>).delegateTarget.parentNode as Element)
|
||||||
|
|
||||||
const instance = Dropdown.getOrCreateInstance(getToggleButton!) as Dropdown
|
const instance = Dropdown.getOrCreateInstance(getToggleButton!) as Dropdown
|
||||||
|
|
||||||
if (isUpOrDownEvent) {
|
if (isUpOrDownEvent) {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
instance.show()
|
instance.show()
|
||||||
instance._selectMenuItem(event as any)
|
instance._selectMenuItem(event)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ const SELECTOR_DIALOG = '.modal-dialog'
|
|||||||
const SELECTOR_MODAL_BODY = '.modal-body'
|
const SELECTOR_MODAL_BODY = '.modal-body'
|
||||||
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="modal"], [data-tblr-toggle="modal"]'
|
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="modal"], [data-tblr-toggle="modal"]'
|
||||||
|
|
||||||
interface ComponentConfig {
|
type ComponentConfig = {
|
||||||
[key: string]: any
|
backdrop: boolean | 'static'
|
||||||
|
focus: boolean
|
||||||
|
keyboard: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComponentConfigType {
|
type ComponentConfigInput = Partial<ComponentConfig> & Record<string, unknown>
|
||||||
[key: string]: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Default: ComponentConfig = {
|
const Default: ComponentConfig = {
|
||||||
backdrop: true,
|
backdrop: true,
|
||||||
@@ -59,7 +59,7 @@ const Default: ComponentConfig = {
|
|||||||
keyboard: true,
|
keyboard: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultType: ComponentConfigType = {
|
const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||||
backdrop: '(boolean|string)',
|
backdrop: '(boolean|string)',
|
||||||
focus: 'boolean',
|
focus: 'boolean',
|
||||||
keyboard: 'boolean',
|
keyboard: 'boolean',
|
||||||
@@ -70,6 +70,8 @@ const DefaultType: ComponentConfigType = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class Modal extends BaseComponent {
|
class Modal extends BaseComponent {
|
||||||
|
declare _element: HTMLElement
|
||||||
|
declare _config: ComponentConfig
|
||||||
_dialog: HTMLElement | null
|
_dialog: HTMLElement | null
|
||||||
_backdrop: Backdrop
|
_backdrop: Backdrop
|
||||||
_focustrap: FocusTrap
|
_focustrap: FocusTrap
|
||||||
@@ -77,7 +79,7 @@ class Modal extends BaseComponent {
|
|||||||
_isTransitioning: boolean
|
_isTransitioning: boolean
|
||||||
_scrollBar: ScrollBarHelper
|
_scrollBar: ScrollBarHelper
|
||||||
|
|
||||||
constructor(element: HTMLElement | string, config?: Partial<ComponentConfig>) {
|
constructor(element: HTMLElement | string, config?: ComponentConfigInput) {
|
||||||
super(element, config)
|
super(element, config)
|
||||||
|
|
||||||
this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element)
|
this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element)
|
||||||
@@ -94,7 +96,7 @@ class Modal extends BaseComponent {
|
|||||||
return Default
|
return Default
|
||||||
}
|
}
|
||||||
|
|
||||||
static get DefaultType(): ComponentConfigType {
|
static get DefaultType(): Record<keyof ComponentConfig, string> {
|
||||||
return DefaultType
|
return DefaultType
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +117,7 @@ class Modal extends BaseComponent {
|
|||||||
relatedTarget,
|
relatedTarget,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (showEvent.defaultPrevented) {
|
if (showEvent?.defaultPrevented) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +140,7 @@ class Modal extends BaseComponent {
|
|||||||
|
|
||||||
const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)
|
const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)
|
||||||
|
|
||||||
if (hideEvent.defaultPrevented) {
|
if (hideEvent?.defaultPrevented) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +191,7 @@ class Modal extends BaseComponent {
|
|||||||
this._element.setAttribute('role', 'dialog')
|
this._element.setAttribute('role', 'dialog')
|
||||||
this._element.scrollTop = 0
|
this._element.scrollTop = 0
|
||||||
|
|
||||||
const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog)
|
const modalBody = this._dialog ? SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog) : null
|
||||||
if (modalBody) {
|
if (modalBody) {
|
||||||
modalBody.scrollTop = 0
|
modalBody.scrollTop = 0
|
||||||
}
|
}
|
||||||
@@ -271,7 +273,7 @@ class Modal extends BaseComponent {
|
|||||||
|
|
||||||
_triggerBackdropTransition(): void {
|
_triggerBackdropTransition(): void {
|
||||||
const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)
|
const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)
|
||||||
if (hideEvent.defaultPrevented) {
|
if (hideEvent?.defaultPrevented) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,8 +331,12 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
EventHandler.one(target, EVENT_SHOW, (showEvent: Event) => {
|
EventHandler.one(target, EVENT_SHOW, (showEvent: Event) => {
|
||||||
if (showEvent.defaultPrevented) {
|
if (showEvent?.defaultPrevented) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,13 +42,13 @@ const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`
|
|||||||
|
|
||||||
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="offcanvas"], [data-tblr-toggle="offcanvas"]'
|
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="offcanvas"], [data-tblr-toggle="offcanvas"]'
|
||||||
|
|
||||||
interface ComponentConfig {
|
type ComponentConfig = {
|
||||||
[key: string]: any
|
backdrop: boolean | 'static'
|
||||||
|
keyboard: boolean
|
||||||
|
scroll: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComponentConfigType {
|
type ComponentConfigInput = Partial<ComponentConfig> & Record<string, unknown>
|
||||||
[key: string]: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Default: ComponentConfig = {
|
const Default: ComponentConfig = {
|
||||||
backdrop: true,
|
backdrop: true,
|
||||||
@@ -56,7 +56,7 @@ const Default: ComponentConfig = {
|
|||||||
scroll: false,
|
scroll: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultType: ComponentConfigType = {
|
const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||||
backdrop: '(boolean|string)',
|
backdrop: '(boolean|string)',
|
||||||
keyboard: 'boolean',
|
keyboard: 'boolean',
|
||||||
scroll: 'boolean',
|
scroll: 'boolean',
|
||||||
@@ -67,11 +67,13 @@ const DefaultType: ComponentConfigType = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class Offcanvas extends BaseComponent {
|
class Offcanvas extends BaseComponent {
|
||||||
|
declare _element: HTMLElement
|
||||||
|
declare _config: ComponentConfig
|
||||||
_isShown: boolean
|
_isShown: boolean
|
||||||
_backdrop: Backdrop
|
_backdrop: Backdrop
|
||||||
_focustrap: FocusTrap
|
_focustrap: FocusTrap
|
||||||
|
|
||||||
constructor(element: HTMLElement | string, config?: Partial<ComponentConfig>) {
|
constructor(element: HTMLElement | string, config?: ComponentConfigInput) {
|
||||||
super(element, config)
|
super(element, config)
|
||||||
|
|
||||||
this._isShown = false
|
this._isShown = false
|
||||||
@@ -84,7 +86,7 @@ class Offcanvas extends BaseComponent {
|
|||||||
return Default
|
return Default
|
||||||
}
|
}
|
||||||
|
|
||||||
static get DefaultType(): ComponentConfigType {
|
static get DefaultType(): Record<keyof ComponentConfig, string> {
|
||||||
return DefaultType
|
return DefaultType
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +105,7 @@ class Offcanvas extends BaseComponent {
|
|||||||
|
|
||||||
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, { relatedTarget })
|
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, { relatedTarget })
|
||||||
|
|
||||||
if (showEvent.defaultPrevented) {
|
if (showEvent?.defaultPrevented) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +140,7 @@ class Offcanvas extends BaseComponent {
|
|||||||
|
|
||||||
const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)
|
const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)
|
||||||
|
|
||||||
if (hideEvent.defaultPrevented) {
|
if (hideEvent?.defaultPrevented) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,6 +225,10 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (isDisabled(this)) {
|
if (isDisabled(this)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Tooltip from './tooltip'
|
import Tooltip from './tooltip'
|
||||||
|
import type { TooltipConfig, TooltipContent, TooltipContentMap } from './tooltip'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constants
|
* Constants
|
||||||
@@ -16,13 +17,11 @@ const NAME = 'popover'
|
|||||||
const SELECTOR_TITLE = '.popover-header'
|
const SELECTOR_TITLE = '.popover-header'
|
||||||
const SELECTOR_CONTENT = '.popover-body'
|
const SELECTOR_CONTENT = '.popover-body'
|
||||||
|
|
||||||
interface ComponentConfig {
|
type ComponentConfig = TooltipConfig & {
|
||||||
[key: string]: any
|
content: TooltipContent
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComponentConfigType {
|
type ComponentConfigInput = Partial<ComponentConfig> & Record<string, unknown>
|
||||||
[key: string]: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Default: ComponentConfig = {
|
const Default: ComponentConfig = {
|
||||||
...Tooltip.Default,
|
...Tooltip.Default,
|
||||||
@@ -33,7 +32,7 @@ const Default: ComponentConfig = {
|
|||||||
trigger: 'click',
|
trigger: 'click',
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultType: ComponentConfigType = {
|
const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||||
...Tooltip.DefaultType,
|
...Tooltip.DefaultType,
|
||||||
content: '(null|string|element|function)',
|
content: '(null|string|element|function)',
|
||||||
}
|
}
|
||||||
@@ -43,11 +42,18 @@ const DefaultType: ComponentConfigType = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class Popover extends Tooltip {
|
class Popover extends Tooltip {
|
||||||
|
declare _element: HTMLElement
|
||||||
|
declare _config: ComponentConfig
|
||||||
|
|
||||||
|
constructor(element: HTMLElement | string, config?: ComponentConfigInput) {
|
||||||
|
super(element, config)
|
||||||
|
}
|
||||||
|
|
||||||
static get Default(): ComponentConfig {
|
static get Default(): ComponentConfig {
|
||||||
return Default
|
return Default
|
||||||
}
|
}
|
||||||
|
|
||||||
static get DefaultType(): ComponentConfigType {
|
static get DefaultType(): Record<keyof ComponentConfig, string> {
|
||||||
return DefaultType
|
return DefaultType
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,14 +65,14 @@ class Popover extends Tooltip {
|
|||||||
return Boolean(this._getTitle() || this._getContent())
|
return Boolean(this._getTitle() || this._getContent())
|
||||||
}
|
}
|
||||||
|
|
||||||
_getContentForTemplate(): Record<string, any> {
|
_getContentForTemplate(): TooltipContentMap {
|
||||||
return {
|
return {
|
||||||
[SELECTOR_TITLE]: this._getTitle(),
|
[SELECTOR_TITLE]: this._getTitle(),
|
||||||
[SELECTOR_CONTENT]: this._getContent(),
|
[SELECTOR_CONTENT]: this._getContent(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getContent(): any {
|
_getContent(): string | HTMLElement | null {
|
||||||
return this._resolvePossibleFunction(this._config.content)
|
return this._resolvePossibleFunction(this._config.content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,16 @@ import BaseComponent from './base-component'
|
|||||||
import EventHandler from './dom/event-handler'
|
import EventHandler from './dom/event-handler'
|
||||||
import SelectorEngine from './dom/selector-engine'
|
import SelectorEngine from './dom/selector-engine'
|
||||||
import { getElement, isDisabled, isVisible, parseSelector } from './util/index'
|
import { getElement, isDisabled, isVisible, parseSelector } from './util/index'
|
||||||
import type { ComponentConfig, ComponentConfigType } from './types'
|
|
||||||
|
type ComponentConfig = {
|
||||||
|
offset: number | null
|
||||||
|
rootMargin: string
|
||||||
|
smoothScroll: boolean
|
||||||
|
target: HTMLElement | null
|
||||||
|
threshold: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComponentConfigInput = Partial<Omit<ComponentConfig, 'target' | 'threshold'>> & { target?: HTMLElement | string | null; threshold?: number[] | string } & Record<string, unknown>
|
||||||
|
|
||||||
const NAME = 'scrollspy'
|
const NAME = 'scrollspy'
|
||||||
const DATA_KEY = 'bs.scrollspy'
|
const DATA_KEY = 'bs.scrollspy'
|
||||||
@@ -41,7 +50,7 @@ const Default: ComponentConfig = {
|
|||||||
threshold: [0.1, 0.5, 1],
|
threshold: [0.1, 0.5, 1],
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultType: ComponentConfigType = {
|
const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||||
offset: '(number|null)',
|
offset: '(number|null)',
|
||||||
rootMargin: 'string',
|
rootMargin: 'string',
|
||||||
smoothScroll: 'boolean',
|
smoothScroll: 'boolean',
|
||||||
@@ -50,6 +59,8 @@ const DefaultType: ComponentConfigType = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ScrollSpy extends BaseComponent {
|
class ScrollSpy extends BaseComponent {
|
||||||
|
declare _element: HTMLElement
|
||||||
|
declare _config: ComponentConfig
|
||||||
_targetLinks: Map<string, HTMLElement>
|
_targetLinks: Map<string, HTMLElement>
|
||||||
_observableSections: Map<string, HTMLElement>
|
_observableSections: Map<string, HTMLElement>
|
||||||
_rootElement: HTMLElement | null
|
_rootElement: HTMLElement | null
|
||||||
@@ -60,7 +71,7 @@ class ScrollSpy extends BaseComponent {
|
|||||||
parentScrollTop: number
|
parentScrollTop: number
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(element: HTMLElement | string, config?: Partial<ComponentConfig>) {
|
constructor(element: HTMLElement | string, config?: ComponentConfigInput) {
|
||||||
super(element, config)
|
super(element, config)
|
||||||
|
|
||||||
this._targetLinks = new Map()
|
this._targetLinks = new Map()
|
||||||
@@ -79,7 +90,7 @@ class ScrollSpy extends BaseComponent {
|
|||||||
return Default
|
return Default
|
||||||
}
|
}
|
||||||
|
|
||||||
static get DefaultType(): ComponentConfigType {
|
static get DefaultType(): Record<keyof ComponentConfig, string> {
|
||||||
return DefaultType
|
return DefaultType
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,8 +123,9 @@ class ScrollSpy extends BaseComponent {
|
|||||||
|
|
||||||
config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin
|
config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin
|
||||||
|
|
||||||
if (typeof config.threshold === 'string') {
|
const threshold: unknown = config.threshold
|
||||||
config.threshold = config.threshold.split(',').map((value: string) => Number.parseFloat(value))
|
if (typeof threshold === 'string') {
|
||||||
|
config.threshold = threshold.split(',').map((value: string) => Number.parseFloat(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|||||||
@@ -9,7 +9,15 @@ import BaseComponent from './base-component'
|
|||||||
import EventHandler from './dom/event-handler.js'
|
import EventHandler from './dom/event-handler.js'
|
||||||
import { enableDismissTrigger } from './util/component-functions'
|
import { enableDismissTrigger } from './util/component-functions'
|
||||||
import { reflow } from './util/index.js'
|
import { reflow } from './util/index.js'
|
||||||
import type { ComponentConfig, ComponentConfigType, ElementSelector } from './types'
|
import type { ElementSelector } from './types'
|
||||||
|
|
||||||
|
type ComponentConfig = {
|
||||||
|
animation: boolean
|
||||||
|
autohide: boolean
|
||||||
|
delay: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComponentConfigInput = Partial<ComponentConfig> & Record<string, unknown>
|
||||||
|
|
||||||
const NAME = 'toast'
|
const NAME = 'toast'
|
||||||
const DATA_KEY = 'bs.toast'
|
const DATA_KEY = 'bs.toast'
|
||||||
@@ -29,7 +37,7 @@ const CLASS_NAME_HIDE = 'hide'
|
|||||||
const CLASS_NAME_SHOW = 'show'
|
const CLASS_NAME_SHOW = 'show'
|
||||||
const CLASS_NAME_SHOWING = 'showing'
|
const CLASS_NAME_SHOWING = 'showing'
|
||||||
|
|
||||||
const DefaultType: ComponentConfigType = {
|
const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||||
animation: 'boolean',
|
animation: 'boolean',
|
||||||
autohide: 'boolean',
|
autohide: 'boolean',
|
||||||
delay: 'number',
|
delay: 'number',
|
||||||
@@ -42,11 +50,13 @@ const Default: ComponentConfig = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Toast extends BaseComponent {
|
class Toast extends BaseComponent {
|
||||||
|
declare _element: HTMLElement
|
||||||
|
declare _config: ComponentConfig
|
||||||
_timeout: ReturnType<typeof setTimeout> | null
|
_timeout: ReturnType<typeof setTimeout> | null
|
||||||
_hasMouseInteraction: boolean
|
_hasMouseInteraction: boolean
|
||||||
_hasKeyboardInteraction: boolean
|
_hasKeyboardInteraction: boolean
|
||||||
|
|
||||||
constructor(element: ElementSelector, config?: ComponentConfig) {
|
constructor(element: ElementSelector, config?: ComponentConfigInput) {
|
||||||
super(element, config)
|
super(element, config)
|
||||||
|
|
||||||
this._timeout = null
|
this._timeout = null
|
||||||
@@ -59,7 +69,7 @@ class Toast extends BaseComponent {
|
|||||||
return Default
|
return Default
|
||||||
}
|
}
|
||||||
|
|
||||||
static get DefaultType(): ComponentConfigType {
|
static get DefaultType(): Record<keyof ComponentConfig, string> {
|
||||||
return DefaultType
|
return DefaultType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import Manipulator from './dom/manipulator'
|
|||||||
import { execute, findShadowRoot, getElement, getUID, isRTL, noop } from './util/index'
|
import { execute, findShadowRoot, getElement, getUID, isRTL, noop } from './util/index'
|
||||||
import { DefaultAllowlist } from './util/sanitizer'
|
import { DefaultAllowlist } from './util/sanitizer'
|
||||||
import TemplateFactory from './util/template-factory'
|
import TemplateFactory from './util/template-factory'
|
||||||
|
import type { AllowList, SanitizeFn } from './types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constants
|
* Constants
|
||||||
@@ -45,13 +46,41 @@ const EVENT_FOCUSOUT = 'focusout'
|
|||||||
const EVENT_MOUSEENTER = 'mouseenter'
|
const EVENT_MOUSEENTER = 'mouseenter'
|
||||||
const EVENT_MOUSELEAVE = 'mouseleave'
|
const EVENT_MOUSELEAVE = 'mouseleave'
|
||||||
|
|
||||||
interface ComponentConfig {
|
type PopperOffsetData = { placement: Popper.Placement; reference: Popper.Rect; popper: Popper.Rect }
|
||||||
[key: string]: any
|
type PopperOffsetFunction = (popperData: PopperOffsetData) => number[]
|
||||||
|
type PopperConfigFunction = (defaultConfig: Partial<Popper.Options>) => Partial<Popper.Options>
|
||||||
|
|
||||||
|
export type TooltipDelay = number | { show: number; hide: number }
|
||||||
|
export type TooltipContent = string | HTMLElement | null | ((this: HTMLElement, element: HTMLElement) => string | HTMLElement | null)
|
||||||
|
// Keys are selectors inside the template, values the content for that slot
|
||||||
|
export type TooltipContentMap = Record<string, TooltipContent>
|
||||||
|
export type TooltipPlacement = string | ((this: Tooltip, tip: HTMLElement, element: HTMLElement) => string)
|
||||||
|
|
||||||
|
export type TooltipConfig = {
|
||||||
|
allowList: AllowList
|
||||||
|
animation: boolean
|
||||||
|
boundary: Popper.Boundary
|
||||||
|
container: HTMLElement | string | false
|
||||||
|
customClass: string | ((this: HTMLElement, element: HTMLElement) => string)
|
||||||
|
delay: TooltipDelay
|
||||||
|
fallbackPlacements: string[]
|
||||||
|
html: boolean
|
||||||
|
offset: number[] | string | ((popperData: PopperOffsetData, element: HTMLElement) => number[])
|
||||||
|
placement: TooltipPlacement
|
||||||
|
popperConfig: Partial<Popper.Options> | PopperConfigFunction | null
|
||||||
|
sanitize: boolean
|
||||||
|
sanitizeFn: SanitizeFn | null
|
||||||
|
selector: string | false
|
||||||
|
template: string
|
||||||
|
title: TooltipContent
|
||||||
|
trigger: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComponentConfigType {
|
type ComponentConfig = TooltipConfig
|
||||||
[key: string]: string
|
|
||||||
}
|
export type TooltipConfigInput = Partial<TooltipConfig> & Record<string, unknown>
|
||||||
|
|
||||||
|
type ComponentConfigInput = TooltipConfigInput
|
||||||
|
|
||||||
const AttachmentMap: Record<string, string> = {
|
const AttachmentMap: Record<string, string> = {
|
||||||
AUTO: 'auto',
|
AUTO: 'auto',
|
||||||
@@ -81,7 +110,7 @@ const Default: ComponentConfig = {
|
|||||||
trigger: 'hover focus',
|
trigger: 'hover focus',
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultType: ComponentConfigType = {
|
const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||||
allowList: 'object',
|
allowList: 'object',
|
||||||
animation: 'boolean',
|
animation: 'boolean',
|
||||||
boundary: '(string|element)',
|
boundary: '(string|element)',
|
||||||
@@ -106,17 +135,19 @@ const DefaultType: ComponentConfigType = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class Tooltip extends BaseComponent {
|
class Tooltip extends BaseComponent {
|
||||||
|
declare _element: HTMLElement
|
||||||
|
declare _config: ComponentConfig
|
||||||
_isEnabled: boolean
|
_isEnabled: boolean
|
||||||
_timeout: ReturnType<typeof setTimeout> | number
|
_timeout: ReturnType<typeof setTimeout> | number
|
||||||
_isHovered: boolean | null
|
_isHovered: boolean | null
|
||||||
_activeTrigger: Record<string, boolean>
|
_activeTrigger: Record<string, boolean>
|
||||||
_popper: Popper.Instance | null
|
_popper: Popper.Instance | null
|
||||||
_templateFactory: TemplateFactory | null
|
_templateFactory: TemplateFactory | null
|
||||||
_newContent: Record<string, any> | null
|
_newContent: TooltipContentMap | null
|
||||||
tip: HTMLElement | null
|
tip: HTMLElement | null
|
||||||
_hideModalHandler: (() => void) | null
|
_hideModalHandler: () => void
|
||||||
|
|
||||||
constructor(element: HTMLElement | string, config?: Partial<ComponentConfig>) {
|
constructor(element: HTMLElement | string, config?: ComponentConfigInput) {
|
||||||
if (typeof Popper === 'undefined') {
|
if (typeof Popper === 'undefined') {
|
||||||
throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org/docs/v2/)")
|
throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org/docs/v2/)")
|
||||||
}
|
}
|
||||||
@@ -132,7 +163,11 @@ class Tooltip extends BaseComponent {
|
|||||||
this._newContent = null
|
this._newContent = null
|
||||||
|
|
||||||
this.tip = null
|
this.tip = null
|
||||||
this._hideModalHandler = null
|
this._hideModalHandler = () => {
|
||||||
|
if (this._element) {
|
||||||
|
this.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._setListeners()
|
this._setListeners()
|
||||||
|
|
||||||
@@ -145,7 +180,7 @@ class Tooltip extends BaseComponent {
|
|||||||
return Default
|
return Default
|
||||||
}
|
}
|
||||||
|
|
||||||
static get DefaultType(): ComponentConfigType {
|
static get DefaultType(): Record<keyof ComponentConfig, string> {
|
||||||
return DefaultType
|
return DefaultType
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,29 +239,30 @@ class Tooltip extends BaseComponent {
|
|||||||
const shadowRoot = findShadowRoot(this._element)
|
const shadowRoot = findShadowRoot(this._element)
|
||||||
const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element)
|
const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element)
|
||||||
|
|
||||||
if (showEvent.defaultPrevented || !isInTheDom) {
|
if (showEvent?.defaultPrevented || !isInTheDom) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this._disposePopper()
|
this._disposePopper()
|
||||||
|
|
||||||
const tip = this._getTipElement()
|
const tip = this._getTipElement()!
|
||||||
|
|
||||||
this._element.setAttribute('aria-describedby', tip!.getAttribute('id')!)
|
this._element.setAttribute('aria-describedby', tip.getAttribute('id')!)
|
||||||
|
|
||||||
const { container } = this._config
|
const { container } = this._config
|
||||||
|
|
||||||
if (!this._element.ownerDocument.documentElement.contains(this.tip)) {
|
if (!this._element.ownerDocument.documentElement.contains(this.tip)) {
|
||||||
container.append(tip)
|
// `_configAfterMerge` resolved the container to an element
|
||||||
|
;(container as HTMLElement).append(tip)
|
||||||
EventHandler.trigger(this._element, (this.constructor as typeof Tooltip).eventName(EVENT_INSERTED))
|
EventHandler.trigger(this._element, (this.constructor as typeof Tooltip).eventName(EVENT_INSERTED))
|
||||||
}
|
}
|
||||||
|
|
||||||
this._popper = this._createPopper(tip!)
|
this._popper = this._createPopper(tip)
|
||||||
|
|
||||||
tip!.classList.add(CLASS_NAME_SHOW)
|
tip!.classList.add(CLASS_NAME_SHOW)
|
||||||
|
|
||||||
if ('ontouchstart' in document.documentElement) {
|
if ('ontouchstart' in document.documentElement) {
|
||||||
for (const element of [].concat(...(document.body.children as any))) {
|
for (const element of Array.from(document.body.children)) {
|
||||||
EventHandler.on(element, 'mouseover', noop)
|
EventHandler.on(element, 'mouseover', noop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,7 +286,7 @@ class Tooltip extends BaseComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hideEvent = EventHandler.trigger(this._element, (this.constructor as typeof Tooltip).eventName(EVENT_HIDE))
|
const hideEvent = EventHandler.trigger(this._element, (this.constructor as typeof Tooltip).eventName(EVENT_HIDE))
|
||||||
if (hideEvent.defaultPrevented) {
|
if (hideEvent?.defaultPrevented) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +294,7 @@ class Tooltip extends BaseComponent {
|
|||||||
tip!.classList.remove(CLASS_NAME_SHOW)
|
tip!.classList.remove(CLASS_NAME_SHOW)
|
||||||
|
|
||||||
if ('ontouchstart' in document.documentElement) {
|
if ('ontouchstart' in document.documentElement) {
|
||||||
for (const element of [].concat(...(document.body.children as any))) {
|
for (const element of Array.from(document.body.children)) {
|
||||||
EventHandler.off(element, 'mouseover', noop)
|
EventHandler.off(element, 'mouseover', noop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,7 +338,7 @@ class Tooltip extends BaseComponent {
|
|||||||
return this.tip
|
return this.tip
|
||||||
}
|
}
|
||||||
|
|
||||||
_createTipElement(content: Record<string, any>): HTMLElement | null {
|
_createTipElement(content: TooltipContentMap): HTMLElement | null {
|
||||||
const tip = this._getTemplateFactory(content).toHtml() as HTMLElement
|
const tip = this._getTemplateFactory(content).toHtml() as HTMLElement
|
||||||
|
|
||||||
if (!tip) {
|
if (!tip) {
|
||||||
@@ -323,7 +359,7 @@ class Tooltip extends BaseComponent {
|
|||||||
return tip
|
return tip
|
||||||
}
|
}
|
||||||
|
|
||||||
setContent(content: Record<string, any>): void {
|
setContent(content: TooltipContentMap): void {
|
||||||
this._newContent = content
|
this._newContent = content
|
||||||
if (this._isShown()) {
|
if (this._isShown()) {
|
||||||
this._disposePopper()
|
this._disposePopper()
|
||||||
@@ -331,7 +367,7 @@ class Tooltip extends BaseComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getTemplateFactory(content: Record<string, any>): TemplateFactory {
|
_getTemplateFactory(content: TooltipContentMap): TemplateFactory {
|
||||||
if (this._templateFactory) {
|
if (this._templateFactory) {
|
||||||
this._templateFactory.changeContent(content)
|
this._templateFactory.changeContent(content)
|
||||||
} else {
|
} else {
|
||||||
@@ -345,13 +381,13 @@ class Tooltip extends BaseComponent {
|
|||||||
return this._templateFactory
|
return this._templateFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
_getContentForTemplate(): Record<string, any> {
|
_getContentForTemplate(): TooltipContentMap {
|
||||||
return {
|
return {
|
||||||
[SELECTOR_TOOLTIP_INNER]: this._getTitle(),
|
[SELECTOR_TOOLTIP_INNER]: this._getTitle(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getTitle(): string {
|
_getTitle(): string | HTMLElement {
|
||||||
return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title') || this._element.getAttribute('data-tblr-original-title') || ''
|
return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title') || this._element.getAttribute('data-tblr-original-title') || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,7 +409,7 @@ class Tooltip extends BaseComponent {
|
|||||||
return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))
|
return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))
|
||||||
}
|
}
|
||||||
|
|
||||||
_getOffset(): number[] | ((popperData: any) => number[]) {
|
_getOffset(): number[] | PopperOffsetFunction {
|
||||||
const { offset } = this._config
|
const { offset } = this._config
|
||||||
|
|
||||||
if (typeof offset === 'string') {
|
if (typeof offset === 'string') {
|
||||||
@@ -381,14 +417,14 @@ class Tooltip extends BaseComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof offset === 'function') {
|
if (typeof offset === 'function') {
|
||||||
return (popperData: any) => (offset as Function)(popperData, this._element)
|
return (popperData: PopperOffsetData) => offset(popperData, this._element)
|
||||||
}
|
}
|
||||||
|
|
||||||
return offset as number[]
|
return offset
|
||||||
}
|
}
|
||||||
|
|
||||||
_resolvePossibleFunction(arg: any): any {
|
_resolvePossibleFunction<T>(arg: T | ((this: HTMLElement, element: HTMLElement) => T)): T {
|
||||||
return execute(arg, [this._element, this._element])
|
return execute(arg, [this._element, this._element]) as T
|
||||||
}
|
}
|
||||||
|
|
||||||
_getPopperConfig(attachment: string): Partial<Popper.Options> {
|
_getPopperConfig(attachment: string): Partial<Popper.Options> {
|
||||||
@@ -423,7 +459,7 @@ class Tooltip extends BaseComponent {
|
|||||||
name: 'preSetPlacement',
|
name: 'preSetPlacement',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
phase: 'beforeMain',
|
phase: 'beforeMain',
|
||||||
fn: (data: any) => {
|
fn: (data: Popper.ModifierArguments<Record<string, unknown>>) => {
|
||||||
this._getTipElement()!.setAttribute('data-popper-placement', data.state.placement)
|
this._getTipElement()!.setAttribute('data-popper-placement', data.state.placement)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -465,12 +501,6 @@ class Tooltip extends BaseComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._hideModalHandler = () => {
|
|
||||||
if (this._element) {
|
|
||||||
this.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)
|
EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,7 +531,7 @@ class Tooltip extends BaseComponent {
|
|||||||
if (this._isHovered) {
|
if (this._isHovered) {
|
||||||
this.show()
|
this.show()
|
||||||
}
|
}
|
||||||
}, this._config.delay.show)
|
}, this._getDelay().show)
|
||||||
}
|
}
|
||||||
|
|
||||||
_leave(): void {
|
_leave(): void {
|
||||||
@@ -515,7 +545,12 @@ class Tooltip extends BaseComponent {
|
|||||||
if (!this._isHovered) {
|
if (!this._isHovered) {
|
||||||
this.hide()
|
this.hide()
|
||||||
}
|
}
|
||||||
}, this._config.delay.hide)
|
}, this._getDelay().hide)
|
||||||
|
}
|
||||||
|
|
||||||
|
_getDelay(): { show: number; hide: number } {
|
||||||
|
const { delay } = this._config
|
||||||
|
return typeof delay === 'number' ? { show: delay, hide: delay } : delay
|
||||||
}
|
}
|
||||||
|
|
||||||
_setTimeout(handler: () => void, timeout: number): void {
|
_setTimeout(handler: () => void, timeout: number): void {
|
||||||
@@ -527,7 +562,7 @@ class Tooltip extends BaseComponent {
|
|||||||
return Object.values(this._activeTrigger).includes(true)
|
return Object.values(this._activeTrigger).includes(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
_getConfig(config: Partial<ComponentConfig>): ComponentConfig {
|
_getConfig(config?: ComponentConfigInput): ComponentConfig {
|
||||||
const dataAttributes = Manipulator.getDataAttributes(this._element)
|
const dataAttributes = Manipulator.getDataAttributes(this._element)
|
||||||
|
|
||||||
for (const dataAttribute of Object.keys(dataAttributes)) {
|
for (const dataAttribute of Object.keys(dataAttributes)) {
|
||||||
@@ -536,18 +571,18 @@ class Tooltip extends BaseComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config = {
|
let merged: Record<string, unknown> = {
|
||||||
...dataAttributes,
|
...dataAttributes,
|
||||||
...(typeof config === 'object' && config ? config : {}),
|
...(typeof config === 'object' && config ? config : {}),
|
||||||
}
|
}
|
||||||
config = this._mergeConfigObj(config)
|
merged = this._mergeConfigObj(merged)
|
||||||
config = this._configAfterMerge(config)
|
merged = this._configAfterMerge(merged as ComponentConfig)
|
||||||
this._typeCheckConfig(config)
|
this._typeCheckConfig(merged)
|
||||||
return config
|
return merged as ComponentConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
_configAfterMerge(config: ComponentConfig): ComponentConfig {
|
_configAfterMerge(config: ComponentConfig): ComponentConfig {
|
||||||
config.container = config.container === false ? document.body : getElement(config.container)
|
config.container = config.container === false ? document.body : (getElement(config.container) as HTMLElement)
|
||||||
|
|
||||||
if (typeof config.delay === 'number') {
|
if (typeof config.delay === 'number') {
|
||||||
config.delay = {
|
config.delay = {
|
||||||
@@ -556,22 +591,25 @@ class Tooltip extends BaseComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof config.title === 'number') {
|
// Numbers are accepted at runtime (`data-bs-title="42"`) even though the type says string
|
||||||
config.title = config.title.toString()
|
const raw = config as Record<string, unknown>
|
||||||
|
if (typeof raw.title === 'number') {
|
||||||
|
raw.title = raw.title.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof config.content === 'number') {
|
if (typeof raw.content === 'number') {
|
||||||
config.content = config.content.toString()
|
raw.content = raw.content.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
_getDelegateConfig(): Partial<ComponentConfig> {
|
_getDelegateConfig(): ComponentConfigInput {
|
||||||
const config: Partial<ComponentConfig> = {}
|
const config: Record<string, unknown> = {}
|
||||||
|
const defaults = (this.constructor as typeof Tooltip).Default as Record<string, unknown>
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(this._config)) {
|
for (const [key, value] of Object.entries(this._config)) {
|
||||||
if ((this.constructor as typeof Tooltip).Default[key] !== value) {
|
if (defaults[key] !== value) {
|
||||||
config[key] = value
|
config[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* --------------------------------------------------------------------------
|
* --------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type ComponentConfig = Record<string, any>
|
export type ComponentConfig = Record<string, unknown>
|
||||||
|
|
||||||
export type ComponentConfigType = Record<string, string>
|
export type ComponentConfigType = Record<string, string>
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const CLASS_NAME_FADE = 'fade'
|
|||||||
const CLASS_NAME_SHOW = 'show'
|
const CLASS_NAME_SHOW = 'show'
|
||||||
const EVENT_MOUSEDOWN = `mousedown.bs.${NAME}`
|
const EVENT_MOUSEDOWN = `mousedown.bs.${NAME}`
|
||||||
|
|
||||||
interface BackdropConfig {
|
type BackdropConfig = {
|
||||||
className: string
|
className: string
|
||||||
clickCallback: (() => void) | null
|
clickCallback: (() => void) | null
|
||||||
isAnimated: boolean
|
isAnimated: boolean
|
||||||
|
|||||||
@@ -9,11 +9,34 @@ import EventHandler from '../dom/event-handler.js'
|
|||||||
import SelectorEngine from '../dom/selector-engine.js'
|
import SelectorEngine from '../dom/selector-engine.js'
|
||||||
import { isDisabled } from './index'
|
import { isDisabled } from './index'
|
||||||
|
|
||||||
|
type ComponentInstance = unknown
|
||||||
|
|
||||||
|
interface PluginComponent {
|
||||||
|
NAME: string
|
||||||
|
getOrCreateInstance(element: HTMLElement | string | null): ComponentInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventActionData {
|
||||||
|
targets: HTMLElement[]
|
||||||
|
event: Event
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginEventActionData extends EventActionData {
|
||||||
|
instances: ComponentInstance[]
|
||||||
|
}
|
||||||
|
|
||||||
interface DismissibleComponent {
|
interface DismissibleComponent {
|
||||||
EVENT_KEY: string
|
EVENT_KEY: string
|
||||||
NAME: string
|
NAME: string
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
getOrCreateInstance(element: HTMLElement | string | null): ComponentInstance
|
||||||
getOrCreateInstance(element: HTMLElement | string | null): any
|
}
|
||||||
|
|
||||||
|
const callMethod = (instance: ComponentInstance, method: string): void => {
|
||||||
|
const callable = (instance as Record<string, unknown>)[method]
|
||||||
|
|
||||||
|
if (typeof callable === 'function') {
|
||||||
|
callable.call(instance)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const enableDismissTrigger = (component: DismissibleComponent, method = 'hide'): void => {
|
const enableDismissTrigger = (component: DismissibleComponent, method = 'hide'): void => {
|
||||||
@@ -30,10 +53,37 @@ const enableDismissTrigger = (component: DismissibleComponent, method = 'hide'):
|
|||||||
}
|
}
|
||||||
|
|
||||||
const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`)
|
const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`)
|
||||||
const instance = component.getOrCreateInstance(target)
|
callMethod(component.getOrCreateInstance(target), method)
|
||||||
|
|
||||||
instance[method]()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export { enableDismissTrigger }
|
const eventAction = (onEvent: string, stringSelector: string, callback: (data: EventActionData) => void): void => {
|
||||||
|
const selector = `${stringSelector}:not(.disabled):not(:disabled)`
|
||||||
|
|
||||||
|
EventHandler.on(document, onEvent, selector, function (this: HTMLElement, event: Event) {
|
||||||
|
if (['A', 'AREA'].includes(this.tagName)) {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetSelector = SelectorEngine.getSelectorFromElement(this)
|
||||||
|
const targets = targetSelector ? SelectorEngine.find(targetSelector) : [this]
|
||||||
|
|
||||||
|
callback({ targets, event })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventActionOnPlugin = (Plugin: PluginComponent, onEvent: string, stringSelector: string, method: string, callback: ((data: PluginEventActionData) => void) | null = null): void => {
|
||||||
|
eventAction(`${onEvent}.${Plugin.NAME}`, stringSelector, (data) => {
|
||||||
|
const instances = data.targets.filter(Boolean).map((element) => Plugin.getOrCreateInstance(element))
|
||||||
|
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
callback({ ...data, instances })
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const instance of instances) {
|
||||||
|
callMethod(instance, method)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export { enableDismissTrigger, eventAction, eventActionOnPlugin }
|
||||||
@@ -20,7 +20,7 @@ const TAB_KEY = 'Tab'
|
|||||||
const TAB_NAV_FORWARD = 'forward'
|
const TAB_NAV_FORWARD = 'forward'
|
||||||
const TAB_NAV_BACKWARD = 'backward'
|
const TAB_NAV_BACKWARD = 'backward'
|
||||||
|
|
||||||
interface FocusTrapConfig {
|
type FocusTrapConfig = {
|
||||||
autofocus: boolean
|
autofocus: boolean
|
||||||
trapElement: HTMLElement | null
|
trapElement: HTMLElement | null
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ class FocusTrap extends Config {
|
|||||||
_handleFocusin(event: FocusEvent): void {
|
_handleFocusin(event: FocusEvent): void {
|
||||||
const { trapElement } = this._config
|
const { trapElement } = this._config
|
||||||
|
|
||||||
if (event.target === document || event.target === trapElement || trapElement!.contains(event.target as Node)) {
|
if (!trapElement || event.target === document || event.target === trapElement || trapElement.contains(event.target as Node)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const MILLISECONDS_MULTIPLIER = 1000
|
|||||||
const TRANSITION_END = 'transitionend'
|
const TRANSITION_END = 'transitionend'
|
||||||
|
|
||||||
const parseSelector = (selector: string): string => {
|
const parseSelector = (selector: string): string => {
|
||||||
if (selector && window.CSS && window.CSS.escape) {
|
if (selector && typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
|
||||||
selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`)
|
selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const POINTER_TYPE_PEN = 'pen'
|
|||||||
const CLASS_NAME_POINTER_EVENT = 'pointer-event'
|
const CLASS_NAME_POINTER_EVENT = 'pointer-event'
|
||||||
const SWIPE_THRESHOLD = 40
|
const SWIPE_THRESHOLD = 40
|
||||||
|
|
||||||
interface SwipeConfig {
|
type SwipeConfig = {
|
||||||
endCallback: (() => void) | null
|
endCallback: (() => void) | null
|
||||||
leftCallback: (() => void) | null
|
leftCallback: (() => void) | null
|
||||||
rightCallback: (() => void) | null
|
rightCallback: (() => void) | null
|
||||||
@@ -43,8 +43,8 @@ const DefaultType: ComponentConfigType = {
|
|||||||
class Swipe extends Config {
|
class Swipe extends Config {
|
||||||
declare _config: SwipeConfig & ComponentConfig
|
declare _config: SwipeConfig & ComponentConfig
|
||||||
_element: HTMLElement
|
_element: HTMLElement
|
||||||
_deltaX: number
|
declare _deltaX: number
|
||||||
_supportPointerEvents: boolean
|
declare _supportPointerEvents: boolean
|
||||||
|
|
||||||
constructor(element: HTMLElement, config?: ComponentConfig) {
|
constructor(element: HTMLElement, config?: ComponentConfig) {
|
||||||
super()
|
super()
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type { AllowList, ComponentConfig, ComponentConfigType, SanitizeFn } from
|
|||||||
|
|
||||||
const NAME = 'TemplateFactory'
|
const NAME = 'TemplateFactory'
|
||||||
|
|
||||||
interface TemplateFactoryConfig {
|
type TemplateFactoryConfig = {
|
||||||
allowList: AllowList
|
allowList: AllowList
|
||||||
content: Record<string, unknown>
|
content: Record<string, unknown>
|
||||||
extraClass: string | (() => string)
|
extraClass: string | (() => string)
|
||||||
@@ -104,7 +104,7 @@ class TemplateFactory extends Config {
|
|||||||
|
|
||||||
_typeCheckConfig(config: ComponentConfig): void {
|
_typeCheckConfig(config: ComponentConfig): void {
|
||||||
super._typeCheckConfig(config)
|
super._typeCheckConfig(config)
|
||||||
this._checkContent(config.content)
|
this._checkContent(config.content as Record<string, unknown>)
|
||||||
}
|
}
|
||||||
|
|
||||||
_checkContent(arg: Record<string, unknown>): void {
|
_checkContent(arg: Record<string, unknown>): void {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const countupElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLE
|
|||||||
|
|
||||||
if (countupElements.length) {
|
if (countupElements.length) {
|
||||||
countupElements.forEach(function (element: HTMLElement) {
|
countupElements.forEach(function (element: HTMLElement) {
|
||||||
let options: Record<string, any> = {}
|
let options: Record<string, unknown> = {}
|
||||||
try {
|
try {
|
||||||
const dataOptions = element.getAttribute('data-countup') ? JSON.parse(element.getAttribute('data-countup')!) : {}
|
const dataOptions = element.getAttribute('data-countup') ? JSON.parse(element.getAttribute('data-countup')!) : {}
|
||||||
options = Object.assign(
|
options = Object.assign(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Dropdown } from './bootstrap'
|
|||||||
const dropdownTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="dropdown"]'))
|
const dropdownTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="dropdown"]'))
|
||||||
dropdownTriggerList.map(function (dropdownTriggerEl: HTMLElement) {
|
dropdownTriggerList.map(function (dropdownTriggerEl: HTMLElement) {
|
||||||
const options = {
|
const options = {
|
||||||
boundary: dropdownTriggerEl.getAttribute('data-bs-boundary') === 'viewport' ? document.documentElement : 'clippingParents',
|
boundary: dropdownTriggerEl.getAttribute('data-bs-boundary') === 'viewport' ? document.documentElement : ('clippingParents' as const),
|
||||||
}
|
}
|
||||||
return new Dropdown(dropdownTriggerEl, options)
|
return new Dropdown(dropdownTriggerEl, options)
|
||||||
})
|
})
|
||||||
|
|||||||
Vendored
+3
-3
@@ -4,12 +4,12 @@ interface Window {
|
|||||||
CountUp: new (
|
CountUp: new (
|
||||||
target: HTMLElement,
|
target: HTMLElement,
|
||||||
endVal: number,
|
endVal: number,
|
||||||
options?: any,
|
options?: Record<string, unknown>,
|
||||||
) => {
|
) => {
|
||||||
error: boolean
|
error: boolean
|
||||||
start: () => void
|
start: () => void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IMask?: new (element: HTMLElement, options: { mask: string; lazy?: boolean }) => any
|
IMask?: new (element: HTMLElement, options: { mask: string; lazy?: boolean }) => unknown
|
||||||
Sortable?: new (element: HTMLElement, options?: any) => any
|
Sortable?: new (element: HTMLElement, options?: Record<string, unknown>) => unknown
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ const maskElementList: HTMLElement[] = [].slice.call(document.querySelectorAll<H
|
|||||||
maskElementList.map(function (maskEl: HTMLElement) {
|
maskElementList.map(function (maskEl: HTMLElement) {
|
||||||
window.IMask &&
|
window.IMask &&
|
||||||
new window.IMask(maskEl, {
|
new window.IMask(maskEl, {
|
||||||
mask: maskEl.dataset.mask,
|
mask: maskEl.dataset.mask as string,
|
||||||
lazy: maskEl.dataset.maskVisible !== 'true',
|
lazy: maskEl.dataset.maskVisible !== 'true',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const sortableElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTML
|
|||||||
|
|
||||||
if (sortableElements.length) {
|
if (sortableElements.length) {
|
||||||
sortableElements.forEach(function (element: HTMLElement) {
|
sortableElements.forEach(function (element: HTMLElement) {
|
||||||
let options: Record<string, any> = {}
|
let options: Record<string, unknown> = {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rawOptions = element.getAttribute('data-sortable')
|
const rawOptions = element.getAttribute('data-sortable')
|
||||||
|
|||||||
@@ -5,14 +5,12 @@
|
|||||||
*/
|
*/
|
||||||
import { themeDefaults, type ThemeKey } from './src/theme-config'
|
import { themeDefaults, type ThemeKey } from './src/theme-config'
|
||||||
|
|
||||||
const params = new Proxy(new URLSearchParams(window.location.search), {
|
const params = new URLSearchParams(window.location.search)
|
||||||
get: (searchParams: URLSearchParams, prop: string): string | null => searchParams.get(prop),
|
|
||||||
})
|
|
||||||
|
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)')
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
|
||||||
for (const key in themeDefaults) {
|
for (const key in themeDefaults) {
|
||||||
const param = params[key]
|
const param = params.get(key)
|
||||||
let selectedValue: string
|
let selectedValue: string
|
||||||
|
|
||||||
if (!!param) {
|
if (!!param) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, beforeAll, afterEach, vi } from 'vitest'
|
import { describe, it, expect, beforeAll, afterEach, vi } from 'vitest'
|
||||||
import BaseComponent from '../../../src/bootstrap/base-component'
|
import BaseComponent from '../../../src/bootstrap/base-component'
|
||||||
import { enableDismissTrigger } from '../../../src/bootstrap/util/component-functions'
|
import { enableDismissTrigger, eventActionOnPlugin } from '../../../src/bootstrap/util/component-functions'
|
||||||
import { clearFixture, createEvent, getFixture } from '../../helpers/fixture'
|
import { clearFixture, createEvent, getFixture } from '../../helpers/fixture'
|
||||||
|
|
||||||
class DummyClass extends BaseComponent {
|
class DummyClass extends BaseComponent {
|
||||||
@@ -85,4 +85,64 @@ describe('Component Functions', () => {
|
|||||||
vi.restoreAllMocks()
|
vi.restoreAllMocks()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('eventActionOnPlugin', () => {
|
||||||
|
it('should get plugin for the trigger element and execute given method on click', () => {
|
||||||
|
fixtureEl.innerHTML = '<button type="button" data-bs-toggle="test"></button>'
|
||||||
|
|
||||||
|
const spyGet = vi.spyOn(DummyClass, 'getOrCreateInstance')
|
||||||
|
const spyTest = vi.spyOn(DummyClass.prototype, 'testMethod')
|
||||||
|
|
||||||
|
eventActionOnPlugin(DummyClass, 'click', '[data-bs-toggle="test"]', 'testMethod')
|
||||||
|
fixtureEl.querySelector('[data-bs-toggle="test"]')!.dispatchEvent(createEvent('click'))
|
||||||
|
|
||||||
|
expect(spyGet).toHaveBeenCalledWith(fixtureEl.querySelector('[data-bs-toggle="test"]'))
|
||||||
|
expect(spyTest).toHaveBeenCalled()
|
||||||
|
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should resolve targets from data-bs-target and call the callback with instances', () => {
|
||||||
|
fixtureEl.innerHTML = ['<button type="button" data-bs-toggle="test2" data-bs-target=".target"></button>', '<div class="target"></div>', '<div class="target"></div>'].join('')
|
||||||
|
|
||||||
|
const spyTest = vi.spyOn(DummyClass.prototype, 'testMethod')
|
||||||
|
const callback = vi.fn()
|
||||||
|
|
||||||
|
eventActionOnPlugin(DummyClass, 'click', '[data-bs-toggle="test2"]', 'testMethod', callback)
|
||||||
|
fixtureEl.querySelector('[data-bs-toggle="test2"]')!.dispatchEvent(createEvent('click'))
|
||||||
|
|
||||||
|
expect(spyTest).toHaveBeenCalledTimes(2)
|
||||||
|
expect(callback).toHaveBeenCalledTimes(1)
|
||||||
|
expect(callback.mock.calls[0][0].instances).toHaveLength(2)
|
||||||
|
expect(callback.mock.calls[0][0].targets).toEqual([...fixtureEl.querySelectorAll('.target')])
|
||||||
|
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not trigger if disabled', () => {
|
||||||
|
fixtureEl.innerHTML = '<button type="button" disabled data-bs-toggle="test3"></button>'
|
||||||
|
|
||||||
|
const spy = vi.spyOn(DummyClass, 'getOrCreateInstance')
|
||||||
|
|
||||||
|
eventActionOnPlugin(DummyClass, 'click', '[data-bs-toggle="test3"]', 'testMethod')
|
||||||
|
fixtureEl.querySelector('[data-bs-toggle="test3"]')!.dispatchEvent(createEvent('click'))
|
||||||
|
|
||||||
|
expect(spy).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should preventDefault for <a> elements', () => {
|
||||||
|
fixtureEl.innerHTML = '<a href="#" data-bs-toggle="test4"></a>'
|
||||||
|
|
||||||
|
const preventSpy = vi.spyOn(Event.prototype, 'preventDefault')
|
||||||
|
|
||||||
|
eventActionOnPlugin(DummyClass, 'click', '[data-bs-toggle="test4"]', 'testMethod')
|
||||||
|
fixtureEl.querySelector('[data-bs-toggle="test4"]')!.dispatchEvent(createEvent('click'))
|
||||||
|
|
||||||
|
expect(preventSpy).toHaveBeenCalled()
|
||||||
|
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
+3
-11
@@ -6,7 +6,7 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"strict": false,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
@@ -17,14 +17,6 @@
|
|||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"allowJs": true
|
"allowJs": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["js/**/*"],
|
||||||
"js/**/*"
|
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
"dist",
|
|
||||||
"**/*.test.ts",
|
|
||||||
"**/*.spec.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user