@tnotesjs/core 0.1.27 → 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,801 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vitepress/components/Layout/sidebarHitTest.ts
|
|
3
|
+
*
|
|
4
|
+
* 语雀式落点命中:Y 轴行带 + 迟滞,输出 moveAfter / prependChild(含拖到列表顶)。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
flattenVisibleSidebar,
|
|
9
|
+
getDropProjection,
|
|
10
|
+
getDepthContentLeft,
|
|
11
|
+
getSubtreeDepthSpan,
|
|
12
|
+
findTreeItem,
|
|
13
|
+
isDragExternalToTargetParent,
|
|
14
|
+
shouldPrependInsideExternalNoteRow,
|
|
15
|
+
SIDEBAR_ARROW_WIDTH,
|
|
16
|
+
SIDEBAR_INDENT_SIZE,
|
|
17
|
+
SIDEBAR_ROW_BASE_PADDING,
|
|
18
|
+
type DragSource,
|
|
19
|
+
type DropIndicatorMeta,
|
|
20
|
+
type DropProjectionResult,
|
|
21
|
+
type FlatSidebarRow,
|
|
22
|
+
type SidebarTreeItem,
|
|
23
|
+
} from './sidebarDragLogic'
|
|
24
|
+
import { computeSidebarNodeId } from '../../../utils/tocNodeId'
|
|
25
|
+
|
|
26
|
+
import type { TocMoveAction } from '../../../utils/tocNodeId'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
export type DropIndicatorMode = DropIndicatorMeta['mode']
|
|
30
|
+
|
|
31
|
+
export interface DropIntent {
|
|
32
|
+
action: TocMoveAction
|
|
33
|
+
/** 文件夹 prependChild 时有值;拖到列表顶(prepend 到根容器)时省略 */
|
|
34
|
+
targetNodeId: string | null
|
|
35
|
+
sourceNodeId: string
|
|
36
|
+
indicator: DropIndicatorMeta
|
|
37
|
+
/** 用于 overlay:蓝线相对 targetNodeId 行;moveAfter 恒为 after(目标行下方) */
|
|
38
|
+
linePlacement: 'before' | 'after'
|
|
39
|
+
hoverRowIndex: number
|
|
40
|
+
/** 语雀式:拖到侧栏列表顶,无 target_uuid、无 overlay,仅预览树让位 */
|
|
41
|
+
isRootPrepend?: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface RowMetric {
|
|
45
|
+
nodeId: string
|
|
46
|
+
tocLineIndex: number | null
|
|
47
|
+
rect: DOMRect
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface HitTestState {
|
|
51
|
+
lastVerticalPlacement: 'before' | 'after' | null
|
|
52
|
+
stickyPreferInside: boolean | null
|
|
53
|
+
/** 父级下首位 prepend 迟滞:避免与 moveAfter 闪烁 */
|
|
54
|
+
stickyPrependParentNodeId: string | null
|
|
55
|
+
lastIntentFingerprint: string | null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface HitTestInput {
|
|
59
|
+
tree: SidebarTreeItem[]
|
|
60
|
+
flat: FlatSidebarRow[]
|
|
61
|
+
dragSource: DragSource
|
|
62
|
+
sourceNodeId: string
|
|
63
|
+
clientX: number
|
|
64
|
+
clientY: number
|
|
65
|
+
navLeft: number
|
|
66
|
+
navTop: number
|
|
67
|
+
navWidth: number
|
|
68
|
+
rowMetrics: RowMetric[]
|
|
69
|
+
maxDepth: number
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const VERTICAL_HYSTERESIS_RATIO = 0.35
|
|
73
|
+
/** 同级第一项:扩大上半区,便于 prependChild(父) */
|
|
74
|
+
const FIRST_SIBLING_BEFORE_RATIO = 0.72
|
|
75
|
+
const PREPEND_FIRST_CHILD_ZONE_EXIT_PX = 6
|
|
76
|
+
const INSIDE_ENTER_RATIO = 0.55
|
|
77
|
+
const INSIDE_EXIT_RATIO = 0.3
|
|
78
|
+
const ROW_HIT_PADDING_PX = 8
|
|
79
|
+
|
|
80
|
+
export function createHitTestState(): HitTestState {
|
|
81
|
+
return {
|
|
82
|
+
lastVerticalPlacement: null,
|
|
83
|
+
stickyPreferInside: null,
|
|
84
|
+
stickyPrependParentNodeId: null,
|
|
85
|
+
lastIntentFingerprint: null,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function dropIntentFingerprint(intent: DropIntent): string {
|
|
90
|
+
return `${intent.action}|${intent.targetNodeId ?? ''}|${intent.isRootPrepend ? 1 : 0}|${intent.indicator.mode}|${intent.indicator.activeDepth}|${intent.linePlacement}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function findFirstRootRow(
|
|
94
|
+
flat: FlatSidebarRow[],
|
|
95
|
+
rowMetrics: RowMetric[],
|
|
96
|
+
): { flatIndex: number; row: FlatSidebarRow; metric: RowMetric } | null {
|
|
97
|
+
const rootRows = flat.filter((row) => row.depth === 0)
|
|
98
|
+
if (!rootRows.length) return null
|
|
99
|
+
|
|
100
|
+
const rootTocIndices = new Set(
|
|
101
|
+
rootRows
|
|
102
|
+
.map((row) => row.tocLineIndex)
|
|
103
|
+
.filter((index): index is number => index !== null),
|
|
104
|
+
)
|
|
105
|
+
const rootMetrics = rowMetrics
|
|
106
|
+
.filter(
|
|
107
|
+
(m) => m.tocLineIndex !== null && rootTocIndices.has(m.tocLineIndex),
|
|
108
|
+
)
|
|
109
|
+
.sort((a, b) => a.rect.top - b.rect.top)
|
|
110
|
+
|
|
111
|
+
const metric = rootMetrics[0]
|
|
112
|
+
if (!metric || metric.tocLineIndex === null) return null
|
|
113
|
+
|
|
114
|
+
const flatIndex = flat.findIndex(
|
|
115
|
+
(row) => row.tocLineIndex === metric.tocLineIndex,
|
|
116
|
+
)
|
|
117
|
+
if (flatIndex < 0) return null
|
|
118
|
+
|
|
119
|
+
return { flatIndex, row: flat[flatIndex], metric }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 语雀:nav 顶栏与首行之间的空隙 → prepend 到列表顶 */
|
|
123
|
+
function isNavTopGap(
|
|
124
|
+
clientY: number,
|
|
125
|
+
navTop: number,
|
|
126
|
+
firstRowRect: DOMRect,
|
|
127
|
+
): boolean {
|
|
128
|
+
return clientY >= navTop && clientY < firstRowRect.top + 4
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 首行上半区 → prepend 到列表顶(与 nav 顶空隙语义一致) */
|
|
132
|
+
function isFirstRootUpperPrependZone(
|
|
133
|
+
clientY: number,
|
|
134
|
+
firstRowRect: DOMRect,
|
|
135
|
+
): boolean {
|
|
136
|
+
const mid = firstRowRect.top + firstRowRect.height / 2
|
|
137
|
+
return clientY >= firstRowRect.top && clientY < mid
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isFirstSiblingRow(flat: FlatSidebarRow[], index: number): boolean {
|
|
141
|
+
return flat[index].depth > 0 && findPrevSiblingIndex(flat, index) < 0
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function findFirstChildRowIndex(
|
|
145
|
+
flat: FlatSidebarRow[],
|
|
146
|
+
parentIndex: number,
|
|
147
|
+
): number {
|
|
148
|
+
const parentDepth = flat[parentIndex].depth
|
|
149
|
+
for (let i = parentIndex + 1; i < flat.length; i++) {
|
|
150
|
+
if (flat[i].depth <= parentDepth) break
|
|
151
|
+
if (flat[i].depth === parentDepth + 1) return i
|
|
152
|
+
}
|
|
153
|
+
return -1
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getMetricForRow(
|
|
157
|
+
rowMetrics: RowMetric[],
|
|
158
|
+
tocLineIndex: number | null,
|
|
159
|
+
): RowMetric | undefined {
|
|
160
|
+
if (tocLineIndex === null) return undefined
|
|
161
|
+
return rowMetrics.find((m) => m.tocLineIndex === tocLineIndex)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isClientYInPrependFirstChildZone(
|
|
165
|
+
clientY: number,
|
|
166
|
+
parentMetric: RowMetric,
|
|
167
|
+
firstChildMetric: RowMetric,
|
|
168
|
+
sticky: boolean,
|
|
169
|
+
): boolean {
|
|
170
|
+
const zoneTop = parentMetric.rect.top + parentMetric.rect.height * 0.3
|
|
171
|
+
const zoneBottom = sticky
|
|
172
|
+
? firstChildMetric.rect.bottom + PREPEND_FIRST_CHILD_ZONE_EXIT_PX
|
|
173
|
+
: firstChildMetric.rect.top +
|
|
174
|
+
firstChildMetric.rect.height * FIRST_SIBLING_BEFORE_RATIO
|
|
175
|
+
return clientY >= zoneTop && clientY <= zoneBottom
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function tryResolvePrependFirstChildZone(
|
|
179
|
+
tree: SidebarTreeItem[],
|
|
180
|
+
flat: FlatSidebarRow[],
|
|
181
|
+
rowMetrics: RowMetric[],
|
|
182
|
+
dragSource: DragSource,
|
|
183
|
+
sourceNodeId: string,
|
|
184
|
+
clientY: number,
|
|
185
|
+
state: HitTestState,
|
|
186
|
+
): DropIntent | null {
|
|
187
|
+
if (state.stickyPrependParentNodeId) {
|
|
188
|
+
const parentIndex = flat.findIndex(
|
|
189
|
+
(row) => nodeIdForFlatRow(row, tree) === state.stickyPrependParentNodeId,
|
|
190
|
+
)
|
|
191
|
+
if (parentIndex >= 0) {
|
|
192
|
+
const firstChildIndex = findFirstChildRowIndex(flat, parentIndex)
|
|
193
|
+
if (firstChildIndex >= 0) {
|
|
194
|
+
const parentMetric = getMetricForRow(
|
|
195
|
+
rowMetrics,
|
|
196
|
+
flat[parentIndex].tocLineIndex,
|
|
197
|
+
)
|
|
198
|
+
const childMetric = getMetricForRow(
|
|
199
|
+
rowMetrics,
|
|
200
|
+
flat[firstChildIndex].tocLineIndex,
|
|
201
|
+
)
|
|
202
|
+
if (
|
|
203
|
+
parentMetric &&
|
|
204
|
+
childMetric &&
|
|
205
|
+
isClientYInPrependFirstChildZone(
|
|
206
|
+
clientY,
|
|
207
|
+
parentMetric,
|
|
208
|
+
childMetric,
|
|
209
|
+
true,
|
|
210
|
+
) &&
|
|
211
|
+
flat[firstChildIndex].tocLineIndex !== dragSource.tocLineIndex
|
|
212
|
+
) {
|
|
213
|
+
return buildPrependFirstChildIntent(
|
|
214
|
+
tree,
|
|
215
|
+
flat,
|
|
216
|
+
parentIndex,
|
|
217
|
+
sourceNodeId,
|
|
218
|
+
firstChildIndex,
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
state.stickyPrependParentNodeId = null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (let parentIndex = 0; parentIndex < flat.length; parentIndex++) {
|
|
227
|
+
const parentRow = flat[parentIndex]
|
|
228
|
+
if (parentRow.kind !== 'folder' || !parentRow.collapsed) {
|
|
229
|
+
continue
|
|
230
|
+
}
|
|
231
|
+
if (parentRow.tocLineIndex === dragSource.tocLineIndex) continue
|
|
232
|
+
|
|
233
|
+
const dragRow = flat.find(
|
|
234
|
+
(row) => row.tocLineIndex === dragSource.tocLineIndex,
|
|
235
|
+
)
|
|
236
|
+
if (!dragRow || !isDragExternalToTargetParent(flat, dragRow, parentRow)) {
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const parentMetric = getMetricForRow(rowMetrics, parentRow.tocLineIndex)
|
|
241
|
+
if (!parentMetric) continue
|
|
242
|
+
|
|
243
|
+
const zoneTop = parentRow.hasChildren
|
|
244
|
+
? parentMetric.rect.top + parentMetric.rect.height * 0.3
|
|
245
|
+
: parentMetric.rect.top + parentMetric.rect.height * 0.5
|
|
246
|
+
if (clientY < zoneTop || clientY > parentMetric.rect.bottom) continue
|
|
247
|
+
|
|
248
|
+
const intent = buildPrependFirstChildIntent(
|
|
249
|
+
tree,
|
|
250
|
+
flat,
|
|
251
|
+
parentIndex,
|
|
252
|
+
sourceNodeId,
|
|
253
|
+
parentIndex,
|
|
254
|
+
)
|
|
255
|
+
if (!intent) continue
|
|
256
|
+
|
|
257
|
+
state.stickyPrependParentNodeId = intent.targetNodeId
|
|
258
|
+
state.lastVerticalPlacement = 'before'
|
|
259
|
+
state.stickyPreferInside = null
|
|
260
|
+
return intent
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (let i = 0; i < flat.length; i++) {
|
|
264
|
+
if (!isFirstSiblingRow(flat, i)) continue
|
|
265
|
+
|
|
266
|
+
const parentIndex = findParentRowIndex(flat, i)
|
|
267
|
+
if (parentIndex < 0) continue
|
|
268
|
+
|
|
269
|
+
const parentMetric = getMetricForRow(rowMetrics, flat[parentIndex].tocLineIndex)
|
|
270
|
+
const childMetric = getMetricForRow(rowMetrics, flat[i].tocLineIndex)
|
|
271
|
+
if (!parentMetric || !childMetric) continue
|
|
272
|
+
if (flat[i].tocLineIndex === dragSource.tocLineIndex) continue
|
|
273
|
+
|
|
274
|
+
if (
|
|
275
|
+
!isClientYInPrependFirstChildZone(
|
|
276
|
+
clientY,
|
|
277
|
+
parentMetric,
|
|
278
|
+
childMetric,
|
|
279
|
+
false,
|
|
280
|
+
)
|
|
281
|
+
) {
|
|
282
|
+
continue
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const intent = buildPrependFirstChildIntent(
|
|
286
|
+
tree,
|
|
287
|
+
flat,
|
|
288
|
+
parentIndex,
|
|
289
|
+
sourceNodeId,
|
|
290
|
+
i,
|
|
291
|
+
)
|
|
292
|
+
if (!intent) continue
|
|
293
|
+
|
|
294
|
+
state.stickyPrependParentNodeId = intent.targetNodeId
|
|
295
|
+
state.lastVerticalPlacement = 'before'
|
|
296
|
+
state.stickyPreferInside = null
|
|
297
|
+
return intent
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return null
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function verticalPlacementForRowHit(
|
|
304
|
+
rect: DOMRect,
|
|
305
|
+
clientY: number,
|
|
306
|
+
): 'before' | 'after' {
|
|
307
|
+
const mid = rect.top + rect.height / 2
|
|
308
|
+
return clientY < mid ? 'before' : 'after'
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function tryResolveIntoExternalNoteZone(
|
|
312
|
+
tree: SidebarTreeItem[],
|
|
313
|
+
flat: FlatSidebarRow[],
|
|
314
|
+
rowMetrics: RowMetric[],
|
|
315
|
+
dragSource: DragSource,
|
|
316
|
+
sourceNodeId: string,
|
|
317
|
+
clientY: number,
|
|
318
|
+
): DropIntent | null {
|
|
319
|
+
const dragIndex = flat.findIndex(
|
|
320
|
+
(row) => row.tocLineIndex === dragSource.tocLineIndex,
|
|
321
|
+
)
|
|
322
|
+
if (dragIndex < 0) return null
|
|
323
|
+
|
|
324
|
+
for (let i = 0; i < flat.length; i++) {
|
|
325
|
+
const overRow = flat[i]
|
|
326
|
+
if (!overRow.noteIndex) continue
|
|
327
|
+
if (overRow.tocLineIndex === dragSource.tocLineIndex) continue
|
|
328
|
+
|
|
329
|
+
const metric = getMetricForRow(rowMetrics, overRow.tocLineIndex)
|
|
330
|
+
if (!metric) continue
|
|
331
|
+
|
|
332
|
+
const hitTop = metric.rect.top - ROW_HIT_PADDING_PX
|
|
333
|
+
const hitBottom = metric.rect.bottom + ROW_HIT_PADDING_PX
|
|
334
|
+
if (clientY < hitTop || clientY > hitBottom) continue
|
|
335
|
+
|
|
336
|
+
const verticalPlacement = verticalPlacementForRowHit(metric.rect, clientY)
|
|
337
|
+
const dragRow = flat[dragIndex]
|
|
338
|
+
const nestIntoNextRowUpperHalf =
|
|
339
|
+
i === dragIndex + 1 &&
|
|
340
|
+
verticalPlacement === 'before' &&
|
|
341
|
+
isDragExternalToTargetParent(flat, dragRow, overRow)
|
|
342
|
+
if (
|
|
343
|
+
!shouldPrependInsideExternalNoteRow(
|
|
344
|
+
flat,
|
|
345
|
+
dragSource,
|
|
346
|
+
i,
|
|
347
|
+
overRow,
|
|
348
|
+
verticalPlacement,
|
|
349
|
+
) &&
|
|
350
|
+
!nestIntoNextRowUpperHalf
|
|
351
|
+
) {
|
|
352
|
+
continue
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const intent = buildPrependFirstChildIntent(
|
|
356
|
+
tree,
|
|
357
|
+
flat,
|
|
358
|
+
i,
|
|
359
|
+
sourceNodeId,
|
|
360
|
+
i,
|
|
361
|
+
)
|
|
362
|
+
if (!intent) continue
|
|
363
|
+
|
|
364
|
+
return intent
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return null
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function buildRootPrependIntent(
|
|
371
|
+
sourceNodeId: string,
|
|
372
|
+
hoverRowIndex: number,
|
|
373
|
+
): DropIntent {
|
|
374
|
+
return {
|
|
375
|
+
action: 'prependChild',
|
|
376
|
+
targetNodeId: null,
|
|
377
|
+
sourceNodeId,
|
|
378
|
+
isRootPrepend: true,
|
|
379
|
+
indicator: { mode: 'root-prepend', depthLevels: [0], activeDepth: 0 },
|
|
380
|
+
linePlacement: 'before',
|
|
381
|
+
hoverRowIndex,
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function nodeIdForFlatRow(row: FlatSidebarRow, tree: SidebarTreeItem[]): string {
|
|
386
|
+
if (row.nodeId) return row.nodeId
|
|
387
|
+
if (row.tocLineIndex !== null) {
|
|
388
|
+
const item = findTreeItemByTocLine(tree, row.tocLineIndex)
|
|
389
|
+
if (item) return computeSidebarNodeId(item)
|
|
390
|
+
}
|
|
391
|
+
if (row.noteIndex) return `note:${row.noteIndex}`
|
|
392
|
+
if (row.folderPath?.length) return `folder:${row.folderPath.join('/')}`
|
|
393
|
+
return `line:${row.tocLineIndex ?? 0}`
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function findPrevSiblingIndex(
|
|
397
|
+
flat: FlatSidebarRow[],
|
|
398
|
+
index: number,
|
|
399
|
+
): number {
|
|
400
|
+
const depth = flat[index].depth
|
|
401
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
402
|
+
if (flat[i].depth === depth) return i
|
|
403
|
+
if (flat[i].depth < depth) break
|
|
404
|
+
}
|
|
405
|
+
return -1
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function findParentRowIndex(flat: FlatSidebarRow[], index: number): number {
|
|
409
|
+
const depth = flat[index].depth
|
|
410
|
+
if (depth <= 0) return -1
|
|
411
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
412
|
+
if (flat[i].depth < depth) return i
|
|
413
|
+
}
|
|
414
|
+
return -1
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function buildPrependFirstChildIntent(
|
|
418
|
+
tree: SidebarTreeItem[],
|
|
419
|
+
flat: FlatSidebarRow[],
|
|
420
|
+
parentIndex: number,
|
|
421
|
+
sourceNodeId: string,
|
|
422
|
+
hoverRowIndex: number,
|
|
423
|
+
): DropIntent | null {
|
|
424
|
+
const parentRow = flat[parentIndex]
|
|
425
|
+
const parentNodeId = nodeIdForFlatRow(parentRow, tree)
|
|
426
|
+
if (!parentNodeId || parentNodeId === sourceNodeId) return null
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
action: 'prependChild',
|
|
430
|
+
targetNodeId: parentNodeId,
|
|
431
|
+
sourceNodeId,
|
|
432
|
+
indicator: {
|
|
433
|
+
mode: 'inside',
|
|
434
|
+
depthLevels: [parentRow.depth],
|
|
435
|
+
activeDepth: parentRow.depth,
|
|
436
|
+
},
|
|
437
|
+
linePlacement: 'before',
|
|
438
|
+
hoverRowIndex,
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function findTreeItemByTocLine(
|
|
443
|
+
items: SidebarTreeItem[],
|
|
444
|
+
tocLineIndex: number,
|
|
445
|
+
): SidebarTreeItem | null {
|
|
446
|
+
for (const node of items) {
|
|
447
|
+
if (node.tocLineIndex === tocLineIndex) return node
|
|
448
|
+
if (node.items?.length) {
|
|
449
|
+
const found = findTreeItemByTocLine(node.items, tocLineIndex)
|
|
450
|
+
if (found) return found
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return null
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function resolveVerticalPlacement(
|
|
457
|
+
rect: DOMRect,
|
|
458
|
+
clientY: number,
|
|
459
|
+
state: HitTestState,
|
|
460
|
+
isFirstSibling = false,
|
|
461
|
+
): 'before' | 'after' {
|
|
462
|
+
const beforeRatio = isFirstSibling
|
|
463
|
+
? FIRST_SIBLING_BEFORE_RATIO
|
|
464
|
+
: VERTICAL_HYSTERESIS_RATIO
|
|
465
|
+
const mid = rect.top + rect.height / 2
|
|
466
|
+
const topBound = rect.top + rect.height * beforeRatio
|
|
467
|
+
const bottomBound = rect.top + rect.height * (1 - VERTICAL_HYSTERESIS_RATIO)
|
|
468
|
+
|
|
469
|
+
if (state.lastVerticalPlacement === 'before' && clientY < bottomBound) {
|
|
470
|
+
return 'before'
|
|
471
|
+
}
|
|
472
|
+
if (state.lastVerticalPlacement === 'after' && clientY > topBound) {
|
|
473
|
+
return 'after'
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const split = isFirstSibling
|
|
477
|
+
? rect.top + rect.height * beforeRatio
|
|
478
|
+
: mid
|
|
479
|
+
const next = clientY < split ? 'before' : 'after'
|
|
480
|
+
state.lastVerticalPlacement = next
|
|
481
|
+
return next
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function findHoverRowIndex(
|
|
485
|
+
flat: FlatSidebarRow[],
|
|
486
|
+
dragSource: DragSource,
|
|
487
|
+
clientY: number,
|
|
488
|
+
rowMetrics: RowMetric[],
|
|
489
|
+
): number {
|
|
490
|
+
const metricByNode = new Map(rowMetrics.map((m) => [m.tocLineIndex, m]))
|
|
491
|
+
|
|
492
|
+
let bestIndex = -1
|
|
493
|
+
let bestDistance = Number.POSITIVE_INFINITY
|
|
494
|
+
|
|
495
|
+
for (let i = 0; i < flat.length; i++) {
|
|
496
|
+
const row = flat[i]
|
|
497
|
+
if (row.tocLineIndex === dragSource.tocLineIndex) continue
|
|
498
|
+
|
|
499
|
+
const metric =
|
|
500
|
+
row.tocLineIndex !== null
|
|
501
|
+
? metricByNode.get(row.tocLineIndex)
|
|
502
|
+
: rowMetrics.find(
|
|
503
|
+
(m) =>
|
|
504
|
+
m.nodeId === nodeIdForFlatRow(row, []) ||
|
|
505
|
+
(row.noteIndex && m.nodeId === `note:${row.noteIndex}`),
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
const rect = metric?.rect
|
|
509
|
+
if (!rect) continue
|
|
510
|
+
|
|
511
|
+
const hitTop = rect.top - ROW_HIT_PADDING_PX
|
|
512
|
+
const hitBottom = rect.bottom + ROW_HIT_PADDING_PX
|
|
513
|
+
if (clientY < hitTop || clientY > hitBottom) continue
|
|
514
|
+
|
|
515
|
+
const distance = Math.abs(clientY - (rect.top + rect.height / 2))
|
|
516
|
+
if (distance < bestDistance) {
|
|
517
|
+
bestDistance = distance
|
|
518
|
+
bestIndex = i
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (bestIndex >= 0) return bestIndex
|
|
523
|
+
|
|
524
|
+
for (let i = 0; i < flat.length; i++) {
|
|
525
|
+
const row = flat[i]
|
|
526
|
+
if (row.tocLineIndex === dragSource.tocLineIndex) continue
|
|
527
|
+
const metric =
|
|
528
|
+
row.tocLineIndex !== null ? metricByNode.get(row.tocLineIndex) : null
|
|
529
|
+
if (!metric) continue
|
|
530
|
+
const centerY = metric.rect.top + metric.rect.height / 2
|
|
531
|
+
const distance = Math.abs(clientY - centerY)
|
|
532
|
+
if (distance < bestDistance) {
|
|
533
|
+
bestDistance = distance
|
|
534
|
+
bestIndex = i
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return bestDistance <= 48 ? bestIndex : -1
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function stabilizePointerOffset(
|
|
542
|
+
offsetX: number,
|
|
543
|
+
indentSize: number,
|
|
544
|
+
overRow: FlatSidebarRow,
|
|
545
|
+
verticalPlacement: 'before' | 'after',
|
|
546
|
+
state: HitTestState,
|
|
547
|
+
): number {
|
|
548
|
+
const canInside =
|
|
549
|
+
overRow.hasChildren && verticalPlacement === 'after' && offsetX > 0
|
|
550
|
+
if (!canInside) {
|
|
551
|
+
state.stickyPreferInside = null
|
|
552
|
+
return offsetX
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const enterAt = indentSize * INSIDE_ENTER_RATIO
|
|
556
|
+
const exitAt = indentSize * INSIDE_EXIT_RATIO
|
|
557
|
+
|
|
558
|
+
if (state.stickyPreferInside === null) {
|
|
559
|
+
state.stickyPreferInside = offsetX >= enterAt
|
|
560
|
+
} else if (state.stickyPreferInside) {
|
|
561
|
+
if (offsetX < exitAt) state.stickyPreferInside = false
|
|
562
|
+
} else if (offsetX >= enterAt) {
|
|
563
|
+
state.stickyPreferInside = true
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return state.stickyPreferInside ? indentSize : Math.min(offsetX, 0)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function projectionToIntent(
|
|
570
|
+
tree: SidebarTreeItem[],
|
|
571
|
+
flat: FlatSidebarRow[],
|
|
572
|
+
projection: DropProjectionResult,
|
|
573
|
+
sourceNodeId: string,
|
|
574
|
+
linePlacement: 'before' | 'after',
|
|
575
|
+
hoverRowIndex: number,
|
|
576
|
+
): DropIntent | null {
|
|
577
|
+
const { target, indicator } = projection
|
|
578
|
+
|
|
579
|
+
if (target.placement === 'inside') {
|
|
580
|
+
const targetNodeId = resolveTargetNodeId(tree, flat, target)
|
|
581
|
+
if (!targetNodeId || targetNodeId === sourceNodeId) return null
|
|
582
|
+
return {
|
|
583
|
+
action: 'prependChild',
|
|
584
|
+
targetNodeId,
|
|
585
|
+
sourceNodeId,
|
|
586
|
+
indicator,
|
|
587
|
+
linePlacement,
|
|
588
|
+
hoverRowIndex,
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (target.placement === 'before') {
|
|
593
|
+
const prevIndex = findPrevSiblingIndex(flat, hoverRowIndex)
|
|
594
|
+
if (prevIndex < 0) {
|
|
595
|
+
const overRow = flat[hoverRowIndex]
|
|
596
|
+
if (overRow.depth === 0) {
|
|
597
|
+
return buildRootPrependIntent(sourceNodeId, hoverRowIndex)
|
|
598
|
+
}
|
|
599
|
+
const parentIndex = findParentRowIndex(flat, hoverRowIndex)
|
|
600
|
+
if (parentIndex < 0) return null
|
|
601
|
+
return buildPrependFirstChildIntent(
|
|
602
|
+
tree,
|
|
603
|
+
flat,
|
|
604
|
+
parentIndex,
|
|
605
|
+
sourceNodeId,
|
|
606
|
+
hoverRowIndex,
|
|
607
|
+
)
|
|
608
|
+
}
|
|
609
|
+
const targetNodeId = nodeIdForFlatRow(flat[prevIndex], tree)
|
|
610
|
+
return {
|
|
611
|
+
action: 'moveAfter',
|
|
612
|
+
targetNodeId,
|
|
613
|
+
sourceNodeId,
|
|
614
|
+
indicator,
|
|
615
|
+
linePlacement: 'after',
|
|
616
|
+
hoverRowIndex,
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const targetNodeId = resolveTargetNodeId(tree, flat, target)
|
|
621
|
+
if (!targetNodeId || targetNodeId === sourceNodeId) return null
|
|
622
|
+
|
|
623
|
+
return {
|
|
624
|
+
action: 'moveAfter',
|
|
625
|
+
targetNodeId,
|
|
626
|
+
sourceNodeId,
|
|
627
|
+
indicator,
|
|
628
|
+
linePlacement: 'after',
|
|
629
|
+
hoverRowIndex,
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function resolveTargetNodeId(
|
|
634
|
+
tree: SidebarTreeItem[],
|
|
635
|
+
flat: FlatSidebarRow[],
|
|
636
|
+
target: DropProjectionResult['target'],
|
|
637
|
+
): string | null {
|
|
638
|
+
if (target.targetTocLineIndex !== undefined) {
|
|
639
|
+
const row = flat.find((r) => r.tocLineIndex === target.targetTocLineIndex)
|
|
640
|
+
if (row) return nodeIdForFlatRow(row, tree)
|
|
641
|
+
const item = findTreeItemByTocLine(tree, target.targetTocLineIndex)
|
|
642
|
+
if (item) return computeSidebarNodeId(item)
|
|
643
|
+
}
|
|
644
|
+
if (target.targetNoteIndex) return `note:${target.targetNoteIndex}`
|
|
645
|
+
if (target.targetFolderPath?.length) {
|
|
646
|
+
return `folder:${target.targetFolderPath.join('/')}`
|
|
647
|
+
}
|
|
648
|
+
return null
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export function collectRowMetrics(nav: HTMLElement): RowMetric[] {
|
|
652
|
+
const metrics: RowMetric[] = []
|
|
653
|
+
const rows = nav.querySelectorAll<HTMLElement>('.sidebar-row[data-toc-line-index]')
|
|
654
|
+
rows.forEach((row) => {
|
|
655
|
+
const raw = row.dataset.tocLineIndex
|
|
656
|
+
if (raw === undefined || raw === '') return
|
|
657
|
+
const tocLineIndex = Number(raw)
|
|
658
|
+
const nodeId =
|
|
659
|
+
row.dataset.nodeId ??
|
|
660
|
+
(row.dataset.noteId
|
|
661
|
+
? `note:${row.dataset.noteId}`
|
|
662
|
+
: `line:${tocLineIndex}`)
|
|
663
|
+
metrics.push({
|
|
664
|
+
nodeId,
|
|
665
|
+
tocLineIndex,
|
|
666
|
+
rect: row.getBoundingClientRect(),
|
|
667
|
+
})
|
|
668
|
+
})
|
|
669
|
+
return metrics
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export function resolveDropIntent(
|
|
673
|
+
input: HitTestInput,
|
|
674
|
+
state: HitTestState,
|
|
675
|
+
): DropIntent | null {
|
|
676
|
+
const {
|
|
677
|
+
tree,
|
|
678
|
+
flat,
|
|
679
|
+
dragSource,
|
|
680
|
+
sourceNodeId,
|
|
681
|
+
clientX,
|
|
682
|
+
clientY,
|
|
683
|
+
navLeft,
|
|
684
|
+
dragSource: source,
|
|
685
|
+
} = input
|
|
686
|
+
|
|
687
|
+
const firstRoot = findFirstRootRow(flat, input.rowMetrics)
|
|
688
|
+
if (
|
|
689
|
+
firstRoot &&
|
|
690
|
+
dragSource.tocLineIndex !== firstRoot.row.tocLineIndex &&
|
|
691
|
+
(isNavTopGap(clientY, input.navTop, firstRoot.metric.rect) ||
|
|
692
|
+
isFirstRootUpperPrependZone(clientY, firstRoot.metric.rect))
|
|
693
|
+
) {
|
|
694
|
+
state.stickyPrependParentNodeId = null
|
|
695
|
+
return buildRootPrependIntent(sourceNodeId, firstRoot.flatIndex)
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const prependFirstChild = tryResolvePrependFirstChildZone(
|
|
699
|
+
tree,
|
|
700
|
+
flat,
|
|
701
|
+
input.rowMetrics,
|
|
702
|
+
dragSource,
|
|
703
|
+
sourceNodeId,
|
|
704
|
+
clientY,
|
|
705
|
+
state,
|
|
706
|
+
)
|
|
707
|
+
if (prependFirstChild) {
|
|
708
|
+
return prependFirstChild
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const intoExternalNote = tryResolveIntoExternalNoteZone(
|
|
712
|
+
tree,
|
|
713
|
+
flat,
|
|
714
|
+
input.rowMetrics,
|
|
715
|
+
dragSource,
|
|
716
|
+
sourceNodeId,
|
|
717
|
+
clientY,
|
|
718
|
+
)
|
|
719
|
+
if (intoExternalNote) {
|
|
720
|
+
return intoExternalNote
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const hoverRowIndex = findHoverRowIndex(
|
|
724
|
+
flat,
|
|
725
|
+
dragSource,
|
|
726
|
+
clientY,
|
|
727
|
+
input.rowMetrics,
|
|
728
|
+
)
|
|
729
|
+
if (hoverRowIndex < 0) return null
|
|
730
|
+
|
|
731
|
+
const overRow = flat[hoverRowIndex]
|
|
732
|
+
const metric = input.rowMetrics.find(
|
|
733
|
+
(m) => m.tocLineIndex === overRow.tocLineIndex,
|
|
734
|
+
)
|
|
735
|
+
const rect = metric?.rect
|
|
736
|
+
if (!rect) return null
|
|
737
|
+
|
|
738
|
+
const verticalPlacement = resolveVerticalPlacement(
|
|
739
|
+
rect,
|
|
740
|
+
clientY,
|
|
741
|
+
state,
|
|
742
|
+
isFirstSiblingRow(flat, hoverRowIndex),
|
|
743
|
+
)
|
|
744
|
+
const indentSize = SIDEBAR_INDENT_SIZE
|
|
745
|
+
const baseIndentLeft =
|
|
746
|
+
navLeft +
|
|
747
|
+
SIDEBAR_ROW_BASE_PADDING +
|
|
748
|
+
overRow.depth * indentSize +
|
|
749
|
+
SIDEBAR_ARROW_WIDTH
|
|
750
|
+
const rawOffsetX = clientX - baseIndentLeft
|
|
751
|
+
const offsetX = stabilizePointerOffset(
|
|
752
|
+
rawOffsetX,
|
|
753
|
+
indentSize,
|
|
754
|
+
overRow,
|
|
755
|
+
verticalPlacement,
|
|
756
|
+
state,
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
const dragged =
|
|
760
|
+
source.noteIndex !== undefined
|
|
761
|
+
? findTreeItem(tree, source.noteIndex)
|
|
762
|
+
: findTreeItemByTocLine(tree, source.tocLineIndex)
|
|
763
|
+
const maxDepth =
|
|
764
|
+
input.maxDepth > 0 ? input.maxDepth : Number.POSITIVE_INFINITY
|
|
765
|
+
|
|
766
|
+
const projection = getDropProjection(
|
|
767
|
+
tree,
|
|
768
|
+
flat,
|
|
769
|
+
dragSource,
|
|
770
|
+
hoverRowIndex,
|
|
771
|
+
verticalPlacement,
|
|
772
|
+
{
|
|
773
|
+
maxDepth,
|
|
774
|
+
indentSize,
|
|
775
|
+
draggedSubtreeDepthSpan: dragged ? getSubtreeDepthSpan(dragged) : 0,
|
|
776
|
+
},
|
|
777
|
+
{ offsetX, clientX, navLeft },
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
if (!projection) return null
|
|
781
|
+
|
|
782
|
+
return projectionToIntent(
|
|
783
|
+
tree,
|
|
784
|
+
flat,
|
|
785
|
+
projection,
|
|
786
|
+
sourceNodeId,
|
|
787
|
+
verticalPlacement,
|
|
788
|
+
hoverRowIndex,
|
|
789
|
+
)
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
export function buildFlatForHitTest(
|
|
793
|
+
tree: SidebarTreeItem[],
|
|
794
|
+
dragTocLineIndex: number,
|
|
795
|
+
): FlatSidebarRow[] {
|
|
796
|
+
return flattenVisibleSidebar(tree, 0, null, null, {
|
|
797
|
+
forceCollapsedTocLineIndex: dragTocLineIndex,
|
|
798
|
+
})
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
export { getDepthContentLeft }
|