第二次漫游:双链、xanadu 与渐进学习
这是一次漫游,不同于之前的文章,主要进行一个发散式的闲聊,以及共享我思考的上下文。
渐进学习手册已经修稿完了,增删了一些内容,再加上这两天做的这张图就能完结了:
我把图中提到的可选选项都丢到了文章末尾,有需求直接去拿。
Roam 式双链用法其实一直挺简单的,没有分类、文档树这些元素,点进 DailyNotes 里记就完了,必要时打个 [[块引用]]
点进去看反链汇总,没什么复杂的,回归辅助思考的本质。
可能是太过简单,导致使用起来反而有压力,所以需要社区有讨论,让社区来进行背书:放心用,大胆记,真的就这么简单,别整那些复杂的玩具浪费动力。如果你觉得这样记笔记会发展成屎山,那么其它方法也一样会成屎山。
比如我 2020 年的时候就老问我的思源启蒙老师 @zhangzz “真的就这么简单?”,反复确认几次才敢这么用 DailyNotes 。
记录、整理存在着压力
- 我用双链的时候正巧碰到 @deerain 来游说反链改版,后来又写了双向链接时代的快速无压记录,这里哈桑老师敏锐地察觉到 Roam 带来的使用体验不在“互相关联与网状”上,而在于记录与整理的压力处理上。
- 当时我一听,这和我的经验相符啊,关联知识点确实没感觉到对我有啥用,传统的先分类再进去记笔记步骤太多了,写几次就烦了,直接进行一个摆。
- 也就从这里,我开始了第 N 次笔记入门,当时写下了实践中对双链和写作的一些思考。
很多人被传统记录流程拦在了门前,包括我
- 在这里我想共享的视角是,像之前的我一样不记录的人也是有的。
- 我没有记录的内在动力,看到炫酷的展示我也会尝试两天,然后放弃。
- 记录和整理对我而言太累太累,最重要的是,之前的我从未感受到过记录和整理有任何意义或者奖励,它对我的学习而言没有任何帮助。
- 大多有内在动力的人无法理解,他们从笔记里获得了激励,或者养成了习惯发展为了爱好,但对我来说,笔记的记录和整理过程里只有无聊,以及动力的损耗。
- 像我这样被挡在门槛前的人,因为从未进门,所以也很少有人在门里听见我们的声音。
笔记的不同使用目的
- 笔记这个词太过宽泛,并不能反映我们的具体需求,例如有些人只是想要个资料库,进行一些信息管理,有些人是想借助工具帮助自己成长,这是完全不同的需求,我最早在为什么任何笔记系统在 SuperMemo 面前都是垃圾 (如 OneNote 和 Evernote)的评论区里看到这种矛盾,这是不深入笔记社区就无法了解到的。
- 无论国内还是国外都是。
为什么我不说知识管理
- 大家对知识管理的定义也很模糊,当我提到知识管理也会被理解为资料管理、创意管理、参考管理、资源管理等等,所以不如直接描述自己的目的,比如:发展自己的想法,让自己变得更有洞察力
- 而我的目的,只是想让自己有积极起来的欲望。
是表达促进了学习,而不是记录和整理
- 在笔记社区另一个有趣的点就是讨论整理,分目的来讨论的话,有些信息管理是要去做整理的,比如个人的重要的证件信息、密码之类的。
- 但如果有人告诉你不去整理就无法学习成长,别信,有坑。
- 这就像有人和你说记笔记就能获得好成绩一样,太扯了我的朋友。
- 如果记笔记的过程中有什么能促进学习,那只有表达,表达促进了学习,学习又增强了表达,只有见理不明才会说话不清。
表达也存在着压力
-
将语言组织成有逻辑的长句对我来说有点吃力,之前记笔记被劝退也有这方面的原因,后来学到了使用大纲快速固定想法:
- 只敲下想法的关键词,回车,缩进,写下新的想法关键词,不考虑修辞,不考虑逻辑。
-
这时候我才明白,为什么 FLY 老哥说他的块引用会在很深的大纲层级中使用,而不仅仅是在大纲首项中用。
-
这种速记有让我爽到,它让我的记录第一次跟上了思考的速度,以往我都是写写停停,琢磨下修辞,然后开始走神犯困。
哈桑的 DailyNotes 流程
- 有点混淆的是,哈桑老师分享的是自己解析出来的个人流程,有 Page 级的引用和上下文反链,但是没怎么说块级引用。
- 在【笔记方法分享】conor 的写作工作流 | 块引用的作用里看出块级引用的作用,就在 FLY 老哥分享的 Conor 工作流里,我学到了第二个关于双链的知识点:DailyNotes 中的记录是一种记忆的锚点,可以通过记录的上下文事件唤醒记忆,它也可以用来索引。
- 所以准确的说,DailyNotes 流程的全称应该是“哈桑的 DailyNotes 流程”。
ADHD、Xanadu 与非线性写作
-
提出 Project Xanadu 的 Ted Nelson 有严重的注意力缺陷障碍(Attention Deficit Disorder, ADD),他很容易迷失在大脑产生的疯狂的大量联想中,同样他也无法忍受传统写作和结构:
-
Ted Nelson 喜欢文字,它是记忆的工具,但他讨厌传统的写作和编辑方式强加了一个虚假的的和有限的秩序。他对书中流畅、渐进的叙述不感兴趣。他希望所有的东西都被保存在所有混乱的流动之中,这样就可以根据需要重建了。——上都的诅咒(The Curse of Xanadu)
-
Ted Nelson 希望将内容(content)从结构(structure)中分开,这样同一个内容可以有多个结构,读者和作者可以根据自己的需求从不同的角度看内容。计算机历史人物中的另类,Ted Nelson:将想法和表达从四方的纸质监狱中解放出来
-
-
在了解 Ted Nelson 之前我对 Project Xanadu 并没有什么共鸣,那些愿景离我太远,什么对信息追溯到其来源、看到谁引用了原版重新混合和扩展了原版,什么出版系统付费啊,这都是多人模式下的功能,我是单机玩家没什么实感。
-
但是当知道他的注意力缺陷障碍后,这我懂啊,切换了视角不去看那些愿景,而是基于 Ted Nelson 对注意力解放的追求来看,Project Xanadu 也可以拿来当非线性写作用:
-
可以先尽情写一堆可能用到或用不到的想法片段,而选择展示哪些內容,如何组织起來则是分成了另一件任务。
-
双向链接就可以用来跟踪所有不同的想法,因为这些想法会发展成不同的写作路径,同一个想法可能存在于不同的上下文语境中。
-
沿着链接能激活不同的上下文语境,因为都是自己思考的内容,所以很容易激活大脑中的相关记忆,就像在自己的大脑中漫游,SuperMemo18 加入的神经学习就是类似这种模式。
-
当然,前提是你链接的是自己大脑里的想法,不是完全陌生的信息,我试过链接到资料,漫游到它们时得停好久重新阅读,很不爽。
神经学习、想法与双链
- SuperMemo18 加入的神经学习有一个链接功能,不过是单链,可以手动建立双链。
- 刚开始用神经学习时我就热衷于打链接,使用后感觉再次上当了:这玩意跟双链的互相关联一样没啥效果啊。
- 初期我是拿来当分类用,后来发现不行,和双链软件一样完全没效果,和 SM 社区交流后发现得链接重要的 idea,不能只是分类、预整理:
-
【概念】和【链接】是想要将我们脑内的知识网络映射出来,【概念】就应该是脑子里一瞬间能反应过来的事物,而不是庞大臃肿的分类学集合,那些包含太多卡片的【概念】,其内部的卡片间逻辑无法真正映射到脑中,事物间的联系被模糊了,而这应该是神经学习使用者不想见到的。
- 嘿巧了,在思源里也能这么用双链啊!从那时起我开始用双链发展我的想法,不只是单纯的信息记录和整理。
- 现在以我的三年来的经验来看:双链对知识和想法的组织极其有用
- 我无法告诉你它对信息管理用处大不大,只从记录的视角来看它改变了规则,将整理的压力转移到未来,但就像【共同探讨】移动块 / 反链 / 快速无压记录 / 标签 评论区种所讨论的那样:“整理的压力一直都有,无论是现在还是未来,它都在那里,区别只在于你什么时候处理这个压力。”
- 这句话对信息管理来说是对的,但如果你像我一样是为了发展想法、获得成长来使用思源,那规则完全不一样了,我们完全不需要在思源里整理数据。
- 我们只需要在思源里发展想法,进行表达。当进行表达的那一刻,就已经做出了在大脑里进行了整理。
- 这种需求不需要必须建立索引,记录的上下文事件、想法之间的联系都能帮助定位到笔记,它们的使用成本又很低。
- 也就是说我们在大脑里建立索引,而不是在笔记里。
- 除此之外整理信息是不会获得内在奖励的,它会损耗你的内在动力,但发展想法会产生内在奖励,提升你的内在动力,这是极大的不同,也是为什么使用双链和大纲能越写越爽。
- 现在就剩下一个问题,哈桑老师所说的双链无压到底是不是 Roam 故意设计出来的?
ADHD、Conor White-Sullivan 和上下文反链
-
RoamResearch 刚开始可以看成 Project Xanadu 的单机版,现在有没有加入类似 Xanadu 的愿景我不好说,但是仅从设计来看,RoamResearch 解放注意力更直接:
- 有一个笔记的最底层 DailyNotes,可以当成写任何东西的草稿纸
- 有一个 Page 级的引用
[[
,配合大纲能进行快速输入 - 有一个块级引用
((
,可以将快速输入的内容进行再组织 - 最重要的,它和 Xanadu 不同的是加入了上下文反链,这改变了游戏规则,诞生了一系列突破想象力的功能和用法,例如反链转移、空白节点、反链的筛选等等。
-
巧合的是,Conor 也患有 ADHD,他曾多次提到这点并推荐 ADHD 患者使用 RoamResearch 来改善记录:
-
哈桑老师对 Roam 的判断是对的,或许是能感同身受传统记录和结构带来的压力,那些热爱写作的 ADHD 患者会去尝试做出改变,果然替身使者是会相互吸引的。
-
后来我在 Roam 社区看到了渐进形式化这个概念,酷!它把我想要解释的东西全部串联起来:Roam 的设计、传统记录的压力、编程注意力、流畅的非线性写作、结构化的思想工具等等,于是一次漫游:学习中的快乐与束缚就诞生了。
渐进形式化
-
支持渐进形式化的软件很适合我,比如 Roam 式双链和 SuperMemo。
-
它们以一定的程度贴合我跳跃的思维,我能直观地理解如何定制和使用它们,用来适应我的特定方式,并且它们支持了我这样做。
-
比如:
- 我不想先分类再记录,那么双链就给了我选择可以不去分类。
- 我不想进行正式的、有逻辑的表达,那就能使用大纲快速输入想法。
- 我很少能阅读完一本书,看了两三页就开始犯困,只想看有趣的内容,渐进阅读鼓励我这么做。
-
这些软件对使用中的压力进行了优化,这真的很棒。
SuperMemo 中的压力优化
- 说起压力,Anki 类软件用起来的压力对我来说是最大的,每天都要日清太反人类了,用之前雄心壮志,考完试之后再也不想打开。
- SuperMemo 原来也是,后来出现了优先级队列、复习自动排序和自动推迟这些优化压力的功能,官方 wiki 也一直在引导使用这些功能,以尽可能低的摩擦力找到学习中产生的内在动力。
- 我非常喜欢 SuperMemo 中的渐进主义:当感觉到无聊或者注意力不集中就可以跳过。
- 以及作者对知识的态度:用你的新知识来娱乐。
- 从学习中得到动力之后我就像赚到了第一桶金,想去玩点花活,去尝试了很多学习策略,后来发现还是表达最经济实惠,消耗的动力不多,效果也挺好。
- 另外我还在 SM 中使用提取练习这个消耗大量动力的策略,表达的低消耗能保持我动力的收支平衡。
各个语境下的自上而下与自下而上
-
说起这个是突然想起好玩的事,这两个词是个筐,你往里装什么都行,只要能解释的通。
-
在笔记社区常用自上而下和自下而上这两个词,分别指代传统记录和双链的不分类记录。
-
我曾经用自上而下来描述渐进阅读,用自下而上来形容双链,产生了融合两者的想法:
- 既有自上而下,又有自下而上。
-
所以当我看到 Andy 老师的自上而下,再自下而上时,我兴奋地认为他和我的想法一样,结果他不是在讲笔记方法,而是在说学习上的大方向:先整体,再细节,通过整体激发直觉与兴趣,再去学习基础。
-
在文章里他提到的还原论是通过将复杂的系统分解成基本组分,这个过程类似渐进阅读,是自上而下地拆解。
-
但是他反而用自下而上来指代还原论,我想 Andy 老师可能是在说拆解后从基本部分开始学习的过程是自下而上的,我拗了好久才理解。
-
后来又在注意力的认知神经机制是什么里看到这两个词,在这里:
- 自下而上指的是通过视觉等信息的外界刺激,由外部环境信息驱动注意力。
- 自上而下指的是根据当前任务的目标和以往的知识对视觉通路中信息的进行的调控,这是大脑内部信息驱动的注意力。
-
挺有趣的,我想起之前哈桑老师在 MOC - 管理链接而非本体里提到过面向主题和面向结构,后来又在哈桑式 DailyNote 工作流整理之难里提到过“如果预先维护一个结构,那就失去 daily notes 的意义了”。
-
我从注意力的角度来思考,发现面向主题和面向结构本质都是在做注意力编程:
- 面向主题要求持久思考某个主题,然后在思考中建立自己的结构。
- 面向结构里,如果热衷维护结构则会把注意力都放在维护上,容易忽视思考。
-
而一些工具就是依靠结构对注意力的编程实现的,比如模版啊、写作结构都是的。
-
我们可以进行扩展,比如间隔重复系统是在时间线上编程注意力,卡片的提示也是在编程注意力,这些都在我之前的文章中。
到这里,我的任务就已经结束,如果您想与我进行更深入的交流,可以加入:
思源笔记民间交流群:538155062
思源笔记频道:频道号 SiYuanTFT42
一些资源:
-
渐进阅读流程图
-
关于双链:
-
临时哈桑的游说
-
哈桑老师的好文章
-
FLY 老哥的文章和教程:
-
大米的文章:
-
图中提到的可选选项:
-
DailyNotes 汇总展示
-
方法一:可使用 F 的文档流插件对所有 DailyNotes 进行汇总展示
-
在集市里找到文档流插件下载并启用
-
安装好后在左上角点击文档流插件图标
-
选择 SQL 查询
-
输入搜索所有 DailyNotes 的查询语句,例如:
-
select B.* from blocks as B join attributes as A on B.id = A.block_id where A.name like 'custom-dailynote-202%%%%%' order by A.value desc
-
-
-
方法二:在汇总所有 DailyNotes 的时候,会遇到文档数量太多导致卡顿的情况,我是用烟火酱的块嵌入分页辅助挂件解决的,挂件已经下架,可以在这里找到,在浏览嵌入块的时候可以利用下面的预览折叠列表功能查看子级。
-
-
使用查询挂件建立主题的表格索引
-
打开集市下载查询挂件
-
在思源里使用“/挂件”唤出查询挂件,点击Query 图标唤出代码块
-
复制烟火酱分享的 SQL 填入代码块
- Query 挂件实现统计每月新建了哪些【主题】 - 链滴 (ld246.com)
- 记得将链滴小尾巴去掉
-
-
思源增强插件,在思源里提升双链使用的体验,有页内反链和反链筛选等功能,在集市里下载启用即可
- 使用预览:
-
HBLX 主题的功能:聚焦折叠的列表大纲后自动展开、预览折叠的列表大纲
-
效果图
-
新版本记得打开点击聚焦选项(选项怎么又多了
-
使用方法
-
打开思源笔记设置
-
点击进入外观——代码片段
-
在 CSS 一栏添加下方给出的代码片段并启用:
-
在 JS 一栏添加下方给出的代码片段并启用
-
配合使用的 CSS
-
/*列表折叠*/ .protyle-wysiwyg [data-node-id].li[fold="1"]>.h1>[spellcheck]:not(.fn__flex-1.history__text.protyle [data-node-id].li[fold="1"]>.h1>[spellcheck])::after, .protyle-wysiwyg [data-node-id].li[fold="1"]>.h2>[spellcheck]:not(.fn__flex-1.history__text.protyle [data-node-id].li[fold="1"]>.h2>[spellcheck])::after, .protyle-wysiwyg [data-node-id].li[fold="1"]>.h3>[spellcheck]:not(.fn__flex-1.history__text.protyle [data-node-id].li[fold="1"]>.h3>[spellcheck])::after, .protyle-wysiwyg [data-node-id].li[fold="1"]>.h4>[spellcheck]:not(.fn__flex-1.history__text.protyle [data-node-id].li[fold="1"]>.h4>[spellcheck])::after, .protyle-wysiwyg [data-node-id].li[fold="1"]>.h5>[spellcheck]:not(.fn__flex-1.history__text.protyle [data-node-id].li[fold="1"]>.h5>[spellcheck])::after, .protyle-wysiwyg [data-node-id].li[fold="1"]>.h6>[spellcheck]:not(.fn__flex-1.history__text.protyle [data-node-id].li[fold="1"]>.h6>[spellcheck])::after, .protyle-wysiwyg [data-node-id].li[fold="1"]>.p>[spellcheck]:not(.fn__flex-1.history__text.protyle [data-node-id].li[fold="1"]>.p>[spellcheck])::after { content: " · · · "; font-family: "Trebuchet MS"; display: inline; font-weight: bold; vertical-align: 5%; font-size: 75%; color: rgb(95, 99, 104); word-break: break-all; border: 1px solid rgb(95, 99, 104); margin-left: 9px; border-radius: 5px; }
-
-
JS
/** * HBXL主题特性js */ window.HBuilderXLight = {}; window.HBuilderXLight.ButtonControl = {}; //渲染进程和主进程通信 const { ipcRenderer } = require('electron'); ipcRenderer.on("siyuan-send_windows", (event, data) => { //console.log(data); switch (data.Value) { case "ButtonControl"://按钮状态控制 var ControlButtonIDs = data.ControlButtonIDs; for (let index = 0; index < ControlButtonIDs.length; index++) { const ControlButtonID = ControlButtonIDs[index]; window.HBuilderXLight.ButtonControl[ControlButtonID.ButtonID][ControlButtonID.ControlFun](); } break; default: break; } }); function broadcast(data) { ipcRenderer.send("siyuan-send_windows", data); } /**----------------------------------自动展开悬浮窗折叠列表,展开搜索条目折叠列表,聚焦单独列表-----体验优化----------------------------------*/ function autoOpenList() { setInterval(() => { //找到所有的悬浮窗 var Preview = document.querySelectorAll("[data-oid]"); //如果发现悬浮窗内首行是折叠列表就展开并打上标记 if (Preview.length != 0) { for (let index = 0; index < Preview.length; index++) { diguiTooONE_2(Preview[index], (v) => { if (v.classList.contains("block__content")) { var vs = v.children; for (let index = 0; index < vs.length; index++) { var obj = vs[index].children[1] if (obj == null) continue; const element = obj.firstElementChild.firstElementChild; if (element == null) continue; if (!element.classList.contains("li")) continue;//判断是否是列表 if (element.getAttribute("foldTag") != null) continue;//判断是否存在标记 if (element.getAttribute("foid") == 0) continue;//判断是折叠 element.setAttribute("fold", 0); element.setAttribute("foldTag", true); } return true; } return false; }, 7) } } var searchPreview = document.querySelector("#searchPreview [data-doc-type='NodeListItem'].protyle-wysiwyg.protyle-wysiwyg--attr>div:nth-child(1)"); if (searchPreview == null) { searchPreview = document.querySelector("#searchPreview [data-doc-type='NodeList'].protyle-wysiwyg.protyle-wysiwyg--attr>div:nth-child(1)>div:nth-child(1)"); } if (searchPreview != null && searchPreview.getAttribute("data-type") == "NodeListItem" && searchPreview.getAttribute("fold") == 1) { if (searchPreview.getAttribute("foldTag") == null) {//判断是否存在标记 searchPreview.setAttribute("fold", 0); searchPreview.setAttribute("foldTag", true); } } var contentLIst = []; var item1 = document.querySelectorAll(".layout-tab-container>.fn__flex-1.protyle:not(.fn__none) [data-doc-type='NodeListItem'].protyle-wysiwyg.protyle-wysiwyg--attr>div:nth-child(1)"); var item2 = document.querySelectorAll(".layout-tab-container>.fn__flex-1.protyle:not(.fn__none) [data-doc-type='NodeList'].protyle-wysiwyg.protyle-wysiwyg--attr>div:nth-child(1)>div:nth-child(1)"); contentLIst = [...item1, ...item2]; for (let index = 0; index < contentLIst.length; index++) { const element = contentLIst[index]; if (element != null && element.getAttribute("data-type") == "NodeListItem" && element.getAttribute("fold") == 1) { if (element.getAttribute("foldTag") != null) continue;//判断是否存在标记 element.setAttribute("fold", 0); element.setAttribute("foldTag", true); } } }, 500) } /**----------------------------------列表折叠内容预览查看---------------------------------- */ function collapsedListPreview() { BodyEventRunFun("mouseover", collapsedListPreviewEvent, 3000) } function collapsedListPreviewEvent() { var _turn = [...document.querySelectorAll(".layout-tab-container>.fn__flex-1.protyle:not(.fn__none) [data-node-id].li[fold='1']"), ...document.querySelectorAll("[data-oid] [data-node-id].li[fold='1']"), ...document.querySelectorAll("#searchPreview [data-node-id].li[fold='1']")];//查询页面所有的折叠列表 var turn = []; for (let index = 0; index < _turn.length; index++) {//找到列表第一列表项(父项) const element = _turn[index].children[1]; var item = element.className; if (item == "p" || item == "h1" || item == "h2" || item == "h3" || item == "h4" || item == "h5" || item == "h6") { turn.push(element.firstElementChild) } } //检查注册事件的折叠列表是否恢复未折叠状态,是清除事件和去除标志属性 var ListPreview = [...document.querySelectorAll(".layout-tab-container>.fn__flex-1.protyle:not(.fn__none) [ListPreview]"), ...document.querySelectorAll("[data-oid] [ListPreview]"), ...document.querySelectorAll("#searchPreview [ListPreview]")]; for (let index = 0; index < ListPreview.length; index++) { const element = ListPreview[index]; var fold = element.parentElement.getAttribute("fold") if (fold == null || fold == 0) { element.removeAttribute("ListPreview"); var item = element.firstElementChild; myRemoveEvent(item, "mouseenter", LIstIn);//解绑鼠标进入 myRemoveEvent(item.parentElement.parentElement, "mouseleave", LIstout);//解绑鼠标离开 var items = Array.from(item.parentElement.parentElement.children); for (let index = 0; index < items.length; index++) { const element = items[index]; if (element.getAttribute("triggerBlock") != null) { element.remove(); } } } } for (let index = 0; index < turn.length; index++) {//重新注册、筛选未注册鼠标事件折叠列表 const element = turn[index]; var elementPP = element.parentElement.parentElement; if (element.parentElement.getAttribute("ListPreview") != null) { myRemoveEvent(element, "mouseenter", LIstIn);//解绑鼠标进入 myRemoveEvent(elementPP, "mouseleave", LIstout);//解绑鼠标离开 AddEvent(element, "mouseenter", LIstIn);//注册鼠标进入 AddEvent(elementPP, "mouseleave", LIstout);//注册鼠标离开 } else { element.parentElement.setAttribute("ListPreview", true); AddEvent(element, "mouseenter", LIstIn);//注册鼠标进入 AddEvent(elementPP, "mouseleave", LIstout);//注册鼠标离开 } } } var flag22 = false; function LIstout(e) { items = Array.from(e.target.children); flag22 = false; for (let index = 0; index < items.length; index++) { const element = items[index]; if (element.getAttribute("triggerBlock") != null) { element.remove(); } } } function LIstIns(e) { var id = setInterval(() => { if (!flag22) { clearInterval(id); return; } var obj = e.target; var timeDiv = addinsertCreateElement(obj, "div"); timeDiv.style.display = "inline-block"; timeDiv.style.width = "0px"; timeDiv.style.height = "16px"; var X = timeDiv.offsetLeft; var Y = timeDiv.offsetTop; timeDiv.remove(); var item = obj.parentElement.parentElement; if (item == null) return; var items = item.children var itemobj = items[items.length - 1]; if (itemobj != null && itemobj.getAttribute("triggerBlock") != null) { var items1 = items[items.length - 1]; items1.style.top = (Y + 35) + "px"; items1.style.left = (obj.offsetLeft + 35) + "px"; var items2 = items[items.length - 2]; items2.style.top = (Y + 2) + "px"; items2.style.left = (X + 45) + "px"; return; } }, 500); } function LIstIn(e) { flag22 = true; var obj = e.target; var timeDiv = addinsertCreateElement(obj, "div"); timeDiv.style.display = "inline-block"; timeDiv.style.width = "0px"; timeDiv.style.height = "16px"; var X = timeDiv.offsetLeft; var Y = timeDiv.offsetTop; timeDiv.remove(); var f = obj.parentElement.parentElement; if (!f) return; var items = f.children; var itemobj = items[items.length - 1]; if (itemobj != null && itemobj.getAttribute("triggerBlock") != null) return; var triggerBlock1 = CreatetriggerBlock(e)//创建触发块1 //设置触发块样式,将触发块显示在〔 ··· 〕第二行位置 triggerBlock1.style.top = (Y + 35) + "px"; triggerBlock1.style.left = (obj.offsetLeft + 35) + "px"; AddEvent(triggerBlock1, "mouseenter", () => { //一秒延时后搜索打开的悬浮窗,将悬浮窗中的列表展开,重复检查三次 setTimeout(Suspended, 1000) });//注册鼠标进入 var triggerBlock2 = CreatetriggerBlock(e)//创建触发块2 //设置触发块样式,将触发块显示在〔 ··· 〕位置 triggerBlock2.style.top = (Y + 2) + "px"; triggerBlock2.style.left = (X + 45) + "px"; AddEvent(triggerBlock2, "mouseenter", () => { //一秒延时后搜索打开的悬浮窗,将悬浮窗中的列表展开,重复检查三次 setTimeout(Suspended, 1000) });//注册鼠标进入 //一秒延时后搜索打开的悬浮窗,将悬浮窗中的列表展开,重复检查三次 var previewID = obj.parentElement.parentElement.getAttribute("data-node-id"); var jisu = 0; function Suspended() { jisu++; var y = false; if (jisu == 3) return var Sd = document.querySelectorAll("[data-oid]"); if (Sd.length >= 1) { //如果找到那么就将悬浮窗中列表展开 for (let index = 0; index < Sd.length; index++) { const element = Sd[index]; var item = element.children[1].firstElementChild.children[1].firstElementChild.firstElementChild; if (item == null) continue; if (item.getAttribute("data-node-id") == previewID) { item.setAttribute("fold", 0); y = true; } } } if (!y) { setTimeout(Suspended, 800) } } LIstIns(e); } function CreatetriggerBlock(e) { var objParent = e.target.parentElement; var triggerBlock = addinsertCreateElement(objParent.parentElement, "div");//创建触发块 //设置触发块样式,将触发块显示在〔 ··· 〕位置 triggerBlock.setAttribute("triggerBlock", true); triggerBlock.style.position = "absolute"; triggerBlock.style.width = "40px"; triggerBlock.style.height = "15px"; //triggerBlock.style.background="red"; triggerBlock.style.display = "flex"; triggerBlock.style.zIndex = "9"; triggerBlock.style.cursor = "pointer"; triggerBlock.style.WebkitUserModify = "read-only"; triggerBlock.setAttribute("contenteditable", "false"); triggerBlock.innerHTML = ""; //获取折叠列表ID,设置悬浮窗 //protyle-wysiwyg__embed data-id var previewID = objParent.parentElement.getAttribute("data-node-id"); triggerBlock.setAttribute("class", "protyle-attr"); triggerBlock.style.opacity = "0"; //在触发块内创建思源超链接 triggerBlock.innerHTML = "<span data-type='a' class='list-A' data-href=siyuan://blocks/" + previewID + ">####</span>"; //将这个思源连接样式隐藏 var a = triggerBlock.firstElementChild; a.style.fontSize = "15px"; a.style.lineHeight = "15px"; a.style.border = "none"; return triggerBlock; } /**----------------鼠标中键标题、列表文本折叠/展开----------------*/ function collapseExpand_Head_List() { var flag45 = false; AddEvent(document.body, "mouseup", () => { flag45 = false; }); AddEvent(document.body, "mousedown", (e) => { if (e.button == 2) { flag45 = true; return } if (flag45 || e.shiftKey || e.altKey || e.button != 1) return; var target = e.target; if (target.getAttribute("contenteditable") == null) { isFatherFather(target, (v) => { if (v.getAttribute("contenteditable") != null) { target = v; return true; } return false; }, 10); } var targetParentElement = target.parentElement; if (targetParentElement == null) return; //是标题吗? if (targetParentElement != null && targetParentElement.getAttribute("data-type") == "NodeHeading") { var targetParentElementParentElement = targetParentElement.parentElement; //标题父元素是列表吗? if (targetParentElementParentElement != null && targetParentElementParentElement.getAttribute("data-type") == "NodeListItem") { e.preventDefault(); //列表项实现折叠 _collapseExpand_NodeListItem(target); } else { e.preventDefault(); //标题块标项实现折叠 _collapseExpand_NodeHeading(target); } } else {//是列表 var targetParentElementParentElement = targetParentElement.parentElement; if (targetParentElementParentElement != null && targetParentElementParentElement.getAttribute("data-type") == "NodeListItem") { e.preventDefault(); //列表项实现折叠 _collapseExpand_NodeListItem(target); } } }); //标题,块标实现折叠 function _collapseExpand_NodeHeading(element) { var i = 0; while (element.className != "protyle" && element.className != "fn__flex-1 protyle" && element.className != "block__edit fn__flex-1 protyle" && element.className != "fn__flex-1 spread-search__preview protyle") { if (i == 999) return; i++; element = element.parentElement; } var ddddd = element.children; for (let index = ddddd.length - 1; index >= 0; index--) { const element = ddddd[index]; if (element.className == "protyle-gutters") { var fold = diguiTooONE_1(element, (v) => { return v.getAttribute("data-type") === "fold"; }) if (fold != null) fold.click(); return; } } } //列表,列表项实现折叠 function _collapseExpand_NodeListItem(element) { //在悬浮窗中第一个折叠元素吗? var SiyuanFloatingWindow = isSiyuanFloatingWindow(element); if (SiyuanFloatingWindow) { var vs = isFatherFather(element, (v) => v.classList.contains("li"), 7); if (vs != null && (vs.previousElementSibling == null)) { var foid = vs.getAttribute("fold"); if (foid == null || foid == "0") {//判断是折叠 vs.setAttribute("fold", "1"); } else { vs.setAttribute("fold", "0"); } return; } } var i = 0; while (element.getAttribute("contenteditable") == null) { if (i == 999) return; i++; element = element.parentElement; } var elementParentElement = element.parentElement.parentElement; var fold = elementParentElement.getAttribute("fold"); if (elementParentElement.children.length == 3) return; if (fold == null || fold == "0") { setBlockfold_1(elementParentElement.getAttribute("data-node-id")); } else { setBlockfold_0(elementParentElement.getAttribute("data-node-id")); } } } /** * * @param {*} element 元素是否在思源悬浮窗中 * @returns 是返回悬浮窗元素,否返回null */ function isSiyuanFloatingWindow(element) { return isFatherFather(element, (v) => { if (v.getAttribute("data-oid") != null) { return true; } return false; }); } /**----------------------------------查找匹配列表条目前的图标可以鼠标悬停打开悬浮窗--------------------------------------*/ function findMatchingListEntries() { BodyEventRunFun("mouseover", _findMatchingListEntries, 3000); } function _findMatchingListEntries() { var searchList = document.getElementById("searchList"); var globalSearchList = document.getElementById("globalSearchList"); if (searchList != null) { var searchLists = searchList.children; for (let index = 0; index < searchLists.length; index++) { const element = searchLists[index]; //var b3_list_item__text=element.children[1]; //if(element.getAttribute("title")!=null)break; //element.setAttribute("title",b3_list_item__text.innerText); var itemlianjie = element.firstElementChild; if (itemlianjie == null || itemlianjie.getAttribute("data-defids") != null) break; itemlianjie.setAttribute("data-defids", '[""]'); itemlianjie.setAttribute("class", "b3-list-item__graphic popover__block"); itemlianjie.setAttribute("data-id", element.getAttribute("data-node-id")); } }; if (globalSearchList != null) { var globalSearchLists = globalSearchList.children; for (let index = 0; index < globalSearchLists.length; index++) { const element = globalSearchLists[index]; //var b3_list_item__text=element.children[1]; //if(element.getAttribute("title")!=null)break; //element.setAttribute("title",b3_list_item__text.innerText); var itemlianjie = element.firstElementChild; if (itemlianjie == null || itemlianjie.getAttribute("data-defids") != null) break; itemlianjie.setAttribute("data-defids", '[""]'); itemlianjie.setAttribute("class", "b3-list-item__graphic popover__block"); itemlianjie.setAttribute("data-id", element.getAttribute("data-node-id")); } }; } /**------------------------------------思源悬浮窗头栏中键关闭------------------------------------- */ function theFloatingWindowIsClosed() { AddEvent(document.body, "mousedown", (e) => { if (e.button != 1) return; var element = e.target; var className = element.className; if (className == "block__icons block__icons--border" || className == "fn__space fn__flex-1" || className == "maxMinButton") { element = element.parentElement; } else { return; } diguiTooONE_1(element, (v) => { if (v.getAttribute("data-type") == "close") { v.click(); return true; } return false; }) }); } /**----------------------思源悬浮窗检测到单个段落块,单个列表项,面包屑前一级展示/反链计数悬浮窗反链引用高亮标记----------------------- */ /*请无视糟糕中文代码 */ function SuspendedWindowNoSection() { setInterval(() => { //获取所有悬浮窗内容类型,排除一个以元素和非段落块,查找面包屑模拟点击倒数第二级展开内容 document.querySelectorAll("[data-oid]:not([zhankai])").forEach((da) => { if (da.querySelector(".fn__loading")) return; //有内容没加载 var protyles; try { protyles = da.children[1].children; if (protyles == null) return; } catch (error) { return; } for (let index = 0; index < protyles.length; index++) { const element = protyles[index]; /***********************************************排除失效引用但还站一个悬浮窗格的情况 */ if (element.querySelector("[data-doc-type]" && element.firstElementChild != null) == null) return; } if (da.getAttribute("zhankai") != null) return;//已经处理过的悬浮窗 da.setAttribute("zhankai", true);//标记已经处理,防止下次循环多次处理 Array.from(da.children[1].children).forEach((g) => { var v = g.querySelector("[data-doc-type]");//查询基力悬浮窗内容类型元素 if (v.children.length != 1 && v.getAttribute("data-doc-type") != "NodeHeading") return;//剔除data-doc-type存在多个子元素的悬浮窗 var 悬浮窗类型 = "引用悬浮窗"; var 反链引用ID; var defmark = v.querySelector(".def--mark"); if (defmark) { 悬浮窗类型 = "反链悬浮窗";//判断悬浮窗类型 反链引用ID = defmark.getAttribute("data-id"); } //console.log(悬浮窗类型, 反链引用ID); var 悬浮窗出现的单独块是什么块; //悬浮窗出现的单独块是什么块? switch (v.firstElementChild.className) { case "render-node"://嵌入块 悬浮窗出现的单独块是什么块 = "嵌入块"; break; case "p"://段落块 悬浮窗出现的单独块是什么块 = "段落块"; break; case "li"://单独列表项块//列表只要有三个子元素就是单独列表项块 if (v.firstElementChild.children.length == 3) 悬浮窗出现的单独块是什么块 = "列表块"; else 悬浮窗出现的单独块是什么块 = ""; break; case "sb"://单独超级快 悬浮窗出现的单独块是什么块 = "超级块"; break; default: 悬浮窗出现的单独块是什么块 = ""; break; } var 引用ID; var 拟点击面包屑模元素; var 时间累加器 = 0; var iD = setInterval(() => { 时间累加器 += 100; if (时间累加器 > 3000) clearInterval(iD);//超时 try { if (悬浮窗出现的单独块是什么块 != "超级块") { 引用ID = v.parentElement.previousElementSibling.firstElementChild.lastElementChild.getAttribute("data-node-id");//记录原块id } else {//思源超级块不显示面包屑上因此需要单独处理 引用ID = v.firstElementChild.getAttribute("data-node-id"); } 拟点击面包屑模元素 = v.parentElement.previousElementSibling.firstElementChild.lastElementChild.previousElementSibling.previousElementSibling.children[1]; clearInterval(iD);//上段代码没报错就证明元素获取成功了,关闭定时器 if (悬浮窗出现的单独块是什么块 != "") { 悬浮窗面包屑跳转(); } else { 不需要模拟点击的反链展示(); } } catch (error) { console.log("面包屑模拟点击元素获取错误", v.parentElement.previousElementSibling.firstElementChild.lastElementChild); } }, 100); function 悬浮窗面包屑跳转() { 拟点击面包屑模元素.click(); const observer = new MutationObserver(() => {//创建监视 observer.disconnect();//关闭监视 setTimeout(() => { switch (悬浮窗类型) { case "反链悬浮窗": switch (悬浮窗出现的单独块是什么块) { case "嵌入块": var ts = v.parentElement.querySelectorAll(`[data-content="select * from blocks where id='${反链引用ID}'"]`); for (let index = 0; index < ts.length; index++) { const element = ts[index]; element.classList.add("blockhighlighting")///块高亮 if (index == 0) 高亮内容跳转到悬浮窗中间(element, v); } break; default: var ts = v.parentElement.querySelectorAll(`[data-id='${反链引用ID}'][data-subtype]`); for (let index = 0; index < ts.length; index++) { const element = ts[index]; element.classList.add("blockRefhighlighting")//引用高亮 isFatherFather(element, (f) => { if (f.getAttribute("contenteditable") != null) { f.classList.add("blockhighlighting")//块高亮 if (index == 0) 高亮内容跳转到悬浮窗中间(element, v); return true; } if (f.tagName == "TD" || f.tagName == "TR") { f.classList.add("blockhighlighting")//块高亮 if (index == 0) 高亮内容跳转到悬浮窗中间(element, v); return true; } return false; }, 4) } break; } break; case "引用悬浮窗": switch (悬浮窗出现的单独块是什么块) { case "嵌入块": var ts = v.parentElement.querySelector(`[data-node-id='${引用ID}'][data-type="NodeBlockQueryEmbed"]`); ts.classList.add("blockhighlighting")//引用高亮 高亮内容跳转到悬浮窗中间(ts, v); break; default: var ts = v.parentElement.querySelector(`[data-node-id='${引用ID}']`); ts.classList.add("blockhighlighting")//引用高亮 高亮内容跳转到悬浮窗中间(ts, v); break; } break; default: break; } }, 500) }); observer.observe(v, { attributes: false, childList: true, subtree: false }); } function 不需要模拟点击的反链展示() { switch (悬浮窗类型) { case "反链悬浮窗": var protyle_content = v.parentElement; var ts = protyle_content.querySelectorAll(`[data-id='${反链引用ID}']`); for (let index = 0; index < ts.length; index++) { const element = ts[index]; element.classList.add("blockRefhighlighting")//引用高亮 isFatherFather(element, (f) => { if (f.getAttribute("contenteditable") != null) { f.classList.add("blockhighlighting")//块高亮 if (index == 0) 高亮内容跳转到悬浮窗中间(element, v) return true; } if (f.tagName == "TD" || f.tagName == "TR") { f.classList.add("blockhighlighting")//块高亮 if (index == 0) 高亮内容跳转到悬浮窗中间(element, v) return true; } return false; }, 4) } break; case "引用悬浮窗": var protyle_content = v.parentElement; var ts = protyle_content.querySelector(`[data-node-id='${引用ID}']`); //ts.scrollIntoView(true); // ts.classList.add("blockRefhighlighting")//引用高亮 break; default: break; } } function 高亮内容跳转到悬浮窗中间(element, v) { setTimeout(() => { element.scrollIntoView(false); setTimeout(() => { var tim = v.parentElement; //console.log(tim, tim.scrollTop); if (tim.scrollTop != 0) { tim.scrollBy(0, tim.offsetHeight / 2); } }, 100); isFatherFather(v, (g) => { if (g.getAttribute("class") == "block__content") { g.scrollTo(0, 0); return true; } return false; }, 5) }, 100); } }); }); }, 1000); } /* 操作 */ /** * 获得所选择的块对应的块 ID * @returns {string} 块 ID * @returns { * id: string, // 块 ID * type: string, // 块类型 * subtype: string, // 块子类型(若没有则为 null) * } * @returns {null} 没有找到块 ID */ function getBlockSelected() { let node_list = document.querySelectorAll('.protyle-wysiwyg--select'); if (node_list.length === 1 && node_list[0].dataset.nodeId != null) return { id: node_list[0].dataset.nodeId, type: node_list[0].dataset.type, subtype: node_list[0].dataset.subtype, }; return null; } function ClickMonitor() { window.addEventListener('mouseup', MenuShow) } function MenuShow() { setTimeout(() => { let selectinfo = getBlockSelected() if (selectinfo) { let selecttype = selectinfo.type let selectid = selectinfo.id if (selecttype == "NodeList" || selecttype == "NodeTable") { setTimeout(() => InsertMenuItem(selectid, selecttype), 0) } } }, 0); } function InsertMenuItem(selectid, selecttype) { let commonMenu = document.querySelector("commonMenu") let readonly = commonMenu.querySelector(".b3-menu__item--readonly") let selectview = commonMenu.querySelector('[id="viewselect"]') if (readonly) { if (!selectview) { commonMenu.insertBefore(ViewSelect(selectid, selecttype), readonly) commonMenu.insertBefore(MenuSeparator(), readonly) } } } function ViewMonitor(event) { let id = event.currentTarget.getAttribute("data-node-id") let attrName = 'custom-' + event.currentTarget.getAttribute("custom-attr-name") let attrValue = event.currentTarget.getAttribute("custom-attr-value") let blocks = document.querySelectorAll(`.protyle-wysiwyg [data-node-id="${id}"]`) if (blocks) { blocks.forEach(block => block.setAttribute(attrName, attrValue)) } let attrs = {} attrs[attrName] = attrValue 设置思源块属性(id, attrs) } //+++++++++++++++++++++++++++++++++思源API++++++++++++++++++++++++++++++++++++ //思源官方API文档 https://github.com/siyuan-note/siyuan/blob/master/API_zh_CN.md async function queryAPI(sqlstmt, then, obj = null) { await 向思源请求数据("/api/query/sql", { stmt: sqlstmt }).then((v) => then(v.data, obj)) } /** * * @param {*} 内容块id * @param {*} 回调函数 * @param {*} 传递对象 */ async function 根据ID获取人类可读路径(内容块id, then, obj = null) { await 向思源请求数据('/api/filetree/getHPathByID', { id: 内容块id }).then((v) => then(v.data, obj)) } async function 以id获取文档聚焦内容(id, then, obj = null) { await 向思源请求数据('/api/filetree/getDoc', { id: id, k: "", mode: 0, size: 36, }).then((v) => then(v.data, obj)) } async function 更新块(id, dataType, data, then = null, obj = null) { await 向思源请求数据('/api/block/updateBlock', { id: id, dataType: dataType, data: data, }).then((v) => { if (then) then(v.data, obj); }) } async function 设置思源块属性(内容块id, 属性对象) { let url = '/api/attr/setBlockAttrs' return 解析响应体(向思源请求数据(url, { id: 内容块id, attrs: 属性对象, })) } async function 获取块属性(内容块id, then = null, obj = null) { let url = '/api/attr/getBlockAttrs' return 向思源请求数据(url, { id: 内容块id }).then((v) => { if (then) then(v.data, obj); }) } async function 向思源请求数据(url, data) { const response = await fetch(url, { body: JSON.stringify(data), method: 'POST', headers: { Authorization: `Token ''`, } }); if (response.status === 200) return await response.json(); else return null; } async function 解析响应体(response) { let r = await response return r.code === 0 ? r.data : null } async function 获取文件(path, then = null, obj = null) { let url = '/api/file/getFile'; await 向思源请求数据(url, { path: path }).then((v) => { if (then) then(v, obj); }); } /** * * @param {*} notebookID 笔记本id * @param {*} dataPath 父文档路径 * @param {*} sortType 文档排序类型 * @param {*} then 回调函数 * @param {*} obj 传递对象 */ async function 获取子文档数据(notebookID, dataPath, sortType, then = null, obj = null) { let url = "/api/filetree/listDocsByPath"; await 向思源请求数据(url, { notebook: notebookID, path: dataPath, sort: sortType, }).then((v) => { if (then) then(v.data.files, obj); }); } async function 写入文件(path, filedata, then = null, obj = null, isDir = false, modTime = Date.now()) { let blob = new Blob([filedata]); let file = new File([blob], path.split('/').pop()); let formdata = new FormData(); formdata.append("path", path); formdata.append("file", file); formdata.append("isDir", isDir); formdata.append("modTime", modTime); await fetch( "/api/file/putFile", { body: formdata, method: "POST", headers: { Authorization: `Token ""`, }, }).then((v) => { console.log(v); setTimeout(() => { if (then) then(obj); }, 200) }); } async function 写入文件2(path, filedata, then = null, obj = null, isDir = false, modTime = Date.now()) { let blob = new Blob([filedata]); let file = new File([blob], path.split('/').pop()); let formdata = new FormData(); formdata.append("path", path); formdata.append("file", file); formdata.append("isDir", isDir); formdata.append("modTime", modTime); const response = await fetch( "/api/file/putFile", { body: formdata, method: "POST", headers: { Authorization: `Token ""`, }, }); if (response.status === 200) { if (then) then(obj); } } //+++++++++++++++++++++++++++++++++辅助API++++++++++++++++++++++++++++++++++++ /** * 方便为主题功能添加开关按钮,并选择是否拥有记忆状态 * @param {*} ButtonID 按钮ID。 * @param {*} ButtonTitle 按钮作用提示文字。 * @param {*} ButtonFunctionType 按钮功能类型。:EnumButtonFunctionType 枚举 * @param {*} ButtonCharacteristicType 按钮特性类型。:EnumButtonCharacteristicType 枚举 * @param {*} NoButtonSvg 按钮激活Svg图标路径 * @param {*} OffButtonSvg 按钮未激活Svg图标路径 * @param {*} OnClickRunFun 按钮开启执行函数 * @param {*} OffClickRunFun 按钮关闭执行函数 * @param {*} Memory 是否设置记忆状态 true为是留空或false为不设置记忆状态。 */ function HBuiderXThemeToolbarAddButton(ButtonID, ButtonFunctionType, ButtonCharacteristicType, ButtonTitle, ONButtonSvgURL, OffButtonSvgURL, OnClickRunFun, OffClickRunFun, Memory = false) { //确认主题功能区toolbar是否存在,不存在就创建 var HBuiderXToolbar = document.getElementById("HBuiderXToolbar"); if (HBuiderXToolbar == null) { if (isPc()) { HBuiderXToolbar = document.createElement("div"); HBuiderXToolbar.id = "HBuiderXToolbar"; HBuiderXToolbar.style.marginRight = "3px"; HBuiderXToolbar.style.marginTop = "6px"; document.getElementById("windowControls").parentElement.insertBefore(HBuiderXToolbar, document.getElementById("windowControls")); HBuiderXToolbar.style.width = "35px"; HBuiderXToolbar.style.overflow = "hidden"; AddEvent(HBuiderXToolbar, "mouseover", () => { HBuiderXToolbar.style.width = "auto" }) AddEvent(HBuiderXToolbar, "mouseout", () => { HBuiderXToolbar.style.width = "35px" }) } else if (isBrowser()) { HBuiderXToolbar = insertCreateAfter(document.getElementById("barMode"), "div", "HBuiderXToolbar"); HBuiderXToolbar.style.marginRight = "3px"; HBuiderXToolbar.style.marginTop = "4px"; HBuiderXToolbar.style.width = "35px"; HBuiderXToolbar.style.overflow = "hidden"; AddEvent(HBuiderXToolbar, "mouseover", () => { HBuiderXToolbar.style.width = "auto" }) AddEvent(HBuiderXToolbar, "mouseout", () => { HBuiderXToolbar.style.width = "35px" }) } else if (isPcWindow()) { HBuiderXToolbar = insertCreateBefore(document.getElementById("minWindow"), "div", "HBuiderXToolbar"); HBuiderXToolbar.style.position = "absolute"; HBuiderXToolbar.style.height = "25px"; HBuiderXToolbar.style.paddingTop = "2px"; HBuiderXToolbar.style.overflowY = "scroll"; HBuiderXToolbar.style.right = "77px"; HBuiderXToolbar.style.width = "35px"; HBuiderXToolbar.style.overflow = "hidden"; AddEvent(HBuiderXToolbar, "mouseover", () => { HBuiderXToolbar.style.width = "auto" }) AddEvent(HBuiderXToolbar, "mouseout", () => { HBuiderXToolbar.style.width = "35px" }) } else if (isPhone()) { HBuiderXToolbar = insertCreateBefore(document.getElementById("toolbarEdit"), "div", "HBuiderXToolbar"); HBuiderXToolbar.style.position = "relative"; HBuiderXToolbar.style.height = "25px"; HBuiderXToolbar.style.overflowY = "scroll"; HBuiderXToolbar.style.paddingTop = "7px"; HBuiderXToolbar.style.marginRight = "9px"; } } var addButton = addinsertCreateElement(HBuiderXToolbar, "div"); addButton.style.width = "17px"; addButton.style.height = "100%"; addButton.style.float = "left"; addButton.style.marginLeft = "10px"; addButton.style.backgroundImage = "url(" + OffButtonSvgURL + ")"; addButton.style.backgroundRepeat = "no-repeat"; addButton.style.backgroundPosition = "left top"; addButton.style.backgroundSize = "100%"; addButton.style.cursor = "pointer"; addButton.setAttribute("title", ButtonTitle); addButton.id = ButtonID; var offon = "0"; var isclick = 0; var broadcastData_off = { "Value": "ButtonControl", "ControlButtonIDs": [{ "ButtonID": ButtonID, "ControlFun": "ControlFun_off" }] }; var broadcastData_on = { "Value": "ButtonControl", "ControlButtonIDs": [{ "ButtonID": ButtonID, "ControlFun": "ControlFun_on" }] }; if (Memory) { GetItem(ButtonID, (v) => { offon = v; if (offon == "1") { _on(); } else if (offon == null || offon == undefined) { offon = "0"; } }); } AddEvent(addButton, "click", () => { window.HBuilderXLight.ButtonControl[ButtonID].Isclick = isclick = 1; if (offon == "0" || offon == null || offon == undefined) { broadcast(broadcastData_on); return; } if (offon == "1") { broadcast(broadcastData_off); return; } }); AddEvent(addButton, "mouseover", () => { if (offon == "0" || offon == null || offon == undefined) { addButton.style.filter = "drop-shadow(rgb(0, 0, 0) 0px 0)"; } }); AddEvent(addButton, "mouseout", () => { if (offon == "0" || offon == null || offon == undefined) { addButton.style.filter = "none"; } }); function buttonCharacteristicDispose() { switch (ButtonCharacteristicType) { case 2: if (ButtonFunctionType == 2) { var topicfilterButtons = []; var ButtonControl = window.HBuilderXLight.ButtonControl; for (var t in ButtonControl) { if (t != ButtonID && ButtonControl[t].ButtonFunctionType == 2 && ButtonControl[t].OffOn == "1") { //console.log(t, ButtonControl[t].OffOn); ButtonControl[t].Isclick = 1; topicfilterButtons.push(ButtonControl[t]); } } if (topicfilterButtons.length == 0) { window.HBuilderXLight.ButtonControl[ButtonID].Isclick = isclick = 0; return; } //console.log(ButtonControl[ButtonID].Isclick); if (window.HBuilderXLight.ButtonControl[ButtonID].Isclick == 1) { var index = (topicfilterButtons.length) - 1; var id = setInterval(() => { if (index >= 0) { //console.log(window.HBuilderXLight.ButtonControl[ButtonID].Isclick); const element = topicfilterButtons[index]; element["ControlFun_off"](); } else { // console.log(window.HBuilderXLight.ButtonControl[ButtonID].Isclick); clearInterval(id); window.HBuilderXLight.ButtonControl[ButtonID].Isclick = isclick = 0; } index--; }, 300) } else { for (let index = 0; index < topicfilterButtons.length; index++) { const element = topicfilterButtons[index]; element["ControlFun_off"](); } } } break; default: break; } } function _off() { addButton.style.backgroundImage = "url(" + OffButtonSvgURL + ")"; addButton.style.filter = "none"; window.HBuilderXLight.ButtonControl[ButtonID].OffOn = offon = "0"; //console.log(window.HBuilderXLight.ButtonControl[ButtonID].Isclick); if (Memory && window.HBuilderXLight.ButtonControl[ButtonID].Isclick == 1) { //console.log("bbbbbbbbbbbb") SetItem(ButtonID, "0", () => { OffClickRunFun(addButton); window.HBuilderXLight.ButtonControl[ButtonID].Isclick = isclick = 0; }); } else { //console.log("aaaaaaaaaaa") OffClickRunFun(addButton); window.HBuilderXLight.ButtonControl[ButtonID].Isclick = isclick = 0; }; } function _on() { addButton.style.backgroundImage = "url(" + ONButtonSvgURL + ")"; addButton.style.filter = "drop-shadow(rgb(0, 0, 0) 0px 0)"; window.HBuilderXLight.ButtonControl[ButtonID].OffOn = offon = "1"; //console.log(window.HBuilderXLight.ButtonControl[ButtonID].Isclick); if (Memory && window.HBuilderXLight.ButtonControl[ButtonID].Isclick == 1) { SetItem(ButtonID, "1", () => { OnClickRunFun(addButton); buttonCharacteristicDispose(); }); } else { OnClickRunFun(addButton); buttonCharacteristicDispose(); } } window.HBuilderXLight.ButtonControl[ButtonID] = { "ControlFun_off": _off, "ControlFun_on": _on, "OffOn": offon, "Isclick": isclick, "ButtonFunctionType": ButtonFunctionType, "ButtonCharacteristicType": ButtonCharacteristicType } } function setItem(key, value) { window.HBuilderXLight.config[key] = value; } function getItem(key) { return window.HBuilderXLight.config[key] === undefined ? null : window.HBuilderXLight.config[key]; } function removeItem(key) { delete window.HBuilderXLight.config[key]; } function SetItem(key, value, fun = null) { 获取文件("/data/widgets/HBuilderX-Light.config.json", (config) => { if (config) {//不存在配置文件就要创建 config[key] = value; //console.log(config); 写入文件2("/data/widgets/HBuilderX-Light.config.json", JSON.stringify(config, undefined, 4), fun); } else { 写入文件2("/data/widgets/HBuilderX-Light.config.json", JSON.stringify({ "HBuilderXLight": 1, key: value }, undefined, 4), fun); } }); } function GetItem(key, then = null) { 获取文件("/data/widgets/HBuilderX-Light.config.json", (config) => { if (config) {//不存在配置文件就要创建 // console.log(config[key]); try { if (then) then(config[key]); } catch (error) { if (then) then(null); } } else { 写入文件2("/data/widgets/HBuilderX-Light.config.json", JSON.stringify({ "HBuilderXLight": 1 }, undefined, 4), then(null)); } }); } function RemoveItem(key, fun = null) { 获取文件("/data/widgets/HBuilderX-Light.config.json", (config) => { if (config) {//不存在配置文件就要创建 delete config[key]; //console.log(config); 写入文件2("/data/widgets/HBuilderX-Light.config.json", JSON.stringify(config, undefined, 4), fun); } else { 写入文件2("/data/widgets/HBuilderX-Light.config.json", JSON.stringify({ "HBuilderXLight": 1 }, undefined, 4), fun); } }); } /** * 在DIV光标位置插入内容 * @param {*} content */ function insertContent(content) { if (content) { var sel = window.getSelection(); if (sel.rangeCount > 0) { var range = sel.getRangeAt(0); //获取选择范围 range.deleteContents(); //删除选中的内容 var el = document.createElement("div"); //创建一个空的div外壳 el.innerHTML = content; //设置div内容为我们想要插入的内容。 var frag = document.createDocumentFragment(); //创建一个空白的文档片段,便于之后插入dom树 var node = el.firstChild; var lastNode = frag.appendChild(node); range.insertNode(frag); //设置选择范围的内容为插入的内容 var contentRange = range.cloneRange(); //克隆选区 contentRange.setStartAfter(lastNode); //设置光标位置为插入内容的末尾 contentRange.collapse(true); //移动光标位置到末尾 sel.removeAllRanges(); //移出所有选区 sel.addRange(contentRange); //添加修改后的选区 } } } /** * 获取DIV文本光标位置 * @param {*} element * @returns */ function getPosition(element) { var caretOffset = 0; var doc = element.ownerDocument || element.document; var win = doc.defaultView || doc.parentWindow; var sel; if (typeof win.getSelection != "undefined") { //谷歌、火狐 sel = win.getSelection(); if (sel.rangeCount > 0) { var range = sel.getRangeAt(0); var preCaretRange = range.cloneRange(); //克隆一个选区 preCaretRange.selectNodeContents(element); //设置选区的节点内容为当前节点 preCaretRange.setEnd(range.endContainer, range.endOffset); //重置选中区域的结束位置 caretOffset = preCaretRange.toString().length; } } else if ((sel = doc.selection) && sel.type != "Control") { //IE var textRange = sel.createRange(); var preCaretTextRange = doc.body.createTextRange(); preCaretTextRange.moveToElementText(element); preCaretTextRange.setEndPoint("EndToEnd", textRange); caretOffset = preCaretTextRange.text.length; } return caretOffset; }; /** * 在指定DIV索引位置设置光标 * @param {*} element * @param {*} index */ function setCursor(element, index) { var codeEl = element.firstChild; var selection = window.getSelection(); // 创建新的光标对象 let range = selection.getRangeAt(0); // 光标对象的范围界定为新建的代码节点 range.selectNodeContents(codeEl) // 光标位置定位在代码节点的最大长度 // console.log(codeEl.length); range.setStart(codeEl, index); // 使光标开始和光标结束重叠 range.collapse(true) selection.removeAllRanges() selection.addRange(range) } /** * 获得文本的占用的宽度 * @param {*} text 字符串文班 * @param {*} font 文本字体的样式 * @returns */ function getTextWidth(text, font) { var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas")); var context = canvas.getContext("2d"); context.font = font; var metrics = context.measureText(text); return metrics.width; } /** * 触发元素的事件 * @param {触发元素事件} type * @param {*} element * @param {*} detail */ function trigger(type, element) { var customEvent = new Event(type, { bubbles: false, cancelable: true }); element.dispatchEvent(customEvent); } /** * 向body注入新style覆盖原本的css * @param {css文本字符串} csstxt */ function injectionCss(csstxt) { var styleElement = document.createElement('style'); styleElement.innerText = t; document.body.appendChild(styleElement); }; /** * 向指定父级创建追加一个子元素,并可选添加ID, * @param {Element} fatherElement * @param {string} addElementTxt 要创建添加的元素标签 * @param {string} setId * @returns addElementObject */ function addinsertCreateElement(fatherElement, addElementTxt, setId = null) { if (!fatherElement) console.error("指定元素对象不存在!"); if (!addElementTxt) console.error("未指定字符串!"); var element = document.createElement(addElementTxt); if (setId) element.id = setId; fatherElement.appendChild(element); return element; } /** * 向指定元素后创建插入一个元素,可选添加ID * @param {*} targetElement 目标元素 * @param {*} addElementTxt 要创建添加的元素标签 * @param {*} setId 为创建元素设置ID */ function insertCreateAfter(targetElement, addElementTxt, setId = null) { if (!targetElement) console.error("指定元素对象不存在!"); if (!addElementTxt) console.error("未指定字符串!"); var element = document.createElement(addElementTxt); if (setId) element.id = setId; var parent = targetElement.parentNode;//得到父节点 if (parent.lastChild === targetElement) { //如果最后一个子节点是当前元素那么直接添加即可 parent.appendChild(element); return element; } else { parent.insertBefore(element, targetElement.nextSibling);//否则,当前节点的下一个节点之前添加 return element; } } /** * 向指定元素前创建插入一个元素,可选添加ID * @param {*} targetElement 目标元素 * @param {*} addElementTxt 要创建添加的元素标签 * @param {*} setId 为创建元素设置ID */ function insertCreateBefore(targetElement, addElementTxt, setId = null) { if (!targetElement) console.error("指定元素对象不存在!"); if (!addElementTxt) console.error("未指定字符串!"); var element = document.createElement(addElementTxt); if (setId) element.id = setId; targetElement.parentElement.insertBefore(element, targetElement); return element; } /** * 为元素注册监听事件 * @param {Element} element * @param {string} strType * @param {Fun} fun */ function AddEvent(element, strType, fun) { //判断浏览器有没有addEventListener方法 if (element.addEventListener) { element.addEventListener(strType, fun, false); //判断浏览器有没 有attachEvent IE8的方法 } else if (element.attachEvent) { element.attachEvent("on" + strType, fun); //如果都没有则使用 元素.事件属性这个基本方法 } else { element["on" + strType] = fun; } } /** * 为元素解绑监听事件 * @param {Element} element ---注册事件元素对象 * @param {String} strType ---注册事件名(不加on 如"click") * @param {Function} fun ---回调函数 * */ function myRemoveEvent(element, strType, fun) { //判断浏览器有没有addEventListener方法 if (element.addEventListener) { // addEventListener方法专用删除方法 element.removeEventListener(strType, fun, false); //判断浏览器有没有attachEvent IE8的方法 } else if (element.attachEvent) { // attachEvent方法专用删除事件方法 element.detachEvent("on" + strType, fun); //如果都没有则使用 元素.事件属性这个基本方法 } else { //删除事件用null element["on" + strType] = null; } } /** * 加载脚本文件 * @param {string} url 脚本地址 * @param {string} type 脚本类型 */ function loadScript(url, type = 'module') { let script = document.createElement('script'); if (type) script.setAttribute('type', type); script.setAttribute('src', url); document.head.appendChild(script); } /** * 得到思源toolbar * @returns */ function getSiYuanToolbar() { return document.getElementById("toolbar"); } /** * 得到HBuiderXToolbar * @returns */ function getHBuiderXToolbar() { return document.getElementById("HBuiderXToolbar"); } /**简单判断目前思源是否是手机模式 */ function isPhone() { return document.getElementById("toolbarEdit") != null; } /**简单判断目前思源是否是pc窗口模式 */ function isPcWindow() { return document.body.classList.contains("body--window"); } /**简单判断目前思源是pc模式 */ function isPc() { return document.getElementById("windowControls") != null; } /** * 加载样式文件 * @param {string} url 样式地址 * @param {string} id 样式 ID */ function loadStyle(url, id, cssName) { var headElement = document.head; let style = document.getElementById(id); if (id != null) { if (style) headElement.removeChild(style); } style = document.createElement('link'); if (id != null) style.id = id; style.setAttribute('type', 'text/css'); style.setAttribute('rel', 'stylesheet'); style.setAttribute('href', url); if (cssName != null) style.setAttribute("class", cssName); headElement.appendChild(style); return style; } /** * 取出两个数组的不同元素 * @param {*} arr1 * @param {*} arr2 * @returns */ function getArrDifference(arr1, arr2) { return arr1.concat(arr2).filter(function (v, i, arr) { return arr.indexOf(v) === arr.lastIndexOf(v); }); } /** * 取出两个数组的相同元素 * @param {*} arr1 * @param {*} arr2 * @returns */ function getArrEqual(arr1, arr2) { let newArr = []; for (let i = 0; i < arr2.length; i++) { for (let j = 0; j < arr1.length; j++) { if (arr1[j] === arr2[i]) { newArr.push(arr1[j]); } } } return newArr; } /** * 思源吭叽元素属性解析看是否包含那种行级元素类型 * @param {} attributes * @param {*} attribute * @returns */ function attributesContains(attributes, attribute) { if (attribute == true) return; if (attributes == null) return false; var arr = attributes.split(" "); if (arr.length != 0) { arr.forEach((v) => { if (v == attribute) attribute = true; }); return attribute == true ? true : false; } else { return attributes == attribute; } } /** * 间隔执行指定次数的函数(不立即执行) * @param {*} time 间隔时间s * @param {*} frequency 执行次数 * @param {*} Fun 执行函数 */ function IntervalFunTimes(time, frequency, Fun) { for (let i = 0; i < frequency; i++) { sleep(time * i).then(v => { Fun(); }) } function sleep(time2) { return new Promise((resolve, reject) => { setTimeout(() => { resolve() }, time2) }) } } /** * 获得当前浏览器缩放系数 默认值为1 * @returns */ function detectZoom() { var ratio = 0, screen = window.screen, ua = navigator.userAgent.toLowerCase(); if (window.devicePixelRatio !== undefined) { ratio = window.devicePixelRatio; } else if (~ua.indexOf('msie')) { if (screen.deviceXDPI && screen.logicalXDPI) { ratio = screen.deviceXDPI / screen.logicalXDPI; } } else if (window.outerWidth !== undefined && window.innerWidth !== undefined) { ratio = window.outerWidth / window.innerWidth; } if (ratio) { ratio = Math.round(ratio * 100); } return ratio * 0.01; }; /** * 递归DOM元素查找深度子级的一批符合条件的元素返回数组 * @param {*} element 要查找DOM元素 * @param {*} judgeFun 查找函数 : fun(v) return true or false * @returns array */ function diguiTooALL(element, judgeFun) { var target = []; if (element == null) return null; if (judgeFun == null) return null; digui(element); return target; function digui(elem) { var child = elem.children; if (child.length == 0) return; for (let index = 0; index < child.length; index++) { const element2 = child[index]; if (judgeFun(element2)) { target.push(element2); digui(element2); } else { digui(element2); } } } }; /** * 递归DOM元素查找深度子级的第一个符合条件的元素 - 子级的子级深度搜索赶紧后在搜索下一个子级 * @param {*} element 要查找DOM元素 * @param {*} judgeFun 查找函数: fun(v) return true or false * @returns element */ function diguiTooONE_1(element, judgeFun, xianz = 999) { if (element == null) return null; if (judgeFun == null) return null; var i = xianz <= 0 ? 10 : xianz; return digui(element); function digui(elem) { if (i <= 0) return null; i--; var child = elem.children; if (child.length == 0) return null; for (let index = 0; index < child.length; index++) { const element2 = child[index]; if (judgeFun(element2)) { return element2; } else { var item = digui(element2); if (item == null) continue; return item; } } return null; } } /** * 递归DOM元素查找深度子级的第一个符合条件的元素-同层全部筛选一遍在依次深度搜索。 * @param {*} element 要查找DOM元素 * @param {*} judgeFun 查找函数 : fun(v) return true or false * @param {*} xianz 限制递归最大次数 * @returns element */ function diguiTooONE_2(element, judgeFun, xianz = 999) { if (element == null || element.firstElementChild == null) return null; if (judgeFun == null) return null; var i = xianz <= 0 ? 10 : xianz; return digui(element); function digui(elem) { if (i <= 0) return null; i--; var child = elem.children; var newchild = []; for (let index = 0; index < child.length; index++) { const element2 = child[index]; if (judgeFun(element2)) { return element2; } else { if (newchild.firstElementChild != null) newchild.push(element2); } } for (let index = 0; index < newchild.length; index++) { const element2 = newchild[index]; var item = digui(element2); if (item == null) continue; return item; } return null; } } /** * 不断查找元素父级的父级知道这个父级符合条件函数 * @param {*} element 起始元素 * @param {*} judgeFun 条件函数 * @param {*} upTimes 限制向上查找父级次数 * @returns 返回符合条件的父级,或null */ function isFatherFather(element, judgeFun, upTimes) { var i = 0; for (; ;) { if (!element) return null; if (upTimes < 1 || i >= upTimes) return null; if (judgeFun(element)) return element; element = element.parentElement; i++; } } /** * 获得焦点所在的块 * @return {HTMLElement} 光标所在块 * @return {null} 光标不在块内 */ function getFocusedBlock() { let block = window.getSelection() && window.getSelection().focusNode && window.getSelection().focusNode.parentElement; // 当前光标 while (block != null && block.dataset.nodeId == null) block = block.parentElement; return block; } /** * 获得指定块位于的编辑区 * @params {HTMLElement} * @return {HTMLElement} 光标所在块位于的编辑区 * @return {null} 光标不在块内 */ function getTargetEditor(block) { while (block != null && !block.classList.contains('protyle-content')) block = block.parentElement; return block; } /** * 清除选中文本 */ function clearSelections() { if (window.getSelection) { var selection = window.getSelection(); selection.removeAllRanges(); } else if (document.selection && document.selection.empty) { document.selection.empty(); } } /** * body全局事件频率优化执行 * @param {*} eventStr 那种事件如 "mouseover" * @param {*} fun(e) 执行函数,e:事件对象 * @param {*} accurate 精确度:每隔多少毫秒检测一次触发事件执行 * @param {*} delay 检测到事件触发后延时执行的ms * @param {*} frequency 执行后再延时重复执行几次 * @param {*} frequencydelay 执行后再延时重复执行之间的延时时间ms */ function BodyEventRunFun(eventStr, fun, accurate = 100, delay = 0, frequency = 1, frequencydelay = 16) { var isMove = true; var _e = null; AddEvent(document.body, eventStr, (e) => { isMove = true; _e = e }) setInterval(() => { if (!isMove) return; isMove = false; setTimeout(() => { fun(_e); if (frequency == 1) return; if (frequencydelay < 16) frequencydelay = 16; var _frequencydelay = frequencydelay; for (let index = 0; index < frequency; index++) { setTimeout(() => { fun(_e); }, frequencydelay); frequencydelay += _frequencydelay; } }, delay); }, accurate); } /** * 为元素添加思源悬浮打开指定ID块内容悬浮窗事件 * @param {*} element 绑定的元素 * @param {*} id 悬浮窗内打开的块的ID */ function suspensionToOpenSiyuanSuspensionWindow(element, id) { element.setAttribute("data-defids", '[""]'); element.classList.add("popover__block"); element.setAttribute("data-id", id); } /** * 为元素添加思源点击打开指定ID块内容悬浮窗事件 * @param {*} element 绑定的元素 * @param {*} id 悬浮窗内打开的块的ID */ function clickToOpenSiyuanFloatingWindow(element, id) { element.classList.add("protyle-wysiwyg__embed"); element.setAttribute("data-id", id); } /** * 控制台打印输出 * @param {*} obj */ function c(...data) { console.log(data); } /** * 安全While循环 * frequency:限制循环次数 * 返回值不等于null终止循环 */ function WhileSafety(fun, frequency = 99999) { var i = 0; if (frequency <= 0) { console.log("安全循环次数小于等于0") return; } while (i < frequency) { var _return = fun(); if (_return != null || _return != undefined) return _return; i++; } } /**设置思源块展开 */ function setBlockfold_0(BlockId) { 设置思源块属性(BlockId, { "fold": "0" }); } /**设置思源块折叠 */ function setBlockfold_1(BlockId) { 设置思源块属性(BlockId, { "fold": "1" }); } /** * 得到光标编辑状态下的显示commonMenu菜单; * @returns */ function getcommonMenu_Cursor() { if ((window.getSelection ? window.getSelection() : document.selection.createRange().text).toString().length != 0) return null; var commonMenu = document.querySelector("#commonMenu:not(.fn__none)"); if (commonMenu == null) return null; if (commonMenu.firstChild == null) return null; if (commonMenu.children.length < 8) { return commonMenu; } return null; } /** * 得到光标选中编辑状态下的显示commonMenu菜单; * @returns */ function getcommonMenu_Cursor2() { if ((window.getSelection ? window.getSelection() : document.selection.createRange().text).toString().length != 0) { return document.querySelector("#commonMenu:not(.fn__none)"); }; return null; } /** * 得到快选中状态下的显示commonMenu菜单; * @returns */ function getcommonMenu_Bolck() { var commonMenu = document.querySelector("#commonMenu:not(.fn__none)"); if (commonMenu.children.length < 8) { return commonMenu; } return null; } function getNotebookID(docId, then) { queryAPI(`SELECT box, path FROM blocks WHERE id = '${docId}'`, (g) => { let notebook = g[0].box; then(notebook); }); } function getDocPath(docId, then) { queryAPI(`SELECT box, path FROM blocks WHERE id = '${docId}'`, (g) => { let thisDocPath = g[0].path; then(thisDocPath); }); } /**++++++++++++++++++++++++++++++++按需调用++++++++++++++++++++++++++++++ */ function 调整新窗口头栏位置() { if (!document.body.classList.contains("body--window")) return; const toolbar__window = document.querySelector("body > .toolbar__window"); const layouts = document.getElementById("layouts")?.parentElement; var toolbarTxt = insertCreateBefore(toolbar__window.firstElementChild, "div", "toolbarTxt"); if (toolbar__window && layouts) { document.body.insertBefore(toolbar__window, layouts); } /************************************************************窗口一些元素调整 */ var layoutTabBarReadonly = document.querySelector(".layout-tab-bar.layout-tab-bar--readonly.fn__flex-1"); layoutTabBarReadonly.style.paddingRight = "0px"; layoutTabBarReadonly.style.marginTop = "-26px"; /**********************************************************更改子窗口标题 */ let _id = /^\d{14}\-[0-9a-z]{7}$/; let _url = /^siyuan:\/\/blocks\/(\d{14}\-[0-9a-z]{7})\/*(?:(?:\?)(\w+=\w+)(?:(?:\&)(\w+=\w+))*)?$/; /** * 获得目标的块 ID * @params {HTMLElement} target: 目标 * @return {string} 块 ID * @return {null} 没有找到块 ID */ function getTargetBlockID(target) { let element = target; while (element != null && !(element.localName === 'a' && element.href || element.dataset.href || _id.test(element.dataset.nodeId) || _id.test(element.dataset.oid) || _id.test(element.dataset.id) || _id.test(element.dataset.rootId) )) element = element.parentElement; if (element != null) { if (_id.test(element.dataset.nodeId)) return element.dataset.nodeId; if (_id.test(element.dataset.oid)) return element.dataset.oid; if (_id.test(element.dataset.id)) return element.dataset.id; if (_id.test(element.dataset.oid)) return element.dataset.rootId; if (_url.test(element.dataset.href)) return url2id(element.dataset.href); if (_url.test(element.href)) return url2id(element.href); return element.href || element.dataset.href || null; } else return null; } function url2id(url) { let results = _url.exec(url); if (results && results.length >= 2) { return results[1]; } return null; } var reg = new RegExp('<[^>]+>', 'gi'); //过滤所有的html标签,不包括内容 var title = document.querySelector("title"); title.innerText = "[#] 思源子窗口 - HBuilderX [#]"; toolbarTxt.innerText = "[#] 思源子窗口 - HBuilderX [#]"; AddEvent(document.body, "click", (e) => { //console.log(e); var title = document.querySelector("title"); var TargetBlockID = getTargetBlockID(e.target); if (TargetBlockID == null) { title.innerText = "[#] 思源子窗口 - HBuilderX [#]"; toolbarTxt.innerText = "[#] 思源子窗口 - HBuilderX [#]"; return; }; titleTxt(TargetBlockID); }) function titleTxt(TargetBlockID) { 以id获取文档聚焦内容(TargetBlockID, (v) => { var htmltxt = v.content; var element = document.createElement("div"); element.innerHTML = htmltxt; htmltxt = diguiTooONE_1(element, (v) => { return v.getAttribute("contenteditable") == "true"; }) var txt = (htmltxt.innerText).replace(reg, ''); if (txt == "" || txt == "") { txt = "[#] 思源子窗口 - HBuilderX [#]"; 根据ID获取人类可读路径(TargetBlockID, (v) => { title.innerText = "[#] " + v.substring(1, v.length) + " [#]"; toolbarTxt.innerText = "[#] " + v.substring(1, v.length) + " [#]"; }) return; } if (txt.length > 25) { title.innerText = "[#] " + txt.substring(0, 25) + "..."; toolbarTxt.innerText = "[#] " + txt.substring(0, 25) + "..."; } else { title.innerText = "[#] " + txt + " [#]"; toolbarTxt.innerText = "[#] " + txt + " [#]"; } element.remove(); }); } } 调整新窗口头栏位置(); var Funs; if (isPhone()) { Funs = [ collapseExpand_Head_List,//鼠标中键标题、列表文本折叠/展开 () => console.log("==============>来自HBXL的附加CSS和特性JS_已经执行<==============") ]; /* */ } else { Funs = [ autoOpenList,//自动展开悬浮窗内折叠列表(第一次折叠) collapsedListPreview,//折叠列表内容预览查看 collapseExpand_Head_List,//鼠标中键标题、列表文本折叠/展开 theFloatingWindowIsClosed,//思源悬浮窗头栏中键关闭 SuspendedWindowNoSection,//思源悬浮窗检测到单个段落块,单个列表项,面包屑前一级展示 () => console.log("==============>来自HBXL的附加CSS和特性JS_已经执行<==============") ]; /* // createHBuiderXToolbar();//创建BuiderXToolbar //setTimeout(() => ClickMonitor(), 3000);//各种列表转xx showDocumentCreationDate();//为打开文档标题下面显示文档创建日期 displayParentChildDocuments2();//为文档展示父子文档 autoOpenList();//自动展开悬浮窗内折叠列表(第一次折叠) collapsedListPreview();//折叠列表内容预览查看 collapseExpand_Head_List();//鼠标中键标题、列表文本折叠/展开 // findMatchingListEntries();//查找匹配列表条目前的图标可以鼠标悬停打开悬浮窗 theFloatingWindowIsClosed();//思源悬浮窗头栏中键关闭 zoomOutToRestoreTheFloatingWindow();//钉住悬浮窗增强 SuspendedWindowNoSection();//思源悬浮窗检测到单个段落块,单个列表项,面包屑前一级展示 */ } setTimeout(() => { Funs.reverse(); var index = (Funs.length) - 1; var ID = setInterval(() => { if (index >= 0) { Funs[index](); } else { clearInterval(ID); } index--; }, 100) }, 1000) /****************************思源API操作**************************/ async function 设置思源块属性(内容块id, 属性对象) { let url = '/api/attr/setBlockAttrs' return 解析响应体(向思源请求数据(url, { id: 内容块id, attrs: 属性对象, })) } async function 向思源请求数据(url, data) { let resData = null await fetch(url, { body: JSON.stringify(data), method: 'POST', headers: { Authorization: `Token ''`, } }).then(function (response) { resData = response.json() }) return resData } async function 解析响应体(response) { let r = await response return r.code === 0 ? r.data : null } /* 操作 */ /** * 获得所选择的块对应的块 ID * @returns {string} 块 ID * @returns { * id: string, // 块 ID * type: string, // 块类型 * subtype: string, // 块子类型(若没有则为 null) * } * @returns {null} 没有找到块 ID */ function getBlockSelected() { let node_list = document.querySelectorAll('.protyle-wysiwyg--select'); if (node_list.length === 1 && node_list[0].dataset.nodeId != null) return { id: node_list[0].dataset.nodeId, type: node_list[0].dataset.type, subtype: node_list[0].dataset.subtype, }; return null; } function ClickMonitor () { window.addEventListener('mouseup', MenuShow) } function MenuShow() { setTimeout(() => { let selectinfo = getBlockSelected() if(selectinfo){ let selecttype = selectinfo.type let selectid = selectinfo.id if(selecttype=="NodeList"||selecttype=="NodeTable"||selecttype=="NodeParagraph"){ setTimeout(()=>InsertMenuItem(selectid,selecttype), 0) } } }, 0); } function InsertMenuItem(selectid,selecttype){ let commonMenu = document.querySelector(".b3-menu__items") let readonly = commonMenu.querySelector(".b3-menu__item--readonly") let selectview = commonMenu.querySelector('[id="viewselect"]') if(readonly){ if(!selectview){ commonMenu.insertBefore(ViewSelect(selectid,selecttype),readonly) commonMenu.insertBefore(MenuSeparator(),readonly) } } } function ViewMonitor(event){ let id = event.currentTarget.getAttribute("data-node-id") let attrName = 'custom-'+event.currentTarget.getAttribute("custom-attr-name") let attrValue = event.currentTarget.getAttribute("custom-attr-value") let blocks = document.querySelectorAll(`.protyle-wysiwyg [data-node-id="${id}"]`) if(blocks){ blocks.forEach(block=>block.setAttribute(attrName,attrValue)) } let attrs={} attrs[attrName] =attrValue 设置思源块属性(id,attrs) } setTimeout(()=>ClickMonitor(),1000) // REF https://github.com/Zuoqiu-Yingyi/siyuan-theme-dark-plus/blob/main/theme.js window.theme = {}; /* 颜色配置文件列表 */ window.theme.colors = [ 'palette/autumn.css', 'palette/blue.css', 'palette/cyan.css', 'palette/dark.css', 'palette/green.css', 'palette/light.css', 'palette/spring.css', 'palette/tonight.css', 'palette/fountain.css', 'palette/bearlight.css', 'palette/fruitspink.css', 'palette/mediawiki.css', 'palette/qianlan.css', 'palette/yinxiang.css', 'palette/pbook.css', ]; /* DOM 节点 ID */ window.theme.IDs = { STYLE_COLOR: 'custom-id-style-theme-color', BUTTON_TOOLBAR_CHANGE_COLOR: 'custom-id-button-toolbar-change-color', LOCAL_STORAGE_COLOR_HREF: 'langzhou-color-href', }; /* 循环迭代器 */ window.theme.Iterator = function* (items) { // REF [ES6中的迭代器(Iterator)和生成器(Generator) - 小火柴的蓝色理想 - 博客园](https://www.cnblogs.com/xiaohuochai/p/7253466.html) for (let i = 0; true; i = (i + 1) % items.length) { yield items[i]; } } /** * 加载样式文件 * @params {string} href 样式地址 * @params {string} id 样式 ID */ window.theme.loadStyle = function (href, id = null) { let style = document.createElement('link'); if (id) style.id = id; style.type = 'text/css'; style.rel = 'stylesheet'; style.href = href; document.head.appendChild(style); } /** * 更新样式文件 * @params {string} id 样式文件 ID * @params {string} href 样式文件地址 */ window.theme.updateStyle = function (id, href) { let style = document.getElementById(id); if (style) { style.setAttribute('href', href); } else { window.theme.loadStyle(href, id); } } setTimeout(() => { const drag = document.getElementById('drag'); // 标题栏 const themeStyle = document.getElementById('themeStyle'); // 当前主题引用路径 if (drag && themeStyle) { const THEME_ROOT = new URL(themeStyle.href).pathname.replace('theme.css', ''); // 当前主题根目录 /* 通过颜色配置文件列表生成完整 URL 路径 */ const colors_href = []; window.theme.colors.forEach(color => colors_href.push(`${THEME_ROOT}${color}`)); window.theme.iter = window.theme.Iterator(colors_href); var color_href = window.siyuan?.storage[window.theme.IDs.LOCAL_STORAGE_COLOR_HREF]; if (!color_href) { localStorage.getItem(window.theme.IDs.LOCAL_STORAGE_COLOR_HREF); } if (color_href) { // 将迭代器调整为当前配色 for (let i = 0; i < window.theme.colors.length; ++i) { if (window.theme.iter.next().value === color_href) break; } } else { // 迭代器第一个为当前配色 color_href = window.theme.iter.next().value; localStorage.setItem(window.theme.IDs.LOCAL_STORAGE_COLOR_HREF, color_href); setLocalStorageVal(window.theme.IDs.LOCAL_STORAGE_COLOR_HREF, color_href); } /* 加载配色文件 */ window.theme.updateStyle(window.theme.IDs.STYLE_COLOR, color_href); const button_change_color = document.createElement('button'); // 切换主题颜色按钮 button_change_color.id = window.theme.IDs.BUTTON_TOOLBAR_CHANGE_COLOR; button_change_color.className = 'toolbar__item b3-tooltips b3-tooltips__sw'; button_change_color.ariaLabel = '切换主题颜色'; button_change_color.innerHTML = `<svg><use xlink:href="#iconTheme"></use></svg>`; button_change_color.addEventListener('click', e => { color_href = window.theme.iter.next().value; localStorage.setItem(window.theme.IDs.LOCAL_STORAGE_COLOR_HREF, color_href); setLocalStorageVal(window.theme.IDs.LOCAL_STORAGE_COLOR_HREF, color_href); window.theme.updateStyle(window.theme.IDs.STYLE_COLOR, color_href); }); // REF [JS DOM 编程复习笔记 -- insertAdjacentHTML(九) - 知乎](https://zhuanlan.zhihu.com/p/425616377) drag.insertAdjacentElement('afterend', button_change_color); drag.insertAdjacentHTML('afterend', `<div class="protyle-toolbar__divider"></div>`); } }, 0); /** * 发送API请求 * @param {*} data * @param {*} url * @returns */ async function postRequest(data, url){ let result; await fetch(url, { body: JSON.stringify(data), method: 'POST', headers: { "Authorization": "Token ", "Content-Type": "application/json" } }).then((response) => { result = response.json(); }); return result; } /** * 设置LocalStorage * @param {*} ikey * @param {*} ival */ async function setLocalStorageVal(ikey, ival) { let url = "/api/storage/setLocalStorageVal"; let response = await postRequest({app: getAppId(), key: ikey, val: ival}, url); if (window.top.siyuan.storage != undefined) { window.top.siyuan.storage[ikey] = ival; } function getAppId() { let wsurl = window.top.siyuan.ws.ws.url; let appIdMatchResult = wsurl.match(new RegExp(`(\\?app=|&app=)[^&]+`, "g")); if (appIdMatchResult.length == 1){ return appIdMatchResult[0].substring(5); }else if (appIdMatchResult.length > 1) { console.warn("正则获取appId错误", appIdMatchResult); return appIdMatchResult[0].substring(5); }else { console.error("正则获取appId错误", appIdMatchResult); return ""; } } }
-
-
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于