Enable strict TypeScript in the Bootstrap port and add the bootstrap-component skill (#3044)

This commit is contained in:
Paweł Kuna authored and GitHub committed 2026-09-13 01:11:07 +02:00
1 parent 107e493275
commit 1ffbeeca42
32 files changed
+643 -189

No files matched your search

+1
View File
@@ -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 |
+224
View File
@@ -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)
+5 -9
View File
@@ -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`.
+1
View File
@@ -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
+1 -1
View File
@@ -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 {
+2
View File
@@ -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
} }
+19 -6
View File
@@ -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
} }
+16 -5
View File
@@ -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()
} }
+17 -14
View File
@@ -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] || {}
+41 -21
View File
@@ -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
} }
+19 -13
View File
@@ -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
} }
+16 -10
View File
@@ -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
} }
+15 -9
View File
@@ -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)
} }
} }
+18 -6
View File
@@ -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
+14 -4
View File
@@ -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
} }
+90 -52
View File
@@ -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
} }
} }
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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 }
+2 -2
View File
@@ -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
} }
+1 -1
View File
@@ -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)}`)
} }
+3 -3
View File
@@ -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 {
+1 -1
View File
@@ -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(
+1 -1
View File
@@ -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)
}) })
+3 -3
View File
@@ -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
} }
+1 -1
View File
@@ -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',
}) })
}) })
+1 -1
View File
@@ -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')
+2 -4
View File
@@ -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
View File
@@ -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"
]
} }