// ==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;
}
}
})();