some more work re inspector

This commit is contained in:
gorhill 2015-06-28 17:42:08 -04:00
parent 58b6552207
commit 39b0d719c0
8 changed files with 871 additions and 644 deletions

View File

@ -100,6 +100,10 @@ MessagingListeners.prototype.remove = function(callback) {
this.listeners.splice(this.listeners.indexOf(callback), 1);
};
MessagingListeners.prototype.removeAll = function() {
this.listeners = [];
};
MessagingListeners.prototype.process = function(msg) {
var listeners = this.listeners;
var n = listeners.length;
@ -209,6 +213,10 @@ MessagingChannel.prototype.removeListener = function(callback) {
this.listeners.remove(callback);
};
MessagingChannel.prototype.removeAllListeners = function() {
this.listeners.removeAll();
};
/******************************************************************************/
vAPI.messaging = {

View File

@ -94,6 +94,10 @@ MessagingListeners.prototype.remove = function(callback) {
this.listeners.splice(this.listeners.indexOf(callback), 1);
};
MessagingListeners.prototype.removeAll = function() {
this.listeners = [];
};
MessagingListeners.prototype.process = function(msg) {
var listeners = this.listeners;
var n = listeners.length;
@ -199,6 +203,10 @@ MessagingChannel.prototype.removeListener = function(callback) {
this.listeners.remove(callback);
};
MessagingChannel.prototype.removeAllListeners = function() {
this.listeners.removeAll();
};
/******************************************************************************/
vAPI.messaging = {

View File

@ -0,0 +1,88 @@
#domInspector {
border-top: 1px solid #ccc;
display: none;
max-height: 40%;
min-height: 40%;
overflow: auto;
}
#domInspector.enabled {
display: block;
}
#domInspector .permatoolbar {
position: absolute;
}
#domInspector .permatoolbar .highlightMode.invert {
transform: rotate(180deg);
}
#domInspector > ul:first-of-type {
padding-left: 0.5em;
}
#domInspector ul {
background-color: #fff;
margin: 0;
padding-left: 1em;
}
#domInspector li {
list-style-type: none;
white-space: nowrap;
}
#domInspector li.isCosmeticHide,
#domInspector li.isCosmeticHide ul,
#domInspector li.isCosmeticHide li {
background-color: #fee;
}
#domInspector li > * {
margin-right: 1em;
}
#domInspector li > span:first-child {
color: #000;
cursor: default;
display: inline-block;
margin-right: 0;
opacity: 0.5;
visibility: hidden;
width: 1em;
}
#domInspector li > span:first-child:hover {
opacity: 1;
}
#domInspector li > *:last-child {
margin-right: 0;
}
#domInspector li > span:first-child:before {
content: '\a0';
}
#domInspector li.branch > span:first-child:before {
content: '\25b8';
visibility: visible;
}
#domInspector li.branch.show > span:first-child:before {
content: '\25be';
visibility: visible;
}
#domInspector li.branch.hasCosmeticHide > span:first-child:before {
color: red;
}
#domInspector li > code {
cursor: pointer;
font: 12px/1.4 monospace;
}
#domInspector li > code.off {
text-decoration: line-through;
}
#domInspector li > span {
color: #aaa;
}
#domInspector li > code.filter {
color: red;
}
#domInspector li > ul {
display: none;
}
#domInspector li.show > ul {
display: block;
}
#cosmeticFilteringDialog .dialog textarea {
height: 40vh;
}

View File

@ -16,6 +16,12 @@ body {
input:focus {
background-color: #ffe;
}
textarea {
box-sizing: border-box;
direction: ltr;
resize: none;
width: 100%;
}
.permatoolbar {
background-color: white;
border: 0;
@ -55,85 +61,6 @@ input:focus {
padding: 0.2em 0;
}
#domInspector {
border-top: 1px solid #ccc;
display: none;
max-height: 40%;
min-height: 40%;
overflow: auto;
}
#domInspector.enabled {
display: block;
}
#domInspector > ul:first-child {
padding-left: 0;
}
#domInspector ul {
background-color: #fff;
margin: 0;
padding-left: 1em;
}
#domInspector li {
list-style-type: none;
white-space: nowrap;
}
#domInspector li.isCosmeticHide,
#domInspector li.isCosmeticHide ul,
#domInspector li.isCosmeticHide li {
background-color: #fee;
}
#domInspector li > * {
margin-right: 1em;
}
#domInspector li > span:first-child {
color: #000;
cursor: default;
display: inline-block;
margin-right: 0;
opacity: 0.5;
visibility: hidden;
width: 1em;
}
#domInspector li > span:first-child:hover {
opacity: 1;
}
#domInspector li > *:last-child {
margin-right: 0;
}
#domInspector li > span:first-child:before {
content: '\a0';
}
#domInspector li.branch > span:first-child:before {
content: '\25b8';
visibility: visible;
}
#domInspector li.branch.show > span:first-child:before {
content: '\25be';
visibility: visible;
}
#domInspector li.branch.hasCosmeticHide > span:first-child:before {
color: red;
}
#domInspector li > code {
cursor: pointer;
font: 12px/1.4 monospace;
}
#domInspector li > code.off {
text-decoration: line-through;
}
#domInspector li > span {
color: #aaa;
}
#domInspector li > code.filter {
color: red;
}
#domInspector li > ul {
display: none;
}
#domInspector li.show > ul {
display: block;
}
#events {
border-top: 1px solid #ccc;
font: 13px sans-serif;
@ -623,11 +550,7 @@ body.colorBlind #netFilteringDialog .dialog > div.containers > div.dynamic tr.e
margin: 0.75em 0;
}
#netFilteringDialog .dialog > div.containers > div.static textarea {
box-sizing: border-box;
direction: ltr;
height: 6em;
resize: none;
width: 100%;
}
#netFilteringDialog .dialog > div.containers > div.static > p:nth-of-type(2) {
text-align: center;

View File

@ -0,0 +1,671 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2015 Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
/* global vAPI, uDom */
/******************************************************************************/
(function() {
'use strict';
/******************************************************************************/
// Don't bother if the browser is not modern enough.
if ( typeof Map === undefined || typeof WeakMap === undefined ) {
return;
}
/******************************************************************************/
var logger = self.logger;
var messager = logger.messager;
var inspectedTabId = '';
var inspectedHostname = '';
var pollTimer = null;
var fingerprint = null;
var showdomButton = uDom.nodeFromId('showdom');
var inspector = uDom.nodeFromId('domInspector');
var domTree = uDom.nodeFromId('domTree');
var tabSelector = uDom.nodeFromId('pageSelector');
/******************************************************************************/
var nodeFromDomEntry = function(entry) {
var node, value;
var li = document.createElement('li');
li.setAttribute('id', entry.nid);
// expander/collapser
node = document.createElement('span');
li.appendChild(node);
// selector
node = document.createElement('code');
node.textContent = entry.sel;
li.appendChild(node);
// descendant count
value = entry.cnt || 0;
node = document.createElement('span');
node.textContent = value !== 0 ? value.toLocaleString() : '';
node.setAttribute('data-cnt', value);
li.appendChild(node);
// cosmetic filter
if ( entry.filter !== undefined ) {
node = document.createElement('code');
node.classList.add('filter');
node.textContent = entry.filter;
li.appendChild(node);
li.classList.add('isCosmeticHide');
}
return li;
};
/******************************************************************************/
var appendListItem = function(ul, li) {
ul.appendChild(li);
// Ancestor nodes of a node which is affected by a cosmetic filter will
// be marked as "containing cosmetic filters", for user convenience.
if ( li.classList.contains('isCosmeticHide') === false ) {
return;
}
for (;;) {
li = li.parentElement.parentElement;
if ( li === null ) {
break;
}
li.classList.add('hasCosmeticHide');
}
};
/******************************************************************************/
var renderDOMFull = function(response) {
var ul = inspector.removeChild(domTree);
logger.removeAllChildren(domTree);
var lvl = 0;
var entries = response.layout;
var n = entries.length;
var li, entry;
for ( var i = 0; i < n; i++ ) {
entry = entries[i];
if ( entry.lvl === lvl ) {
li = nodeFromDomEntry(entry);
appendListItem(ul, li);
//expandIfBlockElement(li);
continue;
}
if ( entry.lvl > lvl ) {
ul = document.createElement('ul');
li.appendChild(ul);
li.classList.add('branch');
li = nodeFromDomEntry(entry);
appendListItem(ul, li);
//expandIfBlockElement(li);
lvl = entry.lvl;
continue;
}
// entry.lvl < lvl
while ( entry.lvl < lvl ) {
ul = li.parentNode;
li = ul.parentNode;
ul = li.parentNode;
lvl -= 1;
}
li = nodeFromDomEntry(entry);
ul.appendChild(li);
}
while ( ul.parentNode !== null ) {
ul = ul.parentNode;
}
ul.firstElementChild.classList.add('show');
inspector.appendChild(domTree);
};
/******************************************************************************/
var patchIncremental = function(from, delta) {
var span, cnt;
var li = from.parentElement.parentElement;
var patchCosmeticHide = delta >= 0 &&
from.classList.contains('isCosmeticFilter') &&
li.classList.contains('hasCosmeticFilter') === false;
// Include descendants count when removing a node
if ( delta < 0 ) {
delta -= countFromNode(from);
}
for ( ; li.localName === 'li'; li = li.parentElement.parentElement ) {
span = li.children[2];
if ( delta !== 0 ) {
cnt = countFromNode(li) + delta;
span.textContent = cnt !== 0 ? cnt.toLocaleString() : '';
span.setAttribute('data-cnt', cnt);
}
if ( patchCosmeticHide ) {
li.classList.add('hasCosmeticFilter');
}
}
};
/******************************************************************************/
var renderDOMIncremental = function(response) {
// Process each journal entry:
// 1 = node added
// -1 = node removed
var journal = response.journal;
var nodes = response.nodes;
var entry, previous, li, ul;
for ( var i = 0, n = journal.length; i < n; i++ ) {
entry = journal[i];
// Remove node
if ( entry.what === -1 ) {
li = document.getElementById(entry.nid);
if ( li === null ) {
continue;
}
patchIncremental(li, -1);
li.parentNode.removeChild(li);
continue;
}
// Modify node
if ( entry.what === 0 ) {
// TODO: update selector/filter
continue;
}
// Add node as sibling
if ( entry.what === 1 && entry.l ) {
previous = document.getElementById(entry.l);
// This should not happen
if ( previous === null ) {
// throw new Error('No left sibling!?');
continue;
}
ul = previous.parentElement;
li = nodeFromDomEntry(nodes[entry.nid]);
ul.insertBefore(li, previous.nextElementSibling);
patchIncremental(li, 1);
continue;
}
// Add node as child
if ( entry.what === 1 && entry.u ) {
li = document.getElementById(entry.u);
// This should not happen
if ( li === null ) {
// throw new Error('No parent!?');
continue;
}
ul = li.querySelector('ul');
if ( ul === null ) {
ul = document.createElement('ul');
li.appendChild(ul);
li.classList.add('branch');
}
li = nodeFromDomEntry(nodes[entry.nid]);
ul.appendChild(li);
patchIncremental(li, 1);
continue;
}
}
};
/******************************************************************************/
var countFromNode = function(li) {
var span = li.children[2];
var cnt = parseInt(span.getAttribute('data-cnt'), 10);
return isNaN(cnt) ? cnt : 0;
};
/******************************************************************************/
var selectorFromNode = function(node, nth) {
var selector = '';
var code;
if ( nth === undefined ) {
nth = 1;
}
while ( node !== null ) {
if ( node.localName === 'li' ) {
code = node.querySelector('code:nth-of-type(' + nth + ')');
if ( code !== null ) {
selector = code.textContent + ' > ' + selector;
if ( selector.indexOf('#') !== -1 ) {
break;
}
nth = 1;
}
}
node = node.parentElement;
}
return selector.slice(0, -3);
};
/******************************************************************************/
var nidFromNode = function(node) {
var li = node;
while ( li !== null ) {
if ( li.localName === 'li' ) {
return li.id || '';
}
li = li.parentElement;
}
return '';
};
/******************************************************************************/
var startDialog = (function() {
var dialog = uDom.nodeFromId('cosmeticFilteringDialog');
var candidateFilters = [];
var onClick = function(ev) {
var target = ev.target;
// click outside the dialog proper
if ( target.classList.contains('modalDialog') ) {
return stop();
}
ev.stopPropagation();
};
var stop = function() {
dialog.removeEventListener('click', onClick, true);
document.body.removeChild(dialog);
};
var start = function() {
// Collect all selectors which are currently toggled
var node, filters = [];
var nodes = domTree.querySelectorAll('code.off');
for ( var i = 0; i < nodes.length; i++ ) {
node = nodes[i];
if ( node.classList.contains('filter') ) {
filters.push({
prefix: '#@#',
nid: '',
selector: node.textContent
});
} else {
filters.push({
prefix: '##',
nid: nidFromNode(node),
selector: node.textContent
});
}
}
// TODO: Send filters through dom-inspector.js for further processing.
candidateFilters = filters;
var taValue = [], filter;
for ( i = 0; i < filters.length; i++ ) {
filter = filters[i];
taValue.push(inspectedHostname + filter.prefix + filter.selector);
}
dialog.querySelector('textarea').value = taValue.join('\n');
document.body.appendChild(dialog);
dialog.addEventListener('click', onClick, true);
};
return start;
})();
/******************************************************************************/
var onClick = function(ev) {
ev.stopPropagation();
if ( inspectedTabId === '' ) {
return;
}
var target = ev.target;
var parent = target.parentElement;
// Expand/collapse branch
if (
target.localName === 'span' &&
parent instanceof HTMLLIElement &&
parent.classList.contains('branch') &&
target === parent.firstElementChild
) {
target.parentElement.classList.toggle('show');
return;
}
// Toggle selector
if ( target.localName === 'code' ) {
var original = target.classList.contains('filter') === false;
messager.send({
what: 'postMessageTo',
senderTabId: null,
senderChannel: 'logger-ui.js',
receiverTabId: inspectedTabId,
receiverChannel: 'dom-inspector.js',
msg: {
what: 'toggleNodes',
original: original,
target: original !== target.classList.toggle('off'),
selector: selectorFromNode(target, original ? 1 : 2),
nid: original ? nidFromNode(target) : ''
}
});
var cantCreate = inspector.querySelector('#domTree .off') === null;
inspector.querySelector('.permatoolbar .revert').classList.toggle('disabled', cantCreate);
inspector.querySelector('.permatoolbar .commit').classList.toggle('disabled', cantCreate);
return;
}
};
/******************************************************************************/
var onMouseOver = (function() {
var mouseoverTarget = null;
var mouseoverTimer = null;
var timerHandler = function() {
mouseoverTimer = null;
messager.send({
what: 'postMessageTo',
senderTabId: null,
senderChannel: 'logger-ui.js',
receiverTabId: inspectedTabId,
receiverChannel: 'dom-inspector.js',
msg: {
what: 'highlightOne',
selector: selectorFromNode(mouseoverTarget),
nid: nidFromNode(mouseoverTarget),
scrollTo: true
}
});
};
return function(ev) {
if ( inspectedTabId === '' ) {
return;
}
// Find closest `li`
var target = ev.target;
while ( target !== null ) {
if ( target.localName === 'li' ) {
break;
}
target = target.parentElement;
}
if ( target === mouseoverTarget ) {
return;
}
mouseoverTarget = target;
if ( mouseoverTimer === null ) {
mouseoverTimer = vAPI.setTimeout(timerHandler, 50);
}
};
})();
/******************************************************************************/
var currentTabId = function() {
if ( showdomButton.classList.contains('active') === false ) {
return '';
}
var tabId = logger.tabIdFromClassName(tabSelector.value) || '';
return tabId !== 'bts' ? tabId : '';
};
/******************************************************************************/
var cancelPollTimer = function() {
if ( pollTimer !== null ) {
clearTimeout(pollTimer);
pollTimer = null;
}
};
/******************************************************************************/
var onDOMFetched = function(response) {
if ( response === undefined || currentTabId() !== inspectedTabId ) {
shutdownInspector(inspectedTabId);
injectInspectorAsync(250);
return;
}
switch ( response.status ) {
case 'full':
renderDOMFull(response);
fingerprint = response.fingerprint;
inspectedHostname = response.hostname;
break;
case 'incremental':
renderDOMIncremental(response);
break;
case 'nochange':
case 'busy':
break;
default:
break;
}
fetchDOMAsync();
};
/******************************************************************************/
var fetchDOM = function() {
messager.send({
what: 'postMessageTo',
senderTabId: null,
senderChannel: 'logger-ui.js',
receiverTabId: inspectedTabId,
receiverChannel: 'dom-inspector.js',
msg: {
what: 'domLayout',
fingerprint: fingerprint
}
});
pollTimer = vAPI.setTimeout(function() {
pollTimer = null;
onDOMFetched();
}, 1001);
};
/******************************************************************************/
var fetchDOMAsync = function(delay) {
if ( pollTimer !== null ) {
return;
}
pollTimer = vAPI.setTimeout(function() {
pollTimer = null;
fetchDOM();
}, delay || 1001);
};
/******************************************************************************/
var injectInspector = function() {
var tabId = currentTabId();
// No valid tab, go back
if ( tabId === '' ) {
injectInspectorAsync();
return;
}
inspectedTabId = tabId;
fingerprint = null;
messager.send({
what: 'scriptlet',
tabId: tabId,
scriptlet: 'dom-inspector'
});
fetchDOMAsync(250);
};
/******************************************************************************/
var injectInspectorAsync = function(delay) {
if ( pollTimer !== null ) {
return;
}
if ( showdomButton.classList.contains('active') === false ) {
return;
}
pollTimer = vAPI.setTimeout(function() {
pollTimer = null;
injectInspector();
}, delay || 1001);
};
/******************************************************************************/
var shutdownInspector = function(tabId) {
messager.send({
what: 'postMessageTo',
senderTabId: null,
senderChannel: 'logger-ui.js',
receiverTabId: tabId,
receiverChannel: 'dom-inspector.js',
msg: { what: 'shutdown', }
});
logger.removeAllChildren(domTree);
cancelPollTimer();
inspectedTabId = '';
};
/******************************************************************************/
var onTabIdChanged = function() {
if ( inspectedTabId !== currentTabId() ) {
shutdownInspector();
injectInspectorAsync(250);
}
};
/******************************************************************************/
var toggleHighlightMode = function() {
messager.send({
what: 'postMessageTo',
senderTabId: null,
senderChannel: 'logger-ui.js',
receiverTabId: inspectedTabId,
receiverChannel: 'dom-inspector.js',
msg: {
what: 'highlightMode',
invert: uDom.nodeFromSelector('#domInspector .permatoolbar .highlightMode').classList.toggle('invert')
}
});
};
/******************************************************************************/
var revert = function() {
uDom('#domTree .off').removeClass('off');
messager.send({
what: 'postMessageTo',
senderTabId: null,
senderChannel: 'logger-ui.js',
receiverTabId: inspectedTabId,
receiverChannel: 'dom-inspector.js',
msg: { what: 'resetToggledNodes' }
});
inspector.querySelector('.permatoolbar .revert').classList.add('disabled');
inspector.querySelector('.permatoolbar .commit').classList.add('disabled');
};
/******************************************************************************/
var onMessage = function(request) {
var msg = request.what === 'postMessageTo' ? request.msg : request;
switch ( msg.what ) {
case 'domLayout':
cancelPollTimer();
onDOMFetched(msg);
break;
default:
break;
}
};
/******************************************************************************/
var toggleOn = function() {
window.addEventListener('beforeunload', toggleOff);
tabSelector.addEventListener('change', onTabIdChanged);
domTree.addEventListener('click', onClick, true);
domTree.addEventListener('mouseover', onMouseOver, true);
uDom.nodeFromSelector('#domInspector .permatoolbar .highlightMode').addEventListener('click', toggleHighlightMode);
uDom.nodeFromSelector('#domInspector .permatoolbar .revert').addEventListener('click', revert);
uDom.nodeFromSelector('#domInspector .permatoolbar .commit').addEventListener('click', startDialog);
inspector.classList.add('enabled');
messager.addListener(onMessage);
injectInspector();
// Adjust tree view for toolbar height
domTree.style.setProperty(
'margin-top',
inspector.querySelector('.permatoolbar').clientHeight + 'px'
);
};
/******************************************************************************/
var toggleOff = function() {
messager.removeListener(onMessage);
cancelPollTimer();
shutdownInspector();
window.removeEventListener('beforeunload', toggleOff);
tabSelector.removeEventListener('change', onTabIdChanged);
domTree.removeEventListener('click', onClick, true);
domTree.removeEventListener('mouseover', onMouseOver, true);
uDom.nodeFromSelector('#domInspector .permatoolbar .highlightMode').removeEventListener('click', toggleHighlightMode);
uDom.nodeFromSelector('#domInspector .permatoolbar .revert').removeEventListener('click', revert);
uDom.nodeFromSelector('#domInspector .permatoolbar .commit').removeEventListener('click', startDialog);
inspectedTabId = '';
inspector.classList.remove('enabled');
};
/******************************************************************************/
var toggle = function() {
if ( showdomButton.classList.toggle('active') ) {
toggleOn();
} else {
toggleOff();
}
};
/******************************************************************************/
showdomButton.addEventListener('click', toggle);
/******************************************************************************/
})();

View File

@ -30,6 +30,30 @@
/******************************************************************************/
var logger = self.logger = {};
var messager = logger.messager = vAPI.messaging.channel('logger-ui.js');
/******************************************************************************/
var removeAllChildren = logger.removeAllChildren = function(node) {
while ( node.firstChild ) {
node.removeChild(node.firstChild);
}
};
/******************************************************************************/
var tabIdFromClassName = logger.tabIdFromClassName = function(className) {
var matches = className.match(/(?:^| )tab_([^ ]+)(?: |$)/);
if ( matches === null ) {
return '';
}
return matches[1];
};
/******************************************************************************/
/******************************************************************************/
// Adjust top padding of content table, to match that of toolbar height.
(function() {
@ -47,8 +71,6 @@
/******************************************************************************/
var messager = vAPI.messaging.channel('logger-ui.js');
var tbody = document.querySelector('#events tbody');
var trJunkyard = [];
var tdJunkyard = [];
@ -61,7 +83,6 @@ var allTabIdsToken;
var hiddenTemplate = document.querySelector('#hiddenTemplate > span');
var reRFC3986 = /^([^:\/?#]+:)?(\/\/[^\/?#]*)?([^?#]*)(\?[^#]*)?(#.*)?/;
var netFilteringDialog = uDom.nodeFromId('netFilteringDialog');
var filterFinderDialog = uDom.nodeFromId('filterFinderDialog');
var prettyRequestTypes = {
'main_frame': 'doc',
@ -98,14 +119,6 @@ var dateOptions = {
/******************************************************************************/
var removeAllChildren = function(node) {
while ( node.firstChild ) {
node.removeChild(node.firstChild);
}
};
/******************************************************************************/
var classNameFromTabId = function(tabId) {
if ( tabId === noTabId ) {
return 'tab_bts';
@ -116,503 +129,6 @@ var classNameFromTabId = function(tabId) {
return '';
};
/******************************************************************************/
var tabIdFromClassName = function(className) {
var matches = className.match(/(?:^| )tab_([^ ]+)(?: |$)/);
if ( matches === null ) {
return '';
}
return matches[1];
};
/******************************************************************************/
/******************************************************************************/
// DOM inspector
(function domInspector() {
// Don't bother if the browser is not modern enough.
if ( typeof Map === undefined || typeof WeakMap === undefined ) {
return;
}
var inspectedTabId = '';
var currentSelector = '';
var showdomButton = uDom.nodeFromId('showdom');
var inspector = uDom.nodeFromId('domInspector');
var tabSelector = uDom.nodeFromId('pageSelector');
var nodeFromDomEntry = function(entry) {
var node, value;
var li = document.createElement('li');
li.setAttribute('id', entry.nid);
// expander/collapser
node = document.createElement('span');
li.appendChild(node);
// selector
node = document.createElement('code');
node.textContent = entry.sel;
li.appendChild(node);
// descendant count
value = entry.cnt || 0;
node = document.createElement('span');
node.textContent = value !== 0 ? value.toLocaleString() : '';
node.setAttribute('data-cnt', value);
li.appendChild(node);
// cosmetic filter
if ( entry.filter !== undefined ) {
node = document.createElement('code');
node.classList.add('filter');
node.textContent = entry.filter;
li.appendChild(node);
li.classList.add('isCosmeticHide');
}
return li;
};
var appendListItem = function(ul, li) {
ul.appendChild(li);
// Ancestor nodes of a node which is affected by a cosmetic filter will
// be marked as "containing cosmetic filters", for user convenience.
if ( li.classList.contains('isCosmeticHide') === false ) {
return;
}
for (;;) {
li = li.parentElement.parentElement;
if ( li === null ) {
break;
}
li.classList.add('hasCosmeticHide');
}
};
var renderDOMFull = function(response) {
var ul = document.createElement('ul');
var lvl = 0;
var entries = response.layout;
var n = entries.length;
var li, entry;
for ( var i = 0; i < n; i++ ) {
entry = entries[i];
if ( entry.lvl === lvl ) {
li = nodeFromDomEntry(entry);
appendListItem(ul, li);
//expandIfBlockElement(li);
continue;
}
if ( entry.lvl > lvl ) {
ul = document.createElement('ul');
li.appendChild(ul);
li.classList.add('branch');
li = nodeFromDomEntry(entry);
appendListItem(ul, li);
//expandIfBlockElement(li);
lvl = entry.lvl;
continue;
}
// entry.lvl < lvl
while ( entry.lvl < lvl ) {
ul = li.parentNode;
li = ul.parentNode;
ul = li.parentNode;
lvl -= 1;
}
li = nodeFromDomEntry(entry);
ul.appendChild(li);
}
while ( ul.parentNode !== null ) {
ul = ul.parentNode;
}
ul.firstElementChild.classList.add('show');
removeAllChildren(inspector);
inspector.appendChild(ul);
};
var patchIncremental = function(from, delta) {
var span, cnt;
var li = from.parentElement.parentElement;
var patchCosmeticHide = delta >= 0 &&
from.classList.contains('isCosmeticFilter') &&
li.classList.contains('hasCosmeticFilter') === false;
// Include descendants count when removing a node
if ( delta < 0 ) {
delta -= countFromNode(from);
}
for ( ; li.localName === 'li'; li = li.parentElement.parentElement ) {
span = li.children[2];
if ( delta !== 0 ) {
cnt = countFromNode(li) + delta;
span.textContent = cnt !== 0 ? cnt.toLocaleString() : '';
span.setAttribute('data-cnt', cnt);
}
if ( patchCosmeticHide ) {
li.classList.add('hasCosmeticFilter');
}
}
};
var renderDOMIncremental = function(response) {
// Process each journal entry:
// 1 = node added
// -1 = node removed
var journal = response.journal;
var nodes = response.nodes;
var entry, previous, li, ul;
for ( var i = 0, n = journal.length; i < n; i++ ) {
entry = journal[i];
// Remove node
if ( entry.what === -1 ) {
li = document.getElementById(entry.nid);
if ( li === null ) {
continue;
}
patchIncremental(li, -1);
li.parentNode.removeChild(li);
continue;
}
// Modify node
if ( entry.what === 0 ) {
// TODO: update selector/filter
continue;
}
// Add node as sibling
if ( entry.what === 1 && entry.l ) {
previous = document.getElementById(entry.l);
// This should not happen
if ( previous === null ) {
// throw new Error('No left sibling!?');
continue;
}
ul = previous.parentElement;
li = nodeFromDomEntry(nodes[entry.nid]);
ul.insertBefore(li, previous.nextElementSibling);
patchIncremental(li, 1);
continue;
}
// Add node as child
if ( entry.what === 1 && entry.u ) {
li = document.getElementById(entry.u);
// This should not happen
if ( li === null ) {
// throw new Error('No parent!?');
continue;
}
ul = li.querySelector('ul');
if ( ul === null ) {
ul = document.createElement('ul');
li.appendChild(ul);
li.classList.add('branch');
}
li = nodeFromDomEntry(nodes[entry.nid]);
ul.appendChild(li);
patchIncremental(li, 1);
continue;
}
}
};
var countFromNode = function(li) {
var span = li.children[2];
var cnt = parseInt(span.getAttribute('data-cnt'), 10);
return isNaN(cnt) ? cnt : 0;
};
var selectorFromNode = function(node, nth) {
var selector = '';
var code;
if ( nth === undefined ) {
nth = 1;
}
while ( node !== null ) {
if ( node.localName === 'li' ) {
code = node.querySelector('code:nth-of-type(' + nth + ')');
if ( code !== null ) {
selector = code.textContent + ' > ' + selector;
if ( selector.indexOf('#') !== -1 ) {
break;
}
nth = 1;
}
}
node = node.parentElement;
}
return selector.slice(0, -3);
};
var onClick = function(ev) {
ev.stopPropagation();
if ( inspectedTabId === '' ) {
return;
}
var target = ev.target;
var parent = target.parentElement;
// Expand/collapse branch
if (
target.localName === 'span' &&
parent instanceof HTMLLIElement &&
parent.classList.contains('branch') &&
target === parent.firstElementChild
) {
target.parentElement.classList.toggle('show');
return;
}
// Toggle selector
if ( target.localName === 'code' ) {
var original = target.classList.contains('filter') === false;
messager.send({
what: 'postMessageTo',
senderTabId: null,
senderChannel: 'logger-ui.js',
receiverTabId: inspectedTabId,
receiverChannel: 'dom-inspector.js',
msg: {
what: 'toggleNodes',
original: original,
target: original !== target.classList.toggle('off'),
selector: selectorFromNode(target, original ? 1 : 2)
}
});
return;
}
// Highlight and scrollto
if ( target.localName === 'code' ) {
messager.send({
what: 'postMessageTo',
senderTabId: null,
senderChannel: 'logger-ui.js',
receiverTabId: inspectedTabId,
receiverChannel: 'dom-inspector.js',
msg: {
what: 'highlight',
selector: selectorFromNode(target),
scrollTo: true
}
});
return;
}
};
var onMouseOver = (function() {
var mouseoverTarget = null;
var mouseoverTimer = null;
var timerHandler = function() {
mouseoverTimer = null;
messager.send({
what: 'postMessageTo',
senderTabId: null,
senderChannel: 'logger-ui.js',
receiverTabId: inspectedTabId,
receiverChannel: 'dom-inspector.js',
msg: {
what: 'highlight',
selector: selectorFromNode(mouseoverTarget),
scrollTo: true
}
});
};
return function(ev) {
if ( inspectedTabId === '' ) {
return;
}
// Find closest `li`
var target = ev.target;
while ( target !== null ) {
if ( target.localName === 'li' ) {
break;
}
target = target.parentElement;
}
if ( target === mouseoverTarget ) {
return;
}
mouseoverTarget = target;
if ( mouseoverTimer === null ) {
mouseoverTimer = vAPI.setTimeout(timerHandler, 50);
}
};
})();
var pollTimer = null;
var fingerprint = null;
var currentTabId = function() {
if ( showdomButton.classList.contains('active') === false ) {
return '';
}
var tabId = tabIdFromClassName(tabSelector.value) || '';
return tabId !== 'bts' ? tabId : '';
};
var cancelPollTimer = function() {
if ( pollTimer !== null ) {
clearTimeout(pollTimer);
pollTimer = null;
}
};
var onDOMFetched = function(response) {
if ( response === undefined || currentTabId() !== inspectedTabId ) {
shutdownInspector(inspectedTabId);
injectInspectorAsync(250);
return;
}
switch ( response.status ) {
case 'full':
renderDOMFull(response);
fingerprint = response.fingerprint;
break;
case 'incremental':
renderDOMIncremental(response);
break;
case 'nochange':
case 'busy':
break;
default:
break;
}
fetchDOMAsync();
};
var fetchDOM = function() {
messager.send({
what: 'postMessageTo',
senderTabId: null,
senderChannel: 'logger-ui.js',
receiverTabId: inspectedTabId,
receiverChannel: 'dom-inspector.js',
msg: {
what: 'domLayout',
fingerprint: fingerprint
}
});
pollTimer = vAPI.setTimeout(function() {
pollTimer = null;
onDOMFetched();
}, 1001);
};
var fetchDOMAsync = function(delay) {
if ( pollTimer !== null ) {
return;
}
pollTimer = vAPI.setTimeout(function() {
pollTimer = null;
fetchDOM();
}, delay || 1001);
};
var injectInspector = function() {
var tabId = currentTabId();
// No valid tab, go back
if ( tabId === '' ) {
injectInspectorAsync();
return;
}
inspectedTabId = tabId;
fingerprint = null;
messager.send({
what: 'scriptlet',
tabId: tabId,
scriptlet: 'dom-inspector'
});
fetchDOMAsync(250);
};
var injectInspectorAsync = function(delay) {
if ( pollTimer !== null ) {
return;
}
if ( showdomButton.classList.contains('active') === false ) {
return;
}
pollTimer = vAPI.setTimeout(function() {
pollTimer = null;
injectInspector();
}, delay || 1001);
};
var shutdownInspector = function(tabId) {
messager.send({
what: 'postMessageTo',
senderTabId: null,
senderChannel: 'logger-ui.js',
receiverTabId: tabId,
receiverChannel: 'dom-inspector.js',
msg: { what: 'shutdown', }
});
removeAllChildren(inspector);
cancelPollTimer();
inspectedTabId = '';
};
var onTabIdChanged = function() {
if ( inspectedTabId !== currentTabId() ) {
shutdownInspector();
injectInspectorAsync(250);
}
};
var onMessage = function(request) {
var msg = request.what === 'postMessageTo' ? request.msg : request;
switch ( msg.what ) {
case 'domLayout':
cancelPollTimer();
onDOMFetched(msg);
break;
default:
break;
}
};
var toggleOn = function() {
window.addEventListener('beforeunload', toggleOff);
inspector.addEventListener('click', onClick, true);
inspector.addEventListener('mouseover', onMouseOver, true);
tabSelector.addEventListener('change', onTabIdChanged);
inspector.classList.add('enabled');
messager.addListener(onMessage);
injectInspector();
};
var toggleOff = function() {
messager.removeListener(onMessage);
cancelPollTimer();
shutdownInspector();
window.removeEventListener('beforeunload', toggleOff);
inspector.removeEventListener('click', onClick, true);
inspector.removeEventListener('mouseover', onMouseOver, true);
tabSelector.removeEventListener('change', onTabIdChanged);
currentSelector = inspectedTabId = '';
inspector.classList.remove('enabled');
};
var toggle = function() {
if ( showdomButton.classList.toggle('active') ) {
toggleOn();
} else {
toggleOff();
}
};
showdomButton.addEventListener('click', toggle);
})();
/******************************************************************************/
/******************************************************************************/
@ -1776,6 +1292,7 @@ var netFilteringManager = (function() {
var reverseLookupManager = (function() {
var reSentence1 = /\{\{filter\}\}/g;
var sentence1Template = vAPI.i18n('loggerStaticFilteringFinderSentence1');
var filterFinderDialog = uDom.nodeFromId('filterFinderDialog');
var removeAllChildren = function(node) {
while ( node.firstChild ) {

View File

@ -144,8 +144,9 @@ var svgOcean = null;
var svgIslands = null;
var svgRoot = null;
var pickerRoot = null;
var currentSelector = '';
var highlightedElements = [];
var nodeToIdMap = new WeakMap(); // No need to iterate
var toggledNodes = new Map();
/******************************************************************************/
@ -175,7 +176,6 @@ var domLayout = (function() {
};
var idGenerator = 0;
var nodeToIdMap = new WeakMap(); // No need to iterate
// This will be used to uniquely identify nodes across process.
@ -209,7 +209,7 @@ var domLayout = (function() {
}
return out;
})();
/*
var matchesSelector = (function() {
if ( typeof Element.prototype.matches === 'function' ) {
return 'matches';
@ -222,26 +222,7 @@ var domLayout = (function() {
}
return '';
})();
var hasManyMatches = function(node, selector) {
var fnName = matchesSelector;
if ( fnName === '' ) {
return true;
}
var child = node.firstElementChild;
var match = false;
while ( child !== null ) {
if ( child[fnName](selector) ) {
if ( match ) {
return true;
}
match = true;
}
child = child.nextElementSibling;
}
return false;
};
*/
var selectorFromNode = function(node) {
var str, attr, pos, sw, i;
var tag = node.localName;
@ -276,18 +257,6 @@ var domLayout = (function() {
selector += '[' + attr + sw + '="' + cssEscape(str) + '"]';
}
}
// The resulting selector must cause only one element to be selected. If
// it's not the case, further narrow using `nth-of-type` pseudo-class.
if ( hasManyMatches(node.parentElement, selector) ) {
i = 1;
while ( node.previousElementSibling ) {
node = node.previousElementSibling;
if ( node.localName === tag ) {
i += 1;
}
}
selector += ':nth-of-type(' + i + ')';
}
return selector;
};
@ -543,7 +512,8 @@ var domLayout = (function() {
var response = {
what: 'domLayout',
fingerprint: domFingerprint()
fingerprint: domFingerprint(),
hostname: window.location.hostname
};
// No mutation observer means we need to send full layout
@ -593,7 +563,8 @@ var domLayout = (function() {
/******************************************************************************/
var highlightElements = function(elems, scrollTo) {
var highlightElements = function(scrollTo) {
var elems = highlightedElements;
var wv = pickerRoot.contentWindow.innerWidth;
var hv = pickerRoot.contentWindow.innerHeight;
var ocean = ['M0 0h' + wv + 'v' + hv + 'h-' + wv, 'z'];
@ -684,15 +655,31 @@ var elementsFromSelector = function(filter) {
/******************************************************************************/
var highlight = function(scrollTo) {
var elements = elementsFromSelector(currentSelector);
highlightElements(elements, scrollTo);
var selectNodes = function(selector, nid) {
var nodes = elementsFromSelector(selector);
if ( nid === '' ) {
return nodes;
}
var i = nodes.length;
while ( i-- ) {
if ( nodeToIdMap.get(nodes[i]) === nid ) {
return [nodes[i]];
}
}
return [];
};
/******************************************************************************/
var hightlightNodes = function(selector, nid, scrollTo) {
highlightedElements = selectNodes(selector, nid);
highlightElements(scrollTo);
};
/******************************************************************************/
var onScrolled = function() {
highlight();
highlightElements();
};
/******************************************************************************/
@ -703,8 +690,7 @@ var onScrolled = function() {
// hidden, any = remove display property, don't remember original state
// hidden, hidden = set display to `none`
var toggleNodes = function(selector, originalState, targetState) {
var nodes = document.querySelectorAll(selector);
var toggleNodes = function(nodes, originalState, targetState) {
var i = nodes.length;
if ( i === 0 ) {
return;
@ -759,13 +745,13 @@ var resetToggledNodes = function() {
var shutdown = function() {
resetToggledNodes();
domLayout.shutdown();
localMessager.removeListener(onMessage);
localMessager.removeAllListeners();
localMessager.close();
localMessager = null;
window.removeEventListener('scroll', onScrolled, true);
document.documentElement.removeChild(pickerRoot);
pickerRoot = svgRoot = svgOcean = svgIslands = null;
currentSelector = '';
highlightedElements = [];
};
/******************************************************************************/
@ -779,15 +765,22 @@ var onMessage = function(request) {
response = domLayout.get(msg.fingerprint);
break;
case 'highlight':
currentSelector = msg.selector;
highlight(msg.scrollTo);
case 'highlightMode':
svgRoot.classList.toggle('invert', msg.invert);
break;
case 'highlightOne':
hightlightNodes(msg.selector, msg.nid, msg.scrollTo);
break;
case 'resetToggledNodes':
resetToggledNodes();
break;
case 'toggleNodes':
toggleNodes(msg.selector, msg.original, msg.target);
currentSelector = msg.selector;
highlight(true);
highlightedElements = selectNodes(msg.selector, msg.nid);
toggleNodes(highlightedElements, msg.original, msg.target);
highlightElements(true);
break;
case 'shutdown':
@ -863,6 +856,13 @@ pickerRoot.onload = function() {
'stroke: #FFF;',
'stroke-width: 0.5px;',
'}',
'svg.invert > path:first-child {',
'fill: rgba(0,0,255,0.1);',
'}',
'svg.invert > path + path {',
'fill: rgba(0,0,0,0.75);',
'stroke: #000;',
'}',
''
].join('\n');
pickerDoc.body.appendChild(style);
@ -876,7 +876,7 @@ pickerRoot.onload = function() {
window.addEventListener('scroll', onScrolled, true);
highlight();
highlightElements();
localMessager.addListener(onMessage);
};

View File

@ -4,6 +4,7 @@
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="css/common.css">
<link rel="stylesheet" type="text/css" href="css/logger-ui.css">
<link rel="stylesheet" type="text/css" href="css/logger-ui-inspector.css">
<title data-i18n="statsPageName"></title>
</head>
<body>
@ -20,6 +21,14 @@
</div>
<div id="domInspector">
<div class="permatoolbar">
<div>
<span class="button fa highlightMode">&#xf042;</span>
<span class="button fa revert disabled">&#xf12d;</span>
<span class="button fa commit disabled">&#xf0c7;</span>
</div>
</div>
<ul id="domTree"></ul>
</div>
<div id="events" class="compactView f">
@ -88,7 +97,9 @@
<div class="dialog"></div>
</div>
<div id="cosmeticFilteringDialog" class="modalDialog">
<div class="dialog"></div>
<div class="dialog">
<p><textarea class="cosmeticFilters" value=""></textarea>
</div>
</div>
<div id="filterFinderDialogSentence1"><span><span></span><code></code><span></span></span></div>
</div>
@ -98,6 +109,7 @@
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/logger-ui.js"></script>
<script src="js/logger-ui-inspector.js"></script>
</body>
</html>