@dxos/react-ui-editor 0.4.7 → 0.4.8-main.0602afb

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 (110) hide show
  1. package/dist/lib/browser/index.mjs +2489 -2651
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/TextEditor.d.ts +18 -34
  5. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +79 -5
  7. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  8. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +0 -1
  9. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
  10. package/dist/types/src/extensions/automerge/automerge.d.ts +1 -1
  11. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  12. package/dist/types/src/extensions/automerge/automerge.stories.d.ts +1 -2
  13. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  14. package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
  15. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  16. package/dist/types/src/extensions/automerge/defs.d.ts +1 -15
  17. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  18. package/dist/types/src/extensions/automerge/index.d.ts +0 -1
  19. package/dist/types/src/extensions/automerge/index.d.ts.map +1 -1
  20. package/dist/types/src/extensions/automerge/sync.d.ts +2 -1
  21. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
  22. package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -1
  23. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  24. package/dist/types/src/{hooks → extensions/awareness}/awareness-provider.d.ts +1 -1
  25. package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -0
  26. package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -0
  27. package/dist/types/src/extensions/awareness/index.d.ts +3 -0
  28. package/dist/types/src/extensions/awareness/index.d.ts.map +1 -0
  29. package/dist/types/src/extensions/comments.d.ts +2 -2
  30. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  31. package/dist/types/src/extensions/cursor-line-margin.d.ts +6 -7
  32. package/dist/types/src/extensions/cursor-line-margin.d.ts.map +1 -1
  33. package/dist/types/src/extensions/cursor.d.ts +1 -1
  34. package/dist/types/src/extensions/cursor.d.ts.map +1 -1
  35. package/dist/types/src/extensions/doc.d.ts +3 -0
  36. package/dist/types/src/extensions/doc.d.ts.map +1 -0
  37. package/dist/types/src/extensions/factories.d.ts +49 -0
  38. package/dist/types/src/extensions/factories.d.ts.map +1 -0
  39. package/dist/types/src/extensions/index.d.ts +3 -1
  40. package/dist/types/src/extensions/index.d.ts.map +1 -1
  41. package/dist/types/src/extensions/markdown/bundle.d.ts +5 -4
  42. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  43. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  44. package/dist/types/src/extensions/markdown/formatting.d.ts +1 -1
  45. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  46. package/dist/types/src/extensions/markdown/table.d.ts +2 -2
  47. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -1
  48. package/dist/types/src/extensions/types.d.ts +9 -0
  49. package/dist/types/src/extensions/types.d.ts.map +1 -0
  50. package/dist/types/src/hooks/index.d.ts +1 -4
  51. package/dist/types/src/hooks/index.d.ts.map +1 -1
  52. package/dist/types/src/hooks/useDocAccessor.d.ts +7 -0
  53. package/dist/types/src/hooks/useDocAccessor.d.ts.map +1 -0
  54. package/dist/types/src/hooks/useTextEditor.d.ts +3 -44
  55. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  56. package/dist/types/src/hooks/useTextEditor.stories.d.ts +6 -1
  57. package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -1
  58. package/dist/types/src/themes/default.d.ts +0 -3
  59. package/dist/types/src/themes/default.d.ts.map +1 -1
  60. package/package.json +24 -24
  61. package/src/components/TextEditor/TextEditor.stories.tsx +508 -23
  62. package/src/components/TextEditor/TextEditor.tsx +37 -149
  63. package/src/components/Toolbar/Toolbar.stories.tsx +42 -42
  64. package/src/extensions/automerge/automerge.stories.tsx +16 -17
  65. package/src/extensions/automerge/automerge.ts +2 -1
  66. package/src/extensions/automerge/cursor.ts +1 -1
  67. package/src/extensions/automerge/defs.ts +1 -22
  68. package/src/extensions/automerge/index.ts +0 -1
  69. package/src/extensions/automerge/sync.ts +2 -9
  70. package/src/extensions/automerge/update-automerge.ts +2 -1
  71. package/src/{hooks → extensions/awareness}/awareness-provider.ts +1 -1
  72. package/src/extensions/{awareness.ts → awareness/awareness.ts} +1 -1
  73. package/src/extensions/awareness/index.ts +6 -0
  74. package/src/extensions/command/state.ts +1 -1
  75. package/src/extensions/comments.ts +9 -5
  76. package/src/extensions/cursor-line-margin.ts +17 -18
  77. package/src/extensions/cursor.ts +1 -1
  78. package/src/extensions/doc.ts +14 -0
  79. package/src/extensions/factories.ts +170 -0
  80. package/src/extensions/index.ts +3 -1
  81. package/src/extensions/markdown/bundle.ts +11 -28
  82. package/src/extensions/markdown/decorate.ts +1 -3
  83. package/src/extensions/markdown/formatting.ts +15 -10
  84. package/src/extensions/markdown/image.ts +1 -0
  85. package/src/extensions/markdown/table.ts +11 -3
  86. package/src/extensions/types.ts +16 -0
  87. package/src/hooks/index.ts +1 -5
  88. package/src/hooks/useDocAccessor.ts +21 -0
  89. package/src/hooks/useTextEditor.stories.tsx +39 -34
  90. package/src/hooks/useTextEditor.ts +11 -120
  91. package/src/index.ts +1 -1
  92. package/src/themes/default.ts +1 -28
  93. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +0 -103
  94. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +0 -1
  95. package/dist/types/src/extensions/awareness.d.ts.map +0 -1
  96. package/dist/types/src/extensions/outliner.d.ts +0 -4
  97. package/dist/types/src/extensions/outliner.d.ts.map +0 -1
  98. package/dist/types/src/hooks/awareness-provider.d.ts.map +0 -1
  99. package/dist/types/src/hooks/defs.d.ts +0 -20
  100. package/dist/types/src/hooks/defs.d.ts.map +0 -1
  101. package/dist/types/src/hooks/useEditorView.d.ts +0 -20
  102. package/dist/types/src/hooks/useEditorView.d.ts.map +0 -1
  103. package/dist/types/src/hooks/useTextModel.d.ts +0 -33
  104. package/dist/types/src/hooks/useTextModel.d.ts.map +0 -1
  105. package/src/components/TextEditor/MarkdownEditor.stories.tsx +0 -524
  106. package/src/extensions/outliner.ts +0 -12
  107. package/src/hooks/defs.ts +0 -30
  108. package/src/hooks/useEditorView.ts +0 -32
  109. package/src/hooks/useTextModel.ts +0 -121
  110. /package/dist/types/src/extensions/{awareness.d.ts → awareness/awareness.d.ts} +0 -0
@@ -5,26 +5,26 @@
5
5
  // Based upon @acheronfail’s implementation, fetched 16 Feb 2024
6
6
  // https://discuss.codemirror.net/t/cursorscrollmargin-for-v6/7448/5
7
7
 
8
- import { type EditorState, type Extension, Facet, Transaction } from '@codemirror/state';
8
+ import { type EditorState, type Extension, Transaction } from '@codemirror/state';
9
9
  import { EditorView } from '@codemirror/view';
10
10
 
11
11
  import { log } from '@dxos/log';
12
12
 
13
- /**
14
- * Number of lines above/below the cursor to keep visible in the viewport.
15
- * Defaults to 3.
16
- */
17
- export const cursorLineMarginFacet = Facet.define<number, number>({
18
- combine: (input) => input[0] ?? 3,
19
- });
20
-
21
13
  const annotation = 'cursorLineMargin';
22
14
  const lineAtPos = (s: EditorState, pos: number) => s.doc.lineAt(pos).number;
23
15
 
24
- // Seems to the best approximation of CM5's `cursorScrollMargin`
25
- // https://discuss.codemirror.net/t/cursorscrollmargin-for-v6/7448
26
- export const cursorLineMargin: Extension = EditorView.updateListener.of(
27
- ({ transactions, state, selectionSet, startState, view }) => {
16
+ type Options = {
17
+ lines?: number;
18
+ };
19
+
20
+ // TODO(burdon): Remove. Buggy and creates scrolling problems. Find better (more general) way to deal with HALO notch.
21
+ export const cursorLineMargin = (options: Options = { lines: 3 }): Extension => {
22
+ const { lines } = options;
23
+ if (!lines || lines <= 0) {
24
+ return [];
25
+ }
26
+
27
+ return EditorView.updateListener.of(({ transactions, state, selectionSet, startState, view }) => {
28
28
  // make sure we don't trigger an infinite loop and ignore our own changes
29
29
  if (transactions.length < 1 || transactions.some((tr) => tr.isUserEvent(annotation))) {
30
30
  return;
@@ -32,7 +32,6 @@ export const cursorLineMargin: Extension = EditorView.updateListener.of(
32
32
 
33
33
  const s = state;
34
34
  const { main } = s.selection;
35
- const cursorLineMargin = s.facet(cursorLineMarginFacet);
36
35
 
37
36
  // editor rect
38
37
  const rect = view.dom.getBoundingClientRect();
@@ -59,7 +58,7 @@ export const cursorLineMargin: Extension = EditorView.updateListener.of(
59
58
 
60
59
  // const needsScrollTop = mainLine <= visTopLine + cursorLineMargin;
61
60
  const needsScrollTop = false;
62
- const needsScrollBot = mainLine >= visBotLine - cursorLineMargin;
61
+ const needsScrollBot = mainLine >= visBotLine - lines;
63
62
 
64
63
  // the scroll margins are overlapping
65
64
  if (needsScrollTop && needsScrollBot) {
@@ -88,8 +87,8 @@ export const cursorLineMargin: Extension = EditorView.updateListener.of(
88
87
  annotations: Transaction.userEvent.of(annotation),
89
88
  effects: EditorView.scrollIntoView(s.selection.main.head, {
90
89
  y: needsScrollTop ? 'start' : 'end',
91
- yMargin: view.defaultLineHeight * cursorLineMargin,
90
+ yMargin: view.defaultLineHeight * lines,
92
91
  }),
93
92
  });
94
- },
95
- );
93
+ });
94
+ };
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { type EditorState, Facet } from '@codemirror/state';
6
6
 
7
- import type { Range } from '../hooks';
7
+ import type { Range } from './types';
8
8
 
9
9
  /**
10
10
  * Converts indexes into the text document into stable peer-independent cursors.
@@ -0,0 +1,14 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Facet } from '@codemirror/state';
6
+
7
+ import { invariant } from '@dxos/invariant';
8
+
9
+ export const documentId = Facet.define<string, string>({
10
+ combine: (providers) => {
11
+ invariant(providers.length <= 1);
12
+ return providers[0];
13
+ },
14
+ });
@@ -0,0 +1,170 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { closeBrackets } from '@codemirror/autocomplete';
6
+ import { history } from '@codemirror/commands';
7
+ import { bracketMatching } from '@codemirror/language';
8
+ import { EditorState, type Extension } from '@codemirror/state';
9
+ import {
10
+ EditorView,
11
+ crosshairCursor,
12
+ drawSelection,
13
+ dropCursor,
14
+ highlightActiveLine,
15
+ lineNumbers,
16
+ placeholder,
17
+ scrollPastEnd,
18
+ } from '@codemirror/view';
19
+ import defaultsDeep from 'lodash.defaultsdeep';
20
+
21
+ import { generateName } from '@dxos/display-name';
22
+ import { type DocAccessor } from '@dxos/echo-schema';
23
+ import { log } from '@dxos/log';
24
+ import { type Space } from '@dxos/react-client/echo';
25
+ import { type Identity } from '@dxos/react-client/halo';
26
+ import { type ThemeMode } from '@dxos/react-ui';
27
+ import { type HuePalette, hueTokens } from '@dxos/react-ui-theme';
28
+ import { hexToHue, isNotFalsy } from '@dxos/util';
29
+
30
+ import { automerge } from './automerge';
31
+ import { awareness, SpaceAwarenessProvider } from './awareness';
32
+ import { type ThemeStyles } from '../styles';
33
+ import { defaultTheme } from '../themes';
34
+
35
+ // TODO(burdon): Move into extensions folder.
36
+
37
+ //
38
+ // Basic
39
+ //
40
+
41
+ /**
42
+ * https://codemirror.net/docs/extensions
43
+ * https://github.com/codemirror/basic-setup
44
+ */
45
+ // TODO(burdon): Reconcile with createMarkdownExtensions.
46
+ export type BasicExtensionsOptions = {
47
+ allowMultipleSelections?: boolean;
48
+ bracketMatching?: boolean;
49
+ closeBrackets?: boolean;
50
+ crosshairCursor?: boolean;
51
+ dropCursor?: boolean;
52
+ drawSelection?: boolean;
53
+ editable?: boolean;
54
+ highlightActiveLine?: boolean;
55
+ history?: boolean;
56
+ lineNumbers?: boolean;
57
+ lineWrapping?: boolean;
58
+ placeholder?: string;
59
+ readonly?: boolean;
60
+ scrollPastEnd?: boolean;
61
+ tabSize?: number;
62
+ };
63
+
64
+ const defaults: BasicExtensionsOptions = {
65
+ bracketMatching: true,
66
+ closeBrackets: true,
67
+ drawSelection: true,
68
+ editable: true,
69
+ history: true,
70
+ lineWrapping: true,
71
+ };
72
+
73
+ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extension => {
74
+ const props: BasicExtensionsOptions = defaultsDeep({}, _props, defaults);
75
+ return [
76
+ // TODO(burdon): Doesn't catch errors in keymap functions.
77
+ EditorView.exceptionSink.of((err) => {
78
+ log.catch(err);
79
+ }),
80
+
81
+ props.allowMultipleSelections && EditorState.allowMultipleSelections.of(true),
82
+ props.bracketMatching && bracketMatching(),
83
+ props.closeBrackets && closeBrackets(),
84
+ props.crosshairCursor && crosshairCursor(),
85
+ props.dropCursor && dropCursor(),
86
+ props.drawSelection && drawSelection(),
87
+ props.highlightActiveLine && highlightActiveLine(),
88
+ props.history && history(),
89
+ props.lineNumbers && lineNumbers(),
90
+ props.lineWrapping && EditorView.lineWrapping,
91
+ props.placeholder && placeholder(props.placeholder),
92
+ props.readonly && [EditorState.readOnly.of(true), EditorView.editable.of(false)],
93
+ props.scrollPastEnd && scrollPastEnd(),
94
+ props.tabSize && EditorState.tabSize.of(props.tabSize),
95
+ ].filter(isNotFalsy);
96
+ };
97
+
98
+ //
99
+ // Theme
100
+ //
101
+
102
+ export type ThemeExtensionsOptions = {
103
+ theme?: ThemeStyles;
104
+ themeMode?: ThemeMode;
105
+ slots?: {
106
+ editor?: {
107
+ className?: string;
108
+ };
109
+ content?: {
110
+ className?: string;
111
+ };
112
+ };
113
+ };
114
+
115
+ const defaultSlots = {
116
+ editor: {
117
+ className: 'w-full bs-full',
118
+ },
119
+ content: {
120
+ className: '!p-2',
121
+ },
122
+ };
123
+
124
+ export const createThemeExtensions = ({ theme, themeMode, slots: _slots }: ThemeExtensionsOptions = {}): Extension => {
125
+ const slots = defaultsDeep({}, _slots, defaultSlots);
126
+ return [
127
+ EditorView.baseTheme(defaultTheme),
128
+ EditorView.darkTheme.of(themeMode === 'dark'),
129
+ theme && EditorView.theme(theme),
130
+ slots.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
131
+ slots.content?.className && EditorView.contentAttributes.of({ class: slots.content.className }),
132
+ ].filter(isNotFalsy);
133
+ };
134
+
135
+ //
136
+ // Data
137
+ //
138
+
139
+ export type DataExtensionsProps = {
140
+ id: string;
141
+ text: DocAccessor; // TODO(burdon): Rename content.
142
+ space?: Space;
143
+ identity?: Identity | null;
144
+ };
145
+
146
+ // TODO(burdon): Factor out automerge defs and extension (not hook).
147
+ // TODO(burdon): Move out of react-ui-editor (remove echo deps).
148
+ export const createDataExtensions = ({ id, text, space, identity }: DataExtensionsProps): Extension[] => {
149
+ const extensions: Extension[] = [automerge(text)];
150
+
151
+ if (space && identity) {
152
+ const peerId = identity?.identityKey.toHex();
153
+ const { cursorLightValue, cursorDarkValue } =
154
+ hueTokens[(identity?.profile?.data?.hue as HuePalette | undefined) ?? hexToHue(peerId ?? '0')];
155
+ const awarenessProvider = new SpaceAwarenessProvider({
156
+ space,
157
+ channel: `awareness.${id}`,
158
+ peerId: identity.identityKey.toHex(),
159
+ info: {
160
+ displayName: identity.profile?.displayName ?? generateName(identity.identityKey.toHex()),
161
+ color: cursorDarkValue,
162
+ lightColor: cursorLightValue,
163
+ },
164
+ });
165
+
166
+ extensions.push(awareness(awarenessProvider));
167
+ }
168
+
169
+ return extensions;
170
+ };
@@ -11,10 +11,12 @@ export * from './command';
11
11
  export * from './comments';
12
12
  export * from './cursor';
13
13
  export * from './cursor-line-margin';
14
+ export * from './doc';
14
15
  export * from './dnd';
16
+ export * from './factories';
15
17
  export * from './listener';
16
18
  export * from './markdown';
17
19
  export * from './mention';
18
20
  export * from './modes';
19
- export * from './outliner';
21
+ export * from './types';
20
22
  export * from './typewriter';
@@ -2,51 +2,34 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
6
- import { history, historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
5
+ import { closeBracketsKeymap } from '@codemirror/autocomplete';
6
+ import { historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
7
7
  import { markdownLanguage, markdown } from '@codemirror/lang-markdown';
8
- import { bracketMatching, defaultHighlightStyle, indentOnInput, syntaxHighlighting } from '@codemirror/language';
8
+ import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
9
9
  import { languages } from '@codemirror/language-data';
10
10
  import { searchKeymap } from '@codemirror/search';
11
- import { EditorState, type Extension } from '@codemirror/state';
11
+ import { type Extension } from '@codemirror/state';
12
12
  import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
13
- import { EditorView, drawSelection, dropCursor, keymap, placeholder } from '@codemirror/view';
13
+ import { keymap } from '@codemirror/view';
14
14
 
15
15
  import { type ThemeMode } from '@dxos/react-ui';
16
- import { isNotFalsy } from '@dxos/util';
17
16
 
18
17
  import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
19
- import { type TextEditorProps } from '../../components';
20
18
 
21
19
  export type MarkdownBundleOptions = {
22
20
  themeMode?: ThemeMode;
23
- } & Pick<TextEditorProps, 'lineWrapping' | 'placeholder'>;
21
+ };
24
22
 
25
23
  /**
26
- * Markdown bundle.
24
+ * Creates markdown extensions.
25
+ * To be used in conjunction with createBasicExtensions.
26
+ *
27
27
  * Refs:
28
28
  * https://codemirror.net/docs/community
29
29
  * https://codemirror.net/docs/ref/#codemirror.basicSetup
30
30
  */
31
- export const createMarkdownExtensions = ({
32
- themeMode,
33
- placeholder: _placeholder,
34
- lineWrapping = true,
35
- }: MarkdownBundleOptions = {}): Extension[] => {
31
+ export const createMarkdownExtensions = ({ themeMode }: MarkdownBundleOptions = {}): Extension[] => {
36
32
  return [
37
- // TODO(burdon): Reconcile with createBasicExtensions.
38
- lineWrapping && EditorView.lineWrapping,
39
- EditorState.allowMultipleSelections.of(true),
40
- EditorState.tabSize.of(2),
41
-
42
- bracketMatching(),
43
- closeBrackets(),
44
- dropCursor(),
45
- drawSelection(),
46
- history(),
47
- indentOnInput(),
48
- _placeholder && placeholder(_placeholder),
49
-
50
33
  // Main extension.
51
34
  // https://github.com/codemirror/lang-markdown
52
35
  // https://codemirror.net/5/mode/markdown/index.html (demo).
@@ -86,5 +69,5 @@ export const createMarkdownExtensions = ({
86
69
  // https://codemirror.net/docs/ref/#commands.standardKeymap
87
70
  ...standardKeymap,
88
71
  ]),
89
- ].filter(isNotFalsy);
72
+ ];
90
73
  };
@@ -148,9 +148,7 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
148
148
  marks[1].from,
149
149
  node.to,
150
150
  options.renderLinkButton
151
- ? Decoration.replace({
152
- widget: new LinkButton(url, options.renderLinkButton),
153
- })
151
+ ? Decoration.replace({ widget: new LinkButton(url, options.renderLinkButton) })
154
152
  : hide,
155
153
  );
156
154
  }
@@ -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
- // Iterate the selected range. For each textblock, determine a
194
- // start and end position, the overlap of the selected range and
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, { userEvent: enable ? 'format.style.add' : 'format.style.remove', scrollIntoView: true }),
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
 
@@ -987,7 +992,7 @@ export const toggleCodeblock: StateCommand = (target) => {
987
992
 
988
993
  export type FormattingOptions = {};
989
994
 
990
- export const formatting = (options: FormattingOptions = {}): Extension => {
995
+ export const formattingKeymap = (options: FormattingOptions = {}): Extension => {
991
996
  return [
992
997
  keymap.of([
993
998
  {
@@ -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({
@@ -3,7 +3,14 @@
3
3
  //
4
4
 
5
5
  import { syntaxTree } from '@codemirror/language';
6
- import { type EditorState, type RangeSet, RangeSetBuilder, StateField, type Transaction } from '@codemirror/state';
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' }));
@@ -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
+ };
@@ -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 './useEditorView';
6
+ export * from './useDocAccessor';
10
7
  export * from './useTextEditor';
11
- export * from './useTextModel';
@@ -0,0 +1,21 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { useMemo } from 'react';
6
+
7
+ import { createDocAccessor, type DocAccessor, getTextContent, type TextObject } from '@dxos/echo-schema';
8
+
9
+ // TODO(burdon): Factor out.
10
+ export const useDocAccessor = <T = any>(
11
+ text: TextObject,
12
+ ): { id: string; doc: string | undefined; accessor: DocAccessor<T> } => {
13
+ return useMemo(
14
+ () => ({
15
+ id: text.id,
16
+ doc: getTextContent(text),
17
+ accessor: createDocAccessor<T>(text),
18
+ }),
19
+ [text],
20
+ );
21
+ };
@@ -4,34 +4,28 @@
4
4
 
5
5
  import '@dxosTheme';
6
6
 
7
- import React, { useMemo, useState } from 'react';
7
+ import React, { useState } from 'react';
8
8
 
9
9
  import { Toolbar as NaturalToolbar, Select, useThemeContext, Tooltip } from '@dxos/react-ui';
10
10
  import { attentionSurface, mx, textBlockWidth } from '@dxos/react-ui-theme';
11
11
  import { withTheme } from '@dxos/storybook-utils';
12
12
 
13
13
  import { useActionHandler } from './useActionHandler';
14
- import { createBasicExtensions, createThemeExtensions, useTextEditor } from './useTextEditor';
14
+ import { useTextEditor } from './useTextEditor';
15
15
  import { Toolbar } from '../components';
16
+ import { createBasicExtensions, createThemeExtensions } from '../extensions';
16
17
  import {
17
18
  type EditorMode,
18
19
  EditorModes,
19
20
  decorateMarkdown,
20
21
  createMarkdownExtensions,
21
- formatting,
22
- image,
22
+ formattingKeymap,
23
23
  table,
24
24
  useFormattingState,
25
+ image,
25
26
  } from '../extensions';
26
- import { markdownTheme } from '../themes';
27
27
  import translations from '../translations';
28
28
 
29
- // TODO(burdon): Demo toolbar with hooks.
30
- // TODO(burdon): Build components from hooks and adapters for model/extensions, etc.
31
- // TODO(burdon): Remove BaseTextEditor.
32
- // TODO(burdon): Move scrolling container layout/logic into TextEditor/MarkdownEditor components.
33
- // TODO(burdon): Define keymap in composer framework format.
34
-
35
29
  type StoryProps = {
36
30
  autoFocus?: boolean;
37
31
  placeholder?: string;
@@ -43,29 +37,24 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
43
37
  const { themeMode } = useThemeContext();
44
38
  const [formattingState, trackFormatting] = useFormattingState();
45
39
  const [editorMode, setEditorMode] = useState<EditorMode>('default');
46
- const extensions = useMemo(
47
- () => [
48
- editorMode ? EditorModes[editorMode] : [],
49
- createBasicExtensions({ readonly }),
50
- createThemeExtensions({
51
- themeMode,
52
- theme: markdownTheme,
53
- slots: {
54
- editor: { className: 'p-2' },
55
- },
56
- }),
57
- // TODO(burdon): Move lineWrapping.
58
- createMarkdownExtensions({ placeholder, lineWrapping: true }),
59
- // TODO(burdon): Move into markdown bundle (with React callbacks).
60
- decorateMarkdown(),
61
- formatting(),
62
- image(),
63
- table(),
64
- trackFormatting,
65
- ],
66
- [editorMode, themeMode, placeholder, trackFormatting, readonly],
40
+ const { parentRef, view } = useTextEditor(
41
+ () => ({
42
+ autoFocus,
43
+ doc,
44
+ extensions: [
45
+ editorMode ? EditorModes[editorMode] : [],
46
+ createBasicExtensions({ placeholder, lineWrapping: true, readonly }),
47
+ createMarkdownExtensions({ themeMode }),
48
+ createThemeExtensions({ themeMode }),
49
+ decorateMarkdown(),
50
+ formattingKeymap(),
51
+ image(),
52
+ table(),
53
+ trackFormatting,
54
+ ],
55
+ }),
56
+ [editorMode, themeMode, placeholder, readonly],
67
57
  );
68
- const { parentRef, view } = useTextEditor({ autoFocus, doc, extensions });
69
58
 
70
59
  const handleAction = useActionHandler(view);
71
60
 
@@ -116,7 +105,6 @@ const EditorModeToolbar = ({
116
105
 
117
106
  export default {
118
107
  title: 'react-ui-editor/useTextEditor',
119
- // component: TextEditor,
120
108
  decorators: [withTheme],
121
109
  render: (args: StoryProps) => (
122
110
  <Tooltip.Provider>
@@ -127,6 +115,23 @@ export default {
127
115
  };
128
116
 
129
117
  export const Default = {
118
+ render: () => {
119
+ const { parentRef } = useTextEditor();
120
+ return <div role='textbox' className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />;
121
+ },
122
+ };
123
+
124
+ export const Basic = {
125
+ render: () => {
126
+ const { themeMode } = useThemeContext();
127
+ const { parentRef } = useTextEditor(() => ({
128
+ extensions: [createBasicExtensions({ placeholder: 'Enter text...' }), createThemeExtensions({ themeMode })],
129
+ }));
130
+ return <div role='textbox' className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />;
131
+ },
132
+ };
133
+
134
+ export const Markdown = {
130
135
  args: {
131
136
  autoFocus: true,
132
137
  placeholder: 'Text...',