Isolate element picker's svg layers from page content

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

Related commit:
- 9eb455ab5e

In the previous commit, the element picker dialog was
isolated from the page content. This commit is to also
isolate the svg layers from the page content.

With this commit, there is no longer a need for an anonymous
iframe and the isolated world iframe is now directly
embedded in the page.

As a result, pages are now unable to interfere with any
of the element picker user interface. Pages can now only
see an iframe, but are unable to see the content of that
iframe. The styles applied to the iframe are from a user
stylesheet, so as to ensure pages can't override the
iframe's style properties set by uBO.
This commit is contained in:
Raymond Hill 2020-09-03 10:27:35 -04:00
parent c02bba3cfa
commit d23f9c6a8b
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
7 changed files with 615 additions and 612 deletions

View file

@ -153,6 +153,9 @@ vAPI.MessagingConnection = class {
listeners.add(listener); listeners.add(listener);
vAPI.messaging.getPort(); // Ensure a port instance exists vAPI.messaging.getPort(); // Ensure a port instance exists
} }
static removeListener(listener) {
listeners.delete(listener);
}
static connectTo(from, to, handler) { static connectTo(from, to, handler) {
const port = vAPI.messaging.getPort(); const port = vAPI.messaging.getPort();
if ( port === null ) { return; } if ( port === null ) { return; }

View file

@ -12,6 +12,22 @@ html#ublock0-epicker,
#ublock0-epicker :focus { #ublock0-epicker :focus {
outline: none; outline: none;
} }
#ublock0-epicker aside {
background-color: #eee;
border: 1px solid #aaa;
bottom: 4px;
box-sizing: border-box;
cursor: default;
display: none;
min-width: 24em;
padding: 4px;
position: fixed;
right: 4px;
width: calc(40% - 4px);
}
#ublock0-epicker.paused:not(.zap) aside {
display: block;
}
#ublock0-epicker ul, #ublock0-epicker ul,
#ublock0-epicker li, #ublock0-epicker li,
#ublock0-epicker div { #ublock0-epicker div {
@ -53,7 +69,7 @@ html#ublock0-epicker,
#ublock0-epicker #preview { #ublock0-epicker #preview {
float: left; float: left;
} }
#ublock0-epicker body.preview #preview { #ublock0-epicker.preview #preview {
background-color: hsl(204, 100%, 83%); background-color: hsl(204, 100%, 83%);
border-color: hsl(204, 50%, 60%); border-color: hsl(204, 50%, 60%);
} }
@ -141,18 +157,7 @@ html#ublock0-epicker,
#ublock0-epicker #candidateFilters .changeFilter li:hover { #ublock0-epicker #candidateFilters .changeFilter li:hover {
background-color: white; 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/gorhill/uBlock/issues/3449
https://github.com/uBlockOrigin/uBlock-issues/issues/55 https://github.com/uBlockOrigin/uBlock-issues/issues/55
@ -162,23 +167,55 @@ html#ublock0-epicker,
60% { opacity: 1.0; } 60% { opacity: 1.0; }
100% { opacity: 0.1; } 100% { opacity: 0.1; }
} }
#ublock0-epicker body.paused > aside { #ublock0-epicker.paused aside {
opacity: 0.1; opacity: 0.1;
visibility: visible; visibility: visible;
z-index: 100; z-index: 100;
} }
#ublock0-epicker body.paused > aside:not(:hover):not(.show) { #ublock0-epicker.paused:not(.show):not(.hide) aside:not(:hover) {
animation-duration: 1.6s; animation-duration: 1.6s;
animation-name: startDialog; animation-name: startDialog;
animation-timing-function: linear; animation-timing-function: linear;
} }
#ublock0-epicker body.paused > aside:hover { #ublock0-epicker.paused aside:hover {
opacity: 1; opacity: 1;
} }
#ublock0-epicker body.paused > aside.show { #ublock0-epicker.paused.show aside {
opacity: 1; opacity: 1;
} }
#ublock0-epicker body.paused > aside.hide { #ublock0-epicker.paused.hide aside {
opacity: 0.1; opacity: 0.1;
} }
#ublock0-epicker svg {
cursor: crosshair;
box-sizing: border-box;
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
#ublock0-epicker.paused svg {
cursor: not-allowed;
}
#ublock0-epicker svg > path:first-child {
fill: rgba(0,0,0,0.5);
fill-rule: evenodd;
}
#ublock0-epicker svg > path + path {
stroke: #F00;
stroke-width: 0.5px;
fill: rgba(255,63,63,0.20);
}
#ublock0-epicker.zap svg > path + path {
stroke: #FF0;
stroke-width: 0.5px;
fill: rgba(255,255,63,0.20);
}
#ublock0-epicker.preview svg > path {
fill: rgba(0,0,0,0.10);
}
#ublock0-epicker.preview svg > path + path {
stroke: none;
}

View file

@ -1,60 +0,0 @@
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

@ -30,8 +30,25 @@
if ( typeof vAPI !== 'object' ) { return; } if ( typeof vAPI !== 'object' ) { return; }
const $id = id => document.getElementById(id);
const $stor = selector => document.querySelector(selector);
const $storAll = selector => document.querySelectorAll(selector);
const pickerRoot = document.documentElement;
const dialog = $stor('aside');
const taCandidate = $stor('textarea');
let staticFilteringParser;
const svgRoot = $stor('svg');
const svgOcean = svgRoot.children[0];
const svgIslands = svgRoot.children[1];
const NoPaths = 'M0 0';
const epickerId = (( ) => { const epickerId = (( ) => {
const url = new URL(self.location.href); const url = new URL(self.location.href);
if ( url.searchParams.has('zap') ) {
pickerRoot.classList.add('zap');
}
return url.searchParams.get('epid'); return url.searchParams.get('epid');
})(); })();
if ( epickerId === null ) { return; } if ( epickerId === null ) { return; }
@ -43,12 +60,6 @@ let filterResultset = [];
/******************************************************************************/ /******************************************************************************/
const $id = id => document.getElementById(id);
const $stor = selector => document.querySelector(selector);
const $storAll = selector => document.querySelectorAll(selector);
/******************************************************************************/
const filterFromTextarea = function() { const filterFromTextarea = function() {
const s = taCandidate.value.trim(); const s = taCandidate.value.trim();
if ( s === '' ) { return ''; } if ( s === '' ) { return ''; }
@ -121,7 +132,7 @@ const candidateFromFilterChoice = function(filterChoice) {
// - Discard narrowing directives. // - Discard narrowing directives.
// - Remove the id if one or more classes exist // - Remove the id if one or more classes exist
// TODO: should remove tag name too? ¯\_(ツ)_/¯ // TODO: should remove tag name too? ¯\_(ツ)_/¯
if ( filterChoice.modifier ) { if ( filterChoice.broad ) {
filter = filter.replace(/:nth-of-type\(\d+\)/, ''); filter = filter.replace(/:nth-of-type\(\d+\)/, '');
// https://github.com/uBlockOrigin/uBlock-issues/issues/162 // https://github.com/uBlockOrigin/uBlock-issues/issues/162
// Mind escaped periods: they do not denote a class identifier. // Mind escaped periods: they do not denote a class identifier.
@ -167,6 +178,127 @@ const candidateFromFilterChoice = function(filterChoice) {
/******************************************************************************/ /******************************************************************************/
const onSvgClicked = function(ev) {
// If zap mode, highlight element under mouse, this makes the zapper usable
// on touch screens.
if ( pickerRoot.classList.contains('zap') ) {
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
what: 'zapElementAtPoint',
mx: ev.clientX,
my: ev.clientY,
options: {
stay: ev.shiftKey || ev.type === 'touch',
highlight: ev.target !== svgIslands,
},
});
return;
}
// https://github.com/chrisaljoudi/uBlock/issues/810#issuecomment-74600694
// Unpause picker if:
// - click outside dialog AND
// - not in preview mode
if ( pickerRoot.classList.contains('paused') ) {
if ( pickerRoot.classList.contains('preview') === false ) {
unpausePicker();
}
return;
}
// Force dialog to always be visible when using a touch-driven device.
if ( ev.type === 'touch' ) {
pickerRoot.classList.add('show');
}
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
what: 'filterElementAtPoint',
mx: ev.clientX,
my: ev.clientY,
broad: ev.ctrlKey,
});
};
/*******************************************************************************
Swipe right:
If picker not paused: quit picker
If picker paused and dialog visible: hide dialog
If picker paused and dialog not visible: quit picker
Swipe left:
If picker paused and dialog not visible: show dialog
*/
const onSvgTouch = (( ) => {
let startX = 0, startY = 0;
let t0 = 0;
return ev => {
if ( ev.type === 'touchstart' ) {
startX = ev.touches[0].screenX;
startY = ev.touches[0].screenY;
t0 = ev.timeStamp;
return;
}
if ( startX === undefined ) { return; }
if ( ev.cancelable === false ) { return; }
const stopX = ev.changedTouches[0].screenX;
const stopY = ev.changedTouches[0].screenY;
const angle = Math.abs(Math.atan2(stopY - startY, stopX - startX));
const distance = Math.sqrt(
Math.pow(stopX - startX, 2),
Math.pow(stopY - startY, 2)
);
// Interpret touch events as a tap if:
// - Swipe is not valid; and
// - The time between start and stop was less than 200ms.
const duration = ev.timeStamp - t0;
if ( distance < 32 && duration < 200 ) {
onSvgClicked({
type: 'touch',
target: ev.target,
clientX: ev.changedTouches[0].pageX,
clientY: ev.changedTouches[0].pageY,
});
ev.preventDefault();
return;
}
if ( distance < 64 ) { return; }
const angleUpperBound = Math.PI * 0.25 * 0.5;
const swipeRight = angle < angleUpperBound;
if ( swipeRight === false && angle < Math.PI - angleUpperBound ) {
return;
}
ev.preventDefault();
// Swipe left.
if ( swipeRight === false ) {
if ( pickerRoot.classList.contains('paused') ) {
pickerRoot.classList.remove('hide');
pickerRoot.classList.add('show');
}
return;
}
// Swipe right.
if (
pickerRoot.classList.contains('zap') &&
svgIslands.getAttribute('d') !== NoPaths
) {
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
what: 'unhighlight'
});
return;
}
else if (
pickerRoot.classList.contains('paused') &&
pickerRoot.classList.contains('show')
) {
pickerRoot.classList.remove('show');
pickerRoot.classList.add('hide');
return;
}
quitPicker();
};
})();
/******************************************************************************/
const onCandidateChanged = function() { const onCandidateChanged = function() {
const filter = filterFromTextarea(); const filter = filterFromTextarea();
const bad = filter === '!'; const bad = filter === '!';
@ -188,9 +320,9 @@ const onCandidateChanged = function() {
/******************************************************************************/ /******************************************************************************/
const onPreviewClicked = function() { const onPreviewClicked = function() {
const state = pickerBody.classList.toggle('preview'); const state = pickerRoot.classList.toggle('preview');
vAPI.MessagingConnection.sendTo(epickerConnectionId, { vAPI.MessagingConnection.sendTo(epickerConnectionId, {
what: 'dialogPreview', what: 'togglePreview',
state, state,
}); });
}; };
@ -221,38 +353,27 @@ const onCreateClicked = function() {
/******************************************************************************/ /******************************************************************************/
const onPickClicked = function(ev) { const onPickClicked = function() {
if ( unpausePicker();
(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() { const onQuitClicked = function() {
vAPI.MessagingConnection.sendTo(epickerConnectionId, { quitPicker();
what: 'dialogQuit'
});
}; };
/******************************************************************************/ /******************************************************************************/
const onCandidateClicked = function(ev) { const onCandidateClicked = function(ev) {
let li = ev.target.closest('li'); let li = ev.target.closest('li');
if ( li === null ) { return; }
const ul = li.closest('.changeFilter'); const ul = li.closest('.changeFilter');
if ( ul === null ) { return; } if ( ul === null ) { return; }
const choice = { const choice = {
filters: Array.from(ul.querySelectorAll('li')).map(a => a.textContent), filters: Array.from(ul.querySelectorAll('li')).map(a => a.textContent),
slot: 0, slot: 0,
modifier: ev.ctrlKey || ev.metaKey broad: ev.ctrlKey || ev.metaKey
}; };
while ( li.previousElementSibling !== null ) { while ( li.previousElementSibling !== null ) {
li = li.previousElementSibling; li = li.previousElementSibling;
@ -275,6 +396,7 @@ const onKeyPressed = function(ev) {
/******************************************************************************/ /******************************************************************************/
const onStartMoving = (( ) => { const onStartMoving = (( ) => {
let isTouch = false;
let mx0 = 0, my0 = 0; let mx0 = 0, my0 = 0;
let mx1 = 0, my1 = 0; let mx1 = 0, my1 = 0;
let r0 = 0, b0 = 0; let r0 = 0, b0 = 0;
@ -290,44 +412,101 @@ const onStartMoving = (( ) => {
}; };
const moveAsync = ev => { const moveAsync = ev => {
if ( ev.isTrusted === false ) { return; }
eatEvent(ev);
if ( timer !== undefined ) { return; } if ( timer !== undefined ) { return; }
if ( isTouch ) {
const touch = ev.touches[0];
mx1 = touch.pageX;
my1 = touch.pageY;
} else {
mx1 = ev.pageX; mx1 = ev.pageX;
my1 = ev.pageY; my1 = ev.pageY;
}
timer = self.requestAnimationFrame(move); timer = self.requestAnimationFrame(move);
}; };
const stop = ev => { const stop = ev => {
if ( ev.isTrusted === false ) { return; }
if ( dialog.classList.contains('moving') === false ) { return; } if ( dialog.classList.contains('moving') === false ) { return; }
dialog.classList.remove('moving'); dialog.classList.remove('moving');
if ( isTouch ) {
self.removeEventListener('touchmove', moveAsync, { capture: true });
} else {
self.removeEventListener('mousemove', moveAsync, { capture: true }); self.removeEventListener('mousemove', moveAsync, { capture: true });
self.removeEventListener('mouseup', stop, { capture: true, once: true }); }
eatEvent(ev); eatEvent(ev);
}; };
return function(ev) { return function(ev) {
if ( ev.isTrusted === false ) { return; }
const target = dialog.querySelector('#toolbar'); const target = dialog.querySelector('#toolbar');
if ( ev.target !== target ) { return; } if ( ev.target !== target ) { return; }
if ( dialog.classList.contains('moving') ) { return; } if ( dialog.classList.contains('moving') ) { return; }
mx0 = ev.pageX; my0 = ev.pageY; isTouch = ev.type.startsWith('touch');
if ( isTouch ) {
const touch = ev.touches[0];
mx0 = touch.pageX;
my0 = touch.pageY;
} else {
mx0 = ev.pageX;
my0 = ev.pageY;
}
const style = self.getComputedStyle(dialog); const style = self.getComputedStyle(dialog);
r0 = parseInt(style.right, 10); r0 = parseInt(style.right, 10);
b0 = parseInt(style.bottom, 10); b0 = parseInt(style.bottom, 10);
const rect = dialog.getBoundingClientRect(); const rect = dialog.getBoundingClientRect();
rMax = pickerBody.clientWidth - 4 - rect.width ; rMax = pickerRoot.clientWidth - 4 - rect.width ;
bMax = pickerBody.clientHeight - 4 - rect.height; bMax = pickerRoot.clientHeight - 4 - rect.height;
dialog.classList.add('moving'); dialog.classList.add('moving');
if ( isTouch ) {
self.addEventListener('touchmove', moveAsync, { capture: true });
self.addEventListener('touchend', stop, { capture: true, once: true });
} else {
self.addEventListener('mousemove', moveAsync, { capture: true }); self.addEventListener('mousemove', moveAsync, { capture: true });
self.addEventListener('mouseup', stop, { capture: true, once: true }); self.addEventListener('mouseup', stop, { capture: true, once: true });
}
eatEvent(ev); eatEvent(ev);
}; };
})(); })();
/******************************************************************************/ /******************************************************************************/
const svgListening = (( ) => {
let on = false;
let timer;
let mx = 0, my = 0;
const onTimer = ( ) => {
timer = undefined;
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
what: 'highlightElementAtPoint',
mx,
my,
});
};
const onHover = ev => {
mx = ev.clientX;
my = ev.clientY;
if ( timer === undefined ) {
timer = self.requestAnimationFrame(onTimer);
}
};
return state => {
if ( state === on ) { return; }
on = state;
if ( on ) {
document.addEventListener('mousemove', onHover, { passive: true });
return;
}
document.removeEventListener('mousemove', onHover, { passive: true });
if ( timer !== undefined ) {
self.cancelAnimationFrame(timer);
timer = undefined;
}
};
})();
/******************************************************************************/
const eatEvent = function(ev) { const eatEvent = function(ev) {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
@ -336,9 +515,9 @@ const eatEvent = function(ev) {
/******************************************************************************/ /******************************************************************************/
const showDialog = function(details) { const showDialog = function(details) {
pickerBody.classList.add('paused'); pausePicker();
const { netFilters, cosmeticFilters, filter, options } = details; const { netFilters, cosmeticFilters, filter, options = {} } = details;
// https://github.com/gorhill/uBlock/issues/738 // https://github.com/gorhill/uBlock/issues/738
// Trim dots. // Trim dots.
@ -390,7 +569,7 @@ const showDialog = function(details) {
const filterChoice = { const filterChoice = {
filters: filter.filters, filters: filter.filters,
slot: filter.slot, slot: filter.slot,
modifier: options.modifier || false broad: options.broad || false
}; };
taCandidate.value = candidateFromFilterChoice(filterChoice); taCandidate.value = candidateFromFilterChoice(filterChoice);
@ -399,44 +578,61 @@ const showDialog = function(details) {
/******************************************************************************/ /******************************************************************************/
// Let's have the element picker code flushed from memory when no longer const pausePicker = function() {
// in use: to ensure this, release all local references. pickerRoot.classList.add('paused');
svgListening(false);
const stopPicker = function() {
vAPI.shutdown.remove(stopPicker);
}; };
/******************************************************************************/ /******************************************************************************/
const pickerBody = document.body; const unpausePicker = function() {
const dialog = $stor('aside'); pickerRoot.classList.remove('paused', 'preview');
const taCandidate = $stor('textarea'); vAPI.MessagingConnection.sendTo(epickerConnectionId, {
let staticFilteringParser; what: 'togglePreview',
state: false,
});
svgListening(true);
};
/******************************************************************************/ /******************************************************************************/
const startDialog = function() { const startPicker = function() {
dialog.addEventListener('click', eatEvent); self.addEventListener('keydown', onKeyPressed, true);
const svg = $stor('svg');
svg.addEventListener('click', onSvgClicked);
svg.addEventListener('touchstart', onSvgTouch);
svg.addEventListener('touchend', onSvgTouch);
unpausePicker();
if ( pickerRoot.classList.contains('zap') ) { return; }
taCandidate.addEventListener('input', onCandidateChanged); taCandidate.addEventListener('input', onCandidateChanged);
$stor('body').addEventListener('mousedown', onPickClicked);
$id('preview').addEventListener('click', onPreviewClicked); $id('preview').addEventListener('click', onPreviewClicked);
$id('create').addEventListener('click', onCreateClicked); $id('create').addEventListener('click', onCreateClicked);
$id('pick').addEventListener('click', onPickClicked); $id('pick').addEventListener('click', onPickClicked);
$id('quit').addEventListener('click', onQuitClicked); $id('quit').addEventListener('click', onQuitClicked);
$id('candidateFilters').addEventListener('click', onCandidateClicked); $id('candidateFilters').addEventListener('click', onCandidateClicked);
$id('toolbar').addEventListener('mousedown', onStartMoving); $id('toolbar').addEventListener('mousedown', onStartMoving);
self.addEventListener('keydown', onKeyPressed, true); $id('toolbar').addEventListener('touchstart', onStartMoving);
staticFilteringParser = new vAPI.StaticFilteringParser({ interactive: true }); staticFilteringParser = new vAPI.StaticFilteringParser({ interactive: true });
}; };
/******************************************************************************/ /******************************************************************************/
const quitPicker = function() {
vAPI.MessagingConnection.sendTo(epickerConnectionId, { what: 'quitPicker' });
vAPI.MessagingConnection.disconnectFrom(epickerConnectionId);
};
/******************************************************************************/
const onPickerMessage = function(msg) { const onPickerMessage = function(msg) {
switch ( msg.what ) { switch ( msg.what ) {
case 'showDialog': case 'showDialog':
showDialog(msg); showDialog(msg);
break; break;
case 'filterResultset': case 'filterResultset': {
filterResultset = msg.resultset; filterResultset = msg.resultset;
$id('resultsetCount').textContent = filterResultset.length; $id('resultsetCount').textContent = filterResultset.length;
if ( filterResultset.length !== 0 ) { if ( filterResultset.length !== 0 ) {
@ -446,6 +642,20 @@ const onPickerMessage = function(msg) {
} }
break; break;
} }
case 'svgListening': {
svgListening(msg.on);
break;
}
case 'svgPaths': {
let { ocean, islands } = msg;
ocean += islands;
svgOcean.setAttribute('d', ocean);
svgIslands.setAttribute('d', islands || NoPaths);
break;
}
default:
break;
}
}; };
/******************************************************************************/ /******************************************************************************/
@ -453,16 +663,15 @@ const onPickerMessage = function(msg) {
const onConnectionMessage = function(msg) { const onConnectionMessage = function(msg) {
switch ( msg.what ) { switch ( msg.what ) {
case 'connectionBroken': case 'connectionBroken':
stopPicker();
break; break;
case 'connectionMessage': case 'connectionMessage':
onPickerMessage(msg.payload); onPickerMessage(msg.payload);
break; break;
case 'connectionAccepted': case 'connectionAccepted':
epickerConnectionId = msg.id; epickerConnectionId = msg.id;
startDialog(); startPicker();
vAPI.MessagingConnection.sendTo(epickerConnectionId, { vAPI.MessagingConnection.sendTo(epickerConnectionId, {
what: 'dialogInit', what: 'start',
}); });
break; break;
} }

View file

@ -711,26 +711,6 @@ const onMessage = function(request, sender, callback) {
// Async // Async
switch ( request.what ) { switch ( request.what ) {
case 'elementPickerArguments':
const xhr = new XMLHttpRequest();
xhr.open('GET', 'css/epicker.css', true);
xhr.overrideMimeType('text/html;charset=utf-8');
xhr.responseType = 'text';
xhr.onload = function() {
this.onload = null;
callback({
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();
return;
default: default:
break; break;
} }
@ -739,6 +719,16 @@ const onMessage = function(request, sender, callback) {
let response; let response;
switch ( request.what ) { switch ( request.what ) {
case 'elementPickerArguments':
response = {
target: µb.epickerArgs.target,
mouse: µb.epickerArgs.mouse,
zap: µb.epickerArgs.zap,
eprom: µb.epickerArgs.eprom,
pickerURL: vAPI.getURL(`/web_accessible_resources/epicker-ui.html${vAPI.warSecret()}`),
};
µb.epickerArgs.target = '';
break;
case 'elementPickerEprom': case 'elementPickerEprom':
µb.epickerArgs.eprom = request; µb.epickerArgs.eprom = request;
break; break;

View file

@ -30,24 +30,13 @@
/******************************************************************************/ /******************************************************************************/
if ( window.top !== window || typeof vAPI !== 'object' ) { return; }
/******************************************************************************/
const epickerId = vAPI.randomToken(); const epickerId = vAPI.randomToken();
let epickerConnectionId; let epickerConnectionId;
/******************************************************************************/
let pickerRoot = document.querySelector(`[${vAPI.sessionId}]`); let pickerRoot = document.querySelector(`[${vAPI.sessionId}]`);
if ( pickerRoot !== null ) { return; } if ( pickerRoot !== null ) { return; }
let pickerBootArgs; let pickerBootArgs;
let pickerBody = null;
let svgOcean = null;
let svgIslands = null;
let svgRoot = null;
let dialog = null;
const netFilterCandidates = []; const netFilterCandidates = [];
const cosmeticFilterCandidates = []; const cosmeticFilterCandidates = [];
@ -103,8 +92,8 @@ const getElementBoundingClientRect = function(elem) {
return { return {
height: bottom - top, height: bottom - top,
left: left, left,
top: top, top,
width: right - left width: right - left
}; };
}; };
@ -113,45 +102,40 @@ const getElementBoundingClientRect = function(elem) {
const highlightElements = function(elems, force) { const highlightElements = function(elems, force) {
// To make mouse move handler more efficient // To make mouse move handler more efficient
if ( !force && elems.length === targetElements.length ) { if (
if ( elems.length === 0 || elems[0] === targetElements[0] ) { (force !== true) &&
(elems.length === targetElements.length) &&
(elems.length === 0 || elems[0] === targetElements[0])
) {
return; return;
} }
} targetElements = [];
targetElements = elems;
const ow = pickerRoot.contentWindow.innerWidth; const ow = self.innerWidth;
const oh = pickerRoot.contentWindow.innerHeight; const oh = self.innerHeight;
const ocean = [
'M0 0',
'h', ow,
'v', oh,
'h-', ow,
'z'
];
const islands = []; const islands = [];
for ( let i = 0; i < elems.length; i++ ) { for ( const elem of elems ) {
const elem = elems[i];
if ( elem === pickerRoot ) { continue; } if ( elem === pickerRoot ) { continue; }
targetElements.push(elem);
const rect = getElementBoundingClientRect(elem); const rect = getElementBoundingClientRect(elem);
// Ignore offscreen areas
// Ignore if it's not on the screen if (
if ( rect.left > ow || rect.top > oh || rect.left > ow || rect.top > oh ||
rect.left + rect.width < 0 || rect.top + rect.height < 0 ) { rect.left + rect.width < 0 || rect.top + rect.height < 0
) {
continue; continue;
} }
islands.push(
const poly = 'M' + rect.left + ' ' + rect.top + `M${rect.left} ${rect.top}h${rect.width}v${rect.height}h-${rect.width}z`
'h' + rect.width + );
'v' + rect.height +
'h-' + rect.width +
'z';
ocean.push(poly);
islands.push(poly);
} }
svgOcean.setAttribute('d', ocean.join(''));
svgIslands.setAttribute('d', islands.join('') || 'M0 0'); vAPI.MessagingConnection.sendTo(epickerConnectionId, {
what: 'svgPaths',
ocean: `M0 0h${ow}v${oh}h-${ow}z`,
islands: islands.join(''),
});
}; };
/******************************************************************************/ /******************************************************************************/
@ -170,8 +154,6 @@ const mergeStrings = function(urls) {
// The differ works at line granularity: we insert a linefeed after // The differ works at line granularity: we insert a linefeed after
// each character to trick the differ to work at character granularity. // each character to trick the differ to work at character granularity.
const diffs = differ.diff_main( const diffs = differ.diff_main(
//urls[i].replace(/.(?=.)/g, '$&\n'),
//merged.replace(/.(?=.)/g, '$&\n')
urls[i].split('').join('\n'), urls[i].split('').join('\n'),
merged.split('').join('\n') merged.split('').join('\n')
); );
@ -552,24 +534,20 @@ const filtersFrom = function(x, y) {
} }
// https://github.com/gorhill/uBlock/issues/1545 // https://github.com/gorhill/uBlock/issues/1545
// Network filter candidates from all other elements found at point (x, y). // Network filter candidates from all other elements found at
// point (x, y).
if ( typeof x === 'number' ) { if ( typeof x === 'number' ) {
let attrName = vAPI.sessionId + '-clickblind'; const attrName = vAPI.sessionId + '-clickblind';
let previous;
elem = first; elem = first;
while ( elem !== null ) { while ( elem !== null ) {
previous = elem; const previous = elem;
elem.setAttribute(attrName, ''); elem.setAttribute(attrName, '');
elem = elementFromPoint(x, y); elem = elementFromPoint(x, y);
if ( elem === null || elem === previous ) { if ( elem === null || elem === previous ) { break; }
break;
}
netFilterFromElement(elem); netFilterFromElement(elem);
} }
let elems = document.querySelectorAll(`[${attrName}]`); for ( const elem of document.querySelectorAll(`[${attrName}]`) ) {
i = elems.length; elem.removeAttribute(attrName);
while ( i-- ) {
elems[i].removeAttribute(attrName);
} }
netFilterFromElement(document.body); netFilterFromElement(document.body);
@ -761,7 +739,7 @@ const filterToDOMInterface = (( ) => {
if ( filter === '' || filter === '!' ) { if ( filter === '' || filter === '!' ) {
lastFilter = ''; lastFilter = '';
lastResultset = []; lastResultset = [];
return; return lastResultset;
} }
lastFilter = filter; lastFilter = filter;
lastAction = undefined; lastAction = undefined;
@ -868,7 +846,6 @@ const filterToDOMInterface = (( ) => {
// immediately rather than wait for the next page load. // immediately rather than wait for the next page load.
const preview = function(state, permanent = false) { const preview = function(state, permanent = false) {
previewing = state !== false; previewing = state !== false;
pickerBody.classList.toggle('preview', previewing);
if ( previewing === false ) { if ( previewing === false ) {
return unapply(); return unapply();
} }
@ -909,15 +886,6 @@ const filterToDOMInterface = (( ) => {
/******************************************************************************/ /******************************************************************************/
const showDialog = function(options) { const showDialog = function(options) {
pausePicker();
options = options || {};
// Typically the dialog will be forced to be visible when using a
// touch-aware device.
dialog.classList.toggle('show', options.show === true);
dialog.classList.remove('hide');
vAPI.MessagingConnection.sendTo(epickerConnectionId, { vAPI.MessagingConnection.sendTo(epickerConnectionId, {
what: 'showDialog', what: 'showDialog',
hostname: self.location.hostname, hostname: self.location.hostname,
@ -931,18 +899,73 @@ const showDialog = function(options) {
/******************************************************************************/ /******************************************************************************/
const elementFromPoint = (( ) => {
let lastX, lastY;
return (x, y) => {
if ( x !== undefined ) {
lastX = x; lastY = y;
} else if ( lastX !== undefined ) {
x = lastX; y = lastY;
} else {
return null;
}
if ( !pickerRoot ) { return null; }
const magicAttr = `${vAPI.sessionId}-clickblind`;
pickerRoot.setAttribute(magicAttr, '');
let elem = document.elementFromPoint(x, y);
if ( elem === document.body || elem === document.documentElement ) {
elem = null;
}
// https://github.com/uBlockOrigin/uBlock-issues/issues/380
pickerRoot.removeAttribute(magicAttr);
return elem;
};
})();
/******************************************************************************/
const highlightElementAtPoint = function(mx, my) {
const elem = elementFromPoint(mx, my);
highlightElements(elem ? [ elem ] : []);
};
/******************************************************************************/
const filterElementAtPoint = function(mx, my, broad) {
if ( filtersFrom(mx, my) === 0 ) { return; }
showDialog({ broad });
};
/******************************************************************************/
// https://www.reddit.com/r/uBlockOrigin/comments/bktxtb/scrolling_doesnt_work/emn901o // https://www.reddit.com/r/uBlockOrigin/comments/bktxtb/scrolling_doesnt_work/emn901o
// Override 'fixed' position property on body element if present. // Override 'fixed' position property on body element if present.
const zap = function() { // With touch-driven devices, first highlight the element and remove only
if ( targetElements.length === 0 ) { return; } // when tapping again the highlighted area.
const zapElementAtPoint = function(mx, my, options) {
if ( options.highlight ) {
const elem = elementFromPoint(mx, my);
if ( elem ) {
highlightElements([ elem ]);
}
return;
}
let elem = targetElements.length !== 0 && targetElements[0] || null;
if ( elem === null && mx !== undefined ) {
elem = elementFromPoint(mx, my);
}
if ( elem instanceof HTMLElement === false ) { return; }
const getStyleValue = function(elem, prop) { const getStyleValue = function(elem, prop) {
const style = window.getComputedStyle(elem); const style = window.getComputedStyle(elem);
return style ? style[prop] : ''; return style ? style[prop] : '';
}; };
let elem = targetElements[0];
// Heuristic to detect scroll-locking: remove such lock when detected. // Heuristic to detect scroll-locking: remove such lock when detected.
if ( if (
parseInt(getStyleValue(elem, 'zIndex'), 10) >= 1000 || parseInt(getStyleValue(elem, 'zIndex'), 10) >= 1000 ||
@ -960,173 +983,8 @@ const zap = function() {
} }
} }
elem.parentNode.removeChild(elem); elem.remove();
elem = elementFromPoint(); highlightElementAtPoint(mx, my);
highlightElements(elem ? [ elem ] : []);
};
/******************************************************************************/
const elementFromPoint = (( ) => {
let lastX, lastY;
return (x, y) => {
if ( x !== undefined ) {
lastX = x; lastY = y;
} else if ( lastX !== undefined ) {
x = lastX; y = lastY;
} else {
return null;
}
if ( !pickerRoot ) { return null; }
pickerRoot.style.setProperty('pointer-events', 'none', 'important');
let elem = document.elementFromPoint(x, y);
if ( elem === document.body || elem === document.documentElement ) {
elem = null;
}
// https://github.com/uBlockOrigin/uBlock-issues/issues/380
pickerRoot.style.setProperty('pointer-events', 'auto', 'important');
return elem;
};
})();
/******************************************************************************/
const onSvgHovered = (( ) => {
let timer;
let mx = 0, my = 0;
const onTimer = function() {
timer = undefined;
const elem = elementFromPoint(mx, my);
highlightElements(elem ? [elem] : []);
};
return function onMove(ev) {
mx = ev.clientX;
my = ev.clientY;
if ( timer === undefined ) {
timer = vAPI.setTimeout(onTimer, 40);
}
};
})();
/*******************************************************************************
Swipe right:
If picker not paused: quit picker
If picker paused and dialog visible: hide dialog
If picker paused and dialog not visible: quit picker
Swipe left:
If picker paused and dialog not visible: show dialog
*/
const onSvgTouchStartStop = (( ) => {
var startX,
startY;
return function onTouch(ev) {
if ( ev.type === 'touchstart' ) {
startX = ev.touches[0].screenX;
startY = ev.touches[0].screenY;
return;
}
if ( startX === undefined ) { return; }
if ( ev.cancelable === false ) { return; }
var stopX = ev.changedTouches[0].screenX,
stopY = ev.changedTouches[0].screenY,
angle = Math.abs(Math.atan2(stopY - startY, stopX - startX)),
distance = Math.sqrt(
Math.pow(stopX - startX, 2),
Math.pow(stopY - startY, 2)
);
// Interpret touch events as a click events if swipe is not valid.
if ( distance < 32 ) {
onSvgClicked({
type: 'touch',
target: ev.target,
clientX: ev.changedTouches[0].pageX,
clientY: ev.changedTouches[0].pageY,
isTrusted: ev.isTrusted
});
ev.preventDefault();
return;
}
if ( distance < 64 ) { return; }
var angleUpperBound = Math.PI * 0.25 * 0.5,
swipeRight = angle < angleUpperBound;
if ( swipeRight === false && angle < Math.PI - angleUpperBound ) {
return;
}
ev.preventDefault();
// Swipe left.
if ( swipeRight === false ) {
if ( pickerBody.classList.contains('paused') ) {
dialog.classList.remove('hide');
dialog.classList.add('show');
}
return;
}
// Swipe right.
if (
pickerBody.classList.contains('paused') &&
dialog.classList.contains('show')
) {
dialog.classList.remove('show');
dialog.classList.add('hide');
return;
}
stopPicker();
};
})();
/******************************************************************************/
const onSvgClicked = function(ev) {
if ( ev.isTrusted === false ) { return; }
// If zap mode, highlight element under mouse, this makes the zapper usable
// on touch screens.
if ( pickerBootArgs.zap ) {
let elem = targetElements.lenght !== 0 && targetElements[0];
if ( !elem || ev.target !== svgIslands ) {
elem = elementFromPoint(ev.clientX, ev.clientY);
if ( elem !== null ) {
highlightElements([elem]);
return;
}
}
zap();
if ( !ev.shiftKey ) {
stopPicker();
}
return;
}
// https://github.com/chrisaljoudi/uBlock/issues/810#issuecomment-74600694
// Unpause picker if:
// - click outside dialog AND
// - not in preview mode
if ( pickerBody.classList.contains('paused') ) {
if ( filterToDOMInterface.previewing === false ) {
unpausePicker();
}
return;
}
if ( filtersFrom(ev.clientX, ev.clientY) === 0 ) {
return;
}
showDialog({
show: ev.type === 'touch',
modifier: ev.ctrlKey
});
};
/******************************************************************************/
const svgListening = function(on) {
const action = (on ? 'add' : 'remove') + 'EventListener';
svgRoot[action]('mousemove', onSvgHovered, { passive: true });
}; };
/******************************************************************************/ /******************************************************************************/
@ -1139,7 +997,7 @@ const onKeyPressed = function(ev) {
) { ) {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
zap(); zapElementAtPoint();
return; return;
} }
// Esc // Esc
@ -1147,7 +1005,7 @@ const onKeyPressed = function(ev) {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
filterToDOMInterface.preview(false); filterToDOMInterface.preview(false);
stopPicker(); quitPicker();
return; return;
} }
}; };
@ -1158,67 +1016,18 @@ const onKeyPressed = function(ev) {
// May need to dynamically adjust the height of the overlay + new position // May need to dynamically adjust the height of the overlay + new position
// of highlighted elements. // of highlighted elements.
const onScrolled = function() { const onViewportChanged = function() {
highlightElements(targetElements, true); highlightElements(targetElements, true);
}; };
/******************************************************************************/ /******************************************************************************/
const pausePicker = function() {
pickerBody.classList.add('paused');
svgListening(false);
};
/******************************************************************************/
const unpausePicker = function() {
filterToDOMInterface.preview(false);
pickerBody.classList.remove('paused');
svgListening(true);
};
/******************************************************************************/
// 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);
targetElements = [];
candidateElements = [];
bestCandidateFilter = null;
if ( pickerRoot === null ) { return; }
// https://github.com/gorhill/uBlock/issues/2060
if ( vAPI.domFilterer instanceof Object ) {
vAPI.userStylesheet.remove(pickerCSS);
vAPI.userStylesheet.apply();
vAPI.domFilterer.unexcludeNode(pickerRoot);
}
window.removeEventListener('scroll', onScrolled, true);
svgListening(false);
pickerRoot.remove();
pickerRoot = pickerBody = svgRoot = svgOcean = svgIslands = dialog = null;
window.focus();
};
/******************************************************************************/
// Auto-select a specific target, if any, and if possible // Auto-select a specific target, if any, and if possible
const startPicker = function() { const startPicker = function() {
svgRoot.addEventListener('click', onSvgClicked); self.addEventListener('scroll', onViewportChanged, { passive: true });
svgRoot.addEventListener('touchstart', onSvgTouchStartStop); self.addEventListener('resize', onViewportChanged, { passive: true });
svgRoot.addEventListener('touchend', onSvgTouchStartStop); self.addEventListener('keydown', onKeyPressed, true);
svgListening(true);
self.addEventListener('scroll', onScrolled, true);
pickerRoot.contentWindow.addEventListener('keydown', onKeyPressed, true);
pickerRoot.contentWindow.focus();
// Try using mouse position // Try using mouse position
if ( if (
@ -1227,8 +1036,7 @@ const startPicker = function() {
vAPI.mouseClick.x > 0 vAPI.mouseClick.x > 0
) { ) {
if ( filtersFrom(vAPI.mouseClick.x, vAPI.mouseClick.y) !== 0 ) { if ( filtersFrom(vAPI.mouseClick.x, vAPI.mouseClick.y) !== 0 ) {
showDialog(); return showDialog();
return;
} }
} }
@ -1259,35 +1067,55 @@ const startPicker = function() {
} }
elem.scrollIntoView({ behavior: 'smooth', block: 'start' }); elem.scrollIntoView({ behavior: 'smooth', block: 'start' });
filtersFrom(elem); filtersFrom(elem);
showDialog({ modifier: true }); return showDialog({ broad: true });
return;
} }
// A target was specified, but it wasn't found: abort. // A target was specified, but it wasn't found: abort.
stopPicker(); quitPicker();
};
/******************************************************************************/
// Let's have the element picker code flushed from memory when no longer
// in use: to ensure this, release all local references.
const quitPicker = function() {
self.removeEventListener('scroll', onViewportChanged, { passive: true });
self.removeEventListener('resize', onViewportChanged, { passive: true });
self.removeEventListener('keydown', onKeyPressed, true);
vAPI.shutdown.remove(quitPicker);
vAPI.MessagingConnection.disconnectFrom(epickerConnectionId);
vAPI.MessagingConnection.removeListener(onConnectionMessage);
vAPI.userStylesheet.remove(pickerCSS);
vAPI.userStylesheet.apply();
if ( pickerRoot === null ) { return; }
// https://github.com/gorhill/uBlock/issues/2060
if ( vAPI.domFilterer instanceof Object ) {
vAPI.domFilterer.unexcludeNode(pickerRoot);
}
pickerRoot.remove();
pickerRoot = null;
self.focus();
}; };
/******************************************************************************/ /******************************************************************************/
const onDialogMessage = function(msg) { const onDialogMessage = function(msg) {
switch ( msg.what ) { switch ( msg.what ) {
case 'dialogInit': case 'start':
startPicker(); startPicker();
break; if ( targetElements.length === 0 ) {
case 'dialogPreview': highlightElements([], true);
filterToDOMInterface.preview(msg.state); }
break; break;
case 'dialogCreate': case 'dialogCreate':
filterToDOMInterface.queryAll(msg); filterToDOMInterface.queryAll(msg);
filterToDOMInterface.preview(true, true); filterToDOMInterface.preview(true, true);
stopPicker(); quitPicker();
break;
case 'dialogPick':
unpausePicker();
break;
case 'dialogQuit':
filterToDOMInterface.preview(false);
stopPicker();
break; break;
case 'dialogSetFilter': { case 'dialogSetFilter': {
const resultset = filterToDOMInterface.queryAll(msg); const resultset = filterToDOMInterface.queryAll(msg);
@ -1303,6 +1131,28 @@ const onDialogMessage = function(msg) {
}); });
break; break;
} }
case 'quitPicker':
filterToDOMInterface.preview(false);
quitPicker();
break;
case 'highlightElementAtPoint':
highlightElementAtPoint(msg.mx, msg.my);
break;
case 'unhighlight':
highlightElements([]);
break;
case 'filterElementAtPoint':
filterElementAtPoint(msg.mx, msg.my, msg.broad);
break;
case 'zapElementAtPoint':
zapElementAtPoint(msg.mx, msg.my, msg.options);
if ( msg.options.highlight !== true && msg.options.stay !== true ) {
quitPicker();
}
break;
case 'togglePreview':
filterToDOMInterface.preview(msg.state);
break;
default: default:
break; break;
} }
@ -1311,18 +1161,13 @@ const onDialogMessage = function(msg) {
/******************************************************************************/ /******************************************************************************/
const onConnectionMessage = function(msg) { const onConnectionMessage = function(msg) {
if ( if ( msg.from !== `epickerDialog-${epickerId}` ) { return; }
msg.from !== `epickerDialog-${epickerId}` ||
msg.to !== `epicker-${epickerId}`
) {
return;
}
switch ( msg.what ) { switch ( msg.what ) {
case 'connectionRequested': case 'connectionRequested':
epickerConnectionId = msg.id; epickerConnectionId = msg.id;
return true; return true;
case 'connectionBroken': case 'connectionBroken':
stopPicker(); quitPicker();
break; break;
case 'connectionMessage': case 'connectionMessage':
onDialogMessage(msg.payload); onDialogMessage(msg.payload);
@ -1332,16 +1177,60 @@ const onConnectionMessage = function(msg) {
/******************************************************************************/ /******************************************************************************/
pickerRoot = document.createElement('iframe'); // epicker-ui.html will be injected in the page through an iframe, and
pickerRoot.setAttribute(vAPI.sessionId, ''); // is a sandboxed so as to prevent the page from interfering with its
// content and behavior.
//
// The purpose of epicker.js is to:
// - Install the element picker UI, and wait for the component to establish
// a direct communication channel.
// - Lookup candidate filters from elements at a specific position.
// - Highlight element(s) at a specific position or according to whether
// they match candidate filters;
// - Preview the result of applying a candidate filter;
//
// When the element picker is installed on a page, the only change the page
// sees is an iframe with a random attribute. The page can't see the content
// of the iframe, and cannot interfere with its style properties. However the
// page can remove the iframe.
// We need extra messaging capabilities + fetch/process picker arguments.
{
const results = await Promise.all([
vAPI.messaging.extend(),
vAPI.messaging.send('elementPicker', { what: 'elementPickerArguments' }),
]);
if ( results[0] !== true ) { return; }
pickerBootArgs = results[1];
if ( typeof pickerBootArgs !== 'object' || pickerBootArgs === null ) {
return;
}
// Restore net filter union data if origin is the same.
const eprom = pickerBootArgs.eprom || null;
if ( eprom !== null && eprom.lastNetFilterSession === lastNetFilterSession ) {
lastNetFilterHostname = eprom.lastNetFilterHostname || '';
lastNetFilterUnion = eprom.lastNetFilterUnion || '';
}
}
// The DOM filterer will not be present when cosmetic filtering is disabled.
if (
pickerBootArgs.zap !== true &&
vAPI.domFilterer instanceof Object === false
) {
return;
}
// https://github.com/gorhill/uBlock/issues/1529
// In addition to inline styles, harden the element picker styles by using
// dedicated CSS rules.
const pickerCSSStyle = [ const pickerCSSStyle = [
'background: transparent', 'background: transparent',
'border: 0', 'border: 0',
'border-radius: 0', 'border-radius: 0',
'box-shadow: none', 'box-shadow: none',
'display: block', 'display: block',
'height: 100%', 'height: 100vh',
'left: 0', 'left: 0',
'margin: 0', 'margin: 0',
'max-height: none', 'max-height: none',
@ -1351,6 +1240,7 @@ const pickerCSSStyle = [
'opacity: 1', 'opacity: 1',
'outline: 0', 'outline: 0',
'padding: 0', 'padding: 0',
'pointer-events: auto',
'position: fixed', 'position: fixed',
'top: 0', 'top: 0',
'visibility: visible', 'visibility: visible',
@ -1358,107 +1248,40 @@ const pickerCSSStyle = [
'z-index: 2147483647', 'z-index: 2147483647',
'' ''
].join(' !important;'); ].join(' !important;');
pickerRoot.style.cssText = pickerCSSStyle;
// https://github.com/uBlockOrigin/uBlock-issues/issues/393
// This needs to be injected as an inline style, *never* as a user style,
// hence why it's not added above as part of the pickerCSSStyle
// properties.
pickerRoot.style.setProperty('pointer-events', 'auto', 'important');
const pickerCSS = ` const pickerCSS = `
[${vAPI.sessionId}] { :root [${vAPI.sessionId}] {
${pickerCSSStyle} ${pickerCSSStyle}
} }
[${vAPI.sessionId}-clickblind] { :root [${vAPI.sessionId}-clickblind] {
pointer-events: none !important; pointer-events: none !important;
} }
`; `;
{
const pickerRootLoaded = new Promise(resolve => {
pickerRoot.addEventListener('load', ( ) => { resolve(); }, { once: true });
});
document.documentElement.append(pickerRoot);
const results = await Promise.all([
vAPI.messaging.send('elementPicker', { what: 'elementPickerArguments' }),
pickerRootLoaded,
]);
pickerBootArgs = results[0];
// The DOM filterer will not be present when cosmetic filtering is
// disabled.
if (
pickerBootArgs.zap !== true &&
vAPI.domFilterer instanceof Object === false
) {
pickerRoot.remove();
return;
}
// Restore net filter union data if origin is the same.
const eprom = pickerBootArgs.eprom || null;
if ( eprom !== null && eprom.lastNetFilterSession === lastNetFilterSession ) {
lastNetFilterHostname = eprom.lastNetFilterHostname || '';
lastNetFilterUnion = eprom.lastNetFilterUnion || '';
}
const frameDoc = pickerRoot.contentDocument;
// Provide an id users can use as anchor to personalize uBO's element
// picker style properties.
frameDoc.documentElement.id = 'ublock0-epicker';
// https://github.com/gorhill/uBlock/issues/2240
// https://github.com/uBlockOrigin/uBlock-issues/issues/170
// Remove the already declared inline style tag: we will create a new
// one based on the removed one, and replace the old one.
const style = frameDoc.createElement('style');
style.textContent = pickerBootArgs.frameCSS;
frameDoc.head.appendChild(style);
pickerBody = frameDoc.body;
pickerBody.setAttribute('lang', navigator.language);
pickerBody.classList.toggle('zap', pickerBootArgs.zap === true);
svgRoot = frameDoc.createElementNS('http://www.w3.org/2000/svg', 'svg');
svgOcean = frameDoc.createElementNS('http://www.w3.org/2000/svg', 'path');
svgRoot.append(svgOcean);
svgIslands = frameDoc.createElementNS('http://www.w3.org/2000/svg', 'path');
svgRoot.append(svgIslands);
pickerBody.append(svgRoot);
dialog = frameDoc.createElement('iframe');
pickerBody.append(dialog);
}
highlightElements([], true);
// https://github.com/gorhill/uBlock/issues/1529
// In addition to inline styles, harden the element picker styles by using
// dedicated CSS rules.
vAPI.userStylesheet.add(pickerCSS); vAPI.userStylesheet.add(pickerCSS);
vAPI.userStylesheet.apply(); vAPI.userStylesheet.apply();
vAPI.shutdown.add(stopPicker); pickerRoot = document.createElement('iframe');
pickerRoot.setAttribute(vAPI.sessionId, '');
// https://github.com/gorhill/uBlock/issues/3497 document.documentElement.append(pickerRoot);
// https://github.com/uBlockOrigin/uBlock-issues/issues/1215
// Instantiate isolated element picker dialog.
if ( pickerBootArgs.zap === true ) {
startPicker();
return;
}
// https://github.com/gorhill/uBlock/issues/2060 // https://github.com/gorhill/uBlock/issues/2060
if ( vAPI.domFilterer instanceof Object ) {
vAPI.domFilterer.excludeNode(pickerRoot); vAPI.domFilterer.excludeNode(pickerRoot);
}
vAPI.shutdown.add(quitPicker);
if ( await vAPI.messaging.extend() !== true ) { return; }
vAPI.MessagingConnection.addListener(onConnectionMessage); vAPI.MessagingConnection.addListener(onConnectionMessage);
dialog.contentWindow.location = `${pickerBootArgs.dialogURL}&epid=${epickerId}`; {
const url = new URL(pickerBootArgs.pickerURL);
url.searchParams.set('epid', epickerId);
if ( pickerBootArgs.zap ) {
url.searchParams.set('zap', '1');
}
pickerRoot.contentWindow.location = url.href;
}
/******************************************************************************/ /******************************************************************************/

View file

@ -4,7 +4,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>uBlock Origin Element Picker</title> <title>uBlock Origin Element Picker</title>
<link rel="stylesheet" href="../css/epicker-dialog.css"> <link rel="stylesheet" href="../css/epicker-ui.css">
</head> </head>
<body> <body>
@ -35,13 +35,14 @@
</li> </li>
</ul> </ul>
</aside> </aside>
<svg><path d></path><path d></path></svg>
<script src="../js/vapi.js"></script> <script src="../js/vapi.js"></script>
<script src="../js/vapi-common.js"></script> <script src="../js/vapi-common.js"></script>
<script src="../js/vapi-client.js"></script> <script src="../js/vapi-client.js"></script>
<script src="../js/vapi-client-extra.js"></script> <script src="../js/vapi-client-extra.js"></script>
<script src="../js/i18n.js"></script> <script src="../js/i18n.js"></script>
<script src="../js/epicker-dialog.js"></script> <script src="../js/epicker-ui.js"></script>
<script src="../js/static-filtering-parser.js"></script> <script src="../js/static-filtering-parser.js"></script>
</body> </body>