@dxos/react-ui-editor 0.8.2-main.fbd8ed0 → 0.8.2-staging.7ac8446

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 (193) hide show
  1. package/dist/lib/browser/index.mjs +5088 -5521
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +4915 -5354
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/lib/node-esm/index.mjs +5088 -5521
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/types/src/{components/EditorToolbar/EditorToolbar.stories.d.ts → InputMode.stories.d.ts} +7 -3
  11. package/dist/types/src/InputMode.stories.d.ts.map +1 -0
  12. package/dist/types/src/{stories/TextEditorBasic.stories.d.ts → TextEditor.stories.d.ts} +35 -5
  13. package/dist/types/src/TextEditor.stories.d.ts.map +1 -0
  14. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts +1 -1
  15. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
  16. package/dist/types/src/components/EditorToolbar/blocks.d.ts +3 -4
  17. package/dist/types/src/components/EditorToolbar/blocks.d.ts.map +1 -1
  18. package/dist/types/src/components/EditorToolbar/comment.d.ts +3 -4
  19. package/dist/types/src/components/EditorToolbar/comment.d.ts.map +1 -1
  20. package/dist/types/src/components/EditorToolbar/formatting.d.ts +3 -4
  21. package/dist/types/src/components/EditorToolbar/formatting.d.ts.map +1 -1
  22. package/dist/types/src/components/EditorToolbar/headings.d.ts +3 -4
  23. package/dist/types/src/components/EditorToolbar/headings.d.ts.map +1 -1
  24. package/dist/types/src/components/EditorToolbar/lists.d.ts +3 -4
  25. package/dist/types/src/components/EditorToolbar/lists.d.ts.map +1 -1
  26. package/dist/types/src/components/EditorToolbar/util.d.ts +20 -14
  27. package/dist/types/src/components/EditorToolbar/util.d.ts.map +1 -1
  28. package/dist/types/src/components/EditorToolbar/{view-mode.d.ts → viewMode.d.ts} +4 -5
  29. package/dist/types/src/components/EditorToolbar/viewMode.d.ts.map +1 -0
  30. package/dist/types/src/defaults.d.ts +0 -1
  31. package/dist/types/src/defaults.d.ts.map +1 -1
  32. package/dist/types/src/extensions/annotations.d.ts.map +1 -1
  33. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  34. package/dist/types/src/extensions/automerge/automerge.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/cursor.d.ts.map +1 -1
  37. package/dist/types/src/extensions/automerge/defs.d.ts +1 -1
  38. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  39. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
  40. package/dist/types/src/extensions/automerge/update-automerge.d.ts +1 -1
  41. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  42. package/dist/types/src/extensions/automerge/update-codemirror.d.ts +1 -1
  43. package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -1
  44. package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -1
  45. package/dist/types/src/extensions/blast.d.ts.map +1 -1
  46. package/dist/types/src/extensions/command/command.d.ts +10 -5
  47. package/dist/types/src/extensions/command/command.d.ts.map +1 -1
  48. package/dist/types/src/extensions/command/hint.d.ts +2 -4
  49. package/dist/types/src/extensions/command/hint.d.ts.map +1 -1
  50. package/dist/types/src/extensions/command/index.d.ts +0 -1
  51. package/dist/types/src/extensions/command/index.d.ts.map +1 -1
  52. package/dist/types/src/extensions/command/menu.d.ts +2 -7
  53. package/dist/types/src/extensions/command/menu.d.ts.map +1 -1
  54. package/dist/types/src/extensions/command/preview.d.ts +12 -0
  55. package/dist/types/src/extensions/command/preview.d.ts.map +1 -0
  56. package/dist/types/src/extensions/command/state.d.ts +11 -9
  57. package/dist/types/src/extensions/command/state.d.ts.map +1 -1
  58. package/dist/types/src/extensions/comments.d.ts +7 -9
  59. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  60. package/dist/types/src/extensions/debug.d.ts.map +1 -1
  61. package/dist/types/src/extensions/dnd.d.ts.map +1 -1
  62. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  63. package/dist/types/src/extensions/folding.d.ts.map +1 -1
  64. package/dist/types/src/extensions/index.d.ts +0 -1
  65. package/dist/types/src/extensions/index.d.ts.map +1 -1
  66. package/dist/types/src/extensions/listener.d.ts.map +1 -1
  67. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  68. package/dist/types/src/extensions/markdown/changes.d.ts.map +1 -1
  69. package/dist/types/src/extensions/markdown/debug.d.ts.map +1 -1
  70. package/dist/types/src/extensions/markdown/decorate.d.ts +1 -5
  71. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  72. package/dist/types/src/extensions/markdown/formatting.d.ts +3 -3
  73. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  74. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  75. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
  76. package/dist/types/src/extensions/markdown/index.d.ts +0 -1
  77. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  78. package/dist/types/src/extensions/markdown/link.d.ts +1 -4
  79. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
  80. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -1
  81. package/dist/types/src/extensions/mention.d.ts.map +1 -1
  82. package/dist/types/src/extensions/modes.d.ts +5 -5
  83. package/dist/types/src/extensions/modes.d.ts.map +1 -1
  84. package/dist/types/src/extensions/selection.d.ts.map +1 -1
  85. package/dist/types/src/extensions/typewriter.d.ts.map +1 -1
  86. package/dist/types/src/fragments.d.ts +3 -0
  87. package/dist/types/src/fragments.d.ts.map +1 -0
  88. package/dist/types/src/hooks/index.d.ts +1 -0
  89. package/dist/types/src/hooks/index.d.ts.map +1 -1
  90. package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
  91. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
  92. package/dist/types/src/hooks/useTextEditor.d.ts +1 -2
  93. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  94. package/dist/types/src/styles/theme.d.ts.map +1 -1
  95. package/dist/types/src/types.d.ts +0 -5
  96. package/dist/types/src/types.d.ts.map +1 -1
  97. package/dist/types/src/util/cursor.d.ts.map +1 -1
  98. package/dist/types/src/util/debug.d.ts.map +1 -1
  99. package/dist/types/src/util/dom.d.ts.map +1 -1
  100. package/dist/types/src/util/facet.d.ts.map +1 -1
  101. package/dist/types/src/util/react.d.ts +1 -6
  102. package/dist/types/src/util/react.d.ts.map +1 -1
  103. package/dist/types/tsconfig.tsbuildinfo +1 -1
  104. package/package.json +27 -37
  105. package/src/InputMode.stories.tsx +124 -0
  106. package/src/TextEditor.stories.tsx +856 -0
  107. package/src/components/EditorToolbar/EditorToolbar.tsx +34 -33
  108. package/src/components/EditorToolbar/blocks.ts +6 -27
  109. package/src/components/EditorToolbar/comment.ts +4 -11
  110. package/src/components/EditorToolbar/formatting.ts +7 -34
  111. package/src/components/EditorToolbar/headings.ts +8 -9
  112. package/src/components/EditorToolbar/lists.ts +7 -26
  113. package/src/components/EditorToolbar/util.ts +17 -17
  114. package/src/components/EditorToolbar/{view-mode.ts → viewMode.ts} +8 -9
  115. package/src/defaults.ts +3 -5
  116. package/src/extensions/automerge/automerge.stories.tsx +17 -11
  117. package/src/extensions/automerge/automerge.test.tsx +4 -4
  118. package/src/extensions/automerge/automerge.ts +2 -2
  119. package/src/extensions/automerge/defs.ts +2 -1
  120. package/src/extensions/automerge/sync.ts +4 -4
  121. package/src/extensions/automerge/update-automerge.ts +1 -1
  122. package/src/extensions/automerge/update-codemirror.ts +4 -3
  123. package/src/extensions/command/command.ts +27 -9
  124. package/src/extensions/command/hint.ts +30 -33
  125. package/src/extensions/command/index.ts +0 -1
  126. package/src/extensions/command/menu.ts +8 -11
  127. package/src/extensions/command/preview.ts +79 -0
  128. package/src/extensions/command/state.ts +61 -41
  129. package/src/extensions/comments.ts +9 -9
  130. package/src/extensions/folding.tsx +1 -1
  131. package/src/extensions/index.ts +0 -1
  132. package/src/extensions/markdown/changes.ts +2 -3
  133. package/src/extensions/markdown/decorate.ts +10 -12
  134. package/src/extensions/markdown/formatting.ts +6 -6
  135. package/src/extensions/markdown/image.ts +11 -12
  136. package/src/extensions/markdown/index.ts +0 -1
  137. package/src/extensions/markdown/link.ts +24 -33
  138. package/src/extensions/markdown/styles.ts +2 -2
  139. package/src/extensions/modes.ts +6 -5
  140. package/src/fragments.ts +19 -0
  141. package/src/hooks/index.ts +1 -0
  142. package/src/hooks/useActionHandler.ts +12 -0
  143. package/src/hooks/useTextEditor.ts +3 -4
  144. package/src/styles/theme.ts +0 -3
  145. package/src/types.ts +0 -7
  146. package/src/util/react.tsx +2 -20
  147. package/dist/lib/browser/testing/index.mjs +0 -67
  148. package/dist/lib/browser/testing/index.mjs.map +0 -7
  149. package/dist/lib/node/testing/index.cjs +0 -101
  150. package/dist/lib/node/testing/index.cjs.map +0 -7
  151. package/dist/lib/node-esm/testing/index.mjs +0 -69
  152. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  153. package/dist/types/src/components/EditorToolbar/EditorToolbar.stories.d.ts.map +0 -1
  154. package/dist/types/src/components/EditorToolbar/image.d.ts +0 -16
  155. package/dist/types/src/components/EditorToolbar/image.d.ts.map +0 -1
  156. package/dist/types/src/components/EditorToolbar/search.d.ts +0 -17
  157. package/dist/types/src/components/EditorToolbar/search.d.ts.map +0 -1
  158. package/dist/types/src/components/EditorToolbar/view-mode.d.ts.map +0 -1
  159. package/dist/types/src/extensions/command/action.d.ts +0 -17
  160. package/dist/types/src/extensions/command/action.d.ts.map +0 -1
  161. package/dist/types/src/extensions/markdown/outliner.d.ts +0 -12
  162. package/dist/types/src/extensions/markdown/outliner.d.ts.map +0 -1
  163. package/dist/types/src/extensions/preview/index.d.ts +0 -2
  164. package/dist/types/src/extensions/preview/index.d.ts.map +0 -1
  165. package/dist/types/src/extensions/preview/preview.d.ts +0 -39
  166. package/dist/types/src/extensions/preview/preview.d.ts.map +0 -1
  167. package/dist/types/src/stories/TextEditorBasic.stories.d.ts.map +0 -1
  168. package/dist/types/src/stories/TextEditorComments.stories.d.ts +0 -13
  169. package/dist/types/src/stories/TextEditorComments.stories.d.ts.map +0 -1
  170. package/dist/types/src/stories/TextEditorPreview.stories.d.ts +0 -13
  171. package/dist/types/src/stories/TextEditorPreview.stories.d.ts.map +0 -1
  172. package/dist/types/src/stories/TextEditorSpecial.stories.d.ts +0 -19
  173. package/dist/types/src/stories/TextEditorSpecial.stories.d.ts.map +0 -1
  174. package/dist/types/src/stories/story-utils.d.ts +0 -53
  175. package/dist/types/src/stories/story-utils.d.ts.map +0 -1
  176. package/dist/types/src/testing/RefPopover.d.ts +0 -21
  177. package/dist/types/src/testing/RefPopover.d.ts.map +0 -1
  178. package/dist/types/src/testing/index.d.ts +0 -2
  179. package/dist/types/src/testing/index.d.ts.map +0 -1
  180. package/src/components/EditorToolbar/EditorToolbar.stories.tsx +0 -90
  181. package/src/components/EditorToolbar/image.ts +0 -16
  182. package/src/components/EditorToolbar/search.ts +0 -19
  183. package/src/extensions/command/action.ts +0 -49
  184. package/src/extensions/markdown/outliner.ts +0 -235
  185. package/src/extensions/preview/index.ts +0 -5
  186. package/src/extensions/preview/preview.ts +0 -271
  187. package/src/stories/TextEditorBasic.stories.tsx +0 -333
  188. package/src/stories/TextEditorComments.stories.tsx +0 -99
  189. package/src/stories/TextEditorPreview.stories.tsx +0 -239
  190. package/src/stories/TextEditorSpecial.stories.tsx +0 -107
  191. package/src/stories/story-utils.tsx +0 -327
  192. package/src/testing/RefPopover.tsx +0 -74
  193. package/src/testing/index.ts +0 -5
@@ -4,9 +4,10 @@
4
4
  // Ref: https://github.com/automerge/automerge-codemirror
5
5
  //
6
6
 
7
- import { type Heads, type Prop } from '@automerge/automerge';
8
7
  import { Annotation, StateEffect, type StateField, type EditorState, type Transaction } from '@codemirror/state';
9
8
 
9
+ import { type Heads, type Prop } from '@dxos/automerge/automerge';
10
+
10
11
  export type State = {
11
12
  path: Prop[];
12
13
  lastHeads: Heads;
@@ -4,10 +4,10 @@
4
4
  // Ref: https://github.com/automerge/automerge-codemirror
5
5
  //
6
6
 
7
- import { next as A } from '@automerge/automerge';
8
7
  import { type StateField } from '@codemirror/state';
9
8
  import { type EditorView } from '@codemirror/view';
10
9
 
10
+ import { next as A } from '@dxos/automerge/automerge';
11
11
  import { type IDocHandle } from '@dxos/react-client/echo';
12
12
 
13
13
  import { getLastHeads, getPath, isReconcile, reconcileAnnotation, type State, updateHeads } from './defs';
@@ -57,15 +57,15 @@ export class Syncer {
57
57
  onAutomergeChange(view: EditorView) {
58
58
  // Get the diff between the updated state of the document and the heads and apply that to the codemirror doc.
59
59
  const oldHeads = getLastHeads(view.state, this._state);
60
- const newHeads = A.getHeads(this._handle.doc()!);
61
- const diff = A.equals(oldHeads, newHeads) ? [] : A.diff(this._handle.doc()!, oldHeads, newHeads);
60
+ const newHeads = A.getHeads(this._handle.docSync()!);
61
+ const diff = A.equals(oldHeads, newHeads) ? [] : A.diff(this._handle.docSync()!, oldHeads, newHeads);
62
62
 
63
63
  const selection = view.state.selection;
64
64
  const path = getPath(view.state, this._state);
65
65
  updateCodeMirror(view, selection, path, diff);
66
66
 
67
67
  // TODO(burdon): Test conflicts?
68
- // A.getConflicts(this._handle.doc()!, path[0]);
68
+ // A.getConflicts(this._handle.docSync()!, path[0]);
69
69
 
70
70
  view.dispatch({
71
71
  effects: updateHeads(newHeads),
@@ -4,9 +4,9 @@
4
4
  // Ref: https://github.com/automerge/automerge-codemirror
5
5
  //
6
6
 
7
- import { next as A, type Heads } from '@automerge/automerge';
8
7
  import { type EditorState, type StateField, type Transaction, type Text } from '@codemirror/state';
9
8
 
9
+ import { next as A, type Heads } from '@dxos/automerge/automerge';
10
10
  import { type IDocHandle } from '@dxos/react-client/echo';
11
11
 
12
12
  import { type State } from './defs';
@@ -4,6 +4,9 @@
4
4
  // Ref: https://github.com/automerge/automerge-codemirror
5
5
  //
6
6
 
7
+ import { ChangeSet, type ChangeSpec, type EditorSelection, type EditorState } from '@codemirror/state';
8
+ import { type EditorView } from '@codemirror/view';
9
+
7
10
  import {
8
11
  type DelPatch,
9
12
  type InsertPatch,
@@ -11,9 +14,7 @@ import {
11
14
  type Prop,
12
15
  type PutPatch,
13
16
  type SpliceTextPatch,
14
- } from '@automerge/automerge';
15
- import { ChangeSet, type ChangeSpec, type EditorSelection, type EditorState } from '@codemirror/state';
16
- import { type EditorView } from '@codemirror/view';
17
+ } from '@dxos/automerge/automerge';
17
18
 
18
19
  import { reconcileAnnotation } from './defs';
19
20
 
@@ -5,25 +5,35 @@
5
5
  import { type Extension } from '@codemirror/state';
6
6
  import { EditorView, keymap } from '@codemirror/view';
7
7
 
8
- import { closeEffect, commandKeyBindings } from './action';
9
- import { hintViewPlugin, type HintOptions } from './hint';
10
- import { floatingMenu, type FloatingMenuOptions } from './menu';
11
- import { commandConfig, commandState, type PopupOptions } from './state';
8
+ import { hintViewPlugin } from './hint';
9
+ import { floatingMenu } from './menu';
10
+ import { preview, type PreviewOptions } from './preview';
11
+ import { closeEffect, commandConfig, commandKeyBindings, commandState } from './state';
12
12
 
13
13
  // TODO(burdon): Create knowledge base for CM notes and ideas.
14
14
  // https://discuss.codemirror.net/t/inline-code-hints-like-vscode/5533/4
15
15
  // https://github.com/saminzadeh/codemirror-extension-inline-suggestion
16
16
  // https://github.com/ChromeDevTools/devtools-frontend/blob/main/front_end/ui/components/text_editor/config.ts#L370
17
17
 
18
- export type CommandOptions = Partial<PopupOptions & FloatingMenuOptions & HintOptions>;
18
+ // TODO(burdon): Discriminated union.
19
+ export type CommandAction = {
20
+ insert?: string;
21
+ };
22
+
23
+ export type CommandOptions = {
24
+ onHint: () => string | undefined;
25
+ onRenderDialog: (el: HTMLElement, cb: (action?: CommandAction) => void) => void;
26
+ onRenderMenu: (el: HTMLElement, cb: () => void) => void;
27
+ } & Pick<PreviewOptions, 'onRenderPreview'>;
19
28
 
20
- export const command = (options: CommandOptions = {}): Extension => {
29
+ export const command = (options: CommandOptions): Extension => {
21
30
  return [
22
- keymap.of(commandKeyBindings),
23
31
  commandConfig.of(options),
24
32
  commandState,
25
- options.renderMenu ? floatingMenu({ renderMenu: options.renderMenu }) : [],
26
- options.onHint ? hintViewPlugin({ onHint: options.onHint }) : [],
33
+ keymap.of(commandKeyBindings),
34
+ preview(options),
35
+ floatingMenu(options),
36
+ hintViewPlugin(options),
27
37
  EditorView.focusChangeEffect.of((_, focusing) => {
28
38
  return focusing ? closeEffect.of(null) : null;
29
39
  }),
@@ -31,6 +41,14 @@ export const command = (options: CommandOptions = {}): Extension => {
31
41
  '.cm-tooltip': {
32
42
  background: 'transparent',
33
43
  },
44
+ '.cm-preview': {
45
+ marginLeft: '-1rem',
46
+ marginRight: '-1rem',
47
+ padding: '1rem',
48
+ borderRadius: '1rem',
49
+ background: 'var(--dx-modalSurface)',
50
+ border: '1px solid var(--dx-separator)',
51
+ },
34
52
  }),
35
53
  ];
36
54
  };
@@ -5,42 +5,10 @@
5
5
  import { RangeSetBuilder } from '@codemirror/state';
6
6
  import { Decoration, EditorView, ViewPlugin, type ViewUpdate, WidgetType } from '@codemirror/view';
7
7
 
8
+ import { type CommandOptions } from './command';
8
9
  import { commandState } from './state';
9
10
  import { clientRectsFor, flattenRect } from '../../util';
10
11
 
11
- export type HintOptions = {
12
- onHint: () => string | undefined;
13
- };
14
-
15
- export const hintViewPlugin = ({ onHint }: HintOptions) =>
16
- ViewPlugin.fromClass(
17
- class {
18
- deco = Decoration.none;
19
- update(update: ViewUpdate) {
20
- const builder = new RangeSetBuilder<Decoration>();
21
- const cState = update.view.state.field(commandState, false);
22
- if (!cState?.tooltip) {
23
- const selection = update.view.state.selection.main;
24
- const line = update.view.state.doc.lineAt(selection.from);
25
- // Only show if blank line.
26
- // TODO(burdon): Clashes with placeholder if pos === 0.
27
- // TODO(burdon): Show after delay or if blank line above?
28
- if (selection.from === selection.to && line.from === line.to) {
29
- const hint = onHint();
30
- if (hint) {
31
- builder.add(selection.from, selection.to, Decoration.widget({ widget: new CommandHint(hint) }));
32
- }
33
- }
34
- }
35
-
36
- this.deco = builder.finish();
37
- }
38
- },
39
- {
40
- provide: (plugin) => [EditorView.decorations.of((view) => view.plugin(plugin)?.deco ?? Decoration.none)],
41
- },
42
- );
43
-
44
12
  class CommandHint extends WidgetType {
45
13
  constructor(readonly content: string | HTMLElement) {
46
14
  super();
@@ -80,3 +48,32 @@ class CommandHint extends WidgetType {
80
48
  return false;
81
49
  }
82
50
  }
51
+
52
+ export const hintViewPlugin = ({ onHint }: CommandOptions) =>
53
+ ViewPlugin.fromClass(
54
+ class {
55
+ deco = Decoration.none;
56
+ update(update: ViewUpdate) {
57
+ const builder = new RangeSetBuilder<Decoration>();
58
+ const cState = update.view.state.field(commandState, false);
59
+ if (!cState?.tooltip) {
60
+ const selection = update.view.state.selection.main;
61
+ const line = update.view.state.doc.lineAt(selection.from);
62
+ // Only show if blank line.
63
+ // TODO(burdon): Clashes with placeholder if pos === 0.
64
+ // TODO(burdon): Show after delay or if blank line above?
65
+ if (selection.from === selection.to && line.from === line.to) {
66
+ const hint = onHint();
67
+ if (hint) {
68
+ builder.add(selection.from, selection.to, Decoration.widget({ widget: new CommandHint(hint) }));
69
+ }
70
+ }
71
+ }
72
+
73
+ this.deco = builder.finish();
74
+ }
75
+ },
76
+ {
77
+ provide: (plugin) => [EditorView.decorations.of((view) => view.plugin(plugin)?.deco ?? Decoration.none)],
78
+ },
79
+ );
@@ -2,5 +2,4 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './action';
6
5
  export * from './command';
@@ -4,16 +4,12 @@
4
4
 
5
5
  import { type BlockInfo, type EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
6
6
 
7
- import { closeEffect, openCommand, openEffect } from './action';
8
- import { type RenderCallback } from '../../types';
9
-
10
- export type FloatingMenuOptions = {
11
- renderMenu: RenderCallback<{ onAction: () => void }>;
12
- };
7
+ import { type CommandOptions } from './command';
8
+ import { closeEffect, openCommand, openEffect } from './state';
13
9
 
14
10
  // TODO(burdon): Trigger completion on click.
15
11
  // TODO(burdon): Hide when dialog is open.
16
- export const floatingMenu = (options: FloatingMenuOptions) =>
12
+ export const floatingMenu = (options: CommandOptions) =>
17
13
  ViewPlugin.fromClass(
18
14
  class {
19
15
  button: HTMLElement;
@@ -35,11 +31,13 @@ export const floatingMenu = (options: FloatingMenuOptions) =>
35
31
  this.button.style.zIndex = '10';
36
32
  this.button.style.display = 'none';
37
33
 
38
- options.renderMenu(this.button, { onAction: () => openCommand(view) }, view);
34
+ options.onRenderMenu(this.button, () => {
35
+ openCommand(view);
36
+ });
39
37
  container.appendChild(this.button);
40
38
 
41
39
  // Listen for scroll events.
42
- container.addEventListener('scroll', this.scheduleUpdate.bind(this));
40
+ container.addEventListener('scroll', this.scheduleUpdate);
43
41
  this.scheduleUpdate();
44
42
  }
45
43
 
@@ -58,8 +56,7 @@ export const floatingMenu = (options: FloatingMenuOptions) =>
58
56
  if (this.rafId != null) {
59
57
  cancelAnimationFrame(this.rafId);
60
58
  }
61
-
62
- this.rafId = requestAnimationFrame(this.updateButtonPosition.bind(this));
59
+ this.rafId = requestAnimationFrame(() => this.updateButtonPosition());
63
60
  }
64
61
 
65
62
  updateButtonPosition() {
@@ -0,0 +1,79 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
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';
14
+ import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
15
+
16
+ export type PreviewOptions = {
17
+ onRenderPreview: (el: HTMLElement, props: { url: string; text: string }) => void;
18
+ };
19
+
20
+ /**
21
+ * Create image decorations.
22
+ */
23
+ export const preview = (options: PreviewOptions): Extension => {
24
+ return [
25
+ StateField.define<DecorationSet>({
26
+ create: (state) => buildDecorations(state, options),
27
+ update: (_: RangeSet<Decoration>, tr: Transaction) => buildDecorations(tr.state, options),
28
+ // TODO(burdon): Make atomic.
29
+ provide: (field) => EditorView.decorations.from(field),
30
+ }),
31
+ ];
32
+ };
33
+
34
+ // TODO(burdon): Make atomic.
35
+ const buildDecorations = (state: EditorState, options: PreviewOptions) => {
36
+ const builder = new RangeSetBuilder<Decoration>();
37
+ syntaxTree(state).iterate({
38
+ enter: (node) => {
39
+ if (node.name === 'Link') {
40
+ const urlNode = node.node.getChild('URL');
41
+ if (urlNode) {
42
+ const text = state.sliceDoc(node.from + 1, urlNode.from - 2);
43
+ const url = state.sliceDoc(urlNode.from, urlNode.to);
44
+ builder.add(
45
+ node.from,
46
+ node.to,
47
+ Decoration.replace({
48
+ block: true, // Prevent cursor from entering.
49
+ widget: new PreviewWidget(options.onRenderPreview, url, text),
50
+ }),
51
+ );
52
+ }
53
+ }
54
+ },
55
+ });
56
+
57
+ return builder.finish();
58
+ };
59
+
60
+ class PreviewWidget extends WidgetType {
61
+ constructor(
62
+ readonly _onRenderPreview: PreviewOptions['onRenderPreview'],
63
+ readonly _url: string,
64
+ readonly _text: string,
65
+ ) {
66
+ super();
67
+ }
68
+
69
+ override eq(other: this) {
70
+ return this._url === (other as any as PreviewWidget)._url;
71
+ }
72
+
73
+ override toDOM(view: EditorView) {
74
+ const root = document.createElement('div');
75
+ root.classList.add('cm-preview');
76
+ this._onRenderPreview(root, { url: this._url, text: this._text });
77
+ return root;
78
+ }
79
+ }
@@ -2,24 +2,25 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { StateField } from '@codemirror/state';
6
- import { showTooltip, type EditorView, type Tooltip, type TooltipView } from '@codemirror/view';
5
+ import { StateEffect, StateField } from '@codemirror/state';
6
+ import {
7
+ showTooltip,
8
+ type Command,
9
+ type EditorView,
10
+ type KeyBinding,
11
+ type Tooltip,
12
+ type TooltipView,
13
+ } from '@codemirror/view';
7
14
 
8
- import { closeEffect, type Action, openEffect } from './action';
9
15
  import { type CommandOptions } from './command';
10
- import { type RenderCallback } from '../../types';
11
16
  import { singleValueFacet } from '../../util';
12
17
 
13
- export const commandConfig = singleValueFacet<CommandOptions>();
14
-
15
- export type PopupOptions = {
16
- renderDialog: RenderCallback<{ onAction: (action?: Action) => void }>;
17
- };
18
-
19
18
  type CommandState = {
20
19
  tooltip?: Tooltip | null;
21
20
  };
22
21
 
22
+ export const commandConfig = singleValueFacet<CommandOptions>();
23
+
23
24
  export const commandState = StateField.define<CommandState>({
24
25
  create: () => ({}),
25
26
  update: (state, tr) => {
@@ -28,8 +29,8 @@ export const commandState = StateField.define<CommandState>({
28
29
  return {};
29
30
  }
30
31
 
31
- const { renderDialog } = tr.state.facet(commandConfig);
32
- if (effect.is(openEffect) && renderDialog) {
32
+ if (effect.is(openEffect)) {
33
+ const options = tr.state.facet(commandConfig);
33
34
  const { pos, fullWidth } = effect.value;
34
35
  const tooltip: Tooltip = {
35
36
  pos,
@@ -37,49 +38,38 @@ export const commandState = StateField.define<CommandState>({
37
38
  arrow: false,
38
39
  strictSide: true,
39
40
  create: (view: EditorView) => {
40
- const root = document.createElement('div');
41
-
41
+ const dom = document.createElement('div');
42
42
  const tooltipView: TooltipView = {
43
- dom: root,
43
+ dom,
44
44
  mount: (view: EditorView) => {
45
45
  if (fullWidth) {
46
- const parent = root.parentElement!;
46
+ const parent = dom.parentElement!;
47
47
  const { paddingLeft, paddingRight } = window.getComputedStyle(parent);
48
48
  const widthWithoutPadding = parent.clientWidth - parseFloat(paddingLeft) - parseFloat(paddingRight);
49
- root.style.width = `${widthWithoutPadding}px`;
49
+ dom.style.width = `${widthWithoutPadding}px`;
50
50
  }
51
51
 
52
52
  // Render react component.
53
- renderDialog(
54
- root,
55
- {
56
- onAction: (action) => {
57
- view.dispatch({ effects: closeEffect.of(null) });
58
- switch (action?.type) {
59
- case 'insert': {
60
- // Insert into editor.
61
- const text = action.text + '\n';
62
- view.dispatch({
63
- changes: { from: pos, insert: text },
64
- selection: { anchor: pos + text.length },
65
- });
66
- break;
67
- }
68
- }
69
-
70
- // NOTE: Truncates text if set focus immediately.
71
- requestAnimationFrame(() => view.focus());
72
- },
73
- },
74
- view,
75
- );
53
+ options.onRenderDialog(dom, (action) => {
54
+ view.dispatch({ effects: closeEffect.of(null) });
55
+ if (action?.insert?.length) {
56
+ // Insert into editor.
57
+ const text = action.insert + '\n';
58
+ view.dispatch({
59
+ changes: { from: pos, insert: text },
60
+ selection: { anchor: pos + text.length },
61
+ });
62
+ }
63
+
64
+ // NOTE: Truncates text if set focus immediately.
65
+ requestAnimationFrame(() => view.focus());
66
+ });
76
67
  },
77
68
  };
78
69
 
79
70
  return tooltipView;
80
71
  },
81
72
  };
82
-
83
73
  return { tooltip };
84
74
  }
85
75
  }
@@ -88,3 +78,33 @@ export const commandState = StateField.define<CommandState>({
88
78
  },
89
79
  provide: (field) => [showTooltip.from(field, (value) => value.tooltip ?? null)],
90
80
  });
81
+
82
+ export const openEffect = StateEffect.define<{ pos: number; fullWidth?: boolean }>();
83
+ export const closeEffect = StateEffect.define<null>();
84
+
85
+ export const openCommand: Command = (view: EditorView) => {
86
+ if (view.state.field(commandState, false)) {
87
+ const selection = view.state.selection.main;
88
+ const line = view.state.doc.lineAt(selection.from);
89
+ if (line.from === selection.from && line.from === line.to) {
90
+ view.dispatch({ effects: openEffect.of({ pos: selection.anchor, fullWidth: true }) });
91
+ return true;
92
+ }
93
+ }
94
+
95
+ return false;
96
+ };
97
+
98
+ export const closeCommand: Command = (view: EditorView) => {
99
+ if (view.state.field(commandState, false)) {
100
+ view.dispatch({ effects: closeEffect.of(null) });
101
+ return true;
102
+ }
103
+
104
+ return false;
105
+ };
106
+
107
+ export const commandKeyBindings: readonly KeyBinding[] = [
108
+ { key: '/', run: openCommand },
109
+ { key: 'Escape', run: closeCommand },
110
+ ];
@@ -25,13 +25,13 @@ import sortBy from 'lodash.sortby';
25
25
  import { useEffect, useMemo } from 'react';
26
26
 
27
27
  import { debounce, type CleanupFn } from '@dxos/async';
28
- import { type Live } from '@dxos/live-object';
28
+ import { type ReactiveObject } from '@dxos/live-object';
29
29
  import { log } from '@dxos/log';
30
30
  import { isNonNullable } from '@dxos/util';
31
31
 
32
32
  import { documentId } from './selection';
33
33
  import { type EditorToolbarState } from '../components';
34
- import { type RenderCallback, type Comment, type Range } from '../types';
34
+ import { type Comment, type Range } from '../types';
35
35
  import { Cursor, overlap, singleValueFacet, callbackWrapper } from '../util';
36
36
 
37
37
  //
@@ -345,10 +345,6 @@ export type CommentsOptions = {
345
345
  * Key shortcut to create a new thread.
346
346
  */
347
347
  key?: string;
348
- /**
349
- * Called to render tooltip.
350
- */
351
- renderTooltip?: RenderCallback<{ shortcut: string }>;
352
348
  /**
353
349
  * Called to create a new thread and return the thread id.
354
350
  */
@@ -365,6 +361,10 @@ export type CommentsOptions = {
365
361
  * Called to notify which thread is currently closest to the cursor.
366
362
  */
367
363
  onSelect?: (state: CommentsState) => void;
364
+ /**
365
+ * Called to render tooltip.
366
+ */
367
+ onHover?: (el: Element, shortcut: string) => void;
368
368
  };
369
369
 
370
370
  const optionsFacet = singleValueFacet<CommentsOptions>();
@@ -408,7 +408,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
408
408
  // Hover tooltip (for key shortcut hints, etc.)
409
409
  // TODO(burdon): Factor out to generic hints extension for current selection/line.
410
410
  //
411
- options.renderTooltip &&
411
+ options.onHover &&
412
412
  hoverTooltip(
413
413
  (view, pos) => {
414
414
  const selection = view.state.selection.main;
@@ -419,7 +419,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
419
419
  above: true,
420
420
  create: () => {
421
421
  const el = document.createElement('div');
422
- options.renderTooltip!(el, { shortcut }, view);
422
+ options.onHover!(el, shortcut);
423
423
  return { dom: el, offset: { x: 0, y: 8 } };
424
424
  },
425
425
  };
@@ -606,7 +606,7 @@ export const createExternalCommentSync = (
606
606
  },
607
607
  );
608
608
 
609
- export const useCommentState = (state: Live<EditorToolbarState>): Extension => {
609
+ export const useCommentState = (state: ReactiveObject<EditorToolbarState>): Extension => {
610
610
  return useMemo(
611
611
  () =>
612
612
  EditorView.updateListener.of((update) => {
@@ -29,7 +29,7 @@ export const folding = (_props: FoldingOptions = {}): Extension => [
29
29
  const el = createElement('div', { className: 'flex h-full items-center' });
30
30
  return renderRoot(
31
31
  el,
32
- <Icon icon='ph--caret-right--bold' size={3} classNames={['mx-3 cursor-pointer', open && 'rotate-90']} />,
32
+ <Icon icon='ph--caret-right--regular' size={3} classNames={['mx-3 cursor-pointer', open && 'rotate-90']} />,
33
33
  );
34
34
  },
35
35
  }),
@@ -18,6 +18,5 @@ export * from './listener';
18
18
  export * from './markdown';
19
19
  export * from './mention';
20
20
  export * from './modes';
21
- export * from './preview';
22
21
  export * from './selection';
23
22
  export * from './typewriter';
@@ -56,9 +56,8 @@ export const adjustChanges = () => {
56
56
  // Check for URL.
57
57
  const url = getValidUrl(update.view.state.sliceDoc(fromB, toB));
58
58
  if (url) {
59
- // Check if pasting inside existing link.
60
59
  const node = tree.resolveInner(fromA, -1);
61
- const invalidPositions = new Set(['Code', 'CodeText', 'FencedCode', 'Link', 'LinkMark', 'URL']);
60
+ const invalidPositions = new Set(['Link', 'LinkMark', 'Code', 'CodeText', 'FencedCode', 'URL']);
62
61
  if (!invalidPositions.has(node?.name)) {
63
62
  const replacedText = tr.startState.sliceDoc(fromA, toA);
64
63
  adjustments.push({ from: fromA, to: toB, insert: createLink(url, replacedText) });
@@ -85,7 +84,7 @@ export const adjustChanges = () => {
85
84
  }
86
85
  }
87
86
 
88
- // TODO(burdon): Is this the right way to augment changes? Alt: EditorState.transactionFilter
87
+ // TODO(burdon): Is this the right way to augment changes?
89
88
  if (adjustments.length) {
90
89
  setTimeout(() => {
91
90
  update.view.dispatch(