Restore jQuery bridge for Bootstrap component events (#3168)

This commit is contained in:
Bartosz-Do authored and GitHub committed 2026-09-24 18:16:22 +02:00
1 parent fa70c0368f
commit 531529a13c
17 files changed
+359 -19

No files matched your search

+20
View File
@@ -8,6 +8,8 @@
import BaseComponent from './base-component'
import EventHandler from './dom/event-handler.js'
import { enableDismissTrigger } from './util/component-functions'
import { defineJQueryPlugin } from './util/index.js'
import type { JQueryCollectionLike } from './types'
const NAME = 'alert'
const DATA_KEY = 'bs.alert'
@@ -41,8 +43,26 @@ class Alert extends BaseComponent {
EventHandler.trigger(this._element, EVENT_CLOSED)
this.dispose()
}
static jQueryInterface(this: JQueryCollectionLike, config?: unknown): unknown {
return this.each(function (this: HTMLElement) {
const data = Alert.getOrCreateInstance(this) as unknown as Record<string, (arg?: unknown) => unknown>
if (typeof config !== 'string') {
return
}
if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
throw new TypeError(`No method named "${config}"`)
}
data[config](this)
})
}
}
enableDismissTrigger(Alert, 'close')
defineJQueryPlugin(Alert)
export default Alert
+14
View File
@@ -7,6 +7,8 @@
import BaseComponent from './base-component'
import EventHandler from './dom/event-handler.js'
import { defineJQueryPlugin } from './util/index.js'
import type { JQueryCollectionLike } from './types'
const NAME = 'button'
const DATA_KEY = 'bs.button'
@@ -27,6 +29,16 @@ class Button extends BaseComponent {
toggle(): void {
this._element.setAttribute('aria-pressed', String(this._element.classList.toggle(CLASS_NAME_ACTIVE)))
}
static jQueryInterface(this: JQueryCollectionLike, config?: unknown): unknown {
return this.each(function (this: HTMLElement) {
const data = Button.getOrCreateInstance(this) as Button
if (config === 'toggle') {
data.toggle()
}
})
}
}
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, (event: Event) => {
@@ -41,4 +53,6 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, (event: Ev
data.toggle()
})
defineJQueryPlugin(Button)
export default Button
+23 -1
View File
@@ -9,7 +9,8 @@ import BaseComponent from './base-component'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
import SelectorEngine from './dom/selector-engine'
import { getNextActiveElement, isRTL, isVisible, reflow, triggerTransitionEnd } from './util/index'
import { defineJQueryPlugin, getNextActiveElement, isRTL, isVisible, reflow, triggerTransitionEnd } from './util/index'
import type { ComponentConfig as BaseComponentConfig, JQueryCollectionLike } from './types'
import Swipe from './util/swipe'
type ComponentConfig = {
@@ -393,6 +394,25 @@ class Carousel extends BaseComponent {
return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT
}
static jQueryInterface(this: JQueryCollectionLike, config?: unknown): unknown {
return this.each(function (this: HTMLElement) {
const data = Carousel.getOrCreateInstance(this, config as BaseComponentConfig) as unknown as Record<string, (arg?: unknown) => unknown>
if (typeof config === 'number') {
data.to(config)
return
}
if (typeof config === 'string') {
if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
}
})
}
}
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, function (this: HTMLElement, event: Event) {
@@ -431,4 +451,6 @@ EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
}
})
defineJQueryPlugin(Carousel)
export default Carousel
+23 -2
View File
@@ -8,9 +8,9 @@
import BaseComponent from './base-component'
import EventHandler from './dom/event-handler'
import SelectorEngine from './dom/selector-engine'
import { getElement, reflow } from './util/index'
import { defineJQueryPlugin, getElement, reflow } from './util/index'
import type { DelegatedEvent } from './dom/event-handler'
import type { ElementSelector } from './types'
import type { ComponentConfig as BaseComponentConfig, ElementSelector, JQueryCollectionLike } from './types'
type ComponentConfig = {
parent: HTMLElement | null
@@ -247,6 +247,25 @@ class Collapse extends BaseComponent {
element.setAttribute('aria-expanded', String(isOpen))
}
}
static jQueryInterface(this: JQueryCollectionLike, config?: unknown): unknown {
const _config: BaseComponentConfig = {}
if (typeof config === 'string' && /show|hide/.test(config)) {
_config.toggle = false
}
return this.each(function (this: HTMLElement) {
const data = Collapse.getOrCreateInstance(this, _config) as unknown as Record<string, (arg?: unknown) => unknown>
if (typeof config === 'string') {
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
}
})
}
}
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (this: HTMLElement, event: Event) {
@@ -260,4 +279,6 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
}
})
defineJQueryPlugin(Collapse)
export default Collapse
+33 -2
View File
@@ -5,6 +5,9 @@
* --------------------------------------------------------------------------
*/
import { getjQuery } from '../util/index.js'
import type { JQueryEventLike } from '../types'
// `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
@@ -261,9 +264,37 @@ const EventHandler = {
return null
}
const evt = hydrateObj(new Event(event, { bubbles: true, cancelable: true }), args)
const $ = getjQuery()
const typeEvent = getTypeEvent(event)
const inNamespace = event !== typeEvent
element.dispatchEvent(evt)
let jQueryEvent: JQueryEventLike | null = null
let bubbles = true
let nativeDispatch = true
let defaultPrevented = false
if (inNamespace && $) {
jQueryEvent = $.Event(event, args)
$(element).trigger(jQueryEvent)
bubbles = !jQueryEvent.isPropagationStopped()
nativeDispatch = !jQueryEvent.isImmediatePropagationStopped()
defaultPrevented = jQueryEvent.isDefaultPrevented()
}
const evt = hydrateObj(new Event(event, { bubbles, cancelable: true }), args)
if (defaultPrevented) {
evt.preventDefault()
}
if (nativeDispatch) {
element.dispatchEvent(evt)
}
if (evt.defaultPrevented && jQueryEvent) {
jQueryEvent.preventDefault()
}
return evt
},
+20 -1
View File
@@ -10,8 +10,9 @@ import BaseComponent from './base-component'
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 { defineJQueryPlugin, execute, getElement, getNextActiveElement, isDisabled, isElement, isRTL, isVisible, noop } from './util/index'
import type { DelegatedEvent } from './dom/event-handler'
import type { ComponentConfig as BaseComponentConfig, JQueryCollectionLike } from './types'
type PopperOffsetData = { placement: Popper.Placement; reference: Popper.Rect; popper: Popper.Rect }
type PopperOffsetFunction = (popperData: PopperOffsetData) => number[]
@@ -395,6 +396,22 @@ class Dropdown extends BaseComponent {
getToggleButton!.focus()
}
}
static jQueryInterface(this: JQueryCollectionLike, config?: unknown): unknown {
return this.each(function (this: HTMLElement) {
const data = Dropdown.getOrCreateInstance(this, config as BaseComponentConfig) as unknown as Record<string, (arg?: unknown) => unknown>
if (typeof config !== 'string') {
return
}
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
})
}
}
EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown.dataApiKeydownHandler)
@@ -406,4 +423,6 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
;(Dropdown.getOrCreateInstance(this) as Dropdown).toggle()
})
defineJQueryPlugin(Dropdown)
export default Dropdown
+20 -1
View File
@@ -11,8 +11,9 @@ import SelectorEngine from './dom/selector-engine'
import Backdrop from './util/backdrop'
import { enableDismissTrigger } from './util/component-functions'
import FocusTrap from './util/focustrap'
import { isRTL, isVisible, reflow } from './util/index'
import { defineJQueryPlugin, isRTL, isVisible, reflow } from './util/index'
import ScrollBarHelper from './util/scrollbar'
import type { ComponentConfig as BaseComponentConfig, JQueryCollectionLike } from './types'
/**
* Constants
@@ -318,6 +319,22 @@ class Modal extends BaseComponent {
this._element.style.paddingLeft = ''
this._element.style.paddingRight = ''
}
static jQueryInterface(this: JQueryCollectionLike, config?: unknown, relatedTarget?: unknown): unknown {
return this.each(function (this: HTMLElement) {
const data = Modal.getOrCreateInstance(this, config as BaseComponentConfig) as unknown as Record<string, (arg?: unknown) => unknown>
if (typeof config !== 'string') {
return
}
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config](relatedTarget)
})
}
}
/**
@@ -359,4 +376,6 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
enableDismissTrigger(Modal)
defineJQueryPlugin(Modal)
export default Modal
+20 -1
View File
@@ -11,8 +11,9 @@ import SelectorEngine from './dom/selector-engine'
import Backdrop from './util/backdrop'
import { enableDismissTrigger } from './util/component-functions'
import FocusTrap from './util/focustrap'
import { isDisabled, isVisible } from './util/index'
import { defineJQueryPlugin, isDisabled, isVisible } from './util/index'
import ScrollBarHelper from './util/scrollbar'
import type { ComponentConfig as BaseComponentConfig, JQueryCollectionLike } from './types'
/**
* Constants
@@ -212,6 +213,22 @@ class Offcanvas extends BaseComponent {
EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)
})
}
static jQueryInterface(this: JQueryCollectionLike, config?: unknown): unknown {
return this.each(function (this: HTMLElement) {
const data = Offcanvas.getOrCreateInstance(this, config as BaseComponentConfig) as unknown as Record<string, (arg?: unknown) => unknown>
if (typeof config !== 'string') {
return
}
if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
throw new TypeError(`No method named "${config}"`)
}
data[config](this)
})
}
}
/**
@@ -264,4 +281,6 @@ EventHandler.on(window, EVENT_RESIZE, () => {
enableDismissTrigger(Offcanvas)
defineJQueryPlugin(Offcanvas)
export default Offcanvas
+20
View File
@@ -7,6 +7,8 @@
import Tooltip from './tooltip'
import type { TooltipConfig, TooltipContent, TooltipContentMap } from './tooltip'
import { defineJQueryPlugin } from './util/index'
import type { ComponentConfig as BaseComponentConfig, JQueryCollectionLike } from './types'
/**
* Constants
@@ -75,6 +77,24 @@ class Popover extends Tooltip {
_getContent(): string | HTMLElement | null {
return this._resolvePossibleFunction(this._config.content)
}
static jQueryInterface(this: JQueryCollectionLike, config?: unknown): unknown {
return this.each(function (this: HTMLElement) {
const data = Popover.getOrCreateInstance(this, config as BaseComponentConfig) as unknown as Record<string, (arg?: unknown) => unknown>
if (typeof config !== 'string') {
return
}
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
})
}
}
defineJQueryPlugin(Popover)
export default Popover
+20 -1
View File
@@ -8,7 +8,8 @@
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 { defineJQueryPlugin, getElement, isDisabled, isVisible, parseSelector } from './util/index'
import type { ComponentConfig as BaseComponentConfig, JQueryCollectionLike } from './types'
type ComponentConfig = {
offset: number | null
@@ -253,6 +254,22 @@ class ScrollSpy extends BaseComponent {
node.classList.remove(CLASS_NAME_ACTIVE)
}
}
static jQueryInterface(this: JQueryCollectionLike, config?: unknown): unknown {
return this.each(function (this: HTMLElement) {
const data = ScrollSpy.getOrCreateInstance(this, config as BaseComponentConfig) as unknown as Record<string, (arg?: unknown) => unknown>
if (typeof config !== 'string') {
return
}
if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
})
}
}
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
@@ -261,4 +278,6 @@ EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
}
})
defineJQueryPlugin(ScrollSpy)
export default ScrollSpy
+20 -1
View File
@@ -8,7 +8,8 @@
import BaseComponent from './base-component'
import EventHandler from './dom/event-handler'
import SelectorEngine from './dom/selector-engine'
import { getNextActiveElement, isDisabled } from './util/index'
import { defineJQueryPlugin, getNextActiveElement, isDisabled } from './util/index'
import type { JQueryCollectionLike } from './types'
const NAME = 'tab'
const DATA_KEY = 'bs.tab'
@@ -245,6 +246,22 @@ class Tab extends BaseComponent {
_getOuterElement(elem: HTMLElement): HTMLElement {
return elem.closest(SELECTOR_OUTER) || elem
}
static jQueryInterface(this: JQueryCollectionLike, config?: unknown): unknown {
return this.each(function (this: HTMLElement) {
const data = Tab.getOrCreateInstance(this) as unknown as Record<string, (arg?: unknown) => unknown>
if (typeof config !== 'string') {
return
}
if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
})
}
}
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (this: HTMLElement, event: Event) {
@@ -265,4 +282,6 @@ EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
}
})
defineJQueryPlugin(Tab)
export default Tab
+18 -2
View File
@@ -8,8 +8,8 @@
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 { ElementSelector } from './types'
import { defineJQueryPlugin, reflow } from './util/index.js'
import type { ComponentConfig as BaseComponentConfig, ElementSelector, JQueryCollectionLike } from './types'
type ComponentConfig = {
animation: boolean
@@ -196,8 +196,24 @@ class Toast extends BaseComponent {
clearTimeout(this._timeout!)
this._timeout = null
}
static jQueryInterface(this: JQueryCollectionLike, config?: unknown): unknown {
return this.each(function (this: HTMLElement) {
const data = Toast.getOrCreateInstance(this, config as BaseComponentConfig) as unknown as Record<string, (arg?: unknown) => unknown>
if (typeof config === 'string') {
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config](this)
}
})
}
}
enableDismissTrigger(Toast)
defineJQueryPlugin(Toast)
export default Toast
+20 -2
View File
@@ -9,10 +9,10 @@ import * as Popper from '@popperjs/core'
import BaseComponent from './base-component'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
import { execute, findShadowRoot, getElement, getUID, isRTL, noop } from './util/index'
import { defineJQueryPlugin, 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'
import type { AllowList, ComponentConfig as BaseComponentConfig, JQueryCollectionLike, SanitizeFn } from './types'
/**
* Constants
@@ -631,6 +631,24 @@ class Tooltip extends BaseComponent {
this.tip = null
}
}
static jQueryInterface(this: JQueryCollectionLike, config?: unknown): unknown {
return this.each(function (this: HTMLElement) {
const data = Tooltip.getOrCreateInstance(this, config as BaseComponentConfig) as unknown as Record<string, (arg?: unknown) => unknown>
if (typeof config !== 'string') {
return
}
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
})
}
}
defineJQueryPlugin(Tooltip)
export default Tooltip
+29
View File
@@ -25,3 +25,32 @@ export interface BaseComponentStatic extends ConfigStatic {
export type AllowList = Record<string, (string | RegExp)[]>
export type SanitizeFn = (unsafeHtml: string) => string
export interface JQueryEventLike {
isPropagationStopped(): boolean
isImmediatePropagationStopped(): boolean
isDefaultPrevented(): boolean
preventDefault(): void
}
export interface JQueryCollectionLike {
trigger(event: JQueryEventLike): unknown
each(callback: (this: HTMLElement, index: number) => void): unknown
}
export interface JQueryPluginFunction {
(this: JQueryCollectionLike, config?: unknown, ...args: unknown[]): unknown
Constructor?: unknown
noConflict?: () => JQueryPluginFunction
}
export interface JQueryStaticLike {
fn: Record<string, JQueryPluginFunction>
(selector: EventTarget): JQueryCollectionLike
Event(name: string, args?: Record<string, unknown>): JQueryEventLike
}
export interface JQueryPluginStatic {
NAME: string
jQueryInterface: JQueryPluginFunction
}
+45 -1
View File
@@ -5,6 +5,8 @@
* --------------------------------------------------------------------------
*/
import type { JQueryPluginStatic, JQueryStaticLike } from '../types'
const MAX_UID = 1_000_000
const MILLISECONDS_MULTIPLIER = 1000
const TRANSITION_END = 'transitionend'
@@ -156,6 +158,48 @@ const reflow = (element: HTMLElement): void => {
const isRTL = (): boolean => document.documentElement.dir === 'rtl'
const getjQuery = (): JQueryStaticLike | null => {
if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {
return window.jQuery
}
return null
}
const DOMContentLoadedCallbacks: Array<() => void> = []
const onDOMContentLoaded = (callback: () => void): void => {
if (document.readyState === 'loading') {
if (!DOMContentLoadedCallbacks.length) {
document.addEventListener('DOMContentLoaded', () => {
for (const domCallback of DOMContentLoadedCallbacks) {
domCallback()
}
})
}
DOMContentLoadedCallbacks.push(callback)
} else {
callback()
}
}
const defineJQueryPlugin = (plugin: JQueryPluginStatic): void => {
onDOMContentLoaded(() => {
const $ = getjQuery()
if ($) {
const name = plugin.NAME
const JQUERY_NO_CONFLICT = $.fn[name]
$.fn[name] = plugin.jQueryInterface
$.fn[name].Constructor = plugin
$.fn[name].noConflict = () => {
$.fn[name] = JQUERY_NO_CONFLICT
return plugin.jQueryInterface
}
}
})
}
const execute = (possibleCallback: unknown, args: unknown[] = [], defaultValue: unknown = possibleCallback): unknown => {
return typeof possibleCallback === 'function' ? possibleCallback.call(args[0], ...args.slice(1)) : defaultValue
}
@@ -206,4 +250,4 @@ const getNextActiveElement = <T>(list: T[], activeElement: T, shouldGetNext: boo
return list[Math.max(0, Math.min(index, listLength - 1))]
}
export { execute, executeAfterTransition, findShadowRoot, getElement, getNextActiveElement, getTransitionDurationFromElement, getUID, isDisabled, isElement, isRTL, isVisible, noop, parseSelector, reflow, triggerTransitionEnd, toType }
export { defineJQueryPlugin, execute, executeAfterTransition, findShadowRoot, getElement, getjQuery, getNextActiveElement, getTransitionDurationFromElement, getUID, isDisabled, isElement, isRTL, isVisible, noop, onDOMContentLoaded, parseSelector, reflow, triggerTransitionEnd, toType }
+9 -4
View File
@@ -1,9 +1,14 @@
import type { JQueryStaticLike } from './bootstrap/types'
// Plugins that are loaded separately, not bundled: IMask (https://imask.js.org),
// SortableJS (https://sortablejs.github.io/Sortable/) and Vanilla Calendar Pro
// (https://vanilla-calendar.pro). The instance types live in the component
// modules, so they are part of the published `dist/types`.
interface Window {
VanillaCalendarPro?: typeof import('vanilla-calendar-pro')
IMask?: new (element: HTMLElement, options: { mask: string; lazy?: boolean }) => import('./input-mask').IMaskInstance
Sortable?: new (element: HTMLElement, options?: Record<string, unknown>) => import('./sortable').SortableInstance
declare global {
interface Window {
VanillaCalendarPro?: typeof import('vanilla-calendar-pro')
IMask?: new (element: HTMLElement, options: { mask: string; lazy?: boolean }) => import('./input-mask').IMaskInstance
Sortable?: new (element: HTMLElement, options?: Record<string, unknown>) => import('./sortable').SortableInstance
jQuery?: JQueryStaticLike
}
}