实现可手动修改数据库主键引用块的标题

实现可手动修改数据库主键引用块的标题

前言

众所周知,思源数据库主键引用块或文档的标题是和块紧密关联的,无法自定义修改。

看到小伙伴们有自定义标题的需求,于是根据此回复帖子的原理,实现了该功能。

该功能可在不同的数据库下自定义不同的标题。

效果演示

r85.gif

代码
// 功能:修改数据库主键引用块的标题
// 版本:0.0.2
// 更新记录
// 0.0.1 初始版本,实现了可手动修改数据库主键引用块的标题
// 0.0.2 修复多个数据库切换焦点失效问题
// 问题反馈:https://ld246.com/article/1731215224093
// 使用方法:把代码放到思源js代码片段即可,修改方式和普通字段一样,点击编辑即可。
// 删除自定义标题方法:在输入框输入空值即可删除,或进入引用块属性面板,删除dbid-开头的自定义属性即可,可参考下面的原理帖子。
// 原理:https://ld246.com/article/1730621536283/comment/1731085168853?r=wilsons#comments
(()=>{
    let documentListened = false;
    observeElementCreation(
        document.body,
        '.av__row:not(.av__row--header) [data-dtype="block"]:has([data-type="block-ref"])',
        async cell => {
            // 获取数据库id
            const av = cell.closest('[data-av-type="table"][data-av-id]');
            const avId = av?.dataset.avId;
            const text = cell.querySelector('.av__celltext--ref');
            if(text) text.dataset.text = text.textContent;
            let blockId;
            if(avId) {
                // 获取块属性
                const block = cell.querySelector('[data-type="block-ref"]');
                blockId = block?.dataset.id;
                if(blockId){
                    let blockAttrs = {};
                    try {
                         blockAttrs = await fetchSyncPost('/api/attr/getBlockAttrs', {"id": blockId});
                    } catch(e) {
                        console.log(e);
                    }
                    if(blockAttrs.code === 0 && blockAttrs.data) {
                        blockAttrs = blockAttrs.data;
                        const customText = blockAttrs['custom-dbid-' + avId];
                        if(customText){
                            // 修改文本名称
                            if(text) text.textContent = customText;
                        }
                    }
                }
            }
          
            // 单元格单击事件,动态添加输入框
            cell.addEventListener('click', (event) => {
                // 如果引用块标题则返回
                if(event.target.tagName === 'SPAN' && event.target.classList.contains('av__celltext--ref')) {
                    return;
                }
                event.stopPropagation();
                // 移除多余的激活单元格样式
                const selectCells = document.querySelectorAll('.av__cell.av__cell--active.av__cell--select');
                selectCells.forEach(item => {
                    if(item !== cell) {
                        item.classList.remove('av__cell--active');
                        item.classList.remove('av__cell--select');
                    }
                });
                // 添加输入框并获取焦点
                addAvMask(cell);
                const inputElement = document.querySelector('.av__mask--rename .b3-text-field');
                inputElement.select();
                inputElement.focus();
                // 输入框点击事件
                inputElement.addEventListener('click', (event) => {
                    event.stopPropagation();
                });
                // 输入框按键事件
                inputElement.addEventListener('keydown', (event) => {
                    if (event.isComposing) {
                        return;
                    }
                    if(event.key === "Escape"|| event.key === "Tab" || (event.key === "Enter" && !event.shiftKey && isNotCtrl(event))){
                        event.preventDefault();
                        event.stopPropagation();
                        updateCellValue(blockId, avId, inputElement, text, cell, av, event);
                    }
                });
                // 输入框关闭事件
                inputElement.parentElement.addEventListener('click', (event) => {
                    updateCellValue(blockId, avId, inputElement, text, cell, av);
                });
            });
            // 更新按钮阻止冒泡
            cell.querySelector('[data-type="block-more"]')?.addEventListener('click', (event) => {
                event.stopPropagation();
            });
            // 监听document按键事件(仅在第一个单元格被添加时创建一次)
            if(!documentListened) {
                documentListened = true;
                document.addEventListener('keydown', (event) => {
                    if (event.isComposing) {
                        return;
                    }
                    // 当按回车时打开输入框
                    if (event.key === "Enter" && !event.shiftKey && isNotCtrl(event)) {
                        const inputElement = document.querySelector('.av__mask--rename .b3-text-field');
                        if(!inputElement) {
                            const selectCell = document.querySelector('.av__cell.av__cell--active.av__cell--select[data-dtype="block"]:has([data-type="block-ref"])');
                            if(selectCell) selectCell.click();
                        }
                        event.preventDefault();
                        event.stopPropagation();
                    }
                });
            }
        }
    );
    // 更新单元格
    async function updateCellValue(blockId, avId, inputElement, text, cell, av, event) {
        if(blockId && avId && inputElement) {
            let value = inputElement.value.trim();
            // 修改块属性,空值删除,注意这里的key不能用custom-av开头
            await fetchSyncPost('/api/attr/setBlockAttrs', {
                "id": blockId,
                "attrs": {
                    ["custom-dbid-" + avId]: value
                }
            });
            // 如果value为空,尝试获取块的内容
            text = text || cell.querySelector('.av__celltext--ref');
            if(!value && text) {
                value = text.dataset.text || '';
            }
            // 更新文本值
            if(text) text.textContent = value;
            // 删除输入框
            inputElement.parentElement.remove();
            // 修复av__cell--active样式丢失问题
            cell.classList.add('av__cell--active');
            // 解决多个表格切换焦点失效问题
            av?.querySelector('.av__header .av__title')?.focus();
            // 解决tab,方向键无法改变焦点问题
            pressKeyboard({key: 'Escape', keyCode: 27}, document);
            // 模拟触发tab键
            if(event?.key === "Tab"){
                pressKeyboard({key: 'Tab', keyCode: 9});
            }
        }
    }
    // 添加输入框
    function addAvMask(cell) {
        // see https://github.com/siyuan-note/siyuan/blob/487c48427ae8e3b840d209047cc316273c7a3931/app/src/protyle/render/av/cell.ts#L363
        let html = "";
        let cellRect = cell.getBoundingClientRect();
        let height = cellRect.height;
        const contentElement = cell.closest('.protyle-content');
        if (contentElement) {
            const contentRect = contentElement.getBoundingClientRect();
            if (cellRect.bottom > contentRect.bottom) {
                height = contentRect.bottom - cellRect.top;
            }
        }
        const style = `style="padding-top: 6.5px;position:absolute;left: ${cellRect.left}px;top: ${cellRect.top}px;width:${Math.max(cellRect.width, 25)}px;height: ${height}px"`;
        html = `<textarea ${style} spellcheck="false" class="b3-text-field">${cell.firstElementChild.textContent}</textarea>`;
        document.body.insertAdjacentHTML("beforeend", `<div class="av__mask av__mask--rename" style="z-index: ${++window.siyuan.zIndex}">${html}</div>`);
    }
    // 是否非ctrl键true是,false不是
    function isNotCtrl(event) {
        if (!event.metaKey && !event.ctrlKey) {
            return true;
        }
        return false;
    }
    // 模拟按键
    function pressKeyboard(keyInit, element) {
        element = element || getProtyleEl();
        keyInit["bubbles"] = true;
        let keydownEvent = new KeyboardEvent('keydown', keyInit);
        element?.dispatchEvent(keydownEvent);
        let keyUpEvent = new KeyboardEvent('keyup', keyInit);
        element?.dispatchEvent(keyUpEvent);
    }
    // 获取当前文档编辑器元素
    function getProtyleEl() {
        return document.querySelector('[data-type="wnd"].layout__wnd--active .protyle:not(.fn__none) .protyle-wysiwyg.protyle-wysiwyg--attr');
    }
    // 请求api
    async function fetchSyncPost(url, data, returnType = 'json') {
        const init = {
            method: "POST",
        };
        if (data) {
            if (data instanceof FormData) {
                init.body = data;
            } else {
                init.body = JSON.stringify(data);
            }
        }
        try {
            const res = await fetch(url, init);
            const res2 = returnType === 'json' ? await res.json() : await res.text();
            return res2;
        } catch(e) {
            console.log(e);
            return returnType === 'json' ? {code:e.code||1, msg: e.message||"", data: null} : "";
        }
    }
    // 监听元素被创建
    function observeElementCreation(parentNode, selector, onElementCreated) {
        // 配置观察器选项
        const config = { 
            childList: true, // 观察直接子节点的添加和移除
            subtree: true    // 观察所有后代节点
        };
        // 当检测到变动时执行的回调函数
        const callback = function(mutationsList, observer) {
            for (let mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            // 使用 querySelectorAll 查找所有符合条件的新元素
                            const elements = node.querySelectorAll(selector);
                            elements.forEach(element => {
                                onElementCreated(element); // 调用外部提供的回调函数
                            });
                        }
                    });
                }
            }
        };
        // 创建一个观察器实例并传入回调函数
        const observer = new MutationObserver(callback);
        // 开始观察目标节点
        observer.observe(parentNode, config);
        // 返回一个函数来停止观察
        return () => observer.disconnect();
    }
    // 统计代码
    (function tongji(enable) {
        if(!enable) return;
        !function(p){"use strict";!function(t){var s=window,e=document,i=p,c="".concat("https:"===e.location.protocol?"https://":"http://","sdk.51.la/js-sdk-pro.min.js"),n=e.createElement("script"),r=e.getElementsByTagName("script")[0];n.type="text/javascript",n.setAttribute("charset","UTF-8"),n.async=!0,n.src=c,n.id="LA_COLLECT",i.d=n;var o=function(){s.LA.ids.push(i)};s.LA?s.LA.ids&&o():(s.LA=p,s.LA.ids=[],o()),r.parentNode.insertBefore(n,r)}()}({id:"KctUAV1SBR5tEp7H",ck:"KctUAV1SBR5tEp7H"});
    })(1);
})()

备份代码地址:点这里查看

使用方法

把上面的代码放到思源 js 代码片段即可。

数据库主键修改方式和普通字段一样,点击编辑即可。

删除自定义标题方法

在输入框输入空值即可删除或进入引用块属性面板,删除 dbid-开头的自定义属性即可,可参考这个原理帖子

免责声明

请严格测试后谨慎使用,由此造成的任何后果均与脚本与作者无关。

image.png

留下你的脚步
推荐阅读