From 806032cdc4f188c23a038efb97f2c24758360472 Mon Sep 17 00:00:00 2001 From: gorhill Date: Mon, 20 Nov 2017 08:42:32 -0500 Subject: [PATCH] improve DOM inspector - Fix regressions reported in #3159 - Fix #2001 - Fix some item points in #407 --- platform/chromium/vapi-background.js | 143 +++---- platform/chromium/vapi-client.js | 339 ++++++++++++----- platform/firefox/vapi-background.js | 111 +----- platform/firefox/vapi-client.js | 130 +++---- platform/webext/vapi-usercss.js | 2 +- src/css/logger-ui-inspector.css | 44 ++- src/css/logger-ui.css | 23 +- src/js/logger-ui-inspector.js | 397 ++++++++------------ src/js/logger-ui.js | 18 +- src/js/scriptlets/dom-inspector.js | 542 +++++++++------------------ src/js/ublock.js | 4 +- src/logger-ui.html | 10 +- 12 files changed, 744 insertions(+), 1019 deletions(-) diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index 62b37f0cc1394..b8d19bc27de39 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -735,32 +735,21 @@ vAPI.messaging.listen = function(listenerName, callback) { vAPI.messaging.onPortMessage = (function() { var messaging = vAPI.messaging, - toAuxPending = new Map(); - - var supportsUserStylesheets = vAPI.supportsUserStylesheets; + supportsUserStylesheets = vAPI.supportsUserStylesheets; // Use a wrapper to avoid closure and to allow reuse. - var CallbackWrapper = function(port, request, timeout) { + var CallbackWrapper = function(port, request) { this.callback = this.proxy.bind(this); // bind once - this.init(port, request, timeout); + this.init(port, request); }; CallbackWrapper.prototype = { - timerId: null, - init: function(port, request, timeout) { + init: function(port, request) { this.port = port; this.request = request; - if ( timeout !== undefined ) { - this.timerId = vAPI.setTimeout(this.callback, timeout); - } return this; }, proxy: function(response) { - if ( this.timerId !== null ) { - clearTimeout(this.timerId); - toAuxPending.delete(this.timerId); - this.timerId = null; - } // https://github.com/chrisaljoudi/uBlock/issues/383 if ( messaging.ports.has(this.port.name) ) { this.port.postMessage({ @@ -777,67 +766,53 @@ vAPI.messaging.onPortMessage = (function() { var callbackWrapperJunkyard = []; - var callbackWrapperFactory = function(port, request, timeout) { + var callbackWrapperFactory = function(port, request) { var wrapper = callbackWrapperJunkyard.pop(); if ( wrapper ) { - return wrapper.init(port, request, timeout); - } - return new CallbackWrapper(port, request, timeout); - }; - - var toAux = function(details, portFrom) { - var port, portTo, - chromiumTabId = toChromiumTabId(details.toTabId); - - // TODO: This could be an issue with a lot of tabs: easy to address - // with a port name to tab id map. - // When sending to an auxiliary process, the target is always the - // port associated with the root frame. - for ( var entry of messaging.ports ) { - port = entry[1]; - if ( port.sender.frameId === 0 && port.sender.tab.id === chromiumTabId ) { - portTo = port; - break; - } - } - - var wrapper; - if ( details.auxProcessId !== undefined ) { - wrapper = callbackWrapperFactory(portFrom, details, 1023); + return wrapper.init(port, request); } - - // Destination not found: - if ( portTo === undefined ) { - if ( wrapper !== undefined ) { - wrapper.callback(); - } - return; - } - - if ( wrapper !== undefined ) { - toAuxPending.set(wrapper.timerId, wrapper); - } - - portTo.postMessage({ - mainProcessId: wrapper && wrapper.timerId, - channelName: details.toChannel, - msg: details.msg - }); - }; - - var toAuxResponse = function(details) { - var mainProcessId = details.mainProcessId; - if ( mainProcessId === undefined ) { return; } - var wrapper = toAuxPending.get(mainProcessId); - if ( wrapper === undefined ) { return; } - toAuxPending.delete(mainProcessId); - wrapper.callback(details.msg); + return new CallbackWrapper(port, request); }; - var toFramework = function(msg, sender) { - var tabId = sender && sender.tab && sender.tab.id; + var toFramework = function(request, port) { + var sender = port && port.sender; + if ( !sender ) { return; } + var tabId = sender.tab && sender.tab.id; if ( !tabId ) { return; } + var msg = request.msg, + toPort; switch ( msg.what ) { + case 'connectionAccepted': + case 'connectionRefused': + toPort = messaging.ports.get(msg.fromToken); + if ( toPort !== undefined ) { + msg.tabId = tabId.toString(); + toPort.postMessage(request); + } else { + msg.what = 'connectionBroken'; + port.postMessage(request); + } + break; + case 'connectionRequested': + msg.tabId = '' + tabId.toString(); + for ( toPort of messaging.ports.values() ) { + toPort.postMessage(request); + } + break; + case 'connectionBroken': + case 'connectionCheck': + case 'connectionMessage': + toPort = messaging.ports.get( + port.name === msg.fromToken ? msg.toToken : msg.fromToken + ); + if ( toPort !== undefined ) { + msg.tabId = tabId.toString(); + toPort.postMessage(request); + } else { + msg.what = 'connectionBroken'; + port.postMessage(request); + } + break; case 'userCSS': var details = { code: undefined, @@ -863,23 +838,23 @@ vAPI.messaging.onPortMessage = (function() { } }; - return function(request, port) { - // Auxiliary process to auxiliary process - if ( request.toTabId !== undefined ) { - toAux(request, port); - return; - } - - // Auxiliary process to auxiliary process: response - if ( request.mainProcessId !== undefined ) { - toAuxResponse(request); - return; + // https://bugzilla.mozilla.org/show_bug.cgi?id=1392067 + // Workaround: manually remove ports matching removed tab. + chrome.tabs.onRemoved.addListener(function(tabId) { + for ( var port of messaging.ports.values() ) { + var tab = port.sender && port.sender.tab; + if ( !tab ) { continue; } + if ( tab.id === tabId ) { + vAPI.messaging.onPortDisconnect(port); + } } + }); + return function(request, port) { // Content process to main process: framework handler. // No callback supported/needed for now. - if ( request.channelName === 'vapi-background' ) { - toFramework(request.msg, port.sender); + if ( request.channelName === 'vapi' ) { + toFramework(request, port); return; } @@ -952,8 +927,8 @@ vAPI.messaging.broadcast = function(message) { broadcast: true, msg: message }; - for ( var entry of this.ports ) { - entry[1].postMessage(messageWrapper); + for ( var port of this.ports.values() ) { + port.postMessage(messageWrapper); } }; diff --git a/platform/chromium/vapi-client.js b/platform/chromium/vapi-client.js index 1cd30146c528b..0dfbd0dacf9ab 100644 --- a/platform/chromium/vapi-client.js +++ b/platform/chromium/vapi-client.js @@ -71,13 +71,35 @@ vAPI.messaging = { port: null, portTimer: null, portTimerDelay: 10000, - channels: Object.create(null), - channelCount: 0, - pending: Object.create(null), - pendingCount: 0, + channels: new Map(), + connections: new Map(), + pending: new Map(), auxProcessId: 1, shuttingDown: false, + Connection: function(handler, details) { + var messaging = vAPI.messaging; + this.messaging = messaging; + this.handler = handler; + this.id = details.id; + this.to = details.to; + this.toToken = details.toToken; + this.from = details.from; + this.fromToken = details.fromToken; + this.checkBound = this.check.bind(this); + this.checkTimer = undefined; + // On Firefox it appears ports are not automatically disconnected when + // navigating to another page. + if ( messaging.Connection.pagehide !== undefined ) { return; } + messaging.Connection.pagehide = function() { + for ( var connection of this.connections.values() ) { + connection.disconnect(); + connection.handler(connection.toDetails('connectionBroken')); + } + }.bind(messaging); + window.addEventListener('pagehide', messaging.Connection.pagehide); + }, + shutdown: function() { this.shuttingDown = true; this.destroyPort(); @@ -87,46 +109,65 @@ vAPI.messaging = { this.port = null; vAPI.shutdown.exec(); }, - disconnectListenerCallback: null, + disconnectListenerBound: null, messageListener: function(details) { - if ( !details ) { - return; - } + if ( !details ) { return; } // Sent to all channels - if ( details.broadcast === true && !details.channelName ) { - for ( var channelName in this.channels ) { + if ( details.broadcast ) { + for ( var channelName of this.channels.keys() ) { this.sendToChannelListeners(channelName, details.msg); } return; } // Response to specific message previously sent + var listener; if ( details.auxProcessId ) { - var listener = this.pending[details.auxProcessId]; - delete this.pending[details.auxProcessId]; - delete details.auxProcessId; // TODO: why? - if ( listener ) { - this.pendingCount -= 1; + listener = this.pending.get(details.auxProcessId); + if ( listener !== undefined ) { + this.pending.delete(details.auxProcessId); listener(details.msg); return; } } - // Sent to a specific channel - var response = this.sendToChannelListeners(details.channelName, details.msg); - - // Respond back if required - if ( details.mainProcessId === undefined ) { - return; - } - var port = this.connect(); - if ( port !== null ) { - port.postMessage({ - mainProcessId: details.mainProcessId, - msg: response - }); + if ( details.channelName !== 'vapi' ) { return; } + + // Internal handler + var connection; + + switch ( details.msg.what ) { + case 'connectionAccepted': + case 'connectionBroken': + case 'connectionCheck': + case 'connectionMessage': + case 'connectionRefused': + connection = this.connections.get(details.msg.id); + if ( connection === undefined ) { return; } + connection.receive(details.msg); + break; + case 'connectionRequested': + var listeners = this.channels.get(details.msg.to); + if ( listeners === undefined ) { return; } + var port = this.getPort(); + if ( port === null ) { return; } + for ( listener of listeners ) { + if ( listener(details.msg) !== true ) { continue; } + details.msg.what = 'connectionAccepted'; + details.msg.toToken = port.name; + connection = new this.Connection(listener, details.msg); + this.connections.set(connection.id, connection); + break; + } + if ( details.msg.what !== 'connectionAccepted' ) { + details.msg.what = 'connectionRefused'; + } + port.postMessage(details); + break; + default: + break; } }, messageListenerCallback: null, @@ -135,15 +176,16 @@ vAPI.messaging = { this.portTimer = null; if ( this.port !== null && - this.channelCount === 0 && - this.pendingCount === 0 + this.channels.size === 0 && + this.connections.size === 0 && + this.pending.size === 0 ) { return this.destroyPort(); } - this.portTimer = vAPI.setTimeout(this.portPollerCallback, this.portTimerDelay); + this.portTimer = vAPI.setTimeout(this.portPollerBound, this.portTimerDelay); this.portTimerDelay = Math.min(this.portTimerDelay * 2, 60 * 60 * 1000); }, - portPollerCallback: null, + portPollerBound: null, destroyPort: function() { if ( this.portTimer !== null ) { @@ -154,20 +196,21 @@ vAPI.messaging = { if ( port !== null ) { port.disconnect(); port.onMessage.removeListener(this.messageListenerCallback); - port.onDisconnect.removeListener(this.disconnectListenerCallback); + port.onDisconnect.removeListener(this.disconnectListenerBound); this.port = null; } - if ( this.channelCount !== 0 ) { - this.channels = Object.create(null); - this.channelCount = 0; + this.channels.clear(); + if ( this.connections.size !== 0 ) { + for ( var connection of this.connections.values() ) { + connection.receive({ what: 'connectionBroken' }); + } + this.connections.clear(); } // service pending callbacks - if ( this.pendingCount !== 0 ) { - var pending = this.pending, callback; - this.pending = Object.create(null); - this.pendingCount = 0; - for ( var auxId in pending ) { - callback = pending[auxId]; + if ( this.pending.size !== 0 ) { + var pending = this.pending; + this.pending = new Map(); + for ( var callback of pending.values() ) { if ( typeof callback === 'function' ) { callback(null); } @@ -176,13 +219,11 @@ vAPI.messaging = { }, createPort: function() { - if ( this.shuttingDown ) { - return null; - } + if ( this.shuttingDown ) { return null; } if ( this.messageListenerCallback === null ) { this.messageListenerCallback = this.messageListener.bind(this); - this.disconnectListenerCallback = this.disconnectListener.bind(this); - this.portPollerCallback = this.portPoller.bind(this); + this.disconnectListenerBound = this.disconnectListener.bind(this); + this.portPollerBound = this.portPoller.bind(this); } try { this.port = chrome.runtime.connect({name: vAPI.sessionId}) || null; @@ -191,102 +232,109 @@ vAPI.messaging = { } if ( this.port !== null ) { this.port.onMessage.addListener(this.messageListenerCallback); - this.port.onDisconnect.addListener(this.disconnectListenerCallback); - } - this.portTimerDelay = 10000; - if ( this.portTimer === null ) { - this.portTimer = vAPI.setTimeout(this.portPollerCallback, this.portTimerDelay); + this.port.onDisconnect.addListener(this.disconnectListenerBound); + this.portTimerDelay = 10000; + if ( this.portTimer === null ) { + this.portTimer = vAPI.setTimeout( + this.portPollerBound, + this.portTimerDelay + ); + } } return this.port; }, - connect: function() { + getPort: function() { return this.port !== null ? this.port : this.createPort(); }, send: function(channelName, message, callback) { - this.sendTo(channelName, message, undefined, undefined, callback); - }, - - sendTo: function(channelName, message, toTabId, toChannel, callback) { // Too large a gap between the last request and the last response means // the main process is no longer reachable: memory leaks and bad // performance become a risk -- especially for long-lived, dynamic // pages. Guard against this. - if ( this.pendingCount > 25 ) { + if ( this.pending.size > 25 ) { vAPI.shutdown.exec(); } - var port = this.connect(); + var port = this.getPort(); if ( port === null ) { - if ( typeof callback === 'function' ) { - callback(); - } + if ( typeof callback === 'function' ) { callback(); } return; } var auxProcessId; if ( callback ) { auxProcessId = this.auxProcessId++; - this.pending[auxProcessId] = callback; - this.pendingCount += 1; + this.pending.set(auxProcessId, callback); } port.postMessage({ channelName: channelName, auxProcessId: auxProcessId, - toTabId: toTabId, - toChannel: toChannel, msg: message }); }, - addChannelListener: function(channelName, callback) { - if ( typeof callback !== 'function' ) { - return; - } - var listeners = this.channels[channelName]; - if ( listeners !== undefined && listeners.indexOf(callback) !== -1 ) { - console.error('Duplicate listener on channel "%s"', channelName); - return; - } - if ( listeners === undefined ) { - this.channels[channelName] = [callback]; - this.channelCount += 1; - } else { - listeners.push(callback); - } - this.connect(); + connectTo: function(from, to, handler) { + var port = this.getPort(); + if ( port === null ) { return; } + var connection = new this.Connection(handler, { + id: from + '-' + to + '-' + vAPI.sessionId, + to: to, + from: from, + fromToken: port.name + }); + this.connections.set(connection.id, connection); + port.postMessage({ + channelName: 'vapi', + msg: { + what: 'connectionRequested', + id: connection.id, + from: from, + fromToken: port.name, + to: to + } + }); + return connection.id; }, - removeChannelListener: function(channelName, callback) { - if ( typeof callback !== 'function' ) { - return; - } - var listeners = this.channels[channelName]; + disconnectFrom: function(connectionId) { + var connection = this.connections.get(connectionId); + if ( connection === undefined ) { return; } + connection.disconnect(); + }, + + sendTo: function(connectionId, payload) { + var connection = this.connections.get(connectionId); + if ( connection === undefined ) { return; } + connection.send(payload); + }, + + addChannelListener: function(channelName, listener) { + var listeners = this.channels.get(channelName); if ( listeners === undefined ) { - return; - } - var pos = listeners.indexOf(callback); - if ( pos === -1 ) { - console.error('Listener not found on channel "%s"', channelName); - return; + this.channels.set(channelName, [ listener ]); + } else if ( listeners.indexOf(listener) === -1 ) { + listeners.push(listener); } + this.getPort(); + }, + + removeChannelListener: function(channelName, listener) { + var listeners = this.channels.get(channelName); + if ( listeners === undefined ) { return; } + var pos = listeners.indexOf(listener); + if ( pos === -1 ) { return; } listeners.splice(pos, 1); if ( listeners.length === 0 ) { - delete this.channels[channelName]; - this.channelCount -= 1; + this.channels.delete(channelName); } }, removeAllChannelListeners: function(channelName) { - var listeners = this.channels[channelName]; - if ( listeners === undefined ) { - return; - } - delete this.channels[channelName]; - this.channelCount -= 1; + this.channels.delete(channelName); }, sendToChannelListeners: function(channelName, msg) { - var listeners = this.channels[channelName]; + var listeners = this.channels.get(channelName); if ( listeners === undefined ) { return; } listeners = listeners.slice(0); var response; @@ -300,9 +348,94 @@ vAPI.messaging = { /******************************************************************************/ +vAPI.messaging.Connection.prototype = { + toDetails: function(what, payload) { + return { + what: what, + id: this.id, + from: this.from, + fromToken: this.fromToken, + to: this.to, + toToken: this.toToken, + payload: payload + }; + }, + disconnect: function() { + if ( this.checkTimer !== undefined ) { + clearTimeout(this.checkTimer); + this.checkTimer = undefined; + } + this.messaging.connections.delete(this.id); + var port = this.messaging.getPort(); + if ( port === null ) { return; } + port.postMessage({ + channelName: 'vapi', + msg: this.toDetails('connectionBroken') + }); + }, + checkAsync: function() { + if ( this.checkTimer !== undefined ) { + clearTimeout(this.checkTimer); + } + this.checkTimer = vAPI.setTimeout(this.checkBound, 499); + }, + check: function() { + this.checkTimer = undefined; + if ( this.messaging.connections.has(this.id) === false ) { return; } + var port = this.messaging.getPort(); + if ( port === null ) { return; } + port.postMessage({ + channelName: 'vapi', + msg: this.toDetails('connectionCheck') + }); + this.checkAsync(); + }, + receive: function(details) { + switch ( details.what ) { + case 'connectionAccepted': + this.toToken = details.toToken; + this.handler(details); + this.checkAsync(); + break; + case 'connectionBroken': + this.messaging.connections.delete(this.id); + this.handler(details); + break; + case 'connectionMessage': + this.handler(details); + this.checkAsync(); + break; + case 'connectionCheck': + var port = this.messaging.getPort(); + if ( port === null ) { return; } + if ( this.messaging.connections.has(this.id) ) { + this.checkAsync(); + } else { + details.what = 'connectionBroken'; + port.postMessage({ channelName: 'vapi', msg: details }); + } + break; + case 'connectionRefused': + this.messaging.connections.delete(this.id); + this.handler(details); + break; + } + }, + send: function(payload) { + var port = this.messaging.getPort(); + if ( port === null ) { return; } + port.postMessage({ + channelName: 'vapi', + msg: this.toDetails('connectionMessage', payload) + }); + } +}; + +/******************************************************************************/ + vAPI.shutdown.add(function() { vAPI.messaging.shutdown(); - delete window.vAPI; + window.vAPI = undefined; }); // https://www.youtube.com/watch?v=rT5zCHn0tsg diff --git a/platform/firefox/vapi-background.js b/platform/firefox/vapi-background.js index 40157849fc883..c3579826e72df 100644 --- a/platform/firefox/vapi-background.js +++ b/platform/firefox/vapi-background.js @@ -1572,31 +1572,22 @@ vAPI.messaging.listen = function(listenerName, callback) { vAPI.messaging.onMessage = (function() { var messaging = vAPI.messaging; - var toAuxPending = {}; // Use a wrapper to avoid closure and to allow reuse. - var CallbackWrapper = function(messageManager, listenerId, channelName, auxProcessId, timeout) { + var CallbackWrapper = function(messageManager, listenerId, channelName, auxProcessId) { this.callback = this.proxy.bind(this); // bind once - this.init(messageManager, listenerId, channelName, auxProcessId, timeout); + this.init(messageManager, listenerId, channelName, auxProcessId); }; - CallbackWrapper.prototype.init = function(messageManager, listenerId, channelName, auxProcessId, timeout) { + CallbackWrapper.prototype.init = function(messageManager, listenerId, channelName, auxProcessId) { this.messageManager = messageManager; this.listenerId = listenerId; this.channelName = channelName; this.auxProcessId = auxProcessId; - this.timerId = timeout !== undefined ? - vAPI.setTimeout(this.callback, timeout) : - null; return this; }; CallbackWrapper.prototype.proxy = function(response) { - if ( this.timerId !== null ) { - clearTimeout(this.timerId); - delete toAuxPending[this.timerId]; - this.timerId = null; - } var message = JSON.stringify({ auxProcessId: this.auxProcessId, channelName: this.channelName, @@ -1619,97 +1610,15 @@ vAPI.messaging.onMessage = (function() { var callbackWrapperJunkyard = []; - var callbackWrapperFactory = function(messageManager, listenerId, channelName, auxProcessId, timeout) { + var callbackWrapperFactory = function(messageManager, listenerId, channelName, auxProcessId) { var wrapper = callbackWrapperJunkyard.pop(); if ( wrapper ) { - return wrapper.init(messageManager, listenerId, channelName, auxProcessId, timeout); + return wrapper.init(messageManager, listenerId, channelName, auxProcessId); } - return new CallbackWrapper(messageManager, listenerId, channelName, auxProcessId, timeout); - }; - - // "Auxiliary process": any process other than main process. - var toAux = function(target, details) { - var messageManagerFrom = target.messageManager; - - // Message came from a popup, and its message manager is not usable. - // So instead we broadcast to the parent window. - if ( !messageManagerFrom ) { - messageManagerFrom = getOwnerWindow( - target.webNavigation.QueryInterface(Ci.nsIDocShell).chromeEventHandler - ).messageManager; - } - - var wrapper; - if ( details.auxProcessId !== undefined ) { - var channelNameRaw = details.channelName; - var pos = channelNameRaw.indexOf('|'); - wrapper = callbackWrapperFactory( - messageManagerFrom, - channelNameRaw.slice(0, pos), - channelNameRaw.slice(pos + 1), - details.auxProcessId, - 1023 - ); - } - - var messageManagerTo = null; - var browser = tabWatcher.browserFromTabId(details.toTabId); - if ( browser !== null && browser.messageManager ) { - messageManagerTo = browser.messageManager; - } - if ( messageManagerTo === null ) { - if ( wrapper !== undefined ) { - wrapper.callback(); - } - return; - } - - // As per HTML5, timer id is always an integer, thus suitable to be used - // as a key, and which value is safe to use across process boundaries. - if ( wrapper !== undefined ) { - toAuxPending[wrapper.timerId] = wrapper; - } - - var targetId = location.host + ':broadcast'; - var payload = JSON.stringify({ - mainProcessId: wrapper && wrapper.timerId, - channelName: details.toChannel, - msg: details.msg - }); - - if ( messageManagerTo.sendAsyncMessage ) { - messageManagerTo.sendAsyncMessage(targetId, payload); - } else { - messageManagerTo.broadcastAsyncMessage(targetId, payload); - } - }; - - var toAuxResponse = function(details) { - var mainProcessId = details.mainProcessId; - if ( mainProcessId === undefined ) { - return; - } - if ( toAuxPending.hasOwnProperty(mainProcessId) === false ) { - return; - } - var wrapper = toAuxPending[mainProcessId]; - delete toAuxPending[mainProcessId]; - wrapper.callback(details.msg); + return new CallbackWrapper(messageManager, listenerId, channelName, auxProcessId); }; return function({target, data}) { - // Auxiliary process to auxiliary process - if ( data.toTabId !== undefined ) { - toAux(target, data); - return; - } - - // Auxiliary process to auxiliary process: response - if ( data.mainProcessId !== undefined ) { - toAuxResponse(data); - return; - } - // Auxiliary process to main process var messageManager = target.messageManager; @@ -1748,15 +1657,11 @@ vAPI.messaging.onMessage = (function() { if ( typeof listener === 'function' ) { r = listener(data.msg, sender, callback); } - if ( r !== messaging.UNHANDLED ) { - return; - } + if ( r !== messaging.UNHANDLED ) { return; } // Auxiliary process to main process: default handler r = messaging.defaultHandler(data.msg, sender, callback); - if ( r !== messaging.UNHANDLED ) { - return; - } + if ( r !== messaging.UNHANDLED ) { return; } // Auxiliary process to main process: no handler console.error('uBlock> messaging > unknown request: %o', data); diff --git a/platform/firefox/vapi-client.js b/platform/firefox/vapi-client.js index d30bbe631ac6d..aeb83cd193a22 100644 --- a/platform/firefox/vapi-client.js +++ b/platform/firefox/vapi-client.js @@ -150,22 +150,18 @@ var processUserCSS = function(details, callback) { /******************************************************************************/ vAPI.messaging = { - channels: Object.create(null), - channelCount: 0, - pending: Object.create(null), - pendingCount: 0, + channels: new Map(), + pending: new Map(), auxProcessId: 1, connected: false, messageListener: function(msg) { var details = JSON.parse(msg); - if ( !details ) { - return; - } + if ( !details ) { return; } // Sent to all channels if ( details.broadcast && !details.channelName ) { - for ( var channelName in this.channels ) { + for ( var channelName of this.channels.keys() ) { this.sendToChannelListeners(channelName, details.msg); } return; @@ -173,42 +169,27 @@ vAPI.messaging = { // Response to specific message previously sent if ( details.auxProcessId ) { - var listener = this.pending[details.auxProcessId]; - delete this.pending[details.auxProcessId]; - delete details.auxProcessId; // TODO: why? + var listener = this.pending.get(details.auxProcessId); + this.pending.delete(details.auxProcessId); if ( listener ) { - this.pendingCount -= 1; listener(details.msg); return; } } // Sent to a specific channel - var response = this.sendToChannelListeners(details.channelName, details.msg); - - // Respond back if required - if ( details.mainProcessId === undefined ) { - return; - } - sendAsyncMessage('ublock0:background', { - mainProcessId: details.mainProcessId, - msg: response - }); + this.sendToChannelListeners(details.channelName, details.msg); }, builtinListener: function(msg) { if ( msg.cmd === 'injectScript' ) { // injectScript is not always present. // - See contentObserver.initContentScripts in frameModule.js - if ( typeof self.injectScript !== 'function' ) { - return; - } + if ( typeof self.injectScript !== 'function' ) { return; } var details = msg.details; // Whether to inject in all child frames. Default to only top frame. var allFrames = details.allFrames || false; - if ( allFrames !== true && window !== window.top ) { - return; - } + if ( allFrames !== true && window !== window.top ) { return; } // https://github.com/gorhill/uBlock/issues/876 // Enforce `details.runAt`. Default to `document_end`. var runAt = details.runAt || 'document_end'; @@ -225,7 +206,7 @@ vAPI.messaging = { } if ( msg.cmd === 'shutdownSandbox' ) { vAPI.shutdown.exec(); - this.stop(); + vAPI.messaging.stop(); if ( typeof self.outerShutdown === 'function' ) { outerShutdown(); } @@ -263,7 +244,7 @@ vAPI.messaging = { toggleListenerCallback: null, start: function() { - this.addChannelListener('vAPI', this.builtinListener.bind(this)); + this.addChannelListener('vAPI', this.builtinListener); if ( this.toggleListenerCallback === null ) { this.toggleListenerCallback = this.toggleListener.bind(this); } @@ -277,14 +258,11 @@ vAPI.messaging = { window.removeEventListener('pageshow', this.toggleListenerCallback, true); } this.disconnect(); - this.channels = Object.create(null); - this.channelCount = 0; + this.channels.clear(); // service pending callbacks - var pending = this.pending, callback; - this.pending = Object.create(null); - this.pendingCount = 0; - for ( var auxId in pending ) { - callback = pending[auxId]; + var pending = this.pending; + this.pending = new Map(); + for ( var callback of pending.values() ) { if ( typeof callback === 'function' ) { callback(null); } @@ -307,17 +285,11 @@ vAPI.messaging = { send: function(channelName, message, callback) { // User stylesheets are handled content-side on legacy Firefox. - if ( channelName === 'vapi-background' && message.what === 'userCSS' ) { + if ( channelName === 'vapi' && message.what === 'userCSS' ) { return processUserCSS(message, callback); } - this.sendTo(channelName, message, undefined, undefined, callback); - }, - - sendTo: function(channelName, message, toTabId, toChannel, callback) { if ( !this.connected ) { - if ( typeof callback === 'function' ) { - callback(); - } + if ( typeof callback === 'function' ) { callback(); } return; } // Too large a gap between the last request and the last response means @@ -331,67 +303,57 @@ vAPI.messaging = { var auxProcessId; if ( callback ) { auxProcessId = this.auxProcessId++; - this.pending[auxProcessId] = callback; - this.pendingCount += 1; + this.pending.set(auxProcessId, callback); } sendAsyncMessage('ublock0:background', { channelName: self._sandboxId_ + '|' + channelName, auxProcessId: auxProcessId, - toTabId: toTabId, - toChannel: toChannel, msg: message }); }, - addChannelListener: function(channelName, callback) { - if ( typeof callback !== 'function' ) { - return; - } - var listeners = this.channels[channelName]; - if ( listeners !== undefined && listeners.indexOf(callback) !== -1 ) { - console.error('Duplicate listener on channel "%s"', channelName); - return; - } + // TODO: implement as time permits. + connectTo: function(from, to, handler) { + handler({ + what: 'connectionRefused', + from: from, + to: to + }); + }, + + disconnectFrom: function() { + }, + + sendTo: function() { + }, + + addChannelListener: function(channelName, listener) { + var listeners = this.channels.get(channelName); if ( listeners === undefined ) { - this.channels[channelName] = [callback]; - this.channelCount += 1; - } else { - listeners.push(callback); + this.channels.set(channelName, [ listener ]); + } else if ( listeners.indexOf(listener) === -1 ) { + listeners.push(listener); } this.connect(); }, - removeChannelListener: function(channelName, callback) { - if ( typeof callback !== 'function' ) { - return; - } - var listeners = this.channels[channelName]; - if ( listeners === undefined ) { - return; - } - var pos = this.listeners.indexOf(callback); - if ( pos === -1 ) { - console.error('Listener not found on channel "%s"', channelName); - return; - } + removeChannelListener: function(channelName, listener) { + var listeners = this.channels.get(channelName); + if ( listeners === undefined ) { return; } + var pos = this.listeners.indexOf(listener); + if ( pos === -1 ) { return; } listeners.splice(pos, 1); if ( listeners.length === 0 ) { - delete this.channels[channelName]; - this.channelCount -= 1; + this.channels.delete(channelName); } }, removeAllChannelListeners: function(channelName) { - var listeners = this.channels[channelName]; - if ( listeners === undefined ) { - return; - } - delete this.channels[channelName]; - this.channelCount -= 1; + this.channels.delete(channelName); }, sendToChannelListeners: function(channelName, msg) { - var listeners = this.channels[channelName]; + var listeners = this.channels.get(channelName); if ( listeners === undefined ) { return; } listeners = listeners.slice(0); var response; diff --git a/platform/webext/vapi-usercss.js b/platform/webext/vapi-usercss.js index b8199d99be3c2..2483baba55ee4 100644 --- a/platform/webext/vapi-usercss.js +++ b/platform/webext/vapi-usercss.js @@ -33,7 +33,7 @@ vAPI.userStylesheet = { removed: new Set(), apply: function() { if ( this.added.size === 0 && this.removed.size === 0 ) { return; } - vAPI.messaging.send('vapi-background', { + vAPI.messaging.send('vapi', { what: 'userCSS', add: Array.from(this.added), remove: Array.from(this.removed) diff --git a/src/css/logger-ui-inspector.css b/src/css/logger-ui-inspector.css index 420df1bb6d423..2c24aba8b4738 100644 --- a/src/css/logger-ui-inspector.css +++ b/src/css/logger-ui-inspector.css @@ -28,16 +28,23 @@ background-color: #fee; } #domInspector li > * { + font-size: 0.8rem; + display: inline-block; + line-height: 1.2; margin-right: 1em; + vertical-align: middle; + } +#domInspector li > span { + color: #aaa; } #domInspector li > span:first-child { color: #000; cursor: default; - display: inline-block; + font-size: 1rem; margin-right: 0; opacity: 0.5; + padding: 0 4px 0 1px; visibility: hidden; - width: 1em; } #domInspector li > span:first-child:hover { opacity: 1; @@ -54,30 +61,51 @@ } #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; + font-family: monospace; + overflow: hidden; + text-overflow: ellipsis; } #domInspector li > code.off { text-decoration: line-through; } -#domInspector li > span { - color: #aaa; - } #domInspector li > code.filter { color: red; } + #domInspector li > ul { + display: block; + } +#domInspector li:not(.hasCosmeticHide):not(.isCosmeticHide):not(.show) > ul { display: none; } -#domInspector li.show > ul { + +#domInspector.vCompact li:not(.hasCosmeticHide):not(.isCosmeticHide) { + display: none; + } +#domInspector #domTree > li { + display: block; + } +#domInspector.vCompact ul { + display: block; + } +#domInspector li > ul > li:not(.hasCosmeticHide):not(.isCosmeticHide) { + display: none; + } +#domInspector li.show > ul > li:not(.hasCosmeticHide):not(.isCosmeticHide) { display: block; } +#domInspector li:not(.hasCosmeticHide):not(.isCosmeticHide) { + display: block; + } +#domInspector.hCompact li > code:first-of-type { + max-width: 12em; + } #cosmeticFilteringDialog .dialog { text-align: center; diff --git a/src/css/logger-ui.css b/src/css/logger-ui.css index 355c2f11e8430..e0c078a9d3679 100644 --- a/src/css/logger-ui.css +++ b/src/css/logger-ui.css @@ -60,6 +60,7 @@ textarea { #inspectors { bottom: 0; + font: 13px sans-serif; position: absolute; top: 0; width: 100%; @@ -80,18 +81,22 @@ textarea { width: 100%; } -#netInspector { - font: 13px sans-serif; - } -#inspectors.dom #netInspector { - display: none; - } -#netInspector #compactViewToggler.button:before { +.vCompactToggler.button:before { content: '\f102'; } -#netInspector.compactView #compactViewToggler.button:before { +.vCompact .vCompactToggler.button:before { content: '\f103'; } +.hCompactToggler.button:before { + content: '\f100'; + } +.hCompact .hCompactToggler.button:before { + content: '\f101'; + } + +#inspectors.dom #netInspector { + display: none; + } #netInspector #filterButton { opacity: 0.25; } @@ -190,7 +195,7 @@ body #netInspector td { #netInspector tr td:last-of-type { border-right: none; } -#netInspector.compactView td { +#netInspector.vCompact td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; diff --git a/src/js/logger-ui-inspector.js b/src/js/logger-ui-inspector.js index 57f358d36082b..f598ec2db293b 100644 --- a/src/js/logger-ui-inspector.js +++ b/src/js/logger-ui-inspector.js @@ -32,7 +32,11 @@ var showdomButton = uDom.nodeFromId('showdom'); // Don't bother if the browser is not modern enough. -if ( typeof Map === 'undefined' || Map.polyfill || typeof WeakMap === 'undefined' ) { +if ( + typeof Map === 'undefined' || + Map.polyfill || + typeof WeakMap === 'undefined' +) { showdomButton.classList.add('disabled'); return; } @@ -40,13 +44,10 @@ if ( typeof Map === 'undefined' || Map.polyfill || typeof WeakMap === 'undefined /******************************************************************************/ var logger = self.logger; -var messaging = vAPI.messaging; - +var inspectorConnectionId; var inspectedTabId = ''; var inspectedURL = ''; var inspectedHostname = ''; -var pollTimer = null; -var fingerprint = null; var inspector = uDom.nodeFromId('domInspector'); var domTree = uDom.nodeFromId('domTree'); var tabSelector = uDom.nodeFromId('pageSelector'); @@ -55,13 +56,45 @@ var filterToIdMap = new Map(); /******************************************************************************/ +var messaging = vAPI.messaging; + +messaging.addChannelListener('loggerUI', function(msg) { + switch ( msg.what ) { + case 'connectionBroken': + if ( inspectorConnectionId === msg.id ) { + filterToIdMap.clear(); + logger.removeAllChildren(domTree); + inspectorConnectionId = undefined; + } + injectInspector(); + break; + case 'connectionMessage': + if ( msg.payload.what === 'domLayoutFull' ) { + inspectedURL = msg.payload.url; + inspectedHostname = msg.payload.hostname; + renderDOMFull(msg.payload); + } else if ( msg.payload.what === 'domLayoutIncremental' ) { + renderDOMIncremental(msg.payload); + } + break; + case 'connectionRequested': + if ( msg.from !== 'domInspector' ) { return false; } + if ( msg.tabId !== inspectedTabId ) { return false; } + filterToIdMap.clear(); + logger.removeAllChildren(domTree); + inspectorConnectionId = msg.id; + return true; + } +}); + +/******************************************************************************/ + 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); + li.appendChild(document.createElement('span')); // selector node = document.createElement('code'); node.textContent = entry.sel; @@ -190,16 +223,14 @@ var renderDOMIncremental = function(response) { // 1 = node added // -1 = node removed var journal = response.journal; - var nodes = response.nodes; + var nodes = new Map(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; - } + if ( li === null ) { continue; } patchIncremental(li, -1); li.parentNode.removeChild(li); continue; @@ -218,7 +249,7 @@ var renderDOMIncremental = function(response) { continue; } ul = previous.parentElement; - li = nodeFromDomEntry(nodes[entry.nid]); + li = nodeFromDomEntry(nodes.get(entry.nid)); ul.insertBefore(li, previous.nextElementSibling); patchIncremental(li, 1); continue; @@ -237,7 +268,7 @@ var renderDOMIncremental = function(response) { li.appendChild(ul); li.classList.add('branch'); } - li = nodeFromDomEntry(nodes[entry.nid]); + li = nodeFromDomEntry(nodes.get(entry.nid)); ul.appendChild(li); patchIncremental(li, 1); continue; @@ -350,7 +381,7 @@ var startDialog = (function() { }; })(); - var onClick = function(ev) { + var onClicked = function(ev) { var target = ev.target; // click outside the dialog proper @@ -367,83 +398,51 @@ var startDialog = (function() { } }; - var onCooked = function(entries) { - if ( Array.isArray(entries) === false ) { - return; + var showCommitted = function() { + messaging.sendTo(inspectorConnectionId, { + what: 'showCommitted', + hide: hideSelectors.join(',\n'), + unhide: unhideSelectors.join(',\n') + }); + }; + + var showInteractive = function() { + messaging.sendTo(inspectorConnectionId, { + what: 'showInteractive', + hide: hideSelectors.join(',\n'), + unhide: unhideSelectors.join(',\n') + }); + }; + + var start = function() { + hideSelectors = []; + textarea.addEventListener('input', onInputChanged); + var node; + for ( node of domTree.querySelectorAll('code.off') ) { + if ( node.classList.contains('filter') === false ) { + hideSelectors.push(selectorFromNode(node)); + } } - hideSelectors = entries; - var taValue = [], i, node; + var taValue = []; var d = new Date(); taValue.push('! ' + d.toLocaleString() + ' ' + inspectedURL); - for ( i = 0; i < entries.length; i++ ) { - taValue.push(inspectedHostname + '##' + entries[i]); + for ( var selector of hideSelectors ) { + taValue.push(inspectedHostname + '##' + selector); } var ids = new Set(), id; - var nodes = domTree.querySelectorAll('code.filter.off'); - for ( i = 0; i < nodes.length; i++ ) { - node = nodes[i]; + for ( node of domTree.querySelectorAll('code.filter.off') ) { id = node.getAttribute('data-filter-id'); - if ( ids.has(id) ) { - continue; - } + if ( ids.has(id) ) { continue; } ids.add(id); unhideSelectors.push(node.textContent); taValue.push(inspectedHostname + '#@#' + node.textContent); } textarea.value = taValue.join('\n'); document.body.appendChild(dialog); - dialog.addEventListener('click', onClick, true); + dialog.addEventListener('click', onClicked, true); showCommitted(); }; - var showCommitted = function() { - messaging.sendTo( - 'loggerUI', - { - what: 'showCommitted', - hide: hideSelectors.join(',\n'), - unhide: unhideSelectors.join(',\n') - }, - inspectedTabId, - 'domInspector' - ); - }; - - var showInteractive = function() { - messaging.sendTo( - 'loggerUI', - { - what: 'showInteractive', - hide: hideSelectors.join(',\n'), - unhide: unhideSelectors.join(',\n') - }, - inspectedTabId, - 'domInspector' - ); - }; - - var start = function() { - textarea.addEventListener('input', onInputChanged); - var node, entries = []; - var nodes = domTree.querySelectorAll('code.off'); - for ( var i = 0; i < nodes.length; i++ ) { - node = nodes[i]; - if ( node.classList.contains('filter') === false ) { - entries.push({ - nid: nidFromNode(node), - selector: selectorFromNode(node) - }); - } - } - messaging.sendTo( - 'loggerUI', - { what: 'cookFilters', entries: entries }, - inspectedTabId, - 'domInspector', - onCooked - ); - }; - var stop = function() { if ( inputTimer !== null ) { clearTimeout(inputTimer); @@ -453,7 +452,7 @@ var startDialog = (function() { hideSelectors = []; unhideSelectors = []; textarea.removeEventListener('input', onInputChanged); - dialog.removeEventListener('click', onClick, true); + dialog.removeEventListener('click', onClicked, true); document.body.removeChild(dialog); }; @@ -462,12 +461,10 @@ var startDialog = (function() { /******************************************************************************/ -var onClick = function(ev) { +var onClicked = function(ev) { ev.stopPropagation(); - if ( inspectedTabId === '' ) { - return; - } + if ( inspectedTabId === '' ) { return; } var target = ev.target; var parent = target.parentElement; @@ -479,30 +476,28 @@ var onClick = function(ev) { parent.classList.contains('branch') && target === parent.firstElementChild ) { - target.parentElement.classList.toggle('show'); + var state = parent.classList.toggle('show'); + if ( !state ) { + for ( var node of parent.querySelectorAll('.branch') ) { + node.classList.remove('show'); + } + } return; } // Not a node or filter - if ( target.localName !== 'code' ) { - return; - } + if ( target.localName !== 'code' ) { return; } // Toggle cosmetic filter if ( target.classList.contains('filter') ) { - messaging.sendTo( - 'loggerUI', - { - what: 'toggleFilter', - original: false, - target: target.classList.toggle('off'), - selector: selectorFromNode(target), - filter: selectorFromFilter(target), - nid: '' - }, - inspectedTabId, - 'domInspector' - ); + messaging.sendTo(inspectorConnectionId, { + what: 'toggleFilter', + original: false, + target: target.classList.toggle('off'), + selector: selectorFromNode(target), + filter: selectorFromFilter(target), + nid: nidFromNode(target) + }); uDom('[data-filter-id="' + target.getAttribute('data-filter-id') + '"]', inspector).toggleClass( 'off', target.classList.contains('off') @@ -510,18 +505,13 @@ var onClick = function(ev) { } // Toggle node else { - messaging.sendTo( - 'loggerUI', - { - what: 'toggleNodes', - original: true, - target: target.classList.toggle('off') === false, - selector: selectorFromNode(target), - nid: nidFromNode(target) - }, - inspectedTabId, - 'domInspector' - ); + messaging.sendTo(inspectorConnectionId, { + what: 'toggleNodes', + original: true, + target: target.classList.toggle('off') === false, + selector: selectorFromNode(target), + nid: nidFromNode(target) + }); } var cantCreate = domTree.querySelector('.off') === null; @@ -537,38 +527,25 @@ var onMouseOver = (function() { var timerHandler = function() { mouseoverTimer = null; - messaging.sendTo( - 'loggerUI', - { - what: 'highlightOne', - selector: selectorFromNode(mouseoverTarget), - nid: nidFromNode(mouseoverTarget), - scrollTo: true - }, - inspectedTabId, - 'domInspector' - ); + messaging.sendTo(inspectorConnectionId, { + what: 'highlightOne', + selector: selectorFromNode(mouseoverTarget), + nid: nidFromNode(mouseoverTarget), + scrollTo: true + }); }; return function(ev) { - if ( inspectedTabId === '' ) { - return; - } + if ( inspectedTabId === '' ) { return; } // Convenience: skip real-time highlighting if shift key is pressed. - if ( ev.shiftKey ) { - return; - } + if ( ev.shiftKey ) { return; } // Find closest `li` var target = ev.target; while ( target !== null ) { - if ( target.localName === 'li' ) { - break; - } + if ( target.localName === 'li' ) { break; } target = target.parentElement; } - if ( target === mouseoverTarget ) { - return; - } + if ( target === mouseoverTarget ) { return; } mouseoverTarget = target; if ( mouseoverTimer === null ) { mouseoverTimer = vAPI.setTimeout(timerHandler, 50); @@ -579,121 +556,33 @@ var onMouseOver = (function() { /******************************************************************************/ var currentTabId = function() { - if ( showdomButton.classList.contains('active') === false ) { - return ''; - } + if ( showdomButton.classList.contains('active') === false ) { return ''; } var tabId = logger.tabIdFromClassName(tabSelector.value) || ''; return tabId !== 'bts' ? tabId : ''; }; /******************************************************************************/ -var fetchDOMAsync = (function() { - var onFetched = function(response) { - if ( !response || currentTabId() !== inspectedTabId ) { - shutdownInspector(); - injectInspectorAsync(250); - return; - } - - switch ( response.status ) { - case 'full': - renderDOMFull(response); - fingerprint = response.fingerprint; - inspectedURL = response.url; - inspectedHostname = response.hostname; - break; - - case 'incremental': - renderDOMIncremental(response); - break; - - case 'nochange': - case 'busy': - break; - - default: - break; - } - - fetchDOMAsync(); - }; - - var onTimeout = function() { - pollTimer = null; - messaging.sendTo( - 'loggerUI', - { - what: 'domLayout', - fingerprint: fingerprint - }, - inspectedTabId, - 'domInspector', - onFetched - ); - }; - - // Poll for DOM layout data every ~2 seconds at most - return function(delay) { - if ( pollTimer === null ) { - pollTimer = vAPI.setTimeout(onTimeout, delay || 2003); - } - }; -})(); - -/******************************************************************************/ - var injectInspector = function() { var tabId = currentTabId(); - // No valid tab, go back - if ( tabId === '' ) { - injectInspectorAsync(); - return; - } + if ( tabId === '' ) { return; } inspectedTabId = tabId; - fingerprint = null; - messaging.send( - 'loggerUI', - { - 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); + messaging.send('loggerUI', { + what: 'scriptlet', + tabId: tabId, + scriptlet: 'dom-inspector' + }); }; /******************************************************************************/ var shutdownInspector = function() { - if ( inspectedTabId !== '' ) { - messaging.sendTo( - 'loggerUI', - { what: 'shutdown' }, - inspectedTabId, - 'domInspector' - ); + if ( inspectorConnectionId !== undefined ) { + messaging.disconnectFrom(inspectorConnectionId); + inspectorConnectionId = undefined; } logger.removeAllChildren(domTree); - if ( pollTimer !== null ) { - clearTimeout(pollTimer); - pollTimer = null; - } + inspector.classList.add('vCompact'); inspectedTabId = ''; }; @@ -702,34 +591,38 @@ var shutdownInspector = function() { var onTabIdChanged = function() { if ( inspectedTabId !== currentTabId() ) { shutdownInspector(); - injectInspectorAsync(250); + injectInspector(); + } +}; + +/******************************************************************************/ + +var toggleVCompactView = function() { + var state = !inspector.classList.toggle('vCompact'); + var branches = document.querySelectorAll('#domInspector li.branch'); + for ( var branch of branches ) { + branch.classList.toggle('show', state); } }; +var toggleHCompactView = function() { + inspector.classList.toggle('hCompact'); +}; + /******************************************************************************/ var toggleHighlightMode = function() { - messaging.sendTo( - 'loggerUI', - { - what: 'highlightMode', - invert: uDom.nodeFromSelector('#domInspector .permatoolbar .highlightMode').classList.toggle('invert') - }, - inspectedTabId, - 'domInspector' - ); + messaging.sendTo(inspectorConnectionId, { + what: 'highlightMode', + invert: uDom.nodeFromSelector('#domInspector .permatoolbar .highlightMode').classList.toggle('invert') + }); }; /******************************************************************************/ var revert = function() { uDom('#domTree .off').removeClass('off'); - messaging.sendTo( - 'loggerUI', - { what: 'resetToggledNodes' }, - inspectedTabId, - 'domInspector' - ); + messaging.sendTo(inspectorConnectionId, { what: 'resetToggledNodes' }); inspector.querySelector('.permatoolbar .revert').classList.add('disabled'); inspector.querySelector('.permatoolbar .commit').classList.add('disabled'); }; @@ -739,8 +632,10 @@ var revert = function() { var toggleOn = function() { window.addEventListener('beforeunload', toggleOff); tabSelector.addEventListener('change', onTabIdChanged); - domTree.addEventListener('click', onClick, true); + domTree.addEventListener('click', onClicked, true); domTree.addEventListener('mouseover', onMouseOver, true); + uDom.nodeFromSelector('#domInspector .vCompactToggler').addEventListener('click', toggleVCompactView); + uDom.nodeFromSelector('#domInspector .hCompactToggler').addEventListener('click', toggleHCompactView); uDom.nodeFromSelector('#domInspector .permatoolbar .highlightMode').addEventListener('click', toggleHighlightMode); uDom.nodeFromSelector('#domInspector .permatoolbar .revert').addEventListener('click', revert); uDom.nodeFromSelector('#domInspector .permatoolbar .commit').addEventListener('click', startDialog); @@ -753,8 +648,10 @@ var toggleOff = function() { shutdownInspector(); window.removeEventListener('beforeunload', toggleOff); tabSelector.removeEventListener('change', onTabIdChanged); - domTree.removeEventListener('click', onClick, true); + domTree.removeEventListener('click', onClicked, true); domTree.removeEventListener('mouseover', onMouseOver, true); + uDom.nodeFromSelector('#domInspector .vCompactToggler').removeEventListener('click', toggleVCompactView); + uDom.nodeFromSelector('#domInspector .hCompactToggler').removeEventListener('click', toggleHCompactView); uDom.nodeFromSelector('#domInspector .permatoolbar .highlightMode').removeEventListener('click', toggleHighlightMode); uDom.nodeFromSelector('#domInspector .permatoolbar .revert').removeEventListener('click', revert); uDom.nodeFromSelector('#domInspector .permatoolbar .commit').removeEventListener('click', startDialog); diff --git a/src/js/logger-ui.js b/src/js/logger-ui.js index dd00f92d956d0..218f44fb31bb3 100644 --- a/src/js/logger-ui.js +++ b/src/js/logger-ui.js @@ -412,12 +412,8 @@ var synchronizeTabIds = function(newTabIds) { var rowVoided = false; var trs; for ( var tabId in oldTabIds ) { - if ( oldTabIds.hasOwnProperty(tabId) === false ) { - continue; - } - if ( newTabIds.hasOwnProperty(tabId) ) { - continue; - } + if ( oldTabIds.hasOwnProperty(tabId) === false ) { continue; } + if ( newTabIds.hasOwnProperty(tabId) ) { continue; } // Mark or remove voided rows trs = uDom('.tab_' + tabId); if ( autoDeleteVoidRows ) { @@ -440,9 +436,7 @@ var synchronizeTabIds = function(newTabIds) { var option; for ( var i = 0, j = 2; i < tabIds.length; i++ ) { tabId = tabIds[i]; - if ( tabId === noTabId ) { - continue; - } + if ( tabId === noTabId ) { continue; } option = select.options[j]; if ( !option ) { option = document.createElement('option'); @@ -1515,8 +1509,8 @@ var cleanBuffer = function() { /******************************************************************************/ -var toggleCompactView = function() { - uDom.nodeFromId('netInspector').classList.toggle('compactView'); +var toggleVCompactView = function() { + uDom.nodeFromId('netInspector').classList.toggle('vCompact'); }; /******************************************************************************/ @@ -1646,7 +1640,7 @@ uDom('#pageSelector').on('change', pageSelectorChanged); uDom('#refresh').on('click', reloadTab); uDom('#showdom').on('click', toggleInspectors); -uDom('#compactViewToggler').on('click', toggleCompactView); +uDom('#netInspector .vCompactToggler').on('click', toggleVCompactView); uDom('#clean').on('click', cleanBuffer); uDom('#clear').on('click', clearBuffer); uDom('#maxEntries').on('change', onMaxEntriesChanged); diff --git a/src/js/scriptlets/dom-inspector.js b/src/js/scriptlets/dom-inspector.js index e306945e4e69c..826018e8a79c4 100644 --- a/src/js/scriptlets/dom-inspector.js +++ b/src/js/scriptlets/dom-inspector.js @@ -137,6 +137,8 @@ var cssEscape = (function(/*root*/) { /******************************************************************************/ /******************************************************************************/ +var loggerConnectionId; + // Highlighter-related var svgRoot = null; var pickerRoot = null; @@ -153,29 +155,16 @@ var reHasCSSCombinators = /[ >+~]/; /******************************************************************************/ -// Some kind of fingerprint for the DOM, without incurring too much overhead. - -var domFingerprint = function() { - return sessionId; -}; - -/******************************************************************************/ - var domLayout = (function() { - var skipTagNames = { - 'br': true, - 'link': true, - 'meta': true, - 'script': true, - 'style': true - }; - - var resourceAttrNames = { - 'a': 'href', - 'iframe': 'src', - 'img': 'src', - 'object': 'data' - }; + var skipTagNames = new Set([ + 'br', 'head', 'link', 'meta', 'script', 'style', 'title' + ]); + var resourceAttrNames = new Map([ + [ 'a', 'href' ], + [ 'iframe', 'src' ], + [ 'img', 'src' ], + [ 'object', 'data' ] + ]); var idGenerator = 0; @@ -206,11 +195,15 @@ var domLayout = (function() { } } // Tag-specific attributes - if ( resourceAttrNames.hasOwnProperty(tag) ) { - attr = resourceAttrNames[tag]; + attr = resourceAttrNames.get(tag); + if ( attr !== undefined ) { str = node.getAttribute(attr) || ''; str = str.trim(); - pos = str.indexOf('#'); + if ( str.startsWith('data:') ) { + pos = 5; + } else { + pos = str.search(/[#?]/); + } if ( pos !== -1 ) { str = str.slice(0, pos); sw = '^'; @@ -242,13 +235,9 @@ var domLayout = (function() { var domNodeFactory = function(level, node) { var localName = node.localName; - if ( skipTagNames.hasOwnProperty(localName) ) { - return null; - } + if ( skipTagNames.has(localName) ) { return null; } // skip uBlock's own nodes - if ( node.classList.contains(sessionId) ) { - return null; - } + if ( node.classList.contains(sessionId) ) { return null; } if ( level === 0 && localName === 'body' ) { return new DomRoot(); } @@ -260,7 +249,7 @@ var domLayout = (function() { var getLayoutData = function() { var layout = []; var stack = []; - var node = document.body; + var node = document.documentElement; var domNode; var lvl = 0; @@ -324,37 +313,31 @@ var domLayout = (function() { return layout; }; - // Track and report mutations to the DOM + // Track and report mutations of the DOM var mutationObserver = null; - var mutationTimer = null; + var mutationTimer; var addedNodelists = []; var removedNodelist = []; - var journalEntries = []; - var journalNodes = Object.create(null); var previousElementSiblingId = function(node) { var sibling = node; for (;;) { sibling = sibling.previousElementSibling; - if ( sibling === null ) { - return null; - } - if ( skipTagNames.hasOwnProperty(sibling.localName) ) { - continue; - } + if ( sibling === null ) { return null; } + if ( skipTagNames.has(sibling.localName) ) { continue; } return nodeToIdMap.get(sibling); } }; - var journalFromBranch = function(root, added) { + var journalFromBranch = function(root, newNodes, newNodeToIdMap) { var domNode; var node = root.firstElementChild; while ( node !== null ) { domNode = domNodeFactory(undefined, node); if ( domNode !== null ) { - journalNodes[domNode.nid] = domNode; - added.push(node); + newNodeToIdMap.set(domNode.nid, domNode); + newNodes.push(node); } // down if ( node.firstElementChild !== null ) { @@ -368,9 +351,7 @@ var domLayout = (function() { } // up then right for (;;) { - if ( node.parentElement === root ) { - return; - } + if ( node.parentElement === root ) { return; } node = node.parentElement; if ( node.nextElementSibling !== null ) { node = node.nextElementSibling; @@ -381,51 +362,33 @@ var domLayout = (function() { }; var journalFromMutations = function() { - mutationTimer = null; - if ( mutationObserver === null ) { - addedNodelists = []; - removedNodelist = []; - return; - } - - var i, m, nodelist, j, n, node, domNode, nid; + var nodelist, node, domNode, nid; // This is used to temporarily hold all added nodes, before resolving // their node id and relative position. - var added = []; - - for ( i = 0, m = addedNodelists.length; i < m; i++ ) { - nodelist = addedNodelists[i]; - for ( j = 0, n = nodelist.length; j < n; j++ ) { - node = nodelist[j]; - if ( node.nodeType !== 1 ) { - continue; - } - // I don't think this can ever happen - if ( node.parentElement === null ) { - continue; - } + var newNodes = []; + var journalEntries = []; + var newNodeToIdMap = new Map(); + + for ( nodelist of addedNodelists ) { + for ( node of nodelist ) { + if ( node.nodeType !== 1 ) { continue; } + if ( node.parentElement === null ) { continue; } cosmeticFilterMapper.incremental(node); domNode = domNodeFactory(undefined, node); if ( domNode !== null ) { - journalNodes[domNode.nid] = domNode; - added.push(node); + newNodeToIdMap.set(domNode.nid, domNode); + newNodes.push(node); } - journalFromBranch(node, added); + journalFromBranch(node, newNodes, newNodeToIdMap); } } addedNodelists = []; - for ( i = 0, m = removedNodelist.length; i < m; i++ ) { - nodelist = removedNodelist[i]; - for ( j = 0, n = nodelist.length; j < n; j++ ) { - node = nodelist[j]; - if ( node.nodeType !== 1 ) { - continue; - } + for ( nodelist of removedNodelist ) { + for ( node of nodelist ) { + if ( node.nodeType !== 1 ) { continue; } nid = nodeToIdMap.get(node); - if ( nid === undefined ) { - continue; - } + if ( nid === undefined ) { continue; } journalEntries.push({ what: -1, nid: nid @@ -433,8 +396,7 @@ var domLayout = (function() { } } removedNodelist = []; - for ( i = 0, n = added.length; i < n; i++ ) { - node = added[i]; + for ( node of newNodes ) { journalEntries.push({ what: 1, nid: nodeToIdMap.get(node), @@ -442,12 +404,20 @@ var domLayout = (function() { l: previousElementSiblingId(node) }); } + + if ( journalEntries.length === 0 ) { return; } + + vAPI.messaging.sendTo(loggerConnectionId, { + what: 'domLayoutIncremental', + url: window.location.href, + hostname: window.location.hostname, + journal: journalEntries, + nodes: Array.from(newNodeToIdMap) + }); }; var onMutationObserved = function(mutationRecords) { - var record; - for ( var i = 0, n = mutationRecords.length; i < n; i++ ) { - record = mutationRecords[i]; + for ( var record of mutationRecords ) { if ( record.addedNodes.length !== 0 ) { addedNodelists.push(record.addedNodes); } @@ -455,55 +425,27 @@ var domLayout = (function() { removedNodelist.push(record.removedNodes); } } - if ( mutationTimer === null ) { + if ( mutationTimer === undefined ) { mutationTimer = vAPI.setTimeout(journalFromMutations, 1000); } }; // API - var getLayout = function(fingerprint) { - if ( fingerprint !== domFingerprint() ) { - reset(); - } + var getLayout = function() { + cosmeticFilterMapper.reset(); + mutationObserver = new MutationObserver(onMutationObserved); + mutationObserver.observe(document.body, { + childList: true, + subtree: true + }); - var response = { - what: 'domLayout', - fingerprint: domFingerprint(), + return { + what: 'domLayoutFull', url: window.location.href, - hostname: window.location.hostname + hostname: window.location.hostname, + layout: patchLayoutData(getLayoutData()) }; - - if ( document.readyState !== 'complete' ) { - response.status = 'busy'; - return response; - } - - // No mutation observer means we need to send full layout - if ( mutationObserver === null ) { - cosmeticFilterMapper.reset(); - mutationObserver = new MutationObserver(onMutationObserved); - mutationObserver.observe(document.body, { - childList: true, - subtree: true - }); - response.status = 'full'; - response.layout = patchLayoutData(getLayoutData()); - return response; - } - - // Incremental layout - if ( journalEntries.length !== 0 ) { - response.status = 'incremental'; - response.journal = journalEntries; - response.nodes = journalNodes; - journalEntries = []; - journalNodes = Object.create(null); - return response; - } - - response.status = 'nochange'; - return response; }; var reset = function() { @@ -511,9 +453,9 @@ var domLayout = (function() { }; var shutdown = function() { - if ( mutationTimer !== null ) { + if ( mutationTimer !== undefined ) { clearTimeout(mutationTimer); - mutationTimer = null; + mutationTimer = undefined; } if ( mutationObserver !== null ) { mutationObserver.disconnect(); @@ -521,8 +463,6 @@ var domLayout = (function() { } addedNodelists = []; removedNodelist = []; - journalEntries = []; - journalNodes = Object.create(null); nodeToIdMap = new WeakMap(); }; @@ -550,136 +490,6 @@ try { /******************************************************************************/ -var cosmeticFilterFromEntries = function(entries) { - var out = []; - var entry, i = entries.length; - while ( i-- ) { - entry = entries[i]; - out.push(cosmeticFilterFromTarget(entry.nid, entry.selector)); - } - return out; -}; - -/******************************************************************************/ - -// Extract the best possible cosmetic filter, i.e. as specific as possible. - -var cosmeticFilterFromNode = function(elem) { - var tagName = elem.localName; - var prefix = ''; - var suffix = []; - var v, i; - - // Id - v = typeof elem.id === 'string' && cssEscape(elem.id); - if ( v ) { - suffix.push('#', v); - } - - // Class(es) - if ( suffix.length === 0 ) { - v = elem.classList; - if ( v ) { - i = v.length || 0; - while ( i-- ) { - suffix.push('.' + cssEscape(v.item(i))); - } - } - } - - // Tag name - if ( suffix.length === 0 ) { - prefix = tagName; - } - - // Attributes (depends on tag name) - var attributes = [], attr; - switch ( tagName ) { - case 'a': - v = elem.getAttribute('href'); - if ( v ) { - v = v.replace(/\?.*$/, ''); - if ( v.length ) { - attributes.push({ k: 'href', v: v }); - } - } - break; - case 'img': - v = elem.getAttribute('alt'); - if ( v && v.length !== 0 ) { - attributes.push({ k: 'alt', v: v }); - } - break; - default: - break; - } - while ( (attr = attributes.pop()) ) { - if ( attr.v.length === 0 ) { - continue; - } - suffix.push('[', attr.k, '="', cssEscape(attr.v, true), '"]'); - } - - var selector = prefix + suffix.join(''); - - // https://github.com/chrisaljoudi/uBlock/issues/637 - // If the selector is still ambiguous at this point, further narrow using - // `nth-of-type`. It is preferable to use `nth-of-type` as opposed to - // `nth-child`, as `nth-of-type` is less volatile. - var parent = elem.parentElement; - if ( elementsFromSelector(cssScope + selector, parent).length > 1 ) { - i = 1; - while ( elem.previousElementSibling !== null ) { - elem = elem.previousElementSibling; - if ( elem.localName !== tagName ) { - continue; - } - i++; - } - selector += ':nth-of-type(' + i + ')'; - } - - return selector; -}; - -/******************************************************************************/ - -var cosmeticFilterFromTarget = function(nid, coarseSelector) { - var elems = elementsFromSelector(coarseSelector); - var target = null; - var i = elems.length; - while ( i-- ) { - if ( nodeToIdMap.get(elems[i]) === nid ) { - target = elems[i]; - break; - } - } - if ( target === null ) { - return coarseSelector; - } - // Find the most concise selector from the target node - var segments = [], segment; - var node = target; - while ( node !== document.body ) { - segment = cosmeticFilterFromNode(node); - segments.unshift(segment); - if ( segment.charAt(0) === '#' ) { - break; - } - node = node.parentElement; - } - var fineSelector = segments.join(' > '); - if ( fineSelector.charAt(0) === '#' ) { - return fineSelector; - } - if ( fineSelector.charAt(0) === '.' && elementsFromSelector(fineSelector).length === 1 ) { - return fineSelector; - } - return 'body > ' + fineSelector; -}; - -/******************************************************************************/ - var cosmeticFilterMapper = (function() { // https://github.com/gorhill/uBlock/issues/546 var matchesFnName; @@ -735,7 +545,6 @@ var cosmeticFilterMapper = (function() { }; var incremental = function(rootNode) { - vAPI.domFilterer.toggle(false); nodesFromStyleTag(rootNode); }; @@ -821,13 +630,10 @@ var getSvgRootChildren = function() { } }; -var highlightElements = function(scrollTo) { - var wv = pickerRoot.contentWindow.innerWidth; - var hv = pickerRoot.contentWindow.innerHeight; +var highlightElements = function() { 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; var svgRootChildren = getSvgRootChildren(); islands = []; @@ -914,43 +720,23 @@ var highlightElements = function(scrollTo) { islands.push(poly); } svgRootChildren[3].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 onScrolled = function() { - highlightElements(); -}; +var onScrolled = (function() { + var buffered = false; + var timerHandler = function() { + buffered = false; + highlightElements(); + }; + return function() { + if ( buffered === false ) { + window.requestAnimationFrame(timerHandler); + buffered = true; + } + }; +})(); /******************************************************************************/ @@ -979,17 +765,6 @@ var nodesFromFilter = function(selector) { /******************************************************************************/ -var shutdown = function() { - cosmeticFilterMapper.shutdown(); - domLayout.shutdown(); - vAPI.messaging.removeAllChannelListeners('domInspector'); - window.removeEventListener('scroll', onScrolled, true); - document.documentElement.removeChild(pickerRoot); - pickerRoot = svgRoot = null; -}; - -/******************************************************************************/ - var toggleExceptions = function(nodes, targetState) { for ( var node of nodes ) { if ( targetState ) { @@ -1010,25 +785,57 @@ var toggleFilter = function(nodes, targetState) { } }; +var resetToggledNodes = function() { + rwGreenNodes.clear(); + rwRedNodes.clear(); +}; + // https://www.youtube.com/watch?v=L5jRewnxSBY +/******************************************************************************/ + +var start = function() { + var onReady = function(ev) { + if ( ev ) { + document.removeEventListener(ev.type, onReady); + } + vAPI.messaging.sendTo(loggerConnectionId, domLayout.get()); + vAPI.domFilterer.toggle(false); + highlightElements(); + }; + if ( document.readyState === 'loading' ) { + document.addEventListener('DOMContentLoaded', onReady); + } else { + onReady(); + } +}; + +/******************************************************************************/ + +var shutdown = function() { + cosmeticFilterMapper.shutdown(); + domLayout.shutdown(); + vAPI.messaging.disconnectFrom(loggerConnectionId); + window.removeEventListener('scroll', onScrolled, true); + document.documentElement.removeChild(pickerRoot); + pickerRoot = svgRoot = null; +}; + /******************************************************************************/ /******************************************************************************/ var onMessage = function(request) { - var response; + var response, + nodes; switch ( request.what ) { case 'commitFilters': highlightElements(); break; - case 'cookFilters': - response = cosmeticFilterFromEntries(request.entries); - break; - case 'domLayout': - response = domLayout.get(request.fingerprint); + response = domLayout.get(); + highlightElements(); break; case 'highlightMode': @@ -1037,13 +844,18 @@ var onMessage = function(request) { case 'highlightOne': blueNodes = selectNodes(request.selector, request.nid); - highlightElements(request.scrollTo); + highlightElements(); + break; + + case 'resetToggledNodes': + resetToggledNodes(); + highlightElements(); break; case 'showCommitted': blueNodes = []; // TODO: show only the new filters and exceptions. - highlightElements(true); + highlightElements(); break; case 'showInteractive': @@ -1052,23 +864,17 @@ var onMessage = function(request) { break; case 'toggleFilter': - toggleExceptions( - nodesFromFilter(request.filter), - request.target - ); - highlightElements(true); + nodes = selectNodes(request.selector, request.nid); + if ( nodes.length !== 0 ) { nodes[0].scrollIntoView(); } + toggleExceptions(nodesFromFilter(request.filter), request.target); + highlightElements(); break; case 'toggleNodes': - toggleFilter( - selectNodes(request.selector, request.nid), - request.target - ); - highlightElements(true); - break; - - case 'shutdown': - shutdown(); + nodes = selectNodes(request.selector, request.nid); + if ( nodes.length !== 0 ) { nodes[0].scrollIntoView(); } + toggleFilter(nodes, request.target); + highlightElements(); break; default: @@ -1078,42 +884,35 @@ var onMessage = function(request) { return response; }; +var messagingHandler = function(msg) { + switch ( msg.what ) { + case 'connectionAccepted': + loggerConnectionId = msg.id; + start(); + break; + case 'connectionBroken': + shutdown(); + break; + case 'connectionMessage': + onMessage(msg.payload); + break; + } +}; + /******************************************************************************/ // Install DOM inspector widget -pickerRoot = document.createElement('iframe'); -pickerRoot.classList.add(sessionId); -pickerRoot.classList.add('dom-inspector'); -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 bootstrap = function(ev) { + if ( ev ) { + pickerRoot.removeEventListener(ev.type, bootstrap); + } var pickerDoc = this.contentDocument; var style = pickerDoc.createElement('style'); style.textContent = [ 'body {', 'background-color: transparent;', - 'cursor: not-allowed;', '}', 'svg {', 'height: 100%;', @@ -1155,9 +954,34 @@ pickerRoot.onload = function() { cosmeticFilterMapper.reset(); highlightElements(); - vAPI.messaging.addChannelListener('domInspector', onMessage); + vAPI.messaging.connectTo('domInspector', 'loggerUI', messagingHandler); }; +pickerRoot = document.createElement('iframe'); +pickerRoot.classList.add(sessionId); +pickerRoot.classList.add('dom-inspector'); +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', + 'pointer-events:none;', + 'top: 0', + 'visibility: visible', + 'width: 100%', + 'z-index: 2147483647', + '' +].join(' !important;\n'); + +pickerRoot.addEventListener('load', bootstrap); document.documentElement.appendChild(pickerRoot); /******************************************************************************/ diff --git a/src/js/ublock.js b/src/js/ublock.js index d22a36de7554f..9896a43c64af0 100644 --- a/src/js/ublock.js +++ b/src/js/ublock.js @@ -579,14 +579,14 @@ var reInvalidHostname = /[^a-z0-9.\-\[\]:]/, pendingEntries.set(key, new Entry(tabId, scriptlet, callback)); } vAPI.tabs.injectScript(tabId, { - file: 'js/scriptlets/' + scriptlet + '.js' + 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', + file: '/js/scriptlets/' + scriptlet + '.js', allFrames: true }); }; diff --git a/src/logger-ui.html b/src/logger-ui.html index 34740fc696b66..9539f44a90e79 100644 --- a/src/logger-ui.html +++ b/src/logger-ui.html @@ -16,15 +16,17 @@