'use strict';

import {
	isDef,
	isDefAndNotNull,
	isDocument,
	isDocumentFragment,
	isElement,
	isObject,
	isString,
	object,
} from 'metal';
import domData from './domData';
import DomDelegatedEventHandle from './DomDelegatedEventHandle';
import DomEventHandle from './DomEventHandle';

const elementsByTag_ = {};
const supportCache_ = {};
export const customEvents = {};

const LAST_CONTAINER = '__metal_last_container__';
const USE_CAPTURE = {
	blur: true,
	error: true,
	focus: true,
	invalid: true,
	load: true,
	scroll: true,
};

/**
 * Adds the requested CSS classes to an element.
 * @param {!Element|!Nodelist} elements The element or elements to add CSS classes to.
 * @param {string} classes CSS classes to add.
 */
export function addClasses(elements, classes) {
	if (!isObject(elements) || !isString(classes)) {
		return;
	}

	if (!elements.length) {
		elements = [elements];
	}

	for (let i = 0; i < elements.length; i++) {
		if ('classList' in elements[i]) {
			addClassesWithNative_(elements[i], classes);
		} else {
			addClassesWithoutNative_(elements[i], classes);
		}
	}
}

/**
 * Adds the requested CSS classes to an element using classList.
 * @param {!Element} element The element to add CSS classes to.
 * @param {string} classes CSS classes to add.
 * @private
 */
function addClassesWithNative_(element, classes) {
	classes.split(' ').forEach(function(className) {
		if (className) {
			element.classList.add(className);
		}
	});
}

/**
 * IE11 doesn't support Element.classList.add function, this method
 * adds the requested CSS classes to an element without using classList.
 * @param {!Element} element The element to add CSS classes to.
 * @param {string} classes CSS classes to add.
 * @private
 */
function addClassesWithoutNative_(element, classes) {
	const elementClassName = ` ${element.className} `;
	let classesToAppend = '';

	classes = classes.split(' ');

	for (let i = 0; i < classes.length; i++) {
		const className = classes[i];

		if (elementClassName.indexOf(` ${className} `) === -1) {
			classesToAppend += ` ${className}`;
		}
	}

	if (classesToAppend) {
		element.className = element.className + classesToAppend;
	}
}

/**
 * Adds an event listener to the given element, to be triggered via delegate.
 * @param {!Element} element
 * @param {string} eventName
 * @param {!function()} listener
 * @private
 */
function addElementListener_(element, eventName, listener) {
	addToArr_(domData.get(element, 'listeners', {}), eventName, listener);
}

/**
 * Adds an event listener to the given element, to be triggered via delegate
 * selectors.
 * @param {!Element} element
 * @param {string} eventName
 * @param {string} selector
 * @param {!function()} listener
 * @private
 */
function addSelectorListener_(element, eventName, selector, listener) {
	const delegatingData = domData.get(element, 'delegating', {});
	addToArr_(delegatingData[eventName].selectors, selector, listener);
}

/**
 * Adds a value to an array inside an object, creating it first if it doesn't
 * yet exist.
 * @param {!Array} arr
 * @param {string} key
 * @param {*} value
 * @private
 */
function addToArr_(arr, key, value) {
	if (!arr[key]) {
		arr[key] = [];
	}
	arr[key].push(value);
}

/**
 * Attaches a delegate listener, unless there's already one attached.
 * @param {!Element} element
 * @param {string} eventName
 * @private
 */
function attachDelegateEvent_(element, eventName) {
	let delegatingData = domData.get(element, 'delegating', {});
	if (!delegatingData[eventName]) {
		delegatingData[eventName] = {
			handle: on(
				element,
				eventName,
				handleDelegateEvent_,
				!!USE_CAPTURE[eventName]
			),
			selectors: {},
		};
	}
}

/**
 * IE11 doesn't support Element.closest function, this method
 * gets the closest element up the tree from the given element (including
 * itself) that matches the specified selector, or null if none match.
 * @param {Element} element
 * @param {string} selector
 * @return {Element}
 */
export function closest(element, selector) {
	while (element && !match(element, selector)) {
		element = element.parentNode;
	}
	return element;
}

/**
 * IE11 doesn't support Element.classList.add function, this method
 * appends a child node with text or other nodes to a parent node. If
 * child is a HTML string it will be automatically converted to a document
 * fragment before appending it to the parent.
 * @param {!Element} parent The node to append nodes to.
 * @param {!(Element|NodeList|string)} child The thing to append to the parent.
 * @return {!Element} The appended child.
 */
export function append(parent, child) {
	if (isString(child)) {
		child = buildFragment(child);
	}
	if (isNodeListLike(child)) {
		const childArr = Array.prototype.slice.call(child);
		for (let i = 0; i < childArr.length; i++) {
			parent.appendChild(childArr[i]);
		}
	} else {
		parent.appendChild(child);
	}
	return child;
}

/**
 * Helper for converting a HTML string into a document fragment.
 * @param {string} htmlString The HTML string to convert.
 * @return {!Element} The resulting document fragment.
 */
export function buildFragment(htmlString) {
	const tempDiv = document.createElement('div');
	tempDiv.innerHTML = `<br>${htmlString}`;
	tempDiv.removeChild(tempDiv.firstChild);

	const fragment = document.createDocumentFragment();
	while (tempDiv.firstChild) {
		fragment.appendChild(tempDiv.firstChild);
	}
	return fragment;
}

/**
 * Checks if the first element contains the second one.
 * @deprecated Use element1.contains(element2) directly instead of this method
 * @param {!Element} element1
 * @param {!Element} element2
 * @return {boolean}
 */
export function contains(element1, element2) {
	if (isDocument(element1)) {
		// document.contains is not defined on IE9, so call it on documentElement instead.
		return element1.documentElement.contains(element2);
	} else {
		return element1.contains(element2);
	}
}

/**
 * Listens to the specified event on the given DOM element, but only calls the
 * given callback listener when it's triggered by elements that match the
 * given selector or target element.
 * @param {!Element} element The DOM element the event should be listened on.
 * @param {string} eventName The name of the event to listen to.
 * @param {!Element|string} selectorOrTarget Either an element or css selector
 *     that should match the event for the listener to be triggered.
 * @param {!function(!Object)} callback Function to be called when the event
 *     is triggered. It will receive the normalized event object.
 * @param {boolean=} defaultListener Optional flag indicating if this is a default
 *     listener. That means that it would only be executed after all non
 *     default listeners, and only if the event isn't prevented via
 *     `preventDefault`.
 * @return {!EventHandle} Can be used to remove the listener.
 */
export function delegate(
	element,
	eventName,
	selectorOrTarget,
	callback,
	defaultListener
) {
	const customConfig = customEvents[eventName];
	if (customConfig && customConfig.delegate) {
		eventName = customConfig.originalEvent;
		callback = customConfig.handler.bind(customConfig, callback);
	}

	if (defaultListener) {
		// Wrap callback so we don't set property directly on it.
		callback = callback.bind();
		callback.defaultListener_ = true;
	}

	attachDelegateEvent_(element, eventName);
	if (isString(selectorOrTarget)) {
		addSelectorListener_(element, eventName, selectorOrTarget, callback);
	} else {
		addElementListener_(selectorOrTarget, eventName, callback);
	}

	return new DomDelegatedEventHandle(
		isString(selectorOrTarget) ? element : selectorOrTarget,
		eventName,
		callback,
		isString(selectorOrTarget) ? selectorOrTarget : null
	);
}

/**
 * Verifies if the element is able to trigger the Click event,
 * simulating browsers behaviour, avoiding event listeners to be called by triggerEvent method.
 * @param {Element} node Element to be checked.
 * @param {string} eventName The event name.
 * @param {Object=} eventObj
 * @private
 * @return {boolean}
 */
function isAbleToInteractWith_(node, eventName, eventObj) {
	if (eventObj && eventName === 'click' && eventObj.button === 2) {
		// Firefox triggers "click" events on the document for right clicks. This
		// causes our delegate logic to trigger it for regular elements too, which
		// shouldn't happen. Ignoring them here.
		return false;
	}

	const matchesSelector = [
		'BUTTON',
		'INPUT',
		'SELECT',
		'TEXTAREA',
		'FIELDSET',
	];
	if (eventName === 'click' && matchesSelector.indexOf(node.tagName) > -1) {
		return !(node.disabled || parent(node, 'fieldset[disabled]'));
	}
	return true;
}

/**
 * Returns true if the specified value is a NodeList or like one.
 * @param {?} val Variable to test.
 * @return {boolean} Whether variable is like a NodeList.
 */
export function isNodeListLike(val) {
	return (
		isDefAndNotNull(val) &&
		typeof val.length === 'number' &&
		typeof val.item === 'function'
	);
}

/**
 * Inserts node in document as last element.
 * @param {Element} node Element to remove children from.
 */
export function enterDocument(node) {
	node && append(document.body, node);
}

/**
 * Removes node from document.
 * @param {Element} node Element to remove children from.
 */
export function exitDocument(node) {
	if (node && node.parentNode) {
		node.parentNode.removeChild(node);
	}
}

/**
 * This is called when an event is triggered by a delegate listener. All
 * matching listeners of this event type from `target` to `currentTarget` will
 * be triggered.
 * @param {!Event} event The event payload.
 * @return {boolean} False if at least one of the triggered callbacks returns
 *     false, or true otherwise.
 * @private
 */
function handleDelegateEvent_(event) {
	normalizeDelegateEvent_(event);
	let ret = true;
	const container = event.currentTarget;
	const defFns = [];

	ret &= triggerDelegatedListeners_(container, event, defFns);
	ret &= triggerDefaultDelegatedListeners_(defFns, event);

	event.delegateTarget = null;
	event[LAST_CONTAINER] = container;
	return ret;
}

/**
 * Checks if the given element has the requested css class.
 * @param {!Element} element
 * @param {string} className
 * @return {boolean}
 */
export function hasClass(element, className) {
	if ('classList' in element) {
		return hasClassWithNative_(element, className);
	} else {
		return hasClassWithoutNative_(element, className);
	}
}

/**
 * Checks if the given element has the requested css class using classList.
 * @param {!Element} element
 * @param {string} className
 * @return {boolean}
 * @private
 */
function hasClassWithNative_(element, className) {
	return (
		className.indexOf(' ') === -1 && element.classList.contains(className)
	);
}

/**
 * Checks if the given element has the requested css class without using classList.
 * @param {!Element} element
 * @param {string} className
 * @return {boolean}
 * @private
 */
function hasClassWithoutNative_(element, className) {
	return (
		` ${element.className} `.indexOf(` ${className} `) >= 0 &&
		className.split(' ').length === 1
	);
}

/**
 * Checks if the given element is empty or not.
 * @param {!Element} element
 * @return {boolean}
 */
export function isEmpty(element) {
	return element.childNodes.length === 0;
}

/**
 * IE11 doesn't support Element.matches function, this method
 * check if an element matches a given selector.
 * @param {Element} element
 * @param {string} selector
 * @return {boolean}
 */
export function match(element, selector) {
	if (!element || element.nodeType !== 1) {
		return false;
	}

	const p = Element.prototype;
	const m =
		p.matches ||
		p.webkitMatchesSelector ||
		p.mozMatchesSelector ||
		p.msMatchesSelector ||
		p.oMatchesSelector;
	if (m) {
		return m.call(element, selector);
	}

	return matchFallback_(element, selector);
}

/**
 * Check if an element matches a given selector, using an internal implementation
 * instead of calling existing javascript functions.
 * @param {Element} element
 * @param {string} selector
 * @return {boolean}
 * @private
 */
function matchFallback_(element, selector) {
	const parentNode = element.parentNode;

	if (parentNode) {
		const nodes = parentNode.querySelectorAll(selector);

		for (let i = 0; i < nodes.length; ++i) {
			if (nodes[i] === element) {
				return true;
			}
		}
	}

	return false;
}

/**
 * Returns the next sibling of the given element that matches the specified
 * selector, or null if there is none.
 * @param {!Element} element
 * @param {?string} selector
 * @return {Element|null}
 */
export function next(element, selector) {
	do {
		element = element.nextSibling;
		if (element && match(element, selector)) {
			return element;
		}
	} while (element);
	return null;
}

/**
 * Normalizes the event payload for delegate listeners.
 * @param {!Event} event
 * @private
 */
function normalizeDelegateEvent_(event) {
	event.stopPropagation = stopPropagation_;
	event.stopImmediatePropagation = stopImmediatePropagation_;
}

/**
 * Listens to the specified event on the given DOM element. This function normalizes
 * DOM event payloads and functions so they'll work the same way on all supported
 * browsers.
 * @param {!Element|string} element The DOM element to listen to the event on, or
 *   a selector that should be delegated on the entire document.
 * @param {string} eventName The name of the event to listen to.
 * @param {!function(!Object)} callback Function to be called when the event is
 *   triggered. It will receive the normalized event object.
 * @param {boolean} capture Flag indicating if listener should be triggered
 *   during capture phase, instead of during the bubbling phase. Defaults to false.
 * @return {!DomEventHandle} Can be used to remove the listener.
 */
export function on(element, eventName, callback, capture) {
	if (isString(element)) {
		return delegate(document, eventName, element, callback);
	}
	const customConfig = customEvents[eventName];
	if (customConfig && customConfig.event) {
		eventName = customConfig.originalEvent;
		callback = customConfig.handler.bind(customConfig, callback);
	}
	element.addEventListener(eventName, callback, capture);
	return new DomEventHandle(element, eventName, callback, capture);
}

/**
 * IE11 doesn't support once event listener option, this method
 * Listens to the specified event on the given DOM element once. This
 * function normalizes DOM event payloads and functions so they'll work the
 * same way on all supported browsers.
 * @param {!Element} element The DOM element to listen to the event on.
 * @param {string} eventName The name of the event to listen to.
 * @param {!function(!Object)} callback Function to be called when the event
 *   is triggered. It will receive the normalized event object.
 * @return {!DomEventHandle} Can be used to remove the listener.
 */
export function once(element, eventName, callback) {
	const domEventHandle = on(element, eventName, function() {
		domEventHandle.removeListener();
		return callback.apply(this, arguments); // eslint-disable-line
	});
	return domEventHandle;
}

/**
 * Gets the first parent from the given element that matches the specified
 * selector, or null if none match.
 * @param {!Element} element
 * @param {string} selector
 * @return {Element}
 */
export function parent(element, selector) {
	return closest(element.parentNode, selector);
}

/**
 * IE11 doesn't support Element.prepend function, this method
 * inserts a node before first child of the parent. If child is a HTML string
 * it will be converted to document fragment before prepending it to the parent.
 * @param {!Element} parent The node to prepend to.
 * @param {!(Element|NodeList|string)} child The thing to prepend to the parent.
 * @return {!Element} The prepended child.
 */
export function prepend(parent, child) {
	if (isString(child)) {
		child = buildFragment(child);
	}

	if (!isNodeListLike(child) && !isDefAndNotNull(parent.firstChild)) {
		return append(parent, child);
	}

	if (isNodeListLike(child)) {
		const childArr = Array.prototype.slice.call(child);
		for (let i = childArr.length - 1; i >= 0; i--) {
			parent.insertBefore(childArr[i], parent.firstChild);
		}
	} else {
		parent.insertBefore(child, parent.firstChild);
	}

	return child;
}

/**
 * Registers a custom event.
 * @param {string} eventName The name of the custom event.
 * @param {!Object} customConfig An object with information about how the event
 *   should be handled.
 */
export function registerCustomEvent(eventName, customConfig) {
	customEvents[eventName] = customConfig;
}

/**
 * Removes all the child nodes on a DOM node.
 * @param {Element} node Element to remove children from.
 */
export function removeChildren(node) {
	let child;
	while ((child = node.firstChild)) {
		node.removeChild(child);
	}
}

/**
 * Removes the requested CSS classes from an element.
 * @param {!Element|!NodeList} elements The element or elements to remove CSS classes from.
 * @param {string} classes CSS classes to remove.
 */
export function removeClasses(elements, classes) {
	if (!isObject(elements) || !isString(classes)) {
		return;
	}

	if (!elements.length) {
		elements = [elements];
	}

	for (let i = 0; i < elements.length; i++) {
		if ('classList' in elements[i]) {
			removeClassesWithNative_(elements[i], classes);
		} else {
			removeClassesWithoutNative_(elements[i], classes);
		}
	}
}

/**
 * Removes the requested CSS classes from an element using classList.
 * @param {!Element} element The element to remove CSS classes from.
 * @param {string} classes CSS classes to remove.
 * @private
 */
function removeClassesWithNative_(element, classes) {
	classes.split(' ').forEach(function(className) {
		if (className) {
			element.classList.remove(className);
		}
	});
}

/**
 * IE11 doesn't support Element.classList, this method
 * removes the requested CSS classes from an element without using classList.
 * @param {!Element} element The element to remove CSS classes from.
 * @param {string} classes CSS classes to remove.
 * @private
 */
function removeClassesWithoutNative_(element, classes) {
	let elementClassName = ` ${element.className} `;

	classes = classes.split(' ');

	for (let i = 0; i < classes.length; i++) {
		elementClassName = elementClassName.replace(` ${classes[i]} `, ' ');
	}

	element.className = elementClassName.trim();
}

/**
 * Replaces the first element with the second.
 * @deprecated Use element1.parentNode.replaceChild(element2, element1)
 * directly instead of this method
 * @param {Element} element1
 * @param {Element} element2
 */
export function replace(element1, element2) {
	if (element1 && element2 && element1 !== element2 && element1.parentNode) {
		element1.parentNode.replaceChild(element2, element1);
	}
}

/**
 * The function that replaces `stopImmediatePropagation_` for events.
 * @private
 */
function stopImmediatePropagation_() {
	const event = this; // eslint-disable-line
	event.stopped = true;
	event.stoppedImmediate = true;
	Event.prototype.stopImmediatePropagation.call(event);
}

/**
 * The function that replaces `stopPropagation` for events.
 * @private
 */
function stopPropagation_() {
	const event = this; // eslint-disable-line
	event.stopped = true;
	Event.prototype.stopPropagation.call(event);
}

/**
 * Checks if the given element supports the given event type.
 * @param {!Element|string} element The DOM element or element tag name to check.
 * @param {string} eventName The name of the event to check.
 * @return {boolean}
 */
export function supportsEvent(element, eventName) {
	if (customEvents[eventName]) {
		return true;
	}

	if (isString(element)) {
		if (!elementsByTag_[element]) {
			elementsByTag_[element] = document.createElement(element);
		}
		element = elementsByTag_[element];
	}

	const tag = element.tagName;
	if (!supportCache_[tag] || !supportCache_[tag].hasOwnProperty(eventName)) {
		supportCache_[tag] = supportCache_[tag] || {};
		supportCache_[tag][eventName] = 'on' + eventName in element;
	}
	return supportCache_[tag][eventName];
}

/**
 * This triggers all default matched delegated listeners of a given event type.
 * @param {!Array} defFns Array to collect default listeners in, instead
 * @param {!Event} event
 * @return {boolean} False if at least one of the triggered callbacks returns
 *     false, or true otherwise.
 * @private
 */
function triggerDefaultDelegatedListeners_(defFns, event) {
	let ret = true;

	for (let i = 0; i < defFns.length && !event.defaultPrevented; i++) {
		event.delegateTarget = defFns[i].element;
		ret &= defFns[i].fn(event);
	}

	return ret;
}

/**
 * This triggers all matched delegated listeners of a given event type when its
 * delegated target is able to interact.
 * @param {!Element} container
 * @param {!Event} event
 * @param {!Array} defaultFns Array to collect default listeners in, instead
 *     of running them.
 * @return {boolean} False if at least one of the triggered callbacks returns
 *     false, or true otherwise.
 * @private
 */
function triggerDelegatedListeners_(container, event, defaultFns) {
	let ret = true;
	let currElement = event.target;
	const limit = container.parentNode;

	while (currElement && currElement !== limit && !event.stopped) {
		if (isAbleToInteractWith_(currElement, event.type, event)) {
			event.delegateTarget = currElement;
			ret &= triggerElementListeners_(currElement, event, defaultFns);
			ret &= triggerSelectorListeners_(
				container,
				currElement,
				event,
				defaultFns
			);
		}
		currElement = currElement.parentNode;
	}

	return ret;
}

/**
 * Converts the given argument to a DOM element. Strings are assumed to
 * be selectors, and so a matched element will be returned. If the arg
 * is already a DOM element it will be the return value.
 * @param {string|Element|Document} selectorOrElement
 * @return {Element} The converted element, or null if none was found.
 */
export function toElement(selectorOrElement) {
	if (
		isElement(selectorOrElement) ||
		isDocument(selectorOrElement) ||
		isDocumentFragment(selectorOrElement)
	) {
		return selectorOrElement;
	} else if (isString(selectorOrElement)) {
		return document.querySelector(selectorOrElement);
	} else {
		return null;
	}
}

/**
 * Adds or removes one or more classes from an element. If any of the classes
 * is present, it will be removed from the element, or added otherwise.
 * @param {!Element} element The element which classes will be toggled.
 * @param {string} classes The classes which have to added or removed from the element.
 */
export function toggleClasses(element, classes) {
	if (!isObject(element) || !isString(classes)) {
		return;
	}

	if ('classList' in element) {
		toggleClassesWithNative_(element, classes);
	} else {
		toggleClassesWithoutNative_(element, classes);
	}
}

/**
 * Adds or removes one or more classes from an element using classList.
 * If any of the classes is present, it will be removed from the element,
 * or added otherwise.
 * @param {!Element} element The element which classes will be toggled.
 * @param {string} classes The classes which have to added or removed from the element.
 * @private
 */
function toggleClassesWithNative_(element, classes) {
	classes.split(' ').forEach(function(className) {
		element.classList.toggle(className);
	});
}

/**
 * IE11 doesn't support Element.classList, this method
 * adds or removes one or more classes from an element without using classList.
 * If any of the classes is present, it will be removed from the element,
 * or added otherwise.
 * @param {!Element} element The element which classes will be toggled.
 * @param {string} classes The classes which have to added or removed from the element.
 * @private
 */
function toggleClassesWithoutNative_(element, classes) {
	let elementClassName = ` ${element.className} `;

	classes = classes.split(' ');

	for (let i = 0; i < classes.length; i++) {
		const className = ` ${classes[i]} `;
		const classIndex = elementClassName.indexOf(className);

		if (classIndex === -1) {
			elementClassName = `${elementClassName}${classes[i]} `;
		} else {
			const before = elementClassName.substring(0, classIndex);
			const after = elementClassName.substring(
				classIndex + className.length
			); // eslint-disable-line
			elementClassName = `${before} ${after}`;
		}
	}

	element.className = elementClassName.trim();
}

/**
 * Triggers all listeners for the given event type that are stored in the
 * specified element.
 * @param {!Element} element
 * @param {!Event} event
 * @param {!Array} defaultFns Array to collect default listeners in, instead
 *     of running them.
 * @return {boolean} False if at least one of the triggered callbacks returns
 *     false, or true otherwise.
 * @private
 */
function triggerElementListeners_(element, event, defaultFns) {
	const lastContainer = event[LAST_CONTAINER];
	if (!isDef(lastContainer) || !contains(lastContainer, element)) {
		const listeners = domData.get(element, 'listeners', {})[event.type];
		return triggerListeners_(listeners, event, element, defaultFns);
	}
	return true;
}

/**
 * Triggers the specified event on the given element.
 * NOTE: This should mostly be used for testing, not on real code.
 * @param {!Element} element The node that should trigger the event.
 * @param {string} eventName The name of the event to be triggred.
 * @param {Object=} eventObj An object with data that should be on the
 *   triggered event's payload.
 */
export function triggerEvent(element, eventName, eventObj) {
	if (isAbleToInteractWith_(element, eventName, eventObj)) {
		const payload = document.createEvent('HTMLEvents');
		payload.initEvent(eventName, true, true);
		object.mixin(payload, eventObj);
		element.dispatchEvent(payload);
	}
}

/**
 * Triggers the given listeners array.
 * @param {Array<!function()>} listeners
 * @param {!Event} event
 * @param {!Element} element
 * @param {!Array} defaultFns Array to collect default listeners in, instead
 *     of running them.
 * @return {boolean} False if at least one of the triggered callbacks returns
 *     false, or true otherwise.
 * @private
 */
function triggerListeners_(listeners, event, element, defaultFns) {
	let ret = true;
	listeners = listeners || [];
	for (let i = 0; i < listeners.length && !event.stoppedImmediate; i++) {
		if (listeners[i].defaultListener_) {
			defaultFns.push({
				element,
				fn: listeners[i],
			});
		} else {
			ret &= listeners[i](event);
		}
	}
	return ret;
}

/**
 * Triggers all selector listeners for the given event.
 * @param {!Element} container
 * @param {!Element} element
 * @param {!Event} event
 * @param {!Array} defaultFns Array to collect default listeners in, instead
 *     of running them.
 * @return {boolean} False if at least one of the triggered callbacks returns
 *     false, or true otherwise.
 * @private
 */
function triggerSelectorListeners_(container, element, event, defaultFns) {
	let ret = true;
	const data = domData.get(container, 'delegating', {});
	const map = data[event.type].selectors;
	const selectors = Object.keys(map);
	for (let i = 0; i < selectors.length && !event.stoppedImmediate; i++) {
		if (match(element, selectors[i])) {
			const listeners = map[selectors[i]];
			ret &= triggerListeners_(listeners, event, element, defaultFns);
		}
	}
	return ret;
}
