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