@dxos/react-ui-editor 0.4.7 → 0.4.8-main.00e6f19
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 +2633 -2705
- 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 +79 -5
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +0 -1
- 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 +1 -2
- 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 +1 -1
- package/dist/types/src/extensions/markdown/formatting.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/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 +6 -1
- 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/package.json +24 -27
- package/src/components/TextEditor/TextEditor.stories.tsx +520 -23
- package/src/components/TextEditor/TextEditor.tsx +44 -155
- package/src/components/Toolbar/Toolbar.stories.tsx +42 -42
- 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.ts +15 -10
- package/src/extensions/markdown/image.ts +1 -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/useDocAccessor.ts +21 -0
- package/src/hooks/useTextEditor.stories.tsx +40 -35
- package/src/hooks/useTextEditor.ts +11 -120
- package/src/index.ts +1 -1
- package/src/themes/default.ts +1 -28
- 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
|
@@ -5,26 +5,26 @@
|
|
|
5
5
|
// Based upon @acheronfail’s implementation, fetched 16 Feb 2024
|
|
6
6
|
// https://discuss.codemirror.net/t/cursorscrollmargin-for-v6/7448/5
|
|
7
7
|
|
|
8
|
-
import { type EditorState, type Extension,
|
|
8
|
+
import { type EditorState, type Extension, Transaction } from '@codemirror/state';
|
|
9
9
|
import { EditorView } from '@codemirror/view';
|
|
10
10
|
|
|
11
11
|
import { log } from '@dxos/log';
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* Number of lines above/below the cursor to keep visible in the viewport.
|
|
15
|
-
* Defaults to 3.
|
|
16
|
-
*/
|
|
17
|
-
export const cursorLineMarginFacet = Facet.define<number, number>({
|
|
18
|
-
combine: (input) => input[0] ?? 3,
|
|
19
|
-
});
|
|
20
|
-
|
|
21
13
|
const annotation = 'cursorLineMargin';
|
|
22
14
|
const lineAtPos = (s: EditorState, pos: number) => s.doc.lineAt(pos).number;
|
|
23
15
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
type Options = {
|
|
17
|
+
lines?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// TODO(burdon): Remove. Buggy and creates scrolling problems. Find better (more general) way to deal with HALO notch.
|
|
21
|
+
export const cursorLineMargin = (options: Options = { lines: 3 }): Extension => {
|
|
22
|
+
const { lines } = options;
|
|
23
|
+
if (!lines || lines <= 0) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return EditorView.updateListener.of(({ transactions, state, selectionSet, startState, view }) => {
|
|
28
28
|
// make sure we don't trigger an infinite loop and ignore our own changes
|
|
29
29
|
if (transactions.length < 1 || transactions.some((tr) => tr.isUserEvent(annotation))) {
|
|
30
30
|
return;
|
|
@@ -32,7 +32,6 @@ export const cursorLineMargin: Extension = EditorView.updateListener.of(
|
|
|
32
32
|
|
|
33
33
|
const s = state;
|
|
34
34
|
const { main } = s.selection;
|
|
35
|
-
const cursorLineMargin = s.facet(cursorLineMarginFacet);
|
|
36
35
|
|
|
37
36
|
// editor rect
|
|
38
37
|
const rect = view.dom.getBoundingClientRect();
|
|
@@ -59,7 +58,7 @@ export const cursorLineMargin: Extension = EditorView.updateListener.of(
|
|
|
59
58
|
|
|
60
59
|
// const needsScrollTop = mainLine <= visTopLine + cursorLineMargin;
|
|
61
60
|
const needsScrollTop = false;
|
|
62
|
-
const needsScrollBot = mainLine >= visBotLine -
|
|
61
|
+
const needsScrollBot = mainLine >= visBotLine - lines;
|
|
63
62
|
|
|
64
63
|
// the scroll margins are overlapping
|
|
65
64
|
if (needsScrollTop && needsScrollBot) {
|
|
@@ -88,8 +87,8 @@ export const cursorLineMargin: Extension = EditorView.updateListener.of(
|
|
|
88
87
|
annotations: Transaction.userEvent.of(annotation),
|
|
89
88
|
effects: EditorView.scrollIntoView(s.selection.main.head, {
|
|
90
89
|
y: needsScrollTop ? 'start' : 'end',
|
|
91
|
-
yMargin: view.defaultLineHeight *
|
|
90
|
+
yMargin: view.defaultLineHeight * lines,
|
|
92
91
|
}),
|
|
93
92
|
});
|
|
94
|
-
}
|
|
95
|
-
|
|
93
|
+
});
|
|
94
|
+
};
|
package/src/extensions/cursor.ts
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Facet } from '@codemirror/state';
|
|
6
|
+
|
|
7
|
+
import { invariant } from '@dxos/invariant';
|
|
8
|
+
|
|
9
|
+
export const documentId = Facet.define<string, string>({
|
|
10
|
+
combine: (providers) => {
|
|
11
|
+
invariant(providers.length <= 1);
|
|
12
|
+
return providers[0];
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -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
|
}
|
|
@@ -161,7 +161,7 @@ export const setStyle =
|
|
|
161
161
|
({ state, dispatch }) => {
|
|
162
162
|
const marker = inlineMarkerText(type);
|
|
163
163
|
const changes = state.changeByRange((range) => {
|
|
164
|
-
// Special case for markers directly around the cursor, which will often not be parsed as valid styling
|
|
164
|
+
// Special case for markers directly around the cursor, which will often not be parsed as valid styling.
|
|
165
165
|
if (!enable && range.empty) {
|
|
166
166
|
const after = state.doc.sliceString(range.head, range.head + 6);
|
|
167
167
|
const found = after.indexOf(marker);
|
|
@@ -190,16 +190,16 @@ export const setStyle =
|
|
|
190
190
|
let startCovered: boolean | { from: number; to: number } = false;
|
|
191
191
|
let endCovered: boolean | { from: number; to: number } = false;
|
|
192
192
|
let { from, to } = range;
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
// the block's extent, that should be styled/unstyled.
|
|
193
|
+
|
|
194
|
+
// Iterate the selected range. For each textblock, determine a start and end position,
|
|
195
|
+
// the overlap of the selected range and the block's extent, that should be styled/unstyled.
|
|
196
196
|
syntaxTree(state).iterate({
|
|
197
197
|
from,
|
|
198
198
|
to,
|
|
199
199
|
enter: (node) => {
|
|
200
200
|
const { name } = node;
|
|
201
201
|
if (Object.hasOwn(Textblocks, name) && Textblocks[name] !== 'codeblock') {
|
|
202
|
-
// Set up for this textblock
|
|
202
|
+
// Set up for this textblock.
|
|
203
203
|
blockStart = blockContentStart(node);
|
|
204
204
|
blockEnd = blockContentEnd(node, state.doc);
|
|
205
205
|
startCovered = endCovered = false;
|
|
@@ -243,8 +243,7 @@ export const setStyle =
|
|
|
243
243
|
: true;
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
|
-
// Marks of the same type in range, or any mark if we're
|
|
247
|
-
// adding code style, need to be removed.
|
|
246
|
+
// Marks of the same type in range, or any mark if we're adding code style, need to be removed.
|
|
248
247
|
if (markType === type || (type === Inline.Code && enable)) {
|
|
249
248
|
if (node.from >= from && openEnd <= to) {
|
|
250
249
|
changes.push({ from: node.from, to: openEnd });
|
|
@@ -271,7 +270,7 @@ export const setStyle =
|
|
|
271
270
|
},
|
|
272
271
|
leave: (node) => {
|
|
273
272
|
if (Object.hasOwn(Textblocks, node.name) && Textblocks[node.name] !== 'codeblock') {
|
|
274
|
-
// Finish opening/closing the marks for this textblock
|
|
273
|
+
// Finish opening/closing the marks for this textblock.
|
|
275
274
|
const rangeStart = Math.max(from, blockStart);
|
|
276
275
|
const rangeEnd = Math.min(to, blockEnd);
|
|
277
276
|
if (enable) {
|
|
@@ -296,12 +295,14 @@ export const setStyle =
|
|
|
296
295
|
}
|
|
297
296
|
},
|
|
298
297
|
});
|
|
298
|
+
|
|
299
299
|
if (blockStart < 0 && range.empty && enable && !/\S/.test(state.doc.lineAt(range.from).text)) {
|
|
300
300
|
return {
|
|
301
301
|
changes: { from: range.head, insert: marker + marker },
|
|
302
302
|
range: EditorSelection.cursor(range.head + marker.length),
|
|
303
303
|
};
|
|
304
304
|
}
|
|
305
|
+
|
|
305
306
|
const changeSet = state.changes(changes.concat(changesAtEnd));
|
|
306
307
|
return {
|
|
307
308
|
changes: changeSet,
|
|
@@ -313,8 +314,12 @@ export const setStyle =
|
|
|
313
314
|
});
|
|
314
315
|
|
|
315
316
|
dispatch(
|
|
316
|
-
state.update(changes, {
|
|
317
|
+
state.update(changes, {
|
|
318
|
+
userEvent: enable ? 'format.style.add' : 'format.style.remove',
|
|
319
|
+
scrollIntoView: true,
|
|
320
|
+
}),
|
|
317
321
|
);
|
|
322
|
+
|
|
318
323
|
return true;
|
|
319
324
|
};
|
|
320
325
|
|
|
@@ -987,7 +992,7 @@ export const toggleCodeblock: StateCommand = (target) => {
|
|
|
987
992
|
|
|
988
993
|
export type FormattingOptions = {};
|
|
989
994
|
|
|
990
|
-
export const
|
|
995
|
+
export const formattingKeymap = (options: FormattingOptions = {}): Extension => {
|
|
991
996
|
return [
|
|
992
997
|
keymap.of([
|
|
993
998
|
{
|
|
@@ -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({
|
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { syntaxTree } from '@codemirror/language';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type EditorState,
|
|
8
|
+
type Extension,
|
|
9
|
+
type RangeSet,
|
|
10
|
+
RangeSetBuilder,
|
|
11
|
+
StateField,
|
|
12
|
+
type Transaction,
|
|
13
|
+
} from '@codemirror/state';
|
|
7
14
|
import { Decoration, EditorView, WidgetType } from '@codemirror/view';
|
|
8
15
|
|
|
9
16
|
// TODO(burdon): Snippet to create basic table.
|
|
@@ -18,7 +25,7 @@ export type TableOptions = {};
|
|
|
18
25
|
* GFM tables.
|
|
19
26
|
* https://github.github.com/gfm/#tables-extension
|
|
20
27
|
*/
|
|
21
|
-
export const table = (options: TableOptions = {}) => {
|
|
28
|
+
export const table = (options: TableOptions = {}): Extension => {
|
|
22
29
|
return StateField.define<RangeSet<any>>({
|
|
23
30
|
create: (state) => update(state, options),
|
|
24
31
|
update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
|
|
@@ -76,7 +83,7 @@ const update = (state: EditorState, options: TableOptions) => {
|
|
|
76
83
|
|
|
77
84
|
tables.forEach((table) => {
|
|
78
85
|
const hide = state.readOnly || cursor < table.from || cursor > table.to;
|
|
79
|
-
hide
|
|
86
|
+
if (hide) {
|
|
80
87
|
builder.add(
|
|
81
88
|
table.from,
|
|
82
89
|
table.to,
|
|
@@ -84,6 +91,7 @@ const update = (state: EditorState, options: TableOptions) => {
|
|
|
84
91
|
widget: new TableWidget(table),
|
|
85
92
|
}),
|
|
86
93
|
);
|
|
94
|
+
}
|
|
87
95
|
|
|
88
96
|
// Add class for styling.
|
|
89
97
|
builder.add(table.from, table.to, Decoration.mark({ class: 'cm-table' }));
|
package/src/extensions/modes.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { type Extension, Facet } from '@codemirror/state';
|
|
|
6
6
|
import { keymap } from '@codemirror/view';
|
|
7
7
|
import { vim } from '@replit/codemirror-vim';
|
|
8
8
|
|
|
9
|
+
export const focusEvent = 'focus.container';
|
|
10
|
+
|
|
9
11
|
export type EditorMode = 'default' | 'vim' | undefined;
|
|
10
12
|
|
|
11
13
|
export type EditorConfig = {
|
|
@@ -27,7 +29,7 @@ export const EditorModes: { [mode: string]: Extension } = {
|
|
|
27
29
|
key: 'Alt-Escape',
|
|
28
30
|
run: (view) => {
|
|
29
31
|
// Focus container for tab navigation.
|
|
30
|
-
view.dispatch({ userEvent:
|
|
32
|
+
view.dispatch({ userEvent: focusEvent });
|
|
31
33
|
return true;
|
|
32
34
|
},
|
|
33
35
|
},
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension, Transaction } from '@codemirror/state';
|
|
6
|
+
import { EditorView, keymap } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
import { debounce } from '@dxos/async';
|
|
9
|
+
import { invariant } from '@dxos/invariant';
|
|
10
|
+
import { isNotFalsy } from '@dxos/util';
|
|
11
|
+
|
|
12
|
+
import { documentId } from './doc';
|
|
13
|
+
|
|
14
|
+
const scrollAnnotation = 'dxos.org/cm/scrolling';
|
|
15
|
+
|
|
16
|
+
// NOTE: Serializable.
|
|
17
|
+
export type SelectionState = {
|
|
18
|
+
scrollTo: {
|
|
19
|
+
from: number;
|
|
20
|
+
};
|
|
21
|
+
selection: {
|
|
22
|
+
anchor: number;
|
|
23
|
+
head?: number;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type StateOptions = {
|
|
28
|
+
setState: (id: string, state: SelectionState) => void;
|
|
29
|
+
getState: (id: string) => SelectionState | undefined;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const keyPrefix = 'dxos.org/react-ui-editor/state';
|
|
33
|
+
export const localStorageStateStoreAdapter: StateOptions = {
|
|
34
|
+
setState: (id, state) => {
|
|
35
|
+
invariant(id);
|
|
36
|
+
localStorage.setItem(`${keyPrefix}/${id}`, JSON.stringify(state));
|
|
37
|
+
},
|
|
38
|
+
getState: (id) => {
|
|
39
|
+
invariant(id);
|
|
40
|
+
const state = localStorage.getItem(`${keyPrefix}/${id}`);
|
|
41
|
+
return state ? JSON.parse(state) : undefined;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Track scrolling and selection state to be restored when switching to document.
|
|
47
|
+
*/
|
|
48
|
+
export const state = ({ getState, setState }: Partial<StateOptions> = {}): Extension => {
|
|
49
|
+
const setStateDebounced = debounce(setState!, 1_000);
|
|
50
|
+
|
|
51
|
+
return [
|
|
52
|
+
// TODO(burdon): Track scrolling (currently only updates when cursor moves).
|
|
53
|
+
EditorView.updateListener.of(({ view, changes, transactions }) => {
|
|
54
|
+
// TODO(burdon): Don't react to initial scroll.
|
|
55
|
+
const id = view.state.facet(documentId);
|
|
56
|
+
if (!id || transactions.some((tr) => tr.isUserEvent(scrollAnnotation))) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (setState) {
|
|
61
|
+
const { top } = view.dom.getBoundingClientRect();
|
|
62
|
+
const pos = view.posAtCoords({ x: 0, y: top });
|
|
63
|
+
if (pos !== null) {
|
|
64
|
+
const { anchor, head } = view.state.selection.main;
|
|
65
|
+
setStateDebounced(id, {
|
|
66
|
+
scrollTo: { from: pos, yMargin: 0 },
|
|
67
|
+
selection: { anchor, head },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}),
|
|
72
|
+
getState &&
|
|
73
|
+
keymap.of([
|
|
74
|
+
{
|
|
75
|
+
key: 'ctrl-r', // TODO(burdon): Setting to jump back to bookmark.
|
|
76
|
+
run: (view) => {
|
|
77
|
+
const state = getState(view.state.facet(documentId));
|
|
78
|
+
if (state) {
|
|
79
|
+
view.dispatch({
|
|
80
|
+
effects: EditorView.scrollIntoView(state.scrollTo.from, { yMargin: 0 }),
|
|
81
|
+
selection: state.selection,
|
|
82
|
+
annotations: Transaction.userEvent.of(scrollAnnotation),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
]),
|
|
89
|
+
].filter(isNotFalsy);
|
|
90
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// Runtime data structure.
|
|
6
|
+
export type Range = {
|
|
7
|
+
from: number;
|
|
8
|
+
to: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Persistent data structure.
|
|
12
|
+
// TODO(burdon): Rename annotation?
|
|
13
|
+
export type Comment = {
|
|
14
|
+
id: string;
|
|
15
|
+
cursor?: string;
|
|
16
|
+
};
|
package/src/hooks/index.ts
CHANGED
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
export * from './awareness-provider';
|
|
6
|
-
export * from './defs';
|
|
7
|
-
|
|
8
5
|
export * from './useActionHandler';
|
|
9
|
-
export * from './
|
|
6
|
+
export * from './useDocAccessor';
|
|
10
7
|
export * from './useTextEditor';
|
|
11
|
-
export * from './useTextModel';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useMemo } from 'react';
|
|
6
|
+
|
|
7
|
+
import { createDocAccessor, type DocAccessor, getTextContent, type TextObject } from '@dxos/echo-schema';
|
|
8
|
+
|
|
9
|
+
// TODO(burdon): Factor out.
|
|
10
|
+
export const useDocAccessor = <T = any>(
|
|
11
|
+
text: TextObject,
|
|
12
|
+
): { id: string; doc: string | undefined; accessor: DocAccessor<T> } => {
|
|
13
|
+
return useMemo(
|
|
14
|
+
() => ({
|
|
15
|
+
id: text.id,
|
|
16
|
+
doc: getTextContent(text),
|
|
17
|
+
accessor: createDocAccessor<T>(text),
|
|
18
|
+
}),
|
|
19
|
+
[text],
|
|
20
|
+
);
|
|
21
|
+
};
|