您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自由选定页面元素进行筛选
当前为
// ==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; } } })();