diff --git a/platform/chromium/vapi-client.js b/platform/chromium/vapi-client.js index 5d62a5fd8..763ebbbd0 100644 --- a/platform/chromium/vapi-client.js +++ b/platform/chromium/vapi-client.js @@ -100,6 +100,10 @@ MessagingListeners.prototype.remove = function(callback) { this.listeners.splice(this.listeners.indexOf(callback), 1); }; +MessagingListeners.prototype.removeAll = function() { + this.listeners = []; +}; + MessagingListeners.prototype.process = function(msg) { var listeners = this.listeners; var n = listeners.length; @@ -209,6 +213,10 @@ MessagingChannel.prototype.removeListener = function(callback) { this.listeners.remove(callback); }; +MessagingChannel.prototype.removeAllListeners = function() { + this.listeners.removeAll(); +}; + /******************************************************************************/ vAPI.messaging = { diff --git a/platform/firefox/vapi-client.js b/platform/firefox/vapi-client.js index 44bbec0c3..778a7c31d 100644 --- a/platform/firefox/vapi-client.js +++ b/platform/firefox/vapi-client.js @@ -94,6 +94,10 @@ MessagingListeners.prototype.remove = function(callback) { this.listeners.splice(this.listeners.indexOf(callback), 1); }; +MessagingListeners.prototype.removeAll = function() { + this.listeners = []; +}; + MessagingListeners.prototype.process = function(msg) { var listeners = this.listeners; var n = listeners.length; @@ -199,6 +203,10 @@ MessagingChannel.prototype.removeListener = function(callback) { this.listeners.remove(callback); }; +MessagingChannel.prototype.removeAllListeners = function() { + this.listeners.removeAll(); +}; + /******************************************************************************/ vAPI.messaging = { diff --git a/src/css/logger-ui-inspector.css b/src/css/logger-ui-inspector.css new file mode 100644 index 000000000..2dc551152 --- /dev/null +++ b/src/css/logger-ui-inspector.css @@ -0,0 +1,88 @@ +#domInspector { + border-top: 1px solid #ccc; + display: none; + max-height: 40%; + min-height: 40%; + overflow: auto; + } +#domInspector.enabled { + display: block; + } +#domInspector .permatoolbar { + position: absolute; + } +#domInspector .permatoolbar .highlightMode.invert { + transform: rotate(180deg); + } +#domInspector > ul:first-of-type { + padding-left: 0.5em; + } +#domInspector ul { + background-color: #fff; + margin: 0; + padding-left: 1em; + } +#domInspector li { + list-style-type: none; + white-space: nowrap; + } +#domInspector li.isCosmeticHide, +#domInspector li.isCosmeticHide ul, +#domInspector li.isCosmeticHide li { + background-color: #fee; + } +#domInspector li > * { + margin-right: 1em; + } +#domInspector li > span:first-child { + color: #000; + cursor: default; + display: inline-block; + margin-right: 0; + opacity: 0.5; + visibility: hidden; + width: 1em; + } +#domInspector li > span:first-child:hover { + opacity: 1; + } +#domInspector li > *:last-child { + margin-right: 0; + } +#domInspector li > span:first-child:before { + content: '\a0'; + } +#domInspector li.branch > span:first-child:before { + content: '\25b8'; + visibility: visible; + } +#domInspector li.branch.show > span:first-child:before { + content: '\25be'; + visibility: visible; + } +#domInspector li.branch.hasCosmeticHide > span:first-child:before { + color: red; + } +#domInspector li > code { + cursor: pointer; + font: 12px/1.4 monospace; + } +#domInspector li > code.off { + text-decoration: line-through; + } +#domInspector li > span { + color: #aaa; + } +#domInspector li > code.filter { + color: red; + } +#domInspector li > ul { + display: none; + } +#domInspector li.show > ul { + display: block; + } + +#cosmeticFilteringDialog .dialog textarea { + height: 40vh; +} diff --git a/src/css/logger-ui.css b/src/css/logger-ui.css index ee26f7ca7..449b77e5b 100644 --- a/src/css/logger-ui.css +++ b/src/css/logger-ui.css @@ -16,6 +16,12 @@ body { input:focus { background-color: #ffe; } +textarea { + box-sizing: border-box; + direction: ltr; + resize: none; + width: 100%; + } .permatoolbar { background-color: white; border: 0; @@ -55,85 +61,6 @@ input:focus { padding: 0.2em 0; } -#domInspector { - border-top: 1px solid #ccc; - display: none; - max-height: 40%; - min-height: 40%; - overflow: auto; - } -#domInspector.enabled { - display: block; - } -#domInspector > ul:first-child { - padding-left: 0; - } -#domInspector ul { - background-color: #fff; - margin: 0; - padding-left: 1em; - } -#domInspector li { - list-style-type: none; - white-space: nowrap; - } -#domInspector li.isCosmeticHide, -#domInspector li.isCosmeticHide ul, -#domInspector li.isCosmeticHide li { - background-color: #fee; - } -#domInspector li > * { - margin-right: 1em; - } -#domInspector li > span:first-child { - color: #000; - cursor: default; - display: inline-block; - margin-right: 0; - opacity: 0.5; - visibility: hidden; - width: 1em; - } -#domInspector li > span:first-child:hover { - opacity: 1; - } -#domInspector li > *:last-child { - margin-right: 0; - } -#domInspector li > span:first-child:before { - content: '\a0'; - } -#domInspector li.branch > span:first-child:before { - content: '\25b8'; - visibility: visible; - } -#domInspector li.branch.show > span:first-child:before { - content: '\25be'; - visibility: visible; - } -#domInspector li.branch.hasCosmeticHide > span:first-child:before { - color: red; - } -#domInspector li > code { - cursor: pointer; - font: 12px/1.4 monospace; - } -#domInspector li > code.off { - text-decoration: line-through; - } -#domInspector li > span { - color: #aaa; - } -#domInspector li > code.filter { - color: red; - } -#domInspector li > ul { - display: none; - } -#domInspector li.show > ul { - display: block; - } - #events { border-top: 1px solid #ccc; font: 13px sans-serif; @@ -623,11 +550,7 @@ body.colorBlind #netFilteringDialog .dialog > div.containers > div.dynamic tr.e margin: 0.75em 0; } #netFilteringDialog .dialog > div.containers > div.static textarea { - box-sizing: border-box; - direction: ltr; height: 6em; - resize: none; - width: 100%; } #netFilteringDialog .dialog > div.containers > div.static > p:nth-of-type(2) { text-align: center; diff --git a/src/js/logger-ui-inspector.js b/src/js/logger-ui-inspector.js new file mode 100644 index 000000000..f5dcc48e8 --- /dev/null +++ b/src/js/logger-ui-inspector.js @@ -0,0 +1,671 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2015 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 +*/ + +/* global vAPI, uDom */ + +/******************************************************************************/ + +(function() { + +'use strict'; + +/******************************************************************************/ + +// Don't bother if the browser is not modern enough. +if ( typeof Map === undefined || typeof WeakMap === undefined ) { + return; +} + +/******************************************************************************/ + +var logger = self.logger; +var messager = logger.messager; + +var inspectedTabId = ''; +var inspectedHostname = ''; +var pollTimer = null; +var fingerprint = null; +var showdomButton = uDom.nodeFromId('showdom'); +var inspector = uDom.nodeFromId('domInspector'); +var domTree = uDom.nodeFromId('domTree'); +var tabSelector = uDom.nodeFromId('pageSelector'); + +/******************************************************************************/ + +var nodeFromDomEntry = function(entry) { + var node, value; + var li = document.createElement('li'); + li.setAttribute('id', entry.nid); + // expander/collapser + node = document.createElement('span'); + li.appendChild(node); + // selector + node = document.createElement('code'); + node.textContent = entry.sel; + li.appendChild(node); + // descendant count + value = entry.cnt || 0; + node = document.createElement('span'); + node.textContent = value !== 0 ? value.toLocaleString() : ''; + node.setAttribute('data-cnt', value); + li.appendChild(node); + // cosmetic filter + if ( entry.filter !== undefined ) { + node = document.createElement('code'); + node.classList.add('filter'); + node.textContent = entry.filter; + li.appendChild(node); + li.classList.add('isCosmeticHide'); + } + return li; +}; + +/******************************************************************************/ + +var appendListItem = function(ul, li) { + ul.appendChild(li); + // Ancestor nodes of a node which is affected by a cosmetic filter will + // be marked as "containing cosmetic filters", for user convenience. + if ( li.classList.contains('isCosmeticHide') === false ) { + return; + } + for (;;) { + li = li.parentElement.parentElement; + if ( li === null ) { + break; + } + li.classList.add('hasCosmeticHide'); + } +}; + +/******************************************************************************/ + +var renderDOMFull = function(response) { + var ul = inspector.removeChild(domTree); + logger.removeAllChildren(domTree); + + var lvl = 0; + var entries = response.layout; + var n = entries.length; + var li, entry; + for ( var i = 0; i < n; i++ ) { + entry = entries[i]; + if ( entry.lvl === lvl ) { + li = nodeFromDomEntry(entry); + appendListItem(ul, li); + //expandIfBlockElement(li); + continue; + } + if ( entry.lvl > lvl ) { + ul = document.createElement('ul'); + li.appendChild(ul); + li.classList.add('branch'); + li = nodeFromDomEntry(entry); + appendListItem(ul, li); + //expandIfBlockElement(li); + lvl = entry.lvl; + continue; + } + // entry.lvl < lvl + while ( entry.lvl < lvl ) { + ul = li.parentNode; + li = ul.parentNode; + ul = li.parentNode; + lvl -= 1; + } + li = nodeFromDomEntry(entry); + ul.appendChild(li); + } + while ( ul.parentNode !== null ) { + ul = ul.parentNode; + } + ul.firstElementChild.classList.add('show'); + + inspector.appendChild(domTree); +}; + +/******************************************************************************/ + +var patchIncremental = function(from, delta) { + var span, cnt; + var li = from.parentElement.parentElement; + var patchCosmeticHide = delta >= 0 && + from.classList.contains('isCosmeticFilter') && + li.classList.contains('hasCosmeticFilter') === false; + // Include descendants count when removing a node + if ( delta < 0 ) { + delta -= countFromNode(from); + } + for ( ; li.localName === 'li'; li = li.parentElement.parentElement ) { + span = li.children[2]; + if ( delta !== 0 ) { + cnt = countFromNode(li) + delta; + span.textContent = cnt !== 0 ? cnt.toLocaleString() : ''; + span.setAttribute('data-cnt', cnt); + } + if ( patchCosmeticHide ) { + li.classList.add('hasCosmeticFilter'); + } + } +}; + +/******************************************************************************/ + +var renderDOMIncremental = function(response) { + // Process each journal entry: + // 1 = node added + // -1 = node removed + var journal = response.journal; + var nodes = response.nodes; + var entry, previous, li, ul; + for ( var i = 0, n = journal.length; i < n; i++ ) { + entry = journal[i]; + // Remove node + if ( entry.what === -1 ) { + li = document.getElementById(entry.nid); + if ( li === null ) { + continue; + } + patchIncremental(li, -1); + li.parentNode.removeChild(li); + continue; + } + // Modify node + if ( entry.what === 0 ) { + // TODO: update selector/filter + continue; + } + // Add node as sibling + if ( entry.what === 1 && entry.l ) { + previous = document.getElementById(entry.l); + // This should not happen + if ( previous === null ) { + // throw new Error('No left sibling!?'); + continue; + } + ul = previous.parentElement; + li = nodeFromDomEntry(nodes[entry.nid]); + ul.insertBefore(li, previous.nextElementSibling); + patchIncremental(li, 1); + continue; + } + // Add node as child + if ( entry.what === 1 && entry.u ) { + li = document.getElementById(entry.u); + // This should not happen + if ( li === null ) { + // throw new Error('No parent!?'); + continue; + } + ul = li.querySelector('ul'); + if ( ul === null ) { + ul = document.createElement('ul'); + li.appendChild(ul); + li.classList.add('branch'); + } + li = nodeFromDomEntry(nodes[entry.nid]); + ul.appendChild(li); + patchIncremental(li, 1); + continue; + } + } +}; + +/******************************************************************************/ + +var countFromNode = function(li) { + var span = li.children[2]; + var cnt = parseInt(span.getAttribute('data-cnt'), 10); + return isNaN(cnt) ? cnt : 0; +}; + +/******************************************************************************/ + +var selectorFromNode = function(node, nth) { + var selector = ''; + var code; + if ( nth === undefined ) { + nth = 1; + } + while ( node !== null ) { + if ( node.localName === 'li' ) { + code = node.querySelector('code:nth-of-type(' + nth + ')'); + if ( code !== null ) { + selector = code.textContent + ' > ' + selector; + if ( selector.indexOf('#') !== -1 ) { + break; + } + nth = 1; + } + } + node = node.parentElement; + } + return selector.slice(0, -3); +}; + +/******************************************************************************/ + +var nidFromNode = function(node) { + var li = node; + while ( li !== null ) { + if ( li.localName === 'li' ) { + return li.id || ''; + } + li = li.parentElement; + } + return ''; +}; + +/******************************************************************************/ + +var startDialog = (function() { + var dialog = uDom.nodeFromId('cosmeticFilteringDialog'); + var candidateFilters = []; + + var onClick = function(ev) { + var target = ev.target; + + // click outside the dialog proper + if ( target.classList.contains('modalDialog') ) { + return stop(); + } + ev.stopPropagation(); + }; + + var stop = function() { + dialog.removeEventListener('click', onClick, true); + document.body.removeChild(dialog); + }; + + var start = function() { + // Collect all selectors which are currently toggled + var node, filters = []; + var nodes = domTree.querySelectorAll('code.off'); + for ( var i = 0; i < nodes.length; i++ ) { + node = nodes[i]; + if ( node.classList.contains('filter') ) { + filters.push({ + prefix: '#@#', + nid: '', + selector: node.textContent + }); + } else { + filters.push({ + prefix: '##', + nid: nidFromNode(node), + selector: node.textContent + }); + } + } + + // TODO: Send filters through dom-inspector.js for further processing. + + candidateFilters = filters; + var taValue = [], filter; + for ( i = 0; i < filters.length; i++ ) { + filter = filters[i]; + taValue.push(inspectedHostname + filter.prefix + filter.selector); + } + dialog.querySelector('textarea').value = taValue.join('\n'); + document.body.appendChild(dialog); + dialog.addEventListener('click', onClick, true); + }; + + return start; +})(); + +/******************************************************************************/ + +var onClick = function(ev) { + ev.stopPropagation(); + + if ( inspectedTabId === '' ) { + return; + } + + var target = ev.target; + var parent = target.parentElement; + + // Expand/collapse branch + if ( + target.localName === 'span' && + parent instanceof HTMLLIElement && + parent.classList.contains('branch') && + target === parent.firstElementChild + ) { + target.parentElement.classList.toggle('show'); + return; + } + + // Toggle selector + if ( target.localName === 'code' ) { + var original = target.classList.contains('filter') === false; + messager.send({ + what: 'postMessageTo', + senderTabId: null, + senderChannel: 'logger-ui.js', + receiverTabId: inspectedTabId, + receiverChannel: 'dom-inspector.js', + msg: { + what: 'toggleNodes', + original: original, + target: original !== target.classList.toggle('off'), + selector: selectorFromNode(target, original ? 1 : 2), + nid: original ? nidFromNode(target) : '' + } + }); + var cantCreate = inspector.querySelector('#domTree .off') === null; + inspector.querySelector('.permatoolbar .revert').classList.toggle('disabled', cantCreate); + inspector.querySelector('.permatoolbar .commit').classList.toggle('disabled', cantCreate); + return; + } +}; + +/******************************************************************************/ + +var onMouseOver = (function() { + var mouseoverTarget = null; + var mouseoverTimer = null; + + var timerHandler = function() { + mouseoverTimer = null; + messager.send({ + what: 'postMessageTo', + senderTabId: null, + senderChannel: 'logger-ui.js', + receiverTabId: inspectedTabId, + receiverChannel: 'dom-inspector.js', + msg: { + what: 'highlightOne', + selector: selectorFromNode(mouseoverTarget), + nid: nidFromNode(mouseoverTarget), + scrollTo: true + } + }); + }; + + return function(ev) { + if ( inspectedTabId === '' ) { + return; + } + + // Find closest `li` + var target = ev.target; + while ( target !== null ) { + if ( target.localName === 'li' ) { + break; + } + target = target.parentElement; + } + if ( target === mouseoverTarget ) { + return; + } + mouseoverTarget = target; + if ( mouseoverTimer === null ) { + mouseoverTimer = vAPI.setTimeout(timerHandler, 50); + } + }; +})(); + +/******************************************************************************/ + +var currentTabId = function() { + if ( showdomButton.classList.contains('active') === false ) { + return ''; + } + var tabId = logger.tabIdFromClassName(tabSelector.value) || ''; + return tabId !== 'bts' ? tabId : ''; +}; + +/******************************************************************************/ + +var cancelPollTimer = function() { + if ( pollTimer !== null ) { + clearTimeout(pollTimer); + pollTimer = null; + } +}; + +/******************************************************************************/ + +var onDOMFetched = function(response) { + if ( response === undefined || currentTabId() !== inspectedTabId ) { + shutdownInspector(inspectedTabId); + injectInspectorAsync(250); + return; + } + + switch ( response.status ) { + case 'full': + renderDOMFull(response); + fingerprint = response.fingerprint; + inspectedHostname = response.hostname; + break; + + case 'incremental': + renderDOMIncremental(response); + break; + + case 'nochange': + case 'busy': + break; + + default: + break; + } + + fetchDOMAsync(); +}; + +/******************************************************************************/ + +var fetchDOM = function() { + messager.send({ + what: 'postMessageTo', + senderTabId: null, + senderChannel: 'logger-ui.js', + receiverTabId: inspectedTabId, + receiverChannel: 'dom-inspector.js', + msg: { + what: 'domLayout', + fingerprint: fingerprint + } + }); + pollTimer = vAPI.setTimeout(function() { + pollTimer = null; + onDOMFetched(); + }, 1001); +}; + +/******************************************************************************/ + +var fetchDOMAsync = function(delay) { + if ( pollTimer !== null ) { + return; + } + pollTimer = vAPI.setTimeout(function() { + pollTimer = null; + fetchDOM(); + }, delay || 1001); +}; + +/******************************************************************************/ + +var injectInspector = function() { + var tabId = currentTabId(); + // No valid tab, go back + if ( tabId === '' ) { + injectInspectorAsync(); + return; + } + inspectedTabId = tabId; + fingerprint = null; + messager.send({ + what: 'scriptlet', + tabId: tabId, + scriptlet: 'dom-inspector' + }); + fetchDOMAsync(250); +}; + +/******************************************************************************/ + +var injectInspectorAsync = function(delay) { + if ( pollTimer !== null ) { + return; + } + if ( showdomButton.classList.contains('active') === false ) { + return; + } + pollTimer = vAPI.setTimeout(function() { + pollTimer = null; + injectInspector(); + }, delay || 1001); +}; + +/******************************************************************************/ + +var shutdownInspector = function(tabId) { + messager.send({ + what: 'postMessageTo', + senderTabId: null, + senderChannel: 'logger-ui.js', + receiverTabId: tabId, + receiverChannel: 'dom-inspector.js', + msg: { what: 'shutdown', } + }); + logger.removeAllChildren(domTree); + cancelPollTimer(); + inspectedTabId = ''; +}; + +/******************************************************************************/ + +var onTabIdChanged = function() { + if ( inspectedTabId !== currentTabId() ) { + shutdownInspector(); + injectInspectorAsync(250); + } +}; + +/******************************************************************************/ + +var toggleHighlightMode = function() { + messager.send({ + what: 'postMessageTo', + senderTabId: null, + senderChannel: 'logger-ui.js', + receiverTabId: inspectedTabId, + receiverChannel: 'dom-inspector.js', + msg: { + what: 'highlightMode', + invert: uDom.nodeFromSelector('#domInspector .permatoolbar .highlightMode').classList.toggle('invert') + } + }); +}; + +/******************************************************************************/ + +var revert = function() { + uDom('#domTree .off').removeClass('off'); + messager.send({ + what: 'postMessageTo', + senderTabId: null, + senderChannel: 'logger-ui.js', + receiverTabId: inspectedTabId, + receiverChannel: 'dom-inspector.js', + msg: { what: 'resetToggledNodes' } + }); + inspector.querySelector('.permatoolbar .revert').classList.add('disabled'); + inspector.querySelector('.permatoolbar .commit').classList.add('disabled'); +}; + +/******************************************************************************/ + +var onMessage = function(request) { + var msg = request.what === 'postMessageTo' ? request.msg : request; + switch ( msg.what ) { + case 'domLayout': + cancelPollTimer(); + onDOMFetched(msg); + break; + + default: + break; + } +}; + +/******************************************************************************/ + +var toggleOn = function() { + window.addEventListener('beforeunload', toggleOff); + tabSelector.addEventListener('change', onTabIdChanged); + domTree.addEventListener('click', onClick, true); + domTree.addEventListener('mouseover', onMouseOver, true); + uDom.nodeFromSelector('#domInspector .permatoolbar .highlightMode').addEventListener('click', toggleHighlightMode); + uDom.nodeFromSelector('#domInspector .permatoolbar .revert').addEventListener('click', revert); + uDom.nodeFromSelector('#domInspector .permatoolbar .commit').addEventListener('click', startDialog); + inspector.classList.add('enabled'); + messager.addListener(onMessage); + injectInspector(); + // Adjust tree view for toolbar height + domTree.style.setProperty( + 'margin-top', + inspector.querySelector('.permatoolbar').clientHeight + 'px' + ); +}; + +/******************************************************************************/ + +var toggleOff = function() { + messager.removeListener(onMessage); + cancelPollTimer(); + shutdownInspector(); + window.removeEventListener('beforeunload', toggleOff); + tabSelector.removeEventListener('change', onTabIdChanged); + domTree.removeEventListener('click', onClick, true); + domTree.removeEventListener('mouseover', onMouseOver, true); + uDom.nodeFromSelector('#domInspector .permatoolbar .highlightMode').removeEventListener('click', toggleHighlightMode); + uDom.nodeFromSelector('#domInspector .permatoolbar .revert').removeEventListener('click', revert); + uDom.nodeFromSelector('#domInspector .permatoolbar .commit').removeEventListener('click', startDialog); + inspectedTabId = ''; + inspector.classList.remove('enabled'); +}; + +/******************************************************************************/ + +var toggle = function() { + if ( showdomButton.classList.toggle('active') ) { + toggleOn(); + } else { + toggleOff(); + } +}; + +/******************************************************************************/ + +showdomButton.addEventListener('click', toggle); + +/******************************************************************************/ + +})(); + + diff --git a/src/js/logger-ui.js b/src/js/logger-ui.js index f4ce46816..759128693 100644 --- a/src/js/logger-ui.js +++ b/src/js/logger-ui.js @@ -30,6 +30,30 @@ /******************************************************************************/ +var logger = self.logger = {}; +var messager = logger.messager = vAPI.messaging.channel('logger-ui.js'); + +/******************************************************************************/ + +var removeAllChildren = logger.removeAllChildren = function(node) { + while ( node.firstChild ) { + node.removeChild(node.firstChild); + } +}; + +/******************************************************************************/ + +var tabIdFromClassName = logger.tabIdFromClassName = function(className) { + var matches = className.match(/(?:^| )tab_([^ ]+)(?: |$)/); + if ( matches === null ) { + return ''; + } + return matches[1]; +}; + +/******************************************************************************/ +/******************************************************************************/ + // Adjust top padding of content table, to match that of toolbar height. (function() { @@ -47,8 +71,6 @@ /******************************************************************************/ -var messager = vAPI.messaging.channel('logger-ui.js'); - var tbody = document.querySelector('#events tbody'); var trJunkyard = []; var tdJunkyard = []; @@ -61,7 +83,6 @@ var allTabIdsToken; var hiddenTemplate = document.querySelector('#hiddenTemplate > span'); var reRFC3986 = /^([^:\/?#]+:)?(\/\/[^\/?#]*)?([^?#]*)(\?[^#]*)?(#.*)?/; var netFilteringDialog = uDom.nodeFromId('netFilteringDialog'); -var filterFinderDialog = uDom.nodeFromId('filterFinderDialog'); var prettyRequestTypes = { 'main_frame': 'doc', @@ -98,14 +119,6 @@ var dateOptions = { /******************************************************************************/ -var removeAllChildren = function(node) { - while ( node.firstChild ) { - node.removeChild(node.firstChild); - } -}; - -/******************************************************************************/ - var classNameFromTabId = function(tabId) { if ( tabId === noTabId ) { return 'tab_bts'; @@ -116,503 +129,6 @@ var classNameFromTabId = function(tabId) { return ''; }; -/******************************************************************************/ - -var tabIdFromClassName = function(className) { - var matches = className.match(/(?:^| )tab_([^ ]+)(?: |$)/); - if ( matches === null ) { - return ''; - } - return matches[1]; -}; - -/******************************************************************************/ -/******************************************************************************/ - -// DOM inspector - -(function domInspector() { - // Don't bother if the browser is not modern enough. - if ( typeof Map === undefined || typeof WeakMap === undefined ) { - return; - } - - var inspectedTabId = ''; - var currentSelector = ''; - var showdomButton = uDom.nodeFromId('showdom'); - var inspector = uDom.nodeFromId('domInspector'); - var tabSelector = uDom.nodeFromId('pageSelector'); - - var nodeFromDomEntry = function(entry) { - var node, value; - var li = document.createElement('li'); - li.setAttribute('id', entry.nid); - // expander/collapser - node = document.createElement('span'); - li.appendChild(node); - // selector - node = document.createElement('code'); - node.textContent = entry.sel; - li.appendChild(node); - // descendant count - value = entry.cnt || 0; - node = document.createElement('span'); - node.textContent = value !== 0 ? value.toLocaleString() : ''; - node.setAttribute('data-cnt', value); - li.appendChild(node); - // cosmetic filter - if ( entry.filter !== undefined ) { - node = document.createElement('code'); - node.classList.add('filter'); - node.textContent = entry.filter; - li.appendChild(node); - li.classList.add('isCosmeticHide'); - } - return li; - }; - - var appendListItem = function(ul, li) { - ul.appendChild(li); - // Ancestor nodes of a node which is affected by a cosmetic filter will - // be marked as "containing cosmetic filters", for user convenience. - if ( li.classList.contains('isCosmeticHide') === false ) { - return; - } - for (;;) { - li = li.parentElement.parentElement; - if ( li === null ) { - break; - } - li.classList.add('hasCosmeticHide'); - } - }; - - var renderDOMFull = function(response) { - var ul = document.createElement('ul'); - var lvl = 0; - var entries = response.layout; - var n = entries.length; - var li, entry; - for ( var i = 0; i < n; i++ ) { - entry = entries[i]; - if ( entry.lvl === lvl ) { - li = nodeFromDomEntry(entry); - appendListItem(ul, li); - //expandIfBlockElement(li); - continue; - } - if ( entry.lvl > lvl ) { - ul = document.createElement('ul'); - li.appendChild(ul); - li.classList.add('branch'); - li = nodeFromDomEntry(entry); - appendListItem(ul, li); - //expandIfBlockElement(li); - lvl = entry.lvl; - continue; - } - // entry.lvl < lvl - while ( entry.lvl < lvl ) { - ul = li.parentNode; - li = ul.parentNode; - ul = li.parentNode; - lvl -= 1; - } - li = nodeFromDomEntry(entry); - ul.appendChild(li); - } - while ( ul.parentNode !== null ) { - ul = ul.parentNode; - } - ul.firstElementChild.classList.add('show'); - - removeAllChildren(inspector); - inspector.appendChild(ul); - }; - - var patchIncremental = function(from, delta) { - var span, cnt; - var li = from.parentElement.parentElement; - var patchCosmeticHide = delta >= 0 && - from.classList.contains('isCosmeticFilter') && - li.classList.contains('hasCosmeticFilter') === false; - // Include descendants count when removing a node - if ( delta < 0 ) { - delta -= countFromNode(from); - } - for ( ; li.localName === 'li'; li = li.parentElement.parentElement ) { - span = li.children[2]; - if ( delta !== 0 ) { - cnt = countFromNode(li) + delta; - span.textContent = cnt !== 0 ? cnt.toLocaleString() : ''; - span.setAttribute('data-cnt', cnt); - } - if ( patchCosmeticHide ) { - li.classList.add('hasCosmeticFilter'); - } - } - }; - - var renderDOMIncremental = function(response) { - // Process each journal entry: - // 1 = node added - // -1 = node removed - var journal = response.journal; - var nodes = response.nodes; - var entry, previous, li, ul; - for ( var i = 0, n = journal.length; i < n; i++ ) { - entry = journal[i]; - // Remove node - if ( entry.what === -1 ) { - li = document.getElementById(entry.nid); - if ( li === null ) { - continue; - } - patchIncremental(li, -1); - li.parentNode.removeChild(li); - continue; - } - // Modify node - if ( entry.what === 0 ) { - // TODO: update selector/filter - continue; - } - // Add node as sibling - if ( entry.what === 1 && entry.l ) { - previous = document.getElementById(entry.l); - // This should not happen - if ( previous === null ) { - // throw new Error('No left sibling!?'); - continue; - } - ul = previous.parentElement; - li = nodeFromDomEntry(nodes[entry.nid]); - ul.insertBefore(li, previous.nextElementSibling); - patchIncremental(li, 1); - continue; - } - // Add node as child - if ( entry.what === 1 && entry.u ) { - li = document.getElementById(entry.u); - // This should not happen - if ( li === null ) { - // throw new Error('No parent!?'); - continue; - } - ul = li.querySelector('ul'); - if ( ul === null ) { - ul = document.createElement('ul'); - li.appendChild(ul); - li.classList.add('branch'); - } - li = nodeFromDomEntry(nodes[entry.nid]); - ul.appendChild(li); - patchIncremental(li, 1); - continue; - } - } - }; - - var countFromNode = function(li) { - var span = li.children[2]; - var cnt = parseInt(span.getAttribute('data-cnt'), 10); - return isNaN(cnt) ? cnt : 0; - }; - - var selectorFromNode = function(node, nth) { - var selector = ''; - var code; - if ( nth === undefined ) { - nth = 1; - } - while ( node !== null ) { - if ( node.localName === 'li' ) { - code = node.querySelector('code:nth-of-type(' + nth + ')'); - if ( code !== null ) { - selector = code.textContent + ' > ' + selector; - if ( selector.indexOf('#') !== -1 ) { - break; - } - nth = 1; - } - } - node = node.parentElement; - } - return selector.slice(0, -3); - }; - - var onClick = function(ev) { - ev.stopPropagation(); - - if ( inspectedTabId === '' ) { - return; - } - - var target = ev.target; - var parent = target.parentElement; - - // Expand/collapse branch - if ( - target.localName === 'span' && - parent instanceof HTMLLIElement && - parent.classList.contains('branch') && - target === parent.firstElementChild - ) { - target.parentElement.classList.toggle('show'); - return; - } - - // Toggle selector - if ( target.localName === 'code' ) { - var original = target.classList.contains('filter') === false; - messager.send({ - what: 'postMessageTo', - senderTabId: null, - senderChannel: 'logger-ui.js', - receiverTabId: inspectedTabId, - receiverChannel: 'dom-inspector.js', - msg: { - what: 'toggleNodes', - original: original, - target: original !== target.classList.toggle('off'), - selector: selectorFromNode(target, original ? 1 : 2) - } - }); - return; - } - - // Highlight and scrollto - if ( target.localName === 'code' ) { - messager.send({ - what: 'postMessageTo', - senderTabId: null, - senderChannel: 'logger-ui.js', - receiverTabId: inspectedTabId, - receiverChannel: 'dom-inspector.js', - msg: { - what: 'highlight', - selector: selectorFromNode(target), - scrollTo: true - } - }); - return; - } - }; - - var onMouseOver = (function() { - var mouseoverTarget = null; - var mouseoverTimer = null; - - var timerHandler = function() { - mouseoverTimer = null; - messager.send({ - what: 'postMessageTo', - senderTabId: null, - senderChannel: 'logger-ui.js', - receiverTabId: inspectedTabId, - receiverChannel: 'dom-inspector.js', - msg: { - what: 'highlight', - selector: selectorFromNode(mouseoverTarget), - scrollTo: true - } - }); - }; - - return function(ev) { - if ( inspectedTabId === '' ) { - return; - } - - // Find closest `li` - var target = ev.target; - while ( target !== null ) { - if ( target.localName === 'li' ) { - break; - } - target = target.parentElement; - } - if ( target === mouseoverTarget ) { - return; - } - mouseoverTarget = target; - if ( mouseoverTimer === null ) { - mouseoverTimer = vAPI.setTimeout(timerHandler, 50); - } - }; - })(); - - var pollTimer = null; - var fingerprint = null; - - var currentTabId = function() { - if ( showdomButton.classList.contains('active') === false ) { - return ''; - } - var tabId = tabIdFromClassName(tabSelector.value) || ''; - return tabId !== 'bts' ? tabId : ''; - }; - - var cancelPollTimer = function() { - if ( pollTimer !== null ) { - clearTimeout(pollTimer); - pollTimer = null; - } - }; - - var onDOMFetched = function(response) { - if ( response === undefined || currentTabId() !== inspectedTabId ) { - shutdownInspector(inspectedTabId); - injectInspectorAsync(250); - return; - } - - switch ( response.status ) { - case 'full': - renderDOMFull(response); - fingerprint = response.fingerprint; - break; - - case 'incremental': - renderDOMIncremental(response); - break; - - case 'nochange': - case 'busy': - break; - - default: - break; - } - - fetchDOMAsync(); - }; - - var fetchDOM = function() { - messager.send({ - what: 'postMessageTo', - senderTabId: null, - senderChannel: 'logger-ui.js', - receiverTabId: inspectedTabId, - receiverChannel: 'dom-inspector.js', - msg: { - what: 'domLayout', - fingerprint: fingerprint - } - }); - pollTimer = vAPI.setTimeout(function() { - pollTimer = null; - onDOMFetched(); - }, 1001); - }; - - var fetchDOMAsync = function(delay) { - if ( pollTimer !== null ) { - return; - } - pollTimer = vAPI.setTimeout(function() { - pollTimer = null; - fetchDOM(); - }, delay || 1001); - }; - - var injectInspector = function() { - var tabId = currentTabId(); - // No valid tab, go back - if ( tabId === '' ) { - injectInspectorAsync(); - return; - } - inspectedTabId = tabId; - fingerprint = null; - messager.send({ - what: 'scriptlet', - tabId: tabId, - scriptlet: 'dom-inspector' - }); - fetchDOMAsync(250); - }; - - var injectInspectorAsync = function(delay) { - if ( pollTimer !== null ) { - return; - } - if ( showdomButton.classList.contains('active') === false ) { - return; - } - pollTimer = vAPI.setTimeout(function() { - pollTimer = null; - injectInspector(); - }, delay || 1001); - }; - - var shutdownInspector = function(tabId) { - messager.send({ - what: 'postMessageTo', - senderTabId: null, - senderChannel: 'logger-ui.js', - receiverTabId: tabId, - receiverChannel: 'dom-inspector.js', - msg: { what: 'shutdown', } - }); - removeAllChildren(inspector); - cancelPollTimer(); - inspectedTabId = ''; - }; - - var onTabIdChanged = function() { - if ( inspectedTabId !== currentTabId() ) { - shutdownInspector(); - injectInspectorAsync(250); - } - }; - - var onMessage = function(request) { - var msg = request.what === 'postMessageTo' ? request.msg : request; - switch ( msg.what ) { - case 'domLayout': - cancelPollTimer(); - onDOMFetched(msg); - break; - - default: - break; - } - }; - - var toggleOn = function() { - window.addEventListener('beforeunload', toggleOff); - inspector.addEventListener('click', onClick, true); - inspector.addEventListener('mouseover', onMouseOver, true); - tabSelector.addEventListener('change', onTabIdChanged); - inspector.classList.add('enabled'); - messager.addListener(onMessage); - injectInspector(); - }; - - var toggleOff = function() { - messager.removeListener(onMessage); - cancelPollTimer(); - shutdownInspector(); - window.removeEventListener('beforeunload', toggleOff); - inspector.removeEventListener('click', onClick, true); - inspector.removeEventListener('mouseover', onMouseOver, true); - tabSelector.removeEventListener('change', onTabIdChanged); - currentSelector = inspectedTabId = ''; - inspector.classList.remove('enabled'); - }; - - var toggle = function() { - if ( showdomButton.classList.toggle('active') ) { - toggleOn(); - } else { - toggleOff(); - } - }; - - showdomButton.addEventListener('click', toggle); -})(); - /******************************************************************************/ /******************************************************************************/ @@ -1776,6 +1292,7 @@ var netFilteringManager = (function() { var reverseLookupManager = (function() { var reSentence1 = /\{\{filter\}\}/g; var sentence1Template = vAPI.i18n('loggerStaticFilteringFinderSentence1'); + var filterFinderDialog = uDom.nodeFromId('filterFinderDialog'); var removeAllChildren = function(node) { while ( node.firstChild ) { diff --git a/src/js/scriptlets/dom-inspector.js b/src/js/scriptlets/dom-inspector.js index 0d3446088..c97849daf 100644 --- a/src/js/scriptlets/dom-inspector.js +++ b/src/js/scriptlets/dom-inspector.js @@ -144,8 +144,9 @@ var svgOcean = null; var svgIslands = null; var svgRoot = null; var pickerRoot = null; -var currentSelector = ''; +var highlightedElements = []; +var nodeToIdMap = new WeakMap(); // No need to iterate var toggledNodes = new Map(); /******************************************************************************/ @@ -175,7 +176,6 @@ var domLayout = (function() { }; var idGenerator = 0; - var nodeToIdMap = new WeakMap(); // No need to iterate // This will be used to uniquely identify nodes across process. @@ -209,7 +209,7 @@ var domLayout = (function() { } return out; })(); - +/* var matchesSelector = (function() { if ( typeof Element.prototype.matches === 'function' ) { return 'matches'; @@ -222,26 +222,7 @@ var domLayout = (function() { } return ''; })(); - - var hasManyMatches = function(node, selector) { - var fnName = matchesSelector; - if ( fnName === '' ) { - return true; - } - var child = node.firstElementChild; - var match = false; - while ( child !== null ) { - if ( child[fnName](selector) ) { - if ( match ) { - return true; - } - match = true; - } - child = child.nextElementSibling; - } - return false; - }; - +*/ var selectorFromNode = function(node) { var str, attr, pos, sw, i; var tag = node.localName; @@ -276,18 +257,6 @@ var domLayout = (function() { selector += '[' + attr + sw + '="' + cssEscape(str) + '"]'; } } - // The resulting selector must cause only one element to be selected. If - // it's not the case, further narrow using `nth-of-type` pseudo-class. - if ( hasManyMatches(node.parentElement, selector) ) { - i = 1; - while ( node.previousElementSibling ) { - node = node.previousElementSibling; - if ( node.localName === tag ) { - i += 1; - } - } - selector += ':nth-of-type(' + i + ')'; - } return selector; }; @@ -543,7 +512,8 @@ var domLayout = (function() { var response = { what: 'domLayout', - fingerprint: domFingerprint() + fingerprint: domFingerprint(), + hostname: window.location.hostname }; // No mutation observer means we need to send full layout @@ -593,7 +563,8 @@ var domLayout = (function() { /******************************************************************************/ -var highlightElements = function(elems, scrollTo) { +var highlightElements = function(scrollTo) { + var elems = highlightedElements; var wv = pickerRoot.contentWindow.innerWidth; var hv = pickerRoot.contentWindow.innerHeight; var ocean = ['M0 0h' + wv + 'v' + hv + 'h-' + wv, 'z']; @@ -684,15 +655,31 @@ var elementsFromSelector = function(filter) { /******************************************************************************/ -var highlight = function(scrollTo) { - var elements = elementsFromSelector(currentSelector); - highlightElements(elements, scrollTo); +var selectNodes = function(selector, nid) { + var nodes = elementsFromSelector(selector); + if ( nid === '' ) { + return nodes; + } + var i = nodes.length; + while ( i-- ) { + if ( nodeToIdMap.get(nodes[i]) === nid ) { + return [nodes[i]]; + } + } + return []; +}; + +/******************************************************************************/ + +var hightlightNodes = function(selector, nid, scrollTo) { + highlightedElements = selectNodes(selector, nid); + highlightElements(scrollTo); }; /******************************************************************************/ var onScrolled = function() { - highlight(); + highlightElements(); }; /******************************************************************************/ @@ -703,8 +690,7 @@ var onScrolled = function() { // hidden, any = remove display property, don't remember original state // hidden, hidden = set display to `none` -var toggleNodes = function(selector, originalState, targetState) { - var nodes = document.querySelectorAll(selector); +var toggleNodes = function(nodes, originalState, targetState) { var i = nodes.length; if ( i === 0 ) { return; @@ -759,13 +745,13 @@ var resetToggledNodes = function() { var shutdown = function() { resetToggledNodes(); domLayout.shutdown(); - localMessager.removeListener(onMessage); + localMessager.removeAllListeners(); localMessager.close(); localMessager = null; window.removeEventListener('scroll', onScrolled, true); document.documentElement.removeChild(pickerRoot); pickerRoot = svgRoot = svgOcean = svgIslands = null; - currentSelector = ''; + highlightedElements = []; }; /******************************************************************************/ @@ -779,15 +765,22 @@ var onMessage = function(request) { response = domLayout.get(msg.fingerprint); break; - case 'highlight': - currentSelector = msg.selector; - highlight(msg.scrollTo); + case 'highlightMode': + svgRoot.classList.toggle('invert', msg.invert); + break; + + case 'highlightOne': + hightlightNodes(msg.selector, msg.nid, msg.scrollTo); + break; + + case 'resetToggledNodes': + resetToggledNodes(); break; case 'toggleNodes': - toggleNodes(msg.selector, msg.original, msg.target); - currentSelector = msg.selector; - highlight(true); + highlightedElements = selectNodes(msg.selector, msg.nid); + toggleNodes(highlightedElements, msg.original, msg.target); + highlightElements(true); break; case 'shutdown': @@ -863,6 +856,13 @@ pickerRoot.onload = function() { 'stroke: #FFF;', 'stroke-width: 0.5px;', '}', + 'svg.invert > path:first-child {', + 'fill: rgba(0,0,255,0.1);', + '}', + 'svg.invert > path + path {', + 'fill: rgba(0,0,0,0.75);', + 'stroke: #000;', + '}', '' ].join('\n'); pickerDoc.body.appendChild(style); @@ -876,7 +876,7 @@ pickerRoot.onload = function() { window.addEventListener('scroll', onScrolled, true); - highlight(); + highlightElements(); localMessager.addListener(onMessage); }; diff --git a/src/logger-ui.html b/src/logger-ui.html index 5f9fc1f86..624ee8799 100644 --- a/src/logger-ui.html +++ b/src/logger-ui.html @@ -4,6 +4,7 @@ +
+