@dxos/react-ui-editor 0.3.10-main.3645708 → 0.3.10-main.396fa89
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 +1495 -489
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +26 -2
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +3 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +3 -0
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts +3 -2
- package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/basic.d.ts +2 -1
- package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/blast.d.ts +25 -0
- package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/code.d.ts +2 -0
- package/dist/types/src/components/TextEditor/extensions/code.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts +18 -0
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/demo.d.ts +11 -0
- package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/experimental.d.ts +1 -1
- package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/highlight.d.ts +24 -0
- package/dist/types/src/components/TextEditor/extensions/highlight.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/image.d.ts +4 -0
- package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/index.d.ts +8 -0
- package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/listener.d.ts +4 -2
- package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +10 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +9 -3
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/mention.d.ts +6 -0
- package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/table.d.ts +8 -0
- package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +2 -4
- package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/themes/default.d.ts +8 -0
- package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -1
- package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.d.ts +3 -0
- package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts +16 -0
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/handle.d.ts.map +1 -0
- package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/index.d.ts +1 -1
- package/dist/types/src/hooks/automerge/index.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts +5 -0
- package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts +6 -0
- package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/translation/index.d.ts +3 -0
- package/dist/types/src/hooks/automerge/translation/index.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +0 -1
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextModel.d.ts +8 -3
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/{components/TextEditor → hooks/yjs}/yjs.stories.d.ts +2 -2
- package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +1 -0
- package/dist/types/src/styles/markdown.d.ts +3 -2
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/testing/replicator.d.ts.map +1 -1
- package/package.json +22 -19
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +161 -36
- package/src/components/TextEditor/TextEditor.stories.tsx +12 -1
- package/src/components/TextEditor/TextEditor.tsx +49 -24
- package/src/components/TextEditor/extensions/autocomplete.ts +9 -22
- package/src/components/TextEditor/extensions/basic.ts +6 -4
- package/src/components/TextEditor/extensions/blast.ts +373 -0
- package/src/components/TextEditor/extensions/code.ts +99 -0
- package/src/components/TextEditor/extensions/comments.ts +173 -0
- package/src/components/TextEditor/extensions/demo.ts +69 -0
- package/src/components/TextEditor/extensions/experimental.tsx +15 -35
- package/src/components/TextEditor/extensions/highlight.ts +128 -0
- package/src/components/TextEditor/extensions/image.ts +67 -0
- package/src/components/TextEditor/extensions/index.ts +8 -0
- package/src/components/TextEditor/extensions/link.ts +27 -27
- package/src/components/TextEditor/extensions/listener.ts +4 -3
- package/src/components/TextEditor/extensions/markdown/bundle.ts +46 -43
- package/src/components/TextEditor/extensions/markdown/highlight.ts +148 -107
- package/src/components/TextEditor/extensions/mention.ts +31 -0
- package/src/components/TextEditor/extensions/table.ts +119 -0
- package/src/components/TextEditor/extensions/tasklist.ts +89 -81
- package/src/components/TextEditor/themes/default.ts +112 -4
- package/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.ts +12 -9
- package/src/{automerge → hooks/automerge}/automerge.stories.tsx +6 -3
- package/src/{automerge/automerge-plugin → hooks/automerge}/plugin.ts +1 -0
- package/src/{automerge/automerge-plugin/amToCodemirror.ts → hooks/automerge/translation/automerge-to-codemirror.ts} +10 -2
- package/src/{automerge/automerge-plugin/codeMirrorToAm.ts → hooks/automerge/translation/codemirror-to-automerge.ts} +7 -4
- package/src/hooks/automerge/translation/index.ts +6 -0
- package/src/hooks/index.ts +0 -1
- package/src/hooks/useTextModel.ts +58 -41
- package/src/{components/TextEditor → hooks/yjs}/yjs.stories.tsx +2 -2
- package/src/styles/markdown.ts +15 -8
- package/src/testing/replicator.ts +7 -7
- package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts +0 -6
- package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts +0 -7
- package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge.stories.d.ts +0 -9
- package/dist/types/src/automerge/automerge.stories.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
- package/dist/types/src/hooks/useCollaboration.d.ts +0 -9
- package/dist/types/src/hooks/useCollaboration.d.ts.map +0 -1
- package/src/hooks/useCollaboration.ts +0 -45
- package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/handle.d.ts +0 -0
- package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/plugin.d.ts +0 -0
- package/src/{automerge/automerge-plugin → hooks/automerge}/handle.ts +0 -0
- package/src/{automerge/automerge-plugin → hooks/automerge}/index.ts +1 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Range } from '@codemirror/state';
|
|
6
|
+
import {
|
|
7
|
+
ViewPlugin,
|
|
8
|
+
type DecorationSet,
|
|
9
|
+
EditorView,
|
|
10
|
+
type ViewUpdate,
|
|
11
|
+
Decoration,
|
|
12
|
+
type BlockInfo,
|
|
13
|
+
} from '@codemirror/view';
|
|
14
|
+
import get from 'lodash.get';
|
|
15
|
+
|
|
16
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
17
|
+
|
|
18
|
+
import { tokens } from '../../../styles';
|
|
19
|
+
|
|
20
|
+
const CODE_REGEX = /```[\s\S]*?```/gs;
|
|
21
|
+
|
|
22
|
+
// TODO(burdon): Reconcile with theme.
|
|
23
|
+
const styles = EditorView.baseTheme({
|
|
24
|
+
'& .cm-code-block': {
|
|
25
|
+
display: 'block',
|
|
26
|
+
paddingInline: '4px !important',
|
|
27
|
+
},
|
|
28
|
+
'&light .cm-code-block': {
|
|
29
|
+
background: get(tokens, 'extend.colors.neutral.50'),
|
|
30
|
+
},
|
|
31
|
+
'&dark .cm-code-block': {
|
|
32
|
+
background: get(tokens, 'extend.colors.neutral.850'),
|
|
33
|
+
},
|
|
34
|
+
'& .cm-code-block-first': {
|
|
35
|
+
paddingTop: '4px !important',
|
|
36
|
+
borderTopLeftRadius: '4px',
|
|
37
|
+
borderTopRightRadius: '4px',
|
|
38
|
+
},
|
|
39
|
+
'& .cm-code-block-last': {
|
|
40
|
+
paddingBottom: '4px !important',
|
|
41
|
+
borderBottomLeftRadius: '4px',
|
|
42
|
+
borderBottomRightRadius: '4px',
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const getLineRange = (lines: BlockInfo[], from: number, to: number) => {
|
|
47
|
+
const start = lines.findIndex((line) => line.from >= from);
|
|
48
|
+
const end = lines.findIndex((line) => line.to >= to);
|
|
49
|
+
return [start, end];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const code = () => {
|
|
53
|
+
const getDecorations = (view: EditorView) => {
|
|
54
|
+
const decorations: Range<Decoration>[] = [];
|
|
55
|
+
if (view.state.readOnly) {
|
|
56
|
+
const text = view.state.doc.sliceString(0);
|
|
57
|
+
const matches = text.matchAll(CODE_REGEX);
|
|
58
|
+
const blocks = view.viewportLineBlocks;
|
|
59
|
+
for (const match of matches) {
|
|
60
|
+
const range = getLineRange(blocks, match.index!, match.index! + match[0].length);
|
|
61
|
+
for (let i = range[0]; i <= range[1]; i++) {
|
|
62
|
+
const block = blocks[i];
|
|
63
|
+
decorations.push(
|
|
64
|
+
Decoration.line({
|
|
65
|
+
class: mx(
|
|
66
|
+
'cm-code-block',
|
|
67
|
+
i === range[0] && 'cm-code-block-first',
|
|
68
|
+
i === range[1] && 'cm-code-block-last',
|
|
69
|
+
),
|
|
70
|
+
}).range(block.from),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return Decoration.set(decorations);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return [
|
|
80
|
+
ViewPlugin.fromClass(
|
|
81
|
+
class {
|
|
82
|
+
decorations: DecorationSet;
|
|
83
|
+
constructor(view: EditorView) {
|
|
84
|
+
this.decorations = getDecorations(view);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
update(update: ViewUpdate) {
|
|
88
|
+
if (update.docChanged || update.viewportChanged) {
|
|
89
|
+
this.decorations = getDecorations(update.view);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
decorations: (value) => value.decorations,
|
|
95
|
+
},
|
|
96
|
+
),
|
|
97
|
+
styles,
|
|
98
|
+
];
|
|
99
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
import {
|
|
7
|
+
keymap,
|
|
8
|
+
type DecorationSet,
|
|
9
|
+
type Rect,
|
|
10
|
+
type ViewUpdate,
|
|
11
|
+
Decoration,
|
|
12
|
+
EditorView,
|
|
13
|
+
MatchDecorator,
|
|
14
|
+
ViewPlugin,
|
|
15
|
+
WidgetType,
|
|
16
|
+
} from '@codemirror/view';
|
|
17
|
+
|
|
18
|
+
import { debounce } from '@dxos/async';
|
|
19
|
+
import { invariant } from '@dxos/invariant';
|
|
20
|
+
|
|
21
|
+
// TODO(burdon): Reconcile with theme.
|
|
22
|
+
const styles = EditorView.baseTheme({
|
|
23
|
+
'& .cm-bookmark': {
|
|
24
|
+
cursor: 'pointer',
|
|
25
|
+
margin: '4px',
|
|
26
|
+
padding: '4px',
|
|
27
|
+
backgroundColor: 'yellow',
|
|
28
|
+
},
|
|
29
|
+
'& .cm-bookmark-selected': {
|
|
30
|
+
backgroundColor: 'orange',
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
class BookmarkWidget extends WidgetType {
|
|
35
|
+
constructor(private readonly _pos: number, private readonly _id: string, private readonly _handleClick: () => void) {
|
|
36
|
+
super();
|
|
37
|
+
invariant(this._id);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override eq(other: BookmarkWidget) {
|
|
41
|
+
return this._id === other._id;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override toDOM() {
|
|
45
|
+
const span = document.createElement('span');
|
|
46
|
+
span.className = 'cm-bookmark';
|
|
47
|
+
span.textContent = '§';
|
|
48
|
+
span.onclick = () => this._handleClick();
|
|
49
|
+
return span;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type CommentsInfo = {
|
|
54
|
+
active?: string;
|
|
55
|
+
items?: { id: string; pos: number; location: Rect | null }[];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type CommentsOptions = {
|
|
59
|
+
key?: string;
|
|
60
|
+
onCreate?: (from: number, to: number) => string | void;
|
|
61
|
+
onUpdate?: (info: CommentsInfo) => void;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// https://www.markdownguide.org/extended-syntax/#footnotes
|
|
65
|
+
// TODO(burdon): Record span in automerge model?
|
|
66
|
+
// TODO(burdon): Extended markdown: https://www.markdownguide.org/extended-syntax
|
|
67
|
+
export const comments = (options: CommentsOptions = {}): Extension => {
|
|
68
|
+
let active: string | undefined;
|
|
69
|
+
|
|
70
|
+
const bookmarkMatcher = new MatchDecorator({
|
|
71
|
+
regexp: /\[\^(\w+)\]/g,
|
|
72
|
+
decoration: (match, view, pos) => {
|
|
73
|
+
const id = match[1];
|
|
74
|
+
return Decoration.replace({
|
|
75
|
+
id,
|
|
76
|
+
widget: new BookmarkWidget(pos, id, () => handleUpdate({ active: id })),
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const bookmarks = ViewPlugin.fromClass(
|
|
82
|
+
class {
|
|
83
|
+
bookmarks: DecorationSet;
|
|
84
|
+
constructor(view: EditorView) {
|
|
85
|
+
this.bookmarks = bookmarkMatcher.createDeco(view);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
update(update: ViewUpdate) {
|
|
89
|
+
this.bookmarks = bookmarkMatcher.updateDeco(update, this.bookmarks);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
decorations: (instance) => instance.bookmarks,
|
|
94
|
+
provide: (plugin) =>
|
|
95
|
+
EditorView.atomicRanges.of((view) => {
|
|
96
|
+
return view.plugin(plugin)?.bookmarks || Decoration.none;
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const handleUpdate = debounce((data: CommentsInfo) => {
|
|
102
|
+
options.onUpdate?.(data);
|
|
103
|
+
}, 200);
|
|
104
|
+
|
|
105
|
+
return [
|
|
106
|
+
keymap.of([
|
|
107
|
+
{
|
|
108
|
+
key: options?.key ?? 'shift-meta-c',
|
|
109
|
+
run: (view) => {
|
|
110
|
+
// Insert footnote.
|
|
111
|
+
const { from, to, head } = view.state.selection.main;
|
|
112
|
+
const id = options.onCreate?.(from, to);
|
|
113
|
+
if (id) {
|
|
114
|
+
const tag = `[^${id}]`;
|
|
115
|
+
// TODO(burdon): Add selection.
|
|
116
|
+
view.dispatch({
|
|
117
|
+
changes: { from: head, insert: tag },
|
|
118
|
+
selection: { anchor: head + tag.length },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return false;
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
]),
|
|
128
|
+
|
|
129
|
+
bookmarks,
|
|
130
|
+
|
|
131
|
+
// Monitor cursor movement.
|
|
132
|
+
EditorView.updateListener.of((update) => {
|
|
133
|
+
active = undefined;
|
|
134
|
+
if (!options.onUpdate) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { view, state } = update;
|
|
139
|
+
const pos = state.selection.main.head;
|
|
140
|
+
|
|
141
|
+
const decorations: { from: number; to: number; value: Decoration }[] = [];
|
|
142
|
+
const rangeSet = view.plugin(bookmarks)?.bookmarks;
|
|
143
|
+
let closest = Infinity;
|
|
144
|
+
// TODO(burdon): Handle multiple visible ranges in large documents.
|
|
145
|
+
const { from, to } = /* view.visibleRanges?.[0] ?? */ { from: 0, to: state.doc.length };
|
|
146
|
+
rangeSet?.between(from, to, (from, to, value) => {
|
|
147
|
+
decorations.push({ from, to, value });
|
|
148
|
+
const d = Math.min(Math.abs(pos - from), Math.abs(pos - to));
|
|
149
|
+
if (d < closest) {
|
|
150
|
+
// TODO(burdon): Update class of selected.
|
|
151
|
+
active = value.spec.id;
|
|
152
|
+
closest = d;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (decorations.length) {
|
|
157
|
+
handleUpdate({
|
|
158
|
+
active,
|
|
159
|
+
items: decorations.map(
|
|
160
|
+
({
|
|
161
|
+
from,
|
|
162
|
+
value: {
|
|
163
|
+
spec: { id },
|
|
164
|
+
},
|
|
165
|
+
}) => ({ id: id as string, pos: from, location: view.coordsAtPos(from) }),
|
|
166
|
+
),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}),
|
|
170
|
+
|
|
171
|
+
styles,
|
|
172
|
+
];
|
|
173
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
import { keymap } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
// TODO(burdon): Review https://github.com/sergeche/codemirror-movie?tab=readme-ov-file
|
|
9
|
+
|
|
10
|
+
export type DemoOptions = {
|
|
11
|
+
delay?: number;
|
|
12
|
+
items?: string[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const defaultItems = ['hello world!', 'this is a test.', 'this is [DXOS](https://dxos.org)'];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Demo script.
|
|
19
|
+
* Configurable plugin that let's user cycle through pre-configured input script.
|
|
20
|
+
*/
|
|
21
|
+
export const demo = ({ delay = 75, items = defaultItems }: DemoOptions = {}): Extension => {
|
|
22
|
+
let t: any;
|
|
23
|
+
let idx = 0;
|
|
24
|
+
|
|
25
|
+
return [
|
|
26
|
+
keymap.of([
|
|
27
|
+
{
|
|
28
|
+
// Reset.
|
|
29
|
+
key: "alt-meta-'",
|
|
30
|
+
run: (view) => {
|
|
31
|
+
clearTimeout(t);
|
|
32
|
+
idx = 0;
|
|
33
|
+
return true;
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
// Next prompt.
|
|
38
|
+
// TODO(burdon): Press 1-9 to select prompt?
|
|
39
|
+
key: "shift-meta-'",
|
|
40
|
+
run: (view) => {
|
|
41
|
+
clearTimeout(t);
|
|
42
|
+
// TODO(burdon): Add space if needed.
|
|
43
|
+
const text = items[idx++];
|
|
44
|
+
if (idx === items?.length) {
|
|
45
|
+
idx = 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let i = 0;
|
|
49
|
+
const insert = (d = 0) => {
|
|
50
|
+
t = setTimeout(() => {
|
|
51
|
+
const pos = view.state.selection.main.head;
|
|
52
|
+
view.dispatch({
|
|
53
|
+
changes: { from: pos, insert: text[i++] },
|
|
54
|
+
selection: { anchor: pos + 1 },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (i < text.length) {
|
|
58
|
+
insert(Math.random() * delay * (text[i] === ' ' ? 2 : 1));
|
|
59
|
+
}
|
|
60
|
+
}, d);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
insert();
|
|
64
|
+
return true;
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
]),
|
|
68
|
+
];
|
|
69
|
+
};
|
|
@@ -13,27 +13,21 @@ import {
|
|
|
13
13
|
type ViewUpdate,
|
|
14
14
|
} from '@codemirror/view';
|
|
15
15
|
|
|
16
|
-
// TODO(burdon):
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// Notes: Can't edit widget content (or cursor nav through them).
|
|
31
|
-
// - https://discuss.codemirror.net/t/focusing-inputs-within-widgets/5178/7
|
|
32
|
-
// Widgets intentionally always get set to contenteditable=false,
|
|
33
|
-
// or they would become part of CodeMirror’s editable content element.
|
|
34
|
-
// You should be able to introduce new contenteditable=true child elements inside of them.
|
|
16
|
+
// TODO(burdon): Reconcile with theme.
|
|
17
|
+
export const styles = EditorView.baseTheme({
|
|
18
|
+
'.cm-hyperlink-label': {
|
|
19
|
+
cursor: 'pointer',
|
|
20
|
+
textDecoration: 'underline',
|
|
21
|
+
},
|
|
22
|
+
'.cm-hyperlink-bracket': {
|
|
23
|
+
opacity: 0.2,
|
|
24
|
+
},
|
|
25
|
+
'.cm-hyperlink-url': {
|
|
26
|
+
opacity: 0.5,
|
|
27
|
+
fontFamily: 'monospace',
|
|
28
|
+
},
|
|
29
|
+
});
|
|
35
30
|
|
|
36
|
-
// TODO(burdon): URL match?
|
|
37
31
|
const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)(!?)/gi;
|
|
38
32
|
|
|
39
33
|
/**
|
|
@@ -98,7 +92,7 @@ export function hyperLinkExtension() {
|
|
|
98
92
|
}
|
|
99
93
|
},
|
|
100
94
|
{
|
|
101
|
-
decorations: (
|
|
95
|
+
decorations: (value) => value.decorations,
|
|
102
96
|
eventHandlers: {
|
|
103
97
|
// TODO(burdon): CLick to expand link.
|
|
104
98
|
// https://discuss.codemirror.net/t/decorator-iteration-on-click-event/4226/2
|
|
@@ -117,18 +111,4 @@ export function hyperLinkExtension() {
|
|
|
117
111
|
);
|
|
118
112
|
}
|
|
119
113
|
|
|
120
|
-
export const styles = EditorView.baseTheme({
|
|
121
|
-
'.cm-hyperlink-label': {
|
|
122
|
-
cursor: 'pointer',
|
|
123
|
-
textDecoration: 'underline',
|
|
124
|
-
},
|
|
125
|
-
'.cm-hyperlink-bracket': {
|
|
126
|
-
opacity: 0.2,
|
|
127
|
-
},
|
|
128
|
-
'.cm-hyperlink-url': {
|
|
129
|
-
opacity: 0.5,
|
|
130
|
-
fontFamily: 'monospace',
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
|
|
134
114
|
export const hyperlinkWidget: Extension = [hyperLinkExtension(), styles];
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension, StateEffect, StateField } from '@codemirror/state';
|
|
6
|
+
import { Decoration, EditorView } from '@codemirror/view';
|
|
7
|
+
import sortBy from 'lodash.sortby';
|
|
8
|
+
import { useEffect } from 'react';
|
|
9
|
+
|
|
10
|
+
import { type DocumentRange } from '../../../hooks';
|
|
11
|
+
|
|
12
|
+
// TODO(burdon): Detect click, or cursor proximity.
|
|
13
|
+
|
|
14
|
+
// TODO(burdon): Reconcile with theme.
|
|
15
|
+
const styles = EditorView.baseTheme({
|
|
16
|
+
'& .cm-highlight': {
|
|
17
|
+
backgroundColor: 'yellow', // TODO(burdon): Reconcile with tailwind theme.
|
|
18
|
+
},
|
|
19
|
+
'& .cm-highlight::before': {
|
|
20
|
+
content: ']',
|
|
21
|
+
},
|
|
22
|
+
'& .cm-highlight-active': {
|
|
23
|
+
backgroundColor: 'lime',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const marks = {
|
|
28
|
+
highlight: Decoration.mark({ class: 'cm-highlight' }),
|
|
29
|
+
HighlightActive: Decoration.mark({ class: 'cm-highlight-active' }),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type HighlightRange = DocumentRange & { active?: boolean };
|
|
33
|
+
|
|
34
|
+
// TODO(burdon): Make configurable to support multiple layers.
|
|
35
|
+
const setHighlights = StateEffect.define<HighlightRange[]>();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Update field set.
|
|
39
|
+
*/
|
|
40
|
+
// TODO(burdon): Reconcile range from data model, vs. in-markdown characters.
|
|
41
|
+
export const useHighlights = (view: EditorView | undefined, ranges: HighlightRange[] = []) => {
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
view?.dispatch({
|
|
44
|
+
effects: setHighlights.of(ranges),
|
|
45
|
+
});
|
|
46
|
+
}, [view, ranges]);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* State field that tracks highlight ranges.
|
|
51
|
+
*/
|
|
52
|
+
export const highlightStateField = StateField.define<HighlightRange[]>({
|
|
53
|
+
create: () => [],
|
|
54
|
+
update: (threads, tr) => {
|
|
55
|
+
for (const effect of tr.effects) {
|
|
56
|
+
if (effect.is(setHighlights)) {
|
|
57
|
+
return effect.value;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return threads;
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Decorate ranges.
|
|
67
|
+
*/
|
|
68
|
+
// TODO(burdon): Pass in field to make reusable (e.g., comments, search).
|
|
69
|
+
export const highlightDecorations = EditorView.decorations.compute([highlightStateField], (state) => {
|
|
70
|
+
const selectionRanges = state.field(highlightStateField);
|
|
71
|
+
const decorations =
|
|
72
|
+
sortBy(selectionRanges ?? [], (selection) => selection.from)?.flatMap((selection) => {
|
|
73
|
+
const range = { from: selection.from, to: selection.to + 1 };
|
|
74
|
+
if (selection.to > selection.from) {
|
|
75
|
+
if (selection.active) {
|
|
76
|
+
return marks.HighlightActive.range(range.from, range.to);
|
|
77
|
+
} else {
|
|
78
|
+
return marks.highlight.range(range.from, range.to);
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}) ?? [];
|
|
84
|
+
|
|
85
|
+
return Decoration.set(decorations);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// TODO(burdon): Use current range to create comment.
|
|
89
|
+
// TODO(burdon): Allow/prevent overlapping?
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Track cursor entering and existing ranges.
|
|
93
|
+
*/
|
|
94
|
+
const listener = (options: HighlightOptions) => {
|
|
95
|
+
return EditorView.updateListener.of((update) => {
|
|
96
|
+
const { view, state } = update;
|
|
97
|
+
const { /* from, to, */ head } = state.selection.main;
|
|
98
|
+
const ranges = state.field(highlightStateField);
|
|
99
|
+
|
|
100
|
+
// TODO(burdon): Modify range if editing.
|
|
101
|
+
// console.log('::::', update);
|
|
102
|
+
|
|
103
|
+
// Set active.
|
|
104
|
+
let mod = false;
|
|
105
|
+
const newRanges = ranges.map((range) => {
|
|
106
|
+
const active = head >= range.from && head <= range.to;
|
|
107
|
+
if (active !== range.active) {
|
|
108
|
+
mod = true;
|
|
109
|
+
return { ...range, active };
|
|
110
|
+
} else {
|
|
111
|
+
return range;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (mod) {
|
|
116
|
+
view.dispatch({ effects: setHighlights.of(newRanges) });
|
|
117
|
+
options?.onChange?.(newRanges.find((range) => range.active)?.id);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export type HighlightOptions = {
|
|
123
|
+
onChange?: (active?: string) => {};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const highlight = (options: HighlightOptions = {}): Extension => {
|
|
127
|
+
return [highlightStateField, highlightDecorations, listener(options), styles];
|
|
128
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
|
6
|
+
import {
|
|
7
|
+
type EditorState,
|
|
8
|
+
type Extension,
|
|
9
|
+
type RangeSet,
|
|
10
|
+
RangeSetBuilder,
|
|
11
|
+
StateField,
|
|
12
|
+
type Transaction,
|
|
13
|
+
} from '@codemirror/state';
|
|
14
|
+
import { Decoration, EditorView, WidgetType } from '@codemirror/view';
|
|
15
|
+
|
|
16
|
+
export type ImageOptions = {};
|
|
17
|
+
|
|
18
|
+
export const image = (options: ImageOptions = {}): Extension => {
|
|
19
|
+
return StateField.define<RangeSet<any>>({
|
|
20
|
+
create: (state) => update(state, options),
|
|
21
|
+
update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
|
|
22
|
+
provide: (field) => EditorView.decorations.from(field),
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const update = (state: EditorState, options: ImageOptions) => {
|
|
27
|
+
const builder = new RangeSetBuilder();
|
|
28
|
+
const cursor = state.selection.main.head;
|
|
29
|
+
|
|
30
|
+
syntaxTree(state).iterate({
|
|
31
|
+
enter: (node) => {
|
|
32
|
+
if (node.name === 'Image') {
|
|
33
|
+
const urlNode = node.node.getChild('URL');
|
|
34
|
+
if (urlNode) {
|
|
35
|
+
const hide = state.readOnly || cursor < node.from || cursor > node.to;
|
|
36
|
+
const url = state.sliceDoc(urlNode.from, urlNode.to);
|
|
37
|
+
builder.add(
|
|
38
|
+
hide ? node.from : node.to,
|
|
39
|
+
node.to,
|
|
40
|
+
Decoration.replace({
|
|
41
|
+
widget: new ImageWidget(url),
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return builder.finish();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
class ImageWidget extends WidgetType {
|
|
53
|
+
constructor(readonly _url: string) {
|
|
54
|
+
super();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override eq(other: WidgetType) {
|
|
58
|
+
return this._url === (other as any as ImageWidget)?._url;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
toDOM(view: EditorView) {
|
|
62
|
+
const img = document.createElement('img');
|
|
63
|
+
img.setAttribute('src', this._url);
|
|
64
|
+
img.setAttribute('class', 'cm-image');
|
|
65
|
+
return img;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -4,8 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
export * from './autocomplete';
|
|
6
6
|
export * from './basic';
|
|
7
|
+
export * from './blast';
|
|
8
|
+
export * from './code';
|
|
9
|
+
export * from './demo';
|
|
10
|
+
export * from './comments';
|
|
11
|
+
export * from './highlight';
|
|
12
|
+
export * from './image';
|
|
7
13
|
export * from './link';
|
|
8
14
|
export * from './listener';
|
|
9
15
|
export * from './markdown';
|
|
16
|
+
export * from './mention';
|
|
17
|
+
export * from './table';
|
|
10
18
|
export * from './tasklist';
|
|
11
19
|
export * from './tooltip';
|