@dxos/react-ui-editor 0.3.9-main.4e30508 → 0.3.9-main.501fe30

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.
Files changed (73) hide show
  1. package/dist/lib/browser/index.mjs +412 -56
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts +2 -2
  5. package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +1 -1
  6. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts +2 -2
  7. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +1 -1
  8. package/dist/types/src/automerge/automerge-plugin/handle.d.ts +9 -0
  9. package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +1 -0
  10. package/dist/types/src/automerge/automerge-plugin/index.d.ts +2 -2
  11. package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +1 -1
  12. package/dist/types/src/automerge/automerge-plugin/plugin.d.ts +6 -5
  13. package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +1 -1
  14. package/dist/types/src/automerge/automerge.stories.d.ts +1 -0
  15. package/dist/types/src/automerge/automerge.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Editor/Editor.d.ts +0 -1
  17. package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
  18. package/dist/types/src/components/Editor/Editor.stories.d.ts +3 -8
  19. package/dist/types/src/components/Editor/Editor.stories.d.ts.map +1 -1
  20. package/dist/types/src/components/Markdown/Markdown.d.ts +4 -2
  21. package/dist/types/src/components/Markdown/Markdown.d.ts.map +1 -1
  22. package/dist/types/src/components/Markdown/Markdown.stories.d.ts +1 -1
  23. package/dist/types/src/components/Markdown/Markdown.stories.d.ts.map +1 -1
  24. package/dist/types/src/components/Markdown/markdownTheme.d.ts +10 -17
  25. package/dist/types/src/components/Markdown/markdownTheme.d.ts.map +1 -1
  26. package/dist/types/src/components/RichText/RichText.stories.d.ts +2 -3
  27. package/dist/types/src/components/RichText/RichText.stories.d.ts.map +1 -1
  28. package/dist/types/src/components/TextEditor/TextEditor.d.ts +39 -0
  29. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -0
  30. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +18 -0
  31. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -0
  32. package/dist/types/src/components/TextEditor/extensions/hyperlinkDecoration.d.ts +11 -0
  33. package/dist/types/src/components/TextEditor/extensions/hyperlinkDecoration.d.ts.map +1 -0
  34. package/dist/types/src/components/TextEditor/extensions/hyperlinkTooltip.d.ts +8 -0
  35. package/dist/types/src/components/TextEditor/extensions/hyperlinkTooltip.d.ts.map +1 -0
  36. package/dist/types/src/components/TextEditor/extensions/hyperlinkWidget.d.ts +10 -0
  37. package/dist/types/src/components/TextEditor/extensions/hyperlinkWidget.d.ts.map +1 -0
  38. package/dist/types/src/components/TextEditor/extensions/index.d.ts +4 -0
  39. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -0
  40. package/dist/types/src/components/TextEditor/index.d.ts +6 -0
  41. package/dist/types/src/components/TextEditor/index.d.ts.map +1 -0
  42. package/dist/types/src/components/TextEditor/theme.d.ts +9 -0
  43. package/dist/types/src/components/TextEditor/theme.d.ts.map +1 -0
  44. package/dist/types/src/components/index.d.ts +1 -0
  45. package/dist/types/src/components/index.d.ts.map +1 -1
  46. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  47. package/dist/types/stories/html/code-mirror.stories.d.ts.map +1 -1
  48. package/package.json +30 -27
  49. package/src/automerge/automerge-plugin/PatchSemaphore.ts +5 -5
  50. package/src/automerge/automerge-plugin/codeMirrorToAm.ts +10 -7
  51. package/src/automerge/automerge-plugin/handle.ts +14 -0
  52. package/src/automerge/automerge-plugin/index.ts +2 -2
  53. package/src/automerge/automerge-plugin/plugin.ts +39 -10
  54. package/src/automerge/automerge.stories.tsx +31 -47
  55. package/src/components/Editor/Editor.stories.tsx +7 -7
  56. package/src/components/Editor/Editor.tsx +6 -3
  57. package/src/components/Markdown/Markdown.stories.tsx +39 -25
  58. package/src/components/Markdown/Markdown.tsx +63 -30
  59. package/src/components/Markdown/markdownTheme.ts +32 -17
  60. package/src/components/RichText/RichText.stories.tsx +37 -33
  61. package/src/components/TextEditor/TextEditor.stories.tsx +72 -0
  62. package/src/components/TextEditor/TextEditor.tsx +131 -0
  63. package/src/components/TextEditor/extensions/hyperlinkDecoration.ts +99 -0
  64. package/src/components/TextEditor/extensions/hyperlinkTooltip.tsx +61 -0
  65. package/src/components/TextEditor/extensions/hyperlinkWidget.tsx +119 -0
  66. package/src/components/TextEditor/extensions/index.ts +7 -0
  67. package/src/components/TextEditor/index.ts +11 -0
  68. package/src/components/TextEditor/theme.ts +35 -0
  69. package/src/components/index.ts +1 -0
  70. package/src/testing/replicator.ts +0 -1
  71. package/dist/types/src/automerge/demo.d.ts +0 -25
  72. package/dist/types/src/automerge/demo.d.ts.map +0 -1
  73. package/src/automerge/demo.ts +0 -106
@@ -8,15 +8,17 @@ import React, { useState } from 'react';
8
8
 
9
9
  import { PublicKey } from '@dxos/react-client';
10
10
  import { TextKind } from '@dxos/react-client/echo';
11
- import { ClientDecorator, textGenerator, useDataGenerator } from '@dxos/react-client/testing';
11
+ import { ClientRepeater, textGenerator, useDataGenerator } from '@dxos/react-client/testing';
12
12
  import { useId } from '@dxos/react-ui';
13
13
  import { mx } from '@dxos/react-ui-theme';
14
+ import { withTheme } from '@dxos/storybook-utils';
14
15
 
15
16
  import { RichTextEditor, type RichTextEditorProps } from './RichText';
16
17
  import { Replicator, useYjsModel } from '../../testing';
17
18
 
18
19
  export default {
19
20
  component: RichTextEditor,
21
+ decorators: [withTheme],
20
22
  };
21
23
 
22
24
  export const Default = {
@@ -29,40 +31,42 @@ export const Default = {
29
31
  };
30
32
 
31
33
  const replicator = new Replicator(TextKind.RICH);
32
- export const WithYjs = {
33
- render: (args: Omit<RichTextEditorProps, 'model'>) => {
34
- const [generate, setGenerate] = useState(false);
35
- const generateId = useId('generate');
36
34
 
37
- const [id] = useState(PublicKey.random().toHex());
38
- const model = useYjsModel({ id, replicator });
35
+ const Story = (args: Omit<RichTextEditorProps, 'model'>) => {
36
+ const [generate, setGenerate] = useState(false);
37
+ const generateId = useId('generate');
39
38
 
40
- useDataGenerator({
41
- generator: generate ? textGenerator : undefined,
42
- options: { text: typeof model?.content !== 'string' ? model?.content : undefined },
43
- });
39
+ const [id] = useState(PublicKey.random().toHex());
40
+ const model = useYjsModel({ id, replicator });
44
41
 
45
- return (
46
- <main className='flex-1 min-w-0 p-4'>
47
- <div id={generateId} className='flex'>
48
- <input type='checkbox' onChange={(event) => setGenerate(event.target.checked)} />
49
- Generate Data
50
- </div>
51
- <RichTextEditor
52
- {...args}
53
- model={model}
54
- slots={{
55
- editor: {
56
- className: mx(
57
- 'z-0 rounded bg-white text-neutral-900 w-full p-4 dark:bg-neutral-850 dark:text-white min-bs-[12em]',
58
- args.slots?.editor?.className,
59
- ),
60
- },
61
- }}
62
- />
63
- </main>
64
- );
65
- },
42
+ useDataGenerator({
43
+ generator: generate ? textGenerator : undefined,
44
+ options: { text: typeof model?.content !== 'string' ? model?.content : undefined },
45
+ });
46
+
47
+ return (
48
+ <main className='flex-1 min-w-0 p-4'>
49
+ <div id={generateId} className='flex'>
50
+ <input type='checkbox' onChange={(event) => setGenerate(event.target.checked)} />
51
+ Generate Data
52
+ </div>
53
+ <RichTextEditor
54
+ {...args}
55
+ model={model}
56
+ slots={{
57
+ editor: {
58
+ className: mx(
59
+ 'z-0 rounded bg-white text-neutral-900 w-full p-4 dark:bg-neutral-850 dark:text-white min-bs-[12em]',
60
+ args.slots?.editor?.className,
61
+ ),
62
+ },
63
+ }}
64
+ />
65
+ </main>
66
+ );
67
+ };
68
+
69
+ export const WithYjs = {
66
70
  // TODO(wittjosiah): Decorator for doing this without clients being initialized?
67
- decorators: [ClientDecorator({ count: 2 })],
71
+ render: () => <ClientRepeater count={2} Component={Story} />,
68
72
  };
@@ -0,0 +1,72 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxosTheme';
6
+ import { markdown } from '@codemirror/lang-markdown';
7
+ import { ArrowCircleUp } from '@phosphor-icons/react';
8
+ import React, { StrictMode, useState } from 'react';
9
+ import { createRoot } from 'react-dom/client';
10
+
11
+ import { TextObject } from '@dxos/echo-schema';
12
+ import { fixedInsetFlexLayout, getSize, groupSurface, inputSurface, mx } from '@dxos/react-ui-theme';
13
+ import { withTheme } from '@dxos/storybook-utils';
14
+
15
+ import { TextEditor } from './TextEditor';
16
+ import { createHyperlinkTooltip, hyperlinkDecoration } from './extensions';
17
+ import { useTextModel } from '../../model';
18
+
19
+ const text = [
20
+ '',
21
+ '',
22
+ 'This is all about [DXOS](https://dxos.org); read more [here](https://docs.dxos.org/guide/getting-started.html).',
23
+ '',
24
+ '',
25
+ ].join('\n');
26
+
27
+ const hyperLinkTooltip = () =>
28
+ createHyperlinkTooltip((el, url) => {
29
+ const web = new URL(url);
30
+ createRoot(el).render(
31
+ <StrictMode>
32
+ <div className='flex gap-1 items-center'>
33
+ <ArrowCircleUp className={mx(getSize(6), 'text-blue-500')} />
34
+ <p className='pr-1'>{web.origin}</p>
35
+ </div>
36
+ </StrictMode>,
37
+ );
38
+ });
39
+
40
+ const Story = () => {
41
+ const [item] = useState({ text: new TextObject(text) });
42
+ const model = useTextModel({ text: item.text });
43
+
44
+ return (
45
+ <div className={mx(fixedInsetFlexLayout, groupSurface)}>
46
+ <div className='flex justify-center p-8'>
47
+ <div className='w-[800px]'>
48
+ <TextEditor
49
+ model={model}
50
+ extensions={[
51
+ markdown(),
52
+ hyperlinkDecoration({ link: false }),
53
+ hyperLinkTooltip(),
54
+ // EditorView.domEventHandlers({
55
+ // mousedown: (e, view) => {},
56
+ // }),
57
+ ]}
58
+ slots={{ root: { className: mx(inputSurface, 'p-2') } }}
59
+ />
60
+ </div>
61
+ </div>
62
+ </div>
63
+ );
64
+ };
65
+
66
+ export default {
67
+ component: TextEditor,
68
+ decorators: [withTheme],
69
+ render: Story,
70
+ };
71
+
72
+ export const Default = {};
@@ -0,0 +1,131 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { closeBrackets } from '@codemirror/autocomplete';
6
+ import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
7
+ import { EditorState, type Extension } from '@codemirror/state';
8
+ import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
9
+ import { EditorView, placeholder } from '@codemirror/view';
10
+ import React, {
11
+ type KeyboardEvent,
12
+ forwardRef,
13
+ useEffect,
14
+ useImperativeHandle,
15
+ useState,
16
+ useCallback,
17
+ type HTMLAttributes,
18
+ } from 'react';
19
+ import { type StyleSpec } from 'style-mod';
20
+ import { yCollab } from 'y-codemirror.next';
21
+
22
+ import { useThemeContext } from '@dxos/react-ui';
23
+ import { YText } from '@dxos/text-model';
24
+
25
+ import { defaultStyles } from './theme';
26
+ import { type EditorModel, type EditorSlots } from '../../model';
27
+
28
+ export type CursorInfo = {
29
+ from: number;
30
+ to: number;
31
+ line: number;
32
+ lines: number;
33
+ after?: string;
34
+ };
35
+
36
+ export type TextEditorRef = {
37
+ editor: HTMLDivElement | null;
38
+ state?: EditorState;
39
+ view?: EditorView;
40
+ };
41
+
42
+ export type TextEditorProps = {
43
+ model?: EditorModel;
44
+ extensions?: Extension[];
45
+ theme?: {
46
+ [selector: string]: StyleSpec;
47
+ };
48
+ slots?: EditorSlots;
49
+ onKeyDown?: (event: KeyboardEvent, info: CursorInfo) => void;
50
+ } & Pick<HTMLAttributes<HTMLDivElement>, 'onBlur' | 'onFocus'>;
51
+
52
+ /**
53
+ * Simple text editor.
54
+ */
55
+ export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
56
+ ({ model, extensions = [], theme = defaultStyles, slots = {}, onKeyDown, ...props }, forwardedRef) => {
57
+ const { id, content } = model ?? {};
58
+ const { themeMode } = useThemeContext();
59
+
60
+ const [parent, setParent] = useState<HTMLDivElement | null>(null);
61
+ const [state, setState] = useState<EditorState>();
62
+ const [view, setView] = useState<EditorView>();
63
+
64
+ // TODO(burdon): The ref may be instantiated before the view is created.
65
+ useImperativeHandle(
66
+ forwardedRef,
67
+ () => {
68
+ return {
69
+ editor: parent,
70
+ state,
71
+ view,
72
+ };
73
+ },
74
+ [view],
75
+ );
76
+
77
+ useEffect(() => {
78
+ if (!parent) {
79
+ return;
80
+ }
81
+
82
+ view?.destroy();
83
+
84
+ const state = EditorState.create({
85
+ doc: content?.toString(),
86
+ extensions: [
87
+ bracketMatching(),
88
+ closeBrackets(),
89
+ placeholder(slots.editor?.placeholder ?? ''),
90
+ EditorView.lineWrapping,
91
+
92
+ // Themes.
93
+ EditorView.theme(theme),
94
+ ...(themeMode === 'dark'
95
+ ? [syntaxHighlighting(oneDarkHighlightStyle)]
96
+ : [syntaxHighlighting(defaultHighlightStyle)]),
97
+
98
+ // Replication.
99
+ // TODO(burdon): Move to default extensions.
100
+ ...(content instanceof YText ? [yCollab(content, undefined)] : []),
101
+
102
+ // Custom.
103
+ ...extensions,
104
+ ],
105
+ });
106
+
107
+ setState(state);
108
+ setView(new EditorView({ state, parent }));
109
+
110
+ return () => {
111
+ view?.destroy();
112
+ setView(undefined);
113
+ setState(undefined);
114
+ };
115
+ }, [parent, content, themeMode]);
116
+
117
+ const handleKeyDown = useCallback(
118
+ (event: KeyboardEvent) => {
119
+ if (view) {
120
+ const { head, from, to } = view.state.selection.ranges[0];
121
+ const { number } = view.state.doc.lineAt(head);
122
+ const after = view.state.sliceDoc(from);
123
+ onKeyDown?.(event, { from, to, line: number, lines: view.state.doc.lines, after });
124
+ }
125
+ },
126
+ [view],
127
+ );
128
+
129
+ return <div key={id} ref={setParent} {...slots.root} onKeyDown={handleKeyDown} {...props} />;
130
+ },
131
+ );
@@ -0,0 +1,99 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ // Copyright CodeMirror
4
+ //
5
+
6
+ import { syntaxTree } from '@codemirror/language';
7
+ import { type EditorState, type RangeSet, RangeSetBuilder, StateField, type Transaction } from '@codemirror/state';
8
+ import { Decoration, EditorView, WidgetType } from '@codemirror/view';
9
+
10
+ // Adapted from:
11
+ // https://codemirror.net/try/?c=aW1wb3J0IHsgYmFzaWNTZXR1cCwgRWRpdG9yVmlldyB9IGZyb20gImNvZGVtaXJyb3IiCmltcG9ydCB7IG1hcmtkb3duIH0gZnJvbSAiQGNvZGVtaXJyb3IvbGFuZy1tYXJrZG93biIKaW1wb3J0IHsgc3ludGF4VHJlZSB9IGZyb20gIkBjb2RlbWlycm9yL2xhbmd1YWdlIjsKaW1wb3J0IHsgRWRpdG9yU3RhdGUsIFJhbmdlU2V0QnVpbGRlciwgU3RhdGVGaWVsZCB9IGZyb20gIkBjb2RlbWlycm9yL3N0YXRlIjsKaW1wb3J0IHsgRGVjb3JhdGlvbiwgV2lkZ2V0VHlwZSB9IGZyb20gIkBjb2RlbWlycm9yL3ZpZXciOwoKY2xhc3MgTGluayBleHRlbmRzIFdpZGdldFR5cGUgewogIGNvbnN0cnVjdG9yKHRleHQsIHVybCkgewogICAgc3VwZXIoKQogICAgdGhpcy50ZXh0ID0gdGV4dAogICAgdGhpcy51cmwgPSB1cmwKICB9CiAgZXEob3RoZXIpIHsgcmV0dXJuIHRoaXMudGV4dCA9PSBvdGhlci50ZXh0ICYmIHRoaXMudXJsID09IG90aGVyLnVybCB9CiAgdG9ET00oKSB7CiAgICBsZXQgbGluayA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImEiKQogICAgbGluay50ZXh0Q29udGVudCA9IHRoaXMudGV4dAogICAgbGluay5ocmVmID0gdGhpcy51cmwKICAgIHJldHVybiBsaW5rOwogIH0KfQoKbGV0IGRlY29yYXRpb25zRmllbGQgPSBTdGF0ZUZpZWxkLmRlZmluZSh7CiAgY3JlYXRlKCkgewogICAgcmV0dXJuIERlY29yYXRpb24ubm9uZTsKICB9LAogIHVwZGF0ZShfLCB0cikgewogICAgY29uc3QgYnVpbGRlciA9IG5ldyBSYW5nZVNldEJ1aWxkZXIoKTsKICAgIGxldCBjdXJzb3IgPSB0ci5zdGF0ZS5zZWxlY3Rpb24ubWFpbi5oZWFkOwogICAgc3ludGF4VHJlZSh0ci5zdGF0ZSkuaXRlcmF0ZSh7CiAgICAgIGVudGVyOiAobm9kZSkgPT4gewogICAgICAgIGlmICgoY3Vyc29yIDwgbm9kZS5mcm9tIHx8IGN1cnNvciA+IG5vZGUudG8pICYmIG5vZGUubmFtZSA9PSAiTGluayIpIHsKICAgICAgICAgIGxldCBtYXJrcyA9IG5vZGUubm9kZS5nZXRDaGlsZHJlbigiTGlua01hcmsiKTsKICAgICAgICAgIGxldCB0ZXh0ID0gbWFya3MubGVuZ3RoID49IDIgPyB0ci5zdGF0ZS5zbGljZURvYyhtYXJrc1swXS50bywgbWFya3NbMV0uZnJvbSkgOiAiIgoKICAgICAgICAgIGxldCB1cmxOb2RlID0gbm9kZS5ub2RlLmdldENoaWxkKCJVUkwiKTsKICAgICAgICAgIGxldCB1cmwgPSB1cmxOb2RlID8gdHIuc3RhdGUuc2xpY2VEb2ModXJsTm9kZS5mcm9tLCB1cmxOb2RlLnRvKSA6ICIiCiAgICAgICAgICBidWlsZGVyLmFkZChub2RlLmZyb20sIG5vZGUudG8sIERlY29yYXRpb24ucmVwbGFjZSh7CiAgICAgICAgICAgIHdpZGdldDogbmV3IExpbmsodGV4dCwgdXJsKSwKICAgICAgICAgIH0pLAogICAgICAgICAgICAgICAgICAgICApOwogICAgICAgICAgcmV0dXJuIGZhbHNlOwogICAgICAgIH0KICAgICAgICByZXR1cm4gdHJ1ZTsKICAgICAgfSwKICAgIH0pOwogICAgcmV0dXJuIGJ1aWxkZXIuZmluaXNoKCk7CiAgfSwKICBwcm92aWRlOiAoZikgPT4gRWRpdG9yVmlldy5kZWNvcmF0aW9ucy5mcm9tKGYpLAp9KTsKCmxldCB2aWV3ID0gbmV3IEVkaXRvclZpZXcoewogIGRvYzogIiMgSGVsbG8gV29ybGRcblxuW1Rlc3RdKGh0dHBzOi8vZXhhbXBsZS5jb20pXG5cblxuIiwKICBleHRlbnNpb25zOiBbZGVjb3JhdGlvbnNGaWVsZCwgbWFya2Rvd24oKV0sCiAgcGFyZW50OiBkb2N1bWVudC5ib2R5LAp9KTsKCnZpZXcuZm9jdXMoKTs=
12
+ // https://discuss.codemirror.net/t/link-widget-is-clicked-only-if-editor-is-out-of-focus/7433
13
+
14
+ // TODO(burdon): Adapt to make all markdown editable?
15
+
16
+ // TODO(burdon): Research overlays.
17
+ // https://stackoverflow.com/questions/26933344/codemirror-detect-and-create-links-inside-editor
18
+ // https://codemirror.net/5/doc/manual.html#addOverlay
19
+ // https://codemirror.net/5/demo/simplemode.html
20
+ // https://codemirror.net/5/demo/search.html (Search dialog).
21
+
22
+ class Link extends WidgetType {
23
+ constructor(private readonly _anchor: number, private readonly _text: string, private readonly _url?: string) {
24
+ super();
25
+ }
26
+
27
+ override eq(other: any) {
28
+ return this._text === other.text && this._url === other.url;
29
+ }
30
+
31
+ toDOM(view: EditorView) {
32
+ const link = document.createElement('a');
33
+ link.setAttribute('class', 'cm-link');
34
+ link.textContent = this._text;
35
+ if (this._url) {
36
+ link.setAttribute('target', '_blank');
37
+ link.href = this._url;
38
+ } else {
39
+ link.onclick = () => {
40
+ view.dispatch({
41
+ selection: {
42
+ anchor: this._anchor,
43
+ },
44
+ });
45
+ };
46
+ }
47
+
48
+ return link;
49
+ }
50
+ }
51
+
52
+ type HyperlinkDecorationConfig = {
53
+ link?: boolean;
54
+ };
55
+
56
+ /**
57
+ * Creates a state field to replace AST elements with a hyperlink widget.
58
+ * https://codemirror.net/docs/ref/#state.StateField
59
+ */
60
+ export const hyperlinkDecoration = ({ link }: HyperlinkDecorationConfig = {}) => {
61
+ return StateField.define<RangeSet<any>>({
62
+ create: (state) => update(state, link),
63
+ update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, link),
64
+ provide: (field) => EditorView.decorations.from(field),
65
+ });
66
+ };
67
+
68
+ const update = (state: EditorState, link?: boolean) => {
69
+ const builder = new RangeSetBuilder();
70
+ const cursor = state.selection.main.head;
71
+ syntaxTree(state).iterate({
72
+ enter: (node) => {
73
+ // Check if cursor is inside text.
74
+ if (cursor < node.from || cursor > node.to) {
75
+ if (node.name === 'Link') {
76
+ const marks = node.node.getChildren('LinkMark');
77
+ const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
78
+
79
+ const urlNode = node.node.getChild('URL');
80
+ const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
81
+
82
+ builder.add(
83
+ node.from,
84
+ node.to,
85
+ Decoration.replace({
86
+ widget: new Link(node.from, text, link ? url : undefined),
87
+ }),
88
+ );
89
+
90
+ return false;
91
+ }
92
+ }
93
+
94
+ return true;
95
+ },
96
+ });
97
+
98
+ return builder.finish();
99
+ };
@@ -0,0 +1,61 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { hoverTooltip } from '@codemirror/view';
6
+
7
+ import { tooltipContent } from '@dxos/react-ui-theme';
8
+
9
+ const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)/;
10
+
11
+ export type OnHyperlinkHover = (el: Element, url: string) => void;
12
+
13
+ /**
14
+ * https://codemirror.net/examples/tooltip
15
+ * https://codemirror.net/docs/ref/#view.hoverTooltip
16
+ * https://github.com/codemirror/view/blob/main/src/tooltip.ts
17
+ */
18
+ // TODO(burdon): Create config object.
19
+ export const createHyperlinkTooltip = (onHover: OnHyperlinkHover, regexp = markdownLinkRegexp) =>
20
+ hoverTooltip((view, pos) => {
21
+ const { from, text } = view.state.doc.lineAt(pos);
22
+ const p = pos - from;
23
+
24
+ let idx = 0;
25
+ let match;
26
+ do {
27
+ match = text.substring(idx).match(regexp);
28
+ if (!match) {
29
+ match = undefined;
30
+ break;
31
+ }
32
+
33
+ idx = text.indexOf(match[0]);
34
+ if (p >= idx && p < idx + match[0].length) {
35
+ break;
36
+ }
37
+
38
+ idx += match[0].length;
39
+ } while (true);
40
+
41
+ if (!match) {
42
+ return null;
43
+ }
44
+
45
+ const [, , url] = match ?? [];
46
+
47
+ return {
48
+ pos: idx + from,
49
+ end: idx + from + match[0].length,
50
+ above: true,
51
+ create: () => {
52
+ const el = document.createElement('a');
53
+ el.innerText = '_'; // Required so doesn't collapse.
54
+ el.className = tooltipContent({}, 'mb-2 p-1');
55
+ el.setAttribute('target', '_blank');
56
+ el.setAttribute('href', url);
57
+ onHover(el, url);
58
+ return { dom: el };
59
+ },
60
+ };
61
+ });
@@ -0,0 +1,119 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ // Adapted from: https://github.com/uiwjs/react-codemirror/blob/master/extensions/hyper-link/src/index.ts
4
+ //
5
+
6
+ import { type Extension } from '@codemirror/state';
7
+ import {
8
+ Decoration,
9
+ type DecorationSet,
10
+ EditorView,
11
+ MatchDecorator,
12
+ ViewPlugin,
13
+ type ViewUpdate,
14
+ } from '@codemirror/view';
15
+
16
+ // Notes: Can't edit widget content (or cursor nav through them).
17
+ // - https://discuss.codemirror.net/t/focusing-inputs-within-widgets/5178/7
18
+ // Widgets intentionally always get set to contenteditable=false,
19
+ // or they would become part of CodeMirror’s editable content element.
20
+ // You should be able to introduce new contenteditable=true child elements inside of them.
21
+
22
+ const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)(!?)/gi;
23
+
24
+ /**
25
+ * Custom link decorator.
26
+ * NOTE: The default markdown extension formats links by default.
27
+ * @deprecated
28
+ */
29
+ const linkDecorator = () =>
30
+ new MatchDecorator({
31
+ regexp: markdownLinkRegexp,
32
+ decorate: (add, from, to, match, view) => {
33
+ const [_, label, url] = match;
34
+ const p0 = {
35
+ start: from,
36
+ end: from + 1,
37
+ };
38
+
39
+ const p1 = {
40
+ start: from + 1,
41
+ end: from + 1 + label.length,
42
+ };
43
+
44
+ const p2 = {
45
+ start: p1.end,
46
+ end: p1.end + 1,
47
+ };
48
+
49
+ const p3 = {
50
+ start: p1.end + 1,
51
+ end: p1.end + 1 + url.length + 2,
52
+ };
53
+
54
+ add(
55
+ p0.start,
56
+ p0.end,
57
+ Decoration.mark({ class: 'cm-hyperlink-bracket', _attributes: { style: 'display: none' } }),
58
+ );
59
+ add(p1.start, p1.end, Decoration.mark({ class: 'cm-hyperlink-label' }));
60
+ add(
61
+ p2.start,
62
+ p2.end,
63
+ Decoration.mark({ class: 'cm-hyperlink-bracket', _attributes: { style: 'display: none' } }),
64
+ );
65
+ add(p3.start, p3.end, Decoration.mark({ class: 'cm-hyperlink-url', _attributes: { style: 'display: none' } }));
66
+ },
67
+ });
68
+
69
+ export function hyperLinkExtension() {
70
+ return ViewPlugin.fromClass(
71
+ class HyperLinkView {
72
+ readonly decorator: MatchDecorator;
73
+ decorations: DecorationSet;
74
+ constructor(view: EditorView) {
75
+ this.decorator = linkDecorator();
76
+ this.decorations = this.decorator.createDeco(view);
77
+ }
78
+
79
+ update(update: ViewUpdate) {
80
+ if (update.docChanged || update.viewportChanged) {
81
+ this.decorations = this.decorator.updateDeco(update, this.decorations);
82
+ }
83
+ }
84
+ },
85
+ {
86
+ decorations: (view) => view.decorations,
87
+ eventHandlers: {
88
+ // TODO(burdon): CLick to expand link.
89
+ // https://discuss.codemirror.net/t/decorator-iteration-on-click-event/4226/2
90
+ mousedown: function (event, view) {
91
+ const pos = view.posAtDOM(event.target as Node);
92
+ if (pos !== 0) {
93
+ console.log(this);
94
+ }
95
+ },
96
+ // TODO(burdon): Hide when cursor moves away.
97
+ keydown: (event, view) => {
98
+ view.update([]);
99
+ },
100
+ },
101
+ },
102
+ );
103
+ }
104
+
105
+ export const hyperlinkStyle = EditorView.baseTheme({
106
+ '.cm-hyperlink-label': {
107
+ cursor: 'pointer',
108
+ textDecoration: 'underline',
109
+ },
110
+ '.cm-hyperlink-bracket': {
111
+ opacity: 0.2,
112
+ },
113
+ '.cm-hyperlink-url': {
114
+ opacity: 0.5,
115
+ fontFamily: 'monospace',
116
+ },
117
+ });
118
+
119
+ export const hyperlinkWidget: Extension = [hyperLinkExtension(), hyperlinkStyle];
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './hyperlinkDecoration';
6
+ export * from './hyperlinkTooltip';
7
+ export * from './hyperlinkWidget';
@@ -0,0 +1,11 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export { type StyleSpec } from 'style-mod';
6
+ export { tags } from '@lezer/highlight';
7
+
8
+ export * from './TextEditor';
9
+
10
+ export * from './extensions';
11
+ export * from './theme';
@@ -0,0 +1,35 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import get from 'lodash.get';
6
+ import { type StyleSpec } from 'style-mod';
7
+
8
+ import { tailwindConfig, type TailwindConfig } from '@dxos/react-ui-theme';
9
+
10
+ const tokens: TailwindConfig['theme'] = tailwindConfig({}).theme;
11
+
12
+ /**
13
+ * Minimal styles.
14
+ * https://codemirror.net/examples/styling
15
+ */
16
+ // TODO(burdon): If given a "theme" suffix, `__docgen` properties are added to the object.
17
+ export const defaultStyles: {
18
+ [selector: string]: StyleSpec;
19
+ } = {
20
+ '&.cm-focused': {
21
+ outline: 'none',
22
+ },
23
+ '.cm-scroller': {
24
+ fontFamily: get(tokens, 'fontFamily.body', []).join(','),
25
+ overflow: 'visible',
26
+ },
27
+ '.cm-tooltip': {
28
+ backgroundColor: 'transparent',
29
+ border: 'none',
30
+ },
31
+ '.cm-link': {
32
+ color: get(tokens, 'extend.colors.primary.500'),
33
+ textDecoration: 'underline',
34
+ },
35
+ };
@@ -5,3 +5,4 @@
5
5
  export * from './Editor';
6
6
  export * from './Markdown';
7
7
  export * from './RichText';
8
+ export * from './TextEditor';
@@ -186,6 +186,5 @@ export type UseYjsModelOptions = {
186
186
 
187
187
  export const useYjsModel = ({ replicator, id, doc }: UseYjsModelOptions): EditorModel => {
188
188
  const peer = useMemo(() => replicator.createPeer(id, doc), [doc]);
189
-
190
189
  return peer;
191
190
  };
@@ -1,25 +0,0 @@
1
- import { Event } from '@dxos/async';
2
- import { type Doc, next as automerge, type ChangeOptions, type ChangeFn, type Heads } from '@dxos/automerge/automerge';
3
- export declare class Peer {
4
- changeEvent: Event<void>;
5
- storage: Map<string, Uint8Array>;
6
- stats: {
7
- messagesSent: number;
8
- messagesReceived: number;
9
- bytesSent: number;
10
- bytesReceived: number;
11
- snapshotBytes: number;
12
- incrementalBytes: number;
13
- };
14
- doc: Doc<{
15
- text: string;
16
- }>;
17
- change(options: string | ChangeOptions<any> | ChangeFn<any>, callback?: ChangeFn<any>): void;
18
- changeAt(scope: Heads, options: string | ChangeOptions<any> | ChangeFn<any>, callback?: ChangeFn<any>): automerge.Heads | null;
19
- replicate(): {
20
- readable: ReadableStream<Uint8Array>;
21
- writable: WritableStream<Uint8Array>;
22
- };
23
- private _handleChange;
24
- }
25
- //# sourceMappingURL=demo.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"demo.d.ts","sourceRoot":"","sources":["../../../../src/automerge/demo.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EACL,KAAK,GAAG,EACR,IAAI,IAAI,SAAS,EACjB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,KAAK,EAEX,MAAM,2BAA2B,CAAC;AAEnC,qBAAa,IAAI;IACf,WAAW,cAAe;IAE1B,OAAO,0BAAiC;IACxC,KAAK;;;;;;;MAOH;IAEF,GAAG,EAAG,GAAG,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAE5B,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC;IAKrF,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC;IAOrG,SAAS,IAAI;QAAE,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;QAAC,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,CAAA;KAAE;IAgD3F,OAAO,CAAC,aAAa;CAgBtB"}