/*!
* Module Events
*/
/**
* @namespace Events
*/
const MODULE_NAME = 'Events';
//###[ IMPORTS ]########################################################################################################
import {
assert,
isFunction,
isString,
isArray,
isBoolean,
isObject,
isWindow,
isEventTarget,
isPlainObject,
isElement,
orDefault,
hasValue,
isEmpty,
isSelector
} from './basic.js';
import {slugify, replace} from './strings.js';
import {removeFrom} from './arrays.js';
import {detectInteractionType} from './context.js';
import {warn} from './logging.js';
//###[ DATA ]###########################################################################################################
export const
EVENT_MAP = new Map(),
POST_MESSAGE_MAP = new Map()
;
const
DEFAULT_NAMESPACE = '__default',
SWIPE_DIRECTIONS = ['up', 'right', 'down', 'left'],
SWIPE_HANDLERS = new WeakMap(),
SWIPE_TOUCH = {
startX : 0,
startY : 0,
endX : 0,
endY : 0
},
EVENT_OPTION_SUPPORT = {
capture : false,
once : false,
passive : false,
signal : false
}
;
try {
const options = {
get capture(){
EVENT_OPTION_SUPPORT.capture = true;
return false;
},
get once(){
EVENT_OPTION_SUPPORT.once = true;
return false;
},
get passive(){
EVENT_OPTION_SUPPORT.passive = true;
return false;
},
get signal(){
EVENT_OPTION_SUPPORT.signal = true;
return false;
},
};
window.addEventListener('test', null, options);
window.removeEventListener('test', null, options);
} catch (err){}
//###[ HELPERS ]########################################################################################################
/*
* Takes the standard set of event function parameters, sanitizes the values and asserts basic compatability.
* Returns the transformed parameters as an object, with keys of the same name as the relevant parameters.
*
* @private
*/
function prepareEventMethodBaseParams(methodName, targets, events, handler, handlerIsOptional=false){
targets = orDefault(targets, [], 'arr');
assert(targets.length > 0, `${MODULE_NAME}:${methodName} | no targets provided`);
events = orDefault(events, [], 'arr');
assert(events.length > 0, `${MODULE_NAME}:${methodName} | no events provided`);
if( !handlerIsOptional || hasValue(handler) ){
assert(isFunction(handler), `${MODULE_NAME}:${methodName} | handler is not a function`);
}
let
targetsAreEventTargets = true,
delegatedTargetsAreSelectorsAndHaveAncestor = true
;
targets.forEach((target, targetIndex) => {
if( isString(target) ){
const ancestor = (targetIndex > 0) ? targets[targetIndex - 1] : null;
delegatedTargetsAreSelectorsAndHaveAncestor &&= isSelector(target) && isEventTarget(ancestor);
} else {
targetsAreEventTargets &&= isEventTarget(target);
}
});
assert(targetsAreEventTargets, `${MODULE_NAME}:${methodName} | not all targets are event targets`);
assert(
delegatedTargetsAreSelectorsAndHaveAncestor,
`${MODULE_NAME}:${methodName} | not all delegated targets are a selector or have an ancestor`
);
const normalizedEvents = events
.map(event => event.replace(`.${DEFAULT_NAMESPACE}`, '.-default-ns'))
.map(event => replace(event, ['xxyxxx-', '-xxyxx',], ''))
.map(event => slugify(event, {
'_' : 'xxyxx-underscore-xxyxx',
'.' : 'xxyxx-dot-xxyxx',
'*' : 'xxyxx-star-xxyxx',
':' : 'xxyxx-colon-xxyxx'
}))
.map(event => replace(event, [
'xxyxx-underscore-xxyxx',
'xxyxx-dot-xxyxx',
'xxyxx-star-xxyxx',
'xxyxx-colon-xxyxx',
], [
'_',
'.',
'*',
':'
]))
.map(event => event.replace('.-default-ns', `.${DEFAULT_NAMESPACE}`))
;
for( const normalizedEventIndex in normalizedEvents ){
if( normalizedEvents[normalizedEventIndex] !== events[normalizedEventIndex] ){
warn(`${MODULE_NAME}:${methodName} | invalid event name "${events[normalizedEventIndex]}" has been normalized to "${normalizedEvents[normalizedEventIndex]}", please check event handling`);
}
}
events = normalizedEvents;
return {targets, events, handler};
}
/*
* Prepares basic information about the current target in a list of targets.
* The current target is identified by index, since the same target may appear multiple times in a list,
* for example as a target and a delegation ancestor.
*
* @private
*/
function prepareEventMethodAdditionalTargetInfo(methodName, targets, targetIndex){
const
prevTarget = ((targetIndex - 1) >= 0) ? targets[targetIndex - 1] : null,
nextTarget = (targetIndex < (targets.length - 1)) ? targets[targetIndex + 1] : null,
hasDelegation = isSelector(nextTarget),
isDelegation = isSelector(targets[targetIndex])
;
assert(
!isDelegation || (isDelegation && isEventTarget(prevTarget)),
`${MODULE_NAME}:${methodName} | delegation has no ancestor`
);
return {prevTarget, nextTarget, hasDelegation, isDelegation};
}
/*
* Prepares basic information about the current event in a list of events.
* The current event is identified by a complete eventName string containing the event itself,
* as well as the complete dot-separated namespace.
*
* @private
*/
function prepareEventMethodEventInfo(eventName, defaultNamespace=null, defaultEvent=null){
const
eventParts = eventName.replace('.', '/////').split('/////'),
event = (isEmpty(eventParts[0]) || (eventParts[0] === '*')) ? defaultEvent : eventParts[0],
namespace = (isEmpty(eventParts[1]) || (eventParts[1] === '*')) ? defaultNamespace : eventParts[1]
;
return {event, namespace};
}
/*
* Gathers matching events with namespaces for a given target (with or without a delegation).
* Returns the found namespaces and events as a dictionary of namespaces with values of sets containing
* the corresponding event names.
*
* @private
*/
function gatherTargetEvents(target, namespace=null, event=null, delegation=null){
const __methodName__ = 'gatherTargetEvents';
const targetEvents = EVENT_MAP.get(target);
assert(isPlainObject(targetEvents), `${MODULE_NAME}:${__methodName__} | invalid target "${target}"`);
const gatheredTargetEvents = {};
if( !hasValue(namespace) && !hasValue(event) ){
Object.keys(targetEvents).forEach(ns => {
gatheredTargetEvents[ns] = new Set([]);
Object.keys(targetEvents[ns]).forEach(ev => {
if( !hasValue(delegation) || hasValue(targetEvents[ns][ev].delegations[delegation]) ){
gatheredTargetEvents[ns].add(ev);
}
});
});
} else if( !hasValue(event) ){
const nameSpaceScope = targetEvents[namespace];
if( hasValue(nameSpaceScope) ){
gatheredTargetEvents[namespace] = new Set([]);
Object.keys(targetEvents[namespace]).forEach(ev => {
if( !hasValue(delegation) || hasValue(nameSpaceScope[ev].delegations[delegation]) ){
gatheredTargetEvents[namespace].add(ev);
}
});
}
} else if( !hasValue(namespace) ){
Object.keys(targetEvents).forEach(ns => {
const nameSpaceScope = targetEvents[ns];
if(
hasValue(nameSpaceScope[event])
&& (!hasValue(delegation) || hasValue(nameSpaceScope[event].delegations[delegation]))
){
if( !hasValue(gatheredTargetEvents[ns]) ){
gatheredTargetEvents[ns] = new Set([]);
}
gatheredTargetEvents[ns].add(event);
}
});
} else {
const nameSpaceScope = targetEvents[namespace];
if(
hasValue(nameSpaceScope)
&& hasValue(nameSpaceScope[event])
&& (!hasValue(delegation) || hasValue(nameSpaceScope[event].delegations[delegation]))
){
if( !hasValue(gatheredTargetEvents[namespace]) ){
gatheredTargetEvents[namespace] = new Set([]);
}
gatheredTargetEvents[namespace].add(event);
}
}
return gatheredTargetEvents;
}
/*
* Iterates through the event map (starting with a specific target or using all targets) and searches for
* deserted handler definitions. Deletes definitions that do not contain any handlers anymore and recursively
* removes the path back to the starting point(s) if it turns out to be empty afterwards.
*
* @private
*/
function cleanUpEventMap(targets){
targets = hasValue(targets) ? new Set([].concat(targets)) : null;
const desertedTargets = [];
EVENT_MAP.forEach((targetEvents, target) => {
if( !hasValue(targets) || targets.has(target) ){
Object.keys(targetEvents).forEach(targetNamespace => {
Object.keys(targetEvents[targetNamespace]).forEach(targetEvent => {
const targetScope = targetEvents[targetNamespace][targetEvent];
let handlerCount = targetScope.handlers.length;
Object.keys(targetScope.delegations).forEach(delegation => {
const delegationHandlerCount = targetScope.delegations[delegation].handlers.length;
handlerCount += delegationHandlerCount;
if( delegationHandlerCount === 0 ){
delete targetScope.delegations[delegation];
}
});
if( handlerCount === 0){
delete targetEvents[targetNamespace][targetEvent];
}
});
if( Object.keys(targetEvents[targetNamespace]).length === 0 ){
delete targetEvents[targetNamespace];
}
});
if( Object.keys(targetEvents).length === 0 ){
desertedTargets.push(target);
}
}
});
desertedTargets.forEach(desertedTarget => {
EVENT_MAP.delete(desertedTarget);
});
}
/*
* Takes a handler function and returns a new function wrapping the handler, making sure, that the handler only
* executes, if the event target matches the given delegation selector. So, the returned function automatically
* checks if the delegation is actually met.
*
* @private
*/
function createDelegatedHandler(delegation, handler){
return function delegatedHandler(e){
const
delegationSelector = `${delegation}`,
delegationFulfilled = hasValue(e.target?.matches)
? e.target.matches(delegationSelector)
: (
isEventTarget(e.syntheticTarget)
|| (
isArray(e.syntheticTarget)
&& isSelector(e.syntheticTarget[1])
)
? (
isEventTarget(e.syntheticTarget)
? e.syntheticTarget.matches(delegationSelector)
: (e.syntheticTarget[1] === delegationSelector))
: null
)
;
if( delegationFulfilled ){
handler(e);
}
};
}
/*
* Takes a handler function and returns a new function, which, when executed, removes the handler from the exact
* path in the EVENT_MAP, defined by the given target, namespace and event (and, optionally, a delegation selector).
* Using this function, one can undo the setting of a handler, using "on" or "once".
*
* @private
*/
function createHandlerRemover(target, namespace, event, handler, delegation=null, ignoreInvalidScope=false){
const
__methodName__ = 'createHandlerRemover',
targetEvents = EVENT_MAP.get(target)
;
let handlerScope = targetEvents?.[namespace]?.[event];
if( hasValue(delegation) ){
assert(isSelector(delegation), `${MODULE_NAME}:${__methodName__} | invalid delegation "${delegation}"`);
handlerScope = handlerScope.delegations[`${delegation}`];
}
if( !ignoreInvalidScope ){
assert(isPlainObject(handlerScope), `${MODULE_NAME}:${__methodName__} | invalid handlerScope`);
} else if( !isPlainObject(handlerScope) ){
return () => {};
}
return function handlerRemover(){
const removedHandlers = handlerScope.handlers.filter(existingHandler => existingHandler.handler === handler);
handlerScope.handlers = removeFrom(handlerScope.handlers, removedHandlers);
removedHandlers.forEach(removedHandler => {
target.removeEventListener(event, removedHandler.action);
});
cleanUpEventMap(target);
};
}
/*
* Takes a handler function and returns a new function, which, when executed, calls the handler and, afterwards,
* automatically removes the handler from the path in the EVENT_MAP, defined by the given target, namespace and event
* (and, optionally, a delegation selector). So, the returned function is essentially a self-destructing handler.
*
* @private
*/
function createSelfRemovingHandler(target, namespace, event, handler, delegation=null){
return function selfRemovingHandler(e){
handler(e);
createHandlerRemover(target, namespace, event, handler, delegation, true)();
};
}
/*
* Removes (a) handler(s) from a path in the EVENT_MAP, defined by the given target, namespace, event and handler
* (and, optionally, a delegation selector).
*
* @private
*/
function removeLocatedHandler(target, namespace, event, handler, delegation=null){
const
__methodName__ = 'removeLocatedHandler',
targetEvents = EVENT_MAP.get(target),
targetScope = targetEvents?.[namespace]?.[event]
;
assert(isPlainObject(targetScope), `${MODULE_NAME}:${__methodName__} | invalid targetScope`);
let handlerScope;
if( hasValue(delegation) ){
const delegationScope = targetScope.delegations[`${delegation}`];
assert(isPlainObject(delegationScope), `${MODULE_NAME}:${__methodName__} | invalid delegation "${delegation}"`);
handlerScope = delegationScope;
} else {
handlerScope = targetScope;
}
const removedHandlers = handlerScope.handlers.filter(existingHandler => {
return hasValue(handler)
? (handler === existingHandler.handler)
: true
;
});
handlerScope.handlers = removeFrom(handlerScope.handlers, removedHandlers);
removedHandlers.forEach(removedHandler => {
target.removeEventListener(event, removedHandler.action);
target.removeEventListener(event, removedHandler.action, {capture : true});
});
return removedHandlers.length;
}
/*
* Removes all handlers matching the given definition provided by target, namespace, event and handler
* (and, optionally, a delegation selector). Leaving out namespace, event or handler works as a wildcard.
*
* @private
*/
function removeHandlers(target, namespace=null, event=null, handler=null, delegation=null){
const targetEvents = gatherTargetEvents(target, namespace, event, delegation);
let removedCount = 0;
Object.keys(targetEvents).forEach(ns => {
Array.from(targetEvents[ns]).forEach(ev => {
removedCount += removeLocatedHandler(target, ns, ev, handler, delegation);
});
});
return removedCount;
}
/*
* Shorthand-function for "removeHandlers" with more sane parameter order for delegations.
*
* @private
*/
function removeDelegatedHandlers(ancestor, delegation, namespace=null, event=null, handler=null){
return removeHandlers(ancestor, namespace, event, handler, delegation);
}
/*
* Pauses (a) handler(s) from a path in the EVENT_MAP, defined by the given target, namespace, event and handler
* (and, optionally, a delegation selector). If paused is false, the function instead resumes the handlers.
*
* @private
*/
function pauseLocatedHandlers(target, namespace, event, handler, delegation=null, paused=true){
const
__methodName__ = 'pauseLocatedHandlers',
targetEvents = EVENT_MAP.get(target),
targetScope = targetEvents?.[namespace]?.[event]
;
assert(isPlainObject(targetScope), `${MODULE_NAME}:${__methodName__} | invalid targetScope`);
let handlerScope;
if( hasValue(delegation) ){
const delegationScope = targetScope.delegations[`${delegation}`];
assert(isPlainObject(delegationScope), `${MODULE_NAME}:${__methodName__} | invalid delegation "${delegation}"`);
handlerScope = delegationScope;
} else {
handlerScope = targetScope;
}
const pausedHandlers = handlerScope.handlers.filter(existingHandler => {
return hasValue(handler)
? (handler === existingHandler.handler)
: true
;
});
pausedHandlers.forEach(pausedHandler => {
pausedHandler.paused = !!paused;
});
return pausedHandlers.length;
}
/*
* Pauses all handlers matching the given definition provided by target, namespace, event and handler
* (and, optionally, a delegation selector). Leaving out namespace, event or handler works as a wildcard.
* If paused is false, the function instead resumes the handlers.
*
* @private
*/
function pauseHandlers(target, namespace=null, event=null, handler=null, delegation=null, paused=true){
const targetEvents = gatherTargetEvents(target, namespace, event, delegation);
let pausedCount = 0;
Object.keys(targetEvents).forEach(ns => {
Array.from(targetEvents[ns]).forEach(ev => {
pausedCount += pauseLocatedHandlers(target, ns, ev, handler, delegation, paused);
});
});
return pausedCount;
}
/*
* Shorthand-function for "pauseHandlers" with more sane parameter order for delegations.
*
* @private
*/
function pauseDelegatedHandlers(ancestor, delegation, namespace=null, event=null, handler=null, paused=true){
return pauseHandlers(ancestor, namespace, event, handler, delegation, paused);
}
/*
* Takes a handler object and a corresponding action, which is not yet aware of its pause state and
* returns an action function, which checks if the handler is paused, before executing the original action.
* Using this, we can wrap handler actions to automatically react to the handler's pause state, preventing any
* handler execution if the handler is currently paused.
*
* @private
*/
function createPauseAwareAction(managedHandler, nonPauseAwareAction){
return function pauseAwareHandler(e){
if( !managedHandler.paused ){
nonPauseAwareAction(e);
}
};
}
/*
* Takes an event listener options object as provided by the user, to be used as the third parameter of
* addEventListener, and returns a sanitized version, taking into regard what options the browser actually supports
* and falling back to boolean capture values, if the browser does not know about listener options at all.
*
* @private
*/
function compileEventListenerOptions(options){
if( isBoolean(options) ) return options;
if( !isObject(options) ) return null;
const supportedOptions = {};
Object.keys(EVENT_OPTION_SUPPORT).forEach(option => {
if( !!EVENT_OPTION_SUPPORT[option] && hasValue(options[option]) ){
supportedOptions[option] = options[option];
}
});
if( (Object.keys(supportedOptions).length === 0) && !!options.capture ){
return true;
}
return supportedOptions;
}
/*
* Creates a synthetic event to dispatch on an event target.
*
* @private
*/
function createSyntheticEvent(
event,
namespace=null,
payload=null,
bubbles=null,
cancelable=null,
syntheticTarget=null,
EventConstructor=null,
eventOptions=null
){
const __methodName__ = 'createSyntheticEvent';
event = `${event}`;
bubbles = orDefault(bubbles, false, 'bool');
cancelable = orDefault(cancelable, bubbles, 'bool');
eventOptions = isPlainObject(eventOptions) ? eventOptions : {};
let e;
if( isFunction(EventConstructor) ){
if( hasValue(payload) ){
warn(`${MODULE_NAME}:${__methodName__} | can't add payload to event "${EventConstructor.name}", skipping`);
}
e = new EventConstructor(event, {bubbles, cancelable, ...eventOptions});
} else {
e = hasValue(payload)
? new CustomEvent(event, {detail : payload, bubbles, cancelable, ...eventOptions})
// we could use new Event() here, but jsdom and ava cannot use that constructor for dispatchEvent :(
: new CustomEvent(event, {bubbles, cancelable, ...eventOptions})
;
}
if( hasValue(namespace) ){
e.namespace = `${namespace}`;
}
if( isEventTarget(syntheticTarget) ){
e.syntheticTarget = syntheticTarget;
e.syntheticTargetElements = [syntheticTarget]
} else if(
isArray(syntheticTarget)
&& isEventTarget(syntheticTarget[0])
&& isSelector(syntheticTarget[1])
){
e.syntheticTarget = syntheticTarget;
Object.defineProperty(e, 'syntheticTargetElements', {
get(){
return Array.from(syntheticTarget[0].querySelectorAll(`${syntheticTarget[1]}`));
}
});
}
return e;
}
/*
* Updates touch data for a start swipe event.
*
* @private
*/
function updateSwipeTouch(e){
const startOrEnd = ['touchstart', 'mousedown'].includes(e.type) ? 'start' : 'end';
if( ['touchstart', 'touchend'].includes(e.type) ){
SWIPE_TOUCH[`${startOrEnd}X`] = e.changedTouches[0].screenX;
SWIPE_TOUCH[`${startOrEnd}Y`] = e.changedTouches[0].screenY;
} else {
SWIPE_TOUCH[`${startOrEnd}X`] = e.screenX;
SWIPE_TOUCH[`${startOrEnd}Y`] = e.screenY;
}
}
/*
* Tries to find a usable target for post messages, based on a given target element.
*
* @private
*/
function resolvePostMessageTarget(target, method){
target = isWindow(target)
? target
: (
isWindow(target?.contentWindow)
? target.contentWindow
: null
)
;
assert(hasValue(target), `${MODULE_NAME}:${method} | no usable target`);
return target
}
/*
* Default handling for post messages for a window.
*
* @private
*/
function windowPostMessageHandler(e){
const
target = e.currentTarget,
targetPostMessages = POST_MESSAGE_MAP.get(target),
origin = !isEmpty(e.origin) ? e.origin : (!!window.__AVA_ENV__ ? window.location.href : null),
messageType = e.data?.type
;
if( hasValue(targetPostMessages) ){
const messageTypes = hasValue(messageType) ? [messageType] : Object.keys(targetPostMessages);
messageTypes.forEach(messageType => {
(targetPostMessages[messageType] ?? []).forEach(handler => {
if( (handler.origin === '*') || (handler.origin === origin) ){
handler.handler(e);
}
});
});
}
}
/*
* Iterates message handlers for a target, and removes handlers, based on given handler and origin.
*
* @private
*/
function removePostMessageHandlers(targetPostMessages, messageType, origin=null, handler=null){
if( hasValue(targetPostMessages[messageType]) ){
const handlerCountBefore = targetPostMessages[messageType].length;
if( !hasValue(origin) && !hasValue(handler) ){
targetPostMessages[messageType] = [];
} else if( hasValue(origin) && !hasValue(handler) ){
targetPostMessages[messageType] = targetPostMessages[messageType].filter(h => h.origin !== origin);
} else if( !hasValue(origin) && hasValue(handler) ){
targetPostMessages[messageType] = targetPostMessages[messageType].filter(h => h.handler !== handler);
} else if( hasValue(origin, handler) ) {
targetPostMessages[messageType] = targetPostMessages[messageType].filter(
h => (h.origin !== origin) && (h.handler !== handler)
);
}
const handlerCountAfter = targetPostMessages[messageType].length;
if( targetPostMessages[messageType].length === 0 ){
delete targetPostMessages[messageType];
}
return handlerCountBefore - handlerCountAfter;
} else {
return 0;
}
}
//###[ EXPORTS ]########################################################################################################
/**
* @namespace Events:on
*/
/**
* Registers (an) event listener(s) to (a) valid EventTarget(s) (most likely (a) DOM-element(s)).
*
* This method is inspired by jQuery and cash, though not identical.
* You may define one or more targets as well as one or more events to register a handler to, by either providing single
* arguments or arrays. You may also, additionally, namespace events, like in jQuery/cash, by adding it after the event
* name, separated by a dot ('click.namespace').
*
* This method returns a remover function, which removes all event registrations done by this method call.
* So, in essence, calling that function, removes exactly, what was added, in a single call.
*
* @param {EventTarget|Array<EventTarget>} targets - the target(s) to register event handlers on
* @param {String|Array<String>} events - the event name(s) to listen to, can be either a single name or a list of names, each name may also have a namespace, separated by a dot, to target all events/namespaces, you may use "*"/"*.*"
* @param {Function} handler - the callback to execute if the event(s) defined in events are being received on target
* @param {?Object|Boolean} [options=null] - event listener options according to "addEventListener"-syntax, will be ignored, if browser does not support this, if boolean, will be used as "useCapture", the same will happen if options are not supported, but you defined "{capture : true}", "{once : true}" will not be applied directly to the listener, but will, instead, set the "once"-parameter to true (otherwise delegated listeners would self-destroy immediately on any check)
* @param {?Boolean} [once=false] - defines if the handler should only execute once, after which it self-destroys automatically, this will automatically be enabled, if you set options.once to true
* @throws error in case no targets are defined
* @throws error in case no events are defined
* @throws error in case handler is not a function
* @throws error in case targets are not all usable event targets
* @throws error in case delegations are missing viable ancestor targets
* @returns {Function} remover function, which removes all handlers again, added by the current execution
*
* @memberof Events:on
* @alias on
* @see off
* @see once
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener?retiredLocale=de#syntax
* @example
* on(linkElement, 'click', e => { e.stopPropagation(); });
* on(someElementWithCustomEvents, 'crash.test', () => { alert('crashed!'); });
* on([ancestorElement, 'a'], 'click', e => { e.target.classList.add('clicked'); });
* on(buttonElement, 'click', () => { console.log('click twice, but I'll just print once); }, {passive : true, once : true});
* on([ancestorElement, '.btn[data-foobar="test"]'], 'click', () => { console.log('I'll just fire once); }, null, true);
* on(document.body, 'click', e => { console.log(`oh, a bubbled event, let's see what has been clicked: "${e.target}"`); });
* on([foo, foo, 'button', bar], ['mousedown', 'touchstart'], e => { e.target.classList.add('interaction-start'); });
*/
export function on(targets, events, handler, options=null, once=false){
const __methodName__ = 'on';
({targets, events, handler} = prepareEventMethodBaseParams(__methodName__, targets, events, handler));
once = !!once || !!options?.once;
delete options?.once;
const removers = [];
targets.forEach((target, targetIndex) => {
const {
prevTarget,
hasDelegation,
isDelegation
} = prepareEventMethodAdditionalTargetInfo(__methodName__, targets, targetIndex);
let targetEvents = EVENT_MAP.get(target);
if( isDelegation ){
targetEvents = EVENT_MAP.get(prevTarget);
} else if( !hasValue(targetEvents) ){
EVENT_MAP.set(target, {[DEFAULT_NAMESPACE] : {}});
targetEvents = EVENT_MAP.get(target);
}
if( !hasDelegation ){
events.forEach(eventName => {
const {event, namespace} = prepareEventMethodEventInfo(eventName, DEFAULT_NAMESPACE);
if( !hasValue(targetEvents[namespace]) ){
targetEvents[namespace] = {};
}
if( !hasValue(targetEvents[namespace][event]) ){
targetEvents[namespace][event] = {
target : isDelegation ? prevTarget : target,
handlers : [],
delegations : {}
};
}
const targetScope = targetEvents[namespace][event];
let handlerScope, action, remover;
if( isDelegation ){
if( !hasValue(targetScope.delegations[target]) ){
targetScope.delegations[target] = {handlers : []};
}
handlerScope = targetScope.delegations[target];
action = !!once
? createDelegatedHandler(
target,
createSelfRemovingHandler(targetScope.target, namespace, event, handler, target)
)
: createDelegatedHandler(target, handler)
;
remover = createHandlerRemover(targetScope.target, namespace, event, handler, target);
} else {
handlerScope = targetScope;
action = !!once
? createSelfRemovingHandler(targetScope.target, namespace, event, handler)
: handler
;
remover = createHandlerRemover(targetScope.target, namespace, event, handler);
}
const managedHandler = {
handler,
remover,
paused : false,
};
managedHandler.action = createPauseAwareAction(managedHandler, action);
handlerScope.handlers = handlerScope.handlers.concat(managedHandler);
const eventListenerOptions = compileEventListenerOptions(options);
if( hasValue(eventListenerOptions) ){
targetScope.target.addEventListener(event, managedHandler.action, eventListenerOptions);
} else {
targetScope.target.addEventListener(event, managedHandler.action);
}
removers.push(remover);
});
}
});
return (removers.length > 1)
? function(){
removers.forEach(remover => remover());
}
: (
(removers.length > 0)
? removers[0]
: null
)
;
}
/**
* @namespace Events:once
*/
/**
* Registers (an) event listener(s) to (a) valid EventTarget(s) (most likely (a) DOM-element(s)).
*
* This version automatically removes the handler, after it has fired once.
*
* This method is inspired by jQuery and cash, though not identical.
* You may define one or more targets as well as one or more events to register a handler to, by either providing single
* arguments or arrays. You may also, additionally, namespace events, like in jQuery/cash, by adding it after the event
* name, separated by a dot ('click.namespace').
*
* This method returns a remover function, which removes all event registrations done by this method call.
* So, in essence, calling that function, removes exactly, what was added, in a single call.
*
* @param {EventTarget|Array<EventTarget>} targets - the target(s) to register event handlers on
* @param {String|Array<String>} events - the event name(s) to listen to, can be either a single name or a list of names, each name may also have a namespace, separated by a dot, to target all events/namespaces, you may use "*"/"*.*"
* @param {Function} handler - the callback to execute if the event(s) defined in events are being received on target
* @param {?Object|Boolean} [options=null] - event listener options according to "addEventListener"-syntax, will be ignored, if browser does not support this, if boolean, will be used as "useCapture", the same will happen if options are not supported, but you defined "{capture : true}", "{once : true}" makes no sense in this case, because the behaviour will automatically be applied anyway
* @throws error in case no targets are defined
* @throws error in case no events are defined
* @throws error in case handler is not a function
* @throws error in case targets are not all usable event targets
* @throws error in case delegations are missing viable ancestor targets
* @returns {Function} remover function, which removes all handlers again, added by the current execution
*
* @memberof Events:once
* @alias once
* @see on
* @see off
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener?retiredLocale=de#syntax
* @example
* once(linkElement, 'click', e => { e.stopPropagation(); });
* once(someElementWithCustomEvents, 'crash.test', () => { alert('crashed!'); });
* once([ancestorElement, 'a'], 'click', e => { e.target.classList.add('clicked'); });
* once(buttonElement, 'click', () => { console.log('click twice, but I'll just print once); }, {passive : true});
* once([ancestorElement, '.btn[data-foobar="test"]'], 'click', () => { console.log('I'll just fire once); });
* once(document.body, 'click', e => { console.log(`oh, a bubbled event, let's see what has been clicked: "${e.target}"`); });
* once([foo, foo, 'button', bar], ['mousedown', 'touchstart'], e => { e.target.classList.add('interaction-start'); });
*/
export function once(targets, events, handler, options=null){
return on(targets, events, handler, options, true);
}
/**
* @namespace Events:off
*/
/**
* Removes (a), previously defined, event listener(s) on (a) valid EventTarget(s) (most likely (a) DOM-element(s)).
*
* The definition of targets and events works exactly as in "on" and "once", the only difference being, that the handler
* is optional in this case, which results in the removal of all handlers, without targeting a specific one.
*
* To specifically target handlers without a namespace, please use the namespace-string "__default".
*
* This function does _not_ differentiate between removal of capture/non-capture events, but always removes both.
*
* If you try to remove event handlers not previously created with `on` (and therefore there are no fitting target
* entries in the EVENT_MAP), the function will fall back to native `removeEventListener`
* (if `tryNativeRemoval` is true), but in that case, a handler has to be defined and the return value will not
* increment, since we do not know if the removal really worked.
*
* @param {EventTarget|Array<EventTarget>} targets - the target(s) to remove event handlers from
* @param {String|Array<String>} events - the event name(s) to remove, can be either a single name or a list of names, each name may also have a namespace, separated by a dot, to target all events/namespaces, you may use "*"/"*.*"
* @param {?Function} [handler=null] - a specific callback function to remove
* @param {?Boolean} [tryNativeRemoval=true] - if a target is not part of the EVENT_MAP native removeEventListener is used as a fallback if this is true (handler needs to be set in that case)
* @throws error in case no targets are defined
* @throws error in case no events are defined
* @throws error in case a defined handler is not a function
* @throws error in case targets are not all usable event targets
* @throws error in case delegations are missing viable ancestor targets
* @returns {Number} the number of handlers actually removed by the function call, may also be 0 if nothing matched
*
* @memberof Events:off
* @alias off
* @see on
* @see once
* @example
* off(buttonElement, 'click');
* off(bar, '*.__default');
* off(customEventElement, 'crash');
* off([ancestorElement, 'a'], 'click');
* off([ancestorElement, '.btn[data-foobar="test"]'], '*.delegated', fSpecificHandler);
* off(linkElement, '*', fSpecificHandler);
* off(customEventElement, ['*.test', '*.site']);
* off([ancestorElement, 'a', ancestorElement, '.btn[data-foobar="test"]'], '*.*', fSpecificHandler);
* off(buttonElement, '*.*');
*/
export function off(targets, events, handler=null, tryNativeRemoval=true){
const __methodName__ = 'off';
({targets, events, handler} = prepareEventMethodBaseParams(__methodName__, targets, events, handler, true));
tryNativeRemoval = orDefault(tryNativeRemoval, true, 'bool');
let removedCount = 0;
targets.forEach((target, targetIndex) => {
const {
prevTarget,
hasDelegation,
isDelegation
} = prepareEventMethodAdditionalTargetInfo(__methodName__, targets, targetIndex);
if( !hasDelegation ){
const targetEvents = isDelegation ? EVENT_MAP.get(prevTarget) : EVENT_MAP.get(target);
events.forEach(eventName => {
const {event, namespace} = prepareEventMethodEventInfo(eventName);
if( hasValue(targetEvents) ){
if( isDelegation ){
removedCount += removeDelegatedHandlers(prevTarget, target, namespace, event, handler);
} else {
removedCount += removeHandlers(target, namespace, event, handler);
}
cleanUpEventMap(isDelegation ? prevTarget : target);
} else if( tryNativeRemoval ){
if( hasValue(handler) ){
(isDelegation ? prevTarget : target).removeEventListener(eventName, handler);
(isDelegation ? prevTarget : target).removeEventListener(eventName, handler, {capture : true});
} else {
warn(`${MODULE_NAME}:${__methodName__} | native fallback event removal for "${eventName}" not possible, handler is missing`);
}
}
});
}
});
return removedCount;
}
/**
* @namespace Events:pause
*/
/**
* Pauses (a), previously defined, event listener(s), without actually removing anything. Subsequent executions
* of the handler will not fire, while the handler is paused, which also means, that paused handlers, set up to only
* fire once, will not self-destroy while being paused.
*
* The definition of targets and events works exactly as in "on" and "once", the only difference being, that the handler
* is optional in this case, which results in the pausing of all handlers, without targeting a specific one.
*
* To specifically target handlers without a namespace, please use the namespace-string "__default".
*
* @param {EventTarget|Array<EventTarget>} targets - the target(s) to pause event handlers on
* @param {String|Array<String>} events - the event name(s) to pause, can be either a single name or a list of names, each name may also have a namespace, separated by a dot, to target all events/namespaces, you may use "*"/"*.*"
* @param {?Function} [handler=null] - a specific callback function to pause
* @param {?Boolean} [paused=true] - defines if the matched handlers are being paused or resumed
* @throws error in case no targets are defined
* @throws error in case no events are defined
* @throws error in case a defined handler is not a function
* @throws error in case targets are not all usable event targets
* @throws error in case delegations are missing viable ancestor targets
* @returns {Number} the number of handlers actually paused by the function call, may also be 0 if nothing matched
*
* @memberof Events:pause
* @alias pause
* @see on
* @see resume
* @example
* pause(buttonElement, 'click');
* pause(linkElement, '*.__default');
* pause(customEventElement, 'crash');
* pause([ancestorElement, 'a'], 'click');
* pause([ancestorElement, '.btn[data-foobar="test"]'], '*.delegated', fSpecificHandler);
*/
export function pause(targets, events, handler=null, paused=true){
const __methodName__ = 'pause';
({targets, events, handler} = prepareEventMethodBaseParams(__methodName__, targets, events, handler, true));
let pausedCount = 0;
targets.forEach((target, targetIndex) => {
const {
prevTarget,
hasDelegation,
isDelegation
} = prepareEventMethodAdditionalTargetInfo(__methodName__, targets, targetIndex);
if( !hasDelegation ){
const targetEvents = isDelegation ? EVENT_MAP.get(prevTarget) : EVENT_MAP.get(target);
if( hasValue(targetEvents) ){
events.forEach(eventName => {
const {event, namespace} = prepareEventMethodEventInfo(eventName);
if( isDelegation ){
pausedCount += pauseDelegatedHandlers(prevTarget, target, namespace, event, handler, paused);
} else {
pausedCount += pauseHandlers(target, namespace, event, handler, null, paused);
}
});
}
}
});
return pausedCount;
}
/**
* @namespace Events:resume
*/
/**
* Resumes (a), previously paused, event listener(s). Subsequent executions of the handler will fire again.
*
* The definition of targets and events works exactly as in "on" and "once", the only difference being, that the handler
* is optional in this case, which results in the un-pausing of all handlers, without targeting a specific one.
*
* To specifically target handlers without a namespace, please use the namespace-string "__default".
*
* @param {EventTarget|Array<EventTarget>} targets - the target(s) to resume event handlers on
* @param {String|Array<String>} events - the event name(s) to resume, can be either a single name or a list of names, each name may also have a namespace, separated by a dot, to target all events/namespaces, you may use "*"/"*.*"
* @param {?Function} [handler=null] - a specific callback function to resume
* @throws error in case no targets are defined
* @throws error in case no events are defined
* @throws error in case a defined handler is not a function
* @throws error in case targets are not all usable event targets
* @throws error in case delegations are missing viable ancestor targets
* @returns {Number} the number of handlers actually resumed by the function call, may also be 0 if nothing matched
*
* @memberof Events:resume
* @alias resume
* @see on
* @see pause
* @example
* resume(linkElement, '*', fSpecificHandler);
* resume(customEventElement, ['*.test', '*.site']);
* resume([ancestorElement, 'a', ancestorElement, '.btn[data-foobar="test"]'], '*.*', fSpecificHandler);
* resume(buttonElement, '*.*');
*/
export function resume(targets, events, handler=null){
return pause(targets, events, handler, false);
}
/**
* @namespace Events:fire
*/
/**
* Fires event handlers of all matched targets for given events.
*
* This function does not actually dispatch events, but identifies matches in the internal event map, based on
* previously registered handlers using "on" and "once" and executes the attached handlers, providing them a synthetic
* CustomEvent as first parameter, carrying the event name as well as a potential payload. So this, function is
* using the event map as an event bus, instead of the DOM, so these events also will never bubble, but just hit the
* currently present handlers identified exactly by the provided parameters.
*
* The definition of targets and events works exactly as in "on" and "once", the only difference being, that we have no
* handler, since if we'd have the handler already, we could just call it.
*
* Since we do not use the DOM in this function, we also do not have native events, and therefore we do not have normal
* event targets we can work with. Instead, this implementation adds the "syntheticTarget" and the
* "syntheticTargetElements" event properties to the event that is given to the handler. "syntheticTarget" contains
* the defined event map target, either as a EventTarget or an array of an EventTarget and a corresponding delegation
* selector (just as you defined them before), while "syntheticTargetElements" returns the actual elements as an
* iterable array. So, in case of a delegation, this gives you the power to actually work with the current delegation
* targets, without having to write own logic for this.
*
* @param {EventTarget|Array<EventTarget>} targets - the target(s) to execute event handlers on
* @param {String|Array<String>} events - the event name(s) to fire, can be either a single name or a list of names, each name may also have a namespace, separated by a dot, to target all events/namespaces, you may use "*"/"*.*"
* @param {?Object} [payload=null] - a plain object payload to relay to the event handlers via the detail of the CustomEvent given to the handler as first parameter
* @throws error in case no targets are defined
* @throws error in case no events are defined
* @throws error in case targets are not all usable event targets
* @throws error in case delegations are missing viable ancestor targets
* @returns {Number} the number of handlers actually executed by the function call, may also be 0 if nothing matched
*
* @memberof Events:fire
* @alias fire
* @see on
* @see once
* @see emit
* @example
* fire(buttonElement, 'click');
* fire(linkElement, '*.__default', {importantFlag : true});
* fire(divElement, 'crash');
* fire([ancestorElement, 'a'], 'click', {linkWasClicked : true});
* fire([ancestorElement, '.btn[data-foobar="test"]'], '*.delegated');
* fire(linkElement, '*');
* fire([ancestorElement, 'a', ancestorElement, '.btn[data-foobar="test"]'], '*.*');
* fire(buttonElement, 'click.*', {price : 666});
*/
export function fire(targets, events, payload=null){
const __methodName__ = 'fire';
({targets, events} = prepareEventMethodBaseParams(__methodName__, targets, events, null, true));
let fireCount = 0;
targets.forEach((target, targetIndex) => {
const {
prevTarget,
hasDelegation,
isDelegation
} = prepareEventMethodAdditionalTargetInfo(__methodName__, targets, targetIndex);
if( !hasDelegation ){
const targetEvents = isDelegation ? EVENT_MAP.get(prevTarget) : EVENT_MAP.get(target);
if( hasValue(targetEvents) ){
events.forEach(eventName => {
const {event, namespace} = prepareEventMethodEventInfo(eventName);
let gatheredTargetEvents;
if( isDelegation ){
gatheredTargetEvents = gatherTargetEvents(prevTarget, namespace, event, target);
} else {
gatheredTargetEvents = gatherTargetEvents(target, namespace, event);
}
Object.keys(gatheredTargetEvents).forEach(ns => {
Array.from(gatheredTargetEvents[ns]).forEach(ev => {
const
handlerScope = isDelegation
? targetEvents[ns][ev].delegations[target]
: targetEvents[ns][ev]
,
syntheticEvent = isDelegation
? createSyntheticEvent(ev, ns, payload, false, false, [prevTarget, target])
: createSyntheticEvent(ev, ns, payload, false, false, target)
;
handlerScope.handlers.forEach(handler => {
handler.action(syntheticEvent);
fireCount++;
});
});
});
});
}
}
});
return fireCount;
}
/**
* @namespace Events:emit
*/
/**
* Dispatches synthetic events on all given targets.
*
* In contrast to "fire", this function actually dispatches bubbling events on the provided EventTargets. Delegations
* are resolved using "querySelectorAll". This function does not check actual handler presence using the event map, but
* blindly emits what has been given, purely using the DOM as the event bus. Handlers defined with "on" and "once" will
* of course still be triggered if hit, since they always also register a native event listener. The events emitted
* are purely synthetic basic Events and CustomEvents, lacking special properties, which, for example, MouseEvents
* provide. So, using "screenX" in the handler will not work. If you need a certain base class for the created events,
* use the "EventConstructor" to provide the base class and add special options via "eventOptions".
*
* The definition of targets and events works almost as in "on" and "once", the only differences being, that we have no
* handler, and we cannot leave out the event name. Using a wildcard for the namespace will leave out the namespace in
* the created events.
*
* @param {EventTarget|Array<EventTarget>} targets - the target(s) to dispatch events on
* @param {String|Array<String>} events - the event name(s) to emit, can be either a single name or a list of names, each name may also have a namespace, separated by a dot, to target all events/namespaces, you may use "*"/"*.*"
* @param {?Object} [payload=null] - a plain object payload to relay to the event handlers via the detail of the CustomEvent given to the handler as first parameter
* @param {?Function} [EventConstructor=null] - the default constructor is Event/CustomEvent, if you need another specific synthetic event, provide a constructor such as MouseEvent here
* @param {?Object} [eventOptions=null] - use this plain object to provide constructor specific options to use in event construction, this should especially come in handy in case you provide a custom EventConstructor
* @throws error in case no targets are defined
* @throws error in case no events are defined
* @throws error in case targets are not all usable event targets
* @throws error in case delegations are missing viable ancestor targets
* @returns {Number} the number of events actually dispatched by the function call, may also be 0 if nothing matched
*
* @memberof Events:emit
* @alias emit
* @see on
* @see once
* @see fire
* @example
* emit([buttonElement, ancestorElement, 'a'], 'click');
* emit(linkElement, 'click.__default', {defaultClick: true});
* emit([divElement, document.body], 'crash');
* emit([ancestorElement, 'a'], 'click', {trackingId : 'abc123'});
* emit([ancestorElement, '.btn[data-foobar="test"]'], 'click.delegated');
* emit(ancestorElement, ['crash.test', 'crash.site'], {damage : 1000});
* emit([ancestorElement, 'a', ancestorElement, '.btn[data-foobar="test"]'], 'click.delegated', null, null, {bubbles : false});
* emit(buttonElement, 'click.*', {price : 666}, MouseEvent, {bubbles : false});
*/
export function emit(targets, events, payload=null, EventConstructor=null, eventOptions=null){
const __methodName__ = 'emit';
({targets, events} = prepareEventMethodBaseParams(__methodName__, targets, events, null, true));
let emitCount = 0;
targets.forEach((target, targetIndex) => {
const {
prevTarget,
hasDelegation,
isDelegation
} = prepareEventMethodAdditionalTargetInfo(__methodName__, targets, targetIndex);
if( !hasDelegation ){
events.forEach(eventName => {
const {event, namespace} = prepareEventMethodEventInfo(eventName);
assert(hasValue(event), `${MODULE_NAME}:${__methodName__} | missing event name`);
if( isDelegation ){
Array.from(prevTarget.querySelectorAll(target)).forEach(element => {
element.dispatchEvent(
createSyntheticEvent(event, namespace, payload, true, true, null, EventConstructor, eventOptions)
);
emitCount++;
});
} else {
target.dispatchEvent(
createSyntheticEvent(event, namespace, payload, true, true, null, EventConstructor, eventOptions)
);
emitCount++;
}
});
}
});
return emitCount;
}
/**
* @namespace Events:offDetachedElements
*/
/**
* This method completely removes all handlers and listeners for provided targets in case that they
* are actually an element and not part of the DOM (anymore).
*
* The most common use-case for this is to clean the event map after dynamically removing an element from the interface
* during runtime, maybe as a reaction to a user interaction.
*
* Since we are overlaying the DOM event system with a separate (non-weak) event map, handlers in the map do not
* automatically disappear if the event targets, being elements, are removed from the DOM. In that case, we have to
* actually unregister events again, for which this is a handy little helper method.
*
* There are two common ways to use this:
* 1. Just call it with the removed element, after removal of the element. This will only remove all data for that
* element, if it actually is an element and is not currently in the DOM.
* 2. Call it without parameters, to iterate all current targets, check if they are elements and currently not in the
* DOM and remove all handlers and listeners in that case.
*
* So, you can either directly clean-up anything you remove or remove everything, that needs removing and do a general
* clean-up after everything has been done.
*
* Be aware, that the definition of what an element is and if that element is part of the dom is defined by the actual
* event target. So delegations are not automatically covered by this, since they rely on the ancestor element for
* event handling.
*
* @param {?EventTarget|Array<EventTarget>} [targets=null] - the target(s) to remove from the event map, if not set, all event targets in the current event map are used
* @returns {Number} the number of targets for which registered handlers and listeners have been removed
*
* @memberof Events:offDetachedElements
* @alias offDetachedElements
* @example
* button.remove();
* offDetachedElements(button);
* => 1
* link.innerText = 'test';
* button.remove();
* offDetachedElements([link, button]);
* => 1
* offDetachedElements()
* => number of all currently registered targets, being elements and not in the dom
*/
export function offDetachedElements(targets){
targets = orDefault(targets, [], 'arr');
if( targets.length === 0 ){
targets = Array.from(EVENT_MAP.keys());
}
let offCount = 0;
targets.forEach(target => {
if( isElement(target) && !document.body.contains(target) && EVENT_MAP.has(target) ){
offCount++;
off(target, '*');
}
});
return offCount;
}
/**
* @namespace Events:onSwipe
*/
/**
* Defines a handler for a swipe gesture on (an) element(s).
* Offers four swipe directions (up/right/down/left), where triggering the handler depends on the distance
* between touchstart and touchend in relation to the element's width or height, depending on the direction,
* multiplied by a factor to express a percentage.
*
* You may also set this method to also fire upon mouse swipes, by setting "hasToBeTouchDevice" to false.
*
* @param {EventTarget|Array<EventTarget>} targets - the target(s) to register event handlers on
* @param {String} direction - the direction to bind => up/down/left/right
* @param {Function} handler - the callback to execute if the event(s) defined in events are being received on target
* @param {?Number} [dimensionFactor=0.2] - to determine what registers as a swipe we use a percentage of the element's width/height, the touch has to move, default is 20%
* @param {?Boolean} [hasToBeTouchDevice=true] - if true, makes sure the handlers are only active on touch devices, if false, also reacts to mouse swipes
* @param {?String} [eventNameSpace='annex-swipe'] - apply an event namespace, which identifies specific events, helpful for a specific unbind later using the same namespace
* @throws error in case no targets are defined
* @throws error in case unknown direction is defined
* @throws error in case handler is not a function
* @throws error in case targets are not all usable event targets
* @throws error in case delegations are missing viable ancestor targets
* @returns {Function} remover function, which removes all handlers again, added by the current execution
*
* @memberof Events:onSwipe
* @alias onSwipe
* @see offSwipe
* @example
* onSwipe(slider, 'up', e => { e.currentTarget.fadeOut(); });
* onSwipe(slider, 'right', () => { document.body.dispatchEvent(new CustomEvent('load-previous-thing')); }, 0.15, false, 'foobar-prev');
*/
export function onSwipe(targets, direction, handler, dimensionFactor=0.2, hasToBeTouchDevice=true, eventNameSpace='annex-swipe'){
const __methodName__ = 'onSwipe';
direction = orDefault(direction, '', 'str');
dimensionFactor = orDefault(dimensionFactor, 0.2, 'float');
hasToBeTouchDevice = orDefault(hasToBeTouchDevice, true, 'bool');
eventNameSpace = orDefault(eventNameSpace, 'annex-swipe', 'str');
assert(SWIPE_DIRECTIONS.includes(direction), `${MODULE_NAME}:${__methodName__} | unknown direction "${direction}"`);
let events = [`touchstart.${eventNameSpace}-${direction}`, `touchend.${eventNameSpace}-${direction}`];
if( !hasToBeTouchDevice ){
events.push(`mousedown.${eventNameSpace}-${direction}`);
events.push(`mouseup.${eventNameSpace}-${direction}`);
}
({targets, events, handler} = prepareEventMethodBaseParams(__methodName__, targets, events, handler));
const originalHandler = handler;
handler = (hasToBeTouchDevice && (detectInteractionType() !== 'touch')) ? () => {} : originalHandler;
const swipeHandler = SWIPE_HANDLERS.get(originalHandler) ?? (e => {
updateSwipeTouch(e);
if( ['touchend', 'mouseup'].includes(e.type) ){
const
width = e.currentTarget.offsetWidth,
height = e.currentTarget.offsetHeight
;
if(
(!hasToBeTouchDevice || (detectInteractionType() === 'touch'))
&& (
((direction === 'up') && (SWIPE_TOUCH.startY > (SWIPE_TOUCH.endY + height * dimensionFactor)))
|| ((direction === 'right') && (SWIPE_TOUCH.startX < (SWIPE_TOUCH.endX - width * dimensionFactor)))
|| ((direction === 'down') && (SWIPE_TOUCH.startY < (SWIPE_TOUCH.endY - height * dimensionFactor)))
|| ((direction === 'left') && (SWIPE_TOUCH.startX > (SWIPE_TOUCH.endX + width * dimensionFactor)))
)
){
handler(e);
}
}
});
SWIPE_HANDLERS.set(originalHandler, swipeHandler);
return on(targets, events, swipeHandler);
}
/**
* @namespace Events:offSwipe
*/
/**
* Removes (a) handler(s) for a swipe gesture from (an) element(s).
*
* Normally all directions are removed individually, but if you leave out `direction` all directions are removed at once.
*
* @param {EventTarget|Array<EventTarget>} targets - the target(s) to remove event handlers from
* @param {?String} [direction=null] - the direction to remove => up/down/left/right, if empty, all directions are removed
* @param {?Function} [handler=null] - a specific callback function to remove
* @param {?String} [eventNameSpace='annex-swipe'] - event namespace to remove
* @throws error in case no targets are defined
* @throws error in case unknown direction is defined
* @throws error in case a defined handler is not a function
* @throws error in case targets are not all usable event targets
* @throws error in case delegations are missing viable ancestor targets
* @returns {Number} the number of handlers actually removed by the function call, may also be 0 if nothing matched
*
* @memberof Events:offSwipe
* @alias offSwipe
* @see onSwipe
* @example
* offSwipe(slider, 'right');
* offSwipe(slider, 'left', fSpecialHandler, 'foobar-prev');
* offSwipe(slider);
*/
export function offSwipe(targets, direction=null, handler=null, eventNameSpace='annex-swipe'){
const __methodName__ = 'offSwipe';
direction = orDefault(direction, '', 'str');
eventNameSpace = orDefault(eventNameSpace, 'annex-swipe', 'str');
assert(SWIPE_DIRECTIONS.concat('').includes(direction), `${MODULE_NAME}:${__methodName__} | unknown direction "${direction}"`);
const directions = (direction === '') ? SWIPE_DIRECTIONS : [direction];
let removedCount = 0;
directions.forEach(direction => {
let events = [
`touchstart.${eventNameSpace}-${direction}`,
`touchend.${eventNameSpace}-${direction}`,
`mousedown.${eventNameSpace}-${direction}`,
`mouseup.${eventNameSpace}-${direction}`
];
({targets, events, handler} = prepareEventMethodBaseParams(__methodName__, targets, events, handler, true));
if( hasValue(handler) ){
const swipeHandler = SWIPE_HANDLERS.get(handler);
if( hasValue(swipeHandler) ){
removedCount += off(targets, events, swipeHandler);
}
} else {
removedCount += off(targets, events);
}
});
return removedCount;
}
/**
* @namespace Events:onDomReady
*/
/**
* Executes a callback on document ready (DOM parsed, complete and usable, not loaded/onload).
*
* @param {Function} callback - function to execute, once document is parsed and ready
*
* @memberof Events:onDomReady
* @alias onDomReady
* @example
* onDomReady(() => {
* document.body.classList.add('dom-ready');
* });
*/
export function onDomReady(callback){
if( document.readyState !== 'loading' ){
callback();
} else {
const wrappedCallback = () => {
document.removeEventListener('DOMContentLoaded', wrappedCallback);
callback();
};
document.addEventListener('DOMContentLoaded', wrappedCallback);
}
}
/**
* @namespace Events:onPostMessage
*/
/**
* Register an event handler for a post message on a valid target, like a window or an iframe.
*
* The handler will only be executed, if the messageType as well as the origin match. The messageType must be
* part of the payload, using the key "type", which `emitPostMessage` does automatically.
*
* Putting the origin as an obligatory parameter at the second place, is deliberate by design, to force everyone
* to really think about, what to use here. Usually, most people, just throw in the "*" wildcard, paying no attention
* to the security implications. Please really think about what to use here.
*
* A word of advice: keep in mind, that, contrary to most other events in javascript, post messages actually work
* asynchronously (so you cannot be sure, that the handler has been executed, directly after a post message has been
* sent) and that messages/payload are not transferred as-is, but are cloned, using the "structured clone algorithm",
* which means, that not every javascript object is transferable without losses.
*
* @param {Window|HTMLIFrameElement} target - window/iframe to register the handler to (iframes are automatically resolved to the contentWindow)
* @param {String} origin - the origin the received post message has to have, for the handler to get executed (defaults to "*", if receiving a nullish value)
* @param {String} messageType - the type/name the post message has to have, for the handler to get executed (will be checked using the key "type" in the message's payload)
* @param {Function} handler - the handler to execute, if a post message, matching all conditions, is received
* @throws error if target is not usable
* @return a function, which, if executed, removes everything registered by the current call
*
* @memberof Events:onPostMessage
* @alias onPostMessage
* @see offPostMessage
* @see emitPostMessage
* @see https://developer.mozilla.org/en-US/docs/Glossary/Origin
* @see https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
* @example
* const removeAgainFunction = onPostMessage(window, '*', 'foobar-message', () => { doSomething(); });
* onPostMessage(iframeElement, 'https://foobar.com:80/', 'foobar-message', e => { resizeIframe(e.data.payload.height); });
*/
export function onPostMessage(target, origin, messageType, handler){
const __methodName__ = 'onPostMessage';
target = resolvePostMessageTarget(target, __methodName__);
origin = orDefault(origin, '*', 'str');
messageType = `${messageType}`;
assert(isFunction(handler), `${MODULE_NAME}:${__methodName__} | handler is not a function`);
if( !hasValue(POST_MESSAGE_MAP.get(target)) ){
POST_MESSAGE_MAP.set(target, {});
target.addEventListener('message', windowPostMessageHandler);
}
const targetPostMessages = POST_MESSAGE_MAP.get(target);
if( !hasValue(targetPostMessages[messageType]) ){
targetPostMessages[messageType] = [];
}
targetPostMessages[messageType].push({handler, origin});
return () => { offPostMessage(target, origin, messageType, handler); };
}
/**
* @namespace Events:offPostMessage
*/
/**
* Unregister (an) event handler(s) for (a) post message(s) on a valid target, like a window or an iframe.
*
* Similar to `off`, this function can handle rather unspecific cases as well as very specific definitions.
* Just setting the target, removes all registrations for that target. Setting an `origin` and/or a `messageType`
* additionally, only removes handlers, that were registered explicitly for these values. Adding a handler only
* removes that specific handler (without origin and/or messageType, the handler is removed everywhere).
*
* Putting the origin parameter at the second place, is deliberate by design, to force everyone to really think about,
* what to use here. Usually, most people, just throw in the "*" wildcard, paying no attention to the security
* implications, when setting a post message handler. Since we force this on `onPostMessage`, we keep the signature
* here as well, just making everything except target optional.
*
* If you try to remove event handlers not previously created with `onPostMessage` (and therefore there are no fitting
* target entries in the POST_MESSAGE_MAP), the function will fall back to native `removeEventListener`
* (if `tryNativeRemoval` is true), but in that case, a handler has to be defined and the return value will not
* increment, since we do not know if the removal really worked.
*
* @param {Window|HTMLIFrameElement} target - window/iframe to remove handler(s) from (iframes are automatically resolved to the contentWindow)
* @param {?String} [origin=null] - the origin the received post message has to have, for the handler to get executed (defaults to "*", if receiving a nullish value)
* @param {?String} [messageType=null] - the type/name the post message has to have, for the handler to get executed (will be checked using the key "type" in the message's payload)
* @param {?Function} [handler=null] - the handler to execute, if a post message, matching all conditions, is received
* @param {?Boolean} [tryNativeRemoval=true] - if a target is not part of the POST_MESSAGE_MAP native removeEventListener is used as a fallback if this is true (handler needs to be set in that case)
* @throws error if target is not usable
* @return the number of actually removed handlers, that matched the conditions
*
* @memberof Events:offPostMessage
* @alias offPostMessage
* @see onPostMessage
* @see emitPostMessage
* @see https://developer.mozilla.org/en-US/docs/Glossary/Origin
* @example
* const offCount = offPostMessage(window, 'https://foobar.com:80/', 'foobar-message');
* offPostMessage(window, null, null, specialHandlerFunction);
*/
export function offPostMessage(target, origin=null, messageType=null, handler=null, tryNativeRemoval=true){
const __methodName__ = 'offPostMessage';
target = resolvePostMessageTarget(target, __methodName__);
origin = orDefault(origin, null, 'str');
messageType = orDefault(messageType, null, 'str');
tryNativeRemoval = orDefault(tryNativeRemoval, true, 'bool');
if( hasValue(handler) ){
assert(isFunction(handler), `${MODULE_NAME}:${__methodName__} | handler is not a function`);
}
let removedCount = 0;
const targetPostMessages = POST_MESSAGE_MAP.get(target);
if( hasValue(targetPostMessages) ){
const messageTypes = hasValue(messageType) ? [messageType] : Object.keys(targetPostMessages);
messageTypes.forEach(messageType => {
removedCount += removePostMessageHandlers(targetPostMessages, messageType, origin, handler);
});
if( Object.keys(targetPostMessages).length === 0 ){
POST_MESSAGE_MAP.delete(target);
}
} else if( tryNativeRemoval ){
if( hasValue(handler) ){
target.removeEventListener('message', handler);
} else {
warn(`${MODULE_NAME}:${__methodName__} | native fallback event removal for "${messageType}" not possible, handler is missing`);
}
}
if( !hasValue(POST_MESSAGE_MAP.get(target)) ){
target.removeEventListener('message', windowPostMessageHandler);
}
return removedCount;
}
/**
* @namespace Events:emitPostMessage
*/
/**
* Emit/dispatch a post message on a valid target, like a window or an iframe.
*
* Putting the origin as an obligatory parameter at the second place, is deliberate by design, to force everyone
* to really think about, what to use here. Usually, most people, just throw in the "*" wildcard, paying no attention
* to the security implications. Please really think about what to use here.
*
* This function adds the `messageType` automatically to the message/payload using the key `type`. `onPostMessage` will
* use that information additionally to the `origin` to determine if a registration fits the occurred event. The
* `payload` will be placed in the message using the key `payload`. So `e.data` will look like this in the
* handler at the end: `{type : messageType, payload : {...payload}}`
*
* A word of advice: keep in mind, that, contrary to most other events in javascript, post messages actually work
* asynchronously (so you cannot be sure, that the handler has been executed, directly after a post message has been
* sent) and that messages/payload are not transferred as-is, but are cloned, using the "structured clone algorithm",
* which means, that not every javascript object is transferable without losses.
*
* @param {Window|HTMLIFrameElement} target - window/iframe to receive the post message (iframes are automatically resolved to the contentWindow)
* @param {String} origin - the origin the current context has to have, to actually send the post message the received post message has to have, this does NOT set the origin! (defaults to "*", if receiving a nullish value)
* @param {String} messageType - the type/name of the post message (will be checked using the key "type" in the message's payload, which will automatically be set using this function)
* @param {?*} [payload=null] - a payload to add to the message under the key "payload"
* @throws error if target is not usable
* @return the resolved target of the post message
*
* @memberof Events:emitPostMessage
* @alias emitPostMessage
* @see onPostMessage
* @see offPostMessage
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
* @see https://developer.mozilla.org/en-US/docs/Glossary/Origin
* @see https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
* @example
* emitPostMessage(window, '*', 'foobar-message', {timestamp : new Date()});
* emitPostMessage(iframeElement, 'https://foobar.com:80/', 'foobar-message');
*/
export function emitPostMessage(target, origin, messageType, payload=null){
const __methodName__ = 'emitPostMessage';
target = resolvePostMessageTarget(target, __methodName__);
origin = orDefault(origin, '*', 'str');
messageType = `${messageType}`;
const message = {type : messageType};
if( hasValue(payload) ){
message.payload = payload;
}
target.postMessage(message, origin);
return target;
}