uBlock/src/js/scriptlets/element-picker.js
2016-05-13 11:10:09 -04:00

1297 lines
38 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.

/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-2016 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 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';
/******************************************************************************/
if ( typeof vAPI !== 'object' ) {
return;
}
// don't run in frames
if ( window.top !== window ) {
return;
}
var pickerRoot = document.getElementById(vAPI.sessionId);
if ( pickerRoot ) {
return;
}
var pickerBody = null;
var pickerStyle = null;
var svgOcean = null;
var svgIslands = null;
var svgRoot = null;
var dialog = null;
var taCandidate = null;
var netFilterCandidates = [];
var cosmeticFilterCandidates = [];
var targetElements = [];
var candidateElements = [];
var bestCandidateFilter = null;
var previewedElements = [];
var lastNetFilterSession = window.location.host + window.location.pathname;
var lastNetFilterHostname = '';
var lastNetFilterUnion = '';
/******************************************************************************/
// 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 safeQuerySelectorAll = function(node, selector) {
if ( node !== null ) {
try {
return node.querySelectorAll(selector);
} catch (e) {
}
}
return [];
};
/******************************************************************************/
var getElementBoundingClientRect = function(elem) {
var rect = typeof elem.getBoundingClientRect === 'function' ?
elem.getBoundingClientRect() :
{ height: 0, left: 0, top: 0, width: 0 };
// https://github.com/gorhill/uBlock/issues/1024
// Try not returning an empty bounding rect.
if ( rect.width !== 0 && rect.height !== 0 ) {
return rect;
}
var left = rect.left,
right = rect.right,
top = rect.top,
bottom = rect.bottom;
var children = elem.children,
i = children.length;
while ( i-- ) {
rect = getElementBoundingClientRect(children[i]);
if ( rect.width === 0 || rect.height === 0 ) {
continue;
}
if ( rect.left < left ) { left = rect.left; }
if ( rect.right > right ) { right = rect.right; }
if ( rect.top < top ) { top = rect.top; }
if ( rect.bottom > bottom ) { bottom = rect.bottom; }
}
return {
height: bottom - top,
left: left,
top: top,
width: right - left
};
};
/******************************************************************************/
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 = pickerRoot.contentWindow.innerWidth;
var oh = pickerRoot.contentWindow.innerHeight;
var ocean = [
'M0 0',
'h', ow,
'v', oh,
'h-', ow,
'z'
];
var islands = [];
var elem, rect, poly;
for ( var i = 0; i < elems.length; i++ ) {
elem = elems[i];
if ( elem === pickerRoot ) {
continue;
}
rect = getElementBoundingClientRect(elem);
// Ignore if it's not on the screen
if ( rect.left > ow || rect.top > oh ||
rect.left + rect.width < 0 || rect.top + rect.height < 0 ) {
continue;
}
poly = 'M' + rect.left + ' ' + rect.top +
'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 filterElements = function(filter) {
var htmlElem = document.documentElement;
var items = elementsFromFilter(filter);
var i = items.length, item, elem, style;
while ( i-- ) {
item = items[i];
elem = item.elem;
// https://github.com/gorhill/uBlock/issues/1629
if ( elem === pickerRoot ) {
continue;
}
style = elem.style;
if (
(elem !== htmlElem) &&
(item.type === 'cosmetic' ||
item.type === 'network' && item.src !== undefined)
) {
previewedElements.push({
elem: elem,
prop: 'display',
value: style.getPropertyValue('display'),
priority: style.getPropertyPriority('display')
});
style.setProperty('display', 'none', 'important');
}
if ( item.type === 'network' && item.style === 'background-image' ) {
previewedElements.push({
elem: elem,
prop: 'background-image',
value: style.getPropertyValue('background-image'),
priority: style.getPropertyPriority('background-image')
});
style.setProperty('background-image', 'none', 'important');
}
}
};
/******************************************************************************/
var preview = function(filter) {
filterElements(filter);
pickerBody.classList.add('preview');
};
/******************************************************************************/
var unpreview = function() {
var items = previewedElements;
var i = items.length, item;
while ( i-- ) {
item = items[i];
item.elem.style.setProperty(item.prop, item.value, item.priority);
}
previewedElements.length = 0;
pickerBody.classList.remove('preview');
};
/******************************************************************************/
var backgroundImageURLFromElement = function(elem) {
var style = window.getComputedStyle(elem);
var bgImg = style.backgroundImage || '';
var matches = /^url\((["']?)([^"']+)\1\)$/.exec(bgImg);
return matches !== null && matches.length === 3 ? matches[2] : '';
};
/******************************************************************************/
var resourceURLFromElement = function(elem) {
var tagName = elem.localName, s;
if ( (s = netFilter1stSources[tagName]) ) {
s = elem[s];
if ( typeof s === 'string' && s !== '' ) {
return s;
}
}
if ( (s = netFilter2ndSources[tagName]) ) {
s = elem[s];
if ( typeof s === 'string' && s !== '' ) {
return s;
}
}
return backgroundImageURLFromElement(elem);
};
/******************************************************************************/
var netFilterFromUnion = (function() {
var reTokenizer = /[^0-9a-z%*]+|[0-9a-z%]+|\*/gi;
var a = document.createElement('a');
return function(to, out) {
a.href= to;
to = a.pathname + a.search;
var from = lastNetFilterUnion;
// Reset reference filter when dealing with unrelated URLs
if ( from === '' || a.host === '' || a.host !== lastNetFilterHostname ) {
lastNetFilterHostname = a.host;
lastNetFilterUnion = to;
vAPI.messaging.send(
'elementPicker',
{
what: 'elementPickerEprom',
lastNetFilterSession: lastNetFilterSession,
lastNetFilterHostname: lastNetFilterHostname,
lastNetFilterUnion: lastNetFilterUnion
}
);
return;
}
// Related URLs
lastNetFilterHostname = a.host;
var fromTokens = from.match(reTokenizer);
var toTokens = to.match(reTokenizer);
var toCount = toTokens.length, toIndex = 0;
var fromToken, pos;
for ( var fromIndex = 0; fromIndex < fromTokens.length; fromIndex += 1 ) {
fromToken = fromTokens[fromIndex];
if ( fromToken === '*' ) {
continue;
}
pos = toTokens.indexOf(fromToken, toIndex);
if ( pos === -1 ) {
fromTokens[fromIndex] = '*';
continue;
}
if ( pos !== toIndex ) {
fromTokens.splice(fromIndex, 0, '*');
fromIndex += 1;
}
toIndex = pos + 1;
if ( toIndex === toCount ) {
fromTokens = fromTokens.slice(0, fromIndex + 1);
break;
}
}
from = fromTokens.join('').replace(/\*\*+/g, '*');
if ( from !== '/*' && from !== to ) {
var filter = '||' + lastNetFilterHostname + from;
if ( out.indexOf(filter) === -1 ) {
out.push(filter);
}
} else {
from = to;
}
lastNetFilterUnion = from;
// Remember across element picker sessions
vAPI.messaging.send(
'elementPicker',
{
what: 'elementPickerEprom',
lastNetFilterSession: lastNetFilterSession,
lastNetFilterHostname: lastNetFilterHostname,
lastNetFilterUnion: lastNetFilterUnion
}
);
};
})();
/******************************************************************************/
// Extract the best possible net filter, i.e. as specific as possible.
var netFilterFromElement = function(elem) {
if ( elem === null ) {
return 0;
}
if ( elem.nodeType !== 1 ) {
return 0;
}
var src = resourceURLFromElement(elem);
if ( src === '' ) {
return 0;
}
if ( candidateElements.indexOf(elem) === -1 ) {
candidateElements.push(elem);
}
var candidates = netFilterCandidates;
var len = candidates.length;
// Remove fragment
var pos = src.indexOf('#');
if ( pos !== -1 ) {
src = src.slice(0, pos);
}
var filter = src.replace(/^https?:\/\//, '||');
if ( bestCandidateFilter === null ) {
bestCandidateFilter = {
type: 'net',
filters: candidates,
slot: candidates.length
};
}
candidates.push(filter);
// Suggest a less narrow filter if possible
pos = filter.indexOf('?');
if ( pos !== -1 ) {
candidates.push(filter.slice(0, pos));
}
// Suggest a filter which is a result of combining more than one URL.
netFilterFromUnion(src, candidates);
return candidates.length - len;
};
var netFilter1stSources = {
'audio': 'src',
'embed': 'src',
'iframe': 'src',
'img': 'src',
'object': 'data',
'video': 'src'
};
var netFilter2ndSources = {
'img': 'srcset'
};
var filterTypes = {
'audio': 'media',
'embed': 'object',
'iframe': 'subdocument',
'img': 'image',
'object': 'object',
'video': 'media',
};
/******************************************************************************/
// Extract the best possible cosmetic filter, i.e. as specific as possible.
var cosmeticFilterFromElement = function(elem) {
if ( elem === null ) {
return 0;
}
if ( elem.nodeType !== 1 ) {
return 0;
}
if ( candidateElements.indexOf(elem) === -1 ) {
candidateElements.push(elem);
}
var tagName = elem.localName;
var selector = '';
var v, i;
// Id
v = typeof elem.id === 'string' && CSS.escape(elem.id);
if ( v ) {
selector = '#' + v;
}
// Class(es)
if ( selector === '' ) {
v = elem.classList;
if ( v ) {
i = v.length || 0;
while ( i-- ) {
selector += '.' + CSS.escape(v.item(i));
}
}
}
// Tag name
if ( selector === '' ) {
selector = tagName;
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 ) {
selector += '[' + attr.k + '="' + attr.v + '"]';
} else if ( v.lastIndexOf(attr.v, 0) === 0 ) {
selector += '[' + attr.k + '^="' + attr.v + '"]';
} else {
selector += '[' + attr.k + '*="' + attr.v + '"]';
}
}
}
// 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 parentNode = elem.parentNode;
if ( safeQuerySelectorAll(parentNode, cssScope + selector).length > 1 ) {
i = 1;
while ( elem.previousSibling !== null ) {
elem = elem.previousSibling;
if ( typeof elem.localName !== 'string' || elem.localName !== tagName ) {
continue;
}
i++;
}
selector += ':nth-of-type(' + i + ')';
}
if ( bestCandidateFilter === null ) {
bestCandidateFilter = {
type: 'cosmetic',
filters: cosmeticFilterCandidates,
slot: cosmeticFilterCandidates.length
};
}
cosmeticFilterCandidates.push('##' + selector);
return 1;
};
/******************************************************************************/
var filtersFrom = function(x, y) {
bestCandidateFilter = null;
netFilterCandidates.length = 0;
cosmeticFilterCandidates.length = 0;
candidateElements.length = 0;
// We need at least one element.
var first = null;
if ( typeof x === 'number' ) {
first = elementFromPoint(x, y);
} else if ( x instanceof HTMLElement ) {
first = x;
x = undefined;
}
// Network filter from element which was clicked.
if ( first !== null ) {
netFilterFromElement(first);
}
// Cosmetic filter candidates from ancestors.
var elem = first;
while ( elem && elem !== document.body ) {
cosmeticFilterFromElement(elem);
elem = elem.parentNode;
}
// The body tag is needed as anchor only when the immediate child
// uses`nth-of-type`.
var i = cosmeticFilterCandidates.length;
if ( i !== 0 && cosmeticFilterCandidates[i-1].indexOf(':nth-of-type(') !== -1 ) {
cosmeticFilterCandidates.push('##body');
}
// https://github.com/gorhill/uBlock/issues/1545
// Network filter candidates from all other elements found at point (x, y).
if ( typeof x === 'number' ) {
var attrName = vAPI.sessionId + '-clickblind';
var previous;
elem = first;
while ( elem !== null ) {
previous = elem;
elem.setAttribute(attrName, '');
elem = elementFromPoint(x, y);
if ( elem === null || elem === previous ) {
break;
}
netFilterFromElement(elem);
}
var elems = document.querySelectorAll('[' + attrName + ']');
i = elems.length;
while ( i-- ) {
elems[i].removeAttribute(attrName);
}
netFilterFromElement(document.body);
}
return netFilterCandidates.length + cosmeticFilterCandidates.length;
};
/******************************************************************************/
var elementsFromFilter = function(filter) {
var out = [];
filter = filter.trim();
if ( filter === '' ) {
return 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.
var elems, iElem, elem;
if ( filter.lastIndexOf('##', 0) === 0 ) {
try {
elems = document.querySelectorAll(filter.slice(2));
}
catch (e) {
elems = [];
}
iElem = elems.length;
while ( iElem-- ) {
out.push({
type: 'cosmetic',
elem: elems[iElem],
});
}
return out;
}
// Net filters: we need to lookup manually -- translating into a
// foolproof CSS selector is just not possible
// https://github.com/chrisaljoudi/uBlock/issues/945
// Transform into a regular expression, this allows the user to edit and
// insert wildcard(s) into the proposed filter
var reStr = '';
if ( filter.length > 1 && filter.charAt(0) === '/' && filter.slice(-1) === '/' ) {
reStr = filter.slice(1, -1);
}
else {
var rePrefix = '', reSuffix = '';
if ( filter.slice(0, 2) === '||' ) {
filter = filter.replace('||', '');
} else {
if ( filter.charAt(0) === '|' ) {
rePrefix = '^';
filter = filter.slice(1);
}
}
if ( filter.slice(-1) === '|' ) {
reSuffix = '$';
filter = filter.slice(0, -1);
}
reStr = rePrefix +
filter.replace(/[.+?${}()|[\]\\]/g, '\\$&').replace(/[\*^]+/g, '.*') +
reSuffix;
}
var reFilter = null;
try {
reFilter = new RegExp(reStr);
}
catch (e) {
return out;
}
// Lookup by tag names.
var src1stProps = netFilter1stSources;
var src2ndProps = netFilter2ndSources;
var srcProp, src;
elems = document.querySelectorAll(Object.keys(src1stProps).join());
iElem = elems.length;
while ( iElem-- ) {
elem = elems[iElem];
srcProp = src1stProps[elem.localName];
src = elem[srcProp];
if ( typeof src !== 'string' || src.length === 0 ) {
srcProp = src2ndProps[elem.localName];
src = elem[srcProp];
}
if ( src && reFilter.test(src) ) {
out.push({
type: 'network',
elem: elem,
src: srcProp,
opts: filterTypes[elem.localName],
});
}
}
// Find matching background image in current set of candidate elements.
elems = candidateElements;
iElem = elems.length;
while ( iElem-- ) {
elem = elems[iElem];
if ( reFilter.test(backgroundImageURLFromElement(elem)) ) {
out.push({
type: 'network',
elem: elem,
style: 'background-image',
opts: 'image',
});
}
}
return out;
};
// https://www.youtube.com/watch?v=nuUXJ6RfIik
/******************************************************************************/
var userFilterFromCandidate = function() {
var v = taCandidate.value;
var items = elementsFromFilter(v);
if ( items.length === 0 ) {
return false;
}
// https://github.com/gorhill/uBlock/issues/738
// Trim dots.
var hostname = window.location.hostname;
if ( hostname.slice(-1) === '.' ) {
hostname = hostname.slice(0, -1);
}
// Cosmetic filter?
if ( v.lastIndexOf('##', 0) === 0 ) {
return hostname + v;
}
// Assume net filter
var opts = [];
// If no domain included in filter, we need domain option
if ( v.lastIndexOf('||', 0) === -1 ) {
opts.push('domain=' + hostname);
}
var item = items[0];
if ( item.opts ) {
opts.push(item.opts);
}
if ( opts.length ) {
v += '$' + opts.join(',');
}
return v;
};
/******************************************************************************/
var onCandidateChanged = function() {
unpreview();
var elems = [];
var items = elementsFromFilter(taCandidate.value);
for ( var i = 0; i < items.length; i++ ) {
elems.push(items[i].elem);
}
dialog.querySelector('#create').disabled = elems.length === 0;
highlightElements(elems, true);
};
/******************************************************************************/
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 ( filter.lastIndexOf('##', 0) !== 0 ) {
return filter;
}
// At this point, we have a cosmetic filter
// Modifier means "target broadly". Hence:
// - Do not compute exact path.
// - Discard narrowing directives.
if ( filterChoice.modifier ) {
return filter.replace(/:nth-of-type\(\d+\)/, '');
}
// 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 = {
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' ) {
// We have to exit from preview mode: this guarantees matching elements
// will be found for the candidate filter.
unpreview();
var filter = userFilterFromCandidate();
if ( filter ) {
var d = new Date();
vAPI.messaging.send(
'elementPicker',
{
what: 'createUserFilter',
filters: '! ' + d.toLocaleString() + ' ' + window.location.href + '\n' + filter,
}
);
filterElements(taCandidate.value);
stopPicker();
}
}
else if ( ev.target.id === 'pick' ) {
unpausePicker();
}
else if ( ev.target.id === 'quit' ) {
unpreview();
stopPicker();
}
else if ( ev.target.id === 'preview' ) {
if ( pickerBody.classList.contains('preview') ) {
unpreview();
} else {
preview(taCandidate.value);
}
highlightElements(targetElements, true);
}
else if ( ev.target.parentNode.classList.contains('changeFilter') ) {
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 = dialog.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');
dialog.querySelector('ul').style.display = netFilterCandidates.length || cosmeticFilterCandidates.length ? '' : 'none';
dialog.querySelector('#create').disabled = true;
// Auto-select a candidate filter
if ( bestCandidateFilter === null ) {
taCandidate.value = '';
return;
}
var filterChoice = {
filters: bestCandidateFilter.filters,
slot: bestCandidateFilter.slot,
modifier: options.modifier || false
};
taCandidate.value = candidateFromFilterChoice(filterChoice);
onCandidateChanged();
};
/******************************************************************************/
var elementFromPoint = function(x, y) {
if ( !pickerRoot ) {
return null;
}
pickerRoot.style.pointerEvents = 'none';
var elem = document.elementFromPoint(x, y);
if ( elem === document.body || elem === document.documentElement ) {
elem = null;
}
pickerRoot.style.pointerEvents = '';
return elem;
};
/******************************************************************************/
var onSvgHovered = (function() {
var timer = null;
var mx = 0, my = 0;
var onTimer = function() {
timer = null;
var elem = elementFromPoint(mx, my);
highlightElements(elem ? [elem] : []);
};
var onMove = function(ev) {
mx = ev.clientX;
my = ev.clientY;
if ( timer === null ) {
timer = vAPI.setTimeout(onTimer, 40);
}
};
return onMove;
})();
/******************************************************************************/
var onSvgClicked = function(ev) {
// https://github.com/chrisaljoudi/uBlock/issues/810#issuecomment-74600694
// Unpause picker if user click outside dialog
if ( pickerBody.classList.contains('paused') ) {
unpausePicker();
return;
}
if ( filtersFrom(ev.clientX, ev.clientY) === 0 ) {
return;
}
showDialog();
};
/******************************************************************************/
var svgListening = function(on) {
var action = (on ? 'add' : 'remove') + 'EventListener';
svgRoot[action]('mousemove', onSvgHovered);
};
/******************************************************************************/
var onKeyPressed = function(ev) {
if ( ev.which === 27 ) {
ev.stopPropagation();
ev.preventDefault();
stopPicker();
}
};
/******************************************************************************/
// https://github.com/chrisaljoudi/uBlock/issues/190
// May need to dynamically adjust the height of the overlay + new position
// of highlighted elements.
var onScrolled = function() {
highlightElements(targetElements, true);
};
/******************************************************************************/
var pausePicker = function() {
pickerBody.classList.add('paused');
svgListening(false);
};
/******************************************************************************/
var unpausePicker = function() {
unpreview();
pickerBody.classList.remove('paused');
svgListening(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() {
targetElements = [];
candidateElements = [];
bestCandidateFilter = null;
previewedElements = [];
if ( pickerRoot === null ) {
return;
}
window.removeEventListener('scroll', onScrolled, true);
pickerRoot.contentWindow.removeEventListener('keydown', onKeyPressed, true);
taCandidate.removeEventListener('input', onCandidateChanged);
dialog.removeEventListener('click', onDialogClicked);
svgListening(false);
svgRoot.removeEventListener('click', onSvgClicked);
pickerStyle.parentNode.removeChild(pickerStyle);
pickerRoot.parentNode.removeChild(pickerRoot);
pickerRoot.onload = null;
pickerRoot =
pickerBody =
dialog =
svgRoot = svgOcean = svgIslands =
taCandidate = null;
window.focus();
};
/******************************************************************************/
var startPicker = function(details) {
pickerRoot.onload = stopPicker;
var frameDoc = pickerRoot.contentDocument;
var parsedDom = (new DOMParser()).parseFromString(
details.frameContent,
'text/html'
);
// Provide an id users can use as anchor to personalize uBO's element
// picker style properties.
parsedDom.documentElement.id = 'ublock0-epicker';
frameDoc.replaceChild(
frameDoc.adoptNode(parsedDom.documentElement),
frameDoc.documentElement
);
pickerBody = frameDoc.body;
pickerBody.setAttribute('lang', navigator.language);
dialog = pickerBody.querySelector('aside');
dialog.addEventListener('click', onDialogClicked);
taCandidate = dialog.querySelector('textarea');
taCandidate.addEventListener('input', onCandidateChanged);
svgRoot = pickerBody.querySelector('svg');
svgOcean = svgRoot.firstChild;
svgIslands = svgRoot.lastChild;
svgRoot.addEventListener('click', onSvgClicked);
svgListening(true);
window.addEventListener('scroll', onScrolled, true);
pickerRoot.contentWindow.addEventListener('keydown', onKeyPressed, true);
pickerRoot.contentWindow.focus();
// Restore net filter union data if it originate from the same URL.
var eprom = details.eprom || null;
if ( eprom !== null && eprom.lastNetFilterSession === lastNetFilterSession ) {
lastNetFilterHostname = eprom.lastNetFilterHostname || '';
lastNetFilterUnion = eprom.lastNetFilterUnion || '';
}
// Auto-select a specific target, if any, and if possible
highlightElements([], true);
// Try using mouse position
if ( details.clientX !== -1 ) {
if ( filtersFrom(details.clientX, details.clientY) !== 0 ) {
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',
'audio': 'src',
'embed': 'src',
'iframe': 'src',
'img': 'src',
'video': '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 elem, src;
while ( i-- ) {
elem = elems[i];
src = elem[attr];
if ( typeof src !== 'string' || src === '' ) {
continue;
}
if ( src !== url ) {
continue;
}
elem.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
filtersFrom(elem);
showDialog({ modifier: true });
return;
}
// A target was specified, but it wasn't found: abort.
stopPicker();
};
/******************************************************************************/
pickerRoot = document.createElement('iframe');
pickerRoot.id = vAPI.sessionId;
pickerRoot.style.cssText = [
'display: block',
'visibility: visible',
'opacity: 1',
'position: fixed',
'top: 0',
'left: 0',
'width: 100%',
'height: 100%',
'background: transparent',
'margin: 0',
'padding: 0',
'border: 0',
'border-radius: 0',
'box-shadow: none',
'outline: 0',
'z-index: 2147483647',
''
].join('!important;');
// https://github.com/gorhill/uBlock/issues/1529
// In addition to inline styles, harden the element picker styles by using
// a dedicated style tag.
pickerStyle = document.createElement('style');
pickerStyle.textContent = [
'#' + vAPI.sessionId + ' {',
pickerRoot.style.cssText,
'}',
'[' + vAPI.sessionId + '-clickblind] {',
'pointer-events: none !important;',
'}',
''
].join('\n');
document.documentElement.appendChild(pickerStyle);
pickerRoot.onload = function() {
vAPI.shutdown.add(stopPicker);
vAPI.messaging.send(
'elementPicker',
{ what: 'elementPickerArguments' },
startPicker
);
};
document.documentElement.appendChild(pickerRoot);
/******************************************************************************/
// https://www.youtube.com/watch?v=sociXdKnyr8
/******************************************************************************/
})();