@dxos/react-ui-editor 0.4.8-main.3a03fb7 → 0.4.8-main.4a229ac
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/dist/lib/browser/index.mjs +131 -44
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +1 -0
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +11 -5
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +4 -2
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts +1 -0
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts +7 -4
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/image.d.ts +6 -0
- package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
- package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.stories.d.ts +1 -0
- package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +1 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/package.json +25 -24
- package/src/components/TextEditor/TextEditor.tsx +2 -2
- package/src/components/Toolbar/Toolbar.stories.tsx +23 -24
- package/src/components/Toolbar/Toolbar.tsx +101 -35
- package/src/extensions/comments.ts +2 -2
- package/src/extensions/markdown/formatting.test.ts +13 -13
- package/src/extensions/markdown/formatting.ts +91 -83
- package/src/extensions/markdown/image.ts +6 -0
- package/src/hooks/useActionHandler.ts +5 -1
- package/src/hooks/useTextEditor.ts +3 -0
- package/src/translations.ts +1 -0
|
@@ -15,7 +15,9 @@ import {
|
|
|
15
15
|
} from '@codemirror/state';
|
|
16
16
|
import { EditorView, keymap } from '@codemirror/view';
|
|
17
17
|
import { type SyntaxNodeRef, type SyntaxNode } from '@lezer/common';
|
|
18
|
-
import {
|
|
18
|
+
import { useMemo } from 'react';
|
|
19
|
+
|
|
20
|
+
import { useStateRef } from '@dxos/react-async';
|
|
19
21
|
|
|
20
22
|
// Markdown refs:
|
|
21
23
|
// https://github.github.com/gfm
|
|
@@ -56,7 +58,7 @@ export type Formatting = {
|
|
|
56
58
|
listStyle: null | 'ordered' | 'bullet' | 'task';
|
|
57
59
|
};
|
|
58
60
|
|
|
59
|
-
export const
|
|
61
|
+
export const formattingEquals = (a: Formatting, b: Formatting) =>
|
|
60
62
|
a.blockType === b.blockType &&
|
|
61
63
|
a.strong === b.strong &&
|
|
62
64
|
a.emphasis === b.emphasis &&
|
|
@@ -470,90 +472,94 @@ export const removeLink: StateCommand = ({ state, dispatch }) => {
|
|
|
470
472
|
};
|
|
471
473
|
|
|
472
474
|
// Add link markup around the selection
|
|
473
|
-
export const addLink
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
475
|
+
export const addLink =
|
|
476
|
+
({ url, image }: { url?: string; image?: boolean } = {}): StateCommand =>
|
|
477
|
+
({ state, dispatch }) => {
|
|
478
|
+
const changes = state.changeByRange((range) => {
|
|
479
|
+
let { from, to } = range;
|
|
480
|
+
const cutStyles: SyntaxNode[] = [];
|
|
481
|
+
let okay: boolean | null = null;
|
|
482
|
+
// Check whether this range is in a position where a link makes sense.
|
|
483
|
+
syntaxTree(state).iterate({
|
|
484
|
+
from,
|
|
485
|
+
to,
|
|
486
|
+
enter: (node) => {
|
|
487
|
+
if (Object.hasOwn(Textblocks, node.name)) {
|
|
488
|
+
// If the selection spans multiple textblocks or is in a code block, abort.
|
|
489
|
+
okay =
|
|
490
|
+
Textblocks[node.name] !== 'codeblock' &&
|
|
491
|
+
from >= blockContentStart(node) &&
|
|
492
|
+
to <= blockContentEnd(node, state.doc);
|
|
493
|
+
} else if (Object.hasOwn(InlineMarker, node.name)) {
|
|
494
|
+
// Look for inline styles that partially overlap the range.
|
|
495
|
+
// Expand the range over them if they start directly outside, otherwise mark them for later.
|
|
496
|
+
const sNode = node.node;
|
|
497
|
+
if (node.from < from && node.to <= to) {
|
|
498
|
+
if (sNode.firstChild!.to === from) {
|
|
499
|
+
from = node.from;
|
|
500
|
+
} else {
|
|
501
|
+
cutStyles.push(sNode);
|
|
502
|
+
}
|
|
503
|
+
} else if (node.from >= from && node.to > to) {
|
|
504
|
+
if (sNode.lastChild!.from === to) {
|
|
505
|
+
to = node.to;
|
|
506
|
+
} else {
|
|
507
|
+
cutStyles.push(sNode);
|
|
508
|
+
}
|
|
506
509
|
}
|
|
507
510
|
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
+
},
|
|
512
|
+
});
|
|
511
513
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
514
|
+
if (okay === null) {
|
|
515
|
+
// No textblock found around selection. Check if the rest of the line is empty.
|
|
516
|
+
const line = state.doc.lineAt(from);
|
|
517
|
+
okay = to <= line.to && !/\S/.test(line.text.slice(from - line.from));
|
|
518
|
+
}
|
|
519
|
+
if (!okay) {
|
|
520
|
+
return { range };
|
|
521
|
+
}
|
|
520
522
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
523
|
+
const changes: ChangeSpec[] = [];
|
|
524
|
+
// Some changes must be moved to end of change array so that they are applied in the right order
|
|
525
|
+
const changesAfter: ChangeSpec[] = [];
|
|
526
|
+
// Clear existing links.
|
|
527
|
+
removeLinkInner(from, to, changesAfter, state);
|
|
528
|
+
let cursorOffset = 1;
|
|
529
|
+
// Close and reopen inline styles that partially overlap the range.
|
|
530
|
+
for (const style of cutStyles) {
|
|
531
|
+
const type = InlineMarker[style.name];
|
|
532
|
+
const mark = inlineMarkerText(type);
|
|
533
|
+
if (style.from < from) {
|
|
534
|
+
// Extends before.
|
|
535
|
+
changes.push({ from: skipSpaces(from, state.doc, -1), insert: mark });
|
|
536
|
+
changesAfter.push({ from: skipSpaces(from, state.doc, 1, to), insert: mark });
|
|
537
|
+
} else {
|
|
538
|
+
changes.push({ from: skipSpaces(to, state.doc, -1, from), insert: mark });
|
|
539
|
+
const after = skipSpaces(to, state.doc, 1);
|
|
540
|
+
if (after === to) {
|
|
541
|
+
cursorOffset += mark.length;
|
|
542
|
+
}
|
|
543
|
+
changesAfter.push({ from: after, insert: mark });
|
|
540
544
|
}
|
|
541
|
-
changesAfter.push({ from: after, insert: mark });
|
|
542
545
|
}
|
|
546
|
+
|
|
547
|
+
// Add the link markup.
|
|
548
|
+
changes.push({ from, insert: image ? '` });
|
|
549
|
+
const changeSet = state.changes(changes.concat(changesAfter));
|
|
550
|
+
// Put the cursor between the title or parenthesis.
|
|
551
|
+
return {
|
|
552
|
+
changes: changeSet,
|
|
553
|
+
range: EditorSelection.cursor(changeSet.mapPos(to, 1) - cursorOffset - (url ? url.length + 2 : 0)),
|
|
554
|
+
};
|
|
555
|
+
});
|
|
556
|
+
if (changes.changes.empty) {
|
|
557
|
+
return false;
|
|
543
558
|
}
|
|
544
|
-
// Add the link markup.
|
|
545
|
-
changes.push({ from, insert: '[' }, { from: to, insert: ']()' });
|
|
546
|
-
const changeSet = state.changes(changes.concat(changesAfter));
|
|
547
|
-
// Put the cursor between the parenthesis.
|
|
548
|
-
return { changes: changeSet, range: EditorSelection.cursor(changeSet.mapPos(to, 1) - cursorOffset) };
|
|
549
|
-
});
|
|
550
|
-
if (changes.changes.empty) {
|
|
551
|
-
return false;
|
|
552
|
-
}
|
|
553
559
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
};
|
|
560
|
+
dispatch(state.update(changes, { userEvent: 'format.link.add', scrollIntoView: true }));
|
|
561
|
+
return true;
|
|
562
|
+
};
|
|
557
563
|
|
|
558
564
|
//
|
|
559
565
|
// Lists
|
|
@@ -1057,7 +1063,7 @@ export const getFormatting = (state: EditorState): Formatting => {
|
|
|
1057
1063
|
const inline: (boolean | null)[] = [null, null, null, null];
|
|
1058
1064
|
let link: boolean = false;
|
|
1059
1065
|
let blockQuote: boolean | null = null;
|
|
1060
|
-
// False indicates mixed list styles
|
|
1066
|
+
// False indicates mixed list styles.
|
|
1061
1067
|
let listStyle: Formatting['listStyle'] | null | false = null;
|
|
1062
1068
|
|
|
1063
1069
|
// Track block context for list/blockquote handling.
|
|
@@ -1216,16 +1222,18 @@ export const getFormatting = (state: EditorState): Formatting => {
|
|
|
1216
1222
|
};
|
|
1217
1223
|
|
|
1218
1224
|
/**
|
|
1219
|
-
* Hook
|
|
1225
|
+
* Hook provides an extension to compute the current formatting state.
|
|
1220
1226
|
*/
|
|
1221
|
-
export const useFormattingState = (): [Formatting |
|
|
1222
|
-
const [state, setState] =
|
|
1227
|
+
export const useFormattingState = (): [Formatting | undefined, Extension] => {
|
|
1228
|
+
const [state, setState, stateRef] = useStateRef<Formatting>();
|
|
1223
1229
|
const observer = useMemo(
|
|
1224
1230
|
() =>
|
|
1225
1231
|
EditorView.updateListener.of((update) => {
|
|
1226
1232
|
if (update.docChanged || update.selectionSet) {
|
|
1227
1233
|
const newState = getFormatting(update.state);
|
|
1228
|
-
if (!
|
|
1234
|
+
if (!stateRef.current || !formattingEquals(stateRef.current, newState)) {
|
|
1235
|
+
// TODO(burdon): Error on first click on toolbar.
|
|
1236
|
+
// Warning: React has detected a change in the order of Hooks called by ForwardRef. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
|
|
1229
1237
|
setState(newState);
|
|
1230
1238
|
}
|
|
1231
1239
|
}
|
|
@@ -75,7 +75,11 @@ export const useActionHandler = (view?: EditorView | null): ToolbarProps['onActi
|
|
|
75
75
|
break;
|
|
76
76
|
|
|
77
77
|
case 'link':
|
|
78
|
-
(action.data === false ? removeLink : addLink)(view);
|
|
78
|
+
(action.data === false ? removeLink : addLink())(view);
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
case 'image':
|
|
82
|
+
addLink({ url: action.data, image: true })(view);
|
|
79
83
|
break;
|
|
80
84
|
|
|
81
85
|
case 'comment':
|
|
@@ -33,6 +33,8 @@ export const useTextEditor = (cb: () => UseTextEditorProps = () => ({}), deps: D
|
|
|
33
33
|
useEffect(() => {
|
|
34
34
|
let view: EditorView;
|
|
35
35
|
if (parentRef.current) {
|
|
36
|
+
log('create', { id });
|
|
37
|
+
|
|
36
38
|
// https://codemirror.net/docs/ref/#state.EditorStateConfig
|
|
37
39
|
const state = EditorState.create({
|
|
38
40
|
doc,
|
|
@@ -69,6 +71,7 @@ export const useTextEditor = (cb: () => UseTextEditorProps = () => ({}), deps: D
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
return () => {
|
|
74
|
+
log('destroy', { id });
|
|
72
75
|
view?.destroy();
|
|
73
76
|
};
|
|
74
77
|
}, deps);
|
package/src/translations.ts
CHANGED