@dxos/react-ui-editor 0.8.2-main.fbd8ed0 → 0.8.2-staging.42af850
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 +1828 -961
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +3 -64
- package/dist/lib/browser/testing/index.mjs.map +4 -4
- package/dist/lib/node/index.cjs +2008 -1138
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +3 -75
- package/dist/lib/node/testing/index.cjs.map +4 -4
- package/dist/lib/node-esm/index.mjs +1828 -961
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +3 -64
- package/dist/lib/node-esm/testing/index.mjs.map +4 -4
- package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/index.d.ts +1 -1
- package/dist/types/src/components/EditorToolbar/index.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/util.d.ts +4 -6
- package/dist/types/src/components/EditorToolbar/util.d.ts.map +1 -1
- package/dist/types/src/components/Popover/RefDropdownMenu.d.ts +21 -0
- package/dist/types/src/components/Popover/RefDropdownMenu.d.ts.map +1 -0
- package/dist/types/src/{testing → components/Popover}/RefPopover.d.ts +1 -1
- package/dist/types/src/components/Popover/RefPopover.d.ts.map +1 -0
- package/dist/types/src/components/Popover/index.d.ts +3 -0
- package/dist/types/src/components/Popover/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/defaults.d.ts +2 -5
- package/dist/types/src/defaults.d.ts.map +1 -1
- package/dist/types/src/extensions/annotations.d.ts +4 -1
- package/dist/types/src/extensions/annotations.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete.d.ts +1 -2
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -1
- package/dist/types/src/extensions/command/command.d.ts +1 -2
- package/dist/types/src/extensions/command/command.d.ts.map +1 -1
- package/dist/types/src/extensions/command/hint.d.ts +14 -2
- package/dist/types/src/extensions/command/hint.d.ts.map +1 -1
- package/dist/types/src/extensions/command/index.d.ts +2 -0
- package/dist/types/src/extensions/command/index.d.ts.map +1 -1
- package/dist/types/src/extensions/command/menu.d.ts +4 -14
- package/dist/types/src/extensions/command/menu.d.ts.map +1 -1
- package/dist/types/src/extensions/command/state.d.ts +1 -1
- package/dist/types/src/extensions/command/state.d.ts.map +1 -1
- package/dist/types/src/extensions/command/typeahead.d.ts +17 -0
- package/dist/types/src/extensions/command/typeahead.d.ts.map +1 -0
- package/dist/types/src/extensions/comments.d.ts +2 -12
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +4 -0
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +2 -0
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/json.d.ts +7 -0
- package/dist/types/src/extensions/json.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/{editorAction.d.ts → action.d.ts} +1 -1
- package/dist/types/src/extensions/markdown/action.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/bundle.d.ts +2 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/index.d.ts +1 -2
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner/commands.d.ts +10 -0
- package/dist/types/src/extensions/outliner/commands.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/editor.d.ts +5 -0
- package/dist/types/src/extensions/outliner/editor.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/editor.test.d.ts +2 -0
- package/dist/types/src/extensions/outliner/editor.test.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/index.d.ts +4 -0
- package/dist/types/src/extensions/outliner/index.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/outliner.d.ts +13 -0
- package/dist/types/src/extensions/outliner/outliner.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/outliner.test.d.ts +2 -0
- package/dist/types/src/extensions/outliner/outliner.test.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/selection.d.ts +12 -0
- package/dist/types/src/extensions/outliner/selection.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/tree.d.ts +79 -0
- package/dist/types/src/extensions/outliner/tree.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/tree.test.d.ts +2 -0
- package/dist/types/src/extensions/outliner/tree.test.d.ts.map +1 -0
- package/dist/types/src/stories/Command.stories.d.ts +7 -0
- package/dist/types/src/stories/Command.stories.d.ts.map +1 -0
- package/dist/types/src/stories/{TextEditorComments.stories.d.ts → Comments.stories.d.ts} +3 -3
- package/dist/types/src/stories/Comments.stories.d.ts.map +1 -0
- package/dist/types/src/stories/EditorToolbar.stories.d.ts +12 -0
- package/dist/types/src/stories/EditorToolbar.stories.d.ts.map +1 -0
- package/dist/types/src/stories/{TextEditorSpecial.stories.d.ts → Experimental.stories.d.ts} +3 -6
- package/dist/types/src/stories/Experimental.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Markdown.stories.d.ts +46 -0
- package/dist/types/src/stories/Markdown.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Outliner.stories.d.ts +26 -0
- package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Preview.stories.d.ts +10 -0
- package/dist/types/src/stories/Preview.stories.d.ts.map +1 -0
- package/dist/types/src/stories/{TextEditorBasic.stories.d.ts → TextEditor.stories.d.ts} +9 -39
- package/dist/types/src/stories/TextEditor.stories.d.ts.map +1 -0
- package/dist/types/src/stories/{story-utils.d.ts → util.d.ts} +6 -6
- package/dist/types/src/stories/util.d.ts.map +1 -0
- package/dist/types/src/styles/theme.d.ts.map +1 -1
- package/dist/types/src/styles/tokens.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +1 -1
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/util.d.ts +2 -0
- package/dist/types/src/testing/util.d.ts.map +1 -0
- package/package.json +40 -34
- package/src/components/EditorToolbar/EditorToolbar.tsx +81 -57
- package/src/components/EditorToolbar/index.ts +7 -1
- package/src/components/EditorToolbar/util.ts +3 -4
- package/src/components/Popover/RefDropdownMenu.tsx +77 -0
- package/src/{testing → components/Popover}/RefPopover.tsx +5 -4
- package/src/components/Popover/index.ts +6 -0
- package/src/components/index.ts +1 -0
- package/src/defaults.ts +10 -13
- package/src/extensions/annotations.ts +41 -64
- package/src/extensions/autocomplete.ts +5 -6
- package/src/extensions/automerge/automerge.stories.tsx +2 -7
- package/src/extensions/automerge/automerge.test.tsx +3 -2
- package/src/extensions/automerge/sync.ts +3 -3
- package/src/extensions/awareness/awareness-provider.ts +4 -4
- package/src/extensions/awareness/awareness.ts +7 -7
- package/src/extensions/blast.ts +9 -9
- package/src/extensions/command/command.ts +1 -3
- package/src/extensions/command/hint.ts +7 -7
- package/src/extensions/command/index.ts +2 -0
- package/src/extensions/command/menu.ts +75 -50
- package/src/extensions/command/typeahead.ts +116 -0
- package/src/extensions/comments.ts +4 -69
- package/src/extensions/factories.ts +13 -0
- package/src/extensions/index.ts +2 -0
- package/src/extensions/json.ts +56 -0
- package/src/extensions/markdown/bundle.ts +13 -9
- package/src/extensions/markdown/decorate.ts +7 -7
- package/src/extensions/markdown/image.ts +2 -2
- package/src/extensions/markdown/index.ts +1 -2
- package/src/extensions/markdown/styles.ts +2 -1
- package/src/extensions/markdown/table.ts +3 -3
- package/src/extensions/outliner/commands.ts +270 -0
- package/src/extensions/outliner/editor.test.ts +33 -0
- package/src/extensions/outliner/editor.ts +184 -0
- package/src/extensions/outliner/index.ts +7 -0
- package/src/extensions/outliner/outliner.test.ts +99 -0
- package/src/extensions/outliner/outliner.ts +168 -0
- package/src/extensions/outliner/selection.ts +50 -0
- package/src/extensions/outliner/tree.test.ts +164 -0
- package/src/extensions/outliner/tree.ts +315 -0
- package/src/extensions/preview/preview.ts +5 -5
- package/src/stories/Command.stories.tsx +97 -0
- package/src/stories/{TextEditorComments.stories.tsx → Comments.stories.tsx} +13 -14
- package/src/{components/EditorToolbar → stories}/EditorToolbar.stories.tsx +26 -20
- package/src/stories/{TextEditorSpecial.stories.tsx → Experimental.stories.tsx} +9 -30
- package/src/stories/Markdown.stories.tsx +121 -0
- package/src/stories/Outliner.stories.tsx +108 -0
- package/src/stories/{TextEditorPreview.stories.tsx → Preview.stories.tsx} +46 -136
- package/src/stories/TextEditor.stories.tsx +256 -0
- package/src/stories/{story-utils.tsx → util.tsx} +21 -22
- package/src/styles/theme.ts +12 -5
- package/src/styles/tokens.ts +1 -2
- package/src/testing/index.ts +1 -1
- package/src/testing/util.ts +5 -0
- package/dist/types/src/components/EditorToolbar/EditorToolbar.stories.d.ts +0 -53
- package/dist/types/src/components/EditorToolbar/EditorToolbar.stories.d.ts.map +0 -1
- package/dist/types/src/components/EditorToolbar/comment.d.ts +0 -18
- package/dist/types/src/components/EditorToolbar/comment.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/editorAction.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/outliner.d.ts +0 -12
- package/dist/types/src/extensions/markdown/outliner.d.ts.map +0 -1
- package/dist/types/src/stories/TextEditorBasic.stories.d.ts.map +0 -1
- package/dist/types/src/stories/TextEditorComments.stories.d.ts.map +0 -1
- package/dist/types/src/stories/TextEditorPreview.stories.d.ts +0 -13
- package/dist/types/src/stories/TextEditorPreview.stories.d.ts.map +0 -1
- package/dist/types/src/stories/TextEditorSpecial.stories.d.ts.map +0 -1
- package/dist/types/src/stories/story-utils.d.ts.map +0 -1
- package/dist/types/src/testing/RefPopover.d.ts.map +0 -1
- package/src/components/EditorToolbar/comment.ts +0 -30
- package/src/extensions/markdown/outliner.ts +0 -235
- package/src/stories/TextEditorBasic.stories.tsx +0 -333
- /package/src/extensions/markdown/{editorAction.ts → action.ts} +0 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
//
|
2
|
+
// Copyright 2025 DXOS.org
|
3
|
+
//
|
4
|
+
|
5
|
+
import { json, jsonParseLinter } from '@codemirror/lang-json';
|
6
|
+
import { type LintSource, linter } from '@codemirror/lint';
|
7
|
+
import { type Extension } from '@codemirror/state';
|
8
|
+
import Ajv, { type ValidateFunction } from 'ajv';
|
9
|
+
|
10
|
+
import { type JsonSchemaType } from '@dxos/echo-schema';
|
11
|
+
|
12
|
+
export type JsonExtensionsOptions = {
|
13
|
+
schema?: JsonSchemaType;
|
14
|
+
};
|
15
|
+
|
16
|
+
export const createJsonExtensions = ({ schema }: JsonExtensionsOptions = {}): Extension => {
|
17
|
+
let lintSource: LintSource = jsonParseLinter();
|
18
|
+
if (schema) {
|
19
|
+
const ajv = new Ajv({ allErrors: false });
|
20
|
+
const validate = ajv.compile(schema);
|
21
|
+
lintSource = schemaLinter(validate);
|
22
|
+
}
|
23
|
+
|
24
|
+
return [json(), linter(lintSource)];
|
25
|
+
};
|
26
|
+
|
27
|
+
const schemaLinter =
|
28
|
+
(validate: ValidateFunction): LintSource =>
|
29
|
+
(view) => {
|
30
|
+
try {
|
31
|
+
const jsonText = view.state.doc.toString();
|
32
|
+
const jsonData = JSON.parse(jsonText);
|
33
|
+
const valid = validate(jsonData);
|
34
|
+
if (valid) {
|
35
|
+
return [];
|
36
|
+
}
|
37
|
+
|
38
|
+
return (
|
39
|
+
validate.errors?.map((err: any) => ({
|
40
|
+
from: 0,
|
41
|
+
to: jsonText.length,
|
42
|
+
severity: 'error',
|
43
|
+
message: `${err.instancePath || '(root)'} ${err.message}`,
|
44
|
+
})) ?? []
|
45
|
+
);
|
46
|
+
} catch (err: unknown) {
|
47
|
+
return [
|
48
|
+
{
|
49
|
+
from: 0,
|
50
|
+
to: view.state.doc.length,
|
51
|
+
severity: 'error',
|
52
|
+
message: 'Invalid JSON: ' + (err as Error).message,
|
53
|
+
},
|
54
|
+
];
|
55
|
+
}
|
56
|
+
};
|
@@ -12,11 +12,13 @@ import { type Extension } from '@codemirror/state';
|
|
12
12
|
import { keymap } from '@codemirror/view';
|
13
13
|
|
14
14
|
import { type ThemeMode } from '@dxos/react-ui';
|
15
|
+
import { isNotFalsy } from '@dxos/util';
|
15
16
|
|
16
17
|
import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
|
17
18
|
|
18
19
|
export type MarkdownBundleOptions = {
|
19
20
|
themeMode?: ThemeMode;
|
21
|
+
indentWithTab?: boolean;
|
20
22
|
};
|
21
23
|
|
22
24
|
/**
|
@@ -27,7 +29,7 @@ export type MarkdownBundleOptions = {
|
|
27
29
|
* https://codemirror.net/docs/community
|
28
30
|
* https://codemirror.net/docs/ref/#codemirror.basicSetup
|
29
31
|
*/
|
30
|
-
export const createMarkdownExtensions = (
|
32
|
+
export const createMarkdownExtensions = (options: MarkdownBundleOptions = {}): Extension[] => {
|
31
33
|
return [
|
32
34
|
// Main extension.
|
33
35
|
// https://github.com/codemirror/lang-markdown
|
@@ -56,14 +58,16 @@ export const createMarkdownExtensions = ({ themeMode }: MarkdownBundleOptions =
|
|
56
58
|
// Custom styles.
|
57
59
|
syntaxHighlighting(markdownHighlightStyle()),
|
58
60
|
|
59
|
-
keymap.of(
|
60
|
-
|
61
|
-
|
61
|
+
keymap.of(
|
62
|
+
[
|
63
|
+
// https://codemirror.net/docs/ref/#commands.indentWithTab
|
64
|
+
options.indentWithTab !== false && indentWithTab,
|
62
65
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
66
|
+
// https://codemirror.net/docs/ref/#commands.defaultKeymap
|
67
|
+
...defaultKeymap,
|
68
|
+
...completionKeymap,
|
69
|
+
...lintKeymap,
|
70
|
+
].filter(isNotFalsy),
|
71
|
+
),
|
68
72
|
];
|
69
73
|
};
|
@@ -36,7 +36,7 @@ const Unicode = {
|
|
36
36
|
//
|
37
37
|
|
38
38
|
class HorizontalRuleWidget extends WidgetType {
|
39
|
-
override toDOM() {
|
39
|
+
override toDOM(): HTMLSpanElement {
|
40
40
|
const el = document.createElement('span');
|
41
41
|
el.className = 'cm-hr';
|
42
42
|
return el;
|
@@ -51,12 +51,12 @@ class LinkButton extends WidgetType {
|
|
51
51
|
super();
|
52
52
|
}
|
53
53
|
|
54
|
-
override eq(other: this) {
|
54
|
+
override eq(other: this): boolean {
|
55
55
|
return this.url === other.url;
|
56
56
|
}
|
57
57
|
|
58
58
|
// TODO(burdon): Create icon and link directly without react?
|
59
|
-
override toDOM(view: EditorView) {
|
59
|
+
override toDOM(view: EditorView): HTMLSpanElement {
|
60
60
|
const el = document.createElement('span');
|
61
61
|
this.render(el, { url: this.url }, view);
|
62
62
|
return el;
|
@@ -68,11 +68,11 @@ class CheckboxWidget extends WidgetType {
|
|
68
68
|
super();
|
69
69
|
}
|
70
70
|
|
71
|
-
override eq(other: this) {
|
71
|
+
override eq(other: this): boolean {
|
72
72
|
return this._checked === other._checked;
|
73
73
|
}
|
74
74
|
|
75
|
-
override toDOM(view: EditorView) {
|
75
|
+
override toDOM(view: EditorView): HTMLSpanElement {
|
76
76
|
const input = document.createElement('input');
|
77
77
|
input.className = 'cm-task-checkbox dx-checkbox';
|
78
78
|
input.type = 'checkbox';
|
@@ -105,7 +105,7 @@ class CheckboxWidget extends WidgetType {
|
|
105
105
|
return span;
|
106
106
|
}
|
107
107
|
|
108
|
-
override ignoreEvent() {
|
108
|
+
override ignoreEvent(): boolean {
|
109
109
|
return false;
|
110
110
|
}
|
111
111
|
}
|
@@ -118,7 +118,7 @@ class TextWidget extends WidgetType {
|
|
118
118
|
super();
|
119
119
|
}
|
120
120
|
|
121
|
-
override toDOM() {
|
121
|
+
override toDOM(): HTMLSpanElement {
|
122
122
|
const el = document.createElement('span');
|
123
123
|
if (this.className) {
|
124
124
|
el.className = this.className;
|
@@ -99,11 +99,11 @@ class ImageWidget extends WidgetType {
|
|
99
99
|
super();
|
100
100
|
}
|
101
101
|
|
102
|
-
override eq(other: this) {
|
102
|
+
override eq(other: this): boolean {
|
103
103
|
return this._url === other._url;
|
104
104
|
}
|
105
105
|
|
106
|
-
override toDOM(view: EditorView) {
|
106
|
+
override toDOM(view: EditorView): HTMLImageElement {
|
107
107
|
const img = document.createElement('img');
|
108
108
|
img.setAttribute('src', this._url);
|
109
109
|
img.setAttribute('class', 'cm-image');
|
@@ -2,7 +2,7 @@
|
|
2
2
|
// Copyright 2023 DXOS.org
|
3
3
|
//
|
4
4
|
|
5
|
-
export * from './
|
5
|
+
export * from './action';
|
6
6
|
export * from './bundle';
|
7
7
|
export * from './debug';
|
8
8
|
export * from './decorate';
|
@@ -10,5 +10,4 @@ export * from './formatting';
|
|
10
10
|
export * from './highlight';
|
11
11
|
export * from './image';
|
12
12
|
export * from './link';
|
13
|
-
export * from './outliner';
|
14
13
|
export * from './table';
|
@@ -72,8 +72,9 @@ export const formattingStyles = EditorView.theme({
|
|
72
72
|
* Task list.
|
73
73
|
*/
|
74
74
|
'& .cm-task': {
|
75
|
-
display: 'inline-
|
75
|
+
display: 'inline-flex',
|
76
76
|
width: `${bulletListIndentationWidth}px`,
|
77
|
+
height: '20px',
|
77
78
|
},
|
78
79
|
'& .cm-task-checkbox': {
|
79
80
|
display: 'grid',
|
@@ -106,14 +106,14 @@ class TableWidget extends WidgetType {
|
|
106
106
|
super();
|
107
107
|
}
|
108
108
|
|
109
|
-
override eq(other: this) {
|
109
|
+
override eq(other: this): boolean {
|
110
110
|
return (
|
111
111
|
this._table.header?.join() === other._table.header?.join() &&
|
112
112
|
this._table.rows?.join() === other._table.rows?.join()
|
113
113
|
);
|
114
114
|
}
|
115
115
|
|
116
|
-
override toDOM(view: EditorView) {
|
116
|
+
override toDOM(view: EditorView): HTMLDivElement {
|
117
117
|
const div = document.createElement('div');
|
118
118
|
const table = div.appendChild(document.createElement('table'));
|
119
119
|
|
@@ -138,7 +138,7 @@ class TableWidget extends WidgetType {
|
|
138
138
|
return div;
|
139
139
|
}
|
140
140
|
|
141
|
-
override ignoreEvent(e: Event) {
|
141
|
+
override ignoreEvent(e: Event): boolean {
|
142
142
|
return !/^mouse/.test(e.type);
|
143
143
|
}
|
144
144
|
}
|
@@ -0,0 +1,270 @@
|
|
1
|
+
//
|
2
|
+
// Copyright 2025 DXOS.org
|
3
|
+
//
|
4
|
+
|
5
|
+
import { indentMore } from '@codemirror/commands';
|
6
|
+
import { getIndentUnit } from '@codemirror/language';
|
7
|
+
import { type ChangeSpec, EditorSelection, type Extension } from '@codemirror/state';
|
8
|
+
import { type Command, type EditorView, keymap } from '@codemirror/view';
|
9
|
+
|
10
|
+
import { getSelection, selectAll, selectDown, selectNone, selectUp } from './selection';
|
11
|
+
import { getRange, treeFacet } from './tree';
|
12
|
+
|
13
|
+
//
|
14
|
+
// Indentation comnmands.
|
15
|
+
//
|
16
|
+
|
17
|
+
export const indentItemMore: Command = (view: EditorView) => {
|
18
|
+
const pos = getSelection(view.state).from;
|
19
|
+
const tree = view.state.facet(treeFacet);
|
20
|
+
const current = tree.find(pos);
|
21
|
+
if (current) {
|
22
|
+
const previous = tree.prev(current);
|
23
|
+
if (previous && current.level <= previous.level) {
|
24
|
+
// TODO(burdon): Indent descendants?
|
25
|
+
indentMore(view);
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
return true;
|
30
|
+
};
|
31
|
+
|
32
|
+
export const indentItemLess: Command = (view: EditorView) => {
|
33
|
+
const pos = getSelection(view.state).from;
|
34
|
+
const tree = view.state.facet(treeFacet);
|
35
|
+
const current = tree.find(pos);
|
36
|
+
if (current) {
|
37
|
+
if (current.level > 0) {
|
38
|
+
// Unindent current line and all descendants.
|
39
|
+
// NOTE: The markdown extension doesn't provide an indentation service.
|
40
|
+
const indentUnit = getIndentUnit(view.state);
|
41
|
+
const changes: ChangeSpec[] = [];
|
42
|
+
tree.traverse(current, (item) => {
|
43
|
+
const line = view.state.doc.lineAt(item.lineRange.from);
|
44
|
+
changes.push({ from: line.from, to: line.from + indentUnit });
|
45
|
+
});
|
46
|
+
|
47
|
+
if (changes.length > 0) {
|
48
|
+
view.dispatch({ changes });
|
49
|
+
}
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
return true;
|
54
|
+
};
|
55
|
+
|
56
|
+
//
|
57
|
+
// Moving commands.
|
58
|
+
//
|
59
|
+
|
60
|
+
export const moveItemDown: Command = (view: EditorView) => {
|
61
|
+
const pos = getSelection(view.state)?.from;
|
62
|
+
const tree = view.state.facet(treeFacet);
|
63
|
+
const current = tree.find(pos);
|
64
|
+
if (current && current.nextSibling) {
|
65
|
+
const next = current.nextSibling;
|
66
|
+
const currentContent = view.state.doc.sliceString(...getRange(tree, current));
|
67
|
+
const nextContent = view.state.doc.sliceString(...getRange(tree, next));
|
68
|
+
const changes: ChangeSpec[] = [
|
69
|
+
{
|
70
|
+
from: current.lineRange.from,
|
71
|
+
to: current.lineRange.from + currentContent.length,
|
72
|
+
insert: nextContent,
|
73
|
+
},
|
74
|
+
{
|
75
|
+
from: next.lineRange.from,
|
76
|
+
to: next.lineRange.from + nextContent.length,
|
77
|
+
insert: currentContent,
|
78
|
+
},
|
79
|
+
];
|
80
|
+
|
81
|
+
view.dispatch({
|
82
|
+
changes,
|
83
|
+
selection: EditorSelection.cursor(pos + nextContent.length + 1),
|
84
|
+
scrollIntoView: true,
|
85
|
+
});
|
86
|
+
}
|
87
|
+
|
88
|
+
return true;
|
89
|
+
};
|
90
|
+
|
91
|
+
export const moveItemUp: Command = (view: EditorView) => {
|
92
|
+
const pos = getSelection(view.state)?.from;
|
93
|
+
const tree = view.state.facet(treeFacet);
|
94
|
+
const current = tree.find(pos);
|
95
|
+
if (current && current.prevSibling) {
|
96
|
+
const prev = current.prevSibling;
|
97
|
+
const currentContent = view.state.doc.sliceString(...getRange(tree, current));
|
98
|
+
const prevContent = view.state.doc.sliceString(...getRange(tree, prev));
|
99
|
+
const changes: ChangeSpec[] = [
|
100
|
+
{
|
101
|
+
from: prev.lineRange.from,
|
102
|
+
to: prev.lineRange.from + prevContent.length,
|
103
|
+
insert: currentContent,
|
104
|
+
},
|
105
|
+
{
|
106
|
+
from: current.lineRange.from,
|
107
|
+
to: current.lineRange.from + currentContent.length,
|
108
|
+
insert: prevContent,
|
109
|
+
},
|
110
|
+
];
|
111
|
+
|
112
|
+
view.dispatch({
|
113
|
+
changes,
|
114
|
+
selection: EditorSelection.cursor(pos - prevContent.length - 1),
|
115
|
+
scrollIntoView: true,
|
116
|
+
});
|
117
|
+
}
|
118
|
+
|
119
|
+
return true;
|
120
|
+
};
|
121
|
+
|
122
|
+
//
|
123
|
+
// Misc commands.
|
124
|
+
//
|
125
|
+
|
126
|
+
export const deleteItem: Command = (view: EditorView) => {
|
127
|
+
const tree = view.state.facet(treeFacet);
|
128
|
+
const pos = getSelection(view.state).from;
|
129
|
+
const current = tree.find(pos);
|
130
|
+
if (current) {
|
131
|
+
view.dispatch({
|
132
|
+
selection: EditorSelection.cursor(current.lineRange.from),
|
133
|
+
changes: [
|
134
|
+
{
|
135
|
+
from: current.lineRange.from,
|
136
|
+
to: Math.min(current.lineRange.to + 1, view.state.doc.length),
|
137
|
+
},
|
138
|
+
],
|
139
|
+
});
|
140
|
+
}
|
141
|
+
|
142
|
+
return true;
|
143
|
+
};
|
144
|
+
|
145
|
+
export const toggleTask: Command = (view: EditorView) => {
|
146
|
+
const tree = view.state.facet(treeFacet);
|
147
|
+
const pos = getSelection(view.state)?.from;
|
148
|
+
const current = tree.find(pos);
|
149
|
+
if (current) {
|
150
|
+
const type = current.type === 'task' ? 'bullet' : 'task';
|
151
|
+
const indent = ' '.repeat(getIndentUnit(view.state) * current.level);
|
152
|
+
view.dispatch({
|
153
|
+
changes: [
|
154
|
+
{
|
155
|
+
from: current.lineRange.from,
|
156
|
+
to: current.contentRange.from,
|
157
|
+
insert: indent + (type === 'task' ? '- [ ] ' : '- '),
|
158
|
+
},
|
159
|
+
],
|
160
|
+
});
|
161
|
+
}
|
162
|
+
|
163
|
+
return true;
|
164
|
+
};
|
165
|
+
|
166
|
+
export const commands = (): Extension =>
|
167
|
+
keymap.of([
|
168
|
+
//
|
169
|
+
// Indentation.
|
170
|
+
//
|
171
|
+
{
|
172
|
+
key: 'Tab',
|
173
|
+
preventDefault: true,
|
174
|
+
run: indentItemMore,
|
175
|
+
shift: indentItemLess,
|
176
|
+
},
|
177
|
+
|
178
|
+
//
|
179
|
+
// Continuation.
|
180
|
+
//
|
181
|
+
{
|
182
|
+
key: 'Enter',
|
183
|
+
shift: (view) => {
|
184
|
+
const pos = getSelection(view.state).from;
|
185
|
+
const insert = '\n '; // TODO(burdon): Fix parsing.
|
186
|
+
view.dispatch({
|
187
|
+
changes: [{ from: pos, to: pos, insert }],
|
188
|
+
selection: EditorSelection.cursor(pos + insert.length),
|
189
|
+
});
|
190
|
+
return true;
|
191
|
+
},
|
192
|
+
},
|
193
|
+
|
194
|
+
//
|
195
|
+
// Navigation.
|
196
|
+
//
|
197
|
+
{
|
198
|
+
key: 'ArrowDown',
|
199
|
+
// Jump to next item (default moves to end of currentline).
|
200
|
+
run: (view) => {
|
201
|
+
const tree = view.state.facet(treeFacet);
|
202
|
+
const item = tree.find(getSelection(view.state).from);
|
203
|
+
if (
|
204
|
+
item &&
|
205
|
+
view.state.doc.lineAt(item.lineRange.to).number - view.state.doc.lineAt(item.lineRange.from).number === 0
|
206
|
+
) {
|
207
|
+
const next = tree.next(item);
|
208
|
+
if (next) {
|
209
|
+
view.dispatch({ selection: EditorSelection.cursor(next.contentRange.from) });
|
210
|
+
return true;
|
211
|
+
}
|
212
|
+
}
|
213
|
+
|
214
|
+
return false;
|
215
|
+
},
|
216
|
+
},
|
217
|
+
|
218
|
+
//
|
219
|
+
// Line selection.
|
220
|
+
// TODO(burdon): Shortcut to select current item?
|
221
|
+
//
|
222
|
+
{
|
223
|
+
key: 'Mod-a',
|
224
|
+
preventDefault: true,
|
225
|
+
run: selectAll,
|
226
|
+
},
|
227
|
+
{
|
228
|
+
key: 'Escape',
|
229
|
+
preventDefault: true,
|
230
|
+
run: selectNone,
|
231
|
+
},
|
232
|
+
{
|
233
|
+
key: 'ArrowUp',
|
234
|
+
shift: selectUp,
|
235
|
+
},
|
236
|
+
{
|
237
|
+
key: 'ArrowDown',
|
238
|
+
shift: selectDown,
|
239
|
+
},
|
240
|
+
|
241
|
+
//
|
242
|
+
// Move.
|
243
|
+
//
|
244
|
+
{
|
245
|
+
key: 'Alt-ArrowDown',
|
246
|
+
preventDefault: true,
|
247
|
+
run: moveItemDown,
|
248
|
+
},
|
249
|
+
{
|
250
|
+
key: 'Alt-ArrowUp',
|
251
|
+
preventDefault: true,
|
252
|
+
run: moveItemUp,
|
253
|
+
},
|
254
|
+
//
|
255
|
+
// Delete.
|
256
|
+
//
|
257
|
+
{
|
258
|
+
key: 'Mod-Backspace',
|
259
|
+
preventDefault: true,
|
260
|
+
run: deleteItem,
|
261
|
+
},
|
262
|
+
//
|
263
|
+
// Misc.
|
264
|
+
//
|
265
|
+
{
|
266
|
+
key: 'Alt-t',
|
267
|
+
preventDefault: true,
|
268
|
+
run: toggleTask,
|
269
|
+
},
|
270
|
+
]);
|
@@ -0,0 +1,33 @@
|
|
1
|
+
//
|
2
|
+
// Copyright 2025 DXOS.org
|
3
|
+
//
|
4
|
+
|
5
|
+
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
6
|
+
import { EditorSelection, EditorState } from '@codemirror/state';
|
7
|
+
import { describe, test } from 'vitest';
|
8
|
+
|
9
|
+
import { editor } from './editor';
|
10
|
+
import { outlinerTree, treeFacet } from './tree';
|
11
|
+
|
12
|
+
const extensions = [markdown({ base: markdownLanguage }), outlinerTree(), editor()];
|
13
|
+
|
14
|
+
describe('editor', () => {
|
15
|
+
test('empty', ({ expect }) => {
|
16
|
+
const state = EditorState.create({ extensions });
|
17
|
+
const tree = state.facet(treeFacet);
|
18
|
+
expect(tree).to.exist;
|
19
|
+
});
|
20
|
+
|
21
|
+
test('prevent moving out of range', ({ expect }) => {
|
22
|
+
const state = EditorState.create({ doc: '- [ ] ', extensions });
|
23
|
+
const spec = state.update({ selection: EditorSelection.cursor(1) });
|
24
|
+
expect(spec.state.selection.ranges[0].from).to.eq(6);
|
25
|
+
});
|
26
|
+
|
27
|
+
test.skip('prevent deleting task marker', ({ expect }) => {
|
28
|
+
const state = EditorState.create({ doc: '- [ ] ', extensions });
|
29
|
+
state.update({ selection: EditorSelection.cursor(6) });
|
30
|
+
const spec = state.update({ changes: { from: 5, to: 6 } });
|
31
|
+
expect(spec.state.doc.toString()).to.eq('- [ ] ');
|
32
|
+
});
|
33
|
+
});
|