diff --git a/src/utils/events.js b/src/utils/events.js
new file mode 100644
index 000000000..119afe27c
--- /dev/null
+++ b/src/utils/events.js
@@ -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
+ */
+ 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('.')
+
+ // 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
+ })
+
+ // 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
+ */
+ static off (element, event, handler) {
+ if (typeof element === 'string') element = DOMHelper.$(element)
+ if (!element || !element._eventHandlers) return element
+
+ const [eventType, namespace] = event.split('.')
+
+ 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
+ */
+ 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
+ */
+ static delegate (parent, selector, event, handler, options = {}) {
+ if (typeof parent === 'string') parent = DOMHelper.$(parent)
+ if (!parent) return parent
+
+ const [eventType, namespace] = event.split('.')
+ 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
+ */
+ static undelegate (parent, selector, event, handler) {
+ if (typeof parent === 'string') parent = DOMHelper.$(parent)
+ if (!parent || !parent._delegates) return parent
+
+ const [eventType, namespace] = event.split('.')
+ 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)
+
+ if (matches) {
+ this.off(parent, eventWithNamespace, delegate.handler)
+ }
+
+ return !matches
+ })
+
+ 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
+ */
+ 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
+ */
+ 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
+ */
+ 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
diff --git a/tests/utils/events.test.js b/tests/utils/events.test.js
new file mode 100644
index 000000000..c226f1f40
--- /dev/null
+++ b/tests/utils/events.test.js
@@ -0,0 +1,483 @@
+/**
+ * Unit tests for EventHelper utility
+ */
+
+import { beforeEach, describe, expect, it } from 'vitest'
+import EventHelper from '@/utils/events.js'
+import DOMHelper from '@/utils/dom.js'
+
+// Setup test environment
+beforeEach(() => {
+ // Clear document body before each test
+ document.body.innerHTML = ''
+})
+
+describe('EventHelper', () => {
+ describe('Basic event handling', () => {
+ it('should add and trigger event listener', () => {
+ const element = DOMHelper.create('
Test
')
+ let clicked = false
+
+ EventHelper.on(element, 'click', () => {
+ clicked = true
+ })
+
+ EventHelper.trigger(element, 'click')
+ expect(clicked).toBe(true)
+ })
+
+ it('should add event listener with selector', () => {
+ document.body.innerHTML = 'Test
'
+ let clicked = false
+
+ EventHelper.on('#test', 'click', () => {
+ clicked = true
+ })
+
+ DOMHelper.$('#test').click()
+ expect(clicked).toBe(true)
+ })
+
+ it('should remove specific event listener', () => {
+ const element = DOMHelper.create('')
+ let clickCount = 0
+
+ const handler1 = () => clickCount++
+ const handler2 = () => clickCount++
+
+ EventHelper.on(element, 'click', handler1)
+ EventHelper.on(element, 'click', handler2)
+
+ EventHelper.trigger(element, 'click')
+ expect(clickCount).toBe(2)
+
+ EventHelper.off(element, 'click', handler1)
+ EventHelper.trigger(element, 'click')
+ expect(clickCount).toBe(3)
+ })
+
+ it('should handle multiple event types', () => {
+ const element = DOMHelper.create('')
+ const events = []
+
+ EventHelper.on(element, 'click', () => events.push('click'))
+ EventHelper.on(element, 'mouseover', () => events.push('mouseover'))
+
+ EventHelper.trigger(element, 'click')
+ EventHelper.trigger(element, 'mouseover')
+
+ expect(events).toEqual(['click', 'mouseover'])
+ })
+ })
+
+ describe('Namespace support', () => {
+ it('should add namespaced events', () => {
+ const element = DOMHelper.create('')
+ let clicked = false
+
+ EventHelper.on(element, 'click.namespace', () => {
+ clicked = true
+ })
+
+ EventHelper.trigger(element, 'click')
+ expect(clicked).toBe(true)
+ })
+
+ it('should remove events by namespace', () => {
+ const element = DOMHelper.create('')
+ let ns1Count = 0
+ let ns2Count = 0
+
+ EventHelper.on(element, 'click.ns1', () => ns1Count++)
+ EventHelper.on(element, 'click.ns2', () => ns2Count++)
+
+ EventHelper.trigger(element, 'click')
+ expect(ns1Count).toBe(1)
+ expect(ns2Count).toBe(1)
+
+ EventHelper.off(element, 'click.ns1')
+ EventHelper.trigger(element, 'click')
+ expect(ns1Count).toBe(1)
+ expect(ns2Count).toBe(2)
+ })
+
+ it('should handle multiple namespaces', () => {
+ const element = DOMHelper.create('')
+ let count = 0
+
+ EventHelper.on(element, 'click.ns1.ns2', () => count++)
+ EventHelper.trigger(element, 'click')
+ expect(count).toBe(1)
+ })
+
+ it('should remove all events with namespace', () => {
+ const element = DOMHelper.create('')
+ let clickCount = 0
+ let mouseoverCount = 0
+
+ EventHelper.on(element, 'click.ns', () => clickCount++)
+ EventHelper.on(element, 'mouseover.ns', () => mouseoverCount++)
+ EventHelper.on(element, 'keydown', () => {})
+
+ EventHelper.offAll(element, 'ns')
+
+ EventHelper.trigger(element, 'click')
+ EventHelper.trigger(element, 'mouseover')
+
+ expect(clickCount).toBe(0)
+ expect(mouseoverCount).toBe(0)
+ })
+ })
+
+ describe('Event delegation', () => {
+ it('should delegate events to child elements', () => {
+ const container = DOMHelper.create('')
+ let clicked = false
+ let targetText = ''
+
+ EventHelper.delegate(container, '.btn', 'click', e => {
+ clicked = true
+ targetText = e.target.textContent
+ })
+
+ const button = container.querySelector('.btn')
+
+ button.click()
+
+ expect(clicked).toBe(true)
+ expect(targetText).toBe('Click')
+ })
+
+ it('should work with dynamically added elements', () => {
+ const container = DOMHelper.create('')
+ const clicks = []
+
+ EventHelper.delegate(container, '.item', 'click', e => {
+ clicks.push(e.target.textContent)
+ })
+
+ // Add button dynamically
+ const button1 = DOMHelper.create('')
+
+ DOMHelper.append(container, button1)
+ button1.click()
+
+ // Add another button dynamically
+ const button2 = DOMHelper.create('')
+
+ DOMHelper.append(container, button2)
+ button2.click()
+
+ expect(clicks).toEqual(['Item 1', 'Item 2'])
+ })
+
+ it('should not trigger on non-matching elements', () => {
+ const container = DOMHelper.create('No match
')
+ let clicked = false
+
+ EventHelper.delegate(container, '.btn', 'click', () => {
+ clicked = true
+ })
+
+ const span = container.querySelector('span')
+
+ span.click()
+
+ expect(clicked).toBe(false)
+ })
+
+ it('should support delegated event namespacing', () => {
+ const container = DOMHelper.create('')
+ let clicked = false
+
+ EventHelper.delegate(container, '.btn', 'click.ns', () => {
+ clicked = true
+ })
+
+ const button = container.querySelector('.btn')
+
+ button.click()
+
+ expect(clicked).toBe(true)
+ })
+
+ it('should undelegate events', () => {
+ const container = DOMHelper.create('')
+ let clickCount = 0
+
+ const handler = () => clickCount++
+
+ EventHelper.delegate(container, '.btn', 'click.ns', handler)
+
+ const button = container.querySelector('.btn')
+
+ button.click()
+ expect(clickCount).toBe(1)
+
+ EventHelper.undelegate(container, '.btn', 'click.ns')
+ button.click()
+ expect(clickCount).toBe(1)
+ })
+ })
+
+ describe('Custom events and data', () => {
+ it('should trigger custom events with data', () => {
+ const element = DOMHelper.create('')
+ let receivedData = null
+
+ EventHelper.on(element, 'custom', e => {
+ receivedData = e.detail
+ })
+
+ const data = { message: 'Hello', value: 42 }
+
+ EventHelper.trigger(element, 'custom', data)
+
+ expect(receivedData).toEqual(data)
+ })
+
+ it('should support event bubbling', () => {
+ const parent = DOMHelper.create('Child
')
+ const child = parent.querySelector('span')
+ const events = []
+
+ EventHelper.on(parent, 'click', () => events.push('parent'))
+ EventHelper.on(child, 'click', () => events.push('child'))
+
+ child.click()
+
+ expect(events).toEqual(['child', 'parent'])
+ })
+
+ it('should support cancelable events', () => {
+ const element = DOMHelper.create('')
+ let defaultPrevented = false
+
+ EventHelper.on(element, 'custom', e => {
+ e.preventDefault()
+ defaultPrevented = e.defaultPrevented
+ })
+
+ EventHelper.trigger(element, 'custom', {}, { cancelable: true })
+
+ expect(defaultPrevented).toBe(true)
+ })
+ })
+
+ describe('Convenience methods', () => {
+ it('should handle one-time events with one()', () => {
+ const element = DOMHelper.create('')
+ let count = 0
+
+ EventHelper.one(element, 'click', () => {
+ count++
+ })
+
+ EventHelper.trigger(element, 'click')
+ EventHelper.trigger(element, 'click')
+
+ expect(count).toBe(1)
+ })
+
+ it('should handle hover events', () => {
+ const element = DOMHelper.create('')
+ let entered = false
+ let left = false
+
+ EventHelper.hover(element, () => {
+ entered = true
+ }, () => {
+ left = true
+ })
+
+ // Simulate mouseenter and mouseleave
+ EventHelper.trigger(element, 'mouseenter')
+ EventHelper.trigger(element, 'mouseleave')
+
+ expect(entered).toBe(true)
+ expect(left).toBe(true)
+ })
+
+ it('should handle hover with only enter handler', () => {
+ const element = DOMHelper.create('')
+ let entered = false
+
+ EventHelper.hover(element, () => {
+ entered = true
+ })
+
+ EventHelper.trigger(element, 'mouseenter')
+
+ expect(entered).toBe(true)
+ })
+
+ it('should handle hover with only leave handler', () => {
+ const element = DOMHelper.create('')
+ let left = false
+
+ EventHelper.hover(element, null, () => {
+ left = true
+ })
+
+ EventHelper.trigger(element, 'mouseleave')
+
+ expect(left).toBe(true)
+ })
+ })
+
+ describe('Event handler management', () => {
+ it('should store event handlers on element', () => {
+ const element = DOMHelper.create('')
+ const handler = () => {}
+
+ EventHelper.on(element, 'click', handler)
+
+ const handlers = EventHelper.getHandlers(element)
+
+ expect(handlers.click).toBeDefined()
+ expect(handlers.click.length).toBe(1)
+ })
+
+ it('should support multiple handlers for same event', () => {
+ const element = DOMHelper.create('')
+ let count = 0
+
+ EventHelper.on(element, 'click', () => count++)
+ EventHelper.on(element, 'click', () => count++)
+ EventHelper.on(element, 'click', () => count++)
+
+ EventHelper.trigger(element, 'click')
+
+ expect(count).toBe(3)
+ })
+
+ it('should remove all events with offAll()', () => {
+ const element = DOMHelper.create('')
+ let clickCount = 0
+ let mouseoverCount = 0
+
+ EventHelper.on(element, 'click', () => clickCount++)
+ EventHelper.on(element, 'mouseover', () => mouseoverCount++)
+
+ EventHelper.offAll(element)
+
+ EventHelper.trigger(element, 'click')
+ EventHelper.trigger(element, 'mouseover')
+
+ expect(clickCount).toBe(0)
+ expect(mouseoverCount).toBe(0)
+ })
+
+ it('should check if element has listeners', () => {
+ const element = DOMHelper.create('')
+
+ expect(EventHelper.hasListeners(element)).toBe(false)
+
+ EventHelper.on(element, 'click', () => {})
+
+ expect(EventHelper.hasListeners(element)).toBe(true)
+ expect(EventHelper.hasListeners(element, 'click')).toBe(true)
+ expect(EventHelper.hasListeners(element, 'mouseover')).toBe(false)
+
+ EventHelper.on(element, 'click.ns', () => {})
+ expect(EventHelper.hasListeners(element, 'click', 'ns')).toBe(true)
+ expect(EventHelper.hasListeners(element, 'click', 'other')).toBe(false)
+ })
+
+ it('should clean up event storage when empty', () => {
+ const element = DOMHelper.create('')
+ const handler = () => {}
+
+ EventHelper.on(element, 'click', handler)
+ expect(element._eventHandlers).toBeDefined()
+
+ EventHelper.off(element, 'click', handler)
+ expect(element._eventHandlers).toBeUndefined()
+ })
+ })
+
+ describe('Edge cases', () => {
+ it('should handle null/undefined elements gracefully', () => {
+ expect(() => {
+ EventHelper.on(null, 'click', () => {})
+ }).not.toThrow()
+
+ expect(() => {
+ EventHelper.off(undefined, 'click', () => {})
+ }).not.toThrow()
+
+ expect(() => {
+ EventHelper.trigger(null, 'click')
+ }).not.toThrow()
+ })
+
+ it('should handle non-existent selectors gracefully', () => {
+ expect(() => {
+ EventHelper.on('#nonexistent', 'click', () => {})
+ }).not.toThrow()
+
+ expect(() => {
+ EventHelper.off('#nonexistent', 'click', () => {})
+ }).not.toThrow()
+ })
+
+ it('should handle removing non-existent handlers', () => {
+ const element = DOMHelper.create('')
+ const handler = () => {}
+
+ expect(() => {
+ EventHelper.off(element, 'click', handler)
+ }).not.toThrow()
+
+ expect(() => {
+ EventHelper.off(element, 'nonexistent', handler)
+ }).not.toThrow()
+ })
+
+ it('should handle event without handler', () => {
+ const element = DOMHelper.create('')
+
+ expect(() => {
+ EventHelper.on(element, 'click', null)
+ }).toThrow()
+
+ expect(() => {
+ EventHelper.on(element, 'click', undefined)
+ }).toThrow()
+ })
+
+ it('should maintain handler execution order', () => {
+ const element = DOMHelper.create('')
+ const order = []
+
+ EventHelper.on(element, 'click', () => {
+ order.push('first')
+ })
+
+ EventHelper.on(element, 'click', () => {
+ order.push('second')
+ })
+
+ EventHelper.on(element, 'click', () => {
+ order.push('third')
+ })
+
+ element.click()
+
+ expect(order).toEqual(['first', 'second', 'third'])
+ })
+
+ it('should maintain correct this context', () => {
+ const element = DOMHelper.create('')
+ let context = null
+
+ EventHelper.on(element, 'click', function () {
+ context = this
+ })
+
+ element.click()
+
+ expect(context).toBe(element)
+ })
+ })
+})