/******************************************************************************* 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: there’s 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 items = elementsFromFilter(filter); var i = items.length, item, elem, style; while ( i-- ) { item = items[i]; elem = item.elem; style = elem.style; if ( item.type === 'cosmetic' || item.type === 'network' && item.src !== undefined ) { previewedElements.push({ elem: elem, prop: 'display', value: style.display }); style.display = 'none'; } if ( item.type === 'network' && item.style === 'background-image' ) { previewedElements.push({ elem: elem, prop: 'background-image', value: style.backgroundImage }); style.backgroundImage = 'none'; } } }; /******************************************************************************/ 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[item.prop] = item.value; } 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 = { 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.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) if ( suffix.length === 0 ) { v = elem.classList; if ( v ) { i = v.length || 0; while ( i-- ) { suffix.push('.' + CSS.escape(v.item(i))); } } } // Tag name if ( suffix.length === 0 ) { prefix = tagName; } // Attributes (depends on tag name) var attributes = [], attr; switch ( tagName ) { case 'a': v = elem.getAttribute('href'); if ( v ) { v = v.replace(/\?.*$/, ''); if ( v.length ) { attributes.push({ k: 'href', v: v }); } } break; case 'img': v = elem.getAttribute('alt'); if ( v && v.length !== 0 ) { attributes.push({ k: 'alt', v: v }); } break; default: break; } while ( (attr = attributes.pop()) ) { if ( attr.v.length === 0 ) { continue; } 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/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.tagName !== 'string' ) { continue; } if ( elem.tagName.toLowerCase() !== tagName ) { continue; } i++; } selector += ':nth-of-type(' + i + ')'; } if ( bestCandidateFilter === null ) { bestCandidateFilter = { 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 /******************************************************************************/ })();