@dxos/react-ui-editor 0.4.8-main.46f461d → 0.4.8-main.49cea32

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 (31) hide show
  1. package/dist/lib/browser/index.mjs +1289 -1328
  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/TextEditor.stories.d.ts.map +1 -1
  5. package/dist/types/src/components/Toolbar/Toolbar.d.ts +1 -6
  6. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  7. package/dist/types/src/extensions/dnd.d.ts +7 -1
  8. package/dist/types/src/extensions/dnd.d.ts.map +1 -1
  9. package/dist/types/src/extensions/factories.d.ts +2 -1
  10. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  11. package/dist/types/src/extensions/index.d.ts +0 -1
  12. package/dist/types/src/extensions/index.d.ts.map +1 -1
  13. package/dist/types/src/extensions/markdown/action.d.ts +9 -0
  14. package/dist/types/src/extensions/markdown/action.d.ts.map +1 -0
  15. package/dist/types/src/extensions/markdown/index.d.ts +1 -0
  16. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  17. package/dist/types/src/hooks/useActionHandler.d.ts +2 -2
  18. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -1
  19. package/package.json +24 -24
  20. package/src/components/TextEditor/TextEditor.stories.tsx +14 -2
  21. package/src/components/Toolbar/Toolbar.tsx +2 -26
  22. package/src/extensions/command/state.ts +2 -2
  23. package/src/extensions/dnd.ts +31 -9
  24. package/src/extensions/factories.ts +19 -16
  25. package/src/extensions/index.ts +0 -1
  26. package/src/extensions/markdown/action.ts +112 -0
  27. package/src/extensions/markdown/index.ts +1 -0
  28. package/src/hooks/useActionHandler.ts +4 -89
  29. package/dist/types/src/extensions/cursor-line-margin.d.ts +0 -7
  30. package/dist/types/src/extensions/cursor-line-margin.d.ts.map +0 -1
  31. package/src/extensions/cursor-line-margin.ts +0 -94
@@ -42,7 +42,7 @@ import {
42
42
  } from '@dxos/react-ui';
43
43
  import { getSize } from '@dxos/react-ui-theme';
44
44
 
45
- import { type Formatting } from '../../extensions';
45
+ import { type Action, type ActionType, type Formatting } from '../../extensions';
46
46
  import { translationKey } from '../../translations';
47
47
 
48
48
  const tooltipProps = { side: 'top' as const, classNames: 'z-10' };
@@ -57,30 +57,6 @@ const HeadingIcons: { [key: string]: Icon } = {
57
57
  '6': TextHSix,
58
58
  };
59
59
 
60
- // TODO(burdon): Revert to string to make extensible?
61
- export type ActionType =
62
- | 'blockquote'
63
- | 'strong'
64
- | 'codeblock'
65
- | 'comment'
66
- | 'heading'
67
- | 'image'
68
- | 'emphasis'
69
- | 'code'
70
- | 'link'
71
- | 'list-bullet'
72
- | 'list-ordered'
73
- | 'list-task'
74
- | 'mention'
75
- | 'prompt'
76
- | 'strikethrough'
77
- | 'table';
78
-
79
- export type Action = {
80
- type: ActionType;
81
- data?: any;
82
- };
83
-
84
60
  export type ToolbarProps = ThemedClassName<
85
61
  PropsWithChildren<{
86
62
  state: Formatting | undefined;
@@ -358,7 +334,7 @@ const MarkdownCustom = ({ onUpload }: MarkdownCustomOptions = {}) => {
358
334
 
359
335
  useEffect(() => {
360
336
  if (onUpload && acceptedFiles.length) {
361
- setTimeout(async () => {
337
+ requestAnimationFrame(async () => {
362
338
  // NOTE: Clone file since react-dropzone patches in a non-standard `path` property, which confuses IPFS.
363
339
  const f = acceptedFiles[0];
364
340
  const file = new File([f], f.name, {
@@ -59,8 +59,8 @@ export const commandState = StateField.define<CommandState>({
59
59
  selection: { anchor: pos + action.insert.length },
60
60
  });
61
61
  }
62
- // TODO(burdon): Truncates text if set focus immediately.
63
- setTimeout(() => view.focus());
62
+ // NOTE: Truncates text if set focus immediately.
63
+ requestAnimationFrame(() => view.focus());
64
64
  });
65
65
  },
66
66
  };
@@ -3,15 +3,37 @@
3
3
  //
4
4
 
5
5
  import type { Extension } from '@codemirror/state';
6
- import { EditorView } from '@codemirror/view';
6
+ import { dropCursor, EditorView } from '@codemirror/view';
7
7
 
8
- import { log } from '@dxos/log';
8
+ import { getToken } from '../styles';
9
9
 
10
- // TODO(burdon): DND experiment.
11
- export const dnd = (): Extension => {
12
- return EditorView.domEventHandlers({
13
- drop: (event, view) => {
14
- log.info('domEventHandlersDrop', { event });
15
- },
16
- });
10
+ export type DNDOptions = { onDrop?: (view: EditorView, event: { files: FileList }) => void };
11
+
12
+ const styles = EditorView.baseTheme({
13
+ '.cm-dropCursor': {
14
+ borderLeft: `2px solid ${getToken('extend.colors.primary.500')}`,
15
+ color: getToken('extend.colors.primary.500'),
16
+ padding: '0 4px',
17
+ },
18
+ '.cm-dropCursor:after': {
19
+ content: '"←"',
20
+ },
21
+ });
22
+
23
+ export const dropFile = (options: DNDOptions = {}): Extension => {
24
+ return [
25
+ styles,
26
+ dropCursor(),
27
+ EditorView.domEventHandlers({
28
+ drop: (event, view) => {
29
+ event.preventDefault();
30
+ const files = event.dataTransfer?.files;
31
+ const pos = view.posAtCoords(event);
32
+ if (files?.length && pos !== null) {
33
+ view.dispatch({ selection: { anchor: pos } });
34
+ options.onDrop?.(view, { files });
35
+ }
36
+ },
37
+ }),
38
+ ];
17
39
  };
@@ -3,17 +3,16 @@
3
3
  //
4
4
 
5
5
  import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
6
- import { history, historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
6
+ import { defaultKeymap, history, historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
7
7
  import { bracketMatching } from '@codemirror/language';
8
8
  import { searchKeymap } from '@codemirror/search';
9
9
  import { EditorState, type Extension } from '@codemirror/state';
10
10
  import {
11
11
  EditorView,
12
- crosshairCursor,
12
+ type KeyBinding,
13
13
  drawSelection,
14
14
  dropCursor,
15
15
  highlightActiveLine,
16
- highlightSpecialChars,
17
16
  keymap,
18
17
  lineNumbers,
19
18
  placeholder,
@@ -48,37 +47,46 @@ export type BasicExtensionsOptions = {
48
47
  allowMultipleSelections?: boolean;
49
48
  bracketMatching?: boolean;
50
49
  closeBrackets?: boolean;
51
- crosshairCursor?: boolean;
52
50
  dropCursor?: boolean;
53
51
  drawSelection?: boolean;
54
52
  editable?: boolean;
55
53
  highlightActiveLine?: boolean;
56
54
  history?: boolean;
57
55
  indentWithTab?: boolean;
56
+ keymap?: null | 'default' | 'standard';
58
57
  lineNumbers?: boolean;
59
58
  lineWrapping?: boolean;
60
59
  placeholder?: string;
61
60
  readonly?: boolean;
62
61
  search?: boolean;
63
62
  scrollPastEnd?: boolean;
63
+ standardKeymap?: boolean;
64
64
  tabSize?: number;
65
65
  };
66
66
 
67
- const defaults: BasicExtensionsOptions = {
67
+ const defaultBasicOptions: BasicExtensionsOptions = {
68
68
  allowMultipleSelections: true,
69
69
  bracketMatching: true,
70
70
  closeBrackets: true,
71
71
  drawSelection: true,
72
72
  editable: true,
73
73
  history: true,
74
+ keymap: 'standard',
74
75
  lineWrapping: true,
75
76
  search: true,
76
77
  };
77
78
 
79
+ const keymaps: { [key: string]: readonly KeyBinding[] } = {
80
+ // https://codemirror.net/docs/ref/#commands.standardKeymap
81
+ standard: standardKeymap,
82
+ // https://codemirror.net/docs/ref/#commands.defaultKeymap
83
+ default: defaultKeymap,
84
+ };
85
+
78
86
  export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extension => {
79
- const props: BasicExtensionsOptions = defaultsDeep({}, _props, defaults);
87
+ const props: BasicExtensionsOptions = defaultsDeep({}, _props, defaultBasicOptions);
80
88
  return [
81
- // TODO(burdon): Doesn't catch errors in keymap functions.
89
+ // NOTE: Doesn't catch errors in keymap functions.
82
90
  EditorView.exceptionSink.of((err) => {
83
91
  log.catch(err);
84
92
  }),
@@ -86,11 +94,9 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
86
94
  props.allowMultipleSelections && EditorState.allowMultipleSelections.of(true),
87
95
  props.bracketMatching && bracketMatching(),
88
96
  props.closeBrackets && closeBrackets(),
89
- props.crosshairCursor && crosshairCursor(),
90
97
  props.dropCursor && dropCursor(),
91
98
  props.drawSelection && drawSelection(),
92
99
  props.highlightActiveLine && highlightActiveLine(),
93
- highlightSpecialChars(),
94
100
  props.history && history(),
95
101
  props.lineNumbers && lineNumbers(),
96
102
  props.lineWrapping && EditorView.lineWrapping,
@@ -102,12 +108,9 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
102
108
  // https://codemirror.net/docs/ref/#view.KeyBinding
103
109
  keymap.of(
104
110
  [
105
- // https://codemirror.net/docs/ref/#commands.standardKeymap
106
- ...standardKeymap,
107
-
111
+ ...((props.keymap && keymaps[props.keymap]) ?? []),
108
112
  // https://codemirror.net/docs/ref/#commands.indentWithTab
109
- props.indentWithTab && indentWithTab,
110
-
113
+ ...(props.indentWithTab ? [indentWithTab] : []),
111
114
  // https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
112
115
  ...(props.closeBrackets ? closeBracketsKeymap : []),
113
116
  // https://codemirror.net/docs/ref/#commands.historyKeymap
@@ -136,14 +139,14 @@ export type ThemeExtensionsOptions = {
136
139
  };
137
140
  };
138
141
 
139
- const defaultSlots = {
142
+ const defaultThemeSlots = {
140
143
  editor: {
141
144
  className: 'w-full bs-full',
142
145
  },
143
146
  };
144
147
 
145
148
  export const createThemeExtensions = ({ theme, themeMode, slots: _slots }: ThemeExtensionsOptions = {}): Extension => {
146
- const slots = defaultsDeep({}, _slots, defaultSlots);
149
+ const slots = defaultsDeep({}, _slots, defaultThemeSlots);
147
150
  return [
148
151
  EditorView.baseTheme(defaultTheme),
149
152
  EditorView.darkTheme.of(themeMode === 'dark'),
@@ -10,7 +10,6 @@ export * from './blast';
10
10
  export * from './command';
11
11
  export * from './comments';
12
12
  export * from './cursor';
13
- export * from './cursor-line-margin';
14
13
  export * from './doc';
15
14
  export * from './dnd';
16
15
  export * from './factories';
@@ -0,0 +1,112 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type EditorView } from '@codemirror/view';
6
+
7
+ import {
8
+ Inline,
9
+ List,
10
+ addBlockquote,
11
+ addCodeblock,
12
+ addLink,
13
+ addList,
14
+ insertTable,
15
+ removeBlockquote,
16
+ removeCodeblock,
17
+ removeLink,
18
+ removeList,
19
+ setHeading,
20
+ setStyle,
21
+ toggleBlockquote,
22
+ toggleList,
23
+ toggleStyle,
24
+ } from './formatting';
25
+ import { createComment } from '../comments';
26
+
27
+ export type ActionType =
28
+ | 'blockquote'
29
+ | 'strong'
30
+ | 'codeblock'
31
+ | 'comment'
32
+ | 'heading'
33
+ | 'image'
34
+ | 'emphasis'
35
+ | 'code'
36
+ | 'link'
37
+ | 'list-bullet'
38
+ | 'list-ordered'
39
+ | 'list-task'
40
+ | 'mention'
41
+ | 'prompt'
42
+ | 'strikethrough'
43
+ | 'table';
44
+
45
+ export type Action = {
46
+ type: ActionType;
47
+ data?: any;
48
+ };
49
+
50
+ export type ActionHandler = (view: EditorView, action: Action) => void;
51
+
52
+ export const processAction: ActionHandler = (view, action) => {
53
+ let inlineType, listType;
54
+ switch (action.type) {
55
+ case 'heading':
56
+ setHeading(parseInt(action.data))(view);
57
+ break;
58
+
59
+ case 'strong':
60
+ case 'emphasis':
61
+ case 'strikethrough':
62
+ case 'code':
63
+ inlineType =
64
+ action.type === 'strong'
65
+ ? Inline.Strong
66
+ : action.type === 'emphasis'
67
+ ? Inline.Emphasis
68
+ : action.type === 'strikethrough'
69
+ ? Inline.Strikethrough
70
+ : Inline.Code;
71
+ (typeof action.data === 'boolean' ? setStyle(inlineType, action.data) : toggleStyle(inlineType))(view);
72
+ break;
73
+
74
+ case 'list-ordered':
75
+ case 'list-bullet':
76
+ case 'list-task':
77
+ listType =
78
+ action.type === 'list-ordered' ? List.Ordered : action.type === 'list-bullet' ? List.Bullet : List.Task;
79
+ (action.data === false ? removeList(listType) : action.data === true ? addList(listType) : toggleList(listType))(
80
+ view,
81
+ );
82
+ break;
83
+
84
+ case 'blockquote':
85
+ (action.data === false ? removeBlockquote : action.data === true ? addBlockquote : toggleBlockquote)(view);
86
+ break;
87
+ case 'codeblock':
88
+ (action.data === false ? removeCodeblock : addCodeblock)(view);
89
+ break;
90
+ case 'table':
91
+ insertTable(view);
92
+ break;
93
+
94
+ case 'link':
95
+ (action.data === false ? removeLink : addLink())(view);
96
+ break;
97
+
98
+ case 'image':
99
+ addLink({ url: action.data, image: true })(view);
100
+ break;
101
+
102
+ case 'comment':
103
+ createComment(view);
104
+ break;
105
+ }
106
+
107
+ requestAnimationFrame(() => {
108
+ if (!view.hasFocus) {
109
+ view.focus();
110
+ }
111
+ });
112
+ };
@@ -2,6 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ export * from './action';
5
6
  export * from './bundle';
6
7
  export * from './decorate';
7
8
  export * from './formatting';
@@ -2,96 +2,11 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import type { EditorView } from '@codemirror/view';
5
+ import { type EditorView } from '@codemirror/view';
6
6
 
7
- import type { ToolbarProps } from '../components';
8
- import {
9
- Inline,
10
- List,
11
- addBlockquote,
12
- addCodeblock,
13
- addLink,
14
- addList,
15
- createComment,
16
- insertTable,
17
- removeBlockquote,
18
- removeCodeblock,
19
- removeLink,
20
- removeList,
21
- setHeading,
22
- setStyle,
23
- toggleBlockquote,
24
- toggleList,
25
- toggleStyle,
26
- } from '../extensions';
7
+ import { type ToolbarProps } from '../components';
8
+ import { processAction } from '../extensions';
27
9
 
28
10
  export const useActionHandler = (view?: EditorView | null): ToolbarProps['onAction'] => {
29
- return (action) => {
30
- if (!view) {
31
- return;
32
- }
33
-
34
- let inlineType, listType;
35
- switch (action.type) {
36
- case 'heading':
37
- setHeading(parseInt(action.data))(view);
38
- break;
39
-
40
- case 'strong':
41
- case 'emphasis':
42
- case 'strikethrough':
43
- case 'code':
44
- inlineType =
45
- action.type === 'strong'
46
- ? Inline.Strong
47
- : action.type === 'emphasis'
48
- ? Inline.Emphasis
49
- : action.type === 'strikethrough'
50
- ? Inline.Strikethrough
51
- : Inline.Code;
52
- (typeof action.data === 'boolean' ? setStyle(inlineType, action.data) : toggleStyle(inlineType))(view);
53
- break;
54
-
55
- case 'list-ordered':
56
- case 'list-bullet':
57
- case 'list-task':
58
- listType =
59
- action.type === 'list-ordered' ? List.Ordered : action.type === 'list-bullet' ? List.Bullet : List.Task;
60
- (action.data === false
61
- ? removeList(listType)
62
- : action.data === true
63
- ? addList(listType)
64
- : toggleList(listType))(view);
65
- break;
66
-
67
- case 'blockquote':
68
- (action.data === false ? removeBlockquote : action.data === true ? addBlockquote : toggleBlockquote)(view);
69
- break;
70
- case 'codeblock':
71
- (action.data === false ? removeCodeblock : addCodeblock)(view);
72
- break;
73
- case 'table':
74
- insertTable(view);
75
- break;
76
-
77
- case 'link':
78
- (action.data === false ? removeLink : addLink())(view);
79
- break;
80
-
81
- case 'image':
82
- addLink({ url: action.data, image: true })(view);
83
- break;
84
-
85
- case 'comment':
86
- createComment(view);
87
- break;
88
- }
89
-
90
- // TODO(burdon): Hack otherwise remains on heading selector.
91
- setTimeout(() => {
92
- if (!view.hasFocus) {
93
- view.focus();
94
- }
95
- });
96
- };
11
+ return (action) => view && processAction(view, action);
97
12
  };
@@ -1,7 +0,0 @@
1
- import { type Extension } from '@codemirror/state';
2
- type Options = {
3
- lines?: number;
4
- };
5
- export declare const cursorLineMargin: (options?: Options) => Extension;
6
- export {};
7
- //# sourceMappingURL=cursor-line-margin.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cursor-line-margin.d.ts","sourceRoot":"","sources":["../../../../src/extensions/cursor-line-margin.ts"],"names":[],"mappings":"AAOA,OAAO,EAAoB,KAAK,SAAS,EAAe,MAAM,mBAAmB,CAAC;AAQlF,KAAK,OAAO,GAAG;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAGF,eAAO,MAAM,gBAAgB,aAAa,OAAO,KAAkB,SAyElE,CAAC"}
@@ -1,94 +0,0 @@
1
- //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- // Based upon @acheronfail’s implementation, fetched 16 Feb 2024
6
- // https://discuss.codemirror.net/t/cursorscrollmargin-for-v6/7448/5
7
-
8
- import { type EditorState, type Extension, Transaction } from '@codemirror/state';
9
- import { EditorView } from '@codemirror/view';
10
-
11
- import { log } from '@dxos/log';
12
-
13
- const annotation = 'cursorLineMargin';
14
- const lineAtPos = (s: EditorState, pos: number) => s.doc.lineAt(pos).number;
15
-
16
- type Options = {
17
- lines?: number;
18
- };
19
-
20
- // TODO(burdon): Remove. Buggy and creates scrolling problems. Find better (more general) way to deal with HALO notch.
21
- export const cursorLineMargin = (options: Options = { lines: 3 }): Extension => {
22
- const { lines } = options;
23
- if (!lines || lines <= 0) {
24
- return [];
25
- }
26
-
27
- return EditorView.updateListener.of(({ transactions, state, selectionSet, startState, view }) => {
28
- // make sure we don't trigger an infinite loop and ignore our own changes
29
- if (transactions.length < 1 || transactions.some((tr) => tr.isUserEvent(annotation))) {
30
- return;
31
- }
32
-
33
- const s = state;
34
- const { main } = s.selection;
35
-
36
- // editor rect
37
- const rect = view.dom.getBoundingClientRect();
38
- // top and bottom pixel positions of the visible region of the editor
39
- const viewportTop = rect.top - view.documentTop;
40
- const viewportBottom = rect.bottom - view.documentTop;
41
-
42
- // line with main selection
43
- const mainLine = lineAtPos(s, main.head);
44
- // top most visible line
45
- const _visTopLine = lineAtPos(s, view.lineBlockAtHeight(viewportTop).from);
46
-
47
- // bottom most visible line
48
- // NOTE: calculating this is slightly more complex - if the editor is
49
- // larger than all the lines in it, and the `scrollPastEnd()` extension is
50
- // enabled, then we need to calculate where the bottom most visible line
51
- // should be if it extended all the way to the bottom of the editor
52
- const botLineBlk = view.lineBlockAtHeight(viewportBottom);
53
- const visBotLineDoc = lineAtPos(s, Math.min(botLineBlk.to, s.doc.length));
54
- const visBotLine =
55
- botLineBlk.bottom >= viewportBottom
56
- ? visBotLineDoc
57
- : visBotLineDoc + Math.floor((viewportBottom - botLineBlk.bottom) / view.defaultLineHeight);
58
-
59
- // const needsScrollTop = mainLine <= visTopLine + cursorLineMargin;
60
- const needsScrollTop = false;
61
- const needsScrollBot = mainLine >= visBotLine - lines;
62
-
63
- // the scroll margins are overlapping
64
- if (needsScrollTop && needsScrollBot) {
65
- log.warn('is cursorScrollMargin too large for the editor size?');
66
- return;
67
- }
68
-
69
- // no need to scroll, we're in between scroll regions
70
- if (!needsScrollTop && !needsScrollBot) {
71
- return;
72
- }
73
-
74
- // if we're within the margin, but moving the other way, then don't scroll
75
- if (selectionSet) {
76
- const { head } = startState.selection.main;
77
- const oldMainLine = lineAtPos(startState, head);
78
- if (needsScrollTop && oldMainLine < mainLine) {
79
- return;
80
- }
81
- if (needsScrollBot && oldMainLine > mainLine) {
82
- return;
83
- }
84
- }
85
-
86
- view.dispatch({
87
- annotations: Transaction.userEvent.of(annotation),
88
- effects: EditorView.scrollIntoView(s.selection.main.head, {
89
- y: needsScrollTop ? 'start' : 'end',
90
- yMargin: view.defaultLineHeight * lines,
91
- }),
92
- });
93
- });
94
- };