@dxos/react-ui-editor 0.3.10-main.ee543cf → 0.3.10-next.3e4f36f

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 (121) hide show
  1. package/dist/lib/browser/index.mjs +735 -731
  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.map +1 -0
  5. package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts.map +1 -0
  6. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +1 -0
  7. package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +1 -0
  8. package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +1 -0
  9. package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +1 -0
  10. package/dist/types/src/automerge/automerge.stories.d.ts.map +1 -0
  11. package/dist/types/src/components/TextEditor/TextEditor.d.ts +2 -4
  12. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  13. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +4 -9
  14. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/TextEditor/extensions/{basic.d.ts → basic/bundle.d.ts} +1 -1
  16. package/dist/types/src/components/TextEditor/extensions/basic/bundle.d.ts.map +1 -0
  17. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts +3 -0
  18. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts.map +1 -0
  19. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts +10 -0
  20. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts.map +1 -0
  21. package/dist/types/src/components/TextEditor/extensions/change.d.ts +7 -0
  22. package/dist/types/src/components/TextEditor/extensions/change.d.ts.map +1 -0
  23. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts +11 -0
  24. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts.map +1 -0
  25. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts +8 -0
  26. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts.map +1 -0
  27. package/dist/types/src/components/TextEditor/extensions/{experimental.d.ts → hyperlink/hyperlinkWidget.d.ts} +2 -2
  28. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.d.ts.map +1 -0
  29. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts +4 -0
  30. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts.map +1 -0
  31. package/dist/types/src/components/TextEditor/extensions/index.d.ts +2 -5
  32. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
  33. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
  34. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts +1 -1
  35. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +1 -1
  36. package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts +17 -0
  37. package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts.map +1 -0
  38. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts +21 -0
  39. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts.map +1 -0
  40. package/dist/types/src/components/TextEditor/index.d.ts +0 -2
  41. package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
  42. package/dist/types/src/components/TextEditor/yjs.stories.d.ts +0 -1
  43. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +1 -1
  44. package/dist/types/src/hooks/index.d.ts +1 -0
  45. package/dist/types/src/hooks/index.d.ts.map +1 -1
  46. package/dist/types/src/hooks/useCollaboration.d.ts +9 -0
  47. package/dist/types/src/hooks/useCollaboration.d.ts.map +1 -0
  48. package/dist/types/src/hooks/useTextModel.d.ts +4 -4
  49. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  50. package/dist/types/src/styles/markdown.d.ts +10 -12
  51. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  52. package/package.json +32 -20
  53. package/src/{hooks/automerge → automerge}/automerge.stories.tsx +1 -2
  54. package/src/components/TextEditor/TextEditor.stories.tsx +43 -46
  55. package/src/components/TextEditor/TextEditor.tsx +45 -59
  56. package/src/components/TextEditor/extensions/{basic.ts → basic/bundle.ts} +2 -4
  57. package/src/components/TextEditor/extensions/basic/index.ts +6 -0
  58. package/src/components/TextEditor/extensions/basic/theme.ts +49 -0
  59. package/src/components/TextEditor/extensions/{listener.ts → change.ts} +3 -3
  60. package/src/components/TextEditor/extensions/{link.ts → hyperlink/hyperlinkDecoration.ts} +31 -68
  61. package/src/components/TextEditor/extensions/{tooltip.tsx → hyperlink/hyperlinkTooltip.tsx} +9 -9
  62. package/src/components/TextEditor/extensions/{experimental.tsx → hyperlink/hyperlinkWidget.tsx} +2 -17
  63. package/src/components/TextEditor/extensions/hyperlink/index.ts +7 -0
  64. package/src/components/TextEditor/extensions/index.ts +2 -5
  65. package/src/components/TextEditor/extensions/markdown/bundle.ts +11 -15
  66. package/src/components/TextEditor/extensions/markdown/index.ts +1 -1
  67. package/src/components/TextEditor/extensions/markdown/tags.ts +38 -0
  68. package/src/components/TextEditor/extensions/markdown/theme.ts +265 -0
  69. package/src/components/TextEditor/index.ts +0 -3
  70. package/src/components/TextEditor/yjs.stories.tsx +0 -1
  71. package/src/hooks/index.ts +1 -0
  72. package/src/hooks/useCollaboration.ts +45 -0
  73. package/src/hooks/useTextModel.ts +40 -50
  74. package/src/styles/markdown.ts +24 -26
  75. package/src/testing/replicator.ts +2 -2
  76. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +0 -35
  77. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +0 -1
  78. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts +0 -9
  79. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +0 -1
  80. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +0 -1
  81. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +0 -1
  82. package/dist/types/src/components/TextEditor/extensions/link.d.ts +0 -11
  83. package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +0 -1
  84. package/dist/types/src/components/TextEditor/extensions/listener.d.ts +0 -7
  85. package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +0 -1
  86. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +0 -31
  87. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +0 -1
  88. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +0 -5
  89. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +0 -1
  90. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts +0 -12
  91. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +0 -1
  92. package/dist/types/src/components/TextEditor/themes/default.d.ts +0 -17
  93. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +0 -1
  94. package/dist/types/src/components/TextEditor/themes/index.d.ts +0 -2
  95. package/dist/types/src/components/TextEditor/themes/index.d.ts.map +0 -1
  96. package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +0 -1
  97. package/dist/types/src/hooks/automerge/amToCodemirror.d.ts.map +0 -1
  98. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +0 -1
  99. package/dist/types/src/hooks/automerge/codeMirrorToAm.d.ts.map +0 -1
  100. package/dist/types/src/hooks/automerge/handle.d.ts.map +0 -1
  101. package/dist/types/src/hooks/automerge/index.d.ts.map +0 -1
  102. package/dist/types/src/hooks/automerge/plugin.d.ts.map +0 -1
  103. package/src/components/TextEditor/MarkdownEditor.stories.tsx +0 -242
  104. package/src/components/TextEditor/extensions/autocomplete.ts +0 -72
  105. package/src/components/TextEditor/extensions/markdown/highlight.ts +0 -166
  106. package/src/components/TextEditor/extensions/tasklist.ts +0 -113
  107. package/src/components/TextEditor/themes/default.ts +0 -170
  108. package/src/components/TextEditor/themes/index.ts +0 -5
  109. /package/dist/types/src/{hooks/automerge → automerge/automerge-plugin}/PatchSemaphore.d.ts +0 -0
  110. /package/dist/types/src/{hooks/automerge → automerge/automerge-plugin}/amToCodemirror.d.ts +0 -0
  111. /package/dist/types/src/{hooks/automerge → automerge/automerge-plugin}/codeMirrorToAm.d.ts +0 -0
  112. /package/dist/types/src/{hooks/automerge → automerge/automerge-plugin}/handle.d.ts +0 -0
  113. /package/dist/types/src/{hooks/automerge → automerge/automerge-plugin}/index.d.ts +0 -0
  114. /package/dist/types/src/{hooks/automerge → automerge/automerge-plugin}/plugin.d.ts +0 -0
  115. /package/dist/types/src/{hooks/automerge → automerge}/automerge.stories.d.ts +0 -0
  116. /package/src/{hooks/automerge → automerge/automerge-plugin}/PatchSemaphore.ts +0 -0
  117. /package/src/{hooks/automerge → automerge/automerge-plugin}/amToCodemirror.ts +0 -0
  118. /package/src/{hooks/automerge → automerge/automerge-plugin}/codeMirrorToAm.ts +0 -0
  119. /package/src/{hooks/automerge → automerge/automerge-plugin}/handle.ts +0 -0
  120. /package/src/{hooks/automerge → automerge/automerge-plugin}/index.ts +0 -0
  121. /package/src/{hooks/automerge → automerge/automerge-plugin}/plugin.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-editor",
3
- "version": "0.3.10-main.ee543cf",
3
+ "version": "0.3.10-next.3e4f36f",
4
4
  "description": "Document editing experience within a DXOS shell.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -28,30 +28,42 @@
28
28
  "@lezer/highlight": "^1.2.0",
29
29
  "@lezer/markdown": "^1.1.1",
30
30
  "@replit/codemirror-vim": "^6.0.14",
31
+ "@tiptap/core": "2.0.0-beta.220",
32
+ "@tiptap/extension-collaboration": "2.0.0-beta.220",
33
+ "@tiptap/extension-collaboration-cursor": "2.0.0-beta.220",
34
+ "@tiptap/extension-heading": "2.0.0-beta.220",
35
+ "@tiptap/extension-list-item": "2.0.0-beta.220",
36
+ "@tiptap/extension-placeholder": "2.0.0-beta.220",
37
+ "@tiptap/pm": "2.0.0-beta.220",
38
+ "@tiptap/react": "2.0.0-beta.220",
39
+ "@tiptap/starter-kit": "2.0.0-beta.220",
31
40
  "codemirror": "6.0.1",
32
41
  "lib0": "^0.2.65",
33
42
  "lodash.defaultsdeep": "^4.6.1",
34
43
  "lodash.get": "^4.4.2",
44
+ "prosemirror-model": "^1.19.0",
45
+ "prosemirror-state": "^1.4.2",
46
+ "prosemirror-view": "^1.30.1",
35
47
  "style-mod": "^4.1.0",
36
48
  "y-codemirror.next": "^0.3.2",
49
+ "y-prosemirror": "~1.0.20",
37
50
  "y-protocols": "^1.0.5",
38
51
  "yjs": "^13.6.10",
39
- "@dxos/debug": "0.3.10-main.ee543cf",
40
- "@dxos/automerge": "0.3.10-main.ee543cf",
41
- "@dxos/display-name": "0.3.10-main.ee543cf",
42
- "@dxos/echo-schema": "0.3.10-main.ee543cf",
43
- "@dxos/invariant": "0.3.10-main.ee543cf",
44
- "@dxos/log": "0.3.10-main.ee543cf",
45
- "@dxos/protocols": "0.3.10-main.ee543cf",
46
- "@dxos/react-ui": "0.3.10-main.ee543cf",
47
- "@dxos/react-ui-theme": "0.3.10-main.ee543cf",
48
- "@dxos/util": "0.3.10-main.ee543cf",
49
- "@dxos/react-async": "0.3.10-main.ee543cf",
50
- "@dxos/text-model": "0.3.10-main.ee543cf"
52
+ "@dxos/automerge": "0.3.10-next.3e4f36f",
53
+ "@dxos/debug": "0.3.10-next.3e4f36f",
54
+ "@dxos/display-name": "0.3.10-next.3e4f36f",
55
+ "@dxos/echo-schema": "0.3.10-next.3e4f36f",
56
+ "@dxos/invariant": "0.3.10-next.3e4f36f",
57
+ "@dxos/protocols": "0.3.10-next.3e4f36f",
58
+ "@dxos/log": "0.3.10-next.3e4f36f",
59
+ "@dxos/react-async": "0.3.10-next.3e4f36f",
60
+ "@dxos/react-ui-theme": "0.3.10-next.3e4f36f",
61
+ "@dxos/util": "0.3.10-next.3e4f36f",
62
+ "@dxos/text-model": "0.3.10-next.3e4f36f",
63
+ "@dxos/react-ui": "0.3.10-next.3e4f36f"
51
64
  },
52
65
  "devDependencies": {
53
66
  "@automerge/automerge-repo-network-broadcastchannel": "^1.0.19",
54
- "@faker-js/faker": "^8.0.2",
55
67
  "@phosphor-icons/react": "^2.0.5",
56
68
  "@preact/signals-react": "^1.3.6",
57
69
  "@types/lodash.defaultsdeep": "^4.6.6",
@@ -60,17 +72,17 @@
60
72
  "@types/react-dom": "^18.0.6",
61
73
  "react": "^18.2.0",
62
74
  "react-dom": "^18.2.0",
63
- "@dxos/echo-typegen": "0.3.10-main.ee543cf",
64
- "@dxos/config": "0.3.10-main.ee543cf",
65
- "@dxos/react-client": "0.3.10-main.ee543cf",
66
- "@dxos/react-ui": "0.3.10-main.ee543cf",
67
- "@dxos/storybook-utils": "0.3.10-main.ee543cf"
75
+ "@dxos/echo-typegen": "0.3.10-next.3e4f36f",
76
+ "@dxos/config": "0.3.10-next.3e4f36f",
77
+ "@dxos/react-ui": "0.3.10-next.3e4f36f",
78
+ "@dxos/storybook-utils": "0.3.10-next.3e4f36f",
79
+ "@dxos/react-client": "0.3.10-next.3e4f36f"
68
80
  },
69
81
  "peerDependencies": {
70
82
  "@phosphor-icons/react": "^2.0.5",
71
83
  "react": "^18.2.0",
72
84
  "react-dom": "^18.2.0",
73
- "@dxos/react-client": "0.3.10-main.ee543cf"
85
+ "@dxos/react-client": "0.3.10-next.3e4f36f"
74
86
  },
75
87
  "publishConfig": {
76
88
  "access": "public"
@@ -12,8 +12,7 @@ import React, { useEffect, useRef, useState } from 'react';
12
12
  import { type Prop } from '@dxos/automerge/automerge';
13
13
  import { type DocHandle, Repo } from '@dxos/automerge/automerge-repo';
14
14
 
15
- import { type IDocHandle } from './handle';
16
- import { automergePlugin } from './plugin';
15
+ import { type IDocHandle, automergePlugin } from './automerge-plugin';
17
16
 
18
17
  type EditorProps = {
19
18
  handle: IDocHandle;
@@ -3,46 +3,63 @@
3
3
  //
4
4
 
5
5
  import '@dxosTheme';
6
-
7
- import defaultsDeep from 'lodash.defaultsdeep';
8
- import React, { useRef, useState } from 'react';
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';
9
10
 
10
11
  import { TextObject } from '@dxos/echo-schema';
11
- import { fixedInsetFlexLayout, groupSurface, mx } from '@dxos/react-ui-theme';
12
+ import { fixedInsetFlexLayout, getSize, groupSurface, inputSurface, mx } from '@dxos/react-ui-theme';
12
13
  import { withTheme } from '@dxos/storybook-utils';
13
14
 
14
- import { defaultSlots, TextEditor, type TextEditorProps, type TextEditorRef, type TextEditorSlots } from './TextEditor';
15
- import { textTheme } from './themes';
15
+ import { TextEditor } from './TextEditor';
16
+ import { createHyperlinkTooltip, hyperlinkDecoration } from './extensions';
16
17
  import { useTextModel } from '../../hooks';
17
18
 
18
- const initialText = [
19
- '# TextEditor',
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).',
20
23
  '',
21
- 'This is the basic plain text editor within minimal formatting.',
22
- 'You can add custom styles and create custom extensions.',
23
- 'Or use the MarkdownEditor to edit documents.',
24
24
  '',
25
- 'https://dxos.org',
26
25
  ].join('\n');
27
26
 
28
- const Story = ({
29
- text,
30
- automerge,
31
- ...props
32
- }: { text?: string; automerge?: boolean } & Pick<TextEditorProps, 'extensions' | 'slots'>) => {
33
- const ref = useRef<TextEditorRef>(null);
34
- const [item] = useState({ text: new TextObject(text, undefined, undefined, { useAutomergeBackend: automerge }) });
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 = ({ automerge }: { automerge?: boolean }) => {
41
+ const [item] = useState({ text: new TextObject(text, undefined, undefined, { useAutomergeBackend: !!automerge }) });
35
42
  const model = useTextModel({ text: item.text });
36
43
  if (!model) {
37
- return null;
44
+ return <></>;
38
45
  }
39
46
 
40
47
  return (
41
48
  <div className={mx(fixedInsetFlexLayout, groupSurface)}>
42
- <div className='flex justify-center overflow-y-scroll'>
43
- <div className='flex flex-col w-[800px] py-16'>
44
- <TextEditor ref={ref} model={model} {...props} />
45
- <div className='flex shrink-0 h-[300px]'></div>
49
+ <div className='flex justify-center p-8'>
50
+ <div className='w-[800px]'>
51
+ <TextEditor
52
+ model={model}
53
+ extensions={[
54
+ markdown(),
55
+ hyperlinkDecoration({ link: false }),
56
+ hyperLinkTooltip(),
57
+ // EditorView.domEventHandlers({
58
+ // mousedown: (e, view) => {},
59
+ // }),
60
+ ]}
61
+ slots={{ root: { className: mx(inputSurface, 'p-2') } }}
62
+ />
46
63
  </div>
47
64
  </div>
48
65
  </div>
@@ -50,33 +67,13 @@ const Story = ({
50
67
  };
51
68
 
52
69
  export default {
53
- title: 'react-ui-editor/TextEditor',
54
70
  component: TextEditor,
55
71
  decorators: [withTheme],
56
72
  render: Story,
57
73
  };
58
74
 
59
- export const Default = {
60
- render: () => (
61
- <Story
62
- text={initialText}
63
- slots={defaultsDeep(
64
- { editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
65
- defaultSlots,
66
- )}
67
- />
68
- ),
69
- };
75
+ export const Default = {};
70
76
 
71
77
  export const Automerge = {
72
- render: () => (
73
- <Story
74
- text={initialText}
75
- slots={defaultsDeep(
76
- { editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
77
- defaultSlots,
78
- )}
79
- automerge
80
- />
81
- ),
78
+ render: () => <Story automerge />,
82
79
  };
@@ -7,24 +7,23 @@ import { EditorView } from '@codemirror/view';
7
7
  import { useFocusableGroup } from '@fluentui/react-tabster';
8
8
  import { vim } from '@replit/codemirror-vim';
9
9
  import defaultsDeep from 'lodash.defaultsdeep';
10
+ import get from 'lodash.get';
10
11
  import React, {
11
- type ComponentProps,
12
12
  type KeyboardEvent,
13
13
  forwardRef,
14
14
  useCallback,
15
15
  useEffect,
16
16
  useImperativeHandle,
17
17
  useState,
18
+ type ComponentProps,
18
19
  } from 'react';
19
20
 
20
- import { generateName } from '@dxos/display-name';
21
+ import { isDocAccessor } from '@dxos/echo-schema';
21
22
  import { useThemeContext } from '@dxos/react-ui';
22
- import { getColorForValue, inputSurface, mx } from '@dxos/react-ui-theme';
23
23
 
24
- import { basicBundle, markdownBundle } from './extensions';
25
- import { defaultTheme, textTheme } from './themes';
26
- import { type EditorModel } from '../../hooks';
27
- import { type ThemeStyles } from '../../styles';
24
+ import { basicBundle, basicTheme, markdownBundle, markdownTheme } from './extensions';
25
+ import { type EditorModel, useCollaboration } from '../../hooks';
26
+ import type { ThemeStyles } from '../../styles';
28
27
 
29
28
  export const EditorModes = ['default', 'vim'] as const;
30
29
  export type EditorMode = (typeof EditorModes)[number];
@@ -38,7 +37,7 @@ export type CursorInfo = {
38
37
  };
39
38
 
40
39
  export type TextEditorRef = {
41
- root: HTMLDivElement | null;
40
+ editor: HTMLDivElement | null;
42
41
  state?: EditorState;
43
42
  view?: EditorView;
44
43
  };
@@ -66,65 +65,66 @@ export type TextEditorProps = {
66
65
  * NOTE: Rather than adding properties, try to create extensions that can be reused.
67
66
  */
68
67
  export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
69
- ({ model, extensions = [], slots = defaultSlots, editorMode }, forwardedRef) => {
68
+ ({ model, extensions = [], slots, editorMode }, forwardedRef) => {
70
69
  const { themeMode } = useThemeContext();
71
70
  const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
72
71
 
73
- // TODO(burdon): Factor out?
74
- const { awareness, peer } = model;
75
- useEffect(() => {
76
- if (awareness && peer) {
77
- awareness.setLocalStateField('user', {
78
- name: peer.name ?? generateName(peer.id),
79
- color: getColorForValue({ value: peer.id, type: 'color' }),
80
- colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
81
- });
82
- }
83
- }, [awareness, peer, themeMode]);
84
-
85
- const [root, setRoot] = useState<HTMLDivElement | null>(null);
72
+ const [parent, setParent] = useState<HTMLDivElement | null>(null);
86
73
  const [state, setState] = useState<EditorState>();
87
74
  const [view, setView] = useState<EditorView>();
88
- useImperativeHandle(forwardedRef, () => ({ root, state, view }), [view, state, root]);
75
+ useImperativeHandle(
76
+ forwardedRef,
77
+ () => ({
78
+ editor: parent,
79
+ state,
80
+ view,
81
+ }),
82
+ [view, state, parent],
83
+ );
84
+
85
+ // TODO(burdon): Pass in extension?
86
+ const collaboration = useCollaboration(model, themeMode);
89
87
 
90
88
  useEffect(() => {
91
- if (!root) {
89
+ if (!parent) {
92
90
  return;
93
91
  }
94
92
 
93
+ const initialText = isDocAccessor(model.content)
94
+ ? get(model.content.handle.docSync(), model.content.path)
95
+ : model.content?.toString();
96
+
95
97
  const state = EditorState.create({
96
- doc: model.text(),
98
+ doc: initialText,
97
99
  extensions: [
98
100
  // TODO(burdon): Factor out VIM mode?
99
101
  editorMode === 'vim' && vim(),
100
102
 
101
- // Theme.
102
- EditorView.baseTheme(defaultTheme),
103
- EditorView.theme(slots?.editor?.theme ?? {}),
104
- // TODO(burdon): themeMode doesn't change in storybooks.
105
- EditorView.darkTheme.of(themeMode === 'dark'),
103
+ // Replication.
104
+ collaboration,
106
105
 
107
- // Storage and replication.
108
- model.extension,
106
+ // Theme.
107
+ slots?.editor?.theme && EditorView.theme(slots?.editor?.theme),
109
108
 
110
109
  // Custom.
111
110
  ...extensions,
112
111
  ].filter(Boolean) as Extension[],
113
112
  });
114
113
 
114
+ setState(state);
115
+
115
116
  // NOTE: This repaints the editor.
116
117
  // If the new state is derived from the old state, it will likely not be visible other than the cursor resetting.
117
118
  // Ideally this should not be hit except when changing between text objects.
118
119
  view?.destroy();
119
- setView(new EditorView({ state, parent: root }));
120
- setState(state);
120
+ setView(new EditorView({ state, parent }));
121
121
 
122
122
  return () => {
123
123
  view?.destroy();
124
124
  setView(undefined);
125
125
  setState(undefined);
126
126
  };
127
- }, [root, model, themeMode, editorMode]);
127
+ }, [parent, model.content, themeMode, editorMode]);
128
128
 
129
129
  const handleKeyUp = useCallback(
130
130
  (event: KeyboardEvent) => {
@@ -136,7 +136,7 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
136
136
  }
137
137
 
138
138
  case 'Escape': {
139
- editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey) && root?.focus();
139
+ editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey) && parent?.focus();
140
140
  break;
141
141
  }
142
142
  }
@@ -147,7 +147,7 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
147
147
  return (
148
148
  <div
149
149
  key={model.id}
150
- ref={setRoot}
150
+ ref={setParent}
151
151
  tabIndex={0}
152
152
  {...slots?.root}
153
153
  {...(editorMode !== 'vim' && tabsterDOMAttribute)}
@@ -157,16 +157,15 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
157
157
  },
158
158
  );
159
159
 
160
- // TODO(burdon): Set default text theme?
161
160
  export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
162
- ({ extensions = [], slots: _slots, ...props }, forwardedRef) => {
161
+ ({ extensions: _extensions, slots, ...props }, forwardedRef) => {
163
162
  const { themeMode } = useThemeContext();
164
- const slots = defaultsDeep({}, _slots, defaultTextSlots);
163
+ const extensions = [...(_extensions ?? []), basicBundle({ themeMode, placeholder: slots?.editor?.placeholder })];
165
164
  return (
166
165
  <BaseTextEditor
167
166
  ref={forwardedRef}
168
- extensions={[basicBundle({ themeMode, placeholder: slots?.editor?.placeholder }), ...extensions]}
169
- slots={slots}
167
+ extensions={extensions}
168
+ slots={defaultsDeep({}, slots, { editor: { theme: basicTheme } })}
170
169
  {...props}
171
170
  />
172
171
  );
@@ -174,29 +173,16 @@ export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
174
173
  );
175
174
 
176
175
  export const MarkdownEditor = forwardRef<TextEditorRef, TextEditorProps>(
177
- ({ extensions = [], slots: _slots, ...props }, forwardedRef) => {
176
+ ({ extensions: _extensions, slots, ...props }, forwardedRef) => {
178
177
  const { themeMode } = useThemeContext();
179
- const slots = defaultsDeep({}, _slots, defaultSlots);
178
+ const extensions = [...(_extensions ?? []), markdownBundle({ themeMode })];
180
179
  return (
181
180
  <BaseTextEditor
182
181
  ref={forwardedRef}
183
- extensions={[markdownBundle({ themeMode, placeholder: slots?.editor?.placeholder }), ...extensions]}
184
- slots={slots}
182
+ extensions={extensions}
183
+ slots={defaultsDeep({}, slots, { editor: { theme: markdownTheme } })}
185
184
  {...props}
186
185
  />
187
186
  );
188
187
  },
189
188
  );
190
-
191
- export const defaultSlots: TextEditorSlots = {
192
- root: {
193
- className: mx('p-2', inputSurface),
194
- },
195
- };
196
-
197
- export const defaultTextSlots: TextEditorSlots = {
198
- ...defaultSlots,
199
- editor: {
200
- theme: textTheme,
201
- },
202
- };
@@ -17,14 +17,12 @@ export type BasicBundleOptions = {
17
17
 
18
18
  export const basicBundle = ({ themeMode, placeholder: _placeholder }: BasicBundleOptions): Extension[] =>
19
19
  [
20
- // TODO(burdon): Compare with minimalSetup.
21
- // https://codemirror.net/docs/ref/#codemirror.minimalSetup
22
20
  bracketMatching(),
23
21
  closeBrackets(),
24
- EditorView.lineWrapping,
25
-
26
22
  _placeholder && placeholder(_placeholder),
27
23
 
24
+ EditorView.lineWrapping,
25
+
28
26
  // TODO(burdon): Is this required?
29
27
  themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
30
28
  ].filter(Boolean) as Extension[];
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './bundle';
6
+ export * from './theme';
@@ -0,0 +1,49 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import get from 'lodash.get';
6
+ import { type StyleSpec } from 'style-mod';
7
+
8
+ import { tokens } from '../../../../styles';
9
+
10
+ /**
11
+ * Minimal styles.
12
+ * NOTE: The '&' prefix denotes the CM editor root.
13
+ * https://codemirror.net/examples/styling
14
+ */
15
+ // TODO(burdon): Remove?
16
+ export const basicTheme: {
17
+ [selector: string]: StyleSpec;
18
+ } = {
19
+ '&.cm-focused': {
20
+ outline: 'none',
21
+ },
22
+ '& .cm-line': {
23
+ paddingInline: 0,
24
+ lineHeight: 1.6,
25
+ minBlockSize: '1.6em',
26
+ },
27
+ '& .cm-line *': {
28
+ lineHeight: 1.6,
29
+ },
30
+ '& .cm-scroller': {
31
+ fontFamily: get(tokens, 'fontFamily.body', []).join(','),
32
+ overflow: 'visible',
33
+ },
34
+ '& .cm-content': {
35
+ padding: 0,
36
+ caretColor: 'black',
37
+ },
38
+ '.dark & .cm-content': {
39
+ caretColor: 'white',
40
+ },
41
+ '& .cm-tooltip': {
42
+ backgroundColor: 'transparent',
43
+ border: 'none',
44
+ },
45
+ '& .cm-link': {
46
+ color: get(tokens, 'extend.colors.primary.500'),
47
+ textDecoration: 'underline',
48
+ },
49
+ };
@@ -4,12 +4,12 @@
4
4
 
5
5
  import { StateField } from '@codemirror/state';
6
6
 
7
- export type TextListener = (text: string) => void;
8
-
9
7
  /**
10
8
  * Based on https://github.com/codemirror/dev/issues/44#issuecomment-789093799
9
+ * @param onChange
11
10
  */
12
- export const listener = (onChange: TextListener) =>
11
+ // TODO(burdon): Name?
12
+ export const onChangeExtension = (onChange: (text: string) => void) =>
13
13
  StateField.define({
14
14
  create: () => null,
15
15
  update: (_value, transaction) => {
@@ -4,14 +4,7 @@
4
4
  //
5
5
 
6
6
  import { syntaxTree } from '@codemirror/language';
7
- import {
8
- type EditorState,
9
- type Extension,
10
- type RangeSet,
11
- RangeSetBuilder,
12
- StateField,
13
- type Transaction,
14
- } from '@codemirror/state';
7
+ import { type EditorState, type RangeSet, RangeSetBuilder, StateField, type Transaction } from '@codemirror/state';
15
8
  import { Decoration, EditorView, WidgetType } from '@codemirror/view';
16
9
 
17
10
  // Adapted from:
@@ -26,33 +19,13 @@ import { Decoration, EditorView, WidgetType } from '@codemirror/view';
26
19
  // https://codemirror.net/5/demo/simplemode.html
27
20
  // https://codemirror.net/5/demo/search.html (Search dialog).
28
21
 
29
- class LinkButton extends WidgetType {
30
- constructor(
31
- private readonly _pos: number,
32
- private readonly _url: string,
33
- private readonly _onAttach: LinkOptions['onRender'],
34
- ) {
22
+ class Link extends WidgetType {
23
+ constructor(private readonly _anchor: number, private readonly _text: string, private readonly _url?: string) {
35
24
  super();
36
25
  }
37
26
 
38
- override eq(other: LinkButton) {
39
- return this._pos === other._pos && this._url === other._url;
40
- }
41
-
42
- toDOM(view: EditorView) {
43
- const el = document.createElement('span');
44
- this._onAttach!(el, this._url);
45
- return el;
46
- }
47
- }
48
-
49
- class LinkText extends WidgetType {
50
- constructor(private readonly _pos: number, private readonly _text: string, private readonly _url?: string) {
51
- super();
52
- }
53
-
54
- override eq(other: LinkText) {
55
- return this._pos === other._pos && this._url === other._url;
27
+ override eq(other: any) {
28
+ return this._text === other.text && this._url === other.url;
56
29
  }
57
30
 
58
31
  toDOM(view: EditorView) {
@@ -60,14 +33,13 @@ class LinkText extends WidgetType {
60
33
  link.setAttribute('class', 'cm-link');
61
34
  link.textContent = this._text;
62
35
  if (this._url) {
63
- link.setAttribute('rel', 'noreferrer');
64
36
  link.setAttribute('target', '_blank');
65
37
  link.href = this._url;
66
38
  } else {
67
39
  link.onclick = () => {
68
40
  view.dispatch({
69
41
  selection: {
70
- anchor: this._pos,
42
+ anchor: this._anchor,
71
43
  },
72
44
  });
73
45
  };
@@ -77,37 +49,45 @@ class LinkText extends WidgetType {
77
49
  }
78
50
  }
79
51
 
80
- const update = (state: EditorState, options: LinkOptions) => {
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) => {
81
69
  const builder = new RangeSetBuilder();
82
70
  const cursor = state.selection.main.head;
83
71
  syntaxTree(state).iterate({
84
72
  enter: (node) => {
85
73
  // Check if cursor is inside text.
86
- if (node.name === 'Link') {
87
- const marks = node.node.getChildren('LinkMark');
88
- const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
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) : '';
89
78
 
90
- const urlNode = node.node.getChild('URL');
91
- const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
79
+ const urlNode = node.node.getChild('URL');
80
+ const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
92
81
 
93
- if (cursor < node.from || cursor > node.to) {
94
82
  builder.add(
95
83
  node.from,
96
84
  node.to,
97
85
  Decoration.replace({
98
- widget: new LinkText(node.from, text, options.link ? url : undefined),
86
+ widget: new Link(node.from, text, link ? url : undefined),
99
87
  }),
100
88
  );
101
- }
102
89
 
103
- if (options.onRender) {
104
- builder.add(
105
- node.to,
106
- node.to,
107
- Decoration.replace({
108
- widget: new LinkButton(node.from, url, options.onRender),
109
- }),
110
- );
90
+ return false;
111
91
  }
112
92
  }
113
93
 
@@ -117,20 +97,3 @@ const update = (state: EditorState, options: LinkOptions) => {
117
97
 
118
98
  return builder.finish();
119
99
  };
120
-
121
- export type LinkOptions = {
122
- link?: boolean;
123
- onRender?: (el: Element, url: string) => void;
124
- };
125
-
126
- /**
127
- * Creates a state field to replace AST elements with a hyperlink widget.
128
- * https://codemirror.net/docs/ref/#state.StateField
129
- */
130
- export const link = (options: LinkOptions = {}): Extension => {
131
- return StateField.define<RangeSet<any>>({
132
- create: (state) => update(state, options),
133
- update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
134
- provide: (field) => EditorView.decorations.from(field),
135
- });
136
- };