@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
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
<EditorDemoControls
|
|
11
11
|
:code-blocks-theme-list="codeBlocksThemeList"
|
|
12
12
|
v-model:code-blocks-theme="codeBlocksTheme"
|
|
13
|
+
v-model:use-two-editors="useTwoEditors"
|
|
13
14
|
/>
|
|
15
|
+
|
|
14
16
|
<Editor
|
|
15
17
|
class="
|
|
16
18
|
max-w-[700px]
|
|
@@ -34,6 +36,30 @@
|
|
|
34
36
|
menus
|
|
35
37
|
}"
|
|
36
38
|
/>
|
|
39
|
+
<Editor
|
|
40
|
+
v-if="useTwoEditors"
|
|
41
|
+
class="
|
|
42
|
+
max-w-[700px]
|
|
43
|
+
flex-1
|
|
44
|
+
flex
|
|
45
|
+
border
|
|
46
|
+
border-neutral-300
|
|
47
|
+
dark:border-neutral-700
|
|
48
|
+
rounded-sm
|
|
49
|
+
min-h-0
|
|
50
|
+
"
|
|
51
|
+
v-bind="{
|
|
52
|
+
codeBlocksThemeIsDark,
|
|
53
|
+
cssVariables: {
|
|
54
|
+
pmCodeBlockBgColor: codeBlocksThemeBgColor
|
|
55
|
+
},
|
|
56
|
+
docId,
|
|
57
|
+
documentApi,
|
|
58
|
+
linkOptions,
|
|
59
|
+
editorOptions,
|
|
60
|
+
menus
|
|
61
|
+
}"
|
|
62
|
+
/>
|
|
37
63
|
</WRoot>
|
|
38
64
|
</template>
|
|
39
65
|
|
|
@@ -41,6 +67,7 @@
|
|
|
41
67
|
// all imports must be explicit so this also works without nuxt
|
|
42
68
|
import type { EditorOptions } from "@tiptap/core"
|
|
43
69
|
import WRoot from "@witchcraft/ui/components/LibRoot"
|
|
70
|
+
import { useRoute } from "nuxt/app"
|
|
44
71
|
import { reactive, ref, shallowRef } from "vue"
|
|
45
72
|
|
|
46
73
|
import Editor from "./Editor.vue"
|
|
@@ -110,9 +137,13 @@ const menus = shallowRef<Record<string, MenuRenderInfo>>({
|
|
|
110
137
|
}
|
|
111
138
|
})
|
|
112
139
|
|
|
140
|
+
const useYjs = useRoute().query.useYjs as string
|
|
141
|
+
const useTwoEditors = ref(false)
|
|
142
|
+
|
|
113
143
|
const { documentApi } = useTestDocumentApi(
|
|
114
144
|
editorOptions as any,
|
|
115
|
-
testDocuments
|
|
145
|
+
testDocuments,
|
|
146
|
+
{ useCollab: useYjs === "true" }
|
|
116
147
|
)
|
|
117
148
|
const docId = ref("root")
|
|
118
149
|
</script>
|
|
@@ -1,33 +1,74 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
3
|
class="
|
|
4
|
+
max-w-[700px]
|
|
4
5
|
w-full
|
|
5
6
|
flex
|
|
6
|
-
|
|
7
|
+
flex-col
|
|
8
|
+
items-center
|
|
7
9
|
justify-center
|
|
8
10
|
gap-2
|
|
9
|
-
|
|
11
|
+
"
|
|
12
|
+
>
|
|
13
|
+
<div
|
|
14
|
+
class="
|
|
15
|
+
flex
|
|
16
|
+
gap-4
|
|
17
|
+
items-center
|
|
18
|
+
justify-center
|
|
19
|
+
border
|
|
20
|
+
border-neutral-300
|
|
21
|
+
dark:border-neutral-700
|
|
22
|
+
rounded-md
|
|
23
|
+
p-2
|
|
24
|
+
"
|
|
25
|
+
>
|
|
26
|
+
<!-- external is to force a reload, otherwise the editors won't work because of how they're setup for the demo (document api is created here and would need to be recreated with the route changes) -->
|
|
27
|
+
<NuxtLink
|
|
28
|
+
v-if="useYjs !== undefined"
|
|
29
|
+
:to="{ path: '/', query: { } }"
|
|
30
|
+
:external="true"
|
|
31
|
+
>Go to Non-Yjs Example</NuxtLink>
|
|
32
|
+
<NuxtLink
|
|
33
|
+
v-else
|
|
34
|
+
:to="{ path: '/', query: { useYjs: 'true' } }"
|
|
35
|
+
:external="true"
|
|
36
|
+
>Go to Yjs Example </NuxtLink>
|
|
37
|
+
<WCheckbox v-model="useTwoEditors">
|
|
38
|
+
Use Two Editors (same document)
|
|
39
|
+
</WCheckbox>
|
|
40
|
+
</div>
|
|
41
|
+
<div
|
|
42
|
+
class="
|
|
43
|
+
flex
|
|
44
|
+
justify-center
|
|
45
|
+
items-center
|
|
46
|
+
gap-2
|
|
47
|
+
[&>*]:rounded-md
|
|
10
48
|
[&>*]:p-2
|
|
11
49
|
[&>*]:border
|
|
12
50
|
[&>*]:border-neutral-300
|
|
13
51
|
[&>*]:dark:border-neutral-700
|
|
52
|
+
|
|
14
53
|
"
|
|
15
|
-
>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
54
|
+
>
|
|
55
|
+
<div class="flex items-center gap-2">
|
|
56
|
+
<span>
|
|
57
|
+
Global Theme:
|
|
58
|
+
</span>
|
|
59
|
+
<WDarkModeSwitcher/>
|
|
60
|
+
</div>
|
|
61
|
+
<CodeBlockThemePicker
|
|
62
|
+
:code-blocks-theme-list="codeBlocksThemeList"
|
|
63
|
+
v-model:code-blocks-theme="codeBlocksTheme"
|
|
64
|
+
/>
|
|
21
65
|
</div>
|
|
22
|
-
<CodeBlockThemePicker
|
|
23
|
-
:code-blocks-theme-list="codeBlocksThemeList"
|
|
24
|
-
v-model:code-blocks-theme="codeBlocksTheme"
|
|
25
|
-
/>
|
|
26
66
|
</div>
|
|
27
67
|
</template>
|
|
28
68
|
|
|
29
69
|
<script setup lang="ts">
|
|
30
70
|
import WDarkModeSwitcher from "@witchcraft/ui/components/LibDarkModeSwitcher"
|
|
71
|
+
import { useRoute } from "nuxt/app"
|
|
31
72
|
|
|
32
73
|
import CodeBlockThemePicker from "./CodeBlockThemePicker.vue"
|
|
33
74
|
|
|
@@ -35,4 +76,8 @@ defineProps<{
|
|
|
35
76
|
codeBlocksThemeList: string[]
|
|
36
77
|
}>()
|
|
37
78
|
const codeBlocksTheme = defineModel<string>("codeBlocksTheme", { required: true })
|
|
79
|
+
|
|
80
|
+
const useTwoEditors = defineModel<boolean>("useTwoEditors", { required: true })
|
|
81
|
+
|
|
82
|
+
const useYjs = useRoute().query.useYjs as string
|
|
38
83
|
</script>
|
|
@@ -1,56 +1,81 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
import type { Editor } from "@tiptap/core"
|
|
3
|
-
import { Plugin, PluginKey
|
|
3
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state"
|
|
4
|
+
import { Decoration, DecorationSet } from "@tiptap/pm/view"
|
|
4
5
|
|
|
5
6
|
import { isEmbeddedBlock } from "../../EmbeddedDocument/utils/isEmbeddedBlock.js"
|
|
6
7
|
|
|
7
|
-
const ROOT_SELECTION_REGEX = /([0-9 -]*)(\[|$)/
|
|
8
|
+
const ROOT_SELECTION_REGEX = /(DEBUG: [0-9 -]*)(\[|$)/
|
|
8
9
|
const SUB_SELECTION_REGEX = /(\[[0-9 -]*\])/
|
|
9
10
|
|
|
10
11
|
export const debugSelectionPluginKey = new PluginKey("debugSelection")
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
*
|
|
14
|
-
* For embedded editors, adds the selection as `[from - to]`.
|
|
15
|
-
*/
|
|
12
|
+
|
|
13
|
+
/** Renders a floating tooltip directly above the selection point in dev mode only. */
|
|
16
14
|
export const debugSelectionPlugin = (editor: Editor, log: boolean = false): Plugin => {
|
|
17
15
|
let initialized = false
|
|
16
|
+
let currentDisplayString = ""
|
|
17
|
+
|
|
18
18
|
return new Plugin({
|
|
19
19
|
key: debugSelectionPluginKey,
|
|
20
20
|
state: {
|
|
21
|
-
init()
|
|
22
|
-
apply(tr
|
|
23
|
-
if (!import.meta.dev)
|
|
21
|
+
init() { return DecorationSet.empty },
|
|
22
|
+
apply(tr, oldSet) {
|
|
23
|
+
if (!import.meta.dev) return oldSet.map(tr.mapping, tr.doc)
|
|
24
24
|
const sel = `${tr.selection.from} - ${tr.selection.to}`
|
|
25
|
+
|
|
25
26
|
if (isEmbeddedBlock(editor.view)) {
|
|
26
|
-
if (log) {
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
const hasEmbeddedSelection = document.title.match(SUB_SELECTION_REGEX)
|
|
27
|
+
if (log) console.log(`embedded selection: ${sel}`)
|
|
28
|
+
const hasEmbeddedSelection = currentDisplayString.match(SUB_SELECTION_REGEX)
|
|
30
29
|
if (hasEmbeddedSelection) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
)
|
|
30
|
+
currentDisplayString = currentDisplayString.replace(
|
|
31
|
+
SUB_SELECTION_REGEX,
|
|
32
|
+
`[${sel}]`
|
|
33
|
+
)
|
|
36
34
|
} else {
|
|
37
|
-
|
|
35
|
+
// Ensure prefix is present even if starting as an embedded block
|
|
36
|
+
const prefix = currentDisplayString.startsWith("DEBUG: ") ? "" : "DEBUG: "
|
|
37
|
+
currentDisplayString = `${prefix}${currentDisplayString} [${sel}]`
|
|
38
38
|
}
|
|
39
39
|
} else {
|
|
40
|
-
if (log) {
|
|
41
|
-
console.log(`root selection: ${tr.selection.from} - ${tr.selection.to}`)
|
|
42
|
-
}
|
|
43
|
-
|
|
40
|
+
if (log) console.log(`root selection: ${sel}`)
|
|
44
41
|
if (!initialized) {
|
|
45
42
|
initialized = true
|
|
46
|
-
|
|
43
|
+
currentDisplayString = `DEBUG: ${sel}`
|
|
47
44
|
} else {
|
|
48
|
-
|
|
45
|
+
currentDisplayString = currentDisplayString.replace(
|
|
49
46
|
ROOT_SELECTION_REGEX,
|
|
50
|
-
|
|
47
|
+
`DEBUG: ${sel} $2`
|
|
51
48
|
)
|
|
52
49
|
}
|
|
53
|
-
}
|
|
50
|
+
} const widget = document.createElement("div")
|
|
51
|
+
Object.assign(widget.style, {
|
|
52
|
+
position: "absolute",
|
|
53
|
+
top: "110%",
|
|
54
|
+
right: "0",
|
|
55
|
+
zIndex: "100",
|
|
56
|
+
padding: "2px 6px",
|
|
57
|
+
fontSize: "10px",
|
|
58
|
+
fontFamily: "monospace",
|
|
59
|
+
color: "white",
|
|
60
|
+
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
|
61
|
+
borderRadius: "2px",
|
|
62
|
+
pointerEvents: "none",
|
|
63
|
+
whiteSpace: "nowrap",
|
|
64
|
+
lineHeight: "1"
|
|
65
|
+
})
|
|
66
|
+
widget.textContent = currentDisplayString
|
|
67
|
+
|
|
68
|
+
const deco = Decoration.widget(tr.selection.to, widget, {
|
|
69
|
+
side: -1, // Ensure it stays to the left of the cursor
|
|
70
|
+
key: "debug-selection-tooltip"
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
return DecorationSet.create(tr.doc, [deco])
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
props: {
|
|
77
|
+
decorations(state) {
|
|
78
|
+
return debugSelectionPluginKey.getState(state)
|
|
54
79
|
}
|
|
55
80
|
}
|
|
56
81
|
})
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
ref="codeBlockLangPickerEl"
|
|
24
24
|
>
|
|
25
25
|
<!-- and here we only style everything but the suggestions so the input matches the code block background -->
|
|
26
|
-
<!-- @vue-expect-error -->
|
|
27
26
|
<WSimpleInput
|
|
28
27
|
:border="false"
|
|
29
28
|
wrapper-class="lang-picker-input flex-nowrap z-10"
|
|
@@ -1,295 +1,28 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { Plugin, PluginKey } from "@tiptap/pm/state"
|
|
3
|
-
import type { EditorView } from "@tiptap/pm/view"
|
|
4
|
-
import {
|
|
5
|
-
redo,
|
|
6
|
-
undo,
|
|
7
|
-
type UndoPluginState,
|
|
8
|
-
ySyncPlugin,
|
|
9
|
-
yUndoPlugin,
|
|
10
|
-
yUndoPluginKey,
|
|
11
|
-
yXmlFragmentToProseMirrorRootNode } from "y-prosemirror"
|
|
12
|
-
import * as Y from "yjs"
|
|
13
|
-
|
|
14
|
-
const ySyncFilterPluginKey = new PluginKey("ySyncFilter")
|
|
15
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
16
|
-
type YSyncOpts = Parameters<typeof ySyncPlugin>[1]
|
|
17
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
18
|
-
type YUndoOpts = Parameters<typeof yUndoPlugin>[0]
|
|
19
|
-
|
|
20
|
-
declare module "@tiptap/core" {
|
|
21
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
22
|
-
interface Commands<ReturnType> {
|
|
23
|
-
collaboration: {
|
|
24
|
-
/** Sets the fragment to use for the collaboration extension. */
|
|
25
|
-
setFragment: (
|
|
26
|
-
fragment: Y.XmlFragment | undefined,
|
|
27
|
-
options?: { register?: boolean, unregister?: boolean }
|
|
28
|
-
) => ReturnType
|
|
29
|
-
/** Enables or disables collaboration. */
|
|
30
|
-
enableCollaboration: (enable: boolean) => ReturnType
|
|
31
|
-
/**
|
|
32
|
-
* Undo recent changes
|
|
33
|
-
*
|
|
34
|
-
* @example editor.commands.undo()
|
|
35
|
-
*/
|
|
36
|
-
undo: () => ReturnType
|
|
37
|
-
/**
|
|
38
|
-
* Reapply reverted changes
|
|
39
|
-
*
|
|
40
|
-
* @example editor.commands.redo()
|
|
41
|
-
*/
|
|
42
|
-
redo: () => ReturnType
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface CollaborationStorage {
|
|
48
|
-
/**
|
|
49
|
-
* Whether collaboration is currently disabled.
|
|
50
|
-
* Disabling collaboration will prevent any changes from being synced with other users.
|
|
51
|
-
*/
|
|
52
|
-
isDisabled: boolean
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface CollaborationOptions {
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* A raw Y.js fragment, can be used instead of `document` and `field`.
|
|
59
|
-
*
|
|
60
|
-
* @example new Y.Doc().getXmlFragment('body')
|
|
61
|
-
*/
|
|
62
|
-
fragment?: Y.XmlFragment | null
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Fired when the content from Yjs is initially rendered to Tiptap.
|
|
66
|
-
*/
|
|
67
|
-
onFirstRender?: () => void
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Options for the Yjs sync plugin.
|
|
71
|
-
*/
|
|
72
|
-
ySyncOptions?: YSyncOpts
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Options for the Yjs undo plugin.
|
|
76
|
-
*/
|
|
77
|
-
yUndoOptions?: YUndoOpts
|
|
78
|
-
/** @internal */
|
|
79
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
80
|
-
_destroySyncPlugin?: () => void
|
|
81
|
-
}
|
|
82
|
-
type CollabInstance = {
|
|
83
|
-
editor: Editor
|
|
84
|
-
options: CollaborationOptions
|
|
85
|
-
storage: CollaborationStorage
|
|
86
|
-
}
|
|
87
|
-
type ExtendedUndoManager = UndoPluginState["undoManager"] & {
|
|
88
|
-
restore?: () => void
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function createUndoPlugin(self: CollabInstance): Plugin { // Quick fix until there is an official implementation (thanks to @hamflx).
|
|
92
|
-
// See https://github.com/yjs/y-prosemirror/issues/114 and https://github.com/yjs/y-prosemirror/issues/102
|
|
93
|
-
const yUndoPluginInstance = yUndoPlugin(self.options.yUndoOptions)
|
|
94
|
-
const originalUndoPluginView = yUndoPluginInstance.spec.view
|
|
95
|
-
|
|
96
|
-
yUndoPluginInstance.spec.view = (view: EditorView) => {
|
|
97
|
-
const { undoManager } = yUndoPluginKey.getState(view.state) as UndoPluginState & { undoManager: ExtendedUndoManager }
|
|
98
|
-
|
|
99
|
-
if (undoManager.restore) {
|
|
100
|
-
undoManager.restore()
|
|
101
|
-
undoManager.restore = () => {
|
|
102
|
-
// noop
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const viewRet = originalUndoPluginView ? originalUndoPluginView(view) : undefined
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
destroy: () => {
|
|
110
|
-
const hasUndoManSelf = undoManager.trackedOrigins.has(undoManager)
|
|
111
|
-
|
|
112
|
-
const observers = undoManager._observers
|
|
113
|
-
|
|
114
|
-
undoManager.restore = () => {
|
|
115
|
-
if (hasUndoManSelf) {
|
|
116
|
-
undoManager.trackedOrigins.add(undoManager)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
undoManager.doc.on("afterTransaction", undoManager.afterTransactionHandler)
|
|
120
|
-
|
|
121
|
-
undoManager._observers = observers
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (viewRet?.destroy) {
|
|
125
|
-
viewRet.destroy()
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return yUndoPluginInstance
|
|
131
|
-
}
|
|
132
|
-
function createSyncPlugin(
|
|
133
|
-
self: CollabInstance
|
|
134
|
-
): { plugin: Plugin, destroy: () => void } {
|
|
135
|
-
const fragment = self.options.fragment
|
|
136
|
-
if (!fragment) {
|
|
137
|
-
throw new Error("Collaboration requires a fragment.")
|
|
138
|
-
}
|
|
139
|
-
const plugin = ySyncPlugin(fragment, {
|
|
140
|
-
...self.options.ySyncOptions,
|
|
141
|
-
onFirstRender: self.options.onFirstRender
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
let off: (() => void) | undefined
|
|
145
|
-
function destroy(): void {
|
|
146
|
-
off?.()
|
|
147
|
-
// delete self.editor.state[ySyncPluginKey.key]
|
|
148
|
-
}
|
|
149
|
-
if (self.editor.options.enableContentCheck) {
|
|
150
|
-
const onBeforeTransaction = (): false | undefined => {
|
|
151
|
-
try {
|
|
152
|
-
yXmlFragmentToProseMirrorRootNode(fragment, self.editor.schema).check()
|
|
153
|
-
} catch (error) {
|
|
154
|
-
self.editor.emit("contentError", {
|
|
155
|
-
error: error as Error,
|
|
156
|
-
editor: self.editor,
|
|
157
|
-
disableCollaboration: () => {
|
|
158
|
-
fragment.doc?.destroy()
|
|
159
|
-
self.storage.isDisabled = true
|
|
160
|
-
}
|
|
161
|
-
})
|
|
162
|
-
// If the content is invalid, return false to prevent the transaction from being applied
|
|
163
|
-
return false
|
|
164
|
-
}
|
|
165
|
-
return undefined
|
|
166
|
-
}
|
|
167
|
-
fragment.doc?.on("beforeTransaction", onBeforeTransaction)
|
|
168
|
-
off = () => {
|
|
169
|
-
fragment.doc?.off("beforeTransaction", onBeforeTransaction)
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return { plugin, destroy }
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function createSyncFilterPlugin(
|
|
176
|
-
self: CollabInstance
|
|
177
|
-
): Plugin | undefined {
|
|
178
|
-
if (!self.editor.options.enableContentCheck) return
|
|
179
|
-
const fragment = self.options.fragment
|
|
180
|
-
if (!fragment) {
|
|
181
|
-
throw new Error("Collaboration requires a fragment.")
|
|
182
|
-
}
|
|
183
|
-
return new Plugin({
|
|
184
|
-
key: ySyncFilterPluginKey,
|
|
185
|
-
filterTransaction: () => {
|
|
186
|
-
// When collaboration is disabled, prevent any sync transactions from being applied
|
|
187
|
-
if (self.storage.isDisabled) {
|
|
188
|
-
// Destroy the Yjs document to prevent any further sync transactions
|
|
189
|
-
fragment.doc?.destroy()
|
|
190
|
-
|
|
191
|
-
return true
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return true
|
|
195
|
-
}
|
|
196
|
-
})
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function createPlugins(self: CollabInstance): Plugin[] {
|
|
200
|
-
const { plugin: syncPlugin, destroy: destroySyncPlugin } = createSyncPlugin(self)
|
|
201
|
-
self.options._destroySyncPlugin = destroySyncPlugin
|
|
202
|
-
const filterPlugin = createSyncFilterPlugin(self)
|
|
203
|
-
const plugins = [
|
|
204
|
-
syncPlugin,
|
|
205
|
-
createUndoPlugin(self)
|
|
206
|
-
]
|
|
207
|
-
if (filterPlugin) {
|
|
208
|
-
plugins.push(filterPlugin)
|
|
209
|
-
}
|
|
210
|
-
return plugins
|
|
211
|
-
}
|
|
1
|
+
import BaseCollaboration, { type CollaborationOptions } from "@tiptap/extension-collaboration"
|
|
212
2
|
|
|
213
3
|
/**
|
|
214
|
-
*
|
|
4
|
+
* Extension of the base collaboration extension without prosemirror plugins or storage.
|
|
5
|
+
*
|
|
6
|
+
* We can't use tiptap's collaboration extension (or any extension that creates the sync plugin) because it expects to be configured with the document's ydoc per editor.
|
|
7
|
+
*
|
|
8
|
+
* This doesn't mesh well with how the document api works (see {@link DocumentApi}).
|
|
9
|
+
*
|
|
10
|
+
* Instead we let the extension register anything it wants (shortcuts, commands, etc) except the plugins and storage. This way it can just be added to the list of editor extensions normally.
|
|
11
|
+
*
|
|
12
|
+
* Then there is a seperate function to actually create the plugins:
|
|
13
|
+
*
|
|
14
|
+
* {@link createCollaborationPlugins} creates the plugins **per doc** and can be used from your document api without an editor instance. See {@link useTestDocumentApi} for an example.
|
|
215
15
|
*
|
|
216
|
-
*
|
|
16
|
+
* It's also where the `enableContentCheck` option should be set if you need it. It's IGNORED if passed to the extension.
|
|
217
17
|
*/
|
|
218
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
219
|
-
export const Collaboration = Extension.create<CollaborationOptions, CollaborationStorage>({
|
|
220
|
-
name: "collaboration",
|
|
221
|
-
|
|
222
|
-
priority: 1000,
|
|
223
|
-
|
|
224
|
-
addOptions() {
|
|
225
|
-
return {
|
|
226
|
-
document: null,
|
|
227
|
-
field: "default",
|
|
228
|
-
fragment: new Y.Doc().getXmlFragment("prosemirror")
|
|
229
|
-
}
|
|
230
|
-
},
|
|
231
18
|
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
20
|
+
export const Collaboration = BaseCollaboration.extend<Omit<CollaborationOptions, "document" | "field" | "fragment">, Record<string, never>>({
|
|
232
21
|
addStorage() {
|
|
233
|
-
return {
|
|
234
|
-
isDisabled: false
|
|
235
|
-
}
|
|
236
|
-
},
|
|
237
|
-
|
|
238
|
-
onCreate() {
|
|
239
|
-
if (this.editor.extensionManager.extensions.find(extension => extension.name === "history")) {
|
|
240
|
-
// eslint-disable-next-line no-console
|
|
241
|
-
console.warn(
|
|
242
|
-
"[tiptap warn]: \"@tiptap/extension-collaboration\" comes with its own history support and is not compatible with \"@tiptap/extension-history\"."
|
|
243
|
-
)
|
|
244
|
-
}
|
|
22
|
+
return {}
|
|
245
23
|
},
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
// const self = this
|
|
249
|
-
return {
|
|
250
|
-
enableCollaboration: (enable: boolean) => () => {
|
|
251
|
-
this.storage.isDisabled = !enable
|
|
252
|
-
return true
|
|
253
|
-
},
|
|
254
|
-
undo: () => ({ tr, state, dispatch }) => {
|
|
255
|
-
tr.setMeta("preventDispatch", true)
|
|
256
|
-
|
|
257
|
-
const undoManager = yUndoPluginKey.getState(state)!.undoManager as ExtendedUndoManager
|
|
258
|
-
|
|
259
|
-
if (undoManager.undoStack.length === 0) {
|
|
260
|
-
return false
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (!dispatch) {
|
|
264
|
-
return true
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return undo(state)
|
|
268
|
-
},
|
|
269
|
-
redo: () => ({ tr, state, dispatch }) => {
|
|
270
|
-
tr.setMeta("preventDispatch", true)
|
|
271
|
-
|
|
272
|
-
const undoManager = yUndoPluginKey.getState(state)!.undoManager as ExtendedUndoManager
|
|
273
|
-
|
|
274
|
-
if (undoManager.redoStack.length === 0) {
|
|
275
|
-
return false
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (!dispatch) {
|
|
279
|
-
return true
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return redo(state)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
},
|
|
287
|
-
onDestroy() {
|
|
288
|
-
this.options._destroySyncPlugin?.()
|
|
289
|
-
},
|
|
290
|
-
|
|
291
|
-
addProseMirrorPlugins(): Plugin[] {
|
|
292
|
-
const self = this
|
|
293
|
-
return createPlugins(self)
|
|
24
|
+
addProseMirrorPlugins() {
|
|
25
|
+
return []
|
|
294
26
|
}
|
|
295
27
|
})
|
|
28
|
+
|