@dxos/react-ui-editor 0.4.8-main.eb5ae94 → 0.4.8-main.fff1521
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 +2725 -2711
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +18 -34
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +80 -5
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +11 -5
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +4 -2
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts +2 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/defs.d.ts +1 -15
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/index.d.ts +0 -1
- package/dist/types/src/extensions/automerge/index.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/sync.d.ts +2 -1
- package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/{hooks → extensions/awareness}/awareness-provider.d.ts +1 -1
- package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -0
- package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -0
- package/dist/types/src/extensions/awareness/index.d.ts +3 -0
- package/dist/types/src/extensions/awareness/index.d.ts.map +1 -0
- package/dist/types/src/extensions/comments.d.ts +2 -2
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/cursor-line-margin.d.ts +6 -7
- package/dist/types/src/extensions/cursor-line-margin.d.ts.map +1 -1
- package/dist/types/src/extensions/cursor.d.ts +1 -1
- package/dist/types/src/extensions/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/doc.d.ts +3 -0
- package/dist/types/src/extensions/doc.d.ts.map +1 -0
- package/dist/types/src/extensions/factories.d.ts +49 -0
- package/dist/types/src/extensions/factories.d.ts.map +1 -0
- package/dist/types/src/extensions/index.d.ts +4 -1
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts +5 -4
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts +8 -5
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/image.d.ts +6 -0
- package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/table.d.ts +2 -2
- package/dist/types/src/extensions/markdown/table.d.ts.map +1 -1
- package/dist/types/src/extensions/modes.d.ts +1 -0
- package/dist/types/src/extensions/modes.d.ts.map +1 -1
- package/dist/types/src/extensions/state.d.ts +20 -0
- package/dist/types/src/extensions/state.d.ts.map +1 -0
- package/dist/types/src/extensions/types.d.ts +9 -0
- package/dist/types/src/extensions/types.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +1 -4
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useDocAccessor.d.ts +7 -0
- package/dist/types/src/hooks/useDocAccessor.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextEditor.d.ts +3 -44
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.stories.d.ts +7 -0
- package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts +0 -3
- package/dist/types/src/themes/default.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 +25 -27
- package/src/components/TextEditor/TextEditor.stories.tsx +520 -23
- package/src/components/TextEditor/TextEditor.tsx +45 -156
- package/src/components/Toolbar/Toolbar.stories.tsx +54 -55
- package/src/components/Toolbar/Toolbar.tsx +96 -35
- package/src/extensions/automerge/automerge.stories.tsx +16 -17
- package/src/extensions/automerge/automerge.ts +2 -1
- package/src/extensions/automerge/cursor.ts +1 -1
- package/src/extensions/automerge/defs.ts +1 -22
- package/src/extensions/automerge/index.ts +0 -1
- package/src/extensions/automerge/sync.ts +2 -9
- package/src/extensions/automerge/update-automerge.ts +2 -1
- package/src/{hooks → extensions/awareness}/awareness-provider.ts +1 -1
- package/src/extensions/{awareness.ts → awareness/awareness.ts} +1 -1
- package/src/extensions/awareness/index.ts +6 -0
- package/src/extensions/command/state.ts +1 -1
- package/src/extensions/comments.ts +9 -5
- package/src/extensions/cursor-line-margin.ts +17 -18
- package/src/extensions/cursor.ts +1 -1
- package/src/extensions/doc.ts +14 -0
- package/src/extensions/factories.ts +167 -0
- package/src/extensions/index.ts +4 -1
- package/src/extensions/markdown/bundle.ts +11 -28
- package/src/extensions/markdown/decorate.ts +2 -4
- package/src/extensions/markdown/formatting.test.ts +13 -13
- package/src/extensions/markdown/formatting.ts +106 -93
- package/src/extensions/markdown/image.ts +7 -0
- package/src/extensions/markdown/table.ts +11 -3
- package/src/extensions/modes.ts +3 -1
- package/src/extensions/state.ts +90 -0
- package/src/extensions/types.ts +16 -0
- package/src/hooks/index.ts +1 -5
- package/src/hooks/useActionHandler.ts +5 -1
- package/src/hooks/useDocAccessor.ts +21 -0
- package/src/hooks/useTextEditor.stories.tsx +40 -35
- package/src/hooks/useTextEditor.ts +14 -120
- package/src/index.ts +1 -1
- package/src/themes/default.ts +1 -28
- package/src/translations.ts +1 -0
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +0 -103
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +0 -1
- package/dist/types/src/extensions/awareness.d.ts.map +0 -1
- package/dist/types/src/extensions/outliner.d.ts +0 -4
- package/dist/types/src/extensions/outliner.d.ts.map +0 -1
- package/dist/types/src/hooks/awareness-provider.d.ts.map +0 -1
- package/dist/types/src/hooks/defs.d.ts +0 -20
- package/dist/types/src/hooks/defs.d.ts.map +0 -1
- package/dist/types/src/hooks/useEditorView.d.ts +0 -20
- package/dist/types/src/hooks/useEditorView.d.ts.map +0 -1
- package/dist/types/src/hooks/useTextModel.d.ts +0 -33
- package/dist/types/src/hooks/useTextModel.d.ts.map +0 -1
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +0 -524
- package/src/extensions/outliner.ts +0 -12
- package/src/hooks/defs.ts +0 -30
- package/src/hooks/useEditorView.ts +0 -32
- package/src/hooks/useTextModel.ts +0 -121
- /package/dist/types/src/extensions/{awareness.d.ts → awareness/awareness.d.ts} +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { closeBrackets } from '@codemirror/autocomplete';
|
|
6
|
+
import { history } from '@codemirror/commands';
|
|
7
|
+
import { bracketMatching } from '@codemirror/language';
|
|
8
|
+
import { EditorState, type Extension } from '@codemirror/state';
|
|
9
|
+
import {
|
|
10
|
+
EditorView,
|
|
11
|
+
crosshairCursor,
|
|
12
|
+
drawSelection,
|
|
13
|
+
dropCursor,
|
|
14
|
+
highlightActiveLine,
|
|
15
|
+
lineNumbers,
|
|
16
|
+
placeholder,
|
|
17
|
+
scrollPastEnd,
|
|
18
|
+
} from '@codemirror/view';
|
|
19
|
+
import defaultsDeep from 'lodash.defaultsdeep';
|
|
20
|
+
|
|
21
|
+
import { generateName } from '@dxos/display-name';
|
|
22
|
+
import { type DocAccessor } from '@dxos/echo-schema';
|
|
23
|
+
import { log } from '@dxos/log';
|
|
24
|
+
import { type Space } from '@dxos/react-client/echo';
|
|
25
|
+
import { type Identity } from '@dxos/react-client/halo';
|
|
26
|
+
import { type ThemeMode } from '@dxos/react-ui';
|
|
27
|
+
import { type HuePalette, hueTokens } from '@dxos/react-ui-theme';
|
|
28
|
+
import { hexToHue, isNotFalsy } from '@dxos/util';
|
|
29
|
+
|
|
30
|
+
import { automerge } from './automerge';
|
|
31
|
+
import { awareness, SpaceAwarenessProvider } from './awareness';
|
|
32
|
+
import { type ThemeStyles } from '../styles';
|
|
33
|
+
import { defaultTheme } from '../themes';
|
|
34
|
+
|
|
35
|
+
// TODO(burdon): Move into extensions folder.
|
|
36
|
+
|
|
37
|
+
//
|
|
38
|
+
// Basic
|
|
39
|
+
//
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* https://codemirror.net/docs/extensions
|
|
43
|
+
* https://github.com/codemirror/basic-setup
|
|
44
|
+
*/
|
|
45
|
+
// TODO(burdon): Reconcile with createMarkdownExtensions.
|
|
46
|
+
export type BasicExtensionsOptions = {
|
|
47
|
+
allowMultipleSelections?: boolean;
|
|
48
|
+
bracketMatching?: boolean;
|
|
49
|
+
closeBrackets?: boolean;
|
|
50
|
+
crosshairCursor?: boolean;
|
|
51
|
+
dropCursor?: boolean;
|
|
52
|
+
drawSelection?: boolean;
|
|
53
|
+
editable?: boolean;
|
|
54
|
+
highlightActiveLine?: boolean;
|
|
55
|
+
history?: boolean;
|
|
56
|
+
lineNumbers?: boolean;
|
|
57
|
+
lineWrapping?: boolean;
|
|
58
|
+
placeholder?: string;
|
|
59
|
+
readonly?: boolean;
|
|
60
|
+
scrollPastEnd?: boolean;
|
|
61
|
+
tabSize?: number;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const defaults: BasicExtensionsOptions = {
|
|
65
|
+
bracketMatching: true,
|
|
66
|
+
closeBrackets: true,
|
|
67
|
+
drawSelection: true,
|
|
68
|
+
editable: true,
|
|
69
|
+
history: true,
|
|
70
|
+
lineWrapping: true,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extension => {
|
|
74
|
+
const props: BasicExtensionsOptions = defaultsDeep({}, _props, defaults);
|
|
75
|
+
return [
|
|
76
|
+
// TODO(burdon): Doesn't catch errors in keymap functions.
|
|
77
|
+
EditorView.exceptionSink.of((err) => {
|
|
78
|
+
log.catch(err);
|
|
79
|
+
}),
|
|
80
|
+
|
|
81
|
+
props.allowMultipleSelections && EditorState.allowMultipleSelections.of(true),
|
|
82
|
+
props.bracketMatching && bracketMatching(),
|
|
83
|
+
props.closeBrackets && closeBrackets(),
|
|
84
|
+
props.crosshairCursor && crosshairCursor(),
|
|
85
|
+
props.dropCursor && dropCursor(),
|
|
86
|
+
props.drawSelection && drawSelection(),
|
|
87
|
+
props.highlightActiveLine && highlightActiveLine(),
|
|
88
|
+
props.history && history(),
|
|
89
|
+
props.lineNumbers && lineNumbers(),
|
|
90
|
+
props.lineWrapping && EditorView.lineWrapping,
|
|
91
|
+
props.placeholder && placeholder(props.placeholder),
|
|
92
|
+
props.readonly && [EditorState.readOnly.of(true), EditorView.editable.of(false)],
|
|
93
|
+
props.scrollPastEnd && scrollPastEnd(),
|
|
94
|
+
props.tabSize && EditorState.tabSize.of(props.tabSize),
|
|
95
|
+
].filter(isNotFalsy);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
//
|
|
99
|
+
// Theme
|
|
100
|
+
//
|
|
101
|
+
|
|
102
|
+
export type ThemeExtensionsOptions = {
|
|
103
|
+
theme?: ThemeStyles;
|
|
104
|
+
themeMode?: ThemeMode;
|
|
105
|
+
slots?: {
|
|
106
|
+
editor?: {
|
|
107
|
+
className?: string;
|
|
108
|
+
};
|
|
109
|
+
content?: {
|
|
110
|
+
className?: string;
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const defaultSlots = {
|
|
116
|
+
editor: {
|
|
117
|
+
className: 'w-full bs-full',
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const createThemeExtensions = ({ theme, themeMode, slots: _slots }: ThemeExtensionsOptions = {}): Extension => {
|
|
122
|
+
const slots = defaultsDeep({}, _slots, defaultSlots);
|
|
123
|
+
return [
|
|
124
|
+
EditorView.baseTheme(defaultTheme),
|
|
125
|
+
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
126
|
+
theme && EditorView.theme(theme),
|
|
127
|
+
slots.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
|
|
128
|
+
slots.content?.className && EditorView.contentAttributes.of({ class: slots.content.className }),
|
|
129
|
+
].filter(isNotFalsy);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
//
|
|
133
|
+
// Data
|
|
134
|
+
//
|
|
135
|
+
|
|
136
|
+
export type DataExtensionsProps = {
|
|
137
|
+
id: string;
|
|
138
|
+
text: DocAccessor; // TODO(burdon): Rename content.
|
|
139
|
+
space?: Space;
|
|
140
|
+
identity?: Identity | null;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// TODO(burdon): Factor out automerge defs and extension (not hook).
|
|
144
|
+
// TODO(burdon): Move out of react-ui-editor (remove echo deps).
|
|
145
|
+
export const createDataExtensions = ({ id, text, space, identity }: DataExtensionsProps): Extension[] => {
|
|
146
|
+
const extensions: Extension[] = [automerge(text)];
|
|
147
|
+
|
|
148
|
+
if (space && identity) {
|
|
149
|
+
const peerId = identity?.identityKey.toHex();
|
|
150
|
+
const { cursorLightValue, cursorDarkValue } =
|
|
151
|
+
hueTokens[(identity?.profile?.data?.hue as HuePalette | undefined) ?? hexToHue(peerId ?? '0')];
|
|
152
|
+
const awarenessProvider = new SpaceAwarenessProvider({
|
|
153
|
+
space,
|
|
154
|
+
channel: `awareness.${id}`,
|
|
155
|
+
peerId: identity.identityKey.toHex(),
|
|
156
|
+
info: {
|
|
157
|
+
displayName: identity.profile?.displayName ?? generateName(identity.identityKey.toHex()),
|
|
158
|
+
color: cursorDarkValue,
|
|
159
|
+
lightColor: cursorLightValue,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
extensions.push(awareness(awarenessProvider));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return extensions;
|
|
167
|
+
};
|
package/src/extensions/index.ts
CHANGED
|
@@ -11,10 +11,13 @@ export * from './command';
|
|
|
11
11
|
export * from './comments';
|
|
12
12
|
export * from './cursor';
|
|
13
13
|
export * from './cursor-line-margin';
|
|
14
|
+
export * from './doc';
|
|
14
15
|
export * from './dnd';
|
|
16
|
+
export * from './factories';
|
|
15
17
|
export * from './listener';
|
|
16
18
|
export * from './markdown';
|
|
17
19
|
export * from './mention';
|
|
18
20
|
export * from './modes';
|
|
19
|
-
export * from './
|
|
21
|
+
export * from './state';
|
|
22
|
+
export * from './types';
|
|
20
23
|
export * from './typewriter';
|
|
@@ -2,51 +2,34 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { closeBracketsKeymap } from '@codemirror/autocomplete';
|
|
6
|
+
import { historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
|
|
7
7
|
import { markdownLanguage, markdown } from '@codemirror/lang-markdown';
|
|
8
|
-
import {
|
|
8
|
+
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
9
9
|
import { languages } from '@codemirror/language-data';
|
|
10
10
|
import { searchKeymap } from '@codemirror/search';
|
|
11
|
-
import {
|
|
11
|
+
import { type Extension } from '@codemirror/state';
|
|
12
12
|
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
|
13
|
-
import {
|
|
13
|
+
import { keymap } from '@codemirror/view';
|
|
14
14
|
|
|
15
15
|
import { type ThemeMode } from '@dxos/react-ui';
|
|
16
|
-
import { isNotFalsy } from '@dxos/util';
|
|
17
16
|
|
|
18
17
|
import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
|
|
19
|
-
import { type TextEditorProps } from '../../components';
|
|
20
18
|
|
|
21
19
|
export type MarkdownBundleOptions = {
|
|
22
20
|
themeMode?: ThemeMode;
|
|
23
|
-
}
|
|
21
|
+
};
|
|
24
22
|
|
|
25
23
|
/**
|
|
26
|
-
*
|
|
24
|
+
* Creates markdown extensions.
|
|
25
|
+
* To be used in conjunction with createBasicExtensions.
|
|
26
|
+
*
|
|
27
27
|
* Refs:
|
|
28
28
|
* https://codemirror.net/docs/community
|
|
29
29
|
* https://codemirror.net/docs/ref/#codemirror.basicSetup
|
|
30
30
|
*/
|
|
31
|
-
export const createMarkdownExtensions = ({
|
|
32
|
-
themeMode,
|
|
33
|
-
placeholder: _placeholder,
|
|
34
|
-
lineWrapping = true,
|
|
35
|
-
}: MarkdownBundleOptions = {}): Extension[] => {
|
|
31
|
+
export const createMarkdownExtensions = ({ themeMode }: MarkdownBundleOptions = {}): Extension[] => {
|
|
36
32
|
return [
|
|
37
|
-
// TODO(burdon): Reconcile with createBasicExtensions.
|
|
38
|
-
lineWrapping && EditorView.lineWrapping,
|
|
39
|
-
EditorState.allowMultipleSelections.of(true),
|
|
40
|
-
EditorState.tabSize.of(2),
|
|
41
|
-
|
|
42
|
-
bracketMatching(),
|
|
43
|
-
closeBrackets(),
|
|
44
|
-
dropCursor(),
|
|
45
|
-
drawSelection(),
|
|
46
|
-
history(),
|
|
47
|
-
indentOnInput(),
|
|
48
|
-
_placeholder && placeholder(_placeholder),
|
|
49
|
-
|
|
50
33
|
// Main extension.
|
|
51
34
|
// https://github.com/codemirror/lang-markdown
|
|
52
35
|
// https://codemirror.net/5/mode/markdown/index.html (demo).
|
|
@@ -86,5 +69,5 @@ export const createMarkdownExtensions = ({
|
|
|
86
69
|
// https://codemirror.net/docs/ref/#commands.standardKeymap
|
|
87
70
|
...standardKeymap,
|
|
88
71
|
]),
|
|
89
|
-
]
|
|
72
|
+
];
|
|
90
73
|
};
|
|
@@ -48,7 +48,7 @@ class CheckboxWidget extends WidgetType {
|
|
|
48
48
|
|
|
49
49
|
override toDOM(view: EditorView) {
|
|
50
50
|
const input = document.createElement('input');
|
|
51
|
-
input.className = 'cm-task-checkbox';
|
|
51
|
+
input.className = 'cm-task-checkbox ch-checkbox ch-focus-ring -mbs-0.5';
|
|
52
52
|
input.type = 'checkbox';
|
|
53
53
|
input.checked = this._checked;
|
|
54
54
|
if (view.state.readOnly) {
|
|
@@ -148,9 +148,7 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
|
|
|
148
148
|
marks[1].from,
|
|
149
149
|
node.to,
|
|
150
150
|
options.renderLinkButton
|
|
151
|
-
? Decoration.replace({
|
|
152
|
-
widget: new LinkButton(url, options.renderLinkButton),
|
|
153
|
-
})
|
|
151
|
+
? Decoration.replace({ widget: new LinkButton(url, options.renderLinkButton) })
|
|
154
152
|
: hide,
|
|
155
153
|
);
|
|
156
154
|
}
|
|
@@ -9,20 +9,20 @@ import { expect } from 'chai';
|
|
|
9
9
|
import { describe, test } from '@dxos/test';
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
addBlockquote,
|
|
13
|
+
addCodeblock,
|
|
14
|
+
addLink,
|
|
15
|
+
addList,
|
|
13
16
|
addStyle,
|
|
17
|
+
getFormatting,
|
|
14
18
|
removeStyle,
|
|
15
|
-
addLink,
|
|
16
19
|
removeLink,
|
|
17
|
-
addList,
|
|
18
20
|
removeList,
|
|
19
|
-
addBlockquote,
|
|
20
21
|
removeBlockquote,
|
|
21
|
-
addCodeblock,
|
|
22
22
|
removeCodeblock,
|
|
23
|
+
setHeading,
|
|
23
24
|
Inline,
|
|
24
25
|
List,
|
|
25
|
-
getFormatting,
|
|
26
26
|
type Formatting,
|
|
27
27
|
} from './formatting';
|
|
28
28
|
|
|
@@ -204,22 +204,22 @@ describe('removeStyle', () => {
|
|
|
204
204
|
});
|
|
205
205
|
|
|
206
206
|
describe('addLink', () => {
|
|
207
|
-
testCommand('adds a link', '{}', addLink, '[]({})');
|
|
207
|
+
testCommand('adds a link', '{}', addLink(), '[]({})');
|
|
208
208
|
|
|
209
|
-
testCommand('adds a link around text', 'hello {world}', addLink, 'hello [world]({})');
|
|
209
|
+
testCommand('adds a link around text', 'hello {world}', addLink(), 'hello [world]({})');
|
|
210
210
|
|
|
211
|
-
testCommand('clears existing links', '[hello {world}](foo)', addLink, 'hello [world]({})');
|
|
211
|
+
testCommand('clears existing links', '[hello {world}](foo)', addLink(), 'hello [world]({})');
|
|
212
212
|
|
|
213
|
-
testCommand('does nothing across blocks', '{one\n\ntwo}', addLink, null);
|
|
213
|
+
testCommand('does nothing across blocks', '{one\n\ntwo}', addLink(), null);
|
|
214
214
|
|
|
215
|
-
testCommand('does nothing in code blocks', '```\n{one}\n```', addLink, null);
|
|
215
|
+
testCommand('does nothing in code blocks', '```\n{one}\n```', addLink(), null);
|
|
216
216
|
|
|
217
|
-
testCommand('patches up overlapping styles before', '*foo {bar* baz}', addLink, '*foo* [*bar* baz]({})');
|
|
217
|
+
testCommand('patches up overlapping styles before', '*foo {bar* baz}', addLink(), '*foo* [*bar* baz]({})');
|
|
218
218
|
|
|
219
219
|
testCommand(
|
|
220
220
|
'patches up overlapping styles after',
|
|
221
221
|
'one {two ~~three} four~~',
|
|
222
|
-
addLink,
|
|
222
|
+
addLink(),
|
|
223
223
|
'one [two ~~three~~]({}) ~~four~~',
|
|
224
224
|
);
|
|
225
225
|
});
|
|
@@ -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 {
|
|
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
|
|
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 &&
|
|
@@ -161,7 +163,7 @@ export const setStyle =
|
|
|
161
163
|
({ state, dispatch }) => {
|
|
162
164
|
const marker = inlineMarkerText(type);
|
|
163
165
|
const changes = state.changeByRange((range) => {
|
|
164
|
-
// Special case for markers directly around the cursor, which will often not be parsed as valid styling
|
|
166
|
+
// Special case for markers directly around the cursor, which will often not be parsed as valid styling.
|
|
165
167
|
if (!enable && range.empty) {
|
|
166
168
|
const after = state.doc.sliceString(range.head, range.head + 6);
|
|
167
169
|
const found = after.indexOf(marker);
|
|
@@ -190,16 +192,16 @@ export const setStyle =
|
|
|
190
192
|
let startCovered: boolean | { from: number; to: number } = false;
|
|
191
193
|
let endCovered: boolean | { from: number; to: number } = false;
|
|
192
194
|
let { from, to } = range;
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
// the block's extent, that should be styled/unstyled.
|
|
195
|
+
|
|
196
|
+
// Iterate the selected range. For each textblock, determine a start and end position,
|
|
197
|
+
// the overlap of the selected range and the block's extent, that should be styled/unstyled.
|
|
196
198
|
syntaxTree(state).iterate({
|
|
197
199
|
from,
|
|
198
200
|
to,
|
|
199
201
|
enter: (node) => {
|
|
200
202
|
const { name } = node;
|
|
201
203
|
if (Object.hasOwn(Textblocks, name) && Textblocks[name] !== 'codeblock') {
|
|
202
|
-
// Set up for this textblock
|
|
204
|
+
// Set up for this textblock.
|
|
203
205
|
blockStart = blockContentStart(node);
|
|
204
206
|
blockEnd = blockContentEnd(node, state.doc);
|
|
205
207
|
startCovered = endCovered = false;
|
|
@@ -243,8 +245,7 @@ export const setStyle =
|
|
|
243
245
|
: true;
|
|
244
246
|
}
|
|
245
247
|
}
|
|
246
|
-
// Marks of the same type in range, or any mark if we're
|
|
247
|
-
// adding code style, need to be removed.
|
|
248
|
+
// Marks of the same type in range, or any mark if we're adding code style, need to be removed.
|
|
248
249
|
if (markType === type || (type === Inline.Code && enable)) {
|
|
249
250
|
if (node.from >= from && openEnd <= to) {
|
|
250
251
|
changes.push({ from: node.from, to: openEnd });
|
|
@@ -271,7 +272,7 @@ export const setStyle =
|
|
|
271
272
|
},
|
|
272
273
|
leave: (node) => {
|
|
273
274
|
if (Object.hasOwn(Textblocks, node.name) && Textblocks[node.name] !== 'codeblock') {
|
|
274
|
-
// Finish opening/closing the marks for this textblock
|
|
275
|
+
// Finish opening/closing the marks for this textblock.
|
|
275
276
|
const rangeStart = Math.max(from, blockStart);
|
|
276
277
|
const rangeEnd = Math.min(to, blockEnd);
|
|
277
278
|
if (enable) {
|
|
@@ -296,12 +297,14 @@ export const setStyle =
|
|
|
296
297
|
}
|
|
297
298
|
},
|
|
298
299
|
});
|
|
300
|
+
|
|
299
301
|
if (blockStart < 0 && range.empty && enable && !/\S/.test(state.doc.lineAt(range.from).text)) {
|
|
300
302
|
return {
|
|
301
303
|
changes: { from: range.head, insert: marker + marker },
|
|
302
304
|
range: EditorSelection.cursor(range.head + marker.length),
|
|
303
305
|
};
|
|
304
306
|
}
|
|
307
|
+
|
|
305
308
|
const changeSet = state.changes(changes.concat(changesAtEnd));
|
|
306
309
|
return {
|
|
307
310
|
changes: changeSet,
|
|
@@ -313,8 +316,12 @@ export const setStyle =
|
|
|
313
316
|
});
|
|
314
317
|
|
|
315
318
|
dispatch(
|
|
316
|
-
state.update(changes, {
|
|
319
|
+
state.update(changes, {
|
|
320
|
+
userEvent: enable ? 'format.style.add' : 'format.style.remove',
|
|
321
|
+
scrollIntoView: true,
|
|
322
|
+
}),
|
|
317
323
|
);
|
|
324
|
+
|
|
318
325
|
return true;
|
|
319
326
|
};
|
|
320
327
|
|
|
@@ -465,90 +472,94 @@ export const removeLink: StateCommand = ({ state, dispatch }) => {
|
|
|
465
472
|
};
|
|
466
473
|
|
|
467
474
|
// Add link markup around the selection
|
|
468
|
-
export const addLink
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
475
|
+
export const addLink =
|
|
476
|
+
({ url, image }: { url?: string; image?: boolean } = {}): StateCommand =>
|
|
477
|
+
({ state, dispatch }) => {
|
|
478
|
+
const changes = state.changeByRange((range) => {
|
|
479
|
+
let { from, to } = range;
|
|
480
|
+
const cutStyles: SyntaxNode[] = [];
|
|
481
|
+
let okay: boolean | null = null;
|
|
482
|
+
// Check whether this range is in a position where a link makes sense.
|
|
483
|
+
syntaxTree(state).iterate({
|
|
484
|
+
from,
|
|
485
|
+
to,
|
|
486
|
+
enter: (node) => {
|
|
487
|
+
if (Object.hasOwn(Textblocks, node.name)) {
|
|
488
|
+
// If the selection spans multiple textblocks or is in a code block, abort.
|
|
489
|
+
okay =
|
|
490
|
+
Textblocks[node.name] !== 'codeblock' &&
|
|
491
|
+
from >= blockContentStart(node) &&
|
|
492
|
+
to <= blockContentEnd(node, state.doc);
|
|
493
|
+
} else if (Object.hasOwn(InlineMarker, node.name)) {
|
|
494
|
+
// Look for inline styles that partially overlap the range.
|
|
495
|
+
// Expand the range over them if they start directly outside, otherwise mark them for later.
|
|
496
|
+
const sNode = node.node;
|
|
497
|
+
if (node.from < from && node.to <= to) {
|
|
498
|
+
if (sNode.firstChild!.to === from) {
|
|
499
|
+
from = node.from;
|
|
500
|
+
} else {
|
|
501
|
+
cutStyles.push(sNode);
|
|
502
|
+
}
|
|
503
|
+
} else if (node.from >= from && node.to > to) {
|
|
504
|
+
if (sNode.lastChild!.from === to) {
|
|
505
|
+
to = node.to;
|
|
506
|
+
} else {
|
|
507
|
+
cutStyles.push(sNode);
|
|
508
|
+
}
|
|
501
509
|
}
|
|
502
510
|
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
});
|
|
511
|
+
},
|
|
512
|
+
});
|
|
506
513
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
514
|
+
if (okay === null) {
|
|
515
|
+
// No textblock found around selection. Check if the rest of the line is empty.
|
|
516
|
+
const line = state.doc.lineAt(from);
|
|
517
|
+
okay = to <= line.to && !/\S/.test(line.text.slice(from - line.from));
|
|
518
|
+
}
|
|
519
|
+
if (!okay) {
|
|
520
|
+
return { range };
|
|
521
|
+
}
|
|
515
522
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
523
|
+
const changes: ChangeSpec[] = [];
|
|
524
|
+
// Some changes must be moved to end of change array so that they are applied in the right order
|
|
525
|
+
const changesAfter: ChangeSpec[] = [];
|
|
526
|
+
// Clear existing links.
|
|
527
|
+
removeLinkInner(from, to, changesAfter, state);
|
|
528
|
+
let cursorOffset = 1;
|
|
529
|
+
// Close and reopen inline styles that partially overlap the range.
|
|
530
|
+
for (const style of cutStyles) {
|
|
531
|
+
const type = InlineMarker[style.name];
|
|
532
|
+
const mark = inlineMarkerText(type);
|
|
533
|
+
if (style.from < from) {
|
|
534
|
+
// Extends before.
|
|
535
|
+
changes.push({ from: skipSpaces(from, state.doc, -1), insert: mark });
|
|
536
|
+
changesAfter.push({ from: skipSpaces(from, state.doc, 1, to), insert: mark });
|
|
537
|
+
} else {
|
|
538
|
+
changes.push({ from: skipSpaces(to, state.doc, -1, from), insert: mark });
|
|
539
|
+
const after = skipSpaces(to, state.doc, 1);
|
|
540
|
+
if (after === to) {
|
|
541
|
+
cursorOffset += mark.length;
|
|
542
|
+
}
|
|
543
|
+
changesAfter.push({ from: after, insert: mark });
|
|
535
544
|
}
|
|
536
|
-
changesAfter.push({ from: after, insert: mark });
|
|
537
545
|
}
|
|
546
|
+
|
|
547
|
+
// Add the link markup.
|
|
548
|
+
changes.push({ from, insert: image ? '` });
|
|
549
|
+
const changeSet = state.changes(changes.concat(changesAfter));
|
|
550
|
+
// Put the cursor between the title or parenthesis.
|
|
551
|
+
return {
|
|
552
|
+
changes: changeSet,
|
|
553
|
+
range: EditorSelection.cursor(changeSet.mapPos(to, 1) - cursorOffset - (url ? url.length + 2 : 0)),
|
|
554
|
+
};
|
|
555
|
+
});
|
|
556
|
+
if (changes.changes.empty) {
|
|
557
|
+
return false;
|
|
538
558
|
}
|
|
539
|
-
// Add the link markup.
|
|
540
|
-
changes.push({ from, insert: '[' }, { from: to, insert: ']()' });
|
|
541
|
-
const changeSet = state.changes(changes.concat(changesAfter));
|
|
542
|
-
// Put the cursor between the parenthesis.
|
|
543
|
-
return { changes: changeSet, range: EditorSelection.cursor(changeSet.mapPos(to, 1) - cursorOffset) };
|
|
544
|
-
});
|
|
545
|
-
if (changes.changes.empty) {
|
|
546
|
-
return false;
|
|
547
|
-
}
|
|
548
559
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
};
|
|
560
|
+
dispatch(state.update(changes, { userEvent: 'format.link.add', scrollIntoView: true }));
|
|
561
|
+
return true;
|
|
562
|
+
};
|
|
552
563
|
|
|
553
564
|
//
|
|
554
565
|
// Lists
|
|
@@ -987,7 +998,7 @@ export const toggleCodeblock: StateCommand = (target) => {
|
|
|
987
998
|
|
|
988
999
|
export type FormattingOptions = {};
|
|
989
1000
|
|
|
990
|
-
export const
|
|
1001
|
+
export const formattingKeymap = (options: FormattingOptions = {}): Extension => {
|
|
991
1002
|
return [
|
|
992
1003
|
keymap.of([
|
|
993
1004
|
{
|
|
@@ -1052,7 +1063,7 @@ export const getFormatting = (state: EditorState): Formatting => {
|
|
|
1052
1063
|
const inline: (boolean | null)[] = [null, null, null, null];
|
|
1053
1064
|
let link: boolean = false;
|
|
1054
1065
|
let blockQuote: boolean | null = null;
|
|
1055
|
-
// False indicates mixed list styles
|
|
1066
|
+
// False indicates mixed list styles.
|
|
1056
1067
|
let listStyle: Formatting['listStyle'] | null | false = null;
|
|
1057
1068
|
|
|
1058
1069
|
// Track block context for list/blockquote handling.
|
|
@@ -1211,16 +1222,18 @@ export const getFormatting = (state: EditorState): Formatting => {
|
|
|
1211
1222
|
};
|
|
1212
1223
|
|
|
1213
1224
|
/**
|
|
1214
|
-
* Hook
|
|
1225
|
+
* Hook provides an extension to compute the current formatting state.
|
|
1215
1226
|
*/
|
|
1216
|
-
export const useFormattingState = (): [Formatting |
|
|
1217
|
-
const [state, setState] =
|
|
1227
|
+
export const useFormattingState = (): [Formatting | undefined, Extension] => {
|
|
1228
|
+
const [state, setState, stateRef] = useStateRef<Formatting>();
|
|
1218
1229
|
const observer = useMemo(
|
|
1219
1230
|
() =>
|
|
1220
1231
|
EditorView.updateListener.of((update) => {
|
|
1221
1232
|
if (update.docChanged || update.selectionSet) {
|
|
1222
1233
|
const newState = getFormatting(update.state);
|
|
1223
|
-
if (!
|
|
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
|
|
1224
1237
|
setState(newState);
|
|
1225
1238
|
}
|
|
1226
1239
|
}
|
|
@@ -61,6 +61,7 @@ const buildDecorations = (from: number, to: number, state: EditorState) => {
|
|
|
61
61
|
if (urlNode) {
|
|
62
62
|
const hide = state.readOnly || cursor < node.from || cursor > node.to;
|
|
63
63
|
const url = state.sliceDoc(urlNode.from, urlNode.to);
|
|
64
|
+
// TODO(burdon): Doesn't load if scrolling with mouse.
|
|
64
65
|
preloadImage(url);
|
|
65
66
|
decorations.push(
|
|
66
67
|
Decoration.replace({
|
|
@@ -96,3 +97,9 @@ class ImageWidget extends WidgetType {
|
|
|
96
97
|
return img;
|
|
97
98
|
}
|
|
98
99
|
}
|
|
100
|
+
|
|
101
|
+
export type ImageUploadOptions = {
|
|
102
|
+
onSelect: () => { url: string };
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const imageUpload = (options: ImageOptions = {}) => {};
|