diff --git a/src/epicker.html b/src/epicker.html index ac8c2d998..dc5d0d1b9 100644 --- a/src/epicker.html +++ b/src/epicker.html @@ -42,7 +42,15 @@ button:not(:disabled):hover { opacity: 1; } #create:not(:disabled) { - background-color: #ffdca8; + background-color: hsl(36, 100%, 83%); + border-color: hsl(36, 50%, 60%); +} +#preview { + float: left; +} +body.preview #preview { + background-color: hsl(204, 100%, 83%); + border-color: hsl(204, 50%, 60%); } section { border: 0; @@ -78,7 +86,7 @@ ul { overflow: hidden; } aside > ul { - height: 16em; + max-height: 16em; overflow-y: auto; } aside > ul > li:first-of-type { @@ -124,14 +132,19 @@ svg > path:first-child { svg > path + path { stroke: #F00; stroke-width: 0.5px; - fill: rgba(255,31,31,0.25); + fill: rgba(255,63,63,0.20); +} +body.preview svg > path:first-child { + fill: rgba(0,0,0,0.10); +} +body.preview svg > path + path { + fill: rgba(0,0,0,0.10); } aside { background-color: #eee; bottom: 4px; box-sizing: border-box; visibility: hidden; - height: calc(40% - 4px); padding: 4px; position: fixed; right: 4px; @@ -154,7 +167,8 @@ body.paused > aside:hover {
diff --git a/src/js/messaging.js b/src/js/messaging.js index d19f6116f..9f2ee3c4b 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -19,8 +19,6 @@ Home: https://github.com/gorhill/uBlock */ -/* global µBlock, vAPI */ - /******************************************************************************/ /******************************************************************************/ @@ -603,6 +601,7 @@ var onMessage = function(request, sender, callback) { create: vAPI.i18n('pickerCreate'), pick: vAPI.i18n('pickerPick'), quit: vAPI.i18n('pickerQuit'), + preview: vAPI.i18n('pickerPreview'), netFilters: vAPI.i18n('pickerNetFilters'), cosmeticFilters: vAPI.i18n('pickerCosmeticFilters'), cosmeticFiltersHint: vAPI.i18n('pickerCosmeticFiltersHint') diff --git a/src/js/scriptlets/element-picker.js b/src/js/scriptlets/element-picker.js index 0b31918fc..d0da4c52d 100644 --- a/src/js/scriptlets/element-picker.js +++ b/src/js/scriptlets/element-picker.js @@ -19,7 +19,7 @@ Home: https://github.com/gorhill/uBlock */ -/* global CSS, CSSImportRule, CSSStyleRule */ +/* global CSS */ /******************************************************************************/ /******************************************************************************/ @@ -133,7 +133,7 @@ var pickerRoot = document.getElementById(vAPI.sessionId); if ( pickerRoot ) { return; } - +var pickerBody = null; var pickerStyle = null; var svgOcean = null; var svgIslands = null; @@ -145,6 +145,9 @@ var netFilterCandidates = []; var cosmeticFilterCandidates = []; var targetElements = []; +var candidateElements = []; +var bestCandidateFilter = null; +var previewedElements = []; var lastNetFilterSession = window.location.host + window.location.pathname; var lastNetFilterHostname = ''; @@ -267,26 +270,51 @@ var highlightElements = function(elems, force) { var filterElements = function(filter) { var items = elementsFromFilter(filter); - var i = items.length, item; + 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 ) { - item.elem.style.setProperty('display', 'none', 'important'); + previewedElements.push({ + elem: elem, + prop: 'display', + value: style.display + }); + style.display = 'none'; } if ( item.type === 'network' && item.style === 'background-image' ) { - item.elem.style.setProperty('background-image', 'none', 'important'); + previewedElements.push({ + elem: elem, + prop: 'background-image', + value: style.backgroundImage + }); + style.backgroundImage = 'none'; } } }; /******************************************************************************/ -var urlFromCSSPropertyValue = function(value) { - var matches = /^url\((["']?)([^"']+)\1\)$/.exec(value); - return matches !== null && matches.length === 3 ? matches[2] : ''; +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'); }; /******************************************************************************/ @@ -294,7 +322,8 @@ var urlFromCSSPropertyValue = function(value) { var backgroundImageURLFromElement = function(elem) { var style = window.getComputedStyle(elem); var bgImg = style.backgroundImage || ''; - return bgImg !== '' ? urlFromCSSPropertyValue(bgImg) : ''; + var matches = /^url\((["']?)([^"']+)\1\)$/.exec(bgImg); + return matches !== null && matches.length === 3 ? matches[2] : ''; }; /******************************************************************************/ @@ -399,18 +428,25 @@ var netFilterFromUnion = (function() { // Extract the best possible net filter, i.e. as specific as possible. -var netFilterFromElement = function(elem, out) { +var netFilterFromElement = function(elem) { if ( elem === null ) { - return; + return 0; } if ( elem.nodeType !== 1 ) { - return; + return 0; } var src = resourceURLFromElement(elem); if ( src === '' ) { - return; + 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 ) { @@ -419,17 +455,25 @@ var netFilterFromElement = function(elem, out) { var filter = src.replace(/^https?:\/\//, '||'); - // Anchor absolute filter to hostname - out.push(filter); + 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 ) { - out.push(filter.slice(0, pos)); + candidates.push(filter.slice(0, pos)); } // Suggest a filter which is a result of combining more than one URL. - netFilterFromUnion(src, out); + netFilterFromUnion(src, candidates); + + return candidates.length - len; }; var netFilter1stSources = { @@ -458,13 +502,18 @@ var filterTypes = { // Extract the best possible cosmetic filter, i.e. as specific as possible. -var cosmeticFilterFromElement = function(elem, out) { +var cosmeticFilterFromElement = function(elem) { if ( elem === null ) { - return; + return 0; } if ( elem.nodeType !== 1 ) { - return; + return 0; } + + if ( candidateElements.indexOf(elem) === -1 ) { + candidateElements.push(elem); + } + var tagName = elem.tagName.toLowerCase(); var prefix = ''; var suffix = []; @@ -549,22 +598,25 @@ var cosmeticFilterFromElement = function(elem, out) { selector += ':nth-of-type(' + i + ')'; } - out.push('##' + selector); + 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; - - // This is to prevent revisiting the same element more than once. - var visited = typeof Set === 'function' ? - new Set() : - { - add: function() {}, - has: function() { return true; } - }; + candidateElements.length = 0; // We need at least one element. var first = null; @@ -574,16 +626,16 @@ var filtersFrom = function(x, y) { first = x; x = undefined; } - if ( first === null ) { - return 0; + + // Network filter from element which was clicked. + if ( first !== null ) { + netFilterFromElement(first); } - // Extract filter candidates from self and all ancestors. + // Cosmetic filter candidates from ancestors. var elem = first; while ( elem && elem !== document.body ) { - netFilterFromElement(elem, netFilterCandidates); - cosmeticFilterFromElement(elem, cosmeticFilterCandidates); - visited.add(elem); + cosmeticFilterFromElement(elem); elem = elem.parentNode; } // The body tag is needed as anchor only when the immediate child @@ -594,28 +646,27 @@ var filtersFrom = function(x, y) { } // https://github.com/gorhill/uBlock/issues/1545 - // Extract filter candidates from all elements found at point (x, y). + // 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; - for (;;) { + while ( elem !== null ) { previous = elem; elem.setAttribute(attrName, ''); - elem = elementFromPoint(x, y, true); + elem = elementFromPoint(x, y); if ( elem === null || elem === previous ) { break; } - if ( visited.has(elem) === false ) { - netFilterFromElement(elem, netFilterCandidates); - visited.add(elem); - } + netFilterFromElement(elem); } var elems = document.querySelectorAll('[' + attrName + ']'); i = elems.length; while ( i-- ) { elems[i].removeAttribute(attrName); } + + netFilterFromElement(document.body); } return netFilterCandidates.length + cosmeticFilterCandidates.length; @@ -623,45 +674,6 @@ var filtersFrom = function(x, y) { /******************************************************************************/ -var elementsFromStylesheet = function(sheet, reURL, out) { - var rules = sheet.rules; - if ( !rules ) { - return; - } - var iRule = rules.length; - var rule, value, src, elems, iElem; - while ( iRule-- ) { - rule = rules[iRule]; - if ( rule instanceof CSSImportRule ) { - elementsFromStylesheet(rule.styleSheet, reURL, out); - continue; - } - if ( rule instanceof CSSStyleRule === false ) { - continue; - } - value = rule.style.backgroundImage; - if ( value.lastIndexOf('url(', 0) !== 0 ) { - continue; - } - src = urlFromCSSPropertyValue(value); - if ( reURL.test(src) === false ) { - continue; - } - elems = document.querySelectorAll(rule.selectorText); - iElem = elems.length; - while ( iElem-- ) { - out.push({ - type: 'network', - elem: elems[iElem], - style: 'background-image', - opts: 'image' - }); - } - } -}; - -/******************************************************************************/ - var elementsFromFilter = function(filter) { var out = []; @@ -754,8 +766,8 @@ var elementsFromFilter = function(filter) { } } - // Lookup by inline-styled background image. - elems = document.querySelectorAll('[style*="background-image"]'); + // Find matching background image in current set of candidate elements. + elems = candidateElements; iElem = elems.length; while ( iElem-- ) { elem = elems[iElem]; @@ -769,13 +781,6 @@ var elementsFromFilter = function(filter) { } } - // Lookup by stylesheet-styled background image. - var sheets = document.styleSheets; - var iSheet = sheets.length; - while ( iSheet-- ) { - elementsFromStylesheet(sheets[iSheet], reFilter, out); - } - return out; }; @@ -825,13 +830,15 @@ var userFilterFromCandidate = function() { /******************************************************************************/ 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); + highlightElements(elems, true); }; /******************************************************************************/ @@ -846,7 +853,7 @@ var candidateFromFilterChoice = function(filterChoice) { } // For net filters there no such thing as a path - if ( filterChoice.type === 'net' || filterChoice.modifier ) { + if ( filter.lastIndexOf('##', 0) !== 0 || filterChoice.modifier ) { return filter; } @@ -869,7 +876,6 @@ var filterChoiceFromEvent = function(ev) { var li = ev.target; var isNetFilter = li.textContent.slice(0, 2) !== '##'; var r = { - type: isNetFilter ? 'net' : 'cosmetic', filters: isNetFilter ? netFilterCandidates : cosmeticFilterCandidates, slot: 0, modifier: ev.ctrlKey || ev.metaKey @@ -889,6 +895,10 @@ var onDialogClicked = function(ev) { } 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(); @@ -909,9 +919,19 @@ var onDialogClicked = function(ev) { } 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(); @@ -960,39 +980,31 @@ var showDialog = function(options) { dialog.querySelector('#create').disabled = true; // Auto-select a candidate filter - var filterChoice = { - type: '', - filters: [], - slot: 0, - modifier: options.modifier || false - }; - if ( netFilterCandidates.length ) { - filterChoice.type = 'net'; - filterChoice.filters = netFilterCandidates; - } else if ( cosmeticFilterCandidates.length ) { - filterChoice.type = 'cosmetic'; - filterChoice.filters = cosmeticFilterCandidates; + + if ( bestCandidateFilter === null ) { + taCandidate.value = ''; + return; } - taCandidate.value = ''; - if ( filterChoice.type !== '' ) { - taCandidate.value = candidateFromFilterChoice(filterChoice); - onCandidateChanged(); - } + var filterChoice = { + filters: bestCandidateFilter.filters, + slot: bestCandidateFilter.slot, + modifier: options.modifier || false + }; + + taCandidate.value = candidateFromFilterChoice(filterChoice); + onCandidateChanged(); }; /******************************************************************************/ -var elementFromPoint = function(x, y, includeBody) { +var elementFromPoint = function(x, y) { if ( !pickerRoot ) { return null; } pickerRoot.style.pointerEvents = 'none'; var elem = document.elementFromPoint(x, y); - if ( - elem === document.body && !includeBody || - elem === document.documentElement - ) { + if ( elem === document.body || elem === document.documentElement ) { elem = null; } pickerRoot.style.pointerEvents = ''; @@ -1027,7 +1039,7 @@ var onSvgHovered = (function() { var onSvgClicked = function(ev) { // https://github.com/chrisaljoudi/uBlock/issues/810#issuecomment-74600694 // Unpause picker if user click outside dialog - if ( dialog.parentNode.classList.contains('paused') ) { + if ( pickerBody.classList.contains('paused') ) { unpausePicker(); return; } @@ -1067,14 +1079,15 @@ var onScrolled = function() { /******************************************************************************/ var pausePicker = function() { - dialog.parentNode.classList.add('paused'); + pickerBody.classList.add('paused'); svgListening(false); }; /******************************************************************************/ var unpausePicker = function() { - dialog.parentNode.classList.remove('paused'); + unpreview(); + pickerBody.classList.remove('paused'); svgListening(true); }; @@ -1085,6 +1098,9 @@ var unpausePicker = function() { var stopPicker = function() { targetElements = []; + candidateElements = []; + bestCandidateFilter = null; + previewedElements = []; if ( pickerRoot === null ) { return; @@ -1100,6 +1116,7 @@ var stopPicker = function() { pickerRoot.parentNode.removeChild(pickerRoot); pickerRoot.onload = null; pickerRoot = + pickerBody = dialog = svgRoot = svgOcean = svgIslands = taCandidate = null; @@ -1127,15 +1144,16 @@ var startPicker = function(details) { frameDoc.documentElement ); - frameDoc.body.setAttribute('lang', navigator.language); + pickerBody = frameDoc.body; + pickerBody.setAttribute('lang', navigator.language); - dialog = frameDoc.body.querySelector('aside'); + dialog = pickerBody.querySelector('aside'); dialog.addEventListener('click', onDialogClicked); taCandidate = dialog.querySelector('textarea'); taCandidate.addEventListener('input', onCandidateChanged); - svgRoot = frameDoc.body.querySelector('svg'); + svgRoot = pickerBody.querySelector('svg'); svgOcean = svgRoot.firstChild; svgIslands = svgRoot.lastChild; svgRoot.addEventListener('click', onSvgClicked);