aboutsummaryrefslogtreecommitdiffstats
path: root/src/js/udom.js
diff options
context:
space:
mode:
authorDeathamns <deathamns@gmail.com>2014-10-17 21:44:19 +0200
committerDeathamns <deathamns@gmail.com>2014-11-09 17:39:12 +0100
commit5b79bf353647a4dad9d4968d0f246582744f07bc (patch)
tree06f045f4dfbd188a8f1217c491b185f0d41d6d50 /src/js/udom.js
parent96c4e2e2565ffbd7d413ed7721d9610772b03859 (diff)
downloaduBlock-5b79bf353647a4dad9d4968d0f246582744f07bc.zip
uBlock-5b79bf353647a4dad9d4968d0f246582744f07bc.tar.gz
uBlock-5b79bf353647a4dad9d4968d0f246582744f07bc.tar.bz2
Work on vendor API abstraction, and near complete Safari support
Diffstat (limited to 'src/js/udom.js')
-rw-r--r--src/js/udom.js702
1 files changed, 702 insertions, 0 deletions
diff --git a/src/js/udom.js b/src/js/udom.js
new file mode 100644
index 0000000..4e99257
--- /dev/null
+++ b/src/js/udom.js
@@ -0,0 +1,702 @@
+/*******************************************************************************
+
+ µBlock - a Chromium 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/gorhill/uBlock
+*/
+
+/******************************************************************************/
+/******************************************************************************/
+
+// 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() {
+
+/******************************************************************************/
+
+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);
+ p.innerHTML = 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.length = function() {
+ return this.nodes.length;
+};
+
+/******************************************************************************/
+
+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.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.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-- ) {
+ this.nodes[i].innerHTML = 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 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-- ) {
+ this.nodes[i].addEventListener(etype, callback, selector !== undefined);
+ }
+ 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;
+};
+
+/******************************************************************************/
+
+return DOMListFactory;
+
+})();