@dxos/react-ui-editor 0.8.1-staging.391c573 → 0.8.1-staging.97aedb1

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 (55) hide show
  1. package/dist/lib/browser/index.mjs +383 -255
  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 +415 -290
  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 +383 -255
  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/InputMode.stories.d.ts +2 -2
  11. package/dist/types/src/TextEditor.stories.d.ts +5 -40
  12. package/dist/types/src/TextEditor.stories.d.ts.map +1 -1
  13. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
  14. package/dist/types/src/defaults.d.ts +2 -0
  15. package/dist/types/src/defaults.d.ts.map +1 -1
  16. package/dist/types/src/extensions/command/command.d.ts +4 -2
  17. package/dist/types/src/extensions/command/command.d.ts.map +1 -1
  18. package/dist/types/src/extensions/command/hint.d.ts.map +1 -1
  19. package/dist/types/src/extensions/command/menu.d.ts +12 -0
  20. package/dist/types/src/extensions/command/menu.d.ts.map +1 -0
  21. package/dist/types/src/extensions/command/preview.d.ts +12 -0
  22. package/dist/types/src/extensions/command/preview.d.ts.map +1 -0
  23. package/dist/types/src/extensions/command/state.d.ts.map +1 -1
  24. package/dist/types/src/extensions/comments.d.ts +3 -3
  25. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  26. package/dist/types/src/extensions/factories.d.ts +2 -1
  27. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  28. package/dist/types/src/extensions/folding.d.ts +2 -8
  29. package/dist/types/src/extensions/folding.d.ts.map +1 -1
  30. package/dist/types/src/extensions/selection.d.ts +6 -1
  31. package/dist/types/src/extensions/selection.d.ts.map +1 -1
  32. package/dist/types/src/{styles/stack-item-content-class-names.d.ts → fragments.d.ts} +1 -1
  33. package/dist/types/src/fragments.d.ts.map +1 -0
  34. package/dist/types/src/index.d.ts +0 -1
  35. package/dist/types/src/index.d.ts.map +1 -1
  36. package/dist/types/src/styles/theme.d.ts.map +1 -1
  37. package/package.json +27 -27
  38. package/src/InputMode.stories.tsx +4 -4
  39. package/src/TextEditor.stories.tsx +183 -61
  40. package/src/components/EditorToolbar/EditorToolbar.tsx +4 -5
  41. package/src/defaults.ts +12 -0
  42. package/src/extensions/command/command.ts +21 -2
  43. package/src/extensions/command/hint.ts +3 -0
  44. package/src/extensions/command/menu.ts +100 -0
  45. package/src/extensions/command/preview.ts +79 -0
  46. package/src/extensions/command/state.ts +9 -4
  47. package/src/extensions/comments.ts +6 -10
  48. package/src/extensions/factories.ts +4 -3
  49. package/src/extensions/folding.tsx +30 -73
  50. package/src/extensions/selection.ts +41 -21
  51. package/src/{styles/stack-item-content-class-names.ts → fragments.ts} +4 -2
  52. package/src/index.ts +0 -4
  53. package/src/styles/theme.ts +6 -1
  54. package/src/util/debug.ts +1 -1
  55. package/dist/types/src/styles/stack-item-content-class-names.d.ts.map +0 -1
@@ -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
+ }
@@ -4,10 +4,10 @@
4
4
 
5
5
  import { StateEffect, StateField } from '@codemirror/state';
6
6
  import {
7
+ showTooltip,
7
8
  type Command,
8
9
  type EditorView,
9
10
  type KeyBinding,
10
- showTooltip,
11
11
  type Tooltip,
12
12
  type TooltipView,
13
13
  } from '@codemirror/view';
@@ -50,14 +50,17 @@ export const commandState = StateField.define<CommandState>({
50
50
  }
51
51
 
52
52
  // Render react component.
53
- options.onRender(dom, (action) => {
53
+ options.onRenderDialog(dom, (action) => {
54
54
  view.dispatch({ effects: closeEffect.of(null) });
55
55
  if (action?.insert?.length) {
56
+ // Insert into editor.
57
+ const text = action.insert + '\n';
56
58
  view.dispatch({
57
- changes: { from: pos, insert: action.insert },
58
- selection: { anchor: pos + action.insert.length },
59
+ changes: { from: pos, insert: text },
60
+ selection: { anchor: pos + text.length },
59
61
  });
60
62
  }
63
+
61
64
  // NOTE: Truncates text if set focus immediately.
62
65
  requestAnimationFrame(() => view.focus());
63
66
  });
@@ -88,6 +91,7 @@ export const openCommand: Command = (view: EditorView) => {
88
91
  return true;
89
92
  }
90
93
  }
94
+
91
95
  return false;
92
96
  };
93
97
 
@@ -96,6 +100,7 @@ export const closeCommand: Command = (view: EditorView) => {
96
100
  view.dispatch({ effects: closeEffect.of(null) });
97
101
  return true;
98
102
  }
103
+
99
104
  return false;
100
105
  };
101
106
 
@@ -4,12 +4,12 @@
4
4
 
5
5
  import { invertedEffects } from '@codemirror/commands';
6
6
  import {
7
+ type ChangeDesc,
8
+ type EditorState,
7
9
  type Extension,
8
10
  StateEffect,
9
11
  StateField,
10
12
  type Text,
11
- type ChangeDesc,
12
- type EditorState,
13
13
  } from '@codemirror/state';
14
14
  import {
15
15
  hoverTooltip,
@@ -24,7 +24,7 @@ import {
24
24
  import sortBy from 'lodash.sortby';
25
25
  import { useEffect, useMemo } from 'react';
26
26
 
27
- import { debounce, type UnsubscribeCallback } from '@dxos/async';
27
+ import { debounce, type CleanupFn } from '@dxos/async';
28
28
  import { type ReactiveObject } from '@dxos/live-object';
29
29
  import { log } from '@dxos/log';
30
30
  import { isNonNullable } from '@dxos/util';
@@ -181,6 +181,7 @@ const handleCommentClick = EditorView.domEventHandlers({
181
181
  return false;
182
182
  },
183
183
  });
184
+
184
185
  //
185
186
  // Cut-and-paste.
186
187
  //
@@ -575,12 +576,7 @@ const hasActiveSelection = (state: EditorState): boolean => {
575
576
  class ExternalCommentSync implements PluginValue {
576
577
  private readonly unsubscribe: () => void;
577
578
 
578
- constructor(
579
- view: EditorView,
580
- id: string,
581
- subscribe: (sink: () => void) => UnsubscribeCallback,
582
- getComments: () => Comment[],
583
- ) {
579
+ constructor(view: EditorView, id: string, subscribe: (sink: () => void) => CleanupFn, getComments: () => Comment[]) {
584
580
  const updateComments = () => {
585
581
  const comments = getComments();
586
582
  if (id === view.state.facet(documentId)) {
@@ -599,7 +595,7 @@ class ExternalCommentSync implements PluginValue {
599
595
  // TODO(burdon): Needs comment.
600
596
  export const createExternalCommentSync = (
601
597
  id: string,
602
- subscribe: (sink: () => void) => UnsubscribeCallback,
598
+ subscribe: (sink: () => void) => CleanupFn,
603
599
  getComments: () => Comment[],
604
600
  ): Extension =>
605
601
  ViewPlugin.fromClass(
@@ -61,7 +61,8 @@ export type BasicExtensionsOptions = {
61
61
  lineNumbers?: boolean;
62
62
  lineWrapping?: boolean;
63
63
  placeholder?: string;
64
- readonly?: boolean;
64
+ /** If true user cannot edit the text, but they can still select and copy it. */
65
+ readOnly?: boolean;
65
66
  search?: boolean;
66
67
  scrollPastEnd?: boolean;
67
68
  standardKeymap?: boolean;
@@ -73,7 +74,6 @@ const defaultBasicOptions: BasicExtensionsOptions = {
73
74
  bracketMatching: true,
74
75
  closeBrackets: true,
75
76
  drawSelection: true,
76
- editable: true,
77
77
  focus: true,
78
78
  history: true,
79
79
  keymap: 'standard',
@@ -101,13 +101,14 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
101
101
  props.closeBrackets && closeBrackets(),
102
102
  props.dropCursor && dropCursor(),
103
103
  props.drawSelection && drawSelection({ cursorBlinkRate: 1_200 }),
104
+ props.editable !== undefined && EditorView.editable.of(props.editable),
104
105
  props.focus && focus,
105
106
  props.highlightActiveLine && highlightActiveLine(),
106
107
  props.history && history(),
107
108
  props.lineNumbers && lineNumbers(),
108
109
  props.lineWrapping && EditorView.lineWrapping,
109
110
  props.placeholder && placeholder(props.placeholder),
110
- props.readonly && [EditorState.readOnly.of(true), EditorView.editable.of(false)],
111
+ props.readOnly !== undefined && EditorState.readOnly.of(props.readOnly),
111
112
  props.scrollPastEnd && scrollPastEnd(),
112
113
  props.tabSize && EditorState.tabSize.of(props.tabSize),
113
114
 
@@ -2,88 +2,45 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { codeFolding, foldGutter, foldedRanges, foldEffect } from '@codemirror/language';
5
+ import { codeFolding, foldGutter } from '@codemirror/language';
6
6
  import { type Extension } from '@codemirror/state';
7
7
  import { EditorView } from '@codemirror/view';
8
8
  import React from 'react';
9
9
 
10
- import { debounce } from '@dxos/async';
11
10
  import { Icon } from '@dxos/react-ui';
12
11
 
13
- import { documentId } from './selection';
14
12
  import { createElement, renderRoot } from '../util';
15
13
 
16
- export type FoldRange = {
17
- from: number;
18
- to: number;
19
- };
20
-
21
- export type FoldState = {
22
- foldedRanges: FoldRange[];
23
- };
14
+ export type FoldingOptions = {};
24
15
 
25
16
  /**
26
17
  * https://codemirror.net/examples/gutter
27
18
  */
28
- export const folding = (state: Record<string, FoldState> = {}): Extension => {
29
- const setState = (id: string, foldState: FoldState) => {
30
- state[id] = foldState;
31
- };
32
- const setStateDebounced = debounce(setState, 1_000);
33
- let initialized = false;
34
-
35
- return [
36
- codeFolding({
37
- placeholderDOM: () => {
38
- return document.createElement('span'); // Collapse content.
39
- },
40
- }),
41
- foldGutter({
42
- markerDOM: (open) => {
43
- // TODO(burdon): Use sprite directly.
44
- const el = createElement('div', { className: 'flex h-full items-center' });
45
- return renderRoot(
46
- el,
47
- <Icon icon='ph--caret-right--regular' size={3} classNames={['mx-3 cursor-pointer', open && 'rotate-90']} />,
48
- );
49
- },
50
- }),
51
- EditorView.theme({
52
- '.cm-foldGutter': {
53
- opacity: 0.3,
54
- transition: 'opacity 0.3s',
55
- width: '32px',
56
- },
57
- '.cm-foldGutter:hover': {
58
- opacity: 1,
59
- },
60
- }),
61
- EditorView.updateListener.of(({ view }) => {
62
- const id = view.state.facet(documentId);
63
- if (!id) {
64
- return;
65
- }
66
-
67
- // Handle initial state restoration only once
68
- if (!initialized) {
69
- initialized = true;
70
- const foldState = state[id];
71
- if (foldState?.foldedRanges?.length) {
72
- view.dispatch({
73
- effects: foldState.foldedRanges.map((range) => foldEffect.of({ from: range.from, to: range.to })),
74
- });
75
- }
76
- return;
77
- }
78
-
79
- // Track fold changes for saving state
80
- const decorations = foldedRanges(view.state);
81
- const ranges: FoldRange[] = [];
82
- decorations.between(0, view.state.doc.length, (from: number, to: number) => {
83
- ranges.push({ from, to });
84
- });
85
- const foldState: FoldState = { foldedRanges: ranges };
86
- setStateDebounced?.(id, foldState);
87
- }),
88
- ];
89
- };
19
+ // TODO(burdon): Remember folding state (to state).
20
+ export const folding = (_props: FoldingOptions = {}): Extension => [
21
+ codeFolding({
22
+ placeholderDOM: () => {
23
+ return document.createElement('span'); // Collapse content.
24
+ },
25
+ }),
26
+ foldGutter({
27
+ markerDOM: (open) => {
28
+ // TODO(burdon): Use sprite directly.
29
+ const el = createElement('div', { className: 'flex h-full items-center' });
30
+ return renderRoot(
31
+ el,
32
+ <Icon icon='ph--caret-right--regular' size={3} classNames={['mx-3 cursor-pointer', open && 'rotate-90']} />,
33
+ );
34
+ },
35
+ }),
36
+ EditorView.theme({
37
+ '.cm-foldGutter': {
38
+ opacity: 0.3,
39
+ transition: 'opacity 0.3s',
40
+ width: '32px',
41
+ },
42
+ '.cm-foldGutter:hover': {
43
+ opacity: 1,
44
+ },
45
+ }),
46
+ ];
@@ -6,6 +6,8 @@ import { type Extension, Transaction, type TransactionSpec } from '@codemirror/s
6
6
  import { EditorView, keymap } from '@codemirror/view';
7
7
 
8
8
  import { debounce } from '@dxos/async';
9
+ import { invariant } from '@dxos/invariant';
10
+ import { isNotFalsy } from '@dxos/util';
9
11
 
10
12
  import { singleValueFacet } from '../util';
11
13
 
@@ -24,6 +26,11 @@ export type EditorSelectionState = {
24
26
  selection?: EditorSelection;
25
27
  };
26
28
 
29
+ export type EditorStateStore = {
30
+ setState: (id: string, state: EditorSelectionState) => void;
31
+ getState: (id: string) => EditorSelectionState | undefined;
32
+ };
33
+
27
34
  const stateRestoreAnnotation = 'dxos.org/cm/state-restore';
28
35
 
29
36
  export const createEditorStateTransaction = ({ scrollTo, selection }: EditorSelectionState): TransactionSpec => {
@@ -35,13 +42,23 @@ export const createEditorStateTransaction = ({ scrollTo, selection }: EditorSele
35
42
  };
36
43
  };
37
44
 
45
+ export const createEditorStateStore = (keyPrefix: string): EditorStateStore => ({
46
+ getState: (id) => {
47
+ invariant(id);
48
+ const state = localStorage.getItem(`${keyPrefix}/${id}`);
49
+ return state ? JSON.parse(state) : undefined;
50
+ },
51
+
52
+ setState: (id, state) => {
53
+ invariant(id);
54
+ localStorage.setItem(`${keyPrefix}/${id}`, JSON.stringify(state));
55
+ },
56
+ });
57
+
38
58
  /**
39
59
  * Track scrolling and selection state to be restored when switching to document.
40
60
  */
41
- export const selectionState = (state: Record<string, EditorSelectionState> = {}): Extension => {
42
- const setState = (id: string, selectionState: EditorSelectionState) => {
43
- state[id] = selectionState;
44
- };
61
+ export const selectionState = ({ getState, setState }: Partial<EditorStateStore> = {}): Extension => {
45
62
  const setStateDebounced = debounce(setState!, 1_000);
46
63
 
47
64
  return [
@@ -57,24 +74,27 @@ export const selectionState = (state: Record<string, EditorSelectionState> = {})
57
74
  return;
58
75
  }
59
76
 
60
- const { scrollTop } = view.scrollDOM;
61
- const pos = view.posAtCoords({ x: 0, y: scrollTop });
62
- if (pos !== null) {
63
- const { anchor, head } = view.state.selection.main;
64
- setStateDebounced(id, { scrollTo: pos, selection: { anchor, head } });
77
+ if (setState) {
78
+ const { scrollTop } = view.scrollDOM;
79
+ const pos = view.posAtCoords({ x: 0, y: scrollTop });
80
+ if (pos !== null) {
81
+ const { anchor, head } = view.state.selection.main;
82
+ setStateDebounced(id, { scrollTo: pos, selection: { anchor, head } });
83
+ }
65
84
  }
66
85
  }),
67
- keymap.of([
68
- {
69
- key: 'ctrl-r', // TODO(burdon): Setting to jump back to selection.
70
- run: (view) => {
71
- const selection = state[view.state.facet(documentId)];
72
- if (selection) {
73
- view.dispatch(createEditorStateTransaction(selection));
74
- }
75
- return true;
86
+ getState &&
87
+ keymap.of([
88
+ {
89
+ key: 'ctrl-r', // TODO(burdon): Setting to jump back to selection.
90
+ run: (view) => {
91
+ const state = getState(view.state.facet(documentId));
92
+ if (state) {
93
+ view.dispatch(createEditorStateTransaction(state));
94
+ }
95
+ return true;
96
+ },
76
97
  },
77
- },
78
- ]),
79
- ];
98
+ ]),
99
+ ].filter(isNotFalsy);
80
100
  };
@@ -4,10 +4,12 @@
4
4
 
5
5
  import { mx } from '@dxos/react-ui-theme';
6
6
 
7
+ // TODO(burdon): Move this to a common plugin.
8
+
7
9
  export const stackItemContentEditorClassNames = (role?: string) =>
8
10
  mx(
9
- 'dx-focus-ring-inset data-[toolbar=disabled]:pbs-2 attention-surface',
10
- role === 'article' ? 'min-bs-0' : '[&_.cm-scroller]:overflow-hidden [&_.cm-scroller]:min-bs-24',
11
+ 'attention-surface dx-focus-ring-inset data-[toolbar=disabled]:pbs-2',
12
+ role === 'section' ? '[&_.cm-scroller]:overflow-hidden [&_.cm-scroller]:min-bs-24' : 'min-bs-0',
11
13
  );
12
14
 
13
15
  export const stackItemContentToolbarClassNames = (role?: string) =>
package/src/index.ts CHANGED
@@ -14,10 +14,6 @@ export * from './components';
14
14
  export * from './defaults';
15
15
  export * from './extensions';
16
16
  export * from './hooks';
17
- export {
18
- stackItemContentEditorClassNames,
19
- stackItemContentToolbarClassNames,
20
- } from './styles/stack-item-content-class-names';
21
17
  export * from './types';
22
18
  export * from './util';
23
19
 
@@ -77,6 +77,10 @@ export const defaultTheme: ThemeStyles = {
77
77
  background: 'transparent',
78
78
  },
79
79
  '.cm-gutter': {},
80
+ '.cm-gutter.cm-lineNumbers': {
81
+ paddingRight: '4px',
82
+ borderRight: '1px solid var(--dx-separator)',
83
+ },
80
84
  '.cm-gutter.cm-lineNumbers .cm-gutterElement': {
81
85
  minWidth: '40px',
82
86
  alignContent: 'center',
@@ -86,7 +90,7 @@ export const defaultTheme: ThemeStyles = {
86
90
  */
87
91
  '.cm-gutterElement': {
88
92
  alignItems: 'center',
89
- fontSize: '16px',
93
+ fontSize: '12px',
90
94
  },
91
95
 
92
96
  /**
@@ -137,6 +141,7 @@ export const defaultTheme: ThemeStyles = {
137
141
  '.cm-link': {
138
142
  textDecorationLine: 'underline',
139
143
  textDecorationThickness: '1px',
144
+ textDecorationColor: 'var(--dx-separator)',
140
145
  textUnderlineOffset: '2px',
141
146
  borderRadius: '.125rem',
142
147
  },
package/src/util/debug.ts CHANGED
@@ -59,6 +59,6 @@ export const logChanges = (trs: readonly Transaction[]) => {
59
59
  .filter(Boolean);
60
60
 
61
61
  if (changes.length) {
62
- log.info('changes', { changes });
62
+ log('changes', { changes });
63
63
  }
64
64
  };
@@ -1 +0,0 @@
1
- {"version":3,"file":"stack-item-content-class-names.d.ts","sourceRoot":"","sources":["../../../../src/styles/stack-item-content-class-names.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,gCAAgC,UAAW,MAAM,WAI3D,CAAC;AAEJ,eAAO,MAAM,iCAAiC,UAAW,MAAM,WAI5D,CAAC"}