@spark-engine/event

1.13.1 • Public • Published

Spark Engine Event

This is a unified events framework handling standard events, keyboard events, and tap events (for touch screen devices). It features:

  • Easy DOM event management, powered by bean.
  • Custom tap event for properly discerning touch events.
  • Simple keyboard events, powered by keymaster.
  • Event managers for attaching listeners to dom ready, page change, window resize and scroll.
  • Callback manager for enabling, disabling and toggling the ability for callbacks to trigger.
  • Custom event mangaers for registering many callbacks with start, stop, and toggle capability.
  • Throttle and Debounce functions tied to request animation frame for better paint performance.

If you want to trigger events on DOM ready, when a page changes, or during a scroll or resize event, use these event managers to register your functions.

DOM Listeners - Use these functions to attach DOM event listeners.

Keyboard Listeners - Use these functions to attach keyboard event listeners.

Media Query Listeners - trigger callbacks when media queries become true/untrue.

Timing functions - Control when functions are executed and how frequently.

Helpers - These are tiny event utilities which make things a bit nicer.

Event managers

These event managers make it easier to handle event listeners for DOM ready, page transitions, and window scroll. This triggeres event callbacks efficiently and adds some other niceities.

ready( callback )

event.ready() lets you add callbacks to be fired whenever the browser's DOMContentLoaded event is fired. For a site which uses ajax to fetch subsequent pages, it's important to note that this is only fired once with each full page load.

event.ready( function(){ /* do something */ } )

This adds callbacks to an array and fires them each from a single event listener. If an event callback is added after the page has already loaded, it will fire immediately. This will also fire events registered with the change function below, unless you are using Turbolinks which triggers page:change on its own.

change( callback )

event.change() lets you add callbacks to be fired whenever a page:chage event is fired. This is the sort of even that is used in pjax or Turbolinks to signal that the DOM has loaded new content, likely via ajax which means you may need to remove listeners, bootstrap widgets, or whatever you do when content changes.

event.change( function(){ /* do something */ } )

Just like ready, this adds your callback to an array, fired from a single listener. If a function is added after page:change has been fired, it will fire immediately.

beforeVisit( callback )

event.beforeVisit() lets you add callbacks to be fired whenever a page:-before-visit or turbolinks:before-visit event is fired. This enables you to do work on a page between when an interaction triggers a new page load and when that page load request begins. If you are using Turbolinks you'll be able to interact with the event object to inspect things like event.data.url and know which page is being loaded.

beforeUnload( callback )

event.beforeUnload() will trigger whenever you are about to leave the current page, calling event.preventDefault() will throw up the "leave this page?" modal dialog on supporting browsers.

beforeChange( callback )

event.beforeChange() triggers callbacks when the page is about to change, this is a generic rollup of beforeunload, page:request-start and turbolinks:request-start. In Turbolinks world, request-start is triggered for all page transition events which Turbolinks handles, including back navigation, which isn't covered by before-visit. Using beforeChange you can be sure your event triggers before a page is left.

scroll( callback )

event.scroll() lets you add callbacks to be fired whenever the window's scroll event is fired (throttled by requestAnimationFrame).

event.scroll( function(){ /* scrolling is happening */ } )
event.scroll.start( function(){ /* scrolling has started */ } )
event.scroll.end( function(){ /* scrolling has ended */ } )

This fires all callbacks at with requestAnimationFrame() ensuring that events are fired during the browser's natural repaint cycle (every 16ms at 60fps). This helps prevent scattered repaints and jittery graphics performance.

Pause & Resume Callbacks

You may want to deactivate a callback and later reactivate it. To do so, save a reference to the callback when registering it. Then call stop() or start() on the callback reference.

var scrollWatch = event.scroll( function(){ /* scrolling is happening */ } )
scrollWatch.stop()   // prevent callback from executing
scrollWatch.start()  // allow callback to execute again

resize( function )

event.resize() lets you add callbacks to be fired whenever the window's resize event is fired (throttled by requestAnimationFrame).

event.resize( function(){ /* window is being resized */ } )
event.resize.start( function(){ /* resizing just started */ } )
event.resize.end( function(){ /* resizing just ended */ } )

This fires all callbacks at with requestAnimationFrame() ensuring that events are fired during the browser's natural repaint cycle (every 16ms at 60fps). This helps prevent scattered repaints and jittery graphics performance.

Pause & Resume Callbacks

Just like with the scroll event manager, you may want to deactivate a callback and later reactivate it. To do so, save a reference to the callback when registering it. Then call stop() or start() on the callback reference.

var resizeWatch = event.scroll( function(){ /* resizing is happening */ } )
resizeWatch.stop()   // prevent callback from executing
resizeWatch.start()  // allow callback to execute again

Custom Event Managers

You can use a custom event manager to fire start, end and requestAnimationFrame throttled events for any type of event. This can be simpler than managing throttling and debouncing manually.

event.eventManager.new('event', [{ target: Selector or Element (default: window), delay: 150, throttle: true }])

For example, if you want to trigger callbacks when scrolling a panel

var panelScroll = event.eventManager.new('scroll', { target: '#some-panel' })

panelScroll.start(function(event){ /* panel scrolling has started */ })
panelScroll(function(event){       /* panel scrolling is happening */ })       
panelScroll.end(function(event){   /* panel scrolling has ended */ })   

Event Listeners

Much of the documentation below has been copied from dependent libraries and modified where necessary. To dig deeper be sure to reference the documentation for bean and keymaster.

on( element, eventType[, selector], handler[, args ] )

bean.on() lets you attach event listeners to both elements and objects.

Arguments

  • element / object (DOM Element or Object) - an HTML DOM element or any JavaScript Object
  • event type(s) (String) - an event (or multiple events, space separated) to listen to
  • selector (optional String) - a CSS DOM Element selector string to bind the listener to child elements matching the selector
  • handler (Function) - the callback function
  • args (optional) - additional arguments to pas to the callback function when triggered

Optionally, event types and handlers can be passed in an object of the form { 'eventType': handler } as the second argument.

Examples

Var event = require( '@spark-engine/event' )

// simple
event.on( element, 'click', handler );

// optional arguments passed to handler
event.on( element, 'click', function( event, option1, option2 ) {
  console.log( option1, option2 );
}, optional, args );

// multiple events
event.on( element, 'keydown keyup', handler );

// multiple handlers
event.on( element, {
  click:        function ( e ) {},
  mouseover:    function ( e ) {},
  'focus blur': function ( e ) {}
});

Delegation

// event delegated events
event.on( element, 'click', '.content p', handler );

// Alternatively, you can pass an array of elements.
// This cuts down on selector engine work, and is a more performant means of
// delegation if you know your DOM won't be changing:
event.on( element, [el, el2, el3], 'click', handler );
event.on( element, document.querySelectorAll('.myClass'), 'click', handler );

Differences to Bean events

Even though with Bean (the core of the events system) using an object to register multiple events at once means you cannot have delegation or optional arguments, @spark-engine/event processes arguments to allow these features without issue. Here's what that looks like.

// Multiple events + delegation + optional arguments
event.on( element, {
  click:        function ( e ) {},
  mouseover:    function ( e ) {},
  'focus blur': function ( e ) {}
}, '.content p', 'optional', 'argument');

Another difference is that you can create a new 'tap' event which looks like this.

event.on( element, 'tap', function() {} )

This attaches a touchstart event and watches for movement or additional fingers, which would indicate a canceled or uninteneded tap. If a user does not move their finger or add an additional finger, their intent is determined as a tap, and the callback function is executed.

Note: Remember to stop propagation on a tap event if you do not want it to also fire a click event, since mobile devices typically fire a click event a part of the touch event lifecycle.

one( element, eventType[, selector], handler[, args ] )

event.one() is an alias for event.on() except that the handler will only be executed once and then the listener will be removed. If you use one for multiple events, the listener will be removed as each event is fired.

off( element[, eventType[, handler ]] )

event.off() is how you get rid of handlers once you no longer want them active. It's also a good idea to call off on elements before removing them from your DOM; this helps prevent memory leaks.

Arguments

  • element / object (DOM Element or Object) - an HTML DOM element or any JavaScript Object
  • event type(s) (optional String) - an event (or multiple events, space separated) to remove
  • handler (optional Function) - the specific callback function to remove

Optionally, event types and handlers can be passed in an object of the form { 'eventType': handler } as the second argument, just like on().

Examples

// remove a single event handlers
event.off( element, 'click', handler );

// remove all click handlers
event.off( element, 'click' );

// remove handler for all events
event.off( element, handler );

// remove multiple events
event.off( element, 'mousedown mouseup' );

// remove all events
event.off( element );

// remove handlers for events using object literal
event.off( element, { click: clickHandler, keyup: keyupHandler } )

clone( destElement, srcElement[, eventType ] )

event.clone() is a method for cloning events from one DOM element or object to another.

Examples

// clone all events at once by doing this:
event.clone( toElement, fromElement );

// clone events of a specific type
event.clone( toElement, fromElement, 'click' );

fire( element, eventType[, args ] )

event.fire() gives you the ability to trigger events.

Examples

// fire a single event on an element
event.fire( element, 'click' );

// fire multiple types
event.fire( element, 'mousedown mouseup' );

The Event object

Bean implements a variant of the standard DOM Event object, supplied as the argument to your DOM event handler functions. Bean wraps and fixes the native Event object where required, providing a consistent interface across browsers.

// prevent default behavior and propagation (even works on old IE)
event.on( el, 'click', function ( event ) {
  event.preventDefault();
  event.stopPropagation();
});

// a simple shortcut version of the above code
event.on( el, 'click', function ( event ) {
  event.stop();
});

// prevent all subsequent handlers from being triggered for this particular event
event.on( el, 'click', function ( event ) {
  event.stopImmediatePropagation();
});

Note: Your mileage with the Event methods (preventDefault etc.) may vary with delegated events as the events are not intercepted at the element in question.

object support

Everything you can do with an element, you can also do with an object. this is particularly useful for working with classes or plugins.

var inst = new klass();
event.on( inst, 'complete', handler );

//later on...
event.fire( inst, 'complete' );

afterAnimation( element, callback, [startTimeout] )

event.afterAnimation(), a better animation event queue.

This will trigger a callback:

  • After animation completes, or
  • After a timeout if animation is delayed or never starts, or
  • Immediately if animation is not supported by the browser.

This works like event.one( el, 'animationend', callback) but is safe to use on elements which may not have animations, or in browsers which may not support them.

For example:

var el = document.querySelector( '#loose-canon' )
el.classList.add( '.play-it-straight-or-turn-in-your-badge' )

event.afterAnimation( el, function() {
  // save the day
}, 50)

In the example above the optional startTimeout was set. This will trigger the callback if animation has not begun after 50 miliseconds. startTimeout can also be set to true which will wait for 32ms about two animation frames before aborting and triggering the callback.

Keybaord events

keyOn( 'key', handler )

event.keyOn() gives you the ability to trigger events from keypresses.

Examples

// simple
event.keyOn( 'a', function(){ alert( 'you pressed a!' ) } );

// You can also chain modifier keys and stop events like this.
event.keyOn( 'ctrl+r', function() { alert( 'stopped reload!' ); return false } );

// multiple shortcuts that do the same thing
event.keyOn( '⌘+r, ctrl+r', function() { } );

The handler method is called with two arguments set, the keydown event fired, and an object containing, among others, the following two properties:

shortcut: a string that contains the shortcut used, e.g. ctrl+r scope: a string describing the scope (or all)

event.keyOn( '⌘+r, ctrl+r', function( event, handler ){
  console.log( handler.shortcut, handler.scope );
});

// "ctrl+r", "all"

Special Key support

Supported modifier keys: , shift, option, , alt, ctrl, control, command, and .

The following special keys can be used for shortcuts: backspace, tab, clear, enter, return, esc, escape, space, up, down, left, right, home, end, pageup, pagedown, del, delete and f1 through f19.

keyOne( 'key', handler )

event.keyOne() is an alias for key() but it fires once and removes its listener.

This will execute the callback and stop listening for the escape key.

// simple
event.keyOne( 'esc', function() { alert( 'Escaping!' ) } );

keyOff( 'key', handler )

event.keyOff() removes event listeners for keys.

// unbind 'a' handler
key.unbind( 'a' );

// unbind a key only for a single scope
// when no scope is specified it defaults to the current scope (key.getScope())
key.unbind( 'o, enter', 'issues' );
key.unbind( 'o, enter', 'files' );

key

event.key gives you access to the key object for querying key states and managing scope.

Examples

You can query the key object for the state of any key at any point (even in code other than the key shortcut handlers)

Querying Modifier Keys

For example, key.shift is true if the shift key is currently pressed. This allows easy implementation of things like shift+click handlers.

if( event.key.shift ) alert( 'shift is pressed, OMGZ!' );

Other key Queries

key.isPressed( 77 ) is true if the M key is currently pressed.

if( event.key.isPressed("M") ) alert( 'M key is pressed, can ya believe it!?' );
if( event.key.isPressed(77) )  alert( 'M key is pressed, can ya believe it!?' );

You can also get these as an array using...

event.key.getPressedKeyCodes() // returns an array of key codes currently pressed

Key Scopes

If you want to reuse the same shortcut for separate areas in your single page app, Keymaster supports switching between scopes. Use the key.setScope method to set scope.

// define shortcuts with a scope
event.key( 'o, enter', 'issues', function() { /* do something */ } );
event.key( 'o, enter', 'files', function() { /* do something else */ } );

// set the scope (only 'all' and 'issues' shortcuts will be honored)
event.key.setScope( 'issues' ); // default scope is 'all'

Media Query Listeners

media.width( sizes, [callback] )

event.media.width() Check a width media query and register a callback to be triggered when that media query becomes true/untrue.

function handleQuery( query ) {
  if ( query.matches ) { /* query is true */ }
  else                 { /* query is true */ }
}

// Triggler handler when viewport width is between 400px and 800px
queryList = event.media.width( { min: 400, max: 800 }, handleQuery )

media.minWidth( size, [callback] )

event.media.minWidth() Check a min-width media query and register a callback to be triggered when that media query becomes true/untrue.

// Triggler handler when viewport width is greater than 400px
queryList = event.media.minWidth( 400, handleQuery )

media.maxWidth( size, [callback] )

event.media.maxWidth() Check a min-width media query and register a callback to be triggered when that media query becomes true/untrue.

// Triggler handler when viewport width is less than 800px
queryList = event.media.maxWidth( 800, handleQuery )

media.height( sizes, [callback] )

event.media.height() Check a height media query and register a callback to be triggered when that media query becomes true/untrue.

// Triggler handler when viewport height is between than 400px and 800px
queryList = event.media.height( { min: 400, max: 800 }, handleQuery )

media.minHeight( size, [callback] )

event.media.minHeight() Check a min-height media query and register a callback to be triggered when that media query becomes true/untrue.

// Triggler handler when viewport height is greater than 400px
queryList = event.media.minHeight( 400, handleQuery )

media.maxHeight( size, [callback] )

event.media.maxHeight() Check a max-height media query and register a callback to be triggered when that media query becomes true/untrue.

// Triggler handler when viewport height is less than 800px
queryList = event.media.maxHeight( 800, handleQuery )

Timing functions

delay( function, [wait, arguments] )

event.dealy() lets you call functions after a period of time. Execution is queued with requestAnimationFrame() which ensures that callbacks are triggered optimally to prevent unnecessary repainting. If your callback doesn't change the DOM and you need to execute a function more precicely, use setTimeout() instead.

Here we'll call scat() during the browser's repaint cycle immediately following 500ms.

function scat() { console.log( 'skippity bop!' ) }
event.delay( scat, 500 )

We can also pass arugments to the delayed function, and even stop it from being executed.

var handle = event.delay( scat, 2500, some, args )

// stop the delayed call
handle.stop()

repeat( function, [wait, limit, arguments] )

event.repeat() lets you call functions on a regular interval with requestAnimationFrame(), ensuring that callbacks are triggered optimally to prevent unnecessary repainting. If your callback doesn't change the DOM and you need to execute a function more precicely, use setInterval() instead. This adds a helpful limit option which stops repeating after a set number of times.

Here we'll call scat() during the repaint cycle closest to 200ms.

function scat() { console.log( 'skippity bop!' ) }
event.repeat( scat, 200 )

This will call scat() 10 times, during the repaint cycle closest to 200ms.

var handle = event.repeat( scat, 200, 10 )

// Or you can manually stop the repeating
handle.stop()

You can even execute a function after repeating stops.

var handle = event.repeat( scat, 200, 10 )

// Assign a function to trigger when the repeating stops.
handle.complete = function() { console.log ( 'take a bow' ) }

This will trigger a callback with each repaint cycle.

// These are all equivilent
event.repeat( scat )
event.repeat( scat, 0 )
event.repeat( scat, 0, 0 )

You can also pass arguments to the callback function like this.

event.repeat( scat, 200, 10, some, args )

throttle( function, [wait, options] )

event.throttle() prevents functions from being called except on the repaint cycle after every wait miliseconds. This uses requestAnimationFrame() to prevent callbacks from triggering unnecessary repaints. If your callbacks will not affect the DOM and you need more assurance of their execution time, you'll want to use a different throttle function.

var resizer = function() {
  // modify the DOM
}
// Create a throttled version of the resize
var throttledResize = throttle( resize, 200 )

// Only executes on the repait cycle after every 200ms 
event.on( window, 'resize', throttledResize )

To allow a callback to execute with every repaint cycle don't provide a wait time.

var throttledResize = throttle( resize )

To stop a throttled callback from being executed:

// Remove it from the animation queue and prevent it from executing
throttledResize.stop()

Throttled functions pass arguments through to the original callback.

var test = function ( event ) {
  console.log( event.target )
}

event.on( window, 'scroll', throttle( test, 200 ) )

debounce( function, [wait, options] )

event.debounce() prevents functions from being called except on the repaint cycle after every wait miliseconds since the last time the function was executed. This is similar to throttle, exept rather than executing on a regular interval, callbacks are only triggered wait miliseconds after the last time they were called.

For example if you want to validate a form input after someone finishes typing, you'd do this.

var validate = function( event ) { // some awesome code }

event.on( someInput, 'keyup', debounce( validate, 200 ) )

This will wait 200ms after the last keyup event to trigger the script js. You'll notice that the validate function accepts the event argument, this is because arguments are passed through to the callback.

Debounce also accepts an object as the first parameter.

debounce({
  callback: [function],      // Default callback for leading and trailing
  trailing: [bool/function], // bool: Execute trailing callback. function: callback for leading trigger
  leading:  [bool/function], // bool: Execute leading callback. function: callback for leading trigger
  wait:     150,             // Miliseconds to debounce callbacks
  max:      1000,            // Maximum time to debounce callbacks before forcing a trigger
})

These are equivilent ways to create a trailing-only debounced function.

// Equivilent ways to create a trailing-only debounce.

debounce( someFunc, 100 )
debounce( { callback: someFunc, wait: 100 } )
debounce( { trailing: someFunc }, 100 )

These are equivilent ways to create a leading-only debounced function.

debounce( someFunc, 100, { leading: true, trailing: false } )
debounce( { leading: someFunc, trailing: false, wait: 100 } )
debounce( { leading: someFunc, trailing: false }, 100 )

Understanding Leading & Trailing debounce callbacks

As noted above, debounce fires callbacks wait miliseconds from the last call. This is called a trailing callback. This is the default, but you can also fire leading callbacks which fire immediately after the first call but not again until it has been wait miliseconds from the last callback call.

Here's an example where debounce is leading and trailing and set to 150 miliseconds.

// Track leading and trailing by enabling leading in the options

var leadingAndTrailing = debounce( someFunc, 150, { leading: true })
event.on( window, 'scroll', leadingAndTrailing )

Here is what it looks like when the debounced function is triggered. The * represents times a scroll has occured.

                             (150 ms after)           (150 ms after)
----------------|------------------•------------|-------------•-------------
scroll:         *** * *  *                      *** *        

leading:        A                               A          
trailing:                          Ω                          Ω 

To fire leading callbacks only, disable trailing in the options.

var leadingOnly = debounce( someFunc, 150, { leading: true, trailing: false })

For a simple example, lets consider what happens if a click event is fired twice (a double click).

  • When leading the callback fires immediately.
  • When trailing the callback fires wait ms after the last click.
  • When both, the callback fires immediately and then wait ms after the last click.

Different leading and trailing callbacks

You can trigger different leading and trailing callbacks to mark the start and end of a barrage of debounced calls. For example, you might track the beginning and ending of a user scrolling.

Note: To aid in clarity, you can pass an options block first (instead of a function) to the debounce function as demonstrated below.

var watchScroll = debounce({
  leading:  function() { // scrolling has begun },
  trailing: function() { // scrolling has ended }
}, 150)

event.on( window, 'scroll', watchScroll )

Setting a maximum wait time

Set the optional max value to prevent a function from going more than max miliseconds without being executed.

var debouncedFunc = debounce( someFunc, 200, { max: 1000 } )

In the example above if the debounced function is called every 100ms, someFunc will never be executed since the wait is set to 200ms. By setting max to 1000ms, we ensure that someFunc will be called at least once a second.

bubbleFormEvents()

By default, input focus, blur, and form submit events do not bubble up the DOM. You can add bubbling support for these events like this.

event.bubbleFormEvents()

Here's what this does.

  1. On page change, event listeners for focus, blur and submit are added to input and form elements.
  2. When events are fired, a custom event (which will bubble up the DOM) is fired on the target's parent element, passing along the original event object.
  3. Ajax page changes remove and re-add event listeners automatically.

This means you can attach a single listener to the document or a form and respond to these events wihtout having to manage a host of listeners.

scroll.disablePointer()

While scrolling down a page, your pointer may interact with elements which pass under it. This may cause unnecessary repaints, causing a jittery scrolling experience. This little utility simply watches scroll events and disables pointer events while scrolling.

event.scroll.disablePointer()

This registers itself with scroll.start and scroll.stop event managers to optimize event listener usage and offer the best performance possible.

resize.disableAnimation()

This prevents transitions and animations from being triggered when resizing the window, preventing unnecessary repaints, causing a jittery resize experience. This little utility simply watches resize events and disables pointer events while resizing.

event.resize.disableAnimation()

This registers itself with resize.start and resize.stop event managers to optimize event listener usage and

submit()

Triggers a DOM bubbling, cancelable form submsision event.

event.submit( formElement )

Event listeners watching submit on that form element will be able to do their work, or even preventDefault().

Why? Because form.submit() doesn't fire events which can trigger listeners or be canceled. How? This triggers a CustomEvent and if the event is not defaultPrevented it will then fire form.submit().

Readme

Keywords

Package Sidebar

Install

npm i @spark-engine/event

Weekly Downloads

0

Version

1.13.1

License

MIT

Unpacked Size

66.6 kB

Total Files

28

Last publish

Collaborators

  • imathis