@dxos/react-ui-editor 0.4.8-main.05fda5d → 0.4.8-main.06fa7e4
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 +1938 -1920
- 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 +12 -11
- 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.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.d.ts +1 -1
- package/dist/types/src/extensions/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/dnd.d.ts +7 -1
- package/dist/types/src/extensions/dnd.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 +53 -0
- package/dist/types/src/extensions/factories.d.ts.map +1 -0
- package/dist/types/src/extensions/index.d.ts +4 -2
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/action.d.ts +9 -0
- package/dist/types/src/extensions/markdown/action.d.ts.map +1 -0
- 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 +8 -5
- 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/index.d.ts +1 -0
- package/dist/types/src/extensions/markdown/index.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 +2 -1
- 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 +2 -2
- package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useDocAccessor.d.ts +9 -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 +26 -27
- package/src/components/TextEditor/TextEditor.stories.tsx +532 -23
- package/src/components/TextEditor/TextEditor.tsx +45 -156
- package/src/components/Toolbar/Toolbar.stories.tsx +54 -55
- package/src/components/Toolbar/Toolbar.tsx +102 -60
- package/src/extensions/automerge/automerge.stories.tsx +16 -17
- package/src/extensions/automerge/automerge.ts +6 -5
- 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 +3 -3
- package/src/extensions/comments.ts +9 -5
- package/src/extensions/cursor.ts +1 -1
- package/src/extensions/dnd.ts +31 -9
- package/src/extensions/doc.ts +14 -0
- package/src/extensions/factories.ts +193 -0
- package/src/extensions/index.ts +4 -2
- package/src/extensions/markdown/action.ts +112 -0
- package/src/extensions/markdown/bundle.ts +17 -37
- package/src/extensions/markdown/decorate.ts +2 -4
- package/src/extensions/markdown/formatting.test.ts +13 -13
- package/src/extensions/markdown/formatting.ts +110 -97
- package/src/extensions/markdown/image.ts +7 -0
- package/src/extensions/markdown/index.ts +1 -0
- package/src/extensions/markdown/table.ts +11 -3
- package/src/extensions/modes.ts +11 -2
- 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 +4 -85
- package/src/hooks/useDocAccessor.ts +27 -0
- package/src/hooks/useTextEditor.stories.tsx +40 -35
- package/src/hooks/useTextEditor.ts +14 -120
- package/src/index.ts +1 -1
- package/src/themes/default.ts +25 -36
- 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/cursor-line-margin.d.ts +0 -8
- package/dist/types/src/extensions/cursor-line-margin.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/cursor-line-margin.ts +0 -95
- 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
|
@@ -15,7 +15,7 @@ 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, useState } from 'react';
|
|
19
19
|
|
|
20
20
|
// Markdown refs:
|
|
21
21
|
// https://github.github.com/gfm
|
|
@@ -56,7 +56,7 @@ export type Formatting = {
|
|
|
56
56
|
listStyle: null | 'ordered' | 'bullet' | 'task';
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
export const
|
|
59
|
+
export const formattingEquals = (a: Formatting, b: Formatting) =>
|
|
60
60
|
a.blockType === b.blockType &&
|
|
61
61
|
a.strong === b.strong &&
|
|
62
62
|
a.emphasis === b.emphasis &&
|
|
@@ -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
|
{
|
|
@@ -1052,7 +1061,7 @@ export const getFormatting = (state: EditorState): Formatting => {
|
|
|
1052
1061
|
const inline: (boolean | null)[] = [null, null, null, null];
|
|
1053
1062
|
let link: boolean = false;
|
|
1054
1063
|
let blockQuote: boolean | null = null;
|
|
1055
|
-
// False indicates mixed list styles
|
|
1064
|
+
// False indicates mixed list styles.
|
|
1056
1065
|
let listStyle: Formatting['listStyle'] | null | false = null;
|
|
1057
1066
|
|
|
1058
1067
|
// Track block context for list/blockquote handling.
|
|
@@ -1211,21 +1220,25 @@ export const getFormatting = (state: EditorState): Formatting => {
|
|
|
1211
1220
|
};
|
|
1212
1221
|
|
|
1213
1222
|
/**
|
|
1214
|
-
* Hook
|
|
1223
|
+
* Hook provides an extension to compute the current formatting state.
|
|
1215
1224
|
*/
|
|
1216
|
-
export const useFormattingState = (): [Formatting |
|
|
1217
|
-
const [state, setState] = useState<Formatting
|
|
1225
|
+
export const useFormattingState = (): [Formatting | undefined, Extension] => {
|
|
1226
|
+
const [state, setState] = useState<Formatting>();
|
|
1227
|
+
|
|
1218
1228
|
const observer = useMemo(
|
|
1219
1229
|
() =>
|
|
1220
1230
|
EditorView.updateListener.of((update) => {
|
|
1221
1231
|
if (update.docChanged || update.selectionSet) {
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1232
|
+
setState((prevState) => {
|
|
1233
|
+
const newState = getFormatting(update.state);
|
|
1234
|
+
if (!prevState || !formattingEquals(prevState, newState)) {
|
|
1235
|
+
return newState;
|
|
1236
|
+
}
|
|
1237
|
+
return prevState;
|
|
1238
|
+
});
|
|
1226
1239
|
}
|
|
1227
1240
|
}),
|
|
1228
|
-
[],
|
|
1241
|
+
[setState],
|
|
1229
1242
|
);
|
|
1230
1243
|
|
|
1231
1244
|
return [state, observer];
|
|
@@ -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
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
import { type Extension, Facet } from '@codemirror/state';
|
|
6
6
|
import { keymap } from '@codemirror/view';
|
|
7
7
|
import { vim } from '@replit/codemirror-vim';
|
|
8
|
+
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
|
8
9
|
|
|
9
|
-
export
|
|
10
|
+
export const focusEvent = 'focus.container';
|
|
11
|
+
|
|
12
|
+
export type EditorMode = 'default' | 'vim' | 'vscode' | undefined;
|
|
10
13
|
|
|
11
14
|
export type EditorConfig = {
|
|
12
15
|
type: string;
|
|
@@ -19,7 +22,13 @@ export const editorMode = Facet.define<EditorConfig, EditorConfig>({
|
|
|
19
22
|
|
|
20
23
|
export const EditorModes: { [mode: string]: Extension } = {
|
|
21
24
|
default: [],
|
|
25
|
+
vscode: [
|
|
26
|
+
// https://github.com/replit/codemirror-vscode-keymap
|
|
27
|
+
editorMode.of({ type: 'vscode' }),
|
|
28
|
+
keymap.of(vscodeKeymap),
|
|
29
|
+
],
|
|
22
30
|
vim: [
|
|
31
|
+
// https://github.com/replit/codemirror-vim
|
|
23
32
|
vim(),
|
|
24
33
|
editorMode.of({ type: 'vim', noTabster: true }),
|
|
25
34
|
keymap.of([
|
|
@@ -27,7 +36,7 @@ export const EditorModes: { [mode: string]: Extension } = {
|
|
|
27
36
|
key: 'Alt-Escape',
|
|
28
37
|
run: (view) => {
|
|
29
38
|
// Focus container for tab navigation.
|
|
30
|
-
view.dispatch({ userEvent:
|
|
39
|
+
view.dispatch({ userEvent: focusEvent });
|
|
31
40
|
return true;
|
|
32
41
|
},
|
|
33
42
|
},
|
|
@@ -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';
|
|
@@ -2,92 +2,11 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import type
|
|
5
|
+
import { type EditorView } from '@codemirror/view';
|
|
6
6
|
|
|
7
|
-
import type
|
|
8
|
-
import {
|
|
9
|
-
Inline,
|
|
10
|
-
List,
|
|
11
|
-
addBlockquote,
|
|
12
|
-
addCodeblock,
|
|
13
|
-
addLink,
|
|
14
|
-
addList,
|
|
15
|
-
createComment,
|
|
16
|
-
insertTable,
|
|
17
|
-
removeBlockquote,
|
|
18
|
-
removeCodeblock,
|
|
19
|
-
removeLink,
|
|
20
|
-
removeList,
|
|
21
|
-
setHeading,
|
|
22
|
-
setStyle,
|
|
23
|
-
toggleBlockquote,
|
|
24
|
-
toggleList,
|
|
25
|
-
toggleStyle,
|
|
26
|
-
} from '../extensions';
|
|
7
|
+
import { type ToolbarProps } from '../components';
|
|
8
|
+
import { processAction } from '../extensions';
|
|
27
9
|
|
|
28
10
|
export const useActionHandler = (view?: EditorView | null): ToolbarProps['onAction'] => {
|
|
29
|
-
return (action) =>
|
|
30
|
-
if (!view) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
let inlineType, listType;
|
|
35
|
-
switch (action.type) {
|
|
36
|
-
case 'heading':
|
|
37
|
-
setHeading(parseInt(action.data))(view);
|
|
38
|
-
break;
|
|
39
|
-
|
|
40
|
-
case 'strong':
|
|
41
|
-
case 'emphasis':
|
|
42
|
-
case 'strikethrough':
|
|
43
|
-
case 'code':
|
|
44
|
-
inlineType =
|
|
45
|
-
action.type === 'strong'
|
|
46
|
-
? Inline.Strong
|
|
47
|
-
: action.type === 'emphasis'
|
|
48
|
-
? Inline.Emphasis
|
|
49
|
-
: action.type === 'strikethrough'
|
|
50
|
-
? Inline.Strikethrough
|
|
51
|
-
: Inline.Code;
|
|
52
|
-
(typeof action.data === 'boolean' ? setStyle(inlineType, action.data) : toggleStyle(inlineType))(view);
|
|
53
|
-
break;
|
|
54
|
-
|
|
55
|
-
case 'list-ordered':
|
|
56
|
-
case 'list-bullet':
|
|
57
|
-
case 'list-task':
|
|
58
|
-
listType =
|
|
59
|
-
action.type === 'list-ordered' ? List.Ordered : action.type === 'list-bullet' ? List.Bullet : List.Task;
|
|
60
|
-
(action.data === false
|
|
61
|
-
? removeList(listType)
|
|
62
|
-
: action.data === true
|
|
63
|
-
? addList(listType)
|
|
64
|
-
: toggleList(listType))(view);
|
|
65
|
-
break;
|
|
66
|
-
|
|
67
|
-
case 'blockquote':
|
|
68
|
-
(action.data === false ? removeBlockquote : action.data === true ? addBlockquote : toggleBlockquote)(view);
|
|
69
|
-
break;
|
|
70
|
-
case 'codeblock':
|
|
71
|
-
(action.data === false ? removeCodeblock : addCodeblock)(view);
|
|
72
|
-
break;
|
|
73
|
-
case 'table':
|
|
74
|
-
insertTable(view);
|
|
75
|
-
break;
|
|
76
|
-
|
|
77
|
-
case 'link':
|
|
78
|
-
(action.data === false ? removeLink : addLink)(view);
|
|
79
|
-
break;
|
|
80
|
-
|
|
81
|
-
case 'comment':
|
|
82
|
-
createComment(view);
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// TODO(burdon): Hack otherwise remains on heading selector.
|
|
87
|
-
setTimeout(() => {
|
|
88
|
-
if (!view.hasFocus) {
|
|
89
|
-
view.focus();
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
};
|
|
11
|
+
return (action) => view && processAction(view, action);
|
|
93
12
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useMemo } from 'react';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
createDocAccessor,
|
|
9
|
+
type DocAccessor,
|
|
10
|
+
getTextContent,
|
|
11
|
+
type TextObject,
|
|
12
|
+
type EchoReactiveObject,
|
|
13
|
+
} from '@dxos/echo-schema';
|
|
14
|
+
|
|
15
|
+
// TODO(burdon): Factor out.
|
|
16
|
+
export const useDocAccessor = <T = any>(
|
|
17
|
+
text: TextObject | EchoReactiveObject<{ content: string }>,
|
|
18
|
+
): { id: string; doc: string | undefined; accessor: DocAccessor<T> } => {
|
|
19
|
+
return useMemo(
|
|
20
|
+
() => ({
|
|
21
|
+
id: text.id,
|
|
22
|
+
doc: getTextContent(text),
|
|
23
|
+
accessor: createDocAccessor<T>(text),
|
|
24
|
+
}),
|
|
25
|
+
[text],
|
|
26
|
+
);
|
|
27
|
+
};
|