Skip to content

Commit 5018e5d

Browse files
authored
Merge pull request #618 from terwer/dev
feat: support fold blocks - heading
2 parents 53dd17e + 81bf687 commit 5018e5d

File tree

5 files changed

+183
-17
lines changed

5 files changed

+183
-17
lines changed

apps/app/assets/css/fold.styl

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
.fold-block-wrapper
2+
display: flex
3+
align-items baseline
4+
.fold-inner
5+
flex-basis: 100% // 强制折叠内容占满一行
6+
flex-shrink: 0 // 防止被压缩
7+
margin-top: 10px // 可选,增加上方间距
8+
display: block // 确保折叠内容作为块级元素显示
9+
110
/* 按钮样式 */
211
.fold-block-toggle-button
312
display inline-block
@@ -14,4 +23,7 @@
1423
&:hover
1524
color #666 /* 悬停时文字颜色变深 */
1625
&:active
17-
color #444 /* 点击时文字颜色进一步变深 */
26+
color #444 /* 点击时文字颜色进一步变深 */
27+
28+
.protyle-wysiwyg div[fold="1"][data-type=NodeHeading]::before
29+
display none

apps/app/components/static/content/Main.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const VNode = () =>
4949
:data-page-id="props.post.postid"
5050
:data-dataviews="JSON.stringify(props.post.dataViews)"
5151
:data-embedblocks="JSON.stringify(props.post.embedBlocks)"
52+
:data-foldblocks="JSON.stringify(props.post.foldBlocks)"
5253
>
5354
<VNode />
5455
</div>

apps/app/plugins/011.fold.client.ts

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,95 @@
77
* of this license document, but changing it is not allowed.
88
*/
99

10+
import { JsonUtil, StrUtil } from "zhi-common"
11+
12+
const getFoldInner = (foldBlocksDatas: any, foldEl: any) => {
13+
const foldBlockId = foldEl.getAttribute("data-node-id")
14+
15+
if (!foldBlockId || !foldBlocksDatas) {
16+
return null
17+
}
18+
19+
const foldBlock = foldBlocksDatas.foldBlockMap[foldBlockId]
20+
21+
if (!foldBlock) {
22+
return null
23+
}
24+
25+
return foldBlock
26+
}
27+
1028
const addToggleFoldButton = (el: HTMLElement) => {
11-
// 获取所有具有 fold="1" 属性的 div 元素
12-
const divs = el.querySelectorAll("div[fold='1']")
29+
// 从data-foldblocks取数据
30+
const foldBlocksStr = el.getAttribute("data-foldblocks")
31+
const foldBlocksDatas = JsonUtil.safeParse<any>(foldBlocksStr, {} as any)
1332

33+
// 获取所有具有 fold="1" 属性的 div 元素
34+
const foldBlocks = el.querySelectorAll("div[fold='1']")
1435
// 遍历每个 div 元素,添加按钮并设置事件监听器
15-
divs.forEach((div) => {
16-
// 创建 div 元素作为按钮
36+
foldBlocks.forEach((foldEl) => {
37+
const foldBlockId = foldEl.getAttribute("data-node-id")
38+
39+
// 创建切换按钮
1740
const toggleButton = document.createElement("div")
1841
toggleButton.textContent = "+展开" // 默认文本为 +展开
1942
toggleButton.classList.add("fold-block-toggle-button") // 使用 fold-block- 前缀
2043

2144
// 获取 div 的父元素
22-
const parent = div.parentNode
23-
if (!parent) { return }
45+
const parent = foldEl.parentNode
46+
if (!parent) {
47+
return
48+
}
2449

2550
// 创建一个容器包裹按钮和原始元素,避免破坏 DOM 结构
2651
const wrapper = document.createElement("div")
27-
wrapper.style.display = "flex" // 横向排列
28-
wrapper.style.alignItems = "baseline"
29-
parent.insertBefore(wrapper, div)
52+
wrapper.classList.add("fold-block-wrapper")
53+
// 标题不能 flex
54+
if (foldEl.getAttribute("data-type") === "NodeHeading") {
55+
wrapper.style.display = "block"
56+
}
57+
58+
// 插入按钮和原始元素
59+
parent.insertBefore(wrapper, foldEl)
3060
wrapper.appendChild(toggleButton)
31-
wrapper.appendChild(div)
61+
wrapper.appendChild(foldEl)
3262

3363
// 为按钮添加点击事件
3464
toggleButton.addEventListener("click", () => {
35-
// 如果 fold 属性是 "1",则更改为 "0"
36-
if (div.getAttribute("fold") === "1") {
37-
div.setAttribute("fold", "0")
65+
const isFolded = foldEl.getAttribute("fold") === "1"
66+
67+
// 如果当前是折叠状态
68+
if (isFolded) {
69+
foldEl.setAttribute("fold", "0")
3870
toggleButton.textContent = "-收起" // 切换按钮为 -收起
71+
72+
// 获取需要追加的 HTML 内容,可能为空
73+
const newContent = getFoldInner(foldBlocksDatas, foldEl)
74+
// 如果 newContent 有值,动态创建并插入内容
75+
if (!StrUtil.isEmptyString(newContent)) {
76+
// 用于存储动态追加的内容
77+
const foldInner: HTMLElement | null = document.createElement("div")
78+
foldInner.id = "fold-inner-" + foldBlockId
79+
foldInner.innerHTML = newContent || "" // 即使是空内容,也插入空元素
80+
foldInner.classList.add("fold-inner") // 为追加的内容添加特定的类名
81+
82+
// 获取 foldEl 的父元素
83+
const parent = foldEl.parentNode
84+
if (parent) {
85+
// 将 foldInner 插入到 foldEl 的下一个兄弟元素位置
86+
parent.insertBefore(foldInner, foldEl.nextSibling)
87+
}
88+
}
3989
} else {
40-
div.setAttribute("fold", "1")
90+
// 当前是展开状态
91+
foldEl.setAttribute("fold", "1")
4192
toggleButton.textContent = "+展开" // 切换按钮为 +展开
93+
94+
// 展开状态时,删除动态追加的内容
95+
const foldInner = foldEl.parentNode?.querySelector(`#fold-inner-${foldBlockId}`)
96+
if (foldInner) {
97+
foldEl.parentNode?.removeChild(foldInner)
98+
}
4299
}
43100
})
44101
})
@@ -59,7 +116,6 @@ export default defineNuxtPlugin(({ vueApp }) => {
59116
vueApp.directive("fold", {
60117
mounted (el, binding) {
61118
addToggleFoldButton(el)
62-
logger.info("fold directive mounted")
63119
}
64120
})
65121
})

apps/siyuan/src/composables/useEmbedBlock.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const useEmbedBlock = () => {
2525

2626
/**
2727
* 获取嵌入块的内容并返回结果对象
28+
*
2829
* @param editorDom 编辑器的 DOM 字符串
2930
* @param parentDocId 父文档 ID
3031
* @returns 包含嵌入块数据的结果对象
@@ -58,7 +59,6 @@ const useEmbedBlock = () => {
5859
breadcrumb: false,
5960
})
6061

61-
debugger
6262
const resBlocks = res.blocks || []
6363
const content = resBlocks.map((block: { block: { content: string } }) => block.block.content).join("")
6464

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* GNU GENERAL PUBLIC LICENSE
3+
* Version 3, 29 June 2007
4+
*
5+
* Copyright (C) 2025 Terwer, Inc. <https://terwer.space/>
6+
* Everyone is permitted to copy and distribute verbatim copies
7+
* of this license document, but changing it is not allowed.
8+
*/
9+
10+
import * as cheerio from "cheerio"
11+
import {createAppLogger} from "../utils/appLogger.ts"
12+
import {useSiyuanApi} from "./useSiyuanApi.ts"
13+
14+
/**
15+
* 获取折叠块的内容
16+
*
17+
* @author
18+
* @since 6.1.0
19+
* @returns 折叠块相关内容
20+
*/
21+
const useFold = () => {
22+
const logger = createAppLogger("use-fold")
23+
const {kernelApi} = useSiyuanApi()
24+
25+
/**
26+
* 获取折叠块的内容并返回结果对象
27+
*
28+
* @param editorDom 编辑器的 DOM 字符串
29+
* @returns 包含折叠块数据的结果对象
30+
*/
31+
const getFoldBlocks = async (editorDom: string) => {
32+
const $ = cheerio.load(editorDom)
33+
const foldBlocks = $("div[fold='1']")
34+
35+
const results = {
36+
foldBlockMap: {} as Record<string, string>, // 折叠块内容映射
37+
order: [] as string[], // 折叠块的顺序
38+
}
39+
40+
await Promise.all(
41+
foldBlocks.map(async (_, foldBlock) => {
42+
const foldBlockId = $(foldBlock).attr("data-node-id")
43+
44+
if (!foldBlockId || $(foldBlock).attr("data-type") !== "NodeHeading") {
45+
logger.warn("折叠块缺少必要的属性或不是 NodeHeading 类型")
46+
return
47+
}
48+
49+
// 从 document 中读取 session
50+
const protyleElement = document.querySelector("div.protyle")
51+
const session = protyleElement?.getAttribute("data-id")
52+
const app = Math.random().toString(36).substring(8)
53+
54+
if (!session) {
55+
logger.error("未找到有效的 session 数据")
56+
return null
57+
}
58+
59+
// 请求折叠块
60+
try {
61+
const res = await kernelApi.siyuanRequest("/api/transactions", {
62+
session,
63+
app,
64+
transactions: [
65+
{
66+
doOperations: [{action: "unfoldHeading", id: foldBlockId}],
67+
undoOperations: [{action: "foldHeading", id: foldBlockId}],
68+
},
69+
],
70+
reqId: Date.now(),
71+
})
72+
73+
// 提取 doOperations 的 retData
74+
if (Array.isArray(res) && res.length > 0) {
75+
const doOperations = res[0].doOperations || []
76+
const operation = doOperations.find((op: any) => op.retData)
77+
if (operation && operation.retData) {
78+
results.foldBlockMap[foldBlockId] = operation.retData
79+
results.order.push(foldBlockId)
80+
}
81+
}
82+
} catch (error) {
83+
logger.error(`请求折叠块失败,ID: ${foldBlockId}`, error)
84+
}
85+
}).get() // 转换为数组
86+
)
87+
88+
logger.debug("get fold blocks success=>", results)
89+
return results
90+
}
91+
92+
return {
93+
getFoldBlocks,
94+
}
95+
}
96+
97+
export {useFold}

0 commit comments

Comments
 (0)