@witchcraft/editor 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/Editor.d.vue.ts +11 -1
  3. package/dist/runtime/components/Editor.vue.d.ts +11 -1
  4. package/dist/runtime/components/EditorDemoApp.vue +35 -5
  5. package/dist/runtime/components/EditorDemoControls.d.vue.ts +3 -0
  6. package/dist/runtime/components/EditorDemoControls.vue +55 -12
  7. package/dist/runtime/components/EditorDemoControls.vue.d.ts +3 -0
  8. package/dist/runtime/pm/features/Blocks/components/DragTreeHandle.d.vue.ts +2 -2
  9. package/dist/runtime/pm/features/Blocks/components/DragTreeHandle.vue.d.ts +2 -2
  10. package/dist/runtime/pm/features/Collaboration/Collaboration.d.ts +14 -63
  11. package/dist/runtime/pm/features/Collaboration/Collaboration.js +4 -164
  12. package/dist/runtime/pm/features/Collaboration/createCollaborationPlugins.d.ts +16 -0
  13. package/dist/runtime/pm/features/Collaboration/createCollaborationPlugins.js +82 -0
  14. package/dist/runtime/pm/features/CommandsMenus/commandBarMenuItems.js +5 -1
  15. package/dist/runtime/pm/features/DocumentApi/DocumentApi.d.ts +16 -3
  16. package/dist/runtime/pm/features/DocumentApi/DocumentApi.js +19 -2
  17. package/dist/runtime/pm/features/DocumentApi/composables/useEditorContent.js +2 -0
  18. package/dist/runtime/pm/features/DocumentApi/composables/useTestDocumentApi.d.ts +4 -1
  19. package/dist/runtime/pm/features/DocumentApi/composables/useTestDocumentApi.js +39 -8
  20. package/dist/runtime/pm/features/DocumentApi/types.d.ts +26 -48
  21. package/dist/runtime/pm/schema.d.ts +1 -1
  22. package/package.json +35 -34
  23. package/src/runtime/components/EditorDemoApp.vue +36 -5
  24. package/src/runtime/components/EditorDemoControls.vue +57 -12
  25. package/src/runtime/pm/features/Collaboration/Collaboration.ts +19 -286
  26. package/src/runtime/pm/features/Collaboration/createCollaborationPlugins.ts +129 -0
  27. package/src/runtime/pm/features/CommandsMenus/commandBarMenuItems.ts +5 -2
  28. package/src/runtime/pm/features/DocumentApi/DocumentApi.ts +35 -5
  29. package/src/runtime/pm/features/DocumentApi/composables/useEditorContent.ts +2 -0
  30. package/src/runtime/pm/features/DocumentApi/composables/useTestDocumentApi.ts +56 -8
  31. package/src/runtime/pm/features/DocumentApi/types.ts +30 -52
  32. package/dist/runtime/demo/App.d.vue.ts +0 -3
  33. package/dist/runtime/demo/App.vue +0 -100
  34. package/dist/runtime/demo/App.vue.d.ts +0 -3
  35. package/src/runtime/demo/App.vue +0 -113
@@ -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
- /** Like {@link DocumentApi.preEditorInit}, but after initializing and loading the document (and before the transaction listeners are added). */
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 when managing the document state in some other way (e.g. collab.
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
- * Also useful for creating per-doc instances for certain extensions, such as Collaboration.
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
- * This is a bit tricky to do normally 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.
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
- * So this hook can be used to add these additional per-doc instances of extensions. Be sure to clone the properties you are modifying. They are only shallow cloned before being passed to the function.
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
- * collabExt
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 ignore the transaction.
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
- * So we have to ignore all of yjs's transactions, but NOT transactions from partially embedded docs => full state, as these do not pass through yjs.
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 (type: "saving" | "saved", cb: OnSaveDocumentCallback): void
114
- addEventListener (type: "update", cb: OnUpdateDocumentCallback): void
115
- removeEventListener (type: "saving" | "saved", cb: OnSaveDocumentCallback): void
116
- removeEventListener (type: "update", cb: OnUpdateDocumentCallback): void
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({
@@ -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;
@@ -1,113 +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 lang="ts">
30
- import type { EditorOptions } from "@tiptap/core"
31
- import WRoot from "@witchcraft/ui/components/LibRoot"
32
- import { nextTick, reactive, ref, shallowRef } from "vue"
33
-
34
- import Editor from "../components/Editor.vue"
35
- import DemoControls from "../components/EditorDemoControls.vue"
36
- import { useHighlightJsTheme } from "../pm/features/CodeBlock/composables/useHighlightJsTheme.js"
37
- import { defaultCommandBarMenuItems } from "../pm/features/CommandsMenus/commandBarMenuItems"
38
- import CommandBar from "../pm/features/CommandsMenus/components/CommandBar.vue"
39
- import { useTestDocumentApi } from "../pm/features/DocumentApi/composables/useTestDocumentApi.js"
40
- import BubbleMenuLink from "../pm/features/Link/components/BubbleMenuLink.vue"
41
- import type { EditorLinkOptions } from "../pm/features/Link/types.js"
42
- import type { MenuRenderInfo } from "../pm/features/Menus/types"
43
- import { testExtensions } from "../pm/testSchema.js"
44
- import { testDocuments } from "../testDocuments"
45
-
46
- const {
47
- theme: codeBlocksTheme,
48
- knownThemes: codeBlocksThemeList,
49
- themeCss: codeBlocksThemeCss,
50
- isDark: codeBlocksThemeIsDark,
51
- backgroundColor: codeBlocksThemeBgColor
52
- } = useHighlightJsTheme()
53
-
54
- function blur(): void {
55
- const was = codeBlocksTheme.value
56
- codeBlocksTheme.value = ""
57
- nextTick(() => {
58
- codeBlocksTheme.value = was
59
- })
60
- }
61
-
62
- const editorOptions: Partial<EditorOptions> = {
63
- extensions: testExtensions as any
64
- }
65
-
66
- const linkOptions: EditorLinkOptions = {
67
- openInternal: href => {
68
- window.alert(`This would open an internal link to ${href}.`)
69
-
70
- // eslint-disable-next-line no-console
71
- console.log(`Would open internal link to ${href}.`)
72
- }
73
- }
74
- const fakeSuggestions = reactive<string[]>(["some", "suggestions"])
75
-
76
- const menus = shallowRef<Record<string, MenuRenderInfo>>({
77
- linkMenu: {
78
- component: BubbleMenuLink,
79
- props: editor => ({
80
- editor,
81
- linkSuggestions: fakeSuggestions,
82
- getInternalLinkHref(href: string) {
83
- return `internal://${href.replace(/[^\w-]/g, "")}`
84
- }
85
- })
86
- },
87
- commandBar: {
88
- component: CommandBar,
89
- props: editor => ({
90
- editor,
91
- commands: defaultCommandBarMenuItems.commands
92
- }),
93
- popupOptions: {
94
- pinToItemDistance: state => {
95
- const { $from, $to } = state.selection
96
- const fromNode = $from.node(-1)
97
- const toNode = $to.node(-1)
98
- // tables don't support selections outside of each cell, so no need to check we're in the same table or anything
99
- if (fromNode.type !== toNode.type) {
100
- return 0
101
- }
102
- return (fromNode.type.name.startsWith("table")) ? 120 : 0
103
- }
104
- }
105
- }
106
- })
107
-
108
- const { documentApi } = useTestDocumentApi(
109
- editorOptions as any,
110
- testDocuments
111
- )
112
- const docId = ref("root")
113
- </script>