长按删除网页元素(v1.2 高亮+外部关闭+拦截原生菜单)

安卓浏览器:长按元素弹出菜单(删除/取消),带高亮预览、点击外部关闭、阻止原生长按菜单优先显示。

当前为 2025-09-20 提交的版本,查看 最新版本

// ==UserScript==
// @name         长按删除网页元素(v1.2 高亮+外部关闭+拦截原生菜单)
// @namespace    custom-longpress-delete
// @version      1.2
// @description  安卓浏览器:长按元素弹出菜单(删除/取消),带高亮预览、点击外部关闭、阻止原生长按菜单优先显示。
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const LONGPRESS_DELAY = 600;      // 长按触发时间(ms)
    const SUPPRESS_MS_AFTER_HIDE = 350; // 菜单被隐藏后短暂禁止再次触发,避免滑动停止后复现
    const MAX_Z = 2147483647;

    let longPressTimer = null;
    let pressedElement = null;
    const originalStyles = new WeakMap(); // 保存每个元素的原始样式
    let menuVisible = false;
    let touchMoved = false;
    let suppressLongPress = false;
    let overlay = null;
    let lastTouchX = 0, lastTouchY = 0;

    // ====== 创建菜单 ======
    const menu = document.createElement('div');
    menu.id = 'longpress-menu';
    Object.assign(menu.style, {
        position: 'fixed',
        display: 'none',
        padding: '10px 14px',
        borderRadius: '10px',
        background: 'rgba(0,0,0,0.88)',
        color: '#fff',
        fontSize: '16px',
        zIndex: String(MAX_Z),
        userSelect: 'none',
        WebkitUserSelect: 'none',
        WebkitTouchCallout: 'none',
        boxShadow: '0 6px 18px rgba(0,0,0,0.35)',
        minWidth: '130px',
        textAlign: 'center',
        transform: 'translate(-50%, -120%)'
    });

    const delBtn = document.createElement('div');
    delBtn.innerText = '删除元素';
    delBtn.style.padding = '8px 0';
    delBtn.style.cursor = 'pointer';
    delBtn.addEventListener('click', function (ev) {
        ev.stopPropagation();
        if (pressedElement) {
            try { pressedElement.remove(); } catch (err) { /* ignore */ }
        }
        hideMenu();
    }, true);

    const cancelBtn = document.createElement('div');
    cancelBtn.innerText = '取消';
    cancelBtn.style.padding = '8px 0';
    cancelBtn.style.cursor = 'pointer';
    cancelBtn.style.marginTop = '6px';
    cancelBtn.style.borderTop = '1px solid rgba(255,255,255,0.08)';
    cancelBtn.addEventListener('click', function (ev) {
        ev.stopPropagation();
        hideMenu();
    }, true);

    menu.appendChild(delBtn);
    menu.appendChild(cancelBtn);
    document.body.appendChild(menu);

    // ====== 创建/管理全屏透明 overlay(用于拦截原生菜单 & 点击外部关闭) ======
    function ensureOverlay() {
        if (!overlay) {
            overlay = document.createElement('div');
            overlay.id = 'longpress-overlay';
            Object.assign(overlay.style, {
                position: 'fixed',
                top: '0',
                left: '0',
                width: '100%',
                height: '100%',
                background: 'transparent',
                zIndex: String(MAX_Z - 1),
                pointerEvents: 'none', // 开始时不拦截事件,只有在菜单显示后启用
                WebkitUserSelect: 'none',
                WebkitTouchCallout: 'none',
                touchAction: 'none'
            });
            // 点击 overlay 表示点击菜单外,收起菜单
            overlay.addEventListener('touchstart', function (e) {
                if (menuVisible && !e.target.closest('#longpress-menu')) {
                    e.preventDefault();
                    hideMenu();
                }
            }, { passive: false });

            // 阻止 overlay 上的 contextmenu(作为保险)
            overlay.addEventListener('contextmenu', function (e) {
                if (menuVisible) e.preventDefault();
            }, true);

            document.body.appendChild(overlay);
        }
        return overlay;
    }

    function enableOverlay() {
        const ov = ensureOverlay();
        ov.style.pointerEvents = 'auto';
    }
    function disableOverlay() {
        if (overlay) overlay.style.pointerEvents = 'none';
    }

    // 全局拦截 contextmenu(capture 阶段),当菜单可见时阻止浏览器原生菜单弹出
    document.addEventListener('contextmenu', function (e) {
        if (menuVisible) {
            e.preventDefault();
            e.stopPropagation();
        }
    }, true);

    // ====== 展示 / 收起 菜单 ======
    function showMenuAt(x, y) {
        // 位置调整:优先显示在触点上方,避免被手指遮挡
        let left = x;
        let top = y;
        // 初步放在 touch 点上方
        menu.style.left = left + 'px';
        menu.style.top = top + 'px';
        menu.style.display = 'block';
    }

    function showMenu() {
        if (!pressedElement) return;
        menuVisible = true;
        enableOverlay();
        menu.style.display = 'block';
    }

    function hideMenu() {
        if (!menuVisible) {
            // 也确保恢复样式(以防某些情况下未恢复)
            if (pressedElement && originalStyles.has(pressedElement)) {
                const orig = originalStyles.get(pressedElement);
                pressedElement.style.outline = orig.outline || '';
                pressedElement.style.outlineOffset = orig.outlineOffset || '';
                originalStyles.delete(pressedElement);
            }
            pressedElement = null;
            return;
        }
        menuVisible = false;
        menu.style.display = 'none';
        disableOverlay();

        if (pressedElement && originalStyles.has(pressedElement)) {
            const orig = originalStyles.get(pressedElement);
            pressedElement.style.outline = orig.outline || '';
            pressedElement.style.outlineOffset = orig.outlineOffset || '';
            originalStyles.delete(pressedElement);
        }
        pressedElement = null;

        // 短时禁止再次触发,避免滑动停止后又立刻复现
        suppressLongPress = true;
        setTimeout(() => { suppressLongPress = false; }, SUPPRESS_MS_AFTER_HIDE);

        clearTimeout(longPressTimer);
        longPressTimer = null;
    }

    // ====== 触摸事件监听(核心:长按检测 + 高亮) ======
    document.addEventListener('touchstart', function (e) {
        // 若正在短时禁止触发,则忽略
        if (suppressLongPress) return;
        // 不处理点中菜单本身的触摸(菜单的内部交互交由菜单处理)
        if (e.target.closest && e.target.closest('#longpress-menu')) return;

        touchMoved = false;
        const touch = e.touches && e.touches[0];
        if (touch) {
            lastTouchX = touch.clientX;
            lastTouchY = touch.clientY;
        } else {
            lastTouchX = lastTouchY = 0;
        }

        pressedElement = e.target;

        // 启动长按计时器
        longPressTimer = setTimeout(() => {
            // 当长按真正触发时,保存元素原始样式并高亮
            if (!pressedElement) return;
            // 保存原始 outline,以便恢复
            try {
                originalStyles.set(pressedElement, {
                    outline: pressedElement.style.outline || '',
                    outlineOffset: pressedElement.style.outlineOffset || ''
                });
                pressedElement.style.outline = '3px solid rgba(255,0,0,0.62)';
                pressedElement.style.outlineOffset = '-3px';
            } catch (err) {
                // ignore
            }

            // 展示菜单并启用 overlay 拦截原生菜单
            showMenuAt(lastTouchX || window.innerWidth / 2, lastTouchY || window.innerHeight / 2);
            enableOverlay();
            menuVisible = true;

            // 阻止浏览器的默认长按行为(尽可能)
            // 注意:preventDefault 可能影响滚动—这里只在真正触发长按时调用
            try { e.preventDefault(); } catch (err) { /* ignore */ }
        }, LONGPRESS_DELAY);
    }, { passive: false });

    document.addEventListener('touchmove', function (e) {
        touchMoved = true;
        // 只要移动就取消未完成的长按
        if (longPressTimer) {
            clearTimeout(longPressTimer);
            longPressTimer = null;
        }
        // 如果菜单已经显示,移动时应立即收起并短时禁止再次弹出
        if (menuVisible) {
            hideMenu();
        } else {
            // 更新触点坐标,以便长按触发时菜单能出现在最新位置
            const touch = e.touches && e.touches[0];
            if (touch) {
                lastTouchX = touch.clientX;
                lastTouchY = touch.clientY;
            }
        }
    }, { passive: true });

    document.addEventListener('touchend', function (e) {
        // 手指抬起前清计时器
        if (longPressTimer) {
            clearTimeout(longPressTimer);
            longPressTimer = null;
        }
        // 如果菜单已经显示且点击发生在菜单外(overlay 会拦截),overlay 的 touchstart 会触发 hideMenu
        // 这里额外处理一种情况:touchend 发生在页面(而不是 overlay/menu),也需要收起
        if (menuVisible) {
            const target = e.target;
            if (!(target && target.closest && target.closest('#longpress-menu'))) {
                hideMenu();
            }
        } else {
            // 未显示菜单时,重置 pressedElement
            pressedElement = null;
        }
    }, { passive: true });

    // touchcancel 也算是中断,直接收起
    document.addEventListener('touchcancel', function () {
        if (longPressTimer) {
            clearTimeout(longPressTimer);
            longPressTimer = null;
        }
        if (menuVisible) hideMenu();
        pressedElement = null;
    }, { passive: true });

    // 滚动时收起菜单,避免停下又复现
    window.addEventListener('scroll', function () {
        if (menuVisible) hideMenu();
    }, { passive: true });

    // 当页面大小变化时也收起 menu
    window.addEventListener('resize', function () {
        if (menuVisible) hideMenu();
    });

    // 防止菜单自身被网页的 pointer-events:none/transform 等影响(尽量)
    // (这里不做额外处理,仅作为说明)
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址