tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
289
fork
atom
a tool for shared writing and social publishing
289
fork
atom
overview
issues
27
pulls
pipelines
share editor logic between footnote and textblock
awarm.space
6 days ago
0f843a5a
16f80527
+69
-76
2 changed files
expand all
collapse all
unified
split
components
Blocks
TextBlock
mountProsemirror.ts
Footnotes
FootnoteEditor.tsx
+45
-28
components/Blocks/TextBlock/mountProsemirror.ts
···
1
1
import { useLayoutEffect, useRef, useEffect, useState } from "react";
2
2
-
import { EditorState } from "prosemirror-state";
2
2
+
import { EditorState, Transaction } from "prosemirror-state";
3
3
import { EditorView } from "prosemirror-view";
4
4
import { baseKeymap } from "prosemirror-commands";
5
5
import { keymap } from "prosemirror-keymap";
···
10
10
import { produce } from "immer";
11
11
12
12
import { schema } from "./schema";
13
13
+
import { UndoManager } from "src/undoManager";
13
14
import { TextBlockKeymap } from "./keymap";
14
15
import { inputrules } from "./inputRules";
15
16
import { highlightSelectionPlugin } from "./plugins";
···
184
185
useEditorStates.setState((s) => {
185
186
let oldEditorState = this.state;
186
187
let newState = this.state.apply(tr);
187
187
-
let addToHistory = tr.getMeta("addToHistory");
188
188
-
let isBulkOp = tr.getMeta("bulkOp");
189
188
let docHasChanges = tr.steps.length !== 0 || tr.docChanged;
190
189
191
190
// Diff for removed/added footnote nodes
···
212
211
}
213
212
214
213
// Handle undo/redo history with timeout-based grouping
215
215
-
if (addToHistory !== false && docHasChanges) {
216
216
-
if (actionTimeout.current) window.clearTimeout(actionTimeout.current);
217
217
-
else if (!isBulkOp) rep.undoManager.startGroup();
214
214
+
let isBulkOp = tr.getMeta("bulkOp");
215
215
+
let setState = (s: EditorState) => () =>
216
216
+
useEditorStates.setState(
217
217
+
produce((draft) => {
218
218
+
let view = draft.editorStates[entityID]?.view;
219
219
+
if (!view?.hasFocus() && !isBulkOp) view?.focus();
220
220
+
draft.editorStates[entityID]!.editor = s;
221
221
+
}),
222
222
+
);
218
223
219
219
-
if (!isBulkOp) {
220
220
-
actionTimeout.current = window.setTimeout(() => {
221
221
-
rep.undoManager.endGroup();
222
222
-
actionTimeout.current = null;
223
223
-
}, 200);
224
224
-
}
225
225
-
226
226
-
let setState = (s: EditorState) => () =>
227
227
-
useEditorStates.setState(
228
228
-
produce((draft) => {
229
229
-
let view = draft.editorStates[entityID]?.view;
230
230
-
if (!view?.hasFocus() && !isBulkOp) view?.focus();
231
231
-
draft.editorStates[entityID]!.editor = s;
232
232
-
}),
233
233
-
);
234
234
-
235
235
-
rep.undoManager.add({
236
236
-
redo: setState(newState),
237
237
-
undo: setState(oldEditorState),
238
238
-
});
239
239
-
}
224
224
+
trackUndoRedo(
225
225
+
tr,
226
226
+
rep.undoManager,
227
227
+
actionTimeout,
228
228
+
setState(oldEditorState),
229
229
+
setState(newState),
230
230
+
);
240
231
241
232
return {
242
233
editorStates: {
···
255
246
return { mountRef, actionTimeout };
256
247
}
257
248
258
258
-
function useYJSValue(entityID: string) {
249
249
+
export function trackUndoRedo(
250
250
+
tr: Transaction,
251
251
+
undoManager: UndoManager,
252
252
+
actionTimeout: { current: number | null },
253
253
+
undo: () => void,
254
254
+
redo: () => void,
255
255
+
) {
256
256
+
let addToHistory = tr.getMeta("addToHistory");
257
257
+
let isBulkOp = tr.getMeta("bulkOp");
258
258
+
let docHasChanges = tr.steps.length !== 0 || tr.docChanged;
259
259
+
260
260
+
if (addToHistory !== false && docHasChanges) {
261
261
+
if (actionTimeout.current) window.clearTimeout(actionTimeout.current);
262
262
+
else if (!isBulkOp) undoManager.startGroup();
263
263
+
264
264
+
if (!isBulkOp) {
265
265
+
actionTimeout.current = window.setTimeout(() => {
266
266
+
undoManager.endGroup();
267
267
+
actionTimeout.current = null;
268
268
+
}, 200);
269
269
+
}
270
270
+
271
271
+
undoManager.add({ undo, redo });
272
272
+
}
273
273
+
}
274
274
+
275
275
+
export function useYJSValue(entityID: string) {
259
276
const [ydoc] = useState(new Y.Doc());
260
277
const docStateFromReplicache = useEntity(entityID, "block/text");
261
278
let rep = useReplicache();
+24
-48
components/Footnotes/FootnoteEditor.tsx
···
1
1
-
import { useLayoutEffect, useRef, useState, useEffect } from "react";
1
1
+
import { useLayoutEffect, useRef } from "react";
2
2
import { EditorState } from "prosemirror-state";
3
3
import { EditorView } from "prosemirror-view";
4
4
import { baseKeymap, toggleMark } from "prosemirror-commands";
5
5
import { keymap } from "prosemirror-keymap";
6
6
import { ySyncPlugin } from "y-prosemirror";
7
7
-
import * as Y from "yjs";
8
8
-
import * as base64 from "base64-js";
9
7
import { schema } from "components/Blocks/TextBlock/schema";
10
10
-
import { useEntity, useReplicache } from "src/replicache";
8
8
+
import { useReplicache } from "src/replicache";
11
9
import { autolink } from "components/Blocks/TextBlock/autolink-plugin";
10
10
+
import {
11
11
+
useYJSValue,
12
12
+
trackUndoRedo,
13
13
+
} from "components/Blocks/TextBlock/mountProsemirror";
12
14
import { CloseTiny } from "components/Icons/CloseTiny";
13
15
14
16
export function FootnoteEditor(props: {
···
20
22
}) {
21
23
let mountRef = useRef<HTMLDivElement | null>(null);
22
24
let rep = useReplicache();
23
23
-
let value = useFootnoteYJS(props.footnoteEntityID);
25
25
+
let value = useYJSValue(props.footnoteEntityID);
26
26
+
let actionTimeout = useRef<number | null>(null);
24
27
25
28
useLayoutEffect(() => {
26
29
if (!mountRef.current || !value) return;
···
63
66
state,
64
67
editable: () => props.editable,
65
68
dispatchTransaction(this: EditorView, tr) {
69
69
+
let oldState = this.state;
66
70
let newState = this.state.apply(tr);
67
71
this.updateState(newState);
72
72
+
73
73
+
trackUndoRedo(
74
74
+
tr,
75
75
+
rep.undoManager,
76
76
+
actionTimeout,
77
77
+
() => {
78
78
+
this.focus();
79
79
+
this.updateState(oldState);
80
80
+
},
81
81
+
() => {
82
82
+
this.focus();
83
83
+
this.updateState(newState);
84
84
+
},
85
85
+
);
68
86
},
69
87
},
70
88
);
···
76
94
return () => {
77
95
view.destroy();
78
96
};
79
79
-
}, [props.footnoteEntityID, value, props.editable, props.autoFocus]);
97
97
+
}, [props.footnoteEntityID, value, props.editable, props.autoFocus, rep.undoManager]);
80
98
81
99
return (
82
100
<div className="footnote-editor flex items-start gap-2 text-xs group/footnote" data-footnote-editor={props.footnoteEntityID}>
···
112
130
);
113
131
}
114
132
115
115
-
function useFootnoteYJS(footnoteEntityID: string) {
116
116
-
const [ydoc] = useState(new Y.Doc());
117
117
-
const docState = useEntity(footnoteEntityID, "block/text");
118
118
-
let rep = useReplicache();
119
119
-
const [yText] = useState(ydoc.getXmlFragment("prosemirror"));
120
120
-
121
121
-
if (docState) {
122
122
-
const update = base64.toByteArray(docState.data.value);
123
123
-
Y.applyUpdate(ydoc, update);
124
124
-
}
125
125
-
126
126
-
useEffect(() => {
127
127
-
if (!rep.rep) return;
128
128
-
let timeout = null as null | number;
129
129
-
const updateReplicache = async () => {
130
130
-
const update = Y.encodeStateAsUpdate(ydoc);
131
131
-
await rep.rep?.mutate.assertFact({
132
132
-
ignoreUndo: true,
133
133
-
entity: footnoteEntityID,
134
134
-
attribute: "block/text",
135
135
-
data: {
136
136
-
value: base64.fromByteArray(update),
137
137
-
type: "text",
138
138
-
},
139
139
-
});
140
140
-
};
141
141
-
const f = async (_events: Y.YEvent<any>[], transaction: Y.Transaction) => {
142
142
-
if (!transaction.origin) return;
143
143
-
if (timeout) clearTimeout(timeout);
144
144
-
timeout = window.setTimeout(async () => {
145
145
-
updateReplicache();
146
146
-
}, 300);
147
147
-
};
148
148
-
149
149
-
yText.observeDeep(f);
150
150
-
return () => {
151
151
-
yText.unobserveDeep(f);
152
152
-
};
153
153
-
}, [yText, footnoteEntityID, rep, ydoc]);
154
154
-
155
155
-
return yText;
156
156
-
}