diff --git a/src/js/popup.js b/src/js/popup.js index a3bee2f..49fe070 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -496,19 +496,18 @@ function getCollapseState(domain) { } function toggleCollapseState(element) { - element = $(element); - if ( element.parents('#matHead.collapsible').length > 0 ) { - toggleMainCollapseState(element); + var el = uDom(element); + if ( el.ancestors('#matHead.collapsible').length() > 0 ) { + toggleMainCollapseState(el); } else { - toggleSpecificCollapseState(element); + toggleSpecificCollapseState(el); } } function toggleMainCollapseState(element) { - var matHead = element.parents('#matHead.collapsible') - .toggleClass('collapsed'); - var collapsed = matHead.hasClass('collapsed'); - $('#matList .matSection.collapsible').toggleClass('collapsed', collapsed); + var matHead = element.ancestors('#matHead.collapsible').toggleClass('collapsed'); + var collapsed = matHead.hasClassName('collapsed'); + uDom('#matList .matSection.collapsible').toggleClass('collapsed', collapsed); setUserSetting('popupCollapseDomains', collapsed); var specificCollapseStates = getUserSetting('popupCollapseSpecificDomains') || {}; @@ -527,10 +526,9 @@ function toggleMainCollapseState(element) { function toggleSpecificCollapseState(element) { // Remember collapse state forever, but only if it is different // from main collapse switch. - var section = element.parents('.matSection.collapsible') - .toggleClass('collapsed'); + var section = element.ancestors('.matSection.collapsible').toggleClass('collapsed'); var domain = section.prop('domain'); - var collapsed = section.hasClass('collapsed'); + var collapsed = section.hasClassName('collapsed'); var mainCollapseState = getUserSetting('popupCollapseDomains'); var specificCollapseStates = getUserSetting('popupCollapseSpecificDomains') || {}; if ( collapsed !== mainCollapseState ) { @@ -759,58 +757,90 @@ var createMatrixRow = function() { /******************************************************************************/ function renderMatrixHeaderRow() { - var matHead = $('#matHead.collapsible'); + var matHead = uDom('#matHead.collapsible'); matHead.toggleClass('collapsed', getUserSetting('popupCollapseDomains')); var cells = matHead.find('.matCell'); - $(cells[0]).prop({reqType: '*', hostname: '*'}).addClass(getCellClass('*', '*')); - $(cells[1]).prop({reqType: 'cookie', hostname: '*'}).addClass(getCellClass('*', 'cookie')); - $(cells[2]).prop({reqType: 'css', hostname: '*'}).addClass(getCellClass('*', 'css')); - $(cells[3]).prop({reqType: 'image', hostname: '*'}).addClass(getCellClass('*', 'image')); - $(cells[4]).prop({reqType: 'plugin', hostname: '*'}).addClass(getCellClass('*', 'plugin')); - $(cells[5]).prop({reqType: 'script', hostname: '*'}).addClass(getCellClass('*', 'script')); - $(cells[6]).prop({reqType: 'xhr', hostname: '*'}).addClass(getCellClass('*', 'xhr')); - $(cells[7]).prop({reqType: 'frame', hostname: '*'}).addClass(getCellClass('*', 'frame')); - $(cells[8]).prop({reqType: 'other', hostname: '*'}).addClass(getCellClass('*', 'other')); - $('#matHead .matRow').css('display', ''); + uDom(cells.node(0)) + .prop('reqType', '*') + .prop('hostname', '*') + .addClass(getCellClass('*', '*')); + uDom(cells.node(1)) + .prop('reqType', 'cookie') + .prop('hostname', '*') + .addClass(getCellClass('*', 'cookie')); + uDom(cells.node(2)) + .prop('reqType', 'css') + .prop('hostname', '*') + .addClass(getCellClass('*', 'css')); + uDom(cells.node(3)) + .prop('reqType', 'image') + .prop('hostname', '*') + .addClass(getCellClass('*', 'image')); + uDom(cells.node(4)) + .prop('reqType', 'plugin') + .prop('hostname', '*') + .addClass(getCellClass('*', 'plugin')); + uDom(cells.node(5)) + .prop('reqType', 'script') + .prop('hostname', '*') + .addClass(getCellClass('*', 'script')); + uDom(cells.node(6)) + .prop('reqType', 'xhr') + .prop('hostname', '*') + .addClass(getCellClass('*', 'xhr')); + uDom(cells.node(7)) + .prop('reqType', 'frame') + .prop('hostname', '*') + .addClass(getCellClass('*', 'frame')); + uDom(cells.node(8)) + .prop('reqType', 'other') + .prop('hostname', '*') + .addClass(getCellClass('*', 'other')); + uDom('#matHead .matRow').css('display', ''); } /******************************************************************************/ function renderMatrixCellDomain(cell, domain) { - var contents = $(cell) - .prop({reqType: '*', hostname: domain}) + var contents = uDom(cell) + .prop('reqType', '*') + .prop('hostname', domain) .addClass(getCellClass(domain, '*')) .contents(); - contents[0].textContent = '\u202A' + punycode.toUnicode(domain); - contents[1].textContent = ' '; + contents.node(0).textContent = '\u202A' + punycode.toUnicode(domain); + contents.node(1).textContent = ' '; } function renderMatrixCellSubdomain(cell, domain, subomain) { - var contents = $(cell) - .prop({reqType: '*', hostname: subomain}) + var contents = uDom(cell) + .prop('reqType', '*') + .prop('hostname', subomain) .addClass(getCellClass(subomain, '*')) .contents(); - contents[0].textContent = '\u202A' + punycode.toUnicode(subomain.slice(0, subomain.lastIndexOf(domain)-1)) + '.'; - contents[1].textContent = punycode.toUnicode(domain); + contents.node(0).textContent = '\u202A' + punycode.toUnicode(subomain.slice(0, subomain.lastIndexOf(domain)-1)) + '.'; + contents.node(1).textContent = punycode.toUnicode(domain); } function renderMatrixMetaCellDomain(cell, domain) { - var contents = $(cell) - .prop({reqType: '*', hostname: domain}) + var contents = uDom(cell) + .prop('reqType', '*') + .prop('hostname', domain) .addClass(getCellClass(domain, '*')) .contents(); - contents[0].textContent = '\u202A\u2217.' + punycode.toUnicode(domain); - contents[1].textContent = ' '; + contents.node(0).textContent = '\u202A\u2217.' + punycode.toUnicode(domain); + contents.node(1).textContent = ' '; } function renderMatrixCellType(cell, hostname, type, stats) { - cell = $(cell); - cell.prop({reqType: type, hostname: hostname, count: stats.count}) + var ce = uDom(cell) + .prop('reqType', type) + .prop('hostname', hostname) + .prop('count', stats.count) .addClass(getCellClass(hostname, type)); if ( stats.count ) { - cell.text(stats.count); + ce.text(stats.count); } else { - cell.text('\u00A0'); + ce.text('\u00A0'); } } @@ -854,31 +884,31 @@ function makeMatrixMetaRowDomain(domain, stats) { /******************************************************************************/ function renderMatrixMetaCellType(cell, count) { - cell = $(cell); - cell.addClass('ri'); + var ce = uDom(cell); + ce.addClass('ri'); if ( count ) { - cell.text(count); + ce.text(count); } } function makeMatrixMetaRow(stats) { var typeStats = stats.types; - var matrixRow = createMatrixRow().addClass('ro'); - var cells = matrixRow.children('.matCell'); - var contents = $(cells[0]) + var matrixRow = uDom(createMatrixRow()[0]).addClass('ro'); + var cells = matrixRow.find('.matCell'); + var contents = uDom(cells.node(0)) .addClass('matCell rd') .contents(); - contents[0].textContent = ' '; - contents[1].textContent = '\u202A' + typeStats['*'].count + ' blacklisted hostname(s)'; - renderMatrixMetaCellType(cells[1], typeStats.cookie.count); - renderMatrixMetaCellType(cells[2], typeStats.css.count); - renderMatrixMetaCellType(cells[3], typeStats.image.count); - renderMatrixMetaCellType(cells[4], typeStats.plugin.count); - renderMatrixMetaCellType(cells[5], typeStats.script.count); - renderMatrixMetaCellType(cells[6], typeStats.xhr.count); - renderMatrixMetaCellType(cells[7], typeStats.frame.count); - renderMatrixMetaCellType(cells[8], typeStats.other.count); - return matrixRow; + contents.node(0).textContent = ' '; + contents.node(1).textContent = '\u202A' + typeStats['*'].count + ' blacklisted hostname(s)'; + renderMatrixMetaCellType(cells.node(1), typeStats.cookie.count); + renderMatrixMetaCellType(cells.node(2), typeStats.css.count); + renderMatrixMetaCellType(cells.node(3), typeStats.image.count); + renderMatrixMetaCellType(cells.node(4), typeStats.plugin.count); + renderMatrixMetaCellType(cells.node(5), typeStats.script.count); + renderMatrixMetaCellType(cells.node(6), typeStats.xhr.count); + renderMatrixMetaCellType(cells.node(7), typeStats.frame.count); + renderMatrixMetaCellType(cells.node(8), typeStats.other.count); + return $(matrixRow.node(0)); } /******************************************************************************/ @@ -1246,10 +1276,10 @@ function initScopeCell() { function updateScopeCell() { var µm = µMatrix; - $('body') + uDom('body') .removeClass('tScopeGlobal tScopeLocal tScopeSite') .addClass(getClassFromTemporaryScopeKey(targetScope)) - $('#scopeCell').text(targetScope.replace('*', '\u2217')); + uDom('#scopeCell').text(targetScope.replace('*', '\u2217')); } /******************************************************************************/ @@ -1287,7 +1317,7 @@ function updatePersistButton() { button.children('span.badge').text(ruleset.count > 0 ? ruleset.count : ''); var disabled = ruleset.count === 0; button.toggleClass('disabled', disabled); - $('#buttonRevertScope').toggleClass('disabled', disabled); + uDom('#buttonRevertScope').toggleClass('disabled', disabled); } function persistScope() { @@ -1357,7 +1387,7 @@ function mouseleaveMatrixCellHandler() { /******************************************************************************/ function gotoExtensionURL() { - var url = $(this).data('extensionUrl'); + var url = this.getAttribute('data-extension-url'); if ( url ) { messaging.tell({ what: 'gotoExtensionURL', url: url }); } @@ -1366,7 +1396,7 @@ function gotoExtensionURL() { /******************************************************************************/ function gotoExternalURL() { - var url = $(this).data('externalUrl'); + var url = this.getAttribute('data-external-url'); if ( url ) { messaging.tell({ what: 'gotoURL', url: url }); } @@ -1379,7 +1409,7 @@ function dropDownMenuShow() { } function dropDownMenuHide() { - $('.dropdown-menu').removeClass('show'); + uDom('.dropdown-menu').removeClass('show'); } /******************************************************************************/ @@ -1417,8 +1447,8 @@ var bindToTab = function(tabs) { $('#toolbarLeft').remove(); // https://github.com/gorhill/httpswitchboard/issues/191 - $('#noNetTrafficPrompt').text(chrome.i18n.getMessage('matrixNoNetTrafficPrompt')); - $('#noNetTrafficPrompt').css('display', ''); + uDom('#noNetTrafficPrompt').text(chrome.i18n.getMessage('matrixNoNetTrafficPrompt')); + uDom('#noNetTrafficPrompt').css('display', ''); } }; @@ -1475,9 +1505,10 @@ $(function() { $('body').on('click', '.dropdown-menu-capture', dropDownMenuHide); $('#matList').on('click', '.g3Meta', function() { - var separator = $(this); - separator.toggleClass('g3Collapsed'); - setUserSetting('popupHideBlacklisted', separator.hasClass('g3Collapsed')); + var collapsed = uDom(this) + .toggleClass('g3Collapsed') + .hasClass('g3Collapsed'); + setUserSetting('popupHideBlacklisted', collapsed); }); }); diff --git a/src/js/udom.js b/src/js/udom.js new file mode 100644 index 0000000..5782e16 --- /dev/null +++ b/src/js/udom.js @@ -0,0 +1,558 @@ +/******************************************************************************* + + µ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 = []; +}; + +/******************************************************************************/ + +var addNodeToList = function(list, node) { + if ( node ) { + list.nodes.push(node); + } + return list; +}; + +/******************************************************************************/ + +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 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 i = r.length; + while ( i-- ) { + list.nodes.push(r[i]); + } + return list; +}; + +/******************************************************************************/ + +var pTagOfChildTag = { + 'tr': 'table', + 'option': 'select' +}; + +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 isDescendantOf = function(descendant, ancestor) { + while ( descendant.parentNode !== null ) { + if ( descendant.parentNode === ancestor ) { + return true; + } + descendant = descendant.parentNode; + } + return false; +}; + +/******************************************************************************/ + +DOMList.prototype.length = function() { + return this.nodes.length; +}; + +/******************************************************************************/ + +DOMList.prototype.node = function(i) { + return this.nodes[i]; +}; + +/******************************************************************************/ + +DOMList.prototype.subset = function(i, l) { + var r = new DOMList(); + var n = l !== undefined ? l : 1; + 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); +}; + +/******************************************************************************/ + +DOMList.prototype.parent = function() { + var r = new DOMList(); + if ( this.nodes.length ) { + addNodeToList(r, this.nodes[0].parentNode); + } + return r; +}; + +/******************************************************************************/ + +DOMList.prototype.ancestors = function(selector) { + var r = new DOMList(); + if ( this.nodes.length === 0 ) { + return r; + } + var candidates = document.querySelectorAll(selector); + var i = candidates.length; + var j, candidate; + while ( i-- ) { + candidate = candidates[i]; + j = this.nodes.length; + while ( j-- ) { + if ( isDescendantOf(this.nodes[j], candidate) ) { + addNodeToList(r, candidate); + } + } + } + return r; +}; + +/******************************************************************************/ + +DOMList.prototype.find = 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.forEach = function(callback) { + var n = this.nodes.length; + for ( var i = 0; i < n; i++ ) { + callback.bind(this.nodes[i]).call(); + } + return this; +}; + +/******************************************************************************/ + +DOMList.prototype.remove = function() { + var n = this.nodes.length; + var c, p; + for ( var i = 0; i < n; i++ ) { + c = this.nodes[i]; + if ( p = c.parentNode ) { + p.removeChild(c); + } + } + return this; +}; + +/******************************************************************************/ + +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 = DOMListFactory(selector, context); + if ( p.length ) { + var n = this.nodes.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.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 ? 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 re = new RegExp('(^| )' + className + '( |$)'); + var currentState = re.test(node.className); + var newState = targetState; + if ( newState === undefined ) { + newState = !currentState; + } + if ( newState === currentState ) { + return; + } + var newClassName = node.className; + if ( newState ) { + newClassName += ' ' + className; + } else { + newClassName = newClassName.replace(re, ' '); + } + node.className = newClassName.trim(); +}; + +/******************************************************************************/ + +DOMList.prototype.hasClassName = function(className) { + if ( !this.nodes.length ) { + return false; + } + var re = new RegExp('(^| )' + className + '( |$)'); + return re.test(this.nodes[0].className); +}; + +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) { + var classNames = className.split(/\s+/); + var i = this.nodes.length; + var node, j; + while ( i-- ) { + node = this.nodes[i]; + j = classNames.length; + while ( j-- ) { + toggleClass(node, classNames[j], targetState); + } + } + return this; +}; + +/******************************************************************************/ + +var makeEventHandler = function(context, selector, callback) { + return function(event) { + var candidates = context.querySelectorAll(selector); + if ( !candidates.length ) { + return; + } + var node = event.target; + var i; + while ( node && node !== context ) { + i = candidates.length; + while ( i-- ) { + if ( candidates[i] === node ) { + return callback.call(node, event); + } + } + node = node.parentNode; + } + }; +}; + +DOMList.prototype.on = function(etype, selector, callback) { + if ( typeof selector === 'function' ) { + callback = selector; + selector = undefined; + } + var i = this.nodes.length; + while ( i-- ) { + if ( selector !== undefined ) { + this.nodes[i].addEventListener(etype, makeEventHandler(this.nodes[i], selector, callback), true); + } else { + this.nodes[i].addEventListener(etype, 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; +}; + +/******************************************************************************/ + +return DOMListFactory; + +})(); diff --git a/src/popup.html b/src/popup.html index 9c8db35..a180bfa 100644 --- a/src/popup.html +++ b/src/popup.html @@ -74,6 +74,7 @@ +