[js] 7.10 更新: 新增快捷选样式颜色, 批量改颜色!!!.... 太多功能了 [标题 / 块 / 编辑 [ 体验史诗级增强包]

标题

  1. 如果标题是折叠的状态,按下 enter 键,自动创建同级标题
  2. 一键打开/关闭同级所有标题
  3. 自动修改标题序列(基于别人的上面改的)

  1. 在子序号为空的时候,按下 tab 键,则该行转为段落
  2. 如果是段落,按 tab 键会缩进合并到上方列表里面(不完美,因为 moveblock 会强制全局刷新一次)
  3. 优化列表的箭头颜色
  4. 自动更正错误列表序号,包含列表和段落的序列对齐
  5. 块内如果有任务,自动统计任务显示在序列标题的末尾初
  6. 一键关闭同级块/一键展开块的子模块
  7. 编辑的时候高亮,颜色为红色(整合其他人的包)
  8. 段落高亮编辑特效(结合别人的)

工具栏

  1. 段落/标题新增工具栏编辑
  2. 列表下的段落增加工具栏编辑
  3. 新增工具栏功能整合成下拉框,同时允许自动弹出,高亮等
  4. 批量编辑多个块粗体/斜体/下划线/标注,颜色编辑
  5. 新增批量颜色编辑
  6. 新增编辑器增加颜色快捷点选

美中不足需要官方优化的包括:

看了官方有个新的开源社区,貌似也可以做下面功能?不确定有些想法,周末再试
1 列表块的拖动排序,经常排列错,多个手势太繁琐,最好是在思源设置里可选,默认只有一个拖动到元素下方(不要还能拖动成了折叠里面的子项目(消失了),还有经常拖动不到下方)

不出意外,后面应该不会再更新新功能,这几个是拿来练手 ai 编程的.....😄😄😄😄

从此江湖上只有哥的传说,而哥已不再江湖,再默默用思源学习..😂😂😂😂😂

第 1. 编辑器颜色快捷按钮和批量编辑颜色

PixPin20250710190405.gif

第 2.标题自动加序列,改序列号

在之前一位朋友的 css 基础上改的,主要是增加了如果小题在上面,或者不是从 h1 开始,序号正常显示的支持

PixPin20250710190009.gif

第 3.折叠标题创建同级标题

PixPin20250706210348.gif

第 4.一键打开/关闭同级所有标题/块

使用方法:

按照 alt+ 点击标题前的小箭头: 打开/关闭同级标题

按住 alt+ 点击块钱的小箭头: 关闭同级列表,展开该列表下的所有块(含子块)

PixPin20250706213029.gif

第 5. 在子序号为空的时候,按下 tab 键,则该行转为段落

PixPin20250706210703.gif

第 6.如果是段落,按 tab 键会缩进合并到上方列表里面

注意: 不完美,因为 moveblock 会强制全局刷新一次,也就只有官方解决这个问题了

PixPin20250706210910.gif

第 7.优化列表的箭头颜色

PixPin20250706211027.gif

第 8.自动更正错误列表序号,包含列表和段落的序列对齐

PixPin20250701000953.gif

第 9.块内如果有任务,自动统计任务显示在序列标题的末尾初

PixPin20250706211333.gif

第 10. 新增工具栏编辑支持段落/标题/列表下的段落

PixPin20250706211522.gif

第 11. 批量编辑多个块粗体/斜体/下划线/标注(很多人在找的功能吧)

PixPin20250706211740.gif

第 12. 高亮编辑区

PixPin20250707122050.gif

代码 1: 标题和块的优化

注意: 尽管我都做了测试,难免还有问题.在 G_CONFIG 里根据需要自行开关功能

//version: v1.0
//plugin name: 标题自动序号(需要搭配下面的js一并使用)
//author: 少侠,微信:572378589
//update time: 更新时间7/10



/* ================= 标题自动编号模块 ================= */
body {
    counter-reset: h1-count;
}

h1,
.h1 {
    counter-increment: h1-count;
    counter-reset: h2-count;
}

h2,
.h2 {
    counter-increment: h2-count;
    counter-reset: h3-count;
}

h3,
.h3 {
    counter-increment: h3-count;
    counter-reset: h4-count;
}

h4,
.h4 {
    counter-increment: h4-count;
    counter-reset: h5-count;
}

h5,
.h5 {
    counter-increment: h5-count;
    counter-reset: h6-count;
}

h6,
.h6 {
    counter-increment: h6-count;
}

/* 通用计数器样式 */
.protyle-wysiwyg [data-node-id][class^="h"] div:first-child:before,
.b3-typography h1:before,
.b3-typography h2:before,
.b3-typography h3:before,
.b3-typography h4:before,
.b3-typography h5:before,
.b3-typography h6:before {
    display: inline-block !important;
    float: none;
    margin-right: 8px;
    font-size: 100%;
    opacity: 1 !important;
    /* 强制显示防止折叠隐藏 */
}

/* 层级式编号生成规则 */
.protyle-wysiwyg [data-node-id].h1 div:first-child:before,
.b3-typography h1:before {
    content: counter(h1-count) "\00A0";
}

.protyle-wysiwyg [data-node-id].h2 div:first-child:before,
.b3-typography h2:before {
    content: counter(h1-count) "." counter(h2-count) "\00A0";
}

.protyle-wysiwyg [data-node-id].h3 div:first-child:before,
.b3-typography h3:before {
    content: counter(h1-count) "." counter(h2-count) "." counter(h3-count) "\00A0";
}

.protyle-wysiwyg [data-node-id].h4 div:first-child:before,
.b3-typography h4:before {
    content: counter(h1-count) "." counter(h2-count) "." counter(h3-count) "." counter(h4-count) "\00A0";
}

.protyle-wysiwyg [data-node-id].h5 div:first-child:before,
.b3-typography h5:before {
    content: counter(h1-count) "." counter(h2-count) "." counter(h3-count) "." counter(h4-count) "." counter(h5-count) "\00A0";
    font-size: 90% !important;
}

.protyle-wysiwyg [data-node-id].h6 div:first-child:before,
.b3-typography h6:before {
    content: counter(h1-count) "." counter(h2-count) "." counter(h3-count) "." counter(h4-count) "." counter(h5-count) "." counter(h6-count) "\00A0";
    font-size: 85% !important;
}

/* ================= 动态编号适配模块 (JS 辅助) ================= */
/* 适配:当 H2 是顶级标题时 */
body.top-heading-h2 .protyle-wysiwyg [data-node-id].h2 div:first-child:before,
body.top-heading-h2 .b3-typography h2:before {
    content: counter(h2-count) "\00A0";
}
body.top-heading-h2 .protyle-wysiwyg [data-node-id].h3 div:first-child:before,
body.top-heading-h2 .b3-typography h3:before {
    content: counter(h2-count) "." counter(h3-count) "\00A0";
}
body.top-heading-h2 .protyle-wysiwyg [data-node-id].h4 div:first-child:before,
body.top-heading-h2 .b3-typography h4:before {
    content: counter(h2-count) "." counter(h3-count) "." counter(h4-count) "\00A0";
}
body.top-heading-h2 .protyle-wysiwyg [data-node-id].h5 div:first-child:before,
body.top-heading-h2 .b3-typography h5:before {
    content: counter(h2-count) "." counter(h3-count) "." counter(h4-count) "." counter(h5-count) "\00A0";
}
body.top-heading-h2 .protyle-wysiwyg [data-node-id].h6 div:first-child:before,
body.top-heading-h2 .b3-typography h6:before {
    content: counter(h2-count) "." counter(h3-count) "." counter(h4-count) "." counter(h5-count) "." counter(h6-count) "\00A0";
}

/* 适配:当 H3 是顶级标题时 */
body.top-heading-h3 .protyle-wysiwyg [data-node-id].h3 div:first-child:before,
body.top-heading-h3 .b3-typography h3:before {
    content: counter(h3-count) "\00A0";
}
body.top-heading-h3 .protyle-wysiwyg [data-node-id].h4 div:first-child:before,
body.top-heading-h3 .b3-typography h4:before {
    content: counter(h3-count) "." counter(h4-count) "\00A0";
}
body.top-heading-h3 .protyle-wysiwyg [data-node-id].h5 div:first-child:before,
body.top-heading-h3 .b3-typography h5:before {
    content: counter(h3-count) "." counter(h4-count) "." counter(h5-count) "\00A0";
}
body.top-heading-h3 .protyle-wysiwyg [data-node-id].h6 div:first-child:before,
body.top-heading-h3 .b3-typography h6:before {
    content: counter(h3-count) "." counter(h4-count) "." counter(h5-count) "." counter(h6-count) "\00A0";
}

/* 适配:当 H4 是顶级标题时 */
body.top-heading-h4 .protyle-wysiwyg [data-node-id].h4 div:first-child:before,
body.top-heading-h4 .b3-typography h4:before {
    content: counter(h4-count) "\00A0";
}
body.top-heading-h4 .protyle-wysiwyg [data-node-id].h5 div:first-child:before,
body.top-heading-h4 .b3-typography h5:before {
    content: counter(h4-count) "." counter(h5-count) "\00A0";
}
body.top-heading-h4 .protyle-wysiwyg [data-node-id].h6 div:first-child:before,
body.top-heading-h4 .b3-typography h6:before {
    content: counter(h4-count) "." counter(h5-count) "." counter(h6-count) "\00A0";
}

/* 适配:当 H5 是顶级标题时 */
body.top-heading-h5 .protyle-wysiwyg [data-node-id].h5 div:first-child:before,
body.top-heading-h5 .b3-typography h5:before {
    content: counter(h5-count) "\00A0";
}
body.top-heading-h5 .protyle-wysiwyg [data-node-id].h6 div:first-child:before,
body.top-heading-h5 .b3-typography h6:before {
    content: counter(h5-count) "." counter(h6-count) "\00A0";
}

/* 适配:当 H6 是顶级标题时 */
body.top-heading-h6 .protyle-wysiwyg [data-node-id].h6 div:first-child:before,
body.top-heading-h6 .b3-typography h6:before {
    content: counter(h6-count) "\00A0";
}

/* ================= 标题视觉标识模块 ================= */
/* 标题颜色分级系统 */
[data-type="NodeHeading"].h1 {
    color: #d40045 !important;
}

[data-type="NodeHeading"].h2 {
    color: #ff7f00 !important;
}

[data-type="NodeHeading"].h3 {
    color: #66b82b !important;
}

[data-type="NodeHeading"].h4 {
    color: #093f86 !important;
}

[data-type="NodeHeading"].h5 {
    color: #340c81 !important;
}


/* ================= 折叠状态适配模块 ================= */
.protyle-wysiwyg [data-fold="1"] [data-node-id][class^="h"] {
    position: relative;
    padding-right: 4em !important;
    /* 为折叠标记留出空间 */
}
//version: v1.1
//plugin name: 标题和块的优化
//author: 少侠,微信:572378589
//update time: 更新时间7/10
//增加了对于标题序号的支持


(() => {
    // ===================================================================
    // ===================================================================
    //
    //              1. 全局配置中心 (G_CONFIG) - 用户配置区
    //
    // ===================================================================
    // ===================================================================

    const G_CONFIG = {
        /**
         * 功能总开关
         */
        featureToggles: {
            headingEnhancer: true, // 标题折叠创建
            listIndent: true, // 块缩进合并
            listConvert: true, // 列表转段落
            listIndexCorrector: true, // 智能序号更正
            listFoldedStyle: true, // 有序列表折叠样式优化
            taskStats: true, // 任务列表统计显示
            batchFold: true, // 批量折叠展开
            highlightEditingBlock: true, // 高亮编辑块
            headingindex:true, // 标题自动更新序列
        },

        /**
         * 功能调试日志开关
         */
        debugToggles: {
            headingEnhancer: false,
            listIndent: false,
            listConvert: false,
            listIndexCorrector: false,
            listFoldedStyle: false,
            taskStats: false,
            batchFold: false,
            headingindex:false,

        },

        /**
         * CSS 选择器配置
         */
        CSS: {
            focusedTab: '.layout-tab-bar .item--focus',
            protyleWysiwyg: '.protyle-wysiwyg',
        },

        /**
         * API 路径配置
         */
        API: {
            insertBlock: '/api/block/insertBlock',
            moveBlock: '/api/block/moveBlock',
            updateBlock: '/api/block/updateBlock',
            getBlockKramdown: '/api/block/getBlockKramdown',
        },

        /**
         * 功能特定配置
         */
        FEATURES: {
            listIndexCorrector: {
                checkInterval: 3000, // 定时检查间隔3秒
            },
            listFoldedStyle: {
                backgroundColor: 'rgb(1 227 177 / 30%)', // 折叠列表背景色
            },
            taskStats: {
                updateDelay: 1000, // 更新延迟1秒
                statsFormat: '{completed}/{total}个任务', // 统计显示格式
            },
            batchFold: {
                hideGutterAfterClick: true,
                foldDelay: 100,
            },

        }
    };

    // ===================================================================
    // ===================================================================
    //
    //                     2. 核心工具 (通常无需修改)
    //
    // ===================================================================
    // ===================================================================

    const Logger = {
        log(moduleId, ...args) {
            if (G_CONFIG.debugToggles[moduleId]) {
                console.log(`[增强插件][${moduleId}]`, ...args);
            }
        },
        error(moduleId, ...args) {
            console.error(`[增强插件][${moduleId}]`, ...args);
        }
    };

    function waitForElement(selector, timeout = 2000) {
        return new Promise((resolve, reject) => {
            const startTime = Date.now();
            const check = () => {
                const element = document.querySelector(selector);
                if (element) { resolve(element); }
                else if (Date.now() - startTime > timeout) { reject(new Error(`Element "${selector}" not found.`)); }
                else { requestAnimationFrame(check); }
            };
            check();
        });
    }

    // ===================================================================
    // ===================================================================
    //
    //                     3. 功能模块定义区
    //
    // ===================================================================
    // ===================================================================

  

    const HeadingindexModule = {
        id: 'headingindex',
        name: '标题自动更新序列',
        get enabled() { return G_CONFIG.featureToggles[this.id]; },
        log(...args) { Logger.log(this.id, ...args); },
        error(...args) { Logger.error(this.id, ...args); },
  
        init() {
            this.log('模块已启动,开始监控标题层级变化。');
  
            // 初始加载时运行
            if (document.readyState === "loading") {
                document.addEventListener('DOMContentLoaded', () => this.updateHeadingClass());
            } else {
                this.updateHeadingClass();
            }
  
            // 使用 MutationObserver 监听文档变化
            const observer = new MutationObserver((mutations) => {
                for(let mutation of mutations) {
                    // 只关心节点增删
                    if (mutation.type === 'childList') {
                        this.updateHeadingClass();
                        return; // 找到变化后即可退出
                    }
                }
            });
  
            // 观察整个文档的变化
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
  
            // 监听编辑器焦点变化
            document.addEventListener('focusin', (e) => {
                if (this.isInEditor(e.target)) {
                    this.updateHeadingClass();
                }
            }, true);
        },
  
        /**
         * 检查元素是否在编辑器中
         */
        isInEditor(element) {
            const focusedTab = document.querySelector(G_CONFIG.CSS.focusedTab);
            if (!focusedTab) return false;
  
            const activeEditor = document.querySelector(`.protyle[data-id="${focusedTab.dataset.id}"] ${G_CONFIG.CSS.protyleWysiwyg}`);
            return activeEditor && activeEditor.contains(element);
        },
  
        updateHeadingClass() {
            // 获取当前激活的编辑器
            const focusedTab = document.querySelector(G_CONFIG.CSS.focusedTab);
            if (!focusedTab) {
                this.log('未找到激活的标签页');
                return;
            }
  
            const protyleContent = document.querySelector(`.protyle[data-id="${focusedTab.dataset.id}"] ${G_CONFIG.CSS.protyleWysiwyg}`);
            if (!protyleContent) {
                this.log('未找到激活的编辑器内容区');
                return;
            }
  
            // 直接获取所有标题的 data-subtype 属性
            const allHeadings = protyleContent.querySelectorAll('[data-type="NodeHeading"]');
            let topLevel = 6;
  
            allHeadings.forEach(heading => {
                const subtype = heading.getAttribute('data-subtype'); // 获取 h1~h6
                if (subtype) {
                    const level = parseInt(subtype.substring(1)); // 从 "h1" 提取数字 1
                    if (level > 0 && level < topLevel) {
                        topLevel = level;
                    }
                }
            });
  
            // 移除旧的class
            document.body.className = document.body.className.replace(/ top-heading-h\d/g, '');
  
            if (topLevel !== 6) {
                document.body.classList.add(`top-heading-h${topLevel}`);
                this.log(`当前文档最高标题层级为 h${topLevel}, 共发现 ${allHeadings.length} 个标题`);
            } else {
                this.log('当前文档未找到任何标题');
            }
        }
    };

    const HeadingModule = {
        id: 'headingEnhancer',
        name: '标题折叠创建',
        get enabled() { return G_CONFIG.featureToggles[this.id]; },
        log(...args) { Logger.log(this.id, ...args); },
        error(...args) { Logger.error(this.id, ...args); },

        init() {
            this.log('模块已启动,开始监听全局键盘事件。');
            document.addEventListener('keydown', (e) => {
                if (e.key === 'Enter' && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) {
                    this.handleEnterKey(e);
                }
            }, true);
        },

        async handleEnterKey(e) {
            if (this.isProcessing) {
                this.log('[Ignored] Event ignored, another is processing.');
                e.preventDefault(); e.stopPropagation(); return;
            }
            this.isProcessing = true;
            setTimeout(() => { this.isProcessing = false; }, 200);

            const focusedTab = document.querySelector(G_CONFIG.CSS.focusedTab);
            if (!focusedTab) { this.isProcessing = false; return; }
            const activeEditor = document.querySelector(`.protyle[data-id="${focusedTab.dataset.id}"] ${G_CONFIG.CSS.protyleWysiwyg}`);
            if (!activeEditor || !activeEditor.contains(e.target)) { this.isProcessing = false; return; }

            const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) { this.isProcessing = false; return; }
            const range = selection.getRangeAt(0);
            const blockEl = (range.startContainer.nodeType === 3 ? range.startContainer.parentElement : range.startContainer).closest('[data-node-id]');
            if (!blockEl || blockEl.dataset.type !== 'NodeHeading' || blockEl.getAttribute('fold') !== '1') { this.isProcessing = false; return; }



            e.preventDefault(); e.stopPropagation();

            const blockId = blockEl.dataset.nodeId;
            const subtype = blockEl.dataset.subtype;
            const prefix = "#".repeat(parseInt(subtype.substring(1))) + " ";
            const newMarkdown = prefix + '\u200B';

            try {
                const insertResponse = await fetch(G_CONFIG.API.insertBlock, { method: 'POST', body: JSON.stringify({ dataType: "markdown", data: newMarkdown, previousID: blockId }) });
                if (!insertResponse.ok) throw new Error(`insertBlock API call failed`);
                const resultData = await insertResponse.json();
                const newBlockId = resultData?.data?.[0]?.doOperations?.[0]?.id || resultData?.data?.[0]?.do?.[0]?.id;
                if (newBlockId) {
                    const newBlockElement = await waitForElement(`[data-node-id="${newBlockId}"]`);
                    const editableDiv = newBlockElement.querySelector('[contenteditable="true"]');
                    if (!editableDiv) return;
  
                    editableDiv.focus();
                    const textNode = Array.from(editableDiv.childNodes).find(node => node.nodeType === Node.TEXT_NODE);
                    if (!textNode) return;
                    const domRange = document.createRange();
                    domRange.setStart(textNode, textNode.length);
                    domRange.collapse(true);
                    const sel = window.getSelection();
                    sel.removeAllRanges();
                    sel.addRange(domRange);
                }
            } catch (apiError) {
                this.error(`🔴 FAILED during API call:`, apiError);
            }
        }
    };

    const ListIndentModule = {
        id: 'listIndent',
        name: '块缩进合并',
        get enabled() {return G_CONFIG.featureToggles[this.id]; }, // 可后续接入 G_CONFIG.featureToggles
        log(...args) { Logger.log(this.id, ...args); },
        error(...args) { Logger.error(this.id, ...args); },

        init() {
            this.log('模块已启动,监听 Tab 缩进。');
            document.addEventListener('keydown', (e) => {
                if (e.key !== 'Tab' || e.shiftKey || e.ctrlKey || e.altKey || e.metaKey) return;
                this.handleTabKey(e);
            }, true);
        },

        handleTabKey(e) {
            const selection = window.getSelection();
            if (!selection || selection.rangeCount === 0) return;
            const range = selection.getRangeAt(0);
            let blockEl = (range.startContainer.nodeType === 3 ? range.startContainer.parentElement : range.startContainer).closest('[data-node-id]');
            if (!blockEl || blockEl.dataset.type !== 'NodeParagraph') return;

            // 获取前一个同级块
            const parent = blockEl.parentElement;
            if (!parent) return;
            const children = Array.from(parent.children);
            const idx = children.indexOf(blockEl);
            if (idx <= 0) return;
            const prev = children[idx - 1];

            // 前一个块为 NodeList
            if (prev.dataset.type === 'NodeList') {
                // 找到最后一个 NodeListItem
                const listItems = prev.querySelectorAll('[data-type="NodeListItem"]');
                if (!listItems.length) return;
                const lastListItem = listItems[listItems.length - 1];
                this.mergeParagraphToList(blockEl, lastListItem, e);
            }
        },

        /**
         * @param {HTMLElement} paragraphDiv 需要缩进的段落块
         * @param {HTMLElement} prevListItem 目标 NodeListItem
         * @param {KeyboardEvent} e 事件对象
         */
        async mergeParagraphToList(paragraphDiv, prevListItem, e) {
            e.preventDefault();
            e.stopPropagation();
            this.log('模拟原生拖拽事件', paragraphDiv, '到', prevListItem);
            try {
                // 1. 先做DOM操作
                prevListItem.appendChild(paragraphDiv);
                // 主动设置焦点和selection到新位置,提升交互体验
                const editable = paragraphDiv.querySelector('[contenteditable="true"]');
                if (editable) {
                    editable.focus();
                    const textNode = Array.from(editable.childNodes).find(node => node.nodeType === Node.TEXT_NODE);
                    if (textNode) {
                        const domRange = document.createRange();
                        domRange.setStart(textNode, textNode.length);
                        domRange.collapse(true);
                        const sel = window.getSelection();
                        sel.removeAllRanges();
                        sel.addRange(domRange);
                    }
                    this.log('已设置焦点到缩进块', editable, 'selection:', window.getSelection());
                } else {
                    this.log('未找到contenteditable区域');
                }
  
                // 2. 调用 moveBlock API 同步块结构
                const paraId = paragraphDiv.dataset.nodeId;
                const targetParentId = prevListItem.dataset.nodeId;
                if (!paraId || !targetParentId) {
                    this.error('无法获取块ID', paraId, targetParentId);
                    return;
                }
                const children = Array.from(prevListItem.children).filter(
                    el => el.dataset && el.dataset.nodeId && el !== paragraphDiv
                );
                const lastChild = children.length > 0 ? children[children.length - 1] : null;
                const token = localStorage.getItem('authToken') || '';
                const body = { id: paraId, parentID: targetParentId };
                if (lastChild) {
                    body.previousID = lastChild.dataset.nodeId;
                }
  
                // 【已优化】使用 G_CONFIG.API.moveBlock
                const moveRes = await fetch(G_CONFIG.API.moveBlock, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': token ? `Token ${token}` : undefined
                    },
                    body: JSON.stringify(body)
                });
                const moveData = await moveRes.json();
                if (moveData.code === 0) {
                    this.log('moveBlock成功', paraId, '->', targetParentId);
                } else {
                    this.error('API移动块失败', moveData.msg);
                }
            } catch (err) {
                this.error('模拟拖拽失败', err);
            }
        },

         /**
          * 保存指定 NodeListItem 及其子块到后端
          * @param {HTMLElement} nodeListItem 目标 NodeListItem
          */
         async saveNodeListItemToBackend(nodeListItem) {
            const nodeListItemId = nodeListItem.dataset.nodeId;
            if (!nodeListItemId) {
                this.error('保存失败:未获取到 NodeListItem 的 data-node-id');
                return;
            }
            const domStr = nodeListItem.outerHTML;
            console.log('domStr', domStr);
            const token = localStorage.getItem('authToken') || '';
            try {
                // 【已优化】使用 G_CONFIG.API.updateBlock
                const res = await fetch(G_CONFIG.API.updateBlock, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': token ? `Token ${token}` : undefined
                    },
                    body: JSON.stringify({
                        id: nodeListItemId,
                        dataType: 'dom',
                        data: domStr
                    })
                });
                const data = await res.json();
                if (data.code === 0) {
                    this.log('保存成功', nodeListItemId);
                    this.log('data', data);
                } else {
                    this.error('保存失败', data.msg);
                }
            } catch (err) {
                this.error('保存失败', err);
            }
        }
    };

    const ListConvertModule = {
        id: 'listConvert',
        name: '列表转段落',
        get enabled() { return G_CONFIG.featureToggles[this.id]; },
        log(...args) { Logger.log(this.id, ...args); },
        error(...args) { Logger.error(this.id, ...args); },

        init() {
            this.log('模块已启动,监听列表转段落。');
            document.addEventListener('keydown', (e) => {
                if (e.key !== 'Tab' || e.shiftKey || e.ctrlKey || e.altKey || e.metaKey) return;
                this.handleTabKey(e);
            }, true);
        },

        handleTabKey(e) {
            this.log('Tab键被按下,开始检查列表项转换条件');
  
            const selection = window.getSelection();
            if (!selection || selection.rangeCount === 0) {
                this.log('未找到有效的selection,退出处理');
                return;
            }
  
            const range = selection.getRangeAt(0);
            let blockEl = (range.startContainer.nodeType === 3 ? range.startContainer.parentElement : range.startContainer).closest('[data-node-id]');
  
            if (!blockEl) {
                this.log('未找到包含data-node-id的块元素,退出处理');
                return;
            }
  
            this.log('当前块元素类型:', blockEl.dataset.type, '节点ID:', blockEl.dataset.nodeId);
  
            // 查找包含当前块的列表项
            let listItem = null;
            if (blockEl.dataset.type === 'NodeListItem') {
                // 当前块就是列表项
                listItem = blockEl;
                this.log('当前块就是列表项');
            } else {
                // 向上查找包含的列表项
                listItem = blockEl.closest('[data-type="NodeListItem"]');
                if (listItem) {
                    this.log('找到包含的列表项,节点ID:', listItem.dataset.nodeId);
                } else {
                    this.log('未找到包含的列表项,跳过处理');
                    return;
                }
            }
  
            // 检查找到的列表项
            this.log('开始检查列表项转换条件');
            this.checkAndConvertListItem(listItem, e);
        },

        checkAndConvertListItem(listItem, e) {
            this.log('开始检查列表项转换条件,列表项ID:', listItem.dataset.nodeId);
  
            // 检查列表项是否为空
            const paragraphEl = listItem.querySelector('[data-type="NodeParagraph"]');
            if (!paragraphEl) {
                this.log('未找到段落元素,退出转换');
                return;
            }
  
            const editableDiv = paragraphEl.querySelector('[contenteditable="true"]');
            if (!editableDiv) {
                this.log('未找到可编辑区域,退出转换');
                return;
            }
  
            // 检查内容是否为空(去除空白字符)
            const content = editableDiv.textContent || '';
            const trimmedContent = content.trim();
            this.log('段落内容检查:', '原始内容长度:', content.length, '去除空白后长度:', trimmedContent.length);
  
            if (trimmedContent !== '') {
                this.log('段落内容不为空,退出转换');
                return;
            }
  
            // 检查是否有子列表
            const childList = listItem.querySelector('[data-type="NodeList"]');
            if (childList) {
                this.log('检测到子列表,应该执行缩进操作而不是转换段落,退出转换');
                return;
            }
  
            // 检查是否可以缩进到前一个同级列表项
            const canIndent = this.canIndentToPreviousListItem(listItem);
            if (canIndent) {
                this.log('可以缩进到前一个同级列表项,应该执行缩进操作,退出转换');
                return;
            }
  
            this.log('所有转换条件满足:空列表项且无子列表且无法缩进,开始执行转换');
            // 满足条件:空列表项且无子列表且无法缩进,执行转换
            this.convertListItemToParagraph(listItem, e);
        },

        canIndentToPreviousListItem(listItem) {
            this.log('检查是否可以缩进到前一个同级列表项');
  
            // 获取当前列表项的父级列表
            const parentList = listItem.parentElement;
            if (!parentList || parentList.dataset.type !== 'NodeList') {
                this.log('当前列表项不在NodeList中,无法缩进');
                return false;
            }
  
            // 获取同级列表项
            const siblingListItems = Array.from(parentList.children).filter(child => child.dataset.type === 'NodeListItem');
            const currentIndex = siblingListItems.indexOf(listItem);
  
            this.log('同级列表项数量:', siblingListItems.length, '当前索引:', currentIndex);
  
            if (currentIndex <= 0) {
                this.log('当前列表项是第一个或不在列表中,无法缩进');
                return false;
            }
  
            // 检查前一个同级列表项
            const previousListItem = siblingListItems[currentIndex - 1];
            this.log('前一个同级列表项ID:', previousListItem.dataset.nodeId);
  
            // 前一个列表项存在,可以缩进
            this.log('找到前一个同级列表项,可以缩进');
            return true;
        },

        async convertListItemToParagraph(listItem, e) {
            e.preventDefault();
            e.stopPropagation();
  
            this.log('开始转换列表项为段落,列表项ID:', listItem.dataset.nodeId);
  
            try {
                // 1. 创建新的段落DOM
                this.log('步骤1: 创建新的段落DOM结构');
                const paragraphDiv = document.createElement('div');
                paragraphDiv.dataset.nodeId = listItem.dataset.nodeId;
                paragraphDiv.dataset.nodeIndex = listItem.dataset.nodeIndex || '1';
                paragraphDiv.dataset.type = 'NodeParagraph';
                paragraphDiv.className = 'p';
                paragraphDiv.setAttribute('updated', Date.now().toString());
  
                this.log('新段落DOM属性设置完成:', {
                    nodeId: paragraphDiv.dataset.nodeId,
                    nodeIndex: paragraphDiv.dataset.nodeIndex,
                    type: paragraphDiv.dataset.type
                });
  
                // 复制原有的段落内容
                const originalParagraph = listItem.querySelector('[data-type="NodeParagraph"]');
                if (originalParagraph) {
                    this.log('复制原有段落内容');
                    paragraphDiv.innerHTML = originalParagraph.innerHTML;
                } else {
                    this.log('创建空的段落内容结构');
                    // 创建空的段落内容
                    paragraphDiv.innerHTML = `
                        <div contenteditable="true" spellcheck="false"></div>
                        <div class="protyle-attr" contenteditable="false"></div>
                    `;
                }
  
                // 2. 替换DOM
                this.log('步骤2: 执行DOM替换操作');
                const parent = listItem.parentElement;
                if (!parent) {
                    this.error('无法找到父元素,DOM替换失败');
                    return;
                }
  
                this.log('DOM替换前 - 父元素:', parent.tagName, '子元素数量:', parent.children.length);
                parent.replaceChild(paragraphDiv, listItem);
                this.log('DOM替换完成,新段落已插入到父元素中');
  
                // 3. 设置焦点
                this.log('步骤3: 设置焦点到新段落');
                const editable = paragraphDiv.querySelector('[contenteditable="true"]');
                if (editable) {
                    editable.focus();
                    this.log('焦点已设置到可编辑区域');
  
                    const textNode = Array.from(editable.childNodes).find(node => node.nodeType === Node.TEXT_NODE);
                    if (textNode) {
                        const domRange = document.createRange();
                        domRange.setStart(textNode, textNode.length);
                        domRange.collapse(true);
                        const sel = window.getSelection();
                        sel.removeAllRanges();
                        sel.addRange(domRange);
                        this.log('光标位置已设置到文本末尾');
                    } else {
                        this.log('未找到文本节点,光标位置设置跳过');
                    }
                } else {
                    this.log('未找到可编辑区域,焦点设置失败');
                }
  
                // 4. 调用API同步到后端
                this.log('步骤4: 开始同步到后端');
                await this.syncToBackend(paragraphDiv, listItem.dataset.nodeId);
  
                this.log('列表项转段落转换完成,所有步骤执行成功');
  
            } catch (err) {
                this.error('转换过程中发生错误:', err);
            }
        },

        async syncToBackend(paragraphDiv, originalNodeId) {
            this.log('开始同步到后端,节点ID:', originalNodeId);
  
            try {
                const token = localStorage.getItem('authToken') || '';
                this.log('获取认证token:', token ? '已获取' : '未获取');
  
                const domStr = paragraphDiv.outerHTML;
                this.log('生成DOM字符串,长度:', domStr.length);
  
                const requestBody = {
                    id: originalNodeId,
                    dataType: 'dom',
                    data: domStr
                };
                this.log('准备发送API请求,请求体:', requestBody);
  
                const res = await fetch(G_CONFIG.API.updateBlock, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': token ? `Token ${token}` : undefined
                    },
                    body: JSON.stringify(requestBody)
                });
  
                this.log('API请求已发送,响应状态:', res.status);
  
                const data = await res.json();
                this.log('API响应数据:', data);
  
                if (data.code === 0) {
                    this.log('后端同步成功,节点已更新');
                } else {
                    this.error('后端同步失败,错误信息:', data.msg);
                }
            } catch (err) {
                this.error('后端同步过程中发生异常:', err);
            }
        }
    };

    const ListIndexCorrectorModule = {
        id: 'listIndexCorrector',
        name: '智能序号更正',
        get enabled() { return G_CONFIG.featureToggles[this.id]; },
        log(...args) { Logger.log(this.id, ...args); },
        error(...args) { Logger.error(this.id, ...args); },

        isProcessing: false,
        checkInterval: null,

        init() {
            this.log('模块已启动,开始监控列表索引。');
  
            // 启动定时检查
            this.startPeriodicCheck();
  
            // 监听编辑器变化
            this.setupEventListeners();
        },

        /**
         * 启动定时检查
         */
        startPeriodicCheck() {
            const interval = G_CONFIG.FEATURES.listIndexCorrector.checkInterval || 3000;
            this.checkInterval = setInterval(() => {
                if (!this.isProcessing) {
                    this.updateListIndexes();
                }
            }, interval);
            this.log(`定时检查已启动,间隔: ${interval}ms`);
        },

        /**
         * 设置事件监听器
         */
        setupEventListeners() {
            // 监听blur事件
            document.addEventListener('blur', (e) => {
                if (this.isInEditor(e.target)) {
                    setTimeout(() => {
                        this.updateListIndexes();
                    }, 100);
                }
            }, true);

            // 监听键盘事件
            document.addEventListener('keyup', (e) => {
                if (this.isInEditor(e.target)) {
                    const listItem = e.target.closest('[data-type="NodeListItem"]');
                    if (listItem) {
                        setTimeout(() => {
                            this.updateListIndexes();
                        }, 200);
                    }
                }
            }, true);

            // 监听粘贴事件
            document.addEventListener('paste', (e) => {
                if (this.isInEditor(e.target)) {
                    const listItem = e.target.closest('[data-type="NodeListItem"]');
                    if (listItem) {
                        setTimeout(() => {
                            this.updateListIndexes();
                        }, 100);
                    }
                }
            }, true);
        },

        /**
         * 检查元素是否在编辑器中
         */
        isInEditor(element) {
            const focusedTab = document.querySelector(G_CONFIG.CSS.focusedTab);
            if (!focusedTab) return false;
  
            const activeEditor = document.querySelector(`.protyle[data-id="${focusedTab.dataset.id}"] ${G_CONFIG.CSS.protyleWysiwyg}`);
            return activeEditor && activeEditor.contains(element);
        },

        /**
         * 检查用户是否正在编辑
         */
        isUserEditing() {
            const focusedElement = document.activeElement;
            if (!focusedElement) return false;
  
            // 检查是否有焦点在可编辑元素上
            return focusedElement.getAttribute('contenteditable') === 'true' || 
                   focusedElement.closest('[contenteditable="true"]');
        },

        /**
         * 主逻辑函数 - 更新列表索引
         */
        async updateListIndexes() {
            if (this.isProcessing) return;
  
            this.isProcessing = true;
            this.log('--- 开始执行检查 ---');

            try {
                const focusedTab = document.querySelector(G_CONFIG.CSS.focusedTab);
                if (!focusedTab) {
                    this.log('1. 未找到激活的Tab (item--focus),退出。');
                    return;
                }
                const tabId = focusedTab.dataset.id;
                this.log('1. 找到激活的Tab,ID: ' + tabId);

                const activeEditorContainer = document.querySelector(`.protyle[data-id="${tabId}"]`);
                if (!activeEditorContainer) {
                    this.log('2. 未找到与Tab ID匹配的编辑器容器');
                    return;
                }
                const activeEditor = activeEditorContainer.querySelector(G_CONFIG.CSS.protyleWysiwyg);
                if (!activeEditor) {
                    this.log('2. 在编辑器容器内未找到编辑器');
                    return;
                }
  
                const topLevelBlocks = activeEditor.querySelectorAll(':scope > [data-node-id]');
                if (topLevelBlocks.length === 0) {
                    this.log('3. 当前编辑器内没有发现顶级块。');
                    return;
                }
                this.log('3. 获取到 ' + topLevelBlocks.length + ' 个顶级块。');

                const listGroups = this.groupAdjacentOrderedLists(topLevelBlocks);
                this.log('4. 发现 ' + listGroups.length + ' 组需要处理的相邻有序列表。');
  
                for (let i = 0; i < listGroups.length; i++) {
                    this.log('5. 开始处理第 ' + (i + 1) + ' 组...');
                    await this.processListGroup(listGroups[i], activeEditor);
                }
  
                this.log('所有列表组处理完成');
            } catch (err) {
                this.error('更新列表索引失败', err);
            } finally {
                this.isProcessing = false;
            }
        },

        /**
         * 将相邻的有序列表分组
         */
        groupAdjacentOrderedLists(blocks) {
            const groups = [];
            let currentGroup = [];
  
            blocks.forEach(block => {
                const isOrderedList = block.dataset.type === 'NodeList' && block.dataset.subtype === 'o';
                const contentDiv = block.querySelector(':scope > [contenteditable="true"]');
                const isEmptyParagraph = block.dataset.type === 'NodeParagraph' && contentDiv && contentDiv.innerHTML.trim() === '';
  
                if (isOrderedList) {
                    currentGroup.push(block);
                } else if (!isEmptyParagraph) {
                    if (currentGroup.length > 0) {
                        groups.push(currentGroup);
                    }
                    currentGroup = [];
                }
            });
  
            if (currentGroup.length > 0) {
                groups.push(currentGroup);
            }
  
            return groups;
        },

        /**
         * 递归收集需要变更的项目
         */
        collectChangesRecursively(listElement, startNumber, changes, activeEditor) {
            let counter = startNumber;
            const listItems = listElement.querySelectorAll(':scope > .li[data-type="NodeListItem"]');

            for (const item of listItems) {
                const correctMarker = counter + '.';
                const currentMarker = item.dataset.marker;
                const itemId = item.dataset.nodeId;

                if (itemId && currentMarker !== correctMarker) {
                    changes.push({ id: itemId, newMarker: correctMarker });
                }

                // 遍历列表项的所有直接子块
                const childBlocks = item.querySelectorAll(':scope > [data-node-id]');
                let subCounter = 1; // 为该项内部的子列表们维护一个独立的计数器
  
                for (const childBlock of childBlocks) {
                    // 如果子块是一个有序列表,就用 subCounter 继续编号并递归
                    if (childBlock.matches('div[data-type="NodeList"][data-subtype="o"]')) {
                        subCounter = this.collectChangesRecursively(childBlock, subCounter, changes, activeEditor);
                    } else {
                        // 如果遇到任何非列表块(如段落),就将子列表的计数器重置
                        subCounter = 1;
                    }
                }

                counter++; // 为当前层级的下一个项目增加计数器
            }
            return counter; // 返回下一个可用的序号
        },

        /**
         * 处理列表组
         */
        async processListGroup(group, activeEditor) {
            const changes = [];
            let globalCounter = 1;

            this.log('5a. 开始递归收集需要变更的项目...');
            for (const listElement of group) {
                globalCounter = this.collectChangesRecursively(listElement, globalCounter, changes, activeEditor);
            }
            this.log('5b. 收集完成,总计发现 ' + changes.length + ' 个需要变更的项目。');

            await this.executeUpdates(changes, activeEditor);
        },

        /**
         * 执行更新
         */
        async executeUpdates(itemsToUpdate, activeEditor) {
            if (itemsToUpdate.length === 0) {
                this.log('6. 所有索引均正确,无需执行更新。');
                return;
            }

            this.log('6. 待更新列表已生成,总计 ' + itemsToUpdate.length + ' 项。开始执行API调用...');

            const updatePromises = itemsToUpdate.map(itemInfo => {
                return new Promise(async (resolve) => {
                    const focusedElement = activeEditor.querySelector('.block-focus');
                    const itemElement = activeEditor.querySelector(`[data-node-id="${itemInfo.id}"]`);

                    if (itemElement && focusedElement && itemElement.contains(focusedElement)) {
                        this.log('   - ID ' + itemInfo.id + ' 正在编辑中,已跳过本次更新。');
                        return resolve();
                    }
  
                    try {
                        this.log('   - 正在处理 ID: ' + itemInfo.id + '...');
                        const getContentResponse = await fetch(G_CONFIG.API.getBlockKramdown, { 
                            method: 'POST', 
                            body: JSON.stringify({ id: itemInfo.id }) 
                        });
                        if (!getContentResponse.ok) throw new Error('获取块内容失败');
  
                        const blockData = await getContentResponse.json();
  
                        if (blockData && blockData.data && typeof blockData.data.kramdown === 'string') {
                            const originalMarkdown = blockData.data.kramdown;
                            this.log('     原始MD: "' + originalMarkdown.replace(/\n/g, "\\n") + '"');
                            const newMarkdown = originalMarkdown.replace(/^\s*\d+\.\s/, itemInfo.newMarker + ' ');
                            this.log('     新的MD: "' + newMarkdown.replace(/\n/g, "\\n") + '"');
  
                            const payload = {
                                "dataType": "markdown", 
                                "data": newMarkdown, 
                                "id": itemInfo.id
                            };
                            await fetch(G_CONFIG.API.updateBlock, { 
                                method: 'POST', 
                                body: JSON.stringify(payload) 
                            });
                            this.log('     ID ' + itemInfo.id + ' 更新成功。');
                            resolve();
                        } else {
                            throw new Error('API返回数据无效: ' + JSON.stringify(blockData));
                        }
                    } catch (error) {
                        this.error('   - 处理ID为 ' + itemInfo.id + ' 的块时出错:', error);
                        resolve(); 
                    }
                });
            });
  
            await Promise.all(updatePromises);
            this.log('7. 所有项处理完毕。');
        },

        /**
         * 手动触发序号纠正(用于测试)
         */
        manualCorrect() {
            this.log('手动触发序号纠正');
            this.updateListIndexes();
        }
    };

    const ListFoldedStyleModule = {
        id: 'listFoldedStyle',
        name: '有序列表折叠样式优化',
        get enabled() { return G_CONFIG.featureToggles[this.id]; },
        log(...args) { Logger.log(this.id, ...args); },
        error(...args) { Logger.error(this.id, ...args); },

        init() {
            this.log('模块已启动,注入有序列表折叠样式。');
            this.injectFoldedListStyle();
        },

        /**
         * 注入折叠列表样式
         */
        injectFoldedListStyle() {
            try {
                const styleId = 'siyuan-list-folded-style';
  
                // 检查是否已存在样式表
                if (document.getElementById(styleId)) {
                    this.log('样式表已存在,跳过注入');
                    return;
                }

                const style = document.createElement('style');
                style.id = styleId;
                style.textContent = `
                    .protyle-wysiwyg [data-node-id].li[fold="1"]>.protyle-action::after {
                        background-color: ${G_CONFIG.FEATURES.listFoldedStyle.backgroundColor} !important;
                        opacity: ${G_CONFIG.FEATURES.listFoldedStyle.opacity} !important;
                    }
                `;

                document.head.appendChild(style);
                this.log('有序列表折叠样式注入成功');
            } catch (error) {
                this.error('注入折叠列表样式失败:', error);
            }
        },
    };

    const TaskStatsModule = {
        id: 'taskStats',
        name: '任务列表统计显示',
        get enabled() { return G_CONFIG.featureToggles[this.id]; },
        log(...args) { Logger.log(this.id, ...args); },
        error(...args) { Logger.error(this.id, ...args); },

        // 状态管理
        isProcessing: false,
        updateTimer: null,

        init() {
            this.log('模块已启动,监听任务状态变化。');
            this.setupEventListeners();
            this.setupCopyProtection();
            // 立即执行一次初始统计
            this.scheduleUpdate();
        },

        /**
         * 设置事件监听器
         */
        setupEventListeners() {
            // 监听任务状态变化
            document.addEventListener('click', (e) => {
                if (e.target.closest('.protyle-action--task')) {
                    this.scheduleUpdate();
                }
            }, true);

            // 监听列表结构变化
            const observer = new MutationObserver((mutations) => {
                let shouldUpdate = false;
                mutations.forEach((mutation) => {
                    if (mutation.type === 'childList') {
                        mutation.addedNodes.forEach((node) => {
                            if (node.nodeType === Node.ELEMENT_NODE) {
                                if (node.querySelector('.protyle-action--task') || 
                                    node.classList?.contains('protyle-action--task')) {
                                    shouldUpdate = true;
                                }
                            }
                        });
                        mutation.removedNodes.forEach((node) => {
                            if (node.nodeType === Node.ELEMENT_NODE) {
                                if (node.querySelector('.protyle-action--task') || 
                                    node.classList?.contains('protyle-action--task')) {
                                    shouldUpdate = true;
                                }
                            }
                        });
                    }
                });
                if (shouldUpdate) {
                    this.scheduleUpdate();
                }
            });

            // 立即开始监听,不等待DOMContentLoaded
            this.startObserving(observer);
        },

        /**
         * 开始监听编辑器变化
         */
        startObserving(observer) {
            // 立即监听现有的编辑器
            const editors = document.querySelectorAll(G_CONFIG.CSS.protyleWysiwyg);
            editors.forEach(editor => {
                observer.observe(editor, {
                    childList: true,
                    subtree: true
                });
            });

            // 监听新编辑器的创建
            const editorObserver = new MutationObserver((mutations) => {
                mutations.forEach((mutation) => {
                    mutation.addedNodes.forEach((node) => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            const newEditors = node.querySelectorAll?.(G_CONFIG.CSS.protyleWysiwyg) || [];
                            if (node.matches?.(G_CONFIG.CSS.protyleWysiwyg)) {
                                newEditors.push(node);
                            }
                            newEditors.forEach(editor => {
                                observer.observe(editor, {
                                    childList: true,
                                    subtree: true
                                });
                            });
                        }
                    });
                });
            });

            editorObserver.observe(document.body, {
                childList: true,
                subtree: true
            });
        },

        /**
         * 设置复制保护
         */
        setupCopyProtection() {
            document.addEventListener('copy', (e) => {
                const selection = window.getSelection();
                if (!selection || selection.rangeCount === 0) return;

                const range = selection.getRangeAt(0);
                const fragment = range.cloneContents();
  
                // 移除统计信息元素
                const statsElements = fragment.querySelectorAll('[data-task-stats="true"]');
                statsElements.forEach(el => el.remove());

                // 创建新的剪贴板数据
                const newData = new DataTransfer();
                newData.setData('text/html', fragment.innerHTML);
                newData.setData('text/plain', fragment.textContent || '');
  
                e.clipboardData.setData('text/html', newData.getData('text/html'));
                e.clipboardData.setData('text/plain', newData.getData('text/plain'));
  
                e.preventDefault();
                this.log('复制保护已应用,统计信息已过滤');
            }, true);
        },

        /**
         * 安排更新任务
         */
        scheduleUpdate() {
            if (!this.enabled || this.isProcessing) return;

            // 清除之前的定时器
            if (this.updateTimer) {
                clearTimeout(this.updateTimer);
            }

            // 设置新的定时器
            this.updateTimer = setTimeout(() => {
                this.updateTaskStats();
            }, G_CONFIG.FEATURES.taskStats.updateDelay);
        },

        /**
         * 更新任务统计
         */
        async updateTaskStats() {
            this.isProcessing = true;
            this.log('开始更新任务统计');

            try {
                const activeEditor = this.getActiveEditor();
                if (!activeEditor) {
                    this.log('未找到激活的编辑器');
                    return;
                }

                // 清除现有的统计信息
                this.clearExistingStats(activeEditor);

                // 查找需要显示统计信息的列表(只在最外层包裹任务的NodeListItem所属NodeList显示)
                const topLevelTaskLists = this.findTopLevelTaskLists(activeEditor);
                this.log('找到最外层包含任务的列表数量:', topLevelTaskLists.length);

                // 为每个最外层列表添加统计信息
                topLevelTaskLists.forEach(listItem => {
                    this.addStatsToList(listItem);
                });

                this.log('任务统计更新完成');
            } catch (error) {
                this.error('更新任务统计失败:', error);
            } finally {
                this.isProcessing = false;
            }
        },

        /**
         * 获取当前激活的编辑器
         */
        getActiveEditor() {
            const focusedTab = document.querySelector(G_CONFIG.CSS.focusedTab);
            if (!focusedTab) return null;

            const activeEditor = document.querySelector(`.protyle[data-id="${focusedTab.dataset.id}"] ${G_CONFIG.CSS.protyleWysiwyg}`);
            return activeEditor;
        },

        /**
         * 清除现有的统计信息
         */
        clearExistingStats(editor) {
            const statsElements = editor.querySelectorAll('[data-task-stats="true"]');
            statsElements.forEach(el => el.remove());
            this.log('清除现有统计信息,数量:', statsElements.length);
        },

        /**
         * 查找需要显示统计信息的列表(只在最外层包裹任务的NodeListItem所属NodeList显示)
         */
        findTopLevelTaskLists(editor) {
            const result = [];
            const allListItems = editor.querySelectorAll('[data-type="NodeListItem"]');
            this.log('开始查找包含任务的列表项...');
  
            allListItems.forEach(item => {
                // 1. 该item下有任务
                if (item.querySelector('.protyle-action--task')) {
                    const itemNodeId = item.dataset.nodeId;
                    this.log(`找到包含任务的列表项: ${itemNodeId}`);
  
                    // 2. 向上查找祖先NodeListItem,祖先里不能有任务
                    let hasAncestorWithTask = false;
                    let parent = item.parentElement;
                    while (parent) {
                        if (parent.dataset && parent.dataset.type === 'NodeListItem') {
                            if (parent.querySelector('.protyle-action--task')) {
                                hasAncestorWithTask = true;
                                this.log(`  发现祖先列表项 ${parent.dataset.nodeId} 也包含任务,跳过当前项`);
                                break;
                            }
                        }
                        parent = parent.parentElement;
                    }
                    if (!hasAncestorWithTask) {
                        // 只在最外层item所属的NodeList显示
                        const parentList = item.closest('[data-type="NodeList"]');
                        if (parentList && !result.includes(parentList)) {
                            const parentListNodeId = parentList.dataset.nodeId;
                            this.log(`  确定在最外层列表项 ${itemNodeId} 所属的列表 ${parentListNodeId} 显示统计信息`);
                            result.push(item);
                        }
                    }
                }
            });
  
            this.log(`最终确定显示统计信息的列表数量: ${result.length}`);
            result.forEach((list, index) => {
                this.log(`  列表 ${index + 1}: ${list.dataset.nodeId}`);
            });
  
            return result;
        },

        /**
         * 为列表添加统计信息
         */
        addStatsToList(listItem) {
            const stats = this.calculateTaskStats(listItem);
            if (stats.total === 0) return;

            // 创建统计元素
            const statsElement = this.createStatsElement(stats);

            // 找到该列表项中的段落元素
            const paragraphElement = listItem.querySelector('[data-type="NodeParagraph"]');
            if (paragraphElement) {
                // 找到段落中的可编辑内容区域
                const editableDiv = paragraphElement.querySelector('[contenteditable="true"]');
                if (editableDiv) {
                    editableDiv.appendChild(statsElement);
                } else {
                    paragraphElement.appendChild(statsElement);
                }
            } else {
                // 没有段落就加到列表项末尾
                listItem.appendChild(statsElement);
            }
            this.log('为列表项添加统计信息:', stats);
        },

        /**
         * 计算任务统计
         */
        calculateTaskStats(listItem) {
            let total = 0;
            let completed = 0;

            // 查找所有任务复选框(包括子列表中的)
            const taskActions = listItem.querySelectorAll('.protyle-action--task');
  
            taskActions.forEach(action => {
                total++;
                // 检查是否已完成(通过图标判断)
                const icon = action.querySelector('svg use');
                if (icon && icon.getAttribute('xlink:href') === '#iconCheck') {
                    completed++;
                }
            });

            this.log('统计结果:', { total, completed, listId: listItem.dataset.nodeId });
            return { total, completed };
        },

        /**
         * 创建统计元素
         */
        createStatsElement(stats) {
            const statsElement = document.createElement('span');
            statsElement.dataset.taskStats = 'true';
            statsElement.className = 'task-stats';
            statsElement.style.cssText = `
                font-size: 0.9em;
                color: #666;
                margin-left: 8px;
                padding: 2px 6px;
                background-color: #f5f5f5;
                border-radius: 3px;
                display: inline;
                white-space: nowrap;
            `;

            const format = G_CONFIG.FEATURES.taskStats.statsFormat;
            const text = format
                .replace('{completed}', stats.completed)
                .replace('{total}', stats.total);
  
            statsElement.textContent = text;
  
            return statsElement;
        }
    };

    const HighlightEditingBlockModule = {
        id: 'highlightEditingBlock',
        name: '高亮编辑块',
        get enabled() { return true; }, // 如需开关可接入 G_CONFIG
        log(...args) { Logger.log(this.id, ...args); },
        error(...args) { Logger.error(this.id, ...args); },

        init() {
            this.log('模块已启动,监听 selectionchange 事件。');
            document.addEventListener('selectionchange', function() {
                const selection = window.getSelection();
                if (selection.rangeCount > 0) {
                    const range = selection.getRangeAt(0);
                    const node = range.startContainer;
                    let element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;

                    while (element && !element.classList.contains('protyle-wysiwyg')) {
                        element = element.parentElement;
                    }

                    if (element && element.classList.contains('protyle-wysiwyg')) {
                        const highlightedElements = element.querySelectorAll('.highlight');
                        highlightedElements.forEach(el => el.classList.remove('highlight'));

                        let targetElement = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
                        while (targetElement && (!element.contains(targetElement) || !targetElement.classList.contains('p'))) {
                            targetElement = targetElement.parentElement;
                        }

                        if (targetElement && targetElement.classList.contains('p')) {
                            targetElement.classList.add('highlight');
                        }
                    }
                }
            });
            // 注入高亮样式
            if (!document.getElementById('highlight-editing-block-style')) {
                const style = document.createElement('style');
                style.id = 'highlight-editing-block-style';
                style.textContent = `
                    .protyle-wysiwyg .sb:hover {
                        box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.15), -2px -2px 6px rgba(0, 0, 0, 0.15), 0 0 12px rgba(0, 0, 0, 0.1) !important;
                        transition: background-color 0.5s ease-out, box-shadow 0.5s ease-out !important;
                    }
                    .protyle-wysiwyg .p:hover {
                        color:red;
                        box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.15), -2px -2px 6px rgba(0, 0, 0, 0.15), 0 0 12px rgba(0, 0, 0, 0.1) !important;
                        transition: background-color 0.5s ease-out, box-shadow 0.5s ease-out !important;
                    .protyle-wysiwyg .p.highlight {
                        background: #ffe58f !important;
                        transition: background 0.2s;
                    }
                `;
                document.head.appendChild(style);
            }
        }
    };

    const BatchFoldModule = {
        id: 'batchFold',
        name: '批量折叠展开',
        get enabled() { return G_CONFIG.featureToggles[this.id]; },
        log(...args) { Logger.log(this.id, ...args); },
        error(...args) { Logger.error(this.id, ...args); },

        /**
         * 初始化模块
         */
        init() {
            this.log('模块已启动,监听批量折叠展开操作。');
            this.setupEventListeners();
        },

        /**
         * 设置事件监听器
         */
        setupEventListeners() {
            document.addEventListener('mousedown', (event) => {
                this.handleMouseDown(event);
            }, true);
        },

        /**
         * 处理鼠标按下事件
         * @param {MouseEvent} event 鼠标事件对象
         */
        handleMouseDown(event) {
            // 检查是否按住Alt键且是左键点击
            if (!event.altKey || event.button !== 0) return;

            // 检查是否点击了标题前的折叠箭头
            const headingArrow = event.target.closest('.protyle-gutters:has(button[data-type="NodeHeading"]) button[data-type="fold"]');
            if (headingArrow) {
                this.handleHeadingFold(event, headingArrow);
                return;
            }

            // 检查是否点击了块前的折叠箭头
            const blockArrow = event.target.closest('.protyle-gutters:has(button[data-type="NodeListItem"]) button[data-type="fold"]');
            if (blockArrow) {
                this.handleBlockFold(event, blockArrow);
                return;
            }
        },

        /**
         * 处理标题折叠操作
         * @param {MouseEvent} event 鼠标事件对象
         * @param {HTMLElement} arrow 箭头元素
         */
        handleHeadingFold(event, arrow) {
            const headingButton = arrow.parentElement.querySelector('button[data-type="NodeHeading"]');
            if (!headingButton) return;

            const headingId = headingButton.dataset?.nodeId;
            const heading = document.querySelector(`div[data-node-id="${headingId}"]`);
            if (!heading) return;

            const headingType = heading.dataset?.subtype;
            const isFolded = !!heading.getAttribute('fold');
            const isSelected = heading.classList.contains('protyle-wysiwyg--select');
            const selectedSelector = isSelected ? '.protyle-wysiwyg--select' : '';
            const protyle = this.getProtyleByMouseAt(event);

            if (!protyle) return;

            if (this.isCtrlKey(event) && event.altKey && !event.shiftKey) {
                // Ctrl/Meta + Alt + 点击:折叠/展开所有标题
                this.log('执行全局标题折叠/展开操作');
                const allHeadings = protyle.querySelectorAll(`.protyle-wysiwyg [data-type="NodeHeading"]${selectedSelector}`);
                this.foldHeadings(allHeadings, isFolded);
                this.hideGutterAfterClick(headingButton, arrow);
            } else if (event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
                // 仅 Alt + 点击:折叠/展开同级标题
                this.log('执行同级标题折叠/展开操作');
                const sameLevelHeadings = protyle.querySelectorAll(`.protyle-wysiwyg [data-type="NodeHeading"][data-subtype="${headingType}"]${selectedSelector}`);
                this.foldHeadings(sameLevelHeadings, isFolded);
                this.hideGutterAfterClick(headingButton, arrow);
            }
        },

        /**
         * 折叠/展开标题集合
         * @param {NodeList} headings 标题元素集合
         * @param {boolean} isFolded 当前标题是否折叠(用于判断操作方向)
         */
        async foldHeadings(headings, isFolded) {
            // 如果当前标题是折叠状态,我们要展开所有标题
            // 如果当前标题是展开状态,我们要折叠所有标题
            const targetFoldState = !isFolded;
            this.log(`开始${targetFoldState ? '折叠' : '展开'} ${headings.length} 个标题`);
  
            for (const heading of headings) {
                const headingId = heading.dataset?.nodeId;
                if (!headingId) continue;
  
                const isCurrentlyFolded = !!heading.getAttribute('fold');
                if (isCurrentlyFolded === targetFoldState) continue; // 已经是目标状态
  
                try {
                    await this.foldBlock(headingId, targetFoldState);
                    await new Promise(resolve => setTimeout(resolve, G_CONFIG.FEATURES.batchFold.foldDelay));
                } catch (error) {
                    this.error(`${targetFoldState ? '折叠' : '展开'}标题失败:`, headingId, error);
                }
            }
  
            this.log(`${targetFoldState ? '折叠' : '展开'}标题操作完成`);
        },

        /**
         * 处理块折叠操作
         * @param {MouseEvent} event 鼠标事件对象
         * @param {HTMLElement} arrow 箭头元素
         */
        handleBlockFold(event, arrow) {
            // 通过箭头元素找到对应的NodeListItem按钮来获取块ID
            const blockButton = arrow.parentElement.querySelector('button[data-type="NodeListItem"]');
            if (!blockButton) {
                this.log('未找到对应的NodeListItem按钮元素');
                return;
            }

            const blockId = blockButton.dataset?.nodeId;
            if (!blockId) {
                this.log('未找到块ID');
                return;
            }

            const block = document.querySelector(`div[data-node-id="${blockId}"]`);
            if (!block) {
                this.log('未找到对应的块元素:', blockId);
                return;
            }

            this.log('找到目标块:', blockId, '类型:', block.dataset.type);

            // 检查块是否有子内容(子列表或其他可折叠的内容)
            const hasChildren = this.hasFoldableChildren(block);
            if (!hasChildren) {
                this.log('块没有可折叠的子内容,跳过操作');
                return;
            }

            const isFolded = !!block.getAttribute('fold');
            const isSelected = block.classList.contains('protyle-wysiwyg--select');
            const selectedSelector = isSelected ? '.protyle-wysiwyg--select' : '';
            const protyle = this.getProtyleByMouseAt(event);

            if (!protyle) return;

            if (event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
                // Alt + 点击:折叠/展开同级块
                this.log('执行同级块折叠/展开操作');
                if (isFolded) {
                    // 如果当前块是折叠状态,展开该块的所有内容
                    this.expandBlockContent(block);
                } else {
                    // 如果当前块是展开状态,折叠所有同级块(包括当前块)
                    this.foldAllSiblingBlocks(block, protyle, selectedSelector);
                }
  
                // 隐藏按钮
                this.hideGutterAfterClick(blockButton, arrow);
            }
        },

        /**
         * 折叠所有同级块(包括当前块)
         * @param {HTMLElement} currentBlock 当前块元素
         * @param {HTMLElement} protyle 编辑器元素
         * @param {string} selectedSelector 选中范围选择器
         */
        async foldAllSiblingBlocks(currentBlock, protyle, selectedSelector) {
            const blockType = currentBlock.dataset.type;
            const parent = currentBlock.parentElement;
  
            if (!parent) return;

            // 获取所有同级块(包括当前块)
            const siblings = Array.from(parent.children).filter(child => 
                child.dataset.type === blockType && 
                (!selectedSelector || child.matches(selectedSelector))
            );

            this.log(`找到 ${siblings.length} 个同级块`);

            // 过滤出有可折叠子内容的块
            const foldableSiblings = siblings.filter(sibling => this.hasFoldableChildren(sibling));
  
            this.log(`开始折叠 ${foldableSiblings.length} 个有子内容的同级块`);

            for (const sibling of foldableSiblings) {
                const siblingId = sibling.dataset?.nodeId;
                if (!siblingId) continue;

                const isCurrentlyFolded = !!sibling.getAttribute('fold');
                if (isCurrentlyFolded) continue; // 已经是折叠状态

                try {
                    await this.foldBlock(siblingId, true);
                    await new Promise(resolve => setTimeout(resolve, G_CONFIG.FEATURES.batchFold.foldDelay));
                } catch (error) {
                    this.error('折叠同级块失败:', siblingId, error);
                }
            }

            this.log('所有同级块折叠操作完成');
        },

        /**
         * 展开块内容
         * @param {HTMLElement} block 块元素
         */
        async expandBlockContent(block) {
            this.log('开始展开块内容');
  
            const blockId = block.dataset?.nodeId;
            if (!blockId) {
                this.log('未找到块ID,无法展开');
                return;
            }

            // 1. 先展开当前块本身
            try {
                await this.foldBlock(blockId, false);
                this.log('当前块展开成功:', blockId);
                await new Promise(resolve => setTimeout(resolve, G_CONFIG.FEATURES.batchFold.foldDelay));
            } catch (error) {
                this.error('展开当前块失败:', blockId, error);
                return; // 如果当前块展开失败,就不继续展开子块了
            }
  
            // 2. 收集所有需要展开的子块
            const foldedChildren = this.collectFoldedChildren(block);
  
            this.log(`找到 ${foldedChildren.length} 个需要展开的子块`);

            // 3. 展开所有子块
            for (const childId of foldedChildren) {
                try {
                    await this.foldBlock(childId, false);
                    await new Promise(resolve => setTimeout(resolve, G_CONFIG.FEATURES.batchFold.foldDelay));
                } catch (error) {
                    this.error('展开子块失败:', childId, error);
                }
            }

            this.log('块内容展开操作完成');
        },

        /**
         * 收集折叠的子块
         * @param {HTMLElement} block 块元素
         * @returns {string[]} 折叠子块的ID列表
         */
        collectFoldedChildren(block) {
            const foldedIds = [];
  
            const collectFolded = (element) => {
                const children = element.children;
                for (const child of children) {
                    if (child.dataset?.nodeId) {
                        const isFolded = !!child.getAttribute('fold');
                        if (isFolded) {
                            foldedIds.push(child.dataset.nodeId);
                        }
                        // 递归检查子元素
                        collectFolded(child);
                    }
                }
            };

            collectFolded(block);
            return foldedIds;
        },

        /**
         * 折叠/展开单个块
         * @param {string} id 块ID
         * @param {boolean} isFold 是否折叠
         */
        async foldBlock(id, isFold = true) {
            const url = `/api/block/${isFold ? 'foldBlock' : 'unfoldBlock'}`;
            const token = localStorage.getItem('authToken') || '';
  
            try {
                const response = await fetch(url, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': token ? `Token ${token}` : undefined
                    },
                    body: JSON.stringify({ id: id })
                });

                const result = await response.json();
                if (!result || result.code !== 0) {
                    throw new Error(result?.msg || 'API调用失败');
                }

                this.log(`${isFold ? '折叠' : '展开'}块成功:`, id);
            } catch (error) {
                this.error(`${isFold ? '折叠' : '展开'}块失败:`, id, error);
                throw error;
            }
        },

        /**
         * 操作后隐藏按钮
         * @param {HTMLElement} button 按钮元素
         * @param {HTMLElement} arrow 箭头元素
         */
        hideGutterAfterClick(button, arrow) {
            if (G_CONFIG.FEATURES.batchFold.hideGutterAfterClick) {
                button.style.display = 'none';
                arrow.style.display = 'none';
                this.log('操作按钮已隐藏');
            } else {
                // 旋转箭头图标
                const arrowSvg = arrow.querySelector('svg');
                if (arrowSvg) {
                    if (!arrowSvg.style.transform || arrowSvg.style.transform === '') {
                        arrowSvg.style.transform = 'rotate(90deg)';
                    } else {
                        arrowSvg.style.transform = '';
                    }
                }
            }
        },

        /**
         * 通过鼠标位置获取protyle元素
         * @param {MouseEvent} event 鼠标事件对象
         * @returns {HTMLElement|null} protyle元素
         */
        getProtyleByMouseAt(event) {
            const mouseX = event.clientX;
            const mouseY = event.clientY;
            const element = document.elementFromPoint(mouseX, mouseY);
            if (element) return element.closest('.protyle');
            return null;
        },

        /**
         * 判断是否有Ctrl键,兼容Mac
         * @param {KeyboardEvent} event 键盘事件对象
         * @returns {boolean} 是否有Ctrl键
         */
        isCtrlKey(event) {
            if (this.isMac()) return event.metaKey;
            return event.ctrlKey;
        },

        /**
         * 检查块是否有可折叠的子内容
         * @param {HTMLElement} block 块元素
         * @returns {boolean} 是否有可折叠的子内容
         */
        hasFoldableChildren(block) {
            // 检查是否有子列表
            const childList = block.querySelector('[data-type="NodeList"]');
            if (childList) {
                this.log('找到子列表,块可折叠');
                return true;
            }
  
            // 检查是否有其他可折叠的子块(如段落、标题等)
            const childBlocks = block.querySelectorAll('[data-node-id]');
            if (childBlocks.length > 0) {
                this.log('找到子块,块可折叠,子块数量:', childBlocks.length);
                return true;
            }
  
            this.log('块没有可折叠的子内容');
            return false;
        },

        /**
         * 判断是否Mac
         * @returns {boolean} 是否Mac系统
         */
        isMac() {
            return navigator.platform.indexOf("Mac") > -1;
        }
    };



    const G_FEATURE_MODULES = [ 
        HeadingModule, 
        ListIndentModule, 
        ListConvertModule, 
        ListIndexCorrectorModule,
        ListFoldedStyleModule,
        TaskStatsModule,
        HighlightEditingBlockModule,
        BatchFoldModule,

    ];

    // ===================================================================
    // ===================================================================
    //
    //                     4. 主引擎 (通常无需修改)
    //
    // ===================================================================
    // ===================================================================

    function initializeModules() {
        console.log('[增强插件] Main engine started. Initializing modules...');
        G_FEATURE_MODULES.forEach(module => {
            if (module.enabled) {
                if (typeof module.init === 'function') {
                    try { module.init(); }
                    catch (e) { console.error(`[增强插件] Failed to initialize module: ${module.name}`, e); }
                }
            } else {
                console.log(`[增强插件] Module [${module.name}] is disabled via settings.`);
            }
        });
        console.log('[增强插件] All modules processed.');
    }

    setTimeout(initializeModules, 1000);

    // 暴露测试方法到全局
    window.siyuanEnhancer = {
        correctListNumbers: () => {
            const module = G_FEATURE_MODULES.find(m => m.id === 'listIndexCorrector');
            if (module && module.manualCorrect) {
                module.manualCorrect();
            }
        }
    };

    if (window.Protyle && window.Protyle.prototype && window.Protyle.prototype.reload) {
        const originalReload = window.Protyle.prototype.reload;
        let isRefreshDisabled = false;

        window.Protyle.prototype.reload = function (...args) {
            if (isRefreshDisabled) {
                console.log('[MonkeyPatch] 刷新被禁止');
                return;
            }
            return originalReload.apply(this, args);
        };

        // 在需要禁止刷新时
        isRefreshDisabled = true;
        setTimeout(() => {
            isRefreshDisabled = false;
            console.log('刷新已解锁,现在可以正常刷新页面了。');
        }, 1000);
    }
})();

代码 2: 工具箱增强

注意:批量编辑功能,我没有做太多测试,最好你就编辑段落,因为涉及到同时编辑标题和段落,会有些不可控的因素.

超出论坛代码长度了,我打个包:
tooleditor.zip

谢谢,写了好久,欢迎鼓励,打赏我..

imgv302nnd66d13e2a7c24db89e42bbbe2d265adg.jpg

  • 思源笔记

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

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

    26297 引用 • 109318 回帖
  • 代码片段

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

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

    201 引用 • 1443 回帖 • 2 关注

相关帖子

欢迎来到这里!

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

注册 关于
请输入回帖内容 ...
  • chuchen

    感谢分享,我也在用练手,给了我很多启发~

  • MasterYS

    支持一下,一下端上来 5 个 JS 嘛

  • 自动统计块那个太好用了,非常方便统计任务块个数

  • HugZephyr

    请教一下, 你这个光标所在行怎么搞得

    image.png

    2 回复
  • shaoxia 1 评论

    高亮提示在编辑的块,一段 css:

    
    .sb:hover {
      box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.15), -2px -2px 6px rgba(0, 0, 0, 0.15), 0 0 12px rgba(0, 0, 0, 0.1) !important;
      transition: background-color 0.5s ease-out, box-shadow 0.5s ease-out !important;
    }
    .p:hover {
      box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.15), -2px -2px 6px rgba(0, 0, 0, 0.15), 0 0 12px rgba(0, 0, 0, 0.1) !important;
      transition: background-color 0.5s ease-out, box-shadow 0.5s ease-out !important;
    }
    .p.highlight {
      box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.5), -2px -2px 6px rgba(0, 0, 0, 0.5), 0 0 12px rgba(0, 0, 0, 0.2) !important;
      transition: background-color 0.5s ease-out, box-shadow 0.5s ease-out !important;
    }
    .p.highlight:hover {
      box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.5), -2px -2px 6px rgba(0, 0, 0, 0.5), 0 0 12px rgba(0, 0, 0, 0.2) !important;
      transition: background-color 0.5s ease-out, box-shadow 0.5s ease-out !important;
    }
    
    这个只能鼠标悬浮高亮吧, 光标所在行高亮怎么做的
    HugZephyr
  • shaoxia 1 评论

    回头吧,js 太多找不到了,我这几天在做基于标题/块的用户体验的一个终极版的升级 js,等做出来会集成进去的

    okk, 谢谢
    HugZephyr
  • chuchen

    你现在最新的功能跟我自己预想的也很多类似,但也有不同,比如你是无序列表转段落,但是我希望的刚好有个开关控制的段落转无序列表,也就是默认都是无序列表输入的方式。又比如你的 alt 点击列表关闭同级的,我也有个代码片段版本,然后想如何像 logseq 那样可以点击那个悬浮的竖线控制,谢谢分享,挺有意思

  • Lemon9

    最喜欢的两个功能,一个是能直接选中后设置颜色,另一个是能实现跨行设置文字颜色,感谢作者

请输入回帖内容 ...