1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-09-15 07:22:28 +02:00

Add element picker widget to control specificity

Related issue:
- https://github.com/uBlockOrigin/uBlock-issues/issues/851

The ctrl key is no longer used to adjust specificity of
a candidate filter.

A new widget has been added to adjust the specificity of
a candidate filter to various level. The widget will be
visible as long as the candidate filter matches one entry
in the list of suggested candidate cosmetic filters.
This commit is contained in:
Raymond Hill 2020-09-09 09:27:53 -04:00
parent 016a774780
commit 1268f0ae43
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
4 changed files with 247 additions and 69 deletions

View File

@ -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;
}

View File

@ -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 });
};

View File

@ -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;

View File

@ -4,6 +4,7 @@
<head>
<meta charset="utf-8">
<title>uBlock Origin Element Picker</title>
<link rel="stylesheet" href="../css/themes/default.css">
<link rel="stylesheet" href="../css/epicker-ui.css">
</head>
@ -12,7 +13,20 @@
<section>
<div>
<textarea lang="en" dir="ltr" spellcheck="false"></textarea>
<div id="resultsetCount"></div>
<div>
<span id="resultsetModifiers">
<span id="resultsetSpecificity" data-specificity="6">
<span data-specificity="0"></span>
<span data-specificity="1"></span>
<span data-specificity="2"></span>
<span data-specificity="3"></span>
<span data-specificity="4"></span>
<span data-specificity="5"></span>
<span data-specificity="6"></span>
<span data-specificity="7"></span>
<input type="range" min="0" max="7" value="6"></span>
</span>
<span id="resultsetCount"></span></div>
</div>
<div id="toolbar">
<div>
@ -27,10 +41,11 @@
</section>
<ul id="candidateFilters">
<li id="netFilters">
<span data-i18n="pickerNetFilters"></span><ul lang="en" class="changeFilter"></ul>
<span data-i18n="pickerNetFilters"></span>
<ul lang="en" class="changeFilter"></ul>
</li>
<li id="cosmeticFilters">
<span data-i18n="pickerCosmeticFilters"></span> <span data-i18n="pickerCosmeticFiltersHint"></span>
<li id="cosmeticFilters" data-specificity="3">
<span data-i18n="pickerCosmeticFilters"></span>
<ul lang="en" class="changeFilter"></ul>
</li>
</ul>