@dxos/react-ui-editor 0.6.10-main.bbdfaa4 → 0.6.10-staging.3cfcc89
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 +736 -717
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/InputMode.stories.d.ts +1 -0
- package/dist/types/src/InputMode.stories.d.ts.map +1 -1
- package/dist/types/src/TextEditor.stories.d.ts +19 -12
- package/dist/types/src/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/defaults.d.ts +5 -1
- package/dist/types/src/defaults.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete.d.ts +3 -2
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts +1 -0
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/dnd.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +2 -2
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/folding.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/action.d.ts +1 -1
- package/dist/types/src/extensions/markdown/action.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/changes.d.ts +10 -0
- package/dist/types/src/extensions/markdown/changes.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/changes.test.d.ts +2 -0
- package/dist/types/src/extensions/markdown/changes.test.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/debug.d.ts +11 -0
- package/dist/types/src/extensions/markdown/debug.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/index.d.ts +1 -0
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/styles.d.ts +4 -0
- package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +0 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/theme.d.ts +1 -1
- package/dist/types/src/styles/theme.d.ts.map +1 -1
- package/dist/types/src/styles/tokens.d.ts +2 -4
- package/dist/types/src/styles/tokens.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +1 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/package.json +26 -27
- package/src/TextEditor.stories.tsx +122 -74
- package/src/components/Toolbar/Toolbar.tsx +91 -92
- package/src/defaults.ts +16 -11
- package/src/extensions/annotations.ts +2 -2
- package/src/extensions/autocomplete.ts +4 -1
- package/src/extensions/awareness/awareness.ts +1 -1
- package/src/extensions/comments.ts +11 -45
- package/src/extensions/dnd.ts +3 -5
- package/src/extensions/factories.ts +4 -4
- package/src/extensions/folding.tsx +3 -4
- package/src/extensions/markdown/action.ts +1 -0
- package/src/extensions/markdown/bundle.ts +0 -1
- package/src/extensions/markdown/{link-paste.test.ts → changes.test.ts} +2 -2
- package/src/extensions/markdown/changes.ts +148 -0
- package/src/extensions/markdown/debug.ts +44 -0
- package/src/extensions/markdown/decorate.ts +14 -93
- package/src/extensions/markdown/formatting.ts +1 -2
- package/src/extensions/markdown/highlight.ts +2 -2
- package/src/extensions/markdown/index.ts +1 -0
- package/src/extensions/markdown/styles.ts +103 -0
- package/src/index.ts +0 -2
- package/src/styles/theme.ts +85 -147
- package/src/styles/tokens.ts +4 -2
- package/src/translations.ts +1 -0
- package/dist/types/src/extensions/markdown/link-paste.d.ts +0 -9
- package/dist/types/src/extensions/markdown/link-paste.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/link-paste.test.d.ts +0 -2
- package/dist/types/src/extensions/markdown/link-paste.test.d.ts.map +0 -1
- package/src/extensions/markdown/link-paste.ts +0 -107
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
ListBullets,
|
|
13
13
|
ListChecks,
|
|
14
14
|
ListNumbers,
|
|
15
|
+
MagnifyingGlass,
|
|
15
16
|
Paragraph,
|
|
16
17
|
Quotes,
|
|
17
18
|
TextStrikethrough,
|
|
@@ -140,89 +141,6 @@ const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
|
|
|
140
141
|
);
|
|
141
142
|
};
|
|
142
143
|
|
|
143
|
-
//
|
|
144
|
-
// View Mode
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
const ViewModeIcons: Record<EditorViewMode, Icon> = {
|
|
148
|
-
preview: PencilSimple,
|
|
149
|
-
readonly: PencilSimpleSlash,
|
|
150
|
-
source: MarkdownLogo,
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const MarkdownView = ({ mode }: { mode: EditorViewMode }) => {
|
|
154
|
-
const { t } = useTranslation(translationKey);
|
|
155
|
-
const { onAction } = useToolbarContext('ViewMode');
|
|
156
|
-
const ModeIcon = ViewModeIcons[mode ?? 'preview'];
|
|
157
|
-
const suppressNextTooltip = useRef<boolean>(false);
|
|
158
|
-
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false);
|
|
159
|
-
const [selectOpen, setSelectOpen] = useState<boolean>(false);
|
|
160
|
-
return (
|
|
161
|
-
<Tooltip.Root
|
|
162
|
-
open={tooltipOpen}
|
|
163
|
-
onOpenChange={(nextOpen) => {
|
|
164
|
-
if (nextOpen && suppressNextTooltip.current) {
|
|
165
|
-
suppressNextTooltip.current = false;
|
|
166
|
-
return setTooltipOpen(false);
|
|
167
|
-
} else {
|
|
168
|
-
return setTooltipOpen(nextOpen);
|
|
169
|
-
}
|
|
170
|
-
}}
|
|
171
|
-
>
|
|
172
|
-
{/* TODO(thure): `Select` encounters a ref error if used here (repro: select a heading, then select another
|
|
173
|
-
heading). Determine the root cause and fix or report to Radix. */}
|
|
174
|
-
<DropdownMenu.Root
|
|
175
|
-
open={selectOpen}
|
|
176
|
-
onOpenChange={(nextOpen: boolean) => {
|
|
177
|
-
if (!nextOpen) {
|
|
178
|
-
suppressNextTooltip.current = true;
|
|
179
|
-
}
|
|
180
|
-
return setSelectOpen(nextOpen);
|
|
181
|
-
}}
|
|
182
|
-
>
|
|
183
|
-
<Tooltip.Trigger asChild>
|
|
184
|
-
<NaturalToolbar.Button asChild>
|
|
185
|
-
<DropdownMenu.Trigger asChild>
|
|
186
|
-
<Button variant='ghost' classNames={buttonStyles}>
|
|
187
|
-
<span className='sr-only'>{t('mode label')}</span>
|
|
188
|
-
<ModeIcon className={iconStyles} />
|
|
189
|
-
<CaretDown />
|
|
190
|
-
</Button>
|
|
191
|
-
</DropdownMenu.Trigger>
|
|
192
|
-
</NaturalToolbar.Button>
|
|
193
|
-
</Tooltip.Trigger>
|
|
194
|
-
<DropdownMenu.Portal>
|
|
195
|
-
<DropdownMenu.Content classNames='is-min md:is-min' onCloseAutoFocus={(e) => e.preventDefault()}>
|
|
196
|
-
<DropdownMenu.Viewport>
|
|
197
|
-
{EditorViewModes.map((value) => {
|
|
198
|
-
const Icon = ViewModeIcons[value];
|
|
199
|
-
return (
|
|
200
|
-
<DropdownMenu.CheckboxItem
|
|
201
|
-
key={value}
|
|
202
|
-
checked={value === mode}
|
|
203
|
-
onClick={() => onAction?.({ type: 'view-mode', data: value })}
|
|
204
|
-
>
|
|
205
|
-
<Icon className={iconStyles} />
|
|
206
|
-
<span className='whitespace-nowrap grow'>{t(`${value} mode label`)}</span>
|
|
207
|
-
<Check className={value === mode ? 'visible' : 'invisible'} />
|
|
208
|
-
</DropdownMenu.CheckboxItem>
|
|
209
|
-
);
|
|
210
|
-
})}
|
|
211
|
-
</DropdownMenu.Viewport>
|
|
212
|
-
<DropdownMenu.Arrow />
|
|
213
|
-
</DropdownMenu.Content>
|
|
214
|
-
</DropdownMenu.Portal>
|
|
215
|
-
</DropdownMenu.Root>
|
|
216
|
-
<Tooltip.Portal>
|
|
217
|
-
<Tooltip.Content {...tooltipProps}>
|
|
218
|
-
{t('view mode label')}
|
|
219
|
-
<Tooltip.Arrow />
|
|
220
|
-
</Tooltip.Content>
|
|
221
|
-
</Tooltip.Portal>
|
|
222
|
-
</Tooltip.Root>
|
|
223
|
-
);
|
|
224
|
-
};
|
|
225
|
-
|
|
226
144
|
//
|
|
227
145
|
// Heading
|
|
228
146
|
//
|
|
@@ -474,28 +392,109 @@ const MarkdownCustom = ({ onUpload }: MarkdownCustomOptions = {}) => {
|
|
|
474
392
|
);
|
|
475
393
|
};
|
|
476
394
|
|
|
395
|
+
//
|
|
396
|
+
// View Mode
|
|
397
|
+
//
|
|
398
|
+
|
|
399
|
+
const ViewModeIcons: Record<EditorViewMode, Icon> = {
|
|
400
|
+
preview: PencilSimple,
|
|
401
|
+
readonly: PencilSimpleSlash,
|
|
402
|
+
source: MarkdownLogo,
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const MarkdownView = ({ mode }: { mode: EditorViewMode }) => {
|
|
406
|
+
const { t } = useTranslation(translationKey);
|
|
407
|
+
const { onAction } = useToolbarContext('ViewMode');
|
|
408
|
+
const ModeIcon = ViewModeIcons[mode ?? 'preview'];
|
|
409
|
+
const suppressNextTooltip = useRef<boolean>(false);
|
|
410
|
+
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false);
|
|
411
|
+
const [selectOpen, setSelectOpen] = useState<boolean>(false);
|
|
412
|
+
return (
|
|
413
|
+
<Tooltip.Root
|
|
414
|
+
open={tooltipOpen}
|
|
415
|
+
onOpenChange={(nextOpen) => {
|
|
416
|
+
if (nextOpen && suppressNextTooltip.current) {
|
|
417
|
+
suppressNextTooltip.current = false;
|
|
418
|
+
return setTooltipOpen(false);
|
|
419
|
+
} else {
|
|
420
|
+
return setTooltipOpen(nextOpen);
|
|
421
|
+
}
|
|
422
|
+
}}
|
|
423
|
+
>
|
|
424
|
+
{/* TODO(thure): `Select` encounters a ref error if used here (repro: select a heading, then select another
|
|
425
|
+
heading). Determine the root cause and fix or report to Radix. */}
|
|
426
|
+
<DropdownMenu.Root
|
|
427
|
+
open={selectOpen}
|
|
428
|
+
onOpenChange={(nextOpen: boolean) => {
|
|
429
|
+
if (!nextOpen) {
|
|
430
|
+
suppressNextTooltip.current = true;
|
|
431
|
+
}
|
|
432
|
+
return setSelectOpen(nextOpen);
|
|
433
|
+
}}
|
|
434
|
+
>
|
|
435
|
+
<Tooltip.Trigger asChild>
|
|
436
|
+
<NaturalToolbar.Button asChild>
|
|
437
|
+
<DropdownMenu.Trigger asChild>
|
|
438
|
+
<Button variant='ghost' classNames={buttonStyles}>
|
|
439
|
+
<span className='sr-only'>{t('mode label')}</span>
|
|
440
|
+
<ModeIcon className={iconStyles} />
|
|
441
|
+
<CaretDown />
|
|
442
|
+
</Button>
|
|
443
|
+
</DropdownMenu.Trigger>
|
|
444
|
+
</NaturalToolbar.Button>
|
|
445
|
+
</Tooltip.Trigger>
|
|
446
|
+
<DropdownMenu.Portal>
|
|
447
|
+
<DropdownMenu.Content classNames='is-min md:is-min' onCloseAutoFocus={(e) => e.preventDefault()}>
|
|
448
|
+
<DropdownMenu.Viewport>
|
|
449
|
+
{EditorViewModes.map((value) => {
|
|
450
|
+
const Icon = ViewModeIcons[value];
|
|
451
|
+
return (
|
|
452
|
+
<DropdownMenu.CheckboxItem
|
|
453
|
+
key={value}
|
|
454
|
+
checked={value === mode}
|
|
455
|
+
onClick={() => onAction?.({ type: 'view-mode', data: value })}
|
|
456
|
+
>
|
|
457
|
+
<Icon className={iconStyles} />
|
|
458
|
+
<span className='whitespace-nowrap grow'>{t(`${value} mode label`)}</span>
|
|
459
|
+
<Check className={value === mode ? 'visible' : 'invisible'} />
|
|
460
|
+
</DropdownMenu.CheckboxItem>
|
|
461
|
+
);
|
|
462
|
+
})}
|
|
463
|
+
</DropdownMenu.Viewport>
|
|
464
|
+
<DropdownMenu.Arrow />
|
|
465
|
+
</DropdownMenu.Content>
|
|
466
|
+
</DropdownMenu.Portal>
|
|
467
|
+
</DropdownMenu.Root>
|
|
468
|
+
<Tooltip.Portal>
|
|
469
|
+
<Tooltip.Content {...tooltipProps}>
|
|
470
|
+
{t('view mode label')}
|
|
471
|
+
<Tooltip.Arrow />
|
|
472
|
+
</Tooltip.Content>
|
|
473
|
+
</Tooltip.Portal>
|
|
474
|
+
</Tooltip.Root>
|
|
475
|
+
);
|
|
476
|
+
};
|
|
477
|
+
|
|
477
478
|
//
|
|
478
479
|
// Actions
|
|
479
480
|
//
|
|
480
481
|
|
|
481
|
-
// TODO(burdon): Make extensible.
|
|
482
482
|
const MarkdownActions = () => {
|
|
483
483
|
const { onAction, state } = useToolbarContext('MarkdownActions');
|
|
484
484
|
const { t } = useTranslation(translationKey);
|
|
485
485
|
|
|
486
|
-
let
|
|
486
|
+
let commentToolTipKey = 'comment label';
|
|
487
487
|
if (state?.comment) {
|
|
488
|
-
|
|
488
|
+
commentToolTipKey = 'selection overlaps existing comment label';
|
|
489
489
|
} else if (state?.selection === false) {
|
|
490
|
-
|
|
490
|
+
commentToolTipKey = 'select text to comment label';
|
|
491
491
|
}
|
|
492
492
|
|
|
493
493
|
return (
|
|
494
494
|
<>
|
|
495
|
-
{
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
{/* </ToolbarButton> */}
|
|
495
|
+
<ToolbarButton value='search' Icon={MagnifyingGlass} onClick={() => onAction?.({ type: 'search' })}>
|
|
496
|
+
{t('search label')}
|
|
497
|
+
</ToolbarButton>
|
|
499
498
|
<ToolbarButton
|
|
500
499
|
value='comment'
|
|
501
500
|
Icon={ChatText}
|
|
@@ -503,7 +502,7 @@ const MarkdownActions = () => {
|
|
|
503
502
|
onClick={() => onAction?.({ type: 'comment' })}
|
|
504
503
|
disabled={!state || state.comment || !state.selection}
|
|
505
504
|
>
|
|
506
|
-
{t(
|
|
505
|
+
{t(commentToolTipKey)}
|
|
507
506
|
</ToolbarButton>
|
|
508
507
|
</>
|
|
509
508
|
);
|
package/src/defaults.ts
CHANGED
|
@@ -4,32 +4,37 @@
|
|
|
4
4
|
|
|
5
5
|
import { EditorView } from '@codemirror/view';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
8
|
+
|
|
9
|
+
import { fontMono } from './styles';
|
|
10
|
+
|
|
11
|
+
const margin = '!mt-[16px]';
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
14
|
* CodeMirror content width.
|
|
11
15
|
* 40rem = 640px. Corresponds to initial plank width (Google docs, Stashpad, etc.)
|
|
12
16
|
* 50rem = 800px. Maximum content width for solo mode.
|
|
13
17
|
*/
|
|
14
|
-
export const editorContent = '!
|
|
18
|
+
export const editorContent = mx(margin, '!mli-auto w-full max-w-[min(50rem,100%-2rem)]');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Margin for numbers.
|
|
22
|
+
*/
|
|
23
|
+
export const editorFullWidth = mx(margin, '!ml-[3rem]');
|
|
15
24
|
|
|
16
25
|
export const editorWithToolbarLayout =
|
|
17
26
|
'grid grid-cols-1 grid-rows-[min-content_1fr] data-[toolbar=disabled]:grid-rows-[1fr] justify-center content-start overflow-hidden';
|
|
18
27
|
|
|
19
|
-
|
|
28
|
+
// TODO(burdon): Define scrollMargins for fixed gutter.
|
|
29
|
+
export const editorGutter = EditorView.theme({
|
|
30
|
+
// Match margin from content.
|
|
20
31
|
'.cm-gutters': {
|
|
21
|
-
// Match margin from content.
|
|
22
32
|
marginTop: '16px',
|
|
23
|
-
marginBottom: '16px',
|
|
24
|
-
// Inside within content margin.
|
|
25
|
-
marginRight: '-32px',
|
|
26
|
-
width: '32px',
|
|
27
|
-
backgroundColor: 'transparent !important',
|
|
28
33
|
},
|
|
29
34
|
});
|
|
30
35
|
|
|
31
|
-
export const editorMonospace = EditorView.
|
|
36
|
+
export const editorMonospace = EditorView.theme({
|
|
32
37
|
'.cm-content': {
|
|
33
|
-
fontFamily:
|
|
38
|
+
fontFamily: fontMono,
|
|
34
39
|
},
|
|
35
40
|
});
|
|
@@ -69,10 +69,10 @@ export const annotations = (options: AnnotationOptions = {}): Extension => {
|
|
|
69
69
|
];
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
-
const styles = EditorView.
|
|
72
|
+
const styles = EditorView.theme({
|
|
73
73
|
'.cm-annotation': {
|
|
74
74
|
textDecoration: 'underline',
|
|
75
75
|
textDecorationStyle: 'wavy',
|
|
76
|
-
textDecorationColor: '
|
|
76
|
+
textDecorationColor: 'var(--dx-error)',
|
|
77
77
|
},
|
|
78
78
|
});
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import {
|
|
10
10
|
autocompletion,
|
|
11
11
|
completionKeymap,
|
|
12
|
+
type CompletionSource,
|
|
12
13
|
type Completion,
|
|
13
14
|
type CompletionContext,
|
|
14
15
|
type CompletionResult,
|
|
@@ -20,13 +21,14 @@ export type AutocompleteResult = Completion;
|
|
|
20
21
|
|
|
21
22
|
export type AutocompleteOptions = {
|
|
22
23
|
activateOnTyping?: boolean;
|
|
24
|
+
override?: CompletionSource[];
|
|
23
25
|
onSearch?: (text: string) => Completion[];
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* Autocomplete extension.
|
|
28
30
|
*/
|
|
29
|
-
export const autocomplete = ({ activateOnTyping, onSearch }: AutocompleteOptions = {}) => {
|
|
31
|
+
export const autocomplete = ({ activateOnTyping, override, onSearch }: AutocompleteOptions = {}) => {
|
|
30
32
|
const extentions = [
|
|
31
33
|
// https://codemirror.net/docs/ref/#view.keymap
|
|
32
34
|
// https://discuss.codemirror.net/t/how-can-i-replace-the-default-autocompletion-keymap-v6/3322
|
|
@@ -37,6 +39,7 @@ export const autocomplete = ({ activateOnTyping, onSearch }: AutocompleteOptions
|
|
|
37
39
|
// https://codemirror.net/docs/ref/#autocomplete.autocompletion
|
|
38
40
|
autocompletion({
|
|
39
41
|
activateOnTyping,
|
|
42
|
+
override,
|
|
40
43
|
|
|
41
44
|
// closeOnBlur: false,
|
|
42
45
|
// defaultKeymap: false,
|
|
@@ -32,7 +32,6 @@ import { nonNullable } from '@dxos/util';
|
|
|
32
32
|
import { Cursor } from './cursor';
|
|
33
33
|
import { type Comment, type Range } from './types';
|
|
34
34
|
import { overlap } from './util';
|
|
35
|
-
import { getToken } from '../styles';
|
|
36
35
|
import { callbackWrapper } from '../util';
|
|
37
36
|
|
|
38
37
|
//
|
|
@@ -106,53 +105,20 @@ export const commentsState = StateField.define<CommentsState>({
|
|
|
106
105
|
},
|
|
107
106
|
});
|
|
108
107
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const styles = EditorView.baseTheme({
|
|
108
|
+
/**
|
|
109
|
+
* NOTE: Matches search.
|
|
110
|
+
*/
|
|
111
|
+
const styles = EditorView.theme({
|
|
114
112
|
'.cm-comment, .cm-comment-current': {
|
|
113
|
+
margin: '0 -3px',
|
|
114
|
+
padding: '3px',
|
|
115
|
+
borderRadius: '3px',
|
|
116
|
+
backgroundColor: 'var(--dx-cmCommentSurface)',
|
|
117
|
+
color: 'var(--dx-cmComment)',
|
|
115
118
|
cursor: 'pointer',
|
|
116
|
-
borderWidth: '1px',
|
|
117
|
-
borderStyle: 'solid',
|
|
118
|
-
borderRadius: '2px',
|
|
119
|
-
transition: 'background-color 0.1s ease',
|
|
120
|
-
},
|
|
121
|
-
// Light theme.
|
|
122
|
-
'&light .cm-comment': {
|
|
123
|
-
backgroundColor: getToken('extend.colors.yellow.50'),
|
|
124
|
-
mixBlendMode: 'darken',
|
|
125
|
-
borderColor: getToken('extend.colors.yellow.100'),
|
|
126
|
-
},
|
|
127
|
-
'&light .cm-comment:hover': { backgroundColor: getToken('extend.colors.yellow.100') },
|
|
128
|
-
'&light .cm-comment-current': {
|
|
129
|
-
backgroundColor: getToken('extend.colors.primary.100'),
|
|
130
|
-
borderColor: getToken('extend.colors.primary.200'),
|
|
131
|
-
},
|
|
132
|
-
'&light .cm-comment-current:hover': {
|
|
133
|
-
backgroundColor: getToken('extend.colors.primary.150'),
|
|
134
|
-
borderColor: getToken('extend.colors.primary.250'),
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
// Dark theme.
|
|
138
|
-
'&dark .cm-comment': {
|
|
139
|
-
color: getToken('extend.colors.yellow.50'),
|
|
140
|
-
backgroundColor: getToken('extend.colors.yellow.800'),
|
|
141
|
-
borderColor: getToken('extend.colors.yellow.700'),
|
|
142
|
-
mixBlendMode: 'plus-lighter',
|
|
143
|
-
},
|
|
144
|
-
'&dark .cm-comment:hover': {
|
|
145
|
-
backgroundColor: getToken('extend.colors.yellow.700'),
|
|
146
|
-
borderColor: getToken('extend.colors.yellow.650'),
|
|
147
|
-
},
|
|
148
|
-
'&dark .cm-comment-current': {
|
|
149
|
-
color: getToken('extend.colors.primary.50'),
|
|
150
|
-
backgroundColor: getToken('extend.colors.primary.800'),
|
|
151
|
-
borderColor: getToken('extend.colors.primary.700'),
|
|
152
119
|
},
|
|
153
|
-
'
|
|
154
|
-
|
|
155
|
-
borderColor: getToken('extend.colors.primary.650'),
|
|
120
|
+
'.cm-comment:hover, .cm-comment-current': {
|
|
121
|
+
textDecoration: 'underline',
|
|
156
122
|
},
|
|
157
123
|
});
|
|
158
124
|
|
package/src/extensions/dnd.ts
CHANGED
|
@@ -5,14 +5,12 @@
|
|
|
5
5
|
import type { Extension } from '@codemirror/state';
|
|
6
6
|
import { dropCursor, EditorView } from '@codemirror/view';
|
|
7
7
|
|
|
8
|
-
import { getToken } from '../styles';
|
|
9
|
-
|
|
10
8
|
export type DNDOptions = { onDrop?: (view: EditorView, event: { files: FileList }) => void };
|
|
11
9
|
|
|
12
|
-
const styles = EditorView.
|
|
10
|
+
const styles = EditorView.theme({
|
|
13
11
|
'.cm-dropCursor': {
|
|
14
|
-
borderLeft:
|
|
15
|
-
color:
|
|
12
|
+
borderLeft: '2px solid var(--dx-accentText)',
|
|
13
|
+
color: 'var(--dx-accentText)',
|
|
16
14
|
padding: '0 4px',
|
|
17
15
|
},
|
|
18
16
|
'.cm-dropCursor:after': {
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
scrollPastEnd,
|
|
20
20
|
} from '@codemirror/view';
|
|
21
21
|
import defaultsDeep from 'lodash.defaultsdeep';
|
|
22
|
+
import merge from 'lodash.merge';
|
|
22
23
|
|
|
23
24
|
import { generateName } from '@dxos/display-name';
|
|
24
25
|
import { log } from '@dxos/log';
|
|
@@ -128,8 +129,8 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
|
|
|
128
129
|
//
|
|
129
130
|
|
|
130
131
|
export type ThemeExtensionsOptions = {
|
|
131
|
-
theme?: ThemeStyles;
|
|
132
132
|
themeMode?: ThemeMode;
|
|
133
|
+
styles?: ThemeStyles;
|
|
133
134
|
slots?: {
|
|
134
135
|
editor?: {
|
|
135
136
|
className?: string;
|
|
@@ -148,12 +149,11 @@ const defaultThemeSlots = {
|
|
|
148
149
|
|
|
149
150
|
// TODO(burdon): Should only have one baseTheme?
|
|
150
151
|
// https://codemirror.net/examples/styling
|
|
151
|
-
export const createThemeExtensions = ({
|
|
152
|
+
export const createThemeExtensions = ({ themeMode, styles, slots: _slots }: ThemeExtensionsOptions = {}): Extension => {
|
|
152
153
|
const slots = defaultsDeep({}, _slots, defaultThemeSlots);
|
|
153
154
|
return [
|
|
154
|
-
EditorView.baseTheme(defaultTheme),
|
|
155
155
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
156
|
-
|
|
156
|
+
EditorView.baseTheme(styles ? merge({}, defaultTheme, styles) : defaultTheme),
|
|
157
157
|
slots.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
|
|
158
158
|
slots.content?.className && EditorView.contentAttributes.of({ class: slots.content.className }),
|
|
159
159
|
].filter(isNotFalsy);
|
|
@@ -6,7 +6,8 @@ import { codeFolding, foldGutter } from '@codemirror/language';
|
|
|
6
6
|
import { type Extension } from '@codemirror/state';
|
|
7
7
|
import React from 'react';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { Icon } from '@dxos/react-ui';
|
|
10
|
+
import { getSize } from '@dxos/react-ui-theme';
|
|
10
11
|
|
|
11
12
|
import { renderRoot } from './util';
|
|
12
13
|
|
|
@@ -23,9 +24,7 @@ export const folding = (_props: FoldingOptions = {}): Extension => [
|
|
|
23
24
|
markerDOM: (open) => {
|
|
24
25
|
return renderRoot(
|
|
25
26
|
document.createElement('div'),
|
|
26
|
-
<
|
|
27
|
-
<use href={'/icons.svg#ph--caret-right--regular'} />
|
|
28
|
-
</svg>,
|
|
27
|
+
<Icon icon='ph--caret-right--regular' classNames={[getSize(3), 'm-2 cursor-pointer', open && 'rotate-90']} />,
|
|
29
28
|
);
|
|
30
29
|
},
|
|
31
30
|
}),
|
|
@@ -61,7 +61,6 @@ export const createMarkdownExtensions = ({ themeMode }: MarkdownBundleOptions =
|
|
|
61
61
|
syntaxHighlighting(markdownHighlightStyle()),
|
|
62
62
|
|
|
63
63
|
keymap.of([
|
|
64
|
-
// TODO(burdon): Indent by 4 if in task list.
|
|
65
64
|
// https://codemirror.net/docs/ref/#commands.indentWithTab
|
|
66
65
|
indentWithTab,
|
|
67
66
|
|
|
@@ -6,7 +6,7 @@ import { expect } from 'chai';
|
|
|
6
6
|
|
|
7
7
|
import { describe, test } from '@dxos/test';
|
|
8
8
|
|
|
9
|
-
import { createLinkLabel } from './
|
|
9
|
+
import { createLinkLabel } from './changes';
|
|
10
10
|
|
|
11
11
|
const testCases = [
|
|
12
12
|
{ input: 'https://www.example.com', expected: 'example.com' },
|
|
@@ -19,7 +19,7 @@ const testCases = [
|
|
|
19
19
|
{ input: 'ftp://example.com', expected: 'ftp://example.com' },
|
|
20
20
|
];
|
|
21
21
|
|
|
22
|
-
describe('
|
|
22
|
+
describe('changes', () => {
|
|
23
23
|
test('createLinkLabel', () => {
|
|
24
24
|
testCases.forEach(({ input, expected }) => {
|
|
25
25
|
expect(createLinkLabel(new URL(input))).to.eq(expected);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
|
6
|
+
import { type ChangeSpec, Transaction } from '@codemirror/state';
|
|
7
|
+
import { ViewPlugin, type ViewUpdate, type PluginValue } from '@codemirror/view';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Monitors and augments changes.
|
|
11
|
+
*/
|
|
12
|
+
// TODO(burdon): Tests.
|
|
13
|
+
export const adjustChanges = () => {
|
|
14
|
+
return ViewPlugin.fromClass(
|
|
15
|
+
class implements PluginValue {
|
|
16
|
+
update(update: ViewUpdate) {
|
|
17
|
+
const tree = syntaxTree(update.state);
|
|
18
|
+
const adjustments: ChangeSpec[] = [];
|
|
19
|
+
|
|
20
|
+
for (const tr of update.transactions) {
|
|
21
|
+
const event = tr.annotation(Transaction.userEvent);
|
|
22
|
+
switch (event) {
|
|
23
|
+
//
|
|
24
|
+
// Enter
|
|
25
|
+
//
|
|
26
|
+
case 'input': {
|
|
27
|
+
const changes = tr.changes;
|
|
28
|
+
if (changes.empty) {
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
changes.iterChanges((fromA) => {
|
|
33
|
+
const node = tree.resolveInner(fromA, 1);
|
|
34
|
+
if (node?.name === 'BulletList') {
|
|
35
|
+
// Add space to previous line if an empty list item (otherwise it is not interpreted as a Task).
|
|
36
|
+
const { text } = update.state.doc.lineAt(fromA);
|
|
37
|
+
if (text.endsWith(']')) {
|
|
38
|
+
adjustments.push({ from: fromA, to: fromA, insert: ' ' });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//
|
|
47
|
+
// Paste
|
|
48
|
+
//
|
|
49
|
+
case 'input.paste': {
|
|
50
|
+
const changes = tr.changes;
|
|
51
|
+
if (changes.empty) {
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
changes.iterChanges((fromA, toA, fromB, toB, text) => {
|
|
56
|
+
// Check for URL.
|
|
57
|
+
const url = getValidUrl(update.view.state.sliceDoc(fromB, toB));
|
|
58
|
+
if (url) {
|
|
59
|
+
const node = tree.resolveInner(fromA, -1);
|
|
60
|
+
const invalidPositions = new Set(['Link', 'LinkMark', 'Code', 'CodeText', 'FencedCode', 'URL']);
|
|
61
|
+
if (!invalidPositions.has(node?.name)) {
|
|
62
|
+
const replacedText = tr.startState.sliceDoc(fromA, toA);
|
|
63
|
+
adjustments.push({ from: fromA, to: toB, insert: createLink(url, replacedText) });
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
const node = tree.resolveInner(fromA, 1);
|
|
67
|
+
switch (node?.name) {
|
|
68
|
+
case 'Task': {
|
|
69
|
+
// Remove task marker if pasting into task list.
|
|
70
|
+
const str = text.toString();
|
|
71
|
+
const match = str.match(/\s*- \[[ xX]\]\s*(.+)/);
|
|
72
|
+
if (match) {
|
|
73
|
+
const [, replacement] = match;
|
|
74
|
+
adjustments.push({ from: fromA, to: toB, insert: replacement });
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// TODO(burdon): Is this the right way to augment changes?
|
|
88
|
+
if (adjustments.length) {
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
update.view.dispatch(
|
|
91
|
+
update.view.state.update({
|
|
92
|
+
changes: adjustments,
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
//
|
|
103
|
+
// Links
|
|
104
|
+
//
|
|
105
|
+
|
|
106
|
+
export const createLink = (url: URL, label: string): string => {
|
|
107
|
+
// Check if image.
|
|
108
|
+
// Example: https://dxos.network/dxos-logotype-blue.png
|
|
109
|
+
const { host, pathname } = url;
|
|
110
|
+
const [, extension] = pathname.split('.');
|
|
111
|
+
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'];
|
|
112
|
+
if (imageExtensions.includes(extension)) {
|
|
113
|
+
return ``;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!label) {
|
|
117
|
+
label = createLinkLabel(url);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return `[${label}](${url})`;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const createLinkLabel = (url: URL): string => {
|
|
124
|
+
let { protocol, host, pathname } = url;
|
|
125
|
+
if (protocol === 'http:' || protocol === 'https:') {
|
|
126
|
+
protocol = '';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// NOTE(Zan): Consult: https://github.com/dxos/dxos/issues/7331 before changing this.
|
|
130
|
+
// Remove 'www.' if at the beginning of the URL
|
|
131
|
+
host = host.replace(/^www\./, '');
|
|
132
|
+
|
|
133
|
+
return [protocol, host].filter(Boolean).join('//') + (pathname !== '/' ? pathname : '');
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const getValidUrl = (str: string): URL | undefined => {
|
|
137
|
+
const validProtocols = ['http:', 'https:', 'mailto:', 'tel:'];
|
|
138
|
+
try {
|
|
139
|
+
const url = new URL(str);
|
|
140
|
+
if (!validProtocols.includes(url.protocol)) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return url;
|
|
145
|
+
} catch (_err) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
};
|