@dxos/react-ui-editor 0.8.2-main.fbd8ed0 → 0.8.2-staging.4d6ad0f

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 (180) hide show
  1. package/dist/lib/browser/index.mjs +1731 -926
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/browser/testing/index.mjs +3 -64
  5. package/dist/lib/browser/testing/index.mjs.map +4 -4
  6. package/dist/lib/node/index.cjs +1912 -1111
  7. package/dist/lib/node/index.cjs.map +4 -4
  8. package/dist/lib/node/meta.json +1 -1
  9. package/dist/lib/node/testing/index.cjs +3 -75
  10. package/dist/lib/node/testing/index.cjs.map +4 -4
  11. package/dist/lib/node-esm/index.mjs +1731 -926
  12. package/dist/lib/node-esm/index.mjs.map +4 -4
  13. package/dist/lib/node-esm/meta.json +1 -1
  14. package/dist/lib/node-esm/testing/index.mjs +3 -64
  15. package/dist/lib/node-esm/testing/index.mjs.map +4 -4
  16. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
  17. package/dist/types/src/components/EditorToolbar/index.d.ts +1 -1
  18. package/dist/types/src/components/EditorToolbar/index.d.ts.map +1 -1
  19. package/dist/types/src/components/EditorToolbar/util.d.ts +4 -6
  20. package/dist/types/src/components/EditorToolbar/util.d.ts.map +1 -1
  21. package/dist/types/src/components/Popover/RefDropdownMenu.d.ts +21 -0
  22. package/dist/types/src/components/Popover/RefDropdownMenu.d.ts.map +1 -0
  23. package/dist/types/src/{testing → components/Popover}/RefPopover.d.ts +1 -1
  24. package/dist/types/src/components/Popover/RefPopover.d.ts.map +1 -0
  25. package/dist/types/src/components/Popover/index.d.ts +3 -0
  26. package/dist/types/src/components/Popover/index.d.ts.map +1 -0
  27. package/dist/types/src/components/index.d.ts +1 -0
  28. package/dist/types/src/components/index.d.ts.map +1 -1
  29. package/dist/types/src/defaults.d.ts +2 -5
  30. package/dist/types/src/defaults.d.ts.map +1 -1
  31. package/dist/types/src/extensions/annotations.d.ts +4 -1
  32. package/dist/types/src/extensions/annotations.d.ts.map +1 -1
  33. package/dist/types/src/extensions/autocomplete.d.ts +1 -2
  34. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  35. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  36. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
  37. package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -1
  38. package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -1
  39. package/dist/types/src/extensions/command/command.d.ts +1 -2
  40. package/dist/types/src/extensions/command/command.d.ts.map +1 -1
  41. package/dist/types/src/extensions/command/hint.d.ts +14 -2
  42. package/dist/types/src/extensions/command/hint.d.ts.map +1 -1
  43. package/dist/types/src/extensions/command/index.d.ts +2 -0
  44. package/dist/types/src/extensions/command/index.d.ts.map +1 -1
  45. package/dist/types/src/extensions/command/menu.d.ts +7 -8
  46. package/dist/types/src/extensions/command/menu.d.ts.map +1 -1
  47. package/dist/types/src/extensions/command/state.d.ts +1 -1
  48. package/dist/types/src/extensions/command/state.d.ts.map +1 -1
  49. package/dist/types/src/extensions/command/typeahead.d.ts +17 -0
  50. package/dist/types/src/extensions/command/typeahead.d.ts.map +1 -0
  51. package/dist/types/src/extensions/comments.d.ts +2 -12
  52. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  53. package/dist/types/src/extensions/factories.d.ts +4 -0
  54. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  55. package/dist/types/src/extensions/index.d.ts +2 -0
  56. package/dist/types/src/extensions/index.d.ts.map +1 -1
  57. package/dist/types/src/extensions/json.d.ts +7 -0
  58. package/dist/types/src/extensions/json.d.ts.map +1 -0
  59. package/dist/types/src/extensions/markdown/{editorAction.d.ts → action.d.ts} +1 -1
  60. package/dist/types/src/extensions/markdown/action.d.ts.map +1 -0
  61. package/dist/types/src/extensions/markdown/bundle.d.ts +2 -1
  62. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  63. package/dist/types/src/extensions/markdown/index.d.ts +1 -2
  64. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  65. package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -1
  66. package/dist/types/src/extensions/outliner/commands.d.ts +9 -0
  67. package/dist/types/src/extensions/outliner/commands.d.ts.map +1 -0
  68. package/dist/types/src/extensions/outliner/editor.d.ts +5 -0
  69. package/dist/types/src/extensions/outliner/editor.d.ts.map +1 -0
  70. package/dist/types/src/extensions/outliner/editor.test.d.ts +2 -0
  71. package/dist/types/src/extensions/outliner/editor.test.d.ts.map +1 -0
  72. package/dist/types/src/extensions/outliner/index.d.ts +3 -0
  73. package/dist/types/src/extensions/outliner/index.d.ts.map +1 -0
  74. package/dist/types/src/extensions/outliner/outliner.d.ts +10 -0
  75. package/dist/types/src/extensions/outliner/outliner.d.ts.map +1 -0
  76. package/dist/types/src/extensions/outliner/outliner.test.d.ts +2 -0
  77. package/dist/types/src/extensions/outliner/outliner.test.d.ts.map +1 -0
  78. package/dist/types/src/extensions/outliner/selection.d.ts +12 -0
  79. package/dist/types/src/extensions/outliner/selection.d.ts.map +1 -0
  80. package/dist/types/src/extensions/outliner/tree.d.ts +79 -0
  81. package/dist/types/src/extensions/outliner/tree.d.ts.map +1 -0
  82. package/dist/types/src/extensions/outliner/tree.test.d.ts +2 -0
  83. package/dist/types/src/extensions/outliner/tree.test.d.ts.map +1 -0
  84. package/dist/types/src/stories/Command.stories.d.ts +7 -0
  85. package/dist/types/src/stories/Command.stories.d.ts.map +1 -0
  86. package/dist/types/src/stories/{TextEditorComments.stories.d.ts → Comments.stories.d.ts} +3 -3
  87. package/dist/types/src/stories/Comments.stories.d.ts.map +1 -0
  88. package/dist/types/src/stories/EditorToolbar.stories.d.ts +12 -0
  89. package/dist/types/src/stories/EditorToolbar.stories.d.ts.map +1 -0
  90. package/dist/types/src/stories/{TextEditorSpecial.stories.d.ts → Experimental.stories.d.ts} +3 -6
  91. package/dist/types/src/stories/Experimental.stories.d.ts.map +1 -0
  92. package/dist/types/src/stories/Markdown.stories.d.ts +46 -0
  93. package/dist/types/src/stories/Markdown.stories.d.ts.map +1 -0
  94. package/dist/types/src/stories/Outliner.stories.d.ts +26 -0
  95. package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -0
  96. package/dist/types/src/stories/Preview.stories.d.ts +10 -0
  97. package/dist/types/src/stories/Preview.stories.d.ts.map +1 -0
  98. package/dist/types/src/stories/{TextEditorBasic.stories.d.ts → TextEditor.stories.d.ts} +9 -39
  99. package/dist/types/src/stories/TextEditor.stories.d.ts.map +1 -0
  100. package/dist/types/src/stories/{story-utils.d.ts → util.d.ts} +6 -6
  101. package/dist/types/src/stories/util.d.ts.map +1 -0
  102. package/dist/types/src/styles/theme.d.ts.map +1 -1
  103. package/dist/types/src/styles/tokens.d.ts.map +1 -1
  104. package/dist/types/src/testing/index.d.ts +1 -1
  105. package/dist/types/src/testing/index.d.ts.map +1 -1
  106. package/dist/types/src/testing/util.d.ts +2 -0
  107. package/dist/types/src/testing/util.d.ts.map +1 -0
  108. package/package.json +40 -34
  109. package/src/components/EditorToolbar/EditorToolbar.tsx +81 -57
  110. package/src/components/EditorToolbar/index.ts +7 -1
  111. package/src/components/EditorToolbar/util.ts +3 -4
  112. package/src/components/Popover/RefDropdownMenu.tsx +77 -0
  113. package/src/{testing → components/Popover}/RefPopover.tsx +5 -4
  114. package/src/components/Popover/index.ts +6 -0
  115. package/src/components/index.ts +1 -0
  116. package/src/defaults.ts +10 -13
  117. package/src/extensions/annotations.ts +41 -64
  118. package/src/extensions/autocomplete.ts +5 -6
  119. package/src/extensions/automerge/automerge.stories.tsx +2 -7
  120. package/src/extensions/automerge/automerge.test.tsx +3 -2
  121. package/src/extensions/automerge/sync.ts +3 -3
  122. package/src/extensions/awareness/awareness-provider.ts +4 -4
  123. package/src/extensions/awareness/awareness.ts +7 -7
  124. package/src/extensions/blast.ts +9 -9
  125. package/src/extensions/command/command.ts +1 -3
  126. package/src/extensions/command/hint.ts +7 -7
  127. package/src/extensions/command/index.ts +2 -0
  128. package/src/extensions/command/menu.ts +43 -49
  129. package/src/extensions/command/typeahead.ts +116 -0
  130. package/src/extensions/comments.ts +4 -69
  131. package/src/extensions/factories.ts +13 -0
  132. package/src/extensions/index.ts +2 -0
  133. package/src/extensions/json.ts +56 -0
  134. package/src/extensions/markdown/bundle.ts +13 -9
  135. package/src/extensions/markdown/decorate.ts +7 -7
  136. package/src/extensions/markdown/image.ts +2 -2
  137. package/src/extensions/markdown/index.ts +1 -2
  138. package/src/extensions/markdown/styles.ts +2 -1
  139. package/src/extensions/markdown/table.ts +3 -3
  140. package/src/extensions/outliner/commands.ts +242 -0
  141. package/src/extensions/outliner/editor.test.ts +33 -0
  142. package/src/extensions/outliner/editor.ts +180 -0
  143. package/src/extensions/outliner/index.ts +6 -0
  144. package/src/extensions/outliner/outliner.test.ts +99 -0
  145. package/src/extensions/outliner/outliner.ts +162 -0
  146. package/src/extensions/outliner/selection.ts +50 -0
  147. package/src/extensions/outliner/tree.test.ts +164 -0
  148. package/src/extensions/outliner/tree.ts +315 -0
  149. package/src/extensions/preview/preview.ts +5 -5
  150. package/src/stories/Command.stories.tsx +97 -0
  151. package/src/stories/{TextEditorComments.stories.tsx → Comments.stories.tsx} +13 -14
  152. package/src/{components/EditorToolbar → stories}/EditorToolbar.stories.tsx +26 -20
  153. package/src/stories/{TextEditorSpecial.stories.tsx → Experimental.stories.tsx} +9 -30
  154. package/src/stories/Markdown.stories.tsx +121 -0
  155. package/src/stories/Outliner.stories.tsx +108 -0
  156. package/src/stories/{TextEditorPreview.stories.tsx → Preview.stories.tsx} +46 -136
  157. package/src/stories/TextEditor.stories.tsx +256 -0
  158. package/src/stories/{story-utils.tsx → util.tsx} +21 -22
  159. package/src/styles/theme.ts +12 -5
  160. package/src/styles/tokens.ts +1 -2
  161. package/src/testing/index.ts +1 -1
  162. package/src/testing/util.ts +5 -0
  163. package/dist/types/src/components/EditorToolbar/EditorToolbar.stories.d.ts +0 -53
  164. package/dist/types/src/components/EditorToolbar/EditorToolbar.stories.d.ts.map +0 -1
  165. package/dist/types/src/components/EditorToolbar/comment.d.ts +0 -18
  166. package/dist/types/src/components/EditorToolbar/comment.d.ts.map +0 -1
  167. package/dist/types/src/extensions/markdown/editorAction.d.ts.map +0 -1
  168. package/dist/types/src/extensions/markdown/outliner.d.ts +0 -12
  169. package/dist/types/src/extensions/markdown/outliner.d.ts.map +0 -1
  170. package/dist/types/src/stories/TextEditorBasic.stories.d.ts.map +0 -1
  171. package/dist/types/src/stories/TextEditorComments.stories.d.ts.map +0 -1
  172. package/dist/types/src/stories/TextEditorPreview.stories.d.ts +0 -13
  173. package/dist/types/src/stories/TextEditorPreview.stories.d.ts.map +0 -1
  174. package/dist/types/src/stories/TextEditorSpecial.stories.d.ts.map +0 -1
  175. package/dist/types/src/stories/story-utils.d.ts.map +0 -1
  176. package/dist/types/src/testing/RefPopover.d.ts.map +0 -1
  177. package/src/components/EditorToolbar/comment.ts +0 -30
  178. package/src/extensions/markdown/outliner.ts +0 -235
  179. package/src/stories/TextEditorBasic.stories.tsx +0 -333
  180. /package/src/extensions/markdown/{editorAction.ts → action.ts} +0 -0
@@ -95,17 +95,17 @@ export class RemoteSelectionsDecorator implements PluginValue {
95
95
  });
96
96
  }
97
97
 
98
- destroy() {
98
+ destroy(): void {
99
99
  void this._ctx.dispose();
100
100
  this._provider.close();
101
101
  }
102
102
 
103
- update(update: ViewUpdate) {
103
+ update(update: ViewUpdate): void {
104
104
  this._updateLocalSelection(update.view);
105
105
  this._updateRemoteSelections(update.view);
106
106
  }
107
107
 
108
- private _updateLocalSelection(view: EditorView) {
108
+ private _updateLocalSelection(view: EditorView): void {
109
109
  const hasFocus = view.hasFocus && view.dom.ownerDocument.hasFocus();
110
110
  const { anchor = undefined, head = undefined } = hasFocus ? view.state.selection.main : {};
111
111
  if (this._lastAnchor === anchor && this._lastHead === head) {
@@ -125,7 +125,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
125
125
  );
126
126
  }
127
127
 
128
- private _updateRemoteSelections(view: EditorView) {
128
+ private _updateRemoteSelections(view: EditorView): void {
129
129
  const decorations: Range<Decoration>[] = [
130
130
  // TODO(burdon): Factor out for testing.
131
131
  // {
@@ -239,11 +239,11 @@ class RemoteCaretWidget extends WidgetType {
239
239
  return span;
240
240
  }
241
241
 
242
- override updateDOM() {
242
+ override updateDOM(): boolean {
243
243
  return false;
244
244
  }
245
245
 
246
- override eq(widget: this) {
246
+ override eq(widget: this): boolean {
247
247
  return widget._color === this._color;
248
248
  }
249
249
 
@@ -251,7 +251,7 @@ class RemoteCaretWidget extends WidgetType {
251
251
  return -1;
252
252
  }
253
253
 
254
- override ignoreEvent() {
254
+ override ignoreEvent(): boolean {
255
255
  return true;
256
256
  }
257
257
  }
@@ -136,7 +136,7 @@ class Blaster {
136
136
  return this._node;
137
137
  }
138
138
 
139
- initialize() {
139
+ initialize(): void {
140
140
  // console.log('initialize');
141
141
  invariant(!this._canvas && !this._ctx);
142
142
 
@@ -155,7 +155,7 @@ class Blaster {
155
155
  this.resize();
156
156
  }
157
157
 
158
- destroy() {
158
+ destroy(): void {
159
159
  this.stop();
160
160
  // console.log('destroy');
161
161
  if (this._canvas) {
@@ -165,7 +165,7 @@ class Blaster {
165
165
  }
166
166
  }
167
167
 
168
- resize() {
168
+ resize(): void {
169
169
  if (this._node.parentElement && this._canvas) {
170
170
  const { offsetLeft: x, offsetTop: y, offsetWidth: width, offsetHeight: height } = this._node.parentElement;
171
171
  this._canvas.style.top = `${y}px`;
@@ -175,20 +175,20 @@ class Blaster {
175
175
  }
176
176
  }
177
177
 
178
- start() {
178
+ start(): void {
179
179
  // console.log('start');
180
180
  invariant(this._canvas && this._ctx);
181
181
  this._running = true;
182
182
  this.loop();
183
183
  }
184
184
 
185
- stop() {
185
+ stop(): void {
186
186
  // console.log('stop');
187
187
  this._running = false;
188
188
  this._node.style.transform = 'translate(0px, 0px)';
189
189
  }
190
190
 
191
- loop() {
191
+ loop(): void {
192
192
  if (!this._running || !this._canvas || !this._ctx) {
193
193
  return;
194
194
  }
@@ -230,7 +230,7 @@ class Blaster {
230
230
  }
231
231
  }, 100);
232
232
 
233
- drawParticles() {
233
+ drawParticles(): void {
234
234
  for (let i = this._particles.length; i--; i > 0) {
235
235
  const particle = this._particles[i];
236
236
  if (!particle) {
@@ -282,7 +282,7 @@ class Effect1 extends Effect {
282
282
  };
283
283
  }
284
284
 
285
- update(ctx: CanvasRenderingContext2D, particle: Particle) {
285
+ update(ctx: CanvasRenderingContext2D, particle: Particle): void {
286
286
  particle.vy += this._options.particleGravity;
287
287
  particle.x += particle.vx;
288
288
  particle.y += particle.vy;
@@ -313,7 +313,7 @@ class Effect2 extends Effect {
313
313
  };
314
314
  }
315
315
 
316
- update(ctx: CanvasRenderingContext2D, particle: Particle) {
316
+ update(ctx: CanvasRenderingContext2D, particle: Particle): void {
317
317
  particle.vy += this._options.particleGravity;
318
318
  particle.x += particle.vx;
319
319
  particle.y += particle.vy;
@@ -7,7 +7,6 @@ import { EditorView, keymap } from '@codemirror/view';
7
7
 
8
8
  import { closeEffect, commandKeyBindings } from './action';
9
9
  import { hintViewPlugin, type HintOptions } from './hint';
10
- import { floatingMenu, type FloatingMenuOptions } from './menu';
11
10
  import { commandConfig, commandState, type PopupOptions } from './state';
12
11
 
13
12
  // TODO(burdon): Create knowledge base for CM notes and ideas.
@@ -15,14 +14,13 @@ import { commandConfig, commandState, type PopupOptions } from './state';
15
14
  // https://github.com/saminzadeh/codemirror-extension-inline-suggestion
16
15
  // https://github.com/ChromeDevTools/devtools-frontend/blob/main/front_end/ui/components/text_editor/config.ts#L370
17
16
 
18
- export type CommandOptions = Partial<PopupOptions & FloatingMenuOptions & HintOptions>;
17
+ export type CommandOptions = Partial<PopupOptions & HintOptions>;
19
18
 
20
19
  export const command = (options: CommandOptions = {}): Extension => {
21
20
  return [
22
21
  keymap.of(commandKeyBindings),
23
22
  commandConfig.of(options),
24
23
  commandState,
25
- options.renderMenu ? floatingMenu({ renderMenu: options.renderMenu }) : [],
26
24
  options.onHint ? hintViewPlugin({ onHint: options.onHint }) : [],
27
25
  EditorView.focusChangeEffect.of((_, focusing) => {
28
26
  return focusing ? closeEffect.of(null) : null;
@@ -15,7 +15,7 @@ export type HintOptions = {
15
15
  export const hintViewPlugin = ({ onHint }: HintOptions) =>
16
16
  ViewPlugin.fromClass(
17
17
  class {
18
- deco = Decoration.none;
18
+ decorations = Decoration.none;
19
19
  update(update: ViewUpdate) {
20
20
  const builder = new RangeSetBuilder<Decoration>();
21
21
  const cState = update.view.state.field(commandState, false);
@@ -28,25 +28,25 @@ export const hintViewPlugin = ({ onHint }: HintOptions) =>
28
28
  if (selection.from === selection.to && line.from === line.to) {
29
29
  const hint = onHint();
30
30
  if (hint) {
31
- builder.add(selection.from, selection.to, Decoration.widget({ widget: new CommandHint(hint) }));
31
+ builder.add(selection.from, selection.to, Decoration.widget({ widget: new Hint(hint) }));
32
32
  }
33
33
  }
34
34
  }
35
35
 
36
- this.deco = builder.finish();
36
+ this.decorations = builder.finish();
37
37
  }
38
38
  },
39
39
  {
40
- provide: (plugin) => [EditorView.decorations.of((view) => view.plugin(plugin)?.deco ?? Decoration.none)],
40
+ provide: (plugin) => [EditorView.decorations.of((view) => view.plugin(plugin)?.decorations ?? Decoration.none)],
41
41
  },
42
42
  );
43
43
 
44
- class CommandHint extends WidgetType {
44
+ export class Hint extends WidgetType {
45
45
  constructor(readonly content: string | HTMLElement) {
46
46
  super();
47
47
  }
48
48
 
49
- toDOM() {
49
+ toDOM(): HTMLSpanElement {
50
50
  const wrap = document.createElement('span');
51
51
  wrap.className = 'cm-placeholder';
52
52
  wrap.style.pointerEvents = 'none';
@@ -76,7 +76,7 @@ class CommandHint extends WidgetType {
76
76
  return rect;
77
77
  }
78
78
 
79
- override ignoreEvent() {
79
+ override ignoreEvent(): boolean {
80
80
  return false;
81
81
  }
82
82
  }
@@ -4,3 +4,5 @@
4
4
 
5
5
  export * from './action';
6
6
  export * from './command';
7
+ export * from './menu';
8
+ export * from './typeahead';
@@ -2,41 +2,44 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type BlockInfo, type EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
5
+ import { type EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
6
6
 
7
- import { closeEffect, openCommand, openEffect } from './action';
8
- import { type RenderCallback } from '../../types';
7
+ import { closeEffect, openEffect } from './action';
9
8
 
10
9
  export type FloatingMenuOptions = {
11
- renderMenu: RenderCallback<{ onAction: () => void }>;
10
+ icon?: string;
11
+ height?: number;
12
+ padding?: number;
12
13
  };
13
14
 
14
- // TODO(burdon): Trigger completion on click.
15
- // TODO(burdon): Hide when dialog is open.
16
- export const floatingMenu = (options: FloatingMenuOptions) =>
15
+ export const floatingMenu = (options: FloatingMenuOptions = {}) => [
17
16
  ViewPlugin.fromClass(
18
17
  class {
19
- button: HTMLElement;
20
18
  view: EditorView;
19
+ tag: HTMLElement;
21
20
  rafId: number | null = null;
22
21
 
23
22
  constructor(view: EditorView) {
24
23
  this.view = view;
25
24
 
26
- // Position context: scrollDOM
25
+ // Position context.
27
26
  const container = view.scrollDOM;
28
27
  if (getComputedStyle(container).position === 'static') {
29
28
  container.style.position = 'relative';
30
29
  }
31
30
 
32
- // Render menu externally.
33
- this.button = document.createElement('div');
34
- this.button.style.position = 'absolute';
35
- this.button.style.zIndex = '10';
36
- this.button.style.display = 'none';
31
+ const icon = document.createElement('dx-icon');
32
+ icon.setAttribute('icon', options.icon ?? 'ph--dots-three-outline--regular');
37
33
 
38
- options.renderMenu(this.button, { onAction: () => openCommand(view) }, view);
39
- container.appendChild(this.button);
34
+ const button = document.createElement('button');
35
+ button.appendChild(icon);
36
+ button.classList.add('grid', 'items-center', 'justify-center', 'w-8', 'h-8');
37
+
38
+ // TODO(burdon): Custom tag/styles?
39
+ this.tag = document.createElement('dx-ref-tag');
40
+ this.tag.classList.add('border-none', 'fixed', 'p-0');
41
+ this.tag.appendChild(button);
42
+ container.appendChild(this.tag);
40
43
 
41
44
  // Listen for scroll events.
42
45
  container.addEventListener('scroll', this.scheduleUpdate.bind(this));
@@ -46,58 +49,49 @@ export const floatingMenu = (options: FloatingMenuOptions) =>
46
49
  update(update: ViewUpdate) {
47
50
  // TODO(burdon): Timer to fade in/out.
48
51
  if (update.transactions.some((tr) => tr.effects.some((effect) => effect.is(openEffect)))) {
49
- this.button.style.display = 'none';
52
+ this.tag.style.display = 'none';
50
53
  } else if (update.transactions.some((tr) => tr.effects.some((effect) => effect.is(closeEffect)))) {
51
- this.button.style.display = 'block';
54
+ this.tag.style.display = 'block';
52
55
  } else if (update.selectionSet || update.viewportChanged || update.docChanged || update.geometryChanged) {
53
56
  this.scheduleUpdate();
54
57
  }
55
58
  }
56
59
 
57
- scheduleUpdate() {
58
- if (this.rafId != null) {
59
- cancelAnimationFrame(this.rafId);
60
- }
61
-
62
- this.rafId = requestAnimationFrame(this.updateButtonPosition.bind(this));
63
- }
64
-
65
60
  updateButtonPosition() {
66
- const pos = this.view.state.selection.main.head;
67
- const lineBlock: BlockInfo = this.view.lineBlockAt(pos);
68
- const domInfo = this.view.domAtPos(lineBlock.from);
61
+ const { x, width } = this.view.contentDOM.getBoundingClientRect();
69
62
 
70
- // Find nearest HTMLElement for the line block
71
- let node: Node | null = domInfo.node;
72
- while (node && !(node instanceof HTMLElement)) {
73
- node = node.parentNode;
74
- }
75
-
76
- if (!node) {
77
- this.button.style.display = 'none';
63
+ const pos = this.view.state.selection.main.head;
64
+ const line = this.view.lineBlockAt(pos);
65
+ const coords = this.view.coordsAtPos(line.from);
66
+ if (!coords) {
78
67
  return;
79
68
  }
80
69
 
81
- const lineRect = (node as HTMLElement).getBoundingClientRect();
82
- const containerRect = this.view.scrollDOM.getBoundingClientRect();
70
+ const lineHeight = coords.bottom - coords.top;
71
+ const dy = (lineHeight - (options.height ?? 32)) / 2;
83
72
 
84
- // Account for scroll and padding/margin in scrollDOM.
85
- const offsetTop = lineRect.top - containerRect.top + this.view.scrollDOM.scrollTop;
86
- const offsetLeft = this.view.scrollDOM.clientWidth + this.view.scrollDOM.scrollLeft - lineRect.x;
73
+ const offsetTop = coords.top + dy;
74
+ const offsetLeft = x + width + (options.padding ?? 8);
75
+
76
+ this.tag.style.top = `${offsetTop}px`;
77
+ this.tag.style.left = `${offsetLeft}px`;
78
+ this.tag.style.display = 'block';
79
+ }
87
80
 
88
- // TODO(burdon): Position is incorrect if cursor is in fenced code block.
89
- // console.log('offsetTop', lineRect, containerRect);
81
+ scheduleUpdate() {
82
+ if (this.rafId != null) {
83
+ cancelAnimationFrame(this.rafId);
84
+ }
90
85
 
91
- this.button.style.top = `${offsetTop}px`;
92
- this.button.style.left = `${offsetLeft}px`;
93
- this.button.style.display = 'block';
86
+ this.rafId = requestAnimationFrame(this.updateButtonPosition.bind(this));
94
87
  }
95
88
 
96
89
  destroy() {
97
- this.button.remove();
90
+ this.tag.remove();
98
91
  if (this.rafId != null) {
99
92
  cancelAnimationFrame(this.rafId);
100
93
  }
101
94
  }
102
95
  },
103
- );
96
+ ),
97
+ ];
@@ -0,0 +1,116 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { EditorSelection, Prec, RangeSetBuilder, type Extension } from '@codemirror/state';
6
+ import {
7
+ type Command,
8
+ Decoration,
9
+ type DecorationSet,
10
+ type EditorView,
11
+ keymap,
12
+ ViewPlugin,
13
+ type ViewUpdate,
14
+ } from '@codemirror/view';
15
+
16
+ import { Hint } from './hint';
17
+
18
+ export type TypeaheadContext = { line: string };
19
+
20
+ // TODO(burdon): Option to complete only at end of line?
21
+ export type TypeaheadOptions = {
22
+ onComplete?: (context: TypeaheadContext) => string | undefined;
23
+ };
24
+
25
+ /**
26
+ * CodeMirror extension for typeahead completion.
27
+ */
28
+ export const typeahead = ({ onComplete }: TypeaheadOptions = {}): Extension => {
29
+ let hint: string | undefined;
30
+
31
+ const complete: Command = (view: EditorView) => {
32
+ if (!hint) {
33
+ return false;
34
+ }
35
+
36
+ const selection = view.state.selection.main;
37
+ view.dispatch({
38
+ changes: [{ from: selection.from, to: selection.to, insert: hint }],
39
+ selection: EditorSelection.cursor(selection.from + hint.length),
40
+ });
41
+
42
+ return true;
43
+ };
44
+
45
+ return [
46
+ ViewPlugin.fromClass(
47
+ class {
48
+ decorations: DecorationSet = Decoration.none;
49
+ update(update: ViewUpdate) {
50
+ const builder = new RangeSetBuilder<Decoration>();
51
+ const selection = update.view.state.selection.main;
52
+ const line = update.view.state.doc.lineAt(selection.from);
53
+
54
+ // TODO(burdon): Check at end of line and matches start of previous word.
55
+ // TODO(burdon): Context grammar.
56
+ if (selection.from === selection.to && selection.from === line.to) {
57
+ const str = update.state.sliceDoc(line.from, selection.from);
58
+ hint = onComplete?.({ line: str });
59
+ if (hint) {
60
+ builder.add(selection.from, selection.to, Decoration.widget({ widget: new Hint(hint) }));
61
+ }
62
+ }
63
+
64
+ this.decorations = builder.finish();
65
+ }
66
+ },
67
+ {
68
+ decorations: (v) => v.decorations,
69
+ },
70
+ ),
71
+
72
+ // Keys.
73
+ Prec.highest(
74
+ keymap.of([
75
+ {
76
+ key: 'Tab',
77
+ preventDefault: true,
78
+ run: complete,
79
+ },
80
+ {
81
+ key: 'ArrowRight',
82
+ preventDefault: true,
83
+ run: complete,
84
+ },
85
+ ]),
86
+ ),
87
+ ];
88
+ };
89
+
90
+ /**
91
+ * Util to match current line to a static list of completions.
92
+ */
93
+ export const staticCompletion =
94
+ (completions: string[], defaultCompletion?: string) =>
95
+ ({ line }: TypeaheadContext) => {
96
+ if (line.length === 0 && defaultCompletion) {
97
+ return defaultCompletion;
98
+ }
99
+
100
+ const words = line.split(/\s+/).filter(Boolean);
101
+ if (words.length) {
102
+ const word = words.at(-1)!;
103
+ for (const completion of completions) {
104
+ const match = matchCompletion(completion, word);
105
+ if (match) {
106
+ return match;
107
+ }
108
+ }
109
+ }
110
+ };
111
+
112
+ export const matchCompletion = (completion: string, word: string): string | undefined => {
113
+ if (completion.length > word.length && completion.startsWith(word)) {
114
+ return completion.slice(word.length);
115
+ }
116
+ };
@@ -3,14 +3,7 @@
3
3
  //
4
4
 
5
5
  import { invertedEffects } from '@codemirror/commands';
6
- import {
7
- type ChangeDesc,
8
- type EditorState,
9
- type Extension,
10
- StateEffect,
11
- StateField,
12
- type Text,
13
- } from '@codemirror/state';
6
+ import { type ChangeDesc, type Extension, StateEffect, StateField, type Text } from '@codemirror/state';
14
7
  import {
15
8
  hoverTooltip,
16
9
  keymap,
@@ -22,17 +15,15 @@ import {
22
15
  ViewPlugin,
23
16
  } from '@codemirror/view';
24
17
  import sortBy from 'lodash.sortby';
25
- import { useEffect, useMemo } from 'react';
18
+ import { useEffect } from 'react';
26
19
 
27
20
  import { debounce, type CleanupFn } from '@dxos/async';
28
- import { type Live } from '@dxos/live-object';
29
21
  import { log } from '@dxos/log';
30
22
  import { isNonNullable } from '@dxos/util';
31
23
 
32
24
  import { documentId } from './selection';
33
- import { type EditorToolbarState } from '../components';
34
25
  import { type RenderCallback, type Comment, type Range } from '../types';
35
- import { Cursor, overlap, singleValueFacet, callbackWrapper } from '../util';
26
+ import { Cursor, singleValueFacet, callbackWrapper } from '../util';
36
27
 
37
28
  //
38
29
  // State management.
@@ -156,7 +147,7 @@ const commentsDecorations = EditorView.decorations.compute([commentsState], (sta
156
147
  return Decoration.set(decorations);
157
148
  });
158
149
 
159
- const commentClickedEffect = StateEffect.define<string>();
150
+ export const commentClickedEffect = StateEffect.define<string>();
160
151
 
161
152
  const handleCommentClick = EditorView.domEventHandlers({
162
153
  click: (event, view) => {
@@ -545,30 +536,6 @@ export const scrollThreadIntoView = (view: EditorView, id: string, center = true
545
536
  }
546
537
  };
547
538
 
548
- /**
549
- * Query the editor state for the active formatting at the selection.
550
- */
551
- export const selectionOverlapsComment = (state: EditorState): boolean => {
552
- // May not be defined if thread plugin not installed.
553
- const commentState = state.field(commentsState, false);
554
- if (commentState === undefined) {
555
- return false;
556
- }
557
-
558
- const { selection } = state;
559
- for (const range of selection.ranges) {
560
- if (commentState.comments.some(({ range: commentRange }) => overlap(commentRange, range))) {
561
- return true;
562
- }
563
- }
564
-
565
- return false;
566
- };
567
-
568
- const hasActiveSelection = (state: EditorState): boolean => {
569
- return state.selection.ranges.some((range) => !range.empty);
570
- };
571
-
572
539
  /**
573
540
  * Manages external comment synchronization for the editor.
574
541
  * This class subscribes to external comment updates and applies them to the editor view.
@@ -606,19 +573,6 @@ export const createExternalCommentSync = (
606
573
  },
607
574
  );
608
575
 
609
- export const useCommentState = (state: Live<EditorToolbarState>): Extension => {
610
- return useMemo(
611
- () =>
612
- EditorView.updateListener.of((update) => {
613
- if (update.docChanged || update.selectionSet) {
614
- state.comment = selectionOverlapsComment(update.state);
615
- state.selection = hasActiveSelection(update.state);
616
- }
617
- }),
618
- [state],
619
- );
620
- };
621
-
622
576
  /**
623
577
  * @deprecated This hook will be removed in future versions. Use the new comment sync extension instead.
624
578
  * Update comments state field.
@@ -636,22 +590,3 @@ export const useComments = (view: EditorView | null | undefined, id: string, com
636
590
  }
637
591
  });
638
592
  };
639
-
640
- /**
641
- * Hook provides an extension to listen for comment clicks and invoke a handler.
642
- */
643
- export const useCommentClickListener = (onCommentClick: (commentId: string) => void): Extension => {
644
- return useMemo(
645
- () =>
646
- EditorView.updateListener.of((update) => {
647
- update.transactions.forEach((transaction) => {
648
- transaction.effects.forEach((effect) => {
649
- if (effect.is(commentClickedEffect)) {
650
- onCommentClick(effect.value);
651
- }
652
- });
653
- });
654
- }),
655
- [onCommentClick],
656
- );
657
- };
@@ -11,6 +11,7 @@ import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
11
11
  import {
12
12
  EditorView,
13
13
  type KeyBinding,
14
+ ViewPlugin,
14
15
  drawSelection,
15
16
  dropCursor,
16
17
  highlightActiveLine,
@@ -59,6 +60,7 @@ export type BasicExtensionsOptions = {
59
60
  indentWithTab?: boolean;
60
61
  keymap?: null | 'default' | 'standard';
61
62
  lineNumbers?: boolean;
63
+ /** If false then do not set a max-width or side margin on the editor. */
62
64
  lineWrapping?: boolean;
63
65
  placeholder?: string;
64
66
  /** If true user cannot edit the text, but they can still select and copy it. */
@@ -149,6 +151,9 @@ export type ThemeExtensionsOptions = {
149
151
  editor?: {
150
152
  className?: string;
151
153
  };
154
+ scroll?: {
155
+ className?: string;
156
+ };
152
157
  content?: {
153
158
  className?: string;
154
159
  };
@@ -179,6 +184,14 @@ export const createThemeExtensions = ({
179
184
  (themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle)),
180
185
  slots.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
181
186
  slots.content?.className && EditorView.contentAttributes.of({ class: slots.content.className }),
187
+ slots.scroll?.className &&
188
+ ViewPlugin.fromClass(
189
+ class {
190
+ constructor(view: EditorView) {
191
+ view.scrollDOM.classList.add(slots.scroll.className);
192
+ }
193
+ },
194
+ ),
182
195
  ].filter(isNotFalsy);
183
196
  };
184
197
 
@@ -14,10 +14,12 @@ export * from './dnd';
14
14
  export * from './factories';
15
15
  export * from './focus';
16
16
  export * from './folding';
17
+ export * from './json';
17
18
  export * from './listener';
18
19
  export * from './markdown';
19
20
  export * from './mention';
20
21
  export * from './modes';
22
+ export * from './outliner';
21
23
  export * from './preview';
22
24
  export * from './selection';
23
25
  export * from './typewriter';