/**
* @module dom-utils
*/
/**
* Gets siblings that match selector and self _in DOM order_.
*
* @static
* @param {Node} element - Target element.
* @param {string} [selector] - A CSS selector for siblings (not for self).
* @return {Array<Node>} Array of sibling nodes plus target element.
*/
function getSiblingElementsAndSelf( element, selector ) {
return _getSiblingElements( element, selector, true );
}
/**
* Gets siblings that match selector _in DOM order_.
*
* @static
* @param {Node} element - Target element.
* @param {string} [selector] - A CSS selector.
* @return {Array<Node>} Array of sibling nodes.
*/
function getSiblingElements( element, selector ) {
return _getSiblingElements( element, selector );
}
/**
* Returns first sibling element (in DOM order) that optionally matches the provided selector.
*
* @param {Node} element - Target element.
* @param {string} [selector] - A CSS selector.
* @return {Node} First sibling element in DOM order
*/
function getSiblingElement( element, selector = '*' ){
let found;
let current = element.parentElement.firstElementChild;
while ( current && !found ) {
if ( current !== element && current.matches( selector ) ) {
found = current;
}
current = current.nextElementSibling;
}
return found;
}
/**
* Gets siblings that match selector _in DOM order_.
*
* @param {Node} element - Target element.
* @param {string} [selector] - A CSS selector.
* @param {boolean} [includeSelf] - Whether to include self.
* @return {Array<Node>} Array of sibling nodes.
*/
function _getSiblingElements( element, selector = '*', includeSelf = false ) {
const results = [];
let current = element.parentElement.firstElementChild;
while ( current ) {
if ( ( current === element && includeSelf ) || ( current !== element && current.matches( selector ) ) ){
results.push( current );
}
current = current.nextElementSibling;
}
return results;
}
/**
* Gets ancestors that match selector _in DOM order_.
*
* @static
* @param {Node} element - Target element.
* @param {string} [filterSelector] - A CSS selector.
* @param {string} [endSelector] - A CSS selector indicating where to stop. It will include this element if matched by the filter.
* @return {Array<Node>} Array of ancestors.
*/
function getAncestors( element, filterSelector = '*', endSelector ) {
const ancestors = [];
let parent = element.parentElement;
while ( parent ) {
if ( parent.matches( filterSelector ) ) {
// document order
ancestors.unshift( parent );
}
parent = endSelector && parent.matches( endSelector ) ? null : parent.parentElement;
}
return ancestors;
}
/**
* Gets closest ancestor that match selector until the end selector.
*
* @static
* @param {Node} element - Target element.
* @param {string} filterSelector - A CSS selector.
* @param {string} [endSelector] - A CSS selector indicating where to stop. It will include this element if matched by the filter.
* @return {Node} Closest ancestor.
*/
function closestAncestorUntil( element, filterSelector = '*', endSelector ) {
let parent = element.parentElement;
let found = null;
while ( parent && !found ) {
if ( parent.matches( filterSelector ) ) {
found = parent;
}
parent = endSelector && parent.matches( endSelector ) ? null : parent.parentElement;
}
return found;
}
/**
* Gets child elements, that (optionally) match a selector.
*
* @param {Node} element - Target element.
* @param {string} selector - A CSS selector.
* @return {Array<Node>} Array of child elements.
*/
function getChildren( element, selector = '*' ) {
return [ ...element.children ]
.filter( el => el.matches( selector ) );
}
/**
* Gets first child element, that (optionally) matches a selector.
*
* @param {Node} element - Target element.
* @param {string} selector - A CSS selector.
* @return {Node} - First child element.
*/
function getChild( element, selector = '*' ) {
return [ ...element.children ]
.find( el => el.matches( selector ) );
}
/**
* Removes all children elements.
*
* @static
* @param {Node} element - Target element.
* @return {undefined}
*/
function empty( element ) {
[ ...element.children ].forEach( el => el.remove() );
}
/**
* @param {Element} el - Target node
* @return {boolean} Whether previous sibling has same node name
*/
function hasPreviousSiblingElementSameName( el ) {
let found = false;
const nodeName = el.nodeName;
el = el.previousSibling;
while ( el ) {
// Ignore any sibling text and comment nodes (e.g. whitespace with a newline character)
// also deal with repeats that have non-repeat siblings in between them, event though that would be a bug.
if ( el.nodeName && el.nodeName === nodeName ) {
found = true;
break;
}
el = el.previousSibling;
}
return found;
}
/**
* @param {Element} node - Target node
* @param {string} content - Text content to look for
* @return {boolean} Whether previous comment sibling has given text content
*/
function hasPreviousCommentSiblingWithContent( node, content ) {
let found = false;
node = node.previousSibling;
while ( node ) {
if ( node.nodeType === Node.COMMENT_NODE && node.textContent === content ) {
found = true;
break;
}
node = node.previousSibling;
}
return found;
}
/**
* Creates an XPath from a node
*
* @param {Element} node - XML node
* @param {string} [rootNodeName] - Defaults to #document
* @param {boolean} [includePosition] - Whether or not to include the positions `/path/to/repeat[2]/node`
* @return {string} XPath
*/
function getXPath( node, rootNodeName = '#document', includePosition = false ) {
let index;
const steps = [];
let position = '';
if ( !node || node.nodeType !== 1 ) {
return null;
}
const nodeName = node.nodeName;
let parent = node.parentElement;
let parentName = parent ? parent.nodeName : null;
if ( includePosition ) {
index = getRepeatIndex( node );
if ( index > 0 ) {
position = `[${index + 1}]`;
}
}
steps.push( nodeName + position );
while ( parent && parentName !== rootNodeName && parentName !== '#document' ) {
if ( includePosition ) {
index = getRepeatIndex( parent );
position = ( index > 0 ) ? `[${index + 1}]` : '';
}
steps.push( parentName + position );
parent = parent.parentElement;
parentName = parent ? parent.nodeName : null;
}
return `/${steps.reverse().join( '/' )}`;
}
/**
* Obtains the index of a repeat instance within its own series.
*
* @param {Element} node - XML node
* @return {number} index
*/
function getRepeatIndex( node ) {
let index = 0;
const nodeName = node.nodeName;
let prevSibling = node.previousSibling;
while ( prevSibling ) {
// ignore any sibling text and comment nodes (e.g. whitespace with a newline character)
if ( prevSibling.nodeName && prevSibling.nodeName === nodeName ) {
index++;
}
prevSibling = prevSibling.previousSibling;
}
return index;
}
/**
* Adapted from https://stackoverflow.com/a/46522991/3071529
*
* A storage solution aimed at replacing jQuerys data function.
* Implementation Note: Elements are stored in a (WeakMap)[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap].
* This makes sure the data is garbage collected when the node is removed.
*
* @namespace
*/
const elementDataStore = {
/**
* @type {WeakMap}
*/
_storage: new WeakMap(),
/**
* Adds object to element storage. Ensures that element storage exist.
*
* @param {Node} element - target element
* @param {string} key - name of the stored data
* @param {object} obj - stored data
*/
put: function( element, key, obj ) {
if ( !this._storage.has( element ) ) {
this._storage.set( element, new Map() );
}
this._storage.get( element ).set( key, obj );
},
/**
* Return object from element storage.
*
* @param {Node} element - target element
* @param {string} key - name of the stored data
* @return {object} stored data object
*/
get: function( element, key ) {
const item = this._storage.get( element );
return item ? item.get( key ) : item;
},
/**
* Checkes whether element has given storage item.
*
* @param {Node} element - target element
* @param {string} key - name of the stored data
* @return {boolean} whether data is present
*/
has: function( element, key ) {
const item = this._storage.get( element );
return item && item.has( key );
},
/**
* Removes item from element storage. Removes element storage if empty.
*
* @param {Node} element - target element
* @param {string} key - name of the stored data
* @return {object} removed data object
*/
remove: function( element, key ) {
var ret = this._storage.get( element ).delete( key );
if ( !this._storage.get( key ).size === 0 ) {
this._storage.delete( element );
}
return ret;
}
};
class MutationsTracker{
constructor( el = document.documentElement ){
let currentMutations = 0;
let previousMutations = currentMutations;
this.classChanges = new WeakMap();
this.quiet = true;
const mutationObserver = new MutationObserver( mutations => {
mutations.forEach( mutation => {
currentMutations++;
if ( mutation.type === 'attributes' && mutation.attributeName === 'class' ){
const trackedClasses = this.classChanges.get( mutation.target ) || [];
trackedClasses.forEach( obj => {
if( mutation.target.classList.contains( obj.className ) ){
obj.completed = true;
this.classChanges.set( mutation.target, trackedClasses );
}
} );
}
} );
} );
mutationObserver.observe( el, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
} );
const checkInterval = setInterval( () => {
if ( previousMutations === currentMutations ){
this.quiet = true;
mutationObserver.disconnect();
clearInterval( checkInterval );
} else {
this.quiet = false;
previousMutations = currentMutations;
}
}, 100 );
}
_resolveWhenTrue( fn ){
if ( typeof fn !== 'function' ){
return Promise.reject();
}
return new Promise( resolve => {
const checkInterval = setInterval( () => {
if ( fn.call( this ) ){
clearInterval( checkInterval );
resolve();
}
}, 10 );
} );
}
waitForClassChange( element, className ){
const trackedClasses = this.classChanges.get( element ) || [];
if ( !trackedClasses.some( obj => obj.className === className ) ){
trackedClasses.push( { className } );
this.classChanges.set( element, trackedClasses );
}
return this._resolveWhenTrue( () => this.classChanges.get( element ).find( obj => obj.className === className ).completed );
}
waitForQuietness(){
return this._resolveWhenTrue( () => this.quiet );
}
}
export {
/**
* @static
* @see {@link module:dom-utils~elementDataStore|elementDataStore}
*/
elementDataStore,
getSiblingElementsAndSelf,
getSiblingElements,
getSiblingElement,
getAncestors,
getChildren,
getChild,
getRepeatIndex,
getXPath,
hasPreviousCommentSiblingWithContent,
hasPreviousSiblingElementSameName,
closestAncestorUntil,
empty,
MutationsTracker
};