@dxos/react-ui-editor 0.4.2 → 0.4.3-main.c3bfede
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.
- package/dist/lib/browser/index.mjs +233 -185
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +20 -0
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +20 -0
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts +22 -2
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts +20 -0
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +7 -10
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +23 -6
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/{semaphore.d.ts → sync.d.ts} +6 -5
- package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/index.d.ts +1 -0
- package/dist/types/src/styles/index.d.ts.map +1 -1
- package/dist/types/src/styles/layout.d.ts +2 -0
- package/dist/types/src/styles/layout.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +20 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/package.json +24 -24
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +13 -16
- package/src/components/TextEditor/TextEditor.stories.tsx +11 -14
- package/src/components/TextEditor/TextEditor.tsx +3 -3
- package/src/components/TextEditor/automerge.stories.tsx +38 -44
- package/src/components/TextEditor/hooks.stories.tsx +15 -16
- package/src/components/Toolbar/Toolbar.stories.tsx +16 -12
- package/src/components/Toolbar/Toolbar.tsx +156 -129
- package/src/extensions/automerge/automerge.ts +5 -21
- package/src/extensions/automerge/{semaphore.ts → sync.ts} +32 -34
- package/src/index.ts +1 -1
- package/src/styles/index.ts +1 -0
- package/src/styles/layout.ts +6 -0
- package/src/translations.ts +25 -0
- package/dist/types/src/extensions/automerge/semaphore.d.ts.map +0 -1
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
type Icon,
|
|
7
|
-
CaretRight,
|
|
8
7
|
ChatText,
|
|
9
8
|
Code,
|
|
10
9
|
CodeBlock,
|
|
@@ -23,14 +22,25 @@ import {
|
|
|
23
22
|
TextHFive,
|
|
24
23
|
TextHSix,
|
|
25
24
|
TextItalic,
|
|
25
|
+
Quotes,
|
|
26
|
+
CaretDown,
|
|
26
27
|
} from '@phosphor-icons/react';
|
|
27
28
|
import { createContext } from '@radix-ui/react-context';
|
|
28
29
|
import React, { type PropsWithChildren } from 'react';
|
|
29
30
|
|
|
30
|
-
import {
|
|
31
|
-
|
|
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';
|
|
32
41
|
|
|
33
42
|
import { type Formatting } from '../../extensions';
|
|
43
|
+
import { translationKey } from '../../translations';
|
|
34
44
|
|
|
35
45
|
const HeadingIcons: { [key: string]: Icon } = {
|
|
36
46
|
'0': Paragraph,
|
|
@@ -66,61 +76,49 @@ export type Action = {
|
|
|
66
76
|
data?: any;
|
|
67
77
|
};
|
|
68
78
|
|
|
69
|
-
export type ToolbarProps =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
export type ToolbarProps = ThemedClassName<
|
|
80
|
+
PropsWithChildren<{
|
|
81
|
+
state: Formatting | null;
|
|
82
|
+
onAction?: (action: Action) => void;
|
|
83
|
+
}>
|
|
84
|
+
>;
|
|
73
85
|
|
|
74
86
|
const [ToolbarContextProvider, useToolbarContext] = createContext<ToolbarProps>('Toolbar');
|
|
75
87
|
|
|
76
|
-
const ToolbarRoot = ({ children, onAction, state }: ToolbarProps) => {
|
|
88
|
+
const ToolbarRoot = ({ children, onAction, classNames, state }: ToolbarProps) => {
|
|
77
89
|
return (
|
|
78
90
|
<ToolbarContextProvider onAction={onAction} state={state}>
|
|
79
91
|
<DensityProvider density='fine'>
|
|
80
|
-
<
|
|
81
|
-
{
|
|
82
|
-
|
|
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>
|
|
83
97
|
</DensityProvider>
|
|
84
98
|
</ToolbarContextProvider>
|
|
85
99
|
);
|
|
86
100
|
};
|
|
87
101
|
|
|
88
|
-
type ToolbarButtonProps = {
|
|
102
|
+
type ToolbarButtonProps = ToolbarToggleGroupItemProps & {
|
|
89
103
|
Icon: Icon;
|
|
90
|
-
|
|
91
|
-
getState?: (state: Formatting) => boolean;
|
|
92
|
-
onClick: (state: Formatting | null) => Action | undefined;
|
|
93
|
-
} & NonNullable<Pick<ButtonProps, 'title'>>;
|
|
94
|
-
|
|
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;
|
|
104
|
+
};
|
|
99
105
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (action) {
|
|
103
|
-
onAction?.(action);
|
|
104
|
-
event.preventDefault();
|
|
105
|
-
}
|
|
106
|
-
};
|
|
106
|
+
const buttonStyles = 'min-bs-0 p-2';
|
|
107
|
+
const iconStyles = getSize(4);
|
|
107
108
|
|
|
109
|
+
const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
|
|
108
110
|
return (
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
title={title}
|
|
114
|
-
disabled={disabled}
|
|
115
|
-
>
|
|
116
|
-
<Icon className={getSize(5)} />
|
|
117
|
-
</Button>
|
|
111
|
+
<NaturalToolbar.ToggleGroupItem variant='ghost' {...props} classNames={buttonStyles}>
|
|
112
|
+
<Icon className={iconStyles} />
|
|
113
|
+
<span className='sr-only'>{children}</span>
|
|
114
|
+
</NaturalToolbar.ToggleGroupItem>
|
|
118
115
|
);
|
|
119
116
|
};
|
|
120
117
|
|
|
121
|
-
const ToolbarSeparator = () => <div className='grow' />;
|
|
118
|
+
const ToolbarSeparator = () => <div role='separator' className='grow' />;
|
|
122
119
|
|
|
123
120
|
const MarkdownHeading = () => {
|
|
121
|
+
const { t } = useTranslation(translationKey);
|
|
124
122
|
const { onAction, state } = useToolbarContext('MarkdownFormatting');
|
|
125
123
|
const blockType = state ? state.blockType : 'paragraph';
|
|
126
124
|
const header = blockType && /heading(\d)/.exec(blockType);
|
|
@@ -132,11 +130,16 @@ const MarkdownHeading = () => {
|
|
|
132
130
|
value={value ?? '0'}
|
|
133
131
|
onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
|
|
134
132
|
>
|
|
135
|
-
<Select.
|
|
136
|
-
<
|
|
137
|
-
|
|
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>
|
|
138
140
|
<Select.Portal>
|
|
139
141
|
<Select.Content>
|
|
142
|
+
<Select.ScrollUpButton />
|
|
140
143
|
<Select.Viewport>
|
|
141
144
|
<Select.Option value='0'>Paragraph</Select.Option>
|
|
142
145
|
{[1, 2, 3, 4, 5, 6].map((level) => (
|
|
@@ -145,99 +148,117 @@ const MarkdownHeading = () => {
|
|
|
145
148
|
</Select.Option>
|
|
146
149
|
))}
|
|
147
150
|
</Select.Viewport>
|
|
151
|
+
<Select.ScrollDownButton />
|
|
152
|
+
<Select.Arrow />
|
|
148
153
|
</Select.Content>
|
|
149
154
|
</Select.Portal>
|
|
150
155
|
</Select.Root>
|
|
151
156
|
);
|
|
152
157
|
};
|
|
153
158
|
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
);
|
|
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
|
+
];
|
|
186
168
|
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
);
|
|
169
|
+
const MarkdownStyles = () => {
|
|
170
|
+
const { onAction, state } = useToolbarContext('MarkdownStyles');
|
|
171
|
+
const { t } = useTranslation(translationKey);
|
|
209
172
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
);
|
|
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
|
+
};
|
|
228
192
|
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
)
|
|
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
|
+
];
|
|
198
|
+
|
|
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
|
+
};
|
|
217
|
+
|
|
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
|
+
];
|
|
234
|
+
|
|
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
|
+
};
|
|
241
262
|
|
|
242
263
|
const MarkdownStandard = () => (
|
|
243
264
|
<>
|
|
@@ -245,13 +266,19 @@ const MarkdownStandard = () => (
|
|
|
245
266
|
<MarkdownStyles />
|
|
246
267
|
<MarkdownLists />
|
|
247
268
|
<MarkdownBlocks />
|
|
248
|
-
<MarkdownLinks />
|
|
249
269
|
</>
|
|
250
270
|
);
|
|
251
271
|
|
|
252
|
-
const MarkdownExtended = () =>
|
|
253
|
-
|
|
254
|
-
);
|
|
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
|
+
};
|
|
255
282
|
|
|
256
283
|
export const Toolbar = {
|
|
257
284
|
Root: ToolbarRoot,
|
|
@@ -4,15 +4,14 @@
|
|
|
4
4
|
// Ref: https://github.com/automerge/automerge-codemirror
|
|
5
5
|
//
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import { StateField, type Extension, type StateEffect } from '@codemirror/state';
|
|
7
|
+
import { StateField, type Extension } from '@codemirror/state';
|
|
9
8
|
import { EditorView, ViewPlugin } from '@codemirror/view';
|
|
10
9
|
|
|
11
10
|
import { next as A, type Prop } from '@dxos/automerge/automerge';
|
|
12
11
|
|
|
13
12
|
import { cursorConverter } from './cursor';
|
|
14
13
|
import { updateHeadsEffect, type IDocHandle, isReconcile, type State } from './defs';
|
|
15
|
-
import {
|
|
14
|
+
import { Syncer } from './sync';
|
|
16
15
|
import { Cursor } from '../cursor';
|
|
17
16
|
|
|
18
17
|
export type AutomergeOptions = {
|
|
@@ -55,7 +54,7 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
|
55
54
|
},
|
|
56
55
|
});
|
|
57
56
|
|
|
58
|
-
const
|
|
57
|
+
const syncer = new Syncer(handle, syncState);
|
|
59
58
|
|
|
60
59
|
return [
|
|
61
60
|
Cursor.converter.of(cursorConverter(handle, path)),
|
|
@@ -73,7 +72,7 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
_handleChange() {
|
|
76
|
-
|
|
75
|
+
syncer.reconcile(this._view, false);
|
|
77
76
|
}
|
|
78
77
|
},
|
|
79
78
|
),
|
|
@@ -81,23 +80,8 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
|
81
80
|
// Reconcile local updates.
|
|
82
81
|
EditorView.updateListener.of(({ view, changes }) => {
|
|
83
82
|
if (!changes.empty) {
|
|
84
|
-
|
|
83
|
+
syncer.reconcile(view, true);
|
|
85
84
|
}
|
|
86
85
|
}),
|
|
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
|
-
}),
|
|
102
86
|
];
|
|
103
87
|
};
|
|
@@ -12,11 +12,11 @@ import { next as A } from '@dxos/automerge/automerge';
|
|
|
12
12
|
import {
|
|
13
13
|
getLastHeads,
|
|
14
14
|
getPath,
|
|
15
|
-
isReconcile,
|
|
16
|
-
reconcileAnnotation,
|
|
17
|
-
updateHeads,
|
|
18
15
|
type IDocHandle,
|
|
19
16
|
type State,
|
|
17
|
+
reconcileAnnotation,
|
|
18
|
+
updateHeads,
|
|
19
|
+
isReconcile,
|
|
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
|
|
28
|
-
private
|
|
27
|
+
export class Syncer {
|
|
28
|
+
private _pending = false;
|
|
29
29
|
|
|
30
30
|
// prettier-ignore
|
|
31
31
|
constructor(
|
|
@@ -33,46 +33,44 @@ export class PatchSemaphore {
|
|
|
33
33
|
private readonly _state: StateField<State>
|
|
34
34
|
) {}
|
|
35
35
|
|
|
36
|
-
reconcile(view: EditorView) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
this._inReconcile = false;
|
|
36
|
+
reconcile(view: EditorView, editor: boolean) {
|
|
37
|
+
// TODO(burdon): Better way to do mutex?
|
|
38
|
+
if (this._pending) {
|
|
39
|
+
return;
|
|
41
40
|
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
private doReconcile(view: EditorView) {
|
|
45
|
-
const path = getPath(view.state, this._state);
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
this._pending = true;
|
|
43
|
+
if (editor) {
|
|
44
|
+
this.onEditorChange(view);
|
|
45
|
+
} else {
|
|
46
|
+
this.onAutomergeChange(view);
|
|
47
|
+
}
|
|
48
|
+
this._pending = false;
|
|
49
|
+
}
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
onEditorChange(view: EditorView) {
|
|
52
|
+
// Apply the unreconciled transactions to the document.
|
|
52
53
|
const transactions = view.state.field(this._state).unreconciledTransactions.filter((tx) => !isReconcile(tx));
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
selection = selection.map(inverted);
|
|
54
|
+
const newHeads = updateAutomerge(this._state, this._handle, transactions, view.state);
|
|
55
|
+
|
|
56
|
+
if (newHeads) {
|
|
57
57
|
view.dispatch({
|
|
58
|
-
|
|
59
|
-
annotations: reconcileAnnotation.of(
|
|
58
|
+
effects: updateHeads(newHeads),
|
|
59
|
+
annotations: reconcileAnnotation.of(false),
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
|
+
}
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// TODO(alexjg): this is the call that's resetting the editor state on click.
|
|
68
|
-
newHeads = A.getHeads(this._handle.docSync()!);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Now get the diff between the updated state of the document and the heads and apply that to the codemirror doc.
|
|
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()!);
|
|
72
68
|
const diff = A.equals(oldHeads, newHeads) ? [] : A.diff(this._handle.docSync()!, oldHeads, newHeads);
|
|
69
|
+
|
|
70
|
+
const selection = view.state.selection;
|
|
71
|
+
const path = getPath(view.state, this._state);
|
|
73
72
|
updateCodeMirror(view, selection, path, diff);
|
|
74
73
|
|
|
75
|
-
// Update automerge state.
|
|
76
74
|
view.dispatch({
|
|
77
75
|
effects: updateHeads(newHeads),
|
|
78
76
|
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 } from './styles';
|
|
13
|
+
export { getToken, editorWithToolbarLayout } from './styles';
|
|
14
14
|
export * from './themes';
|
|
15
15
|
export * from './util';
|
package/src/styles/index.ts
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
];
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"semaphore.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/automerge/semaphore.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,EAML,KAAK,UAAU,EACf,KAAK,KAAK,EACX,MAAM,QAAQ,CAAC;AAIhB;;GAEG;AACH,qBAAa,cAAc;IAKvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IALzB,OAAO,CAAC,YAAY,CAAS;gBAIV,OAAO,EAAE,UAAU,EACnB,MAAM,EAAE,UAAU,CAAC,KAAK,CAAC;IAG5C,SAAS,CAAC,IAAI,EAAE,UAAU;IAQ1B,OAAO,CAAC,WAAW;CAqCpB"}
|