@verdant-web/tiptap 0.1.6 → 1.0.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/src/react.ts CHANGED
@@ -1,28 +1,16 @@
1
- import { Editor } from '@tiptap/core';
2
1
  import { useEditor, UseEditorOptions } from '@tiptap/react';
3
- import { useWatch } from '@verdant-web/react';
4
- import { AnyEntity, ObjectEntity } from '@verdant-web/store';
5
- import { useCallback, useEffect, useRef } from 'react';
6
-
7
- type AllowedKey<Ent extends AnyEntity<any, any, any>> = Ent extends never
8
- ? string
9
- : Ent extends AnyEntity<any, any, infer Shape>
10
- ? keyof Shape
11
- : never;
12
-
13
- type EntitySnapshot<
14
- Ent extends AnyEntity<any, any, any>,
15
- Key extends AllowedKey<Ent>,
16
- > =
17
- Ent extends AnyEntity<any, any, infer Snap>
18
- ? Key extends keyof Snap
19
- ? Snap[Key]
20
- : any
21
- : never;
2
+ import { AnyEntity } from '@verdant-web/store';
3
+ import { useRef, useState } from 'react';
4
+ import {
5
+ VerdantExtension,
6
+ VerdantExtensionOptions,
7
+ type EntitySnapshot,
8
+ type ValidEntityKey,
9
+ } from './plugins.js';
22
10
 
23
11
  export function useSyncedEditor<
24
12
  Ent extends AnyEntity<any, any, any>,
25
- Key extends AllowedKey<Ent>,
13
+ Key extends ValidEntityKey<Ent>,
26
14
  >(
27
15
  parent: Ent,
28
16
  fieldName: Key,
@@ -30,10 +18,12 @@ export function useSyncedEditor<
30
18
  editorOptions: extraOptions,
31
19
  editorDependencies,
32
20
  nullDocumentDefault,
21
+ extensionOptions,
33
22
  }: {
34
23
  editorOptions?: UseEditorOptions;
35
24
  editorDependencies?: any[];
36
25
  nullDocumentDefault?: EntitySnapshot<Ent, Key>;
26
+ extensionOptions?: Partial<VerdantExtensionOptions>;
37
27
  } = {},
38
28
  ) {
39
29
  const cachedOptions = useRef({
@@ -44,106 +34,26 @@ export function useSyncedEditor<
44
34
  nullDocumentDefault,
45
35
  fieldName,
46
36
  };
47
- const live = useWatch(parent);
48
- const field = live[fieldName] as ObjectEntity<any, any>;
49
- const updatingRef = useRef(false);
50
- const update = useStableCallback((editor: Editor) => {
51
- if (updatingRef.current) {
52
- return;
53
- }
54
-
55
- const newData = editor.getJSON();
56
- const value = parent.get(cachedOptions.current.fieldName) as ObjectEntity<
57
- any,
58
- any
59
- > | null;
60
- if (!value) {
61
- parent.set(cachedOptions.current.fieldName as any, newData);
62
- } else {
63
- value.update(newData, {
64
- merge: false,
65
- dangerouslyDisableMerge: true,
66
- replaceSubObjects: false,
67
- });
68
- }
69
- });
70
-
71
- const cachedInitialContent = useRef(
72
- ensureDocShape(getFieldSnapshot(field, nullDocumentDefault, fieldName)),
37
+ // create a configured version of the Verdant extension, which handles
38
+ // the actual syncing of the editor content to the field
39
+ const [extension] = useState(() =>
40
+ VerdantExtension.configure({
41
+ parent,
42
+ fieldName: fieldName as string | number,
43
+ nullDocumentDefault,
44
+ ...extensionOptions,
45
+ }),
73
46
  );
74
47
  const editor = useEditor(
75
48
  {
76
49
  ...extraOptions,
77
- content: cachedInitialContent.current,
78
- onUpdate: (ctx) => {
79
- update(ctx.editor);
80
- extraOptions?.onUpdate?.(ctx);
81
- },
82
50
  onContentError(props) {
83
51
  console.error('Content error:', props.error);
84
52
  },
53
+ extensions: [extension, ...(extraOptions?.extensions ?? [])],
85
54
  },
86
55
  editorDependencies,
87
56
  );
88
57
 
89
- useEffect(() => {
90
- function updateFromField() {
91
- if (editor && !editor.isDestroyed) {
92
- updatingRef.current = true;
93
- const { from, to } = editor.state.selection;
94
- editor.commands.setContent(
95
- ensureDocShape(
96
- getFieldSnapshot(
97
- field,
98
- cachedOptions.current.nullDocumentDefault,
99
- cachedOptions.current.fieldName,
100
- ),
101
- ),
102
- false,
103
- );
104
- editor.commands.setTextSelection({ from, to });
105
- updatingRef.current = false;
106
- }
107
- }
108
-
109
- updateFromField();
110
-
111
- return field?.subscribe('changeDeep', (target, info) => {
112
- if (!info.isLocal || target === field) {
113
- updateFromField();
114
- }
115
- });
116
- }, [field, editor, cachedOptions]);
117
-
118
58
  return editor;
119
59
  }
120
-
121
- // since the schema doesn't enforce this shape but it's
122
- // needed for the editor to work, we'll ensure it here
123
- function ensureDocShape(json: any) {
124
- for (const node of json.content ?? []) {
125
- // remove undefined nodes
126
- node.content = node.content.filter((n: any) => !!n).map(ensureDocShape);
127
- }
128
- return json;
129
- }
130
-
131
- function getFieldSnapshot(
132
- field: ObjectEntity<any, any> | undefined | null,
133
- nullDocumentDefault: any,
134
- fieldName: string | symbol | number,
135
- ) {
136
- const content = field ? field.getSnapshot() : (nullDocumentDefault ?? null);
137
- if (content === null) {
138
- throw new Error(`The provided field "${String(fieldName)}" is null and a default document was not provided.
139
- Please provide a default document or ensure the field is not null when calling useSyncedEditor, or make your
140
- field schema non-null and specify a default document there.`);
141
- }
142
- return content;
143
- }
144
-
145
- function useStableCallback<T extends (...args: any[]) => any>(callback: T) {
146
- const ref = useRef(callback);
147
- ref.current = callback;
148
- return useCallback((...args: Parameters<T>) => ref.current(...args), []);
149
- }