@dxos/react-ui-editor 0.4.2-main.ec5936e → 0.4.2

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 (41) hide show
  1. package/dist/lib/browser/index.mjs +185 -233
  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/MarkdownEditor.stories.d.ts +0 -20
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +0 -20
  7. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/automerge.stories.d.ts +2 -22
  9. package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/hooks.stories.d.ts +0 -20
  11. package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/Toolbar/Toolbar.d.ts +10 -7
  13. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  14. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +6 -23
  15. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
  16. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  17. package/dist/types/src/extensions/automerge/{sync.d.ts → semaphore.d.ts} +5 -6
  18. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -0
  19. package/dist/types/src/index.d.ts +1 -1
  20. package/dist/types/src/index.d.ts.map +1 -1
  21. package/dist/types/src/styles/index.d.ts +0 -1
  22. package/dist/types/src/styles/index.d.ts.map +1 -1
  23. package/package.json +24 -24
  24. package/src/components/TextEditor/MarkdownEditor.stories.tsx +16 -13
  25. package/src/components/TextEditor/TextEditor.stories.tsx +14 -11
  26. package/src/components/TextEditor/TextEditor.tsx +3 -3
  27. package/src/components/TextEditor/automerge.stories.tsx +44 -38
  28. package/src/components/TextEditor/hooks.stories.tsx +16 -15
  29. package/src/components/Toolbar/Toolbar.stories.tsx +12 -16
  30. package/src/components/Toolbar/Toolbar.tsx +129 -156
  31. package/src/extensions/automerge/automerge.ts +21 -5
  32. package/src/extensions/automerge/{sync.ts → semaphore.ts} +34 -32
  33. package/src/index.ts +1 -1
  34. package/src/styles/index.ts +0 -1
  35. package/dist/types/src/extensions/automerge/sync.d.ts.map +0 -1
  36. package/dist/types/src/styles/layout.d.ts +0 -2
  37. package/dist/types/src/styles/layout.d.ts.map +0 -1
  38. package/dist/types/src/translations.d.ts +0 -20
  39. package/dist/types/src/translations.d.ts.map +0 -1
  40. package/src/styles/layout.ts +0 -6
  41. package/src/translations.ts +0 -25
@@ -4,6 +4,7 @@
4
4
 
5
5
  import {
6
6
  type Icon,
7
+ CaretRight,
7
8
  ChatText,
8
9
  Code,
9
10
  CodeBlock,
@@ -22,25 +23,14 @@ import {
22
23
  TextHFive,
23
24
  TextHSix,
24
25
  TextItalic,
25
- Quotes,
26
- CaretDown,
27
26
  } from '@phosphor-icons/react';
28
27
  import { createContext } from '@radix-ui/react-context';
29
28
  import React, { type PropsWithChildren } from 'react';
30
29
 
31
- import {
32
- DensityProvider,
33
- ElevationProvider,
34
- Select,
35
- type ThemedClassName,
36
- Toolbar as NaturalToolbar,
37
- type ToolbarToggleGroupItemProps,
38
- useTranslation,
39
- } from '@dxos/react-ui';
40
- import { getSize } from '@dxos/react-ui-theme';
30
+ import { Button, type ButtonProps, DensityProvider, Select } from '@dxos/react-ui';
31
+ import { getSize, mx } from '@dxos/react-ui-theme';
41
32
 
42
33
  import { type Formatting } from '../../extensions';
43
- import { translationKey } from '../../translations';
44
34
 
45
35
  const HeadingIcons: { [key: string]: Icon } = {
46
36
  '0': Paragraph,
@@ -76,49 +66,61 @@ export type Action = {
76
66
  data?: any;
77
67
  };
78
68
 
79
- export type ToolbarProps = ThemedClassName<
80
- PropsWithChildren<{
81
- state: Formatting | null;
82
- onAction?: (action: Action) => void;
83
- }>
84
- >;
69
+ export type ToolbarProps = PropsWithChildren<{
70
+ state: Formatting | null;
71
+ onAction?: (action: Action) => void;
72
+ }>;
85
73
 
86
74
  const [ToolbarContextProvider, useToolbarContext] = createContext<ToolbarProps>('Toolbar');
87
75
 
88
- const ToolbarRoot = ({ children, onAction, classNames, state }: ToolbarProps) => {
76
+ const ToolbarRoot = ({ children, onAction, state }: ToolbarProps) => {
89
77
  return (
90
78
  <ToolbarContextProvider onAction={onAction} state={state}>
91
79
  <DensityProvider density='fine'>
92
- <ElevationProvider elevation='chrome'>
93
- <NaturalToolbar.Root classNames={['is-full shrink-0 overflow-x-auto p-1', classNames]}>
94
- {children}
95
- </NaturalToolbar.Root>
96
- </ElevationProvider>
80
+ <div role='toolbar' className='flex w-full shrink-0 p-1 gap-4 items-center whitespace-nowrap overflow-hidden'>
81
+ {children}
82
+ </div>
97
83
  </DensityProvider>
98
84
  </ToolbarContextProvider>
99
85
  );
100
86
  };
101
87
 
102
- type ToolbarButtonProps = ToolbarToggleGroupItemProps & {
88
+ type ToolbarButtonProps = {
103
89
  Icon: Icon;
104
- };
90
+ disable?: (state: Formatting) => boolean;
91
+ getState?: (state: Formatting) => boolean;
92
+ onClick: (state: Formatting | null) => Action | undefined;
93
+ } & NonNullable<Pick<ButtonProps, 'title'>>;
105
94
 
106
- const buttonStyles = 'min-bs-0 p-2';
107
- const iconStyles = getSize(4);
95
+ const ToolbarButton = ({ Icon, onClick, title, getState, disable }: ToolbarButtonProps) => {
96
+ const { onAction, state } = useToolbarContext('ToolbarButton');
97
+ const active = getState && state ? getState(state) : false;
98
+ const disabled = disable && state ? disable(state) : false;
99
+
100
+ const handleClick = (event: React.MouseEvent) => {
101
+ const action = onClick(state);
102
+ if (action) {
103
+ onAction?.(action);
104
+ event.preventDefault();
105
+ }
106
+ };
108
107
 
109
- const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
110
108
  return (
111
- <NaturalToolbar.ToggleGroupItem variant='ghost' {...props} classNames={buttonStyles}>
112
- <Icon className={iconStyles} />
113
- <span className='sr-only'>{children}</span>
114
- </NaturalToolbar.ToggleGroupItem>
109
+ <Button
110
+ variant='ghost'
111
+ classNames={mx('p-2', active && 'ring-[1px]')}
112
+ onMouseDown={handleClick}
113
+ title={title}
114
+ disabled={disabled}
115
+ >
116
+ <Icon className={getSize(5)} />
117
+ </Button>
115
118
  );
116
119
  };
117
120
 
118
- const ToolbarSeparator = () => <div role='separator' className='grow' />;
121
+ const ToolbarSeparator = () => <div className='grow' />;
119
122
 
120
123
  const MarkdownHeading = () => {
121
- const { t } = useTranslation(translationKey);
122
124
  const { onAction, state } = useToolbarContext('MarkdownFormatting');
123
125
  const blockType = state ? state.blockType : 'paragraph';
124
126
  const header = blockType && /heading(\d)/.exec(blockType);
@@ -130,16 +132,11 @@ const MarkdownHeading = () => {
130
132
  value={value ?? '0'}
131
133
  onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
132
134
  >
133
- <Select.Trigger asChild>
134
- <NaturalToolbar.Button variant='ghost' classNames={buttonStyles}>
135
- <span className='sr-only'>{t('heading label')}</span>
136
- <HeadingIcon className={iconStyles} />
137
- <CaretDown className={getSize(2)} weight='bold' />
138
- </NaturalToolbar.Button>
139
- </Select.Trigger>
135
+ <Select.TriggerButton>
136
+ <HeadingIcon className={getSize(5)} />
137
+ </Select.TriggerButton>
140
138
  <Select.Portal>
141
139
  <Select.Content>
142
- <Select.ScrollUpButton />
143
140
  <Select.Viewport>
144
141
  <Select.Option value='0'>Paragraph</Select.Option>
145
142
  {[1, 2, 3, 4, 5, 6].map((level) => (
@@ -148,117 +145,99 @@ const MarkdownHeading = () => {
148
145
  </Select.Option>
149
146
  ))}
150
147
  </Select.Viewport>
151
- <Select.ScrollDownButton />
152
- <Select.Arrow />
153
148
  </Select.Content>
154
149
  </Select.Portal>
155
150
  </Select.Root>
156
151
  );
157
152
  };
158
153
 
159
- const markdownStyles = [
160
- { key: 'strong' as const, Icon: TextB },
161
- { key: 'emphasis' as const, Icon: TextItalic },
162
- { key: 'strikethrough' as const, Icon: TextStrikethrough },
163
- { key: 'code' as const, Icon: Code },
164
- { key: 'link' as const, Icon: Link },
165
- // <ToolbarButton Icon={At} title='Mention' onClick={() => ({ type: 'mention' })} />
166
- // <ToolbarButton Icon={Image} title='Image' onClick={() => ({ type: 'image' })} />
167
- ];
168
-
169
- const MarkdownStyles = () => {
170
- const { onAction, state } = useToolbarContext('MarkdownStyles');
171
- const { t } = useTranslation(translationKey);
172
-
173
- return (
174
- <NaturalToolbar.ToggleGroup
175
- type='multiple'
176
- value={markdownStyles.filter(({ key }) => !!state?.[key]).map(({ key }) => key)}
177
- >
178
- {markdownStyles.map(({ key, Icon }) => (
179
- <ToolbarButton
180
- key={key}
181
- value={key}
182
- Icon={Icon}
183
- disabled={state?.blockType === 'codeblock'}
184
- onClick={() => onAction?.({ type: key, data: state ? !state[key] : null })}
185
- >
186
- {t(`${key} label`)}
187
- </ToolbarButton>
188
- ))}
189
- </NaturalToolbar.ToggleGroup>
190
- );
191
- };
192
-
193
- const markdownLists = [
194
- { key: 'bullet' as const, Icon: ListBullets, actionType: 'list-bullet' as const },
195
- { key: 'ordered' as const, Icon: ListNumbers, actionType: 'list-ordered' as const },
196
- { key: 'task' as const, Icon: ListChecks, actionType: 'list-tasks' as const },
197
- ];
154
+ const MarkdownStyles = () => (
155
+ <div role='none'>
156
+ <ToolbarButton
157
+ Icon={TextB}
158
+ title='String'
159
+ disable={(s) => s.blockType === 'codeblock'}
160
+ getState={(s) => s.strong}
161
+ onClick={(s) => ({ type: 'strong', data: s ? !s.strong : null })}
162
+ />
163
+ <ToolbarButton
164
+ Icon={TextItalic}
165
+ title='Emphasis'
166
+ disable={(s) => s.blockType === 'codeblock'}
167
+ getState={(s) => s.emphasis}
168
+ onClick={(s) => ({ type: 'emphasis', data: s ? !s.emphasis : null })}
169
+ />
170
+ <ToolbarButton
171
+ Icon={TextStrikethrough}
172
+ title='Strike-through'
173
+ disable={(s) => s.blockType === 'codeblock'}
174
+ getState={(s) => s.strikethrough}
175
+ onClick={(s) => ({ type: 'strikethrough', data: s ? !s.strikethrough : null })}
176
+ />
177
+ <ToolbarButton
178
+ Icon={Code}
179
+ title='Inline code'
180
+ disable={(s) => s.blockType === 'codeblock'}
181
+ getState={(s) => s.code}
182
+ onClick={(s) => ({ type: 'code', data: s ? !s.code : null })}
183
+ />
184
+ </div>
185
+ );
198
186
 
199
- const MarkdownLists = () => {
200
- const { onAction, state } = useToolbarContext('MarkdownStyles');
201
- const { t } = useTranslation(translationKey);
202
- return (
203
- <NaturalToolbar.ToggleGroup type='single' value={state?.listStyle ?? undefined}>
204
- {markdownLists.map(({ key, Icon, actionType }) => (
205
- <ToolbarButton
206
- key={key}
207
- value={key}
208
- Icon={Icon}
209
- onClick={() => onAction?.({ type: actionType, data: state ? state.listStyle !== key : null })}
210
- >
211
- {t(`${key} label`)}
212
- </ToolbarButton>
213
- ))}
214
- </NaturalToolbar.ToggleGroup>
215
- );
216
- };
187
+ const MarkdownLists = () => (
188
+ <div role='none'>
189
+ <ToolbarButton
190
+ Icon={ListBullets}
191
+ title='Bullet list'
192
+ getState={(s) => s.listStyle === 'bullet'}
193
+ onClick={(s) => ({ type: 'list-bullet', data: s ? s.listStyle !== 'bullet' : null })}
194
+ />
195
+ <ToolbarButton
196
+ Icon={ListNumbers}
197
+ title='Numbered list'
198
+ getState={(s) => s.listStyle === 'ordered'}
199
+ onClick={(s) => ({ type: 'list-ordered', data: s ? s.listStyle !== 'ordered' : null })}
200
+ />
201
+ <ToolbarButton
202
+ Icon={ListChecks}
203
+ title='Task list'
204
+ getState={(s) => s.listStyle === 'task'}
205
+ onClick={(s) => ({ type: 'list-tasks', data: s ? s.listStyle !== 'task' : null })}
206
+ />
207
+ </div>
208
+ );
217
209
 
218
- const markdownBlocks = [
219
- {
220
- key: 'blockquote' as const,
221
- Icon: Quotes,
222
- pressed: (state: Formatting | null) => state?.blockQuote,
223
- action: (state: Formatting | null) => ({ type: 'blockquote' as const, data: state ? !state.blockQuote : null }),
224
- disabled: (state: Formatting | null) => false,
225
- },
226
- {
227
- key: 'codeblock' as const,
228
- Icon: CodeBlock,
229
- pressed: (state: Formatting | null) => state?.blockType === 'codeblock',
230
- action: (state: Formatting | null) => ({ type: 'blockquote' as const, data: state ? !state.blockQuote : null }),
231
- disabled: (state: Formatting | null) => !state?.blankLine,
232
- },
233
- ];
210
+ const MarkdownBlocks = () => (
211
+ <div role='none'>
212
+ <ToolbarButton
213
+ Icon={CaretRight}
214
+ title='Block quote'
215
+ getState={(s) => s.blockQuote}
216
+ onClick={(s) => ({ type: 'blockquote', data: s ? !s.blockQuote : null })}
217
+ />
218
+ <ToolbarButton
219
+ Icon={CodeBlock}
220
+ title='Code block'
221
+ disable={(s) => !s.blankLine}
222
+ getState={(s) => s.blockType === 'codeblock'}
223
+ onClick={(s) => ({ type: 'codeblock', data: s ? s.blockType !== 'codeblock' : null })}
224
+ />
225
+ <ToolbarButton Icon={Table} title='Table' disable={(s) => !s.blankLine} onClick={() => ({ type: 'table' })} />
226
+ </div>
227
+ );
234
228
 
235
- const MarkdownBlocks = () => {
236
- const { onAction, state } = useToolbarContext('MarkdownStyles');
237
- const { t } = useTranslation(translationKey);
238
- return (
239
- <>
240
- <NaturalToolbar.ToggleGroup
241
- type='multiple'
242
- value={markdownBlocks.filter(({ pressed }) => pressed(state)).map(({ key }) => key)}
243
- >
244
- {markdownBlocks.map(({ key, Icon, action }) => (
245
- <ToolbarButton key={key} value={key} Icon={Icon} onClick={() => onAction?.(action(state))}>
246
- {t(`${key} label`)}
247
- </ToolbarButton>
248
- ))}
249
- </NaturalToolbar.ToggleGroup>
250
- <NaturalToolbar.Button
251
- variant='ghost'
252
- disabled={!state?.blankLine}
253
- onClick={() => onAction?.({ type: 'table' })}
254
- classNames={buttonStyles}
255
- >
256
- <Table className={iconStyles} />
257
- <span className='sr-only'>{t('table label')}</span>
258
- </NaturalToolbar.Button>
259
- </>
260
- );
261
- };
229
+ const MarkdownLinks = () => (
230
+ <div role='none'>
231
+ <ToolbarButton
232
+ Icon={Link}
233
+ title='Link'
234
+ getState={(s) => s.link}
235
+ onClick={(s) => ({ type: 'link', data: s ? !s.link : null })}
236
+ />
237
+ {/* <ToolbarButton Icon={At} title='Mention' onClick={() => ({ type: 'mention' })} /> */}
238
+ {/* <ToolbarButton Icon={Image} title='Image' onClick={() => ({ type: 'image' })} /> */}
239
+ </div>
240
+ );
262
241
 
263
242
  const MarkdownStandard = () => (
264
243
  <>
@@ -266,19 +245,13 @@ const MarkdownStandard = () => (
266
245
  <MarkdownStyles />
267
246
  <MarkdownLists />
268
247
  <MarkdownBlocks />
248
+ <MarkdownLinks />
269
249
  </>
270
250
  );
271
251
 
272
- const MarkdownExtended = () => {
273
- const { onAction } = useToolbarContext('MarkdownStyles');
274
- const { t } = useTranslation(translationKey);
275
- return (
276
- <NaturalToolbar.Button variant='ghost' onClick={() => onAction?.({ type: 'comment' })} classNames={buttonStyles}>
277
- <ChatText className={iconStyles} />
278
- <span className='sr-only'>{t('comment label')}</span>
279
- </NaturalToolbar.Button>
280
- );
281
- };
252
+ const MarkdownExtended = () => (
253
+ <Toolbar.Button Icon={ChatText} title='Create comment' onClick={() => ({ type: 'comment' })} />
254
+ );
282
255
 
283
256
  export const Toolbar = {
284
257
  Root: ToolbarRoot,
@@ -4,14 +4,15 @@
4
4
  // Ref: https://github.com/automerge/automerge-codemirror
5
5
  //
6
6
 
7
- import { StateField, type Extension } from '@codemirror/state';
7
+ import { invertedEffects } from '@codemirror/commands';
8
+ import { StateField, type Extension, type StateEffect } from '@codemirror/state';
8
9
  import { EditorView, ViewPlugin } from '@codemirror/view';
9
10
 
10
11
  import { next as A, type Prop } from '@dxos/automerge/automerge';
11
12
 
12
13
  import { cursorConverter } from './cursor';
13
14
  import { updateHeadsEffect, type IDocHandle, isReconcile, type State } from './defs';
14
- import { Syncer } from './sync';
15
+ import { PatchSemaphore } from './semaphore';
15
16
  import { Cursor } from '../cursor';
16
17
 
17
18
  export type AutomergeOptions = {
@@ -54,7 +55,7 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
54
55
  },
55
56
  });
56
57
 
57
- const syncer = new Syncer(handle, syncState);
58
+ const semaphore = new PatchSemaphore(handle, syncState);
58
59
 
59
60
  return [
60
61
  Cursor.converter.of(cursorConverter(handle, path)),
@@ -72,7 +73,7 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
72
73
  }
73
74
 
74
75
  _handleChange() {
75
- syncer.reconcile(this._view, false);
76
+ semaphore.reconcile(this._view);
76
77
  }
77
78
  },
78
79
  ),
@@ -80,8 +81,23 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
80
81
  // Reconcile local updates.
81
82
  EditorView.updateListener.of(({ view, changes }) => {
82
83
  if (!changes.empty) {
83
- syncer.reconcile(view, true);
84
+ semaphore.reconcile(view);
84
85
  }
85
86
  }),
87
+
88
+ // TODO(burdon): Record undo transactions.
89
+ // See https://github.com/yjs/y-codemirror.next/tree/main
90
+ // https://codemirror.net/examples/inverted-effect
91
+ invertedEffects.of((tr) => {
92
+ // Each change results it three transactions: insert, delete, insert.
93
+ const effects: StateEffect<any>[] = [];
94
+ if (!tr.changes.empty) {
95
+ tr.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
96
+ // effects.push(effectType.of({});
97
+ });
98
+ }
99
+
100
+ return effects;
101
+ }),
86
102
  ];
87
103
  };
@@ -12,11 +12,11 @@ import { next as A } from '@dxos/automerge/automerge';
12
12
  import {
13
13
  getLastHeads,
14
14
  getPath,
15
- type IDocHandle,
16
- type State,
15
+ isReconcile,
17
16
  reconcileAnnotation,
18
17
  updateHeads,
19
- isReconcile,
18
+ type IDocHandle,
19
+ type State,
20
20
  } from './defs';
21
21
  import { updateAutomerge } from './update-automerge';
22
22
  import { updateCodeMirror } from './update-codemirror';
@@ -24,8 +24,8 @@ import { updateCodeMirror } from './update-codemirror';
24
24
  /**
25
25
  * Implements three-way merge (on each mutation).
26
26
  */
27
- export class Syncer {
28
- private _pending = false;
27
+ export class PatchSemaphore {
28
+ private _inReconcile = false;
29
29
 
30
30
  // prettier-ignore
31
31
  constructor(
@@ -33,44 +33,46 @@ export class Syncer {
33
33
  private readonly _state: StateField<State>
34
34
  ) {}
35
35
 
36
- reconcile(view: EditorView, editor: boolean) {
37
- // TODO(burdon): Better way to do mutex?
38
- if (this._pending) {
39
- return;
36
+ reconcile(view: EditorView) {
37
+ if (!this._inReconcile) {
38
+ this._inReconcile = true;
39
+ this.doReconcile(view);
40
+ this._inReconcile = false;
40
41
  }
41
-
42
- this._pending = true;
43
- if (editor) {
44
- this.onEditorChange(view);
45
- } else {
46
- this.onAutomergeChange(view);
47
- }
48
- this._pending = false;
49
42
  }
50
43
 
51
- onEditorChange(view: EditorView) {
52
- // Apply the unreconciled transactions to the document.
53
- const transactions = view.state.field(this._state).unreconciledTransactions.filter((tx) => !isReconcile(tx));
54
- const newHeads = updateAutomerge(this._state, this._handle, transactions, view.state);
44
+ private doReconcile(view: EditorView) {
45
+ const path = getPath(view.state, this._state);
55
46
 
56
- if (newHeads) {
47
+ // Get the heads before the unreconciled transactions are applied.
48
+ const oldHeads = getLastHeads(view.state, this._state);
49
+ let selection = view.state.selection;
50
+
51
+ // First undo all the unreconciled transactions.
52
+ const transactions = view.state.field(this._state).unreconciledTransactions.filter((tx) => !isReconcile(tx));
53
+ const toInvert = transactions.slice().reverse();
54
+ for (const tr of toInvert) {
55
+ const inverted = tr.changes.invert(tr.startState.doc);
56
+ selection = selection.map(inverted);
57
57
  view.dispatch({
58
- effects: updateHeads(newHeads),
59
- annotations: reconcileAnnotation.of(false),
58
+ changes: inverted,
59
+ annotations: reconcileAnnotation.of(true),
60
60
  });
61
61
  }
62
- }
63
62
 
64
- onAutomergeChange(view: EditorView) {
65
- // Get the diff between the updated state of the document and the heads and apply that to the codemirror doc.
66
- const oldHeads = getLastHeads(view.state, this._state);
67
- const newHeads = A.getHeads(this._handle.docSync()!);
68
- const diff = A.equals(oldHeads, newHeads) ? [] : A.diff(this._handle.docSync()!, oldHeads, newHeads);
63
+ // Apply the unreconciled transactions to the document.
64
+ // NOTE: null and undefined each come from automerge and repo respectively.
65
+ let newHeads = updateAutomerge(this._state, this._handle, transactions, view.state);
66
+ if (newHeads === null || newHeads === undefined) {
67
+ // TODO(alexjg): this is the call that's resetting the editor state on click.
68
+ newHeads = A.getHeads(this._handle.docSync()!);
69
+ }
69
70
 
70
- const selection = view.state.selection;
71
- const path = getPath(view.state, this._state);
71
+ // Now get the diff between the updated state of the document and the heads and apply that to the codemirror doc.
72
+ const diff = A.equals(oldHeads, newHeads) ? [] : A.diff(this._handle.docSync()!, oldHeads, newHeads);
72
73
  updateCodeMirror(view, selection, path, diff);
73
74
 
75
+ // Update automerge state.
74
76
  view.dispatch({
75
77
  effects: updateHeads(newHeads),
76
78
  annotations: reconcileAnnotation.of(false),
package/src/index.ts CHANGED
@@ -10,6 +10,6 @@ export { type EditorView, keymap } from '@codemirror/view';
10
10
  export * from './components';
11
11
  export * from './extensions';
12
12
  export * from './hooks';
13
- export { getToken, editorWithToolbarLayout } from './styles';
13
+ export { getToken } from './styles';
14
14
  export * from './themes';
15
15
  export * from './util';
@@ -5,4 +5,3 @@
5
5
  export * from './cursors';
6
6
  export * from './markdown';
7
7
  export * from './tokens';
8
- export * from './layout';
@@ -1 +0,0 @@
1
- {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/automerge/sync.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,EAGL,KAAK,UAAU,EACf,KAAK,KAAK,EAIX,MAAM,QAAQ,CAAC;AAIhB;;GAEG;AACH,qBAAa,MAAM;IAKf,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IALzB,OAAO,CAAC,QAAQ,CAAS;gBAIN,OAAO,EAAE,UAAU,EACnB,MAAM,EAAE,UAAU,CAAC,KAAK,CAAC;IAG5C,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO;IAe3C,cAAc,CAAC,IAAI,EAAE,UAAU;IAa/B,iBAAiB,CAAC,IAAI,EAAE,UAAU;CAenC"}
@@ -1,2 +0,0 @@
1
- export declare const editorWithToolbarLayout = "fixed inset-0 grid grid-cols-1 grid-rows-[min-content_1fr] justify-center content-start overflow-hidden";
2
- //# sourceMappingURL=layout.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../../../src/styles/layout.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,uBAAuB,4GACuE,CAAC"}
@@ -1,20 +0,0 @@
1
- export declare const translationKey = "react-ui-editor";
2
- declare const _default: {
3
- 'en-US': {
4
- "react-ui-editor": {
5
- 'strong label': string;
6
- 'emphasis label': string;
7
- 'strikethrough label': string;
8
- 'code label': string;
9
- 'bullet label': string;
10
- 'ordered label': string;
11
- 'task label': string;
12
- 'blockquote label': string;
13
- 'codeblock label': string;
14
- 'comment label': string;
15
- 'heading label': string;
16
- };
17
- };
18
- }[];
19
- export default _default;
20
- //# sourceMappingURL=translations.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../../src/translations.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,cAAc,oBAAoB,CAAC;;;;;;;;;;;;;;;;;;AAEhD,wBAkBE"}
@@ -1,6 +0,0 @@
1
- //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- export const editorWithToolbarLayout =
6
- 'fixed inset-0 grid grid-cols-1 grid-rows-[min-content_1fr] justify-center content-start overflow-hidden';
@@ -1,25 +0,0 @@
1
- //
2
- // Copyright 2023 DXOS.org
3
- //
4
-
5
- export const translationKey = 'react-ui-editor';
6
-
7
- export default [
8
- {
9
- 'en-US': {
10
- [translationKey]: {
11
- 'strong label': 'Strong',
12
- 'emphasis label': 'Emphasis',
13
- 'strikethrough label': 'Strike-through',
14
- 'code label': 'Code',
15
- 'bullet label': 'Bullet list',
16
- 'ordered label': 'Numbered list',
17
- 'task label': 'Task list',
18
- 'blockquote label': 'Block quote',
19
- 'codeblock label': 'Code block',
20
- 'comment label': 'Create comment',
21
- 'heading label': 'Heading level',
22
- },
23
- },
24
- },
25
- ];