uBlock/src/js/element-picker.js
2015-02-03 18:43:51 -05:00

1043 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*******************************************************************************
µBlock - a Chromium browser extension to block requests.
Copyright (C) 2014 Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
/* global self, vAPI, CSS */
/******************************************************************************/
/******************************************************************************/
/*! http://mths.be/cssescape v0.2.1 by @mathias | MIT license */
;(function(root) {
'use strict';
if (!root.CSS) {
root.CSS = {};
}
var CSS = root.CSS;
var InvalidCharacterError = function(message) {
this.message = message;
};
InvalidCharacterError.prototype = new Error();
InvalidCharacterError.prototype.name = 'InvalidCharacterError';
if (!CSS.escape) {
// http://dev.w3.org/csswg/cssom/#serialize-an-identifier
CSS.escape = function(value) {
var string = String(value);
var length = string.length;
var index = -1;
var codeUnit;
var result = '';
var firstCodeUnit = string.charCodeAt(0);
while (++index < length) {
codeUnit = string.charCodeAt(index);
// Note: theres no need to special-case astral symbols, surrogate
// pairs, or lone surrogates.
// If the character is NULL (U+0000), then throw an
// `InvalidCharacterError` exception and terminate these steps.
if (codeUnit === 0x0000) {
throw new InvalidCharacterError(
'Invalid character: the input contains U+0000.'
);
}
if (
// If the character is in the range [\1-\1F] (U+0001 to U+001F) or is
// U+007F, […]
(codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F ||
// If the character is the first character and is in the range [0-9]
// (U+0030 to U+0039), […]
(index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
// If the character is the second character and is in the range [0-9]
// (U+0030 to U+0039) and the first character is a `-` (U+002D), […]
(
index == 1 &&
codeUnit >= 0x0030 && codeUnit <= 0x0039 &&
firstCodeUnit == 0x002D
)
) {
// http://dev.w3.org/csswg/cssom/#escape-a-character-as-code-point
result += '\\' + codeUnit.toString(16) + ' ';
continue;
}
// If the character is not handled by one of the above rules and is
// greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or
// is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to
// U+005A), or [a-z] (U+0061 to U+007A), […]
if (
codeUnit >= 0x0080 ||
codeUnit == 0x002D ||
codeUnit == 0x005F ||
codeUnit >= 0x0030 && codeUnit <= 0x0039 ||
codeUnit >= 0x0041 && codeUnit <= 0x005A ||
codeUnit >= 0x0061 && codeUnit <= 0x007A
) {
// the character itself
result += string.charAt(index);
continue;
}
// Otherwise, the escaped character.
// http://dev.w3.org/csswg/cssom/#escape-a-character
result += '\\' + string.charAt(index);
}
return result;
};
}
}(self));
/******************************************************************************/
/******************************************************************************/
(function() {
'use strict';
/******************************************************************************/
// don't run in frames
if ( window.top !== window ) {
return;
}
// https://github.com/gorhill/uBlock/issues/314#issuecomment-58878112
// Using an id makes uBlock's CSS rules more specific, thus prevents
// surrounding external rules from winning over own rules.
var µBlockId = CSS.escape('µBlock');
var pickerRoot = document.getElementById(µBlockId);
if ( pickerRoot ) {
return;
}
var localMessager = vAPI.messaging.channel('element-picker.js');
var svgns = 'http://www.w3.org/2000/svg';
var svgRoot = null;
var svgOcean = null;
var svgIslands = null;
var divDialog = null;
var taCandidate = null;
var urlNormalizer = null;
var netFilterCandidates = [];
var cosmeticFilterCandidates = [];
var targetElements = [];
var svgWidth = 0;
var svgHeight = 0;
var elementFromPointCSSProperty = 'pointerEvents';
var onSvgHoveredTimer = null;
/******************************************************************************/
// For browsers not supporting `:scope`, it's not the end of the world: the
// suggested CSS selectors may just end up being more verbose.
var cssScope = ':scope > ';
try {
document.querySelector(':scope *');
} catch (e) {
cssScope = '';
}
/******************************************************************************/
var pickerPaused = function() {
return pickerRoot.classList.contains('paused');
};
/******************************************************************************/
var pausePicker = function() {
pickerRoot.classList.add('paused');
};
/******************************************************************************/
var unpausePicker = function() {
pickerRoot.classList.remove('paused');
};
/******************************************************************************/
var pickerRootDistance = function(elem) {
var distance = 0;
while ( elem ) {
if ( elem === pickerRoot ) {
return distance;
}
elem = elem.parentNode;
distance += 1;
}
return -1;
};
/******************************************************************************/
var highlightElements = function(elems, force) {
// To make mouse move handler more efficient
if ( !force && elems.length === targetElements.length ) {
if ( elems.length === 0 || elems[0] === targetElements[0] ) {
return;
}
}
targetElements = elems;
var ow = parseInt(svgRoot.style.width, 10);
var ocean = [
'M0 0',
'h', ow,
'v', parseInt(svgRoot.style.height, 10),
'h-', ow,
'z'
];
var offx = window.pageXOffset;
var offy = window.pageYOffset;
var islands = [];
var elem, rect, poly;
for ( var i = 0; i < elems.length; i++ ) {
elem = elems[i];
if ( typeof elem.getBoundingClientRect !== 'function' ) {
continue;
}
rect = elem.getBoundingClientRect();
poly = 'M' + (rect.left + offx) + ' ' + (rect.top + offy) +
'h' + rect.width +
'v' + rect.height +
'h-' + rect.width +
'z';
ocean.push(poly);
islands.push(poly);
}
svgOcean.setAttribute('d', ocean.join(''));
svgIslands.setAttribute('d', islands.join('') || 'M0 0');
};
/******************************************************************************/
var removeElements = function(elems) {
var i = elems.length, elem;
while ( i-- ) {
elem = elems[i];
if ( elem.parentNode ) {
elem.parentNode.removeChild(elem);
}
}
};
/******************************************************************************/
// Extract the best possible net filter, i.e. as specific as possible.
var netFilterFromElement = function(elem, out) {
if ( elem === null ) {
return;
}
if ( elem.nodeType !== 1 ) {
return;
}
var tagName = elem.tagName.toLowerCase();
if ( netFilterSources.hasOwnProperty(tagName) === false ) {
return;
}
var src = elem.getAttribute(netFilterSources[tagName]);
if ( typeof src !== 'string' || src.length === 0 ) {
return;
}
// Remove fragment
var pos = src.indexOf('#');
if ( pos !== -1 ) {
src = src.slice(0, pos);
}
// Feed the attribute to a link element, then retrieve back: this
// should normalize it.
urlNormalizer.href = src;
src = urlNormalizer.href;
// Anchor absolute filter to hostname
src = src.replace(/^https?:\/\//, '||');
out.push(src);
// Suggest a less narrow filter if possible
pos = src.indexOf('?');
if ( pos !== -1 ) {
src = src.slice(0, pos);
out.push(src);
}
};
var netFilterSources = {
'iframe': 'src',
'img': 'src',
'object': 'data'
};
/******************************************************************************/
// Extract the best possible cosmetic filter, i.e. as specific as possible.
var cosmeticFilterFromElement = function(elem, out) {
if ( elem === null ) {
return;
}
if ( elem.nodeType !== 1 ) {
return;
}
var tagName = elem.tagName.toLowerCase();
var prefix = '';
var suffix = [];
var v, i;
// Id
v = typeof elem.id === 'string' && CSS.escape(elem.id);
if ( v ) {
suffix.push('#', v);
}
// Class(es)
v = typeof elem.className === 'string' && elem.className.trim();
if ( v.length ) {
v = v.split(/\s+/);
i = v.length;
while ( i-- ) {
v[i] = CSS.escape(v[i]);
}
suffix.push('.', v.join('.'));
}
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;
}
v = elem.getAttribute(attr.k);
if ( attr.v === v ) {
suffix.push('[', attr.k, '="', attr.v, '"]');
} else if ( v.indexOf(attr.v) === 0 ) {
suffix.push('[', attr.k, '^="', attr.v, '"]');
} else {
suffix.push('[', attr.k, '*="', attr.v, '"]');
}
}
var selector = prefix + suffix.join('');
// https://github.com/gorhill/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 parentNode = elem.parentNode;
if ( parentNode !== null && parentNode.querySelectorAll(cssScope + selector).length > 1 ) {
i = 1;
while ( elem.previousSibling !== null ) {
elem = elem.previousSibling;
if ( typeof elem.tagName !== 'string' ) {
continue;
}
if ( elem.tagName.toLowerCase() !== tagName ) {
continue;
}
i++;
}
selector += ':nth-of-type(' + i + ')';
}
out.push('##' + selector);
};
/******************************************************************************/
var filtersFromElement = function(elem) {
netFilterCandidates.length = 0;
cosmeticFilterCandidates.length = 0;
while ( elem && elem !== document.body ) {
netFilterFromElement(elem, netFilterCandidates);
cosmeticFilterFromElement(elem, cosmeticFilterCandidates);
elem = elem.parentNode;
}
};
/******************************************************************************/
var elementsFromFilter = function(filter) {
var out = [];
// Cosmetic filters: these are straight CSS selectors
// TODO: This is still not working well for a[href], because there are
// many ways to compose a valid href to the same effective URL.
// One idea is to normalize all a[href] on the page, but for now I will
// wait and see, as I prefer to refrain from tampering with the page
// content if I can avoid it.
if ( filter.slice(0, 2) === '##' ) {
try {
out = document.querySelectorAll(filter.replace('##', ''));
}
catch (e) {
}
return out;
}
// Net filters: we need to lookup manually -- translating into a
// foolproof CSS selector is just not possible
if ( filter.slice(0, 2) === '||' ) {
filter = filter.replace('||', '');
}
var elems = document.querySelectorAll('iframe,img,object');
var i = elems.length;
var elem, src;
while ( i-- ) {
elem = elems[i];
src = elem.getAttribute(netFilterSources[elem.tagName.toLowerCase()]);
if ( typeof src !== 'string' ) {
continue;
}
if ( src.indexOf(filter) !== -1 ) {
out.push(elem);
}
}
return out;
};
// https://www.youtube.com/watch?v=YI2XuIOW3gM
/******************************************************************************/
var userFilterFromCandidate = function() {
var v = taCandidate.value;
var elems = elementsFromFilter(v);
if ( elems.length === 0 ) {
return false;
}
// Cosmetic filter?
if ( v.slice(0, 2) === '##' ) {
return window.location.hostname + v;
}
// If domain included in filter, no need for domain option
if ( v.slice(0, 2) === '||' ) {
return v;
}
// Assume net filter
return v + '$domain=' + window.location.hostname;
};
/******************************************************************************/
var onCandidateChanged = function() {
var elems = elementsFromFilter(taCandidate.value);
divDialog.querySelector('#create').disabled = elems.length === 0;
highlightElements(elems);
};
/******************************************************************************/
var candidateFromFilterChoice = function(filterChoice) {
var slot = filterChoice.slot;
var filters = filterChoice.filters;
var filter = filters[slot];
if ( filter === undefined ) {
return '';
}
// For net filters there no such thing as a path
if ( filterChoice.type === 'net' || filterChoice.modifier ) {
return filter;
}
// Return path: the target element, then all siblings prepended
var selector = [];
for ( ; slot < filters.length; slot++ ) {
filter = filters[slot];
selector.unshift(filter.replace(/^##/, ''));
// Stop at any element with an id: these are unique in a web page
if ( filter.slice(0, 3) === '###' ) {
break;
}
}
return '##' + selector.join(' > ');
};
/******************************************************************************/
var filterChoiceFromEvent = function(ev) {
var li = ev.target;
var isNetFilter = li.textContent.slice(0, 2) !== '##';
var r = {
type: isNetFilter ? 'net' : 'cosmetic',
filters: isNetFilter ? netFilterCandidates : cosmeticFilterCandidates,
slot: 0,
modifier: ev.ctrlKey || ev.metaKey
};
while ( li.previousSibling !== null ) {
li = li.previousSibling;
r.slot += 1;
}
return r;
};
/******************************************************************************/
var onDialogClicked = function(ev) {
if ( ev.target === null ) {
/* do nothing */
}
else if ( ev.target.id === 'create' ) {
var filter = userFilterFromCandidate();
if ( filter ) {
localMessager.send({ what: 'createUserFilter', filters: filter });
removeElements(elementsFromFilter(taCandidate.value));
stopPicker();
}
}
else if ( ev.target.id === 'pick' ) {
unpausePicker();
}
else if ( ev.target.id === 'quit' ) {
stopPicker();
}
else if ( ev.target.tagName.toLowerCase() === 'li' && pickerRootDistance(ev.target) === 5 ) {
taCandidate.value = candidateFromFilterChoice(filterChoiceFromEvent(ev));
onCandidateChanged();
}
ev.stopPropagation();
ev.preventDefault();
};
/******************************************************************************/
var removeAllChildren = function(parent) {
while ( parent.firstChild ) {
parent.removeChild(parent.firstChild);
}
};
/******************************************************************************/
// TODO: for convenience I could provide a small set of net filters instead
// of just a single one. Truncating the right-most part of the path etc.
var showDialog = function(options) {
pausePicker();
options = options || {};
// Create lists of candidate filters
var populate = function(src, des) {
var root = divDialog.querySelector(des);
var ul = root.querySelector('ul');
removeAllChildren(ul);
var li;
for ( var i = 0; i < src.length; i++ ) {
li = document.createElement('li');
li.textContent = src[i];
ul.appendChild(li);
}
root.style.display = src.length !== 0 ? '' : 'none';
};
populate(netFilterCandidates, '#netFilters');
populate(cosmeticFilterCandidates, '#cosmeticFilters');
divDialog.querySelector('ul').style.display = netFilterCandidates.length || cosmeticFilterCandidates.length ? '' : 'none';
divDialog.querySelector('#create').disabled = true;
// Auto-select a candidate filter
var filterChoice = {
type: '',
filters: [],
slot: 0,
modifier: options.modifier || false
};
if ( netFilterCandidates.length ) {
filterChoice.type = 'net';
filterChoice.filters = netFilterCandidates;
} else if ( cosmeticFilterCandidates.length ) {
filterChoice.type = 'cosmetic';
filterChoice.filters = cosmeticFilterCandidates;
}
taCandidate.value = '';
if ( filterChoice.type !== '' ) {
taCandidate.value = candidateFromFilterChoice(filterChoice);
onCandidateChanged();
}
};
/******************************************************************************/
var elementFromPoint = function(x, y) {
svgRoot.style[elementFromPointCSSProperty] = 'none';
var elem = document.elementFromPoint(x, y);
if ( elem === document.body || elem === document.documentElement ) {
elem = null;
}
svgRoot.style[elementFromPointCSSProperty] = '';
return elem;
};
/******************************************************************************/
var onSvgHovered = function(ev) {
if ( pickerPaused() || onSvgHoveredTimer) {
return;
}
onSvgHoveredTimer = setTimeout(function() {
var elem = elementFromPoint(ev.clientX, ev.clientY);
highlightElements(elem ? [elem] : []);
onSvgHoveredTimer = null;
}, 50);
};
/******************************************************************************/
var onSvgClicked = function(ev) {
if ( pickerPaused() ) {
return;
}
var elem = elementFromPoint(ev.clientX, ev.clientY);
if ( elem === null ) {
return;
}
filtersFromElement(elem);
showDialog();
};
/******************************************************************************/
var onKeyPressed = function(ev) {
if ( ev.key === 27 || ev.keyCode === 27 ) {
ev.stopPropagation();
ev.preventDefault();
stopPicker();
}
};
/******************************************************************************/
// https://github.com/gorhill/uBlock/issues/190
// May need to dynamically adjust the height of the overlay + new position
// of highlighted elements.
var onScrolled = function() {
var newHeight = this.scrollY + this.innerHeight;
if ( newHeight > svgHeight ) {
svgHeight = newHeight;
svgRoot.style.height = svgHeight + 'px';
svgRoot.setAttribute('viewBox', '0 0 ' + svgWidth + ' ' + svgHeight);
}
highlightElements(targetElements, true);
};
/******************************************************************************/
// Let's have the element picker code flushed from memory when no longer
// in use: to ensure this, release all local references.
var stopPicker = function() {
if ( pickerRoot !== null ) {
window.removeEventListener('keydown', onKeyPressed, true);
window.removeEventListener('scroll', onScrolled, true);
taCandidate.removeEventListener('input', onCandidateChanged);
divDialog.removeEventListener('click', onDialogClicked);
svgRoot.removeEventListener('mousemove', onSvgHovered);
svgRoot.removeEventListener('click', onSvgClicked);
pickerRoot.parentNode.removeChild(pickerRoot);
pickerRoot =
divDialog =
svgRoot = svgOcean = svgIslands =
taCandidate =
urlNormalizer = null;
localMessager.close();
}
targetElements = [];
};
/******************************************************************************/
var startPicker = function(details) {
pickerRoot = document.createElement('div');
pickerRoot.id = µBlockId;
pickerRoot.setAttribute('lang', navigator.language);
var pickerStyle = document.createElement('style');
pickerStyle.setAttribute('scoped', '');
pickerStyle.textContent = [
'#µBlock, #µBlock * {',
'background: transparent;',
'background-image: none;',
'border: 0;',
'border-radius: 0;',
'box-shadow: none;',
'color: #000;',
'display: inline;',
'float: none;',
'font: 12px sans-serif;',
'height: auto;',
'letter-spacing: normal;',
'margin: 0;',
'max-width: none;',
'min-height: 0;',
'min-width: 0;',
'outline: 0;',
'overflow: visible;',
'padding: 0;',
'text-transform: none;',
'vertical-align: baseline;',
'width: auto;',
'z-index: auto;',
'}',
'#µBlock {',
'position: absolute;',
'top: 0;',
'left: 0;',
'}',
'#µBlock style, #µBlock script {',
'display: none;',
'}',
'#µBlock ul, #µBlock li, #µBlock div {',
'display: block;',
'}',
'#µBlock *::selection {',
'background-color: Highlight;',
'color: HighlightText;',
'}',
'#µBlock button {',
'border: 1px solid #aaa !important;',
'padding: 6px 8px 4px 8px;',
'box-sizing: border-box;',
'box-shadow: none;',
'border-radius: 3px;',
'display: inline;',
'line-height: 1;',
'color: #444;',
'background-color: #ccc;',
'cursor: pointer;',
'}',
'#µBlock button:hover {',
'background: none;',
'background-color: #eee;',
'background-image: none;',
'}',
'#µBlock button:disabled {',
'color: #999;',
'background-color: #ccc;',
'}',
'#µBlock button#create:not(:disabled) {',
'background-color: #ffdca8;',
'}',
'#µBlock > svg {',
'position: absolute;',
'top: 0;',
'left: 0;',
'pointer-events: auto;',
'cursor: crosshair;',
'z-index: 4999999999;',
'}',
'#µBlock.paused > svg {',
'cursor: wait;',
'}',
'#µBlock > svg > path:first-child {',
'fill: rgba(0,0,0,0.75);',
'fill-rule: evenodd;',
'}',
'#µBlock > svg > path + path {',
'stroke: #F00;',
'stroke-width: 0.5px;',
'fill: rgba(255,0,0,0.25);',
'}',
'#µBlock > div {',
'background-color: rgba(255,255,255,0.9);',
'bottom: 4px;',
'display: none;',
'font: 12px sans-serif;',
'padding: 4px;',
'position: fixed;',
'right: 4px;',
'width: 30em;',
'z-index: 5999999999;',
'}',
'#µBlock.paused > div {',
'opacity: 0.2;',
'display: block;',
'}',
'#µBlock.paused > div:hover {',
'opacity: 1;',
'}',
'#µBlock > div > div {',
'box-sizing: border-box;',
'display: inline-block;',
'height: 8em;',
'padding: 0;',
'position: relative;',
'width: 100%;',
'}',
'#µBlock > div > div > textarea {',
'background-color: white;',
'border: 1px solid #ccc;',
'box-sizing: border-box;',
'font: 11px monospace;',
'height: 100% !important;',
'max-height: 100% !important;',
'min-height: 100% !important;',
'overflow: hidden !important;',
'padding: 2px;',
'resize: none;',
'width: 100% !important;',
'}',
'#µBlock > div > div > div {',
'bottom: 2px;',
'direction: ltr;',
'opacity: 0.2;',
'position: absolute;',
'right: 2px;',
'}',
'#µBlock > div > div > div:hover {',
'opacity: 1;',
'}',
'#µBlock > div > div > div > button {',
'margin-left: 3px !important;',
'}',
'#µBlock > div > ul {',
'margin: 0;',
'list-style-type: none;',
'text-align: left;',
'overflow: hidden;',
'}',
'#µBlock > div > ul > li {',
'padding-top: 3px;',
'}',
'#µBlock > div > ul > li > span:nth-of-type(1) {',
'font-weight: bold;',
'}',
'#µBlock > div > ul > li > span:nth-of-type(2) {',
'font-size: smaller;',
'color: gray;',
'}',
'#µBlock > div > ul > li > ul {',
'background-color: #eee;',
'list-style-type: none;',
'margin: 0 0 0 1em;',
'overflow: hidden;',
'text-align: left;',
'}',
'#µBlock > div > ul > li > ul > li {',
'font: 11px monospace;',
'white-space: nowrap;',
'cursor: pointer;',
'direction: ltr;',
'}',
'#µBlock > div > ul > li > ul > li:hover {',
'background-color: rgba(255,255,255,1.0);',
'}',
''
].join('\n');
pickerRoot.appendChild(pickerStyle);
svgRoot = document.createElementNS(svgns, 'svg');
svgRoot.appendChild(document.createElementNS(svgns, 'path'));
svgRoot.appendChild(document.createElementNS(svgns, 'path'));
svgWidth = document.documentElement.scrollWidth;
svgHeight = Math.max(
document.documentElement.scrollHeight,
window.scrollY + window.innerHeight
);
svgRoot.setAttribute('x', 0);
svgRoot.setAttribute('y', 0);
svgRoot.style.width = svgWidth + 'px';
svgRoot.style.height = svgHeight + 'px';
svgRoot.setAttribute('viewBox', '0 0 ' + svgWidth + ' ' + svgHeight);
svgOcean = svgRoot.firstChild;
svgIslands = svgRoot.lastChild;
pickerRoot.appendChild(svgRoot);
// TODO: do not rely on element ids, they could collide with whatever
// is used in the page. Just use built-in hierarchy of elements as
// selectors.
divDialog = document.createElement('div');
divDialog.innerHTML = [
'<div>',
'<textarea lang="en" dir="ltr" spellcheck="false"></textarea>',
'<div>',
'<button id="create" type="button" disabled="disabled">.</button>',
'<button id="pick" type="button">.</button>',
'<button id="quit" type="button">.</button>',
'</div>',
'</div>',
'<ul>',
'<li id="netFilters"><span>.</span><ul lang="en"></ul></li>',
'<li id="cosmeticFilters"><span>.</span> <span>.</span><ul lang="en"></ul></li>',
'</ul>'
].join('');
pickerRoot.appendChild(divDialog);
// https://github.com/gorhill/uBlock/issues/344#issuecomment-60775958
// Insert in `html` tag, not `body` tag.
document.documentElement.appendChild(pickerRoot);
svgRoot.addEventListener('click', onSvgClicked);
svgRoot.addEventListener('mousemove', onSvgHovered);
divDialog.addEventListener('click', onDialogClicked);
taCandidate = divDialog.querySelector('textarea');
taCandidate.addEventListener('input', onCandidateChanged);
urlNormalizer = document.createElement('a');
window.addEventListener('scroll', onScrolled, true);
window.addEventListener('keydown', onKeyPressed, true);
highlightElements([], true);
var i18nMap = {
'#µBlock > div': '@@bidi_dir',
'#create': 'create',
'#pick': 'pick',
'#quit': 'quit',
'ul > li#netFilters > span:nth-of-type(1)': 'netFilters',
'ul > li#cosmeticFilters > span:nth-of-type(1)': 'cosmeticFilters',
'ul > li#cosmeticFilters > span:nth-of-type(2)': 'cosmeticFiltersHint'
};
if ( details.i18n['@@bidi_dir'] ) {
divDialog.style.direction = details.i18n['@@bidi_dir'];
delete i18nMap['#µBlock > div'];
}
for ( var k in i18nMap ) {
if ( i18nMap.hasOwnProperty(k) === false ) {
continue;
}
divDialog.querySelector(k).firstChild.nodeValue = details.i18n[i18nMap[k]];
}
// First we test if pointer-events are hadnled in Node.elementFromPoint().
// If the browser ignores pointer-events in Node.elementFromPoint(),
// then use the display property instead (e.g., for older Safari).
var elem = elementFromPoint(0, 0);
if ( elem === svgRoot ) {
elementFromPointCSSProperty = 'display';
}
// Auto-select a specific target, if any, and if possible
// Try using mouse position
if ( details.clientX !== -1 ) {
elem = elementFromPoint(details.clientX, details.clientY);
if ( elem !== null ) {
filtersFromElement(elem);
showDialog();
return;
}
}
// No mouse position available, use suggested target
var target = details.target || '';
var pos = target.indexOf('\t');
if ( pos === -1 ) {
return;
}
var srcAttrMap = {
'a': 'href',
'img': 'src',
'iframe': 'src',
'video': 'src',
'audio': 'src'
};
var tagName = target.slice(0, pos);
var url = target.slice(pos + 1);
var attr = srcAttrMap[tagName];
if ( attr === undefined ) {
return;
}
var elems = document.querySelectorAll(tagName + '[' + attr + ']');
var i = elems.length;
var src;
while ( i-- ) {
elem = elems[i];
src = elem[attr];
if ( typeof src !== 'string' || src === '' ) {
continue;
}
if ( src !== url ) {
continue;
}
filtersFromElement(elem);
showDialog({ modifier: true });
return;
}
};
/******************************************************************************/
localMessager.send({ what: 'elementPickerArguments' }, startPicker);
// So the shortcuts will be usable in Firefox
// (also triggers the hiding of the popover in Safari)
window.focus();
/******************************************************************************/
// https://www.youtube.com/watch?v=sociXdKnyr8
/******************************************************************************/
})();