@dxos/react-ui-editor 0.4.8-main.beadd4b → 0.4.8-main.c0be0d1

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.
@@ -16,17 +16,17 @@ import { withTheme } from '@dxos/storybook-utils';
16
16
  import { Toolbar } from './Toolbar';
17
17
  import {
18
18
  type Comment,
19
- decorateMarkdown,
20
19
  comments,
21
20
  createBasicExtensions,
22
21
  createDataExtensions,
22
+ createMarkdownExtensions,
23
23
  createThemeExtensions,
24
+ decorateMarkdown,
24
25
  formattingKeymap,
26
+ image,
25
27
  table,
26
28
  useComments,
27
29
  useFormattingState,
28
- createMarkdownExtensions,
29
- image,
30
30
  } from '../../extensions';
31
31
  import { useActionHandler, useDocAccessor, useTextEditor } from '../../hooks';
32
32
  import translations from '../../translations';
@@ -34,15 +34,17 @@ import translations from '../../translations';
34
34
  faker.seed(101);
35
35
 
36
36
  const Story: FC<{ content: string }> = ({ content }) => {
37
+ console.log('STORY');
37
38
  const { themeMode } = useThemeContext();
38
39
  const [item] = useState({ text: new TextObject(content) });
39
40
  const { id, doc, accessor } = useDocAccessor(item.text);
40
41
  const [formattingState, formattingObserver] = useFormattingState();
41
- const { parentRef, view } = useTextEditor(
42
- () => ({
42
+ const { parentRef, view } = useTextEditor(() => {
43
+ return {
43
44
  id,
44
45
  doc,
45
46
  extensions: [
47
+ formattingObserver,
46
48
  createBasicExtensions(),
47
49
  createMarkdownExtensions({ themeMode }),
48
50
  createThemeExtensions({ themeMode, slots: { editor: { className: 'p-2' } } }),
@@ -58,40 +60,36 @@ const Story: FC<{ content: string }> = ({ content }) => {
58
60
  formattingKeymap(),
59
61
  image(),
60
62
  table(),
61
- formattingObserver,
62
63
  ],
63
- }),
64
- [id, accessor, themeMode],
65
- );
64
+ };
65
+ }, [id, accessor, formattingObserver, themeMode]);
66
+
67
+ const handleAction = useActionHandler(view);
66
68
 
67
69
  const [_comments, setComments] = useState<Comment[]>([]);
68
70
  useComments(view, id, _comments);
69
71
 
70
- const handleAction = useActionHandler(view);
71
-
72
72
  return (
73
- <div role='none' className='fixed inset-0 flex flex-col'>
74
- <Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
75
- <Toolbar.Markdown />
76
- <Toolbar.Custom onUpload={async (file) => ({ url: file.name })} />
77
- <Toolbar.Separator />
78
- <Toolbar.Actions />
79
- </Toolbar.Root>
80
- <div ref={parentRef} className={textBlockWidth} />
81
- </div>
73
+ <Tooltip.Provider>
74
+ <div role='none' className='fixed inset-0 flex flex-col'>
75
+ <Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
76
+ <Toolbar.Markdown />
77
+ <Toolbar.Custom onUpload={async (file) => ({ url: file.name })} />
78
+ <Toolbar.Separator />
79
+ <Toolbar.Actions />
80
+ </Toolbar.Root>
81
+ <div ref={parentRef} className={textBlockWidth} />
82
+ </div>
83
+ </Tooltip.Provider>
82
84
  );
83
85
  };
84
86
 
85
87
  export default {
86
88
  title: 'react-ui-editor/Toolbar',
87
89
  component: Toolbar,
88
- render: (args: any) => (
89
- <Tooltip.Provider>
90
- <Story {...args} />
91
- </Tooltip.Provider>
92
- ),
93
90
  decorators: [withTheme],
94
91
  parameters: { translations, layout: 'fullscreen' },
92
+ render: (args: any) => <Story {...args} />,
95
93
  };
96
94
 
97
95
  const content = [
@@ -13,6 +13,7 @@ import {
13
13
  ListChecks,
14
14
  ListNumbers,
15
15
  Paragraph,
16
+ Quotes,
16
17
  TextStrikethrough,
17
18
  Table,
18
19
  TextB,
@@ -23,7 +24,6 @@ import {
23
24
  TextHFive,
24
25
  TextHSix,
25
26
  TextItalic,
26
- Quotes,
27
27
  } from '@phosphor-icons/react';
28
28
  import { createContext } from '@radix-ui/react-context';
29
29
  import React, { type PropsWithChildren, useEffect, useRef, useState } from 'react';
@@ -33,9 +33,9 @@ import {
33
33
  DensityProvider,
34
34
  ElevationProvider,
35
35
  Select,
36
- type ThemedClassName,
37
36
  Toolbar as NaturalToolbar,
38
37
  Tooltip,
38
+ type ThemedClassName,
39
39
  type ToolbarToggleGroupItemProps as NaturalToolbarToggleGroupItemProps,
40
40
  type ToolbarButtonProps as NaturalToolbarButtonProps,
41
41
  useTranslation,
@@ -83,7 +83,7 @@ export type Action = {
83
83
 
84
84
  export type ToolbarProps = ThemedClassName<
85
85
  PropsWithChildren<{
86
- state: Formatting | null;
86
+ state: Formatting | undefined;
87
87
  onAction?: (action: Action) => void;
88
88
  }>
89
89
  >;
@@ -355,6 +355,7 @@ const MarkdownCustom = ({ onUpload }: MarkdownCustomOptions = {}) => {
355
355
  'image/*': ['.jpg', '.jpeg', '.png', '.gif'],
356
356
  },
357
357
  });
358
+
358
359
  useEffect(() => {
359
360
  if (onUpload && acceptedFiles.length) {
360
361
  setTimeout(async () => {
@@ -388,9 +389,20 @@ const MarkdownActions = () => {
388
389
  const { onAction } = useToolbarContext('MarkdownStyles');
389
390
  const { t } = useTranslation(translationKey);
390
391
  return (
391
- <ToolbarButton value='comment' Icon={ChatText} onClick={() => onAction?.({ type: 'comment' })}>
392
- {t('comment label')}
393
- </ToolbarButton>
392
+ <>
393
+ {/* TODO(burdon): Toggle readonly state. */}
394
+ {/* <ToolbarButton value='comment' Icon={BookOpenText} onClick={() => onAction?.({ type: 'comment' })}> */}
395
+ {/* {t('comment label')} */}
396
+ {/* </ToolbarButton> */}
397
+ <ToolbarButton
398
+ value='comment'
399
+ Icon={ChatText}
400
+ data-testid='editor.toolbar.comment'
401
+ onClick={() => onAction?.({ type: 'comment' })}
402
+ >
403
+ {t('comment label')}
404
+ </ToolbarButton>
405
+ </>
394
406
  );
395
407
  };
396
408
 
@@ -60,16 +60,16 @@ export const automerge = (accessor: DocAccessor): Extension => {
60
60
  ViewPlugin.fromClass(
61
61
  class {
62
62
  constructor(private readonly _view: EditorView) {
63
- accessor.handle.addListener('change', this._handleChange.bind(this));
63
+ accessor.handle.addListener('change', this._handleChange);
64
64
  }
65
65
 
66
66
  destroy() {
67
- accessor.handle.removeListener('change', this._handleChange.bind(this));
67
+ accessor.handle.removeListener('change', this._handleChange);
68
68
  }
69
69
 
70
- _handleChange() {
70
+ readonly _handleChange = () => {
71
71
  syncer.reconcile(this._view, false);
72
- }
72
+ };
73
73
  },
74
74
  ),
75
75
 
@@ -508,14 +508,14 @@ export const focusComment = (view: EditorView, id: string, center = true) => {
508
508
  /**
509
509
  * Update comments state field.
510
510
  */
511
- export const useComments = (view: EditorView | null | undefined, id: string, comments: Comment[] = []) => {
511
+ export const useComments = (view: EditorView | null | undefined, id: string, comments?: Comment[]) => {
512
512
  useEffect(() => {
513
513
  if (view) {
514
514
  // Check same document.
515
515
  // NOTE: Hook might be called before editor state is updated.
516
516
  if (id === view.state.facet(documentId)) {
517
517
  view.dispatch({
518
- effects: setComments.of({ id, comments }),
518
+ effects: setComments.of({ id, comments: comments ?? [] }),
519
519
  });
520
520
  }
521
521
  }
@@ -15,7 +15,9 @@ import {
15
15
  } from '@codemirror/state';
16
16
  import { EditorView, keymap } from '@codemirror/view';
17
17
  import { type SyntaxNodeRef, type SyntaxNode } from '@lezer/common';
18
- import { useState, useMemo } from 'react';
18
+ import { useMemo } from 'react';
19
+
20
+ import { useStateRef } from '@dxos/react-async';
19
21
 
20
22
  // Markdown refs:
21
23
  // https://github.github.com/gfm
@@ -56,7 +58,7 @@ export type Formatting = {
56
58
  listStyle: null | 'ordered' | 'bullet' | 'task';
57
59
  };
58
60
 
59
- export const compareFormatting = (a: Formatting, b: Formatting) =>
61
+ export const formattingEquals = (a: Formatting, b: Formatting) =>
60
62
  a.blockType === b.blockType &&
61
63
  a.strong === b.strong &&
62
64
  a.emphasis === b.emphasis &&
@@ -1061,7 +1063,7 @@ export const getFormatting = (state: EditorState): Formatting => {
1061
1063
  const inline: (boolean | null)[] = [null, null, null, null];
1062
1064
  let link: boolean = false;
1063
1065
  let blockQuote: boolean | null = null;
1064
- // False indicates mixed list styles
1066
+ // False indicates mixed list styles.
1065
1067
  let listStyle: Formatting['listStyle'] | null | false = null;
1066
1068
 
1067
1069
  // Track block context for list/blockquote handling.
@@ -1220,16 +1222,18 @@ export const getFormatting = (state: EditorState): Formatting => {
1220
1222
  };
1221
1223
 
1222
1224
  /**
1223
- * Hook computes the current formatting state.
1225
+ * Hook provides an extension to compute the current formatting state.
1224
1226
  */
1225
- export const useFormattingState = (): [Formatting | null, Extension] => {
1226
- const [state, setState] = useState<Formatting | null>(null);
1227
+ export const useFormattingState = (): [Formatting | undefined, Extension] => {
1228
+ const [state, setState, stateRef] = useStateRef<Formatting>();
1227
1229
  const observer = useMemo(
1228
1230
  () =>
1229
1231
  EditorView.updateListener.of((update) => {
1230
1232
  if (update.docChanged || update.selectionSet) {
1231
1233
  const newState = getFormatting(update.state);
1232
- if (!state || !compareFormatting(state, newState)) {
1234
+ if (!stateRef.current || !formattingEquals(stateRef.current, newState)) {
1235
+ // TODO(burdon): Error on first click on toolbar.
1236
+ // Warning: React has detected a change in the order of Hooks called by ForwardRef. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
1233
1237
  setState(newState);
1234
1238
  }
1235
1239
  }
@@ -4,11 +4,17 @@
4
4
 
5
5
  import { useMemo } from 'react';
6
6
 
7
- import { createDocAccessor, type DocAccessor, getTextContent, type TextObject } from '@dxos/echo-schema';
7
+ import {
8
+ createDocAccessor,
9
+ type DocAccessor,
10
+ getTextContent,
11
+ type TextObject,
12
+ type EchoReactiveObject,
13
+ } from '@dxos/echo-schema';
8
14
 
9
15
  // TODO(burdon): Factor out.
10
16
  export const useDocAccessor = <T = any>(
11
- text: TextObject,
17
+ text: TextObject | EchoReactiveObject<{ content: string }>,
12
18
  ): { id: string; doc: string | undefined; accessor: DocAccessor<T> } => {
13
19
  return useMemo(
14
20
  () => ({
@@ -33,6 +33,8 @@ export const useTextEditor = (cb: () => UseTextEditorProps = () => ({}), deps: D
33
33
  useEffect(() => {
34
34
  let view: EditorView;
35
35
  if (parentRef.current) {
36
+ log('create', { id });
37
+
36
38
  // https://codemirror.net/docs/ref/#state.EditorStateConfig
37
39
  const state = EditorState.create({
38
40
  doc,
@@ -69,6 +71,7 @@ export const useTextEditor = (cb: () => UseTextEditorProps = () => ({}), deps: D
69
71
  }
70
72
 
71
73
  return () => {
74
+ log('destroy', { id });
72
75
  view?.destroy();
73
76
  };
74
77
  }, deps);