From d23f9c6a8b620ae0c61aab7fba7477471bd656fb Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Thu, 3 Sep 2020 10:27:35 -0400 Subject: [PATCH] Isolate element picker's svg layers from page content Related issue: - https://github.com/uBlockOrigin/uBlock-issues/issues/1226 Related commit: - https://github.com/gorhill/uBlock/commit/9eb455ab5eb2d5640f4188e71011b7aa22ab8b43 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. --- platform/chromium/vapi-client-extra.js | 3 + .../{epicker-dialog.css => epicker-ui.css} | 73 +- src/css/epicker.css | 60 -- src/js/{epicker-dialog.js => epicker-ui.js} | 365 +++++++-- src/js/messaging.js | 30 +- src/js/scriptlets/epicker.js | 691 +++++++----------- .../{epicker-dialog.html => epicker-ui.html} | 5 +- 7 files changed, 615 insertions(+), 612 deletions(-) rename src/css/{epicker-dialog.css => epicker-ui.css} (79%) delete mode 100644 src/css/epicker.css rename src/js/{epicker-dialog.js => epicker-ui.js} (61%) rename src/web_accessible_resources/{epicker-dialog.html => epicker-ui.html} (90%) diff --git a/platform/chromium/vapi-client-extra.js b/platform/chromium/vapi-client-extra.js index 383678f84..0328ec7a9 100644 --- a/platform/chromium/vapi-client-extra.js +++ b/platform/chromium/vapi-client-extra.js @@ -153,6 +153,9 @@ vAPI.MessagingConnection = class { listeners.add(listener); vAPI.messaging.getPort(); // Ensure a port instance exists } + static removeListener(listener) { + listeners.delete(listener); + } static connectTo(from, to, handler) { const port = vAPI.messaging.getPort(); if ( port === null ) { return; } diff --git a/src/css/epicker-dialog.css b/src/css/epicker-ui.css similarity index 79% rename from src/css/epicker-dialog.css rename to src/css/epicker-ui.css index 5b0c0e44f..0ff6eecd5 100644 --- a/src/css/epicker-dialog.css +++ b/src/css/epicker-ui.css @@ -12,6 +12,22 @@ html#ublock0-epicker, #ublock0-epicker :focus { 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 li, #ublock0-epicker div { @@ -53,7 +69,7 @@ html#ublock0-epicker, #ublock0-epicker #preview { float: left; } -#ublock0-epicker body.preview #preview { +#ublock0-epicker.preview #preview { background-color: hsl(204, 100%, 83%); border-color: hsl(204, 50%, 60%); } @@ -141,18 +157,7 @@ html#ublock0-epicker, #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 @@ -162,23 +167,55 @@ html#ublock0-epicker, 60% { opacity: 1.0; } 100% { opacity: 0.1; } } -#ublock0-epicker body.paused > aside { +#ublock0-epicker.paused aside { opacity: 0.1; visibility: visible; 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-name: startDialog; animation-timing-function: linear; } -#ublock0-epicker body.paused > aside:hover { +#ublock0-epicker.paused aside:hover { opacity: 1; } -#ublock0-epicker body.paused > aside.show { +#ublock0-epicker.paused.show aside { opacity: 1; } -#ublock0-epicker body.paused > aside.hide { +#ublock0-epicker.paused.hide aside { 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; +} diff --git a/src/css/epicker.css b/src/css/epicker.css deleted file mode 100644 index 1010fb24e..000000000 --- a/src/css/epicker.css +++ /dev/null @@ -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; -} diff --git a/src/js/epicker-dialog.js b/src/js/epicker-ui.js similarity index 61% rename from src/js/epicker-dialog.js rename to src/js/epicker-ui.js index cea9d4397..3b01cd4c3 100644 --- a/src/js/epicker-dialog.js +++ b/src/js/epicker-ui.js @@ -30,8 +30,25 @@ 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 url = new URL(self.location.href); + if ( url.searchParams.has('zap') ) { + pickerRoot.classList.add('zap'); + } return url.searchParams.get('epid'); })(); 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 s = taCandidate.value.trim(); if ( s === '' ) { return ''; } @@ -121,7 +132,7 @@ const candidateFromFilterChoice = function(filterChoice) { // - Discard narrowing directives. // - Remove the id if one or more classes exist // TODO: should remove tag name too? ¯\_(ツ)_/¯ - if ( filterChoice.modifier ) { + if ( filterChoice.broad ) { 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. @@ -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 filter = filterFromTextarea(); const bad = filter === '!'; @@ -188,9 +320,9 @@ const onCandidateChanged = function() { /******************************************************************************/ const onPreviewClicked = function() { - const state = pickerBody.classList.toggle('preview'); + const state = pickerRoot.classList.toggle('preview'); vAPI.MessagingConnection.sendTo(epickerConnectionId, { - what: 'dialogPreview', + what: 'togglePreview', state, }); }; @@ -221,38 +353,27 @@ const onCreateClicked = function() { /******************************************************************************/ -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 onPickClicked = function() { + unpausePicker(); }; /******************************************************************************/ const onQuitClicked = function() { - vAPI.MessagingConnection.sendTo(epickerConnectionId, { - what: 'dialogQuit' - }); + quitPicker(); }; /******************************************************************************/ const onCandidateClicked = function(ev) { let li = ev.target.closest('li'); + if ( li === null ) { return; } 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 + broad: ev.ctrlKey || ev.metaKey }; while ( li.previousElementSibling !== null ) { li = li.previousElementSibling; @@ -275,6 +396,7 @@ const onKeyPressed = function(ev) { /******************************************************************************/ const onStartMoving = (( ) => { + let isTouch = false; let mx0 = 0, my0 = 0; let mx1 = 0, my1 = 0; let r0 = 0, b0 = 0; @@ -290,44 +412,101 @@ const onStartMoving = (( ) => { }; const moveAsync = ev => { - if ( ev.isTrusted === false ) { return; } - eatEvent(ev); if ( timer !== undefined ) { return; } - mx1 = ev.pageX; - my1 = ev.pageY; + if ( isTouch ) { + const touch = ev.touches[0]; + mx1 = touch.pageX; + my1 = touch.pageY; + } else { + 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 }); + if ( isTouch ) { + self.removeEventListener('touchmove', moveAsync, { capture: true }); + } else { + self.removeEventListener('mousemove', moveAsync, { capture: 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; + 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); 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; + rMax = pickerRoot.clientWidth - 4 - rect.width ; + bMax = pickerRoot.clientHeight - 4 - rect.height; dialog.classList.add('moving'); - self.addEventListener('mousemove', moveAsync, { capture: true }); - self.addEventListener('mouseup', stop, { capture: true, once: true }); + 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('mouseup', stop, { capture: true, once: true }); + } 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) { ev.stopPropagation(); ev.preventDefault(); @@ -336,9 +515,9 @@ const eatEvent = function(ev) { /******************************************************************************/ 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 // Trim dots. @@ -390,7 +569,7 @@ const showDialog = function(details) { const filterChoice = { filters: filter.filters, slot: filter.slot, - modifier: options.modifier || false + broad: options.broad || false }; taCandidate.value = candidateFromFilterChoice(filterChoice); @@ -399,52 +578,83 @@ const showDialog = function(details) { /******************************************************************************/ -// 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 pausePicker = function() { + pickerRoot.classList.add('paused'); + svgListening(false); }; /******************************************************************************/ -const pickerBody = document.body; -const dialog = $stor('aside'); -const taCandidate = $stor('textarea'); -let staticFilteringParser; +const unpausePicker = function() { + pickerRoot.classList.remove('paused', 'preview'); + vAPI.MessagingConnection.sendTo(epickerConnectionId, { + what: 'togglePreview', + state: false, + }); + svgListening(true); +}; /******************************************************************************/ -const startDialog = function() { - dialog.addEventListener('click', eatEvent); +const startPicker = function() { + 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); - $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); + $id('toolbar').addEventListener('touchstart', onStartMoving); staticFilteringParser = new vAPI.StaticFilteringParser({ interactive: true }); }; /******************************************************************************/ +const quitPicker = function() { + vAPI.MessagingConnection.sendTo(epickerConnectionId, { what: 'quitPicker' }); + vAPI.MessagingConnection.disconnectFrom(epickerConnectionId); +}; + +/******************************************************************************/ + 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', ''); + 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; } - 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; } }; @@ -452,19 +662,18 @@ const onPickerMessage = function(msg) { 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; + case 'connectionBroken': + break; + case 'connectionMessage': + onPickerMessage(msg.payload); + break; + case 'connectionAccepted': + epickerConnectionId = msg.id; + startPicker(); + vAPI.MessagingConnection.sendTo(epickerConnectionId, { + what: 'start', + }); + break; } }; diff --git a/src/js/messaging.js b/src/js/messaging.js index 38f38fda5..6dac63db5 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -711,26 +711,6 @@ const onMessage = function(request, sender, callback) { // Async 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: break; } @@ -739,6 +719,16 @@ const onMessage = function(request, sender, callback) { let response; 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': µb.epickerArgs.eprom = request; break; diff --git a/src/js/scriptlets/epicker.js b/src/js/scriptlets/epicker.js index 0d420b5d4..7cb13a7e6 100644 --- a/src/js/scriptlets/epicker.js +++ b/src/js/scriptlets/epicker.js @@ -30,24 +30,13 @@ /******************************************************************************/ -if ( window.top !== window || typeof vAPI !== 'object' ) { return; } - -/******************************************************************************/ - const epickerId = vAPI.randomToken(); let epickerConnectionId; -/******************************************************************************/ - let pickerRoot = document.querySelector(`[${vAPI.sessionId}]`); if ( pickerRoot !== null ) { return; } let pickerBootArgs; -let pickerBody = null; -let svgOcean = null; -let svgIslands = null; -let svgRoot = null; -let dialog = null; const netFilterCandidates = []; const cosmeticFilterCandidates = []; @@ -103,8 +92,8 @@ const getElementBoundingClientRect = function(elem) { return { height: bottom - top, - left: left, - top: top, + left, + top, width: right - left }; }; @@ -113,45 +102,40 @@ const getElementBoundingClientRect = function(elem) { const highlightElements = function(elems, force) { // To make mouse move handler more efficient - if ( !force && elems.length === targetElements.length ) { - if ( elems.length === 0 || elems[0] === targetElements[0] ) { - return; - } + if ( + (force !== true) && + (elems.length === targetElements.length) && + (elems.length === 0 || elems[0] === targetElements[0]) + ) { + return; } - targetElements = elems; + targetElements = []; - const ow = pickerRoot.contentWindow.innerWidth; - const oh = pickerRoot.contentWindow.innerHeight; - const ocean = [ - 'M0 0', - 'h', ow, - 'v', oh, - 'h-', ow, - 'z' - ]; + const ow = self.innerWidth; + const oh = self.innerHeight; const islands = []; - for ( let i = 0; i < elems.length; i++ ) { - const elem = elems[i]; + for ( const elem of elems ) { if ( elem === pickerRoot ) { continue; } + targetElements.push(elem); const rect = getElementBoundingClientRect(elem); - - // Ignore if it's not on the screen - if ( rect.left > ow || rect.top > oh || - rect.left + rect.width < 0 || rect.top + rect.height < 0 ) { + // Ignore offscreen areas + if ( + rect.left > ow || rect.top > oh || + rect.left + rect.width < 0 || rect.top + rect.height < 0 + ) { continue; } - - const poly = 'M' + rect.left + ' ' + rect.top + - 'h' + rect.width + - 'v' + rect.height + - 'h-' + rect.width + - 'z'; - ocean.push(poly); - islands.push(poly); + islands.push( + `M${rect.left} ${rect.top}h${rect.width}v${rect.height}h-${rect.width}z` + ); } - 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 // each character to trick the differ to work at character granularity. const diffs = differ.diff_main( - //urls[i].replace(/.(?=.)/g, '$&\n'), - //merged.replace(/.(?=.)/g, '$&\n') urls[i].split('').join('\n'), merged.split('').join('\n') ); @@ -552,24 +534,20 @@ const filtersFrom = function(x, y) { } // 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' ) { - let attrName = vAPI.sessionId + '-clickblind'; - let previous; + const attrName = vAPI.sessionId + '-clickblind'; elem = first; while ( elem !== null ) { - previous = elem; + const previous = elem; elem.setAttribute(attrName, ''); elem = elementFromPoint(x, y); - if ( elem === null || elem === previous ) { - break; - } + if ( elem === null || elem === previous ) { break; } netFilterFromElement(elem); } - let elems = document.querySelectorAll(`[${attrName}]`); - i = elems.length; - while ( i-- ) { - elems[i].removeAttribute(attrName); + for ( const elem of document.querySelectorAll(`[${attrName}]`) ) { + elem.removeAttribute(attrName); } netFilterFromElement(document.body); @@ -761,7 +739,7 @@ const filterToDOMInterface = (( ) => { if ( filter === '' || filter === '!' ) { lastFilter = ''; lastResultset = []; - return; + return lastResultset; } lastFilter = filter; lastAction = undefined; @@ -868,7 +846,6 @@ const filterToDOMInterface = (( ) => { // immediately rather than wait for the next page load. const preview = function(state, permanent = false) { previewing = state !== false; - pickerBody.classList.toggle('preview', previewing); if ( previewing === false ) { return unapply(); } @@ -909,15 +886,6 @@ const filterToDOMInterface = (( ) => { /******************************************************************************/ 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, { what: 'showDialog', 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 // Override 'fixed' position property on body element if present. -const zap = function() { - if ( targetElements.length === 0 ) { return; } +// With touch-driven devices, first highlight the element and remove only +// 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 style = window.getComputedStyle(elem); return style ? style[prop] : ''; }; - let elem = targetElements[0]; // Heuristic to detect scroll-locking: remove such lock when detected. if ( parseInt(getStyleValue(elem, 'zIndex'), 10) >= 1000 || @@ -960,173 +983,8 @@ const zap = function() { } } - elem.parentNode.removeChild(elem); - elem = elementFromPoint(); - 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 }); + elem.remove(); + highlightElementAtPoint(mx, my); }; /******************************************************************************/ @@ -1139,7 +997,7 @@ const onKeyPressed = function(ev) { ) { ev.stopPropagation(); ev.preventDefault(); - zap(); + zapElementAtPoint(); return; } // Esc @@ -1147,7 +1005,7 @@ const onKeyPressed = function(ev) { ev.stopPropagation(); ev.preventDefault(); filterToDOMInterface.preview(false); - stopPicker(); + quitPicker(); return; } }; @@ -1158,67 +1016,18 @@ const onKeyPressed = function(ev) { // May need to dynamically adjust the height of the overlay + new position // of highlighted elements. -const onScrolled = function() { +const onViewportChanged = function() { 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 const startPicker = function() { - svgRoot.addEventListener('click', onSvgClicked); - svgRoot.addEventListener('touchstart', onSvgTouchStartStop); - svgRoot.addEventListener('touchend', onSvgTouchStartStop); - svgListening(true); - - self.addEventListener('scroll', onScrolled, true); - pickerRoot.contentWindow.addEventListener('keydown', onKeyPressed, true); - pickerRoot.contentWindow.focus(); + self.addEventListener('scroll', onViewportChanged, { passive: true }); + self.addEventListener('resize', onViewportChanged, { passive: true }); + self.addEventListener('keydown', onKeyPressed, true); // Try using mouse position if ( @@ -1227,8 +1036,7 @@ const startPicker = function() { vAPI.mouseClick.x > 0 ) { if ( filtersFrom(vAPI.mouseClick.x, vAPI.mouseClick.y) !== 0 ) { - showDialog(); - return; + return showDialog(); } } @@ -1259,89 +1067,170 @@ const startPicker = function() { } elem.scrollIntoView({ behavior: 'smooth', block: 'start' }); filtersFrom(elem); - showDialog({ modifier: true }); - return; + return showDialog({ broad: true }); } // 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) { switch ( msg.what ) { - case 'dialogInit': - startPicker(); - break; - case 'dialogPreview': - filterToDOMInterface.preview(msg.state); - break; - case 'dialogCreate': - filterToDOMInterface.queryAll(msg); - filterToDOMInterface.preview(true, true); - stopPicker(); - break; - case 'dialogPick': - unpausePicker(); - break; - case 'dialogQuit': - filterToDOMInterface.preview(false); - stopPicker(); - break; - case 'dialogSetFilter': { - const resultset = filterToDOMInterface.queryAll(msg); - highlightElements(resultset.map(a => a.elem), true); - if ( msg.filter === '!' ) { break; } - vAPI.MessagingConnection.sendTo(epickerConnectionId, { - what: 'filterResultset', - resultset: resultset.map(a => { - const o = Object.assign({}, a); - o.elem = undefined; - return o; - }), - }); - break; - } - default: - break; + case 'start': + startPicker(); + if ( targetElements.length === 0 ) { + highlightElements([], true); + } + break; + case 'dialogCreate': + filterToDOMInterface.queryAll(msg); + filterToDOMInterface.preview(true, true); + quitPicker(); + break; + case 'dialogSetFilter': { + const resultset = filterToDOMInterface.queryAll(msg); + highlightElements(resultset.map(a => a.elem), true); + if ( msg.filter === '!' ) { break; } + vAPI.MessagingConnection.sendTo(epickerConnectionId, { + what: 'filterResultset', + resultset: resultset.map(a => { + const o = Object.assign({}, a); + o.elem = undefined; + return o; + }), + }); + 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: + break; } }; /******************************************************************************/ const onConnectionMessage = function(msg) { - if ( - msg.from !== `epickerDialog-${epickerId}` || - msg.to !== `epicker-${epickerId}` - ) { - return; - } + if ( msg.from !== `epickerDialog-${epickerId}` ) { return; } switch ( msg.what ) { - case 'connectionRequested': - epickerConnectionId = msg.id; - return true; - case 'connectionBroken': - stopPicker(); - break; - case 'connectionMessage': - onDialogMessage(msg.payload); - break; + case 'connectionRequested': + epickerConnectionId = msg.id; + return true; + case 'connectionBroken': + quitPicker(); + break; + case 'connectionMessage': + onDialogMessage(msg.payload); + break; } }; /******************************************************************************/ -pickerRoot = document.createElement('iframe'); -pickerRoot.setAttribute(vAPI.sessionId, ''); +// epicker-ui.html will be injected in the page through an iframe, and +// 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 = [ 'background: transparent', 'border: 0', 'border-radius: 0', 'box-shadow: none', 'display: block', - 'height: 100%', + 'height: 100vh', 'left: 0', 'margin: 0', 'max-height: none', @@ -1351,6 +1240,7 @@ const pickerCSSStyle = [ 'opacity: 1', 'outline: 0', 'padding: 0', + 'pointer-events: auto', 'position: fixed', 'top: 0', 'visibility: visible', @@ -1358,107 +1248,40 @@ const pickerCSSStyle = [ 'z-index: 2147483647', '' ].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 = ` -[${vAPI.sessionId}] { +:root [${vAPI.sessionId}] { ${pickerCSSStyle} } -[${vAPI.sessionId}-clickblind] { +:root [${vAPI.sessionId}-clickblind] { 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.apply(); -vAPI.shutdown.add(stopPicker); - -// https://github.com/gorhill/uBlock/issues/3497 -// https://github.com/uBlockOrigin/uBlock-issues/issues/1215 -// Instantiate isolated element picker dialog. -if ( pickerBootArgs.zap === true ) { - startPicker(); - return; -} +pickerRoot = document.createElement('iframe'); +pickerRoot.setAttribute(vAPI.sessionId, ''); +document.documentElement.append(pickerRoot); // https://github.com/gorhill/uBlock/issues/2060 -vAPI.domFilterer.excludeNode(pickerRoot); +if ( vAPI.domFilterer instanceof Object ) { + vAPI.domFilterer.excludeNode(pickerRoot); +} + +vAPI.shutdown.add(quitPicker); -if ( await vAPI.messaging.extend() !== true ) { return; } 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; +} /******************************************************************************/ diff --git a/src/web_accessible_resources/epicker-dialog.html b/src/web_accessible_resources/epicker-ui.html similarity index 90% rename from src/web_accessible_resources/epicker-dialog.html rename to src/web_accessible_resources/epicker-ui.html index d87b46566..b1f4b4862 100644 --- a/src/web_accessible_resources/epicker-dialog.html +++ b/src/web_accessible_resources/epicker-ui.html @@ -4,7 +4,7 @@ uBlock Origin Element Picker - + @@ -35,13 +35,14 @@ + - +