├── .gitignore ├── icon.png ├── preview.png ├── README.assets ├── image.png ├── setting.jpg └── hoverShowButton.jpg ├── static ├── layui-v2.8.12 │ ├── tree.html │ ├── font │ │ ├── iconfont.eot │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ └── iconfont.woff2 │ ├── css │ │ └── layui-dark-230803.css │ └── test.html ├── dayjs.min.js ├── listChildDocs copy.css ├── listChildDocs.css └── markmap-view-0.18.12.js ├── .release.py ├── widget.json ├── DEV_INTRO.md ├── .github └── workflows │ └── publish.yml ├── faq.md ├── src ├── setBlockBreadcrumb2AttrHelper.js ├── ref-util.js ├── common.js ├── addChildDocListHelper.js ├── config.js ├── API.js └── addChildDocLinkHelper.js ├── README.md ├── CHANGELOG.md └── README_en_US.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpaqueGlass/listChildDocs/main/icon.png -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpaqueGlass/listChildDocs/main/preview.png -------------------------------------------------------------------------------- /README.assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpaqueGlass/listChildDocs/main/README.assets/image.png -------------------------------------------------------------------------------- /README.assets/setting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpaqueGlass/listChildDocs/main/README.assets/setting.jpg -------------------------------------------------------------------------------- /static/layui-v2.8.12/tree.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpaqueGlass/listChildDocs/main/static/layui-v2.8.12/tree.html -------------------------------------------------------------------------------- /README.assets/hoverShowButton.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpaqueGlass/listChildDocs/main/README.assets/hoverShowButton.jpg -------------------------------------------------------------------------------- /static/layui-v2.8.12/font/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpaqueGlass/listChildDocs/main/static/layui-v2.8.12/font/iconfont.eot -------------------------------------------------------------------------------- /static/layui-v2.8.12/font/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpaqueGlass/listChildDocs/main/static/layui-v2.8.12/font/iconfont.ttf -------------------------------------------------------------------------------- /static/layui-v2.8.12/font/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpaqueGlass/listChildDocs/main/static/layui-v2.8.12/font/iconfont.woff -------------------------------------------------------------------------------- /static/layui-v2.8.12/font/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpaqueGlass/listChildDocs/main/static/layui-v2.8.12/font/iconfont.woff2 -------------------------------------------------------------------------------- /static/layui-v2.8.12/css/layui-dark-230803.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpaqueGlass/listChildDocs/main/static/layui-v2.8.12/css/layui-dark-230803.css -------------------------------------------------------------------------------- /.release.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | with open('CHANGELOG.md', 'r', encoding='utf-8') as f: 4 | readme_str = f.read() 5 | 6 | match_obj = re.search(r'(?<=### )[\s\S]*?(?=#)', readme_str, re.DOTALL) 7 | if match_obj: 8 | h3_title = match_obj.group(0) 9 | with open('result.txt', 'w') as f: 10 | f.write(h3_title) 11 | else: 12 | with open('result.txt', 'w') as f: 13 | f.write("") 14 | -------------------------------------------------------------------------------- /widget.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "listChildDocs", 3 | "author": "OpaqueGlass", 4 | "url": "https://github.com/OpaqueGlass/listChildDocs", 5 | "version": "0.4.6", 6 | "displayName": { 7 | "default": "listChildDocs", 8 | "zh_CN": "列出子文档", 9 | "en_US": "listChildDocs" 10 | }, 11 | "minAppVersion": "3.0.17", 12 | "description": { 13 | "default": "Display child-doc list in widget, or insert list into the doc. [English is not fully supported]", 14 | "zh_CN": "在挂件里显示文档/大纲列表,或向文档中插入文档/大纲列表", 15 | "en_US": "Display child-doc list in widget, or insert list into the doc. [English is not fully supported]" 16 | }, 17 | "readme": { 18 | "default": "README.md", 19 | "zh_CN": "README.md", 20 | "en_US": "README_en_US.md" 21 | }, 22 | "i18n": [ 23 | "zh_CN", 24 | "en_US" 25 | ], 26 | "funding": { 27 | "openCollective": "", 28 | "patreon": "", 29 | "github": "", 30 | "custom": [ 31 | "https://wj.qq.com/s2/12395364/b69f/" 32 | ] 33 | } 34 | } -------------------------------------------------------------------------------- /DEV_INTRO.md: -------------------------------------------------------------------------------- 1 | ## 开发指引 2 | 3 | ### 代码结构 4 | 5 | 此项目是💩山代码,大体上,挂件整体逻辑位于`listChildDocsMainV2`,`ConfigManager`包括设置项的保存和读取策略,`config.js`为历史遗留; 6 | 7 | #### 常见操作说明 8 | 9 | ##### 添加新的独立设置项 10 | 11 | 1. 为设置项选择一个新的key,请参考`ConfigManager.js`中的ConfigSaveManager、defaultConfig;并在此处设置默认值; 12 | 2. 界面UI使用layUi,因此,要在界面上显示设置项,请仿照index.html中的相关按钮设置复制一个,注意修改其中的name和id(如果有); 13 | 3. 在代码中访问,可使用`g_allData["config"][settingKey]`; 14 | 15 | 16 | ##### 添加新模式 17 | 18 | 请参考`listChildDocsClass.js`,继承`Printer`类; 19 | 20 | ### 使用lcd 21 | 22 | 直接使用iframe嵌入,例: 23 | 24 | ``` 25 | result.innerHTML = ``; 26 | ``` 27 | 28 | #### 引入默认设置 29 | 30 | 在frameElement上加上属性`data-default-config`,例:(请注意转义,或直接使用`dataset.defaultConfig` 31 | ``` 32 | data-default-config="{"printMode":11}" 33 | ``` 34 | 35 | #### 强制指定设置 36 | 37 | 在src中指定参数,例 38 | ``` 39 | /widgets/listChildDocs?printMode=11 40 | ``` -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | 18 | - name: Generate Package.zip 19 | run: find . -not -path '*/\.*' -print | xargs zip package.zip 20 | 21 | 22 | - name: Set up Python 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: '3.8' 26 | 27 | - name: Get CHANGELOGS 28 | run: python .release.py 29 | 30 | - name: Pre Release 31 | uses: softprops/action-gh-release@v1 32 | if: contains(github.ref, 'beta') || contains(github.ref, 'alpha') 33 | with: 34 | body_path: ./result.txt 35 | files: package.zip 36 | prerelease: true 37 | token: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Release 40 | uses: softprops/action-gh-release@v1 41 | if: ${{ ! contains(github.ref, 'beta') && ! contains(github.ref, 'alpha') }} 42 | with: 43 | body_path: ./result.txt 44 | files: package.zip 45 | prerelease: false 46 | token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | > 更新于listChildDocs版本v0.2.0-beta1 4 | 5 | 6 | ### 为什么同步前刷新可能导致同步覆盖? 7 | 8 | 如果进行自动刷新时并没有和其他设备进行同步,自动刷新后,设备的旧文档将成为最新,下次同步时将覆盖云端的新文档,可能导致其他设备的编辑丢失。 9 | 10 | > 模式为`默认`、`挂件beta`(目录列表在挂件中展示)时,自动刷新对文档没有修改,不会导致同步覆盖。 11 | 12 | 单击刷新按钮(会同时保存缓存)、双击刷新按钮(保存挂件设置)仍可能导致同步覆盖,请不要在未同步时进行这些操作。 13 | 14 | ### 开启自动刷新后,文档中目录列表何时会进行刷新? 15 | 16 | > 必须先关闭安全模式,挂件才会对文档中的目录列表进行自动刷新。 17 | 18 | - 挂件被加载时(例如以下情景:) 19 | 20 | - 打开挂件所在文档(例如从文档树打开); 21 | - 浏览挂件所在文档时,由于文档较长挂件被动态加载; 22 | - 思源启动时,将恢复上次未关闭的文档,如果这些文档里有挂件,那么也进行刷新; 23 | - 挂件或所在文档显示在浮窗中。 24 | 25 | - 点击页签切换到所在文档时(默认仅windows,可通过修改配置文件`includeOs`更改/禁用); 26 | 27 | ### 我已经知晓同步相关风险,应该如何关闭安全模式? 28 | 29 | 1. 修改`config.js`(或`custom.js`)的`safeMode`为`false`; 30 | 31 | 【建议同时开启只读安全模式`safeModePlus`】 32 | 33 | ## 关于自动插入助手(代码片段) 34 | 35 | ### 自动插入助手有什么用? 36 | 37 | 自动插入助手可以: 38 | - 自动对空白的父文档插入listChildDocs挂件; 39 | - ~~为父文档加入子文档引用或超链接,并随子文档变化进行更新;~~(开发者未完整测试。除非您愿意参与测试并在测试期间持续备份文档,否则请勿使用) 40 | 41 | ### 自动插入助手风险说明 42 | 43 | - 自动插入助手模式为`插入挂件`: 44 | 45 | (默认)设置为打开空白父文档时触发,**(可能导致文档打开卡顿)** 46 | 47 | 若设置为创建子文档时触发,**(可能导致文档编辑卡顿)** 48 | 49 | - 自动插入助手模式为`插入链接`、`插入引用块`、`插入自定义`: 50 | 51 | 可能导致打开、切换文档卡顿。**开发者未测试,这些模式可能存在其他问题,请勿使用** 52 | 53 | 如果有多个窗口界面(包括浏览器页面),将同时运行着多个自动插入助手,有可能导致**重复插入挂件/子文档链接**; 54 | 55 | 出现上述情况,请停止使用自动插入助手。(设置-外观-代码片段-JS 移除代码片段,然后重新启动思源) 56 | 57 | ### 自动插入助手会在什么时候对文档进行修改? 58 | 59 | > 本部分只介绍默认设置下的触发时机,若更改了config.js `helperSettings`,请以配置为准。 60 | 61 | - 自动插入助手模式为`插入挂件`: 62 | 63 | 打开文档时,将获取该文档的子文档。如果打开的文档有子文档且文档为空(只有空格或空段落块也认为空),则插入挂件; 64 | 65 | - 自动插入助手模式为`插入链接`、`插入引用块`、`插入自定义`: 66 | 67 | 打开任意文档时,获取该文档的子文档,如果发现子文档变动,则进行对应修改。 68 | 69 | ### 如何使用自动插入助手? 70 | 71 | > 开发者未对 `插入链接`、`插入引用块`、`插入自定义` 模式进行完整测试,除非您愿意参与测试并在测试期间持续备份文档,否则请勿使用。 72 | 73 | #### 启用 74 | 75 | 1. 在`config.js`(或`custom.js`)中启用只读安全模式`safeModePlus`(设置为`true`)。 76 | 77 | 2. 在 设置-外观-代码片段-JS 中添加代码片段(复制以下内容): 78 | 79 | ```javascript 80 | import("/widgets/listChildDocs/src/addChildDocLinkHelper.js"); 81 | // 如果挂件位置有更改,需要修改 82 | // import("/widgets/挂件所在文件夹/src/addChildDocLinkHelper.js"); 83 | ``` 84 | 85 | #### 禁用 86 | 87 | 在 设置-外观-代码片段-JS 中删除代码片段,然后重新启动思源。 88 | 89 | #### 配置自动插入助手 90 | 91 | 在`config.js`(或`custom.js`)中修改配置`helperSettings`,具体配置项见`config.js`文件内的说明。 -------------------------------------------------------------------------------- /src/setBlockBreadcrumb2AttrHelper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 将指定块的面包屑信息设置到属性中 3 | * @date 2024-10-10 4 | * @version v0.2 5 | */ 6 | (() => { 7 | async function __main__(userInpuId) { 8 | const attributeViewId = await judgeAvId(userInpuId); 9 | const blockIds = await getBlockIdsFromDatabase(attributeViewId); 10 | const blocksBreadcrumbInfos = await rearrangeBlocksBreadCrumbAndSetToAttr(blockIds); 11 | } 12 | 13 | async function judgeAvId(userInputId) { 14 | const sqlResult = await queryAPI(`SELECT markdown FROM blocks WHERE id = '${userInputId}' and type='av'`); 15 | if (sqlResult && sqlResult.length > 0) { 16 | const markdown = sqlResult[0].markdown; 17 | const match = markdown.match(/data-av-id="([^"]+)"/); 18 | if (match && match[1]) { 19 | return match[1]; 20 | } else { 21 | return userInputId; 22 | } 23 | } else { 24 | return userInputId; 25 | } 26 | } 27 | 28 | async function getBlockIdsFromDatabase(attributeViewId) { 29 | let avResponse = { rows: { values: [] } }; 30 | let page = 1; 31 | const pageSize = Number.MAX_SAFE_INTEGER; 32 | let allValues = []; 33 | let tempAvResponse = null; 34 | do { 35 | tempAvResponse = await getAttributeViewPrimaryKeyValues(attributeViewId, page, pageSize); 36 | if (tempAvResponse && tempAvResponse.rows && tempAvResponse.rows.values && tempAvResponse.rows.values.length > 0) { 37 | allValues = allValues.concat(tempAvResponse.rows.values); 38 | page++; 39 | } else { 40 | break; 41 | } 42 | } while (tempAvResponse.rows.values && tempAvResponse.rows.values.length > 0); 43 | 44 | avResponse.rows.values = allValues; 45 | if (avResponse == null) { 46 | throw new Error("获取属性视图失败: " + attributeViewId); 47 | } 48 | const existKey = avResponse.rows?.values?.map((value) => value.blockID) || []; 49 | const result = existKey.filter((key) => key !== null && key !== ''); 50 | console.log(">>>>属性视图中包含的关联块id", result); 51 | return result; 52 | } 53 | 54 | async function rearrangeBlocksBreadCrumbAndSetToAttr(blockIds) { 55 | const blocksBreadcrumbInfos = []; 56 | const attrs = []; 57 | console.group("面包屑API原始信息"); 58 | for (let blockId of blockIds) { 59 | const breadcrumb = await getBlockBreadcrumb(blockId); 60 | console.log(`>>>>${blockId}的块面包屑信息`, breadcrumb); 61 | blocksBreadcrumbInfos.push({ 62 | id: blockId, 63 | breadcrumb: breadcrumb 64 | }); 65 | attrs.push({ 66 | id: blockId, 67 | attrs: { 68 | "custom-block-breadcrumb": breadcrumbInfoPostProcess(breadcrumb), 69 | } 70 | }); 71 | 72 | } 73 | console.groupEnd("面包屑API原始信息"); 74 | console.log("blockAttrs", attrs); 75 | await batchSetBlockAtrs(attrs); 76 | return blocksBreadcrumbInfos; 77 | } 78 | 79 | function breadcrumbInfoPostProcess(blocksBreadcrumbInfo) { 80 | let result = ""; 81 | blocksBreadcrumbInfo.forEach((blockBreadcrumbInfo) => { 82 | if (blockBreadcrumbInfo.type === "NodeHeading") { 83 | result += blockBreadcrumbInfo.name + "/"; 84 | } 85 | }); 86 | return result; 87 | } 88 | 89 | async function batchSetBlockAtrs(blockAttrs) { 90 | let url = "/api/attr/batchSetBlockAttrs"; 91 | let postBody = { 92 | blockAttrs: blockAttrs, 93 | }; 94 | let response = await postRequest(postBody, url); 95 | if (response.code == 0 && response.data != null) { 96 | return response.data; 97 | } 98 | return null; 99 | } 100 | 101 | async function queryAPI(sqlstmt){ 102 | let url = "/api/query/sql"; 103 | let response = await postRequest({stmt: sqlstmt},url); 104 | if (response.code == 0 && response.data != null){ 105 | return response.data; 106 | } 107 | if (response.msg != "") { 108 | throw new Error(`SQL ERROR: ${response.msg}`); 109 | } 110 | 111 | return []; 112 | } 113 | 114 | 115 | async function getAttributeViewPrimaryKeyValues(id, page=1, pageSize=32) { 116 | let url = "/api/av/getAttributeViewPrimaryKeyValues"; 117 | let postBody = { 118 | id: id, 119 | page: page, 120 | pageSize: pageSize 121 | }; 122 | let response = await postRequest(postBody, url); 123 | if (response.code == 0 && response.data != null) { 124 | return response.data; 125 | } 126 | return null; 127 | } 128 | 129 | function getBlockBreadcrumb(blockId, excludeTypes = []) { 130 | let data = { 131 | "id": blockId, 132 | "excludeTypes": excludeTypes 133 | }; 134 | let url = `/api/block/getBlockBreadcrumb`; 135 | return getResponseData(postRequest(data, url)); 136 | } 137 | 138 | async function postRequest(data, url) { 139 | let result; 140 | await fetch(url, { 141 | body: JSON.stringify(data), 142 | method: 'POST', 143 | headers: { 144 | "Authorization": "Token " + window.siyuan?.config?.api?.token ?? "", 145 | "Content-Type": "application/json" 146 | } 147 | }).then((response) => { 148 | result = response.json(); 149 | }); 150 | return result; 151 | } 152 | 153 | async function getResponseData(promiseResponse) { 154 | const response = await promiseResponse; 155 | if (response.code != 0 || response.data == null) { 156 | return null; 157 | } else { 158 | return response.data; 159 | } 160 | } 161 | window["og_test_241010"] = __main__; 162 | console.log("og_test_成功初始化"); 163 | })(); -------------------------------------------------------------------------------- /static/dayjs.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).dayjs=e()}(this,(function(){"use strict";var t=1e3,e=6e4,n=36e5,r="millisecond",i="second",s="minute",u="hour",a="day",o="week",f="month",h="quarter",c="year",d="date",l="Invalid Date",$=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,y=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,M={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),ordinal:function(t){var e=["th","st","nd","rd"],n=t%100;return"["+t+(e[(n-20)%10]||e[n]||e[0])+"]"}},m=function(t,e,n){var r=String(t);return!r||r.length>=e?t:""+Array(e+1-r.length).join(n)+t},v={s:m,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),i=n%60;return(e<=0?"+":"-")+m(r,2,"0")+":"+m(i,2,"0")},m:function t(e,n){if(e.date()1)return t(u[0])}else{var a=e.name;D[a]=e,i=a}return!r&&i&&(g=i),i||!r&&g},w=function(t,e){if(p(t))return t.clone();var n="object"==typeof e?e:{};return n.date=t,n.args=arguments,new _(n)},O=v;O.l=S,O.i=p,O.w=function(t,e){return w(t,{locale:e.$L,utc:e.$u,x:e.$x,$offset:e.$offset})};var _=function(){function M(t){this.$L=S(t.locale,null,!0),this.parse(t)}var m=M.prototype;return m.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(O.u(e))return new Date;if(e instanceof Date)return new Date(e);if("string"==typeof e&&!/Z$/i.test(e)){var r=e.match($);if(r){var i=r[2]-1||0,s=(r[7]||"0").substring(0,3);return n?new Date(Date.UTC(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)):new Date(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)}}return new Date(e)}(t),this.$x=t.x||{},this.init()},m.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},m.$utils=function(){return O},m.isValid=function(){return!(this.$d.toString()===l)},m.isSame=function(t,e){var n=w(t);return this.startOf(e)<=n&&n<=this.endOf(e)},m.isAfter=function(t,e){return w(t)时打开思源块/文档 11 | * 为引入本项目,和原代码相比有更改 12 | * @refer https://github.com/leolee9086/cc-template/blob/6909dac169e720d3354d77685d6cc705b1ae95be/baselib/src/commonFunctionsForSiyuan.js#L118-L141 13 | * @license 木兰宽松许可证 14 | * @param {点击事件} event 15 | */ 16 | let openRefLink = function(event, paramId = ""){ 17 | 18 | let 主界面= window.parent.document 19 | let id; 20 | if (event && event.currentTarget && event.currentTarget.getAttribute("data-id")) { 21 | id = event.currentTarget.getAttribute("data-id"); 22 | }else{ 23 | id = paramId; 24 | } 25 | // 处理笔记本等无法跳转的情况 26 | if (!isValidStr(id)) {return;} 27 | if (event) { 28 | event.preventDefault(); 29 | event.stopPropagation(); 30 | } 31 | let 虚拟链接 = 主界面.createElement("span") 32 | 虚拟链接.setAttribute("data-type","block-ref") 33 | 虚拟链接.setAttribute("data-id",id) 34 | 虚拟链接.style.display = "none";//不显示虚拟链接,防止视觉干扰 35 | let 临时目标 = 主界面.querySelector(".protyle-wysiwyg div[data-node-id] div[contenteditable]") 36 | 临时目标.appendChild(虚拟链接); 37 | let clickEvent = new MouseEvent("click", { 38 | ctrlKey: event ? event.ctrlKey : undefined, 39 | shiftKey: event? event.shiftKey : undefined, 40 | altKey: event ? event.altKey : undefined, 41 | metaKey: event ? event.metaKey : undefined, 42 | bubbles: true 43 | }); 44 | window.getSelection()?.removeAllRanges(); 45 | 虚拟链接.dispatchEvent(clickEvent); 46 | 虚拟链接.remove(); 47 | } 48 | 49 | /** 50 | * 打开浮窗 51 | * 为引入本项目,和原代码相比有更改 52 | * @link https://github.com/leolee9086/cc-template/blob/6909dac169e720d3354d77685d6cc705b1ae95be/index.html#L320-L385 53 | * @param {事件} event 54 | * @returns 55 | */ 56 | let showFloatWnd = function(event){ 57 | //当前鼠标所悬停元素(块)的id 58 | // event.target触发事件的元素, event.currentTarget事件绑定的元素 59 | let blockId = event.target.getAttribute("data-node-id") ? event.target.getAttribute("data-node-id") : event.currentTarget.getAttribute("data-node-id"); 60 | let 挂件自身元素 = getWidgetElem()? getWidgetElem() : window.frameElement; 61 | let 思源主界面 = window.parent.document; 62 | let 挂件坐标 = 获取元素视图坐标(挂件自身元素); 63 | //所引用的对象的id 64 | let linkId = event.target.getAttribute("data-id") ? event.target.getAttribute("data-id"):blockId; 65 | // 处理笔记本等无法跳转的情况 66 | if (!isValidStr(linkId)) return; 67 | let panel = window.parent.document.querySelector(`.block__popover[data-oid="${linkId}"]`); 68 | if (panel) return; 69 | let 虚拟链接 = 思源主界面.createElement("span"); 70 | 虚拟链接.setAttribute("data-type", "block-ref"); 71 | 虚拟链接.setAttribute("data-id", linkId); 72 | let 临时目标 = 思源主界面.querySelector( 73 | ".layout__wnd--active .fn__flex-1.protyle:not(.fn__none) .protyle-wysiwyg div[data-node-id] div[contenteditable]" 74 | ); 75 | 临时目标.appendChild(虚拟链接); 76 | 虚拟链接.style.position = "fixed"; 77 | 虚拟链接.style.opacity = "0";//不显示虚拟链接,防止视觉干扰 78 | 挂件坐标 = 获取元素视图坐标(挂件自身元素); 79 | let Y = event.clientY + 挂件坐标.Y; 80 | let X = event.clientX + 挂件坐标.X; 81 | let testX = event.target.getBoundingClientRect().left + 挂件坐标.X; 82 | let testY = event.target.getBoundingClientRect().top + 挂件坐标.Y; 83 | // console.log("testYX", testY, testX) 84 | 虚拟链接.style.top = (testY + 36) + "px";//这个是临时创建的“block-ref”的位置,不设定应该也没啥? 85 | 虚拟链接.style.left = testX + "px"; 86 | 87 | // 鼠标悬停事件,该事件的bubbles也很关键,让事件冒泡出去 88 | let mouseoverEvent = new MouseEvent("mouseover", { 89 | "button": 0, 90 | "cancelable": false, 91 | "view": window.parent, 92 | "detail": 1, 93 | "screenX": 500, 94 | "screenY": 500, 95 | "clientX": 500, 96 | "clientY": 500, 97 | "bubbles": true, 98 | "ctrlKey": event.ctrlKey, 99 | "metaKey": event.metaKey, 100 | "relatedTarget": window.frameElement 101 | }); 102 | // if (Y < 100 || X < 100) { 103 | // 虚拟链接.remove(); 104 | // return null 105 | // } 106 | 虚拟链接.dispatchEvent(mouseoverEvent); 107 | //不让悬停时挂件highlight(暂时未定位的产生原因,先通过移除class样式临时解决) 108 | window.frameElement.parentElement.parentElement.classList.remove("protyle-wysiwyg--hl"); 109 | //搬运过来有修改,和上面的修改有点...冲突,此部分充满了玄学 110 | //强制重设popover位置,间隔5ms,重设时间1.2s 111 | // let interval = setInterval( ()=>{ 112 | // //参考了https://github.com/leolee9086/cc-template/blob/6909dac169e720d3354d77685d6cc705b1ae95be/index.html#L102-L117 113 | // let panel = window.top.document.querySelector(`.block__popover[data-oid="${linkId}"]`); 114 | // if (panel) { 115 | // panel.style.top = testY + 36 + "px"; 116 | // panel.style.left = testX + "px"; 117 | // console.log("reseted") 118 | // // console.log("Reset",Y,X) 119 | // // panel.style.top = testY + 36 + "px";//呃,不再覆盖链接试一下 120 | // // let left = testX - (panel.offsetWidth / 2 || 0); 121 | // // if (left < 0) left = 0; 122 | // // panel.style.left = left + "px"; 123 | // // panel.style.maxHeight = (window.innerHeight - panel.getBoundingClientRect().top - 8) + "px"; 124 | // // linkId = ""; 125 | // console.log(testY, testX, panel.style.top, panel.style.left); 126 | // // clearInterval(interval); 127 | // } 128 | // }, 300); 129 | setTimeout( ()=> {虚拟链接.remove();}, 3000); 130 | // setTimeout(()=>{clearInterval(interval);}, 1000);//移除重设定时器 131 | // console.log("test", window.top.siyuan.blockPanels); 132 | // 可以考虑由挂件移除blockPanel,但触发事件不好确定 133 | // } else (this.链接id = "") 134 | } 135 | 136 | 137 | 138 | /** 139 | * 获取元素视图坐标 140 | * @refer https://github.com/leolee9086/cc-template/blob/6909dac169e720d3354d77685d6cc705b1ae95be/index.html#L399-L413 141 | * @param element 要获取的元素 142 | * */ 143 | let 获取元素视图坐标 = function(element) { 144 | var scrollTop = 获取文档元素(getWidgetElem()).scrollTop; 145 | var scrollLeft = 获取文档元素(getWidgetElem()).scrollLeft; 146 | let frame宽度 = window.frameElement.offsetWidth 147 | let 左偏移 = 0 148 | let 总宽度 = getWidgetElem().offsetWidth 149 | 左偏移 = (总宽度 - frame宽度) / 2 || 0; 150 | var absolutePosi = 获取元素绝对坐标(element); 151 | var Viewport = { 152 | X: absolutePosi.left - scrollLeft + 左偏移, 153 | Y: absolutePosi.top - scrollTop, 154 | }; 155 | return Viewport; 156 | } 157 | 158 | /** 159 | * 获取挂件自身元素 160 | * @refer https://github.com/leolee9086/cc-template/blob/6909dac169e720d3354d77685d6cc705b1ae95be/baselib/src/commonFunctionsForSiyuan.js#L106-L110 161 | * */ 162 | let getWidgetElem = function(){ 163 | try{ 164 | return window.frameElement.parentElement.parentElement} 165 | catch(e){ 166 | console.error("获取挂件自身元素失败window.frameElement.parentElement.parentElement"); 167 | return null 168 | } 169 | } 170 | 171 | 172 | /** 173 | * 获取元素绝对坐标 174 | * 在原代码基础上无更改 175 | * @refer https://github.com/leolee9086/cc-template/blob/6909dac169e720d3354d77685d6cc705b1ae95be/index.html#L386-L398 176 | * */ 177 | let 获取元素绝对坐标 = function(element) { 178 | element = element 179 | ? element 180 | : window.frameElement.parentElement || window.frameElement; 181 | var result = { left: element.offsetLeft, top: element.offsetTop }; 182 | element.offsetParent ? (element = element.offsetParent) : null; 183 | while (element) { 184 | result["left"] += element.offsetLeft; 185 | result["top"] += element.offsetTop; 186 | element = element.offsetParent; 187 | } 188 | return result; 189 | } 190 | /** 191 | * 获取文档元素 192 | * 在原代码基础上无更改 193 | * @refer https://github.com/leolee9086/cc-template/blob/6909dac169e720d3354d77685d6cc705b1ae95be/index.html#L414-L421 194 | * @param {*} element 195 | * @returns 196 | */ 197 | let 获取文档元素 = function(element) { 198 | let docElement = {}; 199 | while (element && element.classList && !element.classList.contains("protyle-content")) { 200 | element = element.parentElement; 201 | } 202 | docElement = element; 203 | return docElement; 204 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## list Child Docs列出子文档 2 | 3 | [English](./README_en_US.md) 4 | 5 | > 用于[思源笔记](https://github.com/siyuan-note/siyuan)创建、更新指定文档目录列表的挂件。 6 | 7 | - 创建指定文档的目录或大纲列表; 8 | - 挂件中、文档中的有序或无序列表; 9 | - 以markmap导图 或 预览方格形式; 10 | - 支持在挂件加载或标签页切换时刷新; 11 | 12 | > 当前版本:v0.4.5 改进:兼容思源v3.4.0以上版本对挂件宽高的调整,修复挂件展开设置项但需滚动显示的问题; 13 | > 14 | > 详见更新日志CHANGELOG.md;[更新日志(网络)](CHANGELOG.md) ; 15 | 16 | > 插入时模式为`默认`、启用自动刷新、层级1、分栏0(自动分栏)、大纲层级3。 17 | > 18 | > listChildDocs 最初在2022年8月发布,感谢陪伴![现已进入维护阶段](https://github.com/OpaqueGlass/listChildDocs/issues/49),停止功能新增;如遇bug缺陷请反馈。 19 | > 20 | > 如果正在使用文档内创建列表的模式,可考虑改为使用“[目录插件](https://github.com/TinkMingKing/siyuan-plugins-index)”,可以在(listChildDocs设置-全局设置-最下方“重置和危险操作”)中批量删除挂件。 21 | 22 | ### 快速开始 23 | 24 | > 请注意,`listChildDocs`是一个挂件,不是插件;挂件作为文档内容块存在,需要您手动插入到文档中(编辑器内`/`菜单->挂件)。另请参考:[挂件](siyuan://blocks/20210824201257-cy7icrc) 25 | 26 | - 双击刷新按钮保存设置项; 27 | - 鼠标悬停在按钮或设置项旁边的i图标上,将显示提示; 28 | - 关于模式:可以切换到相应模式后点刷新试用一下; 29 | - 建议往下阅读`注意`部分; 30 | - 常见问题: 31 | - 文档中目录列表不能自动刷新? 32 | 默认启用了安全模式,请确认不使用同步后,可关闭安全模式; 33 | - 快捷键(焦点在挂件上才生效,请先点击挂件空白处) 34 | - `Ctrl + S` 打开或关闭设置面板; 35 | - `Ctrl + F` 打开或关闭搜索对话框; 36 | - `F5` 刷新; 37 | - 导图模式:`Alt+滚轮`缩放; 38 | 39 | 40 | ## 设置项说明 41 | 42 | ### 界面 43 | 44 | ![图片](README.assets/setting.jpg) 45 | 46 | 1. 刷新按钮:单击将刷新子文档列表;双击将保存当前设置 ; 47 | 2. 显示/隐藏设置 48 | 3. 挂件内高亮检索; 49 | 4. 刷新情况提示; 50 | 51 | > 设置项变更后,需要**双击刷新按钮保存设置**,否则下次启动时将丢失更改。 52 | 53 | ### 设置项分区 54 | 55 | ![常规设置界面部分提示](README.assets/image.png) 56 | 57 | ### 模式简介 58 | 59 | 模式名称前有`1.`的,将以有序列表的方式创建目录;模式名称前有`1.1.`的,将在有序列表的基础上使用全角空格缩进,创建多级编号的目录; 60 | 61 | 1. 无序列表\* 62 | 63 | - `默认`: (挂件中创建目录)“引用块”; 64 | 65 | - `url`: (文档中创建目录)在挂件下方创建无序列表展示`siyuan://`超链接; 66 | 67 | - `引用块`:(文档中创建目录)在挂件下方创建无序列表展示引用块; 68 | 69 | 2. 有序列表\* 70 | 71 | - `1.默认`:(挂件中)有序列表“引用块”; 72 | 73 | - …… 74 | 3. 带层级序号的列表(请参考下文的代码片段以隐藏列表项圆点) 75 | 76 | 4. ~~任务列表~~ 77 | 78 | - ~~`任务列表`:(文档中)~~ 【已知问题!刷新将创建一个新的任务列表,导致丢失任务的完成信息】(建议使用模板完成创建); 79 | 5. 导图;(依赖Markmap) 80 | 6. 预览方格;(同时提供子文档的开头部分内容预览,子文档为空,则显示子文档的子文档链接) 81 | 7. 按日期分组;(按创建/更新日期倒序分组显示) 82 | 8. 本地文件夹;(从指定的本地文件夹开始,创建文件夹目录列表)(仅桌面端) 83 | 84 | ### 代码片段 85 | 86 | #### 1.1.url模式显示效果修改 87 | 88 | > 由于1.1 模式基于无序列表显示,如要隐藏列表项前的圆点,需要添加以下代码片段。 89 | 90 | 复制以下内容,在 设置-外观-代码片段-CSS 中添加代码片段: 91 | 92 | ```css 93 | /* 不显示无序列表* */ 94 | .list[custom-list-format*=standard-ol-on-ul][data-subtype="u"] .protyle-action { 95 | display: none !important; 96 | border: 0; 97 | } 98 | /* 左侧间距调整 */ 99 | .protyle-wysiwyg [custom-list-format*=standard-ol-on-ul][data-subtype="u"] [data-node-id].li>[data-node-id] { 100 | margin-left: 5px !important; 101 | } 102 | /* 无序列表行距调整 */ 103 | .protyle-wysiwyg [custom-list-format*=standard-ol-on-ul][data-subtype="u"] [data-node-id] { 104 | margin-top: 0px; 105 | margin-bottom: 1px; 106 | padding-top: 0px; 107 | padding-bottom: 0px; 108 | } 109 | 110 | .protyle-wysiwyg [custom-list-format*=standard-ol-on-ul][data-subtype="u"] .li { 111 | min-height: 30px; 112 | } 113 | /* 列表提示线调整 */ 114 | .protyle-wysiwyg [custom-list-format*=standard-ol-on-ul][data-subtype="u"] [data-node-id].li:before { 115 | top: 30px; 116 | left: 15px; 117 | } 118 | ``` 119 | 120 | ### 自定义说明 121 | 122 | v0.2.2版本后,挂件对设置项的保存方式有所修改。挂件将默认把数据保存至`工作空间/data/storage/listChildDocs`文件夹。 123 | 124 | 该文件夹下的数据包括: 125 | 126 | - `data` 文件夹,保存针对文档的挂件设置,一般来说,插件插入的listChildDocs会对应给文档创建一个配置文件; 127 | - `schema` 文件夹,默认模板配置,目前无效; 128 | - `default.json` 挂件插入时的默认设置; 129 | - `global.json` 全局配置文件,需要手动保存全局设置后才会创建; 130 | - `custom.css` 自定义的样式文件,不自动创建; 131 | 132 | #### 默认设置和全局设置项 133 | 134 | 如果是初次升级到v0.2.2及以上版本,请在挂件手动保存默认设置、全局设置。(理论上,挂件将在首次载入时迁移原`custom.js`中保存的设置一次,设置项迁移将保留1个版本后被移除) 135 | 136 | 保存后,您可以打开`工作空间/data/storage/listChildDocs/global.json`手动对配置进行更改; 137 | 138 | 允许的配置项可参考`挂件位置/src/ConfigManager.js`的`defaultGlobalConfig`。您也可以在该文件直接更改,但如果和global.json的设置不同,将以global.json为准。 139 | 140 | #### 自定义样式 141 | 142 | 如果对挂件的默认样式不满意,您可以自行修改,将CSS保存在`工作空间/data/storage/listChildDocs/custom.css`文件下。 143 | 144 | ## ⚠️注意 145 | 146 | > 由于开发者能力所限,挂件还存在以下问题。使用前必读。 147 | 148 | - 直接将子文档目录列表**写入文档中**时: 149 | - 请避免过快地刷新文档列表; 150 | - 如果要多设备同步文档、且挂件所在文档要写其他内容时,**请勿使用自动刷新**[^1]; 151 | - 每次刷新时,将完全更新列表(即使子文档没有变化,也将更新列表全部内容); 152 | - **如果未完成同步,请勿点击刷新按钮**(多端同步前,在旧文档上刷新可能导致同步覆盖)[^1]: 153 | - 单击刷新按钮会更新文档中的目录列表或更新挂件目录列表缓存,文档编辑时间将被更新; 154 | - 双击刷新按钮会保存设置(设定挂件属性),文档编辑时间将被更新; 155 | - 切换页签时自动刷新的方法有点玄学,可能在未来的版本更新中无法使用; 156 | - 关于超级块属性刷新后重写: 157 | - `superBlockBeta`应设为`true`; 158 | - 若刷新后为超级块,属性将写入超级块的直接无序列表子块和超级块本身; 159 | - 若刷新前为超级块,将随机继承一个无序列表子块的属性; 160 | - 如要删除属性,建议直接删除超级块重新设置; 161 | - 关于写入自定义emoji图片: 162 | - 请避免图片路径包括特殊符号,例如`()%& `,如果包括,不能确定实际效果; 163 | - 暂不支持网络emoji; 164 | 165 | > 注:关于多端同步前刷新的详细说明,请阅读[FAQ](https://github.com/OpaqueGlass/listChildDocs/blob/main/faq.md)(挂件所在目录下的faq.md)。 166 | 167 | ## 反馈bug 168 | 169 | 请到github仓库[新建issue](https://github.com/OpaqueGlass/listChildDocs/issues)。您也可以[在这里](https://github.com/OpaqueGlass/listChildDocs/releases)下载历史版本。 170 | 171 | 如您无法访问github仓库,请[在此反馈](https://wj.qq.com/s2/12395364/b69f/)。 172 | 173 | ## 参考&感谢 174 | 175 | 本挂件使用/参考了以下大佬的项目: 176 | 177 | | 开发者 | 项目 | 开源协议 | 备注 | 178 | | ------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------- | 179 | | [leolee9086](https://github.com/leolee9086) | [cc-template](https://github.com/leolee9086/cc-template) | [木兰宽松许可证, 第2版](https://github.com/leolee9086/cc-template/blob/main/LICENSE) | 在挂件中展示“引用块“ | 180 | | [InEase](https://github.com/InEase) | [Note Map](https://github.com/InEase/SiYuan-Xmind) | N/A | API使用方式 | 181 | | [Zuoqiu-Yingyi](https://github.com/Zuoqiu-Yingyi) | [widget-query](https://github.com/Zuoqiu-Yingyi/widget-query) | AGPL-3.0 | 从custom.js导入自定义设置 | 182 | | | [Trilium](https://github.com/zadam/trilium) / note-list-widget | | 预览方格模式css样式,和功能设计 | 183 | 184 | 以下大佬参与代码贡献: 185 | 186 | - [Zuoqiu-Yingyi](https://github.com/Zuoqiu-Yingyi); 187 | 188 | (详见[贡献者(开发者)列表](https://github.com/OpaqueGlass/listChildDocs/graphs/contributors)) 189 | 190 | 191 | ### 依赖 192 | 193 | 1. [jQuery](https://jquery.com/) (本项目中通过jQuery选择页面元素); 194 | 195 | ``` 196 | jQuery JavaScript Library v3.6.0 https://jquery.com/ 197 | Copyright OpenJS Foundation and other contributors 198 | Released under the MIT license https://jquery.org/license 199 | ``` 200 | 201 | 2. [markmap](https://markmap.js.org/); 202 | 203 | ``` 204 | markmap-lib v0.18.12 | MIT License 205 | markmap-view v0.18.12 | MIT License 206 | https://github.com/markmap/markmap 207 | https://markmap.js.org/ 208 | ``` 209 | 210 | 3. [d3.js](https://d3js.org); 211 | 212 | ``` 213 | BSD-3-Clause https://opensource.org/licenses/BSD-3-Clause 214 | https://d3js.org v6.7.0 Copyright 2021 Mike Bostock 215 | ``` 216 | 217 | 4. [day.js](https://day.js.org/); 218 | 219 | ``` 220 | Day.js is licensed under a MIT License. 221 | https://github.com/iamkun/dayjs/ 222 | https://day.js.org/ 223 | ``` 224 | 225 | 5. [artDialog](https://github.com/aui/artDialog); 226 | 227 | ``` 228 | 免费,且开源,基于LGPL-3.0协议。 229 | https://github.com/aui/artDialog 230 | aui.github.io/artDialog/ 231 | ``` 232 | 233 | 6. [layui](https://gitee.com/layui/layui) 234 | 235 | ``` 236 | Layui 采用 MIT 许可发布。其他相关协议亦可参考《免责声明》https://gitee.com/layui/layui/blob/main/DISCLAIMER.md。 237 | ``` 238 | 239 | 240 | ### 图标 241 | 242 | 1. [刷新按钮图标](https://www.iconfinder.com/icons/5402417/refresh_rotate_sync_update_reload_repeat_icon),作者:[amoghdesign](https://www.iconfinder.com/amoghdesign),许可协议:[CC3.0 BY-NC](http://creativecommons.org/licenses/by-nc/3.0/); 243 | 244 | 2. [设置按钮图标](https://lucide.dev/?search=setting),[Lucide](https://github.com/lucide-icons/lucide), [ISC License](https://lucide.dev/license); 245 | 246 | 3. [搜索按钮图标](https://lucide.dev/?search=search) ,[Lucide](https://github.com/lucide-icons/lucide), [ISC License](https://lucide.dev/license)。 247 | 248 | [^1]: 点击刷新按钮将更新目录列表、挂件内目录缓存或保存设置,当前设备文档编辑时间也将更新。如果当前设备未同步,则当前设备的“旧”文档会覆盖云端内容,导致其他设备的编辑丢失。 249 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 更新日志 2 | 3 | ### v0.4.6 (2025年12月13日) 4 | 5 | - 改进:兼容思源v3.4.0+以上版本对挂件宽高的调整,修复挂件展开设置项但需滚动显示的问题; 6 | 7 | ### v0.4.5 (2025年9月10日) 8 | 9 | - 修复:预览模式下挂件未能正确载入设置的问题; 10 | 11 | ### v0.4.4 (2025年8月5日) 12 | 13 | - 改进:支持滚轮平移与缩放同时启用; 14 | - 重新实现导图模式缩放与平移; 15 | - 改进:导图模式自动暂存逻辑; 16 | 17 | ### v0.4.3 (2025年8月2日) 18 | 19 | - 修复:导图等部分模式无法填满可视区域的问题; 20 | 21 | ### v0.4.2 (2025年7月23日) 22 | 23 | - 修复:夜间模式下滚动条样式问题; 24 | 25 | ### v0.4.1 (2025年7月2日) 26 | 27 | - 修复:导图模式折叠展开后文档名称颜色不一致的问题; 28 | - 改进:导图模式默认关闭所有选项; 29 | - 改进:导图模式避免双击选中文本,但导致双击放大缩小; 30 | 31 | ### v0.4.0 (2025年6月27日) 32 | 33 | - 新增:记忆导图折叠状态; 34 | - 改进:记忆导图缩放、平移状态的应用方式和使用场景; 35 | - 改进:文档中模式支持动态图标; 36 | 37 | ### v0.3.7 (2025年6月19日) 38 | 39 | - 修复:大纲为空标题时,无法显示的问题; 40 | - 修复:文档中列表重写IAL时,部分特殊字符未转义的问题; 41 | 42 | ### v0.3.6 (2025年3月22日) 43 | 44 | - 修复:URL+大纲标题居中时,显示html居中代码的问题; 45 | 46 | ### v0.3.5 (2024年11月15日) 47 | 48 | - 修复:尝试修复MAC上快捷键⌘问题; 49 | - 改进:兼容思源3.1.12版本动态图标; 50 | 51 | ### v0.3.4 (2024年11月2日) 52 | 53 | - 修复:当编辑器中存在选区时,无法打开文档的问题; 54 | - 改进:兼容思源3.1.11版本可自定义的默认图标; 55 | 56 | ### v0.3.3-rc1 (2024年10月18日) 57 | 58 | - 改进:在挂件不可见时自动调整高度(兼容层级导航插件隐藏内容区); 59 | - 修复:数据库模式再二次刷新时,由于未能正确过滤已存在的块,导致思源大量警告日志的问题; 60 | 61 | ### v0.3.2 (2024年7月19日) 62 | 63 | - 改进:“导图”模式支持保存缩放与平移状态; 64 | - 修复:“导图”模式存在的潜在内存泄露问题; 65 | 66 | ### v0.3.1 (2024年6月5日) 67 | 68 | - 修复:“数据库”模式遇到`Unexpected end of JSON input`错误的问题(跟随思源v3.0.17接口名变更 https://github.com/siyuan-note/siyuan/issues/10201); 69 | 70 | ### v0.3.0 (2024年5月27日) 71 | 72 | - 改进&修复:更严格地执行安全模式,安全模式启用状态下,页签切换自动刷新也禁用,需要写文档的模式全部禁用; 73 | - 新增:(测试中)新模式,数据库; 74 | 75 | ### v0.2.8 (2024年5月22日) 76 | 77 | - 改进:本地文件夹目录模式支持不同系统选择不同路径; 78 | 79 | ### v0.2.7 (2023-3-14) 80 | - 修复:上一版本引入的字号变小的问题; 81 | 82 | ### v0.2.6 (2023-3-14) 83 | 84 | - 改进:移除部分日志,并减少控制台报错提示; 85 | - 移除:旧设置项迁移功能; 86 | - 新增:从挂件的URL参数中读取设置,并作为启动指定设置;从挂件iframe的`data-default-config`中读取默认设置(可被独立设置覆盖); 87 | - 新增:移除当前独立设置(并跟随全局); 88 | - 改进:支持显示文档树隐藏文档; 89 | 90 | ### v0.2.5 (2023-12-10) 91 | 92 | - 移除:自`widget/custom.js`导入自定义设置; 93 | 自v0.2.2起,设置项保存位置迁移,自v0.2.5起,不再支持`custom.js`中旧设置的自动迁移; 94 | - 移除:文档id定位日志; 95 | 96 | ### v0.2.4 (2023-10-21) 97 | 98 | - 新增:配置载入和保存(设置项模板); 99 | - 修复:修复一些已知问题 100 | - UI:描述错误,自动高度最小值、最大值应当仅填写数字,单位px; 101 | - BUG:文档内模式:出现错误提示、再次刷新成功后未调整挂件高度; 102 | - UI:合并模式独立设置与常规设置选项卡; 103 | - UI:缩小设置项页面,使得设置项更紧凑; 104 | - BUG:`$`符号在文档内模式应被转义;(原本`$`是想留着渲染成数学公式的,但确实很少人用,也不太支持点击 [参考](https://ld246.com/article/1695998247916)) 105 | - ENHANCE:导图模式下,针对首层级情况判断是否加入文档标题(如果只有一个首层级,就不加文档标题); 106 | - BUG: 按日期分组模式下,自动分列数较少的问题; 107 | - BUG:右键子文档菜单中新建子文档失败的问题; 108 | - FEAT:右键点击`../`部分时,将展开对【当前文档】进行修改操作的右键菜单(并非对父文档进行修改); 109 | - BUG: 多次按下`Ctrl+F`出现多个搜索对话框; 110 | - FEAT:右键新建子文档支持连续创建(使用`Ctrl+Enter`); 111 | - 直接点击按钮创建:创建并打开; 112 | - 使用`Ctrl+Enter`保持对话框,可以继续创建或退出; 113 | 114 | ### v0.2.2.1 (v0.2.3) (2023-9-4) 115 | 116 | - 修复:设置排序方式后无法获取文档的问题; 117 | 118 | ### v0.2.2 (2023-9-3) 119 | 120 | > 此版本包括设置页面和设置保存逻辑的更改,可能出现设置项保存后不生效,界面内设置项显示和实际执行的不同等问题,如遇到相关问题请反馈。 121 | 122 | - 重构:设置项保存方式和设置项显示方式;**【因此引入一些问题】** 123 | - 全局、默认设置保存位置迁移到/data/storage/listChildDocs,原custom.js配置将自动迁移一次; 124 | - 支持挂件内保存默认设置、全局设置; 125 | - 新增:删除和重置操作:现在可以一键移除所有其他listChildDocs挂件; 126 | - 新增:新文档内模式:创建本地文件夹目录`file://`URL列表; 127 | - 改进:大纲支持选择开始和结束层级; 128 | - 新增:自动分列; 129 | - 改进:”导图“模式部分设置项转为模式独立设置; 130 | - 新增:额外读取/data/storgae/listChildDocs/custom.css,可自定义挂件内样式; 131 | 132 | ### v0.2.2-beta2 (2023-8-24) 133 | 134 | > 此版本包括设置页面和设置保存逻辑的更改,可能出现设置项保存后不生效,界面内设置项显示和实际执行的不同等问题,如遇到相关问题请反馈。 135 | > 136 | > 此版本包括全局、默认设置项保存位置的更改,custom.js自动迁移将保留2个版本后被移除; 137 | 138 | - 重构:设置项保存方式和设置项显示方式;**【因此引入一些问题】** 139 | - 全局、默认设置保存位置迁移到/data/storage/listChildDocs,原custom.js配置将自动迁移一次; 140 | - 支持挂件内保存默认设置、全局设置; 141 | - 新增:删除和重置操作:现在可以一键移除所有其他listChildDocs挂件; 142 | - 新增:新文档内模式:创建本地文件夹目录`file://`URL列表; 143 | - 改进:大纲支持选择开始和结束层级; 144 | - 新增:自动分列; 145 | - 改进:”导图“模式部分设置项转为模式独立设置; 146 | - 新增:额外读取/data/storgae/listChildDocs/custom.css,可自定义挂件内样式; 147 | 148 | ### v0.2.2-beta1 (2023-7-29) 149 | 150 | - 新增:只在鼠标悬停时显示按钮栏;(默认禁用,需修改config.js `mouseoverButtonArea`) 151 | - 改进:出错时避免更改挂件高度; 152 | - 改进:css中emoji字体匹配顺序(`'Twemoji Mozilla', 'Noto Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Segoe UI', 'Apple Color Emoji', 'Noto Color Emoji', 'Android Emoji'`),如果出现黑白Emoji,[请下载安装Twemoji Mozilla字体](https://github.com/mozilla/twemoji-colr/releases); 153 | 154 | ### v0.2.1 (2023-6-4) 155 | 156 | - 改进:支持设置最大子文档数量; 157 | - 改进:支持设定排序方式; 158 | - 开发:尝试兼容旧版Webview; 159 | - 修复:移动设备时间错误导致块id生成错误的问题; 160 | - 修复:指定目标文档时,获取文档大纲错误的问题; 161 | 162 | ### v0.2.0 (2023-3-2) 163 | 164 | - 新增:导图模式、预览方格模式、按日期分组模式;[#33](https://github.com/OpaqueGlass/listChildDocs/issues/33) [#34](https://github.com/OpaqueGlass/listChildDocs/issues/34) 165 | - 新增:挂件中右键子文档链接显示菜单项(导图模式除外); 166 | - 改进:在一些情况下缓存挂件内目录列表;[#35](https://github.com/OpaqueGlass/listChildDocs/issues/35) 167 | - 改进:一些显示效果优化;[#32](https://github.com/OpaqueGlass/listChildDocs/issues/32) 168 | - 改进:文档中目录列表属性写入方式; 169 | - 新增:(代码片段)自动插入助手; 170 | - 新增:搜索并高亮挂件内文档标题; 171 | - 改进:Shift / Alt / Ctrl + Click 子文档链接; 172 | - 新增:(代码片段)快速插入子文档列表; 173 | - 修复:v2.7.6+版本,挂件beta模式未显示浮窗; 174 | - `config.js`全局设置变更: 175 | - 移除:完全移除`showEndDocOutline`; 176 | - 新增:挂件设置批量操作(`overwriteIndependentSettings`等); 177 | - 新增:导图模式Markmap配置项`markmapConfig`; 178 | - 新增:目录列表初始属性`blockInitAttrs`; 179 | - 新增:缓存配置`loadCacheWhileAutoEnable`、`saveCacheWhileAutoEnable`; 180 | 181 | 182 | ### v0.1.0 (2022-12-28) 183 | 184 | - 新增:支持为其他文档、笔记本、所有已打开的笔记本创建子文档目录;[#22](https://github.com/OpaqueGlass/listChildDocs/issues/22) 185 | - 改进:改变文档中自定义emoji插入方式;[#23](https://github.com/OpaqueGlass/listChildDocs/issues/23) 186 | - 新增:(beta)支持从`widgets/custom.js`导入部分设置项;[#24](https://github.com/OpaqueGlass/listChildDocs/issues/24) 187 | - 改进:扩大挂件内子文档链接有效点击区域;[#25](https://github.com/OpaqueGlass/listChildDocs/issues/25) 188 | - 改进:叶子文档大纲转移为挂件独立属性; 189 | - 修复:引用块模式下,`'`未转义的问题; 190 | - 修复:文档中部分情况下分列错误的问题; 191 | - 新增:文档中任务列表模式;【!已知问题:刷新将创建新列表导致任务进度勾选丢失】 192 | - `config.js`文件全局设置变更: 193 | - 新增:`safeModePlus`检测挂件是否在只读模式下,拦截刷新文档内目录的操作; 194 | - 停用(移除):`showEndDocOutline`叶子文档大纲全局设定,此项保留用于设置迁移; 195 | - 新增:`height_2widget_min` `height_2widget_max`自动高度启用时挂件最小、最大高度; 196 | - 新增:`backToParent`在子文档列表中加入返回父文档的链接`../`; 197 | 198 | ### v0.0.9 (2022-11-8) 199 | 200 | - 新增:文档中支持有序列表模式;[#17](https://github.com/OpaqueGlass/listChildDocs/issues/17) 201 | - 支持创建(以全角空格缩进的)多级序号的目录; 202 | - 改进:`默认`模式显示方式(采用``以解决浏览器、iOS设备`默认`模式无法点击的问题);[#18](https://github.com/OpaqueGlass/listChildDocs/issues/18) 203 | - 改进:`挂件beta`模式浮窗触发方式跟随思源设置;[#19](https://github.com/OpaqueGlass/listChildDocs/issues/19) 204 | - 重构:`挂件beta`模式移除过时的方法; 205 | - 改进:网络emoji的判定,为[siyuan#5897](https://github.com/siyuan-note/siyuan/issues/5897)做准备; 206 | - 改进:放宽插入挂件时自动刷新限制:如果在挂件内显示目录,允许插入挂件后立刻进行自动刷新; 207 | 208 | ### v0.0.8 (2022-10-5) 209 | 210 | - 修复:在思源v2.2.1+版本出现错误提示Failed to execute 'observe';[#15](https://github.com/OpaqueGlass/listChildDocs/issues/15) 211 | - 外观:更换按钮图标;[#14](https://github.com/OpaqueGlass/listChildDocs/issues/14) 212 | - 改进:挂件内列出时支持自动更改挂件高度;[#13](https://github.com/OpaqueGlass/listChildDocs/issues/13) 213 | - 新增:挂件内支持有序列表模式;[#17](https://github.com/OpaqueGlass/listChildDocs/issues/17) 214 | 215 | ### v0.0.7 (2022-9-17) 216 | 217 | - 修复:v0.0.6引入的普通emoji插入失败问题([#11](https://github.com/OpaqueGlass/listChildDocs/issues/11)); 218 | 219 | ### v0.0.6 (2022-9-15) 220 | 221 | - 修复:文档图标为自定义emoji时无法列出子文档的问题([#10](https://github.com/OpaqueGlass/listChildDocs/issues/10)); 222 | - 改进:支持写入自定义emoji图片; 223 | - 改进:执行刷新过程中显示提示词; 224 | - 改进:挂件内字号根据思源编辑器字号设定; 225 | 226 | ### v0.0.5 (2022-9-6) 227 | 228 | - 新增:支持列出所在文档大纲内容([#8](https://github.com/OpaqueGlass/listChildDocs/issues/8)); 229 | - 改进:刷新无序列表时重写原无序列表的属性([#6](https://github.com/OpaqueGlass/listChildDocs/issues/6)); 230 | - 修复:页签切换时自动刷新在v2.1.11+版本失效([#5](https://github.com/OpaqueGlass/listChildDocs/issues/5)); 231 | - 修复:`url`模式下,`<>"&`符号未转义的问题([#9](https://github.com/OpaqueGlass/listChildDocs/issues/9)); 232 | - 改进:部分sql查询放宽结果条件([#7](https://github.com/OpaqueGlass/listChildDocs/issues/7)); 233 | - 改进:超级块按照首层节点分列时分列的分列逻辑; 234 | - 改进:创建的超级块结构逻辑; 235 | 236 | ### v0.0.4 (2022-8-30) 237 | 238 | - 新增:支持目录列表分列(分栏)(写入文档时生成超级块)([#2](https://github.com/OpaqueGlass/listChildDocs/issues/2)); 239 | - 修复:安卓端点击链接后返回,显示前一文档目录的问题([#3](https://github.com/OpaqueGlass/listChildDocs/issues/3)); 240 | - 改进:设置项显示方式,设置项支持部分隐藏; 241 | - 改进:`config.js`新增全局设置项:(部分) 242 | - `emojiEnable`关闭/打开文档emoji-icon写入; 243 | - `floatWindowEnable`挂件beta模式下悬浮窗展示控制; 244 | - `showSettingOnStartUp`启动时显示设置项; 245 | 246 | ### v0.0.3 247 | 248 | - 修复:切换深色模式`挂件beta`字体颜色不更改的问题; 249 | - 改进:层级下拉选择框改为输入框(by [Zuoqiu-Yingyi](https://github.com/Zuoqiu-Yingyi) [PR#1](https://github.com/OpaqueGlass/listChildDocs/pull/1)); 250 | - 改进:更新时间不再显示日期; 251 | - 修复:`挂件beta`下鼠标悬停时的一些显示问题; 252 | 253 | 254 | ### v0.0.2 255 | 256 | - 修复:在重新打开文档后,重复创建块的问题; 257 | 258 | ### v0.0.1 259 | 260 | 从这里开始。 -------------------------------------------------------------------------------- /README_en_US.md: -------------------------------------------------------------------------------- 1 | > [Translating...] This document was translated by Google Translate. 2 | 3 | > A widget for creating and updating sub-document directory lists in [Siyuan Notes](https://github.com/siyuan-note/siyuan). 4 | 5 | > Sorry, this widget has limited support for English. 6 | 7 | - Create a subdocument directory listing of the current document; 8 | - create the current document outline list (level set to `0`); 9 | 10 | - Subdocument directory listing form (selected in `mode`): 11 | - Create a directory in the widget or in the document; 12 | - ordered or unordered list; 13 | - (in the document) `siyuan://` URL or quote block; 14 | - (in the widget) Markmap map; 15 | 16 | - Automatically refresh subdocument directory listings in several specific cases (do not refresh directory in document in safe mode):\* 17 | - The widget is loaded (for example: click on the document tree to open the document); 18 | - Click the document tab; (default only for windows) 19 | 20 | > The widget was initially released in August 2022. Thank you for your support! 21 | > 22 | > As of now, the widget have entered the maintenance phase and no new features will be added. If you encounter any bugs, please [provide feedback](https://github.com/OpaqueGlass/listChildDocs/issues). Thank you. 23 | 24 | ### Quick Start 25 | 26 | > Please note that `listChildDocs` is a widget, not a plugin. Widgets exist as content blocks in the document and need to be manually inserted into the document (via the `/` menu in the editor -> Widget). Please also refer to: Siyuan help guide. 27 | 28 | - Double-click refresh button: Save settings; 29 | - Hover over the "i" icon next to buttons or settings items to display tooltips. 30 | - About modes: You can switch to the corresponding mode and then click refresh to give it a try. 31 | - It is recommended to read the "Note" section below.() 32 | - Frequently Asked Questions: 33 | - Why doesn't the directory list in the document automatically refresh? 34 | 35 | The safe mode is enabled by default. Please make sure that after disabling synchronization, you can also turn off the secure mode. 36 | - Shortcuts (When focus is on widget) 37 | - `Ctrl+S` Show or hide setting panel. 38 | - `F5` Refresh 39 | - `Ctrl+F` Show or hide search dialog. 40 | 41 | 42 | ### Custom Description 43 | 44 | After the v0.2.2 version, the way the widget saves the setting items has been modified. The widget will save the data to `${siyuan_workspace}/data/storage/listChildDocs` folder by default. 45 | 46 | The data under this folder includes: 47 | 48 | - `data` folder, which saves the widget settings for the document. Generally speaking, the listChildDocs inserted by the plugin will create a configuration file for the document; 49 | - `schema` folder, the default template configuration, currently invalid; 50 | - `default.json` default settings when plug-in is inserted; 51 | - `global.json` is the global configuration file, which needs to be created after manually saving the global settings; 52 | - `custom.css` custom style file, not automatically created; 53 | 54 | #### Default and global settings 55 | 56 | If it is the first time to upgrade to v0.2.2 and above, please manually save the default settings and global settings in the widget. (Theoretically, the widget will migrate the settings saved in the original `custom.js` once when it is loaded for the first time.) 57 | 58 | After saving, you can open `workspace/data/storage/listChildDocs/global.json` to manually make changes to the configuration; 59 | 60 | For the allowed configuration items, please refer to `defaultGlobalConfig` in `Pendant location/src/ConfigManager.js`. You can also directly change this file, but if it is different from global.json, global.json will prevail. 61 | 62 | #### Custom Styles 63 | 64 | If you are not satisfied with the default style of the widget, you can modify it by yourself and save the CSS in the `workspace/data/storage/listChildDocs/custom.css` file. 65 | 66 | ### Note 67 | 68 | > Due to the limited ability of developers, the widget also has the following problems. Must read before use. 69 | 70 | - When directly writing a subdocument directory listing **into a document**:` 71 | - Please avoid refreshing the document list too quickly; 72 | - If you want to multi-device sync document, and the document where the widget is located needs to write other content, **do not use auto-refresh**[^1]; 73 | - Every time you refresh, the list will be fully updated (even if the subdocument has not changed, the entire content of the list will be updated); 74 | - **If the synchronization is not completed, please do not click the refresh button** (refresh on the old document before multi-terminal synchronization may cause synchronization overwrite)[^1]: 75 | - Clicking the refresh button will update the directory list in the document or update the widget directory list cache, and the document editing time will be updated; 76 | - Double-clicking the refresh button will save the settings (set widget properties), and the document editing time will be updated; 77 | - The method of automatic refresh when switching tabs is a bit metaphysical, and may not be available in future version updates; 78 | - About rewriting after super block attribute refresh: 79 | - `superBlockBeta` should be set to `true` (already set it in default); 80 | - If it is a super block after refreshing, the attribute will be written into the direct unordered list sub-block of the super block and the super block itself; 81 | - If it is a super block before refreshing, it will randomly inherit the attributes of an unordered list sub-block; 82 | - If you want to delete the attribute, it is recommended to directly delete the super block and reset it; 83 | - About writing custom emoji pictures: 84 | - Please avoid including special symbols in the image path, such as `()%&`, if included, the actual effect cannot be determined; 85 | - Internet emoji is not supported for now; 86 | 87 | 88 | ## Reference & Thanks 89 | 90 | This widget uses/references the following projects: 91 | 92 | | Developer | Project | Open Source Agreement | Detail | 93 | | -------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------- | -------------------------------- | 94 | | [leolee9086](https://github.com/leolee9086) | [cc-template](https://github.com/leolee9086/cc-template) | [Mulan Permissive License, Version 2](https://github.com/leolee9086/cc-template/blob/main/LICENSE) | Display the "reference block" in the widget | 95 | | [InEase](https://github.com/InEase) | [Note Map](https://github.com/InEase/SiYuan-Xmind) | N/A | API Usage | 96 | | [Zuoqiu-Yingyi](https://github.com/Zuoqiu-Yingyi) | [widget-query](https://github.com/Zuoqiu-Yingyi/widget-query) | AGPL-3.0 | From custom. js import custom settings | 97 | | | [Trilium](https://github.com/zadam/trilium) / note-list-widget | | Preview grid mode css style, and functional design | 98 | 99 | The following developer participated in code contributions: 100 | 101 | - [Zuoqiu-Yingyi](https://github.com/Zuoqiu-Yingyi); 102 | 103 | (For details, see [Contributor (Developer) List](https://github.com/OpaqueGlass/listChildDocs/graphs/contributors)) 104 | 105 | 106 | ### Dependencies 107 | 108 | 1. [jQuery](https://jquery.com/) (In this project, page elements are selected through jQuery); 109 | 110 | ``` 111 | jQuery JavaScript Library v3.6.0 https://jquery.com/ 112 | Copyright OpenJS Foundation and other contributors 113 | Released under the MIT license https://jquery.org/license 114 | ``` 115 | 116 | 2. [markmap](https://markmap.js.org/); 117 | 118 | ``` 119 | markmap-lib v0.18.12 | MIT License 120 | markmap-view v0.18.12 | MIT License 121 | https://github.com/markmap/markmap 122 | https://markmap.js.org/ 123 | ``` 124 | 125 | 3. [d3.js](https://d3js.org); 126 | 127 | ``` 128 | BSD-3-Clause https://opensource.org/licenses/BSD-3-Clause 129 | https://d3js.org v6.7.0 Copyright 2021 Mike Bostock 130 | ``` 131 | 132 | 4. [day.js](https://day.js.org/); 133 | 134 | ``` 135 | Day.js is licensed under a MIT License. 136 | https://github.com/iamkun/dayjs/ 137 | https://day.js.org/ 138 | ``` 139 | 140 | 5. [artDialog](https://github.com/aui/artDialog); 141 | 142 | ``` 143 | Free and open source, based on the LGPL-3.0 license. 144 | https://github.com/aui/artDialog 145 | aui.github.io/artDialog/ 146 | ``` 147 | 148 | 6. [layui](https://gitee.com/layui/layui) 149 | 150 | ``` 151 | Layui is released under the MIT license. 152 | For other relevant agreements, please refer to the "Disclaimer" 153 | https://gitee.com/layui/layui/blob/main/DISCLAIMER.md. 154 | ``` 155 | 156 | 157 | ### icon 158 | 159 | 1. [Refresh button icon](https://www.iconfinder.com/icons/5402417/refresh_rotate_sync_update_reload_repeat_icon), author: [amoghdesign](https://www.iconfinder.com/amoghdesign), license agreement: [CC3.0 BY-NC](http://creativecommons.org/licenses/by-nc/3.0/); 160 | 161 | 2. [Setting button icon](https://lucide.dev/?search=setting), [Lucide](https://github.com/lucide-icons/lucide), [ISC License](https://lucide.dev/license); 162 | 163 | 3. [Search button icon](https://lucide.dev/?search=search), [Lucide](https://github.com/lucide-icons/lucide), [ISC License](https://lucide.dev/license). 164 | 165 | [^1]: Clicking the refresh button will update the directory list, directory cache or save settings in the widget, and the current device document editing time will also be updated. If the current device is not synced, the current device's "old" documents will overwrite the cloud content, causing edits from other devices to be lost. -------------------------------------------------------------------------------- /static/listChildDocs copy.css: -------------------------------------------------------------------------------- 1 | .app button { 2 | border: 0px; 3 | /* font-weight: 300; */ 4 | width: 25px; 5 | height: 25px; 6 | border-radius: 3px; 7 | box-sizing: border-box; 8 | /* color: white; */ 9 | padding: 0px; 10 | /* text-align: center; 11 | text-decoration: none; */ 12 | display: inline-block; 13 | /* font-size: 16px; */ 14 | /* min-width: 15px; */ 15 | background-color: #f0f0f022; 16 | cursor: pointer; 17 | } 18 | 19 | /* max-device-width 已过时,但替换为max-width可能导致电脑也变化*/ 20 | @media screen and (max-device-width: 768px) { 21 | .app button { 22 | margin-right: 1.7em; 23 | } 24 | } 25 | 26 | .app select, 27 | .app input, 28 | .ui-dialog input { 29 | border: 1px solid sandybrown; 30 | font-weight: 300; 31 | background-size: auto 1.5em; 32 | background-repeat: no-repeat; 33 | background-position: center; 34 | width: 3em; 35 | height: 1.5em; 36 | border-radius: 0.3em; 37 | box-sizing: border-box; 38 | /* color: white; */ 39 | /* padding: 15px 32px; */ 40 | text-align: center; 41 | text-decoration: none; 42 | display: inline-block; 43 | font-size: 16px; 44 | min-width: 15px; 45 | background-color: #f0f0f022; 46 | /* background-color:darkslategray; 47 | color: white; */ 48 | } 49 | 50 | /* 聚焦选中 */ 51 | .app select:focus-visible, 52 | .app input:focus-visible, 53 | .ui-dialog input:focus-visible { 54 | outline: 0px; 55 | border-width: 2px; 56 | } 57 | 58 | .app input[type="number"], .app input[type="text"]{ 59 | cursor: text; 60 | } 61 | 62 | .ui-dialog input { 63 | width: 100%; 64 | cursor: text; 65 | text-align: left; 66 | } 67 | 68 | .app select:disabled, 69 | .app input:disabled { 70 | border: 1px solid rgba(118, 118, 118, 0.3); 71 | color: rgba(84, 84, 84, 0.3); 72 | /* background-color:darkslategray 73 | color: white; */ 74 | } 75 | 76 | .app input[type="checkbox"] { 77 | width: 1em; 78 | height: 1em; 79 | margin-top: 0.25em; 80 | cursor: pointer; 81 | /* box-sizing: border-box; */ 82 | /* text-align: center; 83 | display: inline-block; */ 84 | } 85 | 86 | .upperbar>* { 87 | margin-left: 0.5em; 88 | } 89 | 90 | #printMode { 91 | width: 5em; 92 | } 93 | 94 | #sortBy { 95 | width: 9em; 96 | } 97 | 98 | #innerSetting select:disabled, #innerSetting input:disabled { 99 | cursor: not-allowed; 100 | } 101 | 102 | /* #savedepth { 103 | width: 3em; 104 | height: 1.5em; 105 | border: 1px solid sandybrown; 106 | margin-left: 0.2em; 107 | display: inline-block; 108 | border-radius: 0.3em; 109 | color: #4CB0F9; 110 | } 111 | 112 | #listcolumn { 113 | border-style: dashed; 114 | border-color: purple; 115 | } */ 116 | 117 | /* #listcolumn:focus{ 118 | border: 2px dashed purple 119 | } */ 120 | /* select:hover, 121 | button:hover, 122 | input:hover { 123 | border-width: 2px; 124 | } */ 125 | 126 | #updateTime { 127 | margin-left: 0.5em; 128 | /* color: darkgray; */ 129 | } 130 | 131 | /* 深夜模式下普通文本颜色 */ 132 | .ordinaryText_dark, [data-darkmode] .upperbardiv span, [data-darkmode] #linksContainer, [data-darkmode] .mode12_date_text{ 133 | color: #C9D1D9; 134 | } 135 | 136 | .upperbar { 137 | float: left; 138 | width: 100%; 139 | display: flex; 140 | text-align: center; 141 | margin-top: 5px; 142 | } 143 | 144 | .upperbardiv { 145 | display: block; 146 | } 147 | 148 | /* 按钮夜间样式 */ 149 | .button_dark, [data-darkmode] .upperbardiv input, [data-darkmode] .upperbardiv select, .dark_dialog input { 150 | background-color: darkslategray; 151 | color: white; 152 | } 153 | 154 | .linksList { 155 | margin-top: 0px; 156 | } 157 | 158 | .noListStyle { 159 | list-style-type: none; 160 | padding-inline-start: 0%; 161 | } 162 | 163 | /* 无序列表单项 */ 164 | .linksListItem { 165 | padding: 4px 0px; 166 | } 167 | 168 | /* 列表单项(深色)*/ 169 | [data-darkmode] .linksListItem { 170 | padding: 4px 0px; 171 | color: #C9D1D9; 172 | } 173 | 174 | /* 挂件内自定义emoji样式 */ 175 | .iconpic { 176 | width: 18px; 177 | height: 18px; 178 | vertical-align: bottom; 179 | } 180 | 181 | /* 子文档区域 */ 182 | #refContainer { 183 | cursor: default; 184 | } 185 | 186 | /* 链接文字样式 */ 187 | .childDocLinks, 188 | .refLinks { 189 | /* color: black; */ 190 | padding: 0 2px; 191 | text-decoration: none; 192 | border-bottom: 1px dashed rgba(0, 0, 0, .1); 193 | } 194 | 195 | /* 暗黑模式下,链接文字样式 */ 196 | [data-darkmode] .linksList { 197 | color: #C9D1D9; 198 | } 199 | 200 | /* 链接文字悬停样式 */ 201 | .linkTextHoverHightLight:hover{ 202 | color: #1186d9; 203 | border-bottom: 1px dashed #4CB0F9; 204 | } 205 | 206 | /* 扩大点击范围为整行时,悬停样式 */ 207 | .itemHoverHighLight:hover { 208 | background-color: #1186d91f; 209 | color: #1186d9; 210 | } 211 | 212 | /* 错误提示文字样式 */ 213 | .errorinfo { 214 | color: red; 215 | text-align: center; 216 | } 217 | 218 | /* 链接区,所有挂件内模式共用 */ 219 | #linksContainer { 220 | column-fill: balance; 221 | clear: both; 222 | } 223 | 224 | /* 属于此类的元素,将被点击处理 */ 225 | .handle-ref-click { 226 | cursor: pointer; 227 | } 228 | 229 | /********************************************** 预览方格模式内css */ 230 | /* 子文档方块区 */ 231 | .mode11-note-box { 232 | max-height: 250px; 233 | overflow: auto; 234 | border-radius: 10px; 235 | padding: 10px 15px 15px 8px; 236 | margin: 5px 5px 5px 0; 237 | flex-direction: column; 238 | flex: 1 0 250px; 239 | display: flex; 240 | background-color: #ebebeb86; 241 | } 242 | /* 子文档方块外围div */ 243 | .mode11-box { 244 | overflow: auto; 245 | flex-wrap: wrap; 246 | display: flex; 247 | } 248 | 249 | /* 子文档方块内的文档标题 */ 250 | .mode11-title { 251 | margin: 10px 0 10px 0; 252 | } 253 | 254 | /* 二级文档标题所在的段落 */ 255 | .mode11-child-p-container > p { 256 | margin: 0 0 0 7px; 257 | width: 95%; 258 | white-space: normal; 259 | font-size: 0.9; 260 | } 261 | /* 预览内链接内容 */ 262 | [data-darkmode] .mode11-doc-content a { 263 | color: #7aa7d4 !important; 264 | } 265 | 266 | /* 子文档内容节选 */ 267 | .mode11-doc-content { 268 | margin: 0 0 0 7px; 269 | width: 95%; 270 | white-space: normal; 271 | overflow: hidden; 272 | } 273 | 274 | /* .mode11-doc-content p { 275 | display: inline; 276 | } */ 277 | 278 | /* mode11 图片显示*/ 279 | .mode11-doc-content .img img { 280 | max-width: 100%; 281 | display: inline-block; 282 | } 283 | 284 | /* 子文档预览内容 */ 285 | .mode11-doc-content [id][updated] { 286 | font-size: 16px; 287 | margin: 5px 0 0 0; 288 | } 289 | 290 | .mode11-note-box:hover { 291 | background-color: #bfbfbf86; 292 | } 293 | 294 | /* 任务列表样式调整 */ 295 | .mode11-doc-content ul:has(> .protyle-task) { 296 | list-style-type: none; 297 | padding-left: 16px; 298 | } 299 | 300 | .mode11-doc-content > ul:has(> .protyle-task) { 301 | padding-left: 0px; 302 | } 303 | 304 | .mode11-doc-content .protyle-task input ~ p { 305 | display: inline-block; 306 | } 307 | 308 | /* mode11 深色模式方块*/ 309 | [data-darkmode] .mode11-note-box { 310 | background-color: #efefef15; 311 | color: #C9D1D9; 312 | } 313 | /* mode11 深色模式方块悬停*/ 314 | [data-darkmode] .mode11-note-box:hover { 315 | background-color: #efefef25; 316 | } 317 | 318 | 319 | /********************************************* 挂件思维导图样式 */ 320 | /* 思维导图链接样式 */ 321 | .markmap_a { 322 | color: black !important; 323 | text-decoration: none; 324 | border-bottom: 1px dashed rgba(0, 0, 0, .1); 325 | /* white-space: nowrap; */ 326 | } 327 | /* 思维导图链接鼠标悬停时样式 */ 328 | .markmap_a:hover { 329 | color: #1186d9 !important; 330 | border-bottom: 1px dashed #4CB0F9 !important; 331 | /* white-space: nowrap; */ 332 | } 333 | 334 | /* 思维导图普通文字深色样式 */ 335 | [data-darkmode] .markmap { 336 | color: #C9D1D9 !important; 337 | } 338 | /* 思维导图链接深色样式 */ 339 | [data-darkmode] .markmap_a { 340 | color: #C9D1D9 !important; 341 | text-decoration: none !important; 342 | /* white-space: nowrap; */ 343 | } 344 | 345 | /* 思维导图悬停样式*/ 346 | [data-darkmode] .markmap_a:hover{ 347 | color: #1186d9 !important; 348 | border-bottom: 1px dashed #4CB0F9 !important; 349 | } 350 | 351 | /***************** 模式12 */ 352 | .mode12_date_text { 353 | font-weight: bold; 354 | } 355 | 356 | 357 | .emojitext { 358 | font-family: 'Twemoji Mozilla', 'Noto Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Segoe UI', 'Apple Color Emoji', 'Noto Color Emoji', 'Android Emoji'; 359 | } 360 | /*****************************对话框*/ 361 | .ui-dialog.dark_dialog { 362 | background-color: #101010; 363 | color: #C9D1D9; 364 | box-shadow: none !important; 365 | } 366 | 367 | .ui-dialog.dark_dialog button { 368 | color: #C9D1D9; 369 | background-color: inherit; 370 | } 371 | 372 | .ui-dialog.dark_dialog button:hover { 373 | color: #C9D1D9; 374 | background-color: #00000055; 375 | } 376 | 377 | .ui-dialog button[i-id="Delete"], .ui-dialog button[i-id="删除"], .ui-dialog.delete_dialog button[i-id=ok] { 378 | background-color: #dd041e99; 379 | color: whitesmoke; 380 | } 381 | 382 | .ui-dialog button[i-id="Delete"]:hover, .ui-dialog button[i-id="删除"]:hover, .ui-dialog.delete_dialog button[i-id=ok]:hover { 383 | background-color: #dd041ecc; 384 | color: whitesmoke; 385 | } 386 | 387 | /* 内容占据全部宽度 */ 388 | .ui-dialog-content[i="content"] { 389 | width: 100%; 390 | } 391 | 392 | /* 检索高亮结果 */ 393 | .search_highlight { 394 | background-color: #efc52f5e; 395 | } 396 | 397 | [data-darkmode] .search_highlight { 398 | background-color: #85d0a35e; 399 | } 400 | 401 | body { 402 | padding: 0; 403 | border: 0; 404 | margin: 0; 405 | width: 100%; 406 | height: 100%; 407 | align-items: center; 408 | justify-content: center; 409 | } 410 | 411 | .app { 412 | width: 100%; 413 | } 414 | 415 | .outerSetting-hide { 416 | opacity: 0; 417 | transition: opacity 0.1s ease; 418 | /* max-height: px; */ 419 | margin-top: 0px; 420 | margin-bottom: 5px; 421 | } 422 | 423 | .outerSetting-show { 424 | opacity: 1; 425 | /* max-height: none; */ 426 | /* margin-top: inherit; 427 | margin-bottom: inherit; */ 428 | } -------------------------------------------------------------------------------- /src/common.js: -------------------------------------------------------------------------------- 1 | /** 2 | * common.js 一些可能常用的方法 3 | */ 4 | 5 | /** 6 | * 检查窗口状况,防止在历史预览页面刷新更改文档 7 | * @param thisDocId 待判断的文档id 8 | * @param config 检查项 9 | * @param thisWidgetId 待判断的widgetId(需config中启用widgetMode 10 | * @return {boolean} true: 当前情况安全,允许执行刷新操作 11 | * UNSTABLE: !此方法大量依赖jQuery定位页面元素 12 | */ 13 | export function isSafelyUpdate(thisDocId, customConfig = null, thisWidgetId = "") { 14 | let config = { 15 | "history": true, // 检查历史页面 16 | "targetDoc": true, // 检查目标文档是否已经打开,并且未启用只读模式 17 | "anyDoc": true, // 检查任意文档是否存在,并且未启用只读模式 18 | "allowWhenError": true, // 发生错误时,默认放行或拦截 19 | "widgetMode": false 20 | } 21 | if (config != null) { 22 | for (let key in customConfig) { 23 | if (key in config) { 24 | config[key] = customConfig[key]; 25 | }else{ 26 | warnPush("传入的自定义检查项配置部分不存在", key); 27 | } 28 | } 29 | } 30 | // logPush($(window.top.document).find(".b3-dialog--open #historyContainer")); // 防止历史预览界面刷新 31 | try{ 32 | // 判定历史预览页面 history 33 | // $(window.top.document).find(".b3-dialog--open #historyContainer").length >= 1 34 | if (window.top.document.querySelectorAll(".b3-dialog--open #historyContainer").length >= 1 && config.history) { 35 | logPush("安全刷新:在历史页面"); 36 | return false; 37 | } 38 | // 旧方法:存在多个编辑窗口只判断第一个的问题;保留用于判断界面是否大改 39 | // if ($(window.top.document).find(".protyle-wysiwyg").attr("contenteditable") == "false") { 40 | // return false; 41 | // } 42 | // $(window.top.document).find(".protyle-wysiwyg").attr("contenteditable") == undefined 43 | let anyDocEditable = window.top.document.querySelector(".protyle-wysiwyg").getAttribute("contenteditable"); 44 | if (anyDocEditable == undefined || anyDocEditable == null) { 45 | warnPush("界面更新,请@开发者重新适配"); 46 | return false; 47 | } 48 | // if (anyDocEditable == "false" && config.anyDoc) { 49 | // warnPush("存在一个文档为只读状态"); 50 | // return false; 51 | // } 52 | // 判定文档已打开&只读模式【挂件所在文档在窗口中,且页面为编辑状态,则放行】 53 | // 只读模式判定警告:若在闪卡页面,且后台开启了当前文档(编辑模式),只读不会拦截 54 | // logPush($(window.top.document).find(`.protyle-background[data-node-id="${thisDocId}"] ~ .protyle-wysiwyg`).attr("contenteditable") == "false"); 55 | // logPush($(window.top.document).find(`.protyle-background[data-node-id="${thisDocId}"] ~ .protyle-wysiwyg`)); 56 | // $(window.top.document).find(`.protyle-background[data-node-id="${thisDocId}"] ~ .protyle-wysiwyg`) 57 | let candidateThisDocEditor = window.top.document.querySelector(`.protyle-background[data-node-id="${thisDocId}"]`); 58 | let candidateThisDocPopup = window.top.document.querySelector(`.block__popover[data-oid="${thisDocId}"] .protyle-wysiwyg`); 59 | debugPush("安全刷新:candidateEditor或Popup判定", candidateThisDocEditor, candidateThisDocPopup, thisDocId); 60 | if ((!isValidStr(candidateThisDocEditor) || candidateThisDocEditor.length <= 0) 61 | && (!isValidStr(candidateThisDocPopup) || candidateThisDocPopup.length <= 0)) { 62 | if (config.widgetMode && config.targetDoc) { 63 | if (window.top.document.querySelectorAll(`.block__popover[data-oid] [data-node-id="${thisWidgetId}"]`).length <= 0) { 64 | warnPush("未在窗口中找到挂件所在的文档(文档所在文档编辑器可能未打开),为防止后台更新,此操作已拦截。"); 65 | return false; 66 | } 67 | }else if (config.targetDoc) { 68 | warnPush("未在窗口中找到目标文档(文档所在文档编辑器可能未打开),为防止后台更新,此操作已拦截。"); 69 | return false; 70 | } 71 | } 72 | if (candidateThisDocEditor == null && candidateThisDocPopup == null) { 73 | warnPush("找不到挂件所在文档"); 74 | } 75 | // 判定只读模式 76 | // $(window.top.document).find(`.protyle-background[data-node-id="${thisDocId}"] ~ .protyle-wysiwyg`).attr("contenteditable") == "false" 77 | if (!isMobile() && config.targetDoc && (candidateThisDocEditor == null ? candidateThisDocPopup : candidateThisDocEditor).getAttribute("contenteditable") == "false") { 78 | logPush("安全刷新:candidateEditor或Popup判定在只读模式", candidateThisDocEditor, candidateThisDocPopup, thisDocId); 79 | return false; 80 | } 81 | }catch (err) { 82 | warnPush(`安全检查时出现错误,已${config.allowWhenError?"放行":"禁止"}刷新操作,错误为:`, err); 83 | return config.allowWhenError; 84 | } 85 | 86 | return true; 87 | } 88 | 89 | 90 | /** 91 | * 判断字符串是否为空 92 | * @param {*} s 93 | * @returns 非空字符串true,空字符串false 94 | */ 95 | export function isValidStr(s){ 96 | if (s == undefined || s == null || s === '') { 97 | return false; 98 | } 99 | return true; 100 | } 101 | 102 | export function isInvalidValue(s) { 103 | if (s === undefined || s === null) { 104 | return true; 105 | } 106 | return false; 107 | } 108 | 109 | /** 110 | * 获取当前更新时间字符串 111 | * @returns 112 | */ 113 | export function getUpdateString(){ 114 | let nowDate = new Date(); 115 | let hours = nowDate.getHours(); 116 | let minutes = nowDate.getMinutes(); 117 | let seconds = nowDate.getSeconds(); 118 | hours = formatTime(hours); 119 | minutes = formatTime(minutes); 120 | seconds = formatTime(seconds); 121 | let timeStr = nowDate.toJSON().replace(new RegExp("-", "g"),"").substring(0, 8) + hours + minutes + seconds; 122 | return timeStr; 123 | function formatTime(num) { 124 | return num < 10 ? '0' + num : num; 125 | } 126 | } 127 | 128 | /** 129 | * 生成一个随机的块id 130 | * @returns 131 | */ 132 | export function generateBlockId(){ 133 | if (window?.Lute?.NewNodeID) { 134 | return window.Lute.NewNodeID(); 135 | } 136 | let timeStr = getUpdateString(); 137 | let alphabet = new Array(); 138 | for (let i = 48; i <= 57; i++) alphabet.push(String.fromCharCode(i)); 139 | for (let i = 97; i <= 122; i++) alphabet.push(String.fromCharCode(i)); 140 | let randomStr = ""; 141 | for (let i = 0; i < 7; i++){ 142 | randomStr += alphabet[Math.floor(Math.random() * alphabet.length)]; 143 | } 144 | let result = timeStr + "-" + randomStr; 145 | return result; 146 | } 147 | 148 | /** 149 | * 转换块属性对象为{: }格式IAL字符串 150 | * @param {*} attrData 其属性值应当为String类型 151 | * @returns 152 | */ 153 | export function transfromAttrToIAL(attrData) { 154 | let result = "{:"; 155 | for (let key in attrData) { 156 | result += ` ${key}=\"${escapeSpecialChars(attrData[key])}\"`; 157 | } 158 | result += "}"; 159 | if (result == "{:}") return null; 160 | return result; 161 | } 162 | 163 | function escapeSpecialChars(str) { 164 | const escapeMap = { 165 | '"': '"', 166 | '{': '{', 167 | '}': '}', 168 | '<': '<', 169 | '>': '>', 170 | '&': '&', 171 | }; 172 | 173 | return str.replace(/[\\"{}<>]/g, (char) => escapeMap[char]); 174 | } 175 | 176 | export function pushDebug(text) { 177 | let areaElem = document.getElementById("debugArea"); 178 | areaElem.value = areaElem.value + `\n${new Date().toLocaleTimeString()}` + text; 179 | areaElem.scrollTop = areaElem.scrollHeight; 180 | } 181 | 182 | 183 | export function isFileNameIllegal(str) { 184 | let regex = /[\\/:*?"<>|]/; 185 | return regex.test(str); 186 | } 187 | 188 | export function getUrlParams() { 189 | const queryString = window.location.search; 190 | // https://developer.mozilla.org/zh-CN/docs/Web/API/URLSearchParams 191 | const urlParams = new URLSearchParams(queryString); 192 | let result = {}; 193 | for (var pair of urlParams.entries()) { 194 | result[pair[0]] = pair[1]; 195 | } 196 | return result; 197 | } 198 | 199 | let cacheIsMacOs = undefined; 200 | export function isEventCtrlKey(event) { 201 | let platform = window.top.siyuan.config.system.os ?? navigator.platform ?? "ERROR"; 202 | platform = platform.toUpperCase(); 203 | let isMacOS = cacheIsMacOs; 204 | if (cacheIsMacOs == undefined) { 205 | for (let platformName of ["DARWIN", "MAC", "IPAD", "IPHONE", "IOS"]) { 206 | if (platform.includes(platformName)) { 207 | isMacOS = true; 208 | break; 209 | } 210 | } 211 | cacheIsMacOs = isMacOS; 212 | } 213 | 214 | if (isMacOS) { 215 | return event.metaKey; 216 | } 217 | return event.ctrlKey; 218 | } 219 | 220 | 221 | // debug push 222 | let g_DEBUG = 2; 223 | const g_NAME = "lcd"; 224 | const g_FULLNAME = "列出子文档"; 225 | 226 | /* 227 | LEVEL 0 忽略所有 228 | LEVEL 1 仅Error 229 | LEVEL 2 Err + Warn 230 | LEVEL 3 Err + Warn + Info 231 | LEVEL 4 Err + Warn + Info + Log 232 | LEVEL 5 Err + Warn + Info + Log + Debug 233 | */ 234 | export function commonPushCheck() { 235 | if (window.top["OpaqueGlassDebugV2"] == undefined || window.top["OpaqueGlassDebugV2"][g_NAME] == undefined) { 236 | return g_DEBUG; 237 | } 238 | return window.top["OpaqueGlassDebugV2"][g_NAME]; 239 | } 240 | 241 | export function debugPush(str, ...args) { 242 | pushDebug(str); 243 | if (commonPushCheck() >= 5) { 244 | console.debug(`${g_FULLNAME}[D] ${new Date().toLocaleString()} ${str}`, ...args); 245 | } 246 | } 247 | 248 | export function logPush(str, ...args) { 249 | pushDebug(str); 250 | if (commonPushCheck() >= 4) { 251 | console.log(`${g_FULLNAME}[L] ${new Date().toLocaleString()} ${str}`, ...args); 252 | } 253 | } 254 | 255 | export function errorPush(str, ... args) { 256 | if (commonPushCheck() >= 1) { 257 | console.error(`${g_FULLNAME}[E] ${new Date().toLocaleString()} ${str}`, ...args); 258 | } 259 | } 260 | 261 | export function warnPush(str, ... args) { 262 | if (commonPushCheck() >= 2) { 263 | console.warn(`${g_FULLNAME}[W] ${new Date().toLocaleString()} ${str}`, ...args); 264 | } 265 | } 266 | 267 | export function checkWorkEnvironment() { 268 | if (window.siyuan == null && window.top.siyuan == null) { 269 | if (window.frameElement) { 270 | return WORK_ENVIRONMENT.IFRAME; 271 | } 272 | return WORK_ENVIRONMENT.SINGLE; 273 | } 274 | let widgetId = ""; 275 | 276 | try { 277 | let widgetNodeDataset = window.frameElement.parentElement.parentElement.dataset; 278 | let widgetParent = window.frameElement.parentElement; 279 | if (widgetParent.getAttribute("id")) { 280 | // 实际上应该是预览模式 281 | return WORK_ENVIRONMENT.WIDGET; 282 | } 283 | let widgetNodeDom = window.frameElement.parentElement.parentElement; 284 | if (isValidStr(widgetNodeDataset["nodeId"]) || isValidStr(widgetNodeDataset["id"])) { 285 | widgetId = widgetNodeDataset["nodeId"]; 286 | if (widgetNodeDataset["workEnviroment"]) { 287 | return widgetNodeDataset["workEnviroment"]; 288 | } else { 289 | if (widgetNodeDataset["type"] == undefined) { 290 | return WORK_ENVIRONMENT.PLUGIN; 291 | } else { 292 | return WORK_ENVIRONMENT.WIDGET; 293 | } 294 | } 295 | } else { 296 | return WORK_ENVIRONMENT.PLUGIN; 297 | } 298 | }catch{ 299 | 300 | } 301 | return WORK_ENVIRONMENT.UNKNOWN; 302 | } 303 | 304 | export const WORK_ENVIRONMENT = { 305 | "UNKNOWN": -1, // 未知 306 | "SINGLE": 0, // 在单独页面 307 | "PLUGIN": 1, // 在思源页面,有window.siyuan上下文 308 | "WIDGET": 2, // 在挂件中,有挂件上下文 309 | "IFRAME":3, // 在页面中嵌入,但无window.siyuan上下文 310 | } 311 | 312 | export function isMobile() { 313 | return window.top.document.getElementById("sidebar") ? true : false; 314 | }; -------------------------------------------------------------------------------- /static/listChildDocs.css: -------------------------------------------------------------------------------- 1 | .upperbar button { 2 | border: 0px; 3 | /* font-weight: 300; */ 4 | width: 25px; 5 | height: 25px; 6 | border-radius: 3px; 7 | box-sizing: border-box; 8 | /* color: white; */ 9 | padding: 0px; 10 | /* text-align: center; 11 | text-decoration: none; */ 12 | display: inline-block; 13 | /* font-size: 16px; */ 14 | /* min-width: 15px; */ 15 | background-color: #f0f0f022; 16 | cursor: pointer; 17 | } 18 | 19 | button:disabled { 20 | cursor:not-allowed; 21 | } 22 | 23 | /* max-device-width 已过时,但替换为max-width可能导致电脑也变化*/ 24 | @media screen and (max-device-width: 768px) { 25 | .app button { 26 | margin-right: 1.7em; 27 | } 28 | } 29 | 30 | .ui-dialog input { 31 | border: 1px solid sandybrown; 32 | font-weight: 300; 33 | background-size: auto 1.5em; 34 | background-repeat: no-repeat; 35 | background-position: center; 36 | width: 3em; 37 | height: 1.5em; 38 | border-radius: 0.3em; 39 | box-sizing: border-box; 40 | /* color: white; */ 41 | /* padding: 15px 32px; */ 42 | text-align: center; 43 | text-decoration: none; 44 | display: inline-block; 45 | font-size: 16px; 46 | min-width: 15px; 47 | background-color: #f0f0f022; 48 | /* background-color:darkslategray; 49 | color: white; */ 50 | } 51 | 52 | /* 聚焦选中 */ 53 | .ui-dialog input:focus-visible { 54 | outline: 0px; 55 | border-width: 2px; 56 | } 57 | 58 | .ui-dialog input { 59 | width: 100%; 60 | cursor: text; 61 | text-align: left; 62 | } 63 | 64 | .upperbar>* { 65 | margin-left: 0.5em; 66 | } 67 | 68 | #printMode { 69 | width: 5em; 70 | } 71 | 72 | #sortBy { 73 | width: 9em; 74 | } 75 | 76 | #innerSetting select:disabled, #innerSetting input:disabled { 77 | cursor: not-allowed; 78 | } 79 | 80 | /* #savedepth { 81 | width: 3em; 82 | height: 1.5em; 83 | border: 1px solid sandybrown; 84 | margin-left: 0.2em; 85 | display: inline-block; 86 | border-radius: 0.3em; 87 | color: #4CB0F9; 88 | } 89 | 90 | #listcolumn { 91 | border-style: dashed; 92 | border-color: purple; 93 | } */ 94 | 95 | /* #listcolumn:focus{ 96 | border: 2px dashed purple 97 | } */ 98 | /* select:hover, 99 | button:hover, 100 | input:hover { 101 | border-width: 2px; 102 | } */ 103 | 104 | #updateTime { 105 | margin-left: 0.5em; 106 | /* color: darkgray; */ 107 | } 108 | 109 | /* 深夜模式下普通文本颜色 */ 110 | .ordinaryText_dark, .dark-mode .upperbardiv span, .dark-mode #linksContainer, .dark-mode .mode12_date_text{ 111 | color: #C9D1D9; 112 | } 113 | 114 | .upperbar { 115 | width: 100%; 116 | display: flex; 117 | text-align: center; 118 | margin-top: 5px; 119 | } 120 | 121 | .upperbardiv { 122 | display: block; 123 | } 124 | 125 | 126 | .linksList { 127 | margin-top: 0px; 128 | /* 依次为 type, image, position */ 129 | list-style: disc none outside; 130 | margin-left: 30px; 131 | } 132 | 133 | .linksList ul { 134 | padding-inline-start: 40px; 135 | } 136 | 137 | .linksList ul ul li { 138 | list-style: circle none outside; 139 | } 140 | 141 | .linksList ul li, ul.linksList > li { 142 | /* 依次为 type, image, position */ 143 | list-style: disc none outside; 144 | } 145 | 146 | .linksList.noListStyle li { 147 | list-style: none; 148 | } 149 | 150 | .noListStyle { 151 | list-style-type: none; 152 | padding-inline-start: 0%; 153 | } 154 | 155 | /* 无序列表单项 */ 156 | .linksListItem { 157 | padding: 4px 0px; 158 | } 159 | 160 | /* 列表单项(深色)*/ 161 | .dark-mode .linksListItem { 162 | padding: 4px 0px; 163 | color: #C9D1D9; 164 | } 165 | 166 | /* 挂件内自定义emoji样式 */ 167 | .iconpic { 168 | width: 18px; 169 | height: 18px; 170 | vertical-align: bottom; 171 | } 172 | 173 | /* 子文档区域 */ 174 | #refContainer { 175 | cursor: default; 176 | } 177 | 178 | /* 链接文字样式 */ 179 | .childDocLinks, 180 | .refLinks { 181 | /* color: black; */ 182 | padding: 0 2px; 183 | text-decoration: none; 184 | border-bottom: 1px dashed rgba(0, 0, 0, .1); 185 | } 186 | 187 | /* 暗黑模式下,链接文字样式 */ 188 | .dark-mode .linksList { 189 | color: #C9D1D9; 190 | } 191 | 192 | /* 链接文字悬停样式 */ 193 | .linkTextHoverHightLight:hover{ 194 | color: #1186d9; 195 | border-bottom: 1px dashed #4CB0F9; 196 | } 197 | 198 | /* 扩大点击范围为整行时,悬停样式 */ 199 | .itemHoverHighLight:hover { 200 | background-color: #1186d91f; 201 | color: #1186d9; 202 | } 203 | 204 | /* 错误提示文字样式 */ 205 | .errorinfo { 206 | color: red; 207 | text-align: center; 208 | } 209 | 210 | /* 链接区,所有挂件内模式共用 */ 211 | #linksContainer { 212 | column-fill: balance; 213 | clear: both; 214 | } 215 | 216 | /* 属于此类的元素,将被点击处理 */ 217 | .handle-ref-click { 218 | cursor: pointer; 219 | } 220 | 221 | /********************************************** 预览方格模式内css */ 222 | /* 子文档方块区 */ 223 | .mode11-note-box { 224 | max-height: 250px; 225 | overflow: auto; 226 | border-radius: 10px; 227 | padding: 10px 15px 15px 8px; 228 | margin: 5px 5px 5px 0; 229 | flex-direction: column; 230 | flex: 1 0 250px; 231 | display: flex; 232 | background-color: #ebebeb86; 233 | } 234 | /* 子文档方块外围div */ 235 | .mode11-box { 236 | overflow: auto; 237 | flex-wrap: wrap; 238 | display: flex; 239 | } 240 | 241 | /* 子文档方块内的文档标题 */ 242 | .mode11-title { 243 | margin: 10px 0 10px 0; 244 | } 245 | 246 | /* 二级文档标题所在的段落 */ 247 | .mode11-child-p-container > p { 248 | margin: 0 0 0 7px; 249 | width: 95%; 250 | white-space: normal; 251 | font-size: 0.9; 252 | } 253 | /* 预览内链接内容 */ 254 | .dark-mode .mode11-doc-content a { 255 | color: #7aa7d4 !important; 256 | } 257 | 258 | /* 子文档内容节选 */ 259 | .mode11-doc-content { 260 | margin: 0 0 0 7px; 261 | width: 95%; 262 | white-space: normal; 263 | overflow: hidden; 264 | } 265 | 266 | /* .mode11-doc-content p { 267 | display: inline; 268 | } */ 269 | 270 | /* mode11 图片显示*/ 271 | .mode11-doc-content .img img { 272 | max-width: 100%; 273 | display: inline-block; 274 | } 275 | 276 | /* 子文档预览内容 */ 277 | .mode11-doc-content [id][updated] { 278 | font-size: 16px; 279 | margin: 5px 0 0 0; 280 | } 281 | 282 | .mode11-note-box:hover { 283 | background-color: #bfbfbf86; 284 | } 285 | 286 | /* 任务列表样式调整 */ 287 | .mode11-doc-content ul:has(> .protyle-task) { 288 | list-style-type: none; 289 | padding-left: 16px; 290 | } 291 | 292 | .mode11-doc-content > ul:has(> .protyle-task) { 293 | padding-left: 0px; 294 | } 295 | 296 | .mode11-doc-content .protyle-task input ~ p { 297 | display: inline-block; 298 | } 299 | 300 | /* mode11 深色模式方块*/ 301 | .dark-mode .mode11-note-box { 302 | background-color: #efefef15; 303 | color: #C9D1D9; 304 | } 305 | /* mode11 深色模式方块悬停*/ 306 | .dark-mode .mode11-note-box:hover { 307 | background-color: #efefef25; 308 | } 309 | 310 | /*滚动条样式调整*/ 311 | body.dark-mode { 312 | scrollbar-width: thin; /* Firefox */ 313 | scrollbar-color: #555 #1e1e1e10; 314 | } 315 | /* body::-webkit-scrollbar { 316 | width: 8px; 317 | } 318 | body.dark-mode::-webkit-scrollbar-track { 319 | background: #1e1e1e10; 320 | } 321 | body.dark-mode::-webkit-scrollbar-thumb { 322 | background-color: #555; 323 | border-radius: 4px; 324 | } 325 | body::-webkit-scrollbar-track { 326 | background: #f0f0f010; 327 | } 328 | body::-webkit-scrollbar-thumb { 329 | background-color: #999; 330 | border-radius: 4px; 331 | } */ 332 | 333 | body { 334 | scrollbar-width: thin; /* Firefox */ 335 | scrollbar-color: #555 #1e1e1e10; 336 | } 337 | ::-webkit-scrollbar { 338 | width: 8px; 339 | } 340 | ::-webkit-scrollbar-track { 341 | background: #1e1e1e10; 342 | } 343 | ::-webkit-scrollbar-thumb { 344 | background-color: #555; 345 | border-radius: 4px; 346 | } 347 | 348 | /********************************************* 挂件思维导图样式 */ 349 | #markmap { 350 | user-select: none; 351 | -webkit-user-select: none; /* Safari */ 352 | -moz-user-select: none; /* Firefox */ 353 | -ms-user-select: none; /* IE/Edge */ 354 | } 355 | 356 | /* 思维导图链接样式 */ 357 | .markmap_a, .markmap-foreign a { 358 | color: black !important; 359 | text-decoration: none; 360 | border-bottom: 1px dashed rgba(0, 0, 0, .1); 361 | /* white-space: nowrap; */ 362 | } 363 | /* 思维导图链接鼠标悬停时样式 */ 364 | .markmap_a:hover, .markmap-foreign a:hover { 365 | color: #1186d9 !important; 366 | border-bottom: 1px dashed #4CB0F9 !important; 367 | /* white-space: nowrap; */ 368 | } 369 | 370 | 371 | /* 思维导图普通文字深色样式 */ 372 | .dark-mode .markmap { 373 | color: #C9D1D9 !important; 374 | } 375 | /* 思维导图链接深色样式 */ 376 | .dark-mode .markmap_a, .dark-mode .markmap-foreign a { 377 | color: #C9D1D9 !important; 378 | text-decoration: none !important; 379 | /* white-space: nowrap; */ 380 | } 381 | 382 | /* 思维导图悬停样式*/ 383 | .dark-mode .markmap_a:hover, .dark-mode .markmap-foreign a:hover{ 384 | color: #1186d9 !important; 385 | border-bottom: 1px dashed #4CB0F9 !important; 386 | } 387 | 388 | /***************** 模式12 */ 389 | .mode12_date_text { 390 | font-weight: bold; 391 | } 392 | 393 | 394 | .emojitext { 395 | font-family: 'Twemoji Mozilla', 'Noto Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Segoe UI', 'Apple Color Emoji', 'Noto Color Emoji', 'Android Emoji'; 396 | } 397 | /*****************************对话框*/ 398 | .ui-dialog.dark_dialog { 399 | background-color: #101010; 400 | color: #C9D1D9; 401 | box-shadow: none !important; 402 | } 403 | 404 | .ui-dialog.dark_dialog button { 405 | color: #C9D1D9; 406 | background-color: inherit; 407 | } 408 | 409 | .ui-dialog.dark_dialog button:hover { 410 | color: #C9D1D9; 411 | background-color: #00000055; 412 | } 413 | 414 | .ui-dialog button[i-id="Delete"], .ui-dialog button[i-id="删除"], .ui-dialog.delete_dialog button[i-id=ok] { 415 | background-color: #dd041e99; 416 | color: whitesmoke; 417 | } 418 | 419 | .ui-dialog button[i-id="Delete"]:hover, .ui-dialog button[i-id="删除"]:hover, .ui-dialog.delete_dialog button[i-id=ok]:hover { 420 | background-color: #dd041ecc; 421 | color: whitesmoke; 422 | } 423 | 424 | /* 内容占据全部宽度 */ 425 | .ui-dialog-content[i="content"] { 426 | width: 100%; 427 | } 428 | 429 | /* 检索高亮结果 */ 430 | .search_highlight { 431 | background-color: #efc52f5e; 432 | } 433 | 434 | .dark-mode .search_highlight { 435 | background-color: #85d0a35e; 436 | } 437 | 438 | body { 439 | padding: 0; 440 | border: 0; 441 | margin: 0; 442 | width: 100%; 443 | height: 100%; 444 | align-items: center; 445 | justify-content: center; 446 | } 447 | 448 | .app { 449 | width: 100%; 450 | } 451 | 452 | .outerSetting-hide { 453 | opacity: 0; 454 | transition: opacity 0.1s ease; 455 | /* max-height: px; */ 456 | margin-top: 0px; 457 | margin-bottom: 5px; 458 | } 459 | 460 | .outerSetting-none { 461 | display: none; 462 | } 463 | 464 | /* 465 | dl::-webkit-scrollbar { 466 | width: 10px; 467 | } */ 468 | 469 | /* 用于修复软件内无法拖动滚动条的问题 */ 470 | 471 | dl::-webkit-scrollbar { 472 | /*滚动条整体样式*/ 473 | width : 10px; /*高宽分别对应横竖滚动条的尺寸*/ 474 | height: 1px; 475 | } 476 | dl::-webkit-scrollbar-thumb { 477 | /*滚动条里面小方块*/ 478 | border-radius: 10px; 479 | /* box-shadow : inset 0 0 5px rgba(0, 0, 0, 0.2); */ 480 | background : #535353; 481 | } 482 | dl::-webkit-scrollbar-track { 483 | /*滚动条里面轨道*/ 484 | /* box-shadow : inset 0 0 5px rgba(0, 0, 0, 0.2); */ 485 | border-radius: 10px; 486 | background : #ededed; 487 | } 488 | 489 | /* layui 表单 换行等导致的错位*/ 490 | .inline-vertical-center { 491 | display: flex; 492 | align-items: center; 493 | } 494 | 495 | .layui-form-item .layui-input-inline.flayui-limit-input-width-num { 496 | width: 65px; 497 | } 498 | 499 | .layui-form-item .layui-input-inline.flayui-limit-input-btn { 500 | width: 45px; 501 | } 502 | 503 | .layui-form-item .layui-input-inline.flayui-limit-short-text-width { 504 | width: 75px; 505 | } 506 | 507 | /* 设置页选项紧凑化*/ 508 | .layui-form-item { 509 | margin-bottom: 5px; 510 | } 511 | 512 | .layui-input, .layui-select, .layui-textarea { 513 | height: 32px; 514 | } 515 | 516 | .layui-form-mid.layui-text-em { 517 | padding: 5px 0 !important; 518 | } 519 | 520 | .layui-form-label { 521 | padding: 5px 15px; 522 | } 523 | 524 | .layui-form-item .layui-inline { 525 | margin-top: 2px; 526 | margin-bottom: 2px; 527 | } 528 | 529 | .layui-btn { 530 | height: 34px; 531 | line-height: 34px 532 | } 533 | 534 | .layui-form-switch { 535 | margin-top: 2px; 536 | margin-bottom: 2px; 537 | } 538 | 539 | #modeSetting { 540 | margin-left: 2%; 541 | } -------------------------------------------------------------------------------- /src/addChildDocListHelper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 创建顶栏按钮,快速插入子文档列表 3 | * 此为历史遗留,现已废弃。 4 | * 插入设置跟随config.js的custom_attr默认设置,只支持url 、 引用块 、 1.url、 1. 引用块模式。 5 | */ 6 | import { getCurrentDocIdF, queryAPI, getSubDocsAPI, insertBlockAPI, updateBlockAPI, prependBlockAPI, getDocOutlineAPI } from "./API.js"; 7 | import { printerList } from "./listChildDocsClass.js"; 8 | import { custom_attr, language } from "./config.js"; 9 | import { isValidStr, isSafelyUpdate, isInvalidValue, transfromAttrToIAL } from "./common.js"; 10 | import { setting } from "./config.js"; 11 | 12 | let myPrinter = new printerList[custom_attr.printMode](); 13 | 14 | // SQL 获取文档详情 15 | async function getTargetBlockBoxPath(currentDocId) { 16 | let queryResult = await queryAPI(`SELECT * FROM blocks WHERE id = '${currentDocId}'`); 17 | if (queryResult == null || queryResult.length != 1) throw new Error("获取当前打开的文档id错误", currentDocId); 18 | let notebook = queryResult[0].box;//笔记本名 19 | let g_targetDocPath = queryResult[0].path;// 块在笔记本下的路径 20 | return [notebook, g_targetDocPath]; 21 | } 22 | 23 | // 递归获取全部 24 | async function main() { 25 | let currentDocId = await getCurrentDocIdF(); 26 | let [notebook, docPath] = await getTargetBlockBoxPath(currentDocId); 27 | let insertText = ""; 28 | insertText = await getText(notebook, docPath, currentDocId); 29 | 30 | await addText2File(insertText, "", currentDocId); 31 | } 32 | 33 | // 绑定顶栏按钮 34 | let barVipElem = document.getElementById("toolbarVIP"); 35 | barVipElem.insertAdjacentHTML("afterend", ` 36 | 37 | `); 38 | let barButton = document.getElementById("barAddChildDocList"); 39 | barButton.addEventListener( 40 | "click", 41 | async function (e) { 42 | e.stopPropagation(); 43 | barButton.setAttribute("disabled", "true"); 44 | barButton.setAttribute("aria-label", "正在插入"); 45 | barButton.classList.add("toolbar__item--disabled"); 46 | try { 47 | await main(); 48 | }catch(err) { 49 | console.error("listChildDocs 快速插入时出现错误", err); 50 | } 51 | barButton.removeAttribute("disabled"); 52 | barButton.setAttribute("aria-label", "快速插入子文档列表"); 53 | barButton.classList.remove("toolbar__item--disabled"); 54 | }, 55 | false 56 | ); 57 | 58 | 59 | // 60 | async function addText2File(markdownText, blockid = "", insertTargetId) { 61 | // 这里insertTargetId一定是文档id,所以isSafelyUpdate可以使用其进行只读判断 62 | if (isSafelyUpdate(insertTargetId, {widgetMode: true}, insertTargetId) == false) { 63 | throw new Error(language["readonly"]); 64 | } 65 | let attrData = {}; 66 | //读取属性.blockid为null时不能去读 67 | if (isValidStr(blockid) && setting.inheritAttrs) { 68 | //判断是否是分列的目录块(是否是超级块) 69 | // let subLists = await queryAPI(`SELECT id FROM blocks WHERE type = 'l' AND parent_id IN (SELECT id from blocks where parent_id = '${blockid}' and type = 's')`); 70 | let subDirectLists = await queryAPI(`SELECT id FROM blocks WHERE type = 'l' AND parent_id = '${blockid}'`); 71 | // console.log("超级块内超级块下的列表数?", subLists.length); 72 | // console.log("超级块下直接的列表数", subDirectLists.length); 73 | //如果是分列的目录块,那么以超级块中一个随机的无序列表的属性为基准,应用于更新后的块 74 | attrData = await getblockAttrAPI(subDirectLists.length >= 1 ? subDirectLists[0].id : blockid); 75 | // console.log("更新前,", subDirectLists, "attrGet", attrData); 76 | attrData = attrData.data; 77 | //避免重新写入id和updated信息 78 | delete attrData.id; 79 | delete attrData.updated; 80 | }else if (setting.blockInitAttrs != undefined){ // 为新创建的列表获取默认属性 81 | attrData = Object.assign({}, setting.blockInitAttrs); 82 | } 83 | // 导入模式属性 84 | let modeCustomAttr = myPrinter.getAttributes(); 85 | if (!isInvalidValue(modeCustomAttr)) { 86 | attrData = Object.assign(attrData, modeCustomAttr); 87 | } 88 | // 分列操作(分列并使得列继承属性) 89 | if (custom_attr.listColumn > 1 && setting.inheritAttrs && setting.superBlockBeta) { 90 | markdownText = myPrinter.splitColumns(markdownText, custom_attr["listColumn"], custom_attr["listDepth"], attrData); 91 | } 92 | 93 | // 将属性以IAL的形式写入text,稍后直接更新块 94 | let blockAttrString = transfromAttrToIAL(attrData); 95 | if (blockAttrString != null) { 96 | markdownText += "\n" + blockAttrString; 97 | } 98 | //创建/更新块 99 | let response; 100 | // 变为后置子块插入 101 | response = await prependBlockAPI(markdownText, insertTargetId); 102 | } 103 | 104 | //获取子文档层级目录输出文本 105 | async function getText(notebook, nowDocPath, currentDocId) { 106 | if (myPrinter == undefined) { 107 | console.error("输出类Printer错误", myPrinter); 108 | throw Error(language["wrongPrintMode"]); 109 | } 110 | let insertData = myPrinter.beforeAll(); 111 | let rawData = ""; 112 | let rowCountStack = new Array(); 113 | rowCountStack.push(1); 114 | 115 | // 单独处理起始为笔记本上级的情况 116 | if (notebook === "/") { 117 | rawData = await getTextFromNotebooks(rowCountStack); 118 | }else{ 119 | // 单独处理 返回父文档../ 120 | // 用户自行指定目标时,不附加../ 121 | if (!isValidStr(custom_attr["targetId"]) && 122 | (setting.backToParent == "true" || (setting.backToParent == "auto" && window.screen.availWidth <= 768)) && 123 | myPrinter.write2file == 0) { 124 | let tempPathData = nowDocPath.split("/"); 125 | // 排除为笔记本、笔记本直接子文档的情况,split后首个为'' 126 | if (tempPathData.length > 2) { 127 | let tempVirtualDocObj = { 128 | id: tempPathData[tempPathData.length - 2], 129 | name: "../", 130 | icon: "1f519"//图标🔙 131 | }; 132 | rawData += myPrinter.align(rowCountStack.length); 133 | rawData += myPrinter.oneDocLink(tempVirtualDocObj, rowCountStack); 134 | rowCountStack[rowCountStack.length - 1]++; 135 | } 136 | } 137 | // 处理大纲和子文档两种情况,子文档情况兼容从笔记本级别列出 138 | if (custom_attr.listDepth == 0) { 139 | rawData = await getDocOutlineText(currentDocId, false, rowCountStack); 140 | } else { 141 | rawData = await getOneLevelText(notebook, nowDocPath, rawData, rowCountStack);//层级从1开始 142 | } 143 | } 144 | 145 | if (rawData == "") { 146 | if (custom_attr.listDepth > 0) { 147 | rawData = myPrinter.noneString(language["noChildDoc"]); 148 | } else { 149 | rawData = myPrinter.noneString(language["noOutline"]); 150 | } 151 | } 152 | insertData += rawData + myPrinter.afterAll(); 153 | return insertData; 154 | } 155 | 156 | /** 157 | * 从笔记本上级列出子文档 158 | * @param {*} notebook 159 | * @param {*} nowDocPath 160 | * @return 返回的内容非累加内容,覆盖返回 161 | */ 162 | async function getTextFromNotebooks(rowCountStack) { 163 | let result = ""; 164 | // 防止没有获取到笔记本列表 165 | if (g_notebooks == null) { 166 | g_notebooks = await getNodebookList(); 167 | } 168 | // 遍历笔记本 169 | for (let i = 0; i < g_notebooks.length; i++) { 170 | // 关闭的笔记本无法跳转,没有创建的意义 171 | if (g_notebooks[i].closed == true) continue; 172 | // 插入笔记本名和笔记本图标(本部分逻辑同getOneLevelText) 173 | let tempVirtualDocObj = { 174 | id: "", 175 | name: g_notebooks[i].name, 176 | icon: g_notebooks[i].icon === "" ? "1f5c3" : g_notebooks[i].icon 177 | }; 178 | result += myPrinter.align(rowCountStack.length); 179 | result += myPrinter.oneDocLink(tempVirtualDocObj, rowCountStack); 180 | // 处理笔记本下级文档 181 | if ((rowCountStack.length + 1) <= custom_attr.listDepth) { 182 | result += myPrinter.beforeChildDocs(rowCountStack.length); 183 | rowCountStack.push(1); 184 | result = await getOneLevelText(g_notebooks[i].id, "/", result, rowCountStack); 185 | rowCountStack.pop(); 186 | result += myPrinter.afterChildDocs(rowCountStack.length); 187 | } 188 | rowCountStack[rowCountStack.length - 1]++; 189 | } 190 | return result; 191 | } 192 | 193 | /** 194 | * 获取一层级子文档输出文本 195 | * @param {*} notebook 196 | * @param {*} nowDocPath 197 | * @param {*} insertData 198 | * @param {*} rowCountStack 199 | * @returns 返回的内容非累加内容,需=接收 200 | */ 201 | async function getOneLevelText(notebook, nowDocPath, insertData, rowCountStack) { 202 | if (rowCountStack.length > custom_attr.listDepth) { 203 | return insertData; 204 | } 205 | let docs = await getSubDocsAPI(notebook, nowDocPath); 206 | //生成写入文本 207 | for (let doc of docs) { 208 | insertData += myPrinter.align(rowCountStack.length); 209 | insertData += myPrinter.oneDocLink(doc, rowCountStack); 210 | if (doc.subFileCount > 0 && (rowCountStack.length + 1) <= custom_attr.listDepth) {//获取下一层级子文档 211 | insertData += myPrinter.beforeChildDocs(rowCountStack.length); 212 | rowCountStack.push(1); 213 | insertData = await getOneLevelText(notebook, doc.path, insertData, rowCountStack); 214 | rowCountStack.pop(); 215 | insertData += myPrinter.afterChildDocs(rowCountStack.length); 216 | } else if (custom_attr.endDocOutline && custom_attr.outlineDepth > 0) {//终端文档列出大纲 217 | let outlines = await getDocOutlineAPI(doc.id); 218 | if (outlines != null) { 219 | insertData += myPrinter.beforeChildDocs(rowCountStack.length); 220 | rowCountStack.push(1); 221 | insertData += getOneLevelOutline(outlines, true, rowCountStack); 222 | rowCountStack.pop(); 223 | insertData += myPrinter.afterChildDocs(rowCountStack.length); 224 | } 225 | } 226 | rowCountStack[rowCountStack.length - 1]++; 227 | } 228 | return insertData; 229 | } 230 | 231 | /** 232 | * 生成文档大纲输出文本 233 | * @param {*} docId 234 | * @param {*} distinguish 区分大纲和页面,如果同时列出文档且需要区分,为true 235 | * @param {*} rowCountStack 生成行数记录 236 | * @return {*} 仅大纲的输出文本,(返回内容为累加内容)如果有其他,请+=保存 237 | */ 238 | async function getDocOutlineText(docId, distinguish, rowCountStack) { 239 | let outlines = await getDocOutlineAPI(docId); 240 | if (outlines == null) { console.warn("获取大纲失败"); return ""; } 241 | let result = ""; 242 | result += getOneLevelOutline(outlines, distinguish, rowCountStack); 243 | return result; 244 | } 245 | 246 | /** 247 | * 生成本层级大纲文本 248 | * @param {*} outlines 大纲对象 249 | * @param {*} distinguish 区分大纲和页面,如果同时列出文档且需要区分,为true 250 | * @param {*} rowCountStack 生成行数记录 251 | * @returns 本层级及其子层级大纲生成文本,请+=保存; 252 | */ 253 | function getOneLevelOutline(outlines, distinguish, rowCountStack) { 254 | //大纲层级是由API返回值确定的,混合列出时不受“层级”listDepth控制 255 | if (outlines == null || outlines == undefined || outlines.length <= 0 256 | || outlines[0].depth >= custom_attr.outlineDepth) return ""; 257 | let result = ""; 258 | for (let outline of outlines) { 259 | if (!isValidStr(outline.name)) {//处理内部大纲类型NodeHeading的情况,也是由于Printer只读取name属性 260 | outline.name = outline.content; 261 | } 262 | if (distinguish) { 263 | outline.name = setting.outlineDistinguishingWords + outline.name; 264 | } 265 | result += myPrinter.align(rowCountStack.length); 266 | result += myPrinter.oneDocLink(outline, rowCountStack); 267 | if (outline.type === "outline" && outline.blocks != null) { 268 | result += myPrinter.beforeChildDocs(); 269 | rowCountStack.push(1); 270 | result += getOneLevelOutline(outline.blocks, distinguish, rowCountStack); 271 | rowCountStack.pop(); 272 | result += myPrinter.afterChildDocs(); 273 | } else if (outline.type == "NodeHeading" && outline.children != null) { 274 | result += myPrinter.beforeChildDocs(); 275 | rowCountStack.push(1); 276 | result += getOneLevelOutline(outline.children, distinguish, rowCountStack); 277 | rowCountStack.pop(); 278 | result += myPrinter.afterChildDocs(); 279 | } else if (outline.type != "outline" && outline.type != "NodeHeading") { 280 | console.warn("未被处理的大纲情况"); 281 | } 282 | rowCountStack[rowCountStack.length - 1]++; 283 | } 284 | return result; 285 | } 286 | -------------------------------------------------------------------------------- /static/layui-v2.8.12/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 测试 - Layui 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 | 16 | 17 |
18 | 19 | 20 |
21 | 22 |
23 |
24 |
    25 |
  • 您当前预览的是:Layui-v
  • 26 |
  • Layui 是一套开源免费的 Web UI(界面)组件库。这是一个极其简洁的演示页面
  • 27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 | 35 |
36 |
37 |
38 |
39 | 40 |
41 | 42 |
43 |
44 | 45 |
46 |
47 |
48 |
49 | 50 |
51 | 52 |
53 |
54 |
55 |
56 | 57 |
58 | 59 |
60 |
61 |
62 | 63 |
64 |
65 | 66 |
67 | 68 |
69 |
70 |
71 |
72 | 73 |
74 | 75 |
76 | 79 | 80 |
81 |
82 |
83 | 84 |
85 | 86 |
87 |
-
88 |
89 | 90 |
91 |
92 |
93 |
94 | 95 |
96 | 114 |
115 |
116 |
117 |
118 | 119 |
120 | 130 |
131 |
132 |
133 | 134 |
135 | 158 |
159 |
160 |
161 |
162 | 163 |
164 | 170 |
171 |
172 | 180 |
181 |
182 | 188 |
189 |
190 | 194 |
195 |
196 |
197 | 198 |
199 | 200 | 201 | 202 |
203 |
204 |
205 | 206 |
207 | 208 | 209 | 210 |
211 |
212 |
213 | 214 |
215 | 216 |
217 |
218 |
219 | 220 |
221 | 222 | 223 | 224 |
225 |
226 |
227 | 228 |
229 | 230 |
231 |
232 |
233 |
234 | 235 | 236 |
237 |
238 |
239 |
240 | 241 | 242 | 243 | 308 | 309 | 310 | -------------------------------------------------------------------------------- /static/markmap-view-0.18.12.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Minified by jsDelivr using Terser v5.39.0. 3 | * Original file: /npm/markmap-view@0.18.12/dist/browser/index.js 4 | * 5 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files 6 | */ 7 | !function(t,e){"use strict";const n=Math.random().toString(36).slice(2,8);let r=0;function i(){}function a(t,e){const n=(t,r)=>e(t,(()=>{var e;return null==(e=t.children)?void 0:e.map((e=>n(e,t)))}),r);return n(t)}function s(){const t={};return t.promise=new Promise(((e,n)=>{t.resolve=e,t.reject=n})),t} 8 | /*! @gera2ld/jsx-dom v2.2.2 | ISC License */ 9 | const o="http://www.w3.org/1999/xlink",l={show:o,actuate:o,href:o};function h(t,e,...n){return function(t,e){let n;if("string"==typeof t)n=1;else{if("function"!=typeof t)throw new Error("Invalid VNode type");n=2}return{vtype:n,type:t,props:e}}(t,e=Object.assign({},e,{children:1===n.length?n[0]:n}))}function c(t){return t.children}const d={isSvg:!1};function p(t,e){Array.isArray(e)||(e=[e]),(e=e.filter(Boolean)).length&&t.append(...e)}const u={className:"class",labelFor:"for"};function g(t,e,n,r){if(e=u[e]||e,!0===n)t.setAttribute(e,"");else if(!1===n)t.removeAttribute(e);else{const i=r?l[e]:void 0;void 0!==i?t.setAttributeNS(i,e,n):t.setAttribute(e,n)}}function m(t,e){return Array.isArray(t)?t.map((t=>m(t,e))).reduce(((t,e)=>t.concat(e)),[]):f(t,e)}function f(t,e=d){if(null==t||"boolean"==typeof t)return null;if(t instanceof Node)return t;if(2===(null==(n=t)?void 0:n.vtype)){const{type:n,props:r}=t;if(n===c){const t=document.createDocumentFragment();if(r.children){p(t,m(r.children,e))}return t}return f(n(r),e)}var n;if((t=>"string"==typeof t||"number"==typeof t)(t))return document.createTextNode(`${t}`);if((t=>1===(null==t?void 0:t.vtype))(t)){let n;const{type:r,props:i}=t;if(e.isSvg||"svg"!==r||(e=Object.assign({},e,{isSvg:!0})),n=e.isSvg?document.createElementNS("http://www.w3.org/2000/svg",r):document.createElement(r),function(t,e,n){for(const r in e)if("key"!==r&&"children"!==r&&"ref"!==r)if("dangerouslySetInnerHTML"===r)t.innerHTML=e[r].__html;else if("innerHTML"===r||"textContent"===r||"innerText"===r||"value"===r&&["textarea","select"].includes(t.tagName)){const n=e[r];null!=n&&(t[r]=n)}else r.startsWith("on")?t[r.toLowerCase()]=e[r]:g(t,r,e[r],n.isSvg)}(n,i,e),i.children){let t=e;e.isSvg&&"foreignObject"===r&&(t=Object.assign({},t,{isSvg:!1}));const a=m(i.children,t);null!=a&&p(n,a)}const{ref:a}=i;return"function"==typeof a&&a(n),n}throw new Error("mount: Invalid Vnode!")}function v(...t){return f(h(...t))}const y=function(t){const e={};return function(...n){const r=`${n[0]}`;let i=e[r];return i||(i={value:t(...n)},e[r]=i),i.value}}((t=>{document.head.append(v("link",{rel:"preload",as:"script",href:t}))})),x={},k={};async function w(t,e){var n;const r="script"===t.type&&(null==(n=t.data)?void 0:n.src)||"";if(t.loaded||(t.loaded=x[r]),!t.loaded){const n=s();if(t.loaded=n.promise,"script"===t.type&&(document.head.append(v("script",{...t.data,onLoad:()=>n.resolve(),onError:n.reject})),r?x[r]=t.loaded:n.resolve()),"iife"===t.type){const{fn:r,getParams:i}=t.data;r(...(null==i?void 0:i(e))||[]),n.resolve()}}await t.loaded}const b="undefined"!=typeof navigator&&navigator.userAgent.includes("Macintosh"),z=e.scaleOrdinal(e.schemeCategory10),S=(t=1,e=3,n=2)=>r=>t+e/n**r.state.depth,E={autoFit:!1,duration:500,embedGlobalCSS:!0,fitRatio:.95,maxInitialScale:2,scrollForPan:b,initialExpandLevel:-1,zoom:!0,pan:!0,toggleRecursively:!1,color:t=>{var e;return z(`${(null==(e=t.state)?void 0:e.path)||""}`)},lineWidth:S(),maxWidth:0,nodeMinHeight:16,paddingX:8,spacingHorizontal:80,spacingVertical:5};function C(t){let e=0;for(let n=0;n>>0).toString(36)}function X(t){if("string"==typeof t){const e=t;t=t=>t.matches(e)}const e=t;return function(){let t=Array.from(this.childNodes);return e&&(t=t.filter((t=>e(t)))),t}}function A(t){var e=0,n=t.children,r=n&&n.length;if(r)for(;--r>=0;)e+=n[r].value;else e=1;t.value=e}function j(t,e){var n,r,i,a,s,o=new M(t),l=+t.value&&(o.value=t.value),h=[o];for(null==e&&(e=R);n=h.pop();)if(l&&(n.value=+n.data.value),(i=e(n.data))&&(s=i.length))for(n.children=new Array(s),a=s-1;a>=0;--a)h.push(r=n.children[a]=new M(i[a])),r.parent=n,r.depth=n.depth+1;return o.eachBefore($)}function R(t){return t.children}function O(t){t.data=t.data.data}function $(t){var e=0;do{t.height=e}while((t=t.parent)&&t.height<++e)}function M(t){this.data=t,this.depth=this.height=0,this.parent=null}M.prototype=j.prototype={constructor:M,count:function(){return this.eachAfter(A)},each:function(t){var e,n,r,i,a=this,s=[a];do{for(e=s.reverse(),s=[];a=e.pop();)if(t(a),n=a.children)for(r=0,i=n.length;r=0;--n)i.push(e[n]);return this},sum:function(t){return this.eachAfter((function(e){for(var n=+t(e.data)||0,r=e.children,i=r&&r.length;--i>=0;)n+=r[i].value;e.value=n}))},sort:function(t){return this.eachBefore((function(e){e.children&&e.children.sort(t)}))},path:function(t){for(var e=this,n=function(t,e){if(t===e)return t;var n=t.ancestors(),r=e.ancestors(),i=null;t=n.pop(),e=r.pop();for(;t===e;)i=t,t=n.pop(),e=r.pop();return i}(e,t),r=[e];e!==n;)e=e.parent,r.push(e);for(var i=r.length;t!==n;)r.splice(i,0,t),t=t.parent;return r},ancestors:function(){for(var t=this,e=[t];t=t.parent;)e.push(t);return e},descendants:function(){var t=[];return this.each((function(e){t.push(e)})),t},leaves:function(){var t=[];return this.eachBefore((function(e){e.children||t.push(e)})),t},links:function(){var t=this,e=[];return t.each((function(n){n!==t&&e.push({source:n.parent,target:n})})),e},copy:function(){return j(this).eachBefore(O)}};const T={version:"2.1.2"},{version:H}=T,B=Object.freeze({children:t=>t.children,nodeSize:t=>t.data.size,spacing:0});function _(t){const e=Object.assign({},B,t);function n(t){const n=e[t];return"function"==typeof n?n:()=>n}function r(t){const e=a(function(){const t=i(),e=n("nodeSize"),r=n("spacing");return class extends t{constructor(t){super(t),Object.assign(this,{x:0,y:0,relX:0,prelim:0,shift:0,change:0,lExt:this,lExtRelX:0,lThr:null,rExt:this,rExtRelX:0,rThr:null})}get size(){return e(this.data)}spacing(t){return r(this.data,t.data)}get x(){return this.data.x}set x(t){this.data.x=t}get y(){return this.data.y}set y(t){this.data.y=t}update(){return N(this),D(this),this}}}(),t,(t=>t.children));return e.update(),e.data}function i(){const t=n("nodeSize"),e=n("spacing");return class n extends j.prototype.constructor{constructor(t){super(t)}copy(){const t=a(this.constructor,this,(t=>t.children));return t.each((t=>t.data=t.data.data)),t}get size(){return t(this)}spacing(t){return e(this,t)}get nodes(){return this.descendants()}get xSize(){return this.size[0]}get ySize(){return this.size[1]}get top(){return this.y}get bottom(){return this.y+this.ySize}get left(){return this.x-this.xSize/2}get right(){return this.x+this.xSize/2}get root(){const t=this.ancestors();return t[t.length-1]}get numChildren(){return this.hasChildren?this.children.length:0}get hasChildren(){return!this.noChildren}get noChildren(){return null===this.children}get firstChild(){return this.hasChildren?this.children[0]:null}get lastChild(){return this.hasChildren?this.children[this.numChildren-1]:null}get extents(){return(this.children||[]).reduce(((t,e)=>n.maxExtents(t,e.extents)),this.nodeExtents)}get nodeExtents(){return{top:this.top,bottom:this.bottom,left:this.left,right:this.right}}static maxExtents(t,e){return{top:Math.min(t.top,e.top),bottom:Math.max(t.bottom,e.bottom),left:Math.min(t.left,e.left),right:Math.max(t.right,e.right)}}}}function a(t,e,n){const r=(e,i)=>{const a=new t(e);Object.assign(a,{parent:i,depth:null===i?0:i.depth+1,height:0,length:1});const s=n(e)||[];return a.children=0===s.length?null:s.map((t=>r(t,a))),a.children&&Object.assign(a,a.children.reduce(((t,e)=>({height:Math.max(t.height,e.height+1),length:t.length+e.length})),a)),a};return r(e,null)}return Object.assign(r,{nodeSize(t){return arguments.length?(e.nodeSize=t,r):e.nodeSize},spacing(t){return arguments.length?(e.spacing=t,r):e.spacing},children(t){return arguments.length?(e.children=t,r):e.children},hierarchy(t,n){const r=void 0===n?e.children:n;return a(i(),t,r)},dump(t){const e=n("nodeSize"),r=t=>n=>{const i=t+" ",a=t+" ",{x:s,y:o}=n,l=e(n),h=n.children||[],c=0===h.length?" ":`,${i}children: [${a}${h.map(r(a)).join(a)}${i}],${t}`;return`{ size: [${l.join(", ")}],${i}x: ${s}, y: ${o}${c}},`};return r("\n")(t)}}),r}_.version=H;const N=(t,e=0)=>(t.y=e,(t.children||[]).reduce(((e,n)=>{const[r,i]=e;N(n,t.y+t.ySize);const a=(0===r?n.lExt:n.rExt).bottom;0!==r&&W(t,r,i);return[r+1,Z(a,r,i)]}),[0,null]),L(t),G(t),t),D=(t,e,n)=>{void 0===e&&(e=-t.relX-t.prelim,n=0);const r=e+t.relX;return t.relX=r+t.prelim-n,t.prelim=0,t.x=n+t.relX,(t.children||[]).forEach((e=>D(e,r,t.x))),t},L=t=>{(t.children||[]).reduce(((t,e)=>{const[n,r]=t,i=n+e.shift,a=r+i+e.change;return e.relX+=a,[i,a]}),[0,0])},W=(t,e,n)=>{const r=t.children[e-1],i=t.children[e];let a=r,s=r.relX,o=i,l=i.relX,h=!0;for(;a&&o;){a.bottom>n.lowY&&(n=n.next);const r=s+a.prelim-(l+o.prelim)+a.xSize/2+o.xSize/2+a.spacing(o);(r>0||r<0&&h)&&(l+=r,F(i,r),P(t,e,n.index,r)),h=!1;const c=a.bottom,d=o.bottom;c<=d&&(a=V(a),a&&(s+=a.relX)),c>=d&&(o=I(o),o&&(l+=o.relX))}!a&&o?K(t,e,o,l):a&&!o&&Y(t,e,a,s)},F=(t,e)=>{t.relX+=e,t.lExtRelX+=e,t.rExtRelX+=e},P=(t,e,n,r)=>{const i=t.children[e],a=e-n;if(a>1){const e=r/a;t.children[n+1].shift+=e,i.shift-=e,i.change-=r-e}},I=t=>t.hasChildren?t.firstChild:t.lThr,V=t=>t.hasChildren?t.lastChild:t.rThr,K=(t,e,n,r)=>{const i=t.firstChild,a=i.lExt,s=t.children[e];a.lThr=n;const o=r-n.relX-i.lExtRelX;a.relX+=o,a.prelim-=o,i.lExt=s.lExt,i.lExtRelX=s.lExtRelX},Y=(t,e,n,r)=>{const i=t.children[e],a=i.rExt,s=t.children[e-1];a.rThr=n;const o=r-n.relX-i.rExtRelX;a.relX+=o,a.prelim-=o,i.rExt=s.rExt,i.rExtRelX=s.rExtRelX},G=t=>{if(t.hasChildren){const e=t.firstChild,n=t.lastChild,r=(e.prelim+e.relX-e.xSize/2+n.relX+n.prelim+n.xSize/2)/2;Object.assign(t,{prelim:r,lExt:e.lExt,lExtRelX:e.lExtRelX,rExt:n.rExt,rExtRelX:n.rExtRelX})}},Z=(t,e,n)=>{for(;null!==n&&t>=n.lowY;)n=n.next;return{lowY:t,index:e,next:n}},q=".markmap {\n --markmap-max-width: 9999px;\n --markmap-a-color: #0097e6;\n --markmap-a-hover-color: #00a8ff;\n --markmap-code-bg: #f0f0f0;\n --markmap-code-color: #555;\n --markmap-highlight-bg: #ffeaa7;\n --markmap-table-border: 1px solid currentColor;\n --markmap-font: 300 16px/20px sans-serif;\n --markmap-circle-open-bg: #fff;\n --markmap-text-color: #333;\n --markmap-highlight-node-bg: #ff02;\n\n font: var(--markmap-font);\n color: var(--markmap-text-color);\n}\n\n .markmap-link {\n fill: none;\n }\n\n .markmap-node > circle {\n cursor: pointer;\n }\n\n .markmap-foreign {\n display: inline-block;\n }\n\n .markmap-foreign p {\n margin: 0;\n }\n\n .markmap-foreign a {\n color: var(--markmap-a-color);\n }\n\n .markmap-foreign a:hover {\n color: var(--markmap-a-hover-color);\n }\n\n .markmap-foreign code {\n padding: 0.25em;\n font-size: calc(1em - 2px);\n color: var(--markmap-code-color);\n background-color: var(--markmap-code-bg);\n border-radius: 2px;\n }\n\n .markmap-foreign pre {\n margin: 0;\n }\n\n .markmap-foreign pre > code {\n display: block;\n }\n\n .markmap-foreign del {\n text-decoration: line-through;\n }\n\n .markmap-foreign em {\n font-style: italic;\n }\n\n .markmap-foreign strong {\n font-weight: bold;\n }\n\n .markmap-foreign mark {\n background: var(--markmap-highlight-bg);\n }\n\n .markmap-foreign table,\n .markmap-foreign th,\n .markmap-foreign td {\n border-collapse: collapse;\n border: var(--markmap-table-border);\n }\n\n .markmap-foreign img {\n display: inline-block;\n }\n\n .markmap-foreign svg {\n fill: currentColor;\n }\n\n .markmap-foreign > div {\n width: var(--markmap-max-width);\n text-align: left;\n }\n\n .markmap-foreign > div > div {\n display: inline-block;\n }\n\n .markmap-highlight rect {\n fill: var(--markmap-highlight-node-bg);\n }\n\n.markmap-dark .markmap {\n --markmap-code-bg: #1a1b26;\n --markmap-code-color: #ddd;\n --markmap-circle-open-bg: #444;\n --markmap-text-color: #eee;\n}\n",J=q,Q="g.markmap-node",U=e.linkHorizontal();function tt(t,n){return t[e.minIndex(t,n)]}function et(t){t.stopPropagation()}const nt=new class{constructor(){this.listeners=[]}tap(t){return this.listeners.push(t),()=>this.revoke(t)}revoke(t){const e=this.listeners.indexOf(t);e>=0&&this.listeners.splice(e,1)}revokeAll(){this.listeners.splice(0)}call(...t){for(const e of this.listeners)e(...t)}};class rt{constructor(t,i){this.options={...E},this._disposeList=[],this.handleZoom=t=>{const{transform:e}=t;this.g.attr("transform",e)},this.handlePan=t=>{t.preventDefault();const n=e.zoomTransform(this.svg.node()),r=n.translate(-t.deltaX/n.k,-t.deltaY/n.k);this.svg.call(this.zoom.transform,r)},this.handleClick=(t,e)=>{let n=this.options.toggleRecursively;(b?t.metaKey:t.ctrlKey)&&(n=!n),this.toggleNode(e,n)},this.ensureView=this.ensureVisible,this.svg=t.datum?t:e.select(t),this.styleNode=this.svg.append("style"),this.zoom=e.zoom().filter((t=>this.options.scrollForPan&&"wheel"===t.type?t.ctrlKey&&!t.button:!(t.ctrlKey&&"wheel"!==t.type||t.button))).on("zoom",this.handleZoom),this.setOptions(i),this.state={id:this.options.id||this.svg.attr("id")||(r+=1,`mm-${n}-${r}`),rect:{x1:0,y1:0,x2:0,y2:0}},this.g=this.svg.append("g"),this.g.append("g").attr("class","markmap-highlight"),this._observer=new ResizeObserver(function(t,e){const n={timer:0};function r(){n.timer&&(window.clearTimeout(n.timer),n.timer=0)}function i(){r(),n.args&&(n.result=t(...n.args))}return function(...t){return r(),n.args=t,n.timer=window.setTimeout(i,e),n.result}}((()=>{this.renderData()}),100)),this._disposeList.push(nt.tap((()=>{this.setData()})),(()=>this._observer.disconnect()))}getStyleContent(){const{style:t}=this.options,{id:e}=this.state,n="function"==typeof t?t(e):"";return[this.options.embedGlobalCSS&&q,n].filter(Boolean).join("\n")}updateStyle(){this.svg.attr("class",function(t,...e){const n=(t||"").split(" ").filter(Boolean);return e.forEach((t=>{t&&n.indexOf(t)<0&&n.push(t)})),n.join(" ")}(this.svg.attr("class"),"markmap",this.state.id));const t=this.getStyleContent();this.styleNode.text(t)}async toggleNode(t,e=!1){var n,r;const i=(null==(n=t.payload)?void 0:n.fold)?0:1;e?a(t,((t,e)=>{t.payload={...t.payload,fold:i},e()})):t.payload={...t.payload,fold:(null==(r=t.payload)?void 0:r.fold)?0:1},await this.renderData(t)}_initializeData(t){let e=0;const{color:n,initialExpandLevel:r}=this.options;let i=0,s=0;return a(t,((t,a,o)=>{var l,h,c,d;s+=1,t.children=null==(l=t.children)?void 0:l.map((t=>({...t}))),e+=1,t.state={...t.state,depth:s,id:e,rect:{x:0,y:0,width:0,height:0},size:[0,0]},t.state.key=[null==(h=null==o?void 0:o.state)?void 0:h.id,t.state.id].filter(Boolean).join(".")+C(t.content),t.state.path=[null==(c=null==o?void 0:o.state)?void 0:c.path,t.state.id].filter(Boolean).join("."),n(t);const p=2===(null==(d=t.payload)?void 0:d.fold);p?i+=1:(i||r>=0&&t.state.depth>=r)&&(t.payload={...t.payload,fold:1}),a(),p&&(i-=1),s-=1})),t}_relayout(){if(!this.state.data)return;this.g.selectAll(X(Q)).selectAll(X("foreignObject")).each((function(t){var e;const n=null==(e=this.firstChild)?void 0:e.firstChild,r=[n.scrollWidth,n.scrollHeight];t.state.size=r}));const{lineWidth:t,paddingX:n,spacingHorizontal:r,spacingVertical:i}=this.options,a=_({}).children((t=>{var e;if(!(null==(e=t.payload)?void 0:e.fold))return t.children})).nodeSize((t=>{const[e,i]=t.data.state.size;return[i,e+(e?2*n:0)+r]})).spacing(((e,n)=>(e.parent===n.parent?i:2*i)+t(e.data))),s=a.hierarchy(this.state.data);a(s);const o=s.descendants();o.forEach((t=>{t.data.state.rect={x:t.y,y:t.x-t.xSize/2,width:t.ySize-r,height:t.xSize}})),this.state.rect={x1:e.min(o,(t=>t.data.state.rect.x))||0,y1:e.min(o,(t=>t.data.state.rect.y))||0,x2:e.max(o,(t=>t.data.state.rect.x+t.data.state.rect.width))||0,y2:e.max(o,(t=>t.data.state.rect.y+t.data.state.rect.height))||0}}setOptions(t){this.options={...this.options,...t},this.options.zoom?this.svg.call(this.zoom):this.svg.on(".zoom",null),this.options.pan?this.svg.on("wheel",this.handlePan):this.svg.on("wheel",null)}async setData(t,e){e&&this.setOptions(e),t&&(this.state.data=this._initializeData(t)),this.state.data&&(this.updateStyle(),await this.renderData())}async setHighlight(t){this.state.highlight=t||void 0,await this.renderData()}_getHighlightRect(t){const n=this.svg.node(),r=4/e.zoomTransform(n).k,i={...t.state.rect};return i.x-=r,i.y-=r,i.width+=2*r,i.height+=2*r,i}async renderData(t){const{paddingX:e,autoFit:n,color:r,maxWidth:i,lineWidth:s}=this.options,o=this.state.data;if(!o)return;const l={},h={},c=[];a(o,((t,e,n)=>{var r;(null==(r=t.payload)?void 0:r.fold)||e(),l[t.state.id]=t,n&&(h[t.state.id]=n.state.id),c.push(t)}));const d={},p={},u=t=>{t&&!d[t.state.id]&&a(t,((e,n)=>{d[e.state.id]=t.state.id,n()}))},g=t=>p[d[t.state.id]]||o.state.rect,m=t=>(l[d[t.state.id]]||o).state.rect;p[o.state.id]=o.state.rect,t&&u(t);let{highlight:f}=this.state;f&&!l[f.state.id]&&(f=void 0);let v=this.g.selectAll(X("g.markmap-highlight")).selectAll(X("rect")).data(f?[this._getHighlightRect(f)]:[]).join("rect").attr("x",(t=>t.x)).attr("y",(t=>t.y)).attr("width",(t=>t.width)).attr("height",(t=>t.height));const y=this.g.selectAll(X(Q)).each((t=>{p[t.state.id]=t.state.rect})).data(c,(t=>t.state.key)),x=y.enter().append("g").attr("data-depth",(t=>t.state.depth)).attr("data-path",(t=>t.state.path)).each((t=>{u(l[h[t.state.id]])})),k=y.exit().each((t=>{u(l[h[t.state.id]])})),w=y.merge(x).attr("class",(t=>{var e;return["markmap-node",(null==(e=t.payload)?void 0:e.fold)&&"markmap-fold"].filter(Boolean).join(" ")})),b=w.selectAll(X("line")).data((t=>[t]),(t=>t.state.key)),z=b.enter().append("line").attr("stroke",(t=>r(t))).attr("stroke-width",0),S=b.merge(z),E=w.selectAll(X("circle")).data((t=>{var e;return(null==(e=t.children)?void 0:e.length)?[t]:[]}),(t=>t.state.key)),C=E.enter().append("circle").attr("stroke-width",0).attr("r",0).on("click",((t,e)=>this.handleClick(t,e))).on("mousedown",et).merge(E).attr("stroke",(t=>r(t))).attr("fill",(t=>{var e;return(null==(e=t.payload)?void 0:e.fold)&&t.children?r(t):"var(--markmap-circle-open-bg)"})),A=this._observer,j=w.selectAll(X("foreignObject")).data((t=>[t]),(t=>t.state.key)),R=j.enter().append("foreignObject").attr("class","markmap-foreign").attr("x",e).attr("y",0).style("opacity",0).on("mousedown",et).on("dblclick",et);R.append("xhtml:div").append("xhtml:div").html((t=>t.content)).attr("xmlns","http://www.w3.org/1999/xhtml"),R.each((function(){var t;const e=null==(t=this.firstChild)?void 0:t.firstChild;A.observe(e)}));const O=k.selectAll(X("foreignObject"));O.each((function(){var t;const e=null==(t=this.firstChild)?void 0:t.firstChild;A.unobserve(e)}));const $=R.merge(j),M=c.flatMap((t=>{var e;return(null==(e=t.payload)?void 0:e.fold)?[]:t.children.map((e=>({source:t,target:e})))})),T=this.g.selectAll(X("path.markmap-link")).data(M,(t=>t.target.state.key)),H=T.exit(),B=T.enter().insert("path","g").attr("class","markmap-link").attr("data-depth",(t=>t.target.state.depth)).attr("data-path",(t=>t.target.state.path)).attr("d",(t=>{const e=g(t.target),n=[e.x+e.width,e.y+e.height];return U({source:n,target:n})})).attr("stroke-width",0).merge(T);this.svg.style("--markmap-max-width",i?`${i}px`:null),await new Promise(requestAnimationFrame),this._relayout(),v=v.data(f?[this._getHighlightRect(f)]:[]).join("rect"),this.transition(v).attr("x",(t=>t.x)).attr("y",(t=>t.y)).attr("width",(t=>t.width)).attr("height",(t=>t.height)),x.attr("transform",(t=>{const e=g(t);return`translate(${e.x+e.width-t.state.rect.width},${e.y+e.height-t.state.rect.height})`})),this.transition(k).attr("transform",(t=>{const e=m(t);return`translate(${e.x+e.width-t.state.rect.width},${e.y+e.height-t.state.rect.height})`})).remove(),this.transition(w).attr("transform",(t=>`translate(${t.state.rect.x},${t.state.rect.y})`));const _=k.selectAll(X("line"));this.transition(_).attr("x1",(t=>t.state.rect.width)).attr("stroke-width",0),z.attr("x1",(t=>t.state.rect.width)).attr("x2",(t=>t.state.rect.width)),S.attr("y1",(t=>t.state.rect.height+s(t)/2)).attr("y2",(t=>t.state.rect.height+s(t)/2)),this.transition(S).attr("x1",-1).attr("x2",(t=>t.state.rect.width+2)).attr("stroke",(t=>r(t))).attr("stroke-width",s);const N=k.selectAll(X("circle"));this.transition(N).attr("r",0).attr("stroke-width",0),C.attr("cx",(t=>t.state.rect.width)).attr("cy",(t=>t.state.rect.height+s(t)/2)),this.transition(C).attr("r",6).attr("stroke-width","1.5"),this.transition(O).style("opacity",0),$.attr("width",(t=>Math.max(0,t.state.rect.width-2*e))).attr("height",(t=>t.state.rect.height)),this.transition($).style("opacity",1),this.transition(H).attr("d",(t=>{const e=m(t.target),n=[e.x+e.width,e.y+e.height+s(t.target)/2];return U({source:n,target:n})})).attr("stroke-width",0).remove(),this.transition(B).attr("stroke",(t=>r(t.target))).attr("stroke-width",(t=>s(t.target))).attr("d",(t=>{const e=t.source,n=t.target,r=[e.state.rect.x+e.state.rect.width,e.state.rect.y+e.state.rect.height+s(e)/2],i=[n.state.rect.x,n.state.rect.y+n.state.rect.height+s(n)/2];return U({source:r,target:i})})),n&&this.fit()}transition(t){const{duration:e}=this.options;return t.transition().duration(e)}async fit(t=this.options.maxInitialScale){const n=this.svg.node(),{width:r,height:a}=n.getBoundingClientRect(),{fitRatio:s}=this.options,{x1:o,y1:l,x2:h,y2:c}=this.state.rect,d=h-o,p=c-l,u=Math.min(r/d*s,a/p*s,t),g=e.zoomIdentity.translate((r-d*u)/2-o*u,(a-p*u)/2-l*u).scale(u);return this.transition(this.svg).call(this.zoom.transform,g).end().catch(i)}findElement(t){let e;return this.g.selectAll(X(Q)).each((function(n){n===t&&(e={data:n,g:this})})),e}async ensureVisible(t,n){var r;const a=null==(r=this.findElement(t))?void 0:r.data;if(!a)return;const s=this.svg.node(),o=s.getBoundingClientRect(),l=e.zoomTransform(s),[h,c]=[a.state.rect.x,a.state.rect.x+a.state.rect.width+2].map((t=>t*l.k+l.x)),[d,p]=[a.state.rect.y,a.state.rect.y+a.state.rect.height].map((t=>t*l.k+l.y)),u={left:0,right:0,top:0,bottom:0,...n},g=[u.left-h,o.width-u.right-c],m=[u.top-d,o.height-u.bottom-p],f=g[0]*g[1]>0?tt(g,Math.abs)/l.k:0,v=m[0]*m[1]>0?tt(m,Math.abs)/l.k:0;if(f||v){const t=l.translate(f,v);return this.transition(this.svg).call(this.zoom.transform,t).end().catch(i)}}async centerNode(t,n){var r;const a=null==(r=this.findElement(t))?void 0:r.data;if(!a)return;const s=this.svg.node(),o=s.getBoundingClientRect(),l=e.zoomTransform(s),h=(a.state.rect.x+a.state.rect.width/2)*l.k+l.x,c=(a.state.rect.y+a.state.rect.height/2)*l.k+l.y,d={left:0,right:0,top:0,bottom:0,...n},p=(d.left+o.width-d.right)/2,u=(d.top+o.height-d.bottom)/2,g=(p-h)/l.k,m=(u-c)/l.k;if(g||m){const t=l.translate(g,m);return this.transition(this.svg).call(this.zoom.transform,t).end().catch(i)}}async rescale(t){const n=this.svg.node(),{width:r,height:a}=n.getBoundingClientRect(),s=r/2,o=a/2,l=e.zoomTransform(n),h=l.translate((s-l.x)*(1-t)/l.k,(o-l.y)*(1-t)/l.k).scale(t);return this.transition(this.svg).call(this.zoom.transform,h).end().catch(i)}destroy(){this.svg.on(".zoom",null),this.svg.html(null),this._disposeList.forEach((t=>{t()}))}static create(t,e,n=null){const r=new rt(t,e);return n&&r.setData(n).then((()=>{r.fit()})),r}}t.Markmap=rt,t.childSelector=X,t.defaultColorFn=z,t.defaultOptions=E,t.deriveOptions=function(t){const n={},r={...t},{color:i,colorFreezeLevel:a,lineWidth:s}=r;if(1===(null==i?void 0:i.length)){const t=i[0];n.color=()=>t}else if(null==i?void 0:i.length){const t=e.scaleOrdinal(i);n.color=e=>t(`${e.state.path}`)}if(a){const t=n.color||E.color;n.color=e=>(e={...e,state:{...e.state,path:e.state.path.split(".").slice(0,a).join(".")}},t(e))}if(s){const t=Array.isArray(s)?s:[s,0,1];n.lineWidth=S(...t)}return["duration","fitRatio","initialExpandLevel","maxInitialScale","maxWidth","nodeMinHeight","paddingX","spacingHorizontal","spacingVertical"].forEach((t=>{const e=r[t];"number"==typeof e&&(n[t]=e)})),["zoom","pan"].forEach((t=>{const e=r[t];null!=e&&(n[t]=!!e)})),n},t.globalCSS=J,t.isMacintosh=b,t.lineWidthFactory=S,t.loadCSS=async function(t){await Promise.all(t.map((t=>async function(t){const e="stylesheet"===t.type&&t.data.href||"";if(t.loaded||(t.loaded=k[e]),!t.loaded){const n=s();t.loaded=n.promise,e&&(k[e]=t.loaded),"style"===t.type?(document.head.append(v("style",{textContent:t.data})),n.resolve()):e&&(document.head.append(v("link",{rel:"stylesheet",...t.data})),fetch(e).then((t=>{if(t.ok)return t.text();throw t})).then((()=>n.resolve()),n.reject))}await t.loaded}(t))))},t.loadJS=async function(t,e){t.forEach((t=>{var e;"script"===t.type&&(null==(e=t.data)?void 0:e.src)&&y(t.data.src)})),e={getMarkmap:()=>window.markmap,...e};for(const n of t)await w(n,e)},t.refreshHook=nt,t.simpleHash=C,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})}(this.markmap=this.markmap||{},d3); 10 | //# sourceMappingURL=/sm/110abf9063f6ee4fb04c8b1324a7593e2b618205c89e4a3ba636f91260ce2b5d.map -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * config.js 3 | * 挂件默认设置和全局配置。 4 | * 5 | * 【如果修改后崩溃或运行不正常,请删除挂件重新下载,或更改前手动备份】 6 | * 请不要删除//双斜杠 7 | * 请不要删除//双斜杠注释前的英文逗号,(如果有) 8 | * 为true 或者 false的设置项,只能填这两者 9 | * 有英文双引号的设置项,只更改英文双引号内的内容,不要删除英文双引号。 10 | * */ 11 | 12 | // dev warn: Main.js多处使用Object.assign浅拷贝对象! 13 | let custom_attr = {//这里列出的是挂件的默认设置,只在创建时写入到挂件中,挂件内属性custom-list-child-docs可覆盖此设置 14 | printMode: "0",//默认格式和输出位置,参数见本文件最下方,或参考modeName(在本文件中搜索) 15 | childListId: "",//子文档列表块id,由挂件自动生成,对应的块将会被本挂件更新,请避免自行修改 16 | listDepth: 1,//列出子文档的最大层级,仅支持数字,过多层级将导致性能或其他潜在问题 17 | auto: true, //创建挂件、打开挂件时是否自动更新,如果您关闭了安全模式、使用同步且目录列表插入文档,请勿设定为true 18 | listColumn: 1,//子文档列表列数,过多的列数将导致显示问题 19 | outlineDepth: 3,//大纲列出层级数,混合列出时此项只控制大纲部分 20 | targetId: "", //统计对象id,统计的目标应当为文档块或笔记本 21 | endDocOutline: false, // 一并列出叶子文档的大纲?(目录中包括最深层级文档的大纲?)影响性能、反应极慢,建议禁用(设置为false)。(i.e.混合列出) 22 | // 如果需要默认隐藏刷新按钮,请删除下面一行前的双斜杠 23 | // hideRefreshBtn: true, 24 | sortBy: 256, //排序模式,具体取值请参考本文件最下方的DOC_SORT_TYPES,默认值15为跟随文档树排序 25 | maxListCount: 0,//控制每个文档的子文档显示数量 26 | }; 27 | // 全局设置 28 | let setting = { 29 | // 将列表写入文件时,此项控制挂件的宽 30 | width_2file: "20em", 31 | // 将列表写入文件时,此项控制挂件的高 32 | height_2file: "4em", 33 | // 将列表写入文件时,此项控制显示设置时挂件的高 34 | height_2file_setting: "9em", 35 | 36 | // 在挂件中显示自动刷新选项,设定true启用、false禁用【!自动刷新可能导致同步覆盖问题,详见README】 37 | showAutoBtn: true, 38 | // 在启动时显示所有设置项,设定true启用 39 | showSettingOnStartUp: false, 40 | // 显示搜索按钮 41 | showSearchBtn: true, 42 | 43 | // 安全模式【!建议开启,设定为true】:安全模式将禁止打开文档时自动刷新文档中的目录列表块 44 | // 可以避免此挂件自行刷新导致可能的同步覆盖问题。 45 | safeMode: false, 46 | // 安全模式PLUS【!可能因为思源版本更新而失效或导致bug,但部分情况下建议开启】 47 | // 避免在历史预览界面、编辑器只读时执行文档更改操作(目前允许挂件设置保存,请注意只读情况下设置保存的风险) 48 | // 【如果您使用自动插入助手,请启用此功能】 49 | safeModePlus: true, 50 | 51 | // 分列截断提示词(仅用于写入文档模式:url、引用块) 52 | divideIndentWord: "(续)", 53 | 54 | // 分列截断方式(仅用于写入文档模式:url、引用块 55 | // 为true: 多层级时,在缩进处截断,使每列行数相同,但层级>=2时体验不佳; 56 | // 为false,按照第一层级分列,每列行数不等 57 | divideColumnAtIndent: false, 58 | 59 | // 为true启用挂件内浮窗(挂件beta模式) 60 | floatWindowEnable: true, 61 | 62 | // 使用玄学的超级块创建方式。如果出现问题,请设置为false(测试中) 63 | superBlockBeta: true, 64 | 65 | // 混合列出时区分提示词(启用叶子文档大纲时,该提示词将被加载大纲的前面) 66 | outlineDistinguishingWords: "@", 67 | 68 | // 刷新列表后重写属性 69 | inheritAttrs: true, 70 | 71 | // 为true则一并写入文档icon Emoji 72 | emojiEnable: true, 73 | // 文档使用自定义emoji时,写入自定义emoji图片 74 | customEmojiEnable: true, 75 | 76 | // 在模式“默认”“挂件beta”下,使得挂件高度跟随目录长度自动调整 77 | autoHeight: false, 78 | // 将列表在挂件内展示、且启用自动高度,此项控制挂件的最小高度(单位px),若不限制,请设为undefined 79 | height_2widget_min: undefined, 80 | // 将列表在挂件内展示、且启用自动高度,此项控制挂件的最大高度(单位px),若不限制,请设为undefined 81 | height_2widget_max: undefined, 82 | 83 | // 【在插入挂件时表现不稳定,可能在第二次打开时设定、保存样式】挂件保存1次自身的显示样式,设置为undefined以禁用 84 | // issue #30 https://github.com/OpaqueGlass/listChildDocs/issues/30 85 | // 示例 "width: 2000px; height: 303px;" 86 | saveDefaultWidgetStyle: undefined, 87 | 88 | /* 挂件配置批量操作 89 | issue #31 https://github.com/OpaqueGlass/listChildDocs/issues/31 90 | !同步用户请注意:以下两个配置启用后挂件将在载入后更新挂件属性,未同步时可能导致同步覆盖 91 | */ 92 | // 载入挂件后以配置文件为准重写独立设置 93 | overwriteIndependentSettings: false, 94 | // 载入挂件后移除独立设置 95 | removeIndependentSettings: false, 96 | // 重载/移除设置时一并删除文档中的原目录列表块;(如果重载为文档中模式,不会执行删除) 97 | deleteChildListBlockWhileReset: true, 98 | // 独立设置重载或移除白名单 99 | // 在这里列出的文档下的挂件,不会执行独立设置重载或移除 100 | // 示例["20220815001720-4xjvir0"] 101 | overwriteOrRemoveWhiteDocList: [], 102 | 103 | // 未完成功能 插入https:// 或 http:// 协议的emoji, 104 | webEmojiEnable: false, 105 | 106 | // 在目录列表第一个加入“../”(前往父文档)(仅挂件内目录),此设定项的类型为字符串,"true"(启用)"false"(禁用)"auto"(仅窄屏设备展示) 107 | backToParent: "auto", 108 | 109 | // 挂件内时,扩大点击响应范围为整行 110 | extendClickArea: true, 111 | 112 | // 适配挂件插入辅助(addChildDocLinkHelper.js)的属性检测模式,为所在文档插入属性(不建议一直开启,请开启此功能后几天关闭) 113 | // 默认情况下,无需打开此功能 114 | addChildDocLinkHelperEnable: false, 115 | 116 | // 首次创建目录块时插入的目录属性 117 | // 请注意,您写入的属性如果是自定义属性,应当以"custom-"开头,示例 "custom-type": "map" 118 | // 请不要写入"id","update"等块固有属性 119 | blockInitAttrs: { 120 | 121 | }, 122 | 123 | // 在页签切换文档时自动刷新功能将在列出的操作系统上启用,不支持不显示页签的客户端 124 | // 若要禁用,值应当为[];如要在windows启用,["windows"];如要在多个操作系统上启用,示例:["linux", "windows"] 125 | includeOs: ["windows"], 126 | 127 | // 导图模式Markmap配置项,详见https://markmap.js.org/docs/json-options 128 | markmapConfig: {}, 129 | // 导图模式:响应挂件大小变化 130 | markmapResizeHandlerEnable: true, 131 | 132 | // 按时间分组模式,显示日期的格式,yyyy将被替换为4位年,MM将被替换为两位数月份,dd将被替换为两位数日 133 | dateTemplate: "MM-dd", 134 | // 按时间分组模式,显示时间的格式,设置为""则不显示时间,HH将被替换为小时(24小时制),mm将被替换为分钟 135 | timeTemplate: "(HH:mm)", 136 | 137 | // 缓存只对挂件中显示的模式有效 138 | // 先载入缓存,再执行自动刷新 139 | loadCacheWhileAutoEnable: false, 140 | // 在自动刷新时也自动保存缓存(!同步用户请注意:多端同步未完成时保存缓存,可能导致同步覆盖) 141 | saveCacheWhileAutoEnable: false, 142 | 143 | // 右键重命名或删除操作 144 | deleteOrRenameEnable: true, 145 | 146 | // 使用Ctrl+F作为搜索快捷键(焦点在挂件内才生效) 147 | searchHotkeyEnable: false, 148 | 149 | // 悬停显示顶部按钮栏 150 | mouseoverButtonArea: false, 151 | }; 152 | // 自动插入助手设置 153 | // 自动插入助手和挂件本体共用setting.safeModePlus(只读安全模式检查设置项),如果您使用自动插入助手,请启用此功能。 154 | let helperSettings = { 155 | // 文档中属性名称 156 | attrName: "custom-add-cdl-helper", 157 | // 模式为插入自定义时,插入的内容模板 158 | docLinkTemplate: "((%DOC_ID% '%DOC_NAME%'))", 159 | // 自动插入模式 160 | /* 【请只使用“插入挂件”模式。其他模式可能存在问题,请勿使用。】 161 | 插入挂件 【插入挂件将不重复插入(通过属性或文档为空判断)】 162 | 插入链接【可能有缺陷,不建议使用】 163 | 插入引用块【可能有缺陷,不建议使用】 164 | 插入自定义【可能有缺陷,不建议使用】 根据docLinkTemplate,插入自定义的内容 165 | */ 166 | mode: "插入挂件",// 除非您了解这部分代码实现,请不要修改模式!Do not edit it unless you understand the codes. 167 | /* 通用 */ 168 | // 插入在父文档结尾?若设置为undefined,则采用对应模式的默认设置 169 | insertAtEnd: undefined, 170 | // 在切换页签(而不是仅仅是打开)时也检查、执行自动插入 171 | switchTabEnable: false, 172 | 173 | /* 仅插入挂件模式 */ 174 | // 检查文档是否为空?设置为false,将通过文档的属性判断是否插入过。 175 | checkEmptyDocInsertWidget: true, 176 | // 选择触发时机: 177 | /* 178 | // "open": 开启空白的父文档时; 179 | // "create": 在空白父文档下创建子文档时(不建议修改为create,在这种实现方式下,将持续获取WebSocket通信并判断是否进行了创建文档操作); 180 | */ 181 | insertWidgetMoment: "open", 182 | // 要插入的挂件路径信息 183 | widgetPath: ["widgets/listChildDocs"], 184 | 185 | /* 插入链接/引用块/自定义模式 */ 186 | // 当发现子文档被删除,移除对应的子文档链接?若设置为undefined,则采用对应模式的默认设置 187 | removeLinkEnable: false, 188 | // 当发现子文档文件名变化时,重写对应的子文档链接?若设置为undefined,则采用对应模式的默认设置 189 | renameLinkEnable: false, 190 | } 191 | //全局设置 192 | let token = "";//API鉴权token,可以不填的样子(在设置-关于中查看) 193 | let zh_CN = { 194 | refreshNeeded: "更新目录失败,找不到原有无序列表块,再次刷新将创建新块。", 195 | insertBlockFailed: "创建或更新无序列表块失败,请稍后刷新重试。", 196 | writeAttrFailed: "写入挂件属性失败,请稍后刷新重试。", 197 | getPathFailed: "查询当前文档所属路径失败,请稍后刷新重试。", 198 | noChildDoc: "似乎没有子文档@_@。", 199 | error: "错误:", 200 | updateTime: "更新时间:", 201 | modifywarn: "此块由listChildDocs挂件创建,若刷新列表,您的更改将会被覆盖。", // 不想显示这个提示的话,改成空字符串""就行 202 | getAttrFailed: "读取挂件属性失败。", 203 | wrongPrintMode: "错误的输出模式设定,已恢复默认值,请刷新重试。", 204 | // 模式提示词 205 | modeName0: "默认", 206 | modeName1: "挂件beta", 207 | modeName2: "url", 208 | modeName3: "引用块", 209 | modeName5: "1.1.默认", 210 | modeName4: "1.1.挂件", 211 | modeName6: "1.url", 212 | modeName7: "1.引用块", 213 | modeName8: "1.1.url", 214 | modeName9: "任务列表", 215 | modeName10: "导图", 216 | modeName11: "预览方格", 217 | modeName12: "按日期分组", 218 | // 界面元素鼠标悬停提示词 219 | refreshBtn: "[单击] 刷新\n[双击] 保存设置", 220 | depthList: "子文档展示层级\n设置为0就可以只显示大纲啦​~\(≧▽≦)/~​​​", 221 | searchBtnTitle: "显示搜索对话框", 222 | modeList: "挂件工作模式", 223 | autoBtn: "自动刷新", 224 | autoNotWork: "\n由于启用了安全模式(safeMode),自动刷新对当前工作模式无效。", 225 | targetIdTitle: "目标文档id\n从这里指定的文档或笔记本开始列出子文档,\n设定为/则从所有已开启的笔记本开始", 226 | disabledBtnHint: "\n因为不支持当前模式,我被禁用了T^T", 227 | endDocOutlineTitle: "启用后,对于目录列表中没有子文档的,将显示大纲", 228 | hideRefreshBtnTitle: "将刷新按钮搬运到设置中,防止误触", 229 | outlineDepthTitle: "大纲层级\n大纲层级和h1、h2等无关,以大纲面板显示的层次为准。", 230 | sortByTitle: "控制文档的排序方式\n请在思源v2.8.7及以上版本使用,较早的版本可能无法排序", 231 | maxListCountTitle: "每个文档的子文档显示数量(设置为0则显示全部)\n不支持思源2.8.5以下版本", 232 | // 错误提示词 233 | getAttrFailedAtInit: "读取挂件属性失败。如果是刚创建挂件,请稍后刷新重试。", 234 | startRefresh: "开始更新子文档列表---来自listChildDocs挂件的通知", 235 | widgetRefLink: "挂件beta", 236 | saved: "设置项已保存", 237 | columnBtn: "子文档展示列数", 238 | settingBtn: "显示/隐藏设置", 239 | // 界面提示词 240 | columnHint: "分列", 241 | depthHint: "层级", 242 | noOutline: "似乎没有文档大纲@_@。", 243 | outlineDepthHint: "大纲层级", 244 | endDocOutlineHint: "叶子文档大纲", 245 | targetIdhint: "目标文档id", 246 | hideRefreshBtnHint: "隐藏刷新按钮", 247 | sortByHint: "排序方式", 248 | maxListCountHint: "子文档最大数量", 249 | autoRefreshHint: "自动刷新", 250 | working: "执行中……", 251 | loadingCache: "载入缓存中", 252 | cacheLoaded: "已载入缓存", 253 | loadCacheFailed: "未能载入文档列表缓存", 254 | wrongTargetId: "错误的目标id。目标id应为存在的文档块id、开启的笔记本id或/", 255 | readonly: "检测到只读模式,已停止对文档的更改操作。", 256 | saveDefaultStyleFailed: "保存默认挂件样式设定失败,如反复出现此问题,请禁用saveDefaultWidgetStyle。", 257 | refreshFinish: "刷新完成", 258 | refreshReject: "刷新被拒绝", 259 | refreshFailed: "刷新出错", 260 | // 自动插入助手提示 261 | helperAddBlockMemo: "自动插入的子文档链接块:在此块下的编辑将在文档变化时被覆盖", 262 | queryFilePathFailed: "获取文档路径失败,文档可能刚创建", 263 | helperErrorHint: "helper执行时发生错误,如果可以,请向开发者反馈:", 264 | // 模式内部提示10 265 | mode10_allow_pan: "启用滚轮平移", 266 | mode10_allow_zoom: "启用(Alt+滚轮)缩放与拖拽移动", 267 | mode10_hint: "折叠、滚轮缩放与拖拽移动状态将在鼠标操作后自动暂存,之后手动保存挂件设置(或双击刷新按钮)才能持久化保存。
没有看到任何内容?点击“重置缩放、平移和折叠状态”按钮,然后手动保存挂件设置", 268 | mode10_reset: "重置缩放、平移和折叠状态", 269 | mode10_default_expand_level_hint: "默认展开层级数", 270 | // 模式内部提示12 271 | mode12_doc_num_text: "展示的文档数", 272 | mode12_update_hint: "按照更新时间排列", 273 | mode12_today: "(今天)", 274 | mode12_yesterday: "(昨天)", 275 | mode12_day_ago: "(%%天前)", 276 | mode12_week_day: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"], 277 | // 模式内部提示13 278 | mode13_cannot_select_folder: "此模式只适用于桌面端(electron 或 nodejs环境),当前无法选择目录。", 279 | mode13_select_folder: "指定本地目录", 280 | mode13_not_select_folder: "您似乎没有选择目录", 281 | mode13_show_what: "显示什么?", 282 | mode13_display_path_here: "[这里显示您选择的路径]", 283 | mode13_only_folder: "仅文件夹", 284 | mode13_only_file: "仅文件", 285 | mode13_show_all: "文件夹和文件", 286 | mode13_cannot_refresh: "此模式仅支持桌面端,移动端、Docker、浏览器环境无法更新。", 287 | mode13_not_select_folder_when_refresh: "您似乎没有选择目录。请在模式设置中指定本机目标路径。", 288 | mode13_trust_sysid: "在当前系统使用相同的目录路径", 289 | mode13_another_sys_warn: "

和您选择目录时操作系统不匹配,继续刷新可能出现异常。

选择“确认”需要您为当前系统重新选择路径(其他系统路径仍然保留),选择“取消”则终止本次刷新。

", 290 | mode13_clear_all_path: "取消指定(当前系统)", 291 | mode13_error_while_select_folder: "选文件夹时出现错误,请重选", 292 | mode14_view_notfound: "没有在紧邻下方块找到数据库", 293 | mode14_first_use: "看起来这是您首次使用", 294 | mode14_first_use_content: "

由于写入不可撤销、个人开发测试不周,因此请只用于对空白数据库的写入,如果包含大量其他内容,请做好备份(深克隆数据库、不是镜像数据库);

使用此模式,请先在挂件紧邻下方块(位于同一父块)使用斜杠菜单创建数据库;使用时,请避免连续刷新。

稍后手动保存挂件设置以忽略此提示

", 295 | // 对话框dialog 296 | dialog_canceled: "已取消", 297 | dialog_delete: "删除", 298 | dialog_delete_hint: "确定要删除所选文档“%%”吗?
请注意,如果有子文档,子文档也将被一并删除。", 299 | dialog_rename: "重命名", 300 | dialog_cancel: "取消", 301 | dialog_confirm: "确定", 302 | dialog_create_doc: "新建子文档", 303 | dialog_option: "已选择", 304 | dialog_search: "搜索", 305 | dialog_search_cancel: "清除高亮", 306 | dialog_search_panel: "搜索文档标题", 307 | dialog_search_nomatch: "无结果", 308 | doc_sort_type: { 309 | FILE_NAME_ASC: "名称字母升序", 310 | FILE_NAME_DESC: "名称字母降序", 311 | NAME_NAT_ASC: "名称自然升序", 312 | NAME_NAT_DESC: "名称自然降序", 313 | MODIFIED_TIME_ASC: "修改时间升序", 314 | MODIFIED_TIME_DESC: "修改时间降序", 315 | CREATED_TIME_ASC: "创建时间升序", 316 | CREATED_TIME_DESC: "创建时间降序", 317 | REF_COUNT_ASC: "引用次数升序", 318 | REF_COUNT_DESC: "引用次数降序", 319 | DOC_SIZE_ASC: "文档大小升序", 320 | DOC_SIZE_DESC: "文档大小降序", 321 | SUB_DOC_COUNT_ASC: "子文档数量升序", 322 | SUB_DOC_COUNT_DESC: "子文档数量降序", 323 | CUSTOM_SORT: "文档树自定义排序", 324 | UNASSIGNED: "跟随文档树排序", 325 | }, 326 | // 弹层提示词 327 | removeDistinctSuccess: "成功删除%1%个挂件的独立设置。", 328 | removeDistinctFailed: "成功删除%1%个挂件的独立设置,失败%2%个。失败的挂件id分别是:%3%", 329 | removeOtherSuccess: "成功删除%1%个挂件", 330 | removeOtherFailed: "成功删除%1%个挂件,失败%2%个。失败的挂件id分别是:%3%", 331 | workResult: "结果", 332 | removeDistinctConfim: "确定要删除其他挂件的独立设置吗?
请注意:1. 挂件在文档中插入的列表也将被一并删除;
2. 受限于API查询数量限制,您可能需要多次执行此操作以确保完全删除。", 333 | removeCurrentDistinctConfim: "确定删除当前挂件独立设置吗?
请注意:1.删除后,当前挂件独立设置将跟随全局,直到下次保存独立设置。
2. 由当前挂件创建的子文档列表也将被一并删除", 334 | removeOtherConfim: "确定要删除其他挂件吗?
请注意:1. 挂件在文档中插入的列表不会被一并删除;
2. 受限于API查询数量限制,您可能需要多次执行此操作以确保完全删除。", 335 | removeFileConfirm: "确定要删除不使用的配置文件吗?
(如要删除所有配置文件,请前往工作空间/data/storage/listChildDocs手动删除)", 336 | removeFileSuccess: "成功删除%1%个配置文件。另有%2%个在使用中的配置文件未清理。", 337 | confirmTitle: "二次确认", 338 | configNameSet: "请输入配置名称", 339 | currentDoc: "当前文档", 340 | deletedSchema: "所选配置已删除", 341 | childDocsCreated: "已创建", 342 | }; 343 | let en_US = {//先当他不存在 We don't fully support English yet. 344 | refreshNeeded: "Failed to refresh directory : couldn't find original directory list block. Click refresh button again to generate a new block. ", 345 | insertBlockFailed: "Failed to create or update the child-docs list block, please try again later. ", 346 | writeAttrFailed: "Failed to write widget properties, please try again later. ", 347 | getPathFailed: "Failed to get the path of current document, please try again later. ", 348 | noChildDoc: "There appears to be no child-docs.", 349 | error: "ERROR: ", 350 | updateTime: "Last update: ", 351 | modifywarn: "Created by listChildDocs widget. Your changes to this block will be overwritten when you click refresh button in the widget", 352 | getAttrFailed: "Failed to get widget properties.", 353 | wrongPrintMode: "Wrong output mode setting, default value restored, please refresh again.", 354 | // 模式提示词 Mode Name 355 | modeName0: "Default", 356 | modeName1: "Widget beta", 357 | modeName2: "siyuan url", 358 | modeName3: "ref block", 359 | modeName5: "1.1.Default", 360 | modeName4: "1.1.Widget", 361 | modeName6: "1.url", 362 | modeName7: "1.ref block", 363 | modeName8: "1.1.url", 364 | modeName9: "todo list", 365 | modeName10: "markmap", 366 | modeName11: "preview box", 367 | modeName12: "group by date", 368 | // 界面元素鼠标悬停提示词 hangover popup words 369 | refreshBtn: "[Click] Refresh\n[Double click] Save Settings", 370 | searchBtnTitle: "Show search dialog", 371 | depthList: "The number of display levels for the child docs", 372 | modeList: "Output mode", 373 | autoBtn: "'Auto' Refresh", 374 | autoNotWork: "\nNot available for current output mode, because safe mode is enabled", 375 | targetIdTitle: "Target doc id\nAlso accept notebookid, '/' as target id.", 376 | disabledBtnHint: "\nDisabled by current mode.", 377 | endDocOutlineTitle: "For the documents that have no subdocuments, display their outline.", 378 | hideRefreshBtnTitle: "Move refresh button into settings.", 379 | outlineDepthTitle: "The number of display levels for the doc outine. ", 380 | sortByTitle: "child docs sort mode\n available in siyuan v2.8.7 and later", 381 | maxListCountTitle: "Maximum number of subdocuments to be displayed for each document. If set to 0, all documents are displayed. Versions earlier than siyuan v2.8.5 are not supported.", 382 | refreshFinish: "Refreshed. ", 383 | refreshReject: "Refresh was rejected. ", 384 | refreshFailed: "An error occurred. ", 385 | // 错误提示词error warn 386 | getAttrFailedAtInit: "Failed to read widget properties. If you just created the widget, please ignore this error and refresh again later.", 387 | startRefresh: "Updating child-doc-list ... --- list child docs widget", 388 | widgetRefLink: "Widget beta", 389 | saved: "Settings have been saved", 390 | // 界面控件提示词 Hint words 391 | columnBtn: "Number of columns", 392 | settingBtn: "Show/hide settings", 393 | columnHint: "Column", 394 | depthHint: "Level", 395 | noOutline: "There appears to be no doc-outline.", 396 | outlineDepthHint: "Outline level", 397 | endDocOutlineHint: "Leaf document outline", 398 | targetIdhint: "Target document id", 399 | hideRefreshBtnHint: "Hide refresh button", 400 | sortByHint: "Sort Mode", 401 | maxListCountHint: "Maximum of sub-docs", 402 | autoRefreshHint: "Auto refresh", 403 | // 404 | working: "Running...", 405 | loadingCache: "Loading...", 406 | cacheLoaded: "Cache loaded.", 407 | loadCacheFailed: "Couldn't load doc-list cache.", 408 | wrongTargetId: "Wrong target doc id. The target id should be an existing document id, an open notebook id or /", 409 | readonly: "Work in read-only mode. Changes to the document are prohibited.", 410 | saveDefaultStyleFailed: "Failed to save default pendant style settings. If this problem occurs repeatedly, please disable saveDefaultWidgetStyle.", 411 | // addChildDocLinkHelper hint text 412 | helperAddBlockMemo: "Child-doc link block: the edits under this block will be overwritten when the child-docs changes.", 413 | queryFilePathFailed: "Failed to get the document path, the document may have just been created.", 414 | helperErrorHint: "An error occured during helper execution. If it's convenient for you, please give feedback to the developer.", 415 | // markmap 416 | mode10_allow_pan: "Enable Pan", 417 | mode10_allow_zoom: "Enable zoom(with Alt Key)", 418 | mode10_hint: "The collapse state will be temporarily saved automatically. To make it persistent, please manually save the widget settings.
Right-click inside the mind map to temporarily save the current zoom and pan state. Then manually save the widget settings to persist it.
Not seeing any content? Click the “Reset Zoom, Pan, and Collapse State” button, then manually refresh and save the widget settings", 419 | mode10_reset: "Reset Zoom, Pan, and Collapse State", 420 | mode10_default_expand_level_hint: "Default Expand Level", 421 | // hint text in mode 422 | mode12_doc_num_text: "the num of doc", 423 | mode12_update_hint: "Order by update time", 424 | mode12_today: "(today)", 425 | mode12_yesterday: "(yesterday)", 426 | mode12_day_ago: "(%% days ago)", 427 | mode12_week_day: ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"], 428 | // 模式内部提示 tips in mode13 429 | mode13_cannot_select_folder: " This mode is only applicable in desktop environments (electron or nodejs), so you cannot select a directory at present.", 430 | mode13_select_folder: "Specify the local directory", 431 | mode13_show_what: "What should be included?", 432 | mode13_display_path_here: "[The path you selected should be displayed here", 433 | mode13_not_select_folder: "It seems you have not selected a directory.", 434 | mode13_only_folder: "Only folders", 435 | mode13_only_file: "Only files", 436 | mode13_show_all: "Folders and files", 437 | mode13_cannot_refresh: "This mode is only supported for desktop, so cannot be updated in mobile, Docker, or browser environments.", 438 | mode13_not_select_folder_when_refresh: "It seems you have not selected a directory. Please specify the local path in the mode settings.", 439 | mode13_trust_sysid: "Allow Refresh in Current System with Same Directory", 440 | mode13_another_sys_warn: "

The operating system you selected does not match. Continuing the refresh may lead to exceptions. Clicking 'Confirm' requires you to reselect the path for the current system (other system paths will remain unchanged). Clicking 'Cancel' will abort the refresh.

", 441 | mode13_mode13_clear_all_path: "Unset Directory (For Current System)", 442 | mode13_error_while_select_folder: "An error occured during selecting folder, please try again", 443 | mode14_view_notfound: "No database found in the immediately adjacent block below.", 444 | mode14_first_use: "It looks like this is your first use", 445 | mode14_first_use_content: "

Since write operations are irreversible and personal development testing is insufficient, please use this mode only for writing to a blank database. If the database contains a lot of other content, make sure to back it up first (Deep clone it, not mirror).

To use this mode, please first use the slash menu to create a database in the block immediately below the widget (within the same parent block). Please avoid refreshing continuously.

Save the widget settings manually later to ignore this prompt.

", 446 | // dialog 447 | dialog_canceled: "Canceled", 448 | dialog_delete: "Delete", 449 | dialog_delete_hint: "Are you sure you want to delete the selected document \"%%\"?
Please note that if there are subdocuments, they will also be deleted.", 450 | dialog_rename: "Rename", 451 | dialog_cancel: "Cancel", 452 | dialog_confirm: "OK", 453 | dialog_create_doc: "Create_child_doc", 454 | dialog_option: "Selected", 455 | dialog_search: "Search", 456 | dialog_search_cancel: "Clear", 457 | dialog_search_panel: "Search by Doc Name", 458 | dialog_search_nomatch: "No match", 459 | doc_sort_type: { 460 | FILE_NAME_ASC: "Name Alphabet ASC", 461 | FILE_NAME_DESC: "Name Alphabet DESC", 462 | NAME_NAT_ASC: "Name Natural ASC", 463 | NAME_NAT_DESC: "Name Natural DESC", 464 | MODIFIED_TIME_ASC: "Modified Time ASC", 465 | MODIFIED_TIME_DESC: "Modified Time DESC", 466 | CREATED_TIME_ASC: "Created Time ASC", 467 | CREATED_TIME_DESC: "Created Time DESC", 468 | REF_COUNT_ASC: "Ref Count ASC", 469 | REF_COUNT_DESC: "Ref Count DESC", 470 | DOC_SIZE_ASC: "Document Size ASC", 471 | DOC_SIZE_DESC: "Document Size DESC", 472 | SUB_DOC_COUNT_ASC: "Sub-docs Count ASC", 473 | SUB_DOC_COUNT_DESC: "Sub-docs Count DESC", 474 | CUSTOM_SORT: "Custom Sorting in the File Tree", 475 | UNASSIGNED: "Follow the File Tree" 476 | }, 477 | // 弹层提示词 478 | removeDistinctSuccess: "Successfully deleted inpendent settings for %1% widgets.", 479 | removeDistinctFailed: "Successfully removed inpendent settings for %1% widgets, failed %2%. Failed widget ids are: %3%", 480 | removeOtherSuccess: "Successfully deleted %1% widgets", 481 | removeOtherFailed: "Successfully deleted %1% widgets, failed %2%. Failed widget ids are: %3%", 482 | workResult: "Result", 483 | removeDistinctConfim: "Are you sure you want to delete the inpendent settings for other widgets?
Notice that the list created by the widget will also be deleted.", 484 | removeCurrentDistinctConfim: "Are you sure you want to delete the current widget independent settings?
Note: 1. After deletion, the current widget inpendent settings will follow the global until the next time you save settings.
2. The child docs list created by the widget will also be deleted ", 485 | removeOtherConfim: "Are you sure you want to delete other widgets?
Notice that the list created by the widget will NOT be deleted.", 486 | removeFileConfirm: "Are you sure you want to delete unused config files?
(If you want to delete all config files, please go to workspace/data/storage/listChildDocs to delete them manually)", 487 | removeFileSuccess: "Successfully deleted %1% profiles. Another %2% active profiles were not cleaned.", 488 | confirmTitle: "Secondary Confirmation", 489 | configNameSet: "Please enter the configuration name", 490 | currentDoc: "Current Doc", 491 | deletedSchema: "The selected schema has been deleted.", 492 | childDocsCreated: "Created" 493 | }; 494 | let language = zh_CN; // 使用的语言 the language in use. Only zh_CN and en_US are available. 495 | // ~~若思源设定非中文,则显示英文~~ 496 | let siyuanLanguage; 497 | try{ 498 | siyuanLanguage = window.top.siyuan.config.lang; 499 | }catch (err){ 500 | console.warn("读取语言信息失败"); 501 | } 502 | if (siyuanLanguage != "zh_CN" && siyuanLanguage != undefined) { 503 | language = en_US; 504 | } 505 | 506 | 507 | // 导入外部config.js 测试功能,如果您不清楚,请避免修改此部分; 508 | 509 | //注:下方的排序分类可能不会随着思源版本而及时更新 510 | const SORT_TYPES = { 511 | FILE_NAME_ASC: {type: 0, name: "文件名升序", englishName: "Name Alphabet ASC"}, 512 | FILE_NAME_DESC: {type: 1, name: "文件名降序", englishName: "Name Alphabet DESC"}, 513 | NAME_NAT_ASC: {type: 4, name: "名称自然升序", englishName: "Name Natural ASC"}, 514 | NAME_NAT_DESC: {type: 5, name: "名称自然降序", englishName: "Name Natural DESC"}, 515 | MODIFIED_TIME_ASC: {type: 2, name: "修改时间升序", englishName: "Modified Time ASC"}, 516 | MODIFIED_TIME_DESC: {type: 3, name: "修改时间降序", englishName: "Modified Time DESC"}, 517 | CREATED_TIME_ASC: {type: 9, name: "创建时间升序", englishName: "Created Time ASC"}, 518 | CREATED_TIME_DESC: {type: 10, name: "创建时间降序", englishName: "Created Time DESC"}, 519 | REF_COUNT_ASC: {type: 7, name: "引用次数升序", englishName: "Ref Count ASC"}, 520 | REF_COUNT_DESC: {type: 8, name: "引用次数降序", englishName: "Ref Count DESC"}, 521 | DOC_SIZE_ASC: {type: 11, name: "文档大小升序", englishName: "Document Size ASC"}, 522 | DOC_SIZE_DESC: {type: 12, name: "文档大小降序", englishName: "Document Size DESC"}, 523 | SUB_DOC_COUNT_ASC: {type: 13, name: "子文档数量升序", englishName: "Sub-docs Count ASC"}, 524 | SUB_DOC_COUNT_DESC: {type: 14, name: "子文档数量降序", englishName: "Sub-docs Count DESC"}, 525 | CUSTOM_SORT: {type: 6, name: "自定义排序", englishName: "Custom Sorting in the File Tree"}, 526 | FOLLOW_DOC_TREE: {type: 256, name: "跟随文档树排序", englishName: "Follow Doc Tree Sorting"}, 527 | }; 528 | 529 | 530 | export {custom_attr, token, language, setting, helperSettings}; 531 | /* printerMode参数 532 | 0 默认 533 | 1 挂件beta 534 | 2 url 535 | 3 引用块 536 | 4 1.1.挂件 537 | 5 1.1.默认 538 | 6 1.url 539 | 7 1.引用块 540 | 8 1.1.url 541 | 9 todo列表(任务列表)url 542 | 10 导图 543 | 11 预览方格 544 | 12 按时间分组 545 | */ -------------------------------------------------------------------------------- /src/API.js: -------------------------------------------------------------------------------- 1 | /** 2 | * API.js 3 | * 用于发送思源api请求。 4 | */ 5 | import {token, setting} from "./config.js"; 6 | import { isValidStr, logPush, warnPush, errorPush, debugPush } from "./common.js"; 7 | /**向思源api发送请求 8 | * @param data 传递的信息(body) 9 | * @param url 请求的地址 10 | */ 11 | export async function postRequest(data, url){ 12 | let result; 13 | await fetch(url, { 14 | body: JSON.stringify(data), 15 | method: 'POST', 16 | headers: { 17 | "Authorization": "Token "+token, 18 | "Content-Type": "application/json" 19 | } 20 | }).then((response) => { 21 | result = response.json(); 22 | }); 23 | return result; 24 | } 25 | 26 | export async function checkResponse4Result(response){ 27 | if (response.code != 0 || response.data == null){ 28 | return null; 29 | }else{ 30 | return response; 31 | } 32 | } 33 | 34 | /** 35 | * 检查请求是否成功,返回0、-1 36 | * @param {*} response 37 | * @returns 成功为0,失败为-1 38 | */ 39 | export async function checkResponse(response){ 40 | if (response.code == 0){ 41 | return 0; 42 | }else{ 43 | return -1; 44 | } 45 | } 46 | 47 | /**SQL(api) 48 | * @param sqlstmt SQL语句 49 | */ 50 | export async function queryAPI(sqlstmt){ 51 | let url = "/api/query/sql"; 52 | let response = await postRequest({stmt: sqlstmt},url); 53 | if (response.code == 0 && response.data != null){ 54 | return response.data; 55 | } 56 | if (response.msg != "") { 57 | throw new Error(`SQL ERROR: ${response.msg}`); 58 | } 59 | 60 | return null; 61 | } 62 | 63 | /**重建索引 64 | * @param docpath 需要重建索引的文档路径 65 | */ 66 | export async function reindexDoc(docpath){ 67 | let url = "/api/filetree/reindexTree"; 68 | let response = await postRequest({path: docpath},url); 69 | return 0; 70 | } 71 | 72 | /**列出子文件(api) 73 | * @param notebookId 笔记本id 74 | * @param path 需要列出子文件的路径 75 | * @param maxListCount 子文档最大显示数量 76 | * @param sort 排序方式(类型号) 77 | */ 78 | export async function getSubDocsAPI(notebookId, path, maxListCount = undefined, sort = undefined, showHidden = undefined){ 79 | let url = "/api/filetree/listDocsByPath"; 80 | let body = { 81 | "notebook": notebookId, 82 | "path": path, 83 | "ignoreMaxListHint": true, 84 | } 85 | if (maxListCount != undefined && maxListCount >= 0) { 86 | body["maxListCount"] = (maxListCount > 32 || maxListCount == 0) ? maxListCount : 32; 87 | } 88 | if (sort != undefined && sort != DOC_SORT_TYPES.FOLLOW_DOC_TREE && sort != DOC_SORT_TYPES.UNASSIGNED) { 89 | body["sort"] = sort; 90 | }else if (false){ 91 | let sortMode = getNotebookSortModeF(notebookId); 92 | if (sortMode) body["sort"] = sortMode; 93 | } 94 | if (showHidden != undefined) { 95 | body["showHidden"] = showHidden; 96 | } 97 | let response = await postRequest(body, url); 98 | if (response.code != 0 || response.data == null){ 99 | return new Array(); 100 | } 101 | 102 | if (maxListCount > 32 || !maxListCount || maxListCount == 0) { 103 | return response.data.files; 104 | }else{ 105 | return response.data.files.slice(0, maxListCount); 106 | } 107 | } 108 | 109 | /** 110 | * 添加属性(API) 111 | * @param attrs 属性对象 112 | * @param 挂件id 113 | * */ 114 | export async function addblockAttrAPI(attrs, blockid){ 115 | let url = "/api/attr/setBlockAttrs"; 116 | let attr = { 117 | id: blockid, 118 | attrs: attrs 119 | } 120 | let result = await postRequest(attr, url); 121 | return checkResponse(result); 122 | } 123 | 124 | /**获取挂件块参数(API) 125 | * @param blockid 126 | * @return response 请访问result.data获取对应的属性 127 | */ 128 | export async function getblockAttrAPI(blockid){ 129 | let url = "/api/attr/getBlockAttrs"; 130 | let response = await postRequest({id: blockid}, url); 131 | if (response.code != 0){ 132 | throw Error("获取挂件块参数失败"); 133 | } 134 | return response; 135 | } 136 | 137 | /** 138 | * 更新块(返回值有删减) 139 | * @param {String} text 更新写入的文本 140 | * @param {String} blockid 更新的块id 141 | * @param {String} textType 文本类型,markdown、dom可选 142 | * @returns 对象,为response.data[0].doOperations[0]的值,返回码为-1时也返回null 143 | */ 144 | export async function updateBlockAPI(text, blockid, textType = "markdown"){ 145 | let url = "/api/block/updateBlock"; 146 | let data = {dataType: textType, data: text, id: blockid}; 147 | let response = await postRequest(data, url); 148 | try{ 149 | if (response.code == 0 && response.data != null && isValidStr(response.data[0].doOperations[0].id)){ 150 | return response.data[0].doOperations[0]; 151 | } 152 | if (response.code == -1){ 153 | warnPush("更新块失败", response.msg); 154 | return null; 155 | } 156 | }catch(err){ 157 | errorPush(err); 158 | warnPush(response.msg); 159 | } 160 | return null; 161 | } 162 | 163 | /** 164 | * 插入块(返回值有删减) 165 | * @param {string} text 文本 166 | * @param {string} blockid 指定的块 167 | * @param {string} textType 插入的文本类型,"markdown" or "dom" 168 | * @param {string} addType 插入到哪里?默认插入为指定块之后,NEXT 为插入到指定块之前, PARENT 为插入为指定块的子块 169 | * @return 对象,为response.data[0].doOperations[0]的值,返回码为-1时也返回null 170 | */ 171 | export async function insertBlockAPI(text, blockid, addType = "previousID", textType = "markdown", ){ 172 | let url = "/api/block/insertBlock"; 173 | let data = {dataType: textType, data: text}; 174 | switch (addType) { 175 | case "parentID": 176 | case "PARENT": 177 | case "parentId": { 178 | data["parentID"] = blockid; 179 | break; 180 | } 181 | case "nextID": 182 | case "NEXT": 183 | case "nextId": { 184 | data["nextID"] = blockid; 185 | break; 186 | } 187 | case "previousID": 188 | case "PREVIOUS": 189 | case "previousId": 190 | default: { 191 | data["previousID"] = blockid; 192 | break; 193 | } 194 | } 195 | let response = await postRequest(data, url); 196 | try{ 197 | if (response.code == 0 && response.data != null && isValidStr(response.data[0].doOperations[0].id)){ 198 | return response.data[0].doOperations[0]; 199 | } 200 | if (response.code == -1){ 201 | warnPush("插入块失败", response.msg); 202 | return null; 203 | } 204 | }catch(err){ 205 | errorPush(err); 206 | warnPush(response.msg); 207 | } 208 | return null; 209 | 210 | } 211 | 212 | /** 213 | * 获取文档大纲 214 | * @param {string} docid 要获取的文档id 215 | * @returns {*} 响应的data部分,为outline对象数组 216 | */ 217 | export async function getDocOutlineAPI(docid){ 218 | let url = "/api/outline/getDocOutline"; 219 | let data = {"id": docid}; 220 | let response = await postRequest(data, url); 221 | if (response.code == 0){ 222 | return response.data; 223 | }else{ 224 | return null; 225 | } 226 | } 227 | 228 | /** 229 | * 插入为后置子块 230 | * @param {*} text 子块文本 231 | * @param {*} parentId 父块id 232 | * @param {*} textType 默认为"markdown" 233 | * @returns 234 | */ 235 | export async function prependBlockAPI(text, parentId, textType = "markdown"){ 236 | let url = "/api/block/prependBlock"; 237 | let data = {"dataType": textType, "data": text, "parentID": parentId}; 238 | let response = await postRequest(data, url); 239 | try{ 240 | if (response.code == 0 && response.data != null && isValidStr(response.data[0].doOperations[0].id)){ 241 | return response.data[0].doOperations[0]; 242 | } 243 | if (response.code == -1){ 244 | warnPush("插入块失败", response.msg); 245 | return null; 246 | } 247 | }catch(err){ 248 | errorPush(err); 249 | warnPush(response.msg); 250 | } 251 | return null; 252 | 253 | } 254 | /** 255 | * 插入为前置子块 256 | * @param {*} text 子块文本 257 | * @param {*} parentId 父块id 258 | * @param {*} textType 默认为markdown 259 | * @returns 260 | */ 261 | export async function appendBlockAPI(text, parentId, textType = "markdown"){ 262 | let url = "/api/block/appendBlock"; 263 | let data = {"dataType": textType, "data": text, "parentID": parentId}; 264 | let response = await postRequest(data, url); 265 | try{ 266 | if (response.code == 0 && response.data != null && isValidStr(response.data[0].doOperations[0].id)){ 267 | return response.data[0].doOperations[0]; 268 | } 269 | if (response.code == -1){ 270 | warnPush("插入块失败", response.msg); 271 | return null; 272 | } 273 | }catch(err){ 274 | errorPush(err); 275 | warnPush(response.msg); 276 | } 277 | return null; 278 | 279 | } 280 | 281 | /** 282 | * 推送普通消息 283 | * @param {string} msgText 推送的内容 284 | * @param {number} timeout 显示时间,单位毫秒 285 | * @return 0正常推送 -1 推送失败 286 | */ 287 | export async function pushMsgAPI(msgText, timeout){ 288 | let url = "/api/notification/pushMsg"; 289 | let response = await postRequest({msg: msgText, timeout: timeout}, url); 290 | if (response.code != 0 || response.data == null || !isValidStr(response.data.id)){ 291 | return -1; 292 | } 293 | return 0; 294 | } 295 | 296 | /** 297 | * 获取当前文档id(伪api) 298 | * 优先使用jquery查询 299 | */ 300 | export async function getCurrentDocIdF(){ 301 | let thisDocId; 302 | let thisWidgetId = getCurrentWidgetId(); 303 | 304 | //依靠widgetId sql查,运行时最稳定方案(但挂件刚插入时查询不到!) 305 | if (isValidStr(thisWidgetId)){ 306 | try { 307 | let queryResult = await queryAPI("SELECT root_id as parentId FROM blocks WHERE id = '" + thisWidgetId + "'"); 308 | if (!(queryResult != null && queryResult.length == 1)) { 309 | debugPush("SQL查询失败", queryResult); 310 | } 311 | if (queryResult!= null && queryResult.length >= 1){ 312 | logPush("获取当前文档idBy方案A"+queryResult[0].parentId); 313 | return queryResult[0].parentId; 314 | } 315 | } catch (error) { 316 | logPush("获取文档idBy方案A失败", error); 317 | } 318 | } 319 | 320 | try{ 321 | if (isValidStr(thisWidgetId)){ 322 | //通过获取挂件所在页面题头图的data-node-id获取文档id【安卓下跳转返回有问题,原因未知】 323 | let thisDocId = window.top.document.querySelector(`div.protyle-content:has(.iframe[data-node-id="${thisWidgetId}"]) .protyle-background`).getAttribute("data-node-id"); 324 | if (isValidStr(thisDocId)){ 325 | logPush("获取当前文档idBy方案B" + thisDocId); 326 | return thisDocId; 327 | } 328 | } 329 | 330 | }catch(err){ 331 | warnPush(err); 332 | } 333 | 334 | // 移动端文档id获取 335 | if (isMobile()) { 336 | try { 337 | // 先前是因为移动端background id更新不及时,所以使用了文档icon获取的方法 338 | let temp; 339 | temp = window.top.document.querySelector(".protyle-breadcrumb .protyle-breadcrumb__item .popover__block[data-id]")?.getAttribute("data-id"); 340 | let iconArray = window.top.document.querySelectorAll(".protyle-breadcrumb .protyle-breadcrumb__item .popover__block[data-id]"); 341 | for (let i = 0; i < iconArray.length; i++) { 342 | let iconOne = iconArray[i]; 343 | if (iconOne.children.length > 0 344 | && iconOne.children[0].getAttribute("xlink:href") == "#iconFile"){ 345 | temp = iconOne.getAttribute("data-id"); 346 | break; 347 | } 348 | } 349 | debugPush("文档图标获取当前文档id", temp); 350 | thisDocId = temp; 351 | }catch(e){ 352 | warnPush("通过文档图标获取当前文档id失败", e); 353 | temp = null; 354 | } 355 | if (!thisDocId) { 356 | thisDocId = window.top.document.querySelector(".protyle.fn__flex-1:not(.fn__none) .protyle-background")?.getAttribute("data-node-id"); 357 | debugPush("使用background的匹配值", thisDocId); 358 | } 359 | return thisDocId; 360 | } 361 | 362 | //widgetId不存在,则使用老方法(存在bug:获取当前展示的页面id(可能不是挂件所在的id)) 363 | if (!isValidStr(thisWidgetId)){ 364 | try{ 365 | thisDocId = window.top.document.querySelector(".layout__wnd--active .protyle.fn__flex-1:not(.fn__none) .protyle-background").getAttribute("data-node-id"); 366 | logPush("获取当前文档idBy方案C" + thisDocId); 367 | }catch(err){ 368 | warnPush("获取当前文档id均失败"); 369 | return null; 370 | } 371 | return thisDocId; 372 | } 373 | return null; 374 | } 375 | 376 | /** 377 | * 获取当前挂件id 378 | * @returns 379 | */ 380 | export function getCurrentWidgetId(){ 381 | try{ 382 | // 预览模式 383 | if (window.frameElement.parentElement.getAttribute("id")) { 384 | return window.frameElement.parentElement.getAttribute("id"); 385 | } 386 | if (!window.frameElement.parentElement.parentElement.dataset.nodeId) { 387 | return window.frameElement.parentElement.parentElement.dataset.id; 388 | }else{ 389 | return window.frameElement.parentElement.parentElement.dataset.nodeId; 390 | } 391 | }catch(err){ 392 | warnPush("getCurrentWidgetId window...nodeId方法失效"); 393 | return null; 394 | } 395 | } 396 | 397 | /** 398 | * 检查运行的操作系统 399 | * @return true 可以运行,当前os在允许列表中 400 | */ 401 | export function checkOs(){ 402 | try{ 403 | if (setting.includeOs.indexOf(window.top.siyuan.config.system.os.toLowerCase()) != -1){ 404 | return true; 405 | } 406 | }catch(err){ 407 | errorPush(err); 408 | warnPush("检查操作系统失败"); 409 | } 410 | 411 | return false; 412 | } 413 | /** 414 | * 删除块 415 | * @param {*} blockid 416 | * @returns 417 | */ 418 | export async function removeBlockAPI(blockid){ 419 | let url = "/api/block/deleteBlock"; 420 | let response = await postRequest({id: blockid}, url); 421 | if (response.code == 0){ 422 | return true; 423 | } 424 | warnPush("删除块失败", response); 425 | return false; 426 | } 427 | 428 | /** 429 | * 获取块kramdown源码 430 | * @param {*} blockid 431 | * @returns kramdown文本 432 | */ 433 | export async function getKramdown(blockid){ 434 | let url = "/api/block/getBlockKramdown"; 435 | let response = await postRequest({id: blockid}, url); 436 | if (response.code == 0 && response.data != null && "kramdown" in response.data){ 437 | return response.data.kramdown; 438 | } 439 | return null; 440 | } 441 | 442 | /** 443 | * 获取笔记本列表 444 | * @returns 445 | "id": "20210817205410-2kvfpfn", 446 | "name": "测试笔记本", 447 | "icon": "1f41b", 448 | "sort": 0, 449 | "closed": false 450 | 451 | */ 452 | export async function getNodebookList() { 453 | let url = "/api/notebook/lsNotebooks"; 454 | let response = await postRequest({}, url); 455 | if (response.code == 0 && response.data != null && "notebooks" in response.data){ 456 | return response.data.notebooks; 457 | } 458 | return null; 459 | } 460 | 461 | /** 462 | * 基于本地window.siyuan获得笔记本信息 463 | * @param {*} notebookId 为空获得所有笔记本信息 464 | * @returns 465 | */ 466 | export function getNotebookInfoLocallyF(notebookId = undefined) { 467 | try { 468 | if (!notebookId) return window.top.siyuan.notebooks; 469 | for (let notebookInfo of window.top.siyuan.notebooks) { 470 | if (notebookInfo.id == notebookId) { 471 | return notebookInfo; 472 | } 473 | } 474 | return undefined; 475 | }catch(err) { 476 | errorPush(err); 477 | return undefined; 478 | } 479 | } 480 | 481 | /** 482 | * 获取笔记本排序规则 483 | * (为“跟随文档树“的,转为文档树排序 484 | * @param {*} notebookId 笔记本id,不传则为文档树排序 485 | * @returns 486 | */ 487 | export function getNotebookSortModeF(notebookId = undefined) { 488 | try { 489 | let fileTreeSort = window.top.siyuan.config.fileTree.sort; 490 | if (!notebookId) return fileTreeSort; 491 | let notebookSortMode = getNotebookInfoLocallyF(notebookId).sortMode; 492 | if (notebookSortMode == DOC_SORT_TYPES.UNASSIGNED || notebookSortMode == DOC_SORT_TYPES.FOLLOW_DOC_TREE) { 493 | return fileTreeSort; 494 | } 495 | return notebookSortMode; 496 | }catch(err) { 497 | errorPush(err); 498 | return undefined; 499 | } 500 | } 501 | 502 | /** 503 | * 批量添加闪卡 504 | * @param {*} ids 505 | * @param {*} deckId 目标牌组Id 506 | * @param {*} oldCardsNum 原有牌组卡牌数(可选) 507 | * @returns (若未传入原卡牌数)添加后牌组内卡牌数, (若传入)返回实际添加的卡牌数; 返回null表示请求失败 508 | */ 509 | export async function addRiffCards(ids, deckId, oldCardsNum = -1) { 510 | let url = "/api/riff/addRiffCards"; 511 | let postBody = { 512 | deckID: deckId, 513 | blockIDs: ids 514 | }; 515 | let response = await postRequest(postBody, url); 516 | if (response.code == 0 && response.data != null && "size" in response.data) { 517 | if (oldCardsNum < 0) { 518 | return response.data.size; 519 | }else{ 520 | return response.data.size - oldCardsNum; 521 | } 522 | } 523 | warnPush("添加闪卡出错", response); 524 | return null; 525 | } 526 | 527 | /** 528 | * 批量移除闪卡 529 | * @param {*} ids 530 | * @param {*} deckId 目标牌组Id 531 | * @param {*} oldCardsNum 原有牌组卡牌数(可选) 532 | * @returns (若未传入原卡牌数)移除后牌组内卡牌数, (若传入)返回实际移除的卡牌数; 返回null表示请求失败 533 | */ 534 | export async function removeRiffCards(ids, deckId, oldCardsNum = -1) { 535 | let url = "/api/riff/removeRiffCards"; 536 | let postBody = { 537 | deckID: deckId, 538 | blockIDs: ids 539 | }; 540 | let response = await postRequest(postBody, url); 541 | if (response.code == 0 && response.data != null && "size" in response.data) { 542 | if (oldCardsNum < 0) { 543 | return response.data.size; 544 | }else{ 545 | return oldCardsNum - response.data.size; 546 | } 547 | } 548 | warnPush("移除闪卡出错", response); 549 | return null; 550 | } 551 | 552 | /** 553 | * 获取全部牌组信息 554 | * @returns 返回数组 555 | * [{"created":"2023-01-05 20:29:48", 556 | * "id":"20230105202948-xn12hz6", 557 | * "name":"Default Deck", 558 | * "size":1, 559 | * "updated":"2023-01-19 21:48:21"}] 560 | */ 561 | export async function getRiffDecks() { 562 | let url = "/api/riff/getRiffDecks"; 563 | let response = await postRequest({}, url); 564 | if (response.code == 0 && response.data != null) { 565 | return response.data; 566 | } 567 | return new Array(); 568 | } 569 | 570 | /** 571 | * 获取文件内容或链接信息 572 | * @param {*} blockid 获取的文件id 573 | * @param {*} size 获取的块数 574 | * @param {*} mode 获取模式,0为获取html;1为 575 | */ 576 | export async function getDoc(blockid, size = 5, mode = 0) { 577 | let url = "/api/filetree/getDoc"; 578 | let response = await postRequest({id: blockid, mode: mode, size: size}, url); 579 | if (response.code == 0 && response.data != null) { 580 | return response.data; 581 | } 582 | return undefined; 583 | } 584 | 585 | /** 586 | * 获取文档导出预览 587 | * @param {*} docid 588 | * @returns 589 | */ 590 | export async function getDocPreview(docid) { 591 | let url = "/api/export/preview"; 592 | let response = await postRequest({id: docid}, url); 593 | if (response.code == 0 && response.data != null) { 594 | return response.data.html; 595 | } 596 | return ""; 597 | } 598 | /** 599 | * 删除文档 600 | * @param {*} notebookid 笔记本id 601 | * @param {*} path 文档所在路径 602 | * @returns 603 | */ 604 | export async function removeDocAPI(notebookid, path) { 605 | let url = "/api/filetree/removeDoc"; 606 | let response = await postRequest({"notebook": notebookid, "path": path}, url); 607 | if (response.code == 0) { 608 | return response.code; 609 | } 610 | warnPush("删除文档时发生错误", response.msg); 611 | return response.code; 612 | } 613 | /** 614 | * 重命名文档 615 | * @param {*} notebookid 笔记本id 616 | * @param {*} path 文档所在路径 617 | * @param {*} title 新文档名 618 | * @returns 619 | */ 620 | export async function renameDocAPI(notebookid, path, title) { 621 | let url = "/api/filetree/renameDoc"; 622 | let response = await postRequest({"notebook": notebookid, "path": path, "title": title}, url); 623 | if (response.code == 0) { 624 | return response.code; 625 | } 626 | warnPush("重命名文档时发生错误", response.msg); 627 | return response.code; 628 | } 629 | 630 | export function isDarkMode() { 631 | if (window.top.siyuan) { 632 | return window.top.siyuan.config.appearance.mode == 1 ? true : false; 633 | } else { 634 | let isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches; 635 | return isDarkMode; 636 | } 637 | } 638 | 639 | /** 640 | * 通过markdown创建文件 641 | * @param {*} notebookid 笔记本id 642 | * @param {*} hpath 示例 /父文档1/父文档2/你要新建的文档名 643 | * @param {*} md 644 | * @returns 645 | */ 646 | export async function createDocWithMdAPI(notebookid, hpath, md) { 647 | let url = "/api/filetree/createDocWithMd"; 648 | let response = await postRequest({"notebook": notebookid, "path": hpath, "markdown": md}, url); 649 | if (response.code == 0 && response.data != null) { 650 | return response.data.id; 651 | } 652 | return null; 653 | } 654 | 655 | /** 656 | * 657 | * @param {*} notebookid 658 | * @param {*} path 待创建的新文档path,即,最后应当为一个随机的id.sy 659 | * @param {*} title 660 | * @returns 661 | */ 662 | export async function createDocWithPath(notebookid, path, title = "Untitled") { 663 | let url = "/api/filetree/createDoc"; 664 | let response = await postRequest({"notebook": notebookid, "path": path, "md": "", "title": title}, url); 665 | if (response.code == 0) { 666 | return true; 667 | } 668 | return false; 669 | } 670 | 671 | /** 672 | * 将对象保存为JSON文件 673 | * @param {*} path 674 | * @param {*} object 675 | * @param {boolean} format 676 | * @returns 677 | */ 678 | export async function putJSONFile(path, object, format = false) { 679 | const url = "/api/file/putFile"; 680 | const pathSplited = path.split("/"); 681 | let fileContent = ""; 682 | if (format) { 683 | fileContent = JSON.stringify(object, null, 4); 684 | } else { 685 | fileContent = JSON.stringify(object); 686 | } 687 | // File的文件名实际上无关,但这里考虑到兼容,将上传文件按照路径进行了重命名 688 | const file = new File([fileContent], pathSplited[pathSplited.length - 1], {type: "text/plain"}); 689 | const data = new FormData(); 690 | data.append("path", path); 691 | data.append("isDir", false); 692 | data.append("modTime", new Date().valueOf()); 693 | data.append("file", file); 694 | return fetch(url, { 695 | body: data, 696 | method: 'POST', 697 | headers: { 698 | "Authorization": "Token "+ token 699 | } 700 | }).then((response) => { 701 | return response.json(); 702 | }); 703 | } 704 | 705 | /** 706 | * 从JSON文件中读取对象 707 | * @param {*} path 708 | * @returns 709 | */ 710 | export async function getJSONFile(path) { 711 | const url = "/api/file/getFile"; 712 | let response = await postRequest({"path": path}, url); 713 | if (response.code == 404) { 714 | return null; 715 | } 716 | return response; 717 | } 718 | 719 | export async function getFileAPI(path) { 720 | const url = "/api/file/getFile"; 721 | let data = {"path": path}; 722 | let result; 723 | let response = await fetch(url, { 724 | body: JSON.stringify(data), 725 | method: 'POST', 726 | headers: { 727 | "Authorization": "Token "+token, 728 | "Content-Type": "application/json" 729 | } 730 | }); 731 | result = await response.text(); 732 | try { 733 | let jsonresult = JSON.parse(result); 734 | if (jsonresult.code == 404) { 735 | return null; 736 | } 737 | return result; 738 | } catch(err) { 739 | 740 | } 741 | return result; 742 | } 743 | 744 | /** 745 | * 列出工作空间下的文件 746 | * @param {*} path 例如"/data/20210808180117-6v0mkxr/20200923234011-ieuun1p.sy" 747 | * @returns isDir, isSymlink, name三个属性 748 | */ 749 | export async function listFileAPI(path) { 750 | const url = "/api/file/readDir"; 751 | let response = await postRequest({"path": path}, url); 752 | if (response.code == 0) { 753 | return response.data; 754 | } 755 | return []; 756 | } 757 | 758 | export async function removeFileAPI(path) { 759 | const url = "/api/file/removeFile"; 760 | let response = await postRequest({"path": path}, url); 761 | if (response.code == 0) { 762 | return true; 763 | } else { 764 | return false; 765 | } 766 | } 767 | 768 | /** 769 | * 添加数据库行 770 | * @param {string} avID 771 | * @param {*} srcs 结构为 id 行id或绑定块id isDetached 是否不是绑定块(绑定块为false) content 非绑定块时的内容 772 | * @param {*} previousID 指定插入位置 773 | * @param {*} ignoreFillFilter 是否忽略填充过滤器(默认true) 774 | * @returns 775 | */ 776 | export async function addAttributeViewBlocks(avID, srcs, previousID = undefined, ignoreFillFilter = undefined) { 777 | let url = "/api/av/addAttributeViewBlocks"; 778 | let postBody = { 779 | avID, 780 | srcs, 781 | previousID, 782 | ignoreFillFilter 783 | }; 784 | let response = await postRequest(postBody, url); 785 | if (response.code == 0) { 786 | return true; 787 | } 788 | warnPush("添加数据库行失败", response); 789 | return false; 790 | } 791 | 792 | // export async function addAttributeViewValues() 793 | 794 | export async function getAttributeView(id) { 795 | let url = "/api/av/getAttributeView"; 796 | let postBody = { 797 | id: id, 798 | }; 799 | let response = await postRequest(postBody, url); 800 | if (response.code == 0 && response.data != null) { 801 | return response.data.av; 802 | } 803 | return null; 804 | } 805 | 806 | export async function getAttributeViewPrimaryKeyValues(id, page=1, pageSize=32) { 807 | let url = "/api/av/getAttributeViewPrimaryKeyValues"; 808 | let postBody = { 809 | id: id, 810 | page: page, 811 | pageSize: pageSize 812 | }; 813 | let response = await postRequest(postBody, url); 814 | if (response.code == 0 && response.data != null) { 815 | return response.data; 816 | } 817 | return null; 818 | } 819 | 820 | /** 821 | * 自定义API:通过DatabaseId获取全部关联块id 822 | * @param {*} attributeViewId 823 | * @returns 824 | */ 825 | export async function getBlockIdsFromDatabase(attributeViewId) { 826 | const pageSize = Number.MAX_SAFE_INTEGER; 827 | const avResponse = await getAttributeViewPrimaryKeyValues(attributeViewId, 1, pageSize); 828 | if (!avResponse) { 829 | throw new Error("获取属性视图失败: " + attributeViewId); 830 | } 831 | if (!avResponse.rows || !avResponse.rows.values) { 832 | return []; 833 | } 834 | return avResponse.rows.values.map(value => value.blockID).filter(key => key); 835 | } 836 | 837 | export function isMobile() { 838 | return window.top.document.getElementById("sidebar") ? true : false; 839 | }; 840 | 841 | export const DOC_SORT_TYPES = { 842 | FILE_NAME_ASC: 0, 843 | FILE_NAME_DESC: 1, 844 | NAME_NAT_ASC: 4, 845 | NAME_NAT_DESC: 5, 846 | CREATED_TIME_ASC: 9, 847 | CREATED_TIME_DESC: 10, 848 | MODIFIED_TIME_ASC: 2, 849 | MODIFIED_TIME_DESC: 3, 850 | REF_COUNT_ASC: 7, 851 | REF_COUNT_DESC: 8, 852 | DOC_SIZE_ASC: 11, 853 | DOC_SIZE_DESC: 12, 854 | SUB_DOC_COUNT_ASC: 13, 855 | SUB_DOC_COUNT_DESC: 14, 856 | CUSTOM_SORT: 6, 857 | FOLLOW_DOC_TREE: 15, 858 | UNASSIGNED: 256, 859 | }; -------------------------------------------------------------------------------- /src/addChildDocLinkHelper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * addChildDocLink.js 全局监视文件创建/删除操作,向父文档插入文本内容 3 | * 此为历史遗留,现已废弃。 4 | * 此代码文件是listChildDocs的一部分,基于AGPL-3.0许可协议开源。(许可协议详见:https://www.gnu.org/licenses/agpl-3.0.txt,或本项目根目录/LICENSE文件) 5 | * THIS FILE IS A PART OF listChildDocs PROJECT, LICENSED UNDER AGPL-3.0 LICENSE (SEE AS https://www.gnu.org/licenses/agpl-3.0.txt). 6 | * @author OpaqueGlass 7 | * 8 | * 使用方法: 9 | * 设置-外观-代码片段-添加js片段: import("/widgets/listChildDocs/src/addChildDocLinkHelper.js"); 10 | * 触发方式/触发条件:websocket message事件,cmd为"create" / "removeDoc" 11 | * 依赖listChildDocs挂件的部分代码,若未经修改,不能单独作为代码片段插入。 12 | * 13 | * 代码标记说明: 14 | * WARN: 警告,这些部分可能和其他js代码冲突,或导致性能问题; 15 | * TODO: 未完成的部分; 16 | * UNSTABLE: 不稳定的实现,可能跟随版本更新而失效; 17 | */ 18 | import { 19 | queryAPI, 20 | getSubDocsAPI, 21 | addblockAttrAPI, 22 | getblockAttrAPI, 23 | appendBlockAPI, 24 | prependBlockAPI, 25 | getKramdown, 26 | removeBlockAPI, 27 | updateBlockAPI 28 | } from './API.js'; 29 | import { 30 | helperSettings, 31 | language, 32 | setting 33 | } from './config.js'; 34 | import { 35 | isSafelyUpdate, 36 | isValidStr 37 | } from './common.js'; 38 | /* 全局变量和快速自定义设置 */ 39 | let g_attrName = helperSettings.attrName; 40 | let g_docLinkTemplate = helperSettings.docLinkTemplate; 41 | // 将文本内容插入到文档末尾? 42 | let g_insertAtEnd = helperSettings.insertAtEnd; 43 | let g_mode = helperSettings.mode; 44 | let g_checkEmptyDocInsertWidget = helperSettings.checkEmptyDocInsertWidget; 45 | let g_removeLink = helperSettings.removeLinkEnable; 46 | let g_renameLink = helperSettings.renameLinkEnable; 47 | let CONSTANTS = { 48 | RANDOM_DELAY: 300, // 插入挂件的延迟最大值,300(之后会乘以10)对应最大延迟3秒 49 | OBSERVER_RANDOM_DELAY: 500, // 插入链接、引用块和自定义时,在OBSERVER_RANDOM_DELAY_ADD的基础上增加延时,单位毫秒 50 | OBSERVER_RANDOM_DELAY_ADD: 100, // 插入链接、引用块和自定义时,延时最小值,单位毫秒 51 | OBSERVER_RETRY_INTERVAL: 1000, // 找不到页签时,重试间隔 52 | } 53 | let g_observerRetryInterval; 54 | let g_observerStartupRefreshTimeout; 55 | let g_tabSwitchTimeout; 56 | let g_TIMER_LABLE_NAME_COMPARE = "acdlh子文件比对"; 57 | let g_insertWidgetPath = helperSettings.widgetPath; 58 | /* 59 | 目前支持g_mode取值为 60 | 插入挂件 add_list_child_docs 61 | 插入链接 add_link 62 | 插入引用块 add_ref 63 | 插入自定义 add_custom 64 | */ 65 | // let g_insertToParentDoc = true; 66 | // let g_insertWidgetToParent = true; 67 | const docInfoBlockTemplate = { 68 | docId: "", // 子文档id 69 | linkId: "", // 链接所在块id 70 | docName: "", // 文档名 71 | } 72 | 73 | /* ******************** 事件触发器(当发生事件时调用处理函数) ******************** */ 74 | 75 | let g_mywebsocket = window.siyuan.ws.ws; 76 | 77 | 78 | /** 79 | * 页签变更触发器 80 | * 使用当前页面监视获得触发,不会和其他页面执行冲突。但无法处理多用户的情况。 81 | * WARN: UNSTABLE: 依赖页签栏、窗口元素。 82 | */ 83 | let g_tabbarElement; 84 | // 处理找不到Element的情况,interval重试寻找 85 | let tabBarObserver = new MutationObserver((mutationList) =>{ 86 | for (let mutation of mutationList) { 87 | // console.log("发现页签变化", mutation); 88 | // if (mutation.addedNodes.length > 0) { 89 | // setTimeout(() => {tabChangeHandler(mutation.addedNodes)}, Math.round(Math.random() * CONSTANTS.OBSERVER_RANDOM_DELAY) + CONSTANTS.OBSERVER_RANDOM_DELAY_ADD); 90 | // } 91 | // 由windowObserver代管。关闭页签后,tabBar移除重设,触发器锚定的元素丢失,不会触发 92 | } 93 | }); 94 | 95 | /**处理分屏的情况:若页签栏刷新,则触发重设页签变更触发器 96 | * WARN: 依赖窗口变化 97 | * */ 98 | let windowObserver = new MutationObserver((mutationList) => { 99 | for (let mutation of mutationList) { 100 | // console.log("发现窗口变化", mutation); 101 | if (mutation.removedNodes.length > 0 || mutation.addedNodes.length > 0) { 102 | // console.log("断开Observer"); 103 | // tabBarObserver.disconnect(); 104 | switchTabObserver.disconnect(); 105 | clearInterval(g_observerRetryInterval); 106 | g_observerRetryInterval = setInterval(observerRetry, CONSTANTS.OBSERVER_RETRY_INTERVAL); 107 | } 108 | 109 | } 110 | 111 | }); 112 | 113 | let switchTabObserver = new MutationObserver(async (mutationList) => { 114 | for (let mutation of mutationList) { 115 | // console.log("发现页签切换", mutation); 116 | clearTimeout(g_tabSwitchTimeout); 117 | g_tabSwitchTimeout = setTimeout(async () => { 118 | console.time(g_TIMER_LABLE_NAME_COMPARE); 119 | try{ 120 | if (helperSettings.switchTabEnable) { 121 | if (g_mode == "插入挂件" || g_mode == "add_list_child_docs") { 122 | await tabChangeWidgetHandler([mutation.target]); 123 | }else{ 124 | await tabChangeHandler([mutation.target]); 125 | } 126 | }else { 127 | if (g_mode == "插入挂件" || g_mode == "add_list_child_docs") { 128 | await tabChangeWidgetHandler(mutation.addedNodes); 129 | }else{ 130 | await tabChangeHandler(mutation.addedNodes); 131 | } 132 | } 133 | 134 | }catch(err) { 135 | console.error(err); 136 | } 137 | console.timeEnd(g_TIMER_LABLE_NAME_COMPARE); 138 | }, Math.round(Math.random() * CONSTANTS.OBSERVER_RANDOM_DELAY) + CONSTANTS.OBSERVER_RANDOM_DELAY_ADD); 139 | } 140 | }); 141 | 142 | // 窗口变化监视器设定 143 | let g_centerLayoutElement = window.siyuan.layout.centerLayout.element; 144 | 145 | // 只有移除link为启用时才执行 146 | function startObserver() { 147 | clearInterval(g_observerRetryInterval); 148 | g_observerRetryInterval = setInterval(observerRetry, CONSTANTS.OBSERVER_RETRY_INTERVAL); 149 | windowObserver.observe(g_centerLayoutElement, {childList: true}); 150 | } 151 | /** 152 | * 重试页签监听 153 | */ 154 | function observerRetry() { 155 | g_tabbarElement = window.siyuan.layout.centerLayout.element.querySelectorAll("[data-type='wnd'] ul.layout-tab-bar"); 156 | if (g_tabbarElement.length > 0) { 157 | // console.log("重新监视页签变化"); 158 | g_tabbarElement.forEach((element)=>{ 159 | if (helperSettings.switchTabEnable) { 160 | switchTabObserver.observe(element, {"attributes": true, "attributeFilter": ["data-activetime"], subtree: true}); 161 | }else{ 162 | switchTabObserver.observe(element, {childList: true}); 163 | } 164 | // 重启监听后立刻执行检查 165 | if (element.children.length > 0) { 166 | clearTimeout(g_observerStartupRefreshTimeout); 167 | g_observerStartupRefreshTimeout = setTimeout(async () => { 168 | console.time(g_TIMER_LABLE_NAME_COMPARE); 169 | try{ 170 | if (g_mode == "插入挂件" || g_mode == "add_list_child_docs") { 171 | await tabChangeWidgetHandler(element.children); 172 | }else{ 173 | await tabChangeHandler(element.children); 174 | } 175 | }catch (err) { 176 | console.error(err); 177 | } 178 | console.timeEnd(g_TIMER_LABLE_NAME_COMPARE); 179 | }, Math.round(Math.random() * CONSTANTS.OBSERVER_RANDOM_DELAY) + CONSTANTS.OBSERVER_RANDOM_DELAY_ADD); 180 | } 181 | }); 182 | clearInterval(g_observerRetryInterval); 183 | } 184 | } 185 | /** 186 | * websocket message事件处理函数 187 | * 由于多个窗口的触发时间一致,这里通过随机延迟避开冲突。 188 | * @param {*} msg 189 | */ 190 | function websocketEventHandler(msg) { 191 | try { 192 | if (msg && msg.data){ 193 | let wsmessage = JSON.parse(msg.data); 194 | if (wsmessage.cmd == "create") { 195 | console.log(wsmessage); 196 | let random = Math.round(Math.random() * CONSTANTS.RANDOM_DELAY) * 10; // *10是为了扩大随机数之间的差距 197 | setTimeout(() => {addWidgetHandler(wsmessage.data)}, random); 198 | console.log("随机延迟", random); 199 | } 200 | // OR "transactions" "removeDoc" 201 | } 202 | }catch(err) { 203 | console.error(language["helperErrorHint"], err); 204 | g_mywebsocket.removeEventListener("message", websocketEventHandler); 205 | } 206 | } 207 | /* ******************** 事件处理(插入/移除执行函数)******************** */ 208 | 209 | /** 210 | * 处理新建文档 211 | * @param msgdata websocket信息的data属性 212 | */ 213 | async function createHandler(msgdata) { 214 | if (!isValidStr(msgdata)) return; 215 | let dividedPath = msgdata.path.split("/"); 216 | let parentDocId = dividedPath[dividedPath.length - 2]; 217 | let newDocId = msgdata.id; 218 | if (!isSafelyUpdate(parentDocId)) { 219 | console.log("只读模式,已停止操作"); 220 | return; 221 | } 222 | // 笔记本根目录下文档不处理 223 | if (parentDocId == "") return; 224 | // 获取新创建的文档名 225 | let newDocName = "Untitled"; 226 | for (let i = 0; i < msgdata.files.length; i++) { 227 | if (msgdata.files[i].path == msgdata.path) { 228 | newDocName = msgdata.files[i].name.substring(0, msgdata.files[i].name.length - 3); 229 | break; 230 | } 231 | } 232 | // 确定未被插入,生成插入链接属性信息 233 | let parentDocAttr = await getCustomAttr(parentDocId); 234 | console.log("获取到父文档属性", parentDocAttr); 235 | if (parentDocAttr && "docInfo" in parentDocAttr) { 236 | for (let docInfoItem of parentDocAttr.docInfo) { 237 | if (docInfoItem.docId == newDocId) { 238 | console.log("其他实例已经添加"); 239 | return; 240 | } 241 | } 242 | } 243 | // 处理插入文档的文本信息,进行关键词替换 244 | let insertText; 245 | insertText = g_docLinkTemplate.replace(new RegExp("%DOC_ID%", "g"), msgdata.id) 246 | .replace(new RegExp("%DOC_NAME%", "g"), newDocName); 247 | console.log(insertText); 248 | let addResponse = null; 249 | if (g_insertAtEnd) { 250 | addResponse = await appendBlockAPI(insertText, parentDocId); 251 | }else{ 252 | addResponse = await prependBlockAPI(insertText, parentDocId); 253 | } 254 | 255 | let childDocLinkId = addResponse.id; 256 | console.log(`helper已自动插入链接(${childDocLinkId})到父文档(${parentDocId})`); 257 | let newDocInfoBlock = Object.assign({}, docInfoBlockTemplate); 258 | newDocInfoBlock.docId = newDocId; 259 | newDocInfoBlock.linkId = childDocLinkId; 260 | if (parentDocAttr && "docInfo" in parentDocAttr) { 261 | parentDocAttr.docInfo.push(newDocInfoBlock); 262 | }else if (parentDocAttr){ 263 | parentDocAttr["docInfo"] = [newDocInfoBlock]; 264 | }else{ 265 | parentDocAttr = {}; 266 | parentDocAttr["docInfo"] = [newDocInfoBlock]; 267 | } 268 | 269 | // 保存链接信息 270 | console.log("写入", parentDocAttr); 271 | await saveCustomAttr(parentDocId, parentDocAttr); 272 | } 273 | 274 | 275 | async function isDocEmpty(docId) { 276 | // 检查父文档是否为空 277 | // 获取父文档内容 278 | let parentDocContent = await getKramdown(docId); 279 | // 简化判断,过长的父文档内容必定有文本,不插入 // 作为参考,空文档的kramdown长度约为400 280 | if (parentDocContent.length > 1000) { 281 | console.log("父文档较长,认为非空,不插入挂件", parentDocContent.length); 282 | return; 283 | } 284 | // console.log(parentDocContent); 285 | // 清理ial和换行、空格 286 | let parentDocPlainText = parentDocContent; 287 | // 清理ial中的对象信息(例:文档块中的scrool字段),防止后面匹配ial出现遗漏 288 | parentDocPlainText = parentDocPlainText.replace(new RegExp('\\"{[^\n]*}\\"', "gm"), "\"\"") 289 | // console.log("替换内部对象中间结果", parentDocPlainText); 290 | // 清理ial 291 | parentDocPlainText = parentDocPlainText.replace(new RegExp('{:[^}]*}', "gm"), ""); 292 | // 清理换行 293 | parentDocPlainText = parentDocPlainText.replace(new RegExp('\n', "gm"), ""); 294 | // 清理空格 295 | parentDocPlainText = parentDocPlainText.replace(new RegExp(' +', "gm"), ""); 296 | console.log(`父文档文本(+标记)为 ${parentDocPlainText}`); 297 | console.log(`父文档内容为空?${parentDocPlainText == ""}`); 298 | if (parentDocPlainText != "") return false; 299 | return true; 300 | } 301 | 302 | /** 303 | * 处理添加挂件 304 | * @param msgdata websocket信息的data属性 305 | */ 306 | async function addWidgetHandler(msgdata) { 307 | if (!isValidStr(msgdata)) return; 308 | let dividedPath = msgdata.path.split("/"); 309 | let parentDocId = dividedPath[dividedPath.length - 2]; 310 | let newDocId = msgdata.id; 311 | if (!isSafelyUpdate(parentDocId, {"targetDoc": false})) { 312 | console.log("只读模式,已停止操作"); 313 | return; 314 | } 315 | if (parentDocId == "") return; 316 | if (g_checkEmptyDocInsertWidget) { 317 | // 检查父文档是否为空 318 | if (!await isDocEmpty(parentDocId)) return; 319 | }else{ 320 | // 获取父文档属性,判断是否插入过挂件 321 | let parentDocAttr = await getblockAttrAPI(parentDocId).data; 322 | if (parentDocAttr != undefined && "id" in parentDocAttr && g_attrName in parentDocAttr) { 323 | return; 324 | } 325 | } 326 | let addedWidgetIds = []; 327 | // 若未插入/文档为空,则插入挂件 328 | for (let widgetPath of g_insertWidgetPath) { 329 | let insertText = ``; 330 | let addResponse; 331 | if (g_insertAtEnd) { 332 | addResponse = await appendBlockAPI(insertText, parentDocId); 333 | }else{ 334 | addResponse = await prependBlockAPI(insertText, parentDocId); 335 | } 336 | if (addResponse == null) { 337 | console.warn(`helper插入挂件失败`, widgetPath); 338 | }else{ 339 | addedWidgetIds.push(addResponse.id); 340 | } 341 | } 342 | 343 | // 写入文档属性 344 | if (!g_checkEmptyDocInsertWidget) { 345 | let attr = {}; 346 | attr[g_attrName] = "{}"; 347 | await addblockAttrAPI(attr, parentDocId); 348 | } 349 | console.log(`helper已自动插入挂件块${addedWidgetIds},于父文档${parentDocId}`); 350 | } 351 | 352 | 353 | /** 354 | * 处理页签变化,对打开的空白父文档执行插入挂件操作 355 | * @param {*} addedNodes 356 | */ 357 | async function tabChangeWidgetHandler(addedNodes) { 358 | let openDocIds = []; 359 | let safelyUpdateFlag = true; 360 | // WARN: UNSTABLE: 获取打开Tab的对应文档id 361 | // addedNodes.forEach(element => { 362 | // 重启监听后立刻执行检查时,传入的addedNodes类型为HTMLCollections,不支持forEach 363 | [].forEach.call(addedNodes, (element) => { 364 | let docDataId = element.getAttribute("data-id"); 365 | // document.querySelector("div[data-id='7fadb0ac-e27d-4d2a-b910-0a8b5c185162']").querySelector(".protyle-background").getAttribute("data-node-id") 366 | if (document.querySelector(`div[data-id="${docDataId}"]`) == null) return; 367 | if (document.querySelector(`div[data-id="${docDataId}"]`).querySelector(".protyle-background") == null) return; 368 | let openDocId = document.querySelector(`div[data-id="${docDataId}"]`).querySelector(".protyle-background").getAttribute("data-node-id"); 369 | if (!isSafelyUpdate(openDocId)) { 370 | safelyUpdateFlag = false; 371 | return; 372 | } 373 | openDocIds.push(openDocId); 374 | }); 375 | if (!safelyUpdateFlag) { 376 | console.log("只读模式,已停止操作"); 377 | return; 378 | } 379 | console.log("刚开启的页签文档id", openDocIds); 380 | if (openDocIds.length <= 0) return; 381 | for (let docId of openDocIds) { 382 | // 判断是否为父文档 383 | let queryResult = await queryAPI(`SELECT * FROM blocks WHERE path like '%${docId}/%' and type = "d"`); 384 | console.log("子文档信息", queryResult); 385 | if (!isValidStr(queryResult) || queryResult <= 0) { 386 | console.log("并非父文档"); 387 | return; 388 | } 389 | // 判断父文档是否为空 390 | if (g_checkEmptyDocInsertWidget) { 391 | // 检查父文档是否为空 392 | if (!await isDocEmpty(docId)) return; 393 | }else{ 394 | // 获取父文档属性,判断是否插入过挂件 395 | let parentDocAttr = await getblockAttrAPI(docId).data; 396 | if (parentDocAttr != undefined && "id" in parentDocAttr && g_attrName in parentDocAttr) { 397 | return; 398 | } 399 | } 400 | // 执行插入 401 | let addedWidgetIds = []; 402 | // 若未插入/文档为空,则插入挂件 403 | for (let widgetPath of g_insertWidgetPath) { 404 | let insertText = ``; 405 | let addResponse; 406 | if (g_insertAtEnd) { 407 | addResponse = await appendBlockAPI(insertText, docId); 408 | }else{ 409 | addResponse = await prependBlockAPI(insertText, docId); 410 | } 411 | if (addResponse == null) { 412 | console.warn(`helper插入挂件失败`, widgetPath); 413 | }else{ 414 | addedWidgetIds.push(addResponse.id); 415 | } 416 | } 417 | 418 | // 写入文档属性 419 | if (!g_checkEmptyDocInsertWidget) { 420 | let attr = {}; 421 | attr[g_attrName] = "{}"; 422 | await addblockAttrAPI(attr, docId); 423 | } 424 | console.log(`helper已自动插入挂件块${addedWidgetIds},于父文档${docId}`); 425 | } 426 | } 427 | 428 | /** 429 | * 处理页签节点变化 430 | * 本部分只检查并执行删除链接,不检查新增 431 | */ 432 | // TODO: 打开页签时,刷新判断其下子文档变动,进行增加、移除操作(对文档属性,检查其在子文档中是否存在) 433 | async function tabChangeHandler(addedNodes) { 434 | let openDocIds = []; 435 | let safelyUpdateFlag = true; 436 | // WARN: UNSTABLE: 获取打开Tab的对应文档id 437 | // addedNodes.forEach(element => { 438 | // 重启监听后立刻执行检查时,传入的addedNodes类型为HTMLCollections,不支持forEach 439 | [].forEach.call(addedNodes, (element) => { 440 | let docDataId = element.getAttribute("data-id"); 441 | // document.querySelector("div[data-id='7fadb0ac-e27d-4d2a-b910-0a8b5c185162']").querySelector(".protyle-background").getAttribute("data-node-id") 442 | if (document.querySelector(`div[data-id="${docDataId}"]`) == null) return; 443 | if (document.querySelector(`div[data-id="${docDataId}"]`).querySelector(".protyle-background") == null) return; 444 | let openDocId = document.querySelector(`div[data-id="${docDataId}"]`).querySelector(".protyle-background").getAttribute("data-node-id"); 445 | if (!isSafelyUpdate(openDocId)) { 446 | safelyUpdateFlag = false; 447 | return; 448 | } 449 | openDocIds.push(openDocId); 450 | }); 451 | if (!safelyUpdateFlag) { 452 | console.log("只读模式,已停止操作"); 453 | return; 454 | } 455 | console.log("刚开启的页签文档id", openDocIds); 456 | if (openDocIds.length <= 0) return; 457 | for (let docId of openDocIds) { 458 | let queryResult = await queryAPI(`SELECT box, path FROM blocks WHERE id = '${docId}'`); 459 | if (queryResult == null || queryResult.length < 1) { 460 | console.warn("获取文档路径失败,文档可能刚创建"); 461 | return; 462 | } 463 | let subDocInfoList = await getSubDocsAPI(queryResult[0].box, queryResult[0].path); 464 | console.log("API子文档信息", subDocInfoList); 465 | if (subDocInfoList == null) { 466 | console.warn("获取子文档无结果"); 467 | return; 468 | } 469 | // 读取属性,获取原来的内容 470 | let docCustomAttr = await getCustomAttr(docId); 471 | if (!docCustomAttr || !("docInfo" in docCustomAttr)) { 472 | console.log("属性为空", docCustomAttr); 473 | docCustomAttr = { 474 | "docInfo": [] 475 | }; 476 | } 477 | // 由于赋值的是引用,修改会同步。 478 | let docInfos = docCustomAttr.docInfo; 479 | console.log("属性中的文件信息", docInfos); 480 | // 已经被删除的文档在属性列表中的下标(需要被移除的链接) 481 | let removeIndex = []; 482 | // 已经添加过链接的子文档在API请求列表中的下标 483 | let existDocSubDocIndex = []; 484 | // 需要修改文件名的文档在两个列表中的下标 485 | let renameNeededDocIndexBlockList = []; 486 | let needUpdateAttrFlag = false; 487 | docInfos.forEach(async (addedDocInfoBlock, index) => { 488 | let currentDocExistFlag = false; 489 | for (let [subDocIndex, subDocInfo] of subDocInfoList.entries()) { 490 | if (addedDocInfoBlock.docId == subDocInfo.id) { 491 | currentDocExistFlag = true; 492 | existDocSubDocIndex.push(subDocIndex); 493 | if (addedDocInfoBlock.docName != subDocInfo.name.substring(0, subDocInfo.name.length - 3)) { 494 | // renameIndexInfo 对象格式: 495 | renameNeededDocIndexBlockList.push({ 496 | attrListIndex: index, 497 | subDocListIndex: subDocIndex 498 | }); 499 | } 500 | break; 501 | } 502 | } 503 | if (!currentDocExistFlag) { 504 | removeIndex.push(index); 505 | await removeBlockAPI(addedDocInfoBlock.linkId); 506 | } 507 | }); 508 | console.log("Remove Indexes", removeIndex); 509 | // 重命名(依赖原有属性中文件列表的顺序,在执行此部分之前,不要增加/删除docInfos数组中的元素) 510 | if (renameNeededDocIndexBlockList.length > 0 && g_renameLink) { 511 | console.log("需要重命名的链接", renameNeededDocIndexBlockList); 512 | for (let renameIndexInfo of renameNeededDocIndexBlockList) { 513 | let docName = subDocInfoList[renameIndexInfo.subDocListIndex].name; 514 | docName = docName.substring(0, docName.length - 3); 515 | docInfos[renameIndexInfo.attrListIndex].docName = docName; 516 | // 更新链接 517 | let updateText; 518 | updateText = g_docLinkTemplate.replace(new RegExp("%DOC_ID%", "g"), subDocInfoList[renameIndexInfo.subDocListIndex].id) 519 | .replace(new RegExp("%DOC_NAME%", "g"), docName); 520 | updateText += `\n{: memo=\"${language["helperAddBlockMemo"]}\"}`; 521 | let updateResponse = await updateBlockAPI(updateText, docInfos[renameIndexInfo.attrListIndex].linkId); 522 | // 更新失败的块,移除 523 | if (updateResponse == null) { 524 | console.warn(`对应文档${docInfos[renameIndexInfo.attrListIndex].docId}的子文档链接块${docInfos[renameIndexInfo.attrListIndex].linkId}更新失败,该块的记录将被移除,稍后重新创建。`); 525 | removeIndex.push(renameIndexInfo.attrListIndex); 526 | let removeInfoBlockIndex = existDocSubDocIndex.indexOf(renameIndexInfo.subDocListIndex); 527 | if (removeInfoBlockIndex != -1) { 528 | existDocSubDocIndex.splice(removeInfoBlockIndex, 1); 529 | } 530 | } 531 | } 532 | needUpdateAttrFlag = true; 533 | }else{ 534 | if (g_renameLink) console.log("未发现子文档文档名变化"); 535 | } 536 | // 删除 537 | if (removeIndex.length > 0 && g_removeLink) { 538 | let removedIds = []; 539 | for (let i = removeIndex.length - 1; i >= 0; i--) { 540 | removedIds.push(docInfos[i].docId); 541 | docInfos.splice(removeIndex[i], 1); 542 | } 543 | needUpdateAttrFlag = true; 544 | }else{ 545 | if (g_removeLink) console.log("未发现文档被删除"); 546 | } 547 | 548 | // 新增文档链接,这些文档链接直接插入到属性中文件列表,在此操作之后,不要删除docInfos中的元素 549 | if (existDocSubDocIndex.length != subDocInfoList.length) { 550 | for (let [index, subDocInfo] of subDocInfoList.entries()) { 551 | if (existDocSubDocIndex.indexOf(index) == -1) { 552 | let newDocInfoBlock = Object.assign({}, docInfoBlockTemplate); 553 | newDocInfoBlock.docId = subDocInfo.id; 554 | newDocInfoBlock.docName = subDocInfo.name.substring(0, subDocInfo.name.length - 3); 555 | let insertText; 556 | insertText = g_docLinkTemplate.replace(new RegExp("%DOC_ID%", "g"), newDocInfoBlock.docId) 557 | .replace(new RegExp("%DOC_NAME%", "g"), newDocInfoBlock.docName); 558 | insertText += `\n{: memo=\"${language["helperAddBlockMemo"]}\"}`; 559 | let addResponse; 560 | if (g_insertAtEnd) { 561 | addResponse = await appendBlockAPI(insertText, docId); 562 | }else{ 563 | addResponse = await prependBlockAPI(insertText, docId); 564 | } 565 | let newLinkId = addResponse.id; 566 | newDocInfoBlock.linkId = newLinkId; 567 | docInfos.push(newDocInfoBlock); 568 | // console.log("插入新文档信息块", newDocInfoBlock); 569 | } 570 | } 571 | needUpdateAttrFlag = true; 572 | }else{ 573 | console.log("未发现新增文档"); 574 | } 575 | if (!needUpdateAttrFlag) return; 576 | console.log("修改后", docInfos); 577 | await saveCustomAttr(docId, docCustomAttr); 578 | } 579 | 580 | return; 581 | } 582 | 583 | /* ******************** 工具方法 ******************** */ 584 | 585 | 586 | async function getCustomAttr(parentDocId) { 587 | let docAttrResponse = await getblockAttrAPI(parentDocId); 588 | docAttrResponse = docAttrResponse.data; 589 | if (docAttrResponse == undefined || !("id" in docAttrResponse) || !(g_attrName in docAttrResponse)) { 590 | console.log("未获取到父文档属性"); 591 | return null; 592 | } 593 | 594 | return JSON.parse(docAttrResponse[g_attrName].replace(new RegExp(""", "g"), "\"")); 595 | } 596 | 597 | async function saveCustomAttr(parentDocId, customAttr) { 598 | let attrString = JSON.stringify(customAttr); 599 | let attr = {}; 600 | attr[g_attrName] = attrString; 601 | let response = await addblockAttrAPI(attr, parentDocId); 602 | } 603 | 604 | /* ******************** 模式切换 ******************** */ 605 | 606 | console.log(`用户设定:插入到结尾?${g_insertAtEnd} 移除?${g_removeLink} 重命名?${g_renameLink}`); 607 | switch (g_mode) { 608 | case "插入挂件": 609 | case "add_list_child_docs":{ 610 | if (!g_insertAtEnd) g_insertAtEnd = false; 611 | /* 612 | * 添加触发器,任意操作均触发,只在新建文件行为发生时执行; 613 | * WARN: 编辑过程中会高频触发,可能导致卡顿; 614 | */ 615 | if (setting.safeModePlus && helperSettings.insertWidgetMoment == "create") g_mywebsocket.addEventListener("message", websocketEventHandler); 616 | if (setting.safeModePlus && helperSettings.insertWidgetMoment == "open") startObserver(); 617 | break; 618 | } 619 | 620 | case "插入链接": 621 | case "add_link": { 622 | g_docLinkTemplate = "[%DOC_NAME%](siyuan://blocks/%DOC_ID%)"; 623 | if (g_insertAtEnd == undefined || g_insertAtEnd == null) g_insertAtEnd = true; 624 | if (g_removeLink == undefined || g_removeLink == null) g_removeLink = true; 625 | if (g_renameLink == undefined || g_renameLink == null) g_renameLink = true; 626 | if (setting.safeModePlus) startObserver(); 627 | break; 628 | } 629 | 630 | case "add_ref": 631 | case "插入引用块": { 632 | g_docLinkTemplate = "((%DOC_ID% '%DOC_NAME%'))"; 633 | if (g_insertAtEnd == undefined || g_insertAtEnd == null) g_insertAtEnd = true; 634 | if (g_removeLink == undefined || g_removeLink == null) g_removeLink = true; 635 | if (g_renameLink == undefined || g_renameLink == null) g_renameLink = false; 636 | if (setting.safeModePlus) startObserver(); 637 | break; 638 | } 639 | 640 | case "add_custom": 641 | case "插入自定义": { 642 | if (g_insertAtEnd == undefined || g_insertAtEnd == null) g_insertAtEnd = true; 643 | if (g_removeLink == undefined || g_removeLink == null) g_removeLink = false; 644 | if (g_renameLink == undefined || g_renameLink == null) g_renameLink = false; 645 | if (setting.safeModePlus) startObserver(); 646 | break; 647 | } 648 | 649 | default: { 650 | console.error("不支持的模式,请检查模式设置是否正确 / Unsupported mode, check your input please."); 651 | } 652 | } 653 | 654 | if (!setting.safeModePlus){ 655 | console.warn("自动插入助手只在开启只读安全模式(safeModePlus)的前提下运行"); 656 | } 657 | 658 | console.log(`未设定值修改为默认后:插入到结尾?${g_insertAtEnd} 移除?${g_removeLink} 重命名?${g_renameLink}`); --------------------------------------------------------------------------------