@dxos/react-ui-editor 0.4.8-main.97e86fe → 0.4.8-main.a4c603b

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 (108) hide show
  1. package/dist/lib/browser/index.mjs +941 -776
  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/MarkdownEditor.stories.d.ts +103 -0
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -0
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +34 -18
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +5 -79
  9. package/dist/types/src/components/TextEditor/TextEditor.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 -1
  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 +15 -1
  17. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  18. package/dist/types/src/extensions/automerge/index.d.ts +1 -0
  19. package/dist/types/src/extensions/automerge/index.d.ts.map +1 -1
  20. package/dist/types/src/extensions/automerge/sync.d.ts +1 -2
  21. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
  22. package/dist/types/src/extensions/automerge/update-automerge.d.ts +1 -2
  23. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  24. package/dist/types/src/extensions/awareness.d.ts.map +1 -0
  25. package/dist/types/src/extensions/comments.d.ts +2 -2
  26. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  27. package/dist/types/src/extensions/cursor-line-margin.d.ts +7 -6
  28. package/dist/types/src/extensions/cursor-line-margin.d.ts.map +1 -1
  29. package/dist/types/src/extensions/cursor.d.ts +1 -1
  30. package/dist/types/src/extensions/cursor.d.ts.map +1 -1
  31. package/dist/types/src/extensions/index.d.ts +1 -3
  32. package/dist/types/src/extensions/index.d.ts.map +1 -1
  33. package/dist/types/src/extensions/markdown/bundle.d.ts +4 -5
  34. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  35. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  36. package/dist/types/src/extensions/markdown/formatting.d.ts +1 -1
  37. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  38. package/dist/types/src/extensions/markdown/table.d.ts +2 -2
  39. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -1
  40. package/dist/types/src/extensions/outliner.d.ts +4 -0
  41. package/dist/types/src/extensions/outliner.d.ts.map +1 -0
  42. package/dist/types/src/{extensions/awareness → hooks}/awareness-provider.d.ts +1 -1
  43. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -0
  44. package/dist/types/src/hooks/defs.d.ts +20 -0
  45. package/dist/types/src/hooks/defs.d.ts.map +1 -0
  46. package/dist/types/src/hooks/index.d.ts +4 -1
  47. package/dist/types/src/hooks/index.d.ts.map +1 -1
  48. package/dist/types/src/hooks/useEditorView.d.ts +20 -0
  49. package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
  50. package/dist/types/src/hooks/useTextEditor.d.ts +44 -3
  51. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  52. package/dist/types/src/hooks/useTextEditor.stories.d.ts +0 -6
  53. package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -1
  54. package/dist/types/src/hooks/useTextModel.d.ts +33 -0
  55. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -0
  56. package/dist/types/src/themes/default.d.ts +3 -0
  57. package/dist/types/src/themes/default.d.ts.map +1 -1
  58. package/package.json +24 -24
  59. package/src/components/TextEditor/MarkdownEditor.stories.tsx +524 -0
  60. package/src/components/TextEditor/TextEditor.stories.tsx +23 -508
  61. package/src/components/TextEditor/TextEditor.tsx +149 -37
  62. package/src/components/Toolbar/Toolbar.stories.tsx +42 -42
  63. package/src/extensions/automerge/automerge.stories.tsx +17 -16
  64. package/src/extensions/automerge/automerge.ts +1 -2
  65. package/src/extensions/automerge/cursor.ts +1 -1
  66. package/src/extensions/automerge/defs.ts +22 -1
  67. package/src/extensions/automerge/index.ts +1 -0
  68. package/src/extensions/automerge/sync.ts +9 -2
  69. package/src/extensions/automerge/update-automerge.ts +1 -2
  70. package/src/extensions/{awareness/awareness.ts → awareness.ts} +1 -1
  71. package/src/extensions/command/state.ts +1 -1
  72. package/src/extensions/comments.ts +5 -9
  73. package/src/extensions/cursor-line-margin.ts +18 -17
  74. package/src/extensions/cursor.ts +1 -1
  75. package/src/extensions/index.ts +1 -3
  76. package/src/extensions/markdown/bundle.ts +28 -11
  77. package/src/extensions/markdown/decorate.ts +4 -2
  78. package/src/extensions/markdown/formatting.ts +10 -15
  79. package/src/extensions/markdown/image.ts +0 -1
  80. package/src/extensions/markdown/table.ts +3 -11
  81. package/src/extensions/outliner.ts +12 -0
  82. package/src/{extensions/awareness → hooks}/awareness-provider.ts +1 -1
  83. package/src/hooks/defs.ts +30 -0
  84. package/src/hooks/index.ts +5 -1
  85. package/src/hooks/useEditorView.ts +32 -0
  86. package/src/hooks/useTextEditor.stories.tsx +34 -39
  87. package/src/hooks/useTextEditor.ts +120 -11
  88. package/src/hooks/useTextModel.ts +121 -0
  89. package/src/index.ts +1 -1
  90. package/src/themes/default.ts +28 -1
  91. package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +0 -1
  92. package/dist/types/src/extensions/awareness/awareness.d.ts.map +0 -1
  93. package/dist/types/src/extensions/awareness/index.d.ts +0 -3
  94. package/dist/types/src/extensions/awareness/index.d.ts.map +0 -1
  95. package/dist/types/src/extensions/doc.d.ts +0 -3
  96. package/dist/types/src/extensions/doc.d.ts.map +0 -1
  97. package/dist/types/src/extensions/factories.d.ts +0 -49
  98. package/dist/types/src/extensions/factories.d.ts.map +0 -1
  99. package/dist/types/src/extensions/types.d.ts +0 -9
  100. package/dist/types/src/extensions/types.d.ts.map +0 -1
  101. package/dist/types/src/hooks/useDocAccessor.d.ts +0 -7
  102. package/dist/types/src/hooks/useDocAccessor.d.ts.map +0 -1
  103. package/src/extensions/awareness/index.ts +0 -6
  104. package/src/extensions/doc.ts +0 -14
  105. package/src/extensions/factories.ts +0 -167
  106. package/src/extensions/types.ts +0 -16
  107. package/src/hooks/useDocAccessor.ts +0 -21
  108. /package/dist/types/src/extensions/{awareness/awareness.d.ts → 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, Transaction } from '@codemirror/state';
8
+ import { type EditorState, type Extension, Facet, 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
+
13
21
  const annotation = 'cursorLineMargin';
14
22
  const lineAtPos = (s: EditorState, pos: number) => s.doc.lineAt(pos).number;
15
23
 
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 }) => {
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 }) => {
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,6 +32,7 @@ export const cursorLineMargin = (options: Options = { lines: 3 }): Extension =>
32
32
 
33
33
  const s = state;
34
34
  const { main } = s.selection;
35
+ const cursorLineMargin = s.facet(cursorLineMarginFacet);
35
36
 
36
37
  // editor rect
37
38
  const rect = view.dom.getBoundingClientRect();
@@ -58,7 +59,7 @@ export const cursorLineMargin = (options: Options = { lines: 3 }): Extension =>
58
59
 
59
60
  // const needsScrollTop = mainLine <= visTopLine + cursorLineMargin;
60
61
  const needsScrollTop = false;
61
- const needsScrollBot = mainLine >= visBotLine - lines;
62
+ const needsScrollBot = mainLine >= visBotLine - cursorLineMargin;
62
63
 
63
64
  // the scroll margins are overlapping
64
65
  if (needsScrollTop && needsScrollBot) {
@@ -87,8 +88,8 @@ export const cursorLineMargin = (options: Options = { lines: 3 }): Extension =>
87
88
  annotations: Transaction.userEvent.of(annotation),
88
89
  effects: EditorView.scrollIntoView(s.selection.main.head, {
89
90
  y: needsScrollTop ? 'start' : 'end',
90
- yMargin: view.defaultLineHeight * lines,
91
+ yMargin: view.defaultLineHeight * cursorLineMargin,
91
92
  }),
92
93
  });
93
- });
94
- };
94
+ },
95
+ );
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { type EditorState, Facet } from '@codemirror/state';
6
6
 
7
- import type { Range } from './types';
7
+ import type { Range } from '../hooks';
8
8
 
9
9
  /**
10
10
  * Converts indexes into the text document into stable peer-independent cursors.
@@ -11,12 +11,10 @@ export * from './command';
11
11
  export * from './comments';
12
12
  export * from './cursor';
13
13
  export * from './cursor-line-margin';
14
- export * from './doc';
15
14
  export * from './dnd';
16
- export * from './factories';
17
15
  export * from './listener';
18
16
  export * from './markdown';
19
17
  export * from './mention';
20
18
  export * from './modes';
21
- export * from './types';
19
+ export * from './outliner';
22
20
  export * from './typewriter';
@@ -2,34 +2,51 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { closeBracketsKeymap } from '@codemirror/autocomplete';
6
- import { historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
5
+ import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
6
+ import { history, historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
7
7
  import { markdownLanguage, markdown } from '@codemirror/lang-markdown';
8
- import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
8
+ import { bracketMatching, defaultHighlightStyle, indentOnInput, syntaxHighlighting } from '@codemirror/language';
9
9
  import { languages } from '@codemirror/language-data';
10
10
  import { searchKeymap } from '@codemirror/search';
11
- import { type Extension } from '@codemirror/state';
11
+ import { EditorState, type Extension } from '@codemirror/state';
12
12
  import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
13
- import { keymap } from '@codemirror/view';
13
+ import { EditorView, drawSelection, dropCursor, keymap, placeholder } from '@codemirror/view';
14
14
 
15
15
  import { type ThemeMode } from '@dxos/react-ui';
16
+ import { isNotFalsy } from '@dxos/util';
16
17
 
17
18
  import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
19
+ import { type TextEditorProps } from '../../components';
18
20
 
19
21
  export type MarkdownBundleOptions = {
20
22
  themeMode?: ThemeMode;
21
- };
23
+ } & Pick<TextEditorProps, 'lineWrapping' | 'placeholder'>;
22
24
 
23
25
  /**
24
- * Creates markdown extensions.
25
- * To be used in conjunction with createBasicExtensions.
26
- *
26
+ * Markdown bundle.
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 = ({ themeMode }: MarkdownBundleOptions = {}): Extension[] => {
31
+ export const createMarkdownExtensions = ({
32
+ themeMode,
33
+ placeholder: _placeholder,
34
+ lineWrapping = true,
35
+ }: MarkdownBundleOptions = {}): Extension[] => {
32
36
  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
+
33
50
  // Main extension.
34
51
  // https://github.com/codemirror/lang-markdown
35
52
  // https://codemirror.net/5/mode/markdown/index.html (demo).
@@ -69,5 +86,5 @@ export const createMarkdownExtensions = ({ themeMode }: MarkdownBundleOptions =
69
86
  // https://codemirror.net/docs/ref/#commands.standardKeymap
70
87
  ...standardKeymap,
71
88
  ]),
72
- ];
89
+ ].filter(isNotFalsy);
73
90
  };
@@ -48,7 +48,7 @@ class CheckboxWidget extends WidgetType {
48
48
 
49
49
  override toDOM(view: EditorView) {
50
50
  const input = document.createElement('input');
51
- input.className = 'cm-task-checkbox ch-checkbox ch-focus-ring -mbs-0.5';
51
+ input.className = 'cm-task-checkbox';
52
52
  input.type = 'checkbox';
53
53
  input.checked = this._checked;
54
54
  if (view.state.readOnly) {
@@ -148,7 +148,9 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
148
148
  marks[1].from,
149
149
  node.to,
150
150
  options.renderLinkButton
151
- ? Decoration.replace({ widget: new LinkButton(url, options.renderLinkButton) })
151
+ ? Decoration.replace({
152
+ widget: new LinkButton(url, options.renderLinkButton),
153
+ })
152
154
  : hide,
153
155
  );
154
156
  }
@@ -161,7 +161,7 @@ export const setStyle =
161
161
  ({ state, dispatch }) => {
162
162
  const marker = inlineMarkerText(type);
163
163
  const changes = state.changeByRange((range) => {
164
- // Special case for markers directly around the cursor, which will often not be parsed as valid styling.
164
+ // Special case for markers directly around the cursor, which will often not be parsed as valid styling
165
165
  if (!enable && range.empty) {
166
166
  const after = state.doc.sliceString(range.head, range.head + 6);
167
167
  const found = after.indexOf(marker);
@@ -190,16 +190,16 @@ export const setStyle =
190
190
  let startCovered: boolean | { from: number; to: number } = false;
191
191
  let endCovered: boolean | { from: number; to: number } = false;
192
192
  let { from, to } = range;
193
-
194
- // 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.
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.
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,7 +243,8 @@ export const setStyle =
243
243
  : true;
244
244
  }
245
245
  }
246
- // Marks of the same type in range, or any mark if we're adding code style, need to be removed.
246
+ // Marks of the same type in range, or any mark if we're
247
+ // adding code style, need to be removed.
247
248
  if (markType === type || (type === Inline.Code && enable)) {
248
249
  if (node.from >= from && openEnd <= to) {
249
250
  changes.push({ from: node.from, to: openEnd });
@@ -270,7 +271,7 @@ export const setStyle =
270
271
  },
271
272
  leave: (node) => {
272
273
  if (Object.hasOwn(Textblocks, node.name) && Textblocks[node.name] !== 'codeblock') {
273
- // Finish opening/closing the marks for this textblock.
274
+ // Finish opening/closing the marks for this textblock
274
275
  const rangeStart = Math.max(from, blockStart);
275
276
  const rangeEnd = Math.min(to, blockEnd);
276
277
  if (enable) {
@@ -295,14 +296,12 @@ export const setStyle =
295
296
  }
296
297
  },
297
298
  });
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
-
306
305
  const changeSet = state.changes(changes.concat(changesAtEnd));
307
306
  return {
308
307
  changes: changeSet,
@@ -314,12 +313,8 @@ export const setStyle =
314
313
  });
315
314
 
316
315
  dispatch(
317
- state.update(changes, {
318
- userEvent: enable ? 'format.style.add' : 'format.style.remove',
319
- scrollIntoView: true,
320
- }),
316
+ state.update(changes, { userEvent: enable ? 'format.style.add' : 'format.style.remove', scrollIntoView: true }),
321
317
  );
322
-
323
318
  return true;
324
319
  };
325
320
 
@@ -992,7 +987,7 @@ export const toggleCodeblock: StateCommand = (target) => {
992
987
 
993
988
  export type FormattingOptions = {};
994
989
 
995
- export const formattingKeymap = (options: FormattingOptions = {}): Extension => {
990
+ export const formatting = (options: FormattingOptions = {}): Extension => {
996
991
  return [
997
992
  keymap.of([
998
993
  {
@@ -61,7 +61,6 @@ 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.
65
64
  preloadImage(url);
66
65
  decorations.push(
67
66
  Decoration.replace({
@@ -3,14 +3,7 @@
3
3
  //
4
4
 
5
5
  import { syntaxTree } from '@codemirror/language';
6
- import {
7
- type EditorState,
8
- type Extension,
9
- type RangeSet,
10
- RangeSetBuilder,
11
- StateField,
12
- type Transaction,
13
- } from '@codemirror/state';
6
+ import { type EditorState, type RangeSet, RangeSetBuilder, StateField, type Transaction } from '@codemirror/state';
14
7
  import { Decoration, EditorView, WidgetType } from '@codemirror/view';
15
8
 
16
9
  // TODO(burdon): Snippet to create basic table.
@@ -25,7 +18,7 @@ export type TableOptions = {};
25
18
  * GFM tables.
26
19
  * https://github.github.com/gfm/#tables-extension
27
20
  */
28
- export const table = (options: TableOptions = {}): Extension => {
21
+ export const table = (options: TableOptions = {}) => {
29
22
  return StateField.define<RangeSet<any>>({
30
23
  create: (state) => update(state, options),
31
24
  update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
@@ -83,7 +76,7 @@ const update = (state: EditorState, options: TableOptions) => {
83
76
 
84
77
  tables.forEach((table) => {
85
78
  const hide = state.readOnly || cursor < table.from || cursor > table.to;
86
- if (hide) {
79
+ hide &&
87
80
  builder.add(
88
81
  table.from,
89
82
  table.to,
@@ -91,7 +84,6 @@ const update = (state: EditorState, options: TableOptions) => {
91
84
  widget: new TableWidget(table),
92
85
  }),
93
86
  );
94
- }
95
87
 
96
88
  // Add class for styling.
97
89
  builder.add(table.from, table.to, Decoration.mark({ class: 'cm-table' }));
@@ -0,0 +1,12 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Extension } from '@codemirror/state';
6
+
7
+ export type OutlinerOptions = {};
8
+
9
+ // TODO(burdon): Experimental component: multiple hierarchical draggable editor sections.
10
+ export const outliner = (options: OutlinerOptions = {}): Extension => {
11
+ return [];
12
+ };
@@ -9,7 +9,7 @@ import { log } from '@dxos/log';
9
9
  import { type Space } from '@dxos/react-client/echo';
10
10
  import { type GossipMessage } from '@dxos/react-client/mesh';
11
11
 
12
- import { type AwarenessInfo, type AwarenessPosition, type AwarenessProvider, type AwarenessState } from './awareness';
12
+ import { type AwarenessInfo, type AwarenessPosition, type AwarenessProvider, type AwarenessState } from '../extensions';
13
13
 
14
14
  type ProtocolMessage =
15
15
  | {
@@ -0,0 +1,30 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Extension } from '@codemirror/state';
6
+
7
+ import { type DocAccessor } from '@dxos/echo-schema';
8
+
9
+ // Runtime data structure.
10
+ export type Range = {
11
+ from: number;
12
+ to: number;
13
+ };
14
+
15
+ // Persistent data structure.
16
+ export type Comment = {
17
+ id: string;
18
+ cursor?: string;
19
+ };
20
+
21
+ /**
22
+ * @deprecated
23
+ */
24
+ // TODO(burdon): Remove.
25
+ export type EditorModel = {
26
+ id: string;
27
+ text: () => string;
28
+ content: string | DocAccessor;
29
+ extension?: Extension;
30
+ };
@@ -2,6 +2,10 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ export * from './awareness-provider';
6
+ export * from './defs';
7
+
5
8
  export * from './useActionHandler';
6
- export * from './useDocAccessor';
9
+ export * from './useEditorView';
7
10
  export * from './useTextEditor';
11
+ export * from './useTextModel';
@@ -0,0 +1,32 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type EditorView } from '@codemirror/view';
6
+ import { type MutableRefObject, useRef, useState } from 'react';
7
+
8
+ /**
9
+ * Hook for accessing the editor view.
10
+ * @deprecated
11
+ *
12
+ * ```tsx
13
+ * const Test = () => {
14
+ * const [editorRef, viewInvalidated] = useEditorView();
15
+ * useEffect(() => {
16
+ * if (editorRef.current && !viewInvalidated) {
17
+ * editorView?.focus();
18
+ * }
19
+ * }, [editorView]);
20
+ * return <TextEditor ref={editorRef} />;
21
+ * };
22
+ * ```
23
+ */
24
+ export const useEditorView = (id: string): [MutableRefObject<EditorView | null>, boolean] => {
25
+ const editorRef = useRef<EditorView | null>(null);
26
+ const [prev, setPrev] = useState<[EditorView | null, string]>([null, '']);
27
+ if (prev[0] !== editorRef.current) {
28
+ setPrev([editorRef.current, id]);
29
+ }
30
+
31
+ return [editorRef, prev[1] !== id];
32
+ };
@@ -4,28 +4,34 @@
4
4
 
5
5
  import '@dxosTheme';
6
6
 
7
- import React, { useState } from 'react';
7
+ import React, { useMemo, 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 { useTextEditor } from './useTextEditor';
14
+ import { createBasicExtensions, createThemeExtensions, useTextEditor } from './useTextEditor';
15
15
  import { Toolbar } from '../components';
16
- import { createBasicExtensions, createThemeExtensions } from '../extensions';
17
16
  import {
18
17
  type EditorMode,
19
18
  EditorModes,
20
19
  decorateMarkdown,
21
20
  createMarkdownExtensions,
22
- formattingKeymap,
21
+ formatting,
22
+ image,
23
23
  table,
24
24
  useFormattingState,
25
- image,
26
25
  } 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
+
29
35
  type StoryProps = {
30
36
  autoFocus?: boolean;
31
37
  placeholder?: string;
@@ -37,24 +43,29 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
37
43
  const { themeMode } = useThemeContext();
38
44
  const [formattingState, trackFormatting] = useFormattingState();
39
45
  const [editorMode, setEditorMode] = useState<EditorMode>('default');
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],
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],
57
67
  );
68
+ const { parentRef, view } = useTextEditor({ autoFocus, doc, extensions });
58
69
 
59
70
  const handleAction = useActionHandler(view);
60
71
 
@@ -105,6 +116,7 @@ const EditorModeToolbar = ({
105
116
 
106
117
  export default {
107
118
  title: 'react-ui-editor/useTextEditor',
119
+ // component: TextEditor,
108
120
  decorators: [withTheme],
109
121
  render: (args: StoryProps) => (
110
122
  <Tooltip.Provider>
@@ -115,23 +127,6 @@ export default {
115
127
  };
116
128
 
117
129
  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 = {
135
130
  args: {
136
131
  autoFocus: true,
137
132
  placeholder: 'Text...',