diff --git a/src/css/epicker-ui.css b/src/css/epicker-ui.css index 0ff6eecd5..983cccb7b 100644 --- a/src/css/epicker-ui.css +++ b/src/css/epicker-ui.css @@ -13,7 +13,7 @@ html#ublock0-epicker, outline: none; } #ublock0-epicker aside { - background-color: #eee; + background-color: var(--default-surface); border: 1px solid #aaa; bottom: 4px; box-sizing: border-box; @@ -45,33 +45,30 @@ html#ublock0-epicker, margin: 0.25em 0 0 0; } #ublock0-epicker button { - background-color: #ccc; - border: 1px solid #aaa; + background-color: var(--button-surface); + border: 0; border-radius: 3px; box-sizing: border-box; box-shadow: none; - color: #000; + color: var(--button-ink); cursor: pointer; - opacity: 0.7; - padding: 4px 6px; + padding: 0.4em 0.8em; } #ublock0-epicker button:disabled { - color: #999; - background-color: #ccc; + color: var(--button-disabled-ink); + background-color: var(--button-disabled-surface); } #ublock0-epicker button:not(:disabled):hover { - opacity: 1; + background-color: var(--button-surface-hover); } #ublock0-epicker #create:not(:disabled) { - background-color: hsl(36, 100%, 83%); - border-color: hsl(36, 50%, 60%); + background-color: var(--button-important-surface); } -#ublock0-epicker #preview { - float: left; +#ublock0-epicker #create:not(:disabled):hover { + background-color: var(--button-important-surface-hover); } #ublock0-epicker.preview #preview { - background-color: hsl(204, 100%, 83%); - border-color: hsl(204, 50%, 60%); + background-color: var(--button-active-surface); } #ublock0-epicker section { border: 0; @@ -87,7 +84,7 @@ html#ublock0-epicker, #ublock0-epicker section.invalidFilter > div:first-child { border-color: red; } -#ublock0-epicker section > div:first-child > textarea { +#ublock0-epicker section textarea { background-color: #fff; border: none; box-sizing: border-box; @@ -102,14 +99,93 @@ html#ublock0-epicker, width: 100%; word-break: break-all; } -#ublock0-epicker #resultsetCount { - background-color: #aaa; +#ublock0-epicker section textarea + div { + background-color: transparent; bottom: 0; - color: white; - padding: 2px 4px; + display: flex; + left: 0; + pointer-events: none; position: absolute; right: 0; } +#resultsetModifiers { + align-items: flex-end; + display: inline-flex; + flex-grow: 1; + justify-content: center; + } +#resultsetSpecificity { + display: inline-flex; + pointer-events: auto; + position: relative; + } +#resultsetSpecificity.hide { + display: none; + } +#resultsetSpecificity [data-specificity] { + background-color: var(--button-surface); + border: 0; + border-left: 1px solid white; + display: inline-block; + height: 1.2em; + width: 1.5em; + } +#resultsetSpecificity[data-specificity="0"] [data-specificity="0"], +#resultsetSpecificity[data-specificity="1"] [data-specificity="0"], +#resultsetSpecificity[data-specificity="1"] [data-specificity="1"], +#resultsetSpecificity[data-specificity="2"] [data-specificity="0"], +#resultsetSpecificity[data-specificity="2"] [data-specificity="1"], +#resultsetSpecificity[data-specificity="2"] [data-specificity="2"], +#resultsetSpecificity[data-specificity="3"] [data-specificity="0"], +#resultsetSpecificity[data-specificity="3"] [data-specificity="1"], +#resultsetSpecificity[data-specificity="3"] [data-specificity="2"], +#resultsetSpecificity[data-specificity="3"] [data-specificity="3"], +#resultsetSpecificity[data-specificity="4"] [data-specificity="0"], +#resultsetSpecificity[data-specificity="4"] [data-specificity="1"], +#resultsetSpecificity[data-specificity="4"] [data-specificity="2"], +#resultsetSpecificity[data-specificity="4"] [data-specificity="3"], +#resultsetSpecificity[data-specificity="4"] [data-specificity="4"], +#resultsetSpecificity[data-specificity="5"] [data-specificity="0"], +#resultsetSpecificity[data-specificity="5"] [data-specificity="1"], +#resultsetSpecificity[data-specificity="5"] [data-specificity="2"], +#resultsetSpecificity[data-specificity="5"] [data-specificity="3"], +#resultsetSpecificity[data-specificity="5"] [data-specificity="4"], +#resultsetSpecificity[data-specificity="5"] [data-specificity="5"], +#resultsetSpecificity[data-specificity="6"] [data-specificity="0"], +#resultsetSpecificity[data-specificity="6"] [data-specificity="1"], +#resultsetSpecificity[data-specificity="6"] [data-specificity="2"], +#resultsetSpecificity[data-specificity="6"] [data-specificity="3"], +#resultsetSpecificity[data-specificity="6"] [data-specificity="4"], +#resultsetSpecificity[data-specificity="6"] [data-specificity="5"], +#resultsetSpecificity[data-specificity="6"] [data-specificity="6"], +#resultsetSpecificity[data-specificity="7"] [data-specificity="0"], +#resultsetSpecificity[data-specificity="7"] [data-specificity="1"], +#resultsetSpecificity[data-specificity="7"] [data-specificity="2"], +#resultsetSpecificity[data-specificity="7"] [data-specificity="3"], +#resultsetSpecificity[data-specificity="7"] [data-specificity="4"], +#resultsetSpecificity[data-specificity="7"] [data-specificity="5"], +#resultsetSpecificity[data-specificity="7"] [data-specificity="6"], +#resultsetSpecificity[data-specificity="7"] [data-specificity="7"] { + background-color: var(--button-active-surface); + } +#resultsetSpecificity input { + box-sizing: border-box; + height: 100%; + left: 0; + margin: 0; + opacity: 0; + padding: 0; + position: absolute; + top: 0; + width: 100%; + } +#resultsetCount { + background-color: #aaa; + color: white; + min-width: 2.2em; + padding: 2px 0; + text-align: center; + } #ublock0-epicker section.invalidFilter #resultsetCount { background-color: red; } diff --git a/src/js/epicker-ui.js b/src/js/epicker-ui.js index d7fd9e3eb..295974d9f 100644 --- a/src/js/epicker-ui.js +++ b/src/js/epicker-ui.js @@ -58,6 +58,11 @@ let filterHostname = ''; let filterOrigin = ''; let resultsetOpt; +let netFilterCandidates = []; +let cosmeticFilterCandidates = []; +let computedCandidateSlot = 0; +let computedCandidate = ''; + /******************************************************************************/ const filterFromTextarea = function() { @@ -110,6 +115,9 @@ const candidateFromFilterChoice = function(filterChoice) { elem.classList.remove('active'); } + computedCandidateSlot = slot; + computedCandidate = ''; + if ( filter === undefined ) { return ''; } // For net filters there no such thing as a path @@ -142,35 +150,82 @@ const candidateFromFilterChoice = function(filterChoice) { return filter; } + // TODO: Maybe add another step to remove attribute values? + const specificity = [ + 0b0000, // remove hierarchy; remove id, nth-of-type, attribute values + 0b0010, // remove hierarchy; remove id, nth-of-type + 0b0011, // remove hierarchy + 0b1000, // trim hierarchy; remove id, nth-of-type, attribute values + 0b1010, // trim hierarchy; remove id, nth-of-type + 0b1100, // remove id, nth-of-type, attribute values + 0b1110, // remove id, nth-of-type + 0b1111, // keep all = most specific + ][ + parseInt( + $id('resultsetSpecificity').getAttribute('data-specificity'), + 10 + ) + ]; + // Return path: the target element, then all siblings prepended - let selector = '', joiner = ''; - for ( ; slot < filters.length; slot++ ) { - filter = filters[slot]; + const paths = []; + for ( let i = slot; i < filters.length; i++ ) { + filter = filters[i].slice(2); + // Remove id, nth-of-type + // https://github.com/uBlockOrigin/uBlock-issues/issues/162 + // Mind escaped periods: they do not denote a class identifier. + if ( (specificity & 0b0001) === 0 ) { + filter = filter.replace(/:nth-of-type\(\d+\)/, ''); + if ( + filter.charAt(0) === '#' && ( + (specificity & 0b1000) === 0 || i === slot + ) + ) { + const pos = filter.search(/[^\\]\./); + if ( pos !== -1 ) { + filter = filter.slice(pos + 1); + } + } + } + // Remove attribute values. + if ( (specificity & 0b0010) === 0 ) { + const match = /^\[([^^=]+)\^?=.+\]$/.exec(filter); + if ( match !== null ) { + filter = `[${match[1]}]`; + } + } // Remove all classes when an id exists. // https://github.com/uBlockOrigin/uBlock-issues/issues/162 // Mind escaped periods: they do not denote a class identifier. - if ( filter.charAt(2) === '#' ) { + if ( filter.charAt(0) === '#' ) { filter = filter.replace(/([^\\])\..+$/, '$1'); } - selector = filter.slice(2) + joiner + selector; + if ( paths.length !== 0 ) { + filter += ' > '; + } + paths.unshift(filter); // Stop at any element with an id: these are unique in a web page - if ( filter.startsWith('###') ) { break; } - // Stop if current selector matches only one element on the page - if ( document.querySelectorAll(selector).length === 1 ) { break; } - joiner = ' > '; + if ( (specificity & 0b1000) === 0 || filter.startsWith('#') ) { break; } } - // https://github.com/gorhill/uBlock/issues/2519 - // https://github.com/uBlockOrigin/uBlock-issues/issues/17 - if ( - slot === filters.length && - selector.startsWith('body > ') === false && - document.querySelectorAll(selector).length > 1 - ) { - selector = 'body > ' + selector; + // Trim hierarchy: remove generic elements from path + if ( (specificity & 0b1100) === 0b1000 ) { + let i = 0; + while ( i < paths.length - 1 ) { + if ( /^[a-z0-9]+ > $/.test(paths[i+1]) ) { + if ( paths[i].endsWith(' > ') ) { + paths[i] = paths[i].slice(0, -2); + } + paths.splice(i + 1, 1); + } else { + i += 1; + } + } } - return '##' + selector; + computedCandidate = `##${paths.join('')}`; + + return computedCandidate; }; /******************************************************************************/ @@ -305,6 +360,10 @@ const onCandidateChanged = function() { $id('resultsetCount').textContent = 'E'; $id('create').setAttribute('disabled', ''); } + $id('resultsetSpecificity').classList.toggle( + 'hide', + taCandidate.value === '' || taCandidate.value !== computedCandidate + ); vAPI.MessagingConnection.sendTo(epickerConnectionId, { what: 'dialogSetFilter', filter, @@ -362,6 +421,20 @@ const onQuitClicked = function() { /******************************************************************************/ +const onSpecificityChanged = function(ev) { + const { target } = ev; + $id('resultsetSpecificity').setAttribute('data-specificity', target.value); + if ( taCandidate.value === computedCandidate ) { + taCandidate.value = candidateFromFilterChoice({ + filters: cosmeticFilterCandidates, + slot: computedCandidateSlot, + }); + onCandidateChanged(); + } +}; + +/******************************************************************************/ + const onCandidateClicked = function(ev) { let li = ev.target.closest('li'); if ( li === null ) { return; } @@ -370,7 +443,6 @@ const onCandidateClicked = function(ev) { const choice = { filters: Array.from(ul.querySelectorAll('li')).map(a => a.textContent), slot: 0, - broad: ev.ctrlKey || ev.metaKey }; while ( li.previousElementSibling !== null ) { li = li.previousElementSibling; @@ -511,10 +583,37 @@ const eatEvent = function(ev) { /******************************************************************************/ +// Create lists of candidate filters. This takes into account whether the +// current mode is narrow or broad. + +const populateCandidates = function(candidates, selector) { + + const root = dialog.querySelector(selector); + const ul = root.querySelector('ul'); + while ( ul.firstChild !== null ) { + ul.firstChild.remove(); + } + for ( let i = 0; i < candidates.length; i++ ) { + const li = document.createElement('li'); + li.textContent = candidates[i]; + ul.appendChild(li); + } + if ( candidates.length !== 0 ) { + root.style.removeProperty('display'); + } else { + root.style.setProperty('display', 'none', 'important'); + } +}; + +/******************************************************************************/ + const showDialog = function(details) { pausePicker(); - const { netFilters, cosmeticFilters, filter, options = {} } = details; + const { netFilters, cosmeticFilters, filter } = details; + + netFilterCandidates = netFilters; + cosmeticFilterCandidates = cosmeticFilters; // https://github.com/gorhill/uBlock/issues/738 // Trim dots. @@ -524,27 +623,8 @@ const showDialog = function(details) { } filterOrigin = details.origin; - // Create lists of candidate filters - const populate = function(src, des) { - const root = dialog.querySelector(des); - const ul = root.querySelector('ul'); - while ( ul.firstChild !== null ) { - ul.firstChild.remove(); - } - for ( let i = 0; i < src.length; i++ ) { - const li = document.createElement('li'); - li.textContent = src[i]; - ul.appendChild(li); - } - if ( src.length !== 0 ) { - root.style.removeProperty('display'); - } else { - root.style.setProperty('display', 'none', 'important'); - } - }; - - populate(netFilters, '#netFilters'); - populate(cosmeticFilters, '#cosmeticFilters'); + populateCandidates(netFilters, '#netFilters'); + populateCandidates(cosmeticFilters, '#cosmeticFilters'); dialog.querySelector('ul').style.display = netFilters.length || cosmeticFilters.length ? '' : 'none'; @@ -566,7 +646,6 @@ const showDialog = function(details) { const filterChoice = { filters: filter.filters, slot: filter.slot, - broad: options.broad || false }; taCandidate.value = candidateFromFilterChoice(filterChoice); @@ -609,9 +688,10 @@ const startPicker = function() { $id('create').addEventListener('click', onCreateClicked); $id('pick').addEventListener('click', onPickClicked); $id('quit').addEventListener('click', onQuitClicked); - $id('candidateFilters').addEventListener('click', onCandidateClicked); $id('toolbar').addEventListener('mousedown', onStartMoving); $id('toolbar').addEventListener('touchstart', onStartMoving); + $id('candidateFilters').addEventListener('click', onCandidateClicked); + $stor('#resultsetSpecificity input').addEventListener('input', onSpecificityChanged); staticFilteringParser = new vAPI.StaticFilteringParser({ interactive: true }); }; diff --git a/src/js/scriptlets/epicker.js b/src/js/scriptlets/epicker.js index 10a9ff920..b32488faa 100644 --- a/src/js/scriptlets/epicker.js +++ b/src/js/scriptlets/epicker.js @@ -517,6 +517,9 @@ const filtersFrom = function(x, y) { } // Cosmetic filter candidates from ancestors. + // https://github.com/gorhill/uBlock/issues/2519 + // https://github.com/uBlockOrigin/uBlock-issues/issues/17 + // Prepend `body` if full selector is ambiguous. let elem = first; while ( elem && elem !== document.body ) { cosmeticFilterFromElement(elem); @@ -526,10 +529,11 @@ const filtersFrom = function(x, y) { // uses `nth-of-type`. let i = cosmeticFilterCandidates.length; if ( i !== 0 ) { - let selector = cosmeticFilterCandidates[i-1]; + const selector = cosmeticFilterCandidates[i-1]; if ( selector.indexOf(':nth-of-type(') !== -1 && - safeQuerySelectorAll(document.body, selector).length > 1 + safeQuerySelectorAll(document.body, selector).length > 1 || + safeQuerySelectorAll(document, cosmeticFilterCandidates.join(' > ')).length > 1 ) { cosmeticFilterCandidates.push('##body'); } @@ -1079,6 +1083,9 @@ const onDialogMessage = function(msg) { break; case 'togglePreview': filterToDOMInterface.preview(msg.state); + if ( msg.state === false ) { + highlightElements(targetElements, true); + } break; default: break; diff --git a/src/web_accessible_resources/epicker-ui.html b/src/web_accessible_resources/epicker-ui.html index b1f4b4862..bef9c9123 100644 --- a/src/web_accessible_resources/epicker-ui.html +++ b/src/web_accessible_resources/epicker-ui.html @@ -4,6 +4,7 @@ uBlock Origin Element Picker + @@ -12,7 +13,20 @@
-
+
+ + + + + + + + + + + + +
@@ -27,10 +41,11 @@