dom-event-filter
v1.0.0
Published
Split your page into contexts and route DOM events (hotkeys, clicks, forms) by mask-objects from config
Maintainers
Readme
DOM Event Filter
DOM Event Filter allows you to split your page into contextual areas and catch events (like hotkeys) by mask-objects from config. Define contexts with HTML attributes, configure event patterns, and get contextual custom events that bubble through your DOM hierarchy.
🚀 Features
- Context-aware Event Handling: Define nested page areas and catch events only where they matter
- Universal Event Support: Handle any DOM events - keyboard, mouse, touch, drag, focus, custom events
- Hotkey Management: Easy keyboard shortcut configuration with modifier key support
- Event Sequences: Support for complex key combinations and multi-step interactions
- Custom Event Generation: Automatic emission of contextual custom events
- Flexible Configuration: Object-based or array-based event configuration
- Performance Optimized: Smart event delegation and sequence timeout management
- Zero Dependencies: Lightweight and self-contained
🔑 Core Concept
The library works by establishing a direct relationship between your configuration structure and your HTML context hierarchy:
- Configuration Structure: The nested structure of your config object defines which contexts an event belongs to
- HTML Context Hierarchy: Elements with
data-contextattributes create contextual areas in your DOM - Event Matching: Events are matched when they occur within the corresponding context hierarchy
- Custom Event Generation: The library generates custom events with names that reflect the context path
Example:
const config = {
editor: {
save: { code: 'KeyS', ctrlKey: true }, // Will trigger by hotkey in editor context
toolbar: {
save: { type: 'click' } // Will trigger in editor > toolbar context
}
}
}<div data-context="editor">
<div data-context="toolbar">
<button>Save</button> <!-- Click here triggers 'toolbar.save' event -->
</div>
<textarea>Your text here...</textarea> <!-- Ctrl+S here triggers 'editor.save' event -->
</div>The library subscribes to specified event types, finds the matching context hierarchy for each event, and re-fires new contextual events with meaningful names.
📦 Installation
npm install dom-event-filter
# or
yarn add dom-event-filter🔥 Quick Start
HTML Structure
<div>
<div data-context="editor">
<textarea>Your text here...</textarea>
<button>Save</button>
</div>
<div data-context="sidebar">
<button id="info-btn">Info</button>
</div>
</div>JavaScript Configuration
import { DomEventFilter } from 'dom-event-filter'
const filter = new DomEventFilter({
// Global hotkeys
save: { code: 'KeyS', ctrlKey: true },
open: { code: 'KeyO', altKey: true },
// Context-specific events
editor: {
save: { code: 'KeyS', ctrlKey: true },
autocomplete: [
{ code: 'Tab' }, // Tab sequence
{ code: 'Tab' }
],
// Form events in editor context
validateInput: { type: 'change', target: 'input[type="text"]' },
focusTextarea: { type: 'focus', target: 'textarea' }
},
// Click events with context
info: {
target: '#info-btn',
type: 'click',
ctrlKey: true
},
// Touch events for mobile
swipeLeft: { type: 'touchend', target: '.swipe-area' },
// Event sequences (Konami code)
konami: [
{ key: 'ArrowUp' },
{ key: 'ArrowUp' },
{ key: 'ArrowDown' },
{ key: 'ArrowDown' },
{ key: 'ArrowLeft' },
{ key: 'ArrowRight' },
{ key: 'ArrowLeft' },
{ key: 'ArrowRight' }
]
}, {
eventType: 'keydown click change focus touchend' // Listen to multiple event types
})
// Listen for contextual events
document.addEventListener('editor.save', e => {
console.log('Save in editor context', e.detail)
})
// Wildcard listeners (requires '*.{{name}}' in resultEventType)
document.addEventListener('*.info', e => {
console.log('Info event from any context', e.detail)
})
// Catch all filtered events
document.addEventListener('DOMFilterEvent', e => {
console.log('Any filtered event', e.detail)
})📖 Configuration
Event Masks
Event masks are standard DOM event properties used for matching.
{
code: 'Enter', // KeyboardEvent.code (layout-independent, recommended)
key: 'Enter', // KeyboardEvent.key (layout-dependent alternative)
ctrlKey: true, // Modifier keys
altKey: false,
shiftKey: true,
metaKey: false,
type: 'keydown', // Event type
target: '#my-button', // CSS selector for target element (optional)
button: 0, // MouseEvent.button
// Any other DOM event property works automatically
}Note: Target selectors are optional - events are matched by type within their context by default.
Context Hierarchy
Contexts are defined using HTML attributes (configurable via contextAttribute setting, defaults to data-context) and support nesting. Events without any context are treated as global (*):
<!-- Global context (no context attribute) -->
<div>
<!-- Events here are global (*) -->
<button>Global Action</button>
</div>
<!-- Named contexts with nesting -->
<div data-context="app">
<!-- Nested editor context -->
<div data-context="editor">
<!-- Events here match both 'app' and 'editor' contexts -->
<input type="text">
</div>
<!-- Nested sidebar context -->
<div data-context="sidebar">
<!-- Events here match both 'app' and 'sidebar' contexts -->
<button>Menu</button>
</div>
</div>Configuration Structure
The configuration object structure directly mirrors your HTML context hierarchy. Each level of nesting in your config corresponds to a level of nesting in your DOM contexts. This relationship is the core of how the library works - it matches events based on where they occur in the context tree.
[contextName]
[nested contextName]
...
[nested contextName]
eventName: { ...eventMask }When you define a nested configuration like editor.toolbar.save, the library will only trigger this event if it happens within an element that has both data-context="editor" and a nested data-context="toolbar".
Object-based Configuration:
const config = {
// Global events (work anywhere on the page)
globalSave: { code: 'KeyS', ctrlKey: true },
// Editor context events
editor: {
save: { code: 'KeyS', ctrlKey: true }, // Hotkey in editor area
focus: { type: 'focus', target: 'input' }, // Focus any input in editor
// Nested toolbar context within editor
toolbar: {
boldBtn: { type: 'click', target: '.bold' }, // Click bold button
italicBtn: { type: 'click', target: '.italic' } // Click italic button
}
}
}Corresponding HTML:
<div data-context="editor">
<!-- Ctrl+S here triggers 'editor.save' -->
<input type="text" placeholder="Type here...">
<div data-context="toolbar">
<!-- Click here triggers 'toolbar.boldBtn' -->
<button class="bold">Bold</button>
<button class="italic">Italic</button>
</div>
</div>The key insight is that configuration structure defines context requirements. The deeper you nest in config, the more specific the DOM context must be for events to match.
Array-based Configuration:
const config = [
{
name: 'save',
context: ['editor'],
mask: { code: 'KeyS', ctrlKey: true }
},
{
name: 'bold',
context: ['dialog', 'toolbar'], // Requires nested context
mask: { code: 'KeyB', ctrlKey: true }
}
]⚙️ Advanced Features
Event Sequences
Define multi-step key combinations with automatic timeout:
const filter = new DomEventFilter({
// Classic Konami code
konami: [
{ code: 'ArrowUp' },
{ code: 'ArrowUp' },
{ code: 'ArrowDown' },
{ code: 'ArrowDown' },
{ code: 'ArrowLeft' },
{ code: 'ArrowRight' },
{ code: 'ArrowLeft' },
{ code: 'ArrowRight' },
{ code: 'KeyB' },
{ code: 'KeyA' }
],
// Double-tab for autocomplete
doubleTab: [
{ code: 'Tab' },
{ code: 'Tab' }
]
}, {
sequenceTimeLimit: 1000 // 1 second timeout between keys
})Custom Settings
const filter = new DomEventFilter(config, {
contextAttribute: 'data-context', // HTML attribute for contexts
eventType: 'keydown click', // Space-separated event types to listen for
rootElement: document.body, // Root element for event delegation
sequenceTimeLimit: 720, // Maximum interval between sequence keys (ms)
resultEventType: [ // Custom event name templates (all are fired)
'{{eventConfig.context[0]}}.{{name}}',
'DOMFilterEvent'
]
})contextAttribute
Type: string | Default: 'data-context'
HTML attribute name used to identify contexts in your DOM. You can use any custom attribute name:
// Use custom attribute
new DomEventFilter(config, { contextAttribute: 'data-area' })<!-- Now use data-area instead of data-context -->
<div data-area="editor">
<div data-area="toolbar">...</div>
</div>eventType
Type: string | Default: 'keydown'
Space-separated list of DOM event types to listen for. This is a performance optimization - only specified event types are processed by the filter. Common combinations:
// Keyboard only
eventType: 'keydown'
// Keyboard and mouse
eventType: 'keydown click'
// Multiple event types
eventType: 'keydown click change focus blur touchend'
// All interaction events
eventType: 'keydown click change focus blur touchstart touchend mousedown mouseup'rootElement
Type: Element | Default: document
Root DOM element for event delegation. All event listeners are attached to this element. Useful for limiting scope to specific parts of your application:
// Listen only within a specific container
const container = document.querySelector('#app-container')
new DomEventFilter(config, { rootElement: container })sequenceTimeLimit
Type: number | Default: 720
Maximum time interval in milliseconds between keys in a sequence. If the timeout expires, the sequence resets. Set to 0 to disable timeout:
// 1 second timeout
sequenceTimeLimit: 1000
// No timeout (sequences never expire)
sequenceTimeLimit: 0
// Very fast sequences (300ms)
sequenceTimeLimit: 300resultEventType
Type: string | Array<string> | Default: ['{{eventConfig.context[0]}}.{{name}}', 'DOMFilterEvent']
Template(s) for generated custom event names. Important: All templates in the array are fired simultaneously for each matched event. Use template variables to create dynamic event names:
// Multiple event formats (all fired for each match)
resultEventType: [
'{{fullContext}}.{{name}}', // 'editor.toolbar.save'
'{{context}}.{{name}}', // 'toolbar.save'
'*.{{name}}', // '*.save' (wildcard)
'{{name}}', // 'save' (name only)
'*', // '*' (catch-all wildcard)
'DOMFilterEvent' // Generic catch-all
]Template Variables:
{{name}}- Event name from configuration key{{context}}- Nearest context name from DOM (closestdata-contextto the event target){{fullContext}}- Complete context path from DOM, dot-joined (e.g.,'editor.toolbar'){{eventConfig.context[0]}}- First context from config definition- Any property from the
eventConfigordetailobject via dot/bracket notation
Note: fullContext is built from the DOM hierarchy (data-context attributes in the event's composed path), not from the config structure. This means it reflects where the event actually happened, not where it was defined.
Custom Event Generation
The library generates custom events based on the resultEventType configuration. All templates in the array are fired simultaneously for each matched event, allowing you to listen for the same event in multiple formats:
const filter = new DomEventFilter(config, {
resultEventType: [
'{{eventConfig.context[0]}}.{{name}}', // 'editor.save'
'{{fullContext}}.{{name}}', // 'editor.toolbar.save'
'*.{{name}}', // '*.save' (wildcard)
'{{name}}', // 'save' (name only)
'DOMFilterEvent' // Generic catch-all
]
})
// All these listeners will trigger for the same editor save event:
document.addEventListener('editor.save', handler1) // First template
document.addEventListener('editor.toolbar.save', handler2) // Second template
document.addEventListener('*.save', handler3) // Wildcard template
document.addEventListener('save', handler4) // Name-only template
document.addEventListener('DOMFilterEvent', handler5) // Generic templateEvent Details
All generated events include detailed information in the event.detail object:
document.addEventListener('editor.save', e => {
const {
name, // 'save' - Event name from config
context, // 'editor' - Immediate context
fullContext, // 'editor.toolbar' - Full context path
composedContexts, // ['toolbar', 'editor'] - Context hierarchy array
originalEvent, // Original DOM event that triggered this
eventConfig // Matched configuration object
} = e.detail
})
// Listen for events from any context
document.addEventListener('*.save', e => {
console.log(`Save triggered in: ${e.detail.fullContext}`)
})
// Generic event listener
document.addEventListener('DOMFilterEvent', e => {
console.log(`Event: ${e.detail.name} in ${e.detail.fullContext}`)
})🎯 Real-world Examples
Code Editor with Context Hierarchy
Configuration:
const editorFilter = new DomEventFilter({
editor: {
// Editor-wide actions
save: { code: 'KeyS', ctrlKey: true },
find: { code: 'KeyF', ctrlKey: true },
// Nested contexts within editor
toolbar: {
bold: { code: 'KeyB', ctrlKey: true },
italic: { code: 'KeyI', ctrlKey: true },
underline: { code: 'KeyU', ctrlKey: true }
},
sidebar: {
toggle: { code: 'KeyB', ctrlKey: true, shiftKey: true },
// Further nesting: file explorer within sidebar
files: {
newFile: { code: 'KeyN', ctrlKey: true },
delete: { code: 'Delete' },
rename: { code: 'F2' }
}
}
}
}, {
resultEventType: ['{{fullContext}}.{{name}}', '*.{{name}}', 'DOMFilterEvent']
})Corresponding HTML:
<div data-context="editor">
<div data-context="toolbar">
<!-- Ctrl+B here triggers: 'editor.toolbar.bold' -->
<button>Bold</button>
</div>
<div data-context="sidebar">
<div data-context="files">
<!-- F2 here triggers: 'editor.sidebar.files.rename' -->
<!-- Delete here triggers: 'editor.sidebar.files.delete' -->
<ul class="file-list">...</ul>
</div>
</div>
<div class="main-content">
<!-- Ctrl+S here triggers: 'editor.save' -->
<!-- Ctrl+F here triggers: 'editor.find' -->
<textarea></textarea>
</div>
</div>Event Handling:
// Specific context events
document.addEventListener('editor.toolbar.bold', e => {
console.log('Bold action in toolbar')
})
// Wildcard listeners (requires '*.{{name}}' in resultEventType)
document.addEventListener('*.save', e => {
console.log(`Save in context: ${e.detail.fullContext}`)
})
// All filtered events
document.addEventListener('DOMFilterEvent', e => {
console.log(`${e.detail.name} triggered in ${e.detail.fullContext}`)
})Modal Dialog Management
const dialogFilter = new DomEventFilter({
// Global escape to close any dialog
closeDialog: { code: 'Escape' },
// Context-specific dialog actions
confirmDialog: {
confirm: { code: 'Enter' },
cancel: { code: 'Escape' }
},
// Form dialog
formDialog: {
submit: { code: 'Enter', ctrlKey: true },
reset: { code: 'KeyR', ctrlKey: true }
}
})
// Handle modal events
document.addEventListener('*.confirm', e => {
e.detail.originalEvent.target.closest('.dialog')?.querySelector('.confirm-btn')?.click()
})Gaming Controls
const gameFilter = new DomEventFilter({
// Movement in game area
game: {
moveUp: { code: 'ArrowUp' },
moveDown: { code: 'ArrowDown' },
moveLeft: { code: 'ArrowLeft' },
moveRight: { code: 'ArrowRight' },
jump: { code: 'Space' },
shoot: { code: 'KeyX' }
},
// Menu controls
menu: {
select: { code: 'Enter' },
back: { code: 'Escape' },
up: { code: 'ArrowUp' },
down: { code: 'ArrowDown' }
},
// Cheat codes
godMode: [
{ code: 'KeyI' }, { code: 'KeyD' }, { code: 'KeyD' }, { code: 'KeyQ' }, { code: 'KeyD' }
],
keyFullAmmo: [
{ code: 'KeyI' }, { code: 'KeyD' }, { code: 'KeyK' }, { code: 'KeyF' }, { code: 'KeyA' }
]
})🔧 API Reference
Constructor
new DomEventFilter(config, settings)- config: Event configuration (Object or Array) - see Configuration section above
- settings: Optional settings object - see Custom Settings section above
Methods
filter.addListeners() // Reinitialize event listeners with current configurationReprocesses the current configuration and rebinds all event listeners. Useful when you need to refresh the filter after changing DOM structure or settings.
Public Properties
filter.eventTypes // Object containing categorized event types for sequence managementThe eventTypes object categorizes DOM events for internal sequence clearing logic. It has this structure:
{
keyboard: ['keydown', 'keypress', 'keyup'],
mouse: ['click', 'mousedown', 'mouseup', 'mouseover', 'mouseout', 'mouseenter', 'mouseleave'],
mouse2: ['auxclick', 'contextmenu', 'dblclick', 'wheel'],
pointer: ['pointerdown', 'pointerup', 'pointercancel', 'pointerover', 'pointerout', 'pointerenter', 'pointerleave'],
touch: ['touchstart', 'touchend', 'touchcancel', 'touchmove'],
drag: ['drag', 'dragstart', 'dragend', 'dragenter', 'dragleave', 'dragover', 'drop'],
nav: ['focus', 'blur', 'focusin', 'focusout'],
forms: ['change', 'input', 'submit', 'reset', 'select'],
clipboard: ['copy', 'cut', 'paste'],
composition: ['compositionstart', 'compositionupdate', 'compositionend']
}Made with ❤️ by Petro Borshchahivskyi
