mirror of
https://github.com/gorhill/uBlock.git
synced 2026-03-11 09:04:36 +00:00
[mv3] Bring back element zapper
This commit is contained in:
parent
0f2a374585
commit
ab458b492a
12 changed files with 856 additions and 37 deletions
|
|
@ -12,6 +12,11 @@
|
|||
"service_worker": "/js/background.js",
|
||||
"type": "module"
|
||||
},
|
||||
"commands": {
|
||||
"enter-zapper-mode": {
|
||||
"description": "__MSG_zapperTipEnter__"
|
||||
}
|
||||
},
|
||||
"declarative_net_request": {
|
||||
"rule_resources": [
|
||||
]
|
||||
|
|
@ -51,6 +56,14 @@
|
|||
"<all_urls>"
|
||||
],
|
||||
"use_dynamic_url": true
|
||||
},
|
||||
{
|
||||
"resources": [
|
||||
"/zapper-ui.html"
|
||||
],
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -278,5 +278,13 @@
|
|||
"strictblockProceed": {
|
||||
"message": "Proceed",
|
||||
"description": "A button to navigate to the blocked page"
|
||||
},
|
||||
"zapperTipEnter": {
|
||||
"message": "Enter element zapper mode",
|
||||
"description": "Tooltip for the button used to enter zapper mode"
|
||||
},
|
||||
"zapperTipQuit": {
|
||||
"message": "Exit element zapper mode",
|
||||
"description": "Tooltip for the button used to exit zapper mode"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,13 +155,13 @@ body.needReload #refresh {
|
|||
}
|
||||
|
||||
.toolRibbon {
|
||||
align-items: center;
|
||||
align-items: start;
|
||||
background-color: var(--popup-toolbar-surface);
|
||||
display: grid;
|
||||
grid-auto-columns: 1fr;
|
||||
grid-auto-flow: column;
|
||||
grid-template: auto / repeat(5, 1fr);
|
||||
justify-items: center;
|
||||
grid-template: auto / repeat(4, 1fr);
|
||||
justify-items: stretch;
|
||||
white-space: normal;
|
||||
}
|
||||
.toolRibbon .tool {
|
||||
|
|
|
|||
50
platform/mv3/extension/css/zapper-ui.css
Normal file
50
platform/mv3/extension/css/zapper-ui.css
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
html#ubol-zapper,
|
||||
#ubol-zapper body {
|
||||
background: transparent;
|
||||
cursor: not-allowed;
|
||||
height: 100vh;
|
||||
height: 100svh;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
width: 100vw;
|
||||
}
|
||||
#ubol-zapper :focus {
|
||||
outline: none;
|
||||
}
|
||||
#ubol-zapper aside {
|
||||
background-color: var(--surface-1);
|
||||
box-sizing: border-box;
|
||||
cursor: default;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
}
|
||||
#ubol-zapper aside > #quit {
|
||||
fill: none;
|
||||
height: 4em;
|
||||
stroke: var(--ink-1);
|
||||
stroke-width: 3px;
|
||||
width: 4em;
|
||||
}
|
||||
#ubol-zapper svg#sea {
|
||||
cursor: crosshair;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
#ubol-zapper svg#sea > path:first-child {
|
||||
fill: rgba(0,0,0,0.5);
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
#ubol-zapper svg#sea > path + path {
|
||||
stroke: #FF0;
|
||||
stroke-width: 0.5px;
|
||||
fill: rgba(255,255,63,0.20);
|
||||
}
|
||||
#ubol-zapper #quit:hover {
|
||||
background-color: var(--surface-2)
|
||||
}
|
||||
|
|
@ -198,6 +198,20 @@ function onMessage(request, sender, callback) {
|
|||
return false;
|
||||
}
|
||||
|
||||
case 'removeCSS': {
|
||||
const tabId = sender?.tab?.id ?? false;
|
||||
const frameId = sender?.frameId ?? false;
|
||||
if ( tabId === false || frameId === false ) { return; }
|
||||
browser.scripting.removeCSS({
|
||||
css: request.css,
|
||||
origin: 'USER',
|
||||
target: { tabId, frameIds: [ frameId ] },
|
||||
}).catch(reason => {
|
||||
console.log(reason);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
@ -414,6 +428,23 @@ function onMessage(request, sender, callback) {
|
|||
|
||||
/******************************************************************************/
|
||||
|
||||
function onCommand(command, tab) {
|
||||
switch ( command ) {
|
||||
case 'enter-zapper-mode': {
|
||||
if ( browser.scripting === undefined ) { return; }
|
||||
browser.scripting.executeScript({
|
||||
files: [ '/js/scripting/zapper.js' ],
|
||||
target: { tabId: tab.id },
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
async function start() {
|
||||
await loadRulesetConfig();
|
||||
|
||||
|
|
@ -466,8 +497,6 @@ async function start() {
|
|||
});
|
||||
}
|
||||
|
||||
runtime.onMessage.addListener(onMessage);
|
||||
|
||||
if ( process.firstRun ) {
|
||||
const enableOptimal = await hasOmnipotence();
|
||||
if ( enableOptimal ) {
|
||||
|
|
@ -493,27 +522,56 @@ async function start() {
|
|||
if ( process.wakeupRun === false ) {
|
||||
adminReadEx('disabledFeatures');
|
||||
}
|
||||
|
||||
browser.permissions.onRemoved.addListener(onPermissionsRemoved);
|
||||
browser.permissions.onAdded.addListener(onPermissionsAdded);
|
||||
}
|
||||
|
||||
// https://github.com/uBlockOrigin/uBOL-home/issues/199
|
||||
// Force a restart of the extension once when an "internal error" occurs
|
||||
start().then(( ) => {
|
||||
localRemove('goodStart');
|
||||
return false;
|
||||
}).catch(reason => {
|
||||
console.trace(reason);
|
||||
if ( process.wakeupRun ) { return; }
|
||||
return localRead('goodStart').then(goodStart => {
|
||||
if ( goodStart === false ) {
|
||||
localRemove('goodStart');
|
||||
return false;
|
||||
}
|
||||
return localWrite('goodStart', false).then(( ) => true);
|
||||
/******************************************************************************/
|
||||
|
||||
const isFullyInitialized = new Promise(resolve => {
|
||||
// https://github.com/uBlockOrigin/uBOL-home/issues/199
|
||||
// Force a restart of the extension once when an "internal error" occurs
|
||||
start().then(( ) => {
|
||||
localRemove('goodStart');
|
||||
return false;
|
||||
}).catch(reason => {
|
||||
console.trace(reason);
|
||||
if ( process.wakeupRun ) { return; }
|
||||
return localRead('goodStart').then(goodStart => {
|
||||
if ( goodStart === false ) {
|
||||
localRemove('goodStart');
|
||||
return false;
|
||||
}
|
||||
return localWrite('goodStart', false).then(( ) => true);
|
||||
});
|
||||
}).then(restart => {
|
||||
if ( restart !== true ) { return; }
|
||||
runtime.reload();
|
||||
}).finally(( ) => {
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
|
||||
runtime.onMessage.addListener((request, sender, callback) => {
|
||||
isFullyInitialized.then(( ) => {
|
||||
const r = onMessage(request, sender, callback);
|
||||
if ( r !== true ) { callback(); }
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
browser.permissions.onRemoved.addListener((...args) => {
|
||||
isFullyInitialized.then(( ) => {
|
||||
onPermissionsRemoved(...args);
|
||||
});
|
||||
});
|
||||
|
||||
browser.permissions.onAdded.addListener((...args) => {
|
||||
isFullyInitialized.then(( ) => {
|
||||
onPermissionsAdded(...args);
|
||||
});
|
||||
});
|
||||
|
||||
browser.commands.onCommand.addListener((...args) => {
|
||||
isFullyInitialized.then(( ) => {
|
||||
onCommand(...args);
|
||||
});
|
||||
}).then(restart => {
|
||||
if ( restart !== true ) { return; }
|
||||
runtime.reload();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -319,6 +319,17 @@ dom.on('[data-i18n-title="popupTipDashboard"]', 'click', ev => {
|
|||
|
||||
/******************************************************************************/
|
||||
|
||||
dom.on('#gotoZapper', 'click', ( ) => {
|
||||
if ( browser.scripting === undefined ) { return; }
|
||||
browser.scripting.executeScript({
|
||||
files: [ '/js/scripting/zapper.js' ],
|
||||
target: { tabId: currentTab.id },
|
||||
});
|
||||
self.close();
|
||||
});
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
async function init() {
|
||||
const [ tab ] = await browser.tabs.query({
|
||||
active: true,
|
||||
|
|
@ -362,9 +373,10 @@ async function init() {
|
|||
isNaN(currentTab.id) === false
|
||||
);
|
||||
|
||||
dom.cl.toggle('#reportFilterIssue', 'enabled',
|
||||
/^https?:\/\//.test(url?.href)
|
||||
);
|
||||
const isNetworkPage = url.protocol === 'http:' || url.protocol === 'https:';
|
||||
|
||||
dom.cl.toggle('#reportFilterIssue', 'enabled', isNetworkPage );
|
||||
dom.cl.toggle('#gotoZapper', 'enabled', isNetworkPage);
|
||||
|
||||
const parent = qs$('#rulesetStats');
|
||||
for ( const details of popupPanelData.rulesetDetails || [] ) {
|
||||
|
|
|
|||
409
platform/mv3/extension/js/scripting/zapper.js
Normal file
409
platform/mv3/extension/js/scripting/zapper.js
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
/*******************************************************************************
|
||||
|
||||
uBlock Origin - a comprehensive, efficient content blocker
|
||||
Copyright (C) 2025-present Raymond Hill
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
|
||||
Home: https://github.com/gorhill/uBlock
|
||||
*/
|
||||
|
||||
(async ( ) => {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const zapper = self.uBOLZapper = self.uBOLZapper || {};
|
||||
|
||||
if ( zapper.injected ) { return; }
|
||||
zapper.injected = true;
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const sendMessage = msg => {
|
||||
try {
|
||||
chrome.runtime.sendMessage(msg).catch(( ) => { });
|
||||
} catch {
|
||||
}
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const zapperSecret = (( ) => {
|
||||
let secret = String.fromCharCode((Math.random() * 26) + 97);
|
||||
do {
|
||||
secret += (Math.floor(Math.random() * 2147483647) + 2147483647)
|
||||
.toString(36)
|
||||
.slice(2);
|
||||
} while ( secret.length < 8 );
|
||||
return secret;
|
||||
})();
|
||||
|
||||
const zapperCSSStyle = [
|
||||
'background: transparent',
|
||||
'border: 0',
|
||||
'border-radius: 0',
|
||||
'box-shadow: none',
|
||||
'color-scheme: light dark',
|
||||
'display: block',
|
||||
'filter: none',
|
||||
'height: 100vh',
|
||||
' height: 100svh',
|
||||
'left: 0',
|
||||
'margin: 0',
|
||||
'max-height: none',
|
||||
'max-width: none',
|
||||
'min-height: unset',
|
||||
'min-width: unset',
|
||||
'opacity: 1',
|
||||
'outline: 0',
|
||||
'padding: 0',
|
||||
'pointer-events: auto',
|
||||
'position: fixed',
|
||||
'top: 0',
|
||||
'transform: none',
|
||||
'visibility: hidden',
|
||||
'width: 100%',
|
||||
'z-index: 2147483647',
|
||||
''
|
||||
].join(' !important;\n');
|
||||
|
||||
const zapperCSS = `
|
||||
:root > [${zapperSecret}] {
|
||||
${zapperCSSStyle}
|
||||
}
|
||||
:root > [${zapperSecret}-loaded] {
|
||||
visibility: visible !important;
|
||||
}
|
||||
:root [${zapperSecret}-click] {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
`;
|
||||
|
||||
sendMessage({ what: 'insertCSS', css: zapperCSS });
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const getElementBoundingClientRect = function(elem) {
|
||||
let rect = typeof elem.getBoundingClientRect === 'function'
|
||||
? elem.getBoundingClientRect()
|
||||
: { height: 0, left: 0, top: 0, width: 0 };
|
||||
|
||||
// https://github.com/gorhill/uBlock/issues/1024
|
||||
// Try not returning an empty bounding rect.
|
||||
if ( rect.width !== 0 && rect.height !== 0 ) {
|
||||
return rect;
|
||||
}
|
||||
if ( elem.shadowRoot instanceof DocumentFragment ) {
|
||||
return getElementBoundingClientRect(elem.shadowRoot);
|
||||
}
|
||||
|
||||
let left = rect.left,
|
||||
right = left + rect.width,
|
||||
top = rect.top,
|
||||
bottom = top + rect.height;
|
||||
|
||||
for ( const child of elem.children ) {
|
||||
rect = getElementBoundingClientRect(child);
|
||||
if ( rect.width === 0 || rect.height === 0 ) { continue; }
|
||||
if ( rect.left < left ) { left = rect.left; }
|
||||
if ( rect.right > right ) { right = rect.right; }
|
||||
if ( rect.top < top ) { top = rect.top; }
|
||||
if ( rect.bottom > bottom ) { bottom = rect.bottom; }
|
||||
}
|
||||
|
||||
return {
|
||||
bottom,
|
||||
height: bottom - top,
|
||||
left,
|
||||
right,
|
||||
top,
|
||||
width: right - left
|
||||
};
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const highlightElement = function(elem) {
|
||||
if ( elem !== true ) {
|
||||
if ( elem !== undefined ) {
|
||||
if ( elem === highlightElement.current ) { return; }
|
||||
if ( elem !== zapperFrame ) {
|
||||
highlightElement.current = elem;
|
||||
}
|
||||
}
|
||||
}
|
||||
elem = highlightElement.current;
|
||||
|
||||
const ow = self.innerWidth;
|
||||
const oh = self.innerHeight;
|
||||
const islands = [];
|
||||
|
||||
if ( elem !== null ) {
|
||||
const rect = getElementBoundingClientRect(elem);
|
||||
// Ignore offscreen areas
|
||||
if (
|
||||
rect.left <= ow && rect.top <= oh &&
|
||||
rect.left + rect.width >= 0 && rect.top + rect.height >= 0
|
||||
) {
|
||||
islands.push(
|
||||
`M${rect.left} ${rect.top}h${rect.width}v${rect.height}h-${rect.width}z`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
zapperFramePort.postMessage({
|
||||
what: 'svgPaths',
|
||||
ocean: `M0 0h${ow}v${oh}h-${ow}z`,
|
||||
islands: islands.join(''),
|
||||
});
|
||||
};
|
||||
highlightElement.current = null;
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
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 ( !zapperFrame ) { return null; }
|
||||
const magicAttr = `${zapperSecret}-click`;
|
||||
zapperFrame.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
|
||||
zapperFrame.removeAttribute(magicAttr);
|
||||
return elem;
|
||||
};
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const highlightElementAtPoint = function(mx, my) {
|
||||
const elem = elementFromPoint(mx, my);
|
||||
highlightElement(elem);
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// https://www.reddit.com/r/uBlockOrigin/comments/bktxtb/scrolling_doesnt_work/emn901o
|
||||
// Override 'fixed' position property on body element if present.
|
||||
|
||||
// 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 ) {
|
||||
highlightElement(elem);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let elemToRemove = highlightElement.current;
|
||||
if ( elemToRemove === null && mx !== undefined ) {
|
||||
elemToRemove = elementFromPoint(mx, my);
|
||||
}
|
||||
|
||||
if ( elemToRemove instanceof Element === false ) { return; }
|
||||
|
||||
const getStyleValue = (elem, prop) => {
|
||||
const style = window.getComputedStyle(elem);
|
||||
return style ? style[prop] : '';
|
||||
};
|
||||
|
||||
// Heuristic to detect scroll-locking: remove such lock when detected.
|
||||
let maybeScrollLocked = elemToRemove.shadowRoot instanceof DocumentFragment;
|
||||
if ( maybeScrollLocked === false ) {
|
||||
let elem = elemToRemove;
|
||||
do {
|
||||
maybeScrollLocked =
|
||||
parseInt(getStyleValue(elem, 'zIndex'), 10) >= 1000 ||
|
||||
getStyleValue(elem, 'position') === 'fixed';
|
||||
elem = elem.parentElement;
|
||||
} while ( elem !== null && maybeScrollLocked === false );
|
||||
}
|
||||
if ( maybeScrollLocked ) {
|
||||
const doc = document;
|
||||
if ( getStyleValue(doc.body, 'overflowY') === 'hidden' ) {
|
||||
doc.body.style.setProperty('overflow', 'auto', 'important');
|
||||
}
|
||||
if ( getStyleValue(doc.body, 'position') === 'fixed' ) {
|
||||
doc.body.style.setProperty('position', 'initial', 'important');
|
||||
}
|
||||
if ( getStyleValue(doc.documentElement, 'position') === 'fixed' ) {
|
||||
doc.documentElement.style.setProperty('position', 'initial', 'important');
|
||||
}
|
||||
if ( getStyleValue(doc.documentElement, 'overflowY') === 'hidden' ) {
|
||||
doc.documentElement.style.setProperty('overflow', 'auto', 'important');
|
||||
}
|
||||
}
|
||||
elemToRemove.remove();
|
||||
highlightElementAtPoint(mx, my);
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onKeyPressed = function(ev) {
|
||||
// Delete
|
||||
if (
|
||||
(ev.key === 'Delete' || ev.key === 'Backspace') ) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
zapElementAtPoint();
|
||||
return;
|
||||
}
|
||||
// Esc
|
||||
if ( ev.key === 'Escape' || ev.which === 27 ) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
quitZapper();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// https://github.com/chrisaljoudi/uBlock/issues/190
|
||||
// May need to dynamically adjust the height of the overlay + new position
|
||||
// of highlighted elements.
|
||||
|
||||
const onViewportChanged = function() {
|
||||
highlightElement(true);
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const startZapper = function() {
|
||||
zapperFrame.focus();
|
||||
self.addEventListener('scroll', onViewportChanged, { passive: true });
|
||||
self.addEventListener('resize', onViewportChanged, { passive: true });
|
||||
self.addEventListener('keydown', onKeyPressed, true);
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const quitZapper = function() {
|
||||
self.removeEventListener('scroll', onViewportChanged, { passive: true });
|
||||
self.removeEventListener('resize', onViewportChanged, { passive: true });
|
||||
self.removeEventListener('keydown', onKeyPressed, true);
|
||||
if ( zapperFramePort ) {
|
||||
zapperFramePort.close();
|
||||
zapperFramePort.onmessage = null;
|
||||
zapperFramePort.onmessageerror = null;
|
||||
zapperFramePort = null;
|
||||
}
|
||||
if ( zapperFrame ) {
|
||||
zapperFrame.remove();
|
||||
zapperFrame = null;
|
||||
}
|
||||
sendMessage({ what: 'removeCSS', css: zapperCSS });
|
||||
zapper.injected = false;
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onFrameMessage = function(msg) {
|
||||
switch ( msg.what ) {
|
||||
case 'start':
|
||||
startZapper();
|
||||
if ( Boolean(zapperFramePort) === false ) { break; }
|
||||
highlightElement(true);
|
||||
break;
|
||||
case 'quitZapper':
|
||||
quitZapper();
|
||||
break;
|
||||
case 'highlightElementAtPoint':
|
||||
highlightElementAtPoint(msg.mx, msg.my);
|
||||
break;
|
||||
case 'unhighlight':
|
||||
highlightElement(null);
|
||||
break;
|
||||
case 'zapElementAtPoint':
|
||||
zapElementAtPoint(msg.mx, msg.my, msg.options);
|
||||
if ( msg.options.highlight !== true && msg.options.stay !== true ) {
|
||||
quitZapper();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// zapper-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 zapper.js is to install the zapper UI, and wait for the
|
||||
// component to establish a direct communication channel.
|
||||
//
|
||||
// When the zapper 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.
|
||||
|
||||
const bootstrap = async ( ) => {
|
||||
const url = new URL(chrome.runtime.getURL('/zapper-ui.html'));
|
||||
return new Promise(resolve => {
|
||||
const frame = document.createElement('iframe');
|
||||
frame.setAttribute(zapperSecret, '');
|
||||
document.documentElement.append(frame);
|
||||
frame.onload = ( ) => {
|
||||
frame.onload = null;
|
||||
frame.setAttribute(`${zapperSecret}-loaded`, '');
|
||||
const channel = new MessageChannel();
|
||||
const port = channel.port1;
|
||||
port.onmessage = ev => {
|
||||
onFrameMessage(ev.data || {});
|
||||
};
|
||||
port.onmessageerror = ( ) => {
|
||||
quitZapper();
|
||||
};
|
||||
frame.contentWindow.postMessage(
|
||||
{ what: 'zapperStart' },
|
||||
url.origin,
|
||||
[ channel.port2 ]
|
||||
);
|
||||
frame.contentWindow.focus();
|
||||
resolve({
|
||||
zapperFrame: frame,
|
||||
zapperFramePort: port,
|
||||
});
|
||||
};
|
||||
frame.contentWindow.location = url.href;
|
||||
});
|
||||
};
|
||||
|
||||
let { zapperFrame, zapperFramePort } = await bootstrap();
|
||||
if ( zapperFrame && zapperFramePort ) { return; }
|
||||
|
||||
quitZapper();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
})();
|
||||
|
||||
|
||||
void 0;
|
||||
|
|
@ -23,9 +23,16 @@ import { dom } from './dom.js';
|
|||
|
||||
/******************************************************************************/
|
||||
|
||||
const mql = self.matchMedia('(prefers-color-scheme: dark)');
|
||||
const theme = mql instanceof Object && mql.matches === true
|
||||
? 'dark'
|
||||
: 'light';
|
||||
dom.cl.toggle(dom.html, 'dark', theme === 'dark');
|
||||
dom.cl.toggle(dom.html, 'light', theme !== 'dark');
|
||||
{
|
||||
const mql = self.matchMedia('(prefers-color-scheme: dark)');
|
||||
const theme = mql instanceof Object && mql.matches === true
|
||||
? 'dark'
|
||||
: 'light';
|
||||
dom.cl.toggle(dom.html, 'dark', theme === 'dark');
|
||||
dom.cl.toggle(dom.html, 'light', theme !== 'dark');
|
||||
}
|
||||
|
||||
{
|
||||
const mql = self.matchMedia('(hover: hover)');
|
||||
dom.cl.toggle(dom.html, 'mobile', mql.matches !== true);
|
||||
}
|
||||
|
|
|
|||
226
platform/mv3/extension/js/zapper-ui.js
Normal file
226
platform/mv3/extension/js/zapper-ui.js
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
/*******************************************************************************
|
||||
|
||||
uBlock Origin - a comprehensive, efficient content blocker
|
||||
Copyright (C) 2025-present Raymond Hill
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
|
||||
Home: https://github.com/gorhill/uBlock
|
||||
*/
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const $id = id => document.getElementById(id);
|
||||
const $stor = selector => document.querySelector(selector);
|
||||
|
||||
const svgRoot = $stor('svg#sea');
|
||||
const svgOcean = svgRoot.children[0];
|
||||
const svgIslands = svgRoot.children[1];
|
||||
const NoPaths = 'M0 0';
|
||||
|
||||
let zapperScriptPort;
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onSvgClicked = function(ev) {
|
||||
// If zap mode, highlight element under mouse, this makes the zapper usable
|
||||
// on touch screens.
|
||||
zapperScriptPort.postMessage({
|
||||
what: 'zapElementAtPoint',
|
||||
mx: ev.clientX,
|
||||
my: ev.clientY,
|
||||
options: {
|
||||
stay: true,
|
||||
highlight: ev.target !== svgIslands,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
|
||||
Swipe right:
|
||||
Remove current highlight
|
||||
|
||||
*/
|
||||
|
||||
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; }
|
||||
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 ) { return; }
|
||||
if ( ev.cancelable ) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
// Swipe right.
|
||||
if ( svgIslands.getAttribute('d') === NoPaths ) { return; }
|
||||
zapperScriptPort.postMessage({
|
||||
what: 'unhighlight'
|
||||
});
|
||||
};
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const svgListening = (( ) => {
|
||||
let on = false;
|
||||
let timer;
|
||||
let mx = 0, my = 0;
|
||||
|
||||
const onTimer = ( ) => {
|
||||
timer = undefined;
|
||||
zapperScriptPort.postMessage({
|
||||
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 onKeyPressed = function(ev) {
|
||||
// Delete
|
||||
if ( ev.key === 'Delete' || ev.key === 'Backspace' ) {
|
||||
zapperScriptPort.postMessage({
|
||||
what: 'zapElementAtPoint',
|
||||
options: { stay: true },
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Esc
|
||||
if ( ev.key === 'Escape' || ev.which === 27 ) {
|
||||
quitZapper();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onScriptMessage = function(msg) {
|
||||
switch ( msg.what ) {
|
||||
case 'svgPaths': {
|
||||
let { ocean, islands } = msg;
|
||||
ocean += islands;
|
||||
svgOcean.setAttribute('d', ocean);
|
||||
svgIslands.setAttribute('d', islands || NoPaths);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const startZapper = function(port) {
|
||||
zapperScriptPort = port;
|
||||
zapperScriptPort.onmessage = ev => {
|
||||
onScriptMessage(ev.data || {});
|
||||
};
|
||||
zapperScriptPort.onmessageerror = ( ) => {
|
||||
quitZapper();
|
||||
};
|
||||
zapperScriptPort.postMessage({ what: 'start' });
|
||||
self.addEventListener('keydown', onKeyPressed, true);
|
||||
$stor('svg#sea').addEventListener('click', onSvgClicked);
|
||||
$stor('svg#sea').addEventListener('touchstart', onSvgTouch, { passive: true });
|
||||
$stor('svg#sea').addEventListener('touchend', onSvgTouch);
|
||||
$id('quit').addEventListener('click', quitZapper );
|
||||
svgListening(true);
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const quitZapper = function() {
|
||||
self.removeEventListener('keydown', onKeyPressed, true);
|
||||
$stor('svg#sea').removeEventListener('click', onSvgClicked);
|
||||
$stor('svg#sea').removeEventListener('touchstart', onSvgTouch, { passive: true });
|
||||
$stor('svg#sea').removeEventListener('touchend', onSvgTouch);
|
||||
$id('quit').removeEventListener('click', quitZapper );
|
||||
svgListening(false);
|
||||
if ( zapperScriptPort ) {
|
||||
zapperScriptPort.postMessage({ what: 'quitZapper' });
|
||||
zapperScriptPort.close();
|
||||
zapperScriptPort.onmessage = null;
|
||||
zapperScriptPort.onmessageerror = null;
|
||||
zapperScriptPort = null;
|
||||
}
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// Wait for the content script to establish communication
|
||||
|
||||
globalThis.addEventListener('message', ev => {
|
||||
const msg = ev.data || {};
|
||||
if ( msg.what !== 'zapperStart' ) { return; }
|
||||
if ( Array.isArray(ev.ports) === false ) { return; }
|
||||
if ( ev.ports.length === 0 ) { return; }
|
||||
startZapper(ev.ports[0]);
|
||||
}, { once: true });
|
||||
|
||||
/******************************************************************************/
|
||||
|
|
@ -27,9 +27,7 @@
|
|||
<!-- -------- -->
|
||||
<div id="filteringModeText"><label data-i18n="popupFilteringModeLabel">_</label><br><span>_</span><span></span></div>
|
||||
<div class="toolRibbon pageTools">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span id="gotoZapper" class="fa-icon tool enabled" data-i18n-title="zapperTipEnter">bolt<span class="caption" data-i18n="zapperTipEnter"></span></span>
|
||||
<span id="showMatchedRules" class="fa-icon tool" tabindex="0" title="Show matched rules">list-alt<span class="caption">Show matched rules</span></span>
|
||||
<span id="reportFilterIssue" class="fa-icon tool enabled" data-i18n-title="popupTipReport">comment-alt<span class="caption" data-i18n="popupTipReport"></span></span>
|
||||
<span id="gotoDashboard" class="fa-icon tool enabled" tabindex="0" data-i18n-title="popupTipDashboard">cogs<span class="caption" data-i18n="popupTipDashboard"></span></span>
|
||||
|
|
|
|||
25
platform/mv3/extension/zapper-ui.html
Normal file
25
platform/mv3/extension/zapper-ui.html
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE html>
|
||||
<html id="ubol-zapper">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>uBO Lite Zapper</title>
|
||||
<link rel="stylesheet" href="/css/default.css">
|
||||
<link rel="stylesheet" href="/css/zapper-ui.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<aside style="right: 2px; bottom: 2px;">
|
||||
<div id="quit" data-i18n-title="zapperTipQuit">
|
||||
<svg viewBox="0 0 64 64"><path d="M16 16L48 48M16 48L48 16" /></svg>
|
||||
</div>
|
||||
</aside>
|
||||
<svg id="sea"><path d></path><path d></path></svg>
|
||||
|
||||
<script src="js/theme.js" type="module"></script>
|
||||
<script src="js/i18n.js" type="module"></script>
|
||||
<script src="/js/zapper-ui.js" type="module"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -22,6 +22,11 @@
|
|||
"strict_min_version": "128.0"
|
||||
}
|
||||
},
|
||||
"commands": {
|
||||
"enter-zapper-mode": {
|
||||
"description": "__MSG_zapperTipEnter__"
|
||||
}
|
||||
},
|
||||
"declarative_net_request": {
|
||||
"rule_resources": [
|
||||
]
|
||||
|
|
@ -59,6 +64,14 @@
|
|||
"matches": [
|
||||
"<all_urls>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resources": [
|
||||
"/zapper-ui.html"
|
||||
],
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue