[js] 各种小界面的过滤器,更多功能等接力

好家伙!原来没留意——发帖还要扣分的,亏本分享

虽然对我来说,分有啥用啊~所以我更新了……


看到隔壁讨论的需求,标签搜索功能 - 链滴

本来想直接回复,字数超了

image.png

论坛有过分享的

[js] 大纲过滤器, 支持大部分面板的过滤 (大纲, 标签, 书签, 书签 +, 文件树, 反链) - 链滴

站在大佬肩膀上按个人需求完善了下,按需修改吧,我才学的 js


再次更新版,兼容了 Achuan 的【块引脚注插件更新 v1.4.1:新增脚注查看面板,支持查看文档的所有脚注 - 链滴】,但不确定是否存在其他兼容性问题,也不确定后续更新是否出现变化。


(() => {
// add filters for all各种过滤器
// 原作者版本https://ld246.com/article/1736828257787 https://github.com/leeyaunlong/siyuan_scripts/
// chuchen接力版:增加按钮循环控制,css优化,支持拼音模糊搜索:首字母或全拼
// 支持混合逻辑语法:'空格 '分割关键词(AND逻辑),'竖线|'分割关键词(OR逻辑),'英文感叹号!'开头的关键词(NOT逻辑),暂不支持转义
// 支持关键词高亮,支持Achuan大佬的最新的脚注面板https://ld246.com/article/1752516343833
// 支持按钮隐藏,在任意reset按钮连续右键3次恢复
// 待完善功能,支持闪卡过滤器,支持手机版
// ================= 配置区 =================
// 过滤器的基本配置,包括每个过滤器的id、占位符、目标选择器和插入位置
const FILTER_CONFIGS = [
    { id: 'tree_filter_container', placeholder: "文档树过滤器", selector: '.fn__flex-1.fn__flex-column.file-tree.sy__file', position: 'BEFORE_BEGIN' },
    { id: 'outline_filter_container', placeholder: "大纲过滤器", selector: '.fn__flex-1.fn__flex-column.file-tree.sy__outline', position: 'BEFORE_BEGIN' },
    { id: 'bmsp_filter_container', placeholder: "书签+过滤器", selector: '.fn__flex-1.b3-list.b3-list--background.custom-bookmark-body', position: 'BEFORE_END' },
    { id: 'tags_filter_container', placeholder: "标签过滤器", selector: '.fn__flex-1.fn__flex-column.file-tree.sy__tag', position: 'APPEND' },
    { id: 'bms_filter_container', placeholder: "书签过滤器", selector: '.fn__flex-1.fn__flex-column.file-tree.sy__bookmark', position: 'APPEND' },
    { id: 'backlink_filter_container', placeholder: "反链过滤器", selector: '.fn__flex-1.fn__flex-column.file-tree.sy__backlink', position: 'BEFORE_BEGIN' },
    { id: 'footnote_filter_container', placeholder: "脚注过滤器", selector: '.footnote-dock__content', position: 'INSERT_BEFORE' },
    // { id: 'flashcard_filter_container', placeholder: "闪卡过滤器", selector: '[data - key= "dialog-viewcards"] .fn__flex - column', position: 'APPEND' },
];
// ================= 样式注入 =================
// 高亮样式
addStyle(`
    .filter_highlight {
        background-color: var(--b3-card-success-background) !important;
    }
`);
// =============== 新增:按钮显示控制参数 ===============
const showFilterButtonDefault = true;
const SIYUAN_APP_ID = 'custom-snippet';
const SIYUAN_KEY = 'myPlugin_showFilterButton';
// =============== 思源本地存储API封装(带Token) ===============
async function setSiyuanLocalStorageItem(key, value, appId) {
    const token = localStorage.getItem('authToken') || '';
    const requestBody = {
        key: key,
        val: value, // 直接用布尔值
        app: appId
    };
    const response = await fetch('/api/storage/setLocalStorageVal', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Token ${token}`
        },
        body: JSON.stringify(requestBody)
    });
    return response.json();
}
async function getSiyuanLocalStorageItem(key, appId) {
    const token = localStorage.getItem('authToken') || '';
    const requestBody = {
        app: appId
    };
    const response = await fetch('/api/storage/getLocalStorage', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Token ${token}`
        },
        body: JSON.stringify(requestBody)
    });
    const result = await response.json();
    if (result.code === 0 && result.data && typeof result.data === 'object') {
        let val = result.data[key];
        if (val === undefined) return null;
        if (val === false || val === 'false') return false;
        if (val === true || val === 'true') return true;
        return val;
    }
    return null;
}

// ================= 工具函数区 =================
// 判断是否为移动端
function isMobile() {
    return !!document.getElementById("sidebar");
}
// 等待某个元素出现,常用于异步加载场景
function whenElementExist(selector, node = document, timeout = 5000) {
    return new Promise((resolve, reject) => {
        const start = Date.now();
        function check() {
            let el;
            try {
                el = typeof selector === 'function' ? selector() : node.querySelector(selector);
            } catch (err) {
                return reject(err);
            }
            if (el) {
                resolve(el);
            } else if (Date.now() - start >= timeout) {
                reject(new Error(`Timed out after ${timeout}ms waiting for element ${selector}`));
            } else {
                requestAnimationFrame(check);
            }
        }
        check();
    });
}
// 等待所有匹配元素出现
function whenAllElementsExist(selector) {
    return new Promise(resolve => {
        const check = () => {
            const elements = document.querySelectorAll(selector);
            if (elements.length > 0) resolve(Array.from(elements));
            else requestAnimationFrame(check);
        };
        check();
    });
}
// 动态插入样式
function addStyle(css) {
    const style = document.createElement('style');
    style.innerHTML = css;
    document.head.appendChild(style);
}
// 防抖函数,减少高频事件触发
function debounce(fn, delay) {
    let timer = null;
    return function(...args) {
        clearTimeout(timer);
        timer = setTimeout(() => fn.apply(this, args), delay);
    };
}

// ================= 过滤器核心逻辑区 =================
// 动态加载拼音库,支持拼音模糊搜索
const loadPinyin = new Promise((resolve) => {
    if (window.pinyinPro) return resolve(window.pinyinPro);
    const sources = [
        'https://cdn.bootcdn.net/ajax/libs/pinyin-pro/3.26.0/index.min.js',
        'https://unpkg.com/pinyin-pro@3.18.2/dist/index.js',
        'https://cdn.jsdelivr.net/npm/pinyin-pro@3.18.2/dist/index.js',
        '/plugins/myFonts/pinyin-pro.min.js'
    ];
    let retryCount = 0;
    const tryNextSource = () => {
        if (retryCount >= sources.length) {
            console.warn('代码片段-过滤器-所有拼音源加载失败,启用纯文本过滤');
            return resolve(null);
        }
        const script = document.createElement('script');
        script.src = sources[retryCount];
        script.onload = () => resolve(window.pinyinPro);
        script.onerror = () => {
            retryCount++;
            tryNextSource();
        };
        document.head.appendChild(script);
    };
    tryNextSource();
});

// 创建并插入过滤器输入框及其事件
function addFilterBox({ id, placeholder, selector, position }) {
    removeExistingInput(id); // 确保先移除旧输入框,避免重复绑定
    const container = createFilterContainer(id, placeholder);
    insertInputContainer(container, selector, position);
    if (document.querySelector(selector + '.fn__none')) {
        container.remove();
    }
    const input = container.querySelector('input');
    const resetButton = container.querySelector('button.reset');
    const closeButton = container.querySelector('button.close');
    // 使用防抖,减少高频输入时的DOM操作
    input.addEventListener('input', debounce(async function () {
        await handleInputEvent(input, selector, id);
    }, 150));
    resetButton.addEventListener('click', function () {
        input.value = '';
        resetFilterDisplay(selector);
    });
    closeButton.addEventListener('click', function () {
        input.value = '';
        resetFilterDisplay(selector);
        container.remove();
    });
}
// 移除已存在的输入框,避免重复插入
function removeExistingInput(id) {
    const existingInput = document.getElementById(id);
    if (existingInput) existingInput.remove();
}
// 创建输入框容器和按钮
function createFilterContainer(id, placeholder) {
    const container = document.createElement('div');
    container.id = id;
    container.style.display = 'flex';
    container.style.alignItems = 'center';
    const input = document.createElement('input');
    input.id = 'filter_input' + id;
    input.type = 'text';
    input.placeholder = placeholder;
    input.style.flex = '1';
    input.style.minWidth = 0;
    input.style.borderWidth = '1px';
    input.style.borderStyle = 'dashed';
    input.style.borderColor = 'var(--b3-theme-on-surface)';
    input.style.backgroundColor = 'var(--b3-theme-surface)';
    input.style.color = 'var(--b3-theme-on-background)';
    input.className = 'b3-text-field fn__block';
    const resetButton = document.createElement('button');
    resetButton.textContent = '↺';
    resetButton.className = 'reset';
    resetButton.style.color = 'var(--color-text-3)';
    resetButton.style.fontWeight = "900";
    resetButton.style.cursor = 'pointer';
    resetButton.style.backgroundColor = 'var(--b3-theme-surface-light)';
    resetButton.style.borderWidth = '1px';
    resetButton.style.borderStyle = 'dotted';
    resetButton.style.borderColor = 'var(--b3-theme-on-surface)';
    resetButton.style.marginRight = '3px';
    // =============== 新增:reset右键连续3次恢复按钮 ===============
    let resetRightClickCount = 0;
    resetButton.addEventListener('contextmenu', function (e) {
        getSiyuanLocalStorageItem(SIYUAN_KEY, SIYUAN_APP_ID).then(val => {
            if (val === false || val === 'false') {
                e.preventDefault();
                resetRightClickCount++;
                if (resetRightClickCount >= 3) {
                    setSiyuanLocalStorageItem(SIYUAN_KEY, true, SIYUAN_APP_ID).then(() => {
                        location.reload();
                    });
                }
                setTimeout(() => { resetRightClickCount = 0; }, 1500);
            }
        });
    });
    const closeButton = document.createElement('button');
    closeButton.textContent = '⨉';
    closeButton.className = 'close';
    closeButton.style.color = 'var(--color-text-3)';
    closeButton.style.fontWeight = "900";
    closeButton.style.cursor = 'pointer';
    closeButton.style.backgroundColor = 'var(--b3-theme-surface-light)';
    closeButton.style.borderWidth = '1px';
    closeButton.style.borderStyle = 'dotted';
    closeButton.style.borderColor = 'var(--b3-theme-on-surface)';
    closeButton.style.marginLeft = '3px';
    container.appendChild(resetButton);
    container.appendChild(input);
    container.appendChild(closeButton);
    return container;
}
// 根据配置插入输入框到指定位置
function insertInputContainer(container, selector, position) {
    const targetElement = document.querySelector(selector);
    if (targetElement) {
        if (position === 'BEFORE') {
            targetElement.parentElement.insertBefore(container, targetElement);
        } else if (position === 'BEFORE_BEGIN') {
            targetElement.parentElement.insertAdjacentElement('beforeBegin', container);
        } else if (position === 'APPEND') {
            targetElement.appendChild(container);
        } else if (position === 'BEFORE_END') {
            targetElement.parentElement.insertAdjacentElement('beforeEnd', container);
        } else if (position === 'PREPEND') {
            targetElement.prepend(container);
        } else if (position === 'AFTER_BEGIN') {
            targetElement.parentElement.insertAdjacentElement('afterBegin', container);
        } else if (position === 'INSERT_BEFORE') {
            targetElement.insertAdjacentElement('beforeBegin', container);
        }
    }
}
// 重置过滤器显示,恢复所有项
function resetFilterDisplay(selector) {
    // 1. 普通面板
    const spans1 = document.querySelectorAll(selector + ' li span.b3-list-item__text.ariaLabel');
    spans1.forEach(span => {
        const listItem = span.parentElement;
        listItem.style.display = '';
        if (span.dataset.originalText) {
            span.textContent = span.dataset.originalText;
        } else {
            span.innerHTML = span.textContent; // 移除高亮
        }
    });
    // 2. 脚注面板
    const spans2 = [
        ...document.querySelectorAll(selector + ' .protyle-wysiwyg[contenteditable="true"] div[contenteditable="true"]'),
        ...document.querySelectorAll(selector + ' .footnote-item span')
    ];
    spans2.forEach(span => {
        if (span.parentElement && span.parentElement.style) {
            span.parentElement.style.display = '';
        }
        // 只对纯文本节点移除高亮
        if (span.childNodes.length === 1 && span.childNodes[0].nodeType === 3) {
            span.innerHTML = span.textContent;
        }
    });
}
// 处理输入事件,执行过滤和高亮(批量DOM操作优化)
async function handleInputEvent(input, selector, id) {
    const filterText = input.value.toLowerCase();
    const orGroups = filterText.split('|').map(group =>
        group.split(' ').filter(k => k.trim()).map(term => ({
            value: term.replace(/^!/, ''),
            isNot: term.startsWith('!')
        }))
    );
    // 针对不同容器id,采用不同的节点获取方式
    let spans;
    if (id === 'backlink_filter_container') {
        spans = [
            ...document.querySelectorAll(selector + ' .p[contenteditable="true"]'),
            ...document.querySelectorAll(selector + ' li span.b3-list-item__text.ariaLabel')
        ];
    } else if (id === 'footnote_filter_container') {
        // 专项优化:整体过滤+原文高亮+拼音缓存
        const items = document.querySelectorAll('.footnote-item');
        const pinyinCache = new Map();
        function getPinyin(text, pinyin) {
            if (pinyinCache.has(text)) return pinyinCache.get(text);
            const initials = pinyin.pinyin(text, { pattern: 'first', type: 'array', toneType: 'none', multiple: true }).join('').toLowerCase();
            const full = pinyin.pinyin(text, { pattern: 'pinyin', type: 'array', toneType: 'none', multiple: true }).join('').toLowerCase();
            const result = { initials, full };
            pinyinCache.set(text, result);
            return result;
        }
        try {
            const pinyin = await loadPinyin;
            if (!pinyin?.pinyin) throw new Error('代码片段-过滤器拼音库初始化失败');
            const highlightTerms = orGroups.flat().map(({ value }) => value).filter(Boolean);
            items.forEach(item => {
                const text = item.innerText;
                const lowerText = text.toLowerCase();
                const { initials: pinyinInitials, full: pinyinFull } = getPinyin(text, pinyin);
                const matchAnyGroup = orGroups.some(group =>
                    group.every(({ value, isNot }) => {
                        const hasMatch = [
                            lowerText.includes(value),
                            pinyinInitials.includes(value),
                            pinyinFull.includes(value)
                        ].some(Boolean);
                        return isNot ? !hasMatch : hasMatch;
                    })
                );
                item.style.display = matchAnyGroup ? '' : 'none';
                // 只对原文命中做高亮
                if (matchAnyGroup) {
                    const spans = item.querySelectorAll('div[contenteditable="true"]');
                    spans.forEach(span => {
                        const spanText = span.textContent;
                        if (!filterText) {
                            // 清空时还原原始HTML
                            if (span.dataset.originalHtml) {
                                span.innerHTML = span.dataset.originalHtml;
                                delete span.dataset.originalHtml;
                            }
                        } else if (span.childNodes.length === 1 && span.childNodes[0].nodeType === 3) {
                            // 高亮前缓存原始HTML
                            if (!span.dataset.originalHtml) {
                                span.dataset.originalHtml = span.innerHTML;
                            }
                            let innerHTML = spanText;
                            highlightTerms.forEach(value => {
                                if (value) {
                                    const reg = new RegExp(value, 'gi');
                                    innerHTML = innerHTML.replace(reg, match => `<span class=\"filter_highlight\">${match}</span>`);
                                }
                            });
                            span.innerHTML = innerHTML;
                        }
                    });
                }
            });
        } catch (e) {
            // 降级为纯文本过滤+高亮
            items.forEach(item => {
                const text = item.innerText;
                const lowerText = text.toLowerCase();
                const matchAnyGroup = orGroups.some(group =>
                    group.every(({ value, isNot }) => {
                        const hasMatch = lowerText.includes(value);
                        return isNot ? !hasMatch : hasMatch;
                    })
                );
                item.style.display = matchAnyGroup ? '' : 'none';
                if (matchAnyGroup) {
                    const spans = item.querySelectorAll('div[contenteditable="true"]');
                    spans.forEach(span => {
                        const spanText = span.textContent;
                        if (!filterText) {
                            if (span.dataset.originalHtml) {
                                span.innerHTML = span.dataset.originalHtml;
                                delete span.dataset.originalHtml;
                            }
                        } else if (span.childNodes.length === 1 && span.childNodes[0].nodeType === 3) {
                            if (!span.dataset.originalHtml) {
                                span.dataset.originalHtml = span.innerHTML;
                            }
                            let innerHTML = spanText;
                            orGroups.flat().forEach(({ value }) => {
                                if (value) {
                                    const reg = new RegExp(value, 'gi');
                                    innerHTML = innerHTML.replace(reg, match => `<span class=\"filter_highlight\">${match}</span>`);
                                }
                            });
                            span.innerHTML = innerHTML;
                        }
                    });
                }
            });
        }
        return;
    } else {
        spans = document.querySelectorAll(selector + ' li span.b3-list-item__text.ariaLabel');
    }
    try {
        const pinyin = await loadPinyin;
        if (!pinyin?.pinyin) throw new Error('代码片段-过滤器拼音库初始化失败');
        const highlightTerms = orGroups.flatMap(group =>
            group.filter(term => !term.isNot).map(term => term.value)
        );
        // 批量DOM操作:先收集所有变更,再统一apply
        const updates = [];
        spans.forEach(span => {
            const originalText = span.dataset.originalText || span.textContent;
            if (!span.dataset.originalText) {
                span.dataset.originalText = originalText;
            }
            const listItem = span.parentElement;
            const text = span.textContent;
            const pinyinInitials = pinyin.pinyin(text, {
                pattern: 'first', type: 'array', toneType: 'none', multiple: true
            }).join('').toLowerCase();
            const pinyinFull = pinyin.pinyin(text, {
                pattern: 'pinyin', type: 'array', toneType: 'none', multiple: true
            }).join('').toLowerCase();
            const matchAnyGroup = orGroups.some(group => {
                return group.every(({ value, isNot }) => {
                    const hasMatch = [
                        text.toLowerCase().includes(value),
                        pinyinInitials.includes(value),
                        pinyinFull.includes(value)
                    ].some(Boolean);
                    return isNot ? !hasMatch : hasMatch;
                });
            });
            updates.push({
                span,
                listItem,
                matchAnyGroup,
                originalText,
                highlightTerms
            });
        });
        // 统一apply
        updates.forEach(({span, listItem, matchAnyGroup, originalText, highlightTerms}) => {
            span.textContent = originalText;
            listItem.style.display = matchAnyGroup ? '' : 'none';
            if (matchAnyGroup && highlightTerms.length > 0) {
                span.innerHTML = '';
                span.appendChild(buildHighlightFragment(originalText, pinyin, highlightTerms));
            }
        });
    } catch (e) {
        const highlightTerms = orGroups.flatMap(group =>
            group.filter(term => !term.isNot).map(term => term.value)
        );
        // 批量DOM操作:先收集所有变更,再统一apply
        const updates = [];
        spans.forEach(span => {
            const originalText = span.dataset.originalText || span.textContent;
            updates.push({span, originalText});
        });
        updates.forEach(({span, originalText}) => {
            span.textContent = originalText;
        });
        spans.forEach(span => {
            fallbackHighlightSpan(span, orGroups, highlightTerms);
        });
    }
}
// 高亮匹配项(支持拼音)
function highlightSpan(span, pinyin, orGroups, highlightTerms) {
    const originalText = span.dataset.originalText || span.textContent;
    span.textContent = originalText;
    if (!span.dataset.originalText) {
        span.dataset.originalText = originalText;
    }
    const listItem = span.parentElement;
    const text = span.textContent;
    const pinyinInitials = pinyin.pinyin(text, {
        pattern: 'first', type: 'array', toneType: 'none', multiple: true
    }).join('').toLowerCase();
    const pinyinFull = pinyin.pinyin(text, {
        pattern: 'pinyin', type: 'array', toneType: 'none', multiple: true
    }).join('').toLowerCase();
    const matchAnyGroup = orGroups.some(group => {
        return group.every(({ value, isNot }) => {
            const hasMatch = [
                text.toLowerCase().includes(value),
                pinyinInitials.includes(value),
                pinyinFull.includes(value)
            ].some(Boolean);
            return isNot ? !hasMatch : hasMatch;
        });
    });
    listItem.style.display = matchAnyGroup ? '' : 'none';
    if (matchAnyGroup && highlightTerms.length > 0) {
        span.innerHTML = '';
        span.appendChild(buildHighlightFragment(originalText, pinyin, highlightTerms));
    }
}
// 构建高亮片段
function buildHighlightFragment(originalText, pinyin, highlightTerms) {
    const fragment = document.createDocumentFragment();
    let lastIndex = 0;
    const pinyinMap = originalText.split('').map((char, index) => ({
        char: char,
        initials: pinyin.pinyin(char, { pattern: 'first', toneType: 'none' })[0]?.toLowerCase() || '',
        index: index
    }));
    const matchPositions = new Set();
    const lowerOriginal = originalText.toLowerCase();
    highlightTerms.forEach(term => {
        const lowerTerm = term.toLowerCase();
        let pos = -1;
        while ((pos = lowerOriginal.indexOf(lowerTerm, pos + 1)) !== -1) {
            for (let i = 0; i < term.length; i++) {
                matchPositions.add(pos + i);
            }
        }
        pinyinMap.forEach((p, index) => {
            if (p.initials === lowerTerm) {
                matchPositions.add(index);
            }
        });
        const initialsSequence = pinyinMap.map(p => p.initials).join('');
        if (/^[a-z]{2,}$/.test(lowerTerm)) {
            let pos = -1;
            while ((pos = initialsSequence.indexOf(lowerTerm, pos + 1)) !== -1) {
                for (let i = 0; i < lowerTerm.length; i++) {
                    const charIndex = pos + i;
                    if (charIndex < pinyinMap.length) {
                        matchPositions.add(pinyinMap[charIndex].index);
                    }
                }
            }
        }
    });
    const sortedPositions = Array.from(matchPositions).sort((a, b) => a - b);
    const regexParts = [];
    let currentStart = sortedPositions[0];
    let currentEnd = currentStart + 1;
    for (let i = 1; i < sortedPositions.length; i++) {
        if (sortedPositions[i] === currentEnd) {
            currentEnd++;
        } else {
            regexParts.push(originalText.slice(currentStart, currentEnd));
            currentStart = sortedPositions[i];
            currentEnd = currentStart + 1;
        }
    }
    if (sortedPositions.length > 0) {
        regexParts.push(originalText.slice(currentStart, currentEnd));
    }
    const regexPattern = regexParts
        .map(str => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
        .join('|');
    if (regexPattern) {
        const regex = new RegExp(`(${regexPattern})`, 'gi');
        let match;
        while ((match = regex.exec(originalText)) !== null) {
            if (match.index > lastIndex) {
                fragment.appendChild(document.createTextNode(
                    originalText.slice(lastIndex, match.index)
                ));
            }
            const highlight = document.createElement('span');
            highlight.className = 'filter_highlight';
            highlight.textContent = match[0];
            fragment.appendChild(highlight);
            lastIndex = regex.lastIndex;
        }
    }
    if (lastIndex < originalText.length) {
        fragment.appendChild(document.createTextNode(
            originalText.slice(lastIndex)
        ));
    }
    return fragment;
}
// 降级高亮处理(无拼音库时)
function fallbackHighlightSpan(span, orGroups, highlightTerms) {
    const originalText = span.dataset.originalText || span.textContent;
    span.textContent = originalText;
    const text = span.textContent.toLowerCase();
    const matchAnyGroup = orGroups.some(group =>
        group.every(({ value, isNot }) => {
            const hasMatch = text.includes(value);
            return isNot ? !hasMatch : hasMatch;
        })
    );
    span.parentElement.style.display = matchAnyGroup ? '' : 'none';
    if (matchAnyGroup && highlightTerms.length > 0) {
        let innerHTML = originalText;
        highlightTerms.forEach(term => {
            const reg = new RegExp(term, 'gi');
            innerHTML = innerHTML.replace(reg, match => `<span class="filter_highlight">${match}</span>`);
        });
        span.innerHTML = innerHTML;
    }
}

// ================= 监听与交互区 =================
// 监听tab切换,动态刷新过滤器
let tabObservers = [];
let layoutDockObserver = null; // 全局唯一observer,避免重复创建
function startTabObserver(parentSelector, activeClass, observerArray, callback) {
    if (observerArray.length > 0) return;
    whenAllElementsExist(parentSelector).then((parentElements) => {
        parentElements.forEach((parentElement) => {
            const observer = new MutationObserver((mutationsList) => {
                for (const mutation of mutationsList) {
                    if (mutation.type === 'childList') {
                        mutation.addedNodes.forEach(node => {
                            if (node.classList?.contains(activeClass)) {
                                callback();
                            }
                        });
                    }
                    if (mutation.type === 'attributes' && mutation.target.classList?.contains(activeClass)) {
                        callback();
                    }
                }
            });
            observer.observe(parentElement, {
                childList: true,
                subtree: true,
                attributes: true,
                attributeFilter: ['class']
            });
            observerArray.push(observer);
        });
    }).catch(() => {
        const documentObserver = new MutationObserver((mutationsList) => {
            for (const mutation of mutationsList) {
                if (mutation.target.classList?.contains(activeClass)) {
                    callback();
                }
            }
        });
        documentObserver.observe(document, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['class']
        });
        observerArray.push(documentObserver);
    });
}
// 停止tab监听
function stopTabObserver(observerArray) {
    observerArray.forEach(observer => observer.disconnect());
    observerArray.length = 0;
}
// 添加dock按钮,控制过滤器开关
function addButton(pin) {
    let flag = false;
    const button = document.createElement('span');
    button.className = 'dock__item ariaLabel';
    button.textContent = '🕸️';
    button.setAttribute('aria-label', '      关闭筛选过滤器\n      右击可隐藏按钮');
    button.onclick = (event) => {
        event.preventDefault();
        event.stopPropagation();
        removeAllFilters();
        if (flag) {
            addAllFilters();
            startTabObserver('.layout-tab-container.fn__flex-1', 'layout__tab--active', tabObservers, addAllFilters);
        } else {
            stopTabObserver(tabObservers);
        }
        flag = !flag;
        button.setAttribute('aria-label', flag ? '      开启筛选过滤器\n      右击可隐藏按钮' : '      关闭筛选过滤器\n      右击可隐藏按钮');
        button.textContent = flag ? '🕸︎' : '🕸️';
    };
    button.addEventListener('contextmenu', (e) => {
        e.preventDefault();
        if (confirm('               隐藏过滤器按钮?\n可通过reset按钮连续右键3次恢复')) {
            setSiyuanLocalStorageItem(SIYUAN_KEY, false, SIYUAN_APP_ID).then(() => {
                button.remove();
            });
        }
    });
    pin.before(button);
}
// 注册清理函数,卸载时自动清理所有过滤器和监听
function registerCleanup() {
    const cleanup = () => {
        removeAllFilters();
        stopTabObserver(tabObservers);
        // 断开全局唯一observer,防止内存泄漏
        if (layoutDockObserver) {
            layoutDockObserver.disconnect();
            layoutDockObserver = null;
        }
        document.querySelectorAll('.dock__item[aria-label*="筛选过滤器"]').forEach(btn => btn.remove());
        document.querySelectorAll('style[data-style="filter_highlight"]').forEach(style => style.remove());
    };
    const currentScript = document.currentScript?.src;
    if (!currentScript) {
        console.error('无法确定当前脚本路径,loadSnippets后续清理函数未注册。');
        return;
    }
    const currentFilename = currentScript.split('/').pop();
    if (window.__registerCleanupHandler) {
        window.__registerCleanupHandler(currentFilename, cleanup);
    } else {
        console.error('清理注册接口不可用');
    }
}

// ================= 主流程区 =================
// 批量添加所有过滤器
function addAllFilters() {
    FILTER_CONFIGS.forEach(config => {
        addFilterBox(config);
    });
}
// 批量移除所有过滤器
function removeAllFilters() {
    FILTER_CONFIGS.forEach(config => {
        const fCs = document.getElementById(config.id);
        if (fCs) {
            fCs.remove();
        }
    });
}
// 主入口,初始化所有功能
async function main() {
    if (isMobile()) return;
    whenElementExist('#dockLeft .dock__items .dock__item--pin').then(async (pin) => {
        let show = await getSiyuanLocalStorageItem(SIYUAN_KEY, SIYUAN_APP_ID);
        if (show === null) show = showFilterButtonDefault;
        if (show === true || show === 'true') addButton(pin);
        addAllFilters();
        startTabObserver('.layout-tab-container.fn__flex-1', 'layout__tab--active', tabObservers, addAllFilters);
    });
    // 监听闪卡弹窗,弹出时自动添加过滤器
    const targetBody = document.querySelector('body');
    const config2 = { childList: true, subtree: true };
    const callback2 = function (mutationsList, observer) {
        for (let mutation of mutationsList) {
            if (mutation.type === 'childList') {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE && node.matches('[data-key="dialog-viewcards"].b3-dialog--open')) {
                        addAllFilters();
                    }
                });
            }
        }
    };
    // 保证全局唯一observer,避免重复创建
    if (layoutDockObserver) {
        layoutDockObserver.disconnect();
    }
    layoutDockObserver = new MutationObserver(callback2);
    layoutDockObserver.observe(targetBody, config2);
    registerCleanup(); // 每次都注册清理,防止泄漏
}

// 立即执行主流程
main();
})();

  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    28446 引用 • 119786 回帖
  • 代码片段

    代码片段分为 CSS 与 JS 两种代码,添加在 [设置 - 外观 - 代码片段] 中,这些代码会在思源笔记加载时自动执行,用于改善笔记的样式或功能。

    用户在该标签下分享代码片段时需在帖子标题前添加 [css] [js] 用于区分代码片段类型。

    285 引用 • 1986 回帖
6 操作
chuchen 在 2025-07-15 20:26:45 更新了该帖
chuchen 在 2025-07-15 17:24:36 更新了该帖
chuchen 在 2025-07-06 14:59:11 更新了该帖
chuchen 在 2025-07-05 17:00:10 更新了该帖 chuchen 在 2025-07-05 16:36:39 更新了该帖 chuchen 在 2025-07-05 15:46:28 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • nightstars 4 评论

    请问你是怎么学的,报班吗?我也想学一下 js 代码,

    刷 b 站的视频,找你能听进去那些,我都开的 2 倍速,先对 js 有个基本概念,知道 dom 是怎么回事,然后在论坛看了各位大佬的代码片段,知道思源的前端是怎么跑起来,然后就结合 ai 了……
    chuchen
    @chuchen 请问,听的时候,还用做笔记吗?还是说,只听就行?
    nightstars
    看你学习习惯 对我来说做笔记还是有必要吧 但更关键的是学计算机语言还是需要动手的,按刻意练习的方法来 实操过才深刻,才能印证一些想法,刚开始的时候有很多似是而非的,当然也不用着急,先一步步建立心理表征,后面在实例里就知道是怎么用的了,我基础也不算扎实,
    chuchen
    忘了说 其实我看的那位老师直接有分享笔记的,评论区也有人分享,我是在那个基础上整理,毕竟获取得太轻松了,没太入脑。另外,现在学习语言变得轻松的很重要的一点是——啥不懂直接问 AI,就很细的知识点,也不需要多智能的 AI,免费的就行,基本都能带出很多相关的知识点。其实这事你只要开始就很快有收获的
    chuchen
  • 其他回帖
  • ZQ11 1 评论

    做的非常好 感觉可以做成一个插件 做得更好一点哈哈哈

    其实我现在也没理解什么场景一定需要用到插件,那比如这个功能,可能用到插件就是可以记录 关闭 这个过滤功能之后 下 打开 思源笔记的时候就能按你选择的来。但是代码片段就很灵活呀 大家都可见的,随时可以更新
    chuchen

推荐标签 标签

  • CSS

    CSS(Cascading Style Sheet)“层叠样式表”是用于控制网页样式并允许将样式信息与网页内容分离的一种标记性语言。

    200 引用 • 545 回帖 • 1 关注
  • Maven

    Maven 是基于项目对象模型(POM)、通过一小段描述信息来管理项目的构建、报告和文档的软件项目管理工具。

    188 引用 • 319 回帖 • 222 关注
  • Windows

    Microsoft Windows 是美国微软公司研发的一套操作系统,它问世于 1985 年,起初仅仅是 Microsoft-DOS 模拟环境,后续的系统版本由于微软不断的更新升级,不但易用,也慢慢的成为家家户户人们最喜爱的操作系统。

    232 引用 • 484 回帖
  • 职场

    找到自己的位置,萌新烦恼少。

    127 引用 • 1708 回帖 • 1 关注
  • OnlyOffice
    4 引用 • 41 关注
  • frp

    frp 是一个可用于内网穿透的高性能的反向代理应用,支持 TCP、UDP、 HTTP 和 HTTPS 协议。

    17 引用 • 7 回帖 • 1 关注
  • 叶归
    25 引用 • 100 回帖 • 37 关注
  • Netty

    Netty 是一个基于 NIO 的客户端-服务器编程框架,使用 Netty 可以让你快速、简单地开发出一个可维护、高性能的网络应用,例如实现了某种协议的客户、服务端应用。

    49 引用 • 33 回帖 • 63 关注
  • ngrok

    ngrok 是一个反向代理,通过在公共的端点和本地运行的 Web 服务器之间建立一个安全的通道。

    7 引用 • 63 回帖 • 668 关注
  • 星云链

    星云链是一个开源公链,业内简单的将其称为区块链上的谷歌。其实它不仅仅是区块链搜索引擎,一个公链的所有功能,它基本都有,比如你可以用它来开发部署你的去中心化的 APP,你可以在上面编写智能合约,发送交易等等。3 分钟快速接入星云链 (NAS) 测试网

    3 引用 • 16 回帖
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    950 引用 • 1460 回帖 • 2 关注
  • TGIF

    Thank God It's Friday! 感谢老天,总算到星期五啦!

    293 引用 • 4496 回帖 • 688 关注
  • Mobi.css

    Mobi.css is a lightweight, flexible CSS framework that focus on mobile.

    1 引用 • 6 回帖 • 799 关注
  • Hibernate

    Hibernate 是一个开放源代码的对象关系映射框架,它对 JDBC 进行了非常轻量级的对象封装,使得 Java 程序员可以随心所欲的使用对象编程思维来操纵数据库。

    39 引用 • 103 回帖 • 740 关注
  • JavaScript

    JavaScript 一种动态类型、弱类型、基于原型的直译式脚本语言,内置支持类型。它的解释器被称为 JavaScript 引擎,为浏览器的一部分,广泛用于客户端的脚本语言,最早是在 HTML 网页上使用,用来给 HTML 网页增加动态功能。

    736 引用 • 1307 回帖 • 2 关注
  • 人工智能

    人工智能(Artificial Intelligence)是研究、开发用于模拟、延伸和扩展人的智能的理论、方法、技术及应用系统的一门技术科学。

    140 引用 • 407 回帖
  • WordPress

    WordPress 是一个使用 PHP 语言开发的博客平台,用户可以在支持 PHP 和 MySQL 数据库的服务器上架设自己的博客。也可以把 WordPress 当作一个内容管理系统(CMS)来使用。WordPress 是一个免费的开源项目,在 GNU 通用公共许可证(GPLv2)下授权发布。

    46 引用 • 114 回帖 • 139 关注
  • V2EX

    V2EX 是创意工作者们的社区。这里目前汇聚了超过 400,000 名主要来自互联网行业、游戏行业和媒体行业的创意工作者。V2EX 希望能够成为创意工作者们的生活和事业的一部分。

    16 引用 • 236 回帖 • 224 关注
  • jQuery

    jQuery 是一套跨浏览器的 JavaScript 库,强化 HTML 与 JavaScript 之间的操作。由 John Resig 在 2006 年 1 月的 BarCamp NYC 上释出第一个版本。全球约有 28% 的网站使用 jQuery,是非常受欢迎的 JavaScript 库。

    63 引用 • 134 回帖 • 736 关注
  • SSL

    SSL(Secure Sockets Layer 安全套接层),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。TLS 与 SSL 在传输层对网络连接进行加密。

    70 引用 • 193 回帖 • 404 关注
  • 支付宝

    支付宝是全球领先的独立第三方支付平台,致力于为广大用户提供安全快速的电子支付/网上支付/安全支付/手机支付体验,及转账收款/水电煤缴费/信用卡还款/AA 收款等生活服务应用。

    29 引用 • 347 回帖 • 2 关注
  • Openfire

    Openfire 是开源的、基于可拓展通讯和表示协议 (XMPP)、采用 Java 编程语言开发的实时协作服务器。Openfire 的效率很高,单台服务器可支持上万并发用户。

    6 引用 • 7 回帖 • 133 关注
  • Wide

    Wide 是一款基于 Web 的 Go 语言 IDE。通过浏览器就可以进行 Go 开发,并有代码自动完成、查看表达式、编译反馈、Lint、实时结果输出等功能。

    欢迎访问我们运维的实例: https://wide.b3log.org

    30 引用 • 218 回帖 • 664 关注
  • 游戏

    沉迷游戏伤身,强撸灰飞烟灭。

    188 引用 • 833 回帖 • 1 关注
  • Node.js

    Node.js 是一个基于 Chrome JavaScript 运行时建立的平台, 用于方便地搭建响应速度快、易于扩展的网络应用。Node.js 使用事件驱动, 非阻塞 I/O 模型而得以轻量和高效。

    139 引用 • 269 回帖 • 1 关注
  • OpenResty

    OpenResty 是一个基于 NGINX 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

    17 引用 • 51 关注
  • BAE

    百度应用引擎(Baidu App Engine)提供了 PHP、Java、Python 的执行环境,以及云存储、消息服务、云数据库等全面的云服务。它可以让开发者实现自动地部署和管理应用,并且提供动态扩容和负载均衡的运行环境,让开发者不用考虑高成本的运维工作,只需专注于业务逻辑,大大降低了开发者学习和迁移的成本。

    19 引用 • 75 回帖 • 702 关注