@dxos/ui-editor 0.8.4-main.fcfe5033a5 → 0.9.0
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/LICENSE +102 -5
- package/README.md +1 -1
- package/dist/lib/browser/index.mjs +1258 -1004
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/types/index.mjs +26 -6
- package/dist/lib/browser/types/index.mjs.map +4 -4
- package/dist/lib/node-esm/index.mjs +1258 -1003
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/types/index.mjs +27 -6
- package/dist/lib/node-esm/types/index.mjs.map +4 -4
- package/dist/types/src/defaults.d.ts.map +1 -1
- package/dist/types/src/extensions/annotations.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete/match.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete/placeholder.d.ts +5 -2
- package/dist/types/src/extensions/autocomplete/placeholder.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete/typeahead.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/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.map +1 -1
- package/dist/types/src/extensions/automerge/sync.d.ts +1 -1
- package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-codemirror.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/blast.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +19 -1
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/debug.d.ts.map +1 -1
- package/dist/types/src/extensions/dnd.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +3 -2
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.test.d.ts +2 -0
- package/dist/types/src/extensions/factories.test.d.ts.map +1 -0
- package/dist/types/src/extensions/focus.d.ts +1 -1
- package/dist/types/src/extensions/index.d.ts +3 -4
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/json.d.ts +1 -1
- package/dist/types/src/extensions/json.d.ts.map +1 -1
- package/dist/types/src/extensions/listener.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/changes.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/debug.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.map +1 -1
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/image.d.ts +13 -2
- package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/image.test.d.ts +2 -0
- package/dist/types/src/extensions/markdown/image.test.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/table.d.ts.map +1 -1
- package/dist/types/src/extensions/mention.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner/menu.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner/outliner.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner/selection.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner/tree.d.ts.map +1 -1
- package/dist/types/src/extensions/preview/preview.d.ts +2 -2
- package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
- package/dist/types/src/extensions/replacer.d.ts.map +1 -1
- package/dist/types/src/extensions/scrolling/auto-scroll.d.ts +18 -0
- package/dist/types/src/extensions/scrolling/auto-scroll.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling/crawler.d.ts +83 -0
- package/dist/types/src/extensions/scrolling/crawler.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling/index.d.ts +6 -0
- package/dist/types/src/extensions/scrolling/index.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling/scroll-past-end.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling/scrollbar-autohide.d.ts +15 -0
- package/dist/types/src/extensions/scrolling/scrollbar-autohide.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling/scroller.d.ts +16 -0
- package/dist/types/src/extensions/scrolling/scroller.d.ts.map +1 -0
- package/dist/types/src/extensions/selection.d.ts.map +1 -1
- package/dist/types/src/extensions/snippets.d.ts +10 -0
- package/dist/types/src/extensions/snippets.d.ts.map +1 -0
- package/dist/types/src/extensions/spacing.d.ts +3 -0
- package/dist/types/src/extensions/spacing.d.ts.map +1 -0
- package/dist/types/src/extensions/submit.d.ts.map +1 -1
- package/dist/types/src/extensions/tags/extended-markdown.d.ts.map +1 -1
- package/dist/types/src/extensions/tags/fader.d.ts.map +1 -1
- package/dist/types/src/extensions/tags/index.d.ts +3 -1
- package/dist/types/src/extensions/tags/index.d.ts.map +1 -1
- package/dist/types/src/extensions/tags/typewriter.d.ts +43 -0
- package/dist/types/src/extensions/tags/typewriter.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/typewriter.test.d.ts +2 -0
- package/dist/types/src/extensions/tags/typewriter.test.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/xml-block-decoration.d.ts +31 -0
- package/dist/types/src/extensions/tags/xml-block-decoration.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/xml-formatting.d.ts +24 -0
- package/dist/types/src/extensions/tags/xml-formatting.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/xml-tags.d.ts +1 -8
- package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -1
- package/dist/types/src/extensions/tags/xml-util.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/theme.d.ts.map +1 -1
- package/dist/types/src/types/types.d.ts +2 -2
- package/dist/types/src/types/types.d.ts.map +1 -1
- package/dist/types/src/util/cursor.d.ts.map +1 -1
- package/dist/types/src/util/debug.d.ts.map +1 -1
- package/dist/types/src/util/decorations.d.ts.map +1 -1
- package/dist/types/src/util/dom.d.ts.map +1 -1
- package/dist/types/src/util/facet.d.ts.map +1 -1
- package/dist/types/src/util/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +55 -57
- package/src/defaults.ts +6 -4
- package/src/extensions/autocomplete/placeholder.ts +37 -18
- package/src/extensions/automerge/automerge.test.tsx +35 -9
- package/src/extensions/automerge/automerge.ts +1 -1
- package/src/extensions/automerge/cursor.ts +1 -1
- package/src/extensions/automerge/sync.ts +1 -1
- package/src/extensions/automerge/update-automerge.ts +1 -1
- package/src/extensions/comments.ts +54 -31
- package/src/extensions/factories.test.ts +88 -0
- package/src/extensions/factories.ts +22 -4
- package/src/extensions/index.ts +3 -4
- package/src/extensions/json.ts +1 -1
- package/src/extensions/markdown/decorate.ts +1 -1
- package/src/extensions/markdown/image.test.ts +54 -0
- package/src/extensions/markdown/image.ts +70 -9
- package/src/extensions/markdown/link.ts +7 -2
- package/src/extensions/outliner/outliner.ts +1 -1
- package/src/extensions/preview/preview.ts +14 -12
- package/src/extensions/scrolling/auto-scroll.ts +261 -0
- package/src/extensions/{scroller.ts → scrolling/crawler.ts} +89 -48
- package/src/extensions/scrolling/index.ts +9 -0
- package/src/extensions/{scroll-past-end.ts → scrolling/scroll-past-end.ts} +6 -6
- package/src/extensions/scrolling/scrollbar-autohide.ts +61 -0
- package/src/extensions/scrolling/scroller.ts +27 -0
- package/src/extensions/snippets.ts +67 -0
- package/src/extensions/spacing.ts +15 -0
- package/src/extensions/tags/index.ts +3 -1
- package/src/extensions/tags/testing/text.md +36 -0
- package/src/extensions/tags/testing/text.txt +35 -0
- package/src/extensions/tags/{wire.test.ts → typewriter.test.ts} +2 -2
- package/src/extensions/tags/typewriter.ts +594 -0
- package/src/extensions/tags/xml-block-decoration.ts +123 -0
- package/src/extensions/tags/xml-formatting.ts +125 -0
- package/src/extensions/tags/xml-tags.ts +6 -32
- package/src/extensions/tags/xml-util.test.ts +90 -3
- package/src/extensions/tags/xml-util.ts +62 -5
- package/src/index.ts +0 -1
- package/src/styles/theme.ts +23 -13
- package/src/typings.d.ts +8 -0
- package/dist/lib/browser/chunk-D724USEC.mjs +0 -34
- package/dist/lib/browser/chunk-D724USEC.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-JRVJWKQF.mjs +0 -36
- package/dist/lib/node-esm/chunk-JRVJWKQF.mjs.map +0 -7
- package/dist/types/src/extensions/auto-scroll.d.ts +0 -8
- package/dist/types/src/extensions/auto-scroll.d.ts.map +0 -1
- package/dist/types/src/extensions/scroll-past-end.d.ts.map +0 -1
- package/dist/types/src/extensions/scroller.d.ts +0 -63
- package/dist/types/src/extensions/scroller.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/wire.d.ts +0 -23
- package/dist/types/src/extensions/tags/wire.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/wire.test.d.ts +0 -2
- package/dist/types/src/extensions/tags/wire.test.d.ts.map +0 -1
- package/dist/types/src/extensions/typewriter.d.ts +0 -10
- package/dist/types/src/extensions/typewriter.d.ts.map +0 -1
- package/src/extensions/auto-scroll.ts +0 -179
- package/src/extensions/tags/wire.ts +0 -459
- package/src/extensions/typewriter.ts +0 -68
- /package/dist/types/src/extensions/{scroll-past-end.d.ts → scrolling/scroll-past-end.d.ts} +0 -0
|
@@ -22,7 +22,7 @@ import { vscodeDarkStyle, vscodeLightStyle } from '@uiw/codemirror-theme-vscode'
|
|
|
22
22
|
import defaultsDeep from 'lodash.defaultsdeep';
|
|
23
23
|
|
|
24
24
|
import { generateName } from '@dxos/display-name';
|
|
25
|
-
import { type DocAccessor } from '@dxos/echo-
|
|
25
|
+
import { type DocAccessor } from '@dxos/echo-client';
|
|
26
26
|
import { log } from '@dxos/log';
|
|
27
27
|
import { type Messenger } from '@dxos/protocols';
|
|
28
28
|
import { type Identity } from '@dxos/protocols/proto/dxos/client/services';
|
|
@@ -33,7 +33,7 @@ import { baseTheme, createFontTheme, editorGutter } from '../styles';
|
|
|
33
33
|
import { automerge } from './automerge';
|
|
34
34
|
import { SpaceAwarenessProvider, awareness } from './awareness';
|
|
35
35
|
import { focus } from './focus';
|
|
36
|
-
import { scrollPastEnd } from './
|
|
36
|
+
import { scrollPastEnd } from './scrolling';
|
|
37
37
|
|
|
38
38
|
//
|
|
39
39
|
// Basic
|
|
@@ -143,6 +143,19 @@ export const createBasicExtensions = (propsProp?: BasicExtensionsOptions): Exten
|
|
|
143
143
|
props.lineWrapping && EditorView.lineWrapping,
|
|
144
144
|
props.placeholder && placeholder(props.placeholder),
|
|
145
145
|
props.readOnly !== undefined && EditorState.readOnly.of(props.readOnly),
|
|
146
|
+
// `EditorState.readOnly` is advisory — CodeMirror doesn't auto-reject doc-changing
|
|
147
|
+
// transactions. Some extensions (e.g. `@codemirror/lang-markdown`'s Enter handler that
|
|
148
|
+
// continues a list) dispatch programmatic edits regardless. Drop user-initiated edits
|
|
149
|
+
// (`input` / `delete` keymap dispatches plus `undo` / `redo` from the history extension)
|
|
150
|
+
// but pass programmatic dispatches — streaming `MarkdownStream` and similar consumers
|
|
151
|
+
// depend on being able to populate the doc themselves.
|
|
152
|
+
props.readOnly &&
|
|
153
|
+
EditorState.transactionFilter.of((tr) =>
|
|
154
|
+
tr.docChanged &&
|
|
155
|
+
(tr.isUserEvent('input') || tr.isUserEvent('delete') || tr.isUserEvent('undo') || tr.isUserEvent('redo'))
|
|
156
|
+
? []
|
|
157
|
+
: tr,
|
|
158
|
+
),
|
|
146
159
|
props.scrollPastEnd && scrollPastEnd(),
|
|
147
160
|
props.tabbable && tabbable,
|
|
148
161
|
props.tabSize && EditorState.tabSize.of(props.tabSize),
|
|
@@ -179,6 +192,7 @@ export const createBasicExtensions = (propsProp?: BasicExtensionsOptions): Exten
|
|
|
179
192
|
export type ThemeExtensionsOptions = {
|
|
180
193
|
monospace?: boolean;
|
|
181
194
|
themeMode?: ThemeMode;
|
|
195
|
+
scrollbarThin?: boolean;
|
|
182
196
|
slots?: {
|
|
183
197
|
editor?: {
|
|
184
198
|
className?: string;
|
|
@@ -217,9 +231,10 @@ export const defaultStyles = {
|
|
|
217
231
|
*/
|
|
218
232
|
export const createThemeExtensions = ({
|
|
219
233
|
monospace,
|
|
220
|
-
|
|
234
|
+
scrollbarThin,
|
|
221
235
|
slots: slotsProp,
|
|
222
236
|
syntaxHighlighting: syntaxHighlightingProp,
|
|
237
|
+
themeMode,
|
|
223
238
|
}: ThemeExtensionsOptions = {}): Extension => {
|
|
224
239
|
const slots: NonNullable<ThemeExtensionsOptions['slots']> = defaultsDeep({}, slotsProp, defaultThemeSlots);
|
|
225
240
|
return [
|
|
@@ -230,13 +245,16 @@ export const createThemeExtensions = ({
|
|
|
230
245
|
syntaxHighlighting(HighlightStyle.define(themeMode === 'dark' ? defaultStyles.dark : defaultStyles.light)),
|
|
231
246
|
slots.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
|
|
232
247
|
slots.content?.className && EditorView.contentAttributes.of({ class: slots.content.className }),
|
|
233
|
-
slots.scroller?.className &&
|
|
248
|
+
(slots.scroller?.className || scrollbarThin) &&
|
|
234
249
|
ViewPlugin.fromClass(
|
|
235
250
|
class {
|
|
236
251
|
constructor(view: EditorView) {
|
|
237
252
|
if (slots.scroller?.className) {
|
|
238
253
|
view.scrollDOM.classList.add(...slots.scroller.className.split(/\s+/));
|
|
239
254
|
}
|
|
255
|
+
if (scrollbarThin) {
|
|
256
|
+
view.scrollDOM.style.setProperty('--scrollbar-size', '4px');
|
|
257
|
+
}
|
|
240
258
|
}
|
|
241
259
|
},
|
|
242
260
|
),
|
package/src/extensions/index.ts
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
export * from './annotations';
|
|
6
6
|
export * from './autocomplete';
|
|
7
|
-
export * from './auto-scroll';
|
|
8
7
|
export * from './automerge';
|
|
9
8
|
export * from './awareness';
|
|
10
9
|
export * from './blast';
|
|
@@ -26,10 +25,10 @@ export * from './modes';
|
|
|
26
25
|
export * from './outliner';
|
|
27
26
|
export * from './preview';
|
|
28
27
|
export * from './replacer';
|
|
29
|
-
export * from './
|
|
30
|
-
export * from './scroller';
|
|
28
|
+
export * from './scrolling';
|
|
31
29
|
export * from './selection';
|
|
30
|
+
export * from './spacing';
|
|
31
|
+
export * from './snippets';
|
|
32
32
|
export * from './state';
|
|
33
33
|
export * from './submit';
|
|
34
34
|
export * from './tags';
|
|
35
|
-
export * from './typewriter';
|
package/src/extensions/json.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { type LintSource, linter } from '@codemirror/lint';
|
|
|
7
7
|
import { type Extension } from '@codemirror/state';
|
|
8
8
|
import Ajv, { type ValidateFunction } from 'ajv';
|
|
9
9
|
|
|
10
|
-
import { type JsonSchemaType } from '@dxos/echo/
|
|
10
|
+
import { type JsonSchema as JsonSchemaType } from '@dxos/echo/JsonSchema';
|
|
11
11
|
|
|
12
12
|
export type JsonExtensionsOptions = {
|
|
13
13
|
schema?: JsonSchemaType;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
|
6
|
+
import { EditorState } from '@codemirror/state';
|
|
7
|
+
import { EditorView } from '@codemirror/view';
|
|
8
|
+
import { describe, test } from 'vitest';
|
|
9
|
+
|
|
10
|
+
import { focus } from '../focus';
|
|
11
|
+
import { image } from './image';
|
|
12
|
+
|
|
13
|
+
const createView = (doc: string, extensions: any[]) => {
|
|
14
|
+
const parent = document.createElement('div');
|
|
15
|
+
return new EditorView({
|
|
16
|
+
state: EditorState.create({
|
|
17
|
+
doc,
|
|
18
|
+
extensions: [markdown({ base: markdownLanguage }), focus, ...extensions],
|
|
19
|
+
}),
|
|
20
|
+
parent,
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const countImageElements = (view: EditorView): number => view.dom.querySelectorAll('img.cm-image').length;
|
|
25
|
+
|
|
26
|
+
describe('image extension', () => {
|
|
27
|
+
test('renders <img> for an http image link by default', ({ expect }) => {
|
|
28
|
+
const view = createView('', [image(), EditorView.editable.of(false)]);
|
|
29
|
+
expect(countImageElements(view)).toBeGreaterThan(0);
|
|
30
|
+
view.destroy();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('honors skip callback to suppress remote image rendering', ({ expect }) => {
|
|
34
|
+
const skip = ({ url }: { name: 'Image'; url: string }) => /^https?:\/\//.test(url);
|
|
35
|
+
const view = createView('', [image({ skip }), EditorView.editable.of(false)]);
|
|
36
|
+
expect(countImageElements(view)).toBe(0);
|
|
37
|
+
view.destroy();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('skip can be selective: blocks http(s) while still rendering file: URLs', ({ expect }) => {
|
|
41
|
+
const skip = ({ url }: { name: 'Image'; url: string }) => /^https?:\/\//.test(url);
|
|
42
|
+
|
|
43
|
+
const blockedView = createView('', [
|
|
44
|
+
image({ skip }),
|
|
45
|
+
EditorView.editable.of(false),
|
|
46
|
+
]);
|
|
47
|
+
expect(countImageElements(blockedView)).toBe(0);
|
|
48
|
+
blockedView.destroy();
|
|
49
|
+
|
|
50
|
+
const allowedView = createView('', [image({ skip }), EditorView.editable.of(false)]);
|
|
51
|
+
expect(countImageElements(allowedView)).toBeGreaterThan(0);
|
|
52
|
+
allowedView.destroy();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -3,23 +3,48 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { syntaxTree } from '@codemirror/language';
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
type EditorState,
|
|
8
|
+
type Extension,
|
|
9
|
+
type Range,
|
|
10
|
+
StateEffect,
|
|
11
|
+
StateField,
|
|
12
|
+
type Transaction,
|
|
13
|
+
} from '@codemirror/state';
|
|
14
|
+
import { Decoration, type DecorationSet, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
|
|
8
15
|
|
|
9
16
|
import { focusField } from '../focus';
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
// Effect dispatched when the viewport extends (e.g., scrolling reveals new content). The state
|
|
19
|
+
// field listens for this and rebuilds decorations across the full doc so images outside the
|
|
20
|
+
// initial parse range get widgetized.
|
|
21
|
+
const rebuildEffect = StateEffect.define<void>();
|
|
22
|
+
|
|
23
|
+
export type ImageNodeData = { name: 'Image'; url: string };
|
|
24
|
+
|
|
25
|
+
export type ImageOptions = {
|
|
26
|
+
/**
|
|
27
|
+
* Predicate that returns true to suppress rendering of an image node.
|
|
28
|
+
* When skipped, the markdown link source is left visible to the user instead of being
|
|
29
|
+
* replaced by an `<img>` widget.
|
|
30
|
+
*/
|
|
31
|
+
skip?: (node: ImageNodeData) => boolean;
|
|
32
|
+
};
|
|
12
33
|
|
|
13
34
|
/**
|
|
14
35
|
* Create image decorations.
|
|
15
36
|
*/
|
|
16
|
-
export const image = (
|
|
37
|
+
export const image = (options: ImageOptions = {}): Extension => {
|
|
17
38
|
return [
|
|
18
39
|
StateField.define<DecorationSet>({
|
|
19
40
|
create: (state) => {
|
|
20
|
-
return Decoration.set(buildDecorations(state, 0, state.doc.length));
|
|
41
|
+
return Decoration.set(buildDecorations(state, 0, state.doc.length, options));
|
|
21
42
|
},
|
|
22
43
|
update: (value: DecorationSet, tr: Transaction) => {
|
|
44
|
+
// Full rebuild when the viewport extended (lazy parse now covers more of the doc).
|
|
45
|
+
if (tr.effects.some((effect) => effect.is(rebuildEffect))) {
|
|
46
|
+
return Decoration.set(buildDecorations(tr.state, 0, tr.state.doc.length, options));
|
|
47
|
+
}
|
|
23
48
|
if (!tr.docChanged && !tr.selection) {
|
|
24
49
|
return value;
|
|
25
50
|
}
|
|
@@ -42,15 +67,25 @@ export const image = (_options: ImageOptions = {}): Extension => {
|
|
|
42
67
|
filterFrom: from,
|
|
43
68
|
filterTo: to,
|
|
44
69
|
filter: () => false,
|
|
45
|
-
add: buildDecorations(tr.state, from, to),
|
|
70
|
+
add: buildDecorations(tr.state, from, to, options),
|
|
46
71
|
});
|
|
47
72
|
},
|
|
48
73
|
provide: (field) => EditorView.decorations.from(field),
|
|
49
74
|
}),
|
|
75
|
+
// Block-replace decorations have to live in a state field, but viewport changes are only
|
|
76
|
+
// observable from a view plugin. Bridge the two by dispatching a rebuild effect whenever
|
|
77
|
+
// the viewport extends so newly-parsed image nodes get widgetized without requiring focus.
|
|
78
|
+
ViewPlugin.define((view) => ({
|
|
79
|
+
update: (update) => {
|
|
80
|
+
if (update.viewportChanged) {
|
|
81
|
+
queueMicrotask(() => view.dispatch({ effects: rebuildEffect.of(undefined) }));
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
})),
|
|
50
85
|
];
|
|
51
86
|
};
|
|
52
87
|
|
|
53
|
-
const buildDecorations = (state: EditorState, from: number, to: number) => {
|
|
88
|
+
const buildDecorations = (state: EditorState, from: number, to: number, options: ImageOptions = {}) => {
|
|
54
89
|
const decorations: Range<Decoration>[] = [];
|
|
55
90
|
const cursor = state.selection.main.head;
|
|
56
91
|
syntaxTree(state).iterate({
|
|
@@ -66,6 +101,11 @@ const buildDecorations = (state: EditorState, from: number, to: number) => {
|
|
|
66
101
|
return;
|
|
67
102
|
}
|
|
68
103
|
|
|
104
|
+
// Consumer-supplied filter (e.g., disable remote-image rendering by setting).
|
|
105
|
+
if (options.skip?.({ name: 'Image', url })) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
69
109
|
preloadImage(url);
|
|
70
110
|
decorations.push(
|
|
71
111
|
Decoration.replace({
|
|
@@ -106,13 +146,34 @@ class ImageWidget extends WidgetType {
|
|
|
106
146
|
const img = document.createElement('img');
|
|
107
147
|
img.setAttribute('src', this._url);
|
|
108
148
|
img.setAttribute('class', 'cm-image');
|
|
149
|
+
const focused = view.state.field(focusField);
|
|
109
150
|
// If focused, hide image until successfully loaded to avoid flickering effects.
|
|
110
|
-
if (
|
|
111
|
-
img.onload = () =>
|
|
151
|
+
if (focused) {
|
|
152
|
+
img.onload = () => {
|
|
153
|
+
img.classList.add('cm-loaded-image');
|
|
154
|
+
collapseIfTrackingPixel(img);
|
|
155
|
+
};
|
|
112
156
|
} else {
|
|
113
157
|
img.classList.add('cm-loaded-image');
|
|
158
|
+
img.onload = () => collapseIfTrackingPixel(img);
|
|
114
159
|
}
|
|
160
|
+
// Error (e.g., blocked tracker URL): also collapse so we don't leave a hole.
|
|
161
|
+
img.onerror = () => collapseLine(img);
|
|
115
162
|
|
|
116
163
|
return img;
|
|
117
164
|
}
|
|
118
165
|
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Tracking pixels are commonly 1×1 (or 0×0) transparent images embedded by mail senders.
|
|
169
|
+
* They add no visual value, so hide them once their natural dimensions are known.
|
|
170
|
+
*/
|
|
171
|
+
const collapseIfTrackingPixel = (img: HTMLImageElement) => {
|
|
172
|
+
if (img.naturalWidth <= 1 && img.naturalHeight <= 1) {
|
|
173
|
+
collapseLine(img);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const collapseLine = (img: HTMLImageElement) => {
|
|
178
|
+
img.style.display = 'none';
|
|
179
|
+
};
|
|
@@ -7,10 +7,15 @@ import { syntaxTree } from '@codemirror/language';
|
|
|
7
7
|
import { hoverTooltip } from '@codemirror/view';
|
|
8
8
|
import { type SyntaxNode } from '@lezer/common';
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { mx, surfaceShadow } from '@dxos/ui-theme';
|
|
11
11
|
|
|
12
12
|
import { type RenderCallback } from '../../types';
|
|
13
13
|
|
|
14
|
+
const tooltipClassName = mx(
|
|
15
|
+
'inline-flex items-center p-1 max-w-64 text-sm bg-inverse-surface text-inverse-fg rounded-sm',
|
|
16
|
+
surfaceShadow({ elevation: 'positioned' }),
|
|
17
|
+
);
|
|
18
|
+
|
|
14
19
|
export const linkTooltip = (renderTooltip: RenderCallback<{ url: string }>) => {
|
|
15
20
|
return hoverTooltip((view, pos, side) => {
|
|
16
21
|
const syntax = syntaxTree(view.state).resolveInner(pos, side);
|
|
@@ -35,7 +40,7 @@ export const linkTooltip = (renderTooltip: RenderCallback<{ url: string }>) => {
|
|
|
35
40
|
above: true,
|
|
36
41
|
create: () => {
|
|
37
42
|
const el = document.createElement('div');
|
|
38
|
-
el.className =
|
|
43
|
+
el.className = tooltipClassName;
|
|
39
44
|
renderTooltip(el, { url: urlText }, view);
|
|
40
45
|
return { dom: el, offset: { x: 0, y: 4 } };
|
|
41
46
|
},
|
|
@@ -156,7 +156,7 @@ const decorations = () => [
|
|
|
156
156
|
},
|
|
157
157
|
|
|
158
158
|
'.cm-list-item-focused': {
|
|
159
|
-
borderColor: 'var(--color-
|
|
159
|
+
borderColor: 'var(--color-focus-ring-subtle)',
|
|
160
160
|
},
|
|
161
161
|
'&:focus-within .cm-list-item-selected': {
|
|
162
162
|
borderColor: 'var(--color-separator)',
|
|
@@ -7,7 +7,8 @@ import { type EditorState, type Extension, RangeSetBuilder, StateEffect, StateFi
|
|
|
7
7
|
import { Decoration, type DecorationSet, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
|
|
8
8
|
import { type SyntaxNode } from '@lezer/common';
|
|
9
9
|
|
|
10
|
-
import { type Database,
|
|
10
|
+
import { type Database, Entity } from '@dxos/echo';
|
|
11
|
+
import { EID, URI } from '@dxos/keys';
|
|
11
12
|
|
|
12
13
|
export type PreviewBlock = {
|
|
13
14
|
link: PreviewLinkRef;
|
|
@@ -78,12 +79,13 @@ const resolveLabel = (
|
|
|
78
79
|
dxnStr: string,
|
|
79
80
|
viewRef: { current: EditorView | undefined },
|
|
80
81
|
): string | undefined => {
|
|
81
|
-
const
|
|
82
|
-
|
|
82
|
+
const echoUri = EID.tryParse(dxnStr);
|
|
83
|
+
const dxnRef = echoUri ?? (dxnStr.startsWith('dxn:') ? URI.make(dxnStr) : undefined);
|
|
84
|
+
if (!dxnRef) {
|
|
83
85
|
return;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
const ref = db.makeRef(
|
|
88
|
+
const ref = db.makeRef(dxnRef);
|
|
87
89
|
const target = ref.target;
|
|
88
90
|
if (target) {
|
|
89
91
|
return Entity.getLabel(target);
|
|
@@ -111,7 +113,7 @@ const buildDecorations = (
|
|
|
111
113
|
switch (node.name) {
|
|
112
114
|
//
|
|
113
115
|
// Inline widget.
|
|
114
|
-
// [Label](
|
|
116
|
+
// [Label](echo:/123)
|
|
115
117
|
//
|
|
116
118
|
case 'Link': {
|
|
117
119
|
const link = getLinkRef(state, node.node);
|
|
@@ -132,7 +134,7 @@ const buildDecorations = (
|
|
|
132
134
|
|
|
133
135
|
//
|
|
134
136
|
// Block widget (transclusion).
|
|
135
|
-
// 
|
|
136
138
|
//
|
|
137
139
|
case 'Image': {
|
|
138
140
|
if (options.addBlockContainer && options.removeBlockContainer) {
|
|
@@ -159,15 +161,15 @@ const buildDecorations = (
|
|
|
159
161
|
|
|
160
162
|
/**
|
|
161
163
|
* Link references.
|
|
162
|
-
* [Label](
|
|
163
|
-
*  Inline reference
|
|
165
|
+
*  Block reference
|
|
164
166
|
*/
|
|
165
167
|
export const getLinkRef = (state: EditorState, node: SyntaxNode): PreviewLinkRef | undefined => {
|
|
166
168
|
const mark = node.getChildren('LinkMark');
|
|
167
169
|
const urlNode = node.getChild('URL');
|
|
168
170
|
if (mark && urlNode) {
|
|
169
171
|
const dxn = state.sliceDoc(urlNode.from, urlNode.to);
|
|
170
|
-
if (dxn.startsWith('dxn:')) {
|
|
172
|
+
if (dxn.startsWith('dxn:') || dxn.startsWith('echo:')) {
|
|
171
173
|
const label = state.sliceDoc(mark[0].to, mark[1].from);
|
|
172
174
|
return {
|
|
173
175
|
block: state.sliceDoc(mark[0].from, mark[0].from + 1) === '!',
|
|
@@ -180,7 +182,7 @@ export const getLinkRef = (state: EditorState, node: SyntaxNode): PreviewLinkRef
|
|
|
180
182
|
|
|
181
183
|
/**
|
|
182
184
|
* Inline widget.
|
|
183
|
-
* [Label](
|
|
185
|
+
* [Label](echo:/123)
|
|
184
186
|
*/
|
|
185
187
|
class PreviewInlineWidget extends WidgetType {
|
|
186
188
|
constructor(
|
|
@@ -209,7 +211,7 @@ class PreviewInlineWidget extends WidgetType {
|
|
|
209
211
|
|
|
210
212
|
/**
|
|
211
213
|
* Block widget (e.g., for surfaces).
|
|
212
|
-
* ![Label][
|
|
214
|
+
* ![Label][echo:/123]
|
|
213
215
|
*/
|
|
214
216
|
class PreviewBlockWidget extends WidgetType {
|
|
215
217
|
constructor(
|
|
@@ -229,7 +231,7 @@ class PreviewBlockWidget extends WidgetType {
|
|
|
229
231
|
|
|
230
232
|
override toDOM(_view: EditorView) {
|
|
231
233
|
const root = document.createElement('div');
|
|
232
|
-
root.classList.add('cm-preview-block', 'dx-density-
|
|
234
|
+
root.classList.add('cm-preview-block', 'dx-density-md');
|
|
233
235
|
this._options.addBlockContainer?.({ link: this._link, el: root });
|
|
234
236
|
return root;
|
|
235
237
|
}
|