@tnotesjs/core 0.1.28 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +3 -1
- package/templates/sub-repo/.gitattributes +30 -0
- package/templates/sub-repo/.github/workflows/deploy.yml +87 -0
- package/templates/sub-repo/.vitepress/config.mts +12 -0
- package/templates/sub-repo/.vitepress/env.d.ts +10 -0
- package/templates/sub-repo/.vitepress/theme/index.ts +12 -0
- package/templates/sub-repo/.vscode/settings.json +330 -0
- package/templates/sub-repo/.vscode/tnotes.code-snippets +110 -0
- package/templates/sub-repo/public/favicon.ico +0 -0
- package/templates/sub-repo/public/logo.png +0 -0
- package/templates/sub-repo/tsconfig.json +45 -0
- package/vitepress/assets/icons/icon__setting_dark.svg +4 -0
- package/vitepress/assets/icons/icon__setting_light.svg +4 -0
- package/vitepress/assets/icons/index.ts +2 -2
- package/vitepress/client/localSearchIndexBridge.ts +24 -0
- package/vitepress/components/Discussions/Discussions.vue +1 -1
- package/vitepress/components/Footprints/Footprints.vue +90 -25
- package/vitepress/components/Layout/CustomSidebar.vue +489 -117
- package/vitepress/components/Layout/Layout.vue +5 -0
- package/vitepress/components/Layout/NavBarSettingsTrigger.vue +10 -2
- package/vitepress/components/Layout/SidebarItems.vue +517 -342
- package/vitepress/components/Layout/SidebarNavBefore.vue +1 -50
- package/vitepress/components/Layout/SidebarResizeHandle.vue +6 -23
- package/vitepress/components/Layout/composables/docLayoutLogic.ts +59 -0
- package/vitepress/components/Layout/composables/useDocLayout.test.ts +69 -0
- package/vitepress/components/Layout/composables/useDocLayout.ts +128 -0
- package/vitepress/components/Layout/composables/useSidebarDrag.ts +514 -0
- package/vitepress/components/Layout/composables/useSidebarLayout.ts +25 -8
- package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +8 -3
- package/vitepress/components/Layout/sidebarCollapsedState.test.ts +183 -0
- package/vitepress/components/Layout/sidebarCollapsedState.ts +241 -0
- package/vitepress/components/Layout/sidebarDragLogic.test.ts +1146 -0
- package/vitepress/components/Layout/sidebarDragLogic.ts +1307 -0
- package/vitepress/components/Layout/sidebarHitTest.test.ts +572 -0
- package/vitepress/components/Layout/sidebarHitTest.ts +801 -0
- package/vitepress/components/Layout/sidebarItemRenderLogic.test.ts +53 -0
- package/vitepress/components/Layout/sidebarItemRenderLogic.ts +50 -0
- package/vitepress/components/MarkMap/MarkMap.vue +67 -10
- package/vitepress/components/Settings/Settings.vue +78 -74
- package/vitepress/components/SidebarCard/FolderTreeItems.vue +268 -0
- package/vitepress/components/SidebarCard/SidebarCard.vue +125 -274
- package/vitepress/components/constants.ts +18 -6
- package/vitepress/components/sidebar.data.ts +13 -11
- package/vitepress/components/sidebarTreeHelpers.test.ts +142 -0
- package/vitepress/components/sidebarTreeHelpers.ts +168 -0
- package/vitepress/components/utils/vscodePaths.test.ts +41 -0
- package/vitepress/components/utils/vscodePaths.ts +52 -0
- package/vitepress/config/index.ts +2 -0
- package/vitepress/plugins/localSearchIndexBuilder.ts +209 -0
- package/vitepress/plugins/localSearchReindex.test.ts +46 -0
- package/vitepress/plugins/localSearchReindexLogic.ts +50 -0
- package/vitepress/plugins/localSearchReindexPatch.test.ts +30 -0
- package/vitepress/plugins/localSearchReindexPatch.ts +37 -0
- package/vitepress/plugins/localSearchReindexPlugin.ts +318 -0
- package/vitepress/plugins/sidebarStructurePlugin.ts +209 -63
- package/vitepress/theme/index.ts +8 -0
- package/vitepress/theme/styles/base.scss +0 -8
- package/vitepress/theme/styles/components/markmap.scss +23 -2
- package/vitepress/theme/styles/components/swiper.scss +1 -1
- package/vitepress/theme/styles/doc-layout.scss +212 -0
- package/vitepress/theme/styles/index.scss +3 -0
- package/vitepress/theme/styles/layout.scss +60 -10
- package/dist/chunk-4KG6RXFS.js +0 -4637
- package/dist/chunk-AGSL3VZE.js +0 -193
- package/dist/cli/index.js +0 -683
- package/dist/index.js +0 -8
- package/dist/vitepress/config/index.js +0 -1178
- package/vitepress/assets/icons/icon__mindmap.svg +0 -1
- package/vitepress/assets/icons/icon__setting.svg +0 -1
- package/vitepress/components/SidebarCard/MindMapView.vue +0 -488
|
@@ -0,0 +1,1307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vitepress/components/Layout/sidebarDragLogic.ts
|
|
3
|
+
*
|
|
4
|
+
* 侧边栏树形拖拽纯逻辑:flat 列表、落点投影、树移动。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { computeSidebarNodeId } from '../../../utils/tocNodeId'
|
|
8
|
+
|
|
9
|
+
import type { DropIntent } from './sidebarHitTest'
|
|
10
|
+
|
|
11
|
+
export interface SidebarTreeItem {
|
|
12
|
+
text: string
|
|
13
|
+
link?: string
|
|
14
|
+
items?: SidebarTreeItem[]
|
|
15
|
+
collapsed?: boolean
|
|
16
|
+
folderPath?: string[]
|
|
17
|
+
tocLineIndex?: number
|
|
18
|
+
/** dev 拖拽 nodeId */
|
|
19
|
+
nodeId?: string
|
|
20
|
+
/** 拖拽预览占位槽(非真实 TOC 项) */
|
|
21
|
+
isDragPlaceholder?: boolean
|
|
22
|
+
/** 拖拽中:源项原位保留空间(语雀式) */
|
|
23
|
+
isDragSourceSlot?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface FlatSidebarRow {
|
|
27
|
+
kind: 'folder' | 'note'
|
|
28
|
+
noteIndex: string | null
|
|
29
|
+
folderPath: string[] | null
|
|
30
|
+
tocLineIndex: number | null
|
|
31
|
+
nodeId: string | null
|
|
32
|
+
depth: number
|
|
33
|
+
parentNoteIndex: string | null
|
|
34
|
+
parentFolderPath: string[] | null
|
|
35
|
+
hasChildren: boolean
|
|
36
|
+
collapsed: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type DropPlacement = 'before' | 'after' | 'inside'
|
|
40
|
+
|
|
41
|
+
export interface DropTarget {
|
|
42
|
+
targetNoteIndex?: string
|
|
43
|
+
targetFolderPath?: string[]
|
|
44
|
+
targetTocLineIndex?: number
|
|
45
|
+
placement: DropPlacement
|
|
46
|
+
projectedDepth: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type DropIndicatorMode = 'line' | 'inside' | 'tail-rail' | 'root-prepend'
|
|
50
|
+
|
|
51
|
+
export interface DropIndicatorMeta {
|
|
52
|
+
mode: DropIndicatorMode
|
|
53
|
+
depthLevels: number[]
|
|
54
|
+
activeDepth: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface DropProjectionResult {
|
|
58
|
+
target: DropTarget
|
|
59
|
+
indicator: DropIndicatorMeta
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DropProjectionConfig {
|
|
63
|
+
maxDepth: number
|
|
64
|
+
indentSize: number
|
|
65
|
+
draggedSubtreeDepthSpan: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const SIDEBAR_ROW_BASE_PADDING = 6
|
|
69
|
+
export const SIDEBAR_ARROW_WIDTH = 20
|
|
70
|
+
/** 语雀式侧边栏固定行高(px) */
|
|
71
|
+
export const SIDEBAR_ROW_HEIGHT = 36
|
|
72
|
+
/** 语雀式侧边栏固定层级缩进(px) */
|
|
73
|
+
export const SIDEBAR_INDENT_SIZE = 28
|
|
74
|
+
|
|
75
|
+
export interface ReorderPayload {
|
|
76
|
+
dragType: 'note' | 'folder'
|
|
77
|
+
dragTocLineIndex: number
|
|
78
|
+
noteIndex?: string
|
|
79
|
+
targetType: 'group' | 'note'
|
|
80
|
+
targetNoteIndex?: string
|
|
81
|
+
targetFolderPath?: string[]
|
|
82
|
+
targetTocLineIndex?: number
|
|
83
|
+
placement: DropPlacement
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface YuqueReorderPayload {
|
|
87
|
+
node_uuid: string
|
|
88
|
+
action: 'moveAfter' | 'prependChild'
|
|
89
|
+
/** 拖入文件夹时必填;拖到列表顶(prepend 到根容器)时省略,与语雀一致 */
|
|
90
|
+
target_uuid?: string
|
|
91
|
+
/** legacy fallback */
|
|
92
|
+
dragTocLineIndex?: number
|
|
93
|
+
placement?: DropPlacement
|
|
94
|
+
targetTocLineIndex?: number
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface DragSource {
|
|
98
|
+
kind: 'note' | 'folder'
|
|
99
|
+
tocLineIndex: number
|
|
100
|
+
noteIndex?: string
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function extractNoteIndexFromLink(link?: string): string | null {
|
|
104
|
+
if (!link) return null
|
|
105
|
+
const match = link.match(/\/notes\/(\d{4})\./)
|
|
106
|
+
return match ? match[1] : null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface FlattenVisibleOptions {
|
|
110
|
+
/** 拖拽预览:将该 TOC 行子树在 flat 中视为折叠(仅显示一行) */
|
|
111
|
+
forceCollapsedTocLineIndex?: number | null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function flattenVisibleSidebar(
|
|
115
|
+
items: SidebarTreeItem[],
|
|
116
|
+
depth = 0,
|
|
117
|
+
parentNoteIndex: string | null = null,
|
|
118
|
+
parentFolderPath: string[] | null = null,
|
|
119
|
+
options: FlattenVisibleOptions = {},
|
|
120
|
+
): FlatSidebarRow[] {
|
|
121
|
+
const result: FlatSidebarRow[] = []
|
|
122
|
+
const forceCollapsed = options.forceCollapsedTocLineIndex
|
|
123
|
+
|
|
124
|
+
for (const item of items) {
|
|
125
|
+
if (item.isDragPlaceholder) continue
|
|
126
|
+
|
|
127
|
+
const noteIndex = extractNoteIndexFromLink(item.link)
|
|
128
|
+
const hasChildren = !!(item.items && item.items.length > 0)
|
|
129
|
+
const treatAsCollapsed =
|
|
130
|
+
!!item.collapsed ||
|
|
131
|
+
(forceCollapsed !== undefined &&
|
|
132
|
+
forceCollapsed !== null &&
|
|
133
|
+
item.tocLineIndex === forceCollapsed)
|
|
134
|
+
|
|
135
|
+
const isFolderLine =
|
|
136
|
+
!noteIndex && (hasChildren || !!(item.folderPath && item.folderPath.length > 0))
|
|
137
|
+
|
|
138
|
+
if (isFolderLine) {
|
|
139
|
+
const folderPath =
|
|
140
|
+
item.folderPath ??
|
|
141
|
+
[...(parentFolderPath ?? []), item.text.replace(/^[✅⏰]\s*/, '')]
|
|
142
|
+
result.push({
|
|
143
|
+
kind: 'folder',
|
|
144
|
+
noteIndex: null,
|
|
145
|
+
folderPath,
|
|
146
|
+
tocLineIndex: item.tocLineIndex ?? null,
|
|
147
|
+
nodeId: item.nodeId ?? computeSidebarNodeId(item),
|
|
148
|
+
depth,
|
|
149
|
+
parentNoteIndex,
|
|
150
|
+
parentFolderPath,
|
|
151
|
+
hasChildren,
|
|
152
|
+
collapsed: !!item.collapsed,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
if (hasChildren && !treatAsCollapsed) {
|
|
156
|
+
result.push(
|
|
157
|
+
...flattenVisibleSidebar(
|
|
158
|
+
item.items!,
|
|
159
|
+
depth + 1,
|
|
160
|
+
null,
|
|
161
|
+
folderPath,
|
|
162
|
+
options,
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!noteIndex) continue
|
|
170
|
+
|
|
171
|
+
result.push({
|
|
172
|
+
kind: 'note',
|
|
173
|
+
noteIndex,
|
|
174
|
+
folderPath: null,
|
|
175
|
+
tocLineIndex: item.tocLineIndex ?? null,
|
|
176
|
+
nodeId: item.nodeId ?? computeSidebarNodeId(item),
|
|
177
|
+
depth,
|
|
178
|
+
parentNoteIndex,
|
|
179
|
+
parentFolderPath,
|
|
180
|
+
hasChildren,
|
|
181
|
+
collapsed: !!item.collapsed,
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
if (hasChildren && !treatAsCollapsed) {
|
|
185
|
+
result.push(
|
|
186
|
+
...flattenVisibleSidebar(
|
|
187
|
+
item.items!,
|
|
188
|
+
depth + 1,
|
|
189
|
+
noteIndex,
|
|
190
|
+
parentFolderPath,
|
|
191
|
+
options,
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return result
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function getSubtreeDepthSpan(item: SidebarTreeItem): number {
|
|
201
|
+
if (!item.items?.length) return 0
|
|
202
|
+
return 1 + Math.max(...item.items.map(getSubtreeDepthSpan))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function findTreeItem(
|
|
206
|
+
items: SidebarTreeItem[],
|
|
207
|
+
noteIndex: string,
|
|
208
|
+
): SidebarTreeItem | null {
|
|
209
|
+
for (const item of items) {
|
|
210
|
+
if (extractNoteIndexFromLink(item.link) === noteIndex) return item
|
|
211
|
+
if (item.items?.length) {
|
|
212
|
+
const found = findTreeItem(item.items, noteIndex)
|
|
213
|
+
if (found) return found
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return null
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function isDescendantInTree(
|
|
220
|
+
items: SidebarTreeItem[],
|
|
221
|
+
ancestorNoteIndex: string,
|
|
222
|
+
candidateNoteIndex: string,
|
|
223
|
+
): boolean {
|
|
224
|
+
const ancestor = findTreeItem(items, ancestorNoteIndex)
|
|
225
|
+
if (!ancestor?.items?.length) return false
|
|
226
|
+
|
|
227
|
+
function walk(nodes: SidebarTreeItem[]): boolean {
|
|
228
|
+
for (const node of nodes) {
|
|
229
|
+
const id = extractNoteIndexFromLink(node.link)
|
|
230
|
+
if (id === candidateNoteIndex) return true
|
|
231
|
+
if (node.items?.length && walk(node.items)) return true
|
|
232
|
+
}
|
|
233
|
+
return false
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return walk(ancestor.items)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function isInvalidDrop(
|
|
240
|
+
tree: SidebarTreeItem[],
|
|
241
|
+
flat: FlatSidebarRow[],
|
|
242
|
+
dragSource: DragSource,
|
|
243
|
+
target: DropTarget,
|
|
244
|
+
config: DropProjectionConfig,
|
|
245
|
+
): boolean {
|
|
246
|
+
if (
|
|
247
|
+
target.targetTocLineIndex !== undefined &&
|
|
248
|
+
target.targetTocLineIndex === dragSource.tocLineIndex
|
|
249
|
+
) {
|
|
250
|
+
return true
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const dragFlatIndex = flat.findIndex(
|
|
254
|
+
(row) => row.tocLineIndex === dragSource.tocLineIndex,
|
|
255
|
+
)
|
|
256
|
+
if (target.targetTocLineIndex !== undefined && dragFlatIndex >= 0) {
|
|
257
|
+
const targetFlatIndex = flat.findIndex(
|
|
258
|
+
(row) => row.tocLineIndex === target.targetTocLineIndex,
|
|
259
|
+
)
|
|
260
|
+
if (
|
|
261
|
+
targetFlatIndex >= 0 &&
|
|
262
|
+
isFlatRowInDragSubtree(flat, dragFlatIndex, targetFlatIndex)
|
|
263
|
+
) {
|
|
264
|
+
return true
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (
|
|
269
|
+
dragSource.noteIndex &&
|
|
270
|
+
target.targetNoteIndex &&
|
|
271
|
+
dragSource.noteIndex === target.targetNoteIndex
|
|
272
|
+
) {
|
|
273
|
+
return true
|
|
274
|
+
}
|
|
275
|
+
if (
|
|
276
|
+
dragSource.noteIndex &&
|
|
277
|
+
target.targetNoteIndex &&
|
|
278
|
+
isDescendantInTree(tree, dragSource.noteIndex, target.targetNoteIndex)
|
|
279
|
+
) {
|
|
280
|
+
return true
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (config.maxDepth > 0 && Number.isFinite(config.maxDepth)) {
|
|
284
|
+
if (target.placement === 'inside') {
|
|
285
|
+
const deepestIndent =
|
|
286
|
+
target.projectedDepth + config.draggedSubtreeDepthSpan
|
|
287
|
+
if (deepestIndent >= config.maxDepth) return true
|
|
288
|
+
} else {
|
|
289
|
+
const maxRootDepth = config.maxDepth - 1 - config.draggedSubtreeDepthSpan
|
|
290
|
+
if (target.projectedDepth > maxRootDepth) return true
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (target.projectedDepth < 0) return true
|
|
295
|
+
|
|
296
|
+
return false
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isFlatRowInDragSubtree(
|
|
300
|
+
flat: FlatSidebarRow[],
|
|
301
|
+
dragFlatIndex: number,
|
|
302
|
+
candidateFlatIndex: number,
|
|
303
|
+
): boolean {
|
|
304
|
+
if (candidateFlatIndex === dragFlatIndex) return true
|
|
305
|
+
const dragRow = flat[dragFlatIndex]
|
|
306
|
+
if (!dragRow || candidateFlatIndex < dragFlatIndex) return false
|
|
307
|
+
|
|
308
|
+
for (let i = dragFlatIndex + 1; i <= candidateFlatIndex; i++) {
|
|
309
|
+
if (flat[i].depth <= dragRow.depth) {
|
|
310
|
+
return false
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return candidateFlatIndex > dragFlatIndex
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function hasDepthLimit(config: DropProjectionConfig): boolean {
|
|
318
|
+
return config.maxDepth > 0 && Number.isFinite(config.maxDepth)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function getMaxRootDepth(
|
|
322
|
+
config: DropProjectionConfig,
|
|
323
|
+
overDepth: number,
|
|
324
|
+
): number {
|
|
325
|
+
if (!hasDepthLimit(config)) return overDepth
|
|
326
|
+
return Math.max(
|
|
327
|
+
0,
|
|
328
|
+
config.maxDepth - 1 - config.draggedSubtreeDepthSpan,
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function clamp(value: number, min: number, max: number): number {
|
|
333
|
+
return Math.min(max, Math.max(min, value))
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function findRowAtDepth(
|
|
337
|
+
flat: FlatSidebarRow[],
|
|
338
|
+
startIndex: number,
|
|
339
|
+
depth: number,
|
|
340
|
+
): FlatSidebarRow | null {
|
|
341
|
+
for (let i = startIndex; i >= 0; i--) {
|
|
342
|
+
if (flat[i].depth === depth) return flat[i]
|
|
343
|
+
if (flat[i].depth < depth) break
|
|
344
|
+
}
|
|
345
|
+
return null
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** 是否为同级块末行 after 落点(语雀:仅此时展示上级导轨) */
|
|
349
|
+
export function isTailAfterDrop(
|
|
350
|
+
flat: FlatSidebarRow[],
|
|
351
|
+
overIndex: number,
|
|
352
|
+
verticalPlacement: 'before' | 'after',
|
|
353
|
+
dragNoteIndex?: string | null,
|
|
354
|
+
dragTocLineIndex?: number | null,
|
|
355
|
+
): boolean {
|
|
356
|
+
if (verticalPlacement !== 'after') return false
|
|
357
|
+
|
|
358
|
+
const overRow = flat[overIndex]
|
|
359
|
+
if (!overRow) return false
|
|
360
|
+
|
|
361
|
+
const next = flat[overIndex + 1]
|
|
362
|
+
if (!next) return true
|
|
363
|
+
|
|
364
|
+
if (next.depth < overRow.depth) return true
|
|
365
|
+
|
|
366
|
+
if (
|
|
367
|
+
dragTocLineIndex !== undefined &&
|
|
368
|
+
dragTocLineIndex !== null &&
|
|
369
|
+
next.tocLineIndex === dragTocLineIndex
|
|
370
|
+
) {
|
|
371
|
+
const dragRow = findFlatRowByTocLineIndex(flat, dragTocLineIndex)
|
|
372
|
+
if (
|
|
373
|
+
dragRow &&
|
|
374
|
+
dragRow.depth === overRow.depth &&
|
|
375
|
+
dragRow.parentNoteIndex === overRow.parentNoteIndex &&
|
|
376
|
+
folderPathsEqual(dragRow.parentFolderPath, overRow.parentFolderPath)
|
|
377
|
+
) {
|
|
378
|
+
return !overRow.hasChildren
|
|
379
|
+
}
|
|
380
|
+
return (
|
|
381
|
+
overRow.depth > 0 &&
|
|
382
|
+
dragRow?.parentNoteIndex === overRow.parentNoteIndex &&
|
|
383
|
+
folderPathsEqual(dragRow.parentFolderPath, overRow.parentFolderPath)
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (dragNoteIndex && next.noteIndex === dragNoteIndex) {
|
|
388
|
+
const dragRow = findFlatRowByNoteIndex(flat, dragNoteIndex)
|
|
389
|
+
if (
|
|
390
|
+
dragRow &&
|
|
391
|
+
dragRow.depth === overRow.depth &&
|
|
392
|
+
dragRow.parentNoteIndex === overRow.parentNoteIndex
|
|
393
|
+
) {
|
|
394
|
+
// 紧邻同级:hover 文件夹父行 → 不算 tail;hover 前一条子笔记 → 仍走 tail 导轨
|
|
395
|
+
return !overRow.hasChildren
|
|
396
|
+
}
|
|
397
|
+
return (
|
|
398
|
+
overRow.depth > 0 &&
|
|
399
|
+
dragRow?.parentNoteIndex === overRow.parentNoteIndex
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return false
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** 拖拽项是否为 flat 列表中紧邻的下一行同级笔记 */
|
|
407
|
+
function isImmediateNextSibling(
|
|
408
|
+
flat: FlatSidebarRow[],
|
|
409
|
+
overIndex: number,
|
|
410
|
+
dragSource: DragSource,
|
|
411
|
+
): boolean {
|
|
412
|
+
const overRow = flat[overIndex]
|
|
413
|
+
const next = flat[overIndex + 1]
|
|
414
|
+
if (!overRow || !next) return false
|
|
415
|
+
|
|
416
|
+
const dragRow = findFlatRowByTocLineIndex(flat, dragSource.tocLineIndex)
|
|
417
|
+
if (!dragRow || next.tocLineIndex !== dragSource.tocLineIndex) return false
|
|
418
|
+
|
|
419
|
+
return (
|
|
420
|
+
dragRow.depth === overRow.depth &&
|
|
421
|
+
dragRow.parentNoteIndex === overRow.parentNoteIndex &&
|
|
422
|
+
folderPathsEqual(dragRow.parentFolderPath, overRow.parentFolderPath)
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function tryInsideFirstForExternalDrag(
|
|
427
|
+
tree: SidebarTreeItem[],
|
|
428
|
+
flat: FlatSidebarRow[],
|
|
429
|
+
dragSource: DragSource,
|
|
430
|
+
parentRow: FlatSidebarRow,
|
|
431
|
+
config: DropProjectionConfig,
|
|
432
|
+
): DropProjectionResult | null {
|
|
433
|
+
const dragRow = findFlatRowByTocLineIndex(flat, dragSource.tocLineIndex)
|
|
434
|
+
if (!dragRow) return null
|
|
435
|
+
|
|
436
|
+
const isExternal = isDragExternalToTargetParent(flat, dragRow, parentRow)
|
|
437
|
+
if (!isExternal) return null
|
|
438
|
+
|
|
439
|
+
const target = buildInsideTarget(parentRow)
|
|
440
|
+
return finalizeProjection(
|
|
441
|
+
tree,
|
|
442
|
+
flat,
|
|
443
|
+
dragSource,
|
|
444
|
+
target,
|
|
445
|
+
buildInsideIndicator(target.projectedDepth),
|
|
446
|
+
config,
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** 语雀:外部拖入折叠父级整行 → prependChild(不区分 before/after 半区) */
|
|
451
|
+
function tryPrependInsideExternalCollapsedParent(
|
|
452
|
+
tree: SidebarTreeItem[],
|
|
453
|
+
flat: FlatSidebarRow[],
|
|
454
|
+
dragSource: DragSource,
|
|
455
|
+
parentRow: FlatSidebarRow,
|
|
456
|
+
config: DropProjectionConfig,
|
|
457
|
+
verticalPlacement: 'before' | 'after',
|
|
458
|
+
): DropProjectionResult | null {
|
|
459
|
+
if (parentRow.kind !== 'folder' || !parentRow.collapsed) return null
|
|
460
|
+
if (verticalPlacement === 'before' && !parentRow.hasChildren) return null
|
|
461
|
+
return tryInsideFirstForExternalDrag(
|
|
462
|
+
tree,
|
|
463
|
+
flat,
|
|
464
|
+
dragSource,
|
|
465
|
+
parentRow,
|
|
466
|
+
config,
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** 语雀:外部笔记/分组拖入笔记行 → prependChild(整行有效,不要求右缩进) */
|
|
471
|
+
function tryPrependInsideExternalNoteRow(
|
|
472
|
+
tree: SidebarTreeItem[],
|
|
473
|
+
flat: FlatSidebarRow[],
|
|
474
|
+
dragSource: DragSource,
|
|
475
|
+
overIndex: number,
|
|
476
|
+
overRow: FlatSidebarRow,
|
|
477
|
+
verticalPlacement: 'before' | 'after',
|
|
478
|
+
config: DropProjectionConfig,
|
|
479
|
+
): DropProjectionResult | null {
|
|
480
|
+
if (
|
|
481
|
+
!shouldPrependInsideExternalNoteRow(
|
|
482
|
+
flat,
|
|
483
|
+
dragSource,
|
|
484
|
+
overIndex,
|
|
485
|
+
overRow,
|
|
486
|
+
verticalPlacement,
|
|
487
|
+
)
|
|
488
|
+
) {
|
|
489
|
+
return null
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const dragRow = findFlatRowByTocLineIndex(flat, dragSource.tocLineIndex)
|
|
493
|
+
if (!dragRow) return null
|
|
494
|
+
|
|
495
|
+
const target = buildInsideTarget(overRow)
|
|
496
|
+
return finalizeProjection(
|
|
497
|
+
tree,
|
|
498
|
+
flat,
|
|
499
|
+
dragSource,
|
|
500
|
+
target,
|
|
501
|
+
buildInsideIndicator(target.projectedDepth),
|
|
502
|
+
config,
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export function isDragExternalToTargetParent(
|
|
507
|
+
flat: FlatSidebarRow[],
|
|
508
|
+
dragRow: FlatSidebarRow,
|
|
509
|
+
parentRow: FlatSidebarRow,
|
|
510
|
+
): boolean {
|
|
511
|
+
if (parentRow.kind === 'folder' && parentRow.folderPath) {
|
|
512
|
+
return !folderPathsEqual(dragRow.parentFolderPath, parentRow.folderPath)
|
|
513
|
+
}
|
|
514
|
+
if (parentRow.noteIndex) {
|
|
515
|
+
return dragRow.parentNoteIndex !== parentRow.noteIndex
|
|
516
|
+
}
|
|
517
|
+
return true
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** 是否应将外部拖拽整行判为拖入笔记行(prependChild / inside) */
|
|
521
|
+
export function shouldPrependInsideExternalNoteRow(
|
|
522
|
+
flat: FlatSidebarRow[],
|
|
523
|
+
dragSource: DragSource,
|
|
524
|
+
overIndex: number,
|
|
525
|
+
overRow: FlatSidebarRow,
|
|
526
|
+
verticalPlacement: 'before' | 'after',
|
|
527
|
+
): boolean {
|
|
528
|
+
if (!overRow.noteIndex) return false
|
|
529
|
+
|
|
530
|
+
const dragIndex = flat.findIndex(
|
|
531
|
+
(row) => row.tocLineIndex === dragSource.tocLineIndex,
|
|
532
|
+
)
|
|
533
|
+
if (dragIndex < 0) return false
|
|
534
|
+
|
|
535
|
+
const dragRow = flat[dragIndex]
|
|
536
|
+
if (!isDragExternalToTargetParent(flat, dragRow, overRow)) return false
|
|
537
|
+
|
|
538
|
+
if (dragSource.kind === 'folder') return true
|
|
539
|
+
|
|
540
|
+
if (verticalPlacement === 'before') return false
|
|
541
|
+
|
|
542
|
+
const nextRow = flat[overIndex + 1]
|
|
543
|
+
if (nextRow?.tocLineIndex === dragSource.tocLineIndex) {
|
|
544
|
+
return overRow.depth === 0 && dragRow.depth === 0
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (
|
|
548
|
+
isTailAfterDrop(
|
|
549
|
+
flat,
|
|
550
|
+
overIndex,
|
|
551
|
+
verticalPlacement,
|
|
552
|
+
dragSource.noteIndex,
|
|
553
|
+
dragSource.tocLineIndex,
|
|
554
|
+
)
|
|
555
|
+
) {
|
|
556
|
+
return false
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return true
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** 由指针绝对 X 计算目标 depth(0..maxSelectableDepth) */
|
|
563
|
+
export function depthFromClientX(
|
|
564
|
+
clientX: number,
|
|
565
|
+
navLeft: number,
|
|
566
|
+
indentSize: number,
|
|
567
|
+
maxSelectableDepth: number,
|
|
568
|
+
): number {
|
|
569
|
+
const base = navLeft + SIDEBAR_ROW_BASE_PADDING + SIDEBAR_ARROW_WIDTH
|
|
570
|
+
const raw = Math.round((clientX - base) / indentSize)
|
|
571
|
+
return clamp(raw, 0, maxSelectableDepth)
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export function getTailDepthLevels(overDepth: number): number[] {
|
|
575
|
+
return Array.from({ length: overDepth + 1 }, (_, index) => index)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export function getDepthContentLeft(depth: number, indentSize: number): number {
|
|
579
|
+
return SIDEBAR_ROW_BASE_PADDING + depth * indentSize + SIDEBAR_ARROW_WIDTH
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** 尾行 after:在选定 depth 对应 ancestor 子树末尾插入 */
|
|
583
|
+
export function resolveTailDropTarget(
|
|
584
|
+
flat: FlatSidebarRow[],
|
|
585
|
+
overIndex: number,
|
|
586
|
+
targetDepth: number,
|
|
587
|
+
): DropTarget {
|
|
588
|
+
const overRow = flat[overIndex]
|
|
589
|
+
const anchor =
|
|
590
|
+
targetDepth < overRow.depth
|
|
591
|
+
? findRowAtDepth(flat, overIndex, targetDepth) ?? overRow
|
|
592
|
+
: overRow
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
targetNoteIndex: anchor.noteIndex ?? undefined,
|
|
596
|
+
targetFolderPath:
|
|
597
|
+
anchor.kind === 'folder' ? anchor.folderPath ?? undefined : undefined,
|
|
598
|
+
targetTocLineIndex: anchor.tocLineIndex ?? undefined,
|
|
599
|
+
placement: 'after',
|
|
600
|
+
projectedDepth: targetDepth,
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function findFlatRowByTocLineIndex(
|
|
605
|
+
flat: FlatSidebarRow[],
|
|
606
|
+
tocLineIndex: number,
|
|
607
|
+
): FlatSidebarRow | undefined {
|
|
608
|
+
return flat.find((row) => row.tocLineIndex === tocLineIndex)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function findFlatRowByNoteIndex(
|
|
612
|
+
flat: FlatSidebarRow[],
|
|
613
|
+
noteIndex: string,
|
|
614
|
+
): FlatSidebarRow | undefined {
|
|
615
|
+
return flat.find((row) => row.noteIndex === noteIndex)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function findFlatRowByFolderPath(
|
|
619
|
+
flat: FlatSidebarRow[],
|
|
620
|
+
folderPath: string[],
|
|
621
|
+
): FlatSidebarRow | undefined {
|
|
622
|
+
const key = folderPath.join('/')
|
|
623
|
+
return flat.find(
|
|
624
|
+
(row) => row.kind === 'folder' && row.folderPath?.join('/') === key,
|
|
625
|
+
)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function folderPathsEqual(
|
|
629
|
+
left: string[] | null | undefined,
|
|
630
|
+
right: string[] | null | undefined,
|
|
631
|
+
): boolean {
|
|
632
|
+
if (!left && !right) return true
|
|
633
|
+
if (!left || !right) return false
|
|
634
|
+
return left.join('/') === right.join('/')
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** 拖拽项不是目标父节点的直接子笔记(从外部拖入文件夹) */
|
|
638
|
+
export function isDragExternalToParent(
|
|
639
|
+
flat: FlatSidebarRow[],
|
|
640
|
+
dragNoteIndex: string,
|
|
641
|
+
parentNoteIndex: string,
|
|
642
|
+
): boolean {
|
|
643
|
+
const dragRow = findFlatRowByNoteIndex(flat, dragNoteIndex)
|
|
644
|
+
if (!dragRow || dragNoteIndex === parentNoteIndex) return false
|
|
645
|
+
return dragRow.parentNoteIndex !== parentNoteIndex
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function buildInsideTarget(parentRow: FlatSidebarRow): DropTarget {
|
|
649
|
+
const base = {
|
|
650
|
+
placement: 'inside' as const,
|
|
651
|
+
projectedDepth: parentRow.depth + 1,
|
|
652
|
+
targetTocLineIndex: parentRow.tocLineIndex ?? undefined,
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (parentRow.kind === 'folder' && parentRow.folderPath) {
|
|
656
|
+
return {
|
|
657
|
+
...base,
|
|
658
|
+
targetFolderPath: parentRow.folderPath,
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return {
|
|
663
|
+
...base,
|
|
664
|
+
targetNoteIndex: parentRow.noteIndex ?? undefined,
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function buildLineIndicator(depth: number): DropIndicatorMeta {
|
|
669
|
+
return {
|
|
670
|
+
mode: 'line',
|
|
671
|
+
depthLevels: [depth],
|
|
672
|
+
activeDepth: depth,
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function buildInsideIndicator(depth: number): DropIndicatorMeta {
|
|
677
|
+
return {
|
|
678
|
+
mode: 'inside',
|
|
679
|
+
depthLevels: [depth],
|
|
680
|
+
activeDepth: depth,
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function buildTailIndicator(overDepth: number, activeDepth: number): DropIndicatorMeta {
|
|
685
|
+
return {
|
|
686
|
+
mode: 'tail-rail',
|
|
687
|
+
depthLevels: getTailDepthLevels(overDepth),
|
|
688
|
+
activeDepth,
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function finalizeProjection(
|
|
693
|
+
tree: SidebarTreeItem[],
|
|
694
|
+
flat: FlatSidebarRow[],
|
|
695
|
+
dragSource: DragSource,
|
|
696
|
+
target: DropTarget,
|
|
697
|
+
indicator: DropIndicatorMeta,
|
|
698
|
+
config: DropProjectionConfig,
|
|
699
|
+
): DropProjectionResult | null {
|
|
700
|
+
if (isInvalidDrop(tree, flat, dragSource, target, config)) return null
|
|
701
|
+
return { target, indicator }
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* 根据 hover 行、垂直落点与指针位置计算 TOC 移动目标。
|
|
706
|
+
*/
|
|
707
|
+
export function getDropProjection(
|
|
708
|
+
tree: SidebarTreeItem[],
|
|
709
|
+
flat: FlatSidebarRow[],
|
|
710
|
+
dragSource: DragSource,
|
|
711
|
+
overIndex: number,
|
|
712
|
+
verticalPlacement: 'before' | 'after',
|
|
713
|
+
config: DropProjectionConfig,
|
|
714
|
+
pointer: {
|
|
715
|
+
offsetX: number
|
|
716
|
+
clientX: number
|
|
717
|
+
navLeft: number
|
|
718
|
+
},
|
|
719
|
+
): DropProjectionResult | null {
|
|
720
|
+
if (overIndex < 0 || overIndex >= flat.length) return null
|
|
721
|
+
|
|
722
|
+
const overRow = flat[overIndex]
|
|
723
|
+
const dragRow = findFlatRowByTocLineIndex(flat, dragSource.tocLineIndex)
|
|
724
|
+
if (!overRow || !dragRow) return null
|
|
725
|
+
if (overRow.tocLineIndex === dragSource.tocLineIndex) return null
|
|
726
|
+
|
|
727
|
+
const dragNoteIndex = dragSource.noteIndex ?? ''
|
|
728
|
+
const maxRootDepth = getMaxRootDepth(config, overRow.depth)
|
|
729
|
+
|
|
730
|
+
const collapsedInside = tryPrependInsideExternalCollapsedParent(
|
|
731
|
+
tree,
|
|
732
|
+
flat,
|
|
733
|
+
dragSource,
|
|
734
|
+
overRow,
|
|
735
|
+
config,
|
|
736
|
+
verticalPlacement,
|
|
737
|
+
)
|
|
738
|
+
if (collapsedInside) return collapsedInside
|
|
739
|
+
|
|
740
|
+
const insideExternalNote = tryPrependInsideExternalNoteRow(
|
|
741
|
+
tree,
|
|
742
|
+
flat,
|
|
743
|
+
dragSource,
|
|
744
|
+
overIndex,
|
|
745
|
+
overRow,
|
|
746
|
+
verticalPlacement,
|
|
747
|
+
config,
|
|
748
|
+
)
|
|
749
|
+
if (insideExternalNote) return insideExternalNote
|
|
750
|
+
|
|
751
|
+
if (
|
|
752
|
+
isTailAfterDrop(
|
|
753
|
+
flat,
|
|
754
|
+
overIndex,
|
|
755
|
+
verticalPlacement,
|
|
756
|
+
dragNoteIndex || null,
|
|
757
|
+
dragSource.tocLineIndex,
|
|
758
|
+
)
|
|
759
|
+
) {
|
|
760
|
+
const pointerDepth = clamp(
|
|
761
|
+
depthFromClientX(
|
|
762
|
+
pointer.clientX,
|
|
763
|
+
pointer.navLeft,
|
|
764
|
+
config.indentSize,
|
|
765
|
+
overRow.depth,
|
|
766
|
+
),
|
|
767
|
+
0,
|
|
768
|
+
Math.min(overRow.depth, maxRootDepth),
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
const parentRow = overRow.parentNoteIndex
|
|
772
|
+
? findFlatRowByNoteIndex(flat, overRow.parentNoteIndex)
|
|
773
|
+
: overRow.parentFolderPath
|
|
774
|
+
? findFlatRowByFolderPath(flat, overRow.parentFolderPath)
|
|
775
|
+
: undefined
|
|
776
|
+
const nextRow = flat[overIndex + 1]
|
|
777
|
+
const draggingLastChildFromTail =
|
|
778
|
+
!!parentRow &&
|
|
779
|
+
!!nextRow &&
|
|
780
|
+
nextRow.tocLineIndex === dragSource.tocLineIndex &&
|
|
781
|
+
(parentRow.noteIndex
|
|
782
|
+
? dragRow.parentNoteIndex === parentRow.noteIndex
|
|
783
|
+
: folderPathsEqual(dragRow.parentFolderPath, parentRow.folderPath))
|
|
784
|
+
const externalSameLevel =
|
|
785
|
+
!!parentRow &&
|
|
786
|
+
isDragExternalToTargetParent(flat, dragRow, parentRow) &&
|
|
787
|
+
dragRow.depth <= parentRow.depth
|
|
788
|
+
|
|
789
|
+
let targetDepth = pointerDepth
|
|
790
|
+
if (draggingLastChildFromTail && parentRow) {
|
|
791
|
+
targetDepth =
|
|
792
|
+
pointerDepth <= parentRow.depth ? parentRow.depth : pointerDepth
|
|
793
|
+
} else if (externalSameLevel && parentRow) {
|
|
794
|
+
targetDepth =
|
|
795
|
+
pointerDepth <= parentRow.depth ? parentRow.depth : overRow.depth
|
|
796
|
+
} else if (
|
|
797
|
+
pointerDepth === overRow.depth &&
|
|
798
|
+
(overRow.parentNoteIndex || overRow.parentFolderPath)
|
|
799
|
+
) {
|
|
800
|
+
const parentForInside = overRow.parentNoteIndex
|
|
801
|
+
? findFlatRowByNoteIndex(flat, overRow.parentNoteIndex)
|
|
802
|
+
: overRow.parentFolderPath
|
|
803
|
+
? findFlatRowByFolderPath(flat, overRow.parentFolderPath)
|
|
804
|
+
: undefined
|
|
805
|
+
if (parentForInside) {
|
|
806
|
+
const insideFirst = tryInsideFirstForExternalDrag(
|
|
807
|
+
tree,
|
|
808
|
+
flat,
|
|
809
|
+
dragSource,
|
|
810
|
+
parentForInside,
|
|
811
|
+
config,
|
|
812
|
+
)
|
|
813
|
+
if (insideFirst) {
|
|
814
|
+
return insideFirst
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const target = resolveTailDropTarget(flat, overIndex, targetDepth)
|
|
820
|
+
return finalizeProjection(
|
|
821
|
+
tree,
|
|
822
|
+
flat,
|
|
823
|
+
dragSource,
|
|
824
|
+
target,
|
|
825
|
+
buildTailIndicator(overRow.depth, targetDepth),
|
|
826
|
+
config,
|
|
827
|
+
)
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (verticalPlacement === 'before') {
|
|
831
|
+
const target: DropTarget =
|
|
832
|
+
overRow.kind === 'folder' && overRow.folderPath
|
|
833
|
+
? {
|
|
834
|
+
targetFolderPath: overRow.folderPath,
|
|
835
|
+
targetTocLineIndex: overRow.tocLineIndex ?? undefined,
|
|
836
|
+
placement: 'before',
|
|
837
|
+
projectedDepth: overRow.depth,
|
|
838
|
+
}
|
|
839
|
+
: {
|
|
840
|
+
targetNoteIndex: overRow.noteIndex ?? undefined,
|
|
841
|
+
targetTocLineIndex: overRow.tocLineIndex ?? undefined,
|
|
842
|
+
placement: 'before',
|
|
843
|
+
projectedDepth: overRow.depth,
|
|
844
|
+
}
|
|
845
|
+
return finalizeProjection(
|
|
846
|
+
tree,
|
|
847
|
+
flat,
|
|
848
|
+
dragSource,
|
|
849
|
+
target,
|
|
850
|
+
buildLineIndicator(overRow.depth),
|
|
851
|
+
config,
|
|
852
|
+
)
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const depthDelta = Math.round(pointer.offsetX / config.indentSize)
|
|
856
|
+
const canNestInside =
|
|
857
|
+
depthDelta >= 1 &&
|
|
858
|
+
overRow.depth + 1 <= maxRootDepth &&
|
|
859
|
+
(!hasDepthLimit(config) || overRow.depth + 1 < config.maxDepth)
|
|
860
|
+
|
|
861
|
+
if (canNestInside) {
|
|
862
|
+
const target = buildInsideTarget(overRow)
|
|
863
|
+
return finalizeProjection(
|
|
864
|
+
tree,
|
|
865
|
+
flat,
|
|
866
|
+
dragSource,
|
|
867
|
+
target,
|
|
868
|
+
buildInsideIndicator(target.projectedDepth),
|
|
869
|
+
config,
|
|
870
|
+
)
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (
|
|
874
|
+
overRow.hasChildren &&
|
|
875
|
+
depthDelta < 1 &&
|
|
876
|
+
isImmediateNextSibling(flat, overIndex, dragSource)
|
|
877
|
+
) {
|
|
878
|
+
const insideFirst = tryInsideFirstForExternalDrag(
|
|
879
|
+
tree,
|
|
880
|
+
flat,
|
|
881
|
+
dragSource,
|
|
882
|
+
overRow,
|
|
883
|
+
config,
|
|
884
|
+
)
|
|
885
|
+
if (insideFirst) return insideFirst
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const target: DropTarget =
|
|
889
|
+
overRow.kind === 'folder' && overRow.folderPath
|
|
890
|
+
? {
|
|
891
|
+
targetFolderPath: overRow.folderPath,
|
|
892
|
+
targetTocLineIndex: overRow.tocLineIndex ?? undefined,
|
|
893
|
+
placement: 'after',
|
|
894
|
+
projectedDepth: overRow.depth,
|
|
895
|
+
}
|
|
896
|
+
: {
|
|
897
|
+
targetNoteIndex: overRow.noteIndex ?? undefined,
|
|
898
|
+
targetTocLineIndex: overRow.tocLineIndex ?? undefined,
|
|
899
|
+
placement: 'after',
|
|
900
|
+
projectedDepth: overRow.depth,
|
|
901
|
+
}
|
|
902
|
+
return finalizeProjection(
|
|
903
|
+
tree,
|
|
904
|
+
flat,
|
|
905
|
+
dragSource,
|
|
906
|
+
target,
|
|
907
|
+
buildLineIndicator(overRow.depth),
|
|
908
|
+
config,
|
|
909
|
+
)
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
export function toReorderPayload(
|
|
913
|
+
dragSource: DragSource,
|
|
914
|
+
target: DropTarget,
|
|
915
|
+
): ReorderPayload {
|
|
916
|
+
const isFolderTarget =
|
|
917
|
+
!!target.targetFolderPath?.length && !target.targetNoteIndex
|
|
918
|
+
|
|
919
|
+
return {
|
|
920
|
+
dragType: dragSource.kind,
|
|
921
|
+
dragTocLineIndex: dragSource.tocLineIndex,
|
|
922
|
+
noteIndex: dragSource.noteIndex,
|
|
923
|
+
targetType:
|
|
924
|
+
target.placement === 'inside' || isFolderTarget ? 'group' : 'note',
|
|
925
|
+
targetTocLineIndex: target.targetTocLineIndex,
|
|
926
|
+
targetFolderPath: target.targetFolderPath,
|
|
927
|
+
targetNoteIndex: target.targetNoteIndex,
|
|
928
|
+
placement: target.placement,
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
interface RemoveResult {
|
|
933
|
+
tree: SidebarTreeItem[]
|
|
934
|
+
removed: SidebarTreeItem | null
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function cloneSidebarTree(items: SidebarTreeItem[]): SidebarTreeItem[] {
|
|
938
|
+
return items.map((item) => ({
|
|
939
|
+
...item,
|
|
940
|
+
items: item.items ? cloneSidebarTree(item.items) : undefined,
|
|
941
|
+
}))
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function itemMatchesDropTarget(
|
|
945
|
+
item: SidebarTreeItem,
|
|
946
|
+
target: DropTarget,
|
|
947
|
+
): boolean {
|
|
948
|
+
if (
|
|
949
|
+
target.targetTocLineIndex !== undefined &&
|
|
950
|
+
item.tocLineIndex === target.targetTocLineIndex
|
|
951
|
+
) {
|
|
952
|
+
return true
|
|
953
|
+
}
|
|
954
|
+
if (target.targetNoteIndex) {
|
|
955
|
+
return extractNoteIndexFromLink(item.link) === target.targetNoteIndex
|
|
956
|
+
}
|
|
957
|
+
if (target.targetFolderPath?.length && !item.link) {
|
|
958
|
+
const path =
|
|
959
|
+
item.folderPath ??
|
|
960
|
+
[item.text.replace(/^[✅⏰]\s*/, '')]
|
|
961
|
+
return path.join('/') === target.targetFolderPath.join('/')
|
|
962
|
+
}
|
|
963
|
+
return false
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
export function removeFromListByTocLineIndex(
|
|
967
|
+
items: SidebarTreeItem[],
|
|
968
|
+
tocLineIndex: number,
|
|
969
|
+
): RemoveResult {
|
|
970
|
+
const tree = cloneSidebarTree(items)
|
|
971
|
+
|
|
972
|
+
for (let i = 0; i < tree.length; i++) {
|
|
973
|
+
if (tree[i].tocLineIndex === tocLineIndex) {
|
|
974
|
+
const [removed] = tree.splice(i, 1)
|
|
975
|
+
return { tree, removed }
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (tree[i].items?.length) {
|
|
979
|
+
const nested = removeFromListByTocLineIndex(tree[i].items!, tocLineIndex)
|
|
980
|
+
if (nested.removed) {
|
|
981
|
+
tree[i] = { ...tree[i], items: nested.tree }
|
|
982
|
+
return { tree, removed: nested.removed }
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return { tree, removed: null }
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function insertRelativeToTarget(
|
|
991
|
+
items: SidebarTreeItem[],
|
|
992
|
+
target: DropTarget,
|
|
993
|
+
subtree: SidebarTreeItem,
|
|
994
|
+
options?: { expandInside?: boolean },
|
|
995
|
+
): SidebarTreeItem[] | null {
|
|
996
|
+
const tree = cloneSidebarTree(items)
|
|
997
|
+
const expandInside = options?.expandInside !== false
|
|
998
|
+
|
|
999
|
+
for (let i = 0; i < tree.length; i++) {
|
|
1000
|
+
if (itemMatchesDropTarget(tree[i], target)) {
|
|
1001
|
+
if (target.placement === 'inside') {
|
|
1002
|
+
tree[i] = {
|
|
1003
|
+
...tree[i],
|
|
1004
|
+
items: [subtree, ...(tree[i].items ?? [])],
|
|
1005
|
+
...(expandInside ? { collapsed: false } : {}),
|
|
1006
|
+
}
|
|
1007
|
+
return tree
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (target.placement === 'before') {
|
|
1011
|
+
tree.splice(i, 0, subtree)
|
|
1012
|
+
return tree
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
tree.splice(i + 1, 0, subtree)
|
|
1016
|
+
return tree
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (tree[i].items?.length) {
|
|
1020
|
+
const nested = insertRelativeToTarget(
|
|
1021
|
+
tree[i].items!,
|
|
1022
|
+
target,
|
|
1023
|
+
subtree,
|
|
1024
|
+
options,
|
|
1025
|
+
)
|
|
1026
|
+
if (nested) {
|
|
1027
|
+
tree[i] = { ...tree[i], items: nested }
|
|
1028
|
+
return tree
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
return null
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function removeFromList(
|
|
1037
|
+
items: SidebarTreeItem[],
|
|
1038
|
+
noteIndex: string,
|
|
1039
|
+
): RemoveResult {
|
|
1040
|
+
const tree: SidebarTreeItem[] = items.map((item) => ({
|
|
1041
|
+
...item,
|
|
1042
|
+
items: item.items ? [...item.items] : undefined,
|
|
1043
|
+
}))
|
|
1044
|
+
|
|
1045
|
+
for (let i = 0; i < tree.length; i++) {
|
|
1046
|
+
const id = extractNoteIndexFromLink(tree[i].link)
|
|
1047
|
+
if (id === noteIndex) {
|
|
1048
|
+
const [removed] = tree.splice(i, 1)
|
|
1049
|
+
return { tree, removed }
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (tree[i].items?.length) {
|
|
1053
|
+
const nested = removeFromList(tree[i].items!, noteIndex)
|
|
1054
|
+
if (nested.removed) {
|
|
1055
|
+
tree[i] = { ...tree[i], items: nested.tree }
|
|
1056
|
+
return { tree, removed: nested.removed }
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
return { tree, removed: null }
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function insertIntoList(
|
|
1065
|
+
items: SidebarTreeItem[],
|
|
1066
|
+
targetNoteIndex: string,
|
|
1067
|
+
placement: DropPlacement,
|
|
1068
|
+
subtree: SidebarTreeItem,
|
|
1069
|
+
): SidebarTreeItem[] | null {
|
|
1070
|
+
const tree: SidebarTreeItem[] = items.map((item) => ({
|
|
1071
|
+
...item,
|
|
1072
|
+
items: item.items ? [...item.items] : undefined,
|
|
1073
|
+
}))
|
|
1074
|
+
|
|
1075
|
+
for (let i = 0; i < tree.length; i++) {
|
|
1076
|
+
const id = extractNoteIndexFromLink(tree[i].link)
|
|
1077
|
+
if (id === targetNoteIndex) {
|
|
1078
|
+
if (placement === 'inside') {
|
|
1079
|
+
const nextItems = [subtree, ...(tree[i].items ?? [])]
|
|
1080
|
+
tree[i] = { ...tree[i], items: nextItems, collapsed: false }
|
|
1081
|
+
return tree
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
if (placement === 'before') {
|
|
1085
|
+
tree.splice(i, 0, subtree)
|
|
1086
|
+
return tree
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
tree.splice(i + 1, 0, subtree)
|
|
1090
|
+
return tree
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
if (tree[i].items?.length) {
|
|
1094
|
+
const nested = insertIntoList(
|
|
1095
|
+
tree[i].items!,
|
|
1096
|
+
targetNoteIndex,
|
|
1097
|
+
placement,
|
|
1098
|
+
subtree,
|
|
1099
|
+
)
|
|
1100
|
+
if (nested) {
|
|
1101
|
+
tree[i] = { ...tree[i], items: nested }
|
|
1102
|
+
return tree
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
return null
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
export function getProjectionFingerprint(
|
|
1111
|
+
projection: DropProjectionResult,
|
|
1112
|
+
): string {
|
|
1113
|
+
const { target, indicator } = projection
|
|
1114
|
+
return [
|
|
1115
|
+
target.placement,
|
|
1116
|
+
target.projectedDepth,
|
|
1117
|
+
target.targetNoteIndex ?? '',
|
|
1118
|
+
target.targetTocLineIndex ?? '',
|
|
1119
|
+
target.targetFolderPath?.join('/') ?? '',
|
|
1120
|
+
indicator.mode,
|
|
1121
|
+
indicator.activeDepth,
|
|
1122
|
+
indicator.depthLevels.join(','),
|
|
1123
|
+
].join('|')
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function findTreeItemByNodeId(
|
|
1127
|
+
items: SidebarTreeItem[],
|
|
1128
|
+
nodeId: string,
|
|
1129
|
+
): SidebarTreeItem | null {
|
|
1130
|
+
for (const node of items) {
|
|
1131
|
+
const id = node.nodeId ?? computeSidebarNodeId(node)
|
|
1132
|
+
if (id === nodeId) return node
|
|
1133
|
+
if (node.items?.length) {
|
|
1134
|
+
const found = findTreeItemByNodeId(node.items, nodeId)
|
|
1135
|
+
if (found) return found
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
return null
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function findSiblingContext(
|
|
1142
|
+
items: SidebarTreeItem[],
|
|
1143
|
+
tocLineIndex: number,
|
|
1144
|
+
): { list: SidebarTreeItem[]; index: number } | null {
|
|
1145
|
+
for (let i = 0; i < items.length; i++) {
|
|
1146
|
+
if (items[i].tocLineIndex === tocLineIndex) {
|
|
1147
|
+
return { list: items, index: i }
|
|
1148
|
+
}
|
|
1149
|
+
if (items[i].items?.length) {
|
|
1150
|
+
const found = findSiblingContext(items[i].items!, tocLineIndex)
|
|
1151
|
+
if (found) return found
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
return null
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
export function isDropIntentNoOp(
|
|
1158
|
+
tree: SidebarTreeItem[],
|
|
1159
|
+
sourceTocLineIndex: number,
|
|
1160
|
+
intent: DropIntent,
|
|
1161
|
+
): boolean {
|
|
1162
|
+
if (intent.isRootPrepend) {
|
|
1163
|
+
const sourceCtx = findSiblingContext(tree, sourceTocLineIndex)
|
|
1164
|
+
return !!sourceCtx && sourceCtx.list === tree && sourceCtx.index === 0
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const sourceCtx = findSiblingContext(tree, sourceTocLineIndex)
|
|
1168
|
+
if (!sourceCtx) return false
|
|
1169
|
+
|
|
1170
|
+
if (!intent.targetNodeId) return false
|
|
1171
|
+
|
|
1172
|
+
const targetItem = findTreeItemByNodeId(tree, intent.targetNodeId)
|
|
1173
|
+
if (!targetItem || targetItem.tocLineIndex === undefined) return false
|
|
1174
|
+
|
|
1175
|
+
if (intent.action === 'prependChild') {
|
|
1176
|
+
const firstChild = targetItem.items?.[0]
|
|
1177
|
+
return firstChild?.tocLineIndex === sourceTocLineIndex
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
const targetCtx = findSiblingContext(tree, targetItem.tocLineIndex)
|
|
1181
|
+
if (!targetCtx || targetCtx.list !== sourceCtx.list) return false
|
|
1182
|
+
|
|
1183
|
+
return sourceCtx.index === targetCtx.index + 1
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function dropIntentToDropTarget(
|
|
1187
|
+
tree: SidebarTreeItem[],
|
|
1188
|
+
intent: DropIntent,
|
|
1189
|
+
): DropTarget | null {
|
|
1190
|
+
if (intent.isRootPrepend) return null
|
|
1191
|
+
|
|
1192
|
+
const item = findTreeItemByNodeId(tree, intent.targetNodeId!)
|
|
1193
|
+
if (!item || item.tocLineIndex === undefined) return null
|
|
1194
|
+
|
|
1195
|
+
const noteIndex = extractNoteIndexFromLink(item.link)
|
|
1196
|
+
const base: DropTarget = {
|
|
1197
|
+
targetTocLineIndex: item.tocLineIndex,
|
|
1198
|
+
targetNoteIndex: noteIndex ?? undefined,
|
|
1199
|
+
targetFolderPath:
|
|
1200
|
+
!noteIndex && item.folderPath?.length ? item.folderPath : undefined,
|
|
1201
|
+
placement: intent.action === 'prependChild' ? 'inside' : 'after',
|
|
1202
|
+
projectedDepth: 0,
|
|
1203
|
+
}
|
|
1204
|
+
return base
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function createDropPlaceholder(sourceTocLineIndex: number): SidebarTreeItem {
|
|
1208
|
+
return {
|
|
1209
|
+
text: '',
|
|
1210
|
+
isDragPlaceholder: true,
|
|
1211
|
+
tocLineIndex: -(sourceTocLineIndex + 1),
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function markDragSourceSlot(
|
|
1216
|
+
items: SidebarTreeItem[],
|
|
1217
|
+
tocLineIndex: number,
|
|
1218
|
+
): SidebarTreeItem[] {
|
|
1219
|
+
return items.map((item) => {
|
|
1220
|
+
const next: SidebarTreeItem = { ...item }
|
|
1221
|
+
if (item.tocLineIndex === tocLineIndex) {
|
|
1222
|
+
next.isDragSourceSlot = true
|
|
1223
|
+
return next
|
|
1224
|
+
}
|
|
1225
|
+
if (item.items?.length) {
|
|
1226
|
+
next.items = markDragSourceSlot(item.items, tocLineIndex)
|
|
1227
|
+
}
|
|
1228
|
+
return next
|
|
1229
|
+
})
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* 拖拽预览树:
|
|
1234
|
+
* - 无落点:源项原位 invisible 占位(总高度不变)
|
|
1235
|
+
* - 有落点:移除源项、在目标处插入占位,兄弟项随拖动方向让位
|
|
1236
|
+
*/
|
|
1237
|
+
export function buildSidebarDragPreviewTree(
|
|
1238
|
+
base: SidebarTreeItem[],
|
|
1239
|
+
dragSource: DragSource,
|
|
1240
|
+
intent: DropIntent | null,
|
|
1241
|
+
): SidebarTreeItem[] {
|
|
1242
|
+
const cloned = cloneSidebarTree(base)
|
|
1243
|
+
|
|
1244
|
+
if (!intent || isDropIntentNoOp(cloned, dragSource.tocLineIndex, intent)) {
|
|
1245
|
+
return markDragSourceSlot(cloned, dragSource.tocLineIndex)
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const target = dropIntentToDropTarget(cloned, intent)
|
|
1249
|
+
if (!target && !intent.isRootPrepend) {
|
|
1250
|
+
return markDragSourceSlot(cloned, dragSource.tocLineIndex)
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
const { tree: withoutSource } = removeFromListByTocLineIndex(
|
|
1254
|
+
cloned,
|
|
1255
|
+
dragSource.tocLineIndex,
|
|
1256
|
+
)
|
|
1257
|
+
const placeholder = createDropPlaceholder(dragSource.tocLineIndex)
|
|
1258
|
+
|
|
1259
|
+
if (intent.isRootPrepend) {
|
|
1260
|
+
return [placeholder, ...withoutSource]
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
const inserted = insertRelativeToTarget(withoutSource, target!, placeholder, {
|
|
1264
|
+
expandInside: false,
|
|
1265
|
+
})
|
|
1266
|
+
if (!inserted) {
|
|
1267
|
+
return markDragSourceSlot(cloned, dragSource.tocLineIndex)
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
return inserted
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
export function applyMoveInSidebarTreeByTocLine(
|
|
1274
|
+
items: SidebarTreeItem[],
|
|
1275
|
+
dragSource: DragSource,
|
|
1276
|
+
target: DropTarget,
|
|
1277
|
+
): SidebarTreeItem[] {
|
|
1278
|
+
const { tree: withoutDrag, removed } = removeFromListByTocLineIndex(
|
|
1279
|
+
items,
|
|
1280
|
+
dragSource.tocLineIndex,
|
|
1281
|
+
)
|
|
1282
|
+
if (!removed) return items
|
|
1283
|
+
|
|
1284
|
+
const inserted = insertRelativeToTarget(withoutDrag, target, removed)
|
|
1285
|
+
return inserted ?? items
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
export function applyMoveInSidebarTree(
|
|
1289
|
+
items: SidebarTreeItem[],
|
|
1290
|
+
dragNoteIndex: string,
|
|
1291
|
+
target: DropTarget,
|
|
1292
|
+
): SidebarTreeItem[] {
|
|
1293
|
+
const { tree: withoutDrag, removed } = removeFromList(items, dragNoteIndex)
|
|
1294
|
+
if (!removed) return items
|
|
1295
|
+
|
|
1296
|
+
const inserted = target.targetNoteIndex
|
|
1297
|
+
? insertIntoList(
|
|
1298
|
+
withoutDrag,
|
|
1299
|
+
target.targetNoteIndex,
|
|
1300
|
+
target.placement,
|
|
1301
|
+
removed,
|
|
1302
|
+
)
|
|
1303
|
+
: null
|
|
1304
|
+
|
|
1305
|
+
return inserted ?? items
|
|
1306
|
+
}
|
|
1307
|
+
|