@dxos/react-ui-editor 0.4.8-main.ac78619 → 0.4.8-main.beadd4b
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 +2703 -2714
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +18 -34
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +80 -5
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +10 -4
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +3 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts +2 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/defs.d.ts +1 -15
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/index.d.ts +0 -1
- package/dist/types/src/extensions/automerge/index.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/sync.d.ts +2 -1
- package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/{hooks → extensions/awareness}/awareness-provider.d.ts +1 -1
- package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -0
- package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -0
- package/dist/types/src/extensions/awareness/index.d.ts +3 -0
- package/dist/types/src/extensions/awareness/index.d.ts.map +1 -0
- package/dist/types/src/extensions/comments.d.ts +2 -2
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/cursor-line-margin.d.ts +6 -7
- package/dist/types/src/extensions/cursor-line-margin.d.ts.map +1 -1
- package/dist/types/src/extensions/cursor.d.ts +1 -1
- package/dist/types/src/extensions/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/doc.d.ts +3 -0
- package/dist/types/src/extensions/doc.d.ts.map +1 -0
- package/dist/types/src/extensions/factories.d.ts +49 -0
- package/dist/types/src/extensions/factories.d.ts.map +1 -0
- package/dist/types/src/extensions/index.d.ts +4 -1
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts +5 -4
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts +5 -2
- 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/extensions/markdown/table.d.ts +2 -2
- package/dist/types/src/extensions/markdown/table.d.ts.map +1 -1
- package/dist/types/src/extensions/modes.d.ts +1 -0
- package/dist/types/src/extensions/modes.d.ts.map +1 -1
- package/dist/types/src/extensions/state.d.ts +20 -0
- package/dist/types/src/extensions/state.d.ts.map +1 -0
- package/dist/types/src/extensions/types.d.ts +9 -0
- package/dist/types/src/extensions/types.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +1 -4
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useDocAccessor.d.ts +7 -0
- package/dist/types/src/hooks/useDocAccessor.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextEditor.d.ts +3 -44
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.stories.d.ts +7 -0
- package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts +0 -3
- package/dist/types/src/themes/default.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 -27
- package/src/components/TextEditor/TextEditor.stories.tsx +520 -23
- package/src/components/TextEditor/TextEditor.tsx +44 -155
- package/src/components/Toolbar/Toolbar.stories.tsx +44 -43
- package/src/components/Toolbar/Toolbar.tsx +86 -32
- package/src/extensions/automerge/automerge.stories.tsx +16 -17
- package/src/extensions/automerge/automerge.ts +2 -1
- package/src/extensions/automerge/cursor.ts +1 -1
- package/src/extensions/automerge/defs.ts +1 -22
- package/src/extensions/automerge/index.ts +0 -1
- package/src/extensions/automerge/sync.ts +2 -9
- package/src/extensions/automerge/update-automerge.ts +2 -1
- package/src/{hooks → extensions/awareness}/awareness-provider.ts +1 -1
- package/src/extensions/{awareness.ts → awareness/awareness.ts} +1 -1
- package/src/extensions/awareness/index.ts +6 -0
- package/src/extensions/command/state.ts +1 -1
- package/src/extensions/comments.ts +9 -5
- package/src/extensions/cursor-line-margin.ts +17 -18
- package/src/extensions/cursor.ts +1 -1
- package/src/extensions/doc.ts +14 -0
- package/src/extensions/factories.ts +167 -0
- package/src/extensions/index.ts +4 -1
- package/src/extensions/markdown/bundle.ts +11 -28
- package/src/extensions/markdown/decorate.ts +2 -4
- package/src/extensions/markdown/formatting.test.ts +13 -13
- package/src/extensions/markdown/formatting.ts +95 -86
- package/src/extensions/markdown/image.ts +7 -0
- package/src/extensions/markdown/table.ts +11 -3
- package/src/extensions/modes.ts +3 -1
- package/src/extensions/state.ts +90 -0
- package/src/extensions/types.ts +16 -0
- package/src/hooks/index.ts +1 -5
- package/src/hooks/useActionHandler.ts +5 -1
- package/src/hooks/useDocAccessor.ts +21 -0
- package/src/hooks/useTextEditor.stories.tsx +40 -35
- package/src/hooks/useTextEditor.ts +11 -120
- package/src/index.ts +1 -1
- package/src/themes/default.ts +1 -28
- package/src/translations.ts +1 -0
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +0 -103
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +0 -1
- package/dist/types/src/extensions/awareness.d.ts.map +0 -1
- package/dist/types/src/extensions/outliner.d.ts +0 -4
- package/dist/types/src/extensions/outliner.d.ts.map +0 -1
- package/dist/types/src/hooks/awareness-provider.d.ts.map +0 -1
- package/dist/types/src/hooks/defs.d.ts +0 -20
- package/dist/types/src/hooks/defs.d.ts.map +0 -1
- package/dist/types/src/hooks/useEditorView.d.ts +0 -20
- package/dist/types/src/hooks/useEditorView.d.ts.map +0 -1
- package/dist/types/src/hooks/useTextModel.d.ts +0 -33
- package/dist/types/src/hooks/useTextModel.d.ts.map +0 -1
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +0 -524
- package/src/extensions/outliner.ts +0 -12
- package/src/hooks/defs.ts +0 -30
- package/src/hooks/useEditorView.ts +0 -32
- package/src/hooks/useTextModel.ts +0 -121
- /package/dist/types/src/extensions/{awareness.d.ts → awareness/awareness.d.ts} +0 -0
|
@@ -161,7 +161,7 @@ export const setStyle =
|
|
|
161
161
|
({ state, dispatch }) => {
|
|
162
162
|
const marker = inlineMarkerText(type);
|
|
163
163
|
const changes = state.changeByRange((range) => {
|
|
164
|
-
// Special case for markers directly around the cursor, which will often not be parsed as valid styling
|
|
164
|
+
// Special case for markers directly around the cursor, which will often not be parsed as valid styling.
|
|
165
165
|
if (!enable && range.empty) {
|
|
166
166
|
const after = state.doc.sliceString(range.head, range.head + 6);
|
|
167
167
|
const found = after.indexOf(marker);
|
|
@@ -190,16 +190,16 @@ export const setStyle =
|
|
|
190
190
|
let startCovered: boolean | { from: number; to: number } = false;
|
|
191
191
|
let endCovered: boolean | { from: number; to: number } = false;
|
|
192
192
|
let { from, to } = range;
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
// the block's extent, that should be styled/unstyled.
|
|
193
|
+
|
|
194
|
+
// Iterate the selected range. For each textblock, determine a start and end position,
|
|
195
|
+
// the overlap of the selected range and the block's extent, that should be styled/unstyled.
|
|
196
196
|
syntaxTree(state).iterate({
|
|
197
197
|
from,
|
|
198
198
|
to,
|
|
199
199
|
enter: (node) => {
|
|
200
200
|
const { name } = node;
|
|
201
201
|
if (Object.hasOwn(Textblocks, name) && Textblocks[name] !== 'codeblock') {
|
|
202
|
-
// Set up for this textblock
|
|
202
|
+
// Set up for this textblock.
|
|
203
203
|
blockStart = blockContentStart(node);
|
|
204
204
|
blockEnd = blockContentEnd(node, state.doc);
|
|
205
205
|
startCovered = endCovered = false;
|
|
@@ -243,8 +243,7 @@ export const setStyle =
|
|
|
243
243
|
: true;
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
|
-
// Marks of the same type in range, or any mark if we're
|
|
247
|
-
// adding code style, need to be removed.
|
|
246
|
+
// Marks of the same type in range, or any mark if we're adding code style, need to be removed.
|
|
248
247
|
if (markType === type || (type === Inline.Code && enable)) {
|
|
249
248
|
if (node.from >= from && openEnd <= to) {
|
|
250
249
|
changes.push({ from: node.from, to: openEnd });
|
|
@@ -271,7 +270,7 @@ export const setStyle =
|
|
|
271
270
|
},
|
|
272
271
|
leave: (node) => {
|
|
273
272
|
if (Object.hasOwn(Textblocks, node.name) && Textblocks[node.name] !== 'codeblock') {
|
|
274
|
-
// Finish opening/closing the marks for this textblock
|
|
273
|
+
// Finish opening/closing the marks for this textblock.
|
|
275
274
|
const rangeStart = Math.max(from, blockStart);
|
|
276
275
|
const rangeEnd = Math.min(to, blockEnd);
|
|
277
276
|
if (enable) {
|
|
@@ -296,12 +295,14 @@ export const setStyle =
|
|
|
296
295
|
}
|
|
297
296
|
},
|
|
298
297
|
});
|
|
298
|
+
|
|
299
299
|
if (blockStart < 0 && range.empty && enable && !/\S/.test(state.doc.lineAt(range.from).text)) {
|
|
300
300
|
return {
|
|
301
301
|
changes: { from: range.head, insert: marker + marker },
|
|
302
302
|
range: EditorSelection.cursor(range.head + marker.length),
|
|
303
303
|
};
|
|
304
304
|
}
|
|
305
|
+
|
|
305
306
|
const changeSet = state.changes(changes.concat(changesAtEnd));
|
|
306
307
|
return {
|
|
307
308
|
changes: changeSet,
|
|
@@ -313,8 +314,12 @@ export const setStyle =
|
|
|
313
314
|
});
|
|
314
315
|
|
|
315
316
|
dispatch(
|
|
316
|
-
state.update(changes, {
|
|
317
|
+
state.update(changes, {
|
|
318
|
+
userEvent: enable ? 'format.style.add' : 'format.style.remove',
|
|
319
|
+
scrollIntoView: true,
|
|
320
|
+
}),
|
|
317
321
|
);
|
|
322
|
+
|
|
318
323
|
return true;
|
|
319
324
|
};
|
|
320
325
|
|
|
@@ -465,90 +470,94 @@ export const removeLink: StateCommand = ({ state, dispatch }) => {
|
|
|
465
470
|
};
|
|
466
471
|
|
|
467
472
|
// Add link markup around the selection
|
|
468
|
-
export const addLink
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
473
|
+
export const addLink =
|
|
474
|
+
({ url, image }: { url?: string; image?: boolean } = {}): StateCommand =>
|
|
475
|
+
({ state, dispatch }) => {
|
|
476
|
+
const changes = state.changeByRange((range) => {
|
|
477
|
+
let { from, to } = range;
|
|
478
|
+
const cutStyles: SyntaxNode[] = [];
|
|
479
|
+
let okay: boolean | null = null;
|
|
480
|
+
// Check whether this range is in a position where a link makes sense.
|
|
481
|
+
syntaxTree(state).iterate({
|
|
482
|
+
from,
|
|
483
|
+
to,
|
|
484
|
+
enter: (node) => {
|
|
485
|
+
if (Object.hasOwn(Textblocks, node.name)) {
|
|
486
|
+
// If the selection spans multiple textblocks or is in a code block, abort.
|
|
487
|
+
okay =
|
|
488
|
+
Textblocks[node.name] !== 'codeblock' &&
|
|
489
|
+
from >= blockContentStart(node) &&
|
|
490
|
+
to <= blockContentEnd(node, state.doc);
|
|
491
|
+
} else if (Object.hasOwn(InlineMarker, node.name)) {
|
|
492
|
+
// Look for inline styles that partially overlap the range.
|
|
493
|
+
// Expand the range over them if they start directly outside, otherwise mark them for later.
|
|
494
|
+
const sNode = node.node;
|
|
495
|
+
if (node.from < from && node.to <= to) {
|
|
496
|
+
if (sNode.firstChild!.to === from) {
|
|
497
|
+
from = node.from;
|
|
498
|
+
} else {
|
|
499
|
+
cutStyles.push(sNode);
|
|
500
|
+
}
|
|
501
|
+
} else if (node.from >= from && node.to > to) {
|
|
502
|
+
if (sNode.lastChild!.from === to) {
|
|
503
|
+
to = node.to;
|
|
504
|
+
} else {
|
|
505
|
+
cutStyles.push(sNode);
|
|
506
|
+
}
|
|
501
507
|
}
|
|
502
508
|
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
});
|
|
509
|
+
},
|
|
510
|
+
});
|
|
506
511
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
512
|
+
if (okay === null) {
|
|
513
|
+
// No textblock found around selection. Check if the rest of the line is empty.
|
|
514
|
+
const line = state.doc.lineAt(from);
|
|
515
|
+
okay = to <= line.to && !/\S/.test(line.text.slice(from - line.from));
|
|
516
|
+
}
|
|
517
|
+
if (!okay) {
|
|
518
|
+
return { range };
|
|
519
|
+
}
|
|
515
520
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
521
|
+
const changes: ChangeSpec[] = [];
|
|
522
|
+
// Some changes must be moved to end of change array so that they are applied in the right order
|
|
523
|
+
const changesAfter: ChangeSpec[] = [];
|
|
524
|
+
// Clear existing links.
|
|
525
|
+
removeLinkInner(from, to, changesAfter, state);
|
|
526
|
+
let cursorOffset = 1;
|
|
527
|
+
// Close and reopen inline styles that partially overlap the range.
|
|
528
|
+
for (const style of cutStyles) {
|
|
529
|
+
const type = InlineMarker[style.name];
|
|
530
|
+
const mark = inlineMarkerText(type);
|
|
531
|
+
if (style.from < from) {
|
|
532
|
+
// Extends before.
|
|
533
|
+
changes.push({ from: skipSpaces(from, state.doc, -1), insert: mark });
|
|
534
|
+
changesAfter.push({ from: skipSpaces(from, state.doc, 1, to), insert: mark });
|
|
535
|
+
} else {
|
|
536
|
+
changes.push({ from: skipSpaces(to, state.doc, -1, from), insert: mark });
|
|
537
|
+
const after = skipSpaces(to, state.doc, 1);
|
|
538
|
+
if (after === to) {
|
|
539
|
+
cursorOffset += mark.length;
|
|
540
|
+
}
|
|
541
|
+
changesAfter.push({ from: after, insert: mark });
|
|
535
542
|
}
|
|
536
|
-
changesAfter.push({ from: after, insert: mark });
|
|
537
543
|
}
|
|
544
|
+
|
|
545
|
+
// Add the link markup.
|
|
546
|
+
changes.push({ from, insert: image ? '` });
|
|
547
|
+
const changeSet = state.changes(changes.concat(changesAfter));
|
|
548
|
+
// Put the cursor between the title or parenthesis.
|
|
549
|
+
return {
|
|
550
|
+
changes: changeSet,
|
|
551
|
+
range: EditorSelection.cursor(changeSet.mapPos(to, 1) - cursorOffset - (url ? url.length + 2 : 0)),
|
|
552
|
+
};
|
|
553
|
+
});
|
|
554
|
+
if (changes.changes.empty) {
|
|
555
|
+
return false;
|
|
538
556
|
}
|
|
539
|
-
// Add the link markup.
|
|
540
|
-
changes.push({ from, insert: '[' }, { from: to, insert: ']()' });
|
|
541
|
-
const changeSet = state.changes(changes.concat(changesAfter));
|
|
542
|
-
// Put the cursor between the parenthesis.
|
|
543
|
-
return { changes: changeSet, range: EditorSelection.cursor(changeSet.mapPos(to, 1) - cursorOffset) };
|
|
544
|
-
});
|
|
545
|
-
if (changes.changes.empty) {
|
|
546
|
-
return false;
|
|
547
|
-
}
|
|
548
557
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
};
|
|
558
|
+
dispatch(state.update(changes, { userEvent: 'format.link.add', scrollIntoView: true }));
|
|
559
|
+
return true;
|
|
560
|
+
};
|
|
552
561
|
|
|
553
562
|
//
|
|
554
563
|
// Lists
|
|
@@ -987,7 +996,7 @@ export const toggleCodeblock: StateCommand = (target) => {
|
|
|
987
996
|
|
|
988
997
|
export type FormattingOptions = {};
|
|
989
998
|
|
|
990
|
-
export const
|
|
999
|
+
export const formattingKeymap = (options: FormattingOptions = {}): Extension => {
|
|
991
1000
|
return [
|
|
992
1001
|
keymap.of([
|
|
993
1002
|
{
|
|
@@ -61,6 +61,7 @@ const buildDecorations = (from: number, to: number, state: EditorState) => {
|
|
|
61
61
|
if (urlNode) {
|
|
62
62
|
const hide = state.readOnly || cursor < node.from || cursor > node.to;
|
|
63
63
|
const url = state.sliceDoc(urlNode.from, urlNode.to);
|
|
64
|
+
// TODO(burdon): Doesn't load if scrolling with mouse.
|
|
64
65
|
preloadImage(url);
|
|
65
66
|
decorations.push(
|
|
66
67
|
Decoration.replace({
|
|
@@ -96,3 +97,9 @@ class ImageWidget extends WidgetType {
|
|
|
96
97
|
return img;
|
|
97
98
|
}
|
|
98
99
|
}
|
|
100
|
+
|
|
101
|
+
export type ImageUploadOptions = {
|
|
102
|
+
onSelect: () => { url: string };
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const imageUpload = (options: ImageOptions = {}) => {};
|
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { syntaxTree } from '@codemirror/language';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type EditorState,
|
|
8
|
+
type Extension,
|
|
9
|
+
type RangeSet,
|
|
10
|
+
RangeSetBuilder,
|
|
11
|
+
StateField,
|
|
12
|
+
type Transaction,
|
|
13
|
+
} from '@codemirror/state';
|
|
7
14
|
import { Decoration, EditorView, WidgetType } from '@codemirror/view';
|
|
8
15
|
|
|
9
16
|
// TODO(burdon): Snippet to create basic table.
|
|
@@ -18,7 +25,7 @@ export type TableOptions = {};
|
|
|
18
25
|
* GFM tables.
|
|
19
26
|
* https://github.github.com/gfm/#tables-extension
|
|
20
27
|
*/
|
|
21
|
-
export const table = (options: TableOptions = {}) => {
|
|
28
|
+
export const table = (options: TableOptions = {}): Extension => {
|
|
22
29
|
return StateField.define<RangeSet<any>>({
|
|
23
30
|
create: (state) => update(state, options),
|
|
24
31
|
update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
|
|
@@ -76,7 +83,7 @@ const update = (state: EditorState, options: TableOptions) => {
|
|
|
76
83
|
|
|
77
84
|
tables.forEach((table) => {
|
|
78
85
|
const hide = state.readOnly || cursor < table.from || cursor > table.to;
|
|
79
|
-
hide
|
|
86
|
+
if (hide) {
|
|
80
87
|
builder.add(
|
|
81
88
|
table.from,
|
|
82
89
|
table.to,
|
|
@@ -84,6 +91,7 @@ const update = (state: EditorState, options: TableOptions) => {
|
|
|
84
91
|
widget: new TableWidget(table),
|
|
85
92
|
}),
|
|
86
93
|
);
|
|
94
|
+
}
|
|
87
95
|
|
|
88
96
|
// Add class for styling.
|
|
89
97
|
builder.add(table.from, table.to, Decoration.mark({ class: 'cm-table' }));
|
package/src/extensions/modes.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { type Extension, Facet } from '@codemirror/state';
|
|
|
6
6
|
import { keymap } from '@codemirror/view';
|
|
7
7
|
import { vim } from '@replit/codemirror-vim';
|
|
8
8
|
|
|
9
|
+
export const focusEvent = 'focus.container';
|
|
10
|
+
|
|
9
11
|
export type EditorMode = 'default' | 'vim' | undefined;
|
|
10
12
|
|
|
11
13
|
export type EditorConfig = {
|
|
@@ -27,7 +29,7 @@ export const EditorModes: { [mode: string]: Extension } = {
|
|
|
27
29
|
key: 'Alt-Escape',
|
|
28
30
|
run: (view) => {
|
|
29
31
|
// Focus container for tab navigation.
|
|
30
|
-
view.dispatch({ userEvent:
|
|
32
|
+
view.dispatch({ userEvent: focusEvent });
|
|
31
33
|
return true;
|
|
32
34
|
},
|
|
33
35
|
},
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension, Transaction } from '@codemirror/state';
|
|
6
|
+
import { EditorView, keymap } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
import { debounce } from '@dxos/async';
|
|
9
|
+
import { invariant } from '@dxos/invariant';
|
|
10
|
+
import { isNotFalsy } from '@dxos/util';
|
|
11
|
+
|
|
12
|
+
import { documentId } from './doc';
|
|
13
|
+
|
|
14
|
+
const scrollAnnotation = 'dxos.org/cm/scrolling';
|
|
15
|
+
|
|
16
|
+
// NOTE: Serializable.
|
|
17
|
+
export type SelectionState = {
|
|
18
|
+
scrollTo: {
|
|
19
|
+
from: number;
|
|
20
|
+
};
|
|
21
|
+
selection: {
|
|
22
|
+
anchor: number;
|
|
23
|
+
head?: number;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type StateOptions = {
|
|
28
|
+
setState: (id: string, state: SelectionState) => void;
|
|
29
|
+
getState: (id: string) => SelectionState | undefined;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const keyPrefix = 'dxos.org/react-ui-editor/state';
|
|
33
|
+
export const localStorageStateStoreAdapter: StateOptions = {
|
|
34
|
+
setState: (id, state) => {
|
|
35
|
+
invariant(id);
|
|
36
|
+
localStorage.setItem(`${keyPrefix}/${id}`, JSON.stringify(state));
|
|
37
|
+
},
|
|
38
|
+
getState: (id) => {
|
|
39
|
+
invariant(id);
|
|
40
|
+
const state = localStorage.getItem(`${keyPrefix}/${id}`);
|
|
41
|
+
return state ? JSON.parse(state) : undefined;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Track scrolling and selection state to be restored when switching to document.
|
|
47
|
+
*/
|
|
48
|
+
export const state = ({ getState, setState }: Partial<StateOptions> = {}): Extension => {
|
|
49
|
+
const setStateDebounced = debounce(setState!, 1_000);
|
|
50
|
+
|
|
51
|
+
return [
|
|
52
|
+
// TODO(burdon): Track scrolling (currently only updates when cursor moves).
|
|
53
|
+
EditorView.updateListener.of(({ view, changes, transactions }) => {
|
|
54
|
+
// TODO(burdon): Don't react to initial scroll.
|
|
55
|
+
const id = view.state.facet(documentId);
|
|
56
|
+
if (!id || transactions.some((tr) => tr.isUserEvent(scrollAnnotation))) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (setState) {
|
|
61
|
+
const { top } = view.dom.getBoundingClientRect();
|
|
62
|
+
const pos = view.posAtCoords({ x: 0, y: top });
|
|
63
|
+
if (pos !== null) {
|
|
64
|
+
const { anchor, head } = view.state.selection.main;
|
|
65
|
+
setStateDebounced(id, {
|
|
66
|
+
scrollTo: { from: pos, yMargin: 0 },
|
|
67
|
+
selection: { anchor, head },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}),
|
|
72
|
+
getState &&
|
|
73
|
+
keymap.of([
|
|
74
|
+
{
|
|
75
|
+
key: 'ctrl-r', // TODO(burdon): Setting to jump back to bookmark.
|
|
76
|
+
run: (view) => {
|
|
77
|
+
const state = getState(view.state.facet(documentId));
|
|
78
|
+
if (state) {
|
|
79
|
+
view.dispatch({
|
|
80
|
+
effects: EditorView.scrollIntoView(state.scrollTo.from, { yMargin: 0 }),
|
|
81
|
+
selection: state.selection,
|
|
82
|
+
annotations: Transaction.userEvent.of(scrollAnnotation),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
]),
|
|
89
|
+
].filter(isNotFalsy);
|
|
90
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// Runtime data structure.
|
|
6
|
+
export type Range = {
|
|
7
|
+
from: number;
|
|
8
|
+
to: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Persistent data structure.
|
|
12
|
+
// TODO(burdon): Rename annotation?
|
|
13
|
+
export type Comment = {
|
|
14
|
+
id: string;
|
|
15
|
+
cursor?: string;
|
|
16
|
+
};
|
package/src/hooks/index.ts
CHANGED
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
export * from './awareness-provider';
|
|
6
|
-
export * from './defs';
|
|
7
|
-
|
|
8
5
|
export * from './useActionHandler';
|
|
9
|
-
export * from './
|
|
6
|
+
export * from './useDocAccessor';
|
|
10
7
|
export * from './useTextEditor';
|
|
11
|
-
export * from './useTextModel';
|
|
@@ -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':
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useMemo } from 'react';
|
|
6
|
+
|
|
7
|
+
import { createDocAccessor, type DocAccessor, getTextContent, type TextObject } from '@dxos/echo-schema';
|
|
8
|
+
|
|
9
|
+
// TODO(burdon): Factor out.
|
|
10
|
+
export const useDocAccessor = <T = any>(
|
|
11
|
+
text: TextObject,
|
|
12
|
+
): { id: string; doc: string | undefined; accessor: DocAccessor<T> } => {
|
|
13
|
+
return useMemo(
|
|
14
|
+
() => ({
|
|
15
|
+
id: text.id,
|
|
16
|
+
doc: getTextContent(text),
|
|
17
|
+
accessor: createDocAccessor<T>(text),
|
|
18
|
+
}),
|
|
19
|
+
[text],
|
|
20
|
+
);
|
|
21
|
+
};
|
|
@@ -4,34 +4,28 @@
|
|
|
4
4
|
|
|
5
5
|
import '@dxosTheme';
|
|
6
6
|
|
|
7
|
-
import React, {
|
|
7
|
+
import React, { useState } from 'react';
|
|
8
8
|
|
|
9
9
|
import { Toolbar as NaturalToolbar, Select, useThemeContext, Tooltip } from '@dxos/react-ui';
|
|
10
10
|
import { attentionSurface, mx, textBlockWidth } from '@dxos/react-ui-theme';
|
|
11
11
|
import { withTheme } from '@dxos/storybook-utils';
|
|
12
12
|
|
|
13
13
|
import { useActionHandler } from './useActionHandler';
|
|
14
|
-
import {
|
|
14
|
+
import { useTextEditor } from './useTextEditor';
|
|
15
15
|
import { Toolbar } from '../components';
|
|
16
|
+
import { createBasicExtensions, createThemeExtensions } from '../extensions';
|
|
16
17
|
import {
|
|
17
18
|
type EditorMode,
|
|
18
19
|
EditorModes,
|
|
19
20
|
decorateMarkdown,
|
|
20
21
|
createMarkdownExtensions,
|
|
21
|
-
|
|
22
|
-
image,
|
|
22
|
+
formattingKeymap,
|
|
23
23
|
table,
|
|
24
24
|
useFormattingState,
|
|
25
|
+
image,
|
|
25
26
|
} from '../extensions';
|
|
26
|
-
import { markdownTheme } from '../themes';
|
|
27
27
|
import translations from '../translations';
|
|
28
28
|
|
|
29
|
-
// TODO(burdon): Demo toolbar with hooks.
|
|
30
|
-
// TODO(burdon): Build components from hooks and adapters for model/extensions, etc.
|
|
31
|
-
// TODO(burdon): Remove BaseTextEditor.
|
|
32
|
-
// TODO(burdon): Move scrolling container layout/logic into TextEditor/MarkdownEditor components.
|
|
33
|
-
// TODO(burdon): Define keymap in composer framework format.
|
|
34
|
-
|
|
35
29
|
type StoryProps = {
|
|
36
30
|
autoFocus?: boolean;
|
|
37
31
|
placeholder?: string;
|
|
@@ -43,29 +37,24 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
|
43
37
|
const { themeMode } = useThemeContext();
|
|
44
38
|
const [formattingState, trackFormatting] = useFormattingState();
|
|
45
39
|
const [editorMode, setEditorMode] = useState<EditorMode>('default');
|
|
46
|
-
const
|
|
47
|
-
() =>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
table(),
|
|
64
|
-
trackFormatting,
|
|
65
|
-
],
|
|
66
|
-
[editorMode, themeMode, placeholder, trackFormatting, readonly],
|
|
40
|
+
const { parentRef, view } = useTextEditor(
|
|
41
|
+
() => ({
|
|
42
|
+
autoFocus,
|
|
43
|
+
doc,
|
|
44
|
+
extensions: [
|
|
45
|
+
editorMode ? EditorModes[editorMode] : [],
|
|
46
|
+
createBasicExtensions({ placeholder, lineWrapping: true, readonly }),
|
|
47
|
+
createMarkdownExtensions({ themeMode }),
|
|
48
|
+
createThemeExtensions({ themeMode }),
|
|
49
|
+
decorateMarkdown(),
|
|
50
|
+
formattingKeymap(),
|
|
51
|
+
image(),
|
|
52
|
+
table(),
|
|
53
|
+
trackFormatting,
|
|
54
|
+
],
|
|
55
|
+
}),
|
|
56
|
+
[editorMode, themeMode, placeholder, readonly],
|
|
67
57
|
);
|
|
68
|
-
const { parentRef, view } = useTextEditor({ autoFocus, doc, extensions });
|
|
69
58
|
|
|
70
59
|
const handleAction = useActionHandler(view);
|
|
71
60
|
|
|
@@ -78,7 +67,7 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
|
78
67
|
<EditorModeToolbar editorMode={editorMode} setEditorMode={setEditorMode} />
|
|
79
68
|
</Toolbar.Root>
|
|
80
69
|
<div role='none' className='grow overflow-hidden'>
|
|
81
|
-
<div
|
|
70
|
+
<div className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />
|
|
82
71
|
</div>
|
|
83
72
|
</div>
|
|
84
73
|
);
|
|
@@ -116,7 +105,6 @@ const EditorModeToolbar = ({
|
|
|
116
105
|
|
|
117
106
|
export default {
|
|
118
107
|
title: 'react-ui-editor/useTextEditor',
|
|
119
|
-
// component: TextEditor,
|
|
120
108
|
decorators: [withTheme],
|
|
121
109
|
render: (args: StoryProps) => (
|
|
122
110
|
<Tooltip.Provider>
|
|
@@ -127,6 +115,23 @@ export default {
|
|
|
127
115
|
};
|
|
128
116
|
|
|
129
117
|
export const Default = {
|
|
118
|
+
render: () => {
|
|
119
|
+
const { parentRef } = useTextEditor();
|
|
120
|
+
return <div className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const Basic = {
|
|
125
|
+
render: () => {
|
|
126
|
+
const { themeMode } = useThemeContext();
|
|
127
|
+
const { parentRef } = useTextEditor(() => ({
|
|
128
|
+
extensions: [createBasicExtensions({ placeholder: 'Enter text...' }), createThemeExtensions({ themeMode })],
|
|
129
|
+
}));
|
|
130
|
+
return <div className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />;
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const Markdown = {
|
|
130
135
|
args: {
|
|
131
136
|
autoFocus: true,
|
|
132
137
|
placeholder: 'Text...',
|