Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
321 changes: 321 additions & 0 deletions src/utils/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
/**
* Bootstrap Table Event System Utility Library
* Provides jQuery-style event handling APIs using native JavaScript
*/

import DOMHelper from './dom.js'

class EventHelper {
/**
* Add event listener with namespace support
* @param {Element|string} element - DOM element or selector
* @param {string} event - Event name with optional namespace (e.g., "click.namespace")
* @param {Function} handler - Event handler function
* @param {Object|boolean} [options] - Event options or useCapture flag
* @returns {Element} The element itself
Comment on lines +9 to +15
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete JSDoc return type documentation. The return type is documented as "The element itself", but the method can also return null when the element parameter is null/undefined or when a selector doesn't match any element (line 19). The return type should be documented as {Element|null} to accurately reflect all possible return values.

Copilot uses AI. Check for mistakes.
*/
static on (element, event, handler, options = {}) {
if (typeof element === 'string') element = DOMHelper.$(element)
if (!element) return element

// Validate handler
if (typeof handler !== 'function') {
throw new Error('Event handler must be a function')
}

// Parse event type and namespace
const [eventType, namespace] = event.split('.')
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Namespace parsing discards multiple namespace segments. When an event string like "click.ns1.ns2" is provided, the split and destructuring at line 27 only captures namespace = "ns1", discarding "ns2". The expected behavior for jQuery-style namespacing is that the entire string after the first dot should be treated as a single namespace. Change the parsing to preserve the full namespace: const parts = event.split('.'); const eventType = parts[0]; const namespace = parts.slice(1).join('.') or use const [eventType, ...nsParts] = event.split('.'); const namespace = nsParts.join('.').

Copilot uses AI. Check for mistakes.

// Initialize event handlers storage if not exists
if (!element._eventHandlers) {
element._eventHandlers = {}
}
if (!element._eventHandlers[eventType]) {
element._eventHandlers[eventType] = []
}

// Create wrapped handler to maintain context
const wrappedHandler = e => {
handler.call(element, e)
}

// Store handler info
element._eventHandlers[eventType].push({
handler,
namespace,
wrappedHandler,
original: handler
})
Comment on lines +43 to +48
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handler is stored twice in the handlers array. Line 44 stores handler while line 47 stores original: handler, which are both references to the same function. This creates redundancy in the stored data structure. Consider removing the handler property and only keeping original.

Copilot uses AI. Check for mistakes.

// Add native event listener
element.addEventListener(eventType, wrappedHandler, options)
return element
}

/**
* Remove event listener with namespace support
* @param {Element|string} element - DOM element or selector
* @param {string} event - Event name with optional namespace
* @param {Function} [handler] - Specific handler to remove (optional)
* @returns {Element} The element itself
Comment on lines +55 to +60
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete JSDoc return type documentation. The return type is documented as "The element itself", but the method can also return null when the element parameter is null/undefined or when a selector doesn't match any element (line 64). The return type should be documented as {Element|null} to accurately reflect all possible return values.

Copilot uses AI. Check for mistakes.
*/
static off (element, event, handler) {
if (typeof element === 'string') element = DOMHelper.$(element)
if (!element || !element._eventHandlers) return element

const [eventType, namespace] = event.split('.')
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Namespace parsing discards multiple namespace segments. When an event string like "click.ns1.ns2" is provided, the split and destructuring at line 66 only captures namespace = "ns1", discarding "ns2". This is the same issue as in the on method. Change the parsing to preserve the full namespace: const parts = event.split('.'); const eventType = parts[0]; const namespace = parts.slice(1).join('.').

Copilot uses AI. Check for mistakes.

if (element._eventHandlers[eventType]) {
// Find handlers to remove
const handlersToRemove = element._eventHandlers[eventType].filter(info => {
const matches =
(!namespace || info.namespace === namespace) &&
(!handler || info.original === handler)

if (matches && info.wrappedHandler) {
element.removeEventListener(eventType, info.wrappedHandler)
}

return matches
})

// Remove handlers from array
element._eventHandlers[eventType] = element._eventHandlers[eventType].filter(info =>
!handlersToRemove.includes(info)
)

// Clean up empty event type arrays
if (element._eventHandlers[eventType].length === 0) {
delete element._eventHandlers[eventType]
}

// Clean up if no handlers left at all
if (Object.keys(element._eventHandlers).length === 0) {
delete element._eventHandlers
}
}

return element
}

/**
* Trigger custom event
* @param {Element|string} element - DOM element or selector
* @param {string} event - Event name
* @param {*} [detail] - Custom data to pass with event
* @param {Object} [options] - Event options
* @returns {Element} The element itself
Comment on lines +101 to +107
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete JSDoc return type documentation. The return type is documented as "The element itself", but the method can also return null when the element parameter is null/undefined or when a selector doesn't match any element (line 111). The return type should be documented as {Element|null} to accurately reflect all possible return values.

Copilot uses AI. Check for mistakes.
*/
static trigger (element, event, detail = {}, options = {}) {
if (typeof element === 'string') element = DOMHelper.$(element)
if (!element) return element

const customEvent = new CustomEvent(event, {
detail,
bubbles: options.bubbles !== false,
cancelable: options.cancelable !== false
})

element.dispatchEvent(customEvent)
return element
}

/**
* Event delegation - add event listener that works for dynamically added elements
* @param {Element|string} parent - Parent element or selector
* @param {string} selector - Child element selector
* @param {string} event - Event name with optional namespace
* @param {Function} handler - Event handler function
* @param {Object|boolean} [options] - Event options or useCapture flag
* @returns {Element} The parent element
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete JSDoc return type documentation. The return type is documented as "The parent element", but the method can also return null when the parent parameter is null/undefined or when a selector doesn't match any element (line 134). The return type should be documented as {Element|null} to accurately reflect all possible return values.

Suggested change
* @returns {Element} The parent element
* @returns {Element|null} The parent element

Copilot uses AI. Check for mistakes.
*/
static delegate (parent, selector, event, handler, options = {}) {
if (typeof parent === 'string') parent = DOMHelper.$(parent)
if (!parent) return parent

const [eventType, namespace] = event.split('.')
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Namespace parsing discards multiple namespace segments. When an event string like "click.ns1.ns2" is provided, the split and destructuring at line 136 only captures namespace = "ns1", discarding "ns2". This is the same issue as in the on and off methods. Change the parsing to preserve the full namespace: const parts = event.split('.'); const eventType = parts[0]; const namespace = parts.slice(1).join('.').

Copilot uses AI. Check for mistakes.
const eventWithNamespace = namespace ? `${eventType}.${namespace}` : eventType

// Delegate handler that checks if target matches selector
const delegateHandler = e => {
const target = e.target.closest(selector)

if (target && parent.contains(target)) {
// Set the correct context and pass target as this
handler.call(target, e)
}
}

// Store delegate handler for potential removal
if (!parent._delegates) {
parent._delegates = []
}
parent._delegates.push({
selector,
eventType,
namespace,
handler: delegateHandler
})

// Add event listener to parent
return this.on(parent, eventWithNamespace, delegateHandler, options)
}

/**
* Remove delegated event listener
* @param {Element|string} parent - Parent element or selector
* @param {string} selector - Child element selector
* @param {string} event - Event name with optional namespace
* @param {Function} [handler] - Specific handler to remove
* @returns {Element} The parent element
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete JSDoc return type documentation. The return type is documented as "The parent element", but the method can also return null when the parent parameter is null/undefined or when a selector doesn't match any element (line 174). The return type should be documented as {Element|null} to accurately reflect all possible return values.

Suggested change
* @returns {Element} The parent element
* @returns {Element|null} The parent element

Copilot uses AI. Check for mistakes.
*/
static undelegate (parent, selector, event, handler) {
if (typeof parent === 'string') parent = DOMHelper.$(parent)
if (!parent || !parent._delegates) return parent

const [eventType, namespace] = event.split('.')
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Namespace parsing discards multiple namespace segments. When an event string like "click.ns1.ns2" is provided, the split and destructuring at line 176 only captures namespace = "ns1", discarding "ns2". This is the same issue as in other methods. Change the parsing to preserve the full namespace: const parts = event.split('.'); const eventType = parts[0]; const namespace = parts.slice(1).join('.').

Copilot uses AI. Check for mistakes.
const eventWithNamespace = namespace ? `${eventType}.${namespace}` : eventType

// Find and remove matching delegate
parent._delegates = parent._delegates.filter(delegate => {
const matches =
delegate.selector === selector &&
delegate.eventType === eventType &&
(!namespace || delegate.namespace === namespace) &&
(!handler || delegate.handler === handler)
Comment on lines +180 to +185
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The undelegate method has a potential bug in handler matching. Line 185 checks (!handler || delegate.handler === handler), but delegate.handler is the wrapped delegate handler created in the delegate method, not the original handler passed by the user. This means the optional handler parameter for selective removal will never match, making it impossible to remove a specific delegated handler while keeping others. The comparison should be against the original handler, which would require storing it in the delegate metadata.

Copilot uses AI. Check for mistakes.

if (matches) {
this.off(parent, eventWithNamespace, delegate.handler)
}

return !matches
})

Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memory leak: the _delegates array is never cleaned up when it becomes empty. After all delegated handlers are removed via undelegate, the empty array remains attached to the parent element. Consider adding cleanup logic similar to the _eventHandlers cleanup in the off method, deleting the _delegates property when the array becomes empty.

Suggested change
// Clean up delegates storage if no delegates remain
if (parent._delegates.length === 0) {
delete parent._delegates
}

Copilot uses AI. Check for mistakes.
return parent
}

/**
* One-time event listener - automatically removed after first trigger
* @param {Element|string} element - DOM element or selector
* @param {string} event - Event name with optional namespace
* @param {Function} handler - Event handler function
* @param {Object|boolean} [options] - Event options or useCapture flag
* @returns {Element} The element itself
Comment on lines +197 to +203
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete JSDoc return type documentation. The return type is documented as "The element itself", but the method can also return null when the element parameter is null/undefined or when a selector doesn't match any element (line 207). The return type should be documented as {Element|null} to accurately reflect all possible return values.

Copilot uses AI. Check for mistakes.
*/
static one (element, event, handler, options = {}) {
if (typeof element === 'string') element = DOMHelper.$(element)
if (!element) return element

const oneTimeHandler = e => {
// Remove the listener before executing
this.off(element, event, oneTimeHandler)
// Execute the original handler
handler.call(element, e)
}

return this.on(element, event, oneTimeHandler, options)
}

/**
* Hover event convenience method - enter and leave handlers
* @param {Element|string} element - DOM element or selector
* @param {Function} enterHandler - Mouse enter handler
* @param {Function} leaveHandler - Mouse leave handler
* @returns {Element} The element itself
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete JSDoc return type documentation. The return type is documented as "The element itself", but the method can also return null when the element parameter is null/undefined or when a selector doesn't match any element (line 228). The return type should be documented as {Element|null} to accurately reflect all possible return values.

Suggested change
* @returns {Element} The element itself
* @returns {Element|null} The element itself or null if no element is found

Copilot uses AI. Check for mistakes.
*/
static hover (element, enterHandler, leaveHandler) {
if (typeof element === 'string') element = DOMHelper.$(element)
if (!element) return element

// Use mouseenter and mouseleave which don't bubble
if (enterHandler) {
this.on(element, 'mouseenter', enterHandler)
}
if (leaveHandler) {
this.on(element, 'mouseleave', leaveHandler)
}

return element
}

/**
* Get all event handlers for an element (for debugging)
* @param {Element|string} element - DOM element or selector
* @returns {Object} Event handlers object
*/
static getHandlers (element) {
if (typeof element === 'string') element = DOMHelper.$(element)
if (!element) return {}

return element._eventHandlers || {}
}

/**
* Remove all event listeners from an element
* @param {Element|string} element - DOM element or selector
* @param {string} [namespace] - Optional namespace to filter by
* @returns {Element} The element itself
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete JSDoc return type documentation. The return type is documented as "The element itself", but the method can also return null when the element parameter is null/undefined or when a selector doesn't match any element (line 261). The return type should be documented as {Element|null} to accurately reflect all possible return values.

Suggested change
* @returns {Element} The element itself
* @returns {Element|null} The element itself or null if no element is found

Copilot uses AI. Check for mistakes.
*/
static offAll (element, namespace) {
if (typeof element === 'string') element = DOMHelper.$(element)
if (!element || !element._eventHandlers) return element

// Get all event types
const eventTypes = Object.keys(element._eventHandlers)

eventTypes.forEach(eventType => {
// Remove all handlers for this event type
const handlersToRemove = element._eventHandlers[eventType].filter(info => !namespace || info.namespace === namespace)

handlersToRemove.forEach(info => {
if (info.wrappedHandler) {
element.removeEventListener(eventType, info.wrappedHandler)
}
})

// Update handlers array
element._eventHandlers[eventType] = element._eventHandlers[eventType].filter(info => namespace && info.namespace !== namespace)

// Clean up if no handlers left
if (element._eventHandlers[eventType].length === 0) {
delete element._eventHandlers[eventType]
}
})

// Clean up if no handlers left at all
if (Object.keys(element._eventHandlers).length === 0) {
delete element._eventHandlers
}

return element
}

/**
* Check if element has event listeners
* @param {Element|string} element - DOM element or selector
* @param {string} [event] - Optional event type to check
* @param {string} [namespace] - Optional namespace to check
* @returns {boolean} Whether the element has listeners
*/
static hasListeners (element, event, namespace) {
if (typeof element === 'string') element = DOMHelper.$(element)
if (!element || !element._eventHandlers) return false

if (!event) {
return Object.keys(element._eventHandlers).length > 0
}

const [eventType] = event.split('.')

if (!element._eventHandlers[eventType]) return false

if (!namespace) {
return element._eventHandlers[eventType].length > 0
}

return element._eventHandlers[eventType].some(info => info.namespace === namespace)
}
}

// Export EventHelper class
export default EventHelper
Loading
Loading