Greasy Fork

Greasy Fork is available in English.

随意筛选

自由选定页面元素进行筛选

当前为 2025-02-12 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        随意筛选
// @name:cn     随意筛选
// @name:en     FilterAnything
// @namespace   hzhbest
// @include     *://*/*
// @description    自由选定页面元素进行筛选
// @description:cn 自由选定页面元素进行筛选
// @description:en Filter any page elements with your free choice
// @version     1.2
// @run-at      document-end
// @license     GNU GPLv3
// ==/UserScript==

// 操作方式:鼠标指针指向筛选目标【个体】,按下激活组合键,再指向另一个筛选目标【个体】,程序自动识别这两【个体】的同一【父级】,并弹出过滤框,按其中文本筛选【个体】
// 高阶操作方式:按下激活组合键后,按住Ctrl 再点击的话,以两个体的共同父级为范围,筛选按下Ctrl 时首个个体的层级;
// 术语:【目标】——待筛选的【个体】元素,数个个体位于同一【父级】的下一层,筛选时以【目标】为单位
//    【父级】——包含数个【目标】的元素,筛选时【父级】显示外框表明待筛选的范围
//    【标记】——鼠标指针指向某个区域并按下激活快捷键以记录【目标】包含的某元素,供识别同一【父级】

// TODO 

(function () {

    'use strict';

    // SECTION - 自定义参数
    const activateKey = "C-M-a";    // 激活组合键(修饰键包括 C-:Ctrl; M-:Alt;最后一个字符为实键,大写则含Shift键)
    const drawmask = true;          // 是否绘制遮罩
    const usectrl = true;           // 使用Ctrl 扩展筛选范围
    // !SECTION


    //SECTION - 预设常量
    const _id = '___FilterAnything';       // 脚本ID
    const css = `
        /* --遮罩样式-- */
        #${_id}_maskt, #${_id}_maskp {pointer-events: none; position: fixed;
            display: none; z-index: 1000000; transition: top 0.5s, left 0.5s, width 0.5s, height 0.5s, outline 3s;}
        #${_id}_maskp {background: #6aa5e94a; border: 3px solid #3708584a;}
        #${_id}_maskt {background: #25d46b4a; border: 2px solid #0f94444a;}
        #${_id}_maskt.show,#${_id}_maskp.show {display: block;}
        #${_id}_maskt.clickable {pointer-events: auto !important;}
        /* --信息框样式-- */
        #${_id}_infobox {position: fixed; padding: 4px; display: none; border: 1px solid #000;
            background: #ffffff9b; color: #000; z-index: 1000001; font-size: 12pt;}
        #${_id}_infobox.show {display: block;}
        #${_id}_infobox>span.___pinfo, #${_id}_infobox>span.___cinfo {color: #0f9444;}
        #${_id}_infobox>span.___tinfo {color: #370858; font-weight: 800; text-decoration: underline;}
        /* --提示框样式-- */
        #${_id}_toptipbox {position: fixed; top: 3px; right: 3px; padding: 4px; display: none; border: 1px solid #000;
            background: #ffffffdf; color: #000; z-index: 1000002; font-size: 10pt;}
        #${_id}_toptipbox.show {display: block;}
        /* --目标元素样式-- */
        .${_id} {outline: 3px solid #8b62e3 !important; outline-offset: -4px;}
        /* --筛选文本框样式-- */
        #${_id}_filterbox {position: fixed; padding: 4px; display: none; border: 1px solid #2a0f63; background: white;
            z-index: 1000003; height:36px; min-width: 200px;}
        #${_id}_filterbox.show {display: block;}
        #${_id}_filterinput {max-width: 100%; height: 100%; border: none; outline: none; color: #000;
            font-size: 12pt; display: inline;}
        #${_id}_btnCloseFilter {display: inline; margin: 0 5px; height: 22px; width: 22px; 
            border: 1px solid #555; color: #555;}
        #${_id}_filtercountbox {display: inline; position: absolute; right: 40px; top: 13px;
            pointer-events: none; }
        /* --被筛选元素样式-- */
        .${_id} *.___filtered {display: none !important;}
    `;                        // 预设CSS
    const _txt = {
        menutxt: ['开始筛选', 'Start Filtering'],
        toptip: ['已标记首个元素,请--ifdraw--以标记第二个元素',
            'First element recorded, --ifdraw-- to mark the second element'],
        toptipifdraw: [['继续按激活组合键', 'press the activation key again'], ['点击', 'click']],
        errtip: ['未找到共同父元素或父元素为最顶层元素,程序终止',
            'No mutual parent element found or the parent element is the topmost element, program terminated'],
        exitip: ['已退出元素标记', 'Exit element recording'],
        filtip: ['输入即筛选,支持正则,Esc清空', 'Input to filter, support regex, Esc to clear']
    };

    //!SECTION


    // SECTION - 全局变量
    var filterTO, mouseMoveTO;
    var fisstMask, secondMask, parentMask, filterbox, filterinputbox, filtercountbox, toptipbox, btnCloseFilter;
    var mousePos = { x: 0, y: 0 }, isCtrlPressed = false;
    var detectStatus = 0;   // 0:未激活 1:已标记首个元素 2:已标记第二个元素
    var firstElem, secondElem, parentElem, parentRect, filterLv = 0;
    var preFelem, preSelem, prePelem, filteredElems, fecnt;
    //!SECTION


    //SECTION - 主程序
    // ~ 初始化
    addCSS(css, _id + '_css');  // 添加CSS
    //language detection
    const _L = (navigator.language.indexOf('zh-') == -1) ? 1 : 0;

    // ~ 动作监听:鼠标位置追踪
    document.addEventListener('mousemove', mouseMoveEvent);

    // ~ 动作监听:键盘按键
    document.addEventListener('keydown', keyhandler);
    document.addEventListener('keyup', (e) => {     // 监测Ctrl 松开,不然快捷键含Ctrl 的话会干扰
        if (e.key == "Control") {
            isCtrlPressed = false;
        }
    });

    // ~ 动作监听:鼠标点击
    if (drawmask) document.addEventListener('click', clickWithMask);


    //!SECTION


    // SECTION - 元素查找

    // ~ 快捷键激活,按顺序标记两个目标元素
    function getFilterTargetElems() {
        if (!toptipbox) toptipbox = creaElemIn('div', document.body);
        toptipbox.id = _id + '_toptipbox';

        switch (detectStatus) {
            case 0:                                     // 未激活状态→进入标记第一个元素阶段
                toptipbox.classList.add('show');
                toptipbox.innerHTML = _txt.toptip[_L].replace("--ifdraw--", _txt.toptipifdraw[_L][drawmask ? 0 : 1]);
                firstElem = findElemAt(mousePos);
                detectStatus = 1;
                filterLv = 1;
                break;
            case 1:                                     // 标记第一个元素状态→进入标记第二个元素阶段并筛选阶段
                secondElem = findElemAt(mousePos);
                parentElem = getMutualParent(firstElem, secondElem);
                if (parentElem.tagName == "BODY") {     // 若共同父元素为body则退出
                    exitFinding('errtip');
                } else {
                    startFiltering();
                }
                break;
        }
    }

    // ~ 点击标记第二个目标元素并筛选
    function clickWithMask(e) {
        if (drawmask && detectStatus == 1 && !!parentMask) {
            e.preventDefault();
            e.stopPropagation();
            startFiltering();
        }
    }

    // ~ 开始筛选
    function startFiltering() {
        detectStatus = 2;
        toptipbox.classList.remove('show');
        if (drawmask) {
            fisstMask.classList.toggle('show', false);
            secondMask.classList.toggle('show', false);
            parentMask.classList.toggle('show', false);
        }
        showFilterInputBox();
        window.addEventListener('scroll', updateFilterInputBox);
    }

    // ~ 跟进鼠标移动事件
    function mouseMoveEvent(e) {
        mousePos.x = e.clientX;
        mousePos.y = e.clientY;
        // 若绘制遮罩模式,则在标记第二个元素阶段绘制遮罩
        if (drawmask && detectStatus == 1) {
            if (!isCtrlPressed) preFelem = firstElem;               // 按下Ctrl 键时第一个元素保持之前扩展后的元素
            preSelem = findElemAt(mousePos);
            prePelem = getMutualParent(preFelem, preSelem);
            clearTimeout(mouseMoveTO);
            if (!!secondMask) makeMaskClickable(secondMask, false);
            if (prePelem.tagName !== "BODY") {
                if (!fisstMask) {
                    fisstMask = creaElemIn('div', document.body);
                    fisstMask.id = _id + '_maskt';
                }
                if (!secondMask) {
                    secondMask = creaElemIn('div', document.body);
                    secondMask.id = _id + '_maskt';
                }
                if (!parentMask) {
                    parentMask = creaElemIn('div', document.body);
                    parentMask.id = _id + '_maskp';
                }
                console.log('isCtrlPressed: ', isCtrlPressed);
                if (!isCtrlPressed) {
                    preFelem = getElemUntil(preFelem, prePelem);
                    preSelem = getElemUntil(preSelem, prePelem);
                } else {
                    console.log('preFelem: ', preFelem);
                    console.log('prePelem: ', prePelem);
                    filterLv = getLvCnt(preFelem, prePelem);        // 在按Ctrl 时第一个元素扩展后的元素为基础算层数
                    console.log('filterLv: ', filterLv);
                    preSelem = getElemUntil(preSelem, prePelem, filterLv);
                    console.log('preSelem: ', preSelem);
                }
                drawMask(getElemRect(preFelem), fisstMask);
                drawMask(getElemRect(preSelem), secondMask);
                drawMask(getElemRect(prePelem), parentMask);
                parentElem = prePelem;
                mouseMoveTO = setTimeout(makeMaskClickable,200,secondMask,true);
            }
        }
    }

    // ~ 查找坐标下的候选元素
    function findElemAt(pos) {
        var elem = document.elementFromPoint(pos.x, pos.y);
        if (elem.id.indexOf(_id + '_mask') == 0) {            // 若鼠标下的元素是遮罩的话,返回body,让getMutualParent也返回body
            return document.body;
        }
        return elem;
    }

    // ~ 查找两元素的最小共同父元素
    function getMutualParent(felem, selem) {
        if (selem.tagName == "BODY") {
            return document.body;
        }
        var pelem = felem.parentNode;
        while (!pelem.contains(selem)) {
            if (pelem.tagName == "BODY") {
                break;
            }
            pelem = pelem.parentNode;
        }
        return pelem;
    }

    // ~ 查找两元素的层数差,找不到则返回-1
    function getLvCnt(lowerElem, upperElem) {
        if (!upperElem.contains(lowerElem)) {
            return -1;
        }
        var lvcnt = 0
        while (lowerElem !== upperElem) {
            lvcnt += 1;
            lowerElem = lowerElem.parentNode;
        }
        return lvcnt;
    }

    // ~ 查找到距顶元素n层为止的父元素
    function getElemUntil(elem, topelem, lvcnt) {
        lvcnt = lvcnt || 1;
        var cnt = getLvCnt(elem, topelem) - lvcnt;
        while (cnt > 0) {
            cnt -= 1;
            elem = elem.parentNode;
        }
        return elem;
    }

    // ~ 查找顶元素下第n层的子元素
    function getElemsAtLv(topelem, lvcnt, elems) {
        elems = elems || [];
        if (lvcnt == 0) {
            elems.push(topelem);
        } else {
            [...topelem.childNodes].forEach((elem) => {
                if (elem.nodeType === 1) {
                    getElemsAtLv(elem, lvcnt - 1, elems);
                }
            });
        }
        return elems;
    }

    // ~ 退出查找元素
    function exitFinding(exittype) {
        toptipbox.innerHTML = _txt[exittype][_L];
        detectStatus = 0;
        if (drawmask) {
            fisstMask.classList.toggle('show', false);
            secondMask.classList.toggle('show', false);
            parentMask.classList.toggle('show', false);
        }
        setTimeout(() => {
            toptipbox.classList.remove('show');
        }, 3000);
    }

    // ~ 获取元素rect
    function getElemRect(elem) {
        var trect = getTrueSize(elem);               // 获取容器元素的trect
        if (!!trect) {                               // trect非false的话
            trect.visible = true;                    // 填入可见属性
            return trect;
        } else {
            var rect = {};
            rect.visible = false;                    // 否则不可见
            return rect;
        }
    }

    // ~ 绘制半透明遮罩
    function drawMask(rect, mask) {
        if (!rect.visible || !mask) {
            return;
        }
        mask.classList.toggle('show', true);
        mask.style = `
            top: ${rect.top}px; left: ${rect.left}px; 
            width: ${rect.right - rect.left}px; height: ${rect.bottom - rect.top}px;
        `;
    }

    // ~ 短暂使遮罩可点击
    function makeMaskClickable(mask, ison) {
        mask.classList.toggle("clickable", ison);
    }

    //!SECTION


    // SECTION - 元素过滤

    // ~ 创建筛选文本框
    function showFilterInputBox() {
        if (detectStatus !== 2) {
            return;
        }
        if (!filterinputbox) {
            filterbox = creaElemIn('div', document.body);
            filterbox.id = _id + '_filterbox';
            filterinputbox = creaElemIn('input', filterbox);
            filterinputbox.type = 'text';
            filterinputbox.id = _id + '_filterinput';
            filterinputbox.placeholder = _txt.filtip[_L];
            btnCloseFilter = creaElemIn('input', filterbox);
            btnCloseFilter.type = 'button';
            btnCloseFilter.value = 'X';
            btnCloseFilter.id = _id + '_btnCloseFilter';
            filtercountbox = creaElemIn('div', filterbox);
            filtercountbox.id = _id + '_filtercountbox';
            filterinputbox.addEventListener('input', filterEvent);
            filterinputbox.addEventListener('keydown', keyhandler);
            filterinputbox.addEventListener('focus', function () {
                filterinputbox.select();
            });
            btnCloseFilter.addEventListener('click', exitFilter);
        }
        filterbox.classList.add('show');
        filterinputbox.focus();
        parentElem.classList.add('___FilterAnything');
        filteredElems = getElemsAtLv(parentElem, filterLv);
        fecnt = filteredElems.length;
        updateFilterInputBox();
    }

    // ~ 更新筛选框位置
    function updateFilterInputBox() {
        parentRect = getElemRect(parentElem);
        var iright = Math.max(10, window.innerWidth - parentRect.right);
        var itop = Math.max(10, parentRect.top - 36);
        filterbox.style = `right: ${iright}px; top: ${itop}px;`;

        var chkFelems = getElemsAtLv(parentElem, filterLv);
        if (chkFelems.length !== fecnt) {
            filteredElems = chkFelems;
            fecnt = filteredElems.length;
            filterEvent();
        }
    }

    // ~ 随输入筛选
    function filterEvent() {
        clearTimeout(filterTO);
        filterTO = setTimeout(filterElem, 500, filterinputbox.value);
    }

    // ~ 筛选元素
    function filterElem(strf) {
        var words = [], wordstmp = [];     // 关键词数组
        strf = strf.trim(); // 去除首尾空格
        var filteredcnt = 0;
        if (strf.length > 0) {
            var erg = strf.match(new RegExp("^ ?/(.+)/([gim]+)?$"));	// 判别是否正则表达式
            if (erg) {
                var ew = erg[1], flag = erg[2] || '';	// 提取出正则表达式的表达式部分和标记部分
                words = [{
                    text: ew,
                    exp: new RegExp(ew, flag)
                }];	//输出单元素数组,含正则对象
            } else {
                wordstmp = strf.split(' ');	// 按空格分割关键词
                wordstmp.forEach((word) => {
                    if (word) {
                        var t = word, ex = false;
                        if (t.indexOf("-") == 0) { // 拒绝符【-】;连字符【--】=“-”
                            if (t.indexOf("--") !== 0) {
                                ex = true;
                            }
                            t = t.slice(1);
                        }
                        words.push({
                            text: t,
                            exclude: ex
                        });	//输出多元素数组,无正则对象
                    }
                });
            }
            if (words.length > 0) {
                filteredElems.forEach((elem) => {
                    const tc = elem.textContent;                    // 兼顾大写(word有大写则只匹配大写)
                    // console.log('tc: ', tc);
                    const tcl = elem.textContent.toLowerCase();     // 同时匹配大小写(word仅小写则大小写都匹配)
                    const ismatched = words.every((word) => {
                        if (word.exp) {
                            return word.exp.test(tc) || word.exp.test(tcl);
                        } else {
                            // console.log('tc.includes(word.text): ', tc.includes(word.text));
                            var ism = tc.includes(word.text);
                            var isml = tcl.includes(word.text);
                            if (word.exclude) {
                                return !(ism || isml);
                            } else {
                                return ism || isml;
                            }
                        }
                    });
                    elem.classList.toggle('___filtered', !ismatched);
                    if (ismatched) filteredcnt++;
                    // console.log('elem: ', elem);
                });
                filtercountbox.innerHTML = `${filteredcnt}/${fecnt}`;
            }
        } else {
            filteredElems.forEach((elem) => {
                elem.classList.remove('___filtered');
                filtercountbox.innerHTML = "";
            });
        }
    }

    // ~ 退出筛选状态
    function exitFilter() {
        parentElem.classList.remove('___FilterAnything');
        filterbox.classList.remove('show');
        filterinputbox.value = "";
        filteredElems.forEach((elem) => {
            elem.classList.remove('___filtered');
        });
        parentElem = null;
        firstElem = null;
        secondElem = null;
        filteredElems = null;
        fecnt = 0;
        filterLv = 0;
        detectStatus = 0;
        window.removeEventListener('scroll', showFilterInputBox);
    }

    //!SECTION






    //SECTION - 通用功能

    /** ~ keyhandler(evt)
     * 接收击键事件,调用相应程序
     * @param {event} evt 键盘按键事件
     */
    function keyhandler(evt) {
        var fullkey = get_key(evt);
        // console.log('fullkey: ', fullkey);
        isCtrlPressed = false; // 重置Ctrl键状态,仅当标记第二个元素时可切换
        switch (fullkey) {
            case "Escape":
                evt.preventDefault();
                evt.stopPropagation();
                if (detectStatus == 1) {
                    exitFinding('exitip');
                } else if (detectStatus == 2) {
                    if (evt.target.id == _id + '_filterinput') {
                        evt.target.value = '';
                        filterEvent();
                    } else {
                        exitFilter();
                    }
                }
                break;
            case activateKey:
                getFilterTargetElems();
            case "C-Control":
                if (usectrl && detectStatus == 1) {
                    isCtrlPressed = true;
                }
        }
    }

    /** ~ get_key(evt)
     * 按键evt.which转换为键名
     * @param {event} evt 键盘按键事件
     * @returns {string} 按键键名
     */
    function get_key(evt) {
        const keyCodeStr = {			//key press 事件返回的which代码对应按键键名对应表对象
            8: 'BAC',
            9: 'TAB',
            10: 'RET',
            13: 'RET',
            27: 'ESC',
            33: 'PageUp',
            34: 'PageDown',
            35: 'End',
            36: 'Home',
            37: 'Left',
            38: 'Up',
            39: 'Right',
            40: 'Down',
            45: 'Insert',
            46: 'Delete',
            112: 'F1',
            113: 'F2',
            114: 'F3',
            115: 'F4',
            116: 'F5',
            117: 'F6',
            118: 'F7',
            119: 'F8',
            120: 'F9',
            121: 'F10',
            122: 'F11',
            123: 'F12'
        };
        const whichStr = {
            32: 'SPC'
        };
        var key = String.fromCharCode(evt.which),
            ctrl = evt.ctrlKey ? 'C-' : '',
            meta = (evt.metaKey || evt.altKey) ? 'M-' : '';
        if (!evt.shiftKey) {
            key = key.toLowerCase();
        }
        if (evt.ctrlKey && evt.which >= 186 && evt.which < 192) {
            key = String.fromCharCode(evt.which - 144);
        }
        if (evt.key && evt.key !== 'Enter' && !/^U\+/.test(evt.key)) {
            key = evt.key;
        } else if (evt.which !== evt.keyCode) {
            key = keyCodeStr[evt.keyCode] || whichStr[evt.which] || key;
        } else if (evt.which <= 32) {
            key = keyCodeStr[evt.keyCode] || whichStr[evt.which];
        }
        return ctrl + meta + key;
    }

    /** ~ creaElemIn(tagname, destin, spos, pos)
     * 在 destin 内创建元素 tagname,通过 spos{ "after", "before" } 和 pos 指定位置
     * @param {string} tagname 创建的元素的元素名
     * @param {node} destin 创建元素插入的父元素
     * @param {string} spos “after”或“before”指定插入方向
     * @param {integer} pos 插入位置所在子元素序号
     * @returns 
     */
    function creaElemIn(tagname, destin, spos, pos) {
        var elem;
        elem = document.createElement(tagname);
        if (!spos) {
            destin.appendChild(elem);
        } else {
            if (spos == "after") {
                destin.insertBefore(elem, destin.childNodes[pos + 1]);
            } else if (spos == "before") {
                destin.insertBefore(elem, destin.childNodes[pos]);
            }
        }
        return elem;
    }

    /** ~ removeNode(node)
     * 移除目标节点
     * @param {node} node 目标节点
     */
    function removeNode(node) {
        if (!!node.parentNode) {
            node.parentNode.removeChild(node);
        }
    }

    /** ~ addCSS(css, cssid)
     * 创建带ID的CSS节点并插入页面
     * @param {string} css CSS内容
     * @param {string} cssid CSS节点ID
     */
    function addCSS(css, cssid) {
        let stylenode = creaElemIn('style', document.getElementsByTagName('head')[0]);
        stylenode.textContent = css;
        stylenode.type = 'text/css';
        stylenode.id = cssid || '';
    }

    //~ - getTrueSize(node)
    //  输入元素,返回元素可见的四边屏幕坐标对象
    function getTrueSize(node) {
        if (node.tagName == "BODY" || node.tagName == "HTML") {
            return false;
        }
        var p = node.getBoundingClientRect();
        return getFourSide(node, p);
    }

    // ~ getFourSide(node, p)
    // 递归获取当前节点不被上层元素遮挡的四边位置
    function getFourSide(node, p) {
        var pn = node.parentNode;
        if (pn.tagName == "BODY") {     // 到顶了
            return p;
        }
        var pp = pn.getBoundingClientRect();
        var po = {
            left: p.left,
            right: p.right,
            top: p.top,
            bottom: p.bottom
        };
        if (pp.right < po.left || pp.left > po.right || pp.top > po.bottom || pp.bottom < po.top) {
            return false;                                   // 四边皆被父节点遮挡,目标节点不可见
        } else {
            var ok = true;
            if (po.left < pp.left) {
                po.left = pp.left;
                ok = false;
            }
            if (po.right > pp.right) {
                po.right = pp.right;
                ok = false;
            }
            if (po.top < pp.top) {
                po.top = pp.top;
                ok = false;
            }
            if (po.bottom > pp.bottom) {
                po.bottom = pp.bottom;
                ok = false;
            }
            if (!ok) {
                po = getFourSide(pn, po);
            }
            return po;
        }
    }



})();