diff options
author | Deathamns <deathamns@gmail.com> | 2014-10-17 21:44:19 +0200 |
---|---|---|
committer | Deathamns <deathamns@gmail.com> | 2014-11-09 17:39:12 +0100 |
commit | 5b79bf353647a4dad9d4968d0f246582744f07bc (patch) | |
tree | 06f045f4dfbd188a8f1217c491b185f0d41d6d50 /src/js/udom.js | |
parent | 96c4e2e2565ffbd7d413ed7721d9610772b03859 (diff) | |
download | uBlock-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.js | 702 |
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; + +})(); |