// ==UserScript==
// @name 本地表格数据筛选
// @name:zh-CN 本地表格数据筛选
// @name:zh-TW 本地表格數據篩選
// @name:en Filter tabular data
// @namespace http://tampermonkey.net/
// @version 2.0.0
// @license GPL-3.0
// @author ShineByPupil
// @description 获取<table>标签的表格元素,根据表头形成筛选列表,本地对数据进行筛选
// @description:zh-CN 获取<table>标签的表格元素,根据表头形成筛选列表,本地对数据进行筛选
// @description:zh-TW 獲取<table>標簽的表格元素,根據表頭形成篩選列表,本地對數據進行篩選
// @description:en Obtain the table element of the <table> tag, form a filtering list based on the table header, and locally filter the data
// @match *://*/*
// @icon data:image/png;base64,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
// @noframes
// @grant none
// ==/UserScript==
(function () {
"use strict";
function findEmptyIndex(array) {
// 寻找第一个空单元的索引
const index = array.findIndex((_, i) => !(i in array));
// 如果找到空单元,则将新元素插入
if (index !== -1) {
return index;
} else {
// 如果数组中没有空单元,则将新元素追加到数组末尾
return array.length;
}
}
// 渲染帧优化
const rafDebounce = function (fn) {
let rafId = null;
return function (...args) {
rafId && cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
fn.apply(this, args);
rafId = null;
});
};
};
class MessageBox extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<div class="message"></div>
<style>
.message {
position: fixed;
z-index: 100;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
background-color: #333;
color: #fff;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
display: none; /* 默认隐藏 */
}
</style>
`;
this.message = this.shadowRoot.querySelector(".message");
}
show(message, duration = 2500) {
this.message.textContent = message;
this.message.style.display = "block"; // 显示消息
// 设置一定时间后自动隐藏消息
setTimeout(() => {
this.message.style.display = "none";
}, duration);
}
}
class SearchDialog extends HTMLElement {
/**
* WeakMap 存储结构说明:
*
* 键:table 表格元素
*
* 值:SearchTable 表格状态容器
*/
weakMap = new Map();
form = null;
startX = 0;
startY = 0;
initialX = 0;
initialY = 0;
x = 0;
y = 0;
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<div class="searchDialog">
<div class="searchDialog__header">
<label class="searchDialog__title">搜索</label>
<span class="closeBtn">✕</span>
</div>
<div class="searchDialog__content scroll-bar"></div>
<div class="searchDialog__footer">
<label>
<input class="searchDialog__notClose" type="checkbox" checked/>
<span style="margin-left: 4px">查询后不关闭</span>
</label>
<input type="button" class="resetBtn" value="重置"/>
<input type="button" class="closeBtn" value="取消"/>
<input type="button" class="confirmBtn primary" value="确定"/>
</div>
</div>
<style>
.searchDialog {
font-size: 12px;
font-family: "Microsoft YaHei", sans-serif;
display: none;
z-index: 9999;
flex-direction: column;
width: 30vw;
min-width: 600px;
box-sizing: border-box;
position: fixed;
background-color: #fff;
border: 1px solid #ddd;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
border-radius: 5px;
user-select: none;
}
.searchDialog__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
font-size: 18px;
cursor: move;
}
.searchDialog__header span {
transition: color 0.3s;
cursor: pointer;
font-size: 20px;
}
.searchDialog__header span:hover {
color: red;
}
.searchDialog__content {
margin: 20px;
padding: 0 10px;
flex: 1;
max-height: 400px;
overflow-y: auto;
overflow-x: hidden;
}
.searchDialog__footer {
padding: 10px;
display: flex;
justify-content: flex-end;
align-items: center;
user-select: none;
}
.searchDialog__footer > label {
display: flex;
}
.searchDialog__footer > label > * {
cursor: pointer;
}
.fade-in {
animation: fadeIn 0.3s;
}
.fade-out {
animation: fadeOut 0.3s;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
input[type='button'] {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
background-color: #fff;
border: 1px solid #dcdfe6;
color: #606266;
text-decoration: none;
cursor: pointer;
margin-left: 10px;
}
input[type='button']:hover {
background-color: #f5f7fa;
border-color: #409eff;
color: #409eff;
}
input[type='button'].primary {
background-color: #409eff;
border-color: #409eff;
color: #fff;
}
input[type='button'].primary:hover {
background-color: #66b1ff;
border-color: #66b1ff;
}
.scroll-bar {
overflow-y: scroll;
}
.scroll-bar::-webkit-scrollbar {
margin: 10px;
height: 10px;
width: 10px;
border: 2px solid #333; /* 设置滚动条的边框颜色 */
}
.scroll-bar::-webkit-scrollbar-track {
background-color: #f1f1f1;
}
.scroll-bar::-webkit-scrollbar-thumb {
background-color: #888;
border-radius: 5px;
}
.scroll-bar::-webkit-scrollbar-thumb:hover {
background-color: #555;
}
</style>
`;
this.dialog = this.shadowRoot.querySelector(".searchDialog");
this.content = this.shadowRoot.querySelector(".searchDialog__content");
this.notClose = this.dialog.querySelector(".searchDialog__notClose");
this.init();
}
show(table) {
if (!this.weakMap.has(table)) {
this.weakMap.set(table, new SearchTable(table));
}
const searchFrom = this.weakMap.get(table).searchFrom;
if (this.form !== searchFrom) {
this.form?.remove();
this.form = searchFrom;
this.content.appendChild(searchFrom);
}
requestAnimationFrame(() => {
this.dialog.style.display = "flex";
this.dialog.style.left = "calc(50% - 15vw)";
this.dialog.style.top = "10vh";
this.dialog.classList.remove("fade-out");
this.dialog.classList.add("fade-in");
});
}
close() {
this.dialog.classList.add("fade-out");
this.dialog.classList.remove("fade-in");
this.dialog.onanimationend = function () {
this.style.display = "none";
this.onanimationend = null;
};
}
init() {
this.initEvent();
this.initTable();
}
initEvent() {
// 点击事件 - footer 按钮组
this.dialog.addEventListener("click", async (event) => {
const {
target,
target: { className, tagName },
} = event;
if (className.includes("closeBtn")) {
this.close(); // 关闭
} else if (className.includes("resetBtn")) {
this.form.reset(); // 重置
} else if (className.includes("confirmBtn")) {
await this.form.confirm(); // 确定
!this.notClose.checked && this.close();
} else if (tagName === "INPUT" && target.type === "checkbox") {
target.parentElement.classList.toggle("active");
}
});
// 键盘事件 - esc 关闭弹窗
document.addEventListener("keydown", (event) => {
if (this.dialog.style.display === "flex" && event.key === "Escape")
this.close();
});
const startDrag = (event) => {
this.startX = event.clientX;
this.startY = event.clientY;
this.initialX = this.x;
this.initialY = this.y;
document.addEventListener("mousemove", onDrag);
document.addEventListener("mouseup", endDrag);
};
const onDrag = rafDebounce((event) => {
event.preventDefault();
const dx = event.clientX - this.startX;
const dy = event.clientY - this.startY;
this.x = this.initialX + dx;
this.y = this.initialY + dy;
this.dialog.style.transform = `translate(${this.x}px, ${this.y}px)`;
});
const endDrag = () => {
document.removeEventListener("mousemove", onDrag);
document.removeEventListener("mouseup", endDrag);
};
// 拖动事件
this.dialog
.querySelector(".searchDialog__header")
.addEventListener("mousedown", startDrag);
}
initTable() {
document.querySelectorAll("table").forEach((table) => {
const thead = table.querySelector("thead");
const tbody = table.querySelector("tbody");
if (thead && tbody) {
const parent = table.parentElement;
parent.classList.add("filter-table");
const template_style = document.createElement("template");
table.classList.add("scroll-bar");
template_style.innerHTML = `
<style>
table {
position: relative;
max-height: 50vh;
overflow-y: auto !important;
}
.scroll-bar {
overflow-y: scroll;
}
.scroll-bar::-webkit-scrollbar {
margin: 10px;
height: 10px;
width: 10px;
border: 2px solid #333; /* 设置滚动条的边框颜色 */
}
.scroll-bar::-webkit-scrollbar-track {
background-color: #f1f1f1;
}
.scroll-bar::-webkit-scrollbar-thumb {
background-color: #888;
border-radius: 5px;
}
.scroll-bar::-webkit-scrollbar-thumb:hover {
background-color: #555;
}
</style>
`;
document.head.appendChild(template_style.content);
const wrapper = document.createElement("div");
const shadow = wrapper.attachShadow({ mode: "open" });
shadow.innerHTML = `
<button class="open-filter-Dialog-btn" title="打开筛选弹窗">F</button>
<style>
button {
position: absolute;
top: 0;
left: 0;
width: 20px;
height: 20px;
line-height: 20px;
padding: 0 4px;
background-color: #fff;
border: 1px solid #409eff;
cursor: pointer;
}
</style>
`;
shadow.querySelector("button").onclick = () => this.show(table);
thead.appendChild(wrapper);
}
});
}
}
class SearchFrom extends HTMLElement {
table = null;
filterMap = null;
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<article class="filter_form">
<span class="add">添加</span>
<form></form>
</article>
<style>
.filter_form {
font-size: 12px;
}
.filter_form form {
position: relative;
margin-top: 4px;
}
.filter_form .add,
.filter_form .del {
user-select: none;
cursor: pointer;
}
</style>
`;
this.article = this.shadowRoot.querySelector("article");
this.form = this.shadowRoot.querySelector("form");
this.init();
}
init() {
this.initEvent();
}
initEvent() {
this.article.addEventListener("wheel", (event) => {
const innerElement = event.composedPath()[0];
if (innerElement.tagName === "SELECT") {
event.preventDefault();
const length = innerElement.options.length;
const index = innerElement.selectedIndex;
const direction = event.wheelDeltaY > 0 ? "up" : "down";
innerElement.selectedIndex =
direction === "up"
? index === 0
? length - 1
: index - 1
: index === length - 1
? 0
: index + 1;
}
});
this.article.addEventListener("click", (event) => {
const innerElement = event.composedPath()[0];
if (innerElement.className.includes("add")) {
this.addSearchInput();
} else if (innerElement.className.includes("del")) {
// 删除规则
innerElement.parentNode.remove();
}
});
this.article.addEventListener("input", (event) => {
const innerElement = event.composedPath()[0];
if (innerElement.tagName === "INPUT") {
innerElement.value
? innerElement.classList.remove("error")
: innerElement.classList.add("error");
}
});
}
reset() {
this.form.innerHTML = "";
}
async confirm() {
try {
this.formTrim();
await this.validate();
this.filter();
} catch (e) {
console.error(e);
}
}
addSearchInput() {
const searchInput = document.createElement("search-input");
searchInput.setAttribute(
"options",
JSON.stringify([...this.filterMap.keys()]),
);
searchInput.filterMap = this.filterMap;
this.form.appendChild(searchInput);
}
formTrim() {
this.form.querySelectorAll("input[type=text]").forEach((input) => {
input.value = input.value.includes(",")
? input.value
.split(",")
.map((n) => n.trim())
.filter((n) => n)
.join(", ")
: input.value.trim();
});
}
validate() {
let flag = true;
this.form.childNodes.forEach((node) => {
const { checkbox, input } = node;
if (checkbox.checked && !input.value) {
flag = false;
input.classList.add("error", "shake");
} else {
input.classList.remove("error");
}
});
return new Promise((resolve, reject) => {
if (flag) {
resolve();
} else {
messageBox.show("表单验证未通过");
reject(new Error("表单验证未通过"));
}
});
}
getRules() {
const rules_AND = [];
const rules_OR = [];
const rules_NOT = [];
this.form.childNodes.forEach((node) => {
const { checkbox, select1, select2, input } = node;
if (checkbox.checked) {
const rules = input.value.split(",").map((keyword) => ({
keyword: keyword.trim(),
colIndexs: Array.from(this.filterMap.get(select2.value)),
}));
switch (select1.value) {
case "AND":
rules_AND.push(...rules);
break;
case "OR":
rules_OR.push(...rules);
break;
case "NOT":
rules_NOT.push(...rules);
break;
}
}
});
return { rules_AND, rules_OR, rules_NOT };
}
filter() {
const data = this.table.searchTable.data;
const rules = this.getRules();
const trList = Array.from(this.table.querySelector("tbody").children);
let count = 0;
// 筛选
data.forEach((trData, i) => {
if (this.isVisible(trData, rules)) {
trList[i].style.display = "";
count++;
} else {
trList[i].style.display = "none";
}
});
messageBox.show(`搜索成功,一共查询出 ${count} 数据`);
}
// 根据给定的规则确定表格行是否可见
isVisible(trData, rules) {
const { rules_AND, rules_OR, rules_NOT } = rules;
const isVisible_AND =
rules_AND.length &&
rules_AND.every((rule) => {
const { keyword, colIndexs } = rule;
return colIndexs.some((index) => trData?.[index]?.includes(keyword));
});
const isVisible_OR =
rules_OR.length &&
rules_OR.some((rule) => {
const { keyword, colIndexs } = rule;
return colIndexs.some((index) => trData?.[index]?.includes(keyword));
});
const isVisible_NOT = rules_NOT.length
? rules_NOT.every((rule) => {
const { keyword, colIndexs } = rule;
return !colIndexs.some((index) =>
trData?.[index]?.includes(keyword),
);
})
: true;
return (
isVisible_NOT &&
(rules_AND.length || rules_OR.length
? isVisible_AND || isVisible_OR
: true)
);
}
}
class SearchInput extends HTMLElement {
static get observedAttributes() {
return ["options"];
}
options = [];
filterMap = null;
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<div class="form-example active">
<input type="checkbox" checked>
<select>
<option label="AND" value="AND"></option>
<option label="OR" value="OR"></option>
<option label="NOT" value="NOT"></option>
</select>
<select></select>
<input type="text"/>
<span class="del">删除</span>
</div>
<style>
.form-example {
display: flex;
align-items: center;
margin: 0 -4px 6px;
}
.form-example > * {
margin: 0 4px;
}
.form-example:not(.active) * {
border-color: #d9d9d9;
color: #d9d9d9;
}
input[type='text'] {
flex: 1;
}
input[type='text'].error {
border-color: red;
}
input[type='text'].shake {
animation: shake 0.6s ease-in-out 1;
}
select, input[type='text'] {
box-sizing: border-box;
height: 32px;
padding: 4px 11px;
border-radius: 4px;
border: 1px solid #d9d9d9;
transition: all 0.3s;
}
select:focus, input:focus {
outline: none;
border-color: #4096ff;
border-inline-end-width: 1px;
}
select:hover, input:hover {
border-color: #4096ff;
border-inline-end-width: 1px;
}
@keyframes shake {
0% { transform: translateX(0); }
25% { transform: translateX(-6px); }
50% { transform: translateX(4px); }
75% { transform: translateX(-2px); }
100% { transform: translateX(0); }
}
</style>
`;
this.example = this.shadowRoot.querySelector(".form-example");
this.checkbox = this.shadowRoot.querySelector("input[type='checkbox']");
this.select1 = this.shadowRoot.querySelector("select:nth-child(2)");
this.select2 = this.shadowRoot.querySelector("select:nth-child(3)");
this.input = this.shadowRoot.querySelector("input[type='text']");
this.del = this.shadowRoot.querySelector(".del");
this.init();
}
attributeChangedCallback(attrName, oldVal, newVal) {
if (attrName === "options") {
this.options = JSON.parse(newVal);
this.select2.innerHTML = this.options
.map((n) => `<option label="${n}" value="${n}"></option>`)
.join("");
}
}
init() {
this.initEvent();
}
initEvent() {
this.checkbox.onclick = () => {
this.example.classList.toggle("active");
};
this.example.onanimationend = function (event) {
const innerElement = event.composedPath()[0];
if (innerElement.className.includes("shake")) {
innerElement.classList.remove("shake");
}
};
}
}
class SearchTable {
constructor(table) {
this.table = table;
table.searchTable = this;
const { header, filterMap } = this.parseTable(table);
this.header = header;
this.filterMap = filterMap;
this.searchFrom = document.createElement("search-from");
this.searchFrom.table = table;
this.searchFrom.filterMap = filterMap;
}
parseTable(table) {
const thead = table.querySelector("thead");
if (!thead) {
throw new Error("缺少表头");
}
let header = []; // 表头数据
thead.querySelectorAll("tr").forEach((tr) => {
header.push(
Array.from(tr.querySelectorAll("th")).map((n) => {
return {
label: n.textContent,
rowspan: n.rowSpan ?? 1, // 高度
colspan: n.colSpan ?? 1, // 宽度
};
}),
);
});
let dp = new Array(header.length).fill(0).map(() => []);
header.forEach((tr, i) => {
tr.forEach((td) => {
const index = findEmptyIndex(dp[i]);
const { colspan, rowspan } = td;
for (let k = i; k < i + rowspan; k++) {
for (let l = index; l < index + colspan; l++) {
dp[k][l] ??= td.label;
}
}
});
});
let filterMap = new Map(); // 过滤器映射
for (let i = 0; i < dp.length; i++) {
for (let j = 0; j < dp[i].length; j++) {
if (dp[i][j]) {
filterMap.has(dp[i][j]) || filterMap.set(dp[i][j], new Set());
filterMap.get(dp[i][j]).add(j);
}
}
}
return { header, filterMap };
}
get data() {
return Array.from(this.table.querySelectorAll("tbody > tr")).map((tr) => {
return Array.from(tr.querySelectorAll("td")).map((n) => {
return n.textContent;
});
});
}
}
customElements.define("message-box", MessageBox);
customElements.define("search-dialog", SearchDialog);
customElements.define("search-from", SearchFrom);
customElements.define("search-input", SearchInput);
const messageBox = document.createElement("message-box");
document.body.appendChild(messageBox);
window.addEventListener("load", function (event) {
const searchDialog = document.createElement("search-dialog");
document.body.appendChild(searchDialog);
});
})();