@threadlabs/looma 0.28.21 → 0.29.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/editor/image-uploads.d.ts +57 -0
- package/editor/link-editing.d.ts +4 -4
- package/editor/requests.d.ts +43 -0
- package/package.json +1 -1
- package/vue/editor/LoomaEditor.d.ts +90 -47
- package/vue/editor/index.d.ts +1 -1
- package/vue/editor/index.js +389 -64
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/core";
|
|
2
|
+
import { type LoomaImageDescriptor } from "./image-delivery.js";
|
|
3
|
+
/** What an image upload request's detail carries: the file to upload. */
|
|
4
|
+
export interface LoomaImageUploadDetail {
|
|
5
|
+
file: File;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Uploads one image and answers with the image to insert, or `null` to insert nothing. Paste,
|
|
9
|
+
* drop, and file selection all upload through it.
|
|
10
|
+
*
|
|
11
|
+
* @ownership The application owns network requests, persistence, and the returned `src`. Looma
|
|
12
|
+
* owns only inserting the image once the answer arrives.
|
|
13
|
+
* @failure A rejection leaves the document unchanged and is reported through the editor's
|
|
14
|
+
* `uploadError` event, so the host chooses retry and user messaging. An aborted `signal` means the
|
|
15
|
+
* author canceled the upload; its rejection is not reported.
|
|
16
|
+
*/
|
|
17
|
+
export type LoomaImageUpload = (file: File, init: {
|
|
18
|
+
readonly signal: AbortSignal;
|
|
19
|
+
readonly progress: (loaded: number, total: number) => void;
|
|
20
|
+
}) => LoomaImageDescriptor | null | PromiseLike<LoomaImageDescriptor | null>;
|
|
21
|
+
export interface LoomaImageUploadsState {
|
|
22
|
+
readonly uploading: boolean;
|
|
23
|
+
/** The current upload's progress as its uploader reports it, or null before it reports any. */
|
|
24
|
+
readonly progress: {
|
|
25
|
+
readonly loaded: number;
|
|
26
|
+
readonly total: number;
|
|
27
|
+
} | null;
|
|
28
|
+
/** The file whose upload failed last, which `retry()` sends again. */
|
|
29
|
+
readonly failed: File | null;
|
|
30
|
+
}
|
|
31
|
+
export interface LoomaImageUploadsOptions {
|
|
32
|
+
editor: Editor;
|
|
33
|
+
/** Read on each upload, so a host can change it while the editor runs. */
|
|
34
|
+
upload: () => LoomaImageUpload | null | undefined;
|
|
35
|
+
onError: (error: unknown, file: File) => void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The editor's image uploads: each file goes to the host's uploader in turn, and the image it
|
|
39
|
+
* returns is inserted at the selection.
|
|
40
|
+
*
|
|
41
|
+
* @ownership The host owns the upload and its URL; this owns the uploading state, the failed file,
|
|
42
|
+
* and inserting the result.
|
|
43
|
+
* @failure A rejected upload leaves the document unchanged, records the file for `retry()`, and is
|
|
44
|
+
* reported through `onError`. A canceled upload leaves the document unchanged and reports nothing.
|
|
45
|
+
*/
|
|
46
|
+
export interface LoomaImageUploads {
|
|
47
|
+
readonly state: LoomaImageUploadsState;
|
|
48
|
+
/** Calls `listener` after every state change; returns the unsubscribe function. */
|
|
49
|
+
subscribe(listener: () => void): () => void;
|
|
50
|
+
/** Uploads each file in turn and inserts its image; does nothing without an uploader. */
|
|
51
|
+
insert(files: Iterable<File>): Promise<void>;
|
|
52
|
+
/** Sends the failed file again. */
|
|
53
|
+
retry(): Promise<void>;
|
|
54
|
+
/** Aborts the uploads in progress and the files still waiting their turn. */
|
|
55
|
+
cancel(): void;
|
|
56
|
+
}
|
|
57
|
+
export declare function createImageUploads(options: LoomaImageUploadsOptions): LoomaImageUploads;
|
package/editor/link-editing.d.ts
CHANGED
|
@@ -11,13 +11,13 @@ export interface LoomaLinkTarget {
|
|
|
11
11
|
* @ownership The host checks access and returns durable, openable href values.
|
|
12
12
|
* @failure A rejection leaves the existing document untouched and shows a retryable search error.
|
|
13
13
|
*/
|
|
14
|
-
export type
|
|
14
|
+
export type LinkSearch = (query: string) => Promise<LoomaLinkTarget[]>;
|
|
15
15
|
/**
|
|
16
16
|
* Describes a saved host link so a renamed record can show its current label.
|
|
17
17
|
* @ownership The host resolves identity and access; Looma keeps the saved href as is.
|
|
18
18
|
* @failure A rejection leaves the original href visible and editable.
|
|
19
19
|
*/
|
|
20
|
-
export type
|
|
20
|
+
export type LinkResolve = (href: string) => Promise<LoomaLinkTarget | null>;
|
|
21
21
|
/** The link form's state and the resolved target of the link at the caret. */
|
|
22
22
|
export interface LoomaLinkEditingState {
|
|
23
23
|
/** Where the form shows: nowhere, in the toolbar's popover, or in place of the link card. */
|
|
@@ -44,8 +44,8 @@ export interface LoomaLinkEditingState {
|
|
|
44
44
|
export interface LoomaLinkEditingOptions {
|
|
45
45
|
editor: Editor;
|
|
46
46
|
/** Read on each use, so a host can change them while the editor runs. */
|
|
47
|
-
search: () =>
|
|
48
|
-
resolve: () =>
|
|
47
|
+
search: () => LinkSearch | null | undefined;
|
|
48
|
+
resolve: () => LinkResolve | null | undefined;
|
|
49
49
|
baseUrl: () => string;
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a responder function receives besides the detail, as in HTML Next's request contract.
|
|
3
|
+
* @lifecycle `signal` aborts when the asker no longer needs the answer; `progress` reaches the asker
|
|
4
|
+
* only while the request is pending.
|
|
5
|
+
*/
|
|
6
|
+
export interface LoomaRequestInit {
|
|
7
|
+
readonly signal: AbortSignal;
|
|
8
|
+
readonly progress: (loaded: number, total: number) => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Answers one of the editor's requests: the answer, `null` for none, or a promise of either. It is
|
|
12
|
+
* the converted-framework form of an HTML Next request, so it always answers.
|
|
13
|
+
* @failure A rejection reaches the asker, which reports it as that request documents.
|
|
14
|
+
*/
|
|
15
|
+
export type LoomaResponder<Detail, Answer> = (detail: Detail, init: LoomaRequestInit) => Answer | null | PromiseLike<Answer | null>;
|
|
16
|
+
export interface LoomaRequestOptions {
|
|
17
|
+
/** Abort to withdraw the request. */
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
/** Receives the responder's progress while the request is pending. */
|
|
20
|
+
progress?: (loaded: number, total: number) => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Asks `name` at `root` as an HTML Next component's `host.request` does: dispatches a bubbling,
|
|
24
|
+
* composed `RequestEvent`, and the nearest responder's answer settles it. A responder that answers
|
|
25
|
+
* with a value, or no responder at all, settles it at once, so the answer is returned as it is; an
|
|
26
|
+
* answer given as a promise is returned as a promise.
|
|
27
|
+
*
|
|
28
|
+
* @contract Settles `null` when no listener answers during the dispatch, or when the answer is `null`.
|
|
29
|
+
* @failure Throws when the signal has already aborted. A promised answer rejects with the signal's
|
|
30
|
+
* reason when it aborts before the answer arrives, and with the answer's own rejection.
|
|
31
|
+
*/
|
|
32
|
+
export declare function askRequest<Answer>(root: Element, name: string, detail: unknown, options?: LoomaRequestOptions): Answer | null | Promise<Answer | null>;
|
|
33
|
+
/** Calls `apply` with an answer from `askRequest` now, or once its promise fulfills. */
|
|
34
|
+
export declare function whenAnswered<Answer>(answer: Answer | null | Promise<Answer | null>, apply: (answer: Answer | null) => void): void | Promise<void>;
|
|
35
|
+
/** Answers a request event with a responder function, as an HTML Next framework function prop does. */
|
|
36
|
+
export declare function respondWithFunction<Detail, Answer>(event: Event, responder: LoomaResponder<Detail, Answer>): void;
|
|
37
|
+
/**
|
|
38
|
+
* Calls `callback` with the request name when a responder announces itself, as HTML Next's
|
|
39
|
+
* `respond()` does, on `root`, one of its ancestors, or the document.
|
|
40
|
+
*
|
|
41
|
+
* @lifecycle Listens on the document until the returned function is called.
|
|
42
|
+
*/
|
|
43
|
+
export declare function onResponderAnnounced(root: Element, callback: (name: string) => void): () => void;
|
package/package.json
CHANGED
|
@@ -2,26 +2,13 @@ import "../../components/ui-editor-surface/ui-editor-surface.css";
|
|
|
2
2
|
import "./looma-editor.css";
|
|
3
3
|
import { type AnyExtension, type Editor, type JSONContent } from "@tiptap/core";
|
|
4
4
|
import { type PropType, type VNode } from "vue";
|
|
5
|
-
import { type LoomaSlashCommand, type LoomaMentionItem, type
|
|
6
|
-
import { type LoomaImageActivationDetail, type LoomaImageAttributeResolver, type
|
|
7
|
-
import { type
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*/
|
|
13
|
-
export interface LoomaImageUploadResult extends Omit<LoomaImageDescriptor, "src"> {
|
|
14
|
-
url: string;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Application-owned upload boundary used by paste, drop, and file selection.
|
|
18
|
-
*
|
|
19
|
-
* @ownership The application owns network requests, persistence, and returned
|
|
20
|
-
* URLs. Looma owns only editor insertion after the promise fulfills.
|
|
21
|
-
* @failure Rejection leaves the document unchanged and is surfaced through the
|
|
22
|
-
* editor's `uploadError` event so the host chooses retry and user messaging.
|
|
23
|
-
*/
|
|
24
|
-
export type LoomaImageUploader = (file: File) => Promise<string | LoomaImageUploadResult>;
|
|
5
|
+
import { type LoomaSlashCommand, type LoomaMentionItem, type LoomaCodeLanguages } from "@threadlabs/looma/editor";
|
|
6
|
+
import { type LoomaImageActivationDetail, type LoomaImageAttributeResolver, type LoomaImageDescriptor, type LoomaImageSourceResolver, type LoomaImageRenditionErrorDetail } from "../../editor/image-delivery.js";
|
|
7
|
+
import { type LoomaImageUploadDetail } from "../../editor/image-uploads.js";
|
|
8
|
+
import { type LoomaResponder } from "../../editor/requests.js";
|
|
9
|
+
import { type LoomaLinkTarget } from "../../editor/link-editing.js";
|
|
10
|
+
export type { LoomaImageUploadDetail } from "../../editor/image-uploads.js";
|
|
11
|
+
export type { LoomaRequestInit, LoomaResponder } from "../../editor/requests.js";
|
|
25
12
|
/**
|
|
26
13
|
* Chooses whether formatting controls follow a selection, occupy persistent
|
|
27
14
|
* editor chrome, or open from an app-owned button (`popover`, with a text-only
|
|
@@ -57,13 +44,63 @@ export interface LoomaEditorSelectionActionDetail {
|
|
|
57
44
|
before: string;
|
|
58
45
|
after: string;
|
|
59
46
|
}
|
|
47
|
+
export type { LoomaLinkTarget } from "../../editor/link-editing.js";
|
|
48
|
+
/** The `slash-commands` request's detail: this editor's default commands. */
|
|
49
|
+
export interface LoomaSlashCommandsDetail {
|
|
50
|
+
defaults: LoomaSlashCommand[];
|
|
51
|
+
}
|
|
52
|
+
/** The `link-search` request's detail: what the author typed in the link field. */
|
|
53
|
+
export interface LoomaLinkSearchDetail {
|
|
54
|
+
query: string;
|
|
55
|
+
}
|
|
56
|
+
/** The `link-resolve` request's detail: a saved link's `href`. */
|
|
57
|
+
export interface LoomaLinkResolveDetail {
|
|
58
|
+
href: string;
|
|
59
|
+
}
|
|
60
|
+
/** The `mention-query` request's detail: what the author typed after `@`, and how many results show. */
|
|
61
|
+
export interface LoomaMentionQueryDetail {
|
|
62
|
+
query: string;
|
|
63
|
+
limit: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Answers `extensions` with Tiptap extensions added after Looma's.
|
|
67
|
+
* @lifecycle The editor asks when it mounts and again when a responder first appears, then rebuilds
|
|
68
|
+
* with the answer and keeps its document.
|
|
69
|
+
*/
|
|
70
|
+
export type LoomaEditorExtensions = LoomaResponder<null, AnyExtension[]>;
|
|
71
|
+
/**
|
|
72
|
+
* Answers `slash-commands` with the commands the slash menu offers: the defaults, changed or
|
|
73
|
+
* extended. `null` keeps the defaults; an empty array offers none.
|
|
74
|
+
* @ownership The host owns its command objects and mutations; Looma asks again with fresh defaults
|
|
75
|
+
* when the prop changes or image uploads become available.
|
|
76
|
+
*/
|
|
77
|
+
export type LoomaEditorSlashCommands = LoomaResponder<LoomaSlashCommandsDetail, LoomaSlashCommand[]>;
|
|
60
78
|
/**
|
|
61
|
-
*
|
|
62
|
-
* @ownership The host
|
|
63
|
-
*
|
|
79
|
+
* Answers `link-search` with host records for a link, without teaching the editor how they are stored.
|
|
80
|
+
* @ownership The host checks access and returns durable, openable `href` values.
|
|
81
|
+
* @failure A rejection leaves the document untouched and shows a retryable search error.
|
|
64
82
|
*/
|
|
65
|
-
export type
|
|
66
|
-
|
|
83
|
+
export type LoomaLinkSearch = LoomaResponder<LoomaLinkSearchDetail, LoomaLinkTarget[]>;
|
|
84
|
+
/**
|
|
85
|
+
* Answers `link-resolve` with a saved link's current record, so a renamed record shows its new label.
|
|
86
|
+
* @ownership The host resolves identity and access; Looma keeps the saved href as is.
|
|
87
|
+
* @failure A rejection or `null` leaves the original href visible and editable.
|
|
88
|
+
*/
|
|
89
|
+
export type LoomaLinkResolve = LoomaResponder<LoomaLinkResolveDetail, LoomaLinkTarget>;
|
|
90
|
+
/**
|
|
91
|
+
* Answers `mention-query` with people to suggest after `@`; `null` falls back to `mentionItems`.
|
|
92
|
+
* @ownership The host owns directory access; Looma filters the answer and clamps it to `limit`.
|
|
93
|
+
* @failure A rejection shows an empty menu.
|
|
94
|
+
*/
|
|
95
|
+
export type LoomaMentionQuery = LoomaResponder<LoomaMentionQueryDetail, LoomaMentionItem[]>;
|
|
96
|
+
/**
|
|
97
|
+
* Answers `image-upload` with the image to insert for a pasted, dropped, or chosen file, or `null`
|
|
98
|
+
* to insert nothing. `progress(loaded, total)` drives the editor's progress status, and `signal`
|
|
99
|
+
* aborts when the author cancels.
|
|
100
|
+
* @ownership The application owns the upload and the returned `src`; Looma owns inserting it.
|
|
101
|
+
* @failure A rejection leaves the document unchanged and emits `uploadError`.
|
|
102
|
+
*/
|
|
103
|
+
export type LoomaImageUploader = LoomaResponder<LoomaImageUploadDetail, LoomaImageDescriptor>;
|
|
67
104
|
/**
|
|
68
105
|
* Turnkey Vue editor that composes Looma's framework-neutral Tiptap extensions
|
|
69
106
|
* with declarative menus, image delivery, and table interaction chrome.
|
|
@@ -110,13 +147,14 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
110
147
|
type: StringConstructor;
|
|
111
148
|
default: string;
|
|
112
149
|
};
|
|
150
|
+
/** Answers `extensions`: the application's Tiptap extensions, added after Looma's. */
|
|
113
151
|
extensions: {
|
|
114
|
-
type: PropType<
|
|
115
|
-
default:
|
|
152
|
+
type: PropType<LoomaEditorExtensions | undefined>;
|
|
153
|
+
default: undefined;
|
|
116
154
|
};
|
|
117
|
-
/**
|
|
155
|
+
/** Answers `slash-commands`: the slash menu's commands, given the defaults. */
|
|
118
156
|
slashCommands: {
|
|
119
|
-
type: PropType<LoomaEditorSlashCommands>;
|
|
157
|
+
type: PropType<LoomaEditorSlashCommands | undefined>;
|
|
120
158
|
default: undefined;
|
|
121
159
|
};
|
|
122
160
|
/** Override the lazy built-in catalog with application-owned code grammars. An empty object disables highlighting. */
|
|
@@ -128,8 +166,9 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
128
166
|
type: PropType<LoomaMentionItem[]>;
|
|
129
167
|
default: () => never[];
|
|
130
168
|
};
|
|
131
|
-
|
|
132
|
-
|
|
169
|
+
/** Answers `mention-query`: people to suggest for what the author typed after `@`. */
|
|
170
|
+
mentionQuery: {
|
|
171
|
+
type: PropType<LoomaMentionQuery | undefined>;
|
|
133
172
|
default: undefined;
|
|
134
173
|
};
|
|
135
174
|
mentionLimit: {
|
|
@@ -141,7 +180,7 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
141
180
|
type: StringConstructor;
|
|
142
181
|
default: string;
|
|
143
182
|
};
|
|
144
|
-
/**
|
|
183
|
+
/** Answers `link-search` and `link-resolve`: host search and identity lookup for links to application records. */
|
|
145
184
|
linkSearch: {
|
|
146
185
|
type: PropType<LoomaLinkSearch | undefined>;
|
|
147
186
|
default: undefined;
|
|
@@ -154,7 +193,8 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
154
193
|
type: StringConstructor;
|
|
155
194
|
default: string;
|
|
156
195
|
};
|
|
157
|
-
|
|
196
|
+
/** Answers `image-upload`: uploads a pasted, dropped, or chosen image and returns the image to insert. */
|
|
197
|
+
imageUpload: {
|
|
158
198
|
type: PropType<LoomaImageUploader | undefined>;
|
|
159
199
|
default: undefined;
|
|
160
200
|
};
|
|
@@ -252,13 +292,14 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
252
292
|
type: StringConstructor;
|
|
253
293
|
default: string;
|
|
254
294
|
};
|
|
295
|
+
/** Answers `extensions`: the application's Tiptap extensions, added after Looma's. */
|
|
255
296
|
extensions: {
|
|
256
|
-
type: PropType<
|
|
257
|
-
default:
|
|
297
|
+
type: PropType<LoomaEditorExtensions | undefined>;
|
|
298
|
+
default: undefined;
|
|
258
299
|
};
|
|
259
|
-
/**
|
|
300
|
+
/** Answers `slash-commands`: the slash menu's commands, given the defaults. */
|
|
260
301
|
slashCommands: {
|
|
261
|
-
type: PropType<LoomaEditorSlashCommands>;
|
|
302
|
+
type: PropType<LoomaEditorSlashCommands | undefined>;
|
|
262
303
|
default: undefined;
|
|
263
304
|
};
|
|
264
305
|
/** Override the lazy built-in catalog with application-owned code grammars. An empty object disables highlighting. */
|
|
@@ -270,8 +311,9 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
270
311
|
type: PropType<LoomaMentionItem[]>;
|
|
271
312
|
default: () => never[];
|
|
272
313
|
};
|
|
273
|
-
|
|
274
|
-
|
|
314
|
+
/** Answers `mention-query`: people to suggest for what the author typed after `@`. */
|
|
315
|
+
mentionQuery: {
|
|
316
|
+
type: PropType<LoomaMentionQuery | undefined>;
|
|
275
317
|
default: undefined;
|
|
276
318
|
};
|
|
277
319
|
mentionLimit: {
|
|
@@ -283,7 +325,7 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
283
325
|
type: StringConstructor;
|
|
284
326
|
default: string;
|
|
285
327
|
};
|
|
286
|
-
/**
|
|
328
|
+
/** Answers `link-search` and `link-resolve`: host search and identity lookup for links to application records. */
|
|
287
329
|
linkSearch: {
|
|
288
330
|
type: PropType<LoomaLinkSearch | undefined>;
|
|
289
331
|
default: undefined;
|
|
@@ -296,7 +338,8 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
296
338
|
type: StringConstructor;
|
|
297
339
|
default: string;
|
|
298
340
|
};
|
|
299
|
-
|
|
341
|
+
/** Answers `image-upload`: uploads a pasted, dropped, or chosen image and returns the image to insert. */
|
|
342
|
+
imageUpload: {
|
|
300
343
|
type: PropType<LoomaImageUploader | undefined>;
|
|
301
344
|
default: undefined;
|
|
302
345
|
};
|
|
@@ -359,7 +402,7 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
359
402
|
onImageActivate?: ((_detail: LoomaImageActivationDetail) => any) | undefined;
|
|
360
403
|
onImageRenditionError?: ((_detail: LoomaImageRenditionErrorDetail) => any) | undefined;
|
|
361
404
|
}>, {
|
|
362
|
-
extensions:
|
|
405
|
+
extensions: LoomaEditorExtensions | undefined;
|
|
363
406
|
editable: boolean;
|
|
364
407
|
label: string;
|
|
365
408
|
placeholder: string;
|
|
@@ -367,15 +410,15 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
367
410
|
disableHighlight: boolean;
|
|
368
411
|
codeLanguages: Readonly<Record<string, import("highlight.js").LanguageFn>> | null;
|
|
369
412
|
modelValue: JSONContent;
|
|
413
|
+
slashCommands: LoomaEditorSlashCommands | undefined;
|
|
414
|
+
mentionQuery: LoomaMentionQuery | undefined;
|
|
415
|
+
linkSearch: LoomaLinkSearch | undefined;
|
|
416
|
+
linkResolve: LoomaLinkResolve | undefined;
|
|
417
|
+
imageUpload: LoomaImageUploader | undefined;
|
|
370
418
|
contentDensity: "compact" | "comfortable";
|
|
371
|
-
slashCommands: LoomaEditorSlashCommands;
|
|
372
419
|
mentionItems: LoomaMentionItem[];
|
|
373
|
-
mentionProvider: LoomaMentionProvider | undefined;
|
|
374
420
|
mentionLimit: number;
|
|
375
|
-
linkSearch: LoomaLinkSearch | undefined;
|
|
376
|
-
linkResolve: LoomaLinkResolve | undefined;
|
|
377
421
|
linkTargetLabel: string;
|
|
378
|
-
uploadImage: LoomaImageUploader | undefined;
|
|
379
422
|
resolveImageAttributes: LoomaImageAttributeResolver | undefined;
|
|
380
423
|
resolveImageSource: LoomaImageSourceResolver | undefined;
|
|
381
424
|
toolbarMode: LoomaEditorToolbarMode;
|
package/vue/editor/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export * from "@threadlabs/looma/editor";
|
|
2
2
|
export * from "./primitives.js";
|
|
3
|
-
export { LoomaEditor, type LoomaEditorToolbarMode, type LoomaEditorSelectionAction, type LoomaEditorSelectionActionDetail, type LoomaEditorRangeAction, type LoomaEditorRangeActionDetail, type LoomaEditorSlashCommands, type LoomaLinkTarget, type LoomaLinkSearch, type LoomaLinkResolve, type LoomaImageUploader, type
|
|
3
|
+
export { LoomaEditor, type LoomaEditorToolbarMode, type LoomaEditorSelectionAction, type LoomaEditorSelectionActionDetail, type LoomaEditorRangeAction, type LoomaEditorRangeActionDetail, type LoomaEditorExtensions, type LoomaEditorSlashCommands, type LoomaSlashCommandsDetail, type LoomaLinkTarget, type LoomaLinkSearch, type LoomaLinkSearchDetail, type LoomaLinkResolve, type LoomaLinkResolveDetail, type LoomaMentionQuery, type LoomaMentionQueryDetail, type LoomaImageUploader, type LoomaImageUploadDetail, type LoomaRequestInit, type LoomaResponder, } from "./LoomaEditor.js";
|
|
4
4
|
export type { LoomaImageActivationDetail, LoomaImageActivationTrigger, LoomaImageAttributeResolver, LoomaImageSourceResolver, LoomaImageDescriptor, LoomaImageRenditionErrorDetail, LoomaImageRenditionAttributes, } from "../../editor/image-delivery.js";
|
package/vue/editor/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { computed, createApp, defineComponent, h, nextTick, onBeforeUnmount, onMounted, reactive, ref, shallowRef, useId, watch } from "vue";
|
|
1
|
+
import { computed, createApp, defineComponent, h, nextTick, onBeforeUnmount, onMounted, onUpdated, reactive, ref, shallowRef, useId, watch } from "vue";
|
|
2
2
|
import { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_ICONS, createLoomaMentionExtension, createLoomaSlashCommandExtension, getActiveTableUiState, getDefaultEditorExtensions, getDefaultSlashCommands, getTableOfContentsEntries, getVisualViewportRect, handleTableAction, handleTableOverlayAction, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeLoomaChipColor, normalizeTableOfContentsConfig } from "@threadlabs/looma/editor";
|
|
3
|
-
import { Button, Callout, Card, Checkbox, Cluster, Disclosure, EditorBlockActions, EditorChipEditor, EditorCodeLanguage, EditorImageToolbar, EditorInsertTableGrid, EditorLinkCard, EditorLinkForm, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, EditorSurface, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, FormField, Icon, IconButton, Image, Input, List, Menu, MenuItem, Popover, Separator, Stack, Text, Tooltip } from "@threadlabs/looma/vue";
|
|
3
|
+
import { Button, Callout, Card, Checkbox, Cluster, Disclosure, EditorBlockActions, EditorChipEditor, EditorCodeLanguage, EditorImageToolbar, EditorInsertTableGrid, EditorLinkCard, EditorLinkForm, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, EditorSurface, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, FormField, Icon, IconButton, Image, Input, List, Menu, MenuItem, Popover, Separator, Stack, StatusMessage, Text, Tooltip } from "@threadlabs/looma/vue";
|
|
4
4
|
import { Editor, Extension, Node as Node$1, getText, getTextSerializersFromSchema, isNodeSelection, mergeAttributes, nodeInputRule, posToDOMRect } from "@tiptap/core";
|
|
5
5
|
import { Editor as Editor$1 } from "@tiptap/vue-3";
|
|
6
6
|
import { closeHistory } from "@tiptap/pm/history";
|
|
@@ -1138,6 +1138,222 @@ function runBlockAction(editor, action) {
|
|
|
1138
1138
|
return true;
|
|
1139
1139
|
}
|
|
1140
1140
|
//#endregion
|
|
1141
|
+
//#region src/editor/image-uploads.ts
|
|
1142
|
+
function createImageUploads(options) {
|
|
1143
|
+
const { editor } = options;
|
|
1144
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1145
|
+
const batches = /* @__PURE__ */ new Set();
|
|
1146
|
+
let state = {
|
|
1147
|
+
uploading: false,
|
|
1148
|
+
progress: null,
|
|
1149
|
+
failed: null
|
|
1150
|
+
};
|
|
1151
|
+
const set = (patch) => {
|
|
1152
|
+
state = {
|
|
1153
|
+
...state,
|
|
1154
|
+
...patch
|
|
1155
|
+
};
|
|
1156
|
+
listeners.forEach((listener) => listener());
|
|
1157
|
+
};
|
|
1158
|
+
const insertOne = async (file, signal) => {
|
|
1159
|
+
const upload = options.upload();
|
|
1160
|
+
if (!upload || editor.isDestroyed || signal.aborted) return;
|
|
1161
|
+
set({
|
|
1162
|
+
uploading: true,
|
|
1163
|
+
progress: null
|
|
1164
|
+
});
|
|
1165
|
+
try {
|
|
1166
|
+
const image = await upload(file, {
|
|
1167
|
+
signal,
|
|
1168
|
+
progress: (loaded, total) => {
|
|
1169
|
+
if (!signal.aborted) set({ progress: {
|
|
1170
|
+
loaded,
|
|
1171
|
+
total
|
|
1172
|
+
} });
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
if (image && !signal.aborted && !editor.isDestroyed) {
|
|
1176
|
+
const width = normalizeImageDimension(image.width);
|
|
1177
|
+
const height = normalizeImageDimension(image.height);
|
|
1178
|
+
editor.chain().focus().insertContent(imageUploadContent({
|
|
1179
|
+
src: image.src,
|
|
1180
|
+
alt: image.alt ?? file.name,
|
|
1181
|
+
...width ? { width } : {},
|
|
1182
|
+
...height ? { height } : {},
|
|
1183
|
+
...image.responsive === true ? { responsive: true } : {}
|
|
1184
|
+
})).run();
|
|
1185
|
+
}
|
|
1186
|
+
if (!signal.aborted) set({ failed: null });
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
if (signal.aborted) return;
|
|
1189
|
+
set({ failed: file });
|
|
1190
|
+
options.onError(error, file);
|
|
1191
|
+
} finally {
|
|
1192
|
+
set({
|
|
1193
|
+
uploading: false,
|
|
1194
|
+
progress: null
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
const run = async (files) => {
|
|
1199
|
+
const batch = new AbortController();
|
|
1200
|
+
batches.add(batch);
|
|
1201
|
+
try {
|
|
1202
|
+
for (const file of files) await insertOne(file, batch.signal);
|
|
1203
|
+
} finally {
|
|
1204
|
+
batches.delete(batch);
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
return {
|
|
1208
|
+
get state() {
|
|
1209
|
+
return state;
|
|
1210
|
+
},
|
|
1211
|
+
subscribe(listener) {
|
|
1212
|
+
listeners.add(listener);
|
|
1213
|
+
return () => {
|
|
1214
|
+
listeners.delete(listener);
|
|
1215
|
+
};
|
|
1216
|
+
},
|
|
1217
|
+
insert: run,
|
|
1218
|
+
async retry() {
|
|
1219
|
+
if (state.failed) await run([state.failed]);
|
|
1220
|
+
},
|
|
1221
|
+
cancel() {
|
|
1222
|
+
batches.forEach((batch) => batch.abort(new DOMException("The upload was canceled.", "AbortError")));
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
//#endregion
|
|
1227
|
+
//#region ../../node_modules/.pnpm/@nextwebwg+html-next@1.0.0-alpha.53/node_modules/@nextwebwg/html-next/dist/requests.js
|
|
1228
|
+
/**
|
|
1229
|
+
* Requests: behavior a component asks its consumer for, answered through a native event's
|
|
1230
|
+
* `respondWith` (https://nextwebwg.org/declarative-components/javascript#requests).
|
|
1231
|
+
*
|
|
1232
|
+
* Every HTML Next bundle on a page (the live runtime, compiled modules and framework helpers) keeps
|
|
1233
|
+
* its requests in one record on the document, so any consumer answers any component, and one
|
|
1234
|
+
* document listener relays late responders. Converted framework components carry this module's
|
|
1235
|
+
* source (scripts/generate-literal-source.ts), so it imports nothing.
|
|
1236
|
+
*/
|
|
1237
|
+
var ports = /* @__PURE__ */ new WeakMap();
|
|
1238
|
+
var port = (event) => {
|
|
1239
|
+
let found = ports.get(event);
|
|
1240
|
+
if (found === void 0) ports.set(event, found = {});
|
|
1241
|
+
return found;
|
|
1242
|
+
};
|
|
1243
|
+
/** A request as its responders receive it: the nearest one answers with `respondWith`. */
|
|
1244
|
+
var RequestEvent = class extends CustomEvent {
|
|
1245
|
+
/** Aborts when the asker no longer needs the answer, or its component disconnects. */
|
|
1246
|
+
signal;
|
|
1247
|
+
constructor(type, init) {
|
|
1248
|
+
super(type, init);
|
|
1249
|
+
this.signal = init.signal;
|
|
1250
|
+
}
|
|
1251
|
+
/** Answer the request with a value or a promise of one; the event stops propagating. */
|
|
1252
|
+
respondWith(answer) {
|
|
1253
|
+
const recorded = port(this);
|
|
1254
|
+
if (this.eventPhase === 0 || recorded.a !== void 0) throw new DOMException(recorded.a === void 0 ? "A request is answered while it is dispatched." : "The request already has an answer.", "InvalidStateError");
|
|
1255
|
+
recorded.a = [answer];
|
|
1256
|
+
this.stopImmediatePropagation();
|
|
1257
|
+
}
|
|
1258
|
+
/** Report progress to the asker while the request is pending. */
|
|
1259
|
+
progress(loaded, total) {
|
|
1260
|
+
const report = this.signal.aborted ? void 0 : ports.get(this)?.p;
|
|
1261
|
+
try {
|
|
1262
|
+
report?.(loaded, total);
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
reportError(error);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
//#endregion
|
|
1269
|
+
//#region src/editor/requests.ts
|
|
1270
|
+
var AskedRequest = class extends RequestEvent {
|
|
1271
|
+
answered = false;
|
|
1272
|
+
answer;
|
|
1273
|
+
report;
|
|
1274
|
+
respondWith(answer) {
|
|
1275
|
+
super.respondWith(answer);
|
|
1276
|
+
this.answered = true;
|
|
1277
|
+
this.answer = answer;
|
|
1278
|
+
}
|
|
1279
|
+
progress(loaded, total) {
|
|
1280
|
+
if (this.signal.aborted) return;
|
|
1281
|
+
try {
|
|
1282
|
+
this.report?.(loaded, total);
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
reportError(error);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
/** Settles with `answer`, or rejects with the signal's reason when it aborts first. */
|
|
1289
|
+
var settle = (answer, signal) => new Promise((resolve, reject) => {
|
|
1290
|
+
const abort = () => reject(signal.reason);
|
|
1291
|
+
if (signal.aborted) abort();
|
|
1292
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1293
|
+
Promise.resolve(answer).then((value) => {
|
|
1294
|
+
signal.removeEventListener("abort", abort);
|
|
1295
|
+
resolve(value);
|
|
1296
|
+
}, (error) => {
|
|
1297
|
+
signal.removeEventListener("abort", abort);
|
|
1298
|
+
reject(error);
|
|
1299
|
+
});
|
|
1300
|
+
});
|
|
1301
|
+
/**
|
|
1302
|
+
* Asks `name` at `root` as an HTML Next component's `host.request` does: dispatches a bubbling,
|
|
1303
|
+
* composed `RequestEvent`, and the nearest responder's answer settles it. A responder that answers
|
|
1304
|
+
* with a value, or no responder at all, settles it at once, so the answer is returned as it is; an
|
|
1305
|
+
* answer given as a promise is returned as a promise.
|
|
1306
|
+
*
|
|
1307
|
+
* @contract Settles `null` when no listener answers during the dispatch, or when the answer is `null`.
|
|
1308
|
+
* @failure Throws when the signal has already aborted. A promised answer rejects with the signal's
|
|
1309
|
+
* reason when it aborts before the answer arrives, and with the answer's own rejection.
|
|
1310
|
+
*/
|
|
1311
|
+
function askRequest(root, name, detail, options = {}) {
|
|
1312
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
1313
|
+
signal.throwIfAborted();
|
|
1314
|
+
const event = new AskedRequest(name, {
|
|
1315
|
+
detail: detail ?? null,
|
|
1316
|
+
signal,
|
|
1317
|
+
bubbles: true,
|
|
1318
|
+
composed: true
|
|
1319
|
+
});
|
|
1320
|
+
event.report = options.progress;
|
|
1321
|
+
root.dispatchEvent(event);
|
|
1322
|
+
const answer = event.answered ? event.answer : null;
|
|
1323
|
+
if (!isThenable(answer)) {
|
|
1324
|
+
event.report = void 0;
|
|
1325
|
+
return answer ?? null;
|
|
1326
|
+
}
|
|
1327
|
+
return settle(answer, signal).then((value) => value ?? null).finally(() => {
|
|
1328
|
+
event.report = void 0;
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
var isThenable = (value) => typeof value?.then === "function";
|
|
1332
|
+
/** Answers a request event with a responder function, as an HTML Next framework function prop does. */
|
|
1333
|
+
function respondWithFunction(event, responder) {
|
|
1334
|
+
const request = event;
|
|
1335
|
+
request.respondWith(responder(request.detail, {
|
|
1336
|
+
signal: request.signal,
|
|
1337
|
+
progress: (loaded, total) => request.progress(loaded, total)
|
|
1338
|
+
}));
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Calls `callback` with the request name when a responder announces itself, as HTML Next's
|
|
1342
|
+
* `respond()` does, on `root`, one of its ancestors, or the document.
|
|
1343
|
+
*
|
|
1344
|
+
* @lifecycle Listens on the document until the returned function is called.
|
|
1345
|
+
*/
|
|
1346
|
+
function onResponderAnnounced(root, callback) {
|
|
1347
|
+
const document = root.ownerDocument;
|
|
1348
|
+
const listener = (event) => {
|
|
1349
|
+
const name = event.detail?.name;
|
|
1350
|
+
const target = event.target;
|
|
1351
|
+
if (typeof name === "string" && target instanceof Node && target.contains(root)) callback(name);
|
|
1352
|
+
};
|
|
1353
|
+
document.addEventListener("responder", listener);
|
|
1354
|
+
return () => document.removeEventListener("responder", listener);
|
|
1355
|
+
}
|
|
1356
|
+
//#endregion
|
|
1141
1357
|
//#region src/editor/formatting-selection.ts
|
|
1142
1358
|
var HOLD_MS = 500;
|
|
1143
1359
|
var HOLD_SLOP_PX = 8;
|
|
@@ -2758,10 +2974,20 @@ var imageNodeView = ({ editor, node, getPos, decorations }) => {
|
|
|
2758
2974
|
};
|
|
2759
2975
|
//#endregion
|
|
2760
2976
|
//#region src/vue/editor/LoomaEditor.ts
|
|
2977
|
+
var EDITOR_REQUESTS = {
|
|
2978
|
+
extensions: "extensions",
|
|
2979
|
+
"slash-commands": "slashCommands",
|
|
2980
|
+
"mention-query": "mentionQuery",
|
|
2981
|
+
"link-search": "linkSearch",
|
|
2982
|
+
"link-resolve": "linkResolve",
|
|
2983
|
+
"image-upload": "imageUpload"
|
|
2984
|
+
};
|
|
2985
|
+
var EDITOR_REQUEST_NAMES = Object.keys(EDITOR_REQUESTS);
|
|
2761
2986
|
var EMPTY_DOCUMENT = {
|
|
2762
2987
|
type: "doc",
|
|
2763
2988
|
content: []
|
|
2764
2989
|
};
|
|
2990
|
+
var uploadProgressText = (progress) => progress && progress.total > 0 ? `Uploading image… ${Math.round(Math.min(1, progress.loaded / progress.total) * 100)}%` : "Uploading image…";
|
|
2765
2991
|
var editorInstanceSequence = 0;
|
|
2766
2992
|
var EMPTY_CAPABILITIES = {
|
|
2767
2993
|
canToggleHeaderRow: false,
|
|
@@ -2854,13 +3080,14 @@ var LoomaEditor = defineComponent({
|
|
|
2854
3080
|
type: String,
|
|
2855
3081
|
default: "Document"
|
|
2856
3082
|
},
|
|
3083
|
+
/** Answers `extensions`: the application's Tiptap extensions, added after Looma's. */
|
|
2857
3084
|
extensions: {
|
|
2858
|
-
type:
|
|
2859
|
-
default:
|
|
3085
|
+
type: Function,
|
|
3086
|
+
default: void 0
|
|
2860
3087
|
},
|
|
2861
|
-
/**
|
|
3088
|
+
/** Answers `slash-commands`: the slash menu's commands, given the defaults. */
|
|
2862
3089
|
slashCommands: {
|
|
2863
|
-
type:
|
|
3090
|
+
type: Function,
|
|
2864
3091
|
default: void 0
|
|
2865
3092
|
},
|
|
2866
3093
|
/** Override the lazy built-in catalog with application-owned code grammars. An empty object disables highlighting. */
|
|
@@ -2872,7 +3099,8 @@ var LoomaEditor = defineComponent({
|
|
|
2872
3099
|
type: Array,
|
|
2873
3100
|
default: () => []
|
|
2874
3101
|
},
|
|
2875
|
-
|
|
3102
|
+
/** Answers `mention-query`: people to suggest for what the author typed after `@`. */
|
|
3103
|
+
mentionQuery: {
|
|
2876
3104
|
type: Function,
|
|
2877
3105
|
default: void 0
|
|
2878
3106
|
},
|
|
@@ -2885,7 +3113,7 @@ var LoomaEditor = defineComponent({
|
|
|
2885
3113
|
type: String,
|
|
2886
3114
|
default: ""
|
|
2887
3115
|
},
|
|
2888
|
-
/**
|
|
3116
|
+
/** Answers `link-search` and `link-resolve`: host search and identity lookup for links to application records. */
|
|
2889
3117
|
linkSearch: {
|
|
2890
3118
|
type: Function,
|
|
2891
3119
|
default: void 0
|
|
@@ -2898,7 +3126,8 @@ var LoomaEditor = defineComponent({
|
|
|
2898
3126
|
type: String,
|
|
2899
3127
|
default: "Item"
|
|
2900
3128
|
},
|
|
2901
|
-
|
|
3129
|
+
/** Answers `image-upload`: uploads a pasted, dropped, or chosen image and returns the image to insert. */
|
|
3130
|
+
imageUpload: {
|
|
2902
3131
|
type: Function,
|
|
2903
3132
|
default: void 0
|
|
2904
3133
|
},
|
|
@@ -3025,8 +3254,12 @@ var LoomaEditor = defineComponent({
|
|
|
3025
3254
|
const mobileToolbarShell = ref(null);
|
|
3026
3255
|
const codeLanguageShell = ref(null);
|
|
3027
3256
|
const dragOver = ref(false);
|
|
3028
|
-
|
|
3029
|
-
const
|
|
3257
|
+
let uploads;
|
|
3258
|
+
const uploadState = shallowRef({
|
|
3259
|
+
uploading: false,
|
|
3260
|
+
progress: null,
|
|
3261
|
+
failed: null
|
|
3262
|
+
});
|
|
3030
3263
|
const tablePickerOpen = ref(false);
|
|
3031
3264
|
const mobileToolbarStyle = ref({});
|
|
3032
3265
|
const mobile = ref(typeof window !== "undefined" && window.innerWidth <= 767);
|
|
@@ -3121,12 +3354,77 @@ var LoomaEditor = defineComponent({
|
|
|
3121
3354
|
chipOpen.value = false;
|
|
3122
3355
|
if (focusChip) root.value?.querySelector(`#${chipAnchorId}`)?.focus();
|
|
3123
3356
|
};
|
|
3124
|
-
const
|
|
3125
|
-
|
|
3126
|
-
|
|
3357
|
+
const found = reactive(/* @__PURE__ */ new Set());
|
|
3358
|
+
const answers = (name) => {
|
|
3359
|
+
const prop = EDITOR_REQUESTS[name];
|
|
3360
|
+
return typeof props[prop] === "function" || attrs[`on${prop[0].toUpperCase()}${prop.slice(1)}`] != null || found.has(name);
|
|
3361
|
+
};
|
|
3362
|
+
const connection = new AbortController();
|
|
3363
|
+
const ask = (name, detail, options = {}) => {
|
|
3364
|
+
if (!root.value || connection.signal.aborted) return null;
|
|
3365
|
+
const signal = options.signal ? AbortSignal.any([options.signal, connection.signal]) : connection.signal;
|
|
3366
|
+
return askRequest(root.value, name, detail, {
|
|
3367
|
+
...options,
|
|
3368
|
+
signal
|
|
3369
|
+
});
|
|
3370
|
+
};
|
|
3371
|
+
const latest = /* @__PURE__ */ new Map();
|
|
3372
|
+
const askLatest = (name, detail) => {
|
|
3373
|
+
latest.get(name)?.abort();
|
|
3374
|
+
const controller = new AbortController();
|
|
3375
|
+
latest.set(name, controller);
|
|
3376
|
+
return ask(name, detail, { signal: controller.signal });
|
|
3377
|
+
};
|
|
3378
|
+
const askThen = (name, detail, apply) => {
|
|
3379
|
+
const answer = askLatest(name, detail);
|
|
3380
|
+
if (!(answer instanceof Promise)) return apply(answer);
|
|
3381
|
+
const controller = latest.get(name);
|
|
3382
|
+
answer.then(apply, (error) => {
|
|
3383
|
+
if (controller !== latest.get(name) || connection.signal.aborted) return;
|
|
3384
|
+
reportError(error);
|
|
3385
|
+
apply(null);
|
|
3386
|
+
});
|
|
3387
|
+
};
|
|
3388
|
+
let slashDefaults;
|
|
3389
|
+
const defaultSlashCommands = () => {
|
|
3390
|
+
const uploads = answers("image-upload");
|
|
3391
|
+
if (slashDefaults?.uploads !== uploads) slashDefaults = {
|
|
3392
|
+
uploads,
|
|
3393
|
+
commands: getDefaultSlashCommands(uploads ? () => fileInput.value?.click() : void 0, openChipEditor, () => openLinkEditor())
|
|
3394
|
+
};
|
|
3395
|
+
return slashDefaults.commands;
|
|
3396
|
+
};
|
|
3397
|
+
const askSlashCommands = () => {
|
|
3398
|
+
const defaults = defaultSlashCommands();
|
|
3399
|
+
askThen("slash-commands", { defaults: [...defaults] }, (answer) => {
|
|
3400
|
+
const commands = answer ?? defaults;
|
|
3401
|
+
const current = slashExtension.options.commands;
|
|
3402
|
+
if (commands.length === current.length && commands.every((command, index) => command === current[index])) return;
|
|
3403
|
+
slashExtension.options.commands = commands;
|
|
3404
|
+
editor.value?.commands.dismissLoomaSlashMenu();
|
|
3405
|
+
});
|
|
3406
|
+
};
|
|
3407
|
+
let consumerExtensions = [];
|
|
3408
|
+
const askExtensions = () => askThen("extensions", null, (extensions) => {
|
|
3409
|
+
const changed = Boolean(extensions?.length || consumerExtensions.length);
|
|
3410
|
+
consumerExtensions = extensions ?? [];
|
|
3411
|
+
if (!session) build(props.modelValue);
|
|
3412
|
+
else if (changed) rebuild();
|
|
3413
|
+
});
|
|
3414
|
+
let responders = /* @__PURE__ */ new Set();
|
|
3415
|
+
const checkResponders = () => {
|
|
3416
|
+
const current = new Set(EDITOR_REQUEST_NAMES.filter(answers));
|
|
3417
|
+
const arrived = (name) => current.has(name) && !responders.has(name);
|
|
3418
|
+
const extensionsArrived = arrived("extensions");
|
|
3419
|
+
const slashChanged = arrived("slash-commands") || current.has("image-upload") !== responders.has("image-upload");
|
|
3420
|
+
responders = current;
|
|
3421
|
+
if (extensionsArrived) askExtensions();
|
|
3422
|
+
if (slashChanged) askSlashCommands();
|
|
3127
3423
|
};
|
|
3424
|
+
onUpdated(checkResponders);
|
|
3425
|
+
let stopAnnouncements;
|
|
3128
3426
|
const slashExtension = createLoomaSlashCommandExtension({
|
|
3129
|
-
commands:
|
|
3427
|
+
commands: defaultSlashCommands(),
|
|
3130
3428
|
onOpenImagePicker: () => fileInput.value?.click(),
|
|
3131
3429
|
onOpenChipEditor: openChipEditor,
|
|
3132
3430
|
onOpenLinkEditor: () => openLinkEditor(),
|
|
@@ -3135,13 +3433,13 @@ var LoomaEditor = defineComponent({
|
|
|
3135
3433
|
}
|
|
3136
3434
|
});
|
|
3137
3435
|
const mentionMenuId = `looma-editor-mention-menu-${++editorInstanceSequence}`;
|
|
3138
|
-
const mentionExtension =
|
|
3436
|
+
const mentionExtension = createLoomaMentionExtension({
|
|
3139
3437
|
limit: props.mentionLimit,
|
|
3140
3438
|
menuId: mentionMenuId,
|
|
3141
|
-
items: async (query, context) => {
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
},
|
|
3439
|
+
items: async (query, context) => await askLatest("mention-query", {
|
|
3440
|
+
query,
|
|
3441
|
+
limit: context.limit
|
|
3442
|
+
}) ?? props.mentionItems,
|
|
3145
3443
|
onStateChange: (state) => {
|
|
3146
3444
|
Object.assign(mention, state);
|
|
3147
3445
|
}
|
|
@@ -3173,13 +3471,16 @@ var LoomaEditor = defineComponent({
|
|
|
3173
3471
|
const loadForCodeBlock = codeHighlighting.loadForCodeBlock;
|
|
3174
3472
|
const editor = shallowRef();
|
|
3175
3473
|
let session;
|
|
3176
|
-
|
|
3474
|
+
let edited = false;
|
|
3475
|
+
const build = (content) => {
|
|
3476
|
+
edited = false;
|
|
3477
|
+
const consumerMention = consumerExtensions.some((extension) => extension.name === "mention");
|
|
3177
3478
|
session = createEditorSession({
|
|
3178
3479
|
extensions: [
|
|
3179
3480
|
...getDefaultEditorExtensions({
|
|
3180
3481
|
placeholder: props.placeholder,
|
|
3181
3482
|
linkBaseUrl: props.linkBaseUrl,
|
|
3182
|
-
mention:
|
|
3483
|
+
mention: consumerMention ? false : mentionExtension,
|
|
3183
3484
|
disableHighlight: props.disableHighlight,
|
|
3184
3485
|
codeLanguages: props.codeLanguages ?? void 0,
|
|
3185
3486
|
codeLowlight: codeHighlighting.lowlight,
|
|
@@ -3191,9 +3492,9 @@ var LoomaEditor = defineComponent({
|
|
|
3191
3492
|
}),
|
|
3192
3493
|
imageDelivery.extension,
|
|
3193
3494
|
slashExtension,
|
|
3194
|
-
...
|
|
3495
|
+
...consumerExtensions
|
|
3195
3496
|
],
|
|
3196
|
-
content
|
|
3497
|
+
content,
|
|
3197
3498
|
editable: props.editable,
|
|
3198
3499
|
editorProps: {
|
|
3199
3500
|
nodeViews: { image: () => ({ dom: document.createElement("span") }) },
|
|
@@ -3243,6 +3544,7 @@ var LoomaEditor = defineComponent({
|
|
|
3243
3544
|
}
|
|
3244
3545
|
},
|
|
3245
3546
|
onUpdate: ({ editor: instance }) => {
|
|
3547
|
+
edited = true;
|
|
3246
3548
|
const value = instance.getJSON();
|
|
3247
3549
|
emit("update:modelValue", value);
|
|
3248
3550
|
emit("update", value);
|
|
@@ -3255,14 +3557,30 @@ var LoomaEditor = defineComponent({
|
|
|
3255
3557
|
editor.value = session.editor;
|
|
3256
3558
|
const engine = createLinkEditing({
|
|
3257
3559
|
editor: session.editor,
|
|
3258
|
-
search: () =>
|
|
3259
|
-
resolve: () =>
|
|
3560
|
+
search: () => answers("link-search") ? async (query) => await askLatest("link-search", { query }) ?? [] : null,
|
|
3561
|
+
resolve: () => async (href) => await askLatest("link-resolve", { href }),
|
|
3260
3562
|
baseUrl: () => props.linkBaseUrl
|
|
3261
3563
|
});
|
|
3262
3564
|
engine.subscribe(() => {
|
|
3263
3565
|
linkState.value = engine.state;
|
|
3264
3566
|
});
|
|
3265
3567
|
linkEditing = engine;
|
|
3568
|
+
const imageUploads = createImageUploads({
|
|
3569
|
+
editor: session.editor,
|
|
3570
|
+
upload: () => async (file, init) => {
|
|
3571
|
+
const image = await ask("image-upload", { file }, init);
|
|
3572
|
+
if (image && !found.has("image-upload")) {
|
|
3573
|
+
found.add("image-upload");
|
|
3574
|
+
checkResponders();
|
|
3575
|
+
}
|
|
3576
|
+
return image;
|
|
3577
|
+
},
|
|
3578
|
+
onError: (error, file) => emit("uploadError", error, file)
|
|
3579
|
+
});
|
|
3580
|
+
imageUploads.subscribe(() => {
|
|
3581
|
+
uploadState.value = imageUploads.state;
|
|
3582
|
+
});
|
|
3583
|
+
uploads = imageUploads;
|
|
3266
3584
|
const instance = session.editor;
|
|
3267
3585
|
formatting = createFormattingSelection({
|
|
3268
3586
|
editor: instance,
|
|
@@ -3274,6 +3592,30 @@ var LoomaEditor = defineComponent({
|
|
|
3274
3592
|
formattingPopup?.show();
|
|
3275
3593
|
}
|
|
3276
3594
|
});
|
|
3595
|
+
};
|
|
3596
|
+
const rebuild = () => {
|
|
3597
|
+
const previous = session;
|
|
3598
|
+
const content = edited ? previous.editor.getJSON() : props.modelValue;
|
|
3599
|
+
linkEditing?.destroy();
|
|
3600
|
+
formatting?.destroy();
|
|
3601
|
+
uploads?.cancel();
|
|
3602
|
+
previous.destroy();
|
|
3603
|
+
build(content);
|
|
3604
|
+
};
|
|
3605
|
+
onMounted(() => {
|
|
3606
|
+
const element = root.value;
|
|
3607
|
+
for (const name of EDITOR_REQUEST_NAMES) element.addEventListener(name, (event) => {
|
|
3608
|
+
const responder = props[EDITOR_REQUESTS[name]];
|
|
3609
|
+
if (typeof responder === "function" && "respondWith" in event) respondWithFunction(event, responder);
|
|
3610
|
+
}, { capture: true });
|
|
3611
|
+
stopAnnouncements = onResponderAnnounced(element, (name) => {
|
|
3612
|
+
if (!(name in EDITOR_REQUESTS)) return;
|
|
3613
|
+
found.add(name);
|
|
3614
|
+
checkResponders();
|
|
3615
|
+
});
|
|
3616
|
+
responders = new Set(EDITOR_REQUEST_NAMES.filter(answers));
|
|
3617
|
+
askExtensions();
|
|
3618
|
+
askSlashCommands();
|
|
3277
3619
|
});
|
|
3278
3620
|
const attachDocument = (element) => {
|
|
3279
3621
|
if (element instanceof HTMLElement) session?.attach(element);
|
|
@@ -3281,6 +3623,8 @@ var LoomaEditor = defineComponent({
|
|
|
3281
3623
|
onBeforeUnmount(() => {
|
|
3282
3624
|
const element = session?.editor.options.element;
|
|
3283
3625
|
element?.parentNode?.replaceChild(element.cloneNode(true), element);
|
|
3626
|
+
connection.abort();
|
|
3627
|
+
stopAnnouncements?.();
|
|
3284
3628
|
linkEditing?.destroy();
|
|
3285
3629
|
session?.destroy();
|
|
3286
3630
|
});
|
|
@@ -3314,10 +3658,10 @@ var LoomaEditor = defineComponent({
|
|
|
3314
3658
|
ref: setLinkFormRoot,
|
|
3315
3659
|
class: "looma-editor__link-form",
|
|
3316
3660
|
existing: Boolean(state.range?.existing),
|
|
3317
|
-
searchable:
|
|
3661
|
+
searchable: answers("link-search"),
|
|
3318
3662
|
mode: state.mode,
|
|
3319
|
-
value:
|
|
3320
|
-
placeholder:
|
|
3663
|
+
value: answers("link-search") ? state.query : state.href,
|
|
3664
|
+
placeholder: answers("link-search") ? `Search ${props.linkTargetLabel.toLowerCase()}s or paste a URL…` : "https://example.com",
|
|
3321
3665
|
newTab: state.newTab,
|
|
3322
3666
|
textEditable: state.textEditable,
|
|
3323
3667
|
text: state.text,
|
|
@@ -3367,10 +3711,7 @@ var LoomaEditor = defineComponent({
|
|
|
3367
3711
|
onRemove: () => linkEditing?.remove()
|
|
3368
3712
|
});
|
|
3369
3713
|
};
|
|
3370
|
-
watch(
|
|
3371
|
-
slashExtension.options.commands = resolveSlashCommands();
|
|
3372
|
-
editor.value?.commands.dismissLoomaSlashMenu();
|
|
3373
|
-
});
|
|
3714
|
+
watch(() => props.slashCommands, askSlashCommands);
|
|
3374
3715
|
watch(() => props.editable, (editable) => {
|
|
3375
3716
|
formatting?.press.cancel();
|
|
3376
3717
|
formatting?.release();
|
|
@@ -3867,41 +4208,18 @@ var LoomaEditor = defineComponent({
|
|
|
3867
4208
|
if (dragLeaveTimer) clearTimeout(dragLeaveTimer);
|
|
3868
4209
|
if (tablePointerFrame !== null) cancelAnimationFrame(tablePointerFrame);
|
|
3869
4210
|
});
|
|
3870
|
-
const insertImage = async (file) => {
|
|
3871
|
-
if (!props.uploadImage || !editor.value) return;
|
|
3872
|
-
uploading.value = true;
|
|
3873
|
-
try {
|
|
3874
|
-
const result = await props.uploadImage(file);
|
|
3875
|
-
const image = typeof result === "string" ? { url: result } : result;
|
|
3876
|
-
const width = normalizeImageDimension(image.width);
|
|
3877
|
-
const height = normalizeImageDimension(image.height);
|
|
3878
|
-
editor.value.chain().focus().insertContent(imageUploadContent({
|
|
3879
|
-
src: image.url,
|
|
3880
|
-
alt: image.alt ?? file.name,
|
|
3881
|
-
...width ? { width } : {},
|
|
3882
|
-
...height ? { height } : {},
|
|
3883
|
-
...image.responsive === true ? { responsive: true } : {}
|
|
3884
|
-
})).run();
|
|
3885
|
-
failedUpload.value = null;
|
|
3886
|
-
} catch (error) {
|
|
3887
|
-
failedUpload.value = { file };
|
|
3888
|
-
emit("uploadError", error, file);
|
|
3889
|
-
} finally {
|
|
3890
|
-
uploading.value = false;
|
|
3891
|
-
}
|
|
3892
|
-
};
|
|
3893
4211
|
const onFileChange = async (event) => {
|
|
3894
4212
|
const input = event.target;
|
|
3895
4213
|
const files = Array.from(input.files ?? []);
|
|
3896
4214
|
input.value = "";
|
|
3897
|
-
|
|
4215
|
+
await uploads?.insert(files);
|
|
3898
4216
|
};
|
|
3899
4217
|
const onDrop = async (event) => {
|
|
3900
4218
|
event.preventDefault();
|
|
3901
4219
|
dragOver.value = false;
|
|
3902
4220
|
if (!props.editable) return;
|
|
3903
4221
|
const files = Array.from(event.dataTransfer?.files ?? []).filter((file) => file.type.startsWith("image/"));
|
|
3904
|
-
|
|
4222
|
+
await uploads?.insert(files);
|
|
3905
4223
|
};
|
|
3906
4224
|
const onContextMenu = (event) => {
|
|
3907
4225
|
const instance = editor.value;
|
|
@@ -4158,7 +4476,7 @@ var LoomaEditor = defineComponent({
|
|
|
4158
4476
|
variant: tablePickerOpen.value ? "solid" : "ghost",
|
|
4159
4477
|
"aria-expanded": tablePickerOpen.value ? "true" : "false"
|
|
4160
4478
|
}, () => loomaIcon("table")),
|
|
4161
|
-
commandButton(
|
|
4479
|
+
commandButton(uploadState.value.uploading ? "Uploading image" : "Insert image", "image", false, uploadState.value.uploading || !answers("image-upload"), () => fileInput.value?.click()),
|
|
4162
4480
|
h("span", {
|
|
4163
4481
|
class: "divider",
|
|
4164
4482
|
"aria-hidden": "true"
|
|
@@ -4349,17 +4667,24 @@ var LoomaEditor = defineComponent({
|
|
|
4349
4667
|
"aria-hidden": "true",
|
|
4350
4668
|
onChange: onFileChange
|
|
4351
4669
|
}),
|
|
4352
|
-
|
|
4670
|
+
uploadState.value.uploading ? h(StatusMessage, { kind: "loading" }, {
|
|
4671
|
+
default: () => uploadProgressText(uploadState.value.progress),
|
|
4672
|
+
action: () => h(Button, {
|
|
4673
|
+
size: "sm",
|
|
4674
|
+
variant: "ghost",
|
|
4675
|
+
onClick: () => uploads?.cancel()
|
|
4676
|
+
}, () => "Cancel")
|
|
4677
|
+
}) : null,
|
|
4678
|
+
uploadState.value.failed ? h("div", {
|
|
4353
4679
|
class: "looma-editor__upload-error",
|
|
4354
4680
|
role: "alert"
|
|
4355
4681
|
}, [h("span", "Couldn’t upload image."), h("button", {
|
|
4356
4682
|
type: "button",
|
|
4357
|
-
disabled:
|
|
4683
|
+
disabled: uploadState.value.uploading,
|
|
4358
4684
|
onClick: () => {
|
|
4359
|
-
|
|
4360
|
-
if (attempt) insertImage(attempt.file);
|
|
4685
|
+
uploads?.retry();
|
|
4361
4686
|
}
|
|
4362
|
-
},
|
|
4687
|
+
}, uploadState.value.uploading ? "Retrying…" : "Retry")]) : null,
|
|
4363
4688
|
dragOver.value ? h("div", {
|
|
4364
4689
|
class: "looma-editor__drop-overlay",
|
|
4365
4690
|
"aria-hidden": "true"
|