Files
testing-tool/entrypoints/rightClickRestorer.content.ts
T
雨霖铃 fa0f1bdcad feat: add right-click restorer for current site
- Add RightClickRestorer page with active unlock trigger
- Content script with dual-world strategy (isolated + main world)
- Monkey patch preventDefault/stopPropagation via background executeScript
- Overlay penetration for pointer-events blocking layers
- Delegated main-world injection to avoid CSP restrictions
- Async message handling for reliable state updates
- i18n translations (zh/en) and unit tests
2026-05-23 12:39:14 +08:00

195 lines
5.1 KiB
TypeScript

import { MessageAction, onMessage, sendMessage } from '@/utils/messages';
const BADGE_ID = 'testing-tools-right-click-restorer-badge';
const BADGE_STYLE_ID = 'testing-tools-right-click-restorer-badge-style';
let isRestored = false;
function updateBadge(): void {
const badge = document.getElementById(BADGE_ID);
if (badge) {
badge.style.opacity = isRestored ? '1' : '0';
}
}
function createBadge(): void {
if (document.getElementById(BADGE_ID)) return;
if (!document.getElementById(BADGE_STYLE_ID)) {
const style = document.createElement('style');
style.id = BADGE_STYLE_ID;
style.textContent = `
#${BADGE_ID} {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 2147483646;
padding: 6px 12px;
background: #2e7d32;
color: #ffffff;
border-radius: 20px;
font-size: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
opacity: 0;
transition: opacity 0.2s ease-out;
pointer-events: none;
user-select: none;
}
@media (prefers-color-scheme: dark) {
#${BADGE_ID} {
background: #4caf50;
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
}
}
`;
document.head.appendChild(style);
}
const badge = document.createElement('div');
badge.id = BADGE_ID;
badge.textContent = '\u53f3\u952e\u5df2\u89e3\u9501';
document.body.appendChild(badge);
}
/* ======================== Main World 注入 ======================== */
async function injectMainWorldScript(): Promise<void> {
try {
const response = await sendMessage(MessageAction.INJECT_MAIN_WORLD_SCRIPT);
if (!response.success) {
console.error('[RightClickRestorer] Background injection failed:', response.message);
}
} catch (err) {
console.error('[RightClickRestorer] Failed to request main world injection:', err);
}
}
/* ======================== Isolated World 事件拦截 ======================== */
function installEventIntercepts(): void {
window.addEventListener(
'contextmenu',
(e) => {
if (!isRestored) return;
e.stopPropagation();
},
true,
);
// 部分网站在 mousedown 阶段阻止 contextmenu
window.addEventListener(
'mousedown',
(e) => {
if (!isRestored) return;
if (e.button === 2 || (e.buttons & 2) !== 0) {
e.stopPropagation();
}
},
true,
);
}
/* ======================== 遮罩层穿透 ======================== */
const MEDIA_TAGS = new Set(['IMG', 'VIDEO', 'CANVAS', 'SVG']);
function initMousePenetration(): void {
window.addEventListener(
'mousedown',
(e) => {
if (!isRestored || e.button !== 2) return;
const elements = document.elementsFromPoint(e.clientX, e.clientY);
if (!elements.length) return;
let targetMedia: HTMLElement | null = null;
for (const el of elements) {
if (MEDIA_TAGS.has(el.tagName)) {
targetMedia = el as HTMLElement;
break;
}
}
if (!targetMedia) return;
const modified: Array<{ el: HTMLElement; original: string | null }> = [];
let foundMedia = false;
for (const el of elements) {
const htmlEl = el as HTMLElement;
if (el === targetMedia) {
foundMedia = true;
const original = htmlEl.style.pointerEvents || null;
htmlEl.style.setProperty('pointer-events', 'all', 'important');
modified.push({ el: htmlEl, original });
continue;
}
if (!foundMedia) {
const original = htmlEl.style.pointerEvents || null;
htmlEl.style.setProperty('pointer-events', 'none', 'important');
modified.push({ el: htmlEl, original });
}
}
setTimeout(() => {
for (const { el, original } of modified) {
if (original === null || original === '') {
el.style.removeProperty('pointer-events');
} else {
el.style.pointerEvents = original;
}
}
}, 300);
},
true,
);
}
/* ======================== 激活保护 ======================== */
async function activateProtection(): Promise<void> {
if (isRestored) return;
await injectMainWorldScript();
// 再次检查,防止并发调用导致重复注册
if (isRestored) return;
installEventIntercepts();
initMousePenetration();
isRestored = true;
updateBadge();
}
/* ======================== 消息通信 ======================== */
function initMessaging(): void {
onMessage(MessageAction.RESTORE_RIGHT_CLICK, async () => {
await activateProtection();
return { success: true, restored: isRestored };
});
onMessage(MessageAction.QUERY_RIGHT_CLICK_STATUS, () => {
return { success: true, restored: isRestored };
});
}
/* ======================== 入口 ======================== */
export default defineContentScript({
matches: ['<all_urls>'],
runAt: 'document_start',
main() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createBadge, { once: true });
} else {
createBadge();
}
initMessaging();
},
});