/******************************************************************************* µBlock - a browser extension to block requests. Copyright (C) 2014 Raymond Hill This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see {http://www.gnu.org/licenses/}. Home: https://github.com/chrisaljoudi/uBlock */ /* global DOMTokenList */ /* exported uDom */ /******************************************************************************/ // It's just a silly, minimalist DOM framework: this allows me to not rely // on jQuery. jQuery contains way too much stuff than I need, and as per // Opera rules, I am not allowed to use a cut-down version of jQuery. So // the code here does *only* what I need, and nothing more, and with a lot // of assumption on passed parameters, etc. I grow it on a per-need-basis only. var uDom = (function() { 'use strict'; /******************************************************************************/ var DOMList = function() { this.nodes = []; }; /******************************************************************************/ Object.defineProperty( DOMList.prototype, 'length', { get: function() { return this.nodes.length; } } ); /******************************************************************************/ var DOMListFactory = function(selector, context) { var r = new DOMList(); if ( typeof selector === 'string' ) { selector = selector.trim(); if ( selector.charAt(0) === '<' ) { return addHTMLToList(r, selector); } if ( selector !== '' ) { return addSelectorToList(r, selector, context); } } if ( selector instanceof Node ) { return addNodeToList(r, selector); } if ( selector instanceof NodeList ) { return addNodeListToList(r, selector); } if ( selector instanceof DOMList ) { return addListToList(r, selector); } return r; }; /******************************************************************************/ DOMListFactory.onLoad = function(callback) { window.addEventListener('load', callback); }; /******************************************************************************/ var addNodeToList = function(list, node) { if ( node ) { list.nodes.push(node); } return list; }; /******************************************************************************/ var addNodeListToList = function(list, nodelist) { if ( nodelist ) { var n = nodelist.length; for ( var i = 0; i < n; i++ ) { list.nodes.push(nodelist[i]); } } return list; }; /******************************************************************************/ var addListToList = function(list, other) { list.nodes = list.nodes.concat(other.nodes); return list; }; /******************************************************************************/ var addSelectorToList = function(list, selector, context) { var p = context || document; var r = p.querySelectorAll(selector); var n = r.length; for ( var i = 0; i < n; i++ ) { list.nodes.push(r[i]); } return list; }; /******************************************************************************/ var pTagOfChildTag = { 'tr': 'table', 'option': 'select' }; // TODO: documentFragment var addHTMLToList = function(list, html) { var matches = html.match(/^<([a-z]+)/); if ( !matches || matches.length !== 2 ) { return this; } var cTag = matches[1]; var pTag = pTagOfChildTag[cTag] || 'div'; var p = document.createElement(pTag); vAPI.insertHTML(p, html); // Find real parent var c = p.querySelector(cTag); p = c.parentNode; while ( p.firstChild ) { list.nodes.push(p.removeChild(p.firstChild)); } return list; }; /******************************************************************************/ var isChildOf = function(child, parent) { return child !== null && parent !== null && child.parentNode === parent; }; /******************************************************************************/ var isDescendantOf = function(descendant, ancestor) { while ( descendant.parentNode !== null ) { if ( descendant.parentNode === ancestor ) { return true; } descendant = descendant.parentNode; } return false; }; /******************************************************************************/ var nodeInNodeList = function(node, nodeList) { var i = nodeList.length; while ( i-- ) { if ( nodeList[i] === node ) { return true; } } return false; }; /******************************************************************************/ var doesMatchSelector = function(node, selector) { if ( !node ) { return false; } if ( node.nodeType !== 1 ) { return false; } if ( selector === undefined ) { return true; } var parentNode = node.parentNode; if ( !parentNode || !parentNode.setAttribute ) { return false; } var doesMatch = false; parentNode.setAttribute('uDom-32kXc6xEZA7o73AMB8vLbLct1RZOkeoO', ''); var grandpaNode = parentNode.parentNode || document; var nl = grandpaNode.querySelectorAll('[uDom-32kXc6xEZA7o73AMB8vLbLct1RZOkeoO] > ' + selector); var i = nl.length; while ( doesMatch === false && i-- ) { doesMatch = nl[i] === node; } parentNode.removeAttribute('uDom-32kXc6xEZA7o73AMB8vLbLct1RZOkeoO'); return doesMatch; }; /******************************************************************************/ DOMList.prototype.nodeAt = function(i) { return this.nodes[i]; }; DOMList.prototype.at = function(i) { return addNodeToList(new DOMList(), this.nodes[i]); }; /******************************************************************************/ DOMList.prototype.toArray = function() { return this.nodes.slice(); }; /******************************************************************************/ DOMList.prototype.pop = function() { return addNodeToList(new DOMList(), this.nodes.pop()); }; /******************************************************************************/ DOMList.prototype.forEach = function(fn) { var n = this.nodes.length; for ( var i = 0; i < n; i++ ) { fn(this.at(i), i); } return this; }; /******************************************************************************/ DOMList.prototype.subset = function(i, l) { var r = new DOMList(); var n = l !== undefined ? l : this.nodes.length; var j = Math.min(i + n, this.nodes.length); if ( i < j ) { r.nodes = this.nodes.slice(i, j); } return r; }; /******************************************************************************/ DOMList.prototype.first = function() { return this.subset(0, 1); }; /******************************************************************************/ DOMList.prototype.next = function(selector) { var r = new DOMList(); var n = this.nodes.length; var node; for ( var i = 0; i < n; i++ ) { node = this.nodes[i]; while ( node.nextSibling !== null ) { node = node.nextSibling; if ( node.nodeType !== 1 ) { continue; } if ( doesMatchSelector(node, selector) === false ) { continue; } addNodeToList(r, node); break; } } return r; }; /******************************************************************************/ DOMList.prototype.parent = function() { var r = new DOMList(); if ( this.nodes.length ) { addNodeToList(r, this.nodes[0].parentNode); } return r; }; /******************************************************************************/ DOMList.prototype.filter = function(filter) { var r = new DOMList(); var filterFunc; if ( typeof filter === 'string' ) { filterFunc = function() { return doesMatchSelector(this, filter); }; } else if ( typeof filter === 'function' ) { filterFunc = filter; } else { filterFunc = function(){ return true; }; } var n = this.nodes.length; var node; for ( var i = 0; i < n; i++ ) { node = this.nodes[i]; if ( filterFunc.apply(node) ) { addNodeToList(r, node); } } return r; }; /******************************************************************************/ // TODO: Avoid possible duplicates DOMList.prototype.ancestors = function(selector) { var r = new DOMList(); var n = this.nodes.length; var node; for ( var i = 0; i < n; i++ ) { node = this.nodes[i].parentNode; while ( node ) { if ( doesMatchSelector(node, selector) ) { addNodeToList(r, node); } node = node.parentNode; } } return r; }; /******************************************************************************/ DOMList.prototype.descendants = function(selector) { var r = new DOMList(); var n = this.nodes.length; var nl; for ( var i = 0; i < n; i++ ) { nl = this.nodes[i].querySelectorAll(selector); addNodeListToList(r, nl); } return r; }; /******************************************************************************/ DOMList.prototype.contents = function() { var r = new DOMList(); var cnodes, cn, ci; var n = this.nodes.length; for ( var i = 0; i < n; i++ ) { cnodes = this.nodes[i].childNodes; cn = cnodes.length; for ( ci = 0; ci < cn; ci++ ) { addNodeToList(r, cnodes.item(ci)); } } return r; }; /******************************************************************************/ DOMList.prototype.remove = function() { var cn, p; var i = this.nodes.length; while ( i-- ) { cn = this.nodes[i]; if ( p = cn.parentNode ) { p.removeChild(cn); } } return this; }; DOMList.prototype.detach = DOMList.prototype.remove; /******************************************************************************/ DOMList.prototype.empty = function() { var node; var i = this.nodes.length; while ( i-- ) { node = this.nodes[i]; while ( node.firstChild ) { node.removeChild(node.firstChild); } } return this; }; /******************************************************************************/ DOMList.prototype.append = function(selector, context) { var p = this.nodes[0]; if ( p ) { var c = DOMListFactory(selector, context); var n = c.nodes.length; for ( var i = 0; i < n; i++ ) { p.appendChild(c.nodes[i]); } } return this; }; /******************************************************************************/ DOMList.prototype.prepend = function(selector, context) { var p = this.nodes[0]; if ( p ) { var c = DOMListFactory(selector, context); var i = c.nodes.length; while ( i-- ) { p.insertBefore(c.nodes[i], p.firstChild); } } return this; }; /******************************************************************************/ DOMList.prototype.appendTo = function(selector, context) { var p = selector instanceof DOMListFactory ? selector : DOMListFactory(selector, context); var n = p.length; for ( var i = 0; i < n; i++ ) { p.nodes[0].appendChild(this.nodes[i]); } return this; }; /******************************************************************************/ DOMList.prototype.insertAfter = function(selector, context) { if ( this.nodes.length === 0 ) { return this; } var p = this.nodes[0].parentNode; if ( !p ) { return this; } var c = DOMListFactory(selector, context); var n = c.nodes.length; for ( var i = 0; i < n; i++ ) { p.appendChild(c.nodes[i]); } return this; }; /******************************************************************************/ DOMList.prototype.insertBefore = function(selector, context) { if ( this.nodes.length === 0 ) { return this; } var referenceNodes = DOMListFactory(selector, context); if ( referenceNodes.nodes.length === 0 ) { return this; } var referenceNode = referenceNodes.nodes[0]; var parentNode = referenceNode.parentNode; if ( !parentNode ) { return this; } var n = this.nodes.length; for ( var i = 0; i < n; i++ ) { parentNode.insertBefore(this.nodes[i], referenceNode); } return this; }; /******************************************************************************/ DOMList.prototype.clone = function(notDeep) { var r = new DOMList(); var n = this.nodes.length; for ( var i = 0; i < n; i++ ) { addNodeToList(r, this.nodes[i].cloneNode(!notDeep)); } return r; }; /******************************************************************************/ DOMList.prototype.nthOfType = function() { if ( this.nodes.length === 0 ) { return 0; } var node = this.nodes[0]; var tagName = node.tagName; var i = 1; while ( node.previousElementSibling !== null ) { node = node.previousElementSibling; if ( typeof node.tagName !== 'string' ) { continue; } if ( node.tagName !== tagName ) { continue; } i++; } return i; }; /******************************************************************************/ DOMList.prototype.attr = function(attr, value) { var i = this.nodes.length; if ( value === undefined && typeof attr !== 'object' ) { return i ? this.nodes[0].getAttribute(attr) : undefined; } if ( typeof attr === 'object' ) { var attrNames = Object.keys(attr); var node, j, attrName; while ( i-- ) { node = this.nodes[i]; j = attrNames.length; while ( j-- ) { attrName = attrNames[j]; node.setAttribute(attrName, attr[attrName]); } } } else { while ( i-- ) { this.nodes[i].setAttribute(attr, value); } } return this; }; /******************************************************************************/ DOMList.prototype.prop = function(prop, value) { var i = this.nodes.length; if ( value === undefined ) { return i !== 0 ? this.nodes[0][prop] : undefined; } while ( i-- ) { this.nodes[i][prop] = value; } return this; }; /******************************************************************************/ DOMList.prototype.css = function(prop, value) { var i = this.nodes.length; if ( value === undefined ) { return i ? this.nodes[0].style[prop] : undefined; } while ( i-- ) { this.nodes[i].style[prop] = value; } return this; }; /******************************************************************************/ DOMList.prototype.val = function(value) { return this.prop('value', value); }; /******************************************************************************/ DOMList.prototype.html = function(html) { var i = this.nodes.length; if ( html === undefined ) { return i ? this.nodes[0].innerHTML : ''; } while ( i-- ) { vAPI.insertHTML(this.nodes[i], html); } return this; }; /******************************************************************************/ DOMList.prototype.text = function(text) { var i = this.nodes.length; if ( text === undefined ) { return i ? this.nodes[0].textContent : ''; } while ( i-- ) { this.nodes[i].textContent = text; } return this; }; /******************************************************************************/ var toggleClass = function(node, className, targetState) { var tokenList = node.classList; if ( tokenList instanceof DOMTokenList === false ) { return; } var currentState = tokenList.contains(className); var newState = targetState; if ( newState === undefined ) { newState = !currentState; } if ( newState === currentState ) { return; } tokenList.toggle(className, newState); }; /******************************************************************************/ DOMList.prototype.hasClass = function(className) { if ( !this.nodes.length ) { return false; } var tokenList = this.nodes[0].classList; return tokenList instanceof DOMTokenList && tokenList.contains(className); }; DOMList.prototype.hasClassName = DOMList.prototype.hasClass; DOMList.prototype.addClass = function(className) { return this.toggleClass(className, true); }; DOMList.prototype.removeClass = function(className) { if ( className !== undefined ) { return this.toggleClass(className, false); } var i = this.nodes.length; while ( i-- ) { this.nodes[i].className = ''; } return this; }; /******************************************************************************/ DOMList.prototype.toggleClass = function(className, targetState) { if ( className.indexOf(' ') !== -1 ) { return this.toggleClasses(className, targetState); } var i = this.nodes.length; while ( i-- ) { toggleClass(this.nodes[i], className, targetState); } return this; }; /******************************************************************************/ DOMList.prototype.toggleClasses = function(classNames, targetState) { var tokens = classNames.split(/\s+/); var i = this.nodes.length; var node, j; while ( i-- ) { node = this.nodes[i]; j = tokens.length; while ( j-- ) { toggleClass(node, tokens[j], targetState); } } return this; }; /******************************************************************************/ var listenerEntries = []; var ListenerEntry = function(target, type, capture, callback) { this.target = target; this.type = type; this.capture = capture; this.callback = callback; target.addEventListener(type, callback, capture); }; ListenerEntry.prototype.dispose = function() { this.target.removeEventListener(this.type, this.callback, this.capture); this.target = null; this.callback = null; }; /******************************************************************************/ var makeEventHandler = function(selector, callback) { return function(event) { var dispatcher = event.currentTarget; if ( !dispatcher || typeof dispatcher.querySelectorAll !== 'function' ) { return; } var receiver = event.target; if ( nodeInNodeList(receiver, dispatcher.querySelectorAll(selector)) ) { callback.call(receiver, event); } }; }; DOMList.prototype.on = function(etype, selector, callback) { if ( typeof selector === 'function' ) { callback = selector; selector = undefined; } else { callback = makeEventHandler(selector, callback); } var i = this.nodes.length; while ( i-- ) { listenerEntries.push(new ListenerEntry(this.nodes[i], etype, selector !== undefined, callback)); } return this; }; /******************************************************************************/ // TODO: Won't work for delegated handlers. Need to figure // what needs to be done. DOMList.prototype.off = function(evtype, callback) { var i = this.nodes.length; while ( i-- ) { this.nodes[i].removeEventListener(evtype, callback); } return this; }; /******************************************************************************/ DOMList.prototype.trigger = function(etype) { var ev = new CustomEvent(etype); var i = this.nodes.length; while ( i-- ) { this.nodes[i].dispatchEvent(ev); } return this; }; /******************************************************************************/ // Cleanup var onBeforeUnload = function() { var entry; while ( entry = listenerEntries.pop() ) { entry.dispose(); } window.removeEventListener('beforeunload', onBeforeUnload); }; window.addEventListener('beforeunload', onBeforeUnload); /******************************************************************************/ return DOMListFactory; })();