// ==UserScript==
// @name 本地表格数据筛选
// @name:zh-CN 本地表格数据筛选
// @name:zh-TW 本地表格數據篩選
// @name:en Filter tabular data
// @namespace http://tampermonkey.net/
// @version 1.0.4
// @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';
let searchDialogDOM = null;
const utils = {
/**
* 在数组中查找第一个空单元的索引。
*
* @param {Array} array - 要查找empty的数组。
* @return {number} 第一个empty的索引,如果没有找到空单元则返回数组的长度。
*/
findEmptyIndex: function (array) {
// 寻找第一个空单元的索引
const index = array.findIndex((_, i) => !(i in array));
// 如果找到空单元,则将新元素插入
if (index !== -1) {
return index;
} else {
// 如果数组中没有空单元,则将新元素追加到数组末尾
return array.length;
}
},
messageBox: null,
/**
* 在屏幕上显示指定时间长度的消息。
*
* @param {string} message - 要显示的消息。
* @param {number} [duration=2500] - 消息应显示的毫秒数。默认为2500毫秒。
* @return {void} 此函数不返回值。
*/
showMessage: function (message, duration = 2500) {
if (!this.messageBox) {
this.messageBox = this.createNode(
`<div id="messageBox" class="c-message"></div>`
);
document.body.appendChild(this.messageBox);
}
this.messageBox.textContent = message;
this.messageBox.style.display = 'block'; // 显示消息
// 设置一定时间后自动隐藏消息
setTimeout(() => {
this.messageBox.style.display = 'none';
}, duration);
},
/**
* 从提供的模板字符串创建一个新的 DOM 节点。
*
* @param {string} template - 要创建节点的 HTML 模板字符串。
* @return {Node} 新创建的 DOM 节点。
*/
createNode: function (template) {
const div = document.createElement('div');
div.innerHTML = template.trim();
return div.firstChild;
},
};
/**
* 初始化函数,用于加载表格筛选。
*
*/
function init() {
window.addEventListener('load', function (event) {
console.log('加载表格筛选');
renderCSS();
findTable();
});
}
/**
* 在页面上查找所有的表格,并为每个表格添加一个按钮,当点击该按钮时,会打开搜索对话框。
* 该按钮位于表格的左上角。
*
* @return {void} 该函数没有返回值。
*/
function findTable() {
const tableList = document.querySelectorAll('table');
if (tableList.length) {
document.querySelectorAll('table').forEach((tableDOM) => {
if (
tableDOM.querySelector('thead') &&
tableDOM.querySelector('tbody')
) {
const thead = tableDOM.querySelector('thead');
const scrollDOM = utils.createNode(
`<div class="filter-table"></div>`
);
tableDOM.parentElement.insertBefore(scrollDOM, tableDOM);
tableDOM.remove();
scrollDOM.appendChild(tableDOM);
const btn = utils.createNode(
`<button class="open-filter-Dialog-btn" title="打开筛选弹窗">F</button>`
);
thead.appendChild(btn);
btn.onclick = () => showSearchDialog(tableDOM);
}
});
}
}
const showSearchDialog = (function () {
const weakMap = new WeakMap();
/**
* 从给定的表格 DOM 元素中解析表格数据。
*
* @param {Element} tableDOM - 要解析的表格 DOM 元素。
* @return {Object} 包含解析后的数据、表头、过滤器映射和表单 DOM 的对象。
*/
function parse(tableDOM) {
let header = []; // 表头数据
// 解析表头
tableDOM.querySelectorAll('thead tr').forEach((trDOM) => {
header.push(
Array.from(trDOM.querySelectorAll('th')).map((n) => {
return {
label: n.textContent,
rowspan: n.rowSpan ?? 1, // 高度
colspan: n.colSpan ?? 1, // 宽度
};
})
);
});
// 每次筛选时,动态解析表格数据,防止表格排序导致顺序变化
const getData = () => {
return Array.from(tableDOM.querySelectorAll('tbody tr')).map(
(trDOM) => {
return Array.from(trDOM.querySelectorAll('td')).map((n) => {
return n.textContent;
});
}
);
};
let dp = new Array(header.length).fill(0).map((n) => new Array()); // 多级表头结构
header.forEach((tr, i) => {
tr.forEach((td, j) => {
const index = utils.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);
}
}
}
const formDOM = renderForm({ filterMap, getData, tableDOM });
return weakMap
.set(tableDOM, { header, filterMap, formDOM })
.get(tableDOM);
}
return function (tableDOM) {
if (!searchDialogDOM) {
searchDialogDOM = renderDialog();
document.body.appendChild(searchDialogDOM);
}
const { formDOM } = weakMap.has(tableDOM)
? weakMap.get(tableDOM)
: parse(tableDOM);
const content = searchDialogDOM.querySelector('.content');
content.childNodes.forEach((node) => {
content.removeChild(node);
});
content.appendChild(formDOM);
searchDialogDOM._show();
};
})();
/**
* 渲染一个带有搜索功能的对话框。
*
* @return {HTMLElement} 渲染的对话框。
*/
function renderDialog() {
// 主体
const dialog = utils.createNode(`
<div id="searchDialog" style="display: none">
<div class="searchDialog__header" draggable="true">
<label class="searchDialog__title">搜索</label>
<span class="closeBtn">✕</span>
</div>
<div class="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="setMaxHeight" value="表格最大高度开关"></input>
<input type="button" class="resetBtn" value="重置"></input>
<input type="button" class="closeBtn" value="取消"></input>
<input type="button" class="confirmBtn primary" value="确定"></input>
</div>
</div>
`);
const header = dialog.querySelector('.searchDialog__header');
// 方法
dialog._show = () => {
dialog.style.display = 'block';
dialog.style.left = 'calc(50% - 15vw)';
dialog.style.top = '10vh';
dialog.classList.remove('fade-out');
dialog.classList.add('fade-in');
};
dialog._hidden = () => {
dialog.classList.add('fade-out');
dialog.classList.remove('fade-in');
dialog.onanimationend = () => {
dialog.style.display = 'none';
dialog.onanimationend = null;
};
};
// 事件
dialog.addEventListener('click', (event) => {
const {
target,
target: { className, tagName },
} = event;
if (className.includes('closeBtn')) {
// 关闭
dialog._hidden();
} else if (className.includes('resetBtn')) {
// 重置
document.dispatchEvent(
new CustomEvent('btnEvent', { detail: { type: 'reset' } })
);
} else if (className.includes('confirmBtn')) {
// 确定
const notClose = dialog.querySelector(
'input[type=checkbox].searchDialog__notClose'
).checked;
document.dispatchEvent(
new CustomEvent('btnEvent', { detail: { type: 'confirm', notClose } })
);
} else if (tagName === 'INPUT' && target.type === 'checkbox') {
target.parentElement.classList.toggle('active');
} else if (className.includes('setMaxHeight')) {
document.dispatchEvent(
new CustomEvent('btnEvent', { detail: { type: 'setMaxHeight' } })
);
}
});
// 监听键盘按下事件
document.addEventListener('keydown', (event) => {
if (dialog.style.display === 'block' && event.key === 'Escape')
dialog._hidden();
});
let offsetX, offsetY;
header.addEventListener('dragstart', (event) => {
event.dataTransfer.effectAllowed = 'move';
// 获取拖动开始时鼠标相对于拖动元素的偏移
offsetX = event.clientX - header.getBoundingClientRect().left;
offsetY = event.clientY - header.getBoundingClientRect().top;
});
header.addEventListener('drag', function (event) {
if (event.clientX && event.clientY) {
// 计算拖动后的位置
const x = event.clientX - offsetX;
const y = event.clientY - offsetY;
// 设置拖动元素的新位置
dialog.style.left = x + 'px';
dialog.style.top = y + 'px';
} else {
dialog.style.left = 'calc(50% - 15vw)';
dialog.style.top = '10vh';
}
});
header.addEventListener('dragover', function (event) {
event.preventDefault();
});
return dialog;
}
/**
* 根据提供的数据和表格 DOM 渲染一个带有筛选选项的表单。
*
* @param {Object} filterMap - 筛选选项的映射。
* @param {Array} data - 用于筛选的数据。
* @param {HTMLElement} tableDOM - 表格的 DOM 元素。
* @return {HTMLElement} 带有筛选选项的表单的 DOM 元素。
*/
function renderForm({ filterMap, getData, tableDOM }) {
const formDOM = utils.createNode(`
<article class="filter_form">
<span class="add">添加</span>
<form></form>
</article>
`);
const inputDOM = utils.createNode(`
<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>
${[...filterMap.keys()]
.map((n) => `<option label="${n}" value="${n}"></option>`)
.join('')}
</select>
<input type="text"/>
<span class="del">删除</span>
</div>
`);
const form = formDOM.querySelector('form');
function formTrim() {
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();
});
}
/**
* 验证表单,检查所有输入字段是否有值。
*
* @return {Promise} 如果所有输入字段都有值,则解析;否则拒绝
*/
function validate() {
let flag = true;
form.childNodes.forEach((node) => {
const [checkbox, , , input] = node.children;
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 {
utils.showMessage('表单验证未通过');
reject(new Error('表单验证未通过'));
}
});
}
/**
* 从表单中获取过滤规则。
*
* @return {Object} 包含过滤规则的对象。该对象有三个属性:
* - rulse_AND:表示并且过滤规则的数组。每个对象有两个属性:
* - keyword:表示要过滤的关键字的字符串。
* - colIndexs:表示要过滤的列索引的数组。
* - rulse_OR:表示或者过滤规则的数组。结构与rulse_AND相同。
* - rulse_NOT:表示非过滤规则的数组。结构与rulse_AND相同。
*/
function getRules() {
const rulse_AND = [];
const rulse_OR = [];
const rulse_NOT = [];
form.childNodes.forEach((node) => {
const [checkbox, select1, select2, input] = node.children;
if (checkbox.checked) {
const rules = input.value.split(',').map((keyword) => ({
keyword: keyword.trim(),
colIndexs: Array.from(filterMap.get(select2.value)),
}));
switch (select1.value) {
case 'AND':
rulse_AND.push(...rules);
break;
case 'OR':
rulse_OR.push(...rules);
break;
case 'NOT':
rulse_NOT.push(...rules);
break;
}
}
});
return {
rulse_AND,
rulse_OR,
rulse_NOT,
};
}
/**
* 根据给定的规则确定表格行是否可见。
*
* @param {Array} trData - 表格行数据。
* @param {Object} rules - 过滤规则。
* @param {Array} rules.rulse_AND - AND 过滤规则。
* @param {Array} rules.rulse_OR - OR 过滤规则。
* @param {Array} rules.rulse_NOT - NOT 过滤规则。
* @param {Object} rules.rulse_AND[].rule - AND 过滤规则。
* @param {string} rules.rulse_AND[].rule.keyword - 过滤关键字。
* @param {Array} rules.rulse_AND[].rule.colIndexs - 过滤列索引。
* @param {Object} rules.rulse_OR[].rule - OR 过滤规则。
* @param {string} rules.rulse_OR[].rule.keyword - 过滤关键字。
* @param {Array} rules.rulse_OR[].rule.colIndexs - 过滤列索引。
* @param {Object} rules.rulse_NOT[].rule - NOT 过滤规则。
* @param {string} rules.rulse_NOT[].rule.keyword - 过滤关键字。
* @param {Array} rules.rulse_NOT[].rule.colIndexs - 过滤列索引。
* @return {boolean} 如果表格行可见,则为 true,否则为 false。
*/
const isVisible = function (trData, rules) {
const { rulse_AND, rulse_OR, rulse_NOT } = rules;
const isVisible_AND =
rulse_AND.length &&
rulse_AND.every((rule) => {
const { keyword, colIndexs } = rule;
return colIndexs.some((index) => trData?.[index]?.includes(keyword));
});
const isVisible_OR =
rulse_OR.length &&
rulse_OR.some((rule) => {
const { keyword, colIndexs } = rule;
return colIndexs.some((index) => trData?.[index]?.includes(keyword));
});
const isVisible_NOT = rulse_NOT.length
? rulse_NOT.every((rule) => {
const { keyword, colIndexs } = rule;
return !colIndexs.some((index) =>
trData?.[index]?.includes(keyword)
);
})
: true;
return (
isVisible_NOT &&
(rulse_AND.length || rulse_OR.length
? isVisible_AND || isVisible_OR
: true)
);
};
/**
* 处理根据给定规则筛选表格行。如果提供了规则,
* 则根据规则筛选表格行,并显示成功消息以及筛选行数。
* 如果没有提供规则,则重置所有表格行的可见性,并显示成功消息以及全部行数。
*
* @return {void} 此函数不返回任何内容。
*/
function handleFilter() {
const data = getData();
const rules = getRules();
const trList = Array.from(tableDOM.querySelector('tbody').children);
let count = 0;
// 筛选
data.forEach((trData, i) => {
if (isVisible(trData, rules)) {
trList[i].style.visibility = 'visible';
count++;
} else {
trList[i].style.visibility = 'collapse';
}
});
utils.showMessage(`搜索成功,一共查询出 ${count} 数据`);
}
form.addEventListener('submit', async (event) => {
try {
event.preventDefault();
formTrim();
await validate();
handleFilter();
searchDialogDOM._hidden();
} catch (e) {
console.error(e);
}
});
formDOM.addEventListener('wheel', (event) => {
if (event.target.tagName === 'SELECT') {
event.preventDefault();
const length = event.target.options.length;
const index = event.target.selectedIndex;
const direction = event.wheelDeltaY > 0 ? 'up' : 'down';
event.target.selectedIndex =
direction === 'up'
? index === 0
? length - 1
: index - 1
: index === length - 1
? 0
: index + 1;
}
});
formDOM.addEventListener('click', (event) => {
if (event.target.className.includes('add')) {
form.appendChild(inputDOM.cloneNode(true));
} else if (event.target.className.includes('del')) {
// 删除规则
event.target.parentNode.remove();
}
});
formDOM.addEventListener('animationend', (event) => {
if (event.target.className.includes('shake')) {
event.target.classList.remove('shake');
}
});
formDOM.addEventListener('input', (event) => {
if (event.target.tagName === 'INPUT') {
event.target.value
? event.target.classList.remove('error')
: event.target.classList.add('error');
}
});
document.addEventListener('btnEvent', async (event) => {
if (formDOM.parentElement) {
switch (event?.detail?.type) {
case 'confirm':
try {
formTrim();
await validate();
handleFilter();
if (!event?.detail?.notClose) {
searchDialogDOM._hidden();
}
} catch (e) {
console.error(e);
}
break;
case 'reset':
form.innerHTML = '';
break;
case 'setMaxHeight':
tableDOM.parentElement.classList.toggle('scroll-bar')
? utils.showMessage('设置滚动条')
: utils.showMessage('恢复原状');
break;
}
}
});
return formDOM;
}
// 放弃。todo (不定高)虚拟滚动
function renderTable(header, dataSource) {
const container = utils.createNode(`
<div class="c-table">
<div class="c-table__content scroll-bar">
<table>
<colgroup></colgroup>
<thead></thead>
<tbody></tbody>
</table>
</div>
</div>
`);
const tableDOM = container.querySelector('table');
const colgroupDOM = container.querySelector('colgroup');
const thDOM = container.querySelector('thead');
const tbDOM = container.querySelector('tbody');
// 根据表头计算colgroup
let i = 23;
while (i--) {
let colDOM = document.createElement('col');
colDOM.setAttribute('width', '100px');
colgroupDOM.appendChild(colDOM);
}
window.colgroupDOM = colgroupDOM;
tableDOM.style.width = 23 * 100 + 'px';
// 渲染表头
header.forEach((tr) => {
let trDOM = document.createElement('tr');
thDOM.appendChild(trDOM);
tr.forEach((td) => {
let thDOM = document.createElement('th');
trDOM.appendChild(thDOM);
thDOM.innerHTML = td.label;
thDOM.rowspan = td.rowspan;
thDOM.colspan = td.colspan;
thDOM.setAttribute('rowspan', td.rowspan);
thDOM.setAttribute('colspan', td.colspan);
});
});
// 渲染表体
dataSource.forEach((td) => {
let trDOM = document.createElement('tr');
trDOM.innerHTML = td.map((n) => `<td><p>${n}</p></td>`).join('');
tbDOM.appendChild(trDOM);
});
return container;
}
/**
* 渲染搜索对话框和页面上其他元素的CSS样式。
*
* @return {void} 此函数不返回任何内容。
*/
function renderCSS() {
let style = utils.createNode(`
<style>
/* 滚动条样式 */
.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;
}
/* 表格样式 */
.filter-table.scroll-bar {
max-height: 80vh;
}
.filter-table table {
}
.filter-table table thead {
position: sticky;
top: 0;
}
.filter-table button.open-filter-Dialog-btn {
position: absolute;
top: 0;
left: 0;
width: 20px;
height: 20px;
line-height: 20px;
padding: 0 4px;
background-color: #fff;
border: 1px solid #409eff;
}
/* 弹窗样式 */
#searchDialog {
font-size: 12px;
font-family: "Microsoft YaHei", sans-serif;
display: flex;
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;
}
#searchDialog .searchDialog__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
font-size: 18px;
}
#searchDialog .searchDialog__header span {
transition: color 0.3s;
cursor: pointer;
font-size: 20px;
}
#searchDialog .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 .searchDialog__footer {
padding: 10px;
display: flex;
justify-content: flex-end;
align-items: center;
user-select: none;
}
#searchDialog .searchDialog__footer > label {
display: flex;
}
#searchDialog .searchDialog__footer > label > * {
cursor: pointer;
}
/* CSS 过渡效果 */
#searchDialog.fade-in {
animation: fadeIn 0.3s;
}
#searchDialog.fade-out {
animation: fadeOut 0.3s;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.c-message {
position: fixed;
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; /* 默认隐藏 */
}
/* 表单样式 */
#searchDialog article.filter_form {
font-size: 12px;
}
#searchDialog article.filter_form form {
position: relative;
margin-top: 4px;
}
#searchDialog article.filter_form .add,
#searchDialog article.filter_form .del {
user-select: none;
cursor: pointer;
}
#searchDialog article.filter_form form .form-example {
display: flex;
align-items: center;
margin: 0 -4px;
margin-bottom: 6px;
}
#searchDialog article.filter_form form .form-example > * {
margin: 0 4px;
}
#searchDialog article.filter_form form .form-example input[type='text'] {
flex: 1;
}
#searchDialog article.filter_form form .form-example input[type='text'].error {
border-color: red;
}
#searchDialog article.filter_form form .form-example input[type='text'].shake {
animation: shake 0.6s ease-in-out 1;
}
#searchDialog article.filter_form form .form-example:not(.active) * {
border-color: #d9d9d9;
color: #d9d9d9;
}
@keyframes shake {
0% { transform: translateX(0); }
25% { transform: translateX(-6px); }
50% { transform: translateX(4px); }
75% { transform: translateX(-2px); }
100% { transform: translateX(0); }
}
/* 组件样式 */
#searchDialog select,
#searchDialog input[type='text'] {
box-sizing: border-box;
height: 32px;
padding: 4px 11px;
border-radius: 4px;
border: 1px solid #d9d9d9;
transition: all 0.3s;
}
#searchDialog select:focus,
#searchDialog input:focus {
outline: none;
border-color: #4096ff;
border-inline-end-width: 1px;
}
#searchDialog select:hover,
#searchDialog input:hover {
border-color: #4096ff;
border-inline-end-width: 1px;
}
#searchDialog 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;
}
#searchDialog input[type='button']:hover {
background-color: #f5f7fa;
border-color: #409eff;
color: #409eff;
}
#searchDialog input[type='button'].primary {
background-color: #409eff;
border-color: #409eff;
color: #fff;
}
#searchDialog input[type='button'].primary:hover {
background-color: #66b1ff;
border-color: #66b1ff;
}
</style>
`);
document.head.appendChild(style);
}
init();
})();