@ecsplico/editor 2.0.0 → 2.1.1

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.
Files changed (278) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +4 -3
  3. package/dist/adapters/anchors.d.ts +80 -0
  4. package/dist/adapters/anchors.js +270 -0
  5. package/dist/adapters/anchors.js.map +1 -0
  6. package/dist/adapters/comments/client.d.ts +64 -0
  7. package/dist/adapters/comments/client.js +536 -0
  8. package/dist/adapters/comments/client.js.map +1 -0
  9. package/dist/adapters/comments/drizzle.d.ts +430 -0
  10. package/dist/adapters/comments/drizzle.js +236 -0
  11. package/dist/adapters/comments/drizzle.js.map +1 -0
  12. package/dist/adapters/comments/errors.d.ts +9 -0
  13. package/dist/adapters/comments/errors.js +19 -0
  14. package/dist/adapters/comments/errors.js.map +1 -0
  15. package/dist/adapters/comments/json.d.ts +19 -0
  16. package/dist/adapters/comments/json.js +94 -0
  17. package/dist/adapters/comments/json.js.map +1 -0
  18. package/dist/adapters/comments/record.js +78 -0
  19. package/dist/adapters/comments/record.js.map +1 -0
  20. package/dist/adapters/comments/server.d.ts +23 -0
  21. package/dist/adapters/comments/server.js +102 -0
  22. package/dist/adapters/comments/server.js.map +1 -0
  23. package/dist/adapters/comments/types.d.ts +109 -0
  24. package/dist/adapters/documents.js +108 -0
  25. package/dist/adapters/documents.js.map +1 -0
  26. package/dist/adapters/drizzle.d.ts +293 -0
  27. package/dist/adapters/drizzle.js +157 -0
  28. package/dist/adapters/drizzle.js.map +1 -0
  29. package/dist/adapters/files.js +30 -0
  30. package/dist/adapters/files.js.map +1 -0
  31. package/dist/adapters/forge.d.ts +58 -0
  32. package/dist/adapters/forge.js +91 -0
  33. package/dist/adapters/forge.js.map +1 -0
  34. package/dist/adapters/index.d.ts +13 -0
  35. package/dist/adapters/index.js +0 -0
  36. package/dist/adapters/json.d.ts +24 -0
  37. package/dist/adapters/json.js +128 -0
  38. package/dist/adapters/json.js.map +1 -0
  39. package/dist/adapters/nitro.d.ts +61 -0
  40. package/dist/adapters/nitro.js +212 -0
  41. package/dist/adapters/nitro.js.map +1 -0
  42. package/dist/adapters/routes.js +16 -0
  43. package/dist/adapters/routes.js.map +1 -0
  44. package/dist/adapters/shared.d.ts +37 -0
  45. package/dist/adapters/shared.js +32 -0
  46. package/dist/adapters/shared.js.map +1 -0
  47. package/dist/adapters/vue.d.ts +67 -0
  48. package/dist/adapters/vue.js +187 -0
  49. package/dist/adapters/vue.js.map +1 -0
  50. package/dist/code/CodeSurface.vue.d.ts +3 -3
  51. package/dist/core/doc-anchors.d.ts +5 -0
  52. package/dist/core/doc-anchors.js +4 -3
  53. package/dist/core/doc-anchors.js.map +1 -1
  54. package/dist/core/editor-ui.js +57 -1
  55. package/dist/core/editor-ui.js.map +1 -1
  56. package/dist/core/surface-session.js +139 -0
  57. package/dist/core/surface-session.js.map +1 -0
  58. package/dist/document/DocumentSurface.d.ts +1 -1
  59. package/dist/document/DocumentSurface.vue.d.ts +3 -3
  60. package/dist/features/ai/agent-op-review.d.ts +2 -2
  61. package/dist/features/ai/agent-op-review.js.map +1 -1
  62. package/dist/features/ai/agent-ops.d.ts +1 -1
  63. package/dist/features/ai/agent-ops.js +9 -6
  64. package/dist/features/ai/agent-ops.js.map +1 -1
  65. package/dist/features/ai/ai-apply.d.ts +1 -1
  66. package/dist/features/ai/ai-review.js +10 -1
  67. package/dist/features/ai/ai-review.js.map +1 -1
  68. package/dist/features/ai/components/AiSelectionMenu.js +78 -252
  69. package/dist/features/ai/components/AiSelectionMenu.js.map +1 -1
  70. package/dist/features/ai/components/AiSelectionMenu.vue.d.ts +2 -1
  71. package/dist/features/ai/components/AiSidebar.js +2 -2
  72. package/dist/features/ai/components/DocumentChatAnswer.js +1 -1
  73. package/dist/features/ai/components/DocumentChatOps.js +1 -0
  74. package/dist/features/ai/components/DocumentChatOps.js.map +1 -1
  75. package/dist/features/ai/components/ModelQuickSwitch.js +1 -1
  76. package/dist/features/ai/components/SettingsAiTab.js +1 -1
  77. package/dist/features/ai/components/SettingsModelProfiles.js +1 -1
  78. package/dist/features/ai/document-chat.d.ts +3 -2
  79. package/dist/features/ai/document-chat.js +16 -19
  80. package/dist/features/ai/document-chat.js.map +1 -1
  81. package/dist/features/ai/index.d.ts +5 -3
  82. package/dist/features/ai/index.js +4 -2
  83. package/dist/features/ai/protocol/agent-ops.d.ts +22 -17
  84. package/dist/features/ai/protocol/agent-ops.js +65 -39
  85. package/dist/features/ai/protocol/agent-ops.js.map +1 -1
  86. package/dist/features/ai/protocol/block-lines.d.ts +89 -0
  87. package/dist/features/ai/protocol/block-lines.js +177 -0
  88. package/dist/features/ai/protocol/block-lines.js.map +1 -0
  89. package/dist/features/ai/protocol/chat.js +10 -10
  90. package/dist/features/ai/protocol/chat.js.map +1 -1
  91. package/dist/features/ai/protocol/index.d.ts +3 -2
  92. package/dist/features/ai/protocol/index.js +3 -2
  93. package/dist/features/ai/react/AiSelectionMenu.d.ts +3 -2
  94. package/dist/features/ai/react/AiSelectionMenu.js +64 -255
  95. package/dist/features/ai/react/AiSelectionMenu.js.map +1 -1
  96. package/dist/features/ai/react/AiSidebar.js +2 -2
  97. package/dist/features/ai/react/DocumentChatAnswer.js +1 -1
  98. package/dist/features/ai/react/DocumentChatOps.js +1 -0
  99. package/dist/features/ai/react/DocumentChatOps.js.map +1 -1
  100. package/dist/features/ai/react/ModelQuickSwitch.js +1 -1
  101. package/dist/features/ai/react/SettingsAiTab.js +1 -1
  102. package/dist/features/ai/react/SettingsModelProfiles.js +1 -1
  103. package/dist/features/ai/react/use-ai-selection-flow.js +34 -0
  104. package/dist/features/ai/react/use-ai-selection-flow.js.map +1 -0
  105. package/dist/features/ai/selection-flow.d.ts +133 -0
  106. package/dist/features/ai/selection-flow.js +283 -0
  107. package/dist/features/ai/selection-flow.js.map +1 -0
  108. package/dist/features/ai/use-ai-assistant.d.ts +1 -1
  109. package/dist/features/ai/use-document-chat.d.ts +1 -1
  110. package/dist/features/authors/components/AuthorColorsPanel.js +1 -1
  111. package/dist/features/authors/feature.js +1 -1
  112. package/dist/features/authors/index.js +1 -1
  113. package/dist/features/authors/react/AuthorColorsPanel.js +1 -1
  114. package/dist/features/comments/components/CommentBubble.js +1 -1
  115. package/dist/features/comments/components/CommentComposer.js +1 -1
  116. package/dist/features/comments/components/CommentThreadView.js +5 -2
  117. package/dist/features/comments/components/CommentThreadView.js.map +1 -1
  118. package/dist/features/comments/components/CommentsMargin.js +108 -230
  119. package/dist/features/comments/components/CommentsMargin.js.map +1 -1
  120. package/dist/features/comments/components/CommentsOverlay.js +1 -1
  121. package/dist/features/comments/feature.js +1 -1
  122. package/dist/features/comments/index.d.ts +4 -2
  123. package/dist/features/comments/index.js +4 -2
  124. package/dist/features/comments/margin-controller.d.ts +44 -0
  125. package/dist/features/comments/margin-controller.js +132 -0
  126. package/dist/features/comments/margin-controller.js.map +1 -0
  127. package/dist/features/comments/margin-layout.d.ts +55 -0
  128. package/dist/features/comments/margin-layout.js +79 -0
  129. package/dist/features/comments/margin-layout.js.map +1 -0
  130. package/dist/features/comments/model.d.ts +8 -0
  131. package/dist/features/comments/model.js +60 -34
  132. package/dist/features/comments/model.js.map +1 -1
  133. package/dist/features/comments/react/CommentBubble.js +1 -1
  134. package/dist/features/comments/react/CommentComposer.js +1 -1
  135. package/dist/features/comments/react/CommentThreadView.js +2 -2
  136. package/dist/features/comments/react/CommentThreadView.js.map +1 -1
  137. package/dist/features/comments/react/CommentsMargin.d.ts +0 -5
  138. package/dist/features/comments/react/CommentsMargin.js +85 -251
  139. package/dist/features/comments/react/CommentsMargin.js.map +1 -1
  140. package/dist/features/comments/react/CommentsOverlay.js +1 -1
  141. package/dist/features/comments/react/use-comments.js +1 -1
  142. package/dist/features/comments/react/use-margin-layout.js +45 -0
  143. package/dist/features/comments/react/use-margin-layout.js.map +1 -0
  144. package/dist/features/comments/react.d.ts +1 -1
  145. package/dist/features/comments/schema.d.ts +23 -0
  146. package/dist/features/comments/schema.js.map +1 -1
  147. package/dist/features/comments/snapshot-equality.js +40 -0
  148. package/dist/features/comments/snapshot-equality.js.map +1 -0
  149. package/dist/features/comments/thread-anchor.js +33 -0
  150. package/dist/features/comments/thread-anchor.js.map +1 -0
  151. package/dist/features/comments/use-comments.d.ts +3 -1
  152. package/dist/features/comments/use-comments.js +1 -0
  153. package/dist/features/comments/use-comments.js.map +1 -1
  154. package/dist/features/comments/use-margin-layout.js +29 -0
  155. package/dist/features/comments/use-margin-layout.js.map +1 -0
  156. package/dist/features/comments/vue.d.ts +1 -1
  157. package/dist/features/composites/accepted-children.d.ts +21 -0
  158. package/dist/features/composites/accepted-children.js +126 -0
  159. package/dist/features/composites/accepted-children.js.map +1 -0
  160. package/dist/features/composites/components/CompositeDetails.js +1 -1
  161. package/dist/features/composites/components/CompositeInspector.js +38 -164
  162. package/dist/features/composites/components/CompositeInspector.js.map +1 -1
  163. package/dist/features/composites/feature.d.ts +2 -1
  164. package/dist/features/composites/feature.js +8 -2
  165. package/dist/features/composites/feature.js.map +1 -1
  166. package/dist/features/composites/index.d.ts +5 -3
  167. package/dist/features/composites/index.js +8 -6
  168. package/dist/features/composites/inspector-model.d.ts +97 -0
  169. package/dist/features/composites/inspector-model.js +255 -0
  170. package/dist/features/composites/inspector-model.js.map +1 -0
  171. package/dist/features/composites/kinds.d.ts +11 -0
  172. package/dist/features/composites/kinds.js +17 -2
  173. package/dist/features/composites/kinds.js.map +1 -1
  174. package/dist/features/composites/outline.d.ts +18 -5
  175. package/dist/features/composites/outline.js +44 -12
  176. package/dist/features/composites/outline.js.map +1 -1
  177. package/dist/features/composites/react/CompositeDetails.js +1 -1
  178. package/dist/features/composites/react/CompositeInspector.d.ts +1 -1
  179. package/dist/features/composites/react/CompositeInspector.js +21 -180
  180. package/dist/features/composites/react/CompositeInspector.js.map +1 -1
  181. package/dist/features/composites/react/CompositeStatusMenu.d.ts +1 -1
  182. package/dist/features/composites/react/CompositeView.js +1 -1
  183. package/dist/features/composites/react/use-composite-inspector.js +29 -0
  184. package/dist/features/composites/react/use-composite-inspector.js.map +1 -0
  185. package/dist/features/composites/schema.d.ts +2 -2
  186. package/dist/features/composites/schema.js +2 -2
  187. package/dist/features/composites/schema.js.map +1 -1
  188. package/dist/features/composites/use-composite-anchors.d.ts +1 -1
  189. package/dist/features/composites/use-composite-inspector.js +41 -0
  190. package/dist/features/composites/use-composite-inspector.js.map +1 -0
  191. package/dist/features/composites/use-composite-rows.d.ts +1 -1
  192. package/dist/features/media/index.d.ts +1 -1
  193. package/dist/features/media/vue.d.ts +1 -1
  194. package/dist/features/mentions/index.d.ts +3 -3
  195. package/dist/features/mentions/index.js +3 -3
  196. package/dist/features/mentions/schema.d.ts +1 -1
  197. package/dist/features/mentions/schema.js +1 -1
  198. package/dist/features/themes/index.d.ts +1 -1
  199. package/dist/features/themes/index.js +1 -1
  200. package/dist/features/transcript/feature.js +1 -1
  201. package/dist/features/transcript/index.js +2 -2
  202. package/dist/i18n/de/ai.js +2 -0
  203. package/dist/i18n/de/ai.js.map +1 -1
  204. package/dist/i18n/de/composites.js +8 -1
  205. package/dist/i18n/de/composites.js.map +1 -1
  206. package/dist/i18n/de/tree.js +14 -1
  207. package/dist/i18n/de/tree.js.map +1 -1
  208. package/dist/i18n/de.d.ts +22 -0
  209. package/dist/i18n/en/ai.js +2 -0
  210. package/dist/i18n/en/ai.js.map +1 -1
  211. package/dist/i18n/en/composites.js +8 -1
  212. package/dist/i18n/en/composites.js.map +1 -1
  213. package/dist/i18n/en/tree.js +14 -1
  214. package/dist/i18n/en/tree.js.map +1 -1
  215. package/dist/integrations/composite-ai/composite-fill.js +2 -2
  216. package/dist/integrations/composite-ai/composite-summary.js +2 -2
  217. package/dist/integrations/composite-ai/index.js +2 -2
  218. package/dist/integrations/transcript-ai/transcript-cleanup.js +1 -1
  219. package/dist/markdown/MarkdownSurface.d.ts +1 -1
  220. package/dist/markdown/MarkdownSurface.js +2 -2
  221. package/dist/markdown/MarkdownSurface.vue.d.ts +9 -9
  222. package/dist/markdown/index.d.ts +2 -2
  223. package/dist/nuxt/runtime/server/routes/collab/[id].js +1 -1
  224. package/dist/react/EditorSurface.d.ts +1 -1
  225. package/dist/react/EditorSurface.js +43 -149
  226. package/dist/react/EditorSurface.js.map +1 -1
  227. package/dist/react/EditorToolbar.js +3 -40
  228. package/dist/react/EditorToolbar.js.map +1 -1
  229. package/dist/schema/advanced.js +2 -2
  230. package/dist/server/adapter.d.ts +2 -2
  231. package/dist/server/collab.js +1 -1
  232. package/dist/server/handlers/ai.d.ts +4 -2
  233. package/dist/server/handlers/ai.js +11 -9
  234. package/dist/server/handlers/ai.js.map +1 -1
  235. package/dist/server/handlers/context.d.ts +1 -1
  236. package/dist/server/handlers/editor.d.ts +1 -1
  237. package/dist/server/handlers/editor.js +2 -2
  238. package/dist/server/host.d.ts +1 -1
  239. package/dist/server/index.d.ts +2 -2
  240. package/dist/settings/index.d.ts +1 -1
  241. package/dist/styles/tokens.css +1 -1
  242. package/dist/tree/DocumentTree.js +159 -387
  243. package/dist/tree/DocumentTree.js.map +1 -1
  244. package/dist/tree/drop-rules.js +37 -0
  245. package/dist/tree/drop-rules.js.map +1 -0
  246. package/dist/tree/react/DocumentTree.d.ts +2 -2
  247. package/dist/tree/react/DocumentTree.js +135 -397
  248. package/dist/tree/react/DocumentTree.js.map +1 -1
  249. package/dist/tree/react/use-tree-model.js +129 -0
  250. package/dist/tree/react/use-tree-model.js.map +1 -0
  251. package/dist/tree/react.js +1 -1
  252. package/dist/tree/tree-host.js +100 -0
  253. package/dist/tree/tree-host.js.map +1 -0
  254. package/dist/tree/tree-items.js +22 -43
  255. package/dist/tree/tree-items.js.map +1 -1
  256. package/dist/tree/tree-model.js +580 -0
  257. package/dist/tree/tree-model.js.map +1 -0
  258. package/dist/tree/tree-rows.js +77 -0
  259. package/dist/tree/tree-rows.js.map +1 -0
  260. package/dist/tree/use-tree-model.js +94 -0
  261. package/dist/tree/use-tree-model.js.map +1 -0
  262. package/dist/tree/vue.js +1 -1
  263. package/dist/vue/AdvancedEditor.vue.d.ts +6 -6
  264. package/dist/vue/SimpleEditor.vue.d.ts +2 -2
  265. package/dist/vue/components/EditorSurface.js +27 -87
  266. package/dist/vue/components/EditorSurface.js.map +1 -1
  267. package/dist/vue/components/EditorSurface.vue.d.ts +6 -6
  268. package/dist/vue/components/EditorToolbar.js +7 -44
  269. package/dist/vue/components/EditorToolbar.js.map +1 -1
  270. package/dist/vue/context.d.ts +1 -1
  271. package/dist/vue/context.js +1 -1
  272. package/dist/vue/context.js.map +1 -1
  273. package/dist/vue/use-editor-ui.d.ts +1 -1
  274. package/dist/vue/use-notifier.d.ts +1 -1
  275. package/docs/integration.md +97 -4
  276. package/docs/llms.md +9 -1
  277. package/docs/usage.md +10 -5
  278. package/package.json +67 -5
@@ -1 +1 @@
1
- {"version":3,"file":"tree-items.js","names":[],"sources":["../../src/tree/tree-items.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { findBlockById } from '../core/schema'\nimport { canMoveBlockInto, compositeKind, type CompositeKind, compositeOutline, defaultCompositeKinds, type OutlineNode } from '../features/composites'\nimport type { Translate } from '../i18n'\nimport { type HeadingOutlineNode, headingOutline } from '../outline'\nimport { folderKey } from './folder-paths'\nimport type { TreeFolder } from './types'\nimport type { NodeFilters } from './tree-view-model'\n\n// What the Vue and the React `DocumentTree` share: the items of the file browser (folders and documents of the store,\n// the open document's outline), their filters, the drop rules and the path arithmetic. No framework, no state.\n\n/** One row of the file browser: a store folder or document, or an entry of the open document's outline. Which fields are set depends on `type`. */\nexport interface TreeItem {\n /** Unique within the tree: `root`, `folder:<path>`, `doc:<id>`, `outline:<block key>` */\n key: string\n label: string\n icon: string\n type: 'root' | 'folder' | 'document' | 'composite' | 'heading'\n /** Folder path of a root/folder item (`''` = root) */\n path?: string\n /** Document id */\n id?: string\n /** Folder path a document item lives in */\n folder?: string\n /** Outline items: block ID of the composite/heading (`null` while it has none) and its position when the outline was read */\n blockId?: string | null\n pos?: number\n /** Composite items: the composite's kind, and whether it has a summary (description) */\n kind?: string\n hasSummary?: boolean\n children?: TreeItem[]\n onSelect?: (e?: Event) => void\n onToggle?: (e?: Event) => void\n}\n\n/** What a drag carries (serialized under `DRAG_MIME`): a document or folder of the store, or a composite of the open document. */\nexport type DragPayload\n = | {\n type: 'document' | 'folder'\n /** Document id */\n id?: string\n /** Folder path of a folder item, or the folder a document currently lives in */\n path: string\n }\n /** Virtual mode: a composite of the open document, by block ID */\n | { type: 'composite', blockId: string }\n\n/** Key of the virtual-mode root item (the open document). */\nexport const VIRTUAL_ROOT = 'vroot'\n\nexport const DRAG_MIME = 'application/x-tip-tree'\n\nexport const parentPath = (folder: string) => folder.slice(0, Math.max(folder.lastIndexOf('/'), 0))\nexport const joinPath = (folder: string, name: string) => (folder ? `${folder}/${name}` : name)\n\n/** Item keys from the root down to `folder`. */\nexport function ancestorKeys(folder: string): string[] {\n const keys = ['root']\n let path = ''\n for (const segment of folder.split('/').filter(Boolean)) {\n path = joinPath(path, segment)\n keys.push(folderKey(path))\n }\n return keys\n}\n\n/** The folder at `path` below (or at) `from`; undefined when there is none. */\nexport function findFolder(path: string, from: TreeFolder | null | undefined): TreeFolder | undefined {\n if (!from) return undefined\n if (from.path === path) return from\n for (const sub of from.folders) {\n const found = path === sub.path || path.startsWith(`${sub.path}/`) ? findFolder(path, sub) : undefined\n if (found) return found\n }\n return undefined\n}\n\n/** Names of the folders directly in `parent`, without the folder at path `except`. */\nexport const folderNamesIn = (tree: TreeFolder | null | undefined, parent: string, except?: string) =>\n (findFolder(parent, tree)?.folders ?? []).filter(sub => sub.path !== except).map(sub => sub.name)\n\nexport function collectFolderPaths(folder: TreeFolder, into: Set<string>) {\n into.add(folder.path)\n folder.folders.forEach(sub => collectFolderPaths(sub, into))\n}\n\n// --- outline of the open document ------------------------------------------------------------------------------\n\nexport type Outline = { structure: 'composite', nodes: OutlineNode[] } | { structure: 'flat', nodes: HeadingOutlineNode[] }\n\nexport const EMPTY_OUTLINE: Outline = { structure: 'composite', nodes: [] }\n\n/** The outline of the editor's document: the composites in composite structure, else the headings. */\nexport function readOutline(editor: Editor | undefined, structure: string, t: Translate): Outline {\n if (!editor || editor.isDestroyed) return EMPTY_OUTLINE\n const { doc } = editor.state\n return structure === 'composite'\n ? { structure: 'composite', nodes: compositeOutline(doc, new Set(), t('tree.untitled')) }\n : { structure: 'flat', nodes: headingOutline(doc, t('tree.untitled')) }\n}\n\n/** The outline entries as items; `select` runs when one is chosen. */\nexport function outlineItems(nodes: readonly (OutlineNode | HeadingOutlineNode)[], kinds: readonly CompositeKind[], select: (item: TreeItem) => void): TreeItem[] {\n return nodes.map((node) => {\n const composite = 'kind' in node\n const item: TreeItem = {\n key: `outline:${node.key}`,\n label: composite ? node.title : node.text,\n icon: composite ? compositeKind(kinds, node.kind).icon : `i-lucide-heading-${node.level}`,\n type: composite ? 'composite' : 'heading',\n blockId: node.id,\n pos: node.pos,\n kind: composite ? node.kind : undefined,\n hasSummary: composite ? node.hasSummary : undefined,\n children: node.children.length ? outlineItems(node.children as (OutlineNode | HeadingOutlineNode)[], kinds, select) : undefined,\n onSelect: () => select(item),\n onToggle: e => e?.preventDefault(), // the chevron toggles, a click reveals\n }\n return item\n })\n}\n\n// --- items -----------------------------------------------------------------------------------------------------\n\n/** The store's folders and documents as items; the open document (`activeId`) lists `outline` below it. */\nexport function folderItem(folder: TreeFolder, activeId: string | undefined, outline: TreeItem[], select: (item: TreeItem) => void): TreeItem {\n const root = folder.path === ''\n const item: TreeItem = {\n key: root ? 'root' : folderKey(folder.path),\n label: folder.name,\n icon: root ? 'i-lucide-hard-drive' : 'i-lucide-folder',\n type: root ? 'root' : 'folder',\n path: folder.path,\n children: [\n ...folder.folders.map(sub => folderItem(sub, activeId, outline, select)),\n ...folder.documents.map((doc) => {\n const document: TreeItem = {\n key: `doc:${doc.id}`,\n label: doc.title,\n icon: 'i-lucide-file',\n type: 'document',\n id: doc.id,\n folder: doc.folder,\n // The open document lists its outline; its chevron (not a row click) opens and closes it.\n children: doc.id === activeId && outline.length ? outline : undefined,\n onSelect: () => select(document),\n onToggle: e => e?.preventDefault(),\n }\n return document\n }),\n ],\n onSelect: () => select(item),\n }\n return item\n}\n\n/** Virtual mode: the open document is the root, its `folder`/`document` composites (and all others) the tree below it. */\nexport function virtualRootItem(label: string, outline: TreeItem[], select: (item: TreeItem) => void): TreeItem {\n const item: TreeItem = {\n key: VIRTUAL_ROOT,\n label,\n icon: 'i-lucide-package',\n type: 'root',\n children: outline.length ? outline : undefined,\n onSelect: () => select(item),\n }\n return item\n}\n\nexport function collectParentKeys(list: readonly TreeItem[], into: string[]) {\n for (const item of list) {\n if (!item.children?.length) continue\n into.push(item.key)\n collectParentKeys(item.children, into)\n }\n return into\n}\n\n/** Adds the keys of `list` and all items below it to `into`; returns `into`. */\nexport function collectKeys(list: readonly TreeItem[], into: Set<string>) {\n for (const item of list) {\n into.add(item.key)\n if (item.children) collectKeys(item.children, into)\n }\n return into\n}\n\n// --- filters ---------------------------------------------------------------------------------------------------\n\nconst fileKindsOf = new WeakMap<Translate, CompositeKind[]>()\n\n/** The kinds a folder lists: Ordner and Dokumente (in the language of `t`; one list per translator). */\nexport function fileKinds(t: Translate): CompositeKind[] {\n let kinds = fileKindsOf.get(t)\n if (!kinds) {\n const builtIn = defaultCompositeKinds(t)\n kinds = ['folder', 'document'].map(id => compositeKind(builtIn, id))\n fileKindsOf.set(t, kinds)\n }\n return kinds\n}\n\n/** What an item is filtered by: the composite's kind, or Ordner/Dokument for the store's folders and documents. */\nfunction itemKind(item: TreeItem): string | undefined {\n return item.kind ?? (item.type === 'folder' || item.type === 'document' ? item.type : undefined)\n}\n\n/** The kinds `item`'s filter can hide below it (`kinds` = those of the open editor). */\nexport function filterKinds(item: TreeItem, env: { structure: string, activeId: string | undefined, kinds: readonly CompositeKind[], t: Translate }): readonly CompositeKind[] {\n const composite = env.structure === 'composite'\n if (item.key === VIRTUAL_ROOT || item.type === 'composite') return env.kinds\n if (item.type === 'document') return composite && item.id === env.activeId ? env.kinds : []\n if (item.type === 'root' || item.type === 'folder') return fileKinds(env.t)\n return []\n}\n\n/**\n * Drops the items of kinds an ancestor's filter hides. Like the outline's own kind filter, their visible descendants\n * move up into the parent's list. A node's own filter applies to everything below it.\n */\nexport function applyNodeFilters(list: readonly TreeItem[], filters: NodeFilters, inherited: ReadonlySet<string> = new Set()): TreeItem[] {\n return list.flatMap((item) => {\n const own = filters[item.key]\n const children = item.children ? applyNodeFilters(item.children, filters, own?.length ? new Set([...inherited, ...own]) : inherited) : undefined\n const kind = itemKind(item)\n if (kind && inherited.has(kind)) return children ?? []\n return [{ ...item, children: children?.length ? children : undefined }]\n })\n}\n\n// --- creating ----------------------------------------------------------------------------------------------------\n\n/** The open composite file: end of composite `blockId`'s body, else (`null`) the end of the document. */\nexport function compositeInsertPos(instance: Editor, blockId: string | null): number {\n const { doc } = instance.state\n const target = blockId ? findBlockById(doc, blockId) : null\n return target?.node.type.name === 'composite' ? target.pos + target.node.nodeSize - 1 : doc.content.size\n}\n\n/** Whether `item` can hold Bausteine of the open file: the file itself or one of its Ordner/Dokument composites. */\nexport function holdsComposites(item: TreeItem, env: { structure: string, editor: Editor | undefined, activeId: string | undefined }): boolean {\n if (env.structure !== 'composite' || !env.editor || env.editor.isDestroyed) return false\n if (item.key === VIRTUAL_ROOT) return true\n if (item.type === 'document') return item.id === env.activeId\n return item.type === 'composite' && !!item.blockId && (item.kind === 'folder' || item.kind === 'document')\n}\n\n// --- drag & drop -----------------------------------------------------------------------------------------------\n\n/** What a dragged item carries: its document or folder, or in virtual mode its composite's block ID. */\nexport function dragPayloadOf(item: TreeItem): DragPayload {\n return item.type === 'composite'\n ? { type: 'composite', blockId: item.blockId! }\n : item.type === 'document'\n ? { type: 'document', id: item.id, path: item.folder ?? '' }\n : { type: 'folder', path: item.path! }\n}\n\n/** Whether `item` may be dragged. Virtual mode moves composites of the open document; blocks without an ID cannot be addressed. */\nexport function isDraggable(item: TreeItem, virtual: boolean): boolean {\n if (virtual) return item.type === 'composite' && !!item.blockId\n return item.type === 'document' || item.type === 'folder'\n}\n\n/**\n * Virtual mode: a composite may be dropped on the document root or a `folder`/`document` composite, but not on itself or\n * inside itself. Otherwise documents and folders may be dropped on the root or a folder, unless that changes nothing or\n * nests a folder in itself.\n */\nexport function canDrop(source: DragPayload | null, target: TreeItem, env: { virtual: boolean, editor: Editor | undefined }): boolean {\n if (!source) return false\n if (source.type === 'composite') {\n const instance = env.editor\n if (!env.virtual || !instance || instance.isDestroyed) return false\n if (target.key === VIRTUAL_ROOT) return canMoveBlockInto(instance, source.blockId, null)\n if (target.type !== 'composite' || !target.blockId || (target.kind !== 'folder' && target.kind !== 'document')) return false\n return canMoveBlockInto(instance, source.blockId, target.blockId)\n }\n if (env.virtual || (target.type !== 'root' && target.type !== 'folder')) return false\n const targetPath = target.path!\n if (source.type === 'document') return source.path !== targetPath\n return targetPath !== source.path && !targetPath.startsWith(`${source.path}/`) && parentPath(source.path) !== targetPath\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tree-items.js","names":[],"sources":["../../src/tree/tree-items.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { compositeKind, type CompositeKind, compositeOutline, defaultCompositeKinds, kindAccepts, type OutlineNode } from '../features/composites'\nimport type { Translate } from '../i18n'\nimport { type HeadingOutlineNode, headingOutline } from '../outline'\nimport { folderKey } from './folder-paths'\nimport type { TreeFolder } from './types'\nimport type { NodeFilters } from './tree-view-model'\n\n// What the Vue and the React `DocumentTree` share: the items of the file browser (folders and documents of the store,\n// the open document's outline), their kind filters, what can be dragged and created, and the path arithmetic. No\n// framework, no state; the drop rules are `drop-rules.ts`, the text filter and the visible rows `tree-rows.ts`.\n\n/** One row of the file browser: a store folder or document, or an entry of the open document's outline. Which fields are set depends on `type`. */\nexport interface TreeItem {\n /** Unique within the tree: `root`, `folder:<path>`, `doc:<id>`, `outline:<block key>` */\n key: string\n label: string\n icon: string\n type: 'root' | 'folder' | 'document' | 'composite' | 'heading'\n /** Folder path of a root/folder item (`''` = root) */\n path?: string\n /** Document id */\n id?: string\n /** Folder path a document item lives in */\n folder?: string\n /** Outline items: block ID of the composite/heading (`null` while it has none) and its position when the outline was read */\n blockId?: string | null\n pos?: number\n /** Composite items: the composite's kind, and whether it has a summary (description) */\n kind?: string\n hasSummary?: boolean\n children?: TreeItem[]\n onSelect?: (e?: Event) => void\n onToggle?: (e?: Event) => void\n}\n\n/** What a drag carries (serialized under `DRAG_MIME`): a document or folder of the store, or a composite of the open document. */\nexport type DragPayload\n = | {\n type: 'document' | 'folder'\n /** Document id */\n id?: string\n /** Folder path of a folder item, or the folder a document currently lives in */\n path: string\n }\n /** Virtual mode: a composite of the open document, by block ID */\n | { type: 'composite', blockId: string }\n\n/** Key of the virtual-mode root item (the open document). */\nexport const VIRTUAL_ROOT = 'vroot'\n\nexport const DRAG_MIME = 'application/x-tip-tree'\n\nexport const parentPath = (folder: string) => folder.slice(0, Math.max(folder.lastIndexOf('/'), 0))\nexport const joinPath = (folder: string, name: string) => (folder ? `${folder}/${name}` : name)\n\n/** Item keys from the root down to `folder`. */\nexport function ancestorKeys(folder: string): string[] {\n const keys = ['root']\n let path = ''\n for (const segment of folder.split('/').filter(Boolean)) {\n path = joinPath(path, segment)\n keys.push(folderKey(path))\n }\n return keys\n}\n\n/** The folder at `path` below (or at) `from`; undefined when there is none. */\nexport function findFolder(path: string, from: TreeFolder | null | undefined): TreeFolder | undefined {\n if (!from) return undefined\n if (from.path === path) return from\n for (const sub of from.folders) {\n const found = path === sub.path || path.startsWith(`${sub.path}/`) ? findFolder(path, sub) : undefined\n if (found) return found\n }\n return undefined\n}\n\n/** Names of the folders directly in `parent`, without the folder at path `except`. */\nexport const folderNamesIn = (tree: TreeFolder | null | undefined, parent: string, except?: string) =>\n (findFolder(parent, tree)?.folders ?? []).filter(sub => sub.path !== except).map(sub => sub.name)\n\nexport function collectFolderPaths(folder: TreeFolder, into: Set<string>) {\n into.add(folder.path)\n folder.folders.forEach(sub => collectFolderPaths(sub, into))\n}\n\n// --- outline of the open document ------------------------------------------------------------------------------\n\nexport type Outline = { structure: 'composite', nodes: OutlineNode[] } | { structure: 'flat', nodes: HeadingOutlineNode[] }\n\nexport const EMPTY_OUTLINE: Outline = { structure: 'composite', nodes: [] }\n\n/** The outline of the editor's document: the composites in composite structure, else the headings. */\nexport function readOutline(editor: Editor | undefined, structure: string, t: Translate): Outline {\n if (!editor || editor.isDestroyed) return EMPTY_OUTLINE\n const { doc } = editor.state\n return structure === 'composite'\n ? { structure: 'composite', nodes: compositeOutline(doc, new Set(), t('tree.untitled')) }\n : { structure: 'flat', nodes: headingOutline(doc, t('tree.untitled')) }\n}\n\n/** The outline entries as items; `select` runs when one is chosen. */\nexport function outlineItems(nodes: readonly (OutlineNode | HeadingOutlineNode)[], kinds: readonly CompositeKind[], select: (item: TreeItem) => void): TreeItem[] {\n return nodes.map((node) => {\n const composite = 'kind' in node\n const item: TreeItem = {\n key: `outline:${node.key}`,\n label: composite ? node.title : node.text,\n icon: composite ? compositeKind(kinds, node.kind).icon : `i-lucide-heading-${node.level}`,\n type: composite ? 'composite' : 'heading',\n blockId: node.id,\n pos: node.pos,\n kind: composite ? node.kind : undefined,\n hasSummary: composite ? node.hasSummary : undefined,\n children: node.children.length ? outlineItems(node.children as (OutlineNode | HeadingOutlineNode)[], kinds, select) : undefined,\n onSelect: () => select(item),\n onToggle: e => e?.preventDefault(), // the chevron toggles, a click reveals\n }\n return item\n })\n}\n\n// --- items -----------------------------------------------------------------------------------------------------\n\n/** The store's folders and documents as items; the open document (`activeId`) lists `outline` below it. */\nexport function folderItem(folder: TreeFolder, activeId: string | undefined, outline: TreeItem[], select: (item: TreeItem) => void): TreeItem {\n const root = folder.path === ''\n const item: TreeItem = {\n key: root ? 'root' : folderKey(folder.path),\n label: folder.name,\n icon: root ? 'i-lucide-hard-drive' : 'i-lucide-folder',\n type: root ? 'root' : 'folder',\n path: folder.path,\n children: [\n ...folder.folders.map(sub => folderItem(sub, activeId, outline, select)),\n ...folder.documents.map((doc) => {\n const document: TreeItem = {\n key: `doc:${doc.id}`,\n label: doc.title,\n icon: 'i-lucide-file',\n type: 'document',\n id: doc.id,\n folder: doc.folder,\n // The open document lists its outline; its chevron (not a row click) opens and closes it.\n children: doc.id === activeId && outline.length ? outline : undefined,\n onSelect: () => select(document),\n onToggle: e => e?.preventDefault(),\n }\n return document\n }),\n ],\n onSelect: () => select(item),\n }\n return item\n}\n\n/** Virtual mode: the open document is the root, its `folder`/`document` composites (and all others) the tree below it. */\nexport function virtualRootItem(label: string, outline: TreeItem[], select: (item: TreeItem) => void): TreeItem {\n const item: TreeItem = {\n key: VIRTUAL_ROOT,\n label,\n icon: 'i-lucide-package',\n type: 'root',\n children: outline.length ? outline : undefined,\n onSelect: () => select(item),\n }\n return item\n}\n\n/** Adds the keys of `list` and all items below it to `into`; returns `into`. */\nexport function collectKeys(list: readonly TreeItem[], into: Set<string>) {\n for (const item of list) {\n into.add(item.key)\n if (item.children) collectKeys(item.children, into)\n }\n return into\n}\n\n// --- filters ---------------------------------------------------------------------------------------------------\n\nconst fileKindsOf = new WeakMap<Translate, CompositeKind[]>()\n\n/** The kinds a folder lists: Ordner and Dokumente (in the language of `t`; one list per translator). */\nexport function fileKinds(t: Translate): CompositeKind[] {\n let kinds = fileKindsOf.get(t)\n if (!kinds) {\n const builtIn = defaultCompositeKinds(t)\n kinds = ['folder', 'document'].map(id => compositeKind(builtIn, id))\n fileKindsOf.set(t, kinds)\n }\n return kinds\n}\n\n/** What an item is filtered by: the composite's kind, or Ordner/Dokument for the store's folders and documents. */\nfunction itemKind(item: TreeItem): string | undefined {\n return item.kind ?? (item.type === 'folder' || item.type === 'document' ? item.type : undefined)\n}\n\n/** The kinds `item`'s filter can hide below it (`kinds` = those of the open editor). */\nexport function filterKinds(item: TreeItem, env: { structure: string, activeId: string | undefined, kinds: readonly CompositeKind[], t: Translate }): readonly CompositeKind[] {\n const composite = env.structure === 'composite'\n if (item.key === VIRTUAL_ROOT || item.type === 'composite') return env.kinds\n if (item.type === 'document') return composite && item.id === env.activeId ? env.kinds : []\n if (item.type === 'root' || item.type === 'folder') return fileKinds(env.t)\n return []\n}\n\n/**\n * Drops the items of kinds an ancestor's filter hides. Like the outline's own kind filter, their visible descendants\n * move up into the parent's list. A node's own filter applies to everything below it.\n */\nexport function applyNodeFilters(list: readonly TreeItem[], filters: NodeFilters, inherited: ReadonlySet<string> = new Set()): TreeItem[] {\n return list.flatMap((item) => {\n const own = filters[item.key]\n const children = item.children ? applyNodeFilters(item.children, filters, own?.length ? new Set([...inherited, ...own]) : inherited) : undefined\n const kind = itemKind(item)\n if (kind && inherited.has(kind)) return children ?? []\n return [{ ...item, children: children?.length ? children : undefined }]\n })\n}\n\n// --- creating ----------------------------------------------------------------------------------------------------\n\n/** What can be created below an item of the open file: Ordner-Bausteine, imported Dokument-Bausteine. */\nexport interface Creatable {\n folder: boolean\n document: boolean\n}\n\n/**\n * What `item` takes as new Bausteine of the open file: the file itself (the virtual root, or its document row) takes\n * every kind, a Baustein what its kind accepts as children (`kindAccepts`). Nothing in a flat structure or without an editor.\n */\nexport function creatableBelow(item: TreeItem, env: { structure: string, hasEditor: boolean, activeId: string | undefined, kinds: readonly CompositeKind[] }): Creatable {\n const none: Creatable = { folder: false, document: false }\n if (env.structure !== 'composite' || !env.hasEditor) return none\n if (item.key === VIRTUAL_ROOT || (item.type === 'document' && item.id === env.activeId)) return { folder: true, document: true }\n if (item.type !== 'composite' || !item.blockId || !item.kind) return none\n return { folder: kindAccepts(env.kinds, item.kind, 'folder'), document: kindAccepts(env.kinds, item.kind, 'document') }\n}\n\n// --- drag & drop -----------------------------------------------------------------------------------------------\n\n/** What a dragged item carries: its document or folder, or in virtual mode its composite's block ID. */\nexport function dragPayloadOf(item: TreeItem): DragPayload {\n return item.type === 'composite'\n ? { type: 'composite', blockId: item.blockId! }\n : item.type === 'document'\n ? { type: 'document', id: item.id, path: item.folder ?? '' }\n : { type: 'folder', path: item.path! }\n}\n\n/** Whether `item` may be dragged. Virtual mode moves composites of the open document; blocks without an ID cannot be addressed. */\nexport function isDraggable(item: TreeItem, virtual: boolean): boolean {\n if (virtual) return item.type === 'composite' && !!item.blockId\n return item.type === 'document' || item.type === 'folder'\n}\n\n"],"mappings":";;;;;;AAiDA,MAAa,eAAe;AAE5B,MAAa,YAAY;AAEzB,MAAa,cAAc,WAAmB,OAAO,MAAM,GAAG,KAAK,IAAI,OAAO,YAAY,GAAG,GAAG,CAAC,CAAC;AAClG,MAAa,YAAY,QAAgB,SAAkB,SAAS,GAAG,OAAO,GAAG,SAAS;;AAG1F,SAAgB,aAAa,QAA0B;CACrD,MAAM,OAAO,CAAC,MAAM;CACpB,IAAI,OAAO;CACX,KAAK,MAAM,WAAW,OAAO,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO,GAAG;EACvD,OAAO,SAAS,MAAM,OAAO;EAC7B,KAAK,KAAK,UAAU,IAAI,CAAC;CAC3B;CACA,OAAO;AACT;;AAGA,SAAgB,WAAW,MAAc,MAA6D;CACpG,IAAI,CAAC,MAAM,OAAO,KAAA;CAClB,IAAI,KAAK,SAAS,MAAM,OAAO;CAC/B,KAAK,MAAM,OAAO,KAAK,SAAS;EAC9B,MAAM,QAAQ,SAAS,IAAI,QAAQ,KAAK,WAAW,GAAG,IAAI,KAAK,EAAE,IAAI,WAAW,MAAM,GAAG,IAAI,KAAA;EAC7F,IAAI,OAAO,OAAO;CACpB;AAEF;;AAGA,MAAa,iBAAiB,MAAqC,QAAgB,YAChF,WAAW,QAAQ,IAAI,CAAC,EAAE,WAAW,CAAC,EAAA,CAAG,QAAO,QAAO,IAAI,SAAS,MAAM,CAAC,CAAC,KAAI,QAAO,IAAI,IAAI;AAElG,SAAgB,mBAAmB,QAAoB,MAAmB;CACxE,KAAK,IAAI,OAAO,IAAI;CACpB,OAAO,QAAQ,SAAQ,QAAO,mBAAmB,KAAK,IAAI,CAAC;AAC7D;AAMA,MAAa,gBAAyB;CAAE,WAAW;CAAa,OAAO,CAAC;AAAE;;AAG1E,SAAgB,YAAY,QAA4B,WAAmB,GAAuB;CAChG,IAAI,CAAC,UAAU,OAAO,aAAa,OAAO;CAC1C,MAAM,EAAE,QAAQ,OAAO;CACvB,OAAO,cAAc,cACjB;EAAE,WAAW;EAAa,OAAO,iBAAiB,qBAAK,IAAI,IAAI,GAAG,EAAE,eAAe,CAAC;CAAE,IACtF;EAAE,WAAW;EAAQ,OAAO,eAAe,KAAK,EAAE,eAAe,CAAC;CAAE;AAC1E;;AAGA,SAAgB,aAAa,OAAsD,OAAiC,QAA8C;CAChK,OAAO,MAAM,KAAK,SAAS;EACzB,MAAM,YAAY,UAAU;EAC5B,MAAM,OAAiB;GACrB,KAAK,WAAW,KAAK;GACrB,OAAO,YAAY,KAAK,QAAQ,KAAK;GACrC,MAAM,YAAY,cAAc,OAAO,KAAK,IAAI,CAAC,CAAC,OAAO,oBAAoB,KAAK;GAClF,MAAM,YAAY,cAAc;GAChC,SAAS,KAAK;GACd,KAAK,KAAK;GACV,MAAM,YAAY,KAAK,OAAO,KAAA;GAC9B,YAAY,YAAY,KAAK,aAAa,KAAA;GAC1C,UAAU,KAAK,SAAS,SAAS,aAAa,KAAK,UAAkD,OAAO,MAAM,IAAI,KAAA;GACtH,gBAAgB,OAAO,IAAI;GAC3B,WAAU,MAAK,GAAG,eAAe;EACnC;EACA,OAAO;CACT,CAAC;AACH;;AAKA,SAAgB,WAAW,QAAoB,UAA8B,SAAqB,QAA4C;CAC5I,MAAM,OAAO,OAAO,SAAS;CAC7B,MAAM,OAAiB;EACrB,KAAK,OAAO,SAAS,UAAU,OAAO,IAAI;EAC1C,OAAO,OAAO;EACd,MAAM,OAAO,wBAAwB;EACrC,MAAM,OAAO,SAAS;EACtB,MAAM,OAAO;EACb,UAAU,CACR,GAAG,OAAO,QAAQ,KAAI,QAAO,WAAW,KAAK,UAAU,SAAS,MAAM,CAAC,GACvE,GAAG,OAAO,UAAU,KAAK,QAAQ;GAC/B,MAAM,WAAqB;IACzB,KAAK,OAAO,IAAI;IAChB,OAAO,IAAI;IACX,MAAM;IACN,MAAM;IACN,IAAI,IAAI;IACR,QAAQ,IAAI;IAEZ,UAAU,IAAI,OAAO,YAAY,QAAQ,SAAS,UAAU,KAAA;IAC5D,gBAAgB,OAAO,QAAQ;IAC/B,WAAU,MAAK,GAAG,eAAe;GACnC;GACA,OAAO;EACT,CAAC,CACH;EACA,gBAAgB,OAAO,IAAI;CAC7B;CACA,OAAO;AACT;;AAGA,SAAgB,gBAAgB,OAAe,SAAqB,QAA4C;CAC9G,MAAM,OAAiB;EACrB,KAAK;EACL;EACA,MAAM;EACN,MAAM;EACN,UAAU,QAAQ,SAAS,UAAU,KAAA;EACrC,gBAAgB,OAAO,IAAI;CAC7B;CACA,OAAO;AACT;;AAGA,SAAgB,YAAY,MAA2B,MAAmB;CACxE,KAAK,MAAM,QAAQ,MAAM;EACvB,KAAK,IAAI,KAAK,GAAG;EACjB,IAAI,KAAK,UAAU,YAAY,KAAK,UAAU,IAAI;CACpD;CACA,OAAO;AACT;AAIA,MAAM,8BAAc,IAAI,QAAoC;;AAG5D,SAAgB,UAAU,GAA+B;CACvD,IAAI,QAAQ,YAAY,IAAI,CAAC;CAC7B,IAAI,CAAC,OAAO;EACV,MAAM,UAAU,sBAAsB,CAAC;EACvC,QAAQ,CAAC,UAAU,UAAU,CAAC,CAAC,KAAI,OAAM,cAAc,SAAS,EAAE,CAAC;EACnE,YAAY,IAAI,GAAG,KAAK;CAC1B;CACA,OAAO;AACT;;AAGA,SAAS,SAAS,MAAoC;CACpD,OAAO,KAAK,SAAS,KAAK,SAAS,YAAY,KAAK,SAAS,aAAa,KAAK,OAAO,KAAA;AACxF;;AAGA,SAAgB,YAAY,MAAgB,KAAmI;CAC7K,MAAM,YAAY,IAAI,cAAc;CACpC,IAAI,KAAK,QAAA,WAAwB,KAAK,SAAS,aAAa,OAAO,IAAI;CACvE,IAAI,KAAK,SAAS,YAAY,OAAO,aAAa,KAAK,OAAO,IAAI,WAAW,IAAI,QAAQ,CAAC;CAC1F,IAAI,KAAK,SAAS,UAAU,KAAK,SAAS,UAAU,OAAO,UAAU,IAAI,CAAC;CAC1E,OAAO,CAAC;AACV;;;;;AAMA,SAAgB,iBAAiB,MAA2B,SAAsB,4BAAiC,IAAI,IAAI,GAAe;CACxI,OAAO,KAAK,SAAS,SAAS;EAC5B,MAAM,MAAM,QAAQ,KAAK;EACzB,MAAM,WAAW,KAAK,WAAW,iBAAiB,KAAK,UAAU,SAAS,KAAK,yBAAS,IAAI,IAAI,CAAC,GAAG,WAAW,GAAG,GAAG,CAAC,IAAI,SAAS,IAAI,KAAA;EACvI,MAAM,OAAO,SAAS,IAAI;EAC1B,IAAI,QAAQ,UAAU,IAAI,IAAI,GAAG,OAAO,YAAY,CAAC;EACrD,OAAO,CAAC;GAAE,GAAG;GAAM,UAAU,UAAU,SAAS,WAAW,KAAA;EAAU,CAAC;CACxE,CAAC;AACH;;;;;AAcA,SAAgB,eAAe,MAAgB,KAA0H;CACvK,MAAM,OAAkB;EAAE,QAAQ;EAAO,UAAU;CAAM;CACzD,IAAI,IAAI,cAAc,eAAe,CAAC,IAAI,WAAW,OAAO;CAC5D,IAAI,KAAK,QAAA,WAAyB,KAAK,SAAS,cAAc,KAAK,OAAO,IAAI,UAAW,OAAO;EAAE,QAAQ;EAAM,UAAU;CAAK;CAC/H,IAAI,KAAK,SAAS,eAAe,CAAC,KAAK,WAAW,CAAC,KAAK,MAAM,OAAO;CACrE,OAAO;EAAE,QAAQ,YAAY,IAAI,OAAO,KAAK,MAAM,QAAQ;EAAG,UAAU,YAAY,IAAI,OAAO,KAAK,MAAM,UAAU;CAAE;AACxH;;AAKA,SAAgB,cAAc,MAA6B;CACzD,OAAO,KAAK,SAAS,cACjB;EAAE,MAAM;EAAa,SAAS,KAAK;CAAS,IAC5C,KAAK,SAAS,aACZ;EAAE,MAAM;EAAY,IAAI,KAAK;EAAI,MAAM,KAAK,UAAU;CAAG,IACzD;EAAE,MAAM;EAAU,MAAM,KAAK;CAAM;AAC3C;;AAGA,SAAgB,YAAY,MAAgB,SAA2B;CACrE,IAAI,SAAS,OAAO,KAAK,SAAS,eAAe,CAAC,CAAC,KAAK;CACxD,OAAO,KAAK,SAAS,cAAc,KAAK,SAAS;AACnD"}
@@ -0,0 +1,580 @@
1
+ import { createStore } from "../core/store.js";
2
+ import { kindAccepts } from "../features/composites/kinds.js";
3
+ import { remapFolderPath } from "./folder-paths.js";
4
+ import { EMPTY_OUTLINE, VIRTUAL_ROOT, ancestorKeys, collectFolderPaths, creatableBelow, dragPayloadOf, filterKinds, folderNamesIn, isDraggable, joinPath, parentPath } from "./tree-items.js";
5
+ import { dropVerdict } from "./drop-rules.js";
6
+ import { deriveTree, filterQuery } from "./tree-rows.js";
7
+ //#region src/tree/tree-model.ts
8
+ const NO_DOCUMENTS = [];
9
+ const NO_KINDS = [];
10
+ const NO_FILTERS = {};
11
+ const UNSET = Symbol("unset");
12
+ const isOutlineKey = (key) => key.startsWith("outline:");
13
+ function sameVerdict(a, b) {
14
+ if (a === b) return true;
15
+ if (!a || !b) return false;
16
+ return a.ok ? b.ok : !b.ok && a.reason === b.reason;
17
+ }
18
+ /** A row's state in `state` (`item` is one of `state.view.items`). */
19
+ function rowOf(state, item, t) {
20
+ const hidden = state.nodeFilters[item.key] ?? [];
21
+ const kinds = filterKinds(item, {
22
+ structure: state.structure,
23
+ activeId: state.activeId,
24
+ kinds: state.kinds,
25
+ t
26
+ });
27
+ const virtualTarget = state.virtual && (item.key === "vroot" ? state.virtualTarget === null : !!item.blockId && item.blockId === state.virtualTarget);
28
+ return {
29
+ ...state.view.rows.get(item.key),
30
+ active: item.id !== void 0 && item.id === state.activeId,
31
+ selected: item.type !== "document" && item.path === state.selectedFolder || virtualTarget,
32
+ drop: state.drag?.hover === item.key ? state.drag.verdict : null,
33
+ renaming: state.renaming?.key === item.key ? state.renaming : null,
34
+ removing: state.removing === item.key,
35
+ filterKinds: kinds,
36
+ hiddenKinds: hidden,
37
+ showFilter: kinds.length > 0 && (item.type === "root" || item.type === "folder" || !!item.children?.length || hidden.length > 0)
38
+ };
39
+ }
40
+ /** A tree model over `options.store`; call `dispose` when its tree goes away. */
41
+ function createTreeModel(options) {
42
+ const { store, view: shared, execute, t } = options;
43
+ const debounceMs = options.outlineDebounceMs ?? 150;
44
+ let editor = options.context.editor;
45
+ let structure = options.context.structure;
46
+ let treeRoot = options.context.treeRoot;
47
+ let unwatchEditor = editor?.onChange(onEditorChange);
48
+ let outlineTimer;
49
+ /** A folder move the host is running: until it is done, a selected folder at the old path follows to the new one. */
50
+ let pendingMove;
51
+ let disposed = false;
52
+ const isVirtual = () => treeRoot === "file" && structure === "composite" && !!editor;
53
+ let core = {
54
+ tree: store.tree.get(),
55
+ documents: store.documents.get() ?? NO_DOCUMENTS,
56
+ activeId: store.activeId.get(),
57
+ virtual: isVirtual(),
58
+ structure,
59
+ kinds: editor?.kinds ?? NO_KINDS,
60
+ hasEditor: !!editor,
61
+ outline: editor ? editor.outline(structure) : EMPTY_OUTLINE,
62
+ nodeFilters: shared.nodeFilters.get(),
63
+ expanded: shared.expanded.get(),
64
+ collapsedOutline: [],
65
+ filterText: "",
66
+ filterOverrides: NO_FILTERS,
67
+ selectedFolder: options.selectedFolder ?? "",
68
+ virtualTarget: null,
69
+ drag: null,
70
+ renaming: null,
71
+ creatingFolder: null,
72
+ removing: null
73
+ };
74
+ let lastView;
75
+ let lastTree = UNSET;
76
+ let folderPaths = /* @__PURE__ */ new Set();
77
+ let lastActiveFolder = UNSET;
78
+ let lastActiveId = UNSET;
79
+ const dropEnv = () => ({
80
+ virtual: core.virtual,
81
+ moveVerdict: editor?.moveVerdict,
82
+ t
83
+ });
84
+ function viewOf(c) {
85
+ const deps = [
86
+ c.tree,
87
+ c.documents,
88
+ c.activeId,
89
+ c.virtual,
90
+ c.outline,
91
+ c.kinds,
92
+ c.nodeFilters,
93
+ c.expanded,
94
+ c.collapsedOutline,
95
+ c.filterText,
96
+ c.filterOverrides
97
+ ];
98
+ if (lastView && deps.every((dep, i) => Object.is(dep, lastView.deps[i]))) return lastView.view;
99
+ const view = deriveTree({
100
+ ...c,
101
+ t,
102
+ select: selectItem
103
+ });
104
+ lastView = {
105
+ deps,
106
+ view
107
+ };
108
+ return view;
109
+ }
110
+ const renameError = (draft, path, base, tree) => draft.trim() === base ? null : store.validateName(draft, folderNamesIn(tree, parentPath(path), path));
111
+ const folderError = (draft, parent, tree) => draft.trim() ? store.validateName(draft, folderNamesIn(tree, parent)) : null;
112
+ /** Measures everything the tree remembers against the current tree: what is gone is dropped, what depends on the tree is recomputed. */
113
+ function reconcile(c, view) {
114
+ let next = c;
115
+ if (c.tree !== lastTree) {
116
+ lastTree = c.tree;
117
+ folderPaths = /* @__PURE__ */ new Set();
118
+ if (c.tree) collectFolderPaths(c.tree, folderPaths);
119
+ if (c.tree && !folderPaths.has(next.selectedFolder)) {
120
+ const followed = pendingMove ? remapFolderPath(next.selectedFolder, pendingMove.from, pendingMove.to) : "";
121
+ next = {
122
+ ...next,
123
+ selectedFolder: folderPaths.has(followed) ? followed : ""
124
+ };
125
+ }
126
+ }
127
+ if (next.virtualTarget !== null && (!next.virtual || !view.keys.has(`outline:${next.virtualTarget}`))) next = {
128
+ ...next,
129
+ virtualTarget: null
130
+ };
131
+ if (next.renaming) {
132
+ const r = next.renaming;
133
+ if (!view.keys.has(r.key)) next = {
134
+ ...next,
135
+ renaming: null
136
+ };
137
+ else {
138
+ const error = renameError(r.draft, r.path, r.base, next.tree);
139
+ if (error !== r.error) next = {
140
+ ...next,
141
+ renaming: {
142
+ ...r,
143
+ error
144
+ }
145
+ };
146
+ }
147
+ }
148
+ if (next.creatingFolder) {
149
+ const f = next.creatingFolder;
150
+ if (next.tree && !folderPaths.has(f.parent)) next = {
151
+ ...next,
152
+ creatingFolder: null
153
+ };
154
+ else {
155
+ const error = folderError(f.draft, f.parent, next.tree);
156
+ if (error !== f.error) next = {
157
+ ...next,
158
+ creatingFolder: {
159
+ ...f,
160
+ error
161
+ }
162
+ };
163
+ }
164
+ }
165
+ if (next.drag) {
166
+ const d = next.drag;
167
+ if (!view.keys.has(d.sourceKey) || d.hover !== null && !view.keys.has(d.hover)) next = {
168
+ ...next,
169
+ drag: null
170
+ };
171
+ else if (d.hover !== null) {
172
+ const target = view.rows.get(d.hover)?.item;
173
+ const verdict = target ? dropVerdict(d.source, target, dropEnv()) : d.verdict;
174
+ if (!sameVerdict(verdict, d.verdict)) next = {
175
+ ...next,
176
+ drag: {
177
+ ...d,
178
+ verdict
179
+ }
180
+ };
181
+ }
182
+ }
183
+ return next;
184
+ }
185
+ const state = createStore({
186
+ ...core,
187
+ view: viewOf(core)
188
+ });
189
+ function publish(c) {
190
+ if (disposed) return;
191
+ const view = viewOf(c);
192
+ core = reconcile(c, view);
193
+ const next = {
194
+ ...core,
195
+ view
196
+ };
197
+ const current = state.get();
198
+ if (Object.keys(next).every((key) => Object.is(next[key], current[key]))) return;
199
+ state.set(next);
200
+ }
201
+ const patch = (changes) => publish({
202
+ ...core,
203
+ ...changes
204
+ });
205
+ /** Reads the store and the shared view state; what opening a document shows is opened. */
206
+ function sync() {
207
+ if (disposed) return;
208
+ const documents = store.documents.get() ?? NO_DOCUMENTS;
209
+ const activeId = store.activeId.get();
210
+ publish({
211
+ ...core,
212
+ tree: store.tree.get(),
213
+ documents,
214
+ activeId,
215
+ nodeFilters: shared.nodeFilters.get(),
216
+ expanded: shared.expanded.get()
217
+ });
218
+ const activeFolder = documents.find((d) => d.id === activeId)?.folder;
219
+ if (activeFolder !== lastActiveFolder) {
220
+ lastActiveFolder = activeFolder;
221
+ expand(ancestorKeys(activeFolder ?? ""));
222
+ if (activeFolder !== void 0) setSelectedFolder(activeFolder);
223
+ }
224
+ if (activeId !== lastActiveId) {
225
+ lastActiveId = activeId;
226
+ if (activeId) expand([`doc:${activeId}`]);
227
+ }
228
+ }
229
+ const unsubscribe = [
230
+ store.tree.subscribe(sync),
231
+ store.documents.subscribe(sync),
232
+ store.activeId.subscribe(sync),
233
+ shared.nodeFilters.subscribe(sync),
234
+ shared.expanded.subscribe(sync)
235
+ ];
236
+ function readOutline() {
237
+ patch({ outline: editor ? editor.outline(structure) : EMPTY_OUTLINE });
238
+ }
239
+ function onEditorChange() {
240
+ clearTimeout(outlineTimer);
241
+ outlineTimer = setTimeout(readOutline, debounceMs);
242
+ if (core.drag) publish(core);
243
+ }
244
+ function setContext(context) {
245
+ if (context.editor === editor && context.structure === structure && context.treeRoot === treeRoot) return;
246
+ const editorChanged = context.editor !== editor;
247
+ const wasVirtual = core.virtual;
248
+ editor = context.editor;
249
+ structure = context.structure;
250
+ treeRoot = context.treeRoot;
251
+ clearTimeout(outlineTimer);
252
+ if (editorChanged) {
253
+ unwatchEditor?.();
254
+ unwatchEditor = editor?.onChange(onEditorChange);
255
+ }
256
+ patch({
257
+ virtual: isVirtual(),
258
+ structure,
259
+ kinds: editor?.kinds ?? NO_KINDS,
260
+ hasEditor: !!editor,
261
+ outline: editor ? editor.outline(structure) : EMPTY_OUTLINE
262
+ });
263
+ if (core.virtual && !wasVirtual) expand([VIRTUAL_ROOT]);
264
+ }
265
+ const filtering = () => filterQuery(core.filterText) !== "";
266
+ function setOpen(key, open) {
267
+ if (filtering()) patch({ filterOverrides: {
268
+ ...core.filterOverrides,
269
+ [key]: open
270
+ } });
271
+ else if (isOutlineKey(key)) patch({ collapsedOutline: open ? core.collapsedOutline.filter((k) => k !== key) : [...core.collapsedOutline.filter((k) => k !== key), key] });
272
+ else if (open) shared.expand([key]);
273
+ else shared.expanded.set(shared.expanded.get().filter((k) => k !== key));
274
+ }
275
+ function expand(keys) {
276
+ const outlineKeys = keys.filter(isOutlineKey);
277
+ const { filterOverrides } = core;
278
+ const overridden = keys.some((key) => key in filterOverrides);
279
+ if (overridden || outlineKeys.some((key) => core.collapsedOutline.includes(key))) patch({
280
+ collapsedOutline: core.collapsedOutline.filter((key) => !outlineKeys.includes(key)),
281
+ filterOverrides: overridden ? Object.fromEntries(Object.entries(filterOverrides).filter(([key]) => !keys.includes(key))) : filterOverrides
282
+ });
283
+ shared.expand(keys.filter((key) => !isOutlineKey(key)));
284
+ }
285
+ function setFilterText(text) {
286
+ patch({
287
+ filterText: text,
288
+ filterOverrides: filterQuery(text) ? core.filterOverrides : NO_FILTERS
289
+ });
290
+ }
291
+ function setSelectedFolder(path) {
292
+ patch({ selectedFolder: path });
293
+ }
294
+ function selectItem(item) {
295
+ if (core.renaming?.key === item.key) return;
296
+ if (item.key === "vroot") return patch({ virtualTarget: null });
297
+ if (item.type === "document") {
298
+ setSelectedFolder(item.folder ?? "");
299
+ execute({
300
+ type: "openDocument",
301
+ id: item.id
302
+ });
303
+ } else if (item.type === "composite" || item.type === "heading") {
304
+ execute({
305
+ type: "revealBlock",
306
+ id: item.blockId ?? null,
307
+ pos: item.pos
308
+ });
309
+ const takesBausteine = !!item.kind && core.kinds.some((kind) => kindAccepts(core.kinds, item.kind, kind.id));
310
+ if (core.virtual && item.blockId && takesBausteine) patch({ virtualTarget: item.blockId });
311
+ } else setSelectedFolder(item.path ?? "");
312
+ }
313
+ function toggle(key) {
314
+ setOpen(key, !state.get().view.rows.get(key)?.open);
315
+ }
316
+ /** A folder moved from `from` to `to`: its open/selected state follows it. */
317
+ function followFolderMove(from, to) {
318
+ shared.followFolderMove(from, to);
319
+ patch({
320
+ selectedFolder: remapFolderPath(core.selectedFolder, from, to),
321
+ filterOverrides: Object.fromEntries(Object.entries(core.filterOverrides).map(([key, open]) => [key.startsWith("folder:") ? `folder:${remapFolderPath(key.slice(7), from, to)}` : key, open]))
322
+ });
323
+ expand(ancestorKeys(parentPath(to)));
324
+ }
325
+ async function moveFolder(from, to) {
326
+ pendingMove = {
327
+ from,
328
+ to
329
+ };
330
+ const moved = await execute({
331
+ type: "moveFolder",
332
+ from,
333
+ to
334
+ });
335
+ pendingMove = void 0;
336
+ if (moved) followFolderMove(from, to);
337
+ }
338
+ function dragStart(item) {
339
+ if (core.renaming?.key === item.key || !isDraggable(item, core.virtual)) return null;
340
+ const source = dragPayloadOf(item);
341
+ patch({ drag: {
342
+ source,
343
+ sourceKey: item.key,
344
+ hover: null,
345
+ verdict: null
346
+ } });
347
+ return source;
348
+ }
349
+ function dragOver(item) {
350
+ const drag = core.drag;
351
+ if (!drag) return null;
352
+ const verdict = dropVerdict(drag.source, item, dropEnv());
353
+ if (drag.hover !== item.key || !sameVerdict(drag.verdict, verdict)) patch({ drag: {
354
+ ...drag,
355
+ hover: item.key,
356
+ verdict
357
+ } });
358
+ return verdict;
359
+ }
360
+ async function drop(item) {
361
+ const drag = core.drag;
362
+ patch({ drag: null });
363
+ if (!drag || !dropVerdict(drag.source, item, dropEnv()).ok) return;
364
+ const { source } = drag;
365
+ if (source.type === "composite") {
366
+ const targetId = item.key === "vroot" ? null : item.blockId;
367
+ if (await execute({
368
+ type: "moveBlock",
369
+ id: source.blockId,
370
+ targetId
371
+ }) && targetId) expand([`outline:${targetId}`]);
372
+ return;
373
+ }
374
+ const targetPath = item.path;
375
+ if (source.type === "document") {
376
+ if (await execute({
377
+ type: "moveDocument",
378
+ id: source.id,
379
+ folder: targetPath
380
+ })) expand(ancestorKeys(targetPath));
381
+ return;
382
+ }
383
+ await moveFolder(source.path, joinPath(targetPath, source.path.slice(source.path.lastIndexOf("/") + 1)));
384
+ }
385
+ function startRename(item) {
386
+ if (item.type !== "folder") return;
387
+ patch({ renaming: {
388
+ key: item.key,
389
+ path: item.path,
390
+ base: item.label,
391
+ draft: item.label,
392
+ error: null
393
+ } });
394
+ }
395
+ async function commitRename() {
396
+ const r = core.renaming;
397
+ if (!r || r.error) return;
398
+ const name = r.draft.trim();
399
+ patch({ renaming: null });
400
+ if (name !== r.base) await moveFolder(r.path, joinPath(parentPath(r.path), name));
401
+ }
402
+ async function blurRename() {
403
+ const r = core.renaming;
404
+ if (!r) return;
405
+ if (r.error) {
406
+ execute({
407
+ type: "warn",
408
+ title: t("tree.folder.renameFailed"),
409
+ description: r.error
410
+ });
411
+ patch({ renaming: null });
412
+ return;
413
+ }
414
+ await commitRename();
415
+ }
416
+ function startCreateFolder(parent) {
417
+ patch({
418
+ selectedFolder: parent,
419
+ creatingFolder: {
420
+ parent,
421
+ draft: "",
422
+ error: null
423
+ }
424
+ });
425
+ }
426
+ async function commitCreateFolder() {
427
+ const f = core.creatingFolder;
428
+ if (!f) return;
429
+ if (!f.draft.trim()) return patch({ creatingFolder: null });
430
+ if (f.error) return;
431
+ patch({ creatingFolder: null });
432
+ if (await execute({
433
+ type: "createFolder",
434
+ path: joinPath(f.parent, f.draft.trim())
435
+ })) expand(ancestorKeys(f.parent));
436
+ }
437
+ async function createDocumentIn(folder) {
438
+ setSelectedFolder(folder);
439
+ if (await execute({
440
+ type: "createDocument",
441
+ folder
442
+ })) expand(ancestorKeys(folder));
443
+ }
444
+ /** Runs `command` for the Baustein of `item` (the file itself when it has none) and shows the result. */
445
+ async function inComposite(item, command) {
446
+ if (await execute(command)) expand([item.blockId ? `outline:${item.blockId}` : item.key]);
447
+ }
448
+ /**
449
+ * What can be created below `item`: folders of the store take documents and sub-folders, the open file (as the root or
450
+ * as a document row) and its Bausteine take what their kind accepts. Other rows take nothing. Folders also list
451
+ * „Umbenennen“ here: renaming a document happens in the editor's title field.
452
+ */
453
+ function addMenu(item) {
454
+ const groups = [];
455
+ if (item.key !== "vroot" && (item.type === "root" || item.type === "folder")) {
456
+ const path = item.path;
457
+ groups.push([{
458
+ label: t("tree.menu.newDocument"),
459
+ icon: "i-lucide-file-plus",
460
+ run: () => void createDocumentIn(path)
461
+ }, {
462
+ label: item.type === "root" ? t("tree.menu.newFolder") : t("tree.menu.newSubfolder"),
463
+ icon: "i-lucide-folder-plus",
464
+ run: () => startCreateFolder(path)
465
+ }]);
466
+ } else {
467
+ const can = creatableBelow(item, {
468
+ structure: core.structure,
469
+ hasEditor: core.hasEditor,
470
+ activeId: core.activeId,
471
+ kinds: core.kinds
472
+ });
473
+ const targetId = item.blockId ?? null;
474
+ const entries = [];
475
+ if (can.folder) entries.push({
476
+ label: t("tree.menu.newCompositeFolder"),
477
+ icon: "i-lucide-folder-plus",
478
+ run: () => void inComposite(item, {
479
+ type: "createBausteinFolder",
480
+ targetId
481
+ })
482
+ });
483
+ if (can.document) entries.push({
484
+ label: t("tree.menu.importFile"),
485
+ icon: "i-lucide-file-up",
486
+ run: () => void inComposite(item, {
487
+ type: "importFiles",
488
+ targetId
489
+ })
490
+ });
491
+ if (entries.length) groups.push(entries);
492
+ }
493
+ if (item.type === "folder") groups.push([{
494
+ label: t("tree.menu.rename"),
495
+ icon: "i-lucide-pencil",
496
+ run: () => startRename(item)
497
+ }]);
498
+ return groups;
499
+ }
500
+ async function remove(item) {
501
+ patch({ removing: item.key });
502
+ try {
503
+ await execute(item.type === "document" ? {
504
+ type: "removeDocument",
505
+ id: item.id,
506
+ folder: item.folder ?? ""
507
+ } : {
508
+ type: "removeFolder",
509
+ path: item.path
510
+ });
511
+ } finally {
512
+ patch({ removing: null });
513
+ }
514
+ }
515
+ sync();
516
+ return {
517
+ state,
518
+ setContext,
519
+ setSelectedFolder,
520
+ setOpen,
521
+ toggle,
522
+ expand,
523
+ setFilterText,
524
+ setKindHidden: (key, kind, hidden) => shared.setKindHidden(key, kind, hidden),
525
+ clearFilter: (key) => shared.clearFilter(key),
526
+ clearAllFilters: () => shared.clearAllFilters(),
527
+ select: selectItem,
528
+ activate(item) {
529
+ selectItem(item);
530
+ if (item.type === "root" || item.type === "folder") toggle(item.key);
531
+ },
532
+ addMenu,
533
+ remove,
534
+ isDraggable: (item) => core.renaming?.key !== item.key && isDraggable(item, core.virtual),
535
+ dragStart,
536
+ dragOver,
537
+ dragLeave(item) {
538
+ if (core.drag?.hover === item.key) patch({ drag: {
539
+ ...core.drag,
540
+ hover: null,
541
+ verdict: null
542
+ } });
543
+ },
544
+ dragEnd: () => patch({ drag: null }),
545
+ drop,
546
+ startRename,
547
+ setRenameDraft(text) {
548
+ const r = core.renaming;
549
+ if (r) patch({ renaming: {
550
+ ...r,
551
+ draft: text,
552
+ error: renameError(text, r.path, r.base, core.tree)
553
+ } });
554
+ },
555
+ cancelRename: () => patch({ renaming: null }),
556
+ commitRename,
557
+ blurRename,
558
+ startCreateFolder,
559
+ setFolderDraft(text) {
560
+ const f = core.creatingFolder;
561
+ if (f) patch({ creatingFolder: {
562
+ ...f,
563
+ draft: text,
564
+ error: folderError(text, f.parent, core.tree)
565
+ } });
566
+ },
567
+ cancelCreateFolder: () => patch({ creatingFolder: null }),
568
+ commitCreateFolder,
569
+ dispose() {
570
+ disposed = true;
571
+ clearTimeout(outlineTimer);
572
+ unwatchEditor?.();
573
+ for (const stop of unsubscribe) stop();
574
+ }
575
+ };
576
+ }
577
+ //#endregion
578
+ export { createTreeModel, rowOf };
579
+
580
+ //# sourceMappingURL=tree-model.js.map