Frontend 1.53+ adopts custom widgets placed in node.widgets into a
store-backed LegacyWidget. The custom ImageListWidget's value then reads
from the store, so uploaded images were never serialized ("images": "[]")
and the instanceof checks for drag/drop and paste stopped matching.
Keep the original "images" STRING widget as the stored value (hidden, same
position so old workflows load) and add a display-only "images_ui" widget
that reads and writes through it. Use an own-property marker instead of
instanceof.
Written against frontend 1.52.7 and 1.55.18 sources; not yet browser-tested.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
666 lines
22 KiB
JavaScript
666 lines
22 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
||
import { api } from "../../scripts/api.js";
|
||
|
||
// Constants for the image list widget - matching ComfyUI style
|
||
const IMAGE_HEIGHT = 200; // Height for image display area
|
||
const BUTTON_HEIGHT = 26; // Standard ComfyUI button height
|
||
const DROP_ZONE_HEIGHT = 78; // Height for drop zone (3x button height)
|
||
const FILENAME_HEIGHT = 20; // Height for filename display
|
||
const DIMENSIONS_HEIGHT = 16; // Height for dimensions text
|
||
const SLOT_PADDING = 8;
|
||
const REMOVE_BUTTON_SIZE = 16;
|
||
|
||
/**
|
||
* Display-only widget for managing a list of images with previews.
|
||
* The list itself lives in the node's hidden "images" STRING widget (valueWidget),
|
||
* which is the widget that gets serialized into workflows and prompts.
|
||
* Styled to match ComfyUI's LoadImage node
|
||
*/
|
||
class ImageListWidget {
|
||
constructor(node, valueWidget) {
|
||
this.node = node;
|
||
this.name = "images_ui";
|
||
this.type = "custom";
|
||
this.serialize = false;
|
||
this.options = { serialize: false };
|
||
this.valueWidget = valueWidget;
|
||
|
||
// Own-property marker: newer frontends re-parent custom widgets, so instanceof can't be used
|
||
this.isReferenceChainList = true;
|
||
|
||
// Cache for loaded image elements with dimensions
|
||
this.imageCache = new Map();
|
||
|
||
// Track if we're currently uploading
|
||
this.isUploading = false;
|
||
|
||
// Hover state for remove buttons
|
||
this.hoverRemoveIndex = -1;
|
||
|
||
// Hover state for add button
|
||
this.hoverAddButton = false;
|
||
|
||
// Hover/drag state for drop zone
|
||
this.dropZoneActive = false;
|
||
}
|
||
|
||
get images() {
|
||
let images;
|
||
try {
|
||
images = JSON.parse(this.valueWidget.value || "[]");
|
||
} catch (e) {
|
||
images = [];
|
||
}
|
||
return Array.isArray(images) ? images.filter((img) => img && typeof img === "string") : [];
|
||
}
|
||
|
||
setImages(images) {
|
||
this.valueWidget.value = JSON.stringify(images);
|
||
this.preloadImages();
|
||
this.updateNodeSize();
|
||
}
|
||
|
||
/**
|
||
* Preload images into cache
|
||
*/
|
||
preloadImages() {
|
||
for (const filename of this.images) {
|
||
if (!this.imageCache.has(filename)) {
|
||
const img = new Image();
|
||
img.src = `/view?filename=${encodeURIComponent(filename)}&type=input`;
|
||
img.onload = () => {
|
||
// Store with dimensions
|
||
this.imageCache.set(filename, {
|
||
img: img,
|
||
width: img.naturalWidth,
|
||
height: img.naturalHeight
|
||
});
|
||
if (this.node) {
|
||
this.node.setDirtyCanvas(true, true);
|
||
}
|
||
};
|
||
// Temporarily store just the image while loading
|
||
this.imageCache.set(filename, { img: img, width: 0, height: 0 });
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Calculate slot height for one image entry
|
||
*/
|
||
getSlotHeight() {
|
||
return FILENAME_HEIGHT + IMAGE_HEIGHT + DIMENSIONS_HEIGHT + SLOT_PADDING * 2;
|
||
}
|
||
|
||
/**
|
||
* Calculate widget height based on number of images
|
||
*/
|
||
computeSize() {
|
||
const numImages = this.images.length;
|
||
const height = (numImages * this.getSlotHeight()) + DROP_ZONE_HEIGHT + SLOT_PADDING + BUTTON_HEIGHT + SLOT_PADDING * 2;
|
||
return [this.node?.size?.[0] || 200, height];
|
||
}
|
||
|
||
/**
|
||
* Add an image to the list
|
||
*/
|
||
addImage(filename) {
|
||
const images = this.images;
|
||
if (filename && !images.includes(filename)) {
|
||
images.push(filename);
|
||
this.setImages(images);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Remove an image from the list
|
||
*/
|
||
removeImage(index) {
|
||
const images = this.images;
|
||
if (index >= 0 && index < images.length) {
|
||
const [filename] = images.splice(index, 1);
|
||
this.imageCache.delete(filename);
|
||
this.setImages(images);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Update node size to fit all images
|
||
*/
|
||
updateNodeSize() {
|
||
if (!this.node) return;
|
||
|
||
// Calculate height needed for this widget
|
||
const [_, widgetHeight] = this.computeSize();
|
||
|
||
// Calculate space taken by other widgets above this one
|
||
let otherWidgetsHeight = 0;
|
||
const widgetIndex = this.node.widgets?.indexOf(this);
|
||
if (widgetIndex > 0) {
|
||
for (let i = 0; i < widgetIndex; i++) {
|
||
const w = this.node.widgets[i];
|
||
if (w.hidden) continue;
|
||
// Standard widget height is ~20-26px, use 26 as safe default
|
||
otherWidgetsHeight += (w.computedHeight || 26) + 4;
|
||
}
|
||
}
|
||
|
||
// Title bar + header space (~70px) + other widgets + this widget's height
|
||
const headerSpace = 70;
|
||
const idealHeight = headerSpace + otherWidgetsHeight + widgetHeight;
|
||
const minHeight = 200;
|
||
|
||
this.node.size[1] = Math.max(minHeight, idealHeight);
|
||
this.node.setDirtyCanvas(true, true);
|
||
}
|
||
|
||
/**
|
||
* Upload an image file
|
||
*/
|
||
async uploadImage(file) {
|
||
if (this.isUploading) return;
|
||
this.isUploading = true;
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append("image", file);
|
||
formData.append("type", "input");
|
||
formData.append("overwrite", "false");
|
||
|
||
const response = await api.fetchApi("/upload/image", {
|
||
method: "POST",
|
||
body: formData,
|
||
});
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
let filename = result.name;
|
||
if (result.subfolder) {
|
||
filename = `${result.subfolder}/${result.name}`;
|
||
}
|
||
this.addImage(filename);
|
||
} else {
|
||
console.error("[ReferenceChain] Upload failed:", response.status);
|
||
}
|
||
} catch (e) {
|
||
console.error("[ReferenceChain] Upload error:", e);
|
||
} finally {
|
||
this.isUploading = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Open file dialog to select images
|
||
*/
|
||
openFileDialog() {
|
||
const input = document.createElement("input");
|
||
input.type = "file";
|
||
input.accept = "image/*";
|
||
input.multiple = true;
|
||
input.onchange = async (e) => {
|
||
const files = e.target.files;
|
||
for (const file of files) {
|
||
await this.uploadImage(file);
|
||
}
|
||
};
|
||
input.click();
|
||
}
|
||
|
||
/**
|
||
* Truncate filename to fit width
|
||
*/
|
||
truncateFilename(ctx, filename, maxWidth) {
|
||
const ellipsis = "...";
|
||
let text = filename;
|
||
|
||
if (ctx.measureText(text).width <= maxWidth) {
|
||
return text;
|
||
}
|
||
|
||
// Try to show end of filename (more useful)
|
||
while (text.length > 0 && ctx.measureText(ellipsis + text).width > maxWidth) {
|
||
text = text.slice(1);
|
||
}
|
||
|
||
return ellipsis + text;
|
||
}
|
||
|
||
/**
|
||
* Draw the widget - ComfyUI style
|
||
*/
|
||
draw(ctx, node, widgetWidth, y, widgetHeight) {
|
||
const margin = 15;
|
||
const x = margin;
|
||
const width = widgetWidth - margin * 2;
|
||
|
||
const images = this.images;
|
||
// Covers values restored on workflow load, which bypass setImages
|
||
this.preloadImages();
|
||
|
||
ctx.save();
|
||
|
||
let currentY = y + SLOT_PADDING;
|
||
const slotHeight = this.getSlotHeight();
|
||
|
||
// Store positions for click handling
|
||
this._slotPositions = [];
|
||
|
||
// Draw each image slot
|
||
for (let i = 0; i < images.length; i++) {
|
||
const filename = images[i];
|
||
const slotY = currentY + i * slotHeight;
|
||
|
||
// Get cached image data
|
||
const cached = this.imageCache.get(filename);
|
||
const img = cached?.img;
|
||
const imgWidth = cached?.width || 0;
|
||
const imgHeight = cached?.height || 0;
|
||
|
||
// --- Filename bar (like LoadImage combo) ---
|
||
const filenameY = slotY;
|
||
|
||
// Background for filename
|
||
ctx.fillStyle = "#232323";
|
||
ctx.beginPath();
|
||
ctx.roundRect(x, filenameY, width, FILENAME_HEIGHT, [4, 4, 0, 0]);
|
||
ctx.fill();
|
||
|
||
// Border
|
||
ctx.strokeStyle = "#3a3a3a";
|
||
ctx.lineWidth = 1;
|
||
ctx.stroke();
|
||
|
||
// Filename text
|
||
ctx.fillStyle = "#cccccc";
|
||
ctx.font = "11px Arial";
|
||
ctx.textAlign = "left";
|
||
ctx.textBaseline = "middle";
|
||
|
||
const displayFilename = this.truncateFilename(ctx, filename, width - REMOVE_BUTTON_SIZE - 20);
|
||
ctx.fillText(displayFilename, x + 8, filenameY + FILENAME_HEIGHT / 2);
|
||
|
||
// --- Remove button (X) in filename bar ---
|
||
const removeX = x + width - REMOVE_BUTTON_SIZE - 4;
|
||
const removeY = filenameY + (FILENAME_HEIGHT - REMOVE_BUTTON_SIZE) / 2 + 1;
|
||
|
||
// Hover highlight
|
||
if (this.hoverRemoveIndex === i) {
|
||
ctx.fillStyle = "#555555";
|
||
ctx.beginPath();
|
||
ctx.roundRect(removeX, removeY, REMOVE_BUTTON_SIZE, REMOVE_BUTTON_SIZE - 2, 2);
|
||
ctx.fill();
|
||
}
|
||
|
||
// Draw X
|
||
ctx.strokeStyle = this.hoverRemoveIndex === i ? "#ffffff" : "#888888";
|
||
ctx.lineWidth = 1.5;
|
||
ctx.beginPath();
|
||
const xPad = 4;
|
||
ctx.moveTo(removeX + xPad, removeY + xPad);
|
||
ctx.lineTo(removeX + REMOVE_BUTTON_SIZE - xPad, removeY + REMOVE_BUTTON_SIZE - 2 - xPad);
|
||
ctx.moveTo(removeX + REMOVE_BUTTON_SIZE - xPad, removeY + xPad);
|
||
ctx.lineTo(removeX + xPad, removeY + REMOVE_BUTTON_SIZE - 2 - xPad);
|
||
ctx.stroke();
|
||
|
||
// --- Image area ---
|
||
const imageY = filenameY + FILENAME_HEIGHT;
|
||
|
||
// Image background
|
||
ctx.fillStyle = "#1a1a1a";
|
||
ctx.fillRect(x, imageY, width, IMAGE_HEIGHT);
|
||
|
||
// Draw image (fill width, maintain aspect ratio)
|
||
if (img && img.complete && imgWidth > 0) {
|
||
const imgAspect = imgWidth / imgHeight;
|
||
const areaAspect = width / IMAGE_HEIGHT;
|
||
|
||
let drawW, drawH, drawX, drawY;
|
||
|
||
if (imgAspect > areaAspect) {
|
||
// Image is wider - fit to width
|
||
drawW = width;
|
||
drawH = width / imgAspect;
|
||
drawX = x;
|
||
drawY = imageY + (IMAGE_HEIGHT - drawH) / 2;
|
||
} else {
|
||
// Image is taller - fit to height
|
||
drawH = IMAGE_HEIGHT;
|
||
drawW = IMAGE_HEIGHT * imgAspect;
|
||
drawX = x + (width - drawW) / 2;
|
||
drawY = imageY;
|
||
}
|
||
|
||
ctx.drawImage(img, drawX, drawY, drawW, drawH);
|
||
} else {
|
||
// Loading placeholder
|
||
ctx.fillStyle = "#333333";
|
||
ctx.font = "12px Arial";
|
||
ctx.textAlign = "center";
|
||
ctx.textBaseline = "middle";
|
||
ctx.fillText("Loading...", x + width / 2, imageY + IMAGE_HEIGHT / 2);
|
||
}
|
||
|
||
// --- Dimensions text ---
|
||
const dimY = imageY + IMAGE_HEIGHT;
|
||
|
||
ctx.fillStyle = "#1a1a1a";
|
||
ctx.beginPath();
|
||
ctx.roundRect(x, dimY, width, DIMENSIONS_HEIGHT, [0, 0, 4, 4]);
|
||
ctx.fill();
|
||
|
||
if (imgWidth > 0 && imgHeight > 0) {
|
||
ctx.fillStyle = "#888888";
|
||
ctx.font = "10px Arial";
|
||
ctx.textAlign = "center";
|
||
ctx.textBaseline = "middle";
|
||
ctx.fillText(`${imgWidth} × ${imgHeight}`, x + width / 2, dimY + DIMENSIONS_HEIGHT / 2);
|
||
}
|
||
|
||
// Store slot position for click handling
|
||
this._slotPositions.push({
|
||
y: slotY,
|
||
height: slotHeight,
|
||
filenameY: filenameY,
|
||
filenameHeight: FILENAME_HEIGHT,
|
||
removeX,
|
||
removeY,
|
||
removeSize: REMOVE_BUTTON_SIZE,
|
||
});
|
||
}
|
||
|
||
// --- Drop zone area ---
|
||
const dropZoneY = currentY + images.length * slotHeight + SLOT_PADDING;
|
||
|
||
// Drop zone background
|
||
ctx.fillStyle = this.dropZoneActive ? "#2a2a2a" : "#1a1a1a";
|
||
ctx.beginPath();
|
||
ctx.roundRect(x, dropZoneY, width, DROP_ZONE_HEIGHT, 4);
|
||
ctx.fill();
|
||
|
||
// Drop zone dashed border
|
||
ctx.strokeStyle = this.dropZoneActive ? "#666666" : "#444444";
|
||
ctx.lineWidth = 1;
|
||
ctx.setLineDash([5, 5]);
|
||
ctx.stroke();
|
||
ctx.setLineDash([]); // Reset dash
|
||
|
||
// Drop zone text
|
||
ctx.fillStyle = this.dropZoneActive ? "#888888" : "#555555";
|
||
ctx.font = "12px Arial";
|
||
ctx.textAlign = "center";
|
||
ctx.textBaseline = "middle";
|
||
ctx.fillText("drop image here", x + width / 2, dropZoneY + DROP_ZONE_HEIGHT / 2);
|
||
|
||
// --- "choose file to upload" button (ComfyUI style) ---
|
||
const buttonY = dropZoneY + DROP_ZONE_HEIGHT + SLOT_PADDING;
|
||
|
||
// Button background
|
||
ctx.fillStyle = this.hoverAddButton ? "#3a3a3a" : "#2a2a2a";
|
||
ctx.beginPath();
|
||
ctx.roundRect(x, buttonY, width, BUTTON_HEIGHT, 4);
|
||
ctx.fill();
|
||
|
||
// Button border
|
||
ctx.strokeStyle = "#4a4a4a";
|
||
ctx.lineWidth = 1;
|
||
ctx.stroke();
|
||
|
||
// Button text
|
||
ctx.fillStyle = "#cccccc";
|
||
ctx.font = "12px Arial";
|
||
ctx.textAlign = "center";
|
||
ctx.textBaseline = "middle";
|
||
ctx.fillText("choose file to upload", x + width / 2, buttonY + BUTTON_HEIGHT / 2);
|
||
|
||
ctx.restore();
|
||
|
||
// Store positions
|
||
this._dropZoneY = dropZoneY;
|
||
this._dropZoneHeight = DROP_ZONE_HEIGHT;
|
||
this._buttonY = buttonY;
|
||
this._buttonHeight = BUTTON_HEIGHT;
|
||
this._x = x;
|
||
this._width = width;
|
||
}
|
||
|
||
/**
|
||
* Handle mouse events
|
||
*/
|
||
mouse(event, pos, node) {
|
||
const [mouseX, mouseY] = pos;
|
||
|
||
if (event.type === "pointermove") {
|
||
// Update hover state for remove buttons
|
||
let newHoverIndex = -1;
|
||
if (this._slotPositions) {
|
||
for (let i = 0; i < this._slotPositions.length; i++) {
|
||
const slot = this._slotPositions[i];
|
||
if (
|
||
mouseX >= slot.removeX &&
|
||
mouseX <= slot.removeX + slot.removeSize &&
|
||
mouseY >= slot.removeY &&
|
||
mouseY <= slot.removeY + slot.removeSize
|
||
) {
|
||
newHoverIndex = i;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Update hover state for add button
|
||
const newHoverAddButton = (
|
||
this._buttonY !== undefined &&
|
||
mouseX >= this._x &&
|
||
mouseX <= this._x + this._width &&
|
||
mouseY >= this._buttonY &&
|
||
mouseY <= this._buttonY + this._buttonHeight
|
||
);
|
||
|
||
if (newHoverIndex !== this.hoverRemoveIndex || newHoverAddButton !== this.hoverAddButton) {
|
||
this.hoverRemoveIndex = newHoverIndex;
|
||
this.hoverAddButton = newHoverAddButton;
|
||
node.setDirtyCanvas(true, true);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
if (event.type === "pointerdown") {
|
||
// Check if clicked on a remove button
|
||
if (this._slotPositions) {
|
||
for (let i = 0; i < this._slotPositions.length; i++) {
|
||
const slot = this._slotPositions[i];
|
||
if (
|
||
mouseX >= slot.removeX &&
|
||
mouseX <= slot.removeX + slot.removeSize &&
|
||
mouseY >= slot.removeY &&
|
||
mouseY <= slot.removeY + slot.removeSize
|
||
) {
|
||
this.removeImage(i);
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Check if clicked on add button
|
||
if (
|
||
this._buttonY !== undefined &&
|
||
mouseX >= this._x &&
|
||
mouseX <= this._x + this._width &&
|
||
mouseY >= this._buttonY &&
|
||
mouseY <= this._buttonY + this._buttonHeight
|
||
) {
|
||
this.openFileDialog();
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function getImageListWidget(node) {
|
||
return node.widgets?.find((w) => w.isReferenceChainList);
|
||
}
|
||
|
||
/**
|
||
* Register the extension
|
||
*/
|
||
app.registerExtension({
|
||
name: "Comfy.ReferenceChain",
|
||
|
||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||
if (nodeData.name !== "ReferenceChainConditioning") {
|
||
return;
|
||
}
|
||
|
||
// Store original onNodeCreated
|
||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||
|
||
nodeType.prototype.onNodeCreated = function () {
|
||
const result = onNodeCreated?.apply(this, arguments);
|
||
|
||
// Keep the "images" STRING widget as the serialized value (same position, so old
|
||
// workflows restore into it) and show a display-only list widget right after it
|
||
const imagesWidget = this.widgets?.find((w) => w.name === "images");
|
||
if (imagesWidget) {
|
||
imagesWidget.hidden = true;
|
||
|
||
const listWidget = this.addCustomWidget(new ImageListWidget(this, imagesWidget));
|
||
this.widgets.splice(this.widgets.indexOf(listWidget), 1);
|
||
this.widgets.splice(this.widgets.indexOf(imagesWidget) + 1, 0, listWidget);
|
||
|
||
listWidget.updateNodeSize();
|
||
}
|
||
|
||
// Remove the default image_input_1 slot (created from optional INPUT_TYPES)
|
||
// since image_input_node_count defaults to 0
|
||
const defaultImageInput = this.inputs?.findIndex(input => input.name === "image_input_1");
|
||
if (defaultImageInput !== undefined && defaultImageInput >= 0) {
|
||
this.removeInput(defaultImageInput);
|
||
}
|
||
|
||
// Add "Update image inputs" button for dynamic IMAGE input slots
|
||
this.addWidget("button", "Update image inputs", null, () => {
|
||
if (!this.inputs) {
|
||
this.inputs = [];
|
||
}
|
||
const target = this.widgets.find(w => w.name === "image_input_node_count")?.value || 0;
|
||
const current = this.inputs.filter(input => input.type === "IMAGE").length;
|
||
if (target === current) return;
|
||
|
||
if (target < current) {
|
||
// Remove excess IMAGE inputs from the end
|
||
for (let i = 0; i < current - target; i++) {
|
||
// Find the last IMAGE input and remove it
|
||
for (let j = this.inputs.length - 1; j >= 0; j--) {
|
||
if (this.inputs[j].type === "IMAGE") {
|
||
this.removeInput(j);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// Add new IMAGE inputs
|
||
for (let i = current + 1; i <= target; i++) {
|
||
this.addInput(`image_input_${i}`, "IMAGE", { shape: 7 });
|
||
}
|
||
}
|
||
});
|
||
|
||
return result;
|
||
};
|
||
|
||
// Resize to fit images restored from a saved workflow
|
||
const onConfigure = nodeType.prototype.onConfigure;
|
||
nodeType.prototype.onConfigure = function () {
|
||
const result = onConfigure?.apply(this, arguments);
|
||
getImageListWidget(this)?.updateNodeSize();
|
||
return result;
|
||
};
|
||
|
||
// Handle drag and drop
|
||
const onDragOver = nodeType.prototype.onDragOver;
|
||
nodeType.prototype.onDragOver = function (e) {
|
||
const result = onDragOver?.apply(this, arguments);
|
||
|
||
if (e.dataTransfer?.types?.includes("Files")) {
|
||
e.preventDefault();
|
||
|
||
// Update drop zone highlight
|
||
const widget = getImageListWidget(this);
|
||
if (widget) {
|
||
if (!widget.dropZoneActive) {
|
||
widget.dropZoneActive = true;
|
||
this.setDirtyCanvas(true, true);
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
return result;
|
||
};
|
||
|
||
const onDragLeave = nodeType.prototype.onDragLeave;
|
||
nodeType.prototype.onDragLeave = function (e) {
|
||
const result = onDragLeave?.apply(this, arguments);
|
||
|
||
// Clear drop zone highlight
|
||
const widget = getImageListWidget(this);
|
||
if (widget) {
|
||
if (widget.dropZoneActive) {
|
||
widget.dropZoneActive = false;
|
||
this.setDirtyCanvas(true, true);
|
||
}
|
||
}
|
||
|
||
return result;
|
||
};
|
||
|
||
const onDragDrop = nodeType.prototype.onDragDrop;
|
||
nodeType.prototype.onDragDrop = async function (e) {
|
||
// Clear drop zone highlight
|
||
const widget = getImageListWidget(this);
|
||
if (widget) {
|
||
widget.dropZoneActive = false;
|
||
this.setDirtyCanvas(true, true);
|
||
}
|
||
|
||
if (e.dataTransfer?.files?.length > 0) {
|
||
if (widget) {
|
||
for (const file of e.dataTransfer.files) {
|
||
if (file.type.startsWith("image/")) {
|
||
await widget.uploadImage(file);
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return onDragDrop?.apply(this, arguments);
|
||
};
|
||
|
||
// Handle paste
|
||
const onPaste = nodeType.prototype.onPaste;
|
||
nodeType.prototype.onPaste = async function (e) {
|
||
const widget = getImageListWidget(this);
|
||
if (widget) {
|
||
const items = e.clipboardData?.items;
|
||
if (items) {
|
||
for (const item of items) {
|
||
if (item.type.startsWith("image/")) {
|
||
const file = item.getAsFile();
|
||
if (file) {
|
||
await widget.uploadImage(file);
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return onPaste?.apply(this, arguments);
|
||
};
|
||
},
|
||
});
|