@threadlabs/looma 0.28.22 → 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 +23 -15
- 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 -32
- package/vue/editor/index.d.ts +1 -1
- package/vue/editor/index.js +332 -53
|
@@ -1,31 +1,37 @@
|
|
|
1
1
|
import type { Editor } from "@tiptap/core";
|
|
2
2
|
import { type LoomaImageDescriptor } from "./image-delivery.js";
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* the image loads and `responsive` opts into host-provided rendition policy.
|
|
7
|
-
*/
|
|
8
|
-
export interface LoomaImageUploadResult extends Omit<LoomaImageDescriptor, "src"> {
|
|
9
|
-
url: string;
|
|
3
|
+
/** What an image upload request's detail carries: the file to upload. */
|
|
4
|
+
export interface LoomaImageUploadDetail {
|
|
5
|
+
file: File;
|
|
10
6
|
}
|
|
11
7
|
/**
|
|
12
|
-
*
|
|
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.
|
|
13
10
|
*
|
|
14
|
-
* @ownership The application owns network requests, persistence, and returned
|
|
15
|
-
*
|
|
16
|
-
* @failure
|
|
17
|
-
*
|
|
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.
|
|
18
16
|
*/
|
|
19
|
-
export type
|
|
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>;
|
|
20
21
|
export interface LoomaImageUploadsState {
|
|
21
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;
|
|
22
28
|
/** The file whose upload failed last, which `retry()` sends again. */
|
|
23
29
|
readonly failed: File | null;
|
|
24
30
|
}
|
|
25
31
|
export interface LoomaImageUploadsOptions {
|
|
26
32
|
editor: Editor;
|
|
27
33
|
/** Read on each upload, so a host can change it while the editor runs. */
|
|
28
|
-
upload: () =>
|
|
34
|
+
upload: () => LoomaImageUpload | null | undefined;
|
|
29
35
|
onError: (error: unknown, file: File) => void;
|
|
30
36
|
}
|
|
31
37
|
/**
|
|
@@ -35,7 +41,7 @@ export interface LoomaImageUploadsOptions {
|
|
|
35
41
|
* @ownership The host owns the upload and its URL; this owns the uploading state, the failed file,
|
|
36
42
|
* and inserting the result.
|
|
37
43
|
* @failure A rejected upload leaves the document unchanged, records the file for `retry()`, and is
|
|
38
|
-
* reported through `onError`.
|
|
44
|
+
* reported through `onError`. A canceled upload leaves the document unchanged and reports nothing.
|
|
39
45
|
*/
|
|
40
46
|
export interface LoomaImageUploads {
|
|
41
47
|
readonly state: LoomaImageUploadsState;
|
|
@@ -45,5 +51,7 @@ export interface LoomaImageUploads {
|
|
|
45
51
|
insert(files: Iterable<File>): Promise<void>;
|
|
46
52
|
/** Sends the failed file again. */
|
|
47
53
|
retry(): Promise<void>;
|
|
54
|
+
/** Aborts the uploads in progress and the files still waiting their turn. */
|
|
55
|
+
cancel(): void;
|
|
48
56
|
}
|
|
49
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,11 +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 LoomaImageSourceResolver, type LoomaImageRenditionErrorDetail } from "../../editor/image-delivery.js";
|
|
7
|
-
import { type
|
|
8
|
-
import { type
|
|
9
|
-
|
|
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";
|
|
10
12
|
/**
|
|
11
13
|
* Chooses whether formatting controls follow a selection, occupy persistent
|
|
12
14
|
* editor chrome, or open from an app-owned button (`popover`, with a text-only
|
|
@@ -42,13 +44,63 @@ export interface LoomaEditorSelectionActionDetail {
|
|
|
42
44
|
before: string;
|
|
43
45
|
after: string;
|
|
44
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[]>;
|
|
78
|
+
/**
|
|
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.
|
|
82
|
+
*/
|
|
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[]>;
|
|
45
96
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
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`.
|
|
49
102
|
*/
|
|
50
|
-
export type
|
|
51
|
-
export type { LoomaLinkResolve, LoomaLinkSearch, LoomaLinkTarget } from "../../editor/link-editing.js";
|
|
103
|
+
export type LoomaImageUploader = LoomaResponder<LoomaImageUploadDetail, LoomaImageDescriptor>;
|
|
52
104
|
/**
|
|
53
105
|
* Turnkey Vue editor that composes Looma's framework-neutral Tiptap extensions
|
|
54
106
|
* with declarative menus, image delivery, and table interaction chrome.
|
|
@@ -95,13 +147,14 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
95
147
|
type: StringConstructor;
|
|
96
148
|
default: string;
|
|
97
149
|
};
|
|
150
|
+
/** Answers `extensions`: the application's Tiptap extensions, added after Looma's. */
|
|
98
151
|
extensions: {
|
|
99
|
-
type: PropType<
|
|
100
|
-
default:
|
|
152
|
+
type: PropType<LoomaEditorExtensions | undefined>;
|
|
153
|
+
default: undefined;
|
|
101
154
|
};
|
|
102
|
-
/**
|
|
155
|
+
/** Answers `slash-commands`: the slash menu's commands, given the defaults. */
|
|
103
156
|
slashCommands: {
|
|
104
|
-
type: PropType<LoomaEditorSlashCommands>;
|
|
157
|
+
type: PropType<LoomaEditorSlashCommands | undefined>;
|
|
105
158
|
default: undefined;
|
|
106
159
|
};
|
|
107
160
|
/** Override the lazy built-in catalog with application-owned code grammars. An empty object disables highlighting. */
|
|
@@ -113,8 +166,9 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
113
166
|
type: PropType<LoomaMentionItem[]>;
|
|
114
167
|
default: () => never[];
|
|
115
168
|
};
|
|
116
|
-
|
|
117
|
-
|
|
169
|
+
/** Answers `mention-query`: people to suggest for what the author typed after `@`. */
|
|
170
|
+
mentionQuery: {
|
|
171
|
+
type: PropType<LoomaMentionQuery | undefined>;
|
|
118
172
|
default: undefined;
|
|
119
173
|
};
|
|
120
174
|
mentionLimit: {
|
|
@@ -126,7 +180,7 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
126
180
|
type: StringConstructor;
|
|
127
181
|
default: string;
|
|
128
182
|
};
|
|
129
|
-
/**
|
|
183
|
+
/** Answers `link-search` and `link-resolve`: host search and identity lookup for links to application records. */
|
|
130
184
|
linkSearch: {
|
|
131
185
|
type: PropType<LoomaLinkSearch | undefined>;
|
|
132
186
|
default: undefined;
|
|
@@ -139,7 +193,8 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
139
193
|
type: StringConstructor;
|
|
140
194
|
default: string;
|
|
141
195
|
};
|
|
142
|
-
|
|
196
|
+
/** Answers `image-upload`: uploads a pasted, dropped, or chosen image and returns the image to insert. */
|
|
197
|
+
imageUpload: {
|
|
143
198
|
type: PropType<LoomaImageUploader | undefined>;
|
|
144
199
|
default: undefined;
|
|
145
200
|
};
|
|
@@ -237,13 +292,14 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
237
292
|
type: StringConstructor;
|
|
238
293
|
default: string;
|
|
239
294
|
};
|
|
295
|
+
/** Answers `extensions`: the application's Tiptap extensions, added after Looma's. */
|
|
240
296
|
extensions: {
|
|
241
|
-
type: PropType<
|
|
242
|
-
default:
|
|
297
|
+
type: PropType<LoomaEditorExtensions | undefined>;
|
|
298
|
+
default: undefined;
|
|
243
299
|
};
|
|
244
|
-
/**
|
|
300
|
+
/** Answers `slash-commands`: the slash menu's commands, given the defaults. */
|
|
245
301
|
slashCommands: {
|
|
246
|
-
type: PropType<LoomaEditorSlashCommands>;
|
|
302
|
+
type: PropType<LoomaEditorSlashCommands | undefined>;
|
|
247
303
|
default: undefined;
|
|
248
304
|
};
|
|
249
305
|
/** Override the lazy built-in catalog with application-owned code grammars. An empty object disables highlighting. */
|
|
@@ -255,8 +311,9 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
255
311
|
type: PropType<LoomaMentionItem[]>;
|
|
256
312
|
default: () => never[];
|
|
257
313
|
};
|
|
258
|
-
|
|
259
|
-
|
|
314
|
+
/** Answers `mention-query`: people to suggest for what the author typed after `@`. */
|
|
315
|
+
mentionQuery: {
|
|
316
|
+
type: PropType<LoomaMentionQuery | undefined>;
|
|
260
317
|
default: undefined;
|
|
261
318
|
};
|
|
262
319
|
mentionLimit: {
|
|
@@ -268,7 +325,7 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
268
325
|
type: StringConstructor;
|
|
269
326
|
default: string;
|
|
270
327
|
};
|
|
271
|
-
/**
|
|
328
|
+
/** Answers `link-search` and `link-resolve`: host search and identity lookup for links to application records. */
|
|
272
329
|
linkSearch: {
|
|
273
330
|
type: PropType<LoomaLinkSearch | undefined>;
|
|
274
331
|
default: undefined;
|
|
@@ -281,7 +338,8 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
281
338
|
type: StringConstructor;
|
|
282
339
|
default: string;
|
|
283
340
|
};
|
|
284
|
-
|
|
341
|
+
/** Answers `image-upload`: uploads a pasted, dropped, or chosen image and returns the image to insert. */
|
|
342
|
+
imageUpload: {
|
|
285
343
|
type: PropType<LoomaImageUploader | undefined>;
|
|
286
344
|
default: undefined;
|
|
287
345
|
};
|
|
@@ -344,7 +402,7 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
344
402
|
onImageActivate?: ((_detail: LoomaImageActivationDetail) => any) | undefined;
|
|
345
403
|
onImageRenditionError?: ((_detail: LoomaImageRenditionErrorDetail) => any) | undefined;
|
|
346
404
|
}>, {
|
|
347
|
-
extensions:
|
|
405
|
+
extensions: LoomaEditorExtensions | undefined;
|
|
348
406
|
editable: boolean;
|
|
349
407
|
label: string;
|
|
350
408
|
placeholder: string;
|
|
@@ -352,15 +410,15 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
|
|
|
352
410
|
disableHighlight: boolean;
|
|
353
411
|
codeLanguages: Readonly<Record<string, import("highlight.js").LanguageFn>> | null;
|
|
354
412
|
modelValue: JSONContent;
|
|
413
|
+
slashCommands: LoomaEditorSlashCommands | undefined;
|
|
414
|
+
mentionQuery: LoomaMentionQuery | undefined;
|
|
415
|
+
linkSearch: LoomaLinkSearch | undefined;
|
|
416
|
+
linkResolve: LoomaLinkResolve | undefined;
|
|
417
|
+
imageUpload: LoomaImageUploader | undefined;
|
|
355
418
|
contentDensity: "compact" | "comfortable";
|
|
356
|
-
slashCommands: LoomaEditorSlashCommands;
|
|
357
419
|
mentionItems: LoomaMentionItem[];
|
|
358
|
-
mentionProvider: LoomaMentionProvider | undefined;
|
|
359
420
|
mentionLimit: number;
|
|
360
|
-
linkSearch: LoomaLinkSearch | undefined;
|
|
361
|
-
linkResolve: LoomaLinkResolve | undefined;
|
|
362
421
|
linkTargetLabel: string;
|
|
363
|
-
uploadImage: LoomaImageUploader | undefined;
|
|
364
422
|
resolveImageAttributes: LoomaImageAttributeResolver | undefined;
|
|
365
423
|
resolveImageSource: LoomaImageSourceResolver | undefined;
|
|
366
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";
|
|
@@ -1142,8 +1142,10 @@ function runBlockAction(editor, action) {
|
|
|
1142
1142
|
function createImageUploads(options) {
|
|
1143
1143
|
const { editor } = options;
|
|
1144
1144
|
const listeners = /* @__PURE__ */ new Set();
|
|
1145
|
+
const batches = /* @__PURE__ */ new Set();
|
|
1145
1146
|
let state = {
|
|
1146
1147
|
uploading: false,
|
|
1148
|
+
progress: null,
|
|
1147
1149
|
failed: null
|
|
1148
1150
|
};
|
|
1149
1151
|
const set = (patch) => {
|
|
@@ -1153,28 +1155,53 @@ function createImageUploads(options) {
|
|
|
1153
1155
|
};
|
|
1154
1156
|
listeners.forEach((listener) => listener());
|
|
1155
1157
|
};
|
|
1156
|
-
const insertOne = async (file) => {
|
|
1158
|
+
const insertOne = async (file, signal) => {
|
|
1157
1159
|
const upload = options.upload();
|
|
1158
|
-
if (!upload || editor.isDestroyed) return;
|
|
1159
|
-
set({
|
|
1160
|
+
if (!upload || editor.isDestroyed || signal.aborted) return;
|
|
1161
|
+
set({
|
|
1162
|
+
uploading: true,
|
|
1163
|
+
progress: null
|
|
1164
|
+
});
|
|
1160
1165
|
try {
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
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 });
|
|
1173
1187
|
} catch (error) {
|
|
1188
|
+
if (signal.aborted) return;
|
|
1174
1189
|
set({ failed: file });
|
|
1175
1190
|
options.onError(error, file);
|
|
1176
1191
|
} finally {
|
|
1177
|
-
set({
|
|
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);
|
|
1178
1205
|
}
|
|
1179
1206
|
};
|
|
1180
1207
|
return {
|
|
@@ -1187,15 +1214,146 @@ function createImageUploads(options) {
|
|
|
1187
1214
|
listeners.delete(listener);
|
|
1188
1215
|
};
|
|
1189
1216
|
},
|
|
1190
|
-
|
|
1191
|
-
for (const file of files) await insertOne(file);
|
|
1192
|
-
},
|
|
1217
|
+
insert: run,
|
|
1193
1218
|
async retry() {
|
|
1194
|
-
if (state.failed) await
|
|
1219
|
+
if (state.failed) await run([state.failed]);
|
|
1220
|
+
},
|
|
1221
|
+
cancel() {
|
|
1222
|
+
batches.forEach((batch) => batch.abort(new DOMException("The upload was canceled.", "AbortError")));
|
|
1195
1223
|
}
|
|
1196
1224
|
};
|
|
1197
1225
|
}
|
|
1198
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
|
|
1199
1357
|
//#region src/editor/formatting-selection.ts
|
|
1200
1358
|
var HOLD_MS = 500;
|
|
1201
1359
|
var HOLD_SLOP_PX = 8;
|
|
@@ -2816,10 +2974,20 @@ var imageNodeView = ({ editor, node, getPos, decorations }) => {
|
|
|
2816
2974
|
};
|
|
2817
2975
|
//#endregion
|
|
2818
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);
|
|
2819
2986
|
var EMPTY_DOCUMENT = {
|
|
2820
2987
|
type: "doc",
|
|
2821
2988
|
content: []
|
|
2822
2989
|
};
|
|
2990
|
+
var uploadProgressText = (progress) => progress && progress.total > 0 ? `Uploading image… ${Math.round(Math.min(1, progress.loaded / progress.total) * 100)}%` : "Uploading image…";
|
|
2823
2991
|
var editorInstanceSequence = 0;
|
|
2824
2992
|
var EMPTY_CAPABILITIES = {
|
|
2825
2993
|
canToggleHeaderRow: false,
|
|
@@ -2912,13 +3080,14 @@ var LoomaEditor = defineComponent({
|
|
|
2912
3080
|
type: String,
|
|
2913
3081
|
default: "Document"
|
|
2914
3082
|
},
|
|
3083
|
+
/** Answers `extensions`: the application's Tiptap extensions, added after Looma's. */
|
|
2915
3084
|
extensions: {
|
|
2916
|
-
type:
|
|
2917
|
-
default:
|
|
3085
|
+
type: Function,
|
|
3086
|
+
default: void 0
|
|
2918
3087
|
},
|
|
2919
|
-
/**
|
|
3088
|
+
/** Answers `slash-commands`: the slash menu's commands, given the defaults. */
|
|
2920
3089
|
slashCommands: {
|
|
2921
|
-
type:
|
|
3090
|
+
type: Function,
|
|
2922
3091
|
default: void 0
|
|
2923
3092
|
},
|
|
2924
3093
|
/** Override the lazy built-in catalog with application-owned code grammars. An empty object disables highlighting. */
|
|
@@ -2930,7 +3099,8 @@ var LoomaEditor = defineComponent({
|
|
|
2930
3099
|
type: Array,
|
|
2931
3100
|
default: () => []
|
|
2932
3101
|
},
|
|
2933
|
-
|
|
3102
|
+
/** Answers `mention-query`: people to suggest for what the author typed after `@`. */
|
|
3103
|
+
mentionQuery: {
|
|
2934
3104
|
type: Function,
|
|
2935
3105
|
default: void 0
|
|
2936
3106
|
},
|
|
@@ -2943,7 +3113,7 @@ var LoomaEditor = defineComponent({
|
|
|
2943
3113
|
type: String,
|
|
2944
3114
|
default: ""
|
|
2945
3115
|
},
|
|
2946
|
-
/**
|
|
3116
|
+
/** Answers `link-search` and `link-resolve`: host search and identity lookup for links to application records. */
|
|
2947
3117
|
linkSearch: {
|
|
2948
3118
|
type: Function,
|
|
2949
3119
|
default: void 0
|
|
@@ -2956,7 +3126,8 @@ var LoomaEditor = defineComponent({
|
|
|
2956
3126
|
type: String,
|
|
2957
3127
|
default: "Item"
|
|
2958
3128
|
},
|
|
2959
|
-
|
|
3129
|
+
/** Answers `image-upload`: uploads a pasted, dropped, or chosen image and returns the image to insert. */
|
|
3130
|
+
imageUpload: {
|
|
2960
3131
|
type: Function,
|
|
2961
3132
|
default: void 0
|
|
2962
3133
|
},
|
|
@@ -3086,6 +3257,7 @@ var LoomaEditor = defineComponent({
|
|
|
3086
3257
|
let uploads;
|
|
3087
3258
|
const uploadState = shallowRef({
|
|
3088
3259
|
uploading: false,
|
|
3260
|
+
progress: null,
|
|
3089
3261
|
failed: null
|
|
3090
3262
|
});
|
|
3091
3263
|
const tablePickerOpen = ref(false);
|
|
@@ -3182,12 +3354,77 @@ var LoomaEditor = defineComponent({
|
|
|
3182
3354
|
chipOpen.value = false;
|
|
3183
3355
|
if (focusChip) root.value?.querySelector(`#${chipAnchorId}`)?.focus();
|
|
3184
3356
|
};
|
|
3185
|
-
const
|
|
3186
|
-
|
|
3187
|
-
|
|
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
|
+
});
|
|
3188
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();
|
|
3423
|
+
};
|
|
3424
|
+
onUpdated(checkResponders);
|
|
3425
|
+
let stopAnnouncements;
|
|
3189
3426
|
const slashExtension = createLoomaSlashCommandExtension({
|
|
3190
|
-
commands:
|
|
3427
|
+
commands: defaultSlashCommands(),
|
|
3191
3428
|
onOpenImagePicker: () => fileInput.value?.click(),
|
|
3192
3429
|
onOpenChipEditor: openChipEditor,
|
|
3193
3430
|
onOpenLinkEditor: () => openLinkEditor(),
|
|
@@ -3196,13 +3433,13 @@ var LoomaEditor = defineComponent({
|
|
|
3196
3433
|
}
|
|
3197
3434
|
});
|
|
3198
3435
|
const mentionMenuId = `looma-editor-mention-menu-${++editorInstanceSequence}`;
|
|
3199
|
-
const mentionExtension =
|
|
3436
|
+
const mentionExtension = createLoomaMentionExtension({
|
|
3200
3437
|
limit: props.mentionLimit,
|
|
3201
3438
|
menuId: mentionMenuId,
|
|
3202
|
-
items: async (query, context) => {
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
},
|
|
3439
|
+
items: async (query, context) => await askLatest("mention-query", {
|
|
3440
|
+
query,
|
|
3441
|
+
limit: context.limit
|
|
3442
|
+
}) ?? props.mentionItems,
|
|
3206
3443
|
onStateChange: (state) => {
|
|
3207
3444
|
Object.assign(mention, state);
|
|
3208
3445
|
}
|
|
@@ -3234,13 +3471,16 @@ var LoomaEditor = defineComponent({
|
|
|
3234
3471
|
const loadForCodeBlock = codeHighlighting.loadForCodeBlock;
|
|
3235
3472
|
const editor = shallowRef();
|
|
3236
3473
|
let session;
|
|
3237
|
-
|
|
3474
|
+
let edited = false;
|
|
3475
|
+
const build = (content) => {
|
|
3476
|
+
edited = false;
|
|
3477
|
+
const consumerMention = consumerExtensions.some((extension) => extension.name === "mention");
|
|
3238
3478
|
session = createEditorSession({
|
|
3239
3479
|
extensions: [
|
|
3240
3480
|
...getDefaultEditorExtensions({
|
|
3241
3481
|
placeholder: props.placeholder,
|
|
3242
3482
|
linkBaseUrl: props.linkBaseUrl,
|
|
3243
|
-
mention:
|
|
3483
|
+
mention: consumerMention ? false : mentionExtension,
|
|
3244
3484
|
disableHighlight: props.disableHighlight,
|
|
3245
3485
|
codeLanguages: props.codeLanguages ?? void 0,
|
|
3246
3486
|
codeLowlight: codeHighlighting.lowlight,
|
|
@@ -3252,9 +3492,9 @@ var LoomaEditor = defineComponent({
|
|
|
3252
3492
|
}),
|
|
3253
3493
|
imageDelivery.extension,
|
|
3254
3494
|
slashExtension,
|
|
3255
|
-
...
|
|
3495
|
+
...consumerExtensions
|
|
3256
3496
|
],
|
|
3257
|
-
content
|
|
3497
|
+
content,
|
|
3258
3498
|
editable: props.editable,
|
|
3259
3499
|
editorProps: {
|
|
3260
3500
|
nodeViews: { image: () => ({ dom: document.createElement("span") }) },
|
|
@@ -3304,6 +3544,7 @@ var LoomaEditor = defineComponent({
|
|
|
3304
3544
|
}
|
|
3305
3545
|
},
|
|
3306
3546
|
onUpdate: ({ editor: instance }) => {
|
|
3547
|
+
edited = true;
|
|
3307
3548
|
const value = instance.getJSON();
|
|
3308
3549
|
emit("update:modelValue", value);
|
|
3309
3550
|
emit("update", value);
|
|
@@ -3316,8 +3557,8 @@ var LoomaEditor = defineComponent({
|
|
|
3316
3557
|
editor.value = session.editor;
|
|
3317
3558
|
const engine = createLinkEditing({
|
|
3318
3559
|
editor: session.editor,
|
|
3319
|
-
search: () =>
|
|
3320
|
-
resolve: () =>
|
|
3560
|
+
search: () => answers("link-search") ? async (query) => await askLatest("link-search", { query }) ?? [] : null,
|
|
3561
|
+
resolve: () => async (href) => await askLatest("link-resolve", { href }),
|
|
3321
3562
|
baseUrl: () => props.linkBaseUrl
|
|
3322
3563
|
});
|
|
3323
3564
|
engine.subscribe(() => {
|
|
@@ -3326,7 +3567,14 @@ var LoomaEditor = defineComponent({
|
|
|
3326
3567
|
linkEditing = engine;
|
|
3327
3568
|
const imageUploads = createImageUploads({
|
|
3328
3569
|
editor: session.editor,
|
|
3329
|
-
upload: () =>
|
|
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
|
+
},
|
|
3330
3578
|
onError: (error, file) => emit("uploadError", error, file)
|
|
3331
3579
|
});
|
|
3332
3580
|
imageUploads.subscribe(() => {
|
|
@@ -3344,6 +3592,30 @@ var LoomaEditor = defineComponent({
|
|
|
3344
3592
|
formattingPopup?.show();
|
|
3345
3593
|
}
|
|
3346
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();
|
|
3347
3619
|
});
|
|
3348
3620
|
const attachDocument = (element) => {
|
|
3349
3621
|
if (element instanceof HTMLElement) session?.attach(element);
|
|
@@ -3351,6 +3623,8 @@ var LoomaEditor = defineComponent({
|
|
|
3351
3623
|
onBeforeUnmount(() => {
|
|
3352
3624
|
const element = session?.editor.options.element;
|
|
3353
3625
|
element?.parentNode?.replaceChild(element.cloneNode(true), element);
|
|
3626
|
+
connection.abort();
|
|
3627
|
+
stopAnnouncements?.();
|
|
3354
3628
|
linkEditing?.destroy();
|
|
3355
3629
|
session?.destroy();
|
|
3356
3630
|
});
|
|
@@ -3384,10 +3658,10 @@ var LoomaEditor = defineComponent({
|
|
|
3384
3658
|
ref: setLinkFormRoot,
|
|
3385
3659
|
class: "looma-editor__link-form",
|
|
3386
3660
|
existing: Boolean(state.range?.existing),
|
|
3387
|
-
searchable:
|
|
3661
|
+
searchable: answers("link-search"),
|
|
3388
3662
|
mode: state.mode,
|
|
3389
|
-
value:
|
|
3390
|
-
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",
|
|
3391
3665
|
newTab: state.newTab,
|
|
3392
3666
|
textEditable: state.textEditable,
|
|
3393
3667
|
text: state.text,
|
|
@@ -3437,10 +3711,7 @@ var LoomaEditor = defineComponent({
|
|
|
3437
3711
|
onRemove: () => linkEditing?.remove()
|
|
3438
3712
|
});
|
|
3439
3713
|
};
|
|
3440
|
-
watch(
|
|
3441
|
-
slashExtension.options.commands = resolveSlashCommands();
|
|
3442
|
-
editor.value?.commands.dismissLoomaSlashMenu();
|
|
3443
|
-
});
|
|
3714
|
+
watch(() => props.slashCommands, askSlashCommands);
|
|
3444
3715
|
watch(() => props.editable, (editable) => {
|
|
3445
3716
|
formatting?.press.cancel();
|
|
3446
3717
|
formatting?.release();
|
|
@@ -4205,7 +4476,7 @@ var LoomaEditor = defineComponent({
|
|
|
4205
4476
|
variant: tablePickerOpen.value ? "solid" : "ghost",
|
|
4206
4477
|
"aria-expanded": tablePickerOpen.value ? "true" : "false"
|
|
4207
4478
|
}, () => loomaIcon("table")),
|
|
4208
|
-
commandButton(uploadState.value.uploading ? "Uploading image" : "Insert image", "image", false, uploadState.value.uploading || !
|
|
4479
|
+
commandButton(uploadState.value.uploading ? "Uploading image" : "Insert image", "image", false, uploadState.value.uploading || !answers("image-upload"), () => fileInput.value?.click()),
|
|
4209
4480
|
h("span", {
|
|
4210
4481
|
class: "divider",
|
|
4211
4482
|
"aria-hidden": "true"
|
|
@@ -4396,6 +4667,14 @@ var LoomaEditor = defineComponent({
|
|
|
4396
4667
|
"aria-hidden": "true",
|
|
4397
4668
|
onChange: onFileChange
|
|
4398
4669
|
}),
|
|
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,
|
|
4399
4678
|
uploadState.value.failed ? h("div", {
|
|
4400
4679
|
class: "looma-editor__upload-error",
|
|
4401
4680
|
role: "alert"
|