From 631443768f2359e6fdb0bc6a3e457a0951e0a9e9 Mon Sep 17 00:00:00 2001 From: gorhill Date: Fri, 26 Jun 2015 00:08:41 -0400 Subject: [PATCH] =?UTF-8?q?dom=20inspector:=20=C3=A9bauche?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- platform/chromium/vapi-background.js | 123 ++++--- platform/chromium/vapi-client.js | 171 ++++++--- platform/chromium/vapi-common.js | 6 +- platform/firefox/bootstrap.js | 10 +- platform/firefox/vapi-background.js | 31 +- platform/firefox/vapi-client.js | 188 +++++++--- src/css/logger-ui.css | 240 +++++++++---- src/epicker.html | 25 +- src/js/logger-ui.js | 514 ++++++++++++++++++++++++--- src/js/messaging.js | 69 +++- src/js/pagestore.js | 4 +- src/js/scriptlets/cosmetic-logger.js | 3 +- src/js/scriptlets/cosmetic-off.js | 2 +- src/js/scriptlets/cosmetic-on.js | 2 +- src/js/scriptlets/dom-fingerprint.js | 66 ++++ src/js/scriptlets/dom-highlight.js | 339 ++++++++++++++++++ src/js/scriptlets/dom-layout.js | 406 +++++++++++++++++++++ src/js/ublock.js | 91 +++-- src/logger-ui.html | 44 ++- 19 files changed, 1978 insertions(+), 356 deletions(-) create mode 100644 src/js/scriptlets/dom-fingerprint.js create mode 100644 src/js/scriptlets/dom-highlight.js create mode 100644 src/js/scriptlets/dom-layout.js diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index 9413033f6..8e67b3d23 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -519,6 +519,62 @@ vAPI.messaging = { /******************************************************************************/ +// This allows to avoid creating a closure for every single message which +// expects an answer. Having a closure created each time a message is processed +// has been always bothering me. Another benefit of the implementation here +// is to reuse the callback proxy object, so less memory churning. +// +// https://developers.google.com/speed/articles/optimizing-javascript +// "Creating a closure is significantly slower then creating an inner +// function without a closure, and much slower than reusing a static +// function" +// +// http://hacksoflife.blogspot.ca/2015/01/the-four-horsemen-of-performance.html +// "the dreaded 'uniformly slow code' case where every function takes 1% +// of CPU and you have to make one hundred separate performance optimizations +// to improve performance at all" +// +// http://jsperf.com/closure-no-closure/2 + +var CallbackWrapper = function(port, request) { + // No need to bind every single time + this.callback = this.proxy.bind(this); + this.messaging = vAPI.messaging; + this.init(port, request); +}; + +CallbackWrapper.junkyard = []; + +CallbackWrapper.factory = function(port, request) { + var wrapper = CallbackWrapper.junkyard.pop(); + if ( wrapper ) { + wrapper.init(port, request); + return wrapper; + } + return new CallbackWrapper(port, request); +}; + +CallbackWrapper.prototype.init = function(port, request) { + this.port = port; + this.request = request; +}; + +CallbackWrapper.prototype.proxy = function(response) { + // https://github.com/chrisaljoudi/uBlock/issues/383 + if ( this.messaging.ports.hasOwnProperty(this.port.name) ) { + this.port.postMessage({ + requestId: this.request.requestId, + channelName: this.request.channelName, + msg: response !== undefined ? response : null + }); + } + // Mark for reuse + this.port = this.request = null; + CallbackWrapper.junkyard.push(this); +}; + +/******************************************************************************/ + vAPI.messaging.listen = function(listenerName, callback) { this.listeners[listenerName] = callback; }; @@ -605,58 +661,25 @@ vAPI.messaging.broadcast = function(message) { /******************************************************************************/ -// This allows to avoid creating a closure for every single message which -// expects an answer. Having a closure created each time a message is processed -// has been always bothering me. Another benefit of the implementation here -// is to reuse the callback proxy object, so less memory churning. -// -// https://developers.google.com/speed/articles/optimizing-javascript -// "Creating a closure is significantly slower then creating an inner -// function without a closure, and much slower than reusing a static -// function" -// -// http://hacksoflife.blogspot.ca/2015/01/the-four-horsemen-of-performance.html -// "the dreaded 'uniformly slow code' case where every function takes 1% -// of CPU and you have to make one hundred separate performance optimizations -// to improve performance at all" -// -// http://jsperf.com/closure-no-closure/2 - -var CallbackWrapper = function(port, request) { - // No need to bind every single time - this.callback = this.proxy.bind(this); - this.messaging = vAPI.messaging; - this.init(port, request); -}; - -CallbackWrapper.junkyard = []; - -CallbackWrapper.factory = function(port, request) { - var wrapper = CallbackWrapper.junkyard.pop(); - if ( wrapper ) { - wrapper.init(port, request); - return wrapper; - } - return new CallbackWrapper(port, request); -}; - -CallbackWrapper.prototype.init = function(port, request) { - this.port = port; - this.request = request; -}; - -CallbackWrapper.prototype.proxy = function(response) { - // https://github.com/chrisaljoudi/uBlock/issues/383 - if ( this.messaging.ports.hasOwnProperty(this.port.name) ) { - this.port.postMessage({ - requestId: this.request.requestId, - channelName: this.request.channelName, - msg: response !== undefined ? response : null +vAPI.messaging.send = function(tabId, channelName, message) { + var port; + var chromiumTabId = toChromiumTabId(tabId); + for ( var portName in this.ports ) { + if ( this.ports.hasOwnProperty(portName) === false ) { + continue; + } + port = this.ports[portName]; + if ( chromiumTabId !== 0 && port.sender.tab.id !== chromiumTabId ) { + continue; + } + port.postMessage({ + channelName: channelName, + msg: message }); + if ( chromiumTabId !== 0 ) { + break; + } } - // Mark for reuse - this.port = this.request = null; - CallbackWrapper.junkyard.push(this); }; /******************************************************************************/ diff --git a/platform/chromium/vapi-client.js b/platform/chromium/vapi-client.js index de7147293..5d62a5fd8 100644 --- a/platform/chromium/vapi-client.js +++ b/platform/chromium/vapi-client.js @@ -73,49 +73,148 @@ vAPI.shutdown = (function() { /******************************************************************************/ +var MessagingListeners = function(callback) { + this.listeners = []; + if ( typeof callback === 'function' ) { + this.listeners.push(callback); + } +}; + +MessagingListeners.prototype.add = function(callback) { + if ( typeof callback !== 'function' ) { + return; + } + if ( this.listeners.indexOf(callback) !== -1 ) { + throw new Error('Duplicate listener.'); + } + this.listeners.push(callback); +}; + +MessagingListeners.prototype.remove = function(callback) { + if ( typeof callback !== 'function' ) { + return; + } + if ( this.listeners.indexOf(callback) === -1 ) { + throw new Error('Listener not found.'); + } + this.listeners.splice(this.listeners.indexOf(callback), 1); +}; + +MessagingListeners.prototype.process = function(msg) { + var listeners = this.listeners; + var n = listeners.length; + for ( var i = 0; i < n; i++ ) { + listeners[i](msg); + } +}; + +/******************************************************************************/ + var messagingConnector = function(response) { if ( !response ) { return; } - var channels = vAPI.messaging.channels; - var channel, listener; + var messaging = vAPI.messaging; + var channels = messaging.channels; + var channel; + // Sent to all channels if ( response.broadcast === true && !response.channelName ) { for ( channel in channels ) { - if ( channels.hasOwnProperty(channel) === false ) { + if ( channels[channel] instanceof MessagingChannel === false ) { continue; } - listener = channels[channel].listener; - if ( typeof listener === 'function' ) { - listener(response.msg); - } + channels[channel].listeners.process(response.msg); } return; } + // Response to specific message previously sent if ( response.requestId ) { - listener = vAPI.messaging.listeners[response.requestId]; - delete vAPI.messaging.listeners[response.requestId]; - delete response.requestId; + var listener = messaging.pending[response.requestId]; + delete messaging.pending[response.requestId]; + delete response.requestId; // TODO: why? + if ( listener ) { + listener(response.msg); + return; + } } - if ( !listener ) { - channel = channels[response.channelName]; - listener = channel && channel.listener; + // Sent to a specific channel + channel = channels[response.channelName]; + if ( channel instanceof MessagingChannel ) { + channel.listeners.process(response.msg); } +}; - if ( typeof listener === 'function' ) { - listener(response.msg); +/******************************************************************************/ + +var MessagingChannel = function(name, callback) { + this.channelName = name; + this.listeners = new MessagingListeners(callback); + this.refCount = 1; + if ( typeof callback === 'function' ) { + var messaging = vAPI.messaging; + if ( messaging.port === null ) { + messaging.setup(); + } } }; +MessagingChannel.prototype.send = function(message, callback) { + var messaging = vAPI.messaging; + if ( messaging.port === null ) { + messaging.setup(); + } + var requestId; + if ( callback ) { + requestId = messaging.requestId++; + messaging.pending[requestId] = callback; + } + messaging.port.postMessage({ + channelName: this.channelName, + requestId: requestId, + msg: message + }); +}; + +MessagingChannel.prototype.close = function() { + this.refCount -= 1; + if ( this.refCount !== 0 ) { + return; + } + var messaging = vAPI.messaging; + delete messaging.channels[this.channelName]; + if ( Object.keys(messaging.channels).length === 0 ) { + messaging.close(); + } +}; + +MessagingChannel.prototype.addListener = function(callback) { + if ( typeof callback !== 'function' ) { + return; + } + this.listeners.add(callback); + var messaging = vAPI.messaging; + if ( messaging.port === null ) { + messaging.setup(); + } +}; + +MessagingChannel.prototype.removeListener = function(callback) { + if ( typeof callback !== 'function' ) { + return; + } + this.listeners.remove(callback); +}; + /******************************************************************************/ vAPI.messaging = { port: null, channels: {}, - listeners: {}, + pending: {}, requestId: 1, setup: function() { @@ -131,43 +230,21 @@ vAPI.messaging = { this.port.onMessage.removeListener(messagingConnector); this.port = null; this.channels = {}; - this.listeners = {}; + this.pending = {}; }, channel: function(channelName, callback) { if ( !channelName ) { return; } - - this.channels[channelName] = { - channelName: channelName, - listener: typeof callback === 'function' ? callback : null, - send: function(message, callback) { - if ( vAPI.messaging.port === null ) { - vAPI.messaging.setup(); - } - - message = { - channelName: this.channelName, - msg: message - }; - - if ( callback ) { - message.requestId = vAPI.messaging.requestId++; - vAPI.messaging.listeners[message.requestId] = callback; - } - - vAPI.messaging.port.postMessage(message); - }, - close: function() { - delete vAPI.messaging.channels[this.channelName]; - if ( Object.keys(vAPI.messaging.channels).length === 0 ) { - vAPI.messaging.close(); - } - } - }; - - return this.channels[channelName]; + var channel = this.channels[channelName]; + if ( channel instanceof MessagingChannel ) { + channel.addListener(callback); + channel.refCount += 1; + } else { + channel = this.channels[channelName] = new MessagingChannel(channelName, callback); + } + return channel; } }; diff --git a/platform/chromium/vapi-common.js b/platform/chromium/vapi-common.js index 0ddc7f486..4df7b9968 100644 --- a/platform/chromium/vapi-common.js +++ b/platform/chromium/vapi-common.js @@ -90,7 +90,11 @@ vAPI.closePopup = function() { // This storage is optional, but it is nice to have, for a more polished user // experience. -vAPI.localStorage = window.localStorage; +// This can throw in some contexts (like in devtool). +try { + vAPI.localStorage = window.localStorage; +} catch (ex) { +} /******************************************************************************/ diff --git a/platform/firefox/bootstrap.js b/platform/firefox/bootstrap.js index 440e9e198..fdca64775 100644 --- a/platform/firefox/bootstrap.js +++ b/platform/firefox/bootstrap.js @@ -26,6 +26,8 @@ /******************************************************************************/ +const {classes: Cc} = Components; + // Accessing the context of the background page: // var win = Services.appShell.hiddenDOMWindow.document.querySelector('iframe[src*=ublock0]').contentWindow; @@ -34,7 +36,7 @@ let version; const hostName = 'ublock0'; const restartListener = { get messageManager() { - return Components.classes['@mozilla.org/parentprocessmessagemanager;1'] + return Cc['@mozilla.org/parentprocessmessagemanager;1'] .getService(Components.interfaces.nsIMessageListenerManager); }, @@ -51,7 +53,7 @@ function startup(data, reason) { version = data.version; } - let appShell = Components.classes['@mozilla.org/appshell/appShellService;1'] + let appShell = Cc['@mozilla.org/appshell/appShellService;1'] .getService(Components.interfaces.nsIAppShellService); let onReady = function(e) { @@ -96,7 +98,7 @@ function startup(data, reason) { return; } - let ww = Components.classes['@mozilla.org/embedcomp/window-watcher;1'] + let ww = Cc['@mozilla.org/embedcomp/window-watcher;1'] .getService(Components.interfaces.nsIWindowWatcher); ww.registerNotification({ @@ -141,7 +143,7 @@ function shutdown(data, reason) { function install(/*aData, aReason*/) { // https://bugzil.la/719376 - Components.classes['@mozilla.org/intl/stringbundle;1'] + Cc['@mozilla.org/intl/stringbundle;1'] .getService(Components.interfaces.nsIStringBundleService) .flushBundles(); } diff --git a/platform/firefox/vapi-background.js b/platform/firefox/vapi-background.js index 647c4c407..f7a4d7202 100644 --- a/platform/firefox/vapi-background.js +++ b/platform/firefox/vapi-background.js @@ -1026,7 +1026,6 @@ var tabWatcher = (function() { tabContainer.removeEventListener('TabSelect', onSelect); } - // Close extension tabs var browser, URI, tabId; for ( var tab of tabBrowser.tabs ) { browser = tabWatcher.browserFromTarget(tab); @@ -1034,6 +1033,7 @@ var tabWatcher = (function() { continue; } URI = browser.currentURI; + // Close extension tabs if ( URI.schemeIs('chrome') && URI.host === location.host ) { vAPI.tabs._remove(tab, getTabBrowser(this)); } @@ -1239,6 +1239,35 @@ vAPI.messaging.setup = function(defaultHandler) { /******************************************************************************/ +vAPI.messaging.send = function(tabId, channelName, message) { + var ffTabId = tabId || ''; + var targetId = location.host + ':broadcast'; + var payload = JSON.stringify({ channelName: channelName, msg: message }); + + if ( ffTabId === '' ) { + this.globalMessageManager.broadcastAsyncMessage(targetId, payload); + return; + } + + var browser = tabWatcher.browserFromTabId(ffTabId); + if ( browser === null ) { + return; + } + + var messageManager = browser.messageManager || null; + if ( messageManager === null ) { + return; + } + + if ( messageManager.sendAsyncMessage ) { + messageManager.sendAsyncMessage(targetId, payload); + } else { + messageManager.broadcastAsyncMessage(targetId, payload); + } +}; + +/******************************************************************************/ + vAPI.messaging.broadcast = function(message) { this.globalMessageManager.broadcastAsyncMessage( location.host + ':broadcast', diff --git a/platform/firefox/vapi-client.js b/platform/firefox/vapi-client.js index 88b11da09..44bbec0c3 100644 --- a/platform/firefox/vapi-client.js +++ b/platform/firefox/vapi-client.js @@ -67,66 +67,158 @@ vAPI.shutdown = (function() { /******************************************************************************/ +var MessagingListeners = function(callback) { + this.listeners = []; + if ( typeof callback === 'function' ) { + this.listeners.push(callback); + } +}; + +MessagingListeners.prototype.add = function(callback) { + if ( typeof callback !== 'function' ) { + return; + } + if ( this.listeners.indexOf(callback) !== -1 ) { + throw new Error('Duplicate listener.'); + } + this.listeners.push(callback); +}; + +MessagingListeners.prototype.remove = function(callback) { + if ( typeof callback !== 'function' ) { + return; + } + if ( this.listeners.indexOf(callback) === -1 ) { + throw new Error('Listener not found.'); + } + this.listeners.splice(this.listeners.indexOf(callback), 1); +}; + +MessagingListeners.prototype.process = function(msg) { + var listeners = this.listeners; + var n = listeners.length; + for ( var i = 0; i < n; i++ ) { + listeners[i](msg); + } +}; + +/******************************************************************************/ + var messagingConnector = function(response) { if ( !response ) { return; } - var channels = vAPI.messaging.channels; - var channel, listener; + var messaging = vAPI.messaging; + var channels = messaging.channels; + var channel; + // Sent to all channels if ( response.broadcast === true && !response.channelName ) { for ( channel in channels ) { - if ( channels.hasOwnProperty(channel) === false ) { + if ( channels[channel] instanceof MessagingChannel === false ) { continue; } - listener = channels[channel].listener; - if ( typeof listener === 'function' ) { - listener(response.msg); - } + channels[channel].listeners.process(response.msg); } return; } + // Response to specific message previously sent if ( response.requestId ) { - listener = vAPI.messaging.listeners[response.requestId]; - delete vAPI.messaging.listeners[response.requestId]; - delete response.requestId; + var listener = messaging.pending[response.requestId]; + delete messaging.pending[response.requestId]; + delete response.requestId; // TODO: why? + if ( listener ) { + listener(response.msg); + return; + } } - if ( !listener ) { - channel = channels[response.channelName]; - listener = channel && channel.listener; + // Sent to a specific channel + channel = channels[response.channelName]; + if ( channel instanceof MessagingChannel ) { + channel.listeners.process(response.msg); } +}; - if ( typeof listener === 'function' ) { - listener(response.msg); +/******************************************************************************/ + +var MessagingChannel = function(name, callback) { + this.channelName = name; + this.listeners = new MessagingListeners(callback); + this.refCount = 1; + if ( typeof callback === 'function' ) { + var messaging = vAPI.messaging; + if ( !messaging.connected ) { + messaging.setup(); + } } }; +MessagingChannel.prototype.send = function(message, callback) { + var messaging = vAPI.messaging; + if ( !messaging.connected ) { + messaging.setup(); + } + var requestId; + if ( callback ) { + requestId = messaging.requestId++; + messaging.pending[requestId] = callback; + } + sendAsyncMessage('ublock0:background', { + channelName: self._sandboxId_ + '|' + this.channelName, + requestId: requestId, + msg: message + }); +}; + +MessagingChannel.prototype.close = function() { + this.refCount -= 1; + if ( this.refCount !== 0 ) { + return; + } + delete vAPI.messaging.channels[this.channelName]; +}; + +MessagingChannel.prototype.addListener = function(callback) { + if ( typeof callback !== 'function' ) { + return; + } + this.listeners.add(callback); + var messaging = vAPI.messaging; + if ( !messaging.connected ) { + messaging.setup(); + } +}; + +MessagingChannel.prototype.removeListener = function(callback) { + if ( typeof callback !== 'function' ) { + return; + } + this.listeners.remove(callback); +}; + /******************************************************************************/ vAPI.messaging = { channels: {}, - listeners: {}, + pending: {}, requestId: 1, + connected: false, + connector: function(msg) { + messagingConnector(JSON.parse(msg)); + }, setup: function() { - this.connector = function(msg) { - messagingConnector(JSON.parse(msg)); - }; - addMessageListener(this.connector); - - this.channels['vAPI'] = {}; - this.channels['vAPI'].listener = function(msg) { + this.connected = true; + this.channels['vAPI'] = new MessagingChannel('vAPI', function(msg) { if ( msg.cmd === 'injectScript' ) { var details = msg.details; - if ( !details.allFrames && window !== window.top ) { return; } - // TODO: investigate why this happens, and if this happens // legitimately (content scripts not injected I suspect, so // that would make this legitimate). @@ -135,55 +227,35 @@ vAPI.messaging = { self.injectScript(details.file); } } - }; + }); }, close: function() { - if ( !this.connector ) { + if ( !this.connected ) { return; } - removeMessageListener(); - this.connector = null; + this.connected = false; this.channels = {}; - this.listeners = {}; + this.pending = {}; }, channel: function(channelName, callback) { if ( !channelName ) { return; } - - this.channels[channelName] = { - channelName: channelName, - listener: typeof callback === 'function' ? callback : null, - send: function(message, callback) { - if ( !vAPI.messaging.connector ) { - vAPI.messaging.setup(); - } - - message = { - channelName: self._sandboxId_ + '|' + this.channelName, - msg: message - }; - - if ( callback ) { - message.requestId = vAPI.messaging.requestId++; - vAPI.messaging.listeners[message.requestId] = callback; - } - - sendAsyncMessage('ublock0:background', message); - }, - close: function() { - delete vAPI.messaging.channels[this.channelName]; - } - }; - - return this.channels[channelName]; + var channel = this.channels[channelName]; + if ( channel instanceof MessagingChannel ) { + channel.addListener(callback); + channel.refCount += 1; + } else { + channel = this.channels[channelName] = new MessagingChannel(channelName, callback); + } + return channel; }, toggleListener: function({type, persisted}) { - if ( !vAPI.messaging.connector ) { + if ( !vAPI.messaging.connected ) { return; } diff --git a/src/css/logger-ui.css b/src/css/logger-ui.css index 9c6379625..ee26f7ca7 100644 --- a/src/css/logger-ui.css +++ b/src/css/logger-ui.css @@ -4,24 +4,27 @@ body { box-sizing: border-box; color: black; -moz-box-sizing: border-box; + display: flex; + flex-direction: column; + height: 100vh; + justify-content: flex-start; margin: 0; - overflow-x: hidden; + overflow: hidden; padding: 0; - width: 100%; + width: 100vw; } -#toolbar { +input:focus { + background-color: #ffe; + } +.permatoolbar { background-color: white; border: 0; - box-sizing: border-box; - left: 0; + font-size: 120%; margin: 0; - padding: 0.5em 1em; - position: fixed; - top: 0; - width: 100%; + padding: 0.25em 0.5em; z-index: 10; } -#toolbar .button { +.permatoolbar .button { background-color: white; border: none; box-sizing: border-box; @@ -31,112 +34,195 @@ body { margin: 0; padding: 8px; } -#toolbar .button.disabled { +.permatoolbar .button.disabled { opacity: 0.2; pointer-events: none; } -#toolbar .button:hover { +.permatoolbar .button.active { + font-weight: bold; + } +.permatoolbar .button:hover { background-color: #eee; } -#toolbar > div { +.permatoolbar > div { white-space: nowrap; } -#toolbar > div:first-of-type { - font-size: 120%; - } -#toolbar > div > * { +.permatoolbar > div > * { vertical-align: middle; } #pageSelector { width: 28em; padding: 0.2em 0; } -body #compactViewToggler.button:before { - content: '\f102'; + +#domInspector { + border-top: 1px solid #ccc; + display: none; + max-height: 40%; + min-height: 40%; + overflow: auto; } -body.compactView #compactViewToggler.button:before { - content: '\f103'; +#domInspector.enabled { + display: block; } -#filterButton { - opacity: 0.25; +#domInspector > ul:first-child { + padding-left: 0; } -body.f #filterButton { - opacity: 1; +#domInspector ul { + background-color: #fff; + margin: 0; + padding-left: 1em; } -#filterInput.bad { +#domInspector li { + list-style-type: none; + white-space: nowrap; + } +#domInspector li.isCosmeticHide, +#domInspector li.isCosmeticHide ul, +#domInspector li.isCosmeticHide li { background-color: #fee; } -#maxEntries { - margin: 0 2em; +#domInspector li > * { + margin-right: 1em; } -input:focus { - background-color: #ffe; +#domInspector li > span:first-child { + color: #000; + cursor: default; + display: inline-block; + margin-right: 0; + opacity: 0.5; + visibility: hidden; + width: 1em; } -#content { - font: 13px sans-serif; - width: 100%; +#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; } -#content table { +#events { + border-top: 1px solid #ccc; + font: 13px sans-serif; + min-height: 60%; + overflow-x: hidden; + overflow-y: auto; + width: 100%; + } +#events .permatoolbar { + position: absolute; + } +#events #compactViewToggler.button:before { + content: '\f102'; + } +#events.compactView #compactViewToggler.button:before { + content: '\f103'; + } +#events #filterButton { + opacity: 0.25; + } +#events.f #filterButton { + opacity: 1; + } +#events #filterInput.bad { + background-color: #fee; + } +#events #maxEntries { + margin: 0 2em; + } +#events table { border: 0; border-collapse: collapse; direction: ltr; table-layout: fixed; width: 100%; } -#content table > colgroup > col:nth-of-type(1) { +#events table > colgroup > col:nth-of-type(1) { width: 5em; } -#content table > colgroup > col:nth-of-type(2) { +#events table > colgroup > col:nth-of-type(2) { width: 2.5em; } -#content table > colgroup > col:nth-of-type(3) { +#events table > colgroup > col:nth-of-type(3) { width: 20%; } -#content table > colgroup > col:nth-of-type(4) { +#events table > colgroup > col:nth-of-type(4) { width: 2.5em; } -#content table > colgroup > col:nth-of-type(5) { +#events table > colgroup > col:nth-of-type(5) { width: 6em; } -#content table > colgroup > col:nth-of-type(6) { +#events table > colgroup > col:nth-of-type(6) { width: calc(100% - 16em - 20%); } -body.f #content table tr.f { +#events.f table tr.f { display: none; } -#content tr.cat_info { +#events tr.cat_info { color: #00f; } -#content tr.blocked { +#events tr.blocked { background-color: rgba(192, 0, 0, 0.1); } -body.colorBlind #content tr.blocked { +body.colorBlind #events tr.blocked { background-color: rgba(0, 19, 110, 0.1); } -#content tr.nooped { +#events tr.nooped { background-color: rgba(108, 108, 108, 0.1); } -body.colorBlind #content tr.nooped { +body.colorBlind #events tr.nooped { background-color: rgba(96, 96, 96, 0.1); } -#content tr.allowed { +#events tr.allowed { background-color: rgba(0, 160, 0, 0.1); } -body.colorBlind #content tr.allowed { +body.colorBlind #events tr.allowed { background-color: rgba(255, 194, 57, 0.1) } -#content tr.cb { +#events tr.cb { background-color: rgba(255, 255, 0, 0.1); } -#content tr.maindoc { +#events tr.maindoc { background-color: #666; color: white; text-align: center; } -body #content td { +body #events td { border: 1px solid #ccc; border-top: none; min-width: 0.5em; @@ -146,87 +232,87 @@ body #content td { word-break: break-all; word-wrap: break-word; } -#content tr td { +#events tr td { border-top: 1px solid #ccc; } -#content tr td:first-of-type { +#events tr td:first-of-type { border-left: none; } -#content tr td:last-of-type { +#events tr td:last-of-type { border-right: none; } -body.compactView #content td { +#events.compactView td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -#content tr td:nth-of-type(1) { +#events tr td:nth-of-type(1) { text-align: right; white-space: nowrap; } -#content tr td:nth-of-type(2) { +#events tr td:nth-of-type(2) { text-align: center; white-space: nowrap; } -#content tr.tab_bts > td:nth-of-type(2):before { +#events tr.tab_bts > td:nth-of-type(2):before { content: '\f070'; font: 1em FontAwesome; } -#content tr.tab:not(.canMtx) { +#events tr.tab:not(.canMtx) { opacity: 0.3; } -#content tr.tab:not(.canMtx):hover { +#events tr.tab:not(.canMtx):hover { opacity: 0.7; } -#content tr.tab:not(.canMtx) > td:nth-of-type(2):before { +#events tr.tab:not(.canMtx) > td:nth-of-type(2):before { content: '\f00d'; font: 1em FontAwesome; } -body:not(.popupOn) #content tr.canMtx td:nth-of-type(2) { +body:not(.popupOn) #events tr.canMtx td:nth-of-type(2) { cursor: zoom-in; } -body:not(.popupOn) #content tr.canMtx td:nth-of-type(2):hover { +body:not(.popupOn) #events tr.canMtx td:nth-of-type(2):hover { background: #ccc; } -#content tr.canLookup td:nth-of-type(3) { +#events tr.canLookup td:nth-of-type(3) { cursor: zoom-in; } -#content tr.cat_net td:nth-of-type(4), -#content tr.cat_cosmetic td:nth-of-type(4) { +#events tr.cat_net td:nth-of-type(4), +#events tr.cat_cosmetic td:nth-of-type(4) { font: 12px monospace; text-align: center; white-space: nowrap; } -#content tr.cat_net td:nth-of-type(4) { +#events tr.cat_net td:nth-of-type(4) { cursor: pointer; position: relative; } -#content tr.cat_net td:nth-of-type(4):hover { +#events tr.cat_net td:nth-of-type(4):hover { background: #ccc; } -#content tr.cat_net td:nth-of-type(6) > span > b { +#events tr.cat_net td:nth-of-type(6) > span > b { font-weight: bold; } -#content tr td:nth-of-type(6) b { +#events tr td:nth-of-type(6) b { font-weight: normal; } -#content tr.blocked td:nth-of-type(6) b { +#events tr.blocked td:nth-of-type(6) b { background-color: rgba(192, 0, 0, 0.2); } -body.colorBlind #content tr.blocked td:nth-of-type(6) b { +body.colorBlind #events tr.blocked td:nth-of-type(6) b { background-color: rgba(0, 19, 110, 0.2); } -#content tr.nooped td:nth-of-type(6) b { +#events tr.nooped td:nth-of-type(6) b { background-color: rgba(108, 108, 108, 0.2); } -body.colorBlind #content tr.nooped td:nth-of-type(6) b { +body.colorBlind #events tr.nooped td:nth-of-type(6) b { background-color: rgba(96, 96, 96, 0.2); } -#content tr.allowed td:nth-of-type(6) b { +#events tr.allowed td:nth-of-type(6) b { background-color: rgba(0, 160, 0, 0.2); } -body.colorBlind #content tr.allowed td:nth-of-type(6) b { +body.colorBlind #events tr.allowed td:nth-of-type(6) b { background-color: rgba(255, 194, 57, 0.2); } @@ -242,7 +328,7 @@ body.colorBlind #content tr.allowed td:nth-of-type(6) b { top: 0; z-index: 200; } -body.popupOn #popupContainer { +#events.popupOn #popupContainer { display: block; } #popupContainer > div { diff --git a/src/epicker.html b/src/epicker.html index f3456b086..4e011e2f8 100644 --- a/src/epicker.html +++ b/src/epicker.html @@ -6,11 +6,11 @@ } html, body { background: transparent !important; - margin: 0; - width: 100%; - height: 100%; - overflow: hidden; font: 12px sans-serif; + height: 100%; + margin: 0; + overflow: hidden; + width: 100%; } ul, li, div { display: block; @@ -43,9 +43,6 @@ button:not(:disabled):hover { #create:not(:disabled) { background-color: #ffdca8; } -aside { - background-color: #eee; -} section { border: 0; box-sizing: border-box; @@ -80,7 +77,7 @@ ul { overflow: hidden; } aside > ul { - height: 12em; + height: 16em; overflow-y: auto; } aside > ul > li:first-of-type { @@ -129,16 +126,20 @@ svg > path + path { fill: rgba(255,31,31,0.25); } aside { + background-color: #eee; bottom: 4px; + box-sizing: border-box; + visibility: hidden; + height: calc(40% - 4px); padding: 4px; - display: none; position: fixed; right: 4px; - width: 30em; + width: calc(40% - 4px); } body.paused > aside { - opacity: 0.2; - display: block; + opacity: 0.1; + visibility: visible; + z-index: 100; } body.paused > aside:hover { opacity: 1; diff --git a/src/js/logger-ui.js b/src/js/logger-ui.js index 192f14eb9..576360a09 100644 --- a/src/js/logger-ui.js +++ b/src/js/logger-ui.js @@ -32,15 +32,24 @@ // Adjust top padding of content table, to match that of toolbar height. -uDom.nodeFromId('content').style.setProperty( - 'margin-top', - uDom.nodeFromId('toolbar').offsetHeight + 'px' -); +(function() { + var parent = uDom.nodeFromSelector('body > .permatoolbar'); + var child = parent.firstElementChild; + var size = child.clientHeight + 'px'; + parent.style.setProperty('min-height', size); + + parent = uDom.nodeFromId('events'); + parent.querySelector('table').style.setProperty( + 'margin-top', + parent.querySelector('.permatoolbar').clientHeight + 'px' + ); +})(); /******************************************************************************/ var messager = vAPI.messaging.channel('logger-ui.js'); -var tbody = document.querySelector('#content tbody'); + +var tbody = document.querySelector('#events tbody'); var trJunkyard = []; var tdJunkyard = []; var firstVarDataCol = 2; // currently, column 2 (0-based index) @@ -89,6 +98,14 @@ var dateOptions = { /******************************************************************************/ +var removeAllChildren = function(node) { + while ( node.firstChild ) { + node.removeChild(node.firstChild); + } +}; + +/******************************************************************************/ + var classNameFromTabId = function(tabId) { if ( tabId === noTabId ) { return 'tab_bts'; @@ -109,6 +126,427 @@ var tabIdFromClassName = function(className) { return matches[1]; }; +/******************************************************************************/ +/******************************************************************************/ + +// DOM inspector + +(function domInspector() { + // Don't bother if the browser is not modern enough. + if ( typeof Map === undefined ) { + return; + } + + var enabled = false; + var state = ''; + var fingerprint = ''; + var fingerprintTimer = null; + var currentTabId = ''; + var currentSelector = ''; + var inspector = uDom.nodeFromId('domInspector'); + var tabSelector = uDom.nodeFromId('pageSelector'); + + var inlineElementTags = [ + 'a', 'abbr', 'acronym', + 'b', 'bdo', 'big', 'br', 'button', + 'cite', 'code', + 'del', 'dfn', + 'em', + 'font', + 'i', 'img', 'input', 'ins', + 'kbd', + 'label', + 'map', + 'object', + 'q', + 'samp', 'select', 'small', 'span', 'strong', 'sub', 'sup', + 'textarea', 'tt', + 'u', + 'var' + ].reduce(function(p, c) { + p[c] = true; + return p; + }, Object.create(null)); + + var startTimer = function() { + if ( fingerprintTimer === null ) { + fingerprintTimer = vAPI.setTimeout(fetchFingerprint, 2000); + } + }; + + var stopTimer = function() { + if ( fingerprintTimer !== null ) { + clearTimeout(fingerprintTimer); + fingerprintTimer = null; + } + }; + + var injectHighlighter = function(tabId) { + if ( tabId === '' ) { + return; + } + messager.send({ + what: 'scriptlet', + tabId: tabId, + scriptlet: 'dom-highlight' + }); + }; + + var removeHighlighter = function(tabId) { + if ( tabId === '' ) { + return; + } + messager.send({ + what: 'sendMessageTo', + tabId: tabId, + channelName: 'scriptlets', + msg: { + what: 'dom-highlight', + action: 'shutdown' + } + }); + }; + + var nodeFromDomEntry = function(entry) { + var node; + var li = document.createElement('li'); + // expander/collapser + node = document.createElement('span'); + li.appendChild(node); + // selector + node = document.createElement('code'); + node.textContent = entry.sel; + li.appendChild(node); + // descendant count + if ( entry.cnt !== 0 ) { + node = document.createElement('span'); + node.textContent = entry.cnt.toLocaleString(); + 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 expandIfBlockElement = function(node) { + var ul = node.parentElement; + if ( ul === null ) { + return; + } + var li = ul.parentElement; + if ( li === null ) { + return; + } + if ( li.classList.contains('show') ) { + return; + } + var tag = node.firstElementChild.textContent; + var pos = tag.search(/[^a-z0-9]/); + if ( pos !== -1 ) { + tag = tag.slice(0, pos); + } + if ( inlineElementTags[tag] !== undefined ) { + return; + } + li.classList.add('show'); + }; + + var renderDOM = function(response) { + var ul = document.createElement('ul'); + var lvl = 0; + var entries = response; + 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); + + // Check for change at regular interval. + fingerprint = entries.length !== 0 ? entries[0].fp : ''; + }; + + 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 ( currentTabId === '' ) { + 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: 'sendMessageTo', + tabId: currentTabId, + channelName: 'scriptlets', + msg: { + what: 'dom-highlight', + action: '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: 'sendMessageTo', + tabId: currentTabId, + channelName: 'scriptlets', + msg: { + what: 'dom-highlight', + action: 'highlight', + selector: selectorFromNode(target), + scrollTo: true + } + }); + return; + } + }; + + var onMouseOver = (function() { + var mouseoverTarget = null; + var mouseoverTimer = null; + + var timerHandler = function() { + mouseoverTimer = null; + messager.send({ + what: 'sendMessageTo', + tabId: currentTabId, + channelName: 'scriptlets', + msg: { + what: 'dom-highlight', + action: 'highlight', + selector: selectorFromNode(mouseoverTarget), + scrollTo: true + } + }); + }; + + return function(ev) { + // 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 onFingerprintFetched = function(response) { + if ( state !== 'fetchingFingerprint' ) { + return; + } + state = ''; + fingerprintTimer = null; + if ( !enabled ) { + return; + } + if ( response === fingerprint ) { + startTimer(); + return; + } + fingerprint = response || ''; + fetchDOM(); + }; + + var fetchFingerprint = function() { + messager.send({ + what: 'scriptlet', + tabId: currentTabId, + scriptlet: 'dom-fingerprint' + }, onFingerprintFetched); + state = 'fetchingFingerprint'; + }; + + var onDOMFetched = function(response) { + if ( state !== 'fetchingDOM' ) { + return; + } + state = ''; + if ( !enabled ) { + return; + } + if ( Array.isArray(response) && response.length !== 0 ) { + renderDOM(response); + injectHighlighter(currentTabId); + } else { + fingerprint = ''; + } + startTimer(); + }; + + var fetchDOM = function() { + if ( currentTabId === '' ) { + removeAllChildren(inspector); + startTimer(); + return; + } + + messager.send({ + what: 'scriptlet', + tabId: currentTabId, + scriptlet: 'dom-layout' + }, onDOMFetched); + state = 'fetchingDOM'; + }; + + var onTabIdChanged = function() { + if ( !enabled ) { + return; + } + var previousTabId = currentTabId; + var tabId = tabIdFromClassName(tabSelector.value) || ''; + currentTabId = tabId !== 'bts' && tabId !== '' ? tabId : ''; + if ( currentTabId !== previousTabId ) { + removeHighlighter(previousTabId); + } + if ( state === 'fetchingDOM' ) { + return; + } + fetchDOM(); + }; + + var toggleOn = function() { + if ( enabled ) { + return; + } + enabled = true; + window.addEventListener('beforeunload', toggleOff); + inspector.addEventListener('click', onClick, true); + inspector.addEventListener('mouseover', onMouseOver, true); + tabSelector.addEventListener('change', onTabIdChanged); + onTabIdChanged(); + inspector.classList.add('enabled'); + }; + + var toggleOff = function() { + removeHighlighter(currentTabId); + window.removeEventListener('beforeunload', toggleOff); + inspector.removeEventListener('click', onClick, true); + inspector.removeEventListener('mouseover', onMouseOver, true); + tabSelector.removeEventListener('change', onTabIdChanged); + removeAllChildren(inspector); + stopTimer(); + currentTabId = currentSelector = fingerprint = ''; + enabled = false; + inspector.classList.remove('enabled'); + }; + + var toggle = function() { + if ( uDom.nodeFromId('showdom').classList.toggle('active') ) { + toggleOn(); + } else { + toggleOff(); + } + }; + + uDom('#showdom').on('click', toggle); +})(); + +/******************************************************************************/ /******************************************************************************/ var regexFromURLFilteringResult = function(result) { @@ -238,14 +676,17 @@ var filterDecompiler = (function() { } // Filter options + // Importance if ( bits & 0x02 ) { opts.push('important'); } + // Party if ( bits & 0x08 ) { opts.push('third-party'); } else if ( bits & 0x04 ) { opts.push('first-party'); } + // Type var typeVal = bits >>> 4 & 0x0F; if ( typeVal ) { opts.push(typeValToTypeName[typeVal]); @@ -552,25 +993,14 @@ var renderLogEntries = function(response) { // dynamically refreshed pages. truncateLog(maxEntries); + // Follow waterfall if not observing top of waterfall. var yDelta = tbody.offsetHeight - height; if ( yDelta === 0 ) { return; } - - // Chromium: - // body.scrollTop = good value - // body.parentNode.scrollTop = 0 - if ( document.body.scrollTop !== 0 ) { - document.body.scrollTop += yDelta; - return; - } - - // Firefox: - // body.scrollTop = 0 - // body.parentNode.scrollTop = good value - var parentNode = document.body.parentNode; - if ( parentNode && parentNode.scrollTop !== 0 ) { - parentNode.scrollTop += yDelta; + var container = uDom.nodeFromId('events'); + if ( container.scrollTop !== 0 ) { + container.scrollTop += yDelta; } }; @@ -648,7 +1078,7 @@ var truncateLog = function(size) { if ( size === 0 ) { size = 5000; } - var tbody = document.querySelector('#content tbody'); + var tbody = document.querySelector('#events tbody'); size = Math.min(size, 10000); var tr; while ( tbody.childElementCount > size ) { @@ -715,11 +1145,11 @@ var pageSelectorChanged = function() { } if ( tabClass !== '' ) { sheet.insertRule( - '#content table tr:not(.' + tabClass + ') { display: none; }', + '#events table tr:not(.' + tabClass + ') { display: none; }', 0 ); } - uDom('#refresh').toggleClass( + uDom('.needtab').toggleClass( 'disabled', tabClass === '' || tabClass === 'tab_bts' ); @@ -780,12 +1210,6 @@ var netFilteringManager = (function() { var targetPageDomain; var targetFrameDomain; - var removeAllChildren = function(node) { - while ( node.firstChild ) { - node.removeChild(node.firstChild); - } - }; - var uglyTypeFromSelector = function(pane) { var prettyType = selectValue('select.type.' + pane); if ( pane === 'static' ) { @@ -1495,10 +1919,10 @@ var rowFilterer = (function() { var filterAll = function() { // Special case: no filter if ( filters.length === 0 ) { - uDom('#content tr').removeClass('f'); + uDom('#events tr').removeClass('f'); return; } - var tbody = document.querySelector('#content tbody'); + var tbody = document.querySelector('#events tbody'); var rows = tbody.rows; var i = rows.length; while ( i-- ) { @@ -1522,8 +1946,7 @@ var rowFilterer = (function() { })(); var onFilterButton = function() { - var cl = document.body.classList; - cl.toggle('f', cl.contains('f') === false); + uDom.nodeFromId('events').classList.toggle('f'); }; uDom('#filterButton').on('click', onFilterButton); @@ -1554,7 +1977,7 @@ var toJunkyard = function(trs) { var clearBuffer = function() { var tabId = uDom.nodeFromId('pageSelector').value || null; - var tbody = document.querySelector('#content tbody'); + var tbody = document.querySelector('#events tbody'); var tr = tbody.lastElementChild; var trPrevious; while ( tr !== null ) { @@ -1577,7 +2000,7 @@ var clearBuffer = function() { /******************************************************************************/ var cleanBuffer = function() { - var rows = uDom('#content tr.tab:not(.canMtx)').remove(); + var rows = uDom('#events tr.tab:not(.canMtx)').remove(); var i = rows.length; while ( i-- ) { trJunkyard.push(rows.nodeAt(i)); @@ -1588,10 +2011,7 @@ var cleanBuffer = function() { /******************************************************************************/ var toggleCompactView = function() { - document.body.classList.toggle( - 'compactView', - document.body.classList.contains('compactView') === false - ); + uDom.nodeFromId('events').classList.toggle('compactView'); }; /******************************************************************************/ @@ -1604,7 +2024,7 @@ var popupManager = (function() { var popupObserver = null; var style = null; var styleTemplate = [ - '#content tr:not(.tab_{{tabId}}) {', + '#events tr:not(.tab_{{tabId}}) {', 'cursor: not-allowed;', 'opacity: 0.2;', '}' @@ -1659,11 +2079,15 @@ var popupManager = (function() { style = uDom.nodeFromId('popupFilterer'); style.textContent = styleTemplate.replace('{{tabId}}', localTabId); - document.body.classList.add('popupOn'); + var parent = uDom.nodeFromId('events'); + var rect = parent.getBoundingClientRect(); + container.style.setProperty('top', rect.top + 'px'); + container.style.setProperty('right', (rect.right - parent.clientWidth) + 'px'); + parent.classList.add('popupOn'); }; var toggleOff = function() { - document.body.classList.remove('popupOn'); + uDom.nodeFromId('events').classList.remove('popupOn'); container.querySelector('div > span:nth-of-type(1)').removeEventListener('click', toggleSize); container.querySelector('div > span:nth-of-type(2)').removeEventListener('click', toggleOff); @@ -1714,9 +2138,9 @@ uDom.onLoad(function() { uDom('#clean').on('click', cleanBuffer); uDom('#clear').on('click', clearBuffer); uDom('#maxEntries').on('change', onMaxEntriesChanged); - uDom('#content table').on('click', 'tr.canMtx > td:nth-of-type(2)', popupManager.toggleOn); - uDom('#content').on('click', 'tr.cat_net > td:nth-of-type(4)', netFilteringManager.toggleOn); - uDom('#content').on('click', 'tr.canLookup > td:nth-of-type(3)', reverseLookupManager.toggleOn); + uDom('#events table').on('click', 'tr.canMtx > td:nth-of-type(2)', popupManager.toggleOn); + uDom('#events').on('click', 'tr.canLookup > td:nth-of-type(3)', reverseLookupManager.toggleOn); + uDom('#events').on('click', 'tr.cat_net > td:nth-of-type(4)', netFilteringManager.toggleOn); // https://github.com/gorhill/uBlock/issues/404 // Ensure page state is in sync with the state of its various widgets. diff --git a/src/js/messaging.js b/src/js/messaging.js index 215fbd389..41e563bca 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -62,10 +62,6 @@ var onMessage = function(request, sender, callback) { µb.assets.get(request.url, callback); return; - case 'reloadAllFilters': - µb.reloadAllFilters(callback); - return; - case 'listsFromNetFilter': µb.staticFilteringReverseLookup.fromNetFilter( request.compiledFilter, @@ -82,6 +78,14 @@ var onMessage = function(request, sender, callback) { ); return; + case 'reloadAllFilters': + µb.reloadAllFilters(callback); + return; + + case 'scriptlet': + µb.scriptlets.inject(request.tabId, request.scriptlet, callback); + return; + default: break; } @@ -137,10 +141,6 @@ var onMessage = function(request, sender, callback) { vAPI.tabs.open(request.details); break; - case 'scriptletGotoImageURL': - µb.scriptletGotoImageURL(request); - break; - case 'reloadTab': if ( vAPI.isBehindTheSceneTabId(request.tabId) === false ) { vAPI.tabs.reload(request.tabId); @@ -150,10 +150,18 @@ var onMessage = function(request, sender, callback) { } break; + case 'scriptletResponse': + µb.scriptlets.report(tabId, request.scriptlet, request.response); + break; + case 'selectFilterLists': µb.selectFilterLists(request.switches); break; + case 'sendMessageTo': + vAPI.messaging.send(request.tabId, request.channelName, request.msg); + break; + case 'toggleHostnameSwitch': µb.toggleHostnameSwitch(request); break; @@ -350,7 +358,7 @@ var getPopupDataLazy = function(tabId, callback) { return; } - µb.surveyCosmeticFilters(tabId, function() { + µb.scriptlets.inject(tabId, 'cosmetic-survey', function() { r.hiddenElementCount = pageStore.hiddenElementCount; callback(r); }); @@ -1373,6 +1381,8 @@ var logCosmeticFilters = function(tabId, details) { /******************************************************************************/ var onMessage = function(request, sender, callback) { + var tabId = sender && sender.tab ? sender.tab.id : 0; + // Async switch ( request.what ) { default: @@ -1381,13 +1391,8 @@ var onMessage = function(request, sender, callback) { // Sync var response; - var tabId = sender && sender.tab ? sender.tab.id : 0; switch ( request.what ) { - case 'gotoImageURL': - response = µb.scriptlets.gotoImageURL; - break; - case 'liveCosmeticFilteringData': var pageStore = µb.pageStoreFromTabId(tabId); if ( pageStore ) { @@ -1412,4 +1417,40 @@ vAPI.messaging.listen('scriptlets', onMessage); })(); + +/******************************************************************************/ +/******************************************************************************/ + +// devtools + +(function() { + +'use strict'; + +/******************************************************************************/ + +var onMessage = function(request, sender, callback) { + // Async + switch ( request.what ) { + default: + break; + } + + // Sync + var response; + + switch ( request.what ) { + default: + return vAPI.messaging.UNHANDLED; + } + + callback(response); +}; + +vAPI.messaging.listen('devtools', onMessage); + +/******************************************************************************/ + +})(); + /******************************************************************************/ diff --git a/src/js/pagestore.js b/src/js/pagestore.js index 378072cd6..00428a3ee 100644 --- a/src/js/pagestore.js +++ b/src/js/pagestore.js @@ -316,11 +316,13 @@ PageStore.prototype.init = function(tabId) { 'cosmetic-filtering' ); if ( this.skipCosmeticFiltering && µb.logger.isEnabled() ) { + // https://github.com/gorhill/uBlock/issues/370 + // Log using `cosmetic-filtering`, not `elemhide`. µb.logger.writeOne( tabId, 'net', µb.staticNetFilteringEngine.toResultString(true), - 'elemhide', + 'cosmetic-filtering', tabContext.rawURL, this.tabHostname, this.tabHostname diff --git a/src/js/scriptlets/cosmetic-logger.js b/src/js/scriptlets/cosmetic-logger.js index 8ef5d987d..bd323768d 100644 --- a/src/js/scriptlets/cosmetic-logger.js +++ b/src/js/scriptlets/cosmetic-logger.js @@ -31,13 +31,11 @@ // https://github.com/gorhill/uBlock/issues/464 if ( document instanceof HTMLDocument === false ) { - //console.debug('cosmetic-logger.js > not a HTLMDocument'); return; } // This can happen if ( typeof vAPI !== 'object' ) { - //console.debug('cosmetic-logger.js > vAPI not found'); return; } @@ -88,6 +86,7 @@ localMessager.send({ matchedSelectors: matchedSelectors }, function() { localMessager.close(); + localMessager = null; }); /******************************************************************************/ diff --git a/src/js/scriptlets/cosmetic-off.js b/src/js/scriptlets/cosmetic-off.js index e5d37b498..1983e9174 100644 --- a/src/js/scriptlets/cosmetic-off.js +++ b/src/js/scriptlets/cosmetic-off.js @@ -36,7 +36,7 @@ if ( document instanceof HTMLDocument === false ) { } // This can happen -if ( !vAPI ) { +if ( typeof vAPI !== 'object' ) { //console.debug('cosmetic-off.js > no vAPI'); return; } diff --git a/src/js/scriptlets/cosmetic-on.js b/src/js/scriptlets/cosmetic-on.js index 90ae71d4a..be9f4c142 100644 --- a/src/js/scriptlets/cosmetic-on.js +++ b/src/js/scriptlets/cosmetic-on.js @@ -36,7 +36,7 @@ if ( document instanceof HTMLDocument === false ) { } // This can happen -if ( !vAPI ) { +if ( typeof vAPI !== 'object' ) { //console.debug('cosmetic-on.js > no vAPI'); return; } diff --git a/src/js/scriptlets/dom-fingerprint.js b/src/js/scriptlets/dom-fingerprint.js new file mode 100644 index 000000000..05a9fb9e4 --- /dev/null +++ b/src/js/scriptlets/dom-fingerprint.js @@ -0,0 +1,66 @@ +/******************************************************************************* + + 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, HTMLDocument */ + +/******************************************************************************/ + +(function() { + +'use strict'; + +/******************************************************************************/ + +// https://github.com/gorhill/uBlock/issues/464 +if ( document instanceof HTMLDocument === false ) { + return; +} + +// This can happen +if ( typeof vAPI !== 'object' ) { + return; +} + +/******************************************************************************/ + +// Some kind of fingerprint for the DOM, without incurring too much overhead. + +var url = window.location.href; +var pos = url.indexOf('#'); +if ( pos !== -1 ) { + url = url.slice(0, pos); +} +var fingerprint = url + '{' + document.getElementsByTagName('*').length.toString() + '}'; + +var localMessager = vAPI.messaging.channel('scriptlets'); +localMessager.send({ + what: 'scriptletResponse', + scriptlet: 'dom-fingerprint', + response: fingerprint +}, function() { + localMessager.close(); +}); + +/******************************************************************************/ + +})(); + +/******************************************************************************/ diff --git a/src/js/scriptlets/dom-highlight.js b/src/js/scriptlets/dom-highlight.js new file mode 100644 index 000000000..1b4c981cf --- /dev/null +++ b/src/js/scriptlets/dom-highlight.js @@ -0,0 +1,339 @@ +/******************************************************************************* + + 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, HTMLDocument */ + +/******************************************************************************/ + +(function() { + +'use strict'; + +/******************************************************************************/ + +// https://github.com/gorhill/uBlock/issues/464 +if ( document instanceof HTMLDocument === false ) { + return; +} + +// This can happen +if ( typeof vAPI !== 'object' ) { + return; +} + +/******************************************************************************/ + +if ( document.querySelector('iframe.dom-highlight.' + vAPI.sessionId) !== null ) { + return; +} + +/******************************************************************************/ + +var localMessager = null; +var svgOcean = null; +var svgIslands = null; +var svgRoot = null; +var pickerRoot = null; +var currentSelector = ''; + +var toggledNodes = new Map(); + +/******************************************************************************/ + +var highlightElements = function(elems, scrollTo) { + var wv = pickerRoot.contentWindow.innerWidth; + var hv = pickerRoot.contentWindow.innerHeight; + var ocean = ['M0 0h' + wv + 'v' + hv + 'h-' + wv, 'z']; + var islands = []; + var elem, rect, poly; + var xl, xr, yt, yb, w, h, ws; + var xlu = Number.MAX_VALUE, xru = 0, ytu = Number.MAX_VALUE, ybu = 0; + + for ( var i = 0; i < elems.length; i++ ) { + elem = elems[i]; + if ( elem === pickerRoot ) { + continue; + } + if ( typeof elem.getBoundingClientRect !== 'function' ) { + continue; + } + + rect = elem.getBoundingClientRect(); + xl = rect.left; + xr = rect.right; + w = rect.width; + yt = rect.top; + yb = rect.bottom; + h = rect.height; + + ws = w.toFixed(1); + poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) + + 'h' + ws + + 'v' + h.toFixed(1) + + 'h-' + ws + + 'z'; + ocean.push(poly); + islands.push(poly); + + if ( !scrollTo ) { + continue; + } + + if ( xl < xlu ) { xlu = xl; } + if ( xr > xru ) { xru = xr; } + if ( yt < ytu ) { ytu = yt; } + if ( yb > ybu ) { ybu = yb; } + } + svgOcean.setAttribute('d', ocean.join('')); + svgIslands.setAttribute('d', islands.join('') || 'M0 0'); + + if ( !scrollTo ) { + return; + } + + // Highlighted area completely within viewport + if ( xlu >= 0 && xru <= wv && ytu >= 0 && ybu <= hv ) { + return; + } + + var dx = 0, dy = 0; + + if ( xru > wv ) { + dx = xru - wv; + xlu -= dx; + } + if ( xlu < 0 ) { + dx += xlu; + } + if ( ybu > hv ) { + dy = ybu - hv; + ytu -= dy; + } + if ( ytu < 0 ) { + dy += ytu; + } + + if ( dx !== 0 || dy !== 0 ) { + window.scrollBy(dx, dy); + } +}; + +/******************************************************************************/ + +var elementsFromSelector = function(filter) { + var out = []; + try { + out = document.querySelectorAll(filter); + } catch (ex) { + } + return out; +}; + +/******************************************************************************/ + +var highlight = function(scrollTo) { + var elements = elementsFromSelector(currentSelector); + highlightElements(elements, scrollTo); +}; + +/******************************************************************************/ + +var onScrolled = function() { + highlight(); +}; + +/******************************************************************************/ + +// original, target = what to do +// any, any = restore saved display property +// any, hidden = set display to `none`, remember original state +// 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 i = nodes.length; + if ( i === 0 ) { + return; + } + var node, value; + while ( i-- ) { + node = nodes[i]; + if ( originalState ) { // any, ? + if ( targetState ) { // any, any + value = toggledNodes.get(node); + if ( value === undefined ) { + continue; + } + if ( value !== null ) { + node.style.removeProperty('display'); + } else { + node.style.setProperty('display', value); + } + toggledNodes.delete(node); + } else { // any, hidden + toggledNodes.set(node, node.style.getPropertyValue('display') || null); + node.style.setProperty('display', 'none'); + } + } else { // hidden, ? + if ( targetState ) { // hidden, any + node.style.setProperty('display', 'initial', 'important'); + } else { // hidden, hidden + node.style.setProperty('display', 'none', 'important'); + } + } + } +}; + +/******************************************************************************/ + +var resetToggledNodes = function() { + // Chromium does not support destructuring as of v43. + for ( var node of toggledNodes.keys() ) { + value = toggledNodes.get(node); + if ( value !== null ) { + node.style.removeProperty('display'); + } else { + node.style.setProperty('display', value); + } + } + toggledNodes.clear(); +}; + +/******************************************************************************/ + +var shutdown = function() { + resetToggledNodes(); + localMessager.removeListener(onMessage); + localMessager.close(); + localMessager = null; + window.removeEventListener('scroll', onScrolled, true); + document.documentElement.removeChild(pickerRoot); + pickerRoot = svgRoot = svgOcean = svgIslands = null; + currentSelector = ''; +}; + +/******************************************************************************/ + +var onMessage = function(msg) { + if ( msg.what !== 'dom-highlight' ) { + return; + } + switch ( msg.action ) { + case 'highlight': + currentSelector = msg.selector; + highlight(msg.scrollTo); + break; + + case 'toggleNodes': + toggleNodes(msg.selector, msg.original, msg.target); + currentSelector = msg.selector; + highlight(true); + break; + + case 'shutdown': + shutdown(); + break; + + default: + break; + } +}; + +/******************************************************************************/ + +(function() { + pickerRoot = document.createElement('iframe'); + pickerRoot.classList.add(vAPI.sessionId); + pickerRoot.classList.add('dom-highlight'); + pickerRoot.style.cssText = [ + 'background: transparent', + 'border: 0', + 'border-radius: 0', + 'box-shadow: none', + 'display: block', + 'height: 100%', + 'left: 0', + 'margin: 0', + 'opacity: 1', + 'position: fixed', + 'outline: 0', + 'padding: 0', + 'top: 0', + 'visibility: visible', + 'width: 100%', + 'z-index: 2147483647', + '' + ].join(' !important;\n'); + + pickerRoot.onload = function() { + pickerRoot.onload = null; + var pickerDoc = this.contentDocument; + + var style = pickerDoc.createElement('style'); + style.textContent = [ + 'body {', + 'background-color: transparent;', + 'cursor: crosshair;', + '}', + 'svg {', + 'height: 100%;', + 'left: 0;', + 'position: fixed;', + 'top: 0;', + 'width: 100%;', + '}', + 'svg > path:first-child {', + 'fill: rgba(0,0,0,0.75);', + 'fill-rule: evenodd;', + '}', + 'svg > path + path {', + 'fill: rgba(0,0,255,0.1);', + 'stroke: #FFF;', + 'stroke-width: 0.5px;', + '}', + '' + ].join('\n'); + pickerDoc.body.appendChild(style); + + svgRoot = pickerDoc.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svgOcean = pickerDoc.createElementNS('http://www.w3.org/2000/svg', 'path'); + svgRoot.appendChild(svgOcean); + svgIslands = pickerDoc.createElementNS('http://www.w3.org/2000/svg', 'path'); + svgRoot.appendChild(svgIslands); + pickerDoc.body.appendChild(svgRoot); + + window.addEventListener('scroll', onScrolled, true); + + localMessager = vAPI.messaging.channel('scriptlets'); + localMessager.addListener(onMessage); + + highlight(); + }; + + document.documentElement.appendChild(pickerRoot); +})(); + +/******************************************************************************/ + +})(); + +/******************************************************************************/ diff --git a/src/js/scriptlets/dom-layout.js b/src/js/scriptlets/dom-layout.js new file mode 100644 index 000000000..1c22de848 --- /dev/null +++ b/src/js/scriptlets/dom-layout.js @@ -0,0 +1,406 @@ +/******************************************************************************* + + 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, HTMLDocument */ + +/******************************************************************************/ +/******************************************************************************/ + +/*! http://mths.be/cssescape v0.2.1 by @mathias | MIT license */ +;(function(root) { + + 'use strict'; + + if (!root.CSS) { + root.CSS = {}; + } + + var CSS = root.CSS; + + var InvalidCharacterError = function(message) { + this.message = message; + }; + InvalidCharacterError.prototype = new Error(); + InvalidCharacterError.prototype.name = 'InvalidCharacterError'; + + if (!CSS.escape) { + // http://dev.w3.org/csswg/cssom/#serialize-an-identifier + CSS.escape = function(value) { + var string = String(value); + var length = string.length; + var index = -1; + var codeUnit; + var result = ''; + var firstCodeUnit = string.charCodeAt(0); + while (++index < length) { + codeUnit = string.charCodeAt(index); + // Note: there’s no need to special-case astral symbols, surrogate + // pairs, or lone surrogates. + + // If the character is NULL (U+0000), then throw an + // `InvalidCharacterError` exception and terminate these steps. + if (codeUnit === 0x0000) { + throw new InvalidCharacterError( + 'Invalid character: the input contains U+0000.' + ); + } + + if ( + // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is + // U+007F, […] + (codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F || + // If the character is the first character and is in the range [0-9] + // (U+0030 to U+0039), […] + (index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) || + // If the character is the second character and is in the range [0-9] + // (U+0030 to U+0039) and the first character is a `-` (U+002D), […] + ( + index == 1 && + codeUnit >= 0x0030 && codeUnit <= 0x0039 && + firstCodeUnit == 0x002D + ) + ) { + // http://dev.w3.org/csswg/cssom/#escape-a-character-as-code-point + result += '\\' + codeUnit.toString(16) + ' '; + continue; + } + + // If the character is not handled by one of the above rules and is + // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or + // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to + // U+005A), or [a-z] (U+0061 to U+007A), […] + if ( + codeUnit >= 0x0080 || + codeUnit == 0x002D || + codeUnit == 0x005F || + codeUnit >= 0x0030 && codeUnit <= 0x0039 || + codeUnit >= 0x0041 && codeUnit <= 0x005A || + codeUnit >= 0x0061 && codeUnit <= 0x007A + ) { + // the character itself + result += string.charAt(index); + continue; + } + + // Otherwise, the escaped character. + // http://dev.w3.org/csswg/cssom/#escape-a-character + result += '\\' + string.charAt(index); + + } + return result; + }; + } + +}(self)); + +/******************************************************************************/ +/******************************************************************************/ + +(function() { + +'use strict'; + +/******************************************************************************/ + +// https://github.com/gorhill/uBlock/issues/464 +if ( document instanceof HTMLDocument === false ) { + return; +} + +// This can happen +if ( typeof vAPI !== 'object' ) { + return; +} + +/******************************************************************************/ + +var skipTagNames = { + 'br': true, + 'link': true, + 'meta': true, + 'script': true, + 'style': true +}; + +var resourceAttrNames = { + 'a': 'href', + 'iframe': 'src', + 'img': 'src', + 'object': 'data' +}; + +/******************************************************************************/ + +// Collect all nodes which are directly affected by cosmetic filters: these +// will be reported in the layout data. + +var nodeToCosmeticFilterMap = (function() { + var out = new WeakMap(); + var styleTags = vAPI.styles || []; + var i = styleTags.length; + var selectors, styleText, j, selector, nodes, k; + while ( i-- ) { + styleText = styleTags[i].textContent; + selectors = styleText.slice(0, styleText.lastIndexOf('\n')).split(/,\n/); + j = selectors.length; + while ( j-- ) { + selector = selectors[j]; + nodes = document.querySelectorAll(selector); + k = nodes.length; + while ( k-- ) { + out.set(nodes[k], selector); + } + } + } + return out; +})(); + +/******************************************************************************/ + +var DomRoot = function() { + this.lvl = 0; + this.sel = 'body'; + var url = window.location.href; + var pos = url.indexOf('#'); + if ( pos !== -1 ) { + url = url.slice(0, pos); + } + this.src = url; + this.top = window === window.top; + this.cnt = 0; + this.fp = fingerprint(); +}; + +var DomNode = function(level, selector, filter) { + this.lvl = level; + this.sel = selector; + this.cnt = 0; + this.filter = filter; +}; + +/******************************************************************************/ + +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 matchesSelector = (function() { + if ( typeof Element.prototype.matches === 'function' ) { + return 'matches'; + } + if ( typeof Element.prototype.mozMatchesSelector === 'function' ) { + return 'mozMatchesSelector'; + } + if ( typeof Element.prototype.webkitMatchesSelector === 'function' ) { + return 'webkitMatchesSelector'; + } + return ''; +})(); + +/******************************************************************************/ + +var selectorFromNode = function(node) { + var str, attr, pos, sw, i; + var tag = node.localName; + var selector = CSS.escape(tag); + // Id + if ( typeof node.id === 'string' ) { + str = node.id.trim(); + if ( str !== '' ) { + selector += '#' + CSS.escape(str); + } + } + // Class + var cl = node.classList; + if ( cl ) { + for ( i = 0; i < cl.length; i++ ) { + selector += '.' + CSS.escape(cl[i]); + } + } + // Tag-specific attributes + if ( resourceAttrNames.hasOwnProperty(tag) ) { + attr = resourceAttrNames[tag]; + str = node.getAttribute(attr) || ''; + str = str.trim(); + pos = str.indexOf('#'); + if ( pos !== -1 ) { + str = str.slice(0, pos); + sw = '^'; + } else { + sw = ''; + } + if ( str !== '' ) { + selector += '[' + attr + sw + '="' + CSS.escape(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; +}; + +/******************************************************************************/ + +var domNodeFactory = function(level, node) { + var localName = node.localName; + if ( skipTagNames.hasOwnProperty(localName) ) { + return null; + } + // skip uBlock's own nodes + if ( node.classList.contains(vAPI.sessionId) ) { + return null; + } + if ( level === 0 && localName === 'body' ) { + return new DomRoot(); + } + var selector = selectorFromNode(node); + var filter = nodeToCosmeticFilterMap.get(node); + return new DomNode(level, selector, filter); +}; + +/******************************************************************************/ + +// Some kind of fingerprint for the DOM, without incurring too much +// overhead. + +var fingerprint = function() { + var url = window.location.href; + var pos = url.indexOf('#'); + if ( pos !== -1 ) { + url = url.slice(0, pos); + } + return url + '{' + document.getElementsByTagName('*').length.toString() + '}'; +}; + +/******************************************************************************/ + +// Collect layout data. + +var domLayout = []; + +(function() { + var dom = domLayout; + var stack = []; + var node = document.body; + var domNode; + var lvl = 0; + + for (;;) { + domNode = domNodeFactory(lvl, node); + if ( domNode !== null ) { + dom.push(domNode); + } + // children + if ( node.firstElementChild !== null ) { + stack.push(node); + lvl += 1; + node = node.firstElementChild; + continue; + } + // sibling + if ( node.nextElementSibling === null ) { + do { + node = stack.pop(); + if ( !node ) { break; } + lvl -= 1; + } while ( node.nextElementSibling === null ); + if ( !node ) { break; } + } + node = node.nextElementSibling; + } +})(); + +/******************************************************************************/ + +// Descendant count for each node. + +(function() { + var dom = domLayout; + var stack = [], ptr; + var lvl = 0; + var domNode, cnt; + var i = dom.length; + + while ( i-- ) { + domNode = dom[i]; + if ( domNode.lvl === lvl ) { + stack[ptr] += 1; + continue; + } + if ( domNode.lvl > lvl ) { + while ( lvl < domNode.lvl ) { + stack.push(0); + lvl += 1; + } + ptr = lvl - 1; + stack[ptr] += 1; + continue; + } + // domNode.lvl < lvl + cnt = stack.pop(); + domNode.cnt = cnt; + lvl -= 1; + ptr = lvl - 1; + stack[ptr] += cnt + 1; + } +})(); + +/******************************************************************************/ + +var localMessager = vAPI.messaging.channel('scriptlets'); +localMessager.send({ + what: 'scriptletResponse', + scriptlet: 'dom-layout', + response: domLayout +}, function() { + localMessager.close(); + localMessager = null; +}); + +/******************************************************************************/ + +})(); + +/******************************************************************************/ diff --git a/src/js/ublock.js b/src/js/ublock.js index eeb5e0577..1b4ddd81a 100644 --- a/src/js/ublock.js +++ b/src/js/ublock.js @@ -296,7 +296,7 @@ var matchWhitelistDirective = function(url, hostname, directive) { return; } this.epickerTarget = targetElement || ''; - vAPI.tabs.injectScript(tabId, { file: 'js/scriptlets/element-picker.js' }); + this.scriptlets.inject(tabId, 'element-picker'); if ( typeof vAPI.tabs.select === 'function' ) { vAPI.tabs.select(tabId); } @@ -378,10 +378,10 @@ var matchWhitelistDirective = function(url, hostname, directive) { // Take action if needed if ( details.name === 'no-cosmetic-filtering' ) { - vAPI.tabs.injectScript(details.tabId, { - file: 'js/scriptlets/cosmetic-' + (details.state ? 'off' : 'on') + '.js', - allFrames: true - }); + this.scriptlets.injectDeep( + details.tabId, + details.state ? 'cosmetic-off' : 'cosmetic-on' + ); return; } @@ -391,23 +391,12 @@ var matchWhitelistDirective = function(url, hostname, directive) { /******************************************************************************/ -µBlock.surveyCosmeticFilters = function(tabId, callback) { - callback = callback || this.noopFunc; - if ( vAPI.isBehindTheSceneTabId(tabId) ) { - callback(); - return; - } - vAPI.tabs.injectScript(tabId, { file: 'js/scriptlets/cosmetic-survey.js' }, callback); -}; - -/******************************************************************************/ - µBlock.logCosmeticFilters = (function() { var tabIdToTimerMap = {}; var injectNow = function(tabId) { delete tabIdToTimerMap[tabId]; - vAPI.tabs.injectScript(tabId, { file: 'js/scriptlets/cosmetic-logger.js' }); + µBlock.scriptlets.inject(tabId, 'cosmetic-logger'); }; var injectAsync = function(tabId) { @@ -425,13 +414,67 @@ var matchWhitelistDirective = function(url, hostname, directive) { /******************************************************************************/ -µBlock.scriptletGotoImageURL = function(details) { - if ( vAPI.isBehindTheSceneTabId(details.tabId) ) { - return; - } - this.scriptlets.gotoImageURL = details.url; - vAPI.tabs.injectScript(details.tabId, { file: 'js/scriptlets/goto-img.js' }); -}; +µBlock.scriptlets = (function() { + var pendingEntries = Object.create(null); + + var Entry = function(tabId, scriptlet, callback) { + this.tabId = tabId; + this.scriptlet = scriptlet; + this.callback = callback; + this.timer = vAPI.setTimeout(this.service.bind(this), 1000); + }; + + Entry.prototype.service = function(response) { + if ( this.timer !== null ) { + clearTimeout(this.timer); + } + delete pendingEntries[makeKey(this.tabId, this.scriptlet)]; + this.callback(response); + }; + + var makeKey = function(tabId, scriptlet) { + return tabId + ' ' + scriptlet; + }; + + var report = function(tabId, scriptlet, response) { + var key = makeKey(tabId, scriptlet); + var entry = pendingEntries[key]; + if ( entry === undefined ) { + return; + } + entry.service(response); + }; + + var inject = function(tabId, scriptlet, callback) { + if ( typeof callback === 'function' ) { + if ( vAPI.isBehindTheSceneTabId(tabId) ) { + callback(); + return; + } + var key = makeKey(tabId, scriptlet); + if ( pendingEntries[key] !== undefined ) { + callback(); + return; + } + pendingEntries[key] = new Entry(tabId, scriptlet, callback); + } + vAPI.tabs.injectScript(tabId, { file: 'js/scriptlets/' + scriptlet + '.js' }); + }; + + // TODO: think about a callback mechanism. + var injectDeep = function(tabId, scriptlet) { + vAPI.tabs.injectScript(tabId, { + file: 'js/scriptlets/' + scriptlet + '.js', + allFrames: true + }); + }; + + return { + inject: inject, + injectDeep: injectDeep, + report: report + }; +})(); /******************************************************************************/ diff --git a/src/logger-ui.html b/src/logger-ui.html index 0ed161d06..5f9fc1f86 100644 --- a/src/logger-ui.html +++ b/src/logger-ui.html @@ -6,37 +6,42 @@ - + -
+
- -
-
- - - - - + + + +
+
+ +
+
+ +
+
+
+ + + + +
- -
-
-
- -
-
+ +
+
+