Improve specificity slider in element picker

The specificity slider will now be more intuitive
by ordering candidates by match count from highest
match count to the left to the lowest match count
to the right.

Candidates with same match counts will be discarded
and replaced with the shortest candidate.
This commit is contained in:
Raymond Hill 2020-10-16 17:12:22 -04:00
parent 97bff47131
commit 4c5197322f
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
2 changed files with 123 additions and 87 deletions

View File

@ -63,6 +63,7 @@ let netFilterCandidates = [];
let cosmeticFilterCandidates = [];
let computedCandidateSlot = 0;
let computedCandidate = '';
let computedSpecificityCandidates = [];
let needBody = false;
/******************************************************************************/
@ -194,7 +195,13 @@ const candidateFromFilterChoice = function(filterChoice) {
$stor(`#cosmeticFilters li:nth-of-type(${slot+1})`)
.classList.add('active');
const specificity = [
return cosmeticCandidatesFromFilterChoice(filterChoice);
};
/******************************************************************************/
const cosmeticCandidatesFromFilterChoice = function(filterChoice) {
const specificities = [
0b0000, // remove hierarchy; remove id, nth-of-type, attribute values
0b0010, // remove hierarchy; remove id, nth-of-type
0b0011, // remove hierarchy
@ -203,89 +210,100 @@ const candidateFromFilterChoice = function(filterChoice) {
0b1100, // remove id, nth-of-type, attribute values
0b1110, // remove id, nth-of-type
0b1111, // keep all = most specific
][ parseInt($stor('#resultsetSpecificity input').value, 10) ];
];
// Return path: the target element, then all siblings prepended
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);
const candidates = [];
let { slot, filters } = filterChoice;
let filter = filters[slot];
for ( const specificity of specificities ) {
// Return path: the target element, then all siblings prepended
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(0) === '#' ) {
filter = filter.replace(/([^\\])\..+$/, '$1');
}
if ( paths.length !== 0 ) {
filter += ' > ';
}
paths.unshift(filter);
// Stop at any element with an id: these are unique in a web page
if ( (specificity & 0b1000) === 0 || filter.startsWith('#') ) {
break;
}
}
// 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;
}
}
}
// 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(0) === '#' ) {
filter = filter.replace(/([^\\])\..+$/, '$1');
}
if ( paths.length !== 0 ) {
filter += ' > ';
}
paths.unshift(filter);
// Stop at any element with an id: these are unique in a web page
if ( (specificity & 0b1000) === 0 || filter.startsWith('#') ) { break; }
}
// 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;
}
if (
needBody &&
paths.length !== 0 &&
paths[0].startsWith('#') === false &&
(specificity & 0b1100) !== 0
) {
paths.unshift('body > ');
}
}
if (
needBody &&
paths.length !== 0 &&
paths[0].startsWith('#') === false &&
(specificity & 0b1100) !== 0
) {
paths.unshift('body > ');
candidates.push(paths);
}
if ( paths.length === 0 ) { return ''; }
renderRange('resultsetDepth', slot, true);
renderRange('resultsetSpecificity');
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
what: 'optimizeCandidate',
paths,
what: 'optimizeCandidates',
candidates,
});
};
/******************************************************************************/
const onCandidateOptimized = function(details) {
const onCandidatesOptimized = function(details) {
$id('resultsetModifiers').classList.remove('hide');
computedCandidate = details.filter;
const i = parseInt($stor('#resultsetSpecificity input').value, 10);
computedSpecificityCandidates = details.candidates;
computedCandidate = computedSpecificityCandidates[i];
cmEditor.setValue(computedCandidate);
cmEditor.clearHistory();
onCandidateChanged();
@ -501,13 +519,11 @@ const onDepthChanged = function() {
/******************************************************************************/
const onSpecificityChanged = function() {
renderRange('resultsetSpecificity');
if ( rawFilterFromTextarea() !== computedCandidate ) { return; }
const text = candidateFromFilterChoice({
filters: cosmeticFilterCandidates,
slot: computedCandidateSlot,
});
if ( text === undefined ) { return; }
cmEditor.setValue(text);
const i = parseInt($stor('#resultsetSpecificity input').value, 10);
computedCandidate = computedSpecificityCandidates[i];
cmEditor.setValue(computedCandidate);
cmEditor.clearHistory();
onCandidateChanged();
};
@ -808,8 +824,8 @@ const quitPicker = function() {
const onPickerMessage = function(msg) {
switch ( msg.what ) {
case 'candidateOptimized':
onCandidateOptimized(msg);
case 'candidatesOptimized':
onCandidatesOptimized(msg);
break;
case 'showDialog':
showDialog(msg);

View File

@ -821,21 +821,41 @@ const filterToDOMInterface = (( ) => {
/******************************************************************************/
const onOptmizeCandidate = function(details) {
const { paths } = details;
let count = Number.MAX_SAFE_INTEGER;
let selector = '';
for ( let i = 0, n = paths.length; i < n; i++ ) {
const s = paths.slice(n - i - 1).join('');
const elems = document.querySelectorAll(s);
if ( elems.length < count ) {
selector = s;
count = elems.length;
const onOptmizeCandidates = function(details) {
const { candidates } = details;
const results = [];
for ( const paths of candidates ) {
let count = Number.MAX_SAFE_INTEGER;
let selector = '';
for ( let i = 0, n = paths.length; i < n; i++ ) {
const s = paths.slice(n - i - 1).join('');
const elems = document.querySelectorAll(s);
if ( elems.length < count ) {
selector = s;
count = elems.length;
}
}
results.push({ selector: `##${selector}`, count });
}
// Sort by most match count and shortest selector to least match count and
// longest selector.
results.sort((a, b) => {
const r = b.count - a.count;
if ( r !== 0 ) { return r; }
return a.selector.length - b.selector.length;
});
// Discard selectors with same match count as shorter ones.
for ( let i = 0; i < results.length - 1; i++ ) {
const a = results[i+0];
const b = results[i+1];
if ( b.count !== a.count ) { continue; }
if ( b.selector.length === a.selector.length ) { continue; }
b.selector = a.selector;
b.count = a.count;
}
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
what: 'candidateOptimized',
filter: `##${selector}`,
what: 'candidatesOptimized',
candidates: results.map(a => a.selector),
});
};
@ -1064,8 +1084,8 @@ const onDialogMessage = function(msg) {
highlightElements([], true);
}
break;
case 'optimizeCandidate':
onOptmizeCandidate(msg);
case 'optimizeCandidates':
onOptmizeCandidates(msg);
break;
case 'dialogCreate':
filterToDOMInterface.queryAll(msg);