[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 引用 • 119783 回帖
  • 代码片段

    代码片段分为 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

推荐标签 标签

  • WiFiDog

    WiFiDog 是一套开源的无线热点认证管理工具,主要功能包括:位置相关的内容递送;用户认证和授权;集中式网络监控。

    1 引用 • 7 回帖 • 633 关注
  • 开源中国

    开源中国是目前中国最大的开源技术社区。传播开源的理念,推广开源项目,为 IT 开发者提供了一个发现、使用、并交流开源技术的平台。目前开源中国社区已收录超过两万款开源软件。

    7 引用 • 86 回帖
  • Markdown

    Markdown 是一种轻量级标记语言,用户可使用纯文本编辑器来排版文档,最终通过 Markdown 引擎将文档转换为所需格式(比如 HTML、PDF 等)。

    173 引用 • 1559 回帖
  • C++

    C++ 是在 C 语言的基础上开发的一种通用编程语言,应用广泛。C++ 支持多种编程范式,面向对象编程、泛型编程和过程化编程。

    110 引用 • 153 回帖
  • Spark

    Spark 是 UC Berkeley AMP lab 所开源的类 Hadoop MapReduce 的通用并行框架。Spark 拥有 Hadoop MapReduce 所具有的优点;但不同于 MapReduce 的是 Job 中间输出结果可以保存在内存中,从而不再需要读写 HDFS,因此 Spark 能更好地适用于数据挖掘与机器学习等需要迭代的 MapReduce 的算法。

    74 引用 • 46 回帖 • 563 关注
  • 千千插件

    千千块(自定义块 css 和 js)
    可以用 ai 提示词来无限创作思源笔记

    32 引用 • 69 回帖
  • Ubuntu

    Ubuntu(友帮拓、优般图、乌班图)是一个以桌面应用为主的 Linux 操作系统,其名称来自非洲南部祖鲁语或豪萨语的“ubuntu”一词,意思是“人性”、“我的存在是因为大家的存在”,是非洲传统的一种价值观,类似华人社会的“仁爱”思想。Ubuntu 的目标在于为一般用户提供一个最新的、同时又相当稳定的主要由自由软件构建而成的操作系统。

    127 引用 • 169 回帖
  • 旅游

    希望你我能在旅途中找到人生的下一站。

    105 引用 • 908 回帖
  • 小说

    小说是以刻画人物形象为中心,通过完整的故事情节和环境描写来反映社会生活的文学体裁。

    33 引用 • 108 回帖
  • WebClipper

    Web Clipper 是一款浏览器剪藏扩展,它可以帮助你把网页内容剪藏到本地。

    3 引用 • 9 回帖 • 2 关注
  • WebSocket

    WebSocket 是 HTML5 中定义的一种新协议,它实现了浏览器与服务器之间的全双工通信(full-duplex)。

    48 引用 • 206 回帖 • 284 关注
  • 周末

    星期六到星期天晚,实行五天工作制后,指每周的最后两天。再过几年可能就是三天了。

    14 引用 • 297 回帖 • 1 关注
  • 阿里巴巴

    阿里巴巴网络技术有限公司(简称:阿里巴巴集团)是以曾担任英语教师的马云为首的 18 人,于 1999 年在中国杭州创立,他们相信互联网能够创造公平的竞争环境,让小企业通过创新与科技扩展业务,并在参与国内或全球市场竞争时处于更有利的位置。

    43 引用 • 221 回帖 • 11 关注
  • GitBook

    GitBook 使您的团队可以轻松编写和维护高质量的文档。 分享知识,提高团队的工作效率,让用户满意。

    3 引用 • 8 回帖
  • 运维

    互联网运维工作,以服务为中心,以稳定、安全、高效为三个基本点,确保公司的互联网业务能够 7×24 小时为用户提供高质量的服务。

    151 引用 • 257 回帖 • 1 关注
  • GraphQL

    GraphQL 是一个用于 API 的查询语言,是一个使用基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。GraphQL 并没有和任何特定数据库或者存储引擎绑定,而是依靠你现有的代码和数据支撑。

    4 引用 • 3 回帖 • 11 关注
  • Follow
    4 引用 • 13 回帖 • 19 关注
  • sts
    2 引用 • 2 回帖 • 260 关注
  • H2

    H2 是一个开源的嵌入式数据库引擎,采用 Java 语言编写,不受平台的限制,同时 H2 提供了一个十分方便的 web 控制台用于操作和管理数据库内容。H2 还提供兼容模式,可以兼容一些主流的数据库,因此采用 H2 作为开发期的数据库非常方便。

    11 引用 • 54 回帖 • 691 关注
  • 电影

    这是一个不能说的秘密。

    125 引用 • 610 回帖
  • SendCloud

    SendCloud 由搜狐武汉研发中心孵化的项目,是致力于为开发者提供高质量的触发邮件服务的云端邮件发送平台,为开发者提供便利的 API 接口来调用服务,让邮件准确迅速到达用户收件箱并获得强大的追踪数据。

    2 引用 • 8 回帖 • 545 关注
  • 持续集成

    持续集成(Continuous Integration)是一种软件开发实践,即团队开发成员经常集成他们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。

    15 引用 • 7 回帖
  • InfluxDB

    InfluxDB 是一个开源的没有外部依赖的时间序列数据库。适用于记录度量,事件及实时分析。

    2 引用 • 123 关注
  • Unity

    Unity 是由 Unity Technologies 开发的一个让开发者可以轻松创建诸如 2D、3D 多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

    27 引用 • 7 回帖 • 92 关注
  • Google

    Google(Google Inc.,NASDAQ:GOOG)是一家美国上市公司(公有股份公司),于 1998 年 9 月 7 日以私有股份公司的形式创立,设计并管理一个互联网搜索引擎。Google 公司的总部称作“Googleplex”,它位于加利福尼亚山景城。Google 目前被公认为是全球规模最大的搜索引擎,它提供了简单易用的免费服务。不作恶(Don't be evil)是谷歌公司的一项非正式的公司口号。

    51 引用 • 200 回帖 • 2 关注
  • 心情

    心是产生任何想法的源泉,心本体会陷入到对自己本体不能理解的状态中,因为心能产生任何想法,不能分出对错,不能分出自己。

    59 引用 • 369 回帖 • 1 关注
  • Shell

    Shell 脚本与 Windows/Dos 下的批处理相似,也就是用各类命令预先放入到一个文件中,方便一次性执行的一个程序文件,主要是方便管理员进行设置或者管理用的。但是它比 Windows 下的批处理更强大,比用其他编程程序编辑的程序效率更高,因为它使用了 Linux/Unix 下的命令。

    126 引用 • 83 回帖 • 1 关注