uBlock/src/js/scriptlets/element-picker.js

1309 lines
38 KiB
JavaScript
Raw Normal View History

2014-07-13 02:32:44 +02:00
/*******************************************************************************
2016-03-06 16:51:06 +01:00
uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-2016 Raymond Hill
2014-07-13 02:32:44 +02:00
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 */
2014-07-13 02:32:44 +02:00
/******************************************************************************/
/******************************************************************************/
/*! http://mths.be/cssescape v0.2.1 by @mathias | MIT license */
;(function(root) {
2015-01-30 06:49:30 +01:00
'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;
};
}
2014-07-13 02:32:44 +02:00
}(self));
2014-07-13 02:32:44 +02:00
/******************************************************************************/
/******************************************************************************/
(function() {
2015-01-30 06:49:30 +01:00
'use strict';
2014-07-13 02:32:44 +02:00
/******************************************************************************/
if ( typeof vAPI !== 'object' ) {
return;
}
// don't run in frames
2014-12-28 21:26:06 +01:00
if ( window.top !== window ) {
return;
}
var pickerRoot = document.getElementById(vAPI.sessionId);
if ( pickerRoot ) {
return;
}
var pickerBody = null;
var pickerStyle = null;
2014-07-13 02:32:44 +02:00
var svgOcean = null;
var svgIslands = null;
2015-03-09 15:11:22 +01:00
var svgRoot = null;
var dialog = null;
2014-07-13 02:32:44 +02:00
var taCandidate = null;
2014-09-28 18:05:46 +02:00
var netFilterCandidates = [];
var cosmeticFilterCandidates = [];
2014-07-13 02:32:44 +02:00
var targetElements = [];
var candidateElements = [];
var bestCandidateFilter = null;
var previewedElements = [];
2014-07-13 02:32:44 +02:00
var lastNetFilterSession = window.location.host + window.location.pathname;
var lastNetFilterHostname = '';
var lastNetFilterUnion = '';
2014-07-13 02:32:44 +02:00
/******************************************************************************/
2015-01-30 06:49:30 +01:00
// 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 = '';
}
/******************************************************************************/
2015-02-16 17:14:37 +01:00
var safeQuerySelectorAll = function(node, selector) {
2015-02-16 17:21:25 +01:00
if ( node !== null ) {
try {
return node.querySelectorAll(selector);
} catch (e) {
}
2015-02-16 17:14:37 +01:00
}
2015-02-16 17:21:25 +01:00
return [];
2015-02-16 17:14:37 +01:00
};
/******************************************************************************/
2015-12-07 17:09:39 +01:00
var getElementBoundingClientRect = function(elem) {
2015-12-07 17:18:56 +01:00
var rect = typeof elem.getBoundingClientRect === 'function' ?
2015-12-07 17:09:39 +01:00
elem.getBoundingClientRect() :
{ height: 0, left: 0, top: 0, width: 0 };
// https://github.com/gorhill/uBlock/issues/1024
// Try not returning an empty bounding rect.
2015-12-07 17:18:56 +01:00
if ( rect.width !== 0 && rect.height !== 0 ) {
return rect;
2015-12-07 17:09:39 +01:00
}
2015-12-07 17:18:56 +01:00
var left = rect.left,
right = rect.right,
top = rect.top,
bottom = rect.bottom;
2015-12-07 17:09:39 +01:00
var children = elem.children,
i = children.length;
while ( i-- ) {
2015-12-07 17:18:56 +01:00
rect = getElementBoundingClientRect(children[i]);
if ( rect.width === 0 || rect.height === 0 ) {
2015-12-07 17:09:39 +01:00
continue;
}
2015-12-07 17:18:56 +01:00
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; }
2015-12-07 17:09:39 +01:00
}
return {
height: bottom - top,
left: left,
top: top,
width: right - left
};
};
/******************************************************************************/
2014-07-13 02:32:44 +02:00
var highlightElements = function(elems, force) {
// To make mouse move handler more efficient
2014-07-13 02:32:44 +02:00
if ( !force && elems.length === targetElements.length ) {
if ( elems.length === 0 || elems[0] === targetElements[0] ) {
return;
}
}
targetElements = elems;
2014-10-09 16:41:20 +02:00
var ow = pickerRoot.contentWindow.innerWidth;
var oh = pickerRoot.contentWindow.innerHeight;
2014-07-13 02:32:44 +02:00
var ocean = [
'M0 0',
'h', ow,
'v', oh,
2014-07-13 02:32:44 +02:00
'h-', ow,
'z'
];
var islands = [];
2014-11-07 12:59:01 +01:00
2014-11-03 15:28:55 +01:00
var elem, rect, poly;
2014-07-13 02:32:44 +02:00
for ( var i = 0; i < elems.length; i++ ) {
2014-11-03 15:28:55 +01:00
elem = elems[i];
if ( elem === pickerRoot ) {
continue;
}
2015-12-07 17:09:39 +01:00
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 +
2014-11-03 15:28:55 +01:00
'h' + rect.width +
'v' + rect.height +
'h-' + rect.width +
'z';
ocean.push(poly);
islands.push(poly);
2014-07-13 02:32:44 +02:00
}
svgOcean.setAttribute('d', ocean.join(''));
svgIslands.setAttribute('d', islands.join('') || 'M0 0');
2014-07-13 02:32:44 +02:00
};
/******************************************************************************/
var filterElements = function(filter) {
2016-04-17 16:15:01 +02:00
var htmlElem = document.documentElement;
var items = elementsFromFilter(filter);
var i = items.length, item, elem, style;
2014-07-13 02:32:44 +02:00
while ( i-- ) {
item = items[i];
elem = item.elem;
2016-05-13 17:10:09 +02:00
// https://github.com/gorhill/uBlock/issues/1629
if ( elem === pickerRoot ) {
continue;
}
style = elem.style;
if (
2016-04-17 16:15:01 +02:00
(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] : '';
};
/******************************************************************************/
// https://github.com/gorhill/uBlock/issues/1725#issuecomment-226479197
2016-06-16 16:55:49 +02:00
// Limit returned string to 1024 characters.
// Also, return only URLs which will be seen by an HTTP observer.
var resourceURLFromElement = function(elem) {
var tagName = elem.localName, s;
2016-06-16 16:55:49 +02:00
if (
(s = netFilter1stSources[tagName]) ||
(s = netFilter2ndSources[tagName])
) {
s = elem[s];
2016-06-16 16:55:49 +02:00
if ( typeof s === 'string' && /^https?:\/\//.test(s) ) {
return s.slice(0, 1024);
}
}
return backgroundImageURLFromElement(elem);
2014-07-13 02:32:44 +02:00
};
/******************************************************************************/
var netFilterFromUnion = (function() {
2015-03-19 16:16:56 +01:00
var reTokenizer = /[^0-9a-z%*]+|[0-9a-z%]+|\*/gi;
2015-03-19 15:13:51 +01:00
var a = document.createElement('a');
return function(to, out) {
2015-03-19 15:13:51 +01:00
a.href= to;
to = a.pathname + a.search;
var from = lastNetFilterUnion;
2015-03-19 15:13:51 +01:00
// Reset reference filter when dealing with unrelated URLs
2015-03-22 05:36:14 +01:00
if ( from === '' || a.host === '' || a.host !== lastNetFilterHostname ) {
lastNetFilterHostname = a.host;
lastNetFilterUnion = to;
2016-03-06 16:51:06 +01:00
vAPI.messaging.send(
'elementPicker',
{
what: 'elementPickerEprom',
lastNetFilterSession: lastNetFilterSession,
lastNetFilterHostname: lastNetFilterHostname,
lastNetFilterUnion: lastNetFilterUnion
}
);
2015-03-19 15:13:51 +01:00
return;
}
// Related URLs
lastNetFilterHostname = a.host;
2015-03-19 15:13:51 +01:00
2015-03-19 16:16:56 +01:00
var fromTokens = from.match(reTokenizer);
var toTokens = to.match(reTokenizer);
var toCount = toTokens.length, toIndex = 0;
var fromToken, pos;
2015-03-19 15:13:51 +01:00
for ( var fromIndex = 0; fromIndex < fromTokens.length; fromIndex += 1 ) {
2015-03-19 16:16:56 +01:00
fromToken = fromTokens[fromIndex];
if ( fromToken === '*' ) {
2015-03-19 15:13:51 +01:00
continue;
}
2015-03-19 16:16:56 +01:00
pos = toTokens.indexOf(fromToken, toIndex);
if ( pos === -1 ) {
2015-03-19 15:13:51 +01:00
fromTokens[fromIndex] = '*';
continue;
}
2015-03-19 16:16:56 +01:00
if ( pos !== toIndex ) {
2015-03-19 15:13:51 +01:00
fromTokens.splice(fromIndex, 0, '*');
fromIndex += 1;
}
2015-03-19 16:16:56 +01:00
toIndex = pos + 1;
if ( toIndex === toCount ) {
fromTokens = fromTokens.slice(0, fromIndex + 1);
break;
}
2015-03-19 15:13:51 +01:00
}
from = fromTokens.join('').replace(/\*\*+/g, '*');
2015-03-22 04:54:09 +01:00
if ( from !== '/*' && from !== to ) {
var filter = '||' + lastNetFilterHostname + from;
if ( out.indexOf(filter) === -1 ) {
out.push(filter);
}
2015-03-19 15:13:51 +01:00
} else {
from = to;
}
lastNetFilterUnion = from;
// Remember across element picker sessions
2016-03-06 16:51:06 +01:00
vAPI.messaging.send(
'elementPicker',
{
what: 'elementPickerEprom',
lastNetFilterSession: lastNetFilterSession,
lastNetFilterHostname: lastNetFilterHostname,
lastNetFilterUnion: lastNetFilterUnion
}
);
2015-03-19 15:13:51 +01:00
};
})();
/******************************************************************************/
2014-07-13 02:32:44 +02:00
// Extract the best possible net filter, i.e. as specific as possible.
var netFilterFromElement = function(elem) {
2014-07-13 02:32:44 +02:00
if ( elem === null ) {
return 0;
2014-07-13 02:32:44 +02:00
}
if ( elem.nodeType !== 1 ) {
return 0;
2014-07-13 02:32:44 +02:00
}
var src = resourceURLFromElement(elem);
if ( src === '' ) {
return 0;
2014-07-13 02:32:44 +02:00
}
if ( candidateElements.indexOf(elem) === -1 ) {
candidateElements.push(elem);
}
var candidates = netFilterCandidates;
var len = candidates.length;
2014-09-28 18:05:46 +02:00
// Remove fragment
var pos = src.indexOf('#');
if ( pos !== -1 ) {
src = src.slice(0, pos);
}
2015-03-19 15:13:51 +01:00
var filter = src.replace(/^https?:\/\//, '||');
if ( bestCandidateFilter === null ) {
bestCandidateFilter = {
2016-04-17 16:15:01 +02:00
type: 'net',
filters: candidates,
slot: candidates.length
};
}
candidates.push(filter);
2015-03-19 15:13:51 +01:00
2014-09-28 18:05:46 +02:00
// Suggest a less narrow filter if possible
2015-03-19 15:13:51 +01:00
pos = filter.indexOf('?');
2014-09-28 18:05:46 +02:00
if ( pos !== -1 ) {
candidates.push(filter.slice(0, pos));
2014-09-28 18:05:46 +02:00
}
2015-03-19 15:13:51 +01:00
// Suggest a filter which is a result of combining more than one URL.
netFilterFromUnion(src, candidates);
return candidates.length - len;
2014-07-13 02:32:44 +02:00
};
2015-06-04 17:17:02 +02:00
var netFilter1stSources = {
'audio': 'src',
'embed': 'src',
2015-02-04 00:43:51 +01:00
'iframe': 'src',
'img': 'src',
'object': 'data',
'video': 'src'
2015-02-04 00:43:51 +01:00
};
2015-06-04 17:17:02 +02:00
var netFilter2ndSources = {
'img': 'srcset'
};
var filterTypes = {
'audio': 'media',
'embed': 'object',
'iframe': 'subdocument',
'img': 'image',
'object': 'object',
'video': 'media',
};
2014-07-13 02:32:44 +02:00
/******************************************************************************/
// Extract the best possible cosmetic filter, i.e. as specific as possible.
2016-06-16 16:55:49 +02:00
// https://github.com/gorhill/uBlock/issues/1725
// Also take into account the `src` attribute for `img` elements -- and limit
// the value to the 1024 first characters.
var cosmeticFilterFromElement = function(elem) {
2014-07-13 02:32:44 +02:00
if ( elem === null ) {
return 0;
2014-07-13 02:32:44 +02:00
}
if ( elem.nodeType !== 1 ) {
return 0;
2014-07-13 02:32:44 +02:00
}
if ( candidateElements.indexOf(elem) === -1 ) {
candidateElements.push(elem);
}
2016-04-17 16:15:01 +02:00
var tagName = elem.localName;
var selector = '';
2015-01-30 06:49:30 +01:00
var v, i;
2014-07-13 02:32:44 +02:00
// Id
v = typeof elem.id === 'string' && CSS.escape(elem.id);
if ( v ) {
2016-04-17 16:15:01 +02:00
selector = '#' + v;
2014-07-13 02:32:44 +02:00
}
// Class(es)
2016-04-17 16:15:01 +02:00
if ( selector === '' ) {
v = elem.classList;
if ( v ) {
i = v.length || 0;
while ( i-- ) {
2016-04-17 16:15:01 +02:00
selector += '.' + CSS.escape(v.item(i));
}
2014-07-13 02:32:44 +02:00
}
}
// Tag name
2016-04-17 16:15:01 +02:00
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 });
}
2014-10-13 15:47:18 +02:00
}
2016-04-17 16:15:01 +02:00
break;
case 'img':
2016-06-16 16:55:49 +02:00
v = elem.getAttribute('src');
if ( v && v.length !== 0 ) {
attributes.push({ k: 'src', v: v.slice(0, 1024) });
break;
}
2016-04-17 16:15:01 +02:00
v = elem.getAttribute('alt');
if ( v && v.length !== 0 ) {
attributes.push({ k: 'alt', v: v });
2016-06-16 16:55:49 +02:00
break;
2016-04-17 16:15:01 +02:00
}
break;
default:
break;
2014-07-13 02:32:44 +02:00
}
2016-04-17 16:15:01 +02:00
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 + '"]';
}
2014-07-13 02:32:44 +02:00
}
}
2015-04-07 03:26:05 +02:00
// https://github.com/chrisaljoudi/uBlock/issues/637
2015-01-30 06:49:30 +01:00
// 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;
2015-02-16 17:21:25 +01:00
if ( safeQuerySelectorAll(parentNode, cssScope + selector).length > 1 ) {
2015-01-30 06:49:30 +01:00
i = 1;
while ( elem.previousSibling !== null ) {
elem = elem.previousSibling;
2016-04-17 16:15:01 +02:00
if ( typeof elem.localName !== 'string' || elem.localName !== tagName ) {
2015-01-30 06:49:30 +01:00
continue;
}
i++;
}
selector += ':nth-of-type(' + i + ')';
}
if ( bestCandidateFilter === null ) {
bestCandidateFilter = {
2016-04-17 16:15:01 +02:00
type: 'cosmetic',
filters: cosmeticFilterCandidates,
slot: cosmeticFilterCandidates.length
};
}
cosmeticFilterCandidates.push('##' + selector);
return 1;
2014-07-13 02:32:44 +02:00
};
/******************************************************************************/
var filtersFrom = function(x, y) {
bestCandidateFilter = null;
2014-09-28 18:05:46 +02:00
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);
2014-09-28 18:05:46 +02:00
elem = elem.parentNode;
2014-07-13 02:32:44 +02:00
}
// 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;
};
/******************************************************************************/
2014-09-28 18:05:46 +02:00
var elementsFromFilter = function(filter) {
var out = [];
filter = filter.trim();
if ( filter === '' ) {
return out;
}
2014-09-28 18:05:46 +02:00
// 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;
2015-03-06 06:06:25 +01:00
if ( filter.lastIndexOf('##', 0) === 0 ) {
2014-09-28 18:05:46 +02:00
try {
elems = document.querySelectorAll(filter.slice(2));
2014-09-28 18:05:46 +02:00
}
catch (e) {
elems = [];
}
iElem = elems.length;
while ( iElem-- ) {
out.push({
type: 'cosmetic',
elem: elems[iElem],
});
2014-07-13 02:32:44 +02:00
}
2014-09-28 18:05:46 +02:00
return out;
2014-07-13 02:32:44 +02:00
}
2014-09-28 18:05:46 +02:00
// Net filters: we need to lookup manually -- translating into a
// foolproof CSS selector is just not possible
2015-03-05 18:52:12 +01:00
2015-04-07 03:26:05 +02:00
// https://github.com/chrisaljoudi/uBlock/issues/945
2015-03-05 18:52:12 +01:00
// 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);
2014-09-28 18:05:46 +02:00
}
2015-03-05 18:52:12 +01:00
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) {
2015-03-05 18:52:12 +01:00
return out;
}
// Lookup by tag names.
2015-06-04 17:17:02 +02:00
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];
2015-06-04 17:17:02 +02:00
if ( typeof src !== 'string' || src.length === 0 ) {
srcProp = src2ndProps[elem.localName];
src = elem[srcProp];
2015-06-04 17:17:02 +02:00
}
2015-03-05 18:52:12 +01:00
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',
});
2014-09-28 18:05:46 +02:00
}
2014-07-13 02:32:44 +02:00
}
2014-09-28 18:05:46 +02:00
return out;
2014-07-13 02:32:44 +02:00
};
// https://www.youtube.com/watch?v=nuUXJ6RfIik
2014-09-28 18:05:46 +02:00
2014-07-13 02:32:44 +02:00
/******************************************************************************/
var userFilterFromCandidate = function() {
2014-09-28 18:05:46 +02:00
var v = taCandidate.value;
var items = elementsFromFilter(v);
if ( items.length === 0 ) {
2014-07-13 02:32:44 +02:00
return false;
}
2015-11-24 21:27:39 +01:00
// https://github.com/gorhill/uBlock/issues/738
// Trim dots.
var hostname = window.location.hostname;
if ( hostname.slice(-1) === '.' ) {
hostname = hostname.slice(0, -1);
}
2014-07-13 02:32:44 +02:00
// Cosmetic filter?
2015-03-06 06:36:45 +01:00
if ( v.lastIndexOf('##', 0) === 0 ) {
2015-11-24 21:27:39 +01:00
return hostname + v;
2014-07-13 02:32:44 +02:00
}
// Assume net filter
var opts = [];
// If no domain included in filter, we need domain option
if ( v.lastIndexOf('||', 0) === -1 ) {
opts.push('domain=' + hostname);
2014-07-13 02:32:44 +02:00
}
2015-03-06 06:36:45 +01:00
var item = items[0];
if ( item.opts ) {
opts.push(item.opts);
}
if ( opts.length ) {
v += '$' + opts.join(',');
}
return v;
2014-07-13 02:32:44 +02:00
};
/******************************************************************************/
2014-09-28 18:05:46 +02:00
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);
2014-07-13 02:32:44 +02:00
};
/******************************************************************************/
2014-09-28 18:05:46 +02:00
var candidateFromFilterChoice = function(filterChoice) {
var slot = filterChoice.slot;
var filters = filterChoice.filters;
var filter = filters[slot];
2014-09-28 18:05:46 +02:00
if ( filter === undefined ) {
return '';
}
// For net filters there no such thing as a path
if ( filter.lastIndexOf('##', 0) !== 0 ) {
2014-09-28 18:05:46 +02:00
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 = [];
2014-09-28 18:05:46 +02:00
for ( ; slot < filters.length; slot++ ) {
filter = filters[slot];
selector.unshift(filter.replace(/^##/, ''));
2014-09-26 00:10:58 +02:00
// Stop at any element with an id: these are unique in a web page
2014-09-28 18:05:46 +02:00
if ( filter.slice(0, 3) === '###' ) {
2014-09-26 00:10:58 +02:00
break;
}
}
return '##' + selector.join(' > ');
};
/******************************************************************************/
2014-09-28 18:05:46 +02:00
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;
};
/******************************************************************************/
2014-07-13 02:32:44 +02:00
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();
2014-07-13 02:32:44 +02:00
var filter = userFilterFromCandidate();
if ( filter ) {
2015-03-06 06:36:45 +01:00
var d = new Date();
2016-03-06 16:51:06 +01:00
vAPI.messaging.send(
'elementPicker',
{
what: 'createUserFilter',
filters: '! ' + d.toLocaleString() + ' ' + window.location.href + '\n' + filter,
}
);
filterElements(taCandidate.value);
2014-07-13 02:32:44 +02:00
stopPicker();
}
}
else if ( ev.target.id === 'pick' ) {
unpausePicker();
}
else if ( ev.target.id === 'quit' ) {
unpreview();
2014-07-13 02:32:44 +02:00
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') ) {
2014-09-28 18:05:46 +02:00
taCandidate.value = candidateFromFilterChoice(filterChoiceFromEvent(ev));
2014-07-13 02:32:44 +02:00
onCandidateChanged();
}
ev.stopPropagation();
ev.preventDefault();
};
/******************************************************************************/
var removeAllChildren = function(parent) {
while ( parent.firstChild ) {
parent.removeChild(parent.firstChild);
}
};
/******************************************************************************/
2014-07-13 17:44:36 +02:00
// 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.
2014-09-28 18:05:46 +02:00
var showDialog = function(options) {
pausePicker();
options = options || {};
// Create lists of candidate filters
var populate = function(src, des) {
var root = dialog.querySelector(des);
2014-09-28 18:05:46 +02:00
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);
2014-07-13 02:32:44 +02:00
}
2014-09-28 18:05:46 +02:00
root.style.display = src.length !== 0 ? '' : 'none';
};
2014-09-28 22:11:32 +02:00
populate(netFilterCandidates, '#netFilters');
populate(cosmeticFilterCandidates, '#cosmeticFilters');
2014-09-28 18:05:46 +02:00
dialog.querySelector('ul').style.display = netFilterCandidates.length || cosmeticFilterCandidates.length ? '' : 'none';
dialog.querySelector('#create').disabled = true;
2014-09-28 18:05:46 +02:00
// Auto-select a candidate filter
if ( bestCandidateFilter === null ) {
taCandidate.value = '';
return;
}
2014-09-28 18:05:46 +02:00
var filterChoice = {
filters: bestCandidateFilter.filters,
slot: bestCandidateFilter.slot,
2014-09-28 18:05:46 +02:00
modifier: options.modifier || false
};
taCandidate.value = candidateFromFilterChoice(filterChoice);
onCandidateChanged();
2014-07-13 02:32:44 +02:00
};
/******************************************************************************/
var elementFromPoint = function(x, y) {
2015-12-03 07:08:37 +01:00
if ( !pickerRoot ) {
return null;
}
pickerRoot.style.pointerEvents = 'none';
2014-09-28 20:38:17 +02:00
var elem = document.elementFromPoint(x, y);
if ( elem === document.body || elem === document.documentElement ) {
2014-09-28 20:38:17 +02:00
elem = null;
}
pickerRoot.style.pointerEvents = '';
2014-09-28 20:38:17 +02:00
return elem;
};
/******************************************************************************/
2015-05-27 21:15:20 +02:00
var onSvgHovered = (function() {
var timer = null;
2015-05-30 19:44:55 +02:00
var mx = 0, my = 0;
2015-05-27 21:15:20 +02:00
var onTimer = function() {
timer = null;
2015-05-30 19:44:55 +02:00
var elem = elementFromPoint(mx, my);
2015-05-27 21:15:20 +02:00
highlightElements(elem ? [elem] : []);
};
var onMove = function(ev) {
2015-05-30 19:44:55 +02:00
mx = ev.clientX;
my = ev.clientY;
if ( timer === null ) {
timer = vAPI.setTimeout(onTimer, 40);
2015-05-27 21:15:20 +02:00
}
};
return onMove;
})();
2014-07-13 02:32:44 +02:00
/******************************************************************************/
2014-09-28 20:38:17 +02:00
var onSvgClicked = function(ev) {
2015-04-07 03:26:05 +02:00
// https://github.com/chrisaljoudi/uBlock/issues/810#issuecomment-74600694
2015-03-09 15:11:22 +01:00
// Unpause picker if user click outside dialog
if ( pickerBody.classList.contains('paused') ) {
2015-03-09 15:11:22 +01:00
unpausePicker();
return;
}
if ( filtersFrom(ev.clientX, ev.clientY) === 0 ) {
2014-09-28 20:38:17 +02:00
return;
}
2014-09-28 18:05:46 +02:00
showDialog();
2014-07-13 02:32:44 +02:00
};
/******************************************************************************/
var svgListening = function(on) {
var action = (on ? 'add' : 'remove') + 'EventListener';
2015-03-09 15:11:22 +01:00
svgRoot[action]('mousemove', onSvgHovered);
};
/******************************************************************************/
var onKeyPressed = function(ev) {
if ( ev.which === 27 ) {
ev.stopPropagation();
ev.preventDefault();
stopPicker();
}
};
/******************************************************************************/
2015-04-07 03:26:05 +02:00
// https://github.com/chrisaljoudi/uBlock/issues/190
2014-08-30 23:22:31 +02:00
// May need to dynamically adjust the height of the overlay + new position
// of highlighted elements.
2014-09-28 20:38:17 +02:00
var onScrolled = function() {
2014-08-30 23:20:14 +02:00
highlightElements(targetElements, true);
};
/******************************************************************************/
2015-03-09 15:11:22 +01:00
var pausePicker = function() {
pickerBody.classList.add('paused');
2015-03-09 15:11:22 +01:00
svgListening(false);
};
/******************************************************************************/
var unpausePicker = function() {
unpreview();
pickerBody.classList.remove('paused');
2015-03-09 15:11:22 +01:00
svgListening(true);
};
/******************************************************************************/
2014-09-28 18:05:46 +02:00
// 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);
2015-03-09 15:11:22 +01:00
svgRoot.removeEventListener('click', onSvgClicked);
pickerStyle.parentNode.removeChild(pickerStyle);
pickerRoot.parentNode.removeChild(pickerRoot);
pickerRoot.onload = null;
pickerRoot =
pickerBody =
dialog =
2015-03-09 15:11:22 +01:00
svgRoot = svgOcean = svgIslands =
taCandidate = null;
window.focus();
};
/******************************************************************************/
var startPicker = function(details) {
2015-02-11 17:49:50 +01:00
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';
2015-02-11 17:49:50 +01:00
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;
2015-03-09 15:11:22 +01:00
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;
}
2015-06-17 19:49:43 +02:00
// A target was specified, but it wasn't found: abort.
stopPicker();
};
/******************************************************************************/
pickerRoot = document.createElement('iframe');
pickerRoot.id = vAPI.sessionId;
pickerRoot.style.cssText = [
2015-02-10 19:36:13 +01:00
'display: block',
'visibility: visible',
'opacity: 1',
'position: fixed',
'top: 0',
'left: 0',
'width: 100%',
'height: 100%',
'background: transparent',
2015-02-10 19:36:13 +01:00
'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() {
2016-03-06 16:51:06 +01:00
vAPI.shutdown.add(stopPicker);
vAPI.messaging.send(
'elementPicker',
{ what: 'elementPickerArguments' },
startPicker
);
};
document.documentElement.appendChild(pickerRoot);
/******************************************************************************/
2014-07-13 02:32:44 +02:00
2014-09-28 20:58:26 +02:00
// https://www.youtube.com/watch?v=sociXdKnyr8
2014-07-13 02:32:44 +02:00
/******************************************************************************/
})();