@threadlabs/looma 0.1.13 → 0.1.14
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.css +42 -0
- package/package.json +1 -1
- package/vue/editor/index.d.ts +41 -3
- package/vue/editor/index.js +272 -3
package/editor.css
CHANGED
|
@@ -149,10 +149,16 @@
|
|
|
149
149
|
|
|
150
150
|
.looma-editor .ProseMirror img {
|
|
151
151
|
max-width: 100%;
|
|
152
|
+
height: auto;
|
|
152
153
|
margin: var(--ui-space-3) 0;
|
|
153
154
|
border-radius: var(--ui-radius-2);
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
.looma-editor .ProseMirror img[data-looma-image]:focus-visible {
|
|
158
|
+
outline: 2px solid var(--ui-focus-ring);
|
|
159
|
+
outline-offset: 3px;
|
|
160
|
+
}
|
|
161
|
+
|
|
156
162
|
.looma-editor__file-input {
|
|
157
163
|
position: absolute;
|
|
158
164
|
width: 0;
|
|
@@ -161,6 +167,42 @@
|
|
|
161
167
|
pointer-events: none;
|
|
162
168
|
}
|
|
163
169
|
|
|
170
|
+
.looma-editor__upload-error {
|
|
171
|
+
display: flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
justify-content: space-between;
|
|
174
|
+
gap: var(--ui-space-3);
|
|
175
|
+
margin-top: var(--ui-space-3);
|
|
176
|
+
padding: var(--ui-space-2) var(--ui-space-3);
|
|
177
|
+
border: 1px solid var(--ui-danger-solid);
|
|
178
|
+
border-radius: var(--ui-radius-2);
|
|
179
|
+
background: var(--ui-danger-soft);
|
|
180
|
+
color: var(--ui-text);
|
|
181
|
+
font-size: var(--ui-font-size-sm);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.looma-editor__upload-error button {
|
|
185
|
+
min-height: 2rem;
|
|
186
|
+
padding: 0 var(--ui-space-3);
|
|
187
|
+
border: 0;
|
|
188
|
+
border-radius: var(--ui-radius-1);
|
|
189
|
+
background: var(--ui-danger-solid);
|
|
190
|
+
color: var(--ui-text-on-accent);
|
|
191
|
+
cursor: pointer;
|
|
192
|
+
font: inherit;
|
|
193
|
+
font-weight: var(--ui-font-semibold);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.looma-editor__upload-error button:focus-visible {
|
|
197
|
+
outline: 2px solid var(--ui-focus-ring);
|
|
198
|
+
outline-offset: 2px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.looma-editor__upload-error button:disabled {
|
|
202
|
+
cursor: wait;
|
|
203
|
+
opacity: 0.65;
|
|
204
|
+
}
|
|
205
|
+
|
|
164
206
|
.looma-editor__drop-overlay {
|
|
165
207
|
position: absolute;
|
|
166
208
|
z-index: var(--ui-editor-drop-overlay-z-index, 10);
|
package/package.json
CHANGED
package/vue/editor/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as vue from 'vue';
|
|
|
4
4
|
import { PropType } from 'vue';
|
|
5
5
|
import { TableContextMenuActionEventDetail, InsertTableEventDetail, TableOverlayActionEventDetail, SlashMenuHighlightEventDetail, SlashMenuSelectEventDetail } from '@threadlabs/looma/editor/ui';
|
|
6
6
|
import { JSONContent, AnyExtension, Editor } from '@tiptap/core';
|
|
7
|
+
import { OverlayTrigger } from '@threadlabs/looma/core';
|
|
7
8
|
|
|
8
9
|
interface VueEditorAdapterEventMap {
|
|
9
10
|
tableAction: TableContextMenuActionEventDetail;
|
|
@@ -87,9 +88,33 @@ declare const EDITOR_ADAPTER_COMPONENT_TAG_MAP: {
|
|
|
87
88
|
readonly EditorTableOverlay: "ui-editor-table-overlay";
|
|
88
89
|
};
|
|
89
90
|
|
|
90
|
-
interface
|
|
91
|
-
|
|
91
|
+
interface LoomaImageDescriptor {
|
|
92
|
+
src: string;
|
|
92
93
|
alt?: string;
|
|
94
|
+
width?: number;
|
|
95
|
+
height?: number;
|
|
96
|
+
responsive?: boolean;
|
|
97
|
+
}
|
|
98
|
+
/** Browser-only attributes resolved by a host image-delivery provider. */
|
|
99
|
+
interface LoomaImageRenditionAttributes {
|
|
100
|
+
src?: string;
|
|
101
|
+
srcset?: string;
|
|
102
|
+
sizes?: string;
|
|
103
|
+
loading?: "eager" | "lazy";
|
|
104
|
+
decoding?: "async" | "auto" | "sync";
|
|
105
|
+
fetchPriority?: "auto" | "high" | "low";
|
|
106
|
+
}
|
|
107
|
+
type LoomaImageAttributeResolver = (image: LoomaImageDescriptor) => LoomaImageRenditionAttributes | undefined;
|
|
108
|
+
type LoomaImageActivationTrigger = Exclude<OverlayTrigger, "programmatic">;
|
|
109
|
+
interface LoomaImageActivationDetail extends LoomaImageDescriptor {
|
|
110
|
+
trigger: LoomaImageActivationTrigger;
|
|
111
|
+
}
|
|
112
|
+
interface LoomaImageRenditionErrorDetail extends LoomaImageDescriptor {
|
|
113
|
+
trigger: "programmatic";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface LoomaImageUploadResult extends Omit<LoomaImageDescriptor, "src"> {
|
|
117
|
+
url: string;
|
|
93
118
|
}
|
|
94
119
|
type LoomaImageUploader = (file: File) => Promise<string | LoomaImageUploadResult>;
|
|
95
120
|
declare const LoomaEditor: vue.DefineComponent<vue.ExtractPropTypes<{
|
|
@@ -124,6 +149,10 @@ declare const LoomaEditor: vue.DefineComponent<vue.ExtractPropTypes<{
|
|
|
124
149
|
type: PropType<LoomaImageUploader | undefined>;
|
|
125
150
|
default: undefined;
|
|
126
151
|
};
|
|
152
|
+
resolveImageAttributes: {
|
|
153
|
+
type: PropType<LoomaImageAttributeResolver | undefined>;
|
|
154
|
+
default: undefined;
|
|
155
|
+
};
|
|
127
156
|
}>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
|
|
128
157
|
[key: string]: any;
|
|
129
158
|
}>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {
|
|
@@ -131,6 +160,8 @@ declare const LoomaEditor: vue.DefineComponent<vue.ExtractPropTypes<{
|
|
|
131
160
|
update: (_value: JSONContent) => true;
|
|
132
161
|
ready: (_editor: Editor) => true;
|
|
133
162
|
uploadError: (_error: unknown, _file: File) => true;
|
|
163
|
+
imageActivate: (_detail: LoomaImageActivationDetail) => true;
|
|
164
|
+
imageRenditionError: (_detail: LoomaImageRenditionErrorDetail) => true;
|
|
134
165
|
}, string, vue.PublicProps, Readonly<vue.ExtractPropTypes<{
|
|
135
166
|
modelValue: {
|
|
136
167
|
type: PropType<JSONContent>;
|
|
@@ -163,17 +194,24 @@ declare const LoomaEditor: vue.DefineComponent<vue.ExtractPropTypes<{
|
|
|
163
194
|
type: PropType<LoomaImageUploader | undefined>;
|
|
164
195
|
default: undefined;
|
|
165
196
|
};
|
|
197
|
+
resolveImageAttributes: {
|
|
198
|
+
type: PropType<LoomaImageAttributeResolver | undefined>;
|
|
199
|
+
default: undefined;
|
|
200
|
+
};
|
|
166
201
|
}>> & Readonly<{
|
|
167
202
|
"onUpdate:modelValue"?: (_value: JSONContent) => any;
|
|
168
203
|
onUpdate?: (_value: JSONContent) => any;
|
|
169
204
|
onReady?: (_editor: Editor) => any;
|
|
170
205
|
onUploadError?: (_error: unknown, _file: File) => any;
|
|
206
|
+
onImageActivate?: (_detail: LoomaImageActivationDetail) => any;
|
|
207
|
+
onImageRenditionError?: (_detail: LoomaImageRenditionErrorDetail) => any;
|
|
171
208
|
}>, {
|
|
172
209
|
placeholder: string;
|
|
173
210
|
modelValue: JSONContent;
|
|
174
211
|
editable: boolean;
|
|
175
212
|
extensions: AnyExtension[];
|
|
176
213
|
uploadImage: LoomaImageUploader | undefined;
|
|
214
|
+
resolveImageAttributes: LoomaImageAttributeResolver | undefined;
|
|
177
215
|
}, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
|
|
178
216
|
|
|
179
|
-
export { EDITOR_ADAPTER_COMPONENT_TAG_MAP, EditorInsertTableGrid, EditorSlashMenu, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, LoomaEditor, type LoomaImageUploadResult, type LoomaImageUploader, type VueEditorAdapterEventMap };
|
|
217
|
+
export { EDITOR_ADAPTER_COMPONENT_TAG_MAP, EditorInsertTableGrid, EditorSlashMenu, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, LoomaEditor, type LoomaImageActivationDetail, type LoomaImageActivationTrigger, type LoomaImageAttributeResolver, type LoomaImageDescriptor, type LoomaImageRenditionAttributes, type LoomaImageRenditionErrorDetail, type LoomaImageUploadResult, type LoomaImageUploader, type VueEditorAdapterEventMap };
|
package/vue/editor/index.js
CHANGED
|
@@ -130,6 +130,188 @@ import {
|
|
|
130
130
|
getVisualViewportRect,
|
|
131
131
|
LOOMA_ICONS
|
|
132
132
|
} from "@threadlabs/looma/core";
|
|
133
|
+
|
|
134
|
+
// src/editor/image-delivery.ts
|
|
135
|
+
import { Extension } from "@tiptap/core";
|
|
136
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
137
|
+
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
138
|
+
function optionalString(value) {
|
|
139
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
140
|
+
}
|
|
141
|
+
function normalizeImageDimension(value) {
|
|
142
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
|
|
143
|
+
}
|
|
144
|
+
function imageDescriptorFromAttrs(attrs) {
|
|
145
|
+
const src = optionalString(attrs.src);
|
|
146
|
+
if (!src) return null;
|
|
147
|
+
const alt = typeof attrs.alt === "string" ? attrs.alt : void 0;
|
|
148
|
+
const width = normalizeImageDimension(attrs.width);
|
|
149
|
+
const height = normalizeImageDimension(attrs.height);
|
|
150
|
+
return {
|
|
151
|
+
src,
|
|
152
|
+
...alt !== void 0 ? { alt } : {},
|
|
153
|
+
...width ? { width } : {},
|
|
154
|
+
...height ? { height } : {},
|
|
155
|
+
...attrs.responsive === true ? { responsive: true } : {}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function transientAttributes(resolver, image) {
|
|
159
|
+
const attributes = {
|
|
160
|
+
"aria-label": image.alt ? `Open image: ${image.alt}` : "Open image",
|
|
161
|
+
"data-looma-image": "",
|
|
162
|
+
role: "button",
|
|
163
|
+
src: image.src,
|
|
164
|
+
tabindex: "0"
|
|
165
|
+
};
|
|
166
|
+
if (!image.responsive || !resolver) return attributes;
|
|
167
|
+
let rendition;
|
|
168
|
+
try {
|
|
169
|
+
rendition = resolver(image);
|
|
170
|
+
} catch {
|
|
171
|
+
return attributes;
|
|
172
|
+
}
|
|
173
|
+
if (!rendition) return attributes;
|
|
174
|
+
const src = optionalString(rendition.src);
|
|
175
|
+
const srcset = optionalString(rendition.srcset);
|
|
176
|
+
const sizes = optionalString(rendition.sizes);
|
|
177
|
+
if (src) attributes.src = src;
|
|
178
|
+
if (srcset) attributes.srcset = srcset;
|
|
179
|
+
if (sizes) attributes.sizes = sizes;
|
|
180
|
+
if (rendition.loading === "eager" || rendition.loading === "lazy") {
|
|
181
|
+
attributes.loading = rendition.loading;
|
|
182
|
+
}
|
|
183
|
+
if (["async", "auto", "sync"].includes(rendition.decoding ?? "")) {
|
|
184
|
+
attributes.decoding = rendition.decoding;
|
|
185
|
+
}
|
|
186
|
+
if (["auto", "high", "low"].includes(rendition.fetchPriority ?? "")) {
|
|
187
|
+
attributes.fetchpriority = rendition.fetchPriority;
|
|
188
|
+
}
|
|
189
|
+
if (src && src !== image.src || srcset) attributes["data-looma-rendition"] = "true";
|
|
190
|
+
return attributes;
|
|
191
|
+
}
|
|
192
|
+
function createDecorations(doc, resolver, failedRenditions) {
|
|
193
|
+
const decorations = [];
|
|
194
|
+
const currentSources = /* @__PURE__ */ new Set();
|
|
195
|
+
doc.descendants((node, position) => {
|
|
196
|
+
if (node.type.name !== "image") return;
|
|
197
|
+
const image = imageDescriptorFromAttrs(node.attrs);
|
|
198
|
+
if (!image) return;
|
|
199
|
+
currentSources.add(image.src);
|
|
200
|
+
decorations.push(Decoration.node(
|
|
201
|
+
position,
|
|
202
|
+
position + node.nodeSize,
|
|
203
|
+
transientAttributes(failedRenditions.has(image.src) ? void 0 : resolver, image)
|
|
204
|
+
));
|
|
205
|
+
});
|
|
206
|
+
for (const src of failedRenditions) {
|
|
207
|
+
if (!currentSources.has(src)) failedRenditions.delete(src);
|
|
208
|
+
}
|
|
209
|
+
return DecorationSet.create(doc, decorations);
|
|
210
|
+
}
|
|
211
|
+
function rangeContainsImage(doc, from, to) {
|
|
212
|
+
const start = Math.max(0, Math.min(from - 1, doc.content.size));
|
|
213
|
+
const end = Math.max(start, Math.min(to + 1, doc.content.size));
|
|
214
|
+
let found = false;
|
|
215
|
+
doc.nodesBetween(start, end, (node) => {
|
|
216
|
+
if (node.type.name !== "image") return !found;
|
|
217
|
+
found = true;
|
|
218
|
+
return false;
|
|
219
|
+
});
|
|
220
|
+
return found;
|
|
221
|
+
}
|
|
222
|
+
function transactionChangesImages(transaction) {
|
|
223
|
+
let changed = false;
|
|
224
|
+
transaction.mapping.maps.forEach((stepMap) => {
|
|
225
|
+
stepMap.forEach((oldStart, oldEnd, newStart, newEnd) => {
|
|
226
|
+
if (rangeContainsImage(transaction.before, oldStart, oldEnd) || rangeContainsImage(transaction.doc, newStart, newEnd)) changed = true;
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
return changed;
|
|
230
|
+
}
|
|
231
|
+
function createLoomaImageDeliveryController(options) {
|
|
232
|
+
const pluginKey = new PluginKey("looma-image-delivery");
|
|
233
|
+
const failedRenditions = /* @__PURE__ */ new Set();
|
|
234
|
+
const refresh = (editor) => {
|
|
235
|
+
editor.view.dispatch(editor.state.tr.setMeta(pluginKey, true));
|
|
236
|
+
};
|
|
237
|
+
const extension = Extension.create({
|
|
238
|
+
name: "loomaImageDelivery",
|
|
239
|
+
addGlobalAttributes() {
|
|
240
|
+
return [{
|
|
241
|
+
types: ["image"],
|
|
242
|
+
attributes: {
|
|
243
|
+
width: {
|
|
244
|
+
default: null,
|
|
245
|
+
parseHTML: (element) => normalizeImageDimension(Number(element.getAttribute("width"))) ?? null,
|
|
246
|
+
renderHTML: (attributes) => normalizeImageDimension(attributes.width) ? { width: normalizeImageDimension(attributes.width) } : {}
|
|
247
|
+
},
|
|
248
|
+
height: {
|
|
249
|
+
default: null,
|
|
250
|
+
parseHTML: (element) => normalizeImageDimension(Number(element.getAttribute("height"))) ?? null,
|
|
251
|
+
renderHTML: (attributes) => normalizeImageDimension(attributes.height) ? { height: normalizeImageDimension(attributes.height) } : {}
|
|
252
|
+
},
|
|
253
|
+
responsive: {
|
|
254
|
+
default: null,
|
|
255
|
+
parseHTML: (element) => element.hasAttribute("data-looma-responsive") || null,
|
|
256
|
+
renderHTML: (attributes) => attributes.responsive === true ? { "data-looma-responsive": "" } : {}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}];
|
|
260
|
+
},
|
|
261
|
+
addProseMirrorPlugins() {
|
|
262
|
+
return [new Plugin({
|
|
263
|
+
key: pluginKey,
|
|
264
|
+
state: {
|
|
265
|
+
init: (_, state) => createDecorations(
|
|
266
|
+
state.doc,
|
|
267
|
+
options.resolveAttributes(),
|
|
268
|
+
failedRenditions
|
|
269
|
+
),
|
|
270
|
+
apply: (transaction, previous) => {
|
|
271
|
+
if (!transaction.getMeta(pluginKey) && !transactionChangesImages(transaction)) {
|
|
272
|
+
return previous.map(transaction.mapping, transaction.doc);
|
|
273
|
+
}
|
|
274
|
+
return createDecorations(
|
|
275
|
+
transaction.doc,
|
|
276
|
+
options.resolveAttributes(),
|
|
277
|
+
failedRenditions
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
props: {
|
|
282
|
+
decorations: (state) => pluginKey.getState(state)
|
|
283
|
+
}
|
|
284
|
+
})];
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
return {
|
|
288
|
+
extension,
|
|
289
|
+
markRenditionFailed: (editor, src) => {
|
|
290
|
+
if (failedRenditions.has(src)) return false;
|
|
291
|
+
failedRenditions.add(src);
|
|
292
|
+
refresh(editor);
|
|
293
|
+
return true;
|
|
294
|
+
},
|
|
295
|
+
reset: (editor) => {
|
|
296
|
+
failedRenditions.clear();
|
|
297
|
+
refresh(editor);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function imageUploadContent(image) {
|
|
302
|
+
return {
|
|
303
|
+
type: "image",
|
|
304
|
+
attrs: {
|
|
305
|
+
src: image.src,
|
|
306
|
+
alt: image.alt,
|
|
307
|
+
...image.width ? { width: image.width } : {},
|
|
308
|
+
...image.height ? { height: image.height } : {},
|
|
309
|
+
...image.responsive === true ? { responsive: true } : {}
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/editor/LoomaEditor.ts
|
|
133
315
|
var EMPTY_DOCUMENT = { type: "doc", content: [] };
|
|
134
316
|
var EMPTY_CAPABILITIES = {
|
|
135
317
|
canAddRowBefore: false,
|
|
@@ -189,13 +371,19 @@ var LoomaEditor = defineComponent2({
|
|
|
189
371
|
uploadImage: {
|
|
190
372
|
type: Function,
|
|
191
373
|
default: void 0
|
|
374
|
+
},
|
|
375
|
+
resolveImageAttributes: {
|
|
376
|
+
type: Function,
|
|
377
|
+
default: void 0
|
|
192
378
|
}
|
|
193
379
|
},
|
|
194
380
|
emits: {
|
|
195
381
|
"update:modelValue": (_value) => true,
|
|
196
382
|
update: (_value) => true,
|
|
197
383
|
ready: (_editor) => true,
|
|
198
|
-
uploadError: (_error, _file) => true
|
|
384
|
+
uploadError: (_error, _file) => true,
|
|
385
|
+
imageActivate: (_detail) => true,
|
|
386
|
+
imageRenditionError: (_detail) => true
|
|
199
387
|
},
|
|
200
388
|
setup(props, { attrs, emit, expose }) {
|
|
201
389
|
const root = ref(null);
|
|
@@ -208,6 +396,7 @@ var LoomaEditor = defineComponent2({
|
|
|
208
396
|
const mobileToolbarShell = ref(null);
|
|
209
397
|
const dragOver = ref(false);
|
|
210
398
|
const uploading = ref(false);
|
|
399
|
+
const failedUpload = ref(null);
|
|
211
400
|
const tablePickerOpen = ref(false);
|
|
212
401
|
const tablePickerStyle = ref({});
|
|
213
402
|
const mobileToolbarStyle = ref({});
|
|
@@ -249,9 +438,13 @@ var LoomaEditor = defineComponent2({
|
|
|
249
438
|
Object.assign(slash, state);
|
|
250
439
|
}
|
|
251
440
|
});
|
|
441
|
+
const imageDelivery = createLoomaImageDeliveryController({
|
|
442
|
+
resolveAttributes: () => props.resolveImageAttributes
|
|
443
|
+
});
|
|
252
444
|
const editor = useEditor({
|
|
253
445
|
extensions: [
|
|
254
446
|
...getDefaultEditorExtensions({ placeholder: props.placeholder }),
|
|
447
|
+
imageDelivery.extension,
|
|
255
448
|
slashExtension,
|
|
256
449
|
...props.extensions
|
|
257
450
|
],
|
|
@@ -265,6 +458,10 @@ var LoomaEditor = defineComponent2({
|
|
|
265
458
|
}
|
|
266
459
|
});
|
|
267
460
|
watch(() => props.editable, (editable) => editor.value?.setEditable(editable));
|
|
461
|
+
watch(() => props.resolveImageAttributes, () => {
|
|
462
|
+
const instance = editor.value;
|
|
463
|
+
if (instance) imageDelivery.reset(instance);
|
|
464
|
+
});
|
|
268
465
|
watch(() => props.modelValue, (value) => {
|
|
269
466
|
const instance = editor.value;
|
|
270
467
|
if (!instance || sameDocument(instance.getJSON(), value)) return;
|
|
@@ -441,6 +638,49 @@ var LoomaEditor = defineComponent2({
|
|
|
441
638
|
nextTick(updateTableUi);
|
|
442
639
|
});
|
|
443
640
|
};
|
|
641
|
+
const imageForElement = (element) => {
|
|
642
|
+
const instance = editor.value;
|
|
643
|
+
if (!instance) return null;
|
|
644
|
+
try {
|
|
645
|
+
const position = instance.view.posAtDOM(element, 0);
|
|
646
|
+
const node = instance.state.doc.nodeAt(position);
|
|
647
|
+
return node?.type.name === "image" ? imageDescriptorFromAttrs(node.attrs) : null;
|
|
648
|
+
} catch {
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
const imageElementForEvent = (event) => {
|
|
653
|
+
const element = event.target instanceof Element ? event.target.closest("img[data-looma-image]") : null;
|
|
654
|
+
return element && root.value?.contains(element) ? element : null;
|
|
655
|
+
};
|
|
656
|
+
const activateImage = (element, trigger) => {
|
|
657
|
+
const image = imageForElement(element);
|
|
658
|
+
if (image) emit("imageActivate", { ...image, trigger });
|
|
659
|
+
};
|
|
660
|
+
const onImageClick = (event) => {
|
|
661
|
+
const element = imageElementForEvent(event);
|
|
662
|
+
if (element && !props.editable) activateImage(element, "pointer");
|
|
663
|
+
};
|
|
664
|
+
const onImageDoubleClick = (event) => {
|
|
665
|
+
const element = imageElementForEvent(event);
|
|
666
|
+
if (element && props.editable) activateImage(element, "pointer");
|
|
667
|
+
};
|
|
668
|
+
const onImageKeyDown = (event) => {
|
|
669
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
670
|
+
const element = imageElementForEvent(event);
|
|
671
|
+
if (!element) return;
|
|
672
|
+
event.preventDefault();
|
|
673
|
+
activateImage(element, "keyboard");
|
|
674
|
+
};
|
|
675
|
+
const onImageError = (event) => {
|
|
676
|
+
const element = imageElementForEvent(event);
|
|
677
|
+
if (!element || element.dataset.loomaRendition !== "true") return;
|
|
678
|
+
const image = imageForElement(element);
|
|
679
|
+
if (!image) return;
|
|
680
|
+
const instance = editor.value;
|
|
681
|
+
if (!instance || !imageDelivery.markRenditionFailed(instance, image.src)) return;
|
|
682
|
+
emit("imageRenditionError", { ...image, trigger: "programmatic" });
|
|
683
|
+
};
|
|
444
684
|
onMounted(() => {
|
|
445
685
|
updateMobileViewport();
|
|
446
686
|
window.addEventListener("resize", onViewportChange);
|
|
@@ -450,6 +690,7 @@ var LoomaEditor = defineComponent2({
|
|
|
450
690
|
document.addEventListener("pointerdown", onDocumentPointerDown, true);
|
|
451
691
|
document.addEventListener("pointerdown", onResizePointerDown, true);
|
|
452
692
|
document.addEventListener("pointerup", onResizePointerUp, true);
|
|
693
|
+
root.value?.addEventListener("error", onImageError, true);
|
|
453
694
|
});
|
|
454
695
|
onBeforeUnmount(() => {
|
|
455
696
|
window.removeEventListener("resize", onViewportChange);
|
|
@@ -459,6 +700,7 @@ var LoomaEditor = defineComponent2({
|
|
|
459
700
|
document.removeEventListener("pointerdown", onDocumentPointerDown, true);
|
|
460
701
|
document.removeEventListener("pointerdown", onResizePointerDown, true);
|
|
461
702
|
document.removeEventListener("pointerup", onResizePointerUp, true);
|
|
703
|
+
root.value?.removeEventListener("error", onImageError, true);
|
|
462
704
|
unbindEditorUi(editor.value);
|
|
463
705
|
if (dragLeaveTimer) clearTimeout(dragLeaveTimer);
|
|
464
706
|
if (tablePointerFrame !== null) cancelAnimationFrame(tablePointerFrame);
|
|
@@ -469,8 +711,18 @@ var LoomaEditor = defineComponent2({
|
|
|
469
711
|
try {
|
|
470
712
|
const result = await props.uploadImage(file);
|
|
471
713
|
const image = typeof result === "string" ? { url: result } : result;
|
|
472
|
-
|
|
714
|
+
const width = normalizeImageDimension(image.width);
|
|
715
|
+
const height = normalizeImageDimension(image.height);
|
|
716
|
+
editor.value.chain().focus().insertContent(imageUploadContent({
|
|
717
|
+
src: image.url,
|
|
718
|
+
alt: image.alt ?? file.name,
|
|
719
|
+
...width ? { width } : {},
|
|
720
|
+
...height ? { height } : {},
|
|
721
|
+
...image.responsive === true ? { responsive: true } : {}
|
|
722
|
+
})).run();
|
|
723
|
+
failedUpload.value = null;
|
|
473
724
|
} catch (error) {
|
|
725
|
+
failedUpload.value = { file };
|
|
474
726
|
emit("uploadError", error, file);
|
|
475
727
|
} finally {
|
|
476
728
|
uploading.value = false;
|
|
@@ -658,7 +910,10 @@ var LoomaEditor = defineComponent2({
|
|
|
658
910
|
dragOver.value = false;
|
|
659
911
|
}, 100);
|
|
660
912
|
},
|
|
661
|
-
onDrop
|
|
913
|
+
onDrop,
|
|
914
|
+
onClick: onImageClick,
|
|
915
|
+
onDblclick: onImageDoubleClick,
|
|
916
|
+
onKeydown: onImageKeyDown
|
|
662
917
|
}, [
|
|
663
918
|
instance && props.editable && !mobile.value ? h2(BubbleMenu, {
|
|
664
919
|
editor: instance,
|
|
@@ -703,6 +958,20 @@ var LoomaEditor = defineComponent2({
|
|
|
703
958
|
"aria-hidden": "true",
|
|
704
959
|
onChange: onFileChange
|
|
705
960
|
}),
|
|
961
|
+
failedUpload.value ? h2("div", {
|
|
962
|
+
class: "looma-editor__upload-error",
|
|
963
|
+
role: "alert"
|
|
964
|
+
}, [
|
|
965
|
+
h2("span", "Couldn\u2019t upload image."),
|
|
966
|
+
h2("button", {
|
|
967
|
+
type: "button",
|
|
968
|
+
disabled: uploading.value,
|
|
969
|
+
onClick: () => {
|
|
970
|
+
const attempt = failedUpload.value;
|
|
971
|
+
if (attempt) void insertImage(attempt.file);
|
|
972
|
+
}
|
|
973
|
+
}, uploading.value ? "Retrying\u2026" : "Retry")
|
|
974
|
+
]) : null,
|
|
706
975
|
dragOver.value ? h2("div", { class: "looma-editor__drop-overlay", "aria-hidden": "true" }, [
|
|
707
976
|
loomaIcon("image"),
|
|
708
977
|
h2("span", "Drop image to upload")
|