@dxos/react-ui-editor 0.3.11-main.fbbdc2a → 0.3.11-main.fe789c1
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 +412 -516
- 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 +9 -5
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +12 -17
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +6 -6
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/comments.stories.d.ts +26 -0
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts +1 -1
- package/dist/types/src/extensions/autocomplete.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/index.d.ts +1 -0
- package/dist/types/src/extensions/automerge/index.d.ts.map +1 -1
- package/dist/types/src/extensions/code.d.ts.map +1 -1
- 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/{util → extensions}/cursor.d.ts +9 -1
- package/dist/types/src/extensions/cursor.d.ts.map +1 -0
- package/dist/types/src/extensions/hr.d.ts.map +1 -1
- package/dist/types/src/extensions/image.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +1 -1
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/link.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/tasklist.d.ts.map +1 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts +1 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextModel.d.ts +0 -5
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/hooks/yjs.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/styles/tokens.d.ts +1 -0
- package/dist/types/src/styles/tokens.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/dist/types/src/util.d.ts.map +1 -0
- package/package.json +23 -22
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +25 -23
- package/src/components/TextEditor/TextEditor.stories.tsx +12 -46
- package/src/components/TextEditor/TextEditor.tsx +68 -64
- package/src/components/TextEditor/comments.stories.tsx +315 -0
- package/src/extensions/autocomplete.ts +18 -17
- package/src/extensions/automerge/automerge.ts +2 -2
- package/src/extensions/automerge/cursor.ts +2 -1
- package/src/extensions/automerge/index.ts +1 -0
- package/src/extensions/awareness.ts +25 -32
- package/src/extensions/code.ts +14 -7
- package/src/extensions/comments.ts +23 -119
- package/src/extensions/cursor.ts +46 -0
- package/src/extensions/hr.ts +4 -5
- package/src/extensions/image.ts +40 -22
- package/src/extensions/index.ts +1 -1
- package/src/extensions/link.ts +86 -88
- package/src/extensions/markdown/highlight.ts +3 -12
- package/src/extensions/table.ts +10 -4
- package/src/extensions/tasklist.ts +3 -5
- package/src/extensions/yjs/cursor.ts +2 -1
- package/src/extensions/yjs/yjs.ts +2 -2
- package/src/hooks/useTextModel.ts +4 -7
- package/src/hooks/yjs.ts +1 -23
- package/src/index.ts +3 -0
- package/src/styles/markdown.ts +0 -2
- package/src/styles/tokens.ts +3 -0
- package/src/themes/default.ts +52 -71
- package/dist/types/src/extensions/mermaid.d.ts +0 -3
- package/dist/types/src/extensions/mermaid.d.ts.map +0 -1
- package/dist/types/src/util/cursor.d.ts.map +0 -1
- package/dist/types/src/util/index.d.ts +0 -3
- package/dist/types/src/util/index.d.ts.map +0 -1
- package/dist/types/src/util/util.d.ts.map +0 -1
- package/src/extensions/mermaid.ts +0 -11
- package/src/util/cursor.ts +0 -29
- package/src/util/index.ts +0 -6
- /package/dist/types/src/{util/util.d.ts → util.d.ts} +0 -0
- /package/src/{util/util.ts → util.ts} +0 -0
|
@@ -3,20 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type Extension, StateEffect, StateField } from '@codemirror/state';
|
|
6
|
-
import {
|
|
7
|
-
hoverTooltip,
|
|
8
|
-
keymap,
|
|
9
|
-
type Command,
|
|
10
|
-
type DecorationSet,
|
|
11
|
-
type ViewUpdate,
|
|
12
|
-
Decoration,
|
|
13
|
-
EditorView,
|
|
14
|
-
MatchDecorator,
|
|
15
|
-
ViewPlugin,
|
|
16
|
-
WidgetType,
|
|
17
|
-
type Rect,
|
|
18
|
-
} from '@codemirror/view';
|
|
19
|
-
import get from 'lodash.get';
|
|
6
|
+
import { hoverTooltip, keymap, type Command, Decoration, EditorView, type Rect } from '@codemirror/view';
|
|
20
7
|
import sortBy from 'lodash.sortby';
|
|
21
8
|
|
|
22
9
|
import { debounce } from '@dxos/async';
|
|
@@ -24,29 +11,21 @@ import { invariant } from '@dxos/invariant';
|
|
|
24
11
|
import { log } from '@dxos/log';
|
|
25
12
|
import { nonNullable } from '@dxos/util';
|
|
26
13
|
|
|
14
|
+
import { Cursor } from './cursor';
|
|
27
15
|
import { type CommentRange, type EditorModel, type Range } from '../hooks';
|
|
28
|
-
import {
|
|
29
|
-
import { callbackWrapper
|
|
16
|
+
import { getToken } from '../styles';
|
|
17
|
+
import { callbackWrapper } from '../util';
|
|
30
18
|
|
|
31
19
|
// TODO(burdon): Handle delete, cut, copy, and paste (separately) text that includes comment range.
|
|
32
20
|
// TODO(burdon): Consider breaking into separate plugin (since not standalone)? Like mermaid?
|
|
33
21
|
|
|
34
22
|
// TODO(burdon): Reconcile with theme.
|
|
35
23
|
const styles = EditorView.baseTheme({
|
|
36
|
-
'& .cm-bookmark': {
|
|
37
|
-
cursor: 'pointer',
|
|
38
|
-
margin: '4px',
|
|
39
|
-
padding: '4px',
|
|
40
|
-
backgroundColor: 'yellow',
|
|
41
|
-
},
|
|
42
|
-
'& .cm-bookmark-selected': {
|
|
43
|
-
backgroundColor: 'orange',
|
|
44
|
-
},
|
|
45
24
|
'& .cm-comment': {
|
|
46
|
-
backgroundColor:
|
|
25
|
+
backgroundColor: getToken('extend.colors.yellow.50'),
|
|
47
26
|
},
|
|
48
27
|
'& .cm-comment-active': {
|
|
49
|
-
backgroundColor:
|
|
28
|
+
backgroundColor: getToken('extend.colors.yellow.100'),
|
|
50
29
|
},
|
|
51
30
|
});
|
|
52
31
|
|
|
@@ -99,7 +78,7 @@ const setCommentState = StateEffect.define<CommentsState>();
|
|
|
99
78
|
const commentsStateField = StateField.define<CommentsState>({
|
|
100
79
|
create: () => ({ ranges: [] }),
|
|
101
80
|
update: (value, tr) => {
|
|
102
|
-
const cursorConverter = tr.state.facet(
|
|
81
|
+
const cursorConverter = tr.state.facet(Cursor.converter);
|
|
103
82
|
|
|
104
83
|
for (const effect of tr.effects) {
|
|
105
84
|
// Update selection.
|
|
@@ -112,7 +91,7 @@ const commentsStateField = StateField.define<CommentsState>({
|
|
|
112
91
|
const { comments } = effect.value;
|
|
113
92
|
const ranges: ExtendedCommentRange[] = comments
|
|
114
93
|
.map((comment) => {
|
|
115
|
-
const range = getRangeFromCursor(cursorConverter, comment.cursor);
|
|
94
|
+
const range = Cursor.getRangeFromCursor(cursorConverter, comment.cursor);
|
|
116
95
|
return range && { ...comment, ...range };
|
|
117
96
|
})
|
|
118
97
|
.filter(nonNullable);
|
|
@@ -155,42 +134,13 @@ const highlightDecorations = EditorView.decorations.compute([commentsStateField]
|
|
|
155
134
|
return Decoration.set(decorations);
|
|
156
135
|
});
|
|
157
136
|
|
|
158
|
-
/**
|
|
159
|
-
* Optional bookmark widget at the start/end of the selection.
|
|
160
|
-
* May correspond to a markdown bookmark.
|
|
161
|
-
*/
|
|
162
|
-
class BookmarkWidget extends WidgetType {
|
|
163
|
-
constructor(
|
|
164
|
-
private readonly _anchor: number,
|
|
165
|
-
private readonly _id: string,
|
|
166
|
-
private readonly _handleClick?: () => void,
|
|
167
|
-
) {
|
|
168
|
-
super();
|
|
169
|
-
invariant(this._id);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
override eq(other: BookmarkWidget) {
|
|
173
|
-
return this._anchor === other._anchor && this._id === other._id;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
override toDOM() {
|
|
177
|
-
const span = document.createElement('span');
|
|
178
|
-
span.className = 'cm-bookmark';
|
|
179
|
-
span.textContent = '※';
|
|
180
|
-
if (this._handleClick) {
|
|
181
|
-
span.onclick = () => this._handleClick!();
|
|
182
|
-
}
|
|
183
|
-
return span;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
137
|
export type CommentsOptions = {
|
|
188
138
|
key?: string;
|
|
189
139
|
footnote?: boolean;
|
|
190
140
|
/**
|
|
191
141
|
* Called to create a new thread and return the thread id.
|
|
192
142
|
*/
|
|
193
|
-
onCreate?: (
|
|
143
|
+
onCreate?: (cursor: string, location?: Rect | null) => string | undefined;
|
|
194
144
|
/**
|
|
195
145
|
* Called to notify which thread is currently closest to the cursor.
|
|
196
146
|
*/
|
|
@@ -198,7 +148,7 @@ export type CommentsOptions = {
|
|
|
198
148
|
/**
|
|
199
149
|
* Called to render tooltip.
|
|
200
150
|
*/
|
|
201
|
-
onHover?: (el: Element) => void;
|
|
151
|
+
onHover?: (el: Element, shortcut: string) => void;
|
|
202
152
|
};
|
|
203
153
|
|
|
204
154
|
/**
|
|
@@ -214,13 +164,15 @@ export type CommentsOptions = {
|
|
|
214
164
|
* 6). Optionally, implements a hoverTooltip to show hints when creating a selection range.
|
|
215
165
|
*/
|
|
216
166
|
export const comments = (options: CommentsOptions = {}): Extension => {
|
|
167
|
+
const { key: shortcut = "meta-'" } = options;
|
|
168
|
+
|
|
217
169
|
const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
|
|
218
170
|
|
|
219
171
|
/**
|
|
220
172
|
* Create comment thread action.
|
|
221
173
|
*/
|
|
222
174
|
const createCommentThread: Command = (view) => {
|
|
223
|
-
const cursorConverter = view.state.facet(
|
|
175
|
+
const cursorConverter = view.state.facet(Cursor.converter);
|
|
224
176
|
|
|
225
177
|
invariant(options.onCreate);
|
|
226
178
|
const { head, from, to } = view.state.selection.main;
|
|
@@ -228,10 +180,10 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
228
180
|
return false;
|
|
229
181
|
}
|
|
230
182
|
|
|
231
|
-
const
|
|
232
|
-
if (
|
|
183
|
+
const cursor = Cursor.getCursorFromRange(cursorConverter, { from, to });
|
|
184
|
+
if (cursor) {
|
|
233
185
|
// Create thread via callback.
|
|
234
|
-
const id = callbackWrapper(options.onCreate)(
|
|
186
|
+
const id = callbackWrapper(options.onCreate)(cursor, view.coordsAtPos(from));
|
|
235
187
|
if (id) {
|
|
236
188
|
// Update range.
|
|
237
189
|
view.dispatch({
|
|
@@ -255,44 +207,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
255
207
|
return false;
|
|
256
208
|
};
|
|
257
209
|
|
|
258
|
-
/**
|
|
259
|
-
* Bookmark widget decoration.
|
|
260
|
-
*/
|
|
261
|
-
const bookmarksViewPlugin = ViewPlugin.fromClass(
|
|
262
|
-
class BookmarkViewPlugin {
|
|
263
|
-
// Match markdown footnotes (option).
|
|
264
|
-
// https://www.markdownguide.org/extended-syntax/#footnotes
|
|
265
|
-
static bookmarkMatcher = new MatchDecorator({
|
|
266
|
-
regexp: /\[\^(\w+)\]/g,
|
|
267
|
-
decoration: (match, view, pos) => {
|
|
268
|
-
const id = match[1];
|
|
269
|
-
return Decoration.replace({
|
|
270
|
-
id,
|
|
271
|
-
widget: new BookmarkWidget(pos, id),
|
|
272
|
-
});
|
|
273
|
-
},
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
bookmarks: DecorationSet;
|
|
277
|
-
constructor(view: EditorView) {
|
|
278
|
-
this.bookmarks = BookmarkViewPlugin.bookmarkMatcher.createDeco(view);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
update(update: ViewUpdate) {
|
|
282
|
-
this.bookmarks = BookmarkViewPlugin.bookmarkMatcher.updateDeco(update, this.bookmarks);
|
|
283
|
-
}
|
|
284
|
-
},
|
|
285
|
-
{
|
|
286
|
-
decorations: (instance) => instance.bookmarks,
|
|
287
|
-
provide: (plugin) =>
|
|
288
|
-
EditorView.atomicRanges.of((view) => {
|
|
289
|
-
return view.plugin(plugin)?.bookmarks || Decoration.none;
|
|
290
|
-
}),
|
|
291
|
-
},
|
|
292
|
-
);
|
|
293
|
-
|
|
294
210
|
return [
|
|
295
|
-
bookmarksViewPlugin,
|
|
296
211
|
commentsStateField,
|
|
297
212
|
highlightDecorations,
|
|
298
213
|
styles,
|
|
@@ -303,7 +218,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
303
218
|
options.onCreate
|
|
304
219
|
? keymap.of([
|
|
305
220
|
{
|
|
306
|
-
key:
|
|
221
|
+
key: shortcut,
|
|
307
222
|
run: callbackWrapper(createCommentThread),
|
|
308
223
|
},
|
|
309
224
|
])
|
|
@@ -325,7 +240,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
325
240
|
create: () => {
|
|
326
241
|
// TODO(burdon): Dispatch to react callback to render (or use SSR)?
|
|
327
242
|
const el = document.createElement('div');
|
|
328
|
-
options.onHover?.(el);
|
|
243
|
+
options.onHover?.(el, shortcut);
|
|
329
244
|
return { dom: el, offset: { x: 0, y: 8 } };
|
|
330
245
|
},
|
|
331
246
|
};
|
|
@@ -342,7 +257,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
342
257
|
// TODO(burdon): Is there a better (finer grained) way to do this?
|
|
343
258
|
//
|
|
344
259
|
EditorView.updateListener.of(({ view, state, changes }) => {
|
|
345
|
-
const cursorConverter = view.state.facet(
|
|
260
|
+
const cursorConverter = view.state.facet(Cursor.converter);
|
|
346
261
|
|
|
347
262
|
//
|
|
348
263
|
// Test if need to recompute indexed range if document changes before the end of the range.
|
|
@@ -353,7 +268,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
353
268
|
changes.iterChanges((from, to, from2, to2) => {
|
|
354
269
|
ranges.forEach((range) => {
|
|
355
270
|
if (from2 === to2) {
|
|
356
|
-
const newRange = getRangeFromCursor(cursorConverter, range.cursor);
|
|
271
|
+
const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
|
|
357
272
|
if (!newRange || newRange.to - newRange.from === 0) {
|
|
358
273
|
// TODO(burdon): Delete range if empty.
|
|
359
274
|
log.info('deleted comment', { thread: range.id });
|
|
@@ -361,7 +276,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
361
276
|
}
|
|
362
277
|
|
|
363
278
|
if (from <= range.to) {
|
|
364
|
-
const newRange = getRangeFromCursor(cursorConverter, range.cursor);
|
|
279
|
+
const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
|
|
365
280
|
Object.assign(range, newRange);
|
|
366
281
|
mod = true;
|
|
367
282
|
}
|
|
@@ -393,6 +308,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
393
308
|
}
|
|
394
309
|
});
|
|
395
310
|
|
|
311
|
+
// TODO(burdon): Fire debounced callback if range selected (to show hint).
|
|
312
|
+
|
|
396
313
|
if (selected.active !== active || selected.closest !== closest) {
|
|
397
314
|
view.dispatch({ effects: setSelection.of(selected) });
|
|
398
315
|
|
|
@@ -406,16 +323,3 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
406
323
|
}),
|
|
407
324
|
];
|
|
408
325
|
};
|
|
409
|
-
|
|
410
|
-
const getCursorFromRange = (cursorConverter: CursorConverter, range: Range) => {
|
|
411
|
-
const from = cursorConverter.toCursor(range.from);
|
|
412
|
-
const to = cursorConverter.toCursor(range.to, -1);
|
|
413
|
-
return [from, to].join(':');
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
const getRangeFromCursor = (cursorConverter: CursorConverter, cursor: string) => {
|
|
417
|
-
const parts = cursor.split(':');
|
|
418
|
-
const from = cursorConverter.fromCursor(parts[0]);
|
|
419
|
-
const to = cursorConverter.fromCursor(parts[1]);
|
|
420
|
-
return from && to ? { from, to } : undefined;
|
|
421
|
-
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Facet } from '@codemirror/state';
|
|
6
|
+
|
|
7
|
+
import type { Range } from '../hooks';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts indexes into the text document into stable peer-independent cursors.
|
|
11
|
+
*
|
|
12
|
+
* See:
|
|
13
|
+
* - https://automerge.org/automerge/api-docs/js/functions/next.getCursor.html
|
|
14
|
+
* - https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
|
|
15
|
+
*
|
|
16
|
+
* @param {assoc} number Negative values will associate the cursor with the previous character
|
|
17
|
+
* while positive - with the next one.
|
|
18
|
+
*/
|
|
19
|
+
export interface CursorConverter {
|
|
20
|
+
toCursor(position: number, assoc?: -1 | 1 | undefined): string;
|
|
21
|
+
fromCursor(cursor: string): number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const defaultCursorConverter: CursorConverter = {
|
|
25
|
+
toCursor: (position) => position.toString(),
|
|
26
|
+
fromCursor: (cursor) => parseInt(cursor),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export class Cursor {
|
|
30
|
+
static readonly converter = Facet.define<CursorConverter, CursorConverter>({
|
|
31
|
+
combine: (providers) => providers[0] ?? defaultCursorConverter,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
static readonly getCursorFromRange = (cursorConverter: CursorConverter, range: Range) => {
|
|
35
|
+
const from = cursorConverter.toCursor(range.from);
|
|
36
|
+
const to = cursorConverter.toCursor(range.to, -1);
|
|
37
|
+
return [from, to].join(':');
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
static readonly getRangeFromCursor = (cursorConverter: CursorConverter, cursor: string) => {
|
|
41
|
+
const parts = cursor.split(':');
|
|
42
|
+
const from = cursorConverter.fromCursor(parts[0]);
|
|
43
|
+
const to = cursorConverter.fromCursor(parts[1]);
|
|
44
|
+
return from !== undefined && to !== undefined ? { from, to } : undefined;
|
|
45
|
+
};
|
|
46
|
+
}
|
package/src/extensions/hr.ts
CHANGED
|
@@ -11,16 +11,15 @@ import {
|
|
|
11
11
|
type ViewUpdate,
|
|
12
12
|
WidgetType,
|
|
13
13
|
} from '@codemirror/view';
|
|
14
|
-
import get from 'lodash.get';
|
|
15
14
|
|
|
16
|
-
import {
|
|
15
|
+
import { getToken } from '../styles';
|
|
17
16
|
|
|
18
17
|
// TODO(burdon): Reconcile with theme.
|
|
19
18
|
const styles = EditorView.baseTheme({
|
|
20
19
|
'& .cm-hr': {
|
|
21
20
|
// TODO(burdon): ???
|
|
22
21
|
// Note that block-level decorations should not have vertical margins,
|
|
23
|
-
borderBottom: `1px solid ${
|
|
22
|
+
borderBottom: `1px solid ${getToken('extend.colors.neutral.200')}`,
|
|
24
23
|
},
|
|
25
24
|
});
|
|
26
25
|
|
|
@@ -29,11 +28,11 @@ class HorizontalRuleWidget extends WidgetType {
|
|
|
29
28
|
super();
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
override eq(other:
|
|
31
|
+
override eq(other: this) {
|
|
33
32
|
return this._pos === (other as any as HorizontalRuleWidget)._pos;
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
toDOM(view: EditorView) {
|
|
35
|
+
override toDOM(view: EditorView) {
|
|
37
36
|
// TODO(burdon): Create <hr> element? Does this clash with markdown parser?
|
|
38
37
|
const el = document.createElement('div');
|
|
39
38
|
el.className = 'cm-hr';
|
package/src/extensions/image.ts
CHANGED
|
@@ -3,28 +3,45 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { syntaxTree } from '@codemirror/language';
|
|
6
|
-
import {
|
|
7
|
-
|
|
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';
|
|
6
|
+
import { type EditorState, type Extension, StateField, type Transaction, type Range } from '@codemirror/state';
|
|
7
|
+
import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
|
|
15
8
|
|
|
16
9
|
export type ImageOptions = {};
|
|
17
10
|
|
|
18
11
|
export const image = (options: ImageOptions = {}): Extension => {
|
|
19
|
-
return StateField.define<
|
|
20
|
-
create: (state) =>
|
|
21
|
-
|
|
12
|
+
return StateField.define<DecorationSet>({
|
|
13
|
+
create: (state) => {
|
|
14
|
+
return Decoration.set(buildDecorations(0, state.doc.length, state));
|
|
15
|
+
},
|
|
16
|
+
update: (value: DecorationSet, tr: Transaction) => {
|
|
17
|
+
if (!tr.docChanged && !tr.selection) {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
// Find range of changes and cursor changes.
|
|
21
|
+
const cursor = tr.state.selection.main.head;
|
|
22
|
+
const oldCursor = tr.changes.mapPos(tr.startState.selection.main.head);
|
|
23
|
+
let from = Math.min(cursor, oldCursor);
|
|
24
|
+
let to = Math.max(cursor, oldCursor);
|
|
25
|
+
tr.changes.iterChangedRanges((fromA, toA, fromB, toB) => {
|
|
26
|
+
from = Math.min(from, fromB);
|
|
27
|
+
to = Math.max(to, toB);
|
|
28
|
+
});
|
|
29
|
+
// Expand to cover lines
|
|
30
|
+
from = tr.state.doc.lineAt(from).from;
|
|
31
|
+
to = tr.state.doc.lineAt(to).to;
|
|
32
|
+
return value.map(tr.changes).update({
|
|
33
|
+
filterFrom: from,
|
|
34
|
+
filterTo: to,
|
|
35
|
+
filter: () => false,
|
|
36
|
+
add: buildDecorations(from, to, tr.state),
|
|
37
|
+
});
|
|
38
|
+
},
|
|
22
39
|
provide: (field) => EditorView.decorations.from(field),
|
|
23
40
|
});
|
|
24
41
|
};
|
|
25
42
|
|
|
26
|
-
const
|
|
27
|
-
const
|
|
43
|
+
const buildDecorations = (from: number, to: number, state: EditorState) => {
|
|
44
|
+
const decorations: Range<Decoration>[] = [];
|
|
28
45
|
const cursor = state.selection.main.head;
|
|
29
46
|
|
|
30
47
|
syntaxTree(state).iterate({
|
|
@@ -34,20 +51,20 @@ const update = (state: EditorState, options: ImageOptions) => {
|
|
|
34
51
|
if (urlNode) {
|
|
35
52
|
const hide = state.readOnly || cursor < node.from || cursor > node.to;
|
|
36
53
|
const url = state.sliceDoc(urlNode.from, urlNode.to);
|
|
37
|
-
|
|
38
|
-
hide ? node.from : node.to,
|
|
39
|
-
node.to,
|
|
54
|
+
decorations.push(
|
|
40
55
|
Decoration.replace({
|
|
41
56
|
block: true, // Prevent cursor from entering.
|
|
42
57
|
widget: new ImageWidget(url),
|
|
43
|
-
}),
|
|
58
|
+
}).range(hide ? node.from : node.to, node.to),
|
|
44
59
|
);
|
|
45
60
|
}
|
|
46
61
|
}
|
|
47
62
|
},
|
|
63
|
+
from,
|
|
64
|
+
to,
|
|
48
65
|
});
|
|
49
66
|
|
|
50
|
-
return
|
|
67
|
+
return decorations;
|
|
51
68
|
};
|
|
52
69
|
|
|
53
70
|
class ImageWidget extends WidgetType {
|
|
@@ -55,15 +72,16 @@ class ImageWidget extends WidgetType {
|
|
|
55
72
|
super();
|
|
56
73
|
}
|
|
57
74
|
|
|
58
|
-
|
|
59
|
-
override eq(other: WidgetType) {
|
|
75
|
+
override eq(other: this) {
|
|
60
76
|
return this._url === (other as any as ImageWidget)._url;
|
|
61
77
|
}
|
|
62
78
|
|
|
63
|
-
toDOM(view: EditorView) {
|
|
79
|
+
override toDOM(view: EditorView) {
|
|
64
80
|
const img = document.createElement('img');
|
|
65
81
|
img.setAttribute('src', this._url);
|
|
66
82
|
img.setAttribute('class', 'cm-image');
|
|
83
|
+
// Images are hidden until successfully loaded to avoid flickering effects.
|
|
84
|
+
img.onload = () => img.classList.add('cm-loaded-image');
|
|
67
85
|
return img;
|
|
68
86
|
}
|
|
69
87
|
}
|
package/src/extensions/index.ts
CHANGED
|
@@ -9,13 +9,13 @@ export * from './basic';
|
|
|
9
9
|
export * from './blast';
|
|
10
10
|
export * from './code';
|
|
11
11
|
export * from './comments';
|
|
12
|
+
export * from './cursor';
|
|
12
13
|
export * from './hr';
|
|
13
14
|
export * from './image';
|
|
14
15
|
export * from './link';
|
|
15
16
|
export * from './listener';
|
|
16
17
|
export * from './markdown';
|
|
17
18
|
export * from './mention';
|
|
18
|
-
export * from './mermaid';
|
|
19
19
|
export * from './table';
|
|
20
20
|
export * from './tasklist';
|
|
21
21
|
export * from './typewriter';
|