@ecsplico/editor 1.0.0 → 2.1.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/CHANGELOG.md +37 -0
- package/README.md +7 -7
- package/dist/adapters/anchors.d.ts +80 -0
- package/dist/adapters/anchors.js +270 -0
- package/dist/adapters/anchors.js.map +1 -0
- package/dist/adapters/comments/client.d.ts +64 -0
- package/dist/adapters/comments/client.js +536 -0
- package/dist/adapters/comments/client.js.map +1 -0
- package/dist/adapters/comments/drizzle.d.ts +430 -0
- package/dist/adapters/comments/drizzle.js +236 -0
- package/dist/adapters/comments/drizzle.js.map +1 -0
- package/dist/adapters/comments/errors.d.ts +9 -0
- package/dist/adapters/comments/errors.js +19 -0
- package/dist/adapters/comments/errors.js.map +1 -0
- package/dist/adapters/comments/json.d.ts +19 -0
- package/dist/adapters/comments/json.js +94 -0
- package/dist/adapters/comments/json.js.map +1 -0
- package/dist/adapters/comments/record.js +78 -0
- package/dist/adapters/comments/record.js.map +1 -0
- package/dist/adapters/comments/server.d.ts +23 -0
- package/dist/adapters/comments/server.js +102 -0
- package/dist/adapters/comments/server.js.map +1 -0
- package/dist/adapters/comments/types.d.ts +109 -0
- package/dist/adapters/documents.js +108 -0
- package/dist/adapters/documents.js.map +1 -0
- package/dist/adapters/drizzle.d.ts +293 -0
- package/dist/adapters/drizzle.js +157 -0
- package/dist/adapters/drizzle.js.map +1 -0
- package/dist/adapters/files.js +30 -0
- package/dist/adapters/files.js.map +1 -0
- package/dist/adapters/forge.d.ts +58 -0
- package/dist/adapters/forge.js +91 -0
- package/dist/adapters/forge.js.map +1 -0
- package/dist/adapters/index.d.ts +13 -0
- package/dist/adapters/index.js +0 -0
- package/dist/adapters/json.d.ts +24 -0
- package/dist/adapters/json.js +128 -0
- package/dist/adapters/json.js.map +1 -0
- package/dist/adapters/nitro.d.ts +61 -0
- package/dist/adapters/nitro.js +212 -0
- package/dist/adapters/nitro.js.map +1 -0
- package/dist/adapters/routes.js +16 -0
- package/dist/adapters/routes.js.map +1 -0
- package/dist/adapters/shared.d.ts +37 -0
- package/dist/adapters/shared.js +32 -0
- package/dist/adapters/shared.js.map +1 -0
- package/dist/adapters/vue.d.ts +67 -0
- package/dist/adapters/vue.js +187 -0
- package/dist/adapters/vue.js.map +1 -0
- package/dist/code/CodeSurface.vue.d.ts +3 -3
- package/dist/core/doc-anchors.d.ts +5 -0
- package/dist/core/doc-anchors.js +4 -3
- package/dist/core/doc-anchors.js.map +1 -1
- package/dist/core/editor-ui.js +57 -1
- package/dist/core/editor-ui.js.map +1 -1
- package/dist/core/notify.d.ts +1 -1
- package/dist/core/notify.js.map +1 -1
- package/dist/core/surface-session.js +139 -0
- package/dist/core/surface-session.js.map +1 -0
- package/dist/document/DocumentSurface.d.ts +1 -1
- package/dist/document/DocumentSurface.vue.d.ts +3 -3
- package/dist/features/ai/agent-op-review.d.ts +2 -2
- package/dist/features/ai/agent-op-review.js.map +1 -1
- package/dist/features/ai/agent-ops.d.ts +1 -1
- package/dist/features/ai/agent-ops.js +9 -6
- package/dist/features/ai/agent-ops.js.map +1 -1
- package/dist/features/ai/ai-apply.d.ts +1 -1
- package/dist/features/ai/ai-review.js +10 -1
- package/dist/features/ai/ai-review.js.map +1 -1
- package/dist/features/ai/components/AiSelectionMenu.js +78 -252
- package/dist/features/ai/components/AiSelectionMenu.js.map +1 -1
- package/dist/features/ai/components/AiSelectionMenu.vue.d.ts +2 -1
- package/dist/features/ai/components/AiSidebar.js +2 -2
- package/dist/features/ai/components/DocumentChatAnswer.js +1 -1
- package/dist/features/ai/components/DocumentChatOps.js +1 -0
- package/dist/features/ai/components/DocumentChatOps.js.map +1 -1
- package/dist/features/ai/components/ModelQuickSwitch.js +1 -1
- package/dist/features/ai/components/SettingsAiTab.js +1 -1
- package/dist/features/ai/components/SettingsModelProfiles.js +1 -1
- package/dist/features/ai/document-chat.d.ts +3 -2
- package/dist/features/ai/document-chat.js +16 -19
- package/dist/features/ai/document-chat.js.map +1 -1
- package/dist/features/ai/index.d.ts +5 -3
- package/dist/features/ai/index.js +4 -2
- package/dist/features/ai/protocol/agent-ops.d.ts +22 -17
- package/dist/features/ai/protocol/agent-ops.js +65 -39
- package/dist/features/ai/protocol/agent-ops.js.map +1 -1
- package/dist/features/ai/protocol/block-lines.d.ts +89 -0
- package/dist/features/ai/protocol/block-lines.js +177 -0
- package/dist/features/ai/protocol/block-lines.js.map +1 -0
- package/dist/features/ai/protocol/chat.js +10 -10
- package/dist/features/ai/protocol/chat.js.map +1 -1
- package/dist/features/ai/protocol/index.d.ts +3 -2
- package/dist/features/ai/protocol/index.js +3 -2
- package/dist/features/ai/react/AiSelectionMenu.d.ts +3 -2
- package/dist/features/ai/react/AiSelectionMenu.js +64 -255
- package/dist/features/ai/react/AiSelectionMenu.js.map +1 -1
- package/dist/features/ai/react/AiSidebar.js +2 -2
- package/dist/features/ai/react/DocumentChatAnswer.js +1 -1
- package/dist/features/ai/react/DocumentChatOps.js +1 -0
- package/dist/features/ai/react/DocumentChatOps.js.map +1 -1
- package/dist/features/ai/react/ModelQuickSwitch.js +1 -1
- package/dist/features/ai/react/SettingsAiTab.js +1 -1
- package/dist/features/ai/react/SettingsModelProfiles.js +1 -1
- package/dist/features/ai/react/use-ai-selection-flow.js +34 -0
- package/dist/features/ai/react/use-ai-selection-flow.js.map +1 -0
- package/dist/features/ai/selection-flow.d.ts +133 -0
- package/dist/features/ai/selection-flow.js +283 -0
- package/dist/features/ai/selection-flow.js.map +1 -0
- package/dist/features/ai/use-ai-assistant.d.ts +1 -1
- package/dist/features/ai/use-document-chat.d.ts +1 -1
- package/dist/features/authors/components/AuthorColorsPanel.js +1 -1
- package/dist/features/authors/feature.js +1 -1
- package/dist/features/authors/index.js +1 -1
- package/dist/features/authors/react/AuthorColorsPanel.js +1 -1
- package/dist/features/comments/components/CommentBubble.js +1 -1
- package/dist/features/comments/components/CommentComposer.js +1 -1
- package/dist/features/comments/components/CommentThreadView.js +5 -2
- package/dist/features/comments/components/CommentThreadView.js.map +1 -1
- package/dist/features/comments/components/CommentsMargin.js +108 -230
- package/dist/features/comments/components/CommentsMargin.js.map +1 -1
- package/dist/features/comments/components/CommentsOverlay.js +1 -1
- package/dist/features/comments/feature.js +1 -1
- package/dist/features/comments/index.d.ts +4 -2
- package/dist/features/comments/index.js +4 -2
- package/dist/features/comments/margin-controller.d.ts +44 -0
- package/dist/features/comments/margin-controller.js +132 -0
- package/dist/features/comments/margin-controller.js.map +1 -0
- package/dist/features/comments/margin-layout.d.ts +55 -0
- package/dist/features/comments/margin-layout.js +79 -0
- package/dist/features/comments/margin-layout.js.map +1 -0
- package/dist/features/comments/model.d.ts +8 -0
- package/dist/features/comments/model.js +60 -34
- package/dist/features/comments/model.js.map +1 -1
- package/dist/features/comments/react/CommentBubble.js +1 -1
- package/dist/features/comments/react/CommentComposer.js +1 -1
- package/dist/features/comments/react/CommentThreadView.js +2 -2
- package/dist/features/comments/react/CommentThreadView.js.map +1 -1
- package/dist/features/comments/react/CommentsMargin.d.ts +0 -5
- package/dist/features/comments/react/CommentsMargin.js +85 -251
- package/dist/features/comments/react/CommentsMargin.js.map +1 -1
- package/dist/features/comments/react/CommentsOverlay.js +1 -1
- package/dist/features/comments/react/use-comments.js +1 -1
- package/dist/features/comments/react/use-margin-layout.js +45 -0
- package/dist/features/comments/react/use-margin-layout.js.map +1 -0
- package/dist/features/comments/react.d.ts +1 -1
- package/dist/features/comments/schema.d.ts +23 -0
- package/dist/features/comments/schema.js.map +1 -1
- package/dist/features/comments/snapshot-equality.js +40 -0
- package/dist/features/comments/snapshot-equality.js.map +1 -0
- package/dist/features/comments/thread-anchor.js +33 -0
- package/dist/features/comments/thread-anchor.js.map +1 -0
- package/dist/features/comments/use-comments.d.ts +3 -1
- package/dist/features/comments/use-comments.js +1 -0
- package/dist/features/comments/use-comments.js.map +1 -1
- package/dist/features/comments/use-margin-layout.js +29 -0
- package/dist/features/comments/use-margin-layout.js.map +1 -0
- package/dist/features/comments/vue.d.ts +1 -1
- package/dist/features/composites/accepted-children.d.ts +21 -0
- package/dist/features/composites/accepted-children.js +126 -0
- package/dist/features/composites/accepted-children.js.map +1 -0
- package/dist/features/composites/components/CompositeDetails.js +1 -1
- package/dist/features/composites/components/CompositeInspector.js +38 -164
- package/dist/features/composites/components/CompositeInspector.js.map +1 -1
- package/dist/features/composites/feature.d.ts +2 -1
- package/dist/features/composites/feature.js +8 -2
- package/dist/features/composites/feature.js.map +1 -1
- package/dist/features/composites/index.d.ts +5 -3
- package/dist/features/composites/index.js +8 -6
- package/dist/features/composites/inspector-model.d.ts +97 -0
- package/dist/features/composites/inspector-model.js +255 -0
- package/dist/features/composites/inspector-model.js.map +1 -0
- package/dist/features/composites/kinds.d.ts +11 -0
- package/dist/features/composites/kinds.js +17 -2
- package/dist/features/composites/kinds.js.map +1 -1
- package/dist/features/composites/outline.d.ts +18 -5
- package/dist/features/composites/outline.js +44 -12
- package/dist/features/composites/outline.js.map +1 -1
- package/dist/features/composites/react/CompositeDetails.js +1 -1
- package/dist/features/composites/react/CompositeInspector.d.ts +1 -1
- package/dist/features/composites/react/CompositeInspector.js +21 -180
- package/dist/features/composites/react/CompositeInspector.js.map +1 -1
- package/dist/features/composites/react/CompositeStatusMenu.d.ts +1 -1
- package/dist/features/composites/react/CompositeView.js +1 -1
- package/dist/features/composites/react/use-composite-inspector.js +29 -0
- package/dist/features/composites/react/use-composite-inspector.js.map +1 -0
- package/dist/features/composites/schema.d.ts +2 -2
- package/dist/features/composites/schema.js +2 -2
- package/dist/features/composites/schema.js.map +1 -1
- package/dist/features/composites/use-composite-anchors.d.ts +1 -1
- package/dist/features/composites/use-composite-inspector.js +41 -0
- package/dist/features/composites/use-composite-inspector.js.map +1 -0
- package/dist/features/composites/use-composite-rows.d.ts +1 -1
- package/dist/features/media/index.d.ts +1 -1
- package/dist/features/media/vue.d.ts +1 -1
- package/dist/features/mentions/index.d.ts +3 -3
- package/dist/features/mentions/index.js +3 -3
- package/dist/features/mentions/schema.d.ts +1 -1
- package/dist/features/mentions/schema.js +1 -1
- package/dist/features/themes/index.d.ts +1 -1
- package/dist/features/themes/index.js +1 -1
- package/dist/features/transcript/feature.js +1 -1
- package/dist/features/transcript/index.js +2 -2
- package/dist/i18n/de/ai.js +2 -0
- package/dist/i18n/de/ai.js.map +1 -1
- package/dist/i18n/de/common.js +1 -2
- package/dist/i18n/de/common.js.map +1 -1
- package/dist/i18n/de/composites.js +8 -1
- package/dist/i18n/de/composites.js.map +1 -1
- package/dist/i18n/de/tree.js +14 -1
- package/dist/i18n/de/tree.js.map +1 -1
- package/dist/i18n/de.d.ts +22 -1
- package/dist/i18n/en/ai.js +2 -0
- package/dist/i18n/en/ai.js.map +1 -1
- package/dist/i18n/en/common.js +1 -2
- package/dist/i18n/en/common.js.map +1 -1
- package/dist/i18n/en/composites.js +8 -1
- package/dist/i18n/en/composites.js.map +1 -1
- package/dist/i18n/en/tree.js +14 -1
- package/dist/i18n/en/tree.js.map +1 -1
- package/dist/integrations/composite-ai/composite-fill.js +2 -2
- package/dist/integrations/composite-ai/composite-summary.js +2 -2
- package/dist/integrations/composite-ai/index.js +2 -2
- package/dist/integrations/transcript-ai/transcript-cleanup.js +1 -1
- package/dist/markdown/MarkdownSurface.d.ts +1 -1
- package/dist/markdown/MarkdownSurface.js +2 -2
- package/dist/markdown/MarkdownSurface.vue.d.ts +7 -7
- package/dist/markdown/index.d.ts +2 -2
- package/dist/nuxt/module.d.ts +0 -6
- package/dist/nuxt/module.js +3 -5
- package/dist/nuxt/module.js.map +1 -1
- package/dist/nuxt/runtime/install-context.d.ts +2 -3
- package/dist/nuxt/runtime/install-context.js +2 -3
- package/dist/nuxt/runtime/install-context.js.map +1 -1
- package/dist/nuxt/runtime/plugins/editor-context-nuxt-ui.client.ts +1 -1
- package/dist/nuxt/runtime/server/routes/collab/[id].js +1 -1
- package/dist/react/EditorSurface.d.ts +1 -1
- package/dist/react/EditorSurface.js +43 -149
- package/dist/react/EditorSurface.js.map +1 -1
- package/dist/react/EditorToolbar.js +3 -40
- package/dist/react/EditorToolbar.js.map +1 -1
- package/dist/react/styles/ui-vars.css +2 -2
- package/dist/schema/advanced.js +2 -2
- package/dist/server/adapter.d.ts +2 -2
- package/dist/server/collab.js +1 -1
- package/dist/server/handlers/ai.d.ts +4 -2
- package/dist/server/handlers/ai.js +11 -9
- package/dist/server/handlers/ai.js.map +1 -1
- package/dist/server/handlers/context.d.ts +1 -1
- package/dist/server/handlers/editor.d.ts +1 -1
- package/dist/server/handlers/editor.js +2 -2
- package/dist/server/host.d.ts +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/settings/index.d.ts +1 -1
- package/dist/styles/ai.css +1 -1
- package/dist/styles/authors.css +1 -1
- package/dist/styles/block-ui.css +1 -1
- package/dist/styles/collab.css +1 -1
- package/dist/styles/comments.css +1 -1
- package/dist/styles/core.css +2 -2
- package/dist/styles/media.css +1 -1
- package/dist/styles/mentions.css +1 -1
- package/dist/styles/themes.css +1 -1
- package/dist/styles/tokens.css +1 -1
- package/dist/styles/transcript.css +1 -1
- package/dist/tree/DocumentTree.js +159 -387
- package/dist/tree/DocumentTree.js.map +1 -1
- package/dist/tree/drop-rules.js +37 -0
- package/dist/tree/drop-rules.js.map +1 -0
- package/dist/tree/react/DocumentTree.d.ts +2 -2
- package/dist/tree/react/DocumentTree.js +135 -397
- package/dist/tree/react/DocumentTree.js.map +1 -1
- package/dist/tree/react/use-tree-model.js +129 -0
- package/dist/tree/react/use-tree-model.js.map +1 -0
- package/dist/tree/react.js +1 -1
- package/dist/tree/tree-host.js +100 -0
- package/dist/tree/tree-host.js.map +1 -0
- package/dist/tree/tree-items.js +22 -43
- package/dist/tree/tree-items.js.map +1 -1
- package/dist/tree/tree-model.js +580 -0
- package/dist/tree/tree-model.js.map +1 -0
- package/dist/tree/tree-rows.js +77 -0
- package/dist/tree/tree-rows.js.map +1 -0
- package/dist/tree/use-tree-model.js +94 -0
- package/dist/tree/use-tree-model.js.map +1 -0
- package/dist/tree/vue.js +1 -1
- package/dist/vue/AdvancedEditor.vue.d.ts +4 -4
- package/dist/vue/SimpleEditor.vue.d.ts +2 -2
- package/dist/vue/components/EditorSurface.js +27 -87
- package/dist/vue/components/EditorSurface.js.map +1 -1
- package/dist/vue/components/EditorSurface.vue.d.ts +4 -4
- package/dist/vue/components/EditorToolbar.js +7 -44
- package/dist/vue/components/EditorToolbar.js.map +1 -1
- package/dist/vue/context.d.ts +4 -4
- package/dist/vue/context.js +3 -2
- package/dist/vue/context.js.map +1 -1
- package/dist/vue/kit/types.d.ts +1 -1
- package/dist/vue/kit/use-ui-kit.d.ts +2 -2
- package/dist/vue/kit/use-ui-kit.js +3 -3
- package/dist/vue/kit/use-ui-kit.js.map +1 -1
- package/dist/vue/use-editor-ui.d.ts +1 -1
- package/dist/vue/use-notifier.d.ts +1 -1
- package/docs/integration.md +98 -5
- package/docs/llms.md +10 -2
- package/docs/usage.md +10 -5
- package/package.json +70 -23
- package/dist/nuxt/runtime/plugins/editor-context-primevue.client.ts +0 -8
- package/dist/vue/kit/primevue/Alert.js +0 -49
- package/dist/vue/kit/primevue/Alert.js.map +0 -1
- package/dist/vue/kit/primevue/Avatar.js +0 -54
- package/dist/vue/kit/primevue/Avatar.js.map +0 -1
- package/dist/vue/kit/primevue/Badge.js +0 -41
- package/dist/vue/kit/primevue/Badge.js.map +0 -1
- package/dist/vue/kit/primevue/Button.js +0 -112
- package/dist/vue/kit/primevue/Button.js.map +0 -1
- package/dist/vue/kit/primevue/Chip.js +0 -38
- package/dist/vue/kit/primevue/Chip.js.map +0 -1
- package/dist/vue/kit/primevue/DropdownMenu.js +0 -98
- package/dist/vue/kit/primevue/DropdownMenu.js.map +0 -1
- package/dist/vue/kit/primevue/FieldGroup.js +0 -22
- package/dist/vue/kit/primevue/FieldGroup.js.map +0 -1
- package/dist/vue/kit/primevue/FormField.js +0 -56
- package/dist/vue/kit/primevue/FormField.js.map +0 -1
- package/dist/vue/kit/primevue/Icon.js +0 -23
- package/dist/vue/kit/primevue/Icon.js.map +0 -1
- package/dist/vue/kit/primevue/Input.js +0 -96
- package/dist/vue/kit/primevue/Input.js.map +0 -1
- package/dist/vue/kit/primevue/InputMenu.js +0 -122
- package/dist/vue/kit/primevue/InputMenu.js.map +0 -1
- package/dist/vue/kit/primevue/Modal.js +0 -59
- package/dist/vue/kit/primevue/Modal.js.map +0 -1
- package/dist/vue/kit/primevue/Popover.js +0 -135
- package/dist/vue/kit/primevue/Popover.js.map +0 -1
- package/dist/vue/kit/primevue/RadioGroup.js +0 -58
- package/dist/vue/kit/primevue/RadioGroup.js.map +0 -1
- package/dist/vue/kit/primevue/Select.js +0 -68
- package/dist/vue/kit/primevue/Select.js.map +0 -1
- package/dist/vue/kit/primevue/SelectMenu.js +0 -94
- package/dist/vue/kit/primevue/SelectMenu.js.map +0 -1
- package/dist/vue/kit/primevue/Separator.js +0 -16
- package/dist/vue/kit/primevue/Separator.js.map +0 -1
- package/dist/vue/kit/primevue/Switch.js +0 -46
- package/dist/vue/kit/primevue/Switch.js.map +0 -1
- package/dist/vue/kit/primevue/Table.js +0 -54
- package/dist/vue/kit/primevue/Table.js.map +0 -1
- package/dist/vue/kit/primevue/Tabs.js +0 -49
- package/dist/vue/kit/primevue/Tabs.js.map +0 -1
- package/dist/vue/kit/primevue/Textarea.js +0 -55
- package/dist/vue/kit/primevue/Textarea.js.map +0 -1
- package/dist/vue/kit/primevue/Tooltip.js +0 -43
- package/dist/vue/kit/primevue/Tooltip.js.map +0 -1
- package/dist/vue/kit/primevue/Tree.js +0 -75
- package/dist/vue/kit/primevue/Tree.js.map +0 -1
- package/dist/vue/kit/primevue/select-options.js +0 -55
- package/dist/vue/kit/primevue/select-options.js.map +0 -1
- package/dist/vue/kit/primevue/shared.js +0 -49
- package/dist/vue/kit/primevue/shared.js.map +0 -1
- package/dist/vue/kit/primevue/trigger.js +0 -25
- package/dist/vue/kit/primevue/trigger.js.map +0 -1
- package/dist/vue/kit/primevue.css +0 -86
- package/dist/vue/kit/primevue.d.ts +0 -33
- package/dist/vue/kit/primevue.js +0 -96
- package/dist/vue/kit/primevue.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nitro.js","names":[],"sources":["../../src/adapters/nitro.ts"],"sourcesContent":["import { type AiModels, type AiModelsConfig, type CollabConnection, type CollabServer, type CollabServerOptions, createAiModels, createCollabServer, createRateLimiter, type EditorAdapter, HttpError, type RateLimiterOptions } from '../server'\nimport {\n type AiHandlerContext,\n assetGet,\n assetPost,\n chatPost,\n completePost,\n generatePost,\n type HandlerContext,\n type HandlerRequest,\n type HandlerResponse,\n modelsGet,\n settingsGet,\n settingsPut,\n tagsGet,\n themeDelete,\n themeGet,\n themePost,\n themesGet,\n transcribePost,\n transcriptCleanupPost,\n usersGet,\n} from '../server/handlers'\nimport { createError, defineEventHandler, defineWebSocketHandler, getQuery, getRouterParams, type H3Event, readBody, readFormData, type Router, setResponseHeaders, setResponseStatus } from 'h3'\nimport type { DocumentAdmin } from './shared'\nimport type { CommentStore } from './comments/types'\nimport { AI_API, COLLAB_PATH, COMMENTS_ROUTE, DOCUMENT_ROUTE, EDITOR_API } from './routes'\nimport { commentsGet, type CommentsRouteContext, commentsPost, type DocumentsContext, documentGet, documentPut } from './documents'\n\n// The editor backend on a plain Nitro app (nitropack 2, no Nuxt module): the framework's own routes (settings, users,\n// tags, assets, themes, the optional AI endpoints), the collaboration websocket and this package's document and comments\n// routes, registered in one call from a Nitro plugin. It is the explicit-wiring twin of `@ecsplico/editor/nuxt`, which\n// registers the same routes by scanning files; both end in the same `@ecsplico/editor/server/handlers`.\n\n/** The part of a Nitro app this entry uses; `nitroApp` of a Nitro plugin fits. */\nexport interface NitroHost {\n router: Pick<Router, 'add' | 'use'>\n hooks: { hook(name: 'close', listener: () => void | Promise<void>): unknown }\n}\n\nexport interface EditorServerOptions {\n /** Storage and identity. An adapter that also manages documents (`json`, `drizzle`) lets `PUT /api/editor/documents/:id` create them. */\n adapter: EditorAdapter & Partial<DocumentAdmin>\n /**\n * The collaboration websocket (`/collab/:id`). Extensions, hooks and Hocuspocus configuration of `createCollabServer`;\n * `{}` takes everything from the adapter. `false` mounts no websocket (documents then only load and save over HTTP).\n * Needs `experimental: { websocket: true }` in the Nitro config.\n */\n collab: Omit<CollabServerOptions, 'adapter'> | false\n /**\n * Mounts the AI endpoints (`/api/ai/*`): presets, shared key, transcription, slots of `createAiModels`; `languageModel`\n * and `listModels` default to the adapter's. Omitted: no AI routes.\n */\n ai?: Partial<AiModelsConfig>\n /** Per-user limit of the AI endpoints; default 30 requests per minute. */\n rateLimit?: RateLimiterOptions\n /** Mounts the comments endpoint (`/api/editor/comments/:id`) over this store; omitted: no comments routes. */\n comments?: { store: CommentStore }\n}\n\nexport interface EditorServer {\n adapter: EditorServerOptions['adapter']\n /** The collaboration server, `undefined` with `collab: false`. */\n collab: CollabServer | undefined\n /** The AI models, `undefined` without `ai`. */\n models: AiModels | undefined\n /** Writes pending collaboration stores and disconnects every client. Nitro's `close` hook calls it. */\n close(): Promise<void>\n}\n\ntype Method = 'get' | 'post' | 'put' | 'delete'\ntype Handler<C> = (ctx: C, request: HandlerRequest) => Promise<HandlerResponse>\n\nconst API = EDITOR_API\n\n/** The `HandlerRequest` of a Nitro event. `signal` aborts when the client goes away. */\nfunction requestOf(event: H3Event): HandlerRequest {\n const abort = new AbortController()\n event.node.res.once('close', () => abort.abort())\n return {\n headers: event.headers,\n params: getRouterParams(event, { decode: true }),\n query: Object.fromEntries(Object.entries(getQuery(event)).map(([key, value]) => [key, value === undefined ? undefined : String(value)])),\n readJson: () => readBody(event),\n readForm: () => readFormData(event),\n signal: abort.signal,\n }\n}\n\n/** An event handler that runs `handler` with `ctx` and sends its `HandlerResponse`; an `HttpError` becomes Nitro's error response. */\nfunction routeOf<C>(ctx: C, handler: Handler<C>) {\n return defineEventHandler(async (event) => {\n let response: HandlerResponse\n try {\n response = await handler(ctx, requestOf(event))\n }\n catch (error) {\n if (!(error instanceof HttpError)) throw error\n setResponseHeaders(event, error.headers)\n throw createError({ statusCode: error.statusCode, statusMessage: error.statusMessage, message: error.statusMessage, data: error.data })\n }\n setResponseStatus(event, response.status)\n setResponseHeaders(event, response.headers)\n // `null` ends the response without a body and keeps the status set above.\n return response.body\n })\n}\n\nconst editorRoutes: [Method, string, Handler<HandlerContext>][] = [\n ['get', `${API}/settings`, settingsGet],\n ['put', `${API}/settings`, settingsPut],\n ['get', `${API}/users`, usersGet],\n ['get', `${API}/tags`, tagsGet],\n ['post', `${API}/assets/:id`, assetPost],\n ['get', `${API}/assets/:id/:name`, assetGet],\n ['get', `${API}/themes`, themesGet],\n ['post', `${API}/themes`, themePost],\n ['get', `${API}/themes/:id`, themeGet],\n ['delete', `${API}/themes/:id`, themeDelete],\n]\n\nconst aiRoutes: [Method, string, Handler<AiHandlerContext>][] = [\n ['get', `${AI_API}/models`, modelsGet],\n ['post', `${AI_API}/chat`, chatPost],\n ['post', `${AI_API}/complete`, completePost],\n ['post', `${AI_API}/generate`, generatePost],\n ['post', `${AI_API}/transcribe`, transcribePost],\n ['post', `${AI_API}/transcript-cleanup`, transcriptCleanupPost],\n]\n\ntype CollabPeer = { _hocuspocus?: CollabConnection }\n\nconst collabRoute = (collab: CollabServer) => defineWebSocketHandler({\n open(peer) {\n ;(peer as unknown as CollabPeer)._hocuspocus = collab.handleConnection(peer.websocket as unknown as Parameters<CollabServer['handleConnection']>[0], peer.request as Request)\n },\n message(peer, message) {\n ;(peer as unknown as CollabPeer)._hocuspocus?.handleMessage(message.uint8Array())\n },\n close(peer, details) {\n ;(peer as unknown as CollabPeer)._hocuspocus?.handleClose({ code: details.code ?? 1000, reason: details.reason ?? '' } as CloseEvent)\n },\n error(peer, error) {\n console.error('[collab]', peer.id, error)\n },\n})\n\n/**\n * Wires the editor backend into `nitroApp` (call it from a Nitro plugin):\n *\n * /api/editor/{settings,users,tags,assets/…,themes/…} the editor's own endpoints\n * /api/editor/documents/:id GET/PUT a document's TipTap JSON (what `useEditorDocument` uses)\n * /api/editor/comments/:id GET/POST comments, with `comments`\n * /api/ai/{models,chat,complete,generate,transcribe,transcript-cleanup} with `ai`\n * /collab/:id the collaboration websocket, unless `collab: false`\n *\n * Every route authenticates through `adapter.authenticate`. The collaboration server is closed with Nitro's `close`\n * hook. The paths are the ones `@ecsplico/editor/nuxt` serves, so the editor's client works unchanged.\n */\nexport function installEditorServer(nitroApp: NitroHost, options: EditorServerOptions): EditorServer {\n const { adapter } = options\n const collab = options.collab === false ? undefined : createCollabServer({ adapter, ...options.collab })\n const models = options.ai && createAiModels({\n presets: {},\n languageModel: adapter.languageModel && ((ref, user) => adapter.languageModel!(ref, user)),\n listModels: adapter.listModels && (user => adapter.listModels!(user)),\n ...options.ai,\n })\n\n const context: DocumentsContext = { adapter, ...(collab && { collab }) }\n for (const [method, path, handler] of editorRoutes) nitroApp.router.add(path, routeOf<HandlerContext>(context, handler), method)\n nitroApp.router.add(DOCUMENT_ROUTE, routeOf(context, documentGet), 'get')\n nitroApp.router.add(DOCUMENT_ROUTE, routeOf(context, documentPut), 'put')\n\n if (options.comments) {\n const comments: CommentsRouteContext = { ...context, store: options.comments.store }\n nitroApp.router.add(COMMENTS_ROUTE, routeOf(comments, commentsGet), 'get')\n nitroApp.router.add(COMMENTS_ROUTE, routeOf(comments, commentsPost), 'post')\n }\n if (models) {\n const ai: AiHandlerContext = { ...context, ai: models, limiter: createRateLimiter(options.rateLimit ?? { max: 30, windowMs: 60_000 }) }\n for (const [method, path, handler] of aiRoutes) nitroApp.router.add(path, routeOf(ai, handler), method)\n }\n // No method: the websocket upgrade resolves its handler by path only.\n if (collab) nitroApp.router.use(`${COLLAB_PATH}/:id`, collabRoute(collab))\n\n const server: EditorServer = { adapter, collab, models, close: async () => { await collab?.close() } }\n nitroApp.hooks.hook('close', () => server.close())\n return server\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/adapters/routes.ts
|
|
2
|
+
const EDITOR_API = "/api/editor";
|
|
3
|
+
const AI_API = "/api/ai";
|
|
4
|
+
const COLLAB_PATH = "/collab";
|
|
5
|
+
/** Route pattern of a document's content (`GET` → TipTap JSON, `PUT` → stores it). */
|
|
6
|
+
const DOCUMENT_ROUTE = `${EDITOR_API}/documents/:id`;
|
|
7
|
+
/** Route pattern of a document's comments endpoint (`handleCommentsRequest`). */
|
|
8
|
+
const COMMENTS_ROUTE = `${EDITOR_API}/comments/:id`;
|
|
9
|
+
/** The content URL of document `id`. */
|
|
10
|
+
const documentUrl = (id) => DOCUMENT_ROUTE.replace(":id", encodeURIComponent(id));
|
|
11
|
+
/** The comments URL of document `id`. */
|
|
12
|
+
const commentsUrl = (id) => COMMENTS_ROUTE.replace(":id", encodeURIComponent(id));
|
|
13
|
+
//#endregion
|
|
14
|
+
export { AI_API, COLLAB_PATH, COMMENTS_ROUTE, DOCUMENT_ROUTE, EDITOR_API, commentsUrl, documentUrl };
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","names":[],"sources":["../../src/adapters/routes.ts"],"sourcesContent":["// The wire paths shared by the Nitro host (`./server/nitro`) and the Vue composable (`./vue`). The editor's own\n// client calls the `/api/editor/*` and `/api/ai/*` paths of `@ecsplico/editor`; the two document paths below are this\n// package's contract between the two entries. Pure strings: this module imports nothing, so neither entry drags the\n// other's framework in.\n\nexport const EDITOR_API = '/api/editor'\nexport const AI_API = '/api/ai'\nexport const COLLAB_PATH = '/collab'\n\n/** Route pattern of a document's content (`GET` → TipTap JSON, `PUT` → stores it). */\nexport const DOCUMENT_ROUTE = `${EDITOR_API}/documents/:id`\n/** Route pattern of a document's comments endpoint (`handleCommentsRequest`). */\nexport const COMMENTS_ROUTE = `${EDITOR_API}/comments/:id`\n\n/** The content URL of document `id`. */\nexport const documentUrl = (id: string) => DOCUMENT_ROUTE.replace(':id', encodeURIComponent(id))\n/** The comments URL of document `id`. */\nexport const commentsUrl = (id: string) => COMMENTS_ROUTE.replace(':id', encodeURIComponent(id))\n"],"mappings":";AAKA,MAAa,aAAa;AAC1B,MAAa,SAAS;AACtB,MAAa,cAAc;;AAG3B,MAAa,iBAAiB,GAAG,WAAW;;AAE5C,MAAa,iBAAiB,GAAG,WAAW;;AAG5C,MAAa,eAAe,OAAe,eAAe,QAAQ,OAAO,mBAAmB,EAAE,CAAC;;AAE/F,MAAa,eAAe,OAAe,eAAe,QAAQ,OAAO,mBAAmB,EAAE,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { MentionUser } from "../features/mentions/mention.js";
|
|
2
|
+
import "../features/mentions/schema.js";
|
|
3
|
+
import { EditorAdapter } from "../server/adapter.js";
|
|
4
|
+
import "../server/index.js";
|
|
5
|
+
import { JSONContent } from "@tiptap/core";
|
|
6
|
+
//#region src/adapters/shared.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* The identity half of every adapter in this package: who the user is, which documents they may open, which host
|
|
9
|
+
* nodes the collaboration schema needs and who can be `@`-mentioned. Storage is the adapter's own business.
|
|
10
|
+
*/
|
|
11
|
+
export interface AdapterHooks {
|
|
12
|
+
/**
|
|
13
|
+
* The user behind a request, `null` = not authenticated (see `EditorAdapter.authenticate`). Default: an anonymous
|
|
14
|
+
* development user outside `NODE_ENV=production`, `null` (everyone refused) in production, so a deployment never
|
|
15
|
+
* serves documents to the world by omission.
|
|
16
|
+
*/
|
|
17
|
+
authenticate?: EditorAdapter['authenticate'];
|
|
18
|
+
/** Per-document access for the collaboration websocket (see `EditorAdapter.authorize`). Omitted: every authenticated user edits every document. */
|
|
19
|
+
authorize?: EditorAdapter['authorize'];
|
|
20
|
+
/** Vue-free TipTap extensions of host-defined nodes and marks for the collaboration schema (see `EditorAdapter.schemaExtensions`). */
|
|
21
|
+
schemaExtensions?: EditorAdapter['schemaExtensions'];
|
|
22
|
+
/** Users that can be `@`-mentioned (`GET /api/editor/users`); no credentials. Default: none. */
|
|
23
|
+
users?: () => MentionUser[] | Promise<MentionUser[]>;
|
|
24
|
+
}
|
|
25
|
+
/** Document management a host needs next to the editor's own storage methods: the adapter contract cannot create documents (`storeContent` only updates). */
|
|
26
|
+
export interface DocumentAdmin {
|
|
27
|
+
/** Creates the document or replaces its content; the content version changes. */
|
|
28
|
+
putDocument(id: string, content: JSONContent): Promise<void>;
|
|
29
|
+
/** Removes the content, the Yjs state and the assets of a document; unknown ids are fine. Call `forgetCollabDocument(id)` first when it may be open. */
|
|
30
|
+
deleteDocument(id: string): Promise<void>;
|
|
31
|
+
/** Ids of all stored documents. */
|
|
32
|
+
listDocumentIds(): Promise<string[]>;
|
|
33
|
+
}
|
|
34
|
+
/** An adapter that stores documents itself (`json`, `drizzle`): the full contract including versioning, settings and document management. */
|
|
35
|
+
export type StoredEditorAdapter = EditorAdapter & Required<Pick<EditorAdapter, 'contentVersion' | 'loadSettings' | 'storeSettings'>> & DocumentAdmin;
|
|
36
|
+
//#endregion
|
|
37
|
+
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/adapters/shared.ts
|
|
2
|
+
const DEV_USER = {
|
|
3
|
+
id: "dev",
|
|
4
|
+
username: "dev",
|
|
5
|
+
color: "#6366f1"
|
|
6
|
+
};
|
|
7
|
+
const authenticateDefault = async () => process.env.NODE_ENV === "production" ? null : { ...DEV_USER };
|
|
8
|
+
/** `hooks` as adapter members; optional hooks are only present when given (the collab server tests for them). */
|
|
9
|
+
function identityPart(hooks) {
|
|
10
|
+
const { authenticate = authenticateDefault, authorize, schemaExtensions, users } = hooks;
|
|
11
|
+
return {
|
|
12
|
+
authenticate: (request) => authenticate(request),
|
|
13
|
+
listUsers: async () => await users?.() ?? [],
|
|
14
|
+
...authorize && { authorize: (user, id) => authorize(user, id) },
|
|
15
|
+
...schemaExtensions && { schemaExtensions: () => schemaExtensions() }
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** The key per-user settings are stored under: the user's `id`, else its `username`. */
|
|
19
|
+
function settingsKey(user) {
|
|
20
|
+
const { id, username } = user;
|
|
21
|
+
for (const candidate of [id, username]) {
|
|
22
|
+
if (typeof candidate === "string" && candidate !== "") return candidate;
|
|
23
|
+
if (typeof candidate === "number") return String(candidate);
|
|
24
|
+
}
|
|
25
|
+
throw new TypeError("@ecsplico/editor: the authenticated user has neither an `id` nor a `username`, so its settings have no key");
|
|
26
|
+
}
|
|
27
|
+
/** An independent copy of the bytes (a driver's `Buffer` may view a larger pool), as the plain `Uint8Array` the adapter contract names. */
|
|
28
|
+
const bytesOf = (data) => new Uint8Array(data);
|
|
29
|
+
//#endregion
|
|
30
|
+
export { bytesOf, identityPart, settingsKey };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.js","names":[],"sources":["../../src/adapters/shared.ts"],"sourcesContent":["import type { JSONContent } from '@tiptap/core'\nimport type { MentionUser } from '../features/mentions/schema'\nimport type { EditorAdapter } from '../server'\n\n/**\n * The identity half of every adapter in this package: who the user is, which documents they may open, which host\n * nodes the collaboration schema needs and who can be `@`-mentioned. Storage is the adapter's own business.\n */\nexport interface AdapterHooks {\n /**\n * The user behind a request, `null` = not authenticated (see `EditorAdapter.authenticate`). Default: an anonymous\n * development user outside `NODE_ENV=production`, `null` (everyone refused) in production, so a deployment never\n * serves documents to the world by omission.\n */\n authenticate?: EditorAdapter['authenticate']\n /** Per-document access for the collaboration websocket (see `EditorAdapter.authorize`). Omitted: every authenticated user edits every document. */\n authorize?: EditorAdapter['authorize']\n /** Vue-free TipTap extensions of host-defined nodes and marks for the collaboration schema (see `EditorAdapter.schemaExtensions`). */\n schemaExtensions?: EditorAdapter['schemaExtensions']\n /** Users that can be `@`-mentioned (`GET /api/editor/users`); no credentials. Default: none. */\n users?: () => MentionUser[] | Promise<MentionUser[]>\n}\n\n/** Document management a host needs next to the editor's own storage methods: the adapter contract cannot create documents (`storeContent` only updates). */\nexport interface DocumentAdmin {\n /** Creates the document or replaces its content; the content version changes. */\n putDocument(id: string, content: JSONContent): Promise<void>\n /** Removes the content, the Yjs state and the assets of a document; unknown ids are fine. Call `forgetCollabDocument(id)` first when it may be open. */\n deleteDocument(id: string): Promise<void>\n /** Ids of all stored documents. */\n listDocumentIds(): Promise<string[]>\n}\n\n/** An adapter that stores documents itself (`json`, `drizzle`): the full contract including versioning, settings and document management. */\nexport type StoredEditorAdapter = EditorAdapter\n & Required<Pick<EditorAdapter, 'contentVersion' | 'loadSettings' | 'storeSettings'>>\n & DocumentAdmin\n\nconst DEV_USER = { id: 'dev', username: 'dev', color: '#6366f1' } as const\n\nconst authenticateDefault: EditorAdapter['authenticate'] = async () => process.env.NODE_ENV === 'production' ? null : { ...DEV_USER }\n\n/** The members of `EditorAdapter` that `AdapterHooks` fill in. */\nexport type IdentityPart = Pick<EditorAdapter, 'authenticate' | 'listUsers'> & Partial<Pick<EditorAdapter, 'authorize' | 'schemaExtensions'>>\n\n/** `hooks` as adapter members; optional hooks are only present when given (the collab server tests for them). */\nexport function identityPart(hooks: AdapterHooks): IdentityPart {\n const { authenticate = authenticateDefault, authorize, schemaExtensions, users } = hooks\n return {\n authenticate: request => authenticate(request),\n listUsers: async () => (await users?.()) ?? [],\n ...(authorize && { authorize: (user: object, id: string) => authorize(user, id) }),\n ...(schemaExtensions && { schemaExtensions: () => schemaExtensions() }),\n }\n}\n\n/** The key per-user settings are stored under: the user's `id`, else its `username`. */\nexport function settingsKey(user: object): string {\n const { id, username } = user as { id?: unknown, username?: unknown }\n for (const candidate of [id, username]) {\n if (typeof candidate === 'string' && candidate !== '') return candidate\n if (typeof candidate === 'number') return String(candidate)\n }\n throw new TypeError('@ecsplico/editor: the authenticated user has neither an `id` nor a `username`, so its settings have no key')\n}\n\n/** An independent copy of the bytes (a driver's `Buffer` may view a larger pool), as the plain `Uint8Array` the adapter contract names. */\nexport const bytesOf = (data: Uint8Array): Uint8Array => new Uint8Array(data)\n"],"mappings":";AAsCA,MAAM,WAAW;CAAE,IAAI;CAAO,UAAU;CAAO,OAAO;AAAU;AAEhE,MAAM,sBAAqD,YAAY,QAAQ,IAAI,aAAa,eAAe,OAAO,EAAE,GAAG,SAAS;;AAMpI,SAAgB,aAAa,OAAmC;CAC9D,MAAM,EAAE,eAAe,qBAAqB,WAAW,kBAAkB,UAAU;CACnF,OAAO;EACL,eAAc,YAAW,aAAa,OAAO;EAC7C,WAAW,YAAa,MAAM,QAAQ,KAAM,CAAC;EAC7C,GAAI,aAAa,EAAE,YAAY,MAAc,OAAe,UAAU,MAAM,EAAE,EAAE;EAChF,GAAI,oBAAoB,EAAE,wBAAwB,iBAAiB,EAAE;CACvE;AACF;;AAGA,SAAgB,YAAY,MAAsB;CAChD,MAAM,EAAE,IAAI,aAAa;CACzB,KAAK,MAAM,aAAa,CAAC,IAAI,QAAQ,GAAG;EACtC,IAAI,OAAO,cAAc,YAAY,cAAc,IAAI,OAAO;EAC9D,IAAI,OAAO,cAAc,UAAU,OAAO,OAAO,SAAS;CAC5D;CACA,MAAM,IAAI,UAAU,4GAA4G;AAClI;;AAGA,MAAa,WAAW,SAAiC,IAAI,WAAW,IAAI"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { AuthorIdentity, CollaborationSession } from "../core/feature.js";
|
|
2
|
+
import "../core/index.js";
|
|
3
|
+
import { CommentsFetch, RestCommentsModel } from "./comments/client.js";
|
|
4
|
+
import { Editor, JSONContent } from "@tiptap/core";
|
|
5
|
+
import { ComputedRef, MaybeRefOrGetter, Ref, ShallowRef } from "vue";
|
|
6
|
+
//#region src/adapters/vue.d.ts
|
|
7
|
+
export interface EditorDocumentOptions {
|
|
8
|
+
/** How requests are sent: `globalThis.fetch`, or a wrapper that adds the base URL and credentials. Used for the content and the comments. */
|
|
9
|
+
fetch: CommentsFetch;
|
|
10
|
+
/**
|
|
11
|
+
* Live collaboration: the collaboration server's websocket URL (`ws://host/collab`; the document id is appended as the
|
|
12
|
+
* provider's `name`) and the optional token its `EditorAdapter.authenticate` receives. With it the Y.Doc is the document:
|
|
13
|
+
* the server loads and stores the content, so the composable neither fetches nor saves it (`modelValue` stays
|
|
14
|
+
* `undefined`, `dirty` stays `false`, `save()` resolves at once).
|
|
15
|
+
*/
|
|
16
|
+
collaboration?: {
|
|
17
|
+
url: string;
|
|
18
|
+
token?: string | (() => string) | (() => Promise<string>);
|
|
19
|
+
};
|
|
20
|
+
/** Who writes: labels remote carets and is the author of new comments; `null` or omitted = nobody (no comments). */
|
|
21
|
+
user?: AuthorIdentity | null | (() => AuthorIdentity | null);
|
|
22
|
+
/** Whether the user may comment now (a reader of a read-only document may be refused); default `true`. */
|
|
23
|
+
canComment?: boolean | (() => boolean);
|
|
24
|
+
}
|
|
25
|
+
/** The props and events of the editor surface (`SimpleEditor`, `AdvancedEditor`, `EditorSurface`) that `useEditorDocument` fills. */
|
|
26
|
+
export interface EditorDocumentBinding {
|
|
27
|
+
/** The document (standalone mode); `undefined` in collaboration mode and until it has loaded. */
|
|
28
|
+
modelValue: JSONContent | undefined;
|
|
29
|
+
'onUpdate:modelValue': (content: JSONContent) => void;
|
|
30
|
+
/** Hands the live editor to the comments model. */
|
|
31
|
+
onReady: (editor: Editor) => void;
|
|
32
|
+
collaboration: CollaborationSession | undefined;
|
|
33
|
+
}
|
|
34
|
+
export interface EditorDocument {
|
|
35
|
+
/** The document's TipTap JSON: the surface's `v-model`. `undefined` until loaded, and always in collaboration mode. */
|
|
36
|
+
modelValue: Ref<JSONContent | undefined>;
|
|
37
|
+
/** `true` once the surface can be mounted: the content has loaded (a missing document starts empty), or the collaboration session exists. Mount with `:key` = the id, so a changed id remounts it. */
|
|
38
|
+
ready: Ref<boolean>;
|
|
39
|
+
/** Edits since the content was loaded or last saved. */
|
|
40
|
+
dirty: ComputedRef<boolean>;
|
|
41
|
+
/** Stores the current content (`PUT`); several calls run one after another and an edit made while one is in flight stays dirty. Rejects with the server's refusal. */
|
|
42
|
+
save(): Promise<void>;
|
|
43
|
+
/** Fetches the content again and drops unsaved edits. */
|
|
44
|
+
reload(): Promise<void>;
|
|
45
|
+
/** The last load or save error, `null` while all is well. */
|
|
46
|
+
error: Ref<Error | null>;
|
|
47
|
+
/** The collaboration session for the surface's `collaboration` prop; `undefined` without the `collaboration` option. */
|
|
48
|
+
collaboration: ShallowRef<CollaborationSession | undefined>;
|
|
49
|
+
/** The comments model of this document (`commentsFeature({ model: () => comments.value })`), threads behind the host's comments endpoint. A new one per id. */
|
|
50
|
+
comments: Readonly<ShallowRef<RestCommentsModel>>;
|
|
51
|
+
/** The live editor, once the surface reported it. */
|
|
52
|
+
editor: ShallowRef<Editor | undefined>;
|
|
53
|
+
/** `v-bind` target for the surface: `<AdvancedEditor v-if="ready" :key="id" v-bind="bind" />`. */
|
|
54
|
+
bind: ComputedRef<EditorDocumentBinding>;
|
|
55
|
+
/** Closes the collaboration session and the comments model; runs with the effect scope that called `useEditorDocument`. */
|
|
56
|
+
dispose(): void;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Loads document `id` from `GET /api/editor/documents/<id>` (a `404` starts an empty document that the first `save()`
|
|
60
|
+
* creates), tracks edits, saves with `PUT`, and prepares the collaboration session and comments model for the editor
|
|
61
|
+
* surface. `id` may be reactive: a change loads the other document and replaces the session and the comments model.
|
|
62
|
+
* Call it in a component's `setup`; the collaboration session is created synchronously, so the surface mounts in the same tick.
|
|
63
|
+
*/
|
|
64
|
+
export declare function useEditorDocument(id: MaybeRefOrGetter<string>, options: EditorDocumentOptions): EditorDocument;
|
|
65
|
+
//#endregion
|
|
66
|
+
export type { CommentsFetch };
|
|
67
|
+
//# sourceMappingURL=vue.d.ts.map
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { commentsUrl, documentUrl } from "./routes.js";
|
|
2
|
+
import { createRestCommentsModel } from "./comments/client.js";
|
|
3
|
+
import * as Y from "yjs";
|
|
4
|
+
import { HocuspocusProvider } from "@hocuspocus/provider";
|
|
5
|
+
import { computed, getCurrentScope, onScopeDispose, ref, shallowRef, toValue, watch } from "vue";
|
|
6
|
+
//#region src/adapters/vue.ts
|
|
7
|
+
const EMPTY_DOCUMENT = {
|
|
8
|
+
type: "doc",
|
|
9
|
+
content: [{ type: "paragraph" }]
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The document as compared for "unsaved changes": without block ids. The editor gives every block of loaded content an
|
|
13
|
+
* id the moment it mounts (stored files have none yet); that is not an edit the user made, so it must not mark a
|
|
14
|
+
* freshly opened document dirty. Saving still writes the ids.
|
|
15
|
+
*/
|
|
16
|
+
const withoutBlockIds = (node) => {
|
|
17
|
+
const { attrs, content, ...rest } = node;
|
|
18
|
+
const kept = attrs && Object.fromEntries(Object.entries(attrs).filter(([name]) => name !== "id"));
|
|
19
|
+
return {
|
|
20
|
+
...rest,
|
|
21
|
+
...kept && Object.keys(kept).length > 0 && { attrs: kept },
|
|
22
|
+
...content && { content: content.map(withoutBlockIds) }
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
const fingerprint = (content) => JSON.stringify(withoutBlockIds(content));
|
|
26
|
+
/** Document ids are percent-encoded into one path segment; the collab provider's `name` is the plain id. */
|
|
27
|
+
function sessionOf(id, { url, token }, user) {
|
|
28
|
+
const ydoc = new Y.Doc();
|
|
29
|
+
const provider = new HocuspocusProvider({
|
|
30
|
+
url: `${url.replace(/\/+$/, "")}/${encodeURIComponent(id)}`,
|
|
31
|
+
name: id,
|
|
32
|
+
document: ydoc,
|
|
33
|
+
...token !== void 0 && { token }
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
session: {
|
|
37
|
+
ydoc,
|
|
38
|
+
provider,
|
|
39
|
+
...user && { user }
|
|
40
|
+
},
|
|
41
|
+
close() {
|
|
42
|
+
provider.destroy();
|
|
43
|
+
ydoc.destroy();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const errorOf = (cause) => cause instanceof Error ? cause : new Error(String(cause));
|
|
48
|
+
/**
|
|
49
|
+
* Loads document `id` from `GET /api/editor/documents/<id>` (a `404` starts an empty document that the first `save()`
|
|
50
|
+
* creates), tracks edits, saves with `PUT`, and prepares the collaboration session and comments model for the editor
|
|
51
|
+
* surface. `id` may be reactive: a change loads the other document and replaces the session and the comments model.
|
|
52
|
+
* Call it in a component's `setup`; the collaboration session is created synchronously, so the surface mounts in the same tick.
|
|
53
|
+
*/
|
|
54
|
+
function useEditorDocument(id, options) {
|
|
55
|
+
const userOf = () => toValue(options.user) ?? null;
|
|
56
|
+
const modelValue = ref();
|
|
57
|
+
const ready = ref(false);
|
|
58
|
+
const error = ref(null);
|
|
59
|
+
const collaboration = shallowRef();
|
|
60
|
+
const editor = shallowRef();
|
|
61
|
+
/** Fingerprint (`fingerprint`) of the JSON the server holds (the empty document while it does not exist), `null` before the first load. */
|
|
62
|
+
const savedJson = ref(null);
|
|
63
|
+
/** The document does not exist on the server yet: the first save creates it even if nothing was typed. */
|
|
64
|
+
let missing = false;
|
|
65
|
+
let closeSession;
|
|
66
|
+
let generation = 0;
|
|
67
|
+
let tail = Promise.resolve();
|
|
68
|
+
const commentsFor = (documentId) => createRestCommentsModel({
|
|
69
|
+
fetch: options.fetch,
|
|
70
|
+
documentId,
|
|
71
|
+
endpoint: commentsUrl(documentId),
|
|
72
|
+
user: userOf,
|
|
73
|
+
canComment: () => toValue(options.canComment) ?? true,
|
|
74
|
+
getEditor: () => editor.value,
|
|
75
|
+
onError: (cause) => {
|
|
76
|
+
error.value = cause;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
const comments = shallowRef(commentsFor(toValue(id)));
|
|
80
|
+
let commentsId = toValue(id);
|
|
81
|
+
async function load(documentId) {
|
|
82
|
+
const mine = ++generation;
|
|
83
|
+
ready.value = false;
|
|
84
|
+
error.value = null;
|
|
85
|
+
modelValue.value = void 0;
|
|
86
|
+
savedJson.value = null;
|
|
87
|
+
missing = false;
|
|
88
|
+
editor.value = void 0;
|
|
89
|
+
closeSession?.();
|
|
90
|
+
closeSession = void 0;
|
|
91
|
+
collaboration.value = void 0;
|
|
92
|
+
if (commentsId !== documentId) {
|
|
93
|
+
comments.value.dispose();
|
|
94
|
+
comments.value = commentsFor(documentId);
|
|
95
|
+
commentsId = documentId;
|
|
96
|
+
}
|
|
97
|
+
if (options.collaboration) {
|
|
98
|
+
const { session, close } = sessionOf(documentId, options.collaboration, userOf());
|
|
99
|
+
closeSession = close;
|
|
100
|
+
collaboration.value = session;
|
|
101
|
+
ready.value = true;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const response = await options.fetch(documentUrl(documentId));
|
|
106
|
+
if (mine !== generation) return;
|
|
107
|
+
if (response.status === 404) {
|
|
108
|
+
modelValue.value = structuredClone(EMPTY_DOCUMENT);
|
|
109
|
+
savedJson.value = fingerprint(EMPTY_DOCUMENT);
|
|
110
|
+
missing = true;
|
|
111
|
+
} else if (response.ok) {
|
|
112
|
+
const content = await response.json();
|
|
113
|
+
if (mine !== generation) return;
|
|
114
|
+
modelValue.value = content;
|
|
115
|
+
savedJson.value = fingerprint(content);
|
|
116
|
+
} else throw new Error(`Loading "${documentId}" failed (${response.status}).`);
|
|
117
|
+
ready.value = true;
|
|
118
|
+
} catch (cause) {
|
|
119
|
+
if (mine === generation) error.value = errorOf(cause);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const dirty = computed(() => !options.collaboration && modelValue.value !== void 0 && savedJson.value !== null && fingerprint(modelValue.value) !== savedJson.value);
|
|
123
|
+
function save() {
|
|
124
|
+
if (options.collaboration) return Promise.resolve();
|
|
125
|
+
const run = async () => {
|
|
126
|
+
const documentId = toValue(id);
|
|
127
|
+
const content = modelValue.value;
|
|
128
|
+
if (content === void 0 || !ready.value) return;
|
|
129
|
+
const saved = fingerprint(content);
|
|
130
|
+
if (saved === savedJson.value && !missing) return;
|
|
131
|
+
const json = JSON.stringify(content);
|
|
132
|
+
const response = await options.fetch(documentUrl(documentId), {
|
|
133
|
+
method: "PUT",
|
|
134
|
+
headers: { "content-type": "application/json" },
|
|
135
|
+
body: json
|
|
136
|
+
});
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
const failure = /* @__PURE__ */ new Error(`Saving "${documentId}" failed (${response.status}).`);
|
|
139
|
+
error.value = failure;
|
|
140
|
+
throw failure;
|
|
141
|
+
}
|
|
142
|
+
error.value = null;
|
|
143
|
+
if (documentId === toValue(id)) {
|
|
144
|
+
savedJson.value = saved;
|
|
145
|
+
missing = false;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const result = tail.then(run, run);
|
|
149
|
+
tail = result.catch(() => {});
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
function dispose() {
|
|
153
|
+
generation++;
|
|
154
|
+
closeSession?.();
|
|
155
|
+
closeSession = void 0;
|
|
156
|
+
collaboration.value = void 0;
|
|
157
|
+
comments.value.dispose();
|
|
158
|
+
}
|
|
159
|
+
watch(() => toValue(id), (documentId) => void load(documentId), { immediate: true });
|
|
160
|
+
if (getCurrentScope()) onScopeDispose(dispose);
|
|
161
|
+
return {
|
|
162
|
+
modelValue,
|
|
163
|
+
ready,
|
|
164
|
+
dirty,
|
|
165
|
+
save,
|
|
166
|
+
reload: () => load(toValue(id)),
|
|
167
|
+
error,
|
|
168
|
+
collaboration,
|
|
169
|
+
comments,
|
|
170
|
+
editor,
|
|
171
|
+
bind: computed(() => ({
|
|
172
|
+
modelValue: modelValue.value,
|
|
173
|
+
"onUpdate:modelValue": (content) => {
|
|
174
|
+
modelValue.value = content;
|
|
175
|
+
},
|
|
176
|
+
onReady: (instance) => {
|
|
177
|
+
editor.value = instance;
|
|
178
|
+
},
|
|
179
|
+
collaboration: collaboration.value
|
|
180
|
+
})),
|
|
181
|
+
dispose
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
export { useEditorDocument };
|
|
186
|
+
|
|
187
|
+
//# sourceMappingURL=vue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vue.js","names":[],"sources":["../../src/adapters/vue.ts"],"sourcesContent":["import { HocuspocusProvider } from '@hocuspocus/provider'\nimport type { Editor, JSONContent } from '@tiptap/core'\nimport type { AuthorIdentity, CollaborationSession } from '../core'\nimport { computed, type ComputedRef, getCurrentScope, type MaybeRefOrGetter, onScopeDispose, ref, type Ref, shallowRef, type ShallowRef, toValue, watch } from 'vue'\nimport * as Y from 'yjs'\nimport { type CommentsFetch, createRestCommentsModel, type RestCommentsModel } from './comments/client'\nimport { commentsUrl, documentUrl } from './routes'\n\n// The Vue side of an editor document: load it from the host's document endpoint, keep track of unsaved edits, save it,\n// and hand the editor surface what it needs (v-model, collaboration session, comments model). The server side is\n// `installEditorServer` (`./server/nitro`); any server that answers the same two routes works.\n\nexport type { CommentsFetch } from './comments/client'\n\nexport interface EditorDocumentOptions {\n /** How requests are sent: `globalThis.fetch`, or a wrapper that adds the base URL and credentials. Used for the content and the comments. */\n fetch: CommentsFetch\n /**\n * Live collaboration: the collaboration server's websocket URL (`ws://host/collab`; the document id is appended as the\n * provider's `name`) and the optional token its `EditorAdapter.authenticate` receives. With it the Y.Doc is the document:\n * the server loads and stores the content, so the composable neither fetches nor saves it (`modelValue` stays\n * `undefined`, `dirty` stays `false`, `save()` resolves at once).\n */\n collaboration?: { url: string, token?: string | (() => string) | (() => Promise<string>) }\n /** Who writes: labels remote carets and is the author of new comments; `null` or omitted = nobody (no comments). */\n user?: AuthorIdentity | null | (() => AuthorIdentity | null)\n /** Whether the user may comment now (a reader of a read-only document may be refused); default `true`. */\n canComment?: boolean | (() => boolean)\n}\n\n/** The props and events of the editor surface (`SimpleEditor`, `AdvancedEditor`, `EditorSurface`) that `useEditorDocument` fills. */\nexport interface EditorDocumentBinding {\n /** The document (standalone mode); `undefined` in collaboration mode and until it has loaded. */\n modelValue: JSONContent | undefined\n 'onUpdate:modelValue': (content: JSONContent) => void\n /** Hands the live editor to the comments model. */\n onReady: (editor: Editor) => void\n collaboration: CollaborationSession | undefined\n}\n\nexport interface EditorDocument {\n /** The document's TipTap JSON: the surface's `v-model`. `undefined` until loaded, and always in collaboration mode. */\n modelValue: Ref<JSONContent | undefined>\n /** `true` once the surface can be mounted: the content has loaded (a missing document starts empty), or the collaboration session exists. Mount with `:key` = the id, so a changed id remounts it. */\n ready: Ref<boolean>\n /** Edits since the content was loaded or last saved. */\n dirty: ComputedRef<boolean>\n /** Stores the current content (`PUT`); several calls run one after another and an edit made while one is in flight stays dirty. Rejects with the server's refusal. */\n save(): Promise<void>\n /** Fetches the content again and drops unsaved edits. */\n reload(): Promise<void>\n /** The last load or save error, `null` while all is well. */\n error: Ref<Error | null>\n /** The collaboration session for the surface's `collaboration` prop; `undefined` without the `collaboration` option. */\n collaboration: ShallowRef<CollaborationSession | undefined>\n /** The comments model of this document (`commentsFeature({ model: () => comments.value })`), threads behind the host's comments endpoint. A new one per id. */\n comments: Readonly<ShallowRef<RestCommentsModel>>\n /** The live editor, once the surface reported it. */\n editor: ShallowRef<Editor | undefined>\n /** `v-bind` target for the surface: `<AdvancedEditor v-if=\"ready\" :key=\"id\" v-bind=\"bind\" />`. */\n bind: ComputedRef<EditorDocumentBinding>\n /** Closes the collaboration session and the comments model; runs with the effect scope that called `useEditorDocument`. */\n dispose(): void\n}\n\nconst EMPTY_DOCUMENT: JSONContent = { type: 'doc', content: [{ type: 'paragraph' }] }\n\n/**\n * The document as compared for \"unsaved changes\": without block ids. The editor gives every block of loaded content an\n * id the moment it mounts (stored files have none yet); that is not an edit the user made, so it must not mark a\n * freshly opened document dirty. Saving still writes the ids.\n */\nconst withoutBlockIds = (node: JSONContent): JSONContent => {\n const { attrs, content, ...rest } = node\n const kept = attrs && Object.fromEntries(Object.entries(attrs).filter(([name]) => name !== 'id'))\n return {\n ...rest,\n ...(kept && Object.keys(kept).length > 0 && { attrs: kept }),\n ...(content && { content: content.map(withoutBlockIds) }),\n }\n}\nconst fingerprint = (content: JSONContent): string => JSON.stringify(withoutBlockIds(content))\n\n/** Document ids are percent-encoded into one path segment; the collab provider's `name` is the plain id. */\nfunction sessionOf(id: string, { url, token }: NonNullable<EditorDocumentOptions['collaboration']>, user: AuthorIdentity | null): { session: CollaborationSession, close: () => void } {\n const ydoc = new Y.Doc()\n const provider = new HocuspocusProvider({ url: `${url.replace(/\\/+$/, '')}/${encodeURIComponent(id)}`, name: id, document: ydoc, ...(token !== undefined && { token }) })\n return {\n session: { ydoc, provider, ...(user && { user }) },\n close() {\n provider.destroy()\n ydoc.destroy()\n },\n }\n}\n\nconst errorOf = (cause: unknown) => cause instanceof Error ? cause : new Error(String(cause))\n\n/**\n * Loads document `id` from `GET /api/editor/documents/<id>` (a `404` starts an empty document that the first `save()`\n * creates), tracks edits, saves with `PUT`, and prepares the collaboration session and comments model for the editor\n * surface. `id` may be reactive: a change loads the other document and replaces the session and the comments model.\n * Call it in a component's `setup`; the collaboration session is created synchronously, so the surface mounts in the same tick.\n */\nexport function useEditorDocument(id: MaybeRefOrGetter<string>, options: EditorDocumentOptions): EditorDocument {\n const userOf = () => toValue(options.user) ?? null\n const modelValue = ref<JSONContent>()\n const ready = ref(false)\n const error = ref<Error | null>(null)\n const collaboration = shallowRef<CollaborationSession>()\n const editor = shallowRef<Editor>()\n /** Fingerprint (`fingerprint`) of the JSON the server holds (the empty document while it does not exist), `null` before the first load. */\n const savedJson = ref<string | null>(null)\n /** The document does not exist on the server yet: the first save creates it even if nothing was typed. */\n let missing = false\n let closeSession: (() => void) | undefined\n let generation = 0\n let tail: Promise<unknown> = Promise.resolve()\n\n const commentsFor = (documentId: string) => createRestCommentsModel({\n fetch: options.fetch,\n documentId,\n endpoint: commentsUrl(documentId),\n user: userOf,\n canComment: () => toValue(options.canComment) ?? true,\n getEditor: () => editor.value,\n onError: (cause) => { error.value = cause },\n })\n const comments = shallowRef(commentsFor(toValue(id)))\n let commentsId = toValue(id)\n\n async function load(documentId: string): Promise<void> {\n const mine = ++generation\n ready.value = false\n error.value = null\n modelValue.value = undefined\n savedJson.value = null\n missing = false\n editor.value = undefined\n closeSession?.()\n closeSession = undefined\n collaboration.value = undefined\n if (commentsId !== documentId) {\n comments.value.dispose()\n comments.value = commentsFor(documentId)\n commentsId = documentId\n }\n\n if (options.collaboration) {\n const { session, close } = sessionOf(documentId, options.collaboration, userOf())\n closeSession = close\n collaboration.value = session\n ready.value = true\n return\n }\n try {\n const response = await options.fetch(documentUrl(documentId))\n if (mine !== generation) return\n if (response.status === 404) {\n modelValue.value = structuredClone(EMPTY_DOCUMENT)\n savedJson.value = fingerprint(EMPTY_DOCUMENT)\n missing = true\n }\n else if (response.ok) {\n const content = await response.json() as JSONContent\n if (mine !== generation) return\n modelValue.value = content\n savedJson.value = fingerprint(content)\n }\n else {\n throw new Error(`Loading \"${documentId}\" failed (${response.status}).`)\n }\n ready.value = true\n }\n catch (cause) {\n if (mine === generation) error.value = errorOf(cause)\n }\n }\n\n const dirty = computed(() => !options.collaboration && modelValue.value !== undefined && savedJson.value !== null && fingerprint(modelValue.value) !== savedJson.value)\n\n function save(): Promise<void> {\n if (options.collaboration) return Promise.resolve()\n const run = async () => {\n const documentId = toValue(id)\n const content = modelValue.value\n if (content === undefined || !ready.value) return\n const saved = fingerprint(content)\n if (saved === savedJson.value && !missing) return\n const json = JSON.stringify(content)\n const response = await options.fetch(documentUrl(documentId), { method: 'PUT', headers: { 'content-type': 'application/json' }, body: json })\n if (!response.ok) {\n const failure = new Error(`Saving \"${documentId}\" failed (${response.status}).`)\n error.value = failure\n throw failure\n }\n error.value = null\n // Only if the document did not change to another id while the request was in flight.\n if (documentId === toValue(id)) {\n savedJson.value = saved\n missing = false\n }\n }\n const result = tail.then(run, run)\n tail = result.catch(() => {})\n return result\n }\n\n function dispose() {\n generation++\n closeSession?.()\n closeSession = undefined\n collaboration.value = undefined\n comments.value.dispose()\n }\n\n watch(() => toValue(id), documentId => void load(documentId), { immediate: true })\n if (getCurrentScope()) onScopeDispose(dispose)\n\n const bind = computed<EditorDocumentBinding>(() => ({\n modelValue: modelValue.value,\n 'onUpdate:modelValue': (content) => { modelValue.value = content },\n onReady: (instance) => { editor.value = instance },\n collaboration: collaboration.value,\n }))\n\n return { modelValue, ready, dirty, save, reload: () => load(toValue(id)), error, collaboration, comments, editor, bind, dispose }\n}\n"],"mappings":"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"}
|
|
@@ -11,16 +11,16 @@ type __VLS_Props = {
|
|
|
11
11
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
12
12
|
view: import("vue").ShallowRef<EditorView | undefined, EditorView | undefined>;
|
|
13
13
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
14
|
-
ready: (view: EditorView) => any;
|
|
15
14
|
"update:modelValue": (text: string) => any;
|
|
15
|
+
ready: (view: EditorView) => any;
|
|
16
16
|
save: (text: string) => any;
|
|
17
17
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
18
|
-
onReady?: ((view: EditorView) => any) | undefined;
|
|
19
18
|
"onUpdate:modelValue"?: ((text: string) => any) | undefined;
|
|
19
|
+
onReady?: ((view: EditorView) => any) | undefined;
|
|
20
20
|
onSave?: ((text: string) => any) | undefined;
|
|
21
21
|
}>, {
|
|
22
|
-
modelValue: string;
|
|
23
22
|
editable: boolean;
|
|
23
|
+
modelValue: string;
|
|
24
24
|
wordWrap: boolean;
|
|
25
25
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
26
26
|
declare const _default: typeof __VLS_export;
|
|
@@ -13,6 +13,10 @@ interface AnchorEntry {
|
|
|
13
13
|
token: unknown | null;
|
|
14
14
|
}
|
|
15
15
|
export declare const docAnchorsKey: PluginKey<ReadonlyMap<string, AnchorEntry>>;
|
|
16
|
+
/**
|
|
17
|
+
* `add` captures an anchor; `remove` releases anchors. `after` (with `remove`) names collapsed anchors that, in this
|
|
18
|
+
* transaction, move behind text inserted at their position instead of staying in front of it.
|
|
19
|
+
*/
|
|
16
20
|
export type DocAnchorsMeta = {
|
|
17
21
|
add: {
|
|
18
22
|
id: string;
|
|
@@ -22,6 +26,7 @@ export type DocAnchorsMeta = {
|
|
|
22
26
|
};
|
|
23
27
|
} | {
|
|
24
28
|
remove: readonly string[];
|
|
29
|
+
after?: readonly string[];
|
|
25
30
|
};
|
|
26
31
|
export interface DocAnchorsOptions {
|
|
27
32
|
/** Position mapping that survives collaborative edits (see the collab feature); without one anchors only map through local transactions. */
|
package/dist/core/doc-anchors.js
CHANGED
|
@@ -23,6 +23,7 @@ const DocAnchors = Extension.create({
|
|
|
23
23
|
init: () => /* @__PURE__ */ new Map(),
|
|
24
24
|
apply(tr, value, _oldState, newState) {
|
|
25
25
|
const meta = tr.getMeta(docAnchorsKey);
|
|
26
|
+
const after = meta && "remove" in meta ? meta.after : void 0;
|
|
26
27
|
if (!meta && (!tr.docChanged || !value.size)) return value;
|
|
27
28
|
let next = value;
|
|
28
29
|
if (tr.docChanged && value.size) {
|
|
@@ -43,11 +44,11 @@ const DocAnchors = Extension.create({
|
|
|
43
44
|
mapped.set(id, entry);
|
|
44
45
|
continue;
|
|
45
46
|
}
|
|
46
|
-
const
|
|
47
|
+
const side = entry.from === entry.to ? after?.includes(id) ? 1 : -1 : null;
|
|
47
48
|
mapped.set(id, {
|
|
48
49
|
...entry,
|
|
49
|
-
from: tr.mapping.map(entry.from,
|
|
50
|
-
to: tr.mapping.map(entry.to, -1)
|
|
50
|
+
from: tr.mapping.map(entry.from, side ?? 1),
|
|
51
|
+
to: tr.mapping.map(entry.to, side ?? -1)
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
54
|
next = mapped;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doc-anchors.js","names":[],"sources":["../../src/core/doc-anchors.ts"],"sourcesContent":["import { type Editor, Extension } from '@tiptap/core'\nimport { type EditorState, Plugin, PluginKey } from '@tiptap/pm/state'\nimport type { AnchorResolver } from './feature'\nimport { newBlockId } from './schema/block-ids'\n\n/** A document range captured with `captureRange`: survives local and remote (collaborative) edits until released. */\nexport interface DocAnchor { readonly id: string }\n\n/** Current positions (-1 when gone) plus, with an `AnchorResolver`, its token for re-resolving after remote changes. */\ninterface AnchorEntry { from: number, to: number, token: unknown | null }\n\nexport const docAnchorsKey = new PluginKey<ReadonlyMap<string, AnchorEntry>>('docAnchors')\n\nexport type DocAnchorsMeta = { add: { id: string, from: number, to: number, startInside: boolean } } | { remove: readonly string[] }\n\nexport interface DocAnchorsOptions {\n /** Position mapping that survives collaborative edits (see the collab feature); without one anchors only map through local transactions. */\n resolver: AnchorResolver | null\n}\n\n/**\n * Edit-surviving ranges for AI flows (selection menu, AI sidebar, reviews, chat ops): anchors map through local\n * transactions and, with an `AnchorResolver`, re-resolve from its tokens on collaboration (y-sync) transactions, which\n * replace the whole document. Client-only: not part of the Vue-free schema.\n */\nexport const DocAnchors = Extension.create<DocAnchorsOptions, { resolver: AnchorResolver | null }>({\n name: 'docAnchors',\n priority: 1000,\n\n addOptions: () => ({ resolver: null }),\n\n addStorage() {\n return { resolver: this.options.resolver }\n },\n\n addProseMirrorPlugins() {\n const { resolver } = this.options\n return [\n new Plugin<ReadonlyMap<string, AnchorEntry>>({\n key: docAnchorsKey,\n state: {\n init: () => new Map(),\n apply(tr, value, _oldState, newState) {\n const meta = tr.getMeta(docAnchorsKey) as DocAnchorsMeta | undefined\n if (!meta && (!tr.docChanged || !value.size)) return value\n let next = value\n if (tr.docChanged && value.size) {\n const synced = resolver?.isYSync(tr) === true\n const mapped = new Map<string, AnchorEntry>()\n for (const [id, entry] of value) {\n if (synced && resolver && entry.token != null) {\n mapped.set(id, { ...entry, ...(resolver.resolve(newState, entry.token) ?? { from: -1, to: -1 }) })\n continue\n }\n if (entry.from < 0) {\n mapped.set(id, entry)\n continue\n }\n // Like the resolver's positions: text inserted at a boundary stays outside (a collapsed anchor keeps it after itself).\n const
|
|
1
|
+
{"version":3,"file":"doc-anchors.js","names":[],"sources":["../../src/core/doc-anchors.ts"],"sourcesContent":["import { type Editor, Extension } from '@tiptap/core'\nimport { type EditorState, Plugin, PluginKey } from '@tiptap/pm/state'\nimport type { AnchorResolver } from './feature'\nimport { newBlockId } from './schema/block-ids'\n\n/** A document range captured with `captureRange`: survives local and remote (collaborative) edits until released. */\nexport interface DocAnchor { readonly id: string }\n\n/** Current positions (-1 when gone) plus, with an `AnchorResolver`, its token for re-resolving after remote changes. */\ninterface AnchorEntry { from: number, to: number, token: unknown | null }\n\nexport const docAnchorsKey = new PluginKey<ReadonlyMap<string, AnchorEntry>>('docAnchors')\n\n/**\n * `add` captures an anchor; `remove` releases anchors. `after` (with `remove`) names collapsed anchors that, in this\n * transaction, move behind text inserted at their position instead of staying in front of it.\n */\nexport type DocAnchorsMeta = { add: { id: string, from: number, to: number, startInside: boolean } } | { remove: readonly string[], after?: readonly string[] }\n\nexport interface DocAnchorsOptions {\n /** Position mapping that survives collaborative edits (see the collab feature); without one anchors only map through local transactions. */\n resolver: AnchorResolver | null\n}\n\n/**\n * Edit-surviving ranges for AI flows (selection menu, AI sidebar, reviews, chat ops): anchors map through local\n * transactions and, with an `AnchorResolver`, re-resolve from its tokens on collaboration (y-sync) transactions, which\n * replace the whole document. Client-only: not part of the Vue-free schema.\n */\nexport const DocAnchors = Extension.create<DocAnchorsOptions, { resolver: AnchorResolver | null }>({\n name: 'docAnchors',\n priority: 1000,\n\n addOptions: () => ({ resolver: null }),\n\n addStorage() {\n return { resolver: this.options.resolver }\n },\n\n addProseMirrorPlugins() {\n const { resolver } = this.options\n return [\n new Plugin<ReadonlyMap<string, AnchorEntry>>({\n key: docAnchorsKey,\n state: {\n init: () => new Map(),\n apply(tr, value, _oldState, newState) {\n const meta = tr.getMeta(docAnchorsKey) as DocAnchorsMeta | undefined\n const after = meta && 'remove' in meta ? meta.after : undefined\n if (!meta && (!tr.docChanged || !value.size)) return value\n let next = value\n if (tr.docChanged && value.size) {\n const synced = resolver?.isYSync(tr) === true\n const mapped = new Map<string, AnchorEntry>()\n for (const [id, entry] of value) {\n if (synced && resolver && entry.token != null) {\n mapped.set(id, { ...entry, ...(resolver.resolve(newState, entry.token) ?? { from: -1, to: -1 }) })\n continue\n }\n if (entry.from < 0) {\n mapped.set(id, entry)\n continue\n }\n // Like the resolver's positions: text inserted at a boundary stays outside (a collapsed anchor keeps it after itself,\n // unless the transaction moves it behind that text).\n const side = entry.from === entry.to ? (after?.includes(id) ? 1 : -1) : null\n mapped.set(id, { ...entry, from: tr.mapping.map(entry.from, side ?? 1), to: tr.mapping.map(entry.to, side ?? -1) })\n }\n next = mapped\n }\n if (meta && 'add' in meta) {\n const { id, from, to, startInside } = meta.add\n next = new Map(next).set(id, { from, to, token: resolver?.capture(newState, { from, to }, startInside) ?? null })\n }\n else if (meta && meta.remove.some(id => next.has(id))) {\n const kept = new Map(next)\n for (const id of meta.remove) kept.delete(id)\n next = kept\n }\n return next\n },\n },\n }),\n ]\n },\n})\n\n/**\n * Captures [from, to] of the current document. Both ends stick to the text before them; `startInside` binds a\n * non-empty range's start to its own first character instead, so text inserted right before it stays outside.\n * Release with `releaseRanges` when the flow ends.\n */\nexport function captureRange(editor: Editor, { from, to }: { from: number, to: number }, options: { startInside?: boolean } = {}): DocAnchor {\n const id = newBlockId()\n const meta: DocAnchorsMeta = { add: { id, from, to, startInside: options.startInside ?? false } }\n editor.view.dispatch(editor.state.tr.setMeta(docAnchorsKey, meta).setMeta('addToHistory', false))\n return { id }\n}\n\n/** Current positions of `anchor`, collapsed (`from === to`) when its content was deleted; null when unknown or gone. */\nexport function resolveRange(state: EditorState, anchor: DocAnchor): { from: number, to: number } | null {\n const entry = docAnchorsKey.getState(state)?.get(anchor.id)\n if (!entry || entry.from < 0 || entry.from > entry.to || entry.to > state.doc.content.size) return null\n return { from: entry.from, to: entry.to }\n}\n\n/** Forgets `anchors` (unknown ones are ignored). */\nexport function releaseRanges(editor: Editor, anchors: readonly DocAnchor[]): void {\n if (editor.isDestroyed || !anchors.length) return\n const meta: DocAnchorsMeta = { remove: anchors.map(a => a.id) }\n editor.view.dispatch(editor.state.tr.setMeta(docAnchorsKey, meta).setMeta('addToHistory', false))\n}\n\ndeclare module '@tiptap/core' {\n interface Storage { docAnchors: { resolver: AnchorResolver | null } }\n}\n\n/** The anchor resolver `editor` was created with; null without a collaboration-aware feature. */\nexport const anchorResolverOf = (editor: Editor): AnchorResolver | null => editor.storage.docAnchors?.resolver ?? null\n"],"mappings":";;;;AAWA,MAAa,gBAAgB,IAAI,UAA4C,YAAY;;;;;;AAkBzF,MAAa,aAAa,UAAU,OAA+D;CACjG,MAAM;CACN,UAAU;CAEV,mBAAmB,EAAE,UAAU,KAAK;CAEpC,aAAa;EACX,OAAO,EAAE,UAAU,KAAK,QAAQ,SAAS;CAC3C;CAEA,wBAAwB;EACtB,MAAM,EAAE,aAAa,KAAK;EAC1B,OAAO,CACL,IAAI,OAAyC;GAC3C,KAAK;GACL,OAAO;IACL,4BAAY,IAAI,IAAI;IACpB,MAAM,IAAI,OAAO,WAAW,UAAU;KACpC,MAAM,OAAO,GAAG,QAAQ,aAAa;KACrC,MAAM,QAAQ,QAAQ,YAAY,OAAO,KAAK,QAAQ,KAAA;KACtD,IAAI,CAAC,SAAS,CAAC,GAAG,cAAc,CAAC,MAAM,OAAO,OAAO;KACrD,IAAI,OAAO;KACX,IAAI,GAAG,cAAc,MAAM,MAAM;MAC/B,MAAM,SAAS,UAAU,QAAQ,EAAE,MAAM;MACzC,MAAM,yBAAS,IAAI,IAAyB;MAC5C,KAAK,MAAM,CAAC,IAAI,UAAU,OAAO;OAC/B,IAAI,UAAU,YAAY,MAAM,SAAS,MAAM;QAC7C,OAAO,IAAI,IAAI;SAAE,GAAG;SAAO,GAAI,SAAS,QAAQ,UAAU,MAAM,KAAK,KAAK;UAAE,MAAM;UAAI,IAAI;SAAG;QAAG,CAAC;QACjG;OACF;OACA,IAAI,MAAM,OAAO,GAAG;QAClB,OAAO,IAAI,IAAI,KAAK;QACpB;OACF;OAGA,MAAM,OAAO,MAAM,SAAS,MAAM,KAAM,OAAO,SAAS,EAAE,IAAI,IAAI,KAAM;OACxE,OAAO,IAAI,IAAI;QAAE,GAAG;QAAO,MAAM,GAAG,QAAQ,IAAI,MAAM,MAAM,QAAQ,CAAC;QAAG,IAAI,GAAG,QAAQ,IAAI,MAAM,IAAI,QAAQ,EAAE;OAAE,CAAC;MACpH;MACA,OAAO;KACT;KACA,IAAI,QAAQ,SAAS,MAAM;MACzB,MAAM,EAAE,IAAI,MAAM,IAAI,gBAAgB,KAAK;MAC3C,OAAO,IAAI,IAAI,IAAI,CAAC,CAAC,IAAI,IAAI;OAAE;OAAM;OAAI,OAAO,UAAU,QAAQ,UAAU;QAAE;QAAM;OAAG,GAAG,WAAW,KAAK;MAAK,CAAC;KAClH,OACK,IAAI,QAAQ,KAAK,OAAO,MAAK,OAAM,KAAK,IAAI,EAAE,CAAC,GAAG;MACrD,MAAM,OAAO,IAAI,IAAI,IAAI;MACzB,KAAK,MAAM,MAAM,KAAK,QAAQ,KAAK,OAAO,EAAE;MAC5C,OAAO;KACT;KACA,OAAO;IACT;GACF;EACF,CAAC,CACH;CACF;AACF,CAAC;;;;;;AAOD,SAAgB,aAAa,QAAgB,EAAE,MAAM,MAAoC,UAAqC,CAAC,GAAc;CAC3I,MAAM,KAAK,WAAW;CACtB,MAAM,OAAuB,EAAE,KAAK;EAAE;EAAI;EAAM;EAAI,aAAa,QAAQ,eAAe;CAAM,EAAE;CAChG,OAAO,KAAK,SAAS,OAAO,MAAM,GAAG,QAAQ,eAAe,IAAI,CAAC,CAAC,QAAQ,gBAAgB,KAAK,CAAC;CAChG,OAAO,EAAE,GAAG;AACd;;AAGA,SAAgB,aAAa,OAAoB,QAAwD;CACvG,MAAM,QAAQ,cAAc,SAAS,KAAK,CAAC,EAAE,IAAI,OAAO,EAAE;CAC1D,IAAI,CAAC,SAAS,MAAM,OAAO,KAAK,MAAM,OAAO,MAAM,MAAM,MAAM,KAAK,MAAM,IAAI,QAAQ,MAAM,OAAO;CACnG,OAAO;EAAE,MAAM,MAAM;EAAM,IAAI,MAAM;CAAG;AAC1C;;AAGA,SAAgB,cAAc,QAAgB,SAAqC;CACjF,IAAI,OAAO,eAAe,CAAC,QAAQ,QAAQ;CAC3C,MAAM,OAAuB,EAAE,QAAQ,QAAQ,KAAI,MAAK,EAAE,EAAE,EAAE;CAC9D,OAAO,KAAK,SAAS,OAAO,MAAM,GAAG,QAAQ,eAAe,IAAI,CAAC,CAAC,QAAQ,gBAAgB,KAAK,CAAC;AAClG;;AAOA,MAAa,oBAAoB,WAA0C,OAAO,QAAQ,YAAY,YAAY"}
|
package/dist/core/editor-ui.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createMetadataRegistry } from "./metadata.js";
|
|
1
2
|
//#region src/core/editor-ui.ts
|
|
2
3
|
/** Each mounted surface's entry by its Tiptap editor (raw, unproxied instance), for components outside its subtree. */
|
|
3
4
|
const entries = /* @__PURE__ */ new WeakMap();
|
|
@@ -32,7 +33,62 @@ function notifierOf(editor, app) {
|
|
|
32
33
|
};
|
|
33
34
|
return notify;
|
|
34
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* The `FeatureContext` a component placed beside an editor (the toolbar) hands feature factories. App-level members
|
|
38
|
+
* (`app`, `locale`, `t`, `settings`, `fetch`, `ai`) come from the app context; everything per-surface (`metadata`, `ui`, `documentId`,
|
|
39
|
+
* `upload`, `collaboration`, `anchorResolver`, `abilities`, `slashCommands`) is read from the entry registered for
|
|
40
|
+
* `getEditor()` each time it is used, so the context follows the editor while it changes and falls back to inert values
|
|
41
|
+
* (an empty metadata registry, no-op chrome, `commentsOpen: false`) while there is none. `notify` is `notifierOf`.
|
|
42
|
+
*/
|
|
43
|
+
function borrowedFeatureContext(options) {
|
|
44
|
+
const { app, getEditor } = options;
|
|
45
|
+
const unwrap = options.unwrap ?? ((editor) => editor);
|
|
46
|
+
const rawEditor = () => {
|
|
47
|
+
const editor = getEditor();
|
|
48
|
+
return editor && unwrap(editor);
|
|
49
|
+
};
|
|
50
|
+
const fallbackMetadata = createMetadataRegistry();
|
|
51
|
+
const entry = () => editorEntryOf(rawEditor());
|
|
52
|
+
const ui = () => entry()?.ui;
|
|
53
|
+
return {
|
|
54
|
+
app,
|
|
55
|
+
locale: app.locale,
|
|
56
|
+
t: app.t,
|
|
57
|
+
settings: options.settings ?? app.settings,
|
|
58
|
+
notify: notifierOf(rawEditor, app),
|
|
59
|
+
fetch: app.fetch,
|
|
60
|
+
ai: app.ai,
|
|
61
|
+
get metadata() {
|
|
62
|
+
return entry()?.ctx.metadata ?? fallbackMetadata;
|
|
63
|
+
},
|
|
64
|
+
ui: {
|
|
65
|
+
openSettings: (tab) => ui()?.openSettings(tab),
|
|
66
|
+
toggleSidebar: () => ui()?.toggleSidebar(),
|
|
67
|
+
toggleComments: () => ui()?.toggleComments(),
|
|
68
|
+
get commentsOpen() {
|
|
69
|
+
return ui()?.commentsOpen ?? false;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
get documentId() {
|
|
73
|
+
return entry()?.ctx.documentId;
|
|
74
|
+
},
|
|
75
|
+
get upload() {
|
|
76
|
+
return entry()?.ctx.upload;
|
|
77
|
+
},
|
|
78
|
+
get collaboration() {
|
|
79
|
+
return entry()?.ctx.collaboration;
|
|
80
|
+
},
|
|
81
|
+
get anchorResolver() {
|
|
82
|
+
return entry()?.ctx.anchorResolver ?? null;
|
|
83
|
+
},
|
|
84
|
+
get abilities() {
|
|
85
|
+
return entry()?.ctx.abilities ?? {};
|
|
86
|
+
},
|
|
87
|
+
slashCommands: () => entry()?.ctx.slashCommands() ?? [],
|
|
88
|
+
getEditor
|
|
89
|
+
};
|
|
90
|
+
}
|
|
35
91
|
//#endregion
|
|
36
|
-
export { editorEntryOf, notifierOf, registerEditor };
|
|
92
|
+
export { borrowedFeatureContext, editorEntryOf, notifierOf, registerEditor };
|
|
37
93
|
|
|
38
94
|
//# sourceMappingURL=editor-ui.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editor-ui.js","names":[],"sources":["../../src/core/editor-ui.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport type { EditorContext } from './context'\nimport type { EditorPreset, FeatureContext } from './feature'\nimport type { BlockAction } from './extension-points'\nimport type { Notifier } from './notify'\n\n/** Editor chrome an `EditorSurface` owns, for the components around it. Settings tabs are the ids features contribute. */\nexport interface EditorUi {\n /** Opens the settings UI, on `tab` when given (else on the tab shown last). */\n openSettings: (tab?: string) => void\n /** Opens or closes the editor's AI sidebar (the surface's `sidebarOpen` model, which the host binds). */\n toggleSidebar: () => void\n /**\n * Opens or closes the editor's comments margin (the surface's `commentsOpen` model, which the host binds). Opening it\n * closes the AI sidebar and vice versa.\n */\n toggleComments: () => void\n /** Whether the comments margin is open (read-only; for toggles' active state). */\n readonly commentsOpen: boolean\n}\n\n/** What a mounted surface registers for its editor instance: the components beside it read it by `editor`. */\nexport interface EditorEntry {\n ui: EditorUi\n preset: EditorPreset\n ctx: FeatureContext\n /** Features' block actions followed by the host's, for the overlay that renders them. */\n blockActions: readonly BlockAction[]\n}\n\n/** Each mounted surface's entry by its Tiptap editor (raw, unproxied instance), for components outside its subtree. */\nconst entries = new WeakMap<Editor, EditorEntry>()\n\n/**\n * Makes `entry` reachable through `editorEntryOf(editor)` (and the framework layers' `useEditorUi(editor)`); returns the\n * unregister function. The surface registers once its editor exists (before emitting `ready`) and unregisters on unmount.\n * Pass the raw editor: framework proxies (e.g. Vue reactive wrappers) are different WeakMap keys.\n */\nexport function registerEditor(editor: Editor, entry: EditorEntry): () => void {\n entries.set(editor, entry)\n return () => {\n if (entries.get(editor) === entry) entries.delete(editor)\n }\n}\n\n/** The surface entry registered for `editor` (the raw instance); undefined for an unregistered or missing editor. */\nexport function editorEntryOf(editor: Editor | undefined): EditorEntry | undefined {\n return editor ? entries.get(editor) : undefined\n}\n\n/**\n * The one notifier policy: a notice goes to the surface notifier of `editor` (`createEditor`/`EditorSurface` of the\n * host) while it has one, else to the app context's. Both are resolved when a notice is shown (also `dismiss`), so a\n * request that finishes after an await still reaches the current surface. Pass `editor` (the raw instance) and `app` as\n * getters when they change over the notifier's life.\n */\nexport function notifierOf(editor: Editor | undefined | (() => Editor | undefined), app: Pick<EditorContext, 'notify'> | (() => Pick<EditorContext, 'notify'>)): Notifier {\n const resolve = (): Notifier => {\n const instance = typeof editor === 'function' ? editor() : editor\n return editorEntryOf(instance)?.ctx.notify ?? (typeof app === 'function' ? app() : app).notify\n }\n const notify: Notifier = notice => resolve()(notice)\n notify.dismiss = (id) => { resolve().dismiss?.(id) }\n return notify\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"editor-ui.js","names":[],"sources":["../../src/core/editor-ui.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport type { EditorContext } from './context'\nimport type { EditorPreset, FeatureContext } from './feature'\nimport type { BlockAction } from './extension-points'\nimport { createMetadataRegistry } from './metadata'\nimport type { Notifier } from './notify'\n\n/** Editor chrome an `EditorSurface` owns, for the components around it. Settings tabs are the ids features contribute. */\nexport interface EditorUi {\n /** Opens the settings UI, on `tab` when given (else on the tab shown last). */\n openSettings: (tab?: string) => void\n /** Opens or closes the editor's AI sidebar (the surface's `sidebarOpen` model, which the host binds). */\n toggleSidebar: () => void\n /**\n * Opens or closes the editor's comments margin (the surface's `commentsOpen` model, which the host binds). Opening it\n * closes the AI sidebar and vice versa.\n */\n toggleComments: () => void\n /** Whether the comments margin is open (read-only; for toggles' active state). */\n readonly commentsOpen: boolean\n}\n\n/** What a mounted surface registers for its editor instance: the components beside it read it by `editor`. */\nexport interface EditorEntry {\n ui: EditorUi\n preset: EditorPreset\n ctx: FeatureContext\n /** Features' block actions followed by the host's, for the overlay that renders them. */\n blockActions: readonly BlockAction[]\n}\n\n/** Each mounted surface's entry by its Tiptap editor (raw, unproxied instance), for components outside its subtree. */\nconst entries = new WeakMap<Editor, EditorEntry>()\n\n/**\n * Makes `entry` reachable through `editorEntryOf(editor)` (and the framework layers' `useEditorUi(editor)`); returns the\n * unregister function. The surface registers once its editor exists (before emitting `ready`) and unregisters on unmount.\n * Pass the raw editor: framework proxies (e.g. Vue reactive wrappers) are different WeakMap keys.\n */\nexport function registerEditor(editor: Editor, entry: EditorEntry): () => void {\n entries.set(editor, entry)\n return () => {\n if (entries.get(editor) === entry) entries.delete(editor)\n }\n}\n\n/** The surface entry registered for `editor` (the raw instance); undefined for an unregistered or missing editor. */\nexport function editorEntryOf(editor: Editor | undefined): EditorEntry | undefined {\n return editor ? entries.get(editor) : undefined\n}\n\n/**\n * The one notifier policy: a notice goes to the surface notifier of `editor` (`createEditor`/`EditorSurface` of the\n * host) while it has one, else to the app context's. Both are resolved when a notice is shown (also `dismiss`), so a\n * request that finishes after an await still reaches the current surface. Pass `editor` (the raw instance) and `app` as\n * getters when they change over the notifier's life.\n */\nexport function notifierOf(editor: Editor | undefined | (() => Editor | undefined), app: Pick<EditorContext, 'notify'> | (() => Pick<EditorContext, 'notify'>)): Notifier {\n const resolve = (): Notifier => {\n const instance = typeof editor === 'function' ? editor() : editor\n return editorEntryOf(instance)?.ctx.notify ?? (typeof app === 'function' ? app() : app).notify\n }\n const notify: Notifier = notice => resolve()(notice)\n notify.dismiss = (id) => { resolve().dismiss?.(id) }\n return notify\n}\n\nexport interface BorrowedFeatureContextOptions {\n app: EditorContext\n /** The editor the borrowing component works on, as its framework layer holds it; `undefined` while there is none. */\n getEditor: () => Editor | undefined\n /** Maps the held editor to the raw instance surfaces register (default: identity). Vue passes `toRaw`. */\n unwrap?: (editor: Editor) => Editor\n /** The `settings` the context exposes (default: the app's); a framework layer may wrap it so rendering tracks changes. */\n settings?: FeatureContext['settings']\n}\n\n/**\n * The `FeatureContext` a component placed beside an editor (the toolbar) hands feature factories. App-level members\n * (`app`, `locale`, `t`, `settings`, `fetch`, `ai`) come from the app context; everything per-surface (`metadata`, `ui`, `documentId`,\n * `upload`, `collaboration`, `anchorResolver`, `abilities`, `slashCommands`) is read from the entry registered for\n * `getEditor()` each time it is used, so the context follows the editor while it changes and falls back to inert values\n * (an empty metadata registry, no-op chrome, `commentsOpen: false`) while there is none. `notify` is `notifierOf`.\n */\nexport function borrowedFeatureContext(options: BorrowedFeatureContextOptions): FeatureContext {\n const { app, getEditor } = options\n const unwrap = options.unwrap ?? (editor => editor)\n const rawEditor = () => {\n const editor = getEditor()\n return editor && unwrap(editor)\n }\n const fallbackMetadata = createMetadataRegistry()\n const entry = () => editorEntryOf(rawEditor())\n const ui = () => entry()?.ui\n return {\n app,\n locale: app.locale,\n t: app.t,\n settings: options.settings ?? app.settings,\n notify: notifierOf(rawEditor, app),\n fetch: app.fetch,\n ai: app.ai,\n get metadata() { return entry()?.ctx.metadata ?? fallbackMetadata },\n ui: {\n openSettings: tab => ui()?.openSettings(tab),\n toggleSidebar: () => ui()?.toggleSidebar(),\n toggleComments: () => ui()?.toggleComments(),\n get commentsOpen() { return ui()?.commentsOpen ?? false },\n },\n get documentId() { return entry()?.ctx.documentId },\n get upload() { return entry()?.ctx.upload },\n get collaboration() { return entry()?.ctx.collaboration },\n get anchorResolver() { return entry()?.ctx.anchorResolver ?? null },\n get abilities() { return entry()?.ctx.abilities ?? {} },\n slashCommands: () => entry()?.ctx.slashCommands() ?? [],\n getEditor,\n }\n}\n"],"mappings":";;;AAgCA,MAAM,0BAAU,IAAI,QAA6B;;;;;;AAOjD,SAAgB,eAAe,QAAgB,OAAgC;CAC7E,QAAQ,IAAI,QAAQ,KAAK;CACzB,aAAa;EACX,IAAI,QAAQ,IAAI,MAAM,MAAM,OAAO,QAAQ,OAAO,MAAM;CAC1D;AACF;;AAGA,SAAgB,cAAc,QAAqD;CACjF,OAAO,SAAS,QAAQ,IAAI,MAAM,IAAI,KAAA;AACxC;;;;;;;AAQA,SAAgB,WAAW,QAAyD,KAAsF;CACxK,MAAM,gBAA0B;EAE9B,OAAO,cADU,OAAO,WAAW,aAAa,OAAO,IAAI,MAC9B,CAAC,EAAE,IAAI,WAAW,OAAO,QAAQ,aAAa,IAAI,IAAI,IAAA,CAAK;CAC1F;CACA,MAAM,UAAmB,WAAU,QAAQ,CAAC,CAAC,MAAM;CACnD,OAAO,WAAW,OAAO;EAAE,QAAQ,CAAC,CAAC,UAAU,EAAE;CAAE;CACnD,OAAO;AACT;;;;;;;;AAmBA,SAAgB,uBAAuB,SAAwD;CAC7F,MAAM,EAAE,KAAK,cAAc;CAC3B,MAAM,SAAS,QAAQ,YAAW,WAAU;CAC5C,MAAM,kBAAkB;EACtB,MAAM,SAAS,UAAU;EACzB,OAAO,UAAU,OAAO,MAAM;CAChC;CACA,MAAM,mBAAmB,uBAAuB;CAChD,MAAM,cAAc,cAAc,UAAU,CAAC;CAC7C,MAAM,WAAW,MAAM,CAAC,EAAE;CAC1B,OAAO;EACL;EACA,QAAQ,IAAI;EACZ,GAAG,IAAI;EACP,UAAU,QAAQ,YAAY,IAAI;EAClC,QAAQ,WAAW,WAAW,GAAG;EACjC,OAAO,IAAI;EACX,IAAI,IAAI;EACR,IAAI,WAAW;GAAE,OAAO,MAAM,CAAC,EAAE,IAAI,YAAY;EAAiB;EAClE,IAAI;GACF,eAAc,QAAO,GAAG,CAAC,EAAE,aAAa,GAAG;GAC3C,qBAAqB,GAAG,CAAC,EAAE,cAAc;GACzC,sBAAsB,GAAG,CAAC,EAAE,eAAe;GAC3C,IAAI,eAAe;IAAE,OAAO,GAAG,CAAC,EAAE,gBAAgB;GAAM;EAC1D;EACA,IAAI,aAAa;GAAE,OAAO,MAAM,CAAC,EAAE,IAAI;EAAW;EAClD,IAAI,SAAS;GAAE,OAAO,MAAM,CAAC,EAAE,IAAI;EAAO;EAC1C,IAAI,gBAAgB;GAAE,OAAO,MAAM,CAAC,EAAE,IAAI;EAAc;EACxD,IAAI,iBAAiB;GAAE,OAAO,MAAM,CAAC,EAAE,IAAI,kBAAkB;EAAK;EAClE,IAAI,YAAY;GAAE,OAAO,MAAM,CAAC,EAAE,IAAI,aAAa,CAAC;EAAE;EACtD,qBAAqB,MAAM,CAAC,EAAE,IAAI,cAAc,KAAK,CAAC;EACtD;CACF;AACF"}
|