@threadlabs/looma 0.28.21 → 0.28.22
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 +49 -0
- package/package.json +1 -1
- package/vue/editor/LoomaEditor.d.ts +3 -18
- package/vue/editor/index.js +79 -33
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/core";
|
|
2
|
+
import { type LoomaImageDescriptor } from "./image-delivery.js";
|
|
3
|
+
/**
|
|
4
|
+
* Host upload result normalized into the editor's durable image descriptor.
|
|
5
|
+
* `url` becomes the stored `src`; optional dimensions preserve layout before
|
|
6
|
+
* the image loads and `responsive` opts into host-provided rendition policy.
|
|
7
|
+
*/
|
|
8
|
+
export interface LoomaImageUploadResult extends Omit<LoomaImageDescriptor, "src"> {
|
|
9
|
+
url: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Application-owned upload boundary used by paste, drop, and file selection.
|
|
13
|
+
*
|
|
14
|
+
* @ownership The application owns network requests, persistence, and returned
|
|
15
|
+
* URLs. Looma owns only editor insertion after the promise fulfills.
|
|
16
|
+
* @failure Rejection leaves the document unchanged and is surfaced through the
|
|
17
|
+
* editor's `uploadError` event so the host chooses retry and user messaging.
|
|
18
|
+
*/
|
|
19
|
+
export type LoomaImageUploader = (file: File) => Promise<string | LoomaImageUploadResult>;
|
|
20
|
+
export interface LoomaImageUploadsState {
|
|
21
|
+
readonly uploading: boolean;
|
|
22
|
+
/** The file whose upload failed last, which `retry()` sends again. */
|
|
23
|
+
readonly failed: File | null;
|
|
24
|
+
}
|
|
25
|
+
export interface LoomaImageUploadsOptions {
|
|
26
|
+
editor: Editor;
|
|
27
|
+
/** Read on each upload, so a host can change it while the editor runs. */
|
|
28
|
+
upload: () => LoomaImageUploader | null | undefined;
|
|
29
|
+
onError: (error: unknown, file: File) => void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The editor's image uploads: each file goes to the host's uploader in turn, and the image it
|
|
33
|
+
* returns is inserted at the selection.
|
|
34
|
+
*
|
|
35
|
+
* @ownership The host owns the upload and its URL; this owns the uploading state, the failed file,
|
|
36
|
+
* and inserting the result.
|
|
37
|
+
* @failure A rejected upload leaves the document unchanged, records the file for `retry()`, and is
|
|
38
|
+
* reported through `onError`.
|
|
39
|
+
*/
|
|
40
|
+
export interface LoomaImageUploads {
|
|
41
|
+
readonly state: LoomaImageUploadsState;
|
|
42
|
+
/** Calls `listener` after every state change; returns the unsubscribe function. */
|
|
43
|
+
subscribe(listener: () => void): () => void;
|
|
44
|
+
/** Uploads each file in turn and inserts its image; does nothing without an uploader. */
|
|
45
|
+
insert(files: Iterable<File>): Promise<void>;
|
|
46
|
+
/** Sends the failed file again. */
|
|
47
|
+
retry(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
export declare function createImageUploads(options: LoomaImageUploadsOptions): LoomaImageUploads;
|
package/package.json
CHANGED
|
@@ -3,25 +3,10 @@ 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
5
|
import { type LoomaSlashCommand, type LoomaMentionItem, type LoomaMentionProvider, type LoomaCodeLanguages } from "@threadlabs/looma/editor";
|
|
6
|
-
import { type LoomaImageActivationDetail, type LoomaImageAttributeResolver, type LoomaImageSourceResolver, type
|
|
6
|
+
import { type LoomaImageActivationDetail, type LoomaImageAttributeResolver, type LoomaImageSourceResolver, type LoomaImageRenditionErrorDetail } from "../../editor/image-delivery.js";
|
|
7
|
+
import { type LoomaImageUploader } from "../../editor/image-uploads.js";
|
|
7
8
|
import { type LoomaLinkResolve, type LoomaLinkSearch } from "../../editor/link-editing.js";
|
|
8
|
-
|
|
9
|
-
* Host upload result normalized into the editor's durable image descriptor.
|
|
10
|
-
* `url` becomes the stored `src`; optional dimensions preserve layout before
|
|
11
|
-
* the image loads and `responsive` opts into host-provided rendition policy.
|
|
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>;
|
|
9
|
+
export type { LoomaImageUploader, LoomaImageUploadResult } from "../../editor/image-uploads.js";
|
|
25
10
|
/**
|
|
26
11
|
* Chooses whether formatting controls follow a selection, occupy persistent
|
|
27
12
|
* editor chrome, or open from an app-owned button (`popover`, with a text-only
|
package/vue/editor/index.js
CHANGED
|
@@ -1138,6 +1138,64 @@ 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
|
+
let state = {
|
|
1146
|
+
uploading: false,
|
|
1147
|
+
failed: null
|
|
1148
|
+
};
|
|
1149
|
+
const set = (patch) => {
|
|
1150
|
+
state = {
|
|
1151
|
+
...state,
|
|
1152
|
+
...patch
|
|
1153
|
+
};
|
|
1154
|
+
listeners.forEach((listener) => listener());
|
|
1155
|
+
};
|
|
1156
|
+
const insertOne = async (file) => {
|
|
1157
|
+
const upload = options.upload();
|
|
1158
|
+
if (!upload || editor.isDestroyed) return;
|
|
1159
|
+
set({ uploading: true });
|
|
1160
|
+
try {
|
|
1161
|
+
const result = await upload(file);
|
|
1162
|
+
const image = typeof result === "string" ? { url: result } : result;
|
|
1163
|
+
const width = normalizeImageDimension(image.width);
|
|
1164
|
+
const height = normalizeImageDimension(image.height);
|
|
1165
|
+
if (!editor.isDestroyed) editor.chain().focus().insertContent(imageUploadContent({
|
|
1166
|
+
src: image.url,
|
|
1167
|
+
alt: image.alt ?? file.name,
|
|
1168
|
+
...width ? { width } : {},
|
|
1169
|
+
...height ? { height } : {},
|
|
1170
|
+
...image.responsive === true ? { responsive: true } : {}
|
|
1171
|
+
})).run();
|
|
1172
|
+
set({ failed: null });
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
set({ failed: file });
|
|
1175
|
+
options.onError(error, file);
|
|
1176
|
+
} finally {
|
|
1177
|
+
set({ uploading: false });
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
return {
|
|
1181
|
+
get state() {
|
|
1182
|
+
return state;
|
|
1183
|
+
},
|
|
1184
|
+
subscribe(listener) {
|
|
1185
|
+
listeners.add(listener);
|
|
1186
|
+
return () => {
|
|
1187
|
+
listeners.delete(listener);
|
|
1188
|
+
};
|
|
1189
|
+
},
|
|
1190
|
+
async insert(files) {
|
|
1191
|
+
for (const file of files) await insertOne(file);
|
|
1192
|
+
},
|
|
1193
|
+
async retry() {
|
|
1194
|
+
if (state.failed) await insertOne(state.failed);
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
//#endregion
|
|
1141
1199
|
//#region src/editor/formatting-selection.ts
|
|
1142
1200
|
var HOLD_MS = 500;
|
|
1143
1201
|
var HOLD_SLOP_PX = 8;
|
|
@@ -3025,8 +3083,11 @@ var LoomaEditor = defineComponent({
|
|
|
3025
3083
|
const mobileToolbarShell = ref(null);
|
|
3026
3084
|
const codeLanguageShell = ref(null);
|
|
3027
3085
|
const dragOver = ref(false);
|
|
3028
|
-
|
|
3029
|
-
const
|
|
3086
|
+
let uploads;
|
|
3087
|
+
const uploadState = shallowRef({
|
|
3088
|
+
uploading: false,
|
|
3089
|
+
failed: null
|
|
3090
|
+
});
|
|
3030
3091
|
const tablePickerOpen = ref(false);
|
|
3031
3092
|
const mobileToolbarStyle = ref({});
|
|
3032
3093
|
const mobile = ref(typeof window !== "undefined" && window.innerWidth <= 767);
|
|
@@ -3263,6 +3324,15 @@ var LoomaEditor = defineComponent({
|
|
|
3263
3324
|
linkState.value = engine.state;
|
|
3264
3325
|
});
|
|
3265
3326
|
linkEditing = engine;
|
|
3327
|
+
const imageUploads = createImageUploads({
|
|
3328
|
+
editor: session.editor,
|
|
3329
|
+
upload: () => props.uploadImage,
|
|
3330
|
+
onError: (error, file) => emit("uploadError", error, file)
|
|
3331
|
+
});
|
|
3332
|
+
imageUploads.subscribe(() => {
|
|
3333
|
+
uploadState.value = imageUploads.state;
|
|
3334
|
+
});
|
|
3335
|
+
uploads = imageUploads;
|
|
3266
3336
|
const instance = session.editor;
|
|
3267
3337
|
formatting = createFormattingSelection({
|
|
3268
3338
|
editor: instance,
|
|
@@ -3867,41 +3937,18 @@ var LoomaEditor = defineComponent({
|
|
|
3867
3937
|
if (dragLeaveTimer) clearTimeout(dragLeaveTimer);
|
|
3868
3938
|
if (tablePointerFrame !== null) cancelAnimationFrame(tablePointerFrame);
|
|
3869
3939
|
});
|
|
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
3940
|
const onFileChange = async (event) => {
|
|
3894
3941
|
const input = event.target;
|
|
3895
3942
|
const files = Array.from(input.files ?? []);
|
|
3896
3943
|
input.value = "";
|
|
3897
|
-
|
|
3944
|
+
await uploads?.insert(files);
|
|
3898
3945
|
};
|
|
3899
3946
|
const onDrop = async (event) => {
|
|
3900
3947
|
event.preventDefault();
|
|
3901
3948
|
dragOver.value = false;
|
|
3902
3949
|
if (!props.editable) return;
|
|
3903
3950
|
const files = Array.from(event.dataTransfer?.files ?? []).filter((file) => file.type.startsWith("image/"));
|
|
3904
|
-
|
|
3951
|
+
await uploads?.insert(files);
|
|
3905
3952
|
};
|
|
3906
3953
|
const onContextMenu = (event) => {
|
|
3907
3954
|
const instance = editor.value;
|
|
@@ -4158,7 +4205,7 @@ var LoomaEditor = defineComponent({
|
|
|
4158
4205
|
variant: tablePickerOpen.value ? "solid" : "ghost",
|
|
4159
4206
|
"aria-expanded": tablePickerOpen.value ? "true" : "false"
|
|
4160
4207
|
}, () => loomaIcon("table")),
|
|
4161
|
-
commandButton(
|
|
4208
|
+
commandButton(uploadState.value.uploading ? "Uploading image" : "Insert image", "image", false, uploadState.value.uploading || !props.uploadImage, () => fileInput.value?.click()),
|
|
4162
4209
|
h("span", {
|
|
4163
4210
|
class: "divider",
|
|
4164
4211
|
"aria-hidden": "true"
|
|
@@ -4349,17 +4396,16 @@ var LoomaEditor = defineComponent({
|
|
|
4349
4396
|
"aria-hidden": "true",
|
|
4350
4397
|
onChange: onFileChange
|
|
4351
4398
|
}),
|
|
4352
|
-
|
|
4399
|
+
uploadState.value.failed ? h("div", {
|
|
4353
4400
|
class: "looma-editor__upload-error",
|
|
4354
4401
|
role: "alert"
|
|
4355
4402
|
}, [h("span", "Couldn’t upload image."), h("button", {
|
|
4356
4403
|
type: "button",
|
|
4357
|
-
disabled:
|
|
4404
|
+
disabled: uploadState.value.uploading,
|
|
4358
4405
|
onClick: () => {
|
|
4359
|
-
|
|
4360
|
-
if (attempt) insertImage(attempt.file);
|
|
4406
|
+
uploads?.retry();
|
|
4361
4407
|
}
|
|
4362
|
-
},
|
|
4408
|
+
}, uploadState.value.uploading ? "Retrying…" : "Retry")]) : null,
|
|
4363
4409
|
dragOver.value ? h("div", {
|
|
4364
4410
|
class: "looma-editor__drop-overlay",
|
|
4365
4411
|
"aria-hidden": "true"
|