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",
|
"service_worker": "/js/background.js",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
|
"commands": {
|
||||||
|
"enter-zapper-mode": {
|
||||||
|
"description": "__MSG_zapperTipEnter__"
|
||||||
|
}
|
||||||
|
},
|
||||||
"declarative_net_request": {
|
"declarative_net_request": {
|
||||||
"rule_resources": [
|
"rule_resources": [
|
||||||
]
|
]
|
||||||
|
|
@ -51,6 +56,14 @@
|
||||||
"<all_urls>"
|
"<all_urls>"
|
||||||
],
|
],
|
||||||
"use_dynamic_url": true
|
"use_dynamic_url": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resources": [
|
||||||
|
"/zapper-ui.html"
|
||||||
|
],
|
||||||
|
"matches": [
|
||||||
|
"<all_urls>"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -278,5 +278,13 @@
|
||||||
"strictblockProceed": {
|
"strictblockProceed": {
|
||||||
"message": "Proceed",
|
"message": "Proceed",
|
||||||
"description": "A button to navigate to the blocked page"
|
"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 {
|
.toolRibbon {
|
||||||
align-items: center;
|
align-items: start;
|
||||||
background-color: var(--popup-toolbar-surface);
|
background-color: var(--popup-toolbar-surface);
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-columns: 1fr;
|
grid-auto-columns: 1fr;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
grid-template: auto / repeat(5, 1fr);
|
grid-template: auto / repeat(4, 1fr);
|
||||||
justify-items: center;
|
justify-items: stretch;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
.toolRibbon .tool {
|
.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;
|
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:
|
default:
|
||||||
break;
|
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() {
|
async function start() {
|
||||||
await loadRulesetConfig();
|
await loadRulesetConfig();
|
||||||
|
|
||||||
|
|
@ -466,8 +497,6 @@ async function start() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
runtime.onMessage.addListener(onMessage);
|
|
||||||
|
|
||||||
if ( process.firstRun ) {
|
if ( process.firstRun ) {
|
||||||
const enableOptimal = await hasOmnipotence();
|
const enableOptimal = await hasOmnipotence();
|
||||||
if ( enableOptimal ) {
|
if ( enableOptimal ) {
|
||||||
|
|
@ -493,27 +522,56 @@ async function start() {
|
||||||
if ( process.wakeupRun === false ) {
|
if ( process.wakeupRun === false ) {
|
||||||
adminReadEx('disabledFeatures');
|
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(( ) => {
|
const isFullyInitialized = new Promise(resolve => {
|
||||||
localRemove('goodStart');
|
// https://github.com/uBlockOrigin/uBOL-home/issues/199
|
||||||
return false;
|
// Force a restart of the extension once when an "internal error" occurs
|
||||||
}).catch(reason => {
|
start().then(( ) => {
|
||||||
console.trace(reason);
|
localRemove('goodStart');
|
||||||
if ( process.wakeupRun ) { return; }
|
return false;
|
||||||
return localRead('goodStart').then(goodStart => {
|
}).catch(reason => {
|
||||||
if ( goodStart === false ) {
|
console.trace(reason);
|
||||||
localRemove('goodStart');
|
if ( process.wakeupRun ) { return; }
|
||||||
return false;
|
return localRead('goodStart').then(goodStart => {
|
||||||
}
|
if ( goodStart === false ) {
|
||||||
return localWrite('goodStart', false).then(( ) => true);
|
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() {
|
async function init() {
|
||||||
const [ tab ] = await browser.tabs.query({
|
const [ tab ] = await browser.tabs.query({
|
||||||
active: true,
|
active: true,
|
||||||
|
|
@ -362,9 +373,10 @@ async function init() {
|
||||||
isNaN(currentTab.id) === false
|
isNaN(currentTab.id) === false
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.cl.toggle('#reportFilterIssue', 'enabled',
|
const isNetworkPage = url.protocol === 'http:' || url.protocol === 'https:';
|
||||||
/^https?:\/\//.test(url?.href)
|
|
||||||
);
|
dom.cl.toggle('#reportFilterIssue', 'enabled', isNetworkPage );
|
||||||
|
dom.cl.toggle('#gotoZapper', 'enabled', isNetworkPage);
|
||||||
|
|
||||||
const parent = qs$('#rulesetStats');
|
const parent = qs$('#rulesetStats');
|
||||||
for ( const details of popupPanelData.rulesetDetails || [] ) {
|
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
|
const mql = self.matchMedia('(prefers-color-scheme: dark)');
|
||||||
? 'dark'
|
const theme = mql instanceof Object && mql.matches === true
|
||||||
: 'light';
|
? 'dark'
|
||||||
dom.cl.toggle(dom.html, 'dark', theme === 'dark');
|
: 'light';
|
||||||
dom.cl.toggle(dom.html, 'light', theme !== 'dark');
|
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 id="filteringModeText"><label data-i18n="popupFilteringModeLabel">_</label><br><span>_</span><span></span></div>
|
||||||
<div class="toolRibbon pageTools">
|
<div class="toolRibbon pageTools">
|
||||||
<span></span>
|
<span id="gotoZapper" class="fa-icon tool enabled" data-i18n-title="zapperTipEnter">bolt<span class="caption" data-i18n="zapperTipEnter"></span></span>
|
||||||
<span></span>
|
|
||||||
<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="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="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>
|
<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"
|
"strict_min_version": "128.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"commands": {
|
||||||
|
"enter-zapper-mode": {
|
||||||
|
"description": "__MSG_zapperTipEnter__"
|
||||||
|
}
|
||||||
|
},
|
||||||
"declarative_net_request": {
|
"declarative_net_request": {
|
||||||
"rule_resources": [
|
"rule_resources": [
|
||||||
]
|
]
|
||||||
|
|
@ -59,6 +64,14 @@
|
||||||
"matches": [
|
"matches": [
|
||||||
"<all_urls>"
|
"<all_urls>"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resources": [
|
||||||
|
"/zapper-ui.html"
|
||||||
|
],
|
||||||
|
"matches": [
|
||||||
|
"<all_urls>"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue