Isolate element picker dialog from page content world

Related issues:
- https://github.com/gorhill/uBlock/issues/3497
- https://github.com/uBlockOrigin/uBlock-issues/issues/1215

To solve above issues, the element picker's dialog is now
isolated from the page content in which it is embedded.

The highly interactive, mouse-driven part of the element
picker is still visible by the page content.
This commit is contained in:
Raymond Hill 2020-09-01 12:32:12 -04:00
parent 43dba2bd0e
commit 9eb455ab5e
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
9 changed files with 1043 additions and 819 deletions

View file

@ -102,8 +102,14 @@ vAPI.messaging = {
},
disconnectListenerBound: null,
// 2020-09-01:
// In Firefox, `details instanceof Object` resolves to `false` despite
// `details` being a valid object. Consequently, falling back to use
// `typeof details`.
// This is an issue which surfaced when the element picker code was
// revisited to isolate the picker dialog DOM from the page DOM.
messageListener: function(details) {
if ( details instanceof Object === false ) { return; }
if ( typeof details !== 'object' || details === null ) { return; }
// Response to specific message previously sent
if ( details.msgId !== undefined ) {

184
src/css/epicker-dialog.css Normal file
View file

@ -0,0 +1,184 @@
html#ublock0-epicker,
#ublock0-epicker body {
background: transparent;
color: black;
cursor: not-allowed;
font: 12px sans-serif;
height: 100vh;
margin: 0;
overflow: hidden;
width: 100vw;
}
#ublock0-epicker :focus {
outline: none;
}
#ublock0-epicker ul,
#ublock0-epicker li,
#ublock0-epicker div {
display: block;
}
#ublock0-epicker #toolbar {
cursor: grab;
display: flex;
justify-content: space-between;
}
#ublock0-epicker aside.moving #toolbar {
cursor: grabbing;
}
#ublock0-epicker ul {
margin: 0.25em 0 0 0;
}
#ublock0-epicker button {
background-color: #ccc;
border: 1px solid #aaa;
border-radius: 3px;
box-sizing: border-box;
box-shadow: none;
color: #000;
cursor: pointer;
opacity: 0.7;
padding: 4px 6px;
}
#ublock0-epicker button:disabled {
color: #999;
background-color: #ccc;
}
#ublock0-epicker button:not(:disabled):hover {
opacity: 1;
}
#ublock0-epicker #create:not(:disabled) {
background-color: hsl(36, 100%, 83%);
border-color: hsl(36, 50%, 60%);
}
#ublock0-epicker #preview {
float: left;
}
#ublock0-epicker body.preview #preview {
background-color: hsl(204, 100%, 83%);
border-color: hsl(204, 50%, 60%);
}
#ublock0-epicker section {
border: 0;
box-sizing: border-box;
display: inline-block;
width: 100%;
}
#ublock0-epicker section > div:first-child {
border: 1px solid #aaa;
margin: 0;
position: relative;
}
#ublock0-epicker section.invalidFilter > div:first-child {
border-color: red;
}
#ublock0-epicker section > div:first-child > textarea {
background-color: #fff;
border: none;
box-sizing: border-box;
color: #000;
font: 11px monospace;
height: 8em;
margin: 0;
overflow: hidden;
overflow-y: auto;
padding: 2px;
resize: none;
width: 100%;
word-break: break-all;
}
#ublock0-epicker #resultsetCount {
background-color: #aaa;
bottom: 0;
color: white;
padding: 2px 4px;
position: absolute;
right: 0;
}
#ublock0-epicker section.invalidFilter #resultsetCount {
background-color: red;
}
#ublock0-epicker section > div:first-child + div {
direction: ltr;
margin: 2px 0;
text-align: right;
}
#ublock0-epicker ul {
padding: 0;
list-style-type: none;
text-align: left;
overflow: hidden;
}
#ublock0-epicker #candidateFilters {
max-height: 16em;
overflow-y: auto;
}
#ublock0-epicker #candidateFilters > li:first-of-type {
margin-bottom: 0.5em;
}
#ublock0-epicker .changeFilter > li > span:nth-of-type(1) {
font-weight: bold;
}
#ublock0-epicker .changeFilter > li > span:nth-of-type(2) {
font-size: smaller;
color: gray;
}
#ublock0-epicker #candidateFilters .changeFilter {
list-style-type: none;
margin: 0 0 0 1em;
overflow: hidden;
text-align: left;
}
#ublock0-epicker #candidateFilters .changeFilter li {
border: 1px solid transparent;
cursor: pointer;
direction: ltr;
font: 11px monospace;
white-space: nowrap;
}
#ublock0-epicker #candidateFilters .changeFilter li.active {
border: 1px dotted gray;
}
#ublock0-epicker #candidateFilters .changeFilter li:hover {
background-color: white;
}
#ublock0-epicker aside {
background-color: #eee;
border: 1px solid #aaa;
bottom: 4px;
box-sizing: border-box;
cursor: default;
min-width: 24em;
padding: 4px;
position: fixed;
right: 4px;
width: calc(40% - 4px);
}
/**
https://github.com/gorhill/uBlock/issues/3449
https://github.com/uBlockOrigin/uBlock-issues/issues/55
**/
@keyframes startDialog {
0% { opacity: 1.0; }
60% { opacity: 1.0; }
100% { opacity: 0.1; }
}
#ublock0-epicker body.paused > aside {
opacity: 0.1;
visibility: visible;
z-index: 100;
}
#ublock0-epicker body.paused > aside:not(:hover):not(.show) {
animation-duration: 1.6s;
animation-name: startDialog;
animation-timing-function: linear;
}
#ublock0-epicker body.paused > aside:hover {
opacity: 1;
}
#ublock0-epicker body.paused > aside.show {
opacity: 1;
}
#ublock0-epicker body.paused > aside.hide {
opacity: 0.1;
}

60
src/css/epicker.css Normal file
View file

@ -0,0 +1,60 @@
html#ublock0-epicker,
#ublock0-epicker body {
background: transparent !important;
box-sizing: border-box !important;
color: black !important;
font: 12px sans-serif !important;
height: 100vh !important;
margin: 0 !important;
overflow: hidden !important;
position: fixed !important;
width: 100vw !important;
}
#ublock0-epicker :focus {
outline: none !important;
}
#ublock0-epicker svg {
cursor: crosshair !important;
box-sizing: border-box;
height: 100% !important;
left: 0 !important;
position: absolute !important;
top: 0 !important;
width: 100% !important;
}
#ublock0-epicker .paused > svg {
cursor: not-allowed !important;
}
#ublock0-epicker svg > path:first-child {
fill: rgba(0,0,0,0.5) !important;
fill-rule: evenodd !important;
}
#ublock0-epicker svg > path + path {
stroke: #F00 !important;
stroke-width: 0.5px !important;
fill: rgba(255,63,63,0.20) !important;
}
#ublock0-epicker body.zap svg > path + path {
stroke: #FF0 !important;
stroke-width: 0.5px !important;
fill: rgba(255,255,63,0.20) !important;
}
#ublock0-epicker body.preview svg > path {
fill: rgba(0,0,0,0.10) !important;
}
#ublock0-epicker body.preview svg > path + path {
stroke: none !important;
}
#ublock0-epicker body > iframe {
border: 0 !important;
box-sizing: border-box !important;
display: none !important;
height: 100% !important;
left: 0 !important;
position: absolute !important;
top: 0 !important;
width: 100% !important;
}
#ublock0-epicker body.paused > iframe {
display: initial !important;
}

View file

@ -1,250 +0,0 @@
<head>
<meta charset="utf-8">
<title>uBlock Origin Element Picker</title>
<style>
html#ublock0-epicker,
#ublock0-epicker body {
background: transparent !important;
color: black !important;
font: 12px sans-serif !important;
height: 100% !important;
margin: 0 !important;
overflow: hidden !important;
width: 100% !important;
}
#ublock0-epicker :focus {
outline: none !important;
}
#ublock0-epicker ul,
#ublock0-epicker li,
#ublock0-epicker div {
display: block !important;
}
#ublock0-epicker #toolbar {
cursor: grab;
display: flex !important;
justify-content: space-between;
}
#ublock0-epicker aside.moving #toolbar {
cursor: grabbing;
}
#ublock0-epicker ul {
margin: 0.25em 0 0 0 !important;
}
#ublock0-epicker button {
background-color: #ccc !important;
border: 1px solid #aaa !important;
border-radius: 3px !important;
box-sizing: border-box !important;
box-shadow: none !important;
color: #000 !important;
cursor: pointer !important;
opacity: 0.7 !important;
padding: 4px 6px !important;
}
#ublock0-epicker button:disabled {
color: #999 !important;
background-color: #ccc !important;
}
#ublock0-epicker button:not(:disabled):hover {
opacity: 1 !important;
}
#ublock0-epicker #create:not(:disabled) {
background-color: hsl(36, 100%, 83%) !important;
border-color: hsl(36, 50%, 60%) !important;
}
#ublock0-epicker #preview {
float: left !important;
}
#ublock0-epicker body.preview #preview {
background-color: hsl(204, 100%, 83%) !important;
border-color: hsl(204, 50%, 60%) !important;
}
#ublock0-epicker section {
border: 0 !important;
box-sizing: border-box !important;
display: inline-block !important;
width: 100% !important;
}
#ublock0-epicker section > div:first-child {
border: 1px solid #aaa !important;
margin: 0 !important;
position: relative !important;
}
#ublock0-epicker section.invalidFilter > div:first-child {
border-color: red !important;
}
#ublock0-epicker section > div:first-child > textarea {
background-color: #fff !important;
border: none !important;
box-sizing: border-box !important;
color: #000 !important;
font: 11px monospace !important;
height: 8em !important;
margin: 0 !important;
overflow: hidden !important;
overflow-y: auto !important;
padding: 2px !important;
resize: none !important;
width: 100% !important;
word-break: break-all !important;
}
#ublock0-epicker #resultsetCount {
background-color: #aaa !important;
bottom: 0 !important;
color: white !important;
padding: 2px 4px !important;
position: absolute !important;
right: 0 !important;
}
#ublock0-epicker section.invalidFilter #resultsetCount {
background-color: red !important;
}
#ublock0-epicker section > div:first-child + div {
direction: ltr !important;
margin: 2px 0 !important;
text-align: right !important;
}
#ublock0-epicker ul {
padding: 0 !important;
list-style-type: none !important;
text-align: left !important;
overflow: hidden !important;
}
#ublock0-epicker #candidateFilters {
max-height: 16em !important;
overflow-y: auto !important;
}
#ublock0-epicker #candidateFilters > li:first-of-type {
margin-bottom: 0.5em !important;
}
#ublock0-epicker .changeFilter > li > span:nth-of-type(1) {
font-weight: bold !important;
}
#ublock0-epicker .changeFilter > li > span:nth-of-type(2) {
font-size: smaller !important;
color: gray !important;
}
#ublock0-epicker #candidateFilters .changeFilter {
list-style-type: none !important;
margin: 0 0 0 1em !important;
overflow: hidden !important;
text-align: left !important;
}
#ublock0-epicker #candidateFilters .changeFilter li {
border: 1px solid transparent;
cursor: pointer !important;
direction: ltr !important;
font: 11px monospace !important;
white-space: nowrap !important;
}
#ublock0-epicker #candidateFilters .changeFilter li.active {
border: 1px dotted gray;
}
#ublock0-epicker #candidateFilters .changeFilter li:hover {
background-color: white !important;
}
#ublock0-epicker svg {
position: fixed !important;
top: 0 !important;
left: 0 !important;
cursor: crosshair !important;
width: 100% !important;
height: 100% !important;
}
#ublock0-epicker .paused > svg {
cursor: not-allowed !important;
}
#ublock0-epicker svg > path:first-child {
fill: rgba(0,0,0,0.5) !important;
fill-rule: evenodd !important;
}
#ublock0-epicker svg > path + path {
stroke: #F00 !important;
stroke-width: 0.5px !important;
fill: rgba(255,63,63,0.20) !important;
}
#ublock0-epicker body.zap svg > path + path {
stroke: #FF0 !important;
stroke-width: 0.5px !important;
fill: rgba(255,255,63,0.20) !important;
}
#ublock0-epicker body.preview svg > path {
fill: rgba(0,0,0,0.10) !important;
}
#ublock0-epicker body.preview svg > path + path {
stroke: none !important;
}
#ublock0-epicker aside {
background-color: #eee !important;
border: 1px solid #aaa !important;
bottom: 4px !important;
box-sizing: border-box !important;
min-width: 24em !important;
padding: 4px !important;
position: fixed !important;
right: 4px !important;
visibility: hidden !important;
width: calc(40% - 4px) !important;
}
#ublock0-epicker body.paused > aside {
opacity: 0.1;
visibility: visible !important;
z-index: 100 !important;
}
/**
https://github.com/gorhill/uBlock/issues/3449
https://github.com/uBlockOrigin/uBlock-issues/issues/55
**/
@keyframes startDialog {
0% { opacity: 1.0; }
60% { opacity: 1.0; }
100% { opacity: 0.1; }
}
#ublock0-epicker body.paused > aside:not(:hover):not(.show) {
animation-duration: 1.6s !important;
animation-name: startDialog !important;
animation-timing-function: linear !important;
}
#ublock0-epicker body.paused > aside:hover {
opacity: 1 !important;
}
#ublock0-epicker body.paused > aside.show {
opacity: 1 !important;
}
#ublock0-epicker body.paused > aside.hide {
opacity: 0.1 !important;
}
</style>
</head>
<body direction="{{bidi_dir}}">
<svg><path d></path><path d></path></svg>
<aside>
<section>
<div>
<textarea lang="en" dir="ltr" spellcheck="false"></textarea>
<div id="resultsetCount"></div>
</div>
<div id="toolbar">
<div>
<button id="preview" type="button">{{preview}}</button>
</div>
<div>
<button id="create" type="button" disabled>{{create}}</button>
<button id="pick" type="button">{{pick}}</button>
<button id="quit" type="button">{{quit}}</button>
</div>
</div>
</section>
<ul id="candidateFilters">
<li id="netFilters">
<span>{{netFilters}}</span><ul lang="en" class="changeFilter"></ul>
</li>
<li id="cosmeticFilters">
<span>{{cosmeticFilters}}</span> <span>{{cosmeticFiltersHint}}</span>
<ul lang="en" class="changeFilter"></ul>
</li>
</ul>
</aside>
</body>

499
src/js/epicker-dialog.js Normal file
View file

@ -0,0 +1,499 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-present 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
*/
'use strict';
/******************************************************************************/
/******************************************************************************/
(( ) => {
/******************************************************************************/
if ( typeof vAPI !== 'object' ) { return; }
const epickerId = (( ) => {
const url = new URL(self.location.href);
return url.searchParams.get('epid');
})();
if ( epickerId === null ) { return; }
let epickerConnectionId;
let filterHostname = '';
let filterOrigin = '';
let filterResultset = [];
/******************************************************************************/
const $id = id => document.getElementById(id);
const $stor = selector => document.querySelector(selector);
const $storAll = selector => document.querySelectorAll(selector);
/******************************************************************************/
const filterFromTextarea = function() {
const s = taCandidate.value.trim();
if ( s === '' ) { return ''; }
const pos = s.indexOf('\n');
const filter = pos === -1 ? s.trim() : s.slice(0, pos).trim();
staticFilteringParser.analyze(filter);
staticFilteringParser.analyzeExtra();
return staticFilteringParser.shouldDiscard() ? '!' : filter;
};
/******************************************************************************/
const userFilterFromCandidate = function(filter) {
if ( filter === '' || filter === '!' ) { return; }
// Cosmetic filter?
if ( filter.startsWith('##') ) {
return filterHostname + filter;
}
// Assume net filter
const opts = [];
// If no domain included in filter, we need domain option
if ( filter.startsWith('||') === false ) {
opts.push(`domain=${filterHostname}`);
}
if ( filterResultset.length !== 0 ) {
const item = filterResultset[0];
if ( item.opts ) {
opts.push(item.opts);
}
}
if ( opts.length ) {
filter += '$' + opts.join(',');
}
return filter;
};
/******************************************************************************/
const candidateFromFilterChoice = function(filterChoice) {
let { slot, filters } = filterChoice;
let filter = filters[slot];
// https://github.com/uBlockOrigin/uBlock-issues/issues/47
for ( const elem of $storAll('#candidateFilters li') ) {
elem.classList.remove('active');
}
if ( filter === undefined ) { return ''; }
// For net filters there no such thing as a path
if ( filter.startsWith('##') === false ) {
$stor(`#netFilters li:nth-of-type(${slot+1})`)
.classList.add('active');
return filter;
}
// At this point, we have a cosmetic filter
$stor(`#cosmeticFilters li:nth-of-type(${slot+1})`)
.classList.add('active');
// Modifier means "target broadly". Hence:
// - Do not compute exact path.
// - Discard narrowing directives.
// - Remove the id if one or more classes exist
// TODO: should remove tag name too? ¯\_(ツ)_/¯
if ( filterChoice.modifier ) {
filter = filter.replace(/:nth-of-type\(\d+\)/, '');
// https://github.com/uBlockOrigin/uBlock-issues/issues/162
// Mind escaped periods: they do not denote a class identifier.
if ( filter.charAt(2) === '#' ) {
const pos = filter.search(/[^\\]\./);
if ( pos !== -1 ) {
filter = '##' + filter.slice(pos + 1);
}
}
return filter;
}
// Return path: the target element, then all siblings prepended
let selector = '', joiner = '';
for ( ; slot < filters.length; slot++ ) {
filter = filters[slot];
// 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) === '#' ) {
filter = filter.replace(/([^\\])\..+$/, '$1');
}
selector = filter.slice(2) + joiner + selector;
// 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 = ' > ';
}
// 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;
}
return '##' + selector;
};
/******************************************************************************/
const onCandidateChanged = function() {
const filter = filterFromTextarea();
const bad = filter === '!';
$stor('section').classList.toggle('invalidFilter', bad);
$id('create').disabled = bad;
if ( bad ) {
$id('resultsetCount').textContent = 'E';
$id('create').setAttribute('disabled', '');
}
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
what: 'dialogSetFilter',
filter,
compiled: filter.startsWith('##')
? staticFilteringParser.result.compiled
: undefined,
});
};
/******************************************************************************/
const onPreviewClicked = function() {
const state = pickerBody.classList.toggle('preview');
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
what: 'dialogPreview',
state,
});
};
/******************************************************************************/
const onCreateClicked = function() {
const candidate = filterFromTextarea();
const filter = userFilterFromCandidate(candidate);
if ( filter !== undefined ) {
vAPI.messaging.send('elementPicker', {
what: 'createUserFilter',
autoComment: true,
filters: filter,
origin: filterOrigin,
pageDomain: filterHostname,
killCache: /^#[$?]?#/.test(candidate) === false,
});
}
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
what: 'dialogCreate',
filter: candidate,
compiled: candidate.startsWith('##')
? staticFilteringParser.result.compiled
: undefined,
});
};
/******************************************************************************/
const onPickClicked = function(ev) {
if (
(ev instanceof MouseEvent) &&
(ev.type === 'mousedown') &&
(ev.which !== 1 || ev.target !== document.body)
) {
return;
}
pickerBody.classList.remove('paused');
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
what: 'dialogPick'
});
};
/******************************************************************************/
const onQuitClicked = function() {
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
what: 'dialogQuit'
});
};
/******************************************************************************/
const onCandidateClicked = function(ev) {
let li = ev.target.closest('li');
const ul = li.closest('.changeFilter');
if ( ul === null ) { return; }
const choice = {
filters: Array.from(ul.querySelectorAll('li')).map(a => a.textContent),
slot: 0,
modifier: ev.ctrlKey || ev.metaKey
};
while ( li.previousElementSibling !== null ) {
li = li.previousElementSibling;
choice.slot += 1;
}
taCandidate.value = candidateFromFilterChoice(choice);
onCandidateChanged();
};
/******************************************************************************/
const onKeyPressed = function(ev) {
// Esc
if ( ev.key === 'Escape' || ev.which === 27 ) {
onQuitClicked();
return;
}
};
/******************************************************************************/
const onStartMoving = (( ) => {
let mx0 = 0, my0 = 0;
let mx1 = 0, my1 = 0;
let r0 = 0, b0 = 0;
let rMax = 0, bMax = 0;
let timer;
const move = ( ) => {
timer = undefined;
let r1 = Math.min(Math.max(r0 - mx1 + mx0, 4), rMax);
let b1 = Math.min(Math.max(b0 - my1 + my0, 4), bMax);
dialog.style.setProperty('right', `${r1}px`, 'important');
dialog.style.setProperty('bottom', `${b1}px`, 'important');
};
const moveAsync = ev => {
if ( ev.isTrusted === false ) { return; }
eatEvent(ev);
if ( timer !== undefined ) { return; }
mx1 = ev.pageX;
my1 = ev.pageY;
timer = self.requestAnimationFrame(move);
};
const stop = ev => {
if ( ev.isTrusted === false ) { return; }
if ( dialog.classList.contains('moving') === false ) { return; }
dialog.classList.remove('moving');
self.removeEventListener('mousemove', moveAsync, { capture: true });
self.removeEventListener('mouseup', stop, { capture: true, once: true });
eatEvent(ev);
};
return function(ev) {
if ( ev.isTrusted === false ) { return; }
const target = dialog.querySelector('#toolbar');
if ( ev.target !== target ) { return; }
if ( dialog.classList.contains('moving') ) { return; }
mx0 = ev.pageX; my0 = ev.pageY;
const style = self.getComputedStyle(dialog);
r0 = parseInt(style.right, 10);
b0 = parseInt(style.bottom, 10);
const rect = dialog.getBoundingClientRect();
rMax = pickerBody.clientWidth - 4 - rect.width ;
bMax = pickerBody.clientHeight - 4 - rect.height;
dialog.classList.add('moving');
self.addEventListener('mousemove', moveAsync, { capture: true });
self.addEventListener('mouseup', stop, { capture: true, once: true });
eatEvent(ev);
};
})();
/******************************************************************************/
const eatEvent = function(ev) {
ev.stopPropagation();
ev.preventDefault();
};
/******************************************************************************/
const showDialog = function(details) {
pickerBody.classList.add('paused');
const { netFilters, cosmeticFilters, filter, options } = details;
// https://github.com/gorhill/uBlock/issues/738
// Trim dots.
filterHostname = details.hostname;
if ( filterHostname.slice(-1) === '.' ) {
filterHostname = filterHostname.slice(0, -1);
}
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');
dialog.querySelector('ul').style.display =
netFilters.length || cosmeticFilters.length ? '' : 'none';
dialog.querySelector('#create').disabled = true;
// Auto-select a candidate filter
// 2020-09-01:
// In Firefox, `details instanceof Object` resolves to `false` despite
// `details` being a valid object. Consequently, falling back to use
// `typeof details`.
// This is an issue which surfaced when the element picker code was
// revisited to isolate the picker dialog DOM from the page DOM.
if ( typeof filter !== 'object' || filter === null ) {
taCandidate.value = '';
return;
}
const filterChoice = {
filters: filter.filters,
slot: filter.slot,
modifier: options.modifier || false
};
taCandidate.value = candidateFromFilterChoice(filterChoice);
onCandidateChanged();
};
/******************************************************************************/
// Let's have the element picker code flushed from memory when no longer
// in use: to ensure this, release all local references.
const stopPicker = function() {
vAPI.shutdown.remove(stopPicker);
};
/******************************************************************************/
const pickerBody = document.body;
const dialog = $stor('aside');
const taCandidate = $stor('textarea');
let staticFilteringParser;
/******************************************************************************/
const startDialog = function() {
dialog.addEventListener('click', eatEvent);
taCandidate.addEventListener('input', onCandidateChanged);
$stor('body').addEventListener('mousedown', onPickClicked);
$id('preview').addEventListener('click', onPreviewClicked);
$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);
self.addEventListener('keydown', onKeyPressed, true);
staticFilteringParser = new vAPI.StaticFilteringParser({ interactive: true });
};
/******************************************************************************/
const onPickerMessage = function(msg) {
switch ( msg.what ) {
case 'showDialog':
showDialog(msg);
break;
case 'filterResultset':
filterResultset = msg.resultset;
$id('resultsetCount').textContent = filterResultset.length;
if ( filterResultset.length !== 0 ) {
$id('create').removeAttribute('disabled');
} else {
$id('create').setAttribute('disabled', '');
}
break;
}
};
/******************************************************************************/
const onConnectionMessage = function(msg) {
switch ( msg.what ) {
case 'connectionBroken':
stopPicker();
break;
case 'connectionMessage':
onPickerMessage(msg.payload);
break;
case 'connectionAccepted':
epickerConnectionId = msg.id;
startDialog();
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
what: 'dialogInit',
});
break;
}
};
vAPI.MessagingConnection.connectTo(
`epickerDialog-${epickerId}`,
`epicker-${epickerId}`,
onConnectionMessage
);
/******************************************************************************/
})();
/*******************************************************************************
DO NOT:
- Remove the following code
- Add code beyond the following code
Reason:
- https://github.com/gorhill/uBlock/pull/3721
- uBO never uses the return value from injected content scripts
**/
void 0;

View file

@ -713,34 +713,19 @@ const onMessage = function(request, sender, callback) {
switch ( request.what ) {
case 'elementPickerArguments':
const xhr = new XMLHttpRequest();
xhr.open('GET', 'epicker.html', true);
xhr.open('GET', 'css/epicker.css', true);
xhr.overrideMimeType('text/html;charset=utf-8');
xhr.responseType = 'text';
xhr.onload = function() {
this.onload = null;
const i18n = {
bidi_dir: document.body.getAttribute('dir'),
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')
};
const reStrings = /\{\{(\w+)\}\}/g;
const replacer = function(a0, string) {
return i18n[string];
};
callback({
frameContent: this.responseText.replace(reStrings, replacer),
frameCSS: this.responseText,
target: µb.epickerArgs.target,
mouse: µb.epickerArgs.mouse,
zap: µb.epickerArgs.zap,
eprom: µb.epickerArgs.eprom,
dialogURL: vAPI.getURL(`/web_accessible_resources/epicker-dialog.html${vAPI.warSecret()}`),
});
µb.epickerArgs.target = '';
};
xhr.send();
@ -754,21 +739,6 @@ const onMessage = function(request, sender, callback) {
let response;
switch ( request.what ) {
case 'compileCosmeticFilterSelector': {
const parser = new vAPI.StaticFilteringParser();
parser.analyze(request.selector);
if ( (parser.flavorBits & parser.BITFlavorExtCosmetic) !== 0 ) {
response = parser.result.compiled;
}
break;
}
// https://github.com/gorhill/uBlock/issues/3497
// This needs to be removed once issue is fixed.
case 'createUserFilter':
µb.createUserFilters(request);
break;
case 'elementPickerEprom':
µb.epickerArgs.eprom = request;
break;

View file

@ -428,7 +428,7 @@ const matchBucket = function(url, hostname, bucket, start) {
});
await vAPI.tabs.executeScript(tabId, {
file: '/js/scriptlets/element-picker.js',
file: '/js/scriptlets/epicker.js',
runAt: 'document_end',
});

View file

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html id="ublock0-epicker">
<head>
<meta charset="utf-8">
<title>uBlock Origin Element Picker</title>
<link rel="stylesheet" href="../css/epicker-dialog.css">
</head>
<body>
<aside>
<section>
<div>
<textarea lang="en" dir="ltr" spellcheck="false"></textarea>
<div id="resultsetCount"></div>
</div>
<div id="toolbar">
<div>
<button id="preview" type="button" data-i18n="pickerPreview"></button>
</div>
<div>
<button id="create" type="button" disabled data-i18n="pickerCreate"></button>
<button id="pick" type="button" data-i18n="pickerPick"></button>
<button id="quit" type="button" data-i18n="pickerQuit"></button>
</div>
</div>
</section>
<ul id="candidateFilters">
<li id="netFilters">
<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>
<ul lang="en" class="changeFilter"></ul>
</li>
</ul>
</aside>
<script src="../js/vapi.js"></script>
<script src="../js/vapi-common.js"></script>
<script src="../js/vapi-client.js"></script>
<script src="../js/vapi-client-extra.js"></script>
<script src="../js/i18n.js"></script>
<script src="../js/epicker-dialog.js"></script>
<script src="../js/static-filtering-parser.js"></script>
</body>
</html>