events_delegate.js

const DOCUMENT_NODE_TYPE = 9;

if ( typeof Element !== 'undefined' && ! Element.prototype.matches ) {
	const proto = Element.prototype;

	proto.matches = proto.matchesSelector ||
		proto.mozMatchesSelector ||
		proto.msMatchesSelector ||
		proto.oMatchesSelector ||
		proto.webkitMatchesSelector;
}

/**
 * @ignore
 */
function closest( element, selector ) {
	while ( element && element.nodeType !== DOCUMENT_NODE_TYPE ) {
		if ( typeof element.matches === 'function' &&
			element.matches( selector ) ) {
			return element;
		}
		element = element.parentNode;
	}
}

/**
 * @ignore
 */
function _delegate( element, selector, type, callback, useCapture ) {
	const listenerFn = listener.apply( this, arguments );

	element.addEventListener( type, listenerFn, useCapture );

	return {
		destroy() {
			element.removeEventListener( type, listenerFn, useCapture );
		},
	};
}

/**
 * @module delegate
 * @description Delegates events to a selector.
 *
 * @since 1.0.0
 *
 * @param {string|Array|NodeList|HTMLElement} elements   A selector string, or element/array of elements.
 * @param {string}                            selector   the selector string to search for and delegate.
 * @param {string|Function}                   type       The event type or a function to execute.
 * @param {Function}                          callback   The callback function to execute.
 * @param {boolean}                           useCapture Whether to use useCapture or not. https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#usecapture
 *
 * @return {unknown[]|{destroy(): void}|*} Destroy method is returned on instance.
 *
 * @example
 * import { delegate } from "@gravityforms/utils";
 *
 * // With the default base (document)
 *
 * delegate( '.btn', 'click', function( e ) {
 * 	console.log( e.delegateTarget );
 * }, false );
 *
 * // With an element as base
 *
 * delegate( document.body, '.btn', 'click', function( e ) {
 * 	console.log( e.delegateTarget );
 * }, false );
 *
 * // With a selector (of existing elements) as base
 *
 * delegate( '.container', '.btn', 'click', function( e ) {
 * 	console.log( e.delegateTarget );
 * }, false );
 *
 * // With an array/array-like of elements as base
 *
 * delegate( document.querySelectorAll( '.container' ), '.btn', 'click', function( e ) {
 * 	console.log( e.delegateTarget );
 * }, false );
 *
 * // Remove delegation
 *
 * // With a single base element (default or specified)
 *
 * const delegation = delegate( document.body, '.btn', 'click', function( e ) {
 * 	console.log( e.delegateTarget );
 * }, false );
 *
 * delegation.destroy();
 *
 * // With multiple elements (via selector or array)
 * // Note: selectors are always treated as multiple elements, even if one or none are matched. delegate() will return an array.
 *
 * const delegations = delegate( '.container', '.btn', 'click', function( e ) {
 * 	console.log( e.delegateTarget );
 * }, false );
 *
 * delegations.forEach( function( delegation ) {
 * 	delegation.destroy();
 * } );
 *
 */
function delegate( elements, selector, type, callback, useCapture = false ) {
	// Handle the regular Element usage
	if ( typeof elements.addEventListener === 'function' ) {
		return _delegate.apply( null, arguments );
	}

	// Handle Element-less usage, it defaults to global delegation
	if ( typeof type === 'function' ) {
		// Use `document` as the first parameter, then apply arguments
		// This is a short way to .unshift `arguments` without running into deoptimizations
		return _delegate.bind( null, document ).apply( null, arguments );
	}

	// Handle Selector-based usage
	if ( typeof elements === 'string' ) {
		elements = document.querySelectorAll( elements );
	}

	// Handle Array-like based usage
	return Array.prototype.map.call( elements, function( element ) {
		return _delegate( element, selector, type, callback, useCapture );
	} );
}

/**
 * @ignore
 */
function listener( element, selector, type, callback ) {
	return function( e ) {
		e.delegateTarget = closest( e.target, selector );

		if ( e.delegateTarget ) {
			callback.call( element, e );
		}
	};
}

export default delegate;