@witchcraft/editor 0.1.1 → 0.2.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.
- package/README.md +1 -0
- package/dist/module.d.mts +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -2
- package/dist/runtime/components/EditorDemoApp.vue +31 -1
- package/dist/runtime/components/EditorDemoControls.d.vue.ts +3 -0
- package/dist/runtime/components/EditorDemoControls.vue +55 -12
- package/dist/runtime/components/EditorDemoControls.vue.d.ts +3 -0
- package/dist/runtime/pm/features/Base/plugins/debugSelectionPlugin.d.ts +1 -5
- package/dist/runtime/pm/features/Base/plugins/debugSelectionPlugin.js +43 -17
- package/dist/runtime/pm/features/CodeBlock/components/CodeBlockView.vue +0 -1
- package/dist/runtime/pm/features/Collaboration/Collaboration.d.ts +14 -63
- package/dist/runtime/pm/features/Collaboration/Collaboration.js +4 -164
- package/dist/runtime/pm/features/Collaboration/createCollaborationPlugins.d.ts +16 -0
- package/dist/runtime/pm/features/Collaboration/createCollaborationPlugins.js +85 -0
- package/dist/runtime/pm/features/DocumentApi/DocumentApi.d.ts +16 -3
- package/dist/runtime/pm/features/DocumentApi/DocumentApi.js +19 -2
- package/dist/runtime/pm/features/DocumentApi/composables/useEditorContent.js +8 -2
- package/dist/runtime/pm/features/DocumentApi/composables/useTestDocumentApi.d.ts +4 -1
- package/dist/runtime/pm/features/DocumentApi/composables/useTestDocumentApi.js +39 -8
- package/dist/runtime/pm/features/DocumentApi/types.d.ts +26 -48
- package/dist/runtime/pm/features/Link/components/BubbleMenuExternalLink.vue +0 -2
- package/dist/runtime/pm/features/Link/components/BubbleMenuInternalLink.vue +0 -1
- package/package.json +38 -37
- package/src/module.ts +3 -3
- package/src/runtime/components/EditorDemoApp.vue +32 -1
- package/src/runtime/components/EditorDemoControls.vue +57 -12
- package/src/runtime/pm/features/Base/plugins/debugSelectionPlugin.ts +53 -28
- package/src/runtime/pm/features/CodeBlock/components/CodeBlockView.vue +0 -1
- package/src/runtime/pm/features/Collaboration/Collaboration.ts +19 -286
- package/src/runtime/pm/features/Collaboration/createCollaborationPlugins.ts +132 -0
- package/src/runtime/pm/features/DocumentApi/DocumentApi.ts +35 -5
- package/src/runtime/pm/features/DocumentApi/composables/useEditorContent.ts +8 -2
- package/src/runtime/pm/features/DocumentApi/composables/useTestDocumentApi.ts +56 -8
- package/src/runtime/pm/features/DocumentApi/types.ts +30 -52
- package/src/runtime/pm/features/Link/components/BubbleMenuExternalLink.vue +0 -2
- package/src/runtime/pm/features/Link/components/BubbleMenuInternalLink.vue +0 -1
- package/src/runtime/pm/utils/generator/createPsuedoSentence.ts +1 -1
- package/dist/runtime/demo/App.d.vue.ts +0 -3
- package/dist/runtime/demo/App.vue +0 -100
- package/dist/runtime/demo/App.vue.d.ts +0 -3
- package/src/runtime/demo/App.vue +0 -113
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { CollaborationOptions } from "@tiptap/extension-collaboration"
|
|
2
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state"
|
|
3
|
+
import type { EditorView } from "@tiptap/pm/view"
|
|
4
|
+
import {
|
|
5
|
+
ySyncPlugin,
|
|
6
|
+
yUndoPlugin,
|
|
7
|
+
yUndoPluginKey,
|
|
8
|
+
yXmlFragmentToProsemirrorJSON
|
|
9
|
+
} from "@tiptap/y-tiptap"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Copied from tiptap's collaboration extension with a few minor changes to make it work with our document api.
|
|
13
|
+
*
|
|
14
|
+
* Changes:
|
|
15
|
+
*
|
|
16
|
+
* - A local object is created for the plugin instance to simulate the extension's storage.
|
|
17
|
+
* - When it needs the editor (for the `contentError` event), every editor currently using the doc is iterated over and sent the event.
|
|
18
|
+
* - The normally editor level `enableContentCheck` option should be set here, it has no effect if passed to the extension.
|
|
19
|
+
*
|
|
20
|
+
* See {@link Collaboration} for more info and {@link useTestDocumentApi} for an example of how to use it.
|
|
21
|
+
*/
|
|
22
|
+
export function createCollaborationPlugins(
|
|
23
|
+
options: CollaborationOptions & { enableContentCheck: boolean },
|
|
24
|
+
schema: any,
|
|
25
|
+
getConnectedEditors?: () => any
|
|
26
|
+
): Plugin[] {
|
|
27
|
+
const storage = {
|
|
28
|
+
isDisabled: false
|
|
29
|
+
}
|
|
30
|
+
const fragment = options.fragment
|
|
31
|
+
? options.fragment
|
|
32
|
+
: options.document?.getXmlFragment(options.field)
|
|
33
|
+
|
|
34
|
+
if (!fragment) {
|
|
35
|
+
throw new Error("No fragment or document provided.")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Quick fix until there is an official implementation (thanks to @hamflx).
|
|
39
|
+
// See https://github.com/yjs/y-prosemirror/issues/114 and https://github.com/yjs/y-prosemirror/issues/102
|
|
40
|
+
const yUndoPluginInstance = yUndoPlugin(options.yUndoOptions)
|
|
41
|
+
const originalUndoPluginView = yUndoPluginInstance.spec.view
|
|
42
|
+
|
|
43
|
+
yUndoPluginInstance.spec.view = (view: EditorView) => {
|
|
44
|
+
const { undoManager } = yUndoPluginKey.getState(view.state)
|
|
45
|
+
|
|
46
|
+
if (undoManager.restore) {
|
|
47
|
+
undoManager.restore()
|
|
48
|
+
undoManager.restore = () => {
|
|
49
|
+
// noop
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const viewRet = originalUndoPluginView ? originalUndoPluginView(view) : undefined
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
destroy: () => {
|
|
57
|
+
const hasUndoManSelf = undoManager.trackedOrigins.has(undoManager)
|
|
58
|
+
|
|
59
|
+
const observers = undoManager._observers
|
|
60
|
+
|
|
61
|
+
undoManager.restore = () => {
|
|
62
|
+
if (hasUndoManSelf) {
|
|
63
|
+
undoManager.trackedOrigins.add(undoManager)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
undoManager.doc.on("afterTransaction", undoManager.afterTransactionHandler)
|
|
67
|
+
|
|
68
|
+
undoManager._observers = observers
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (viewRet?.destroy) {
|
|
72
|
+
viewRet.destroy()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const ySyncPluginOptions: Parameters<typeof ySyncPlugin>[1] = {
|
|
79
|
+
...options.ySyncOptions,
|
|
80
|
+
onFirstRender: options.onFirstRender
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const ySyncPluginInstance = ySyncPlugin(fragment, ySyncPluginOptions)
|
|
84
|
+
|
|
85
|
+
if (options.enableContentCheck) {
|
|
86
|
+
fragment.doc?.on("beforeTransaction", () => {
|
|
87
|
+
try {
|
|
88
|
+
const jsonContent = yXmlFragmentToProsemirrorJSON(fragment)
|
|
89
|
+
|
|
90
|
+
if (jsonContent.content.length === 0) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
schema.nodeFromJSON(jsonContent).check()
|
|
95
|
+
} catch (error) {
|
|
96
|
+
for (const editor of (getConnectedEditors?.() ?? [])) {
|
|
97
|
+
editor.emit("contentError", {
|
|
98
|
+
error: error as Error,
|
|
99
|
+
editor: editor,
|
|
100
|
+
disableCollaboration: () => {
|
|
101
|
+
fragment.doc?.destroy()
|
|
102
|
+
storage.isDisabled = true
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
// If the content is invalid, return false to prevent the transaction from being applied
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return [
|
|
113
|
+
ySyncPluginInstance,
|
|
114
|
+
yUndoPluginInstance,
|
|
115
|
+
options.enableContentCheck
|
|
116
|
+
&& new Plugin({
|
|
117
|
+
key: new PluginKey("filterInvalidContent"),
|
|
118
|
+
filterTransaction: () => {
|
|
119
|
+
// When collaboration is disabled, prevent any sync transactions from being applied
|
|
120
|
+
if (storage.isDisabled !== false) {
|
|
121
|
+
// Destroy the Yjs document to prevent any further sync transactions
|
|
122
|
+
fragment.doc?.destroy()
|
|
123
|
+
|
|
124
|
+
return true
|
|
125
|
+
}
|
|
126
|
+
// TODO should we be returning false when the transaction is a collaboration transaction?
|
|
127
|
+
|
|
128
|
+
return true
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
].filter(Boolean)
|
|
132
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { debounce, type DebounceQueue } from "@alanscodelog/utils/debounce"
|
|
2
2
|
import { unreachable } from "@alanscodelog/utils/unreachable"
|
|
3
3
|
import type { Content, EditorOptions } from "@tiptap/core"
|
|
4
|
+
import { Editor } from "@tiptap/core"
|
|
4
5
|
import type { Schema } from "@tiptap/pm/model"
|
|
5
6
|
import type { EditorState, Plugin, Transaction } from "@tiptap/pm/state"
|
|
6
|
-
import { Editor } from "@tiptap/vue-3"
|
|
7
7
|
import { isProxy } from "vue"
|
|
8
8
|
|
|
9
9
|
import type { DocId, DocumentApiInterface, EmbedId, OnSaveDocumentCallback, OnUpdateDocumentCallback } from "./types.js"
|
|
@@ -30,7 +30,17 @@ import { getEmbedNodeFromDoc } from "./utils/getEmbedNodeFromDoc.js"
|
|
|
30
30
|
*
|
|
31
31
|
* If there are extensions that use onCreate to set state or have plugins that need to change the state on init with appendTransaction, they will not work since there is no view to initialize the plugins. To get around this, plugins can specify a stateInit function that will be called with a transaction from the initial loaded state which it can then modify while having access to `this` and the extension options.
|
|
32
32
|
*
|
|
33
|
-
*
|
|
33
|
+
* ### Internals / How does it work?
|
|
34
|
+
*
|
|
35
|
+
* It can be a bit confusing how this works. It's quite different from how tiptap works.
|
|
36
|
+
*
|
|
37
|
+
* First, the api creates a **single** instance of the editor with the editor options **initially** provided (it can be replaced by passing your own editor instance).
|
|
38
|
+
*
|
|
39
|
+
* On load **per document**, the state of the doc is initialized. The plugins are copied from the single editor instance so that we can properly initialize the state. The plugin's `stateInit` is called, see above.
|
|
40
|
+
*
|
|
41
|
+
* When an editor **component** is mounted, useEditorContent will call the document api's `preEditorInit` to get a configuration for that component. This configuration can be edited in `preEditorInit` and is **per editor component**. It is disconnected from the **per document** configuration.
|
|
42
|
+
*
|
|
43
|
+
* There is no **per document** editor instance. This makes it tricky to work with extensions like Collaboration that expect this setup. They require some weird workarounds unfortunately.
|
|
34
44
|
*
|
|
35
45
|
* See {@link useTestDocumentApi} for an example of how to set things up.
|
|
36
46
|
*/
|
|
@@ -43,7 +53,7 @@ export class DocumentApi<
|
|
|
43
53
|
saving: OnSaveDocumentCallback[]
|
|
44
54
|
} = { update: [], saved: [], saving: [] }
|
|
45
55
|
|
|
46
|
-
private readonly _load: (docId: string, schema: Schema, plugins: Plugin[]) => Promise<{ state: EditorState, data?: T }>
|
|
56
|
+
private readonly _load: (docId: string, schema: Schema, plugins: Plugin[], getConnectedEditors: () => Editor[]) => Promise<{ state: EditorState, data?: T }>
|
|
47
57
|
|
|
48
58
|
private readonly _save?: (docId: string) => Promise<void>
|
|
49
59
|
|
|
@@ -84,6 +94,8 @@ export class DocumentApi<
|
|
|
84
94
|
|
|
85
95
|
editor: Editor
|
|
86
96
|
|
|
97
|
+
connectedEditors: Record<string, Editor[]> = {}
|
|
98
|
+
|
|
87
99
|
constructor({
|
|
88
100
|
editorOptions,
|
|
89
101
|
getTitle,
|
|
@@ -104,7 +116,7 @@ export class DocumentApi<
|
|
|
104
116
|
getTitle?: (docId: string, blockId?: string) => string
|
|
105
117
|
getSuggestions: DocumentApiInterface["getSuggestions"]
|
|
106
118
|
/** Load should create the editor state and return it. It can also optionally return extra data which will be passed to the refCounter's load function. */
|
|
107
|
-
load: (docId: string, schema: Schema, plugins: Plugin[]) => Promise<{ state: EditorState, data?: T }>
|
|
119
|
+
load: (docId: string, schema: Schema, plugins: Plugin[], getConnectedEditors: () => Editor[]) => Promise<{ state: EditorState, data?: T }>
|
|
108
120
|
save?: DocumentApi["_save"]
|
|
109
121
|
saveDebounce?: number
|
|
110
122
|
cache: DocumentApi["_cache"]
|
|
@@ -236,7 +248,8 @@ export class DocumentApi<
|
|
|
236
248
|
const loaded = await this._load(
|
|
237
249
|
docId,
|
|
238
250
|
schema,
|
|
239
|
-
[...this.editor.extensionManager.plugins]
|
|
251
|
+
[...this.editor.extensionManager.plugins],
|
|
252
|
+
() => this.connectedEditors[docId] ?? []
|
|
240
253
|
)
|
|
241
254
|
let state = loaded.state
|
|
242
255
|
const tr = state.tr
|
|
@@ -282,4 +295,21 @@ export class DocumentApi<
|
|
|
282
295
|
return getEmbedJson(nodeWanted) ?? json
|
|
283
296
|
}
|
|
284
297
|
}
|
|
298
|
+
|
|
299
|
+
connectEditor(docId: string, editor: Editor) {
|
|
300
|
+
if (!this.connectedEditors[docId]) {
|
|
301
|
+
this.connectedEditors[docId] = []
|
|
302
|
+
}
|
|
303
|
+
this.connectedEditors[docId].push(editor)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
disconnectEditor(docId: string, editor: Editor) {
|
|
307
|
+
if (!this.connectedEditors[docId]) {
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
const index = this.connectedEditors[docId].indexOf(editor)
|
|
311
|
+
if (index !== -1) {
|
|
312
|
+
this.connectedEditors[docId].splice(index, 1)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
285
315
|
}
|
|
@@ -50,7 +50,11 @@ export function useEditorContent(
|
|
|
50
50
|
|
|
51
51
|
async function load(changed: boolean): Promise<void> {
|
|
52
52
|
if (content.value) {
|
|
53
|
-
editor.value
|
|
53
|
+
if (!editor.value) {
|
|
54
|
+
recreate(options => ({ ...options, content: content.value }))
|
|
55
|
+
} else {
|
|
56
|
+
editor.value?.commands.setContent(content.value)
|
|
57
|
+
}
|
|
54
58
|
} else if (documentApi) {
|
|
55
59
|
if (changed && id.value) {
|
|
56
60
|
await documentApi.load({ docId: id.value })
|
|
@@ -58,6 +62,7 @@ export function useEditorContent(
|
|
|
58
62
|
|
|
59
63
|
recreate(options => documentApi.preEditorInit?.(id.value!, { ...options }, state))
|
|
60
64
|
await nextTick(async () => {
|
|
65
|
+
documentApi.connectEditor(id.value!, editor.value!)
|
|
61
66
|
editor.value!.on("transaction", onTransaction)
|
|
62
67
|
documentApi!.addEventListener("update", onUpdateDocument)
|
|
63
68
|
documentApi.postEditorInit(id.value!, editor.value!)
|
|
@@ -69,6 +74,7 @@ export function useEditorContent(
|
|
|
69
74
|
function unload(oldId: string): void {
|
|
70
75
|
if (oldId !== undefined && documentApi) {
|
|
71
76
|
documentApi.unload({ docId: oldId })
|
|
77
|
+
documentApi.disconnectEditor(id.value!, editor.value!)
|
|
72
78
|
if (attached) {
|
|
73
79
|
documentApi.removeEventListener("update", onUpdateDocument)
|
|
74
80
|
editor.value?.off("transaction", onTransaction)
|
|
@@ -91,7 +97,7 @@ export function useEditorContent(
|
|
|
91
97
|
if (oldId !== undefined && newId !== oldId && documentApi) {
|
|
92
98
|
unload(oldId)
|
|
93
99
|
await load(true)
|
|
94
|
-
} else if (_newContent) {
|
|
100
|
+
} else if (_newContent !== _oldContent && _newContent) {
|
|
95
101
|
await load(false)
|
|
96
102
|
}
|
|
97
103
|
}, { deep: false })
|
|
@@ -1,24 +1,34 @@
|
|
|
1
1
|
import { delay } from "@alanscodelog/utils/delay"
|
|
2
2
|
import { keys } from "@alanscodelog/utils/keys"
|
|
3
3
|
import { unreachable } from "@alanscodelog/utils/unreachable"
|
|
4
|
-
import type { EditorOptions } from "@tiptap/core"
|
|
4
|
+
import type { Editor, EditorOptions } from "@tiptap/core"
|
|
5
5
|
import { createDocument, generateJSON } from "@tiptap/core"
|
|
6
6
|
import type { Schema } from "@tiptap/pm/model"
|
|
7
7
|
import type { Plugin } from "@tiptap/pm/state"
|
|
8
8
|
import { EditorState } from "@tiptap/pm/state"
|
|
9
|
+
import { prosemirrorToYDoc } from "@tiptap/y-tiptap"
|
|
9
10
|
import { type Ref, ref, toRaw } from "vue"
|
|
11
|
+
import type * as Y from "yjs"
|
|
10
12
|
|
|
11
13
|
import { testExtensions } from "../../../testSchema.js"
|
|
14
|
+
import { Collaboration } from "../../Collaboration/Collaboration.js"
|
|
15
|
+
import { createCollaborationPlugins } from "../../Collaboration/createCollaborationPlugins.js"
|
|
12
16
|
import { DocumentApi } from "../DocumentApi.js"
|
|
13
17
|
import type { DocumentApiInterface } from "../types.js"
|
|
14
18
|
|
|
15
|
-
type Cache = Record<string, { state?: EditorState, count: number }>
|
|
19
|
+
type Cache = Record<string, { state?: EditorState, count: number, yDoc?: Y.Doc }>
|
|
16
20
|
|
|
17
21
|
/** Creates a simple instance of the DocumentApi for testing purposes. */
|
|
18
22
|
export function useTestDocumentApi(
|
|
19
23
|
editorOptions: Partial<EditorOptions>,
|
|
20
24
|
embeds: Record<string, { content: any, title?: string }>,
|
|
21
|
-
{
|
|
25
|
+
{
|
|
26
|
+
useCollab = false,
|
|
27
|
+
loadDelay = 0
|
|
28
|
+
}: {
|
|
29
|
+
useCollab?: boolean
|
|
30
|
+
loadDelay?: number
|
|
31
|
+
} = {}
|
|
22
32
|
): {
|
|
23
33
|
cache: Ref<Cache>
|
|
24
34
|
documentApi: DocumentApiInterface
|
|
@@ -51,11 +61,29 @@ export function useTestDocumentApi(
|
|
|
51
61
|
cache.value[docId].state = state
|
|
52
62
|
}
|
|
53
63
|
},
|
|
64
|
+
preEditorInit(docId, options: Partial<EditorOptions>) {
|
|
65
|
+
if (!cache.value[docId]) unreachable()
|
|
66
|
+
const yDoc = cache.value[docId].yDoc
|
|
67
|
+
if (useCollab && !yDoc) unreachable()
|
|
68
|
+
if (!cache.value[docId].state) unreachable()
|
|
69
|
+
|
|
70
|
+
const perDoc = ["history"]
|
|
71
|
+
|
|
72
|
+
options.content = cache.value[docId].state.doc.toJSON()
|
|
73
|
+
options.extensions = [
|
|
74
|
+
...(options.extensions ?? []).filter(ext => !perDoc.includes(ext.name)),
|
|
75
|
+
...(useCollab
|
|
76
|
+
? [Collaboration]
|
|
77
|
+
: [])
|
|
78
|
+
]
|
|
79
|
+
return options
|
|
80
|
+
},
|
|
54
81
|
load: async (
|
|
55
82
|
docId: string,
|
|
56
83
|
schema: Schema,
|
|
57
|
-
plugins: Plugin[]
|
|
58
|
-
|
|
84
|
+
plugins: Plugin[],
|
|
85
|
+
getConnectedEditors: () => Editor[]
|
|
86
|
+
): Promise<{ state: EditorState, data?: { yDoc?: Y.Doc } }> => {
|
|
59
87
|
if (loadDelay) {
|
|
60
88
|
await delay(loadDelay)
|
|
61
89
|
}
|
|
@@ -64,19 +92,39 @@ export function useTestDocumentApi(
|
|
|
64
92
|
throw new Error(`No embed found for docId ${docId} in: ${JSON.stringify(embeds, null, "\t")}`)
|
|
65
93
|
}
|
|
66
94
|
|
|
95
|
+
if (cache.value[docId]?.state) {
|
|
96
|
+
return { state: toRaw(cache.value[docId].state) as any, data: { yDoc: cache.value[docId].yDoc } }
|
|
97
|
+
}
|
|
98
|
+
|
|
67
99
|
const json = generateJSON(embeds[docId].content as any, testExtensions)
|
|
68
100
|
const doc = createDocument(json, schema)
|
|
101
|
+
const yDoc = useCollab ? prosemirrorToYDoc(doc, "prosemirror") : undefined
|
|
102
|
+
|
|
69
103
|
const state = EditorState.create({
|
|
70
104
|
doc,
|
|
71
105
|
schema,
|
|
72
|
-
plugins
|
|
106
|
+
plugins: [
|
|
107
|
+
...plugins,
|
|
108
|
+
...(useCollab
|
|
109
|
+
? createCollaborationPlugins(
|
|
110
|
+
{
|
|
111
|
+
document: yDoc,
|
|
112
|
+
field: "prosemirror",
|
|
113
|
+
enableContentCheck: true
|
|
114
|
+
},
|
|
115
|
+
schema,
|
|
116
|
+
getConnectedEditors
|
|
117
|
+
)
|
|
118
|
+
: [])
|
|
119
|
+
]
|
|
73
120
|
})
|
|
74
|
-
|
|
121
|
+
|
|
122
|
+
return { state, data: { yDoc } } as any
|
|
75
123
|
},
|
|
76
124
|
refCounter: {
|
|
77
125
|
load(docId: string, loaded) {
|
|
78
126
|
// loaded.data can be accessed here if we need it
|
|
79
|
-
cache.value[docId] ??= { ...loaded, count: 0 }
|
|
127
|
+
cache.value[docId] ??= { ...loaded, yDoc: loaded.data!.yDoc, count: 0 }
|
|
80
128
|
cache.value[docId].count++
|
|
81
129
|
},
|
|
82
130
|
unload: (docId: string) => {
|
|
@@ -33,16 +33,33 @@ export type DocumentApiInterface<
|
|
|
33
33
|
getFromCache: (docId: DocId, options?: { errorIfNotFound?: boolean }) => EditorState | undefined
|
|
34
34
|
/** Load should be called the first time, before attempting to load the state. */
|
|
35
35
|
getFullState: (docId: DocId) => EditorState
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* For replacing {@link DocumentApi.preEditorInit} which runs after initializing and loading the document but before the transaction listeners are added.
|
|
38
|
+
*
|
|
39
|
+
* Can be used to add the Collaboration extension for example (see useTestDocumentApi for an example).
|
|
40
|
+
*
|
|
41
|
+
* The default implementation just sets the content:
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* preEditorInit: (_docId, options, state) => {
|
|
45
|
+
* options.content = state.doc.toJSON()
|
|
46
|
+
* return options
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
*/
|
|
37
51
|
postEditorInit: (docId: string, editor: Editor) => void
|
|
52
|
+
connectedEditors: Record<string, Editor[]>
|
|
53
|
+
connectEditor: (docId: string, editor: Editor) => void
|
|
54
|
+
disconnectEditor: (docId: string, editor: Editor) => void
|
|
38
55
|
/**
|
|
39
|
-
* Sets options before initializing the editor. By default just does `options.content = state.doc.toJSON()`, but can be useful
|
|
56
|
+
* Sets options before initializing the editor. By default just does `options.content = state.doc.toJSON()`, but can be useful for using **per editor component** plugins.
|
|
40
57
|
*
|
|
41
|
-
*
|
|
58
|
+
* This is normally a bit tricky to do since the editor component initializes the editor before the document is loaded and is re-used (the wrapper Editor *component*, not the editor) when the document changes.
|
|
42
59
|
*
|
|
43
|
-
*
|
|
60
|
+
* So this hook can be used to add these additional per-editor instances of extensions. Be sure to clone the properties you are modifying. They are only shallow cloned before being passed to the function.
|
|
44
61
|
*
|
|
45
|
-
*
|
|
62
|
+
* If you need **per doc** plugins use `load` instead. See {@link useTestDocumentApi} for an example.
|
|
46
63
|
*
|
|
47
64
|
* ```ts
|
|
48
65
|
* preEditorInit(docId, options: Partial<EditorOptions>, state: EditorState) {
|
|
@@ -51,58 +68,19 @@ export type DocumentApiInterface<
|
|
|
51
68
|
* const ydoc = cache.value[docId].ydoc
|
|
52
69
|
* // it's suggested you add the collab extension only here
|
|
53
70
|
* // otherwise you would have to initially configure it with a dummy document
|
|
54
|
-
* const collabExt = Collaboration.configure({
|
|
55
|
-
* document: ydoc
|
|
56
|
-
* }) as any
|
|
57
71
|
* options.extensions = [
|
|
58
72
|
* ...(options.extensions ?? []),
|
|
59
|
-
*
|
|
73
|
+
* // per editor extensions
|
|
60
74
|
* ]
|
|
61
75
|
* return options
|
|
62
76
|
* },
|
|
63
|
-
* load: async (
|
|
64
|
-
* docId: string,
|
|
65
|
-
* schema: Schema,
|
|
66
|
-
* plugins: Plugin[],
|
|
67
|
-
* ) => {
|
|
68
|
-
* if (cache.value[docId]?.state) {
|
|
69
|
-
* return { state: toRaw(cache.value[docId].state) }
|
|
70
|
-
* }
|
|
71
|
-
* const doc = getFromYourDb(docId)
|
|
72
|
-
* const decoded = toUint8Array(doc.contentBinary)
|
|
73
|
-
* const yDoc = new Y.Doc()
|
|
74
|
-
* Y.applyUpdate(yDoc, decoded)
|
|
75
|
-
*
|
|
76
|
-
* const yjs = initProseMirrorDoc(yDoc.getXmlFragment("prosemirror"), schema)
|
|
77
|
-
* const state = EditorState.create({
|
|
78
|
-
* doc: yjs.doc,
|
|
79
|
-
* schema,
|
|
80
|
-
* plugins:[
|
|
81
|
-
* ...plugins,
|
|
82
|
-
* // the document api's yjs instance
|
|
83
|
-
* ySyncPlugin(yDoc.getXmlFragment("prosemirror"), {mapping:yjs.mapping}),
|
|
84
|
-
* ]
|
|
85
|
-
* })
|
|
86
|
-
* // return the state and any additional data we want refCounter.load to be called with.
|
|
87
|
-
* return { state, doc, yDoc }
|
|
88
|
-
* },
|
|
89
|
-
* updateFilter(tr:Transaction) {
|
|
90
|
-
* const meta = tr.getMeta(ySyncPluginKey)
|
|
91
|
-
* if (meta) return false
|
|
92
|
-
* return true
|
|
93
|
-
* },
|
|
94
77
|
* ```
|
|
95
|
-
* See {@link DocumentApi.updateFilter} for why yjs (and other syncronization mechanisms) might need to ignore transactions.
|
|
96
78
|
*/
|
|
97
79
|
preEditorInit: (docId: string, options: Partial<EditorOptions>, state: EditorState) => Partial<EditorOptions>
|
|
98
80
|
/**
|
|
99
|
-
* Return false to
|
|
100
|
-
*
|
|
101
|
-
* This is useful when using a secondary syncronization mechanism, such as yjs.
|
|
102
|
-
*
|
|
103
|
-
* If you load all editors of a file with yjs's plugin and point to the same ydoc, yjs's plugin will sync them. But that means that when the DocumentApi tries to sync the transactions they will have already been applied and the document update will fail.
|
|
81
|
+
* Return false to prevent applying the transaction to the state in the cache.
|
|
104
82
|
*
|
|
105
|
-
*
|
|
83
|
+
* This used to be needed to ignore yjs transactions, but that's no longer the case. Even with multiple editors loaded to use the same ydoc, everything should work. Leaving the option in case it's needed for some other rare use case.
|
|
106
84
|
*/
|
|
107
85
|
updateFilter?: (tr: Transaction) => boolean | undefined
|
|
108
86
|
updateDocument: (
|
|
@@ -110,10 +88,10 @@ export type DocumentApiInterface<
|
|
|
110
88
|
tr: Transaction,
|
|
111
89
|
selfSymbol?: symbol
|
|
112
90
|
) => void
|
|
113
|
-
addEventListener
|
|
114
|
-
addEventListener
|
|
115
|
-
removeEventListener
|
|
116
|
-
removeEventListener
|
|
91
|
+
addEventListener(type: "saving" | "saved", cb: OnSaveDocumentCallback): void
|
|
92
|
+
addEventListener(type: "update", cb: OnUpdateDocumentCallback): void
|
|
93
|
+
removeEventListener(type: "saving" | "saved", cb: OnSaveDocumentCallback): void
|
|
94
|
+
removeEventListener(type: "update", cb: OnUpdateDocumentCallback): void
|
|
117
95
|
|
|
118
96
|
/** For the embedded document picker, should return suggestions for the search string. */
|
|
119
97
|
getSuggestions: (searchString: string) => Promise<{ title: string, docId: string }[]>
|
|
@@ -123,7 +101,7 @@ export type DocumentApiInterface<
|
|
|
123
101
|
* Tells the document api how to load an unloaded document and any additional data. Whatever this function returns will be passed to the refCounter.load option in the default DocumentApi implementation.
|
|
124
102
|
*
|
|
125
103
|
* ```ts
|
|
126
|
-
* load: async ( docId: string, schema: Schema, plugins: Plugin[],) => {
|
|
104
|
+
* load: async ( docId: string, schema: Schema, plugins: Plugin[], getConnectedEditors: () => Editor[]) => {
|
|
127
105
|
* const dbDoc = getFromYourDb(docId)
|
|
128
106
|
*
|
|
129
107
|
* const state = EditorState.create({
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
ref="el"
|
|
5
5
|
>
|
|
6
6
|
<div class="flex flex-col gap-1">
|
|
7
|
-
<!-- @vue-expect-error -->
|
|
8
7
|
<WSimpleInput
|
|
9
8
|
class="text-input"
|
|
10
9
|
wrapper-class="flex flex-nowrap gap-1"
|
|
@@ -24,7 +23,6 @@
|
|
|
24
23
|
</WLabel>
|
|
25
24
|
</template>
|
|
26
25
|
</WSimpleInput>
|
|
27
|
-
<!-- @vue-expect-error -->
|
|
28
26
|
<WSimpleInput
|
|
29
27
|
class="link-input"
|
|
30
28
|
wrapper-class="flex flex-nowrap gap-1"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { faker } from "@faker-js/faker"
|
|
2
2
|
|
|
3
|
-
export function createPsuedoSentence({ min = 0, max = 10}: { min?: number, max?: number } = {}) {
|
|
3
|
+
export function createPsuedoSentence({ min = 0, max = 10 }: { min?: number, max?: number } = {}) {
|
|
4
4
|
// sentence generated with string.sample (which contains all possible chars) instead of lorem.sentence
|
|
5
5
|
const sentenceLength = faker.number.int({ min, max })
|
|
6
6
|
const sentence = Array.from(
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
-
declare const _default: typeof __VLS_export;
|
|
3
|
-
export default _default;
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<WRoot class="items-center py-4">
|
|
3
|
-
<style>
|
|
4
|
-
{{ codeBlocksThemeCss.join("\n") }}
|
|
5
|
-
</style>
|
|
6
|
-
<DemoControls
|
|
7
|
-
:code-blocks-theme-list="codeBlocksThemeList"
|
|
8
|
-
v-model:code-blocks-theme="codeBlocksTheme"
|
|
9
|
-
@blur="blur"
|
|
10
|
-
/>
|
|
11
|
-
<Editor
|
|
12
|
-
class="max-w-[700px] flex-1 max-h-[700px] flex"
|
|
13
|
-
v-bind="{
|
|
14
|
-
codeBlocksThemeIsDark,
|
|
15
|
-
cssVariables: {
|
|
16
|
-
pmCodeBlockBgColor: codeBlocksThemeBgColor
|
|
17
|
-
},
|
|
18
|
-
docId,
|
|
19
|
-
documentApi,
|
|
20
|
-
linkOptions,
|
|
21
|
-
editorOptions,
|
|
22
|
-
menus
|
|
23
|
-
}"
|
|
24
|
-
/>
|
|
25
|
-
<div class="py-[50px]"/>
|
|
26
|
-
</WRoot>
|
|
27
|
-
</template>
|
|
28
|
-
|
|
29
|
-
<script setup>
|
|
30
|
-
import WRoot from "@witchcraft/ui/components/LibRoot";
|
|
31
|
-
import { nextTick, reactive, ref, shallowRef } from "vue";
|
|
32
|
-
import Editor from "../components/Editor.vue";
|
|
33
|
-
import DemoControls from "../components/EditorDemoControls.vue";
|
|
34
|
-
import { useHighlightJsTheme } from "../pm/features/CodeBlock/composables/useHighlightJsTheme.js";
|
|
35
|
-
import { defaultCommandBarMenuItems } from "../pm/features/CommandsMenus/commandBarMenuItems";
|
|
36
|
-
import CommandBar from "../pm/features/CommandsMenus/components/CommandBar.vue";
|
|
37
|
-
import { useTestDocumentApi } from "../pm/features/DocumentApi/composables/useTestDocumentApi.js";
|
|
38
|
-
import BubbleMenuLink from "../pm/features/Link/components/BubbleMenuLink.vue";
|
|
39
|
-
import { testExtensions } from "../pm/testSchema.js";
|
|
40
|
-
import { testDocuments } from "../testDocuments";
|
|
41
|
-
const {
|
|
42
|
-
theme: codeBlocksTheme,
|
|
43
|
-
knownThemes: codeBlocksThemeList,
|
|
44
|
-
themeCss: codeBlocksThemeCss,
|
|
45
|
-
isDark: codeBlocksThemeIsDark,
|
|
46
|
-
backgroundColor: codeBlocksThemeBgColor
|
|
47
|
-
} = useHighlightJsTheme();
|
|
48
|
-
function blur() {
|
|
49
|
-
const was = codeBlocksTheme.value;
|
|
50
|
-
codeBlocksTheme.value = "";
|
|
51
|
-
nextTick(() => {
|
|
52
|
-
codeBlocksTheme.value = was;
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
const editorOptions = {
|
|
56
|
-
extensions: testExtensions
|
|
57
|
-
};
|
|
58
|
-
const linkOptions = {
|
|
59
|
-
openInternal: (href) => {
|
|
60
|
-
window.alert(`This would open an internal link to ${href}.`);
|
|
61
|
-
console.log(`Would open internal link to ${href}.`);
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
const fakeSuggestions = reactive(["some", "suggestions"]);
|
|
65
|
-
const menus = shallowRef({
|
|
66
|
-
linkMenu: {
|
|
67
|
-
component: BubbleMenuLink,
|
|
68
|
-
props: (editor) => ({
|
|
69
|
-
editor,
|
|
70
|
-
linkSuggestions: fakeSuggestions,
|
|
71
|
-
getInternalLinkHref(href) {
|
|
72
|
-
return `internal://${href.replace(/[^\w-]/g, "")}`;
|
|
73
|
-
}
|
|
74
|
-
})
|
|
75
|
-
},
|
|
76
|
-
commandBar: {
|
|
77
|
-
component: CommandBar,
|
|
78
|
-
props: (editor) => ({
|
|
79
|
-
editor,
|
|
80
|
-
commands: defaultCommandBarMenuItems.commands
|
|
81
|
-
}),
|
|
82
|
-
popupOptions: {
|
|
83
|
-
pinToItemDistance: (state) => {
|
|
84
|
-
const { $from, $to } = state.selection;
|
|
85
|
-
const fromNode = $from.node(-1);
|
|
86
|
-
const toNode = $to.node(-1);
|
|
87
|
-
if (fromNode.type !== toNode.type) {
|
|
88
|
-
return 0;
|
|
89
|
-
}
|
|
90
|
-
return fromNode.type.name.startsWith("table") ? 120 : 0;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
const { documentApi } = useTestDocumentApi(
|
|
96
|
-
editorOptions,
|
|
97
|
-
testDocuments
|
|
98
|
-
);
|
|
99
|
-
const docId = ref("root");
|
|
100
|
-
</script>
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
-
declare const _default: typeof __VLS_export;
|
|
3
|
-
export default _default;
|