@aicayzer/inkkit 0.0.8 → 0.0.10
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/README.md +5 -3
- package/dist/callouts.js +2 -1
- package/dist/clipboard.d.ts +3 -3
- package/dist/clipboard.js +64 -25
- package/dist/dialect.d.ts +1 -1
- package/dist/dialect.js +36 -2
- package/dist/editor.d.ts +27 -1
- package/dist/editor.js +478 -35
- package/dist/files.d.ts +40 -0
- package/dist/files.js +133 -0
- package/dist/images.d.ts +20 -4
- package/dist/images.js +242 -25
- package/dist/index.d.ts +1 -1
- package/dist/labels.d.ts +8 -0
- package/dist/labels.js +8 -0
- package/dist/linked-syntax.d.ts +48 -0
- package/dist/linked-syntax.js +308 -0
- package/dist/literals.js +40 -1
- package/dist/portable-files.d.ts +10 -0
- package/dist/portable-files.js +36 -0
- package/dist/preserve.js +7 -0
- package/dist/print.d.ts +2 -2
- package/dist/print.js +49 -17
- package/dist/search.d.ts +2 -1
- package/dist/search.js +53 -22
- package/dist/source.d.ts +0 -1
- package/dist/source.js +0 -60
- package/dist/style.css +39 -0
- package/dist/text-ranges.d.ts +26 -0
- package/dist/text-ranges.js +233 -0
- package/dist/types.d.ts +84 -2
- package/dist/wiki-view.d.ts +21 -0
- package/dist/wiki-view.js +78 -0
- package/package.json +1 -1
package/dist/files.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { DocumentContext, FileAdapter, FileContext, FilePresentation, FileReference } from './types.js';
|
|
2
|
+
import type { EditorLabels } from './labels.js';
|
|
3
|
+
export interface FileViewRuntime {
|
|
4
|
+
adapter: FileAdapter;
|
|
5
|
+
capture(): {
|
|
6
|
+
context: DocumentContext;
|
|
7
|
+
isCurrent(): boolean;
|
|
8
|
+
};
|
|
9
|
+
active(): boolean;
|
|
10
|
+
labels: Readonly<EditorLabels>;
|
|
11
|
+
error?(error: Error): void;
|
|
12
|
+
}
|
|
13
|
+
export declare function withFileSignal<T>(operation: Promise<T>, signal: AbortSignal): Promise<T>;
|
|
14
|
+
export declare function resolveFile(adapter: FileAdapter, reference: FileReference, context: FileContext): Promise<FilePresentation>;
|
|
15
|
+
export type FileResolutionState = {
|
|
16
|
+
status: 'idle';
|
|
17
|
+
} | {
|
|
18
|
+
status: 'loading';
|
|
19
|
+
} | {
|
|
20
|
+
status: 'resolved';
|
|
21
|
+
presentation: FilePresentation;
|
|
22
|
+
} | {
|
|
23
|
+
status: 'failed';
|
|
24
|
+
error: Error;
|
|
25
|
+
};
|
|
26
|
+
export declare class FileResolution {
|
|
27
|
+
private readonly runtime;
|
|
28
|
+
private readonly changed;
|
|
29
|
+
state: FileResolutionState;
|
|
30
|
+
private reference?;
|
|
31
|
+
private key;
|
|
32
|
+
private request?;
|
|
33
|
+
private dead;
|
|
34
|
+
constructor(runtime: FileViewRuntime, changed: () => void);
|
|
35
|
+
update(reference: FileReference): void;
|
|
36
|
+
retry(): void;
|
|
37
|
+
private current;
|
|
38
|
+
cancel(): void;
|
|
39
|
+
destroy(): void;
|
|
40
|
+
}
|
package/dist/files.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
function abort(signal) {
|
|
2
|
+
if (signal.aborted)
|
|
3
|
+
throw new DOMException('File resolution cancelled', 'AbortError');
|
|
4
|
+
}
|
|
5
|
+
export function withFileSignal(operation, signal) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const cleanup = () => signal.removeEventListener('abort', cancelled);
|
|
8
|
+
const cancelled = () => {
|
|
9
|
+
cleanup();
|
|
10
|
+
reject(new DOMException('File operation cancelled', 'AbortError'));
|
|
11
|
+
};
|
|
12
|
+
if (signal.aborted)
|
|
13
|
+
cancelled();
|
|
14
|
+
else
|
|
15
|
+
signal.addEventListener('abort', cancelled, { once: true });
|
|
16
|
+
operation.then((result) => {
|
|
17
|
+
cleanup();
|
|
18
|
+
if (signal.aborted)
|
|
19
|
+
cancelled();
|
|
20
|
+
else
|
|
21
|
+
resolve(result);
|
|
22
|
+
}, (error) => {
|
|
23
|
+
cleanup();
|
|
24
|
+
reject(error);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export async function resolveFile(adapter, reference, context) {
|
|
29
|
+
abort(context.signal);
|
|
30
|
+
const presentation = await withFileSignal(adapter.resolve(Object.freeze({ ...reference }), context), context.signal);
|
|
31
|
+
abort(context.signal);
|
|
32
|
+
if (!presentation ||
|
|
33
|
+
typeof presentation !== 'object' ||
|
|
34
|
+
!['image', 'audio', 'video', 'pdf', 'file', 'missing', 'error'].includes(presentation.kind))
|
|
35
|
+
throw new TypeError('File adapter returned an unsupported presentation');
|
|
36
|
+
if (presentation.label !== undefined &&
|
|
37
|
+
typeof presentation.label !== 'string')
|
|
38
|
+
throw new TypeError('File presentation label must be text');
|
|
39
|
+
if (presentation.kind === 'error' && typeof presentation.message !== 'string')
|
|
40
|
+
throw new TypeError('File presentation error must include a message');
|
|
41
|
+
if ('url' in presentation) {
|
|
42
|
+
if (typeof presentation.url !== 'string' || !presentation.url.trim())
|
|
43
|
+
throw new TypeError('File presentation needs a resource URL');
|
|
44
|
+
const url = new URL(presentation.url, document.baseURI);
|
|
45
|
+
if (['javascript:', 'vbscript:'].includes(url.protocol))
|
|
46
|
+
throw new TypeError('Executable file presentation URLs are not supported');
|
|
47
|
+
const prefix = presentation.kind === 'pdf' ? 'application/pdf' : `${presentation.kind}/`;
|
|
48
|
+
const mime = presentation.mimeType?.toLowerCase().split(';')[0]?.trim();
|
|
49
|
+
if (mime &&
|
|
50
|
+
(presentation.kind === 'pdf' ? mime !== prefix : !mime.startsWith(prefix)))
|
|
51
|
+
throw new TypeError('File presentation MIME type does not match its kind');
|
|
52
|
+
const dataMime = url.pathname.toLowerCase().split(/[;,]/)[0] ?? '';
|
|
53
|
+
if (url.protocol === 'data:' &&
|
|
54
|
+
(presentation.kind === 'pdf'
|
|
55
|
+
? dataMime !== prefix
|
|
56
|
+
: !dataMime.startsWith(prefix)))
|
|
57
|
+
throw new TypeError('File presentation data URL does not match its kind');
|
|
58
|
+
}
|
|
59
|
+
else if (['image', 'audio', 'video', 'pdf'].includes(presentation.kind)) {
|
|
60
|
+
throw new TypeError('File presentation needs a resource URL');
|
|
61
|
+
}
|
|
62
|
+
return Object.freeze({ ...presentation });
|
|
63
|
+
}
|
|
64
|
+
export class FileResolution {
|
|
65
|
+
runtime;
|
|
66
|
+
changed;
|
|
67
|
+
state = { status: 'idle' };
|
|
68
|
+
reference;
|
|
69
|
+
key = '';
|
|
70
|
+
request;
|
|
71
|
+
dead = false;
|
|
72
|
+
constructor(runtime, changed) {
|
|
73
|
+
this.runtime = runtime;
|
|
74
|
+
this.changed = changed;
|
|
75
|
+
}
|
|
76
|
+
update(reference) {
|
|
77
|
+
const key = JSON.stringify([
|
|
78
|
+
reference.reference,
|
|
79
|
+
reference.kind,
|
|
80
|
+
reference.fragment,
|
|
81
|
+
]);
|
|
82
|
+
this.reference = Object.freeze({ ...reference });
|
|
83
|
+
if (this.key === key && this.request?.isCurrent())
|
|
84
|
+
return;
|
|
85
|
+
this.key = key;
|
|
86
|
+
this.retry();
|
|
87
|
+
}
|
|
88
|
+
retry() {
|
|
89
|
+
this.cancel();
|
|
90
|
+
if (this.dead || !this.reference || !this.runtime.active())
|
|
91
|
+
return;
|
|
92
|
+
const captured = this.runtime.capture();
|
|
93
|
+
const request = {
|
|
94
|
+
controller: new AbortController(),
|
|
95
|
+
isCurrent: captured.isCurrent,
|
|
96
|
+
};
|
|
97
|
+
this.request = request;
|
|
98
|
+
this.state = { status: 'loading' };
|
|
99
|
+
this.changed();
|
|
100
|
+
void resolveFile(this.runtime.adapter, this.reference, {
|
|
101
|
+
...captured.context,
|
|
102
|
+
signal: request.controller.signal,
|
|
103
|
+
}).then((presentation) => {
|
|
104
|
+
if (!this.current(request))
|
|
105
|
+
return;
|
|
106
|
+
this.state = { status: 'resolved', presentation };
|
|
107
|
+
this.changed();
|
|
108
|
+
}, (failure) => {
|
|
109
|
+
if (!this.current(request))
|
|
110
|
+
return;
|
|
111
|
+
const error = failure instanceof Error ? failure : new Error(String(failure));
|
|
112
|
+
this.state = { status: 'failed', error };
|
|
113
|
+
this.changed();
|
|
114
|
+
this.runtime.error?.(error);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
current(request) {
|
|
118
|
+
return (!this.dead &&
|
|
119
|
+
this.request === request &&
|
|
120
|
+
!request.controller.signal.aborted &&
|
|
121
|
+
this.runtime.active() &&
|
|
122
|
+
request.isCurrent());
|
|
123
|
+
}
|
|
124
|
+
cancel() {
|
|
125
|
+
this.request?.controller.abort();
|
|
126
|
+
this.request = undefined;
|
|
127
|
+
this.state = { status: 'idle' };
|
|
128
|
+
}
|
|
129
|
+
destroy() {
|
|
130
|
+
this.dead = true;
|
|
131
|
+
this.cancel();
|
|
132
|
+
}
|
|
133
|
+
}
|
package/dist/images.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Node as ProseNode } from '@milkdown/kit/prose/model';
|
|
2
2
|
import type { EditorView, NodeView } from '@milkdown/kit/prose/view';
|
|
3
3
|
import type { ImageAdapter } from './types.js';
|
|
4
|
+
import { type FileViewRuntime } from './files.js';
|
|
4
5
|
/** A width rides in the alt text, as ``. Other Markdown readers
|
|
5
6
|
* show the picture and keep the number without requiring a private extension. */
|
|
6
7
|
export declare function splitAlt(alt: string): {
|
|
@@ -8,26 +9,41 @@ export declare function splitAlt(alt: string): {
|
|
|
8
9
|
width: number | null;
|
|
9
10
|
};
|
|
10
11
|
export declare function joinAlt(alt: string, width: number | null): string;
|
|
11
|
-
/** Draws the image and the handle that sizes it. The width is kept in the alt text, so the schema is the
|
|
12
|
-
* one commonmark gives us; a picture is never wider than its editor, whatever number the alt text holds. */
|
|
13
12
|
declare class ImageView implements NodeView {
|
|
14
13
|
private node;
|
|
15
14
|
private readonly view;
|
|
16
15
|
private readonly getPos;
|
|
17
16
|
private readonly adapter;
|
|
17
|
+
private readonly runtime?;
|
|
18
|
+
private readonly canMutate;
|
|
18
19
|
dom: HTMLElement;
|
|
19
20
|
private image?;
|
|
21
|
+
private media?;
|
|
22
|
+
private pdf?;
|
|
20
23
|
private width;
|
|
21
24
|
private stopResize?;
|
|
22
|
-
|
|
25
|
+
private resolution?;
|
|
26
|
+
private previewFailed;
|
|
27
|
+
constructor(node: ProseNode, view: EditorView, getPos: () => number | undefined, adapter: ImageAdapter | undefined, runtime?: FileViewRuntime | undefined, canMutate?: () => boolean);
|
|
28
|
+
private reference;
|
|
29
|
+
private active;
|
|
23
30
|
update(node: ProseNode): boolean;
|
|
24
31
|
stopEvent(event: Event): boolean;
|
|
25
32
|
ignoreMutation(): boolean;
|
|
33
|
+
private mediaPolicy;
|
|
26
34
|
private cancelResize;
|
|
35
|
+
private releaseMedia;
|
|
27
36
|
destroy(): void;
|
|
37
|
+
private button;
|
|
38
|
+
private activate;
|
|
39
|
+
private report;
|
|
40
|
+
private card;
|
|
41
|
+
private failPreview;
|
|
28
42
|
private render;
|
|
43
|
+
private renderImage;
|
|
29
44
|
private startResize;
|
|
30
45
|
private commit;
|
|
46
|
+
private canResize;
|
|
31
47
|
}
|
|
32
|
-
export declare function imageView(adapter?: ImageAdapter): import("@milkdown/kit/utils").$View<import("@milkdown/kit/utils").$Node, (node: ProseNode, view: EditorView, getPos: () => number | undefined) => ImageView>;
|
|
48
|
+
export declare function imageView(adapter?: ImageAdapter, runtime?: FileViewRuntime, canMutate?: () => boolean): import("@milkdown/kit/utils").$View<import("@milkdown/kit/utils").$Node, (node: ProseNode, view: EditorView, getPos: () => number | undefined) => ImageView>;
|
|
33
49
|
export {};
|
package/dist/images.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { imageSchema } from '@milkdown/kit/preset/commonmark';
|
|
2
|
+
import { closeHistory } from '@milkdown/kit/prose/history';
|
|
2
3
|
import { $view } from '@milkdown/kit/utils';
|
|
4
|
+
import { FileResolution } from './files.js';
|
|
5
|
+
import { fileReference } from './linked-syntax.js';
|
|
3
6
|
/** A width rides in the alt text, as ``. Other Markdown readers
|
|
4
7
|
* show the picture and keep the number without requiring a private extension. */
|
|
5
8
|
export function splitAlt(alt) {
|
|
@@ -12,75 +15,274 @@ export function joinAlt(alt, width) {
|
|
|
12
15
|
return width == null ? alt : `${alt}|${width}`;
|
|
13
16
|
}
|
|
14
17
|
const minimumWidth = 48;
|
|
15
|
-
/** Draws the image and the handle that sizes it. The width is kept in the alt text, so the schema is the
|
|
16
|
-
* one commonmark gives us; a picture is never wider than its editor, whatever number the alt text holds. */
|
|
17
18
|
class ImageView {
|
|
18
19
|
node;
|
|
19
20
|
view;
|
|
20
21
|
getPos;
|
|
21
22
|
adapter;
|
|
23
|
+
runtime;
|
|
24
|
+
canMutate;
|
|
22
25
|
dom;
|
|
23
26
|
image;
|
|
27
|
+
media;
|
|
28
|
+
pdf;
|
|
24
29
|
width = null;
|
|
25
30
|
stopResize;
|
|
26
|
-
|
|
31
|
+
resolution;
|
|
32
|
+
previewFailed = false;
|
|
33
|
+
constructor(node, view, getPos, adapter, runtime, canMutate = () => true) {
|
|
27
34
|
this.node = node;
|
|
28
35
|
this.view = view;
|
|
29
36
|
this.getPos = getPos;
|
|
30
37
|
this.adapter = adapter;
|
|
38
|
+
this.runtime = runtime;
|
|
39
|
+
this.canMutate = canMutate;
|
|
31
40
|
this.dom = document.createElement('span');
|
|
32
41
|
this.dom.className = 'image';
|
|
33
42
|
this.view.dom.addEventListener('inkkit-cancel-resize', this.cancelResize);
|
|
43
|
+
this.view.dom.addEventListener('inkkit-media-policy', this.mediaPolicy);
|
|
44
|
+
if (runtime) {
|
|
45
|
+
this.resolution = new FileResolution({ ...runtime, active: () => this.active() }, () => {
|
|
46
|
+
this.previewFailed = false;
|
|
47
|
+
this.render();
|
|
48
|
+
});
|
|
49
|
+
this.resolution.update(this.reference());
|
|
50
|
+
}
|
|
34
51
|
this.render();
|
|
35
52
|
}
|
|
53
|
+
reference() {
|
|
54
|
+
return fileReference(this.node);
|
|
55
|
+
}
|
|
56
|
+
active() {
|
|
57
|
+
return (!!this.runtime?.active() &&
|
|
58
|
+
!this.dom.closest('[data-inkkit-folded="true"], [hidden]'));
|
|
59
|
+
}
|
|
36
60
|
update(node) {
|
|
37
61
|
if (node.type !== this.node.type)
|
|
38
62
|
return false;
|
|
63
|
+
if (node.sameMarkup(this.node)) {
|
|
64
|
+
this.node = node;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
39
67
|
this.node = node;
|
|
68
|
+
this.previewFailed = false;
|
|
69
|
+
this.resolution?.update(this.reference());
|
|
40
70
|
this.render();
|
|
41
71
|
return true;
|
|
42
72
|
}
|
|
43
|
-
/// The handle's own events are the view's business, not the document's.
|
|
44
73
|
stopEvent(event) {
|
|
45
|
-
return (event.target instanceof
|
|
46
|
-
event.target.
|
|
74
|
+
return (event.target instanceof Element &&
|
|
75
|
+
!!event.target.closest('.image-handle, .inkkit-file-control, audio, video, iframe'));
|
|
47
76
|
}
|
|
48
77
|
ignoreMutation() {
|
|
49
78
|
return true;
|
|
50
79
|
}
|
|
80
|
+
mediaPolicy = () => {
|
|
81
|
+
if (!this.runtime)
|
|
82
|
+
return;
|
|
83
|
+
if (!this.active()) {
|
|
84
|
+
this.stopResize?.();
|
|
85
|
+
this.stopResize = undefined;
|
|
86
|
+
this.resolution?.cancel();
|
|
87
|
+
this.releaseMedia();
|
|
88
|
+
this.render();
|
|
89
|
+
}
|
|
90
|
+
else
|
|
91
|
+
this.resolution?.update(this.reference());
|
|
92
|
+
};
|
|
51
93
|
cancelResize = () => {
|
|
94
|
+
if (!this.stopResize)
|
|
95
|
+
return;
|
|
52
96
|
this.stopResize?.();
|
|
53
97
|
this.stopResize = undefined;
|
|
54
98
|
this.render();
|
|
55
99
|
};
|
|
100
|
+
releaseMedia() {
|
|
101
|
+
const media = this.media;
|
|
102
|
+
this.media = undefined;
|
|
103
|
+
if (media) {
|
|
104
|
+
media.pause();
|
|
105
|
+
media.removeAttribute('src');
|
|
106
|
+
media.load();
|
|
107
|
+
}
|
|
108
|
+
this.pdf?.removeAttribute('src');
|
|
109
|
+
this.pdf = undefined;
|
|
110
|
+
}
|
|
56
111
|
destroy() {
|
|
57
112
|
this.view.dom.removeEventListener('inkkit-cancel-resize', this.cancelResize);
|
|
113
|
+
this.view.dom.removeEventListener('inkkit-media-policy', this.mediaPolicy);
|
|
58
114
|
this.stopResize?.();
|
|
59
115
|
this.stopResize = undefined;
|
|
116
|
+
this.resolution?.destroy();
|
|
117
|
+
this.releaseMedia();
|
|
118
|
+
}
|
|
119
|
+
button(label, action) {
|
|
120
|
+
const button = document.createElement('button');
|
|
121
|
+
button.type = 'button';
|
|
122
|
+
button.className = 'inkkit-file-control';
|
|
123
|
+
button.textContent = label;
|
|
124
|
+
button.addEventListener('click', (event) => {
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
action();
|
|
127
|
+
});
|
|
128
|
+
return button;
|
|
129
|
+
}
|
|
130
|
+
activate = () => {
|
|
131
|
+
if (!this.runtime || !this.active())
|
|
132
|
+
return;
|
|
133
|
+
const captured = this.runtime.capture();
|
|
134
|
+
try {
|
|
135
|
+
if (captured.isCurrent())
|
|
136
|
+
this.runtime.adapter.open?.(this.reference(), captured.context);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
this.report(error);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
report(error) {
|
|
143
|
+
this.runtime?.error?.(error instanceof Error ? error : new Error(String(error)));
|
|
144
|
+
}
|
|
145
|
+
card(label, status, retry = false) {
|
|
146
|
+
const text = document.createElement('span');
|
|
147
|
+
text.className = 'inkkit-file-label';
|
|
148
|
+
text.textContent = status ? `${label}: ${status}` : label;
|
|
149
|
+
if (status)
|
|
150
|
+
text.setAttribute('role', 'status');
|
|
151
|
+
this.dom.append(text);
|
|
152
|
+
if (retry && this.runtime)
|
|
153
|
+
this.dom.append(this.button(this.runtime.labels.fileRetry, () => this.resolution?.retry()));
|
|
154
|
+
if (this.runtime?.adapter.open) {
|
|
155
|
+
const open = this.button(this.runtime.labels.fileOpen, this.activate);
|
|
156
|
+
open.setAttribute('aria-label', `${this.runtime.labels.fileOpen} ${label}`);
|
|
157
|
+
this.dom.append(open);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
failPreview() {
|
|
161
|
+
this.previewFailed = true;
|
|
162
|
+
this.runtime?.error?.(new Error(this.runtime.labels.filePreviewUnavailable));
|
|
163
|
+
this.render();
|
|
60
164
|
}
|
|
61
165
|
render() {
|
|
62
166
|
this.stopResize?.();
|
|
63
167
|
this.stopResize = undefined;
|
|
64
|
-
|
|
65
|
-
const
|
|
168
|
+
this.releaseMedia();
|
|
169
|
+
const reference = this.reference();
|
|
170
|
+
const { alt, width } = this.node.attrs.inkkitFileRaw
|
|
171
|
+
? {
|
|
172
|
+
alt: reference.label ?? reference.reference,
|
|
173
|
+
width: reference.width ?? null,
|
|
174
|
+
}
|
|
175
|
+
: splitAlt(String(this.node.attrs.alt ?? ''));
|
|
66
176
|
this.width = width;
|
|
67
177
|
this.dom.textContent = '';
|
|
68
178
|
this.image = undefined;
|
|
69
|
-
|
|
70
|
-
|
|
179
|
+
if (this.runtime) {
|
|
180
|
+
this.dom.classList.add('inkkit-file');
|
|
181
|
+
this.dom.oncontextmenu = (event) => {
|
|
182
|
+
if (!this.runtime?.adapter.contextMenu || !this.active())
|
|
183
|
+
return;
|
|
184
|
+
const captured = this.runtime.capture();
|
|
185
|
+
if (!captured.isCurrent())
|
|
186
|
+
return;
|
|
187
|
+
event.preventDefault();
|
|
188
|
+
try {
|
|
189
|
+
this.runtime.adapter.contextMenu(this.reference(), { clientX: event.clientX, clientY: event.clientY }, captured.context);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
this.report(error);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
const state = this.resolution.state;
|
|
196
|
+
const label = state.status === 'resolved'
|
|
197
|
+
? state.presentation.label || alt || reference.reference
|
|
198
|
+
: alt || reference.reference;
|
|
199
|
+
this.dom.setAttribute('aria-label', label);
|
|
200
|
+
this.dom.dataset.inkkitFileState = this.previewFailed
|
|
201
|
+
? 'error'
|
|
202
|
+
: state.status;
|
|
203
|
+
this.dom.dataset.inkkitFileKind =
|
|
204
|
+
state.status === 'resolved' ? state.presentation.kind : 'file';
|
|
205
|
+
if (this.previewFailed) {
|
|
206
|
+
this.card(label, this.runtime.labels.filePreviewUnavailable, true);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (state.status === 'loading' || state.status === 'idle') {
|
|
210
|
+
this.card(label, this.runtime.labels.fileLoading, true);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (state.status === 'failed') {
|
|
214
|
+
this.card(label, this.runtime.labels.fileError, true);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const presentation = state.presentation;
|
|
218
|
+
if (presentation.kind === 'missing' || presentation.kind === 'error') {
|
|
219
|
+
this.card(label, presentation.kind === 'missing'
|
|
220
|
+
? this.runtime.labels.fileMissing
|
|
221
|
+
: this.runtime.labels.fileError, true);
|
|
222
|
+
}
|
|
223
|
+
else if (presentation.kind === 'image') {
|
|
224
|
+
this.renderImage(presentation.url, label, width);
|
|
225
|
+
this.card(label);
|
|
226
|
+
}
|
|
227
|
+
else if (presentation.kind === 'audio' ||
|
|
228
|
+
presentation.kind === 'video') {
|
|
229
|
+
const media = document.createElement(presentation.kind);
|
|
230
|
+
media.controls = true;
|
|
231
|
+
media.preload = 'metadata';
|
|
232
|
+
if (width != null)
|
|
233
|
+
media.style.width = `${width}px`;
|
|
234
|
+
media.setAttribute('aria-label', label);
|
|
235
|
+
media.addEventListener('error', () => {
|
|
236
|
+
if (this.media === media)
|
|
237
|
+
this.failPreview();
|
|
238
|
+
});
|
|
239
|
+
media.src = presentation.url;
|
|
240
|
+
this.media = media;
|
|
241
|
+
this.dom.append(media);
|
|
242
|
+
this.card(label);
|
|
243
|
+
}
|
|
244
|
+
else if (presentation.kind === 'pdf') {
|
|
245
|
+
const pdf = document.createElement('iframe');
|
|
246
|
+
pdf.title = label;
|
|
247
|
+
pdf.setAttribute('sandbox', '');
|
|
248
|
+
pdf.referrerPolicy = 'no-referrer';
|
|
249
|
+
if (width != null)
|
|
250
|
+
pdf.style.width = `${width}px`;
|
|
251
|
+
pdf.addEventListener('error', () => {
|
|
252
|
+
if (this.pdf === pdf)
|
|
253
|
+
this.failPreview();
|
|
254
|
+
});
|
|
255
|
+
pdf.src = presentation.url;
|
|
256
|
+
this.pdf = pdf;
|
|
257
|
+
this.dom.append(pdf);
|
|
258
|
+
this.card(label, this.runtime.labels.filePDFPreview);
|
|
259
|
+
}
|
|
260
|
+
else
|
|
261
|
+
this.card(label);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const presentation = this.adapter?.presentation(String(this.node.attrs.src ?? ''));
|
|
71
265
|
if (!presentation) {
|
|
72
266
|
this.dom.classList.add('image-absent');
|
|
73
|
-
this.dom.textContent = alt || src;
|
|
267
|
+
this.dom.textContent = alt || String(this.node.attrs.src ?? '');
|
|
74
268
|
return;
|
|
75
269
|
}
|
|
76
270
|
this.dom.classList.remove('image-absent');
|
|
271
|
+
this.renderImage(presentation.url, alt, width);
|
|
272
|
+
}
|
|
273
|
+
renderImage(url, alt, width) {
|
|
77
274
|
const image = document.createElement('img');
|
|
78
|
-
image.src = presentation.url;
|
|
79
275
|
image.alt = alt;
|
|
80
276
|
if (this.node.attrs.title)
|
|
81
277
|
image.title = String(this.node.attrs.title);
|
|
82
278
|
if (width != null)
|
|
83
279
|
image.style.width = `${width}px`;
|
|
280
|
+
if (this.runtime)
|
|
281
|
+
image.addEventListener('error', () => {
|
|
282
|
+
if (this.image === image)
|
|
283
|
+
this.failPreview();
|
|
284
|
+
});
|
|
285
|
+
image.src = url;
|
|
84
286
|
this.image = image;
|
|
85
287
|
const handle = document.createElement('span');
|
|
86
288
|
handle.className = 'image-handle';
|
|
@@ -88,53 +290,68 @@ class ImageView {
|
|
|
88
290
|
this.dom.append(image, handle);
|
|
89
291
|
}
|
|
90
292
|
startResize = (event) => {
|
|
293
|
+
event.preventDefault();
|
|
91
294
|
const image = this.image;
|
|
92
|
-
if (!image || !this.
|
|
295
|
+
if (!image || !this.canResize())
|
|
93
296
|
return;
|
|
94
|
-
event.preventDefault();
|
|
95
297
|
const handle = event.currentTarget;
|
|
96
298
|
const startX = event.clientX;
|
|
97
299
|
const startWidth = image.getBoundingClientRect().width;
|
|
98
300
|
const limit = this.view.dom.clientWidth;
|
|
99
301
|
handle.setPointerCapture?.(event.pointerId);
|
|
100
302
|
const move = (moved) => {
|
|
101
|
-
if (!this.
|
|
303
|
+
if (!this.canResize()) {
|
|
102
304
|
this.cancelResize();
|
|
103
305
|
return;
|
|
104
306
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
image.style.width = `${next}px`;
|
|
307
|
+
this.width = Math.round(Math.min(limit, Math.max(minimumWidth, startWidth + moved.clientX - startX)));
|
|
308
|
+
image.style.width = `${this.width}px`;
|
|
108
309
|
};
|
|
109
310
|
const cleanup = () => {
|
|
110
311
|
handle.removeEventListener('pointermove', move);
|
|
111
312
|
handle.removeEventListener('pointerup', done);
|
|
112
|
-
handle.removeEventListener('pointercancel',
|
|
313
|
+
handle.removeEventListener('pointercancel', cancel);
|
|
113
314
|
};
|
|
114
315
|
const done = () => {
|
|
115
316
|
cleanup();
|
|
116
317
|
this.stopResize = undefined;
|
|
117
318
|
this.commit();
|
|
118
319
|
};
|
|
320
|
+
const cancel = () => {
|
|
321
|
+
cleanup();
|
|
322
|
+
this.stopResize = undefined;
|
|
323
|
+
this.render();
|
|
324
|
+
};
|
|
119
325
|
this.stopResize = cleanup;
|
|
120
326
|
handle.addEventListener('pointermove', move);
|
|
121
327
|
handle.addEventListener('pointerup', done);
|
|
122
|
-
handle.addEventListener('pointercancel',
|
|
328
|
+
handle.addEventListener('pointercancel', cancel);
|
|
123
329
|
};
|
|
124
330
|
commit() {
|
|
125
|
-
if (!this.
|
|
331
|
+
if (!this.canResize()) {
|
|
332
|
+
this.render();
|
|
126
333
|
return;
|
|
334
|
+
}
|
|
127
335
|
const pos = this.getPos();
|
|
128
336
|
if (pos == null)
|
|
129
337
|
return;
|
|
130
|
-
const
|
|
338
|
+
const alt = this.node.attrs.inkkitFileRaw
|
|
339
|
+
? (this.reference().label ?? this.reference().reference)
|
|
340
|
+
: splitAlt(String(this.node.attrs.alt ?? '')).alt;
|
|
131
341
|
const next = joinAlt(alt, this.width);
|
|
132
342
|
if (next === this.node.attrs.alt)
|
|
133
343
|
return;
|
|
134
344
|
const { state } = this.view;
|
|
135
|
-
this.view.dispatch(state.tr.setNodeMarkup(pos, undefined, {
|
|
345
|
+
this.view.dispatch(closeHistory(state.tr.setNodeMarkup(pos, undefined, {
|
|
346
|
+
...this.node.attrs,
|
|
347
|
+
alt: next,
|
|
348
|
+
})));
|
|
349
|
+
this.view.dispatch(closeHistory(this.view.state.tr));
|
|
350
|
+
}
|
|
351
|
+
canResize() {
|
|
352
|
+
return this.view.editable && !this.view.composing && this.canMutate();
|
|
136
353
|
}
|
|
137
354
|
}
|
|
138
|
-
export function imageView(adapter) {
|
|
139
|
-
return $view(imageSchema.node, () => (node, view, getPos) => new ImageView(node, view, getPos, adapter));
|
|
355
|
+
export function imageView(adapter, runtime, canMutate = () => true) {
|
|
356
|
+
return $view(imageSchema.node, () => (node, view, getPos) => new ImageView(node, view, getPos, adapter, runtime, canMutate));
|
|
140
357
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,5 +3,5 @@ export type { EditorEvents, EditorOptions, TextInputPreferences, CommandState, E
|
|
|
3
3
|
export type { TableCommand, TableOptions } from './tables.js';
|
|
4
4
|
export type { Heading } from './outline.js';
|
|
5
5
|
export { InkKitError } from './types.js';
|
|
6
|
-
export type { DocumentFormat, DocumentContext, DocumentInput, DocumentSnapshot, EditorErrorCode, ImageAdapter, CapturedImage, PortableImage, ClipboardInput, ClipboardOutput, ClipboardImage, ClipboardDiagram, PrintableDocument, PrintableWarning, } from './types.js';
|
|
6
|
+
export type { DocumentFormat, DocumentContext, DocumentInput, DocumentSnapshot, EditorErrorCode, ImageAdapter, WikiLinkReference, WikiLinkAdapter, FileReference, FileContext, FilePresentation, FileAdapter, CapturedImage, PortableImage, ClipboardInput, ClipboardOutput, ClipboardImage, ClipboardDiagram, PrintableDocument, PrintableWarning, TextRange, ReadableTextSnapshot, TextRect, ViewportInsets, ViewportOptions, ViewportSnapshot, } from './types.js';
|
|
7
7
|
export type { EditorLabels } from './labels.js';
|
package/dist/labels.d.ts
CHANGED
|
@@ -22,6 +22,14 @@ export interface EditorLabels {
|
|
|
22
22
|
tableAlignRight: string;
|
|
23
23
|
tableMoveColumn: string;
|
|
24
24
|
tableMoveRow: string;
|
|
25
|
+
fileLoading: string;
|
|
26
|
+
fileMissing: string;
|
|
27
|
+
fileError: string;
|
|
28
|
+
fileRetry: string;
|
|
29
|
+
fileOpen: string;
|
|
30
|
+
filePreviewUnavailable: string;
|
|
31
|
+
filePDFPreview: string;
|
|
32
|
+
wikiMissing: string;
|
|
25
33
|
}
|
|
26
34
|
export declare const defaultLabels: Readonly<EditorLabels>;
|
|
27
35
|
export declare const labelsCtx: import("@milkdown/kit/utils").$Ctx<Readonly<EditorLabels>, "inkkitLabels">;
|
package/dist/labels.js
CHANGED
|
@@ -19,6 +19,14 @@ export const defaultLabels = {
|
|
|
19
19
|
tableAlignRight: 'Align right',
|
|
20
20
|
tableMoveColumn: 'Move column',
|
|
21
21
|
tableMoveRow: 'Move row',
|
|
22
|
+
fileLoading: 'Loading file…',
|
|
23
|
+
fileMissing: 'File unavailable',
|
|
24
|
+
fileError: 'File could not be loaded',
|
|
25
|
+
fileRetry: 'Retry',
|
|
26
|
+
fileOpen: 'Open',
|
|
27
|
+
filePreviewUnavailable: 'Preview unavailable',
|
|
28
|
+
filePDFPreview: 'Open to view this PDF',
|
|
29
|
+
wikiMissing: 'Link unavailable',
|
|
22
30
|
};
|
|
23
31
|
export const labelsCtx = $ctx(defaultLabels, 'inkkitLabels');
|
|
24
32
|
export function editorLabels(ctx) {
|