@dxos/react-ui-editor 0.4.7-main.733903e → 0.4.7-main.76c1dea

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 (89) hide show
  1. package/dist/lib/browser/index.mjs +997 -728
  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 +9 -0
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  7. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +0 -9
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  9. package/dist/types/src/components/TextEditor/index.d.ts +0 -2
  10. package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
  11. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +1 -0
  12. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
  13. package/dist/types/src/{components/TextEditor → extensions/automerge}/automerge.stories.d.ts +5 -2
  14. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -0
  15. package/dist/types/src/extensions/command/command.d.ts +10 -0
  16. package/dist/types/src/extensions/command/command.d.ts.map +1 -0
  17. package/dist/types/src/extensions/command/hint.d.ts +7 -0
  18. package/dist/types/src/extensions/command/hint.d.ts.map +1 -0
  19. package/dist/types/src/extensions/command/index.d.ts +2 -0
  20. package/dist/types/src/extensions/command/index.d.ts.map +1 -0
  21. package/dist/types/src/extensions/command/state.d.ts +18 -0
  22. package/dist/types/src/extensions/command/state.d.ts.map +1 -0
  23. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  24. package/dist/types/src/extensions/cursor-line-margin.d.ts.map +1 -1
  25. package/dist/types/src/extensions/dnd.d.ts +3 -0
  26. package/dist/types/src/extensions/dnd.d.ts.map +1 -0
  27. package/dist/types/src/extensions/index.d.ts +2 -1
  28. package/dist/types/src/extensions/index.d.ts.map +1 -1
  29. package/dist/types/src/extensions/listener.d.ts +1 -1
  30. package/dist/types/src/extensions/listener.d.ts.map +1 -1
  31. package/dist/types/src/extensions/markdown/bundle.d.ts +2 -2
  32. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  33. package/dist/types/src/extensions/markdown/decorate.d.ts +4 -0
  34. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  35. package/dist/types/src/extensions/mention.d.ts.map +1 -1
  36. package/dist/types/src/extensions/util/dom.d.ts +15 -0
  37. package/dist/types/src/extensions/util/dom.d.ts.map +1 -0
  38. package/dist/types/src/hooks/defs.d.ts +5 -11
  39. package/dist/types/src/hooks/defs.d.ts.map +1 -1
  40. package/dist/types/src/hooks/useEditorView.d.ts.map +1 -1
  41. package/dist/types/src/hooks/useTextEditor.d.ts +20 -5
  42. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  43. package/dist/types/src/{components/TextEditor/hooks.stories.d.ts → hooks/useTextEditor.stories.d.ts} +2 -3
  44. package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -0
  45. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  46. package/dist/types/src/index.d.ts +1 -0
  47. package/dist/types/src/index.d.ts.map +1 -1
  48. package/dist/types/src/styles/index.d.ts +0 -1
  49. package/dist/types/src/styles/index.d.ts.map +1 -1
  50. package/dist/types/src/themes/default.d.ts.map +1 -1
  51. package/package.json +31 -31
  52. package/src/components/TextEditor/MarkdownEditor.stories.tsx +95 -8
  53. package/src/components/TextEditor/TextEditor.stories.tsx +0 -15
  54. package/src/components/TextEditor/TextEditor.tsx +13 -6
  55. package/src/components/TextEditor/index.ts +0 -3
  56. package/src/components/Toolbar/Toolbar.stories.tsx +2 -1
  57. package/src/{components/TextEditor → extensions/automerge}/automerge.stories.tsx +32 -37
  58. package/src/extensions/awareness.ts +3 -3
  59. package/src/extensions/command/command.ts +35 -0
  60. package/src/extensions/command/hint.ts +76 -0
  61. package/src/extensions/command/index.ts +5 -0
  62. package/src/extensions/command/state.ts +106 -0
  63. package/src/extensions/comments.ts +2 -1
  64. package/src/extensions/cursor-line-margin.ts +5 -2
  65. package/src/extensions/dnd.ts +17 -0
  66. package/src/extensions/index.ts +2 -1
  67. package/src/extensions/listener.ts +3 -3
  68. package/src/extensions/markdown/bundle.ts +4 -15
  69. package/src/extensions/markdown/decorate.ts +76 -32
  70. package/src/extensions/mention.ts +3 -1
  71. package/src/extensions/util/dom.ts +36 -0
  72. package/src/hooks/defs.ts +6 -20
  73. package/src/hooks/useActionHandler.ts +11 -11
  74. package/src/hooks/useEditorView.ts +0 -1
  75. package/src/{components/TextEditor/hooks.stories.tsx → hooks/useTextEditor.stories.tsx} +8 -8
  76. package/src/hooks/useTextEditor.ts +69 -14
  77. package/src/hooks/useTextModel.ts +20 -18
  78. package/src/index.ts +2 -0
  79. package/src/styles/index.ts +0 -1
  80. package/src/themes/default.ts +1 -0
  81. package/src/util.ts +1 -1
  82. package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +0 -1
  83. package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +0 -1
  84. package/dist/types/src/extensions/basic.d.ts +0 -9
  85. package/dist/types/src/extensions/basic.d.ts.map +0 -1
  86. package/dist/types/src/styles/cursors.d.ts +0 -5
  87. package/dist/types/src/styles/cursors.d.ts.map +0 -1
  88. package/src/extensions/basic.ts +0 -41
  89. package/src/styles/cursors.ts +0 -21
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { syntaxTree } from '@codemirror/language';
6
- import { RangeSetBuilder, type EditorState } from '@codemirror/state';
6
+ import { RangeSetBuilder, type EditorState, StateEffect } from '@codemirror/state';
7
7
  import { EditorView, Decoration, type DecorationSet, WidgetType, ViewPlugin, type ViewUpdate } from '@codemirror/view';
8
8
 
9
9
  import { mx } from '@dxos/react-ui-theme';
@@ -81,20 +81,21 @@ const horizontalRule = Decoration.replace({ widget: new HorizontalRuleWidget() }
81
81
  const checkedTask = Decoration.replace({ widget: new CheckboxWidget(true) });
82
82
  const uncheckedTask = Decoration.replace({ widget: new CheckboxWidget(false) });
83
83
 
84
- const editingRange = (state: EditorState, range: { from: number; to: number }) => {
84
+ const editingRange = (state: EditorState, range: { from: number; to: number }, focus: boolean) => {
85
85
  const {
86
86
  readOnly,
87
87
  selection: {
88
88
  main: { head },
89
89
  },
90
90
  } = state;
91
- return !readOnly && head >= range.from && head <= range.to;
91
+ return focus && !readOnly && head >= range.from && head <= range.to;
92
92
  };
93
93
 
94
94
  const MarksByParent = new Set(['CodeMark', 'EmphasisMark', 'StrikethroughMark', 'SubscriptMark', 'SuperscriptMark']);
95
95
 
96
- const buildDecorations = (view: EditorView, options: DecorateOptions): DecorationSet => {
97
- const builder = new RangeSetBuilder<Decoration>();
96
+ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boolean) => {
97
+ const deco = new RangeSetBuilder<Decoration>();
98
+ const atomicDeco = new RangeSetBuilder<Decoration>();
98
99
  const { state } = view;
99
100
 
100
101
  for (const { from, to } of view.visibleRanges) {
@@ -103,7 +104,7 @@ const buildDecorations = (view: EditorView, options: DecorateOptions): Decoratio
103
104
  to,
104
105
  enter: (node) => {
105
106
  if (node.name === 'FencedCode') {
106
- const editing = editingRange(state, node);
107
+ const editing = editingRange(state, node, focus);
107
108
  // FencedCode > CodeMark > [CodeInfo] > CodeText > CodeMark
108
109
  for (const block of view.viewportLineBlocks) {
109
110
  if (block.to < node.from) {
@@ -113,27 +114,23 @@ const buildDecorations = (view: EditorView, options: DecorateOptions): Decoratio
113
114
  break;
114
115
  }
115
116
  const first = block.from <= node.from;
116
- const last = block.to >= node.to;
117
- builder.add(
118
- block.from,
119
- block.from,
120
- first ? fencedCodeLineFirst : last ? fencedCodeLineLast : fencedCodeLine,
121
- );
117
+ const last = block.to >= node.to && /^(\s>)*```$/.test(state.doc.sliceString(block.from, block.to));
118
+ deco.add(block.from, block.from, first ? fencedCodeLineFirst : last ? fencedCodeLineLast : fencedCodeLine);
122
119
  if (!editing && (first || last)) {
123
- builder.add(block.from, block.to, hide);
120
+ atomicDeco.add(block.from, block.to, hide);
124
121
  }
125
122
  }
126
123
  return false;
127
124
  } else if (node.name === 'Link') {
128
125
  const marks = node.node.getChildren('LinkMark');
129
126
  const urlNode = node.node.getChild('URL');
130
- const editing = editingRange(state, node);
127
+ const editing = editingRange(state, node, focus);
131
128
  if (urlNode && marks.length >= 2) {
132
129
  const url = state.sliceDoc(urlNode.from, urlNode.to);
133
130
  if (!editing) {
134
- builder.add(node.from, marks[0].to, hide);
131
+ atomicDeco.add(node.from, marks[0].to, hide);
135
132
  }
136
- builder.add(
133
+ deco.add(
137
134
  marks[0].to,
138
135
  marks[1].from,
139
136
  Decoration.mark({
@@ -147,7 +144,7 @@ const buildDecorations = (view: EditorView, options: DecorateOptions): Decoratio
147
144
  }),
148
145
  );
149
146
  if (!editing) {
150
- builder.add(
147
+ atomicDeco.add(
151
148
  marks[1].from,
152
149
  node.to,
153
150
  options.renderLinkButton
@@ -160,54 +157,101 @@ const buildDecorations = (view: EditorView, options: DecorateOptions): Decoratio
160
157
  }
161
158
  } else if (node.name === 'HeaderMark') {
162
159
  const parent = node.node.parent!;
163
- if (/^ATX/.test(parent.name) && !editingRange(state, state.doc.lineAt(node.from))) {
160
+ if (/^ATX/.test(parent.name) && !editingRange(state, state.doc.lineAt(node.from), focus)) {
164
161
  const next = state.doc.sliceString(node.to, node.to + 1);
165
- builder.add(node.from, node.to + (next === ' ' ? 1 : 0), hide);
162
+ atomicDeco.add(node.from, node.to + (next === ' ' ? 1 : 0), hide);
166
163
  }
167
164
  } else if (node.name === 'HorizontalRule') {
168
- if (!editingRange(state, node)) {
169
- builder.add(node.from, node.to, horizontalRule);
165
+ if (!editingRange(state, node, focus)) {
166
+ deco.add(node.from, node.to, horizontalRule);
170
167
  }
171
168
  } else if (node.name === 'TaskMarker') {
172
169
  // Check if cursor is inside text.
173
- if (!editingRange(state, node)) {
170
+ if (!editingRange(state, node, focus)) {
174
171
  const checked = state.doc.sliceString(node.from + 1, node.to - 1) === 'x';
175
- builder.add(node.from - 2, node.from - 1, Decoration.mark({ class: 'cm-task' }));
176
- builder.add(node.from, node.to, checked ? checkedTask : uncheckedTask);
172
+ atomicDeco.add(node.from - 2, node.from - 1, Decoration.mark({ class: 'cm-task' }));
173
+ atomicDeco.add(node.from, node.to, checked ? checkedTask : uncheckedTask);
177
174
  }
178
175
  } else if (MarksByParent.has(node.name)) {
179
- if (!editingRange(state, node.node.parent!)) {
180
- builder.add(node.from, node.to, hide);
176
+ if (!editingRange(state, node.node.parent!, focus)) {
177
+ atomicDeco.add(node.from, node.to, hide);
181
178
  }
182
179
  }
183
180
  },
184
181
  });
185
182
  }
186
183
 
187
- return builder.finish();
184
+ return { deco: deco.finish(), atomicDeco: atomicDeco.finish() };
188
185
  };
189
186
 
190
187
  export interface DecorateOptions {
191
188
  renderLinkButton?: (el: Element, url: string) => void;
189
+ /**
190
+ * Prevents triggering decorations as the cursor moves through the document.
191
+ */
192
+ selectionChangeDelay?: number;
192
193
  }
193
194
 
195
+ const forceUpdate = StateEffect.define<null>();
196
+
194
197
  export const decorateMarkdown = (options: DecorateOptions = {}) => {
195
198
  return [
196
199
  ViewPlugin.fromClass(
197
200
  class {
198
- decorations: DecorationSet;
201
+ deco: DecorationSet;
202
+ atomicDeco: DecorationSet;
203
+ pendingUpdate?: NodeJS.Timeout;
204
+
199
205
  constructor(view: EditorView) {
200
- this.decorations = buildDecorations(view, options);
206
+ ({ deco: this.deco, atomicDeco: this.atomicDeco } = buildDecorations(view, options, view.hasFocus));
201
207
  }
202
208
 
203
209
  update(update: ViewUpdate) {
204
- if (update.docChanged || update.selectionSet || update.viewportChanged) {
205
- this.decorations = buildDecorations(update.view, options);
210
+ if (
211
+ update.docChanged ||
212
+ update.viewportChanged ||
213
+ update.focusChanged ||
214
+ update.transactions.some((tr) => tr.effects.some((e) => e.is(forceUpdate))) ||
215
+ (update.selectionSet && !options.selectionChangeDelay)
216
+ ) {
217
+ ({ deco: this.deco, atomicDeco: this.atomicDeco } = buildDecorations(
218
+ update.view,
219
+ options,
220
+ update.view.hasFocus,
221
+ ));
222
+
223
+ this.clearUpdate();
224
+ } else if (update.selectionSet) {
225
+ this.scheduleUpdate(update.view);
206
226
  }
207
227
  }
228
+
229
+ // TODO(burdon): BUG: If the cursor is at the end of a link at the end of a line,
230
+ // the cursor will float in space or be in the wrong position after the decoration is applied.
231
+ scheduleUpdate(view: EditorView) {
232
+ this.clearUpdate();
233
+ this.pendingUpdate = setTimeout(() => {
234
+ view.dispatch({ effects: forceUpdate.of(null) });
235
+ }, options.selectionChangeDelay);
236
+ }
237
+
238
+ clearUpdate() {
239
+ if (this.pendingUpdate) {
240
+ clearTimeout(this.pendingUpdate);
241
+ this.pendingUpdate = undefined;
242
+ }
243
+ }
244
+
245
+ destroy() {
246
+ this.clearUpdate();
247
+ }
208
248
  },
209
249
  {
210
- decorations: (value) => value.decorations,
250
+ provide: (plugin) => [
251
+ EditorView.atomicRanges.of((view) => view.plugin(plugin)?.atomicDeco ?? Decoration.none),
252
+ EditorView.decorations.of((view) => view.plugin(plugin)?.atomicDeco ?? Decoration.none),
253
+ EditorView.decorations.of((view) => view.plugin(plugin)?.deco ?? Decoration.none),
254
+ ],
211
255
  },
212
256
  ),
213
257
  formattingStyles,
@@ -5,6 +5,8 @@
5
5
  import { autocompletion, type CompletionContext, type CompletionResult } from '@codemirror/autocomplete';
6
6
  import type { Extension } from '@codemirror/state';
7
7
 
8
+ import { log } from '@dxos/log';
9
+
8
10
  export type MentionOptions = {
9
11
  onSearch: (text: string) => string[];
10
12
  };
@@ -21,7 +23,7 @@ export const mention = ({ onSearch }: MentionOptions): Extension => {
21
23
  icons: false,
22
24
  override: [
23
25
  (context: CompletionContext): CompletionResult | null => {
24
- console.log(context);
26
+ log.info('completion context', { context });
25
27
 
26
28
  const match = context.matchBefore(/@(\w+)?/);
27
29
  if (!match || (match.from === match.to && !context.explicit)) {
@@ -0,0 +1,36 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ // Copied from @codemirror/view
6
+
7
+ export interface Rect {
8
+ readonly left: number;
9
+ readonly right: number;
10
+ readonly top: number;
11
+ readonly bottom: number;
12
+ }
13
+
14
+ export const flattenRect = (rect: Rect, left: boolean) => {
15
+ const x = left ? rect.left : rect.right;
16
+ return { left: x, right: x, top: rect.top, bottom: rect.bottom };
17
+ };
18
+
19
+ let scratchRange: Range | null;
20
+
21
+ export const textRange = (node: Text, from: number, to = from) => {
22
+ const range = scratchRange || (scratchRange = document.createRange());
23
+ range.setEnd(node, to);
24
+ range.setStart(node, from);
25
+ return range;
26
+ };
27
+
28
+ export const clientRectsFor = (dom: Node) => {
29
+ if (dom.nodeType === 3) {
30
+ return textRange(dom as Text, 0, dom.nodeValue!.length).getClientRects();
31
+ } else if (dom.nodeType === 1) {
32
+ return (dom as HTMLElement).getClientRects();
33
+ } else {
34
+ return [] as any as DOMRectList;
35
+ }
36
+ };
package/src/hooks/defs.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type Extension, StateField } from '@codemirror/state';
5
+ import { type Extension } from '@codemirror/state';
6
6
 
7
7
  import { type DocAccessor } from '@dxos/echo-schema';
8
8
 
@@ -18,27 +18,13 @@ export type Comment = {
18
18
  cursor?: string;
19
19
  };
20
20
 
21
+ /**
22
+ * @deprecated
23
+ */
24
+ // TODO(burdon): Remove.
21
25
  export type EditorModel = {
22
26
  id: string;
23
27
  text: () => string;
24
- extension?: Extension;
25
-
26
- // TODO(burdon): Remove.
27
28
  content: string | DocAccessor;
28
-
29
- // TODO(burdon): Remove.
30
- peer?: {
31
- id: string;
32
- name?: string;
33
- };
29
+ extension?: Extension;
34
30
  };
35
-
36
- /**
37
- * State field makes the model available to other extensions.
38
- * @deprecated
39
- */
40
- // TODO(burdon): Remove.
41
- export const modelState = StateField.define<EditorModel | undefined>({
42
- create: () => undefined,
43
- update: (model) => model,
44
- });
@@ -6,23 +6,23 @@ import type { EditorView } from '@codemirror/view';
6
6
 
7
7
  import type { ToolbarProps } from '../components';
8
8
  import {
9
- createComment,
10
- insertTable,
11
- setHeading,
12
- setStyle,
13
- toggleStyle,
14
- addLink,
15
- removeLink,
16
9
  Inline,
17
10
  List,
18
- addList,
19
- removeList,
20
- toggleList,
21
11
  addBlockquote,
22
- removeBlockquote,
23
12
  addCodeblock,
13
+ addLink,
14
+ addList,
15
+ createComment,
16
+ insertTable,
17
+ removeBlockquote,
24
18
  removeCodeblock,
19
+ removeLink,
20
+ removeList,
21
+ setHeading,
22
+ setStyle,
25
23
  toggleBlockquote,
24
+ toggleList,
25
+ toggleStyle,
26
26
  } from '../extensions';
27
27
 
28
28
  export const useActionHandler = (view?: EditorView | null): ToolbarProps['onAction'] => {
@@ -21,7 +21,6 @@ import { type MutableRefObject, useRef, useState } from 'react';
21
21
  * };
22
22
  * ```
23
23
  */
24
- // TODO(burdon): Replace with useTextEditor.
25
24
  export const useEditorView = (id: string): [MutableRefObject<EditorView | null>, boolean] => {
26
25
  const editorRef = useRef<EditorView | null>(null);
27
26
  const [prev, setPrev] = useState<[EditorView | null, string]>([null, '']);
@@ -10,7 +10,9 @@ import { Toolbar as NaturalToolbar, Select, useThemeContext, Tooltip } from '@dx
10
10
  import { attentionSurface, mx, textBlockWidth } from '@dxos/react-ui-theme';
11
11
  import { withTheme } from '@dxos/storybook-utils';
12
12
 
13
- import { TextEditor } from './TextEditor';
13
+ import { useActionHandler } from './useActionHandler';
14
+ import { createBasicExtensions, createThemeExtensions, useTextEditor } from './useTextEditor';
15
+ import { Toolbar } from '../components';
14
16
  import {
15
17
  type EditorMode,
16
18
  EditorModes,
@@ -20,11 +22,9 @@ import {
20
22
  image,
21
23
  table,
22
24
  useFormattingState,
23
- } from '../../extensions';
24
- import { createDataExtensions, createThemeExtensions, useActionHandler, useTextEditor } from '../../hooks';
25
- import { markdownTheme } from '../../themes';
26
- import translations from '../../translations';
27
- import { Toolbar } from '../Toolbar';
25
+ } from '../extensions';
26
+ import { markdownTheme } from '../themes';
27
+ import translations from '../translations';
28
28
 
29
29
  // TODO(burdon): Demo toolbar with hooks.
30
30
  // TODO(burdon): Build components from hooks and adapters for model/extensions, etc.
@@ -46,7 +46,7 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
46
46
  const extensions = useMemo(
47
47
  () => [
48
48
  editorMode ? EditorModes[editorMode] : [],
49
- createDataExtensions({ readonly }),
49
+ createBasicExtensions({ readonly }),
50
50
  createThemeExtensions({
51
51
  themeMode,
52
52
  theme: markdownTheme,
@@ -116,7 +116,7 @@ const EditorModeToolbar = ({
116
116
 
117
117
  export default {
118
118
  title: 'react-ui-editor/useTextEditor',
119
- component: TextEditor,
119
+ // component: TextEditor,
120
120
  decorators: [withTheme],
121
121
  render: (args: StoryProps) => (
122
122
  <Tooltip.Provider>
@@ -2,6 +2,9 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { closeBrackets } from '@codemirror/autocomplete';
6
+ import { history } from '@codemirror/commands';
7
+ import { bracketMatching } from '@codemirror/language';
5
8
  import {
6
9
  EditorSelection,
7
10
  EditorState,
@@ -9,7 +12,17 @@ import {
9
12
  type Extension,
10
13
  type StateEffect,
11
14
  } from '@codemirror/state';
12
- import { EditorView } from '@codemirror/view';
15
+ import {
16
+ crosshairCursor,
17
+ drawSelection,
18
+ dropCursor,
19
+ EditorView,
20
+ highlightActiveLine,
21
+ lineNumbers,
22
+ placeholder,
23
+ scrollPastEnd,
24
+ } from '@codemirror/view';
25
+ import defaultsDeep from 'lodash.defaultsdeep';
13
26
  import { type DependencyList, type RefObject, useEffect, useRef, useState } from 'react';
14
27
 
15
28
  import { log } from '@dxos/log';
@@ -38,7 +51,7 @@ export type UseTextEditor = {
38
51
  // TODO(wittjosiah): Does not work in strict mode.
39
52
  export const useTextEditor = (
40
53
  { autoFocus, scrollTo, debug, doc, selection, extensions }: UseTextEditorOptions = {},
41
- deps?: DependencyList,
54
+ deps: DependencyList = [],
42
55
  ): UseTextEditor => {
43
56
  const onUpdate = useRef<() => void>();
44
57
  const [view, setView] = useState<EditorView>();
@@ -114,23 +127,66 @@ export const useTextEditor = (
114
127
 
115
128
  // TODO(burdon): Factor out extension factories.
116
129
 
117
- export type DataExtensionsOptions = {
130
+ /**
131
+ * https://codemirror.net/docs/extensions
132
+ * https://github.com/codemirror/basic-setup
133
+ */
134
+ // TODO(burdon): Reconcile with createMarkdownExtensions.
135
+ export type BasicExtensionsOptions = {
136
+ allowMultipleSelections?: boolean;
137
+ bracketMatching?: boolean;
138
+ closeBrackets?: boolean;
139
+ crosshairCursor?: boolean;
140
+ dropCursor?: boolean;
141
+ drawSelection?: boolean;
142
+ editable?: boolean;
143
+ highlightActiveLine?: boolean;
144
+ history?: boolean;
145
+ lineNumbers?: boolean;
146
+ lineWrapping?: boolean;
147
+ placeholder?: string;
118
148
  readonly?: boolean;
149
+ scrollPastEnd?: boolean;
150
+ tabSize?: number;
151
+ };
152
+
153
+ const defaults: BasicExtensionsOptions = {
154
+ bracketMatching: true,
155
+ closeBrackets: true,
156
+ drawSelection: true,
157
+ editable: true,
158
+ history: true,
159
+ lineWrapping: true,
119
160
  };
120
161
 
121
- // TODO(burdon): Pass in TextObject (remove model).
122
- export const createDataExtensions = ({ readonly = false } = {}): Extension => {
162
+ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extension => {
163
+ const props: BasicExtensionsOptions = defaultsDeep({}, _props, defaults);
123
164
  return [
124
- //
125
- EditorState.readOnly.of(readonly),
126
- EditorView.editable.of(!readonly),
127
- ];
165
+ // TODO(burdon): Doesn't catch errors in keymap functions.
166
+ EditorView.exceptionSink.of((err) => {
167
+ log.catch(err);
168
+ }),
169
+
170
+ props.allowMultipleSelections && EditorState.allowMultipleSelections.of(true),
171
+ props.bracketMatching && bracketMatching(),
172
+ props.closeBrackets && closeBrackets(),
173
+ props.crosshairCursor && crosshairCursor(),
174
+ props.dropCursor && dropCursor(),
175
+ props.drawSelection && drawSelection(),
176
+ props.highlightActiveLine && highlightActiveLine(),
177
+ props.history && history(),
178
+ props.lineNumbers && lineNumbers(),
179
+ props.lineWrapping && EditorView.lineWrapping,
180
+ props.placeholder && placeholder(props.placeholder),
181
+ props.readonly && [EditorState.readOnly.of(true), EditorView.editable.of(false)],
182
+ props.scrollPastEnd && scrollPastEnd(),
183
+ props.tabSize && EditorState.tabSize.of(props.tabSize),
184
+ ].filter(isNotFalsy);
128
185
  };
129
186
 
130
187
  export type ThemeExtensionsOptions = {
131
188
  theme?: ThemeStyles;
132
189
  themeMode?: ThemeMode;
133
- lineWrap?: boolean;
134
190
  slots?: {
135
191
  editor?: {
136
192
  className?: string;
@@ -143,11 +199,10 @@ export type ThemeExtensionsOptions = {
143
199
 
144
200
  export const createThemeExtensions = ({ theme, themeMode, slots }: ThemeExtensionsOptions = {}): Extension => {
145
201
  return [
146
- //
147
202
  EditorView.baseTheme(defaultTheme),
148
- theme && EditorView.theme(theme),
149
203
  EditorView.darkTheme.of(themeMode === 'dark'),
150
- EditorView.editorAttributes.of({ class: slots?.editor?.className ?? '' }),
151
- EditorView.contentAttributes.of({ class: slots?.content?.className ?? '' }),
204
+ theme && EditorView.theme(theme),
205
+ slots?.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
206
+ slots?.content?.className && EditorView.contentAttributes.of({ class: slots.content.className }),
152
207
  ].filter(isNotFalsy);
153
208
  };
@@ -11,12 +11,13 @@ import { type AutomergeTextCompat, getRawDoc } from '@dxos/echo-schema';
11
11
  import { invariant } from '@dxos/invariant';
12
12
  import { isAutomergeObject, type Space, type TextObject } from '@dxos/react-client/echo';
13
13
  import { type Identity } from '@dxos/react-client/halo';
14
+ import { type HuePalette, hueTokens } from '@dxos/react-ui-theme';
15
+ import { hexToHue } from '@dxos/util';
14
16
 
15
17
  import { SpaceAwarenessProvider } from './awareness-provider';
16
- import { type EditorModel, modelState } from './defs';
18
+ import { type EditorModel } from './defs';
17
19
  import { automerge, awareness } from '../extensions';
18
20
  import { type DocAccessor } from '../extensions/automerge/defs';
19
- import { cursorColor } from '../styles';
20
21
 
21
22
  export type useTextExtensionsProps = {
22
23
  id: string;
@@ -29,6 +30,10 @@ export type useTextExtensionsProps = {
29
30
  export const useTextExtensions = ({ id, text, space, identity }: useTextExtensionsProps): Extension[] => {
30
31
  const extensions: Extension[] = [automerge(text)];
31
32
 
33
+ const peerId = identity?.identityKey.toHex();
34
+ const { cursorLightValue, cursorDarkValue } =
35
+ hueTokens[(identity?.profile?.data?.hue as HuePalette | undefined) ?? hexToHue(peerId ?? '0')];
36
+
32
37
  if (space && identity) {
33
38
  const awarenessProvider = new SpaceAwarenessProvider({
34
39
  space,
@@ -36,8 +41,8 @@ export const useTextExtensions = ({ id, text, space, identity }: useTextExtensio
36
41
  peerId: identity.identityKey.toHex(),
37
42
  info: {
38
43
  displayName: identity.profile?.displayName ?? generateName(identity.identityKey.toHex()),
39
- color: cursorColor.color,
40
- lightColor: cursorColor.light,
44
+ color: cursorDarkValue,
45
+ lightColor: cursorLightValue,
41
46
  },
42
47
  });
43
48
 
@@ -64,6 +69,7 @@ export const useTextModel = (props: UseTextModelProps): EditorModel | undefined
64
69
  * For use primarily in stories & tests so the dependence on TextObject can be avoided.
65
70
  * @deprecated
66
71
  */
72
+ // TODO(burdon): Remove.
67
73
  export const useInMemoryTextModel = ({
68
74
  id,
69
75
  defaultContent,
@@ -72,10 +78,10 @@ export const useInMemoryTextModel = ({
72
78
  defaultContent?: string;
73
79
  }): EditorModel & { setContent: Dispatch<SetStateAction<string>> } => {
74
80
  const [content, setContent] = useState(defaultContent ?? '');
75
- return { id, content, setContent, text: () => content };
81
+ return { id, content, text: () => content, setContent };
76
82
  };
77
83
 
78
- const createModel = ({ space, identity, text }: UseTextModelProps) => {
84
+ const createModel = ({ space, identity, text }: UseTextModelProps): EditorModel | undefined => {
79
85
  if (!text) {
80
86
  return undefined;
81
87
  }
@@ -84,6 +90,10 @@ const createModel = ({ space, identity, text }: UseTextModelProps) => {
84
90
  const obj = text as any as AutomergeTextCompat;
85
91
  const doc = getRawDoc(obj, [obj.field]);
86
92
 
93
+ const peerId = identity?.identityKey.toHex();
94
+ const { cursorLightValue, cursorDarkValue } =
95
+ hueTokens[(identity?.profile?.data?.hue as HuePalette | undefined) ?? hexToHue(peerId ?? '0')];
96
+
87
97
  const awarenessProvider =
88
98
  space &&
89
99
  new SpaceAwarenessProvider({
@@ -91,29 +101,21 @@ const createModel = ({ space, identity, text }: UseTextModelProps) => {
91
101
  channel: `automerge.awareness.${obj.id}`,
92
102
  info: {
93
103
  displayName: identity ? identity.profile?.displayName ?? generateName(identity.identityKey.toHex()) : undefined,
94
- color: cursorColor.color,
95
- lightColor: cursorColor.light,
104
+ color: cursorLightValue,
105
+ lightColor: cursorDarkValue,
96
106
  },
97
107
  peerId: identity?.identityKey.toHex() ?? 'Anonymous',
98
108
  });
99
109
 
100
- const extensions = [modelState.init(() => model), automerge({ handle: doc.handle, path: doc.path })];
110
+ const extensions = [automerge({ handle: doc.handle, path: doc.path })];
101
111
  if (awarenessProvider) {
102
112
  extensions.push(awareness(awarenessProvider));
103
113
  }
104
114
 
105
- const model: EditorModel = {
115
+ return {
106
116
  id: obj.id,
107
117
  content: doc,
108
118
  text: () => get(doc.handle.docSync(), doc.path),
109
119
  extension: extensions,
110
- peer: identity
111
- ? {
112
- id: identity.identityKey.toHex(),
113
- name: identity.profile?.displayName,
114
- }
115
- : undefined,
116
120
  };
117
-
118
- return model;
119
121
  };
package/src/index.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  //
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
+
4
5
  import translations from './translations';
5
6
 
6
7
  export { type Extension, type EditorState } from '@codemirror/state';
7
8
  export { type EditorView, keymap } from '@codemirror/view';
9
+ export { tags } from '@lezer/highlight';
8
10
 
9
11
  export { TextKind } from '@dxos/protocols/proto/dxos/echo/model/text';
10
12
  export { Doc, YText, YXmlFragment } from '@dxos/text-model';
@@ -2,7 +2,6 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export * from './cursors';
6
5
  export * from './markdown';
7
6
  export * from './tokens';
8
7
  export * from './layout';
@@ -152,6 +152,7 @@ export const defaultTheme: ThemeStyles = {
152
152
  //
153
153
  '.cm-tooltip': {
154
154
  border: 'none',
155
+ background: 'unset',
155
156
  },
156
157
  '.cm-tooltip-below': {},
157
158
 
package/src/util.ts CHANGED
@@ -36,6 +36,6 @@ export const logChanges = (trs: readonly Transaction[]) => {
36
36
  .filter(Boolean);
37
37
 
38
38
  if (changes.length) {
39
- console.log('changes', changes);
39
+ log.info('changes', { changes });
40
40
  }
41
41
  };
@@ -1 +0,0 @@
1
- {"version":3,"file":"automerge.stories.d.ts","sourceRoot":"","sources":["../../../../../src/components/TextEditor/automerge.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,YAAY,CAAC;AAGpB,OAAO,uBAAuB,CAAC;AAc/B,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAW9D,KAAK,WAAW,GAAG;IACjB,MAAM,EAAE,WAAW,CAAC;CACrB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DF,wBAKE;AAEF,eAAO,MAAM,OAAO,IAAK,CAAC;AAoB1B,eAAO,MAAM,QAAQ;;;CAmBpB,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"hooks.stories.d.ts","sourceRoot":"","sources":["../../../../../src/components/TextEditor/hooks.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,YAAY,CAAC;AAEpB,OAAO,KAA4B,MAAM,OAAO,CAAC;AA4BjD,KAAK,UAAU,GAAG;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;;;;;mBAiFe,UAAU;;;;;;;;;;;;;;;;;;;;;;;;AAJ3B,wBAUE;AAEF,eAAO,MAAM,OAAO;;;;;;CAMnB,CAAC"}