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
+650
-196
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 |
|
||||
| `core-scss` | writing or changing styles in `core/scss/` |
|
||||
| `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/` |
|
||||
| `write-docs` | writing or updating a page in `docs/content/` |
|
||||
| `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
|
||||
description: >-
|
||||
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 JS misbehaves, and before changing anything under
|
||||
`core/js/src/`. Not for demo-page scripts — that is the `astro-scripts`
|
||||
skill.
|
||||
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
|
||||
JS misbehaves, and before changing anything under `core/js/src/`. Not for demo-page scripts — that is the `astro-scripts` skill.
|
||||
---
|
||||
|
||||
# The framework JavaScript
|
||||
@@ -16,10 +11,10 @@ description: >-
|
||||
|
||||
## 1. Two entry points
|
||||
|
||||
| Entry | Ships as | Holds |
|
||||
| --- | --- | --- |
|
||||
| `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 |
|
||||
| Entry | Ships as | Holds |
|
||||
| -------------------- | ---------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| `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 |
|
||||
|
||||
`tabler-theme.js` is loaded right after `<body>` and **not deferred**, so the chosen theme applies before the first paint. It stays tiny on purpose (bundlewatch: 1 kB raw, 800 B minified) — do not add anything to it that is not needed before paint.
|
||||
|
||||
@@ -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.
|
||||
- `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.
|
||||
|
||||
## 3. Tabler's own modules
|
||||
@@ -74,11 +70,11 @@ 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:
|
||||
|
||||
| File | Limit |
|
||||
| --- | --- |
|
||||
| `dist/js/tabler.js` | 64 kB |
|
||||
| `dist/js/tabler.min.js` | 48 kB |
|
||||
| `dist/js/tabler-theme.js` | 1 kB |
|
||||
| File | Limit |
|
||||
| ------------------------- | ----- |
|
||||
| `dist/js/tabler.js` | 64 kB |
|
||||
| `dist/js/tabler.min.js` | 48 kB |
|
||||
| `dist/js/tabler-theme.js` | 1 kB |
|
||||
|
||||
```bash
|
||||
pnpm --filter @tabler/core build
|
||||
|
||||
@@ -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
|
||||
coverage/
|
||||
__screenshots__/
|
||||
.vitest-attachments/
|
||||
__pycache__/
|
||||
.claude/launch.json
|
||||
.claude/settings.local.json
|
||||
@@ -58,7 +58,7 @@ class BaseComponent extends Config {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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}`
|
||||
|
||||
class Button extends BaseComponent {
|
||||
declare _element: HTMLElement
|
||||
|
||||
static get NAME(): string {
|
||||
return NAME
|
||||
}
|
||||
|
||||
@@ -11,7 +11,18 @@ import Manipulator from './dom/manipulator'
|
||||
import SelectorEngine from './dom/selector-engine'
|
||||
import { getNextActiveElement, isRTL, isVisible, reflow, triggerTransitionEnd } from './util/index'
|
||||
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 DATA_KEY = 'bs.carousel'
|
||||
@@ -66,7 +77,7 @@ const Default: ComponentConfig = {
|
||||
wrap: true,
|
||||
}
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
const DefaultType: Record<Exclude<keyof ComponentConfig, 'defaultInterval'>, string> = {
|
||||
interval: '(number|boolean)',
|
||||
keyboard: 'boolean',
|
||||
pause: '(string|boolean)',
|
||||
@@ -76,6 +87,8 @@ const DefaultType: ComponentConfigType = {
|
||||
}
|
||||
|
||||
class Carousel extends BaseComponent {
|
||||
declare _element: HTMLElement
|
||||
declare _config: ComponentConfig
|
||||
_interval: ReturnType<typeof setInterval> | null
|
||||
_activeElement: HTMLElement | null
|
||||
_isSliding: boolean
|
||||
@@ -83,7 +96,7 @@ class Carousel extends BaseComponent {
|
||||
_swipeHelper: Swipe | null
|
||||
_indicatorsElement: HTMLElement | null
|
||||
|
||||
constructor(element: HTMLElement | string, config?: Partial<ComponentConfig>) {
|
||||
constructor(element: HTMLElement | string, config?: ComponentConfigInput) {
|
||||
super(element, config)
|
||||
|
||||
this._interval = null
|
||||
@@ -104,7 +117,7 @@ class Carousel extends BaseComponent {
|
||||
return Default
|
||||
}
|
||||
|
||||
static get DefaultType(): ComponentConfigType {
|
||||
static get DefaultType(): Record<Exclude<keyof ComponentConfig, 'defaultInterval'>, string> {
|
||||
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)
|
||||
|
||||
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 {
|
||||
@@ -302,7 +315,7 @@ class Carousel extends BaseComponent {
|
||||
|
||||
const slideEvent = triggerEvent(EVENT_SLIDE)
|
||||
|
||||
if (slideEvent.defaultPrevented) {
|
||||
if (slideEvent?.defaultPrevented) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,15 @@ import BaseComponent from './base-component'
|
||||
import EventHandler from './dom/event-handler'
|
||||
import SelectorEngine from './dom/selector-engine'
|
||||
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 DATA_KEY = 'bs.collapse'
|
||||
@@ -40,16 +48,18 @@ const Default: ComponentConfig = {
|
||||
toggle: true,
|
||||
}
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||
parent: '(null|element)',
|
||||
toggle: 'boolean',
|
||||
}
|
||||
|
||||
class Collapse extends BaseComponent {
|
||||
declare _element: HTMLElement
|
||||
declare _config: ComponentConfig
|
||||
_isTransitioning: boolean
|
||||
_triggerArray: HTMLElement[]
|
||||
|
||||
constructor(element: ElementSelector, config?: ComponentConfig) {
|
||||
constructor(element: ElementSelector, config?: ComponentConfigInput) {
|
||||
super(element, config)
|
||||
|
||||
this._isTransitioning = false
|
||||
@@ -81,7 +91,7 @@ class Collapse extends BaseComponent {
|
||||
return Default
|
||||
}
|
||||
|
||||
static get DefaultType(): ComponentConfigType {
|
||||
static get DefaultType(): Record<keyof ComponentConfig, string> {
|
||||
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) {
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
(event: Event): void
|
||||
@@ -99,7 +102,7 @@ function bootstrapHandler(element: EventTarget, fn: EventCallback): BootstrapHan
|
||||
EventHandler.off(element, event.type, fn)
|
||||
}
|
||||
|
||||
return fn.apply(element, [event])
|
||||
return fn.apply(element as HTMLElement, [event])
|
||||
} as BootstrapHandler
|
||||
}
|
||||
|
||||
@@ -119,7 +122,7 @@ function bootstrapDelegationHandler(element: EventTarget, selector: string, fn:
|
||||
EventHandler.off(element, event.type, selector, fn)
|
||||
}
|
||||
|
||||
return fn.apply(target, [event])
|
||||
return fn.apply(target as HTMLElement, [event])
|
||||
}
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
|
||||
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 callable = isDelegated ? delegationFunction! : (handler || delegationFunction)!
|
||||
let typeEvent = getTypeEvent(originalTypeEvent)
|
||||
@@ -141,7 +144,7 @@ function normalizeParameters(originalTypeEvent: string, handler: string | EventC
|
||||
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) {
|
||||
return
|
||||
}
|
||||
@@ -150,10 +153,10 @@ function addHandler(element: EventTarget | null, originalTypeEvent: string, hand
|
||||
|
||||
if (originalTypeEvent in customEvents) {
|
||||
const wrapFunction = (fn: EventCallback): EventCallback => {
|
||||
return function (this: EventTarget, event: unknown) {
|
||||
const evt = event as MouseEvent & { delegateTarget: HTMLElement }
|
||||
return function (this: EventTarget, event: Event) {
|
||||
const evt = event as DelegatedEvent<MouseEvent>
|
||||
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 = {
|
||||
on(element: EventTarget | null, event: string, handler: string | EventCallback, delegationFunction?: EventCallback): void {
|
||||
addHandler(element, event, handler, delegationFunction, false)
|
||||
on<E extends Event = Event>(element: EventTarget | null, event: string, handler: string | false | EventCallback<E>, delegationFunction?: EventCallback<E>): void {
|
||||
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 {
|
||||
addHandler(element, event, handler, delegationFunction, true)
|
||||
one<E extends Event = Event>(element: EventTarget | null, event: string, handler: string | false | EventCallback<E>, delegationFunction?: EventCallback<E>): void {
|
||||
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) {
|
||||
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 events = getElementEvents(element)
|
||||
const storeElementEvent = events[typeEvent] || {}
|
||||
|
||||
@@ -11,7 +11,24 @@ import EventHandler from './dom/event-handler'
|
||||
import Manipulator from './dom/manipulator'
|
||||
import SelectorEngine from './dom/selector-engine'
|
||||
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 DATA_KEY = 'bs.dropdown'
|
||||
@@ -64,7 +81,7 @@ const Default: ComponentConfig = {
|
||||
reference: 'toggle',
|
||||
}
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||
autoClose: '(boolean|string)',
|
||||
boundary: '(string|element)',
|
||||
display: 'string',
|
||||
@@ -74,12 +91,14 @@ const DefaultType: ComponentConfigType = {
|
||||
}
|
||||
|
||||
class Dropdown extends BaseComponent {
|
||||
declare _element: HTMLElement
|
||||
declare _config: ComponentConfig
|
||||
_popper: Popper.Instance | null
|
||||
_parent: HTMLElement
|
||||
_menu: HTMLElement
|
||||
_inNavbar: boolean
|
||||
|
||||
constructor(element: HTMLElement | string, config?: Partial<ComponentConfig>) {
|
||||
constructor(element: HTMLElement | string, config?: ComponentConfigInput) {
|
||||
super(element, config)
|
||||
|
||||
this._popper = null
|
||||
@@ -92,7 +111,7 @@ class Dropdown extends BaseComponent {
|
||||
return Default
|
||||
}
|
||||
|
||||
static get DefaultType(): ComponentConfigType {
|
||||
static get DefaultType(): Record<keyof ComponentConfig, string> {
|
||||
return DefaultType
|
||||
}
|
||||
|
||||
@@ -115,14 +134,14 @@ class Dropdown extends BaseComponent {
|
||||
|
||||
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, relatedTarget)
|
||||
|
||||
if (showEvent.defaultPrevented) {
|
||||
if (showEvent?.defaultPrevented) {
|
||||
return
|
||||
}
|
||||
|
||||
this._createPopper()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
if (hideEvent.defaultPrevented) {
|
||||
if (hideEvent?.defaultPrevented) {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -185,14 +204,14 @@ class Dropdown extends BaseComponent {
|
||||
EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)
|
||||
}
|
||||
|
||||
_getConfig(config: Partial<ComponentConfig>): ComponentConfig {
|
||||
config = super._getConfig(config)
|
||||
_getConfig(config?: ComponentConfigInput): ComponentConfig {
|
||||
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.`)
|
||||
}
|
||||
|
||||
return config
|
||||
return merged
|
||||
}
|
||||
|
||||
_createPopper(): void {
|
||||
@@ -250,7 +269,7 @@ class Dropdown extends BaseComponent {
|
||||
return this._element.closest(SELECTOR_NAVBAR) !== null
|
||||
}
|
||||
|
||||
_getOffset(): number[] | ((popperData: any) => number[]) {
|
||||
_getOffset(): number[] | PopperOffsetFunction {
|
||||
const { offset } = this._config
|
||||
|
||||
if (typeof offset === 'string') {
|
||||
@@ -258,10 +277,10 @@ class Dropdown extends BaseComponent {
|
||||
}
|
||||
|
||||
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> {
|
||||
@@ -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))
|
||||
|
||||
if (!items.length) {
|
||||
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 {
|
||||
@@ -333,7 +353,7 @@ class Dropdown extends BaseComponent {
|
||||
continue
|
||||
}
|
||||
|
||||
const relatedTarget: Record<string, any> = { relatedTarget: context._element }
|
||||
const relatedTarget: RelatedTarget = { relatedTarget: context._element }
|
||||
|
||||
if (event.type === 'click') {
|
||||
relatedTarget.clickEvent = event
|
||||
@@ -358,14 +378,14 @@ class Dropdown extends BaseComponent {
|
||||
|
||||
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
|
||||
|
||||
if (isUpOrDownEvent) {
|
||||
event.stopPropagation()
|
||||
instance.show()
|
||||
instance._selectMenuItem(event as any)
|
||||
instance._selectMenuItem(event)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -45,13 +45,13 @@ const SELECTOR_DIALOG = '.modal-dialog'
|
||||
const SELECTOR_MODAL_BODY = '.modal-body'
|
||||
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="modal"], [data-tblr-toggle="modal"]'
|
||||
|
||||
interface ComponentConfig {
|
||||
[key: string]: any
|
||||
type ComponentConfig = {
|
||||
backdrop: boolean | 'static'
|
||||
focus: boolean
|
||||
keyboard: boolean
|
||||
}
|
||||
|
||||
interface ComponentConfigType {
|
||||
[key: string]: string
|
||||
}
|
||||
type ComponentConfigInput = Partial<ComponentConfig> & Record<string, unknown>
|
||||
|
||||
const Default: ComponentConfig = {
|
||||
backdrop: true,
|
||||
@@ -59,7 +59,7 @@ const Default: ComponentConfig = {
|
||||
keyboard: true,
|
||||
}
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||
backdrop: '(boolean|string)',
|
||||
focus: 'boolean',
|
||||
keyboard: 'boolean',
|
||||
@@ -70,6 +70,8 @@ const DefaultType: ComponentConfigType = {
|
||||
*/
|
||||
|
||||
class Modal extends BaseComponent {
|
||||
declare _element: HTMLElement
|
||||
declare _config: ComponentConfig
|
||||
_dialog: HTMLElement | null
|
||||
_backdrop: Backdrop
|
||||
_focustrap: FocusTrap
|
||||
@@ -77,7 +79,7 @@ class Modal extends BaseComponent {
|
||||
_isTransitioning: boolean
|
||||
_scrollBar: ScrollBarHelper
|
||||
|
||||
constructor(element: HTMLElement | string, config?: Partial<ComponentConfig>) {
|
||||
constructor(element: HTMLElement | string, config?: ComponentConfigInput) {
|
||||
super(element, config)
|
||||
|
||||
this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element)
|
||||
@@ -94,7 +96,7 @@ class Modal extends BaseComponent {
|
||||
return Default
|
||||
}
|
||||
|
||||
static get DefaultType(): ComponentConfigType {
|
||||
static get DefaultType(): Record<keyof ComponentConfig, string> {
|
||||
return DefaultType
|
||||
}
|
||||
|
||||
@@ -115,7 +117,7 @@ class Modal extends BaseComponent {
|
||||
relatedTarget,
|
||||
})
|
||||
|
||||
if (showEvent.defaultPrevented) {
|
||||
if (showEvent?.defaultPrevented) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -138,7 +140,7 @@ class Modal extends BaseComponent {
|
||||
|
||||
const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)
|
||||
|
||||
if (hideEvent.defaultPrevented) {
|
||||
if (hideEvent?.defaultPrevented) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -189,7 +191,7 @@ class Modal extends BaseComponent {
|
||||
this._element.setAttribute('role', 'dialog')
|
||||
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) {
|
||||
modalBody.scrollTop = 0
|
||||
}
|
||||
@@ -271,7 +273,7 @@ class Modal extends BaseComponent {
|
||||
|
||||
_triggerBackdropTransition(): void {
|
||||
const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)
|
||||
if (hideEvent.defaultPrevented) {
|
||||
if (hideEvent?.defaultPrevented) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -329,8 +331,12 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
|
||||
EventHandler.one(target, EVENT_SHOW, (showEvent: Event) => {
|
||||
if (showEvent.defaultPrevented) {
|
||||
if (showEvent?.defaultPrevented) {
|
||||
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"]'
|
||||
|
||||
interface ComponentConfig {
|
||||
[key: string]: any
|
||||
type ComponentConfig = {
|
||||
backdrop: boolean | 'static'
|
||||
keyboard: boolean
|
||||
scroll: boolean
|
||||
}
|
||||
|
||||
interface ComponentConfigType {
|
||||
[key: string]: string
|
||||
}
|
||||
type ComponentConfigInput = Partial<ComponentConfig> & Record<string, unknown>
|
||||
|
||||
const Default: ComponentConfig = {
|
||||
backdrop: true,
|
||||
@@ -56,7 +56,7 @@ const Default: ComponentConfig = {
|
||||
scroll: false,
|
||||
}
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||
backdrop: '(boolean|string)',
|
||||
keyboard: 'boolean',
|
||||
scroll: 'boolean',
|
||||
@@ -67,11 +67,13 @@ const DefaultType: ComponentConfigType = {
|
||||
*/
|
||||
|
||||
class Offcanvas extends BaseComponent {
|
||||
declare _element: HTMLElement
|
||||
declare _config: ComponentConfig
|
||||
_isShown: boolean
|
||||
_backdrop: Backdrop
|
||||
_focustrap: FocusTrap
|
||||
|
||||
constructor(element: HTMLElement | string, config?: Partial<ComponentConfig>) {
|
||||
constructor(element: HTMLElement | string, config?: ComponentConfigInput) {
|
||||
super(element, config)
|
||||
|
||||
this._isShown = false
|
||||
@@ -84,7 +86,7 @@ class Offcanvas extends BaseComponent {
|
||||
return Default
|
||||
}
|
||||
|
||||
static get DefaultType(): ComponentConfigType {
|
||||
static get DefaultType(): Record<keyof ComponentConfig, string> {
|
||||
return DefaultType
|
||||
}
|
||||
|
||||
@@ -103,7 +105,7 @@ class Offcanvas extends BaseComponent {
|
||||
|
||||
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, { relatedTarget })
|
||||
|
||||
if (showEvent.defaultPrevented) {
|
||||
if (showEvent?.defaultPrevented) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -138,7 +140,7 @@ class Offcanvas extends BaseComponent {
|
||||
|
||||
const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)
|
||||
|
||||
if (hideEvent.defaultPrevented) {
|
||||
if (hideEvent?.defaultPrevented) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -223,6 +225,10 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isDisabled(this)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import Tooltip from './tooltip'
|
||||
import type { TooltipConfig, TooltipContent, TooltipContentMap } from './tooltip'
|
||||
|
||||
/**
|
||||
* Constants
|
||||
@@ -16,13 +17,11 @@ const NAME = 'popover'
|
||||
const SELECTOR_TITLE = '.popover-header'
|
||||
const SELECTOR_CONTENT = '.popover-body'
|
||||
|
||||
interface ComponentConfig {
|
||||
[key: string]: any
|
||||
type ComponentConfig = TooltipConfig & {
|
||||
content: TooltipContent
|
||||
}
|
||||
|
||||
interface ComponentConfigType {
|
||||
[key: string]: string
|
||||
}
|
||||
type ComponentConfigInput = Partial<ComponentConfig> & Record<string, unknown>
|
||||
|
||||
const Default: ComponentConfig = {
|
||||
...Tooltip.Default,
|
||||
@@ -33,7 +32,7 @@ const Default: ComponentConfig = {
|
||||
trigger: 'click',
|
||||
}
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||
...Tooltip.DefaultType,
|
||||
content: '(null|string|element|function)',
|
||||
}
|
||||
@@ -43,11 +42,18 @@ const DefaultType: ComponentConfigType = {
|
||||
*/
|
||||
|
||||
class Popover extends Tooltip {
|
||||
declare _element: HTMLElement
|
||||
declare _config: ComponentConfig
|
||||
|
||||
constructor(element: HTMLElement | string, config?: ComponentConfigInput) {
|
||||
super(element, config)
|
||||
}
|
||||
|
||||
static get Default(): ComponentConfig {
|
||||
return Default
|
||||
}
|
||||
|
||||
static get DefaultType(): ComponentConfigType {
|
||||
static get DefaultType(): Record<keyof ComponentConfig, string> {
|
||||
return DefaultType
|
||||
}
|
||||
|
||||
@@ -59,14 +65,14 @@ class Popover extends Tooltip {
|
||||
return Boolean(this._getTitle() || this._getContent())
|
||||
}
|
||||
|
||||
_getContentForTemplate(): Record<string, any> {
|
||||
_getContentForTemplate(): TooltipContentMap {
|
||||
return {
|
||||
[SELECTOR_TITLE]: this._getTitle(),
|
||||
[SELECTOR_CONTENT]: this._getContent(),
|
||||
}
|
||||
}
|
||||
|
||||
_getContent(): any {
|
||||
_getContent(): string | HTMLElement | null {
|
||||
return this._resolvePossibleFunction(this._config.content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,16 @@ import BaseComponent from './base-component'
|
||||
import EventHandler from './dom/event-handler'
|
||||
import SelectorEngine from './dom/selector-engine'
|
||||
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 DATA_KEY = 'bs.scrollspy'
|
||||
@@ -41,7 +50,7 @@ const Default: ComponentConfig = {
|
||||
threshold: [0.1, 0.5, 1],
|
||||
}
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||
offset: '(number|null)',
|
||||
rootMargin: 'string',
|
||||
smoothScroll: 'boolean',
|
||||
@@ -50,6 +59,8 @@ const DefaultType: ComponentConfigType = {
|
||||
}
|
||||
|
||||
class ScrollSpy extends BaseComponent {
|
||||
declare _element: HTMLElement
|
||||
declare _config: ComponentConfig
|
||||
_targetLinks: Map<string, HTMLElement>
|
||||
_observableSections: Map<string, HTMLElement>
|
||||
_rootElement: HTMLElement | null
|
||||
@@ -60,7 +71,7 @@ class ScrollSpy extends BaseComponent {
|
||||
parentScrollTop: number
|
||||
}
|
||||
|
||||
constructor(element: HTMLElement | string, config?: Partial<ComponentConfig>) {
|
||||
constructor(element: HTMLElement | string, config?: ComponentConfigInput) {
|
||||
super(element, config)
|
||||
|
||||
this._targetLinks = new Map()
|
||||
@@ -79,7 +90,7 @@ class ScrollSpy extends BaseComponent {
|
||||
return Default
|
||||
}
|
||||
|
||||
static get DefaultType(): ComponentConfigType {
|
||||
static get DefaultType(): Record<keyof ComponentConfig, string> {
|
||||
return DefaultType
|
||||
}
|
||||
|
||||
@@ -112,8 +123,9 @@ class ScrollSpy extends BaseComponent {
|
||||
|
||||
config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin
|
||||
|
||||
if (typeof config.threshold === 'string') {
|
||||
config.threshold = config.threshold.split(',').map((value: string) => Number.parseFloat(value))
|
||||
const threshold: unknown = config.threshold
|
||||
if (typeof threshold === 'string') {
|
||||
config.threshold = threshold.split(',').map((value: string) => Number.parseFloat(value))
|
||||
}
|
||||
|
||||
return config
|
||||
|
||||
@@ -9,7 +9,15 @@ import BaseComponent from './base-component'
|
||||
import EventHandler from './dom/event-handler.js'
|
||||
import { enableDismissTrigger } from './util/component-functions'
|
||||
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 DATA_KEY = 'bs.toast'
|
||||
@@ -29,7 +37,7 @@ const CLASS_NAME_HIDE = 'hide'
|
||||
const CLASS_NAME_SHOW = 'show'
|
||||
const CLASS_NAME_SHOWING = 'showing'
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||
animation: 'boolean',
|
||||
autohide: 'boolean',
|
||||
delay: 'number',
|
||||
@@ -42,11 +50,13 @@ const Default: ComponentConfig = {
|
||||
}
|
||||
|
||||
class Toast extends BaseComponent {
|
||||
declare _element: HTMLElement
|
||||
declare _config: ComponentConfig
|
||||
_timeout: ReturnType<typeof setTimeout> | null
|
||||
_hasMouseInteraction: boolean
|
||||
_hasKeyboardInteraction: boolean
|
||||
|
||||
constructor(element: ElementSelector, config?: ComponentConfig) {
|
||||
constructor(element: ElementSelector, config?: ComponentConfigInput) {
|
||||
super(element, config)
|
||||
|
||||
this._timeout = null
|
||||
@@ -59,7 +69,7 @@ class Toast extends BaseComponent {
|
||||
return Default
|
||||
}
|
||||
|
||||
static get DefaultType(): ComponentConfigType {
|
||||
static get DefaultType(): Record<keyof ComponentConfig, string> {
|
||||
return DefaultType
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import Manipulator from './dom/manipulator'
|
||||
import { execute, findShadowRoot, getElement, getUID, isRTL, noop } from './util/index'
|
||||
import { DefaultAllowlist } from './util/sanitizer'
|
||||
import TemplateFactory from './util/template-factory'
|
||||
import type { AllowList, SanitizeFn } from './types'
|
||||
|
||||
/**
|
||||
* Constants
|
||||
@@ -45,13 +46,41 @@ const EVENT_FOCUSOUT = 'focusout'
|
||||
const EVENT_MOUSEENTER = 'mouseenter'
|
||||
const EVENT_MOUSELEAVE = 'mouseleave'
|
||||
|
||||
interface ComponentConfig {
|
||||
[key: string]: any
|
||||
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>
|
||||
|
||||
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 {
|
||||
[key: string]: string
|
||||
}
|
||||
type ComponentConfig = TooltipConfig
|
||||
|
||||
export type TooltipConfigInput = Partial<TooltipConfig> & Record<string, unknown>
|
||||
|
||||
type ComponentConfigInput = TooltipConfigInput
|
||||
|
||||
const AttachmentMap: Record<string, string> = {
|
||||
AUTO: 'auto',
|
||||
@@ -81,7 +110,7 @@ const Default: ComponentConfig = {
|
||||
trigger: 'hover focus',
|
||||
}
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||
allowList: 'object',
|
||||
animation: 'boolean',
|
||||
boundary: '(string|element)',
|
||||
@@ -106,17 +135,19 @@ const DefaultType: ComponentConfigType = {
|
||||
*/
|
||||
|
||||
class Tooltip extends BaseComponent {
|
||||
declare _element: HTMLElement
|
||||
declare _config: ComponentConfig
|
||||
_isEnabled: boolean
|
||||
_timeout: ReturnType<typeof setTimeout> | number
|
||||
_isHovered: boolean | null
|
||||
_activeTrigger: Record<string, boolean>
|
||||
_popper: Popper.Instance | null
|
||||
_templateFactory: TemplateFactory | null
|
||||
_newContent: Record<string, any> | null
|
||||
_newContent: TooltipContentMap | 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') {
|
||||
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.tip = null
|
||||
this._hideModalHandler = null
|
||||
this._hideModalHandler = () => {
|
||||
if (this._element) {
|
||||
this.hide()
|
||||
}
|
||||
}
|
||||
|
||||
this._setListeners()
|
||||
|
||||
@@ -145,7 +180,7 @@ class Tooltip extends BaseComponent {
|
||||
return Default
|
||||
}
|
||||
|
||||
static get DefaultType(): ComponentConfigType {
|
||||
static get DefaultType(): Record<keyof ComponentConfig, string> {
|
||||
return DefaultType
|
||||
}
|
||||
|
||||
@@ -204,29 +239,30 @@ class Tooltip extends BaseComponent {
|
||||
const shadowRoot = findShadowRoot(this._element)
|
||||
const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element)
|
||||
|
||||
if (showEvent.defaultPrevented || !isInTheDom) {
|
||||
if (showEvent?.defaultPrevented || !isInTheDom) {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
this._popper = this._createPopper(tip!)
|
||||
this._popper = this._createPopper(tip)
|
||||
|
||||
tip!.classList.add(CLASS_NAME_SHOW)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -250,7 +286,7 @@ class Tooltip extends BaseComponent {
|
||||
}
|
||||
|
||||
const hideEvent = EventHandler.trigger(this._element, (this.constructor as typeof Tooltip).eventName(EVENT_HIDE))
|
||||
if (hideEvent.defaultPrevented) {
|
||||
if (hideEvent?.defaultPrevented) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -258,7 +294,7 @@ class Tooltip extends BaseComponent {
|
||||
tip!.classList.remove(CLASS_NAME_SHOW)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -302,7 +338,7 @@ class Tooltip extends BaseComponent {
|
||||
return this.tip
|
||||
}
|
||||
|
||||
_createTipElement(content: Record<string, any>): HTMLElement | null {
|
||||
_createTipElement(content: TooltipContentMap): HTMLElement | null {
|
||||
const tip = this._getTemplateFactory(content).toHtml() as HTMLElement
|
||||
|
||||
if (!tip) {
|
||||
@@ -323,7 +359,7 @@ class Tooltip extends BaseComponent {
|
||||
return tip
|
||||
}
|
||||
|
||||
setContent(content: Record<string, any>): void {
|
||||
setContent(content: TooltipContentMap): void {
|
||||
this._newContent = content
|
||||
if (this._isShown()) {
|
||||
this._disposePopper()
|
||||
@@ -331,7 +367,7 @@ class Tooltip extends BaseComponent {
|
||||
}
|
||||
}
|
||||
|
||||
_getTemplateFactory(content: Record<string, any>): TemplateFactory {
|
||||
_getTemplateFactory(content: TooltipContentMap): TemplateFactory {
|
||||
if (this._templateFactory) {
|
||||
this._templateFactory.changeContent(content)
|
||||
} else {
|
||||
@@ -345,13 +381,13 @@ class Tooltip extends BaseComponent {
|
||||
return this._templateFactory
|
||||
}
|
||||
|
||||
_getContentForTemplate(): Record<string, any> {
|
||||
_getContentForTemplate(): TooltipContentMap {
|
||||
return {
|
||||
[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') || ''
|
||||
}
|
||||
|
||||
@@ -373,7 +409,7 @@ class Tooltip extends BaseComponent {
|
||||
return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))
|
||||
}
|
||||
|
||||
_getOffset(): number[] | ((popperData: any) => number[]) {
|
||||
_getOffset(): number[] | PopperOffsetFunction {
|
||||
const { offset } = this._config
|
||||
|
||||
if (typeof offset === 'string') {
|
||||
@@ -381,14 +417,14 @@ class Tooltip extends BaseComponent {
|
||||
}
|
||||
|
||||
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 {
|
||||
return execute(arg, [this._element, this._element])
|
||||
_resolvePossibleFunction<T>(arg: T | ((this: HTMLElement, element: HTMLElement) => T)): T {
|
||||
return execute(arg, [this._element, this._element]) as T
|
||||
}
|
||||
|
||||
_getPopperConfig(attachment: string): Partial<Popper.Options> {
|
||||
@@ -423,7 +459,7 @@ class Tooltip extends BaseComponent {
|
||||
name: 'preSetPlacement',
|
||||
enabled: true,
|
||||
phase: 'beforeMain',
|
||||
fn: (data: any) => {
|
||||
fn: (data: Popper.ModifierArguments<Record<string, unknown>>) => {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -501,7 +531,7 @@ class Tooltip extends BaseComponent {
|
||||
if (this._isHovered) {
|
||||
this.show()
|
||||
}
|
||||
}, this._config.delay.show)
|
||||
}, this._getDelay().show)
|
||||
}
|
||||
|
||||
_leave(): void {
|
||||
@@ -515,7 +545,12 @@ class Tooltip extends BaseComponent {
|
||||
if (!this._isHovered) {
|
||||
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 {
|
||||
@@ -527,7 +562,7 @@ class Tooltip extends BaseComponent {
|
||||
return Object.values(this._activeTrigger).includes(true)
|
||||
}
|
||||
|
||||
_getConfig(config: Partial<ComponentConfig>): ComponentConfig {
|
||||
_getConfig(config?: ComponentConfigInput): ComponentConfig {
|
||||
const dataAttributes = Manipulator.getDataAttributes(this._element)
|
||||
|
||||
for (const dataAttribute of Object.keys(dataAttributes)) {
|
||||
@@ -536,18 +571,18 @@ class Tooltip extends BaseComponent {
|
||||
}
|
||||
}
|
||||
|
||||
config = {
|
||||
let merged: Record<string, unknown> = {
|
||||
...dataAttributes,
|
||||
...(typeof config === 'object' && config ? config : {}),
|
||||
}
|
||||
config = this._mergeConfigObj(config)
|
||||
config = this._configAfterMerge(config)
|
||||
this._typeCheckConfig(config)
|
||||
return config
|
||||
merged = this._mergeConfigObj(merged)
|
||||
merged = this._configAfterMerge(merged as ComponentConfig)
|
||||
this._typeCheckConfig(merged)
|
||||
return merged as 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') {
|
||||
config.delay = {
|
||||
@@ -556,22 +591,25 @@ class Tooltip extends BaseComponent {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof config.title === 'number') {
|
||||
config.title = config.title.toString()
|
||||
// Numbers are accepted at runtime (`data-bs-title="42"`) even though the type says string
|
||||
const raw = config as Record<string, unknown>
|
||||
if (typeof raw.title === 'number') {
|
||||
raw.title = raw.title.toString()
|
||||
}
|
||||
|
||||
if (typeof config.content === 'number') {
|
||||
config.content = config.content.toString()
|
||||
if (typeof raw.content === 'number') {
|
||||
raw.content = raw.content.toString()
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
_getDelegateConfig(): Partial<ComponentConfig> {
|
||||
const config: Partial<ComponentConfig> = {}
|
||||
_getDelegateConfig(): ComponentConfigInput {
|
||||
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)) {
|
||||
if ((this.constructor as typeof Tooltip).Default[key] !== value) {
|
||||
if (defaults[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>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const CLASS_NAME_FADE = 'fade'
|
||||
const CLASS_NAME_SHOW = 'show'
|
||||
const EVENT_MOUSEDOWN = `mousedown.bs.${NAME}`
|
||||
|
||||
interface BackdropConfig {
|
||||
type BackdropConfig = {
|
||||
className: string
|
||||
clickCallback: (() => void) | null
|
||||
isAnimated: boolean
|
||||
|
||||
@@ -9,11 +9,34 @@ import EventHandler from '../dom/event-handler.js'
|
||||
import SelectorEngine from '../dom/selector-engine.js'
|
||||
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 {
|
||||
EVENT_KEY: string
|
||||
NAME: string
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getOrCreateInstance(element: HTMLElement | string | null): any
|
||||
getOrCreateInstance(element: HTMLElement | string | null): ComponentInstance
|
||||
}
|
||||
|
||||
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 => {
|
||||
@@ -30,10 +53,37 @@ const enableDismissTrigger = (component: DismissibleComponent, method = 'hide'):
|
||||
}
|
||||
|
||||
const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`)
|
||||
const instance = component.getOrCreateInstance(target)
|
||||
|
||||
instance[method]()
|
||||
callMethod(component.getOrCreateInstance(target), 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_BACKWARD = 'backward'
|
||||
|
||||
interface FocusTrapConfig {
|
||||
type FocusTrapConfig = {
|
||||
autofocus: boolean
|
||||
trapElement: HTMLElement | null
|
||||
}
|
||||
@@ -87,7 +87,7 @@ class FocusTrap extends Config {
|
||||
_handleFocusin(event: FocusEvent): void {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ const MILLISECONDS_MULTIPLIER = 1000
|
||||
const TRANSITION_END = 'transitionend'
|
||||
|
||||
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)}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ const POINTER_TYPE_PEN = 'pen'
|
||||
const CLASS_NAME_POINTER_EVENT = 'pointer-event'
|
||||
const SWIPE_THRESHOLD = 40
|
||||
|
||||
interface SwipeConfig {
|
||||
type SwipeConfig = {
|
||||
endCallback: (() => void) | null
|
||||
leftCallback: (() => void) | null
|
||||
rightCallback: (() => void) | null
|
||||
@@ -43,8 +43,8 @@ const DefaultType: ComponentConfigType = {
|
||||
class Swipe extends Config {
|
||||
declare _config: SwipeConfig & ComponentConfig
|
||||
_element: HTMLElement
|
||||
_deltaX: number
|
||||
_supportPointerEvents: boolean
|
||||
declare _deltaX: number
|
||||
declare _supportPointerEvents: boolean
|
||||
|
||||
constructor(element: HTMLElement, config?: ComponentConfig) {
|
||||
super()
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { AllowList, ComponentConfig, ComponentConfigType, SanitizeFn } from
|
||||
|
||||
const NAME = 'TemplateFactory'
|
||||
|
||||
interface TemplateFactoryConfig {
|
||||
type TemplateFactoryConfig = {
|
||||
allowList: AllowList
|
||||
content: Record<string, unknown>
|
||||
extraClass: string | (() => string)
|
||||
@@ -104,7 +104,7 @@ class TemplateFactory extends Config {
|
||||
|
||||
_typeCheckConfig(config: ComponentConfig): void {
|
||||
super._typeCheckConfig(config)
|
||||
this._checkContent(config.content)
|
||||
this._checkContent(config.content as Record<string, unknown>)
|
||||
}
|
||||
|
||||
_checkContent(arg: Record<string, unknown>): void {
|
||||
|
||||
@@ -3,7 +3,7 @@ const countupElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLE
|
||||
|
||||
if (countupElements.length) {
|
||||
countupElements.forEach(function (element: HTMLElement) {
|
||||
let options: Record<string, any> = {}
|
||||
let options: Record<string, unknown> = {}
|
||||
try {
|
||||
const dataOptions = element.getAttribute('data-countup') ? JSON.parse(element.getAttribute('data-countup')!) : {}
|
||||
options = Object.assign(
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Dropdown } from './bootstrap'
|
||||
const dropdownTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="dropdown"]'))
|
||||
dropdownTriggerList.map(function (dropdownTriggerEl: HTMLElement) {
|
||||
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)
|
||||
})
|
||||
|
||||
Vendored
+3
-3
@@ -4,12 +4,12 @@ interface Window {
|
||||
CountUp: new (
|
||||
target: HTMLElement,
|
||||
endVal: number,
|
||||
options?: any,
|
||||
options?: Record<string, unknown>,
|
||||
) => {
|
||||
error: boolean
|
||||
start: () => void
|
||||
}
|
||||
}
|
||||
IMask?: new (element: HTMLElement, options: { mask: string; lazy?: boolean }) => any
|
||||
Sortable?: new (element: HTMLElement, options?: any) => any
|
||||
IMask?: new (element: HTMLElement, options: { mask: string; lazy?: boolean }) => unknown
|
||||
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) {
|
||||
window.IMask &&
|
||||
new window.IMask(maskEl, {
|
||||
mask: maskEl.dataset.mask,
|
||||
mask: maskEl.dataset.mask as string,
|
||||
lazy: maskEl.dataset.maskVisible !== 'true',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ const sortableElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTML
|
||||
|
||||
if (sortableElements.length) {
|
||||
sortableElements.forEach(function (element: HTMLElement) {
|
||||
let options: Record<string, any> = {}
|
||||
let options: Record<string, unknown> = {}
|
||||
|
||||
try {
|
||||
const rawOptions = element.getAttribute('data-sortable')
|
||||
|
||||
@@ -5,14 +5,12 @@
|
||||
*/
|
||||
import { themeDefaults, type ThemeKey } from './src/theme-config'
|
||||
|
||||
const params = new Proxy(new URLSearchParams(window.location.search), {
|
||||
get: (searchParams: URLSearchParams, prop: string): string | null => searchParams.get(prop),
|
||||
})
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
|
||||
for (const key in themeDefaults) {
|
||||
const param = params[key]
|
||||
const param = params.get(key)
|
||||
let selectedValue: string
|
||||
|
||||
if (!!param) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, beforeAll, afterEach, vi } from 'vitest'
|
||||
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'
|
||||
|
||||
class DummyClass extends BaseComponent {
|
||||
@@ -85,4 +85,64 @@ describe('Component Functions', () => {
|
||||
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",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": false,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
@@ -17,14 +17,6 @@
|
||||
"sourceMap": true,
|
||||
"allowJs": true
|
||||
},
|
||||
"include": [
|
||||
"js/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
"include": ["js/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user