@domternal/extension-image 0.2.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/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/index.cjs +741 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +96 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.js +738 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
import { Node, defaultIcons, PluginKey as PluginKey$1, positionFloating } from '@domternal/core';
|
|
2
|
+
import { PluginKey, Plugin, NodeSelection } from '@domternal/pm/state';
|
|
3
|
+
import { InputRule } from '@domternal/pm/inputrules';
|
|
4
|
+
import { Decoration, DecorationSet } from '@domternal/pm/view';
|
|
5
|
+
|
|
6
|
+
// src/Image.ts
|
|
7
|
+
var imageUploadPluginKey = new PluginKey("imageUpload");
|
|
8
|
+
function isValidImageFile(file, allowedMimeTypes, maxFileSize) {
|
|
9
|
+
if (!allowedMimeTypes.includes(file.type)) return false;
|
|
10
|
+
if (maxFileSize > 0 && file.size > maxFileSize) return false;
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
var placeholderCounter = 0;
|
|
14
|
+
function createPlaceholderId() {
|
|
15
|
+
return `image-upload-${String(++placeholderCounter)}`;
|
|
16
|
+
}
|
|
17
|
+
function createPlaceholderElement() {
|
|
18
|
+
const div = document.createElement("div");
|
|
19
|
+
div.className = "domternal-image-uploading";
|
|
20
|
+
return div;
|
|
21
|
+
}
|
|
22
|
+
function imageUploadPlugin(options) {
|
|
23
|
+
const {
|
|
24
|
+
nodeType,
|
|
25
|
+
uploadHandler,
|
|
26
|
+
allowedMimeTypes,
|
|
27
|
+
maxFileSize,
|
|
28
|
+
onUploadStart,
|
|
29
|
+
onUploadError
|
|
30
|
+
} = options;
|
|
31
|
+
function handleFiles(view, files, pos) {
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
const id = createPlaceholderId();
|
|
34
|
+
const tr = view.state.tr;
|
|
35
|
+
tr.setMeta(imageUploadPluginKey, { type: "add", id, pos });
|
|
36
|
+
view.dispatch(tr);
|
|
37
|
+
if (onUploadStart) onUploadStart(file);
|
|
38
|
+
uploadHandler(file).then((url) => {
|
|
39
|
+
const decos = imageUploadPluginKey.getState(
|
|
40
|
+
view.state
|
|
41
|
+
);
|
|
42
|
+
const found = decos?.find(
|
|
43
|
+
void 0,
|
|
44
|
+
void 0,
|
|
45
|
+
(spec) => spec["id"] === id
|
|
46
|
+
);
|
|
47
|
+
const placeholderPos = found?.[0]?.from;
|
|
48
|
+
if (placeholderPos === void 0) return;
|
|
49
|
+
const insertTr = view.state.tr;
|
|
50
|
+
insertTr.setMeta(imageUploadPluginKey, { type: "remove", id });
|
|
51
|
+
insertTr.insert(placeholderPos, nodeType.create({ src: url }));
|
|
52
|
+
view.dispatch(insertTr);
|
|
53
|
+
}).catch((error) => {
|
|
54
|
+
const removeTr = view.state.tr;
|
|
55
|
+
removeTr.setMeta(imageUploadPluginKey, { type: "remove", id });
|
|
56
|
+
view.dispatch(removeTr);
|
|
57
|
+
if (onUploadError) {
|
|
58
|
+
onUploadError(
|
|
59
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
60
|
+
file
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return new Plugin({
|
|
67
|
+
key: imageUploadPluginKey,
|
|
68
|
+
state: {
|
|
69
|
+
init() {
|
|
70
|
+
return DecorationSet.empty;
|
|
71
|
+
},
|
|
72
|
+
apply(tr, decorations) {
|
|
73
|
+
decorations = decorations.map(tr.mapping, tr.doc);
|
|
74
|
+
const action = tr.getMeta(imageUploadPluginKey);
|
|
75
|
+
if (action?.type === "add") {
|
|
76
|
+
const widget = Decoration.widget(
|
|
77
|
+
action.pos,
|
|
78
|
+
createPlaceholderElement(),
|
|
79
|
+
{ id: action.id }
|
|
80
|
+
);
|
|
81
|
+
return decorations.add(tr.doc, [widget]);
|
|
82
|
+
}
|
|
83
|
+
if (action?.type === "remove") {
|
|
84
|
+
const found = decorations.find(
|
|
85
|
+
void 0,
|
|
86
|
+
void 0,
|
|
87
|
+
(spec) => spec["id"] === action.id
|
|
88
|
+
);
|
|
89
|
+
return decorations.remove(found);
|
|
90
|
+
}
|
|
91
|
+
return decorations;
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
props: {
|
|
95
|
+
decorations(state) {
|
|
96
|
+
return imageUploadPluginKey.getState(state);
|
|
97
|
+
},
|
|
98
|
+
handlePaste(view, event) {
|
|
99
|
+
const items = event.clipboardData?.items;
|
|
100
|
+
if (!items) return false;
|
|
101
|
+
const files = [];
|
|
102
|
+
for (const item of Array.from(items)) {
|
|
103
|
+
if (item.kind === "file") {
|
|
104
|
+
const file = item.getAsFile();
|
|
105
|
+
if (file && isValidImageFile(file, allowedMimeTypes, maxFileSize)) {
|
|
106
|
+
files.push(file);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (files.length === 0) return false;
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
handleFiles(view, files, view.state.selection.from);
|
|
113
|
+
return true;
|
|
114
|
+
},
|
|
115
|
+
handleDrop(view, event) {
|
|
116
|
+
const dataTransfer = event.dataTransfer;
|
|
117
|
+
if (!dataTransfer?.files || dataTransfer.files.length === 0)
|
|
118
|
+
return false;
|
|
119
|
+
const imageFiles = [];
|
|
120
|
+
for (const file of Array.from(dataTransfer.files)) {
|
|
121
|
+
if (isValidImageFile(file, allowedMimeTypes, maxFileSize)) {
|
|
122
|
+
imageFiles.push(file);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (imageFiles.length === 0) return false;
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
const pos = view.posAtCoords({
|
|
128
|
+
left: event.clientX,
|
|
129
|
+
top: event.clientY
|
|
130
|
+
});
|
|
131
|
+
if (!pos) return false;
|
|
132
|
+
handleFiles(view, imageFiles, pos.pos);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/Image.ts
|
|
140
|
+
function isValidImageSrc(value, allowBase64) {
|
|
141
|
+
if (value === null || value === void 0) return true;
|
|
142
|
+
if (typeof value !== "string") return false;
|
|
143
|
+
if (value === "") return true;
|
|
144
|
+
if (/^(javascript|vbscript|file):/i.test(value)) return false;
|
|
145
|
+
if (/^data:/i.test(value)) {
|
|
146
|
+
return allowBase64 && /^data:image\//i.test(value);
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
function readFileAsDataURL(file) {
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
const reader = new FileReader();
|
|
153
|
+
reader.onload = () => {
|
|
154
|
+
resolve(reader.result);
|
|
155
|
+
};
|
|
156
|
+
reader.onerror = () => {
|
|
157
|
+
reject(reader.error ?? new Error("FileReader error"));
|
|
158
|
+
};
|
|
159
|
+
reader.readAsDataURL(file);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
var Image = Node.create({
|
|
163
|
+
name: "image",
|
|
164
|
+
group() {
|
|
165
|
+
return this.options.inline ? "inline" : "block";
|
|
166
|
+
},
|
|
167
|
+
inline() {
|
|
168
|
+
return this.options.inline;
|
|
169
|
+
},
|
|
170
|
+
draggable: true,
|
|
171
|
+
atom: true,
|
|
172
|
+
addOptions() {
|
|
173
|
+
return {
|
|
174
|
+
inline: false,
|
|
175
|
+
allowBase64: true,
|
|
176
|
+
HTMLAttributes: {},
|
|
177
|
+
uploadHandler: null,
|
|
178
|
+
allowedMimeTypes: [
|
|
179
|
+
"image/jpeg",
|
|
180
|
+
"image/png",
|
|
181
|
+
"image/gif",
|
|
182
|
+
"image/webp",
|
|
183
|
+
"image/svg+xml",
|
|
184
|
+
"image/avif"
|
|
185
|
+
],
|
|
186
|
+
maxFileSize: 0,
|
|
187
|
+
onUploadStart: null,
|
|
188
|
+
onUploadError: null
|
|
189
|
+
};
|
|
190
|
+
},
|
|
191
|
+
addAttributes() {
|
|
192
|
+
const { options } = this;
|
|
193
|
+
return {
|
|
194
|
+
src: {
|
|
195
|
+
default: null,
|
|
196
|
+
parseHTML: (element) => {
|
|
197
|
+
const src = element.getAttribute("src");
|
|
198
|
+
if (src && !isValidImageSrc(src, options.allowBase64)) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
return src;
|
|
202
|
+
},
|
|
203
|
+
renderHTML: (attributes) => {
|
|
204
|
+
if (!attributes["src"]) return {};
|
|
205
|
+
return { src: attributes["src"] };
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
alt: {
|
|
209
|
+
default: null,
|
|
210
|
+
parseHTML: (element) => element.getAttribute("alt"),
|
|
211
|
+
renderHTML: (attributes) => {
|
|
212
|
+
if (!attributes["alt"]) return {};
|
|
213
|
+
return { alt: attributes["alt"] };
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
title: {
|
|
217
|
+
default: null,
|
|
218
|
+
parseHTML: (element) => element.getAttribute("title"),
|
|
219
|
+
renderHTML: (attributes) => {
|
|
220
|
+
if (!attributes["title"]) return {};
|
|
221
|
+
return { title: attributes["title"] };
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
width: {
|
|
225
|
+
default: null,
|
|
226
|
+
parseHTML: (element) => element.getAttribute("width"),
|
|
227
|
+
renderHTML: (attributes) => {
|
|
228
|
+
if (!attributes["width"]) return {};
|
|
229
|
+
return { width: attributes["width"] };
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
height: {
|
|
233
|
+
default: null,
|
|
234
|
+
parseHTML: (element) => element.getAttribute("height"),
|
|
235
|
+
renderHTML: (attributes) => {
|
|
236
|
+
if (!attributes["height"]) return {};
|
|
237
|
+
return { height: attributes["height"] };
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
loading: {
|
|
241
|
+
default: null,
|
|
242
|
+
parseHTML: (element) => element.getAttribute("loading"),
|
|
243
|
+
renderHTML: (attributes) => {
|
|
244
|
+
if (!attributes["loading"]) return {};
|
|
245
|
+
return { loading: attributes["loading"] };
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
crossorigin: {
|
|
249
|
+
default: null,
|
|
250
|
+
parseHTML: (element) => element.getAttribute("crossorigin"),
|
|
251
|
+
renderHTML: (attributes) => {
|
|
252
|
+
if (!attributes["crossorigin"]) return {};
|
|
253
|
+
return { crossorigin: attributes["crossorigin"] };
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
float: {
|
|
257
|
+
default: "none",
|
|
258
|
+
parseHTML: (element) => {
|
|
259
|
+
const style = element.style;
|
|
260
|
+
if (style.float === "left") return "left";
|
|
261
|
+
if (style.float === "right") return "right";
|
|
262
|
+
if (style.marginLeft === "auto" && style.marginRight === "auto") return "center";
|
|
263
|
+
const align = element.getAttribute("align");
|
|
264
|
+
if (align === "left") return "left";
|
|
265
|
+
if (align === "right") return "right";
|
|
266
|
+
if (align === "center" || align === "middle") return "center";
|
|
267
|
+
return "none";
|
|
268
|
+
},
|
|
269
|
+
renderHTML: (attributes) => {
|
|
270
|
+
const float = attributes["float"];
|
|
271
|
+
if (!float || float === "none") return {};
|
|
272
|
+
if (float === "left") return { style: "float: left; margin: 0 1em 1em 0;" };
|
|
273
|
+
if (float === "right") return { style: "float: right; margin: 0 0 1em 1em;" };
|
|
274
|
+
if (float === "center") return { style: "display: block; margin-left: auto; margin-right: auto;" };
|
|
275
|
+
return {};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
},
|
|
280
|
+
parseHTML() {
|
|
281
|
+
return [{ tag: "img[src]" }];
|
|
282
|
+
},
|
|
283
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
284
|
+
const src = node.attrs["src"];
|
|
285
|
+
if (src && !isValidImageSrc(src, this.options.allowBase64)) {
|
|
286
|
+
return ["img", { ...this.options.HTMLAttributes, ...HTMLAttributes, src: "" }];
|
|
287
|
+
}
|
|
288
|
+
return ["img", { ...this.options.HTMLAttributes, ...HTMLAttributes }];
|
|
289
|
+
},
|
|
290
|
+
leafText(node) {
|
|
291
|
+
return node.attrs["alt"] ?? "";
|
|
292
|
+
},
|
|
293
|
+
addInputRules() {
|
|
294
|
+
const { nodeType, options } = this;
|
|
295
|
+
if (!nodeType) return [];
|
|
296
|
+
return [
|
|
297
|
+
new InputRule(
|
|
298
|
+
/(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'\u201C\u201D\u2018\u2019]([^"'\u201C\u201D\u2018\u2019]+)["'\u201C\u201D\u2018\u2019])?\))$/,
|
|
299
|
+
(state, match, start, end) => {
|
|
300
|
+
const [fullMatch, wrapper, alt, src, title] = match;
|
|
301
|
+
if (!src || !wrapper) return null;
|
|
302
|
+
if (!isValidImageSrc(src, options.allowBase64)) return null;
|
|
303
|
+
const { tr } = state;
|
|
304
|
+
const attrs = {
|
|
305
|
+
src,
|
|
306
|
+
alt: alt ?? null,
|
|
307
|
+
title: title ?? null
|
|
308
|
+
};
|
|
309
|
+
const offset = fullMatch.length - wrapper.length;
|
|
310
|
+
const from = start + offset;
|
|
311
|
+
tr.replaceWith(from, end, nodeType.create(attrs));
|
|
312
|
+
return tr;
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
];
|
|
316
|
+
},
|
|
317
|
+
addToolbarItems() {
|
|
318
|
+
return [
|
|
319
|
+
// Main toolbar insert button
|
|
320
|
+
{
|
|
321
|
+
type: "button",
|
|
322
|
+
name: "image",
|
|
323
|
+
command: "setImage",
|
|
324
|
+
commandArgs: [{ src: "" }],
|
|
325
|
+
icon: "image",
|
|
326
|
+
label: "Insert Image",
|
|
327
|
+
group: "insert",
|
|
328
|
+
priority: 150,
|
|
329
|
+
emitEvent: "insertImage"
|
|
330
|
+
},
|
|
331
|
+
// Bubble menu only: float controls
|
|
332
|
+
{ type: "button", name: "imageFloatNone", command: "setImageFloat", commandArgs: ["none"], icon: "textIndent", label: "Inline", group: "image-float", priority: 100, isActive: { name: "image", attributes: { float: "none" } }, toolbar: false, bubbleMenu: "image" },
|
|
333
|
+
{ type: "button", name: "imageFloatLeft", command: "setImageFloat", commandArgs: ["left"], icon: "textAlignLeft", label: "Float left", group: "image-float", priority: 90, isActive: { name: "image", attributes: { float: "left" } }, toolbar: false, bubbleMenu: "image" },
|
|
334
|
+
{ type: "button", name: "imageFloatCenter", command: "setImageFloat", commandArgs: ["center"], icon: "textAlignCenter", label: "Center", group: "image-float", priority: 80, isActive: { name: "image", attributes: { float: "center" } }, toolbar: false, bubbleMenu: "image" },
|
|
335
|
+
{ type: "button", name: "imageFloatRight", command: "setImageFloat", commandArgs: ["right"], icon: "textAlignRight", label: "Float right", group: "image-float", priority: 70, isActive: { name: "image", attributes: { float: "right" } }, toolbar: false, bubbleMenu: "image" },
|
|
336
|
+
// Bubble menu only: delete
|
|
337
|
+
{ type: "button", name: "deleteImage", command: "deleteImage", icon: "trash", label: "Delete", group: "image-actions", priority: 50, toolbar: false, bubbleMenu: "image" }
|
|
338
|
+
];
|
|
339
|
+
},
|
|
340
|
+
addNodeView() {
|
|
341
|
+
return (node, view, getPos) => {
|
|
342
|
+
const dom = document.createElement("div");
|
|
343
|
+
dom.className = "dm-image-resizable";
|
|
344
|
+
dom.draggable = true;
|
|
345
|
+
const applyFloat = (float) => {
|
|
346
|
+
if (float && float !== "none") {
|
|
347
|
+
dom.setAttribute("data-float", float);
|
|
348
|
+
} else {
|
|
349
|
+
dom.removeAttribute("data-float");
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
applyFloat(node.attrs["float"]);
|
|
353
|
+
const img = document.createElement("img");
|
|
354
|
+
img.src = node.attrs["src"];
|
|
355
|
+
if (node.attrs["alt"]) img.alt = node.attrs["alt"];
|
|
356
|
+
if (node.attrs["title"]) img.title = node.attrs["title"];
|
|
357
|
+
if (node.attrs["width"]) {
|
|
358
|
+
img.style.width = `${String(node.attrs["width"])}px`;
|
|
359
|
+
}
|
|
360
|
+
dom.appendChild(img);
|
|
361
|
+
dom.addEventListener("mousedown", (e) => {
|
|
362
|
+
if (e.target.closest(".dm-image-handle")) return;
|
|
363
|
+
const pos = getPos();
|
|
364
|
+
if (pos === void 0) return;
|
|
365
|
+
const { selection } = view.state;
|
|
366
|
+
if (selection instanceof NodeSelection && selection.from === pos) return;
|
|
367
|
+
e.preventDefault();
|
|
368
|
+
view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, pos)));
|
|
369
|
+
view.focus();
|
|
370
|
+
});
|
|
371
|
+
for (const corner of ["nw", "ne", "sw", "se"]) {
|
|
372
|
+
const handle = document.createElement("div");
|
|
373
|
+
handle.className = `dm-image-handle dm-image-handle-${corner}`;
|
|
374
|
+
handle.addEventListener("mousedown", (e) => {
|
|
375
|
+
e.preventDefault();
|
|
376
|
+
e.stopPropagation();
|
|
377
|
+
const startX = e.clientX;
|
|
378
|
+
const startWidth = img.offsetWidth;
|
|
379
|
+
const isLeft = corner.includes("w");
|
|
380
|
+
const onMouseMove = (ev) => {
|
|
381
|
+
const dx = isLeft ? startX - ev.clientX : ev.clientX - startX;
|
|
382
|
+
const newWidth = Math.max(50, startWidth + dx);
|
|
383
|
+
img.style.width = `${String(newWidth)}px`;
|
|
384
|
+
};
|
|
385
|
+
const onMouseUp = () => {
|
|
386
|
+
document.removeEventListener("mousemove", onMouseMove);
|
|
387
|
+
document.removeEventListener("mouseup", onMouseUp);
|
|
388
|
+
document.body.style.cursor = "";
|
|
389
|
+
document.body.style.userSelect = "";
|
|
390
|
+
const pos = getPos();
|
|
391
|
+
if (pos === void 0) return;
|
|
392
|
+
const currentNode = view.state.doc.nodeAt(pos);
|
|
393
|
+
if (!currentNode) return;
|
|
394
|
+
const tr = view.state.tr.setNodeMarkup(pos, void 0, {
|
|
395
|
+
...currentNode.attrs,
|
|
396
|
+
width: img.offsetWidth
|
|
397
|
+
});
|
|
398
|
+
view.dispatch(tr);
|
|
399
|
+
};
|
|
400
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
401
|
+
document.addEventListener("mouseup", onMouseUp);
|
|
402
|
+
document.body.style.cursor = isLeft ? "nw-resize" : "ne-resize";
|
|
403
|
+
document.body.style.userSelect = "none";
|
|
404
|
+
});
|
|
405
|
+
dom.appendChild(handle);
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
dom,
|
|
409
|
+
update(updatedNode) {
|
|
410
|
+
if (updatedNode.type.name !== "image") return false;
|
|
411
|
+
img.src = updatedNode.attrs["src"];
|
|
412
|
+
img.alt = updatedNode.attrs["alt"];
|
|
413
|
+
img.title = updatedNode.attrs["title"];
|
|
414
|
+
if (updatedNode.attrs["width"]) {
|
|
415
|
+
img.style.width = `${String(updatedNode.attrs["width"])}px`;
|
|
416
|
+
} else {
|
|
417
|
+
img.style.width = "";
|
|
418
|
+
}
|
|
419
|
+
applyFloat(updatedNode.attrs["float"]);
|
|
420
|
+
node = updatedNode;
|
|
421
|
+
return true;
|
|
422
|
+
},
|
|
423
|
+
selectNode() {
|
|
424
|
+
dom.classList.add("ProseMirror-selectednode");
|
|
425
|
+
},
|
|
426
|
+
deselectNode() {
|
|
427
|
+
dom.classList.remove("ProseMirror-selectednode");
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
};
|
|
431
|
+
},
|
|
432
|
+
addCommands() {
|
|
433
|
+
return {
|
|
434
|
+
setImage: (attributes) => ({ tr, dispatch }) => {
|
|
435
|
+
if (!isValidImageSrc(attributes.src, this.options.allowBase64)) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
if (!this.nodeType) return false;
|
|
439
|
+
if (tr.selection.$from.parent.type.spec.code) return false;
|
|
440
|
+
if (dispatch) {
|
|
441
|
+
const node = this.nodeType.create(attributes);
|
|
442
|
+
tr.replaceSelectionWith(node);
|
|
443
|
+
dispatch(tr);
|
|
444
|
+
}
|
|
445
|
+
return true;
|
|
446
|
+
},
|
|
447
|
+
deleteImage: () => ({ tr, dispatch }) => {
|
|
448
|
+
if (dispatch) {
|
|
449
|
+
tr.deleteSelection();
|
|
450
|
+
dispatch(tr);
|
|
451
|
+
}
|
|
452
|
+
return true;
|
|
453
|
+
},
|
|
454
|
+
setImageFloat: (float) => ({ tr, state, dispatch }) => {
|
|
455
|
+
if (!["none", "left", "right", "center"].includes(float)) return false;
|
|
456
|
+
const { selection } = state;
|
|
457
|
+
const node = state.doc.nodeAt(selection.from);
|
|
458
|
+
if (node?.type.name !== "image") return false;
|
|
459
|
+
if (dispatch) {
|
|
460
|
+
tr.setNodeMarkup(selection.from, void 0, {
|
|
461
|
+
...node.attrs,
|
|
462
|
+
float
|
|
463
|
+
});
|
|
464
|
+
dispatch(tr);
|
|
465
|
+
}
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
},
|
|
470
|
+
addProseMirrorPlugins() {
|
|
471
|
+
const plugins = [];
|
|
472
|
+
const editor = this.editor;
|
|
473
|
+
const nodeType = this.nodeType;
|
|
474
|
+
const options = this.options;
|
|
475
|
+
const storage = this.storage;
|
|
476
|
+
if (nodeType) {
|
|
477
|
+
const el = document.createElement("div");
|
|
478
|
+
el.className = "dm-image-popover";
|
|
479
|
+
el.setAttribute("data-dm-editor-ui", "");
|
|
480
|
+
const urlInput = document.createElement("input");
|
|
481
|
+
urlInput.type = "url";
|
|
482
|
+
urlInput.placeholder = "Image URL...";
|
|
483
|
+
urlInput.className = "dm-image-popover-input";
|
|
484
|
+
const applyBtn = document.createElement("button");
|
|
485
|
+
applyBtn.type = "button";
|
|
486
|
+
applyBtn.className = "dm-image-popover-btn dm-image-popover-apply";
|
|
487
|
+
applyBtn.title = "Insert image";
|
|
488
|
+
applyBtn.innerHTML = defaultIcons["check"] ?? "";
|
|
489
|
+
const browseBtn = document.createElement("button");
|
|
490
|
+
browseBtn.type = "button";
|
|
491
|
+
browseBtn.className = "dm-image-popover-btn dm-image-popover-browse";
|
|
492
|
+
browseBtn.title = "Browse files";
|
|
493
|
+
browseBtn.innerHTML = defaultIcons["image"] ?? "";
|
|
494
|
+
el.appendChild(urlInput);
|
|
495
|
+
el.appendChild(applyBtn);
|
|
496
|
+
el.appendChild(browseBtn);
|
|
497
|
+
let isOpen = false;
|
|
498
|
+
let cleanupFloating = null;
|
|
499
|
+
let toggleAnchor = null;
|
|
500
|
+
const showPopover = (anchorElement) => {
|
|
501
|
+
toggleAnchor = anchorElement ?? null;
|
|
502
|
+
urlInput.value = "";
|
|
503
|
+
el.setAttribute("data-show", "");
|
|
504
|
+
isOpen = true;
|
|
505
|
+
storage["isOpen"] = true;
|
|
506
|
+
editor.view.dispatch(editor.view.state.tr);
|
|
507
|
+
const reference = anchorElement ?? {
|
|
508
|
+
getBoundingClientRect: () => {
|
|
509
|
+
const coords = editor.view.coordsAtPos(editor.view.state.selection.from);
|
|
510
|
+
return new DOMRect(coords.left, coords.top, 0, coords.bottom - coords.top);
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
cleanupFloating?.();
|
|
514
|
+
cleanupFloating = positionFloating(reference, el, {
|
|
515
|
+
placement: "bottom",
|
|
516
|
+
offsetValue: 4
|
|
517
|
+
});
|
|
518
|
+
urlInput.focus();
|
|
519
|
+
};
|
|
520
|
+
const hidePopover = () => {
|
|
521
|
+
if (!isOpen) return;
|
|
522
|
+
toggleAnchor = null;
|
|
523
|
+
cleanupFloating?.();
|
|
524
|
+
cleanupFloating = null;
|
|
525
|
+
el.removeAttribute("data-show");
|
|
526
|
+
isOpen = false;
|
|
527
|
+
storage["isOpen"] = false;
|
|
528
|
+
editor.view.dispatch(editor.view.state.tr);
|
|
529
|
+
};
|
|
530
|
+
const closePopover = () => {
|
|
531
|
+
hidePopover();
|
|
532
|
+
editor.view.focus();
|
|
533
|
+
};
|
|
534
|
+
const insertFromFile = (file) => {
|
|
535
|
+
if (options.uploadHandler) {
|
|
536
|
+
options.uploadHandler(file).then((url) => {
|
|
537
|
+
editor.commands.setImage({ src: url });
|
|
538
|
+
}).catch((error) => {
|
|
539
|
+
if (options.onUploadError) {
|
|
540
|
+
options.onUploadError(
|
|
541
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
542
|
+
file
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
} else {
|
|
547
|
+
void readFileAsDataURL(file).then((src) => {
|
|
548
|
+
const { tr } = editor.view.state;
|
|
549
|
+
tr.replaceSelectionWith(nodeType.create({ src }));
|
|
550
|
+
editor.view.dispatch(tr);
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
const applyUrl = () => {
|
|
555
|
+
const src = urlInput.value.trim();
|
|
556
|
+
if (src && isValidImageSrc(src, options.allowBase64)) {
|
|
557
|
+
editor.commands.setImage({ src });
|
|
558
|
+
}
|
|
559
|
+
closePopover();
|
|
560
|
+
};
|
|
561
|
+
const openFileBrowser = () => {
|
|
562
|
+
hidePopover();
|
|
563
|
+
const input = document.createElement("input");
|
|
564
|
+
input.type = "file";
|
|
565
|
+
input.accept = options.allowedMimeTypes.join(",");
|
|
566
|
+
input.addEventListener("change", () => {
|
|
567
|
+
const file = input.files?.[0];
|
|
568
|
+
if (file) insertFromFile(file);
|
|
569
|
+
editor.view.focus();
|
|
570
|
+
});
|
|
571
|
+
input.click();
|
|
572
|
+
};
|
|
573
|
+
const onInsertImage = (data) => {
|
|
574
|
+
if (isOpen) {
|
|
575
|
+
closePopover();
|
|
576
|
+
} else {
|
|
577
|
+
showPopover(data.anchorElement);
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
const onInputKeydown = (e) => {
|
|
581
|
+
if (e.key === "Enter") {
|
|
582
|
+
e.preventDefault();
|
|
583
|
+
applyUrl();
|
|
584
|
+
} else if (e.key === "Escape") {
|
|
585
|
+
e.preventDefault();
|
|
586
|
+
closePopover();
|
|
587
|
+
} else if (e.key === "Tab") {
|
|
588
|
+
e.preventDefault();
|
|
589
|
+
applyBtn.focus();
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
const onButtonKeydown = (e) => {
|
|
593
|
+
if (e.key === "Escape") {
|
|
594
|
+
e.preventDefault();
|
|
595
|
+
closePopover();
|
|
596
|
+
} else if (e.key === "Enter") {
|
|
597
|
+
e.preventDefault();
|
|
598
|
+
e.target.click();
|
|
599
|
+
} else if (e.key === "Tab") {
|
|
600
|
+
e.preventDefault();
|
|
601
|
+
const target = e.target;
|
|
602
|
+
if (e.shiftKey) {
|
|
603
|
+
if (target === applyBtn) urlInput.focus();
|
|
604
|
+
else applyBtn.focus();
|
|
605
|
+
} else {
|
|
606
|
+
if (target === applyBtn) browseBtn.focus();
|
|
607
|
+
else urlInput.focus();
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
const onClickOutside = (e) => {
|
|
612
|
+
if (!isOpen || el.contains(e.target)) return;
|
|
613
|
+
if (toggleAnchor && (toggleAnchor === e.target || toggleAnchor.contains(e.target))) return;
|
|
614
|
+
hidePopover();
|
|
615
|
+
};
|
|
616
|
+
const onPreventBlur = (e) => {
|
|
617
|
+
e.preventDefault();
|
|
618
|
+
};
|
|
619
|
+
let dragCounter = 0;
|
|
620
|
+
const hasImageItems = (dt) => {
|
|
621
|
+
if (!dt?.items) return false;
|
|
622
|
+
for (const item of Array.from(dt.items)) {
|
|
623
|
+
if (item.kind === "file" && item.type.startsWith("image/")) return true;
|
|
624
|
+
}
|
|
625
|
+
return false;
|
|
626
|
+
};
|
|
627
|
+
plugins.push(new Plugin({
|
|
628
|
+
key: new PluginKey$1("imageFileBrowser"),
|
|
629
|
+
props: {
|
|
630
|
+
handleDOMEvents: {
|
|
631
|
+
dragenter(view, event) {
|
|
632
|
+
if (!hasImageItems(event.dataTransfer)) return false;
|
|
633
|
+
dragCounter++;
|
|
634
|
+
view.dom.closest(".dm-editor")?.classList.add("dm-dragover");
|
|
635
|
+
return false;
|
|
636
|
+
},
|
|
637
|
+
dragleave(view) {
|
|
638
|
+
dragCounter--;
|
|
639
|
+
if (dragCounter <= 0) {
|
|
640
|
+
dragCounter = 0;
|
|
641
|
+
view.dom.closest(".dm-editor")?.classList.remove("dm-dragover");
|
|
642
|
+
}
|
|
643
|
+
return false;
|
|
644
|
+
},
|
|
645
|
+
drop(view) {
|
|
646
|
+
dragCounter = 0;
|
|
647
|
+
view.dom.closest(".dm-editor")?.classList.remove("dm-dragover");
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
},
|
|
651
|
+
handlePaste(view, event) {
|
|
652
|
+
if (options.uploadHandler) return false;
|
|
653
|
+
const items = event.clipboardData?.items;
|
|
654
|
+
if (!items) return false;
|
|
655
|
+
for (const item of Array.from(items)) {
|
|
656
|
+
if (item.kind === "file" && item.type.startsWith("image/")) {
|
|
657
|
+
const file = item.getAsFile();
|
|
658
|
+
if (!file) continue;
|
|
659
|
+
if (!options.allowedMimeTypes.includes(file.type)) continue;
|
|
660
|
+
if (options.maxFileSize > 0 && file.size > options.maxFileSize) continue;
|
|
661
|
+
event.preventDefault();
|
|
662
|
+
void readFileAsDataURL(file).then((src) => {
|
|
663
|
+
const { tr } = view.state;
|
|
664
|
+
tr.replaceSelectionWith(nodeType.create({ src }));
|
|
665
|
+
view.dispatch(tr);
|
|
666
|
+
});
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return false;
|
|
671
|
+
},
|
|
672
|
+
handleDrop(view, event) {
|
|
673
|
+
if (options.uploadHandler) return false;
|
|
674
|
+
const files = event.dataTransfer?.files;
|
|
675
|
+
if (!files?.length) return false;
|
|
676
|
+
const file = files[0];
|
|
677
|
+
if (!file || !options.allowedMimeTypes.includes(file.type)) return false;
|
|
678
|
+
if (options.maxFileSize > 0 && file.size > options.maxFileSize) return false;
|
|
679
|
+
event.preventDefault();
|
|
680
|
+
const pos = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
|
681
|
+
if (!pos) return false;
|
|
682
|
+
void readFileAsDataURL(file).then((src) => {
|
|
683
|
+
const tr = view.state.tr;
|
|
684
|
+
tr.insert(pos.pos, nodeType.create({ src }));
|
|
685
|
+
view.dispatch(tr);
|
|
686
|
+
});
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
view() {
|
|
691
|
+
document.body.appendChild(el);
|
|
692
|
+
urlInput.addEventListener("keydown", onInputKeydown);
|
|
693
|
+
applyBtn.addEventListener("mousedown", onPreventBlur);
|
|
694
|
+
applyBtn.addEventListener("click", applyUrl);
|
|
695
|
+
applyBtn.addEventListener("keydown", onButtonKeydown);
|
|
696
|
+
browseBtn.addEventListener("mousedown", onPreventBlur);
|
|
697
|
+
browseBtn.addEventListener("click", openFileBrowser);
|
|
698
|
+
browseBtn.addEventListener("keydown", onButtonKeydown);
|
|
699
|
+
document.addEventListener("mousedown", onClickOutside);
|
|
700
|
+
const dynEditor = editor;
|
|
701
|
+
dynEditor.on("insertImage", onInsertImage);
|
|
702
|
+
return {
|
|
703
|
+
destroy() {
|
|
704
|
+
hidePopover();
|
|
705
|
+
urlInput.removeEventListener("keydown", onInputKeydown);
|
|
706
|
+
applyBtn.removeEventListener("mousedown", onPreventBlur);
|
|
707
|
+
applyBtn.removeEventListener("click", applyUrl);
|
|
708
|
+
applyBtn.removeEventListener("keydown", onButtonKeydown);
|
|
709
|
+
browseBtn.removeEventListener("mousedown", onPreventBlur);
|
|
710
|
+
browseBtn.removeEventListener("click", openFileBrowser);
|
|
711
|
+
browseBtn.removeEventListener("keydown", onButtonKeydown);
|
|
712
|
+
document.removeEventListener("mousedown", onClickOutside);
|
|
713
|
+
dynEditor.off("insertImage", onInsertImage);
|
|
714
|
+
el.remove();
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
}));
|
|
719
|
+
}
|
|
720
|
+
if (options.uploadHandler && nodeType) {
|
|
721
|
+
plugins.push(
|
|
722
|
+
imageUploadPlugin({
|
|
723
|
+
nodeType,
|
|
724
|
+
uploadHandler: options.uploadHandler,
|
|
725
|
+
allowedMimeTypes: options.allowedMimeTypes,
|
|
726
|
+
maxFileSize: options.maxFileSize,
|
|
727
|
+
onUploadStart: options.onUploadStart,
|
|
728
|
+
onUploadError: options.onUploadError
|
|
729
|
+
})
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
return plugins;
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
export { Image, imageUploadPluginKey };
|
|
737
|
+
//# sourceMappingURL=index.js.map
|
|
738
|
+
//# sourceMappingURL=index.js.map
|