suneditor
Version:
Vanilla JavaScript based WYSIWYG web editor
960 lines (819 loc) • 33.6 kB
JavaScript
import { PluginModal } from '../../../interfaces';
import { Modal, Figure } from '../../../modules/contract';
import { FileManager } from '../../../modules/manager';
import { ModalAnchorEditor } from '../../../modules/ui';
import { dom, numbers, env } from '../../../helper';
import { DEFAULT_ACCEPTED_FORMATS, DEFAULT_SVG_SIZE, FORMAT_TYPE, SIZE_UNIT } from './shared/image.constants';
import { CreateHTML_modal } from './render/image.html';
import ImageSizeService from './services/image.size';
import ImageUploadService from './services/image.upload';
const { NO_EVENT } = env;
/**
* @typedef {Object} ImagePluginOptions
* @property {boolean} [canResize=true] - Whether the image element can be resized.
* @property {boolean} [showHeightInput=true] - Whether to display the height input field.
* @property {string} [defaultWidth="auto"] - The default width of the image. If a number is provided, `"px"` will be appended.
* @property {string} [defaultHeight="auto"] - The default height of the image. If a number is provided, `"px"` will be appended.
* @property {boolean} [percentageOnlySize=false] - Whether to allow only percentage-based sizing.
* @property {boolean} [createFileInput=true] - Whether to create a file input element for image uploads.
* @property {boolean} [createUrlInput] - Whether to create a URL input element for image insertion.
* - Defaults to `true`. Always `true` when `createFileInput` is `false`.
* @property {string} [uploadUrl] - The URL endpoint for image file uploads.
* - The server must return:
* ```js
* {
* "result": [
* {
* "url": "https://example.com/image.jpg",
* "name": "image.jpg",
* "size": 123456
* }
* ]
* }
* ```
* @property {Object<string, string>} [uploadHeaders] - Additional headers to include in the file upload request.
* ```js
* { uploadUrl: '/api/upload/image', uploadHeaders: { Authorization: 'Bearer token' } }
* ```
* @property {number} [uploadSizeLimit] - The total upload size limit in bytes.
* @property {number} [uploadSingleSizeLimit] - The single file upload size limit in bytes.
* @property {boolean} [allowMultiple=false] - Whether multiple image uploads are allowed.
* @property {string} [acceptedFormats="image/*"] - The accepted file formats for image uploads.
* @property {boolean} [useFormatType=true] - Whether to enable format type selection (`block` or `inline`).
* @property {'block'|'inline'} [defaultFormatType="block"] - The default image format type (`"block"` or `"inline"`).
* @property {boolean} [keepFormatType=false] - Whether to retain the chosen format type after image insertion.
* @property {boolean} [linkEnableFileUpload] - Whether to enable file uploads for linked images.
* @property {SunEditor.Module.Figure.Controls} [controls] - Figure controls.
* @property {SunEditor.ComponentInsertType} [insertBehavior] - Component insertion behavior for selection and cursor placement.
* - [default: `options.get('componentInsertBehavior')`]
* - For inline components: places cursor near the component, or selects if no nearby range.
* - For block components: executes behavior based on `selectMode`:
* - `auto`: Move cursor to the next line if possible, otherwise select the component.
* - `select`: Always select the inserted component.
* - `line`: Move cursor to the next line if possible, or create a new line and move there.
* - `none`: Do nothing.
*/
/**
* @typedef {Object} ImageState
* @property {string} sizeUnit - Size unit (`'px'` or `'%'`)
* @property {boolean} onlyPercentage - Whether only percentage sizing is allowed
* @property {number} produceIndex - Image production index for batch operations
*/
/**
* @class
* @description Image plugin.
* - This plugin provides image insertion functionality within the editor, supporting both file upload and URL input.
*/
class Image_ extends PluginModal {
static key = 'image';
static className = '';
/**
* @param {Element} node - The node to check.
* @returns {Element|null} Returns a node if the node is a valid component.
*/
static component(node) {
const compNode = dom.check.isFigure(node) || (/^span$/i.test(node.nodeName) && dom.check.isComponentContainer(node)) ? node.firstElementChild : node;
return /^IMG$/i.test(compNode?.nodeName) ? compNode : dom.check.isAnchor(compNode) && /^IMG$/i.test(compNode?.firstElementChild?.nodeName) ? compNode?.firstElementChild : null;
}
#resizing;
#nonResizing;
#linkElement = null;
#linkValue = '';
#align = 'none';
#svgDefaultSize = DEFAULT_SVG_SIZE;
#element = null;
#cover = null;
#container = null;
#caption = null;
/**
* @constructor
* @param {SunEditor.Kernel} kernel - The Kernel instance
* @param {ImagePluginOptions} pluginOptions
*/
constructor(kernel, pluginOptions) {
// plugin basic properties
super(kernel);
this.title = this.$.lang.image;
this.icon = 'image';
this.pluginOptions = {
canResize: pluginOptions.canResize === undefined ? true : pluginOptions.canResize,
showHeightInput: pluginOptions.showHeightInput === undefined ? true : !!pluginOptions.showHeightInput,
defaultWidth: !pluginOptions.defaultWidth ? 'auto' : numbers.is(pluginOptions.defaultWidth) ? pluginOptions.defaultWidth + SIZE_UNIT.PIXEL : pluginOptions.defaultWidth,
defaultHeight: !pluginOptions.defaultHeight ? 'auto' : numbers.is(pluginOptions.defaultHeight) ? pluginOptions.defaultHeight + SIZE_UNIT.PIXEL : pluginOptions.defaultHeight,
percentageOnlySize: !!pluginOptions.percentageOnlySize,
createFileInput: pluginOptions.createFileInput === undefined ? true : pluginOptions.createFileInput,
createUrlInput: pluginOptions.createUrlInput === undefined || !pluginOptions.createFileInput ? true : pluginOptions.createUrlInput,
uploadUrl: typeof pluginOptions.uploadUrl === 'string' ? pluginOptions.uploadUrl : null,
uploadHeaders: pluginOptions.uploadHeaders || null,
uploadSizeLimit: numbers.get(pluginOptions.uploadSizeLimit, 0),
uploadSingleSizeLimit: numbers.get(pluginOptions.uploadSingleSizeLimit, 0),
allowMultiple: !!pluginOptions.allowMultiple,
acceptedFormats: typeof pluginOptions.acceptedFormats !== 'string' || pluginOptions.acceptedFormats.trim() === '*' ? DEFAULT_ACCEPTED_FORMATS : pluginOptions.acceptedFormats.trim() || DEFAULT_ACCEPTED_FORMATS,
useFormatType: pluginOptions.useFormatType ?? true,
defaultFormatType: [FORMAT_TYPE.BLOCK, FORMAT_TYPE.INLINE].includes(pluginOptions.defaultFormatType) ? pluginOptions.defaultFormatType : FORMAT_TYPE.BLOCK,
keepFormatType: pluginOptions.keepFormatType ?? false,
insertBehavior: pluginOptions.insertBehavior,
};
// create HTML
const sizeUnit = this.pluginOptions.percentageOnlySize ? SIZE_UNIT.PERCENTAGE : SIZE_UNIT.PIXEL;
const modalEl = CreateHTML_modal(this.$, this.pluginOptions);
const ctrlAs = this.pluginOptions.useFormatType ? 'as' : '';
const figureControls =
pluginOptions.controls ||
(!this.pluginOptions.canResize
? [[ctrlAs, 'mirror_h', 'mirror_v', 'align', 'caption', 'edit', 'revert', 'copy', 'remove']]
: [
[ctrlAs, 'resize_auto,100,75,50', 'rotate_l', 'rotate_r', 'mirror_h', 'mirror_v'],
['edit', 'align', 'caption', 'revert', 'copy', 'remove'],
]);
// show align
this.alignForm = modalEl.alignForm;
if (!figureControls.some((subArray) => subArray.includes('align'))) this.alignForm.style.display = 'none';
// modules
const Link = this.$.plugins.link ? this.$.plugins.link.pluginOptions : {};
this.anchor = new ModalAnchorEditor(this.$, modalEl.html, {
...Link,
textToDisplay: false,
title: true,
});
this.modal = new Modal(this, this.$, modalEl.html);
this.figure = new Figure(this, this.$, figureControls, {
sizeUnit: sizeUnit,
});
this.fileManager = new FileManager(this, this.$, {
query: 'img',
loadEventName: 'onImageLoad',
actionEventName: 'onImageAction',
});
// members
/** @type {ImageState} */
this.state = {
sizeUnit: sizeUnit,
onlyPercentage: this.pluginOptions.percentageOnlySize,
produceIndex: 0,
};
this.fileModalWrapper = modalEl.fileModalWrapper;
this.imgInputFile = modalEl.imgInputFile;
this.imgUrlFile = modalEl.imgUrlFile;
this.focusElement = this.imgInputFile || this.imgUrlFile;
this.altText = modalEl.altText;
this.captionCheckEl = modalEl.captionCheckEl;
this.captionEl = this.captionCheckEl?.parentElement;
this.previewSrc = modalEl.previewSrc;
this.as = FORMAT_TYPE.BLOCK;
this.#resizing = this.pluginOptions.canResize;
this.#nonResizing = !this.#resizing || !this.pluginOptions.showHeightInput || this.pluginOptions.percentageOnlySize;
this.sizeService = new ImageSizeService(this, modalEl);
this.uploadService = new ImageUploadService(this);
// init
this.$.eventManager.addEvent(modalEl.tabs, 'click', this.#OpenTab.bind(this));
if (this.imgInputFile) this.$.eventManager.addEvent(modalEl.fileRemoveBtn, 'click', this.#RemoveSelectedFiles.bind(this));
if (this.imgUrlFile) this.$.eventManager.addEvent(this.imgUrlFile, 'input', this.#OnLinkPreview.bind(this));
if (this.imgInputFile && this.imgUrlFile) this.$.eventManager.addEvent(this.imgInputFile, 'change', this.#OnfileInputChange.bind(this));
const galleryButton = modalEl.galleryButton;
if (galleryButton) this.$.eventManager.addEvent(galleryButton, 'click', this.#OpenGallery.bind(this));
if (this.pluginOptions.useFormatType) {
this.as = this.pluginOptions.defaultFormatType;
this.asBlock = modalEl.asBlock;
this.asInline = modalEl.asInline;
this.$.eventManager.addEvent([this.asBlock, this.asInline], 'click', this.#OnClickAsButton.bind(this));
}
}
/**
* @template {keyof ImageState} K
* @param {K} key
* @param {ImageState[K]} value
*/
setState(key, value) {
this.state[key] = value;
}
/**
* @override
* @type {PluginModal['open']}
*/
open() {
this.state.produceIndex = 0;
this.modal.open();
}
/**
* @hook Editor.Core
* @type {SunEditor.Hook.Core.RetainFormat}
*/
retainFormat() {
return {
query: 'img',
/** @param {HTMLImageElement} element */
method: (element) => {
const figureInfo = Figure.GetContainer(element);
if (figureInfo && figureInfo.container && (figureInfo.cover || figureInfo.inlineCover)) return;
const { w, h } = this.#ready(element, true);
this.#fileCheck(w, h);
},
};
}
/**
* @hook Editor.EventManager
* @type {SunEditor.Hook.Event.OnFilePasteAndDrop}
*/
onFilePasteAndDrop({ file }) {
if (!/^image/.test(file.type)) return;
this.submitFile([file]);
this.$.focusManager.focus();
}
/**
* @hook Modules.Modal
* @type {SunEditor.Hook.Modal.On}
*/
modalOn(isUpdate) {
if (!isUpdate) {
this.sizeService.on();
if (this.imgInputFile && this.pluginOptions.allowMultiple) this.imgInputFile.setAttribute('multiple', 'multiple');
} else {
if (this.imgInputFile && this.pluginOptions.allowMultiple) this.imgInputFile.removeAttribute('multiple');
}
this.anchor.on(isUpdate);
}
/**
* @hook Modules.Modal
* @type {SunEditor.Hook.Modal.Action}
*/
async modalAction() {
this.#align = /** @type {HTMLInputElement} */ (this.modal.form.querySelector('input[name="suneditor_image_radio"]:checked')).value;
if (this.modal.isUpdate) {
this.#fixTagStructure();
this.$.history.push(false);
}
if (this.imgInputFile && this.imgInputFile.files.length > 0) {
return await this.submitFile(this.imgInputFile.files);
} else if (this.imgUrlFile && this.#linkValue.length > 0) {
return await this.submitURL(this.#linkValue);
}
return false;
}
/**
* @hook Modules.Modal
* @type {SunEditor.Hook.Modal.Init}
*/
modalInit() {
Modal.OnChangeFile(this.fileModalWrapper, []);
if (this.imgInputFile) this.imgInputFile.value = '';
if (this.imgUrlFile) this.#linkValue = this.previewSrc.textContent = this.imgUrlFile.value = '';
if (this.imgInputFile && this.imgUrlFile) {
this.imgUrlFile.disabled = false;
this.previewSrc.style.textDecoration = '';
}
this.altText.value = '';
/** @type {HTMLInputElement} */ (this.modal.form.querySelector('input[name="suneditor_image_radio"][value="none"]')).checked = true;
this.captionCheckEl.checked = false;
this.#element = null;
this.#OpenTab('init');
this.sizeService.init();
if (this.pluginOptions.useFormatType) {
this.#activeAsInline((this.pluginOptions.keepFormatType ? this.as : this.pluginOptions.defaultFormatType) === FORMAT_TYPE.INLINE);
}
this.anchor.init();
}
/**
* @hook Editor.Component
* @type {SunEditor.Hook.Component.Select}
*/
componentSelect(target) {
this.#ready(target);
}
/**
* @hook Editor.Component
* @type {SunEditor.Hook.Component.Edit}
*/
componentEdit() {
this.modal.open();
}
/**
* @hook Editor.Component
* @type {SunEditor.Hook.Component.Destroy}
*/
async componentDestroy(target) {
const targetEl = target || this.#element;
const container = dom.query.getParentElement(targetEl, Figure.is) || targetEl;
const focusEl = container.previousElementSibling || container.nextElementSibling;
const emptyDiv = container.parentNode;
const message = await this.$.eventManager.triggerEvent('onImageDeleteBefore', { element: targetEl, container, align: this.#align, alt: this.altText.value, url: this.#linkValue });
if (message === false) return;
dom.utils.removeItem(container);
this.modalInit();
if (emptyDiv !== this.$.frameContext.get('wysiwyg')) {
this.$.nodeTransform.removeAllParents(
emptyDiv,
function (current) {
return current.childNodes.length === 0;
},
null,
);
}
// focus
this.$.focusManager.focusEdge(focusEl);
this.$.history.push(false);
}
/**
* @description Create an `image` component using the provided files.
* @param {FileList|File[]} fileList File object list
* @returns {Promise<boolean>} If return `false`, the file upload will be canceled
*/
async submitFile(fileList) {
if (fileList.length === 0) return false;
let fileSize = 0;
const files = [];
const singleSizeLimit = this.pluginOptions.uploadSingleSizeLimit;
for (let i = 0, len = fileList.length, f, s; i < len; i++) {
f = fileList[i];
if (!/image/i.test(f.type)) continue;
s = f.size;
if (singleSizeLimit > 0 && s > singleSizeLimit) {
const err = '[SUNEDITOR.imageUpload.fail] Size of uploadable single file: ' + singleSizeLimit / 1000 + 'KB';
const message = await this.$.eventManager.triggerEvent('onImageUploadError', {
error: err,
limitSize: singleSizeLimit,
uploadSize: s,
file: f,
});
this.$.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
return false;
}
files.push(f);
fileSize += s;
}
const limitSize = this.pluginOptions.uploadSizeLimit;
const currentSize = this.fileManager.getSize();
if (limitSize > 0 && fileSize + currentSize > limitSize) {
const err = '[SUNEDITOR.imageUpload.fail] Size of uploadable total images: ' + limitSize / 1000 + 'KB';
const message = await this.$.eventManager.triggerEvent('onImageUploadError', {
error: err,
limitSize,
currentSize,
uploadSize: fileSize,
});
this.$.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
return false;
}
const imgInfo = { files, ...this.#getInfo() };
const handler = function (uploadCallback, infos, newInfos) {
infos = newInfos || infos;
uploadCallback(infos);
}.bind(this, this.uploadService.serverUpload.bind(this.uploadService), imgInfo);
const result = await this.$.eventManager.triggerEvent('onImageUploadBefore', {
info: imgInfo,
handler,
});
if (result === undefined) return true;
if (result === false) return false;
if (result !== null && typeof result === 'object') handler(result);
if (result === true || result === NO_EVENT) handler(null);
}
/**
* @description Create an `image` component using the provided url.
* @param {string} url File url
* @returns {Promise<boolean>} If return `false`, the file upload will be canceled
*/
async submitURL(url) {
if (!(url ||= this.#linkValue)) return false;
const file = { name: url.split('/').pop(), size: 0 };
const imgInfo = {
url,
files: file,
...this.#getInfo(),
};
const handler = function (uploadCallback, infos, newInfos) {
infos = newInfos || infos;
uploadCallback(infos);
}.bind(this, this.uploadService.urlUpload.bind(this.uploadService), imgInfo);
const result = await this.$.eventManager.triggerEvent('onImageUploadBefore', {
info: imgInfo,
handler,
});
if (result === undefined) return true;
if (result === false) return false;
if (result !== null && typeof result === 'object') handler(result);
if (result === true || result === NO_EVENT) handler(null);
return true;
}
/**
* @description Creates a new image component, wraps it in a figure container with an optional anchor,
* - applies size and alignment settings, and inserts it into the editor.
* @param {string} src - The URL of the image to be inserted.
* @param {?Node} anchor - An optional anchor element to wrap the image. If provided, a clone is used.
* @param {string} width - The width value to be applied to the image.
* @param {string} height - The height value to be applied to the image.
* @param {string} align - The alignment setting for the image (e.g., 'left', 'center', 'right').
* @param {{name: string, size: number}} file - File metadata associated with the image
* @param {string} alt - The alternative text for the image.
* @param {boolean} isLast - Indicates whether this is the last file in the batch (used for scroll and insert actions).
*/
create(src, anchor, width, height, align, file, alt, isLast) {
/** @type {HTMLImageElement} */
const oImg = dom.utils.createElement('IMG');
oImg.src = src;
oImg.alt = alt;
anchor = this.#setAnchor(oImg, anchor ? anchor.cloneNode(false) : null);
const figureInfo = Figure.CreateContainer(anchor, 'se-image-container');
const cover = figureInfo.cover;
const container = figureInfo.container;
// caption
if (this.captionCheckEl.checked) {
this.#caption = Figure.CreateCaption(cover, this.$.lang.caption);
}
this.#element = oImg;
this.#cover = cover;
this.#container = container;
this.figure.open(oImg, { nonResizing: this.#nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, infoOnly: true });
// set size
this.sizeService.applySize(width, height);
// align
this.figure.setAlign(oImg, align);
this.fileManager.setFileData(oImg, file);
this.setState('produceIndex', this.state.produceIndex + 1);
oImg.onload = this.#OnloadImg.bind(this, oImg, this.#svgDefaultSize, container);
this.$.component.insert(container, { scrollTo: isLast ? true : false, insertBehavior: isLast ? null : 'line' });
}
/**
* @description Creates a new inline image component, wraps it in an inline figure container with an optional anchor,
* - applies size settings, and inserts it into the editor.
* @param {string} src - The URL of the image to be inserted.
* @param {?Node} anchor - An optional anchor element to wrap the image. If provided, a clone is used.
* @param {string} width - The width value to be applied to the image.
* @param {string} height - The height value to be applied to the image.
* @param {{name: string, size: number}} file - File metadata associated with the image
* @param {string} alt - The alternative text for the image.
* @param {boolean} isLast - Indicates whether this is the last file in the batch (used for scroll and insert actions).
*/
createInline(src, anchor, width, height, file, alt, isLast) {
/** @type {HTMLImageElement} */
const oImg = dom.utils.createElement('IMG');
oImg.src = src;
oImg.alt = alt;
anchor = this.#setAnchor(oImg, anchor ? anchor.cloneNode(false) : null);
const figureInfo = Figure.CreateInlineContainer(anchor, 'se-image-container');
const container = figureInfo.container;
this.#element = oImg;
this.#container = container;
this.figure.open(oImg, { nonResizing: this.#nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, infoOnly: true });
// set size
this.sizeService.applySize(width, height);
this.fileManager.setFileData(oImg, file);
this.setState('produceIndex', this.state.produceIndex + 1);
oImg.onload = this.#OnloadImg.bind(this, oImg, this.#svgDefaultSize, container);
this.$.component.insert(container, { scrollTo: isLast ? true : false, insertBehavior: isLast ? null : 'line' });
}
/**
* @description Prepares the component for selection.
* - Ensures that the controller is properly positioned and initialized.
* - Prevents duplicate event handling if the component is already selected.
* @param {HTMLElement} target - The selected element.
* @param {boolean} [infoOnly=false] - If `true`, only retrieves information without opening the controller.
* @returns {{w: string, h: string}} - The width and height of the component.
*/
#ready(target, infoOnly = false) {
if (!target) return;
const figureInfo = this.figure.open(target, { nonResizing: this.#nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, infoOnly });
this.anchor.set(dom.check.isAnchor(target.parentNode) ? target.parentNode : null);
this.#linkElement = this.anchor.currentTarget;
this.#element = target;
this.#cover = figureInfo.cover;
this.#container = figureInfo.container;
this.#caption = figureInfo.caption;
this.#align = figureInfo.align;
target.style.float = '';
this.sizeService.setOriginSize(String(figureInfo.originWidth || figureInfo.w || ''), String(figureInfo.originHeight || figureInfo.h || ''));
this.altText.value = this.#element.alt;
if (this.imgUrlFile) this.#linkValue = this.previewSrc.textContent = this.imgUrlFile.value = this.#element.src;
/** @type {HTMLInputElement} */
const activeAlign = this.modal.form.querySelector('input[name="suneditor_image_radio"][value="' + this.#align + '"]') || this.modal.form.querySelector('input[name="suneditor_image_radio"][value="none"]');
activeAlign.checked = true;
this.captionCheckEl.checked = !!this.#caption;
const { dw, dh } = this.figure.getSize(target);
if (!this.#resizing) return { w: dw, h: dh };
this.sizeService.ready(figureInfo, dw, dh);
if (this.pluginOptions.useFormatType) {
this.#activeAsInline(this.$.component.isInline(figureInfo.container));
}
return { w: dw, h: dh };
}
/**
* @description Retrieves the current image information.
* @returns {*} - The image data.
*/
#getInfo() {
const { w, h } = this.sizeService.getInputSize();
return {
element: this.#element,
anchor: this.anchor.create(true),
inputWidth: w,
inputHeight: h,
align: this.#align,
isUpdate: this.modal.isUpdate,
alt: this.altText.value,
};
}
/**
* @description Toggles between `block` and `inline` image format.
* @param {boolean} isInline - Whether the image should be `inline`.
*/
#activeAsInline(isInline) {
if (isInline) {
dom.utils.addClass(this.asInline, 'on');
dom.utils.removeClass(this.asBlock, 'on');
this.as = FORMAT_TYPE.INLINE;
// buttns
if (this.alignForm) this.alignForm.style.display = 'none';
// caption
if (this.captionEl) this.captionEl.style.display = 'none';
} else {
dom.utils.addClass(this.asBlock, 'on');
dom.utils.removeClass(this.asInline, 'on');
this.as = FORMAT_TYPE.BLOCK;
// buttns
if (this.alignForm) this.alignForm.style.display = '';
// caption
if (this.captionEl) this.captionEl.style.display = '';
}
}
/**
* @description Updates the selected image size, alt text, and caption.
*/
#fixTagStructure() {
const { w, h } = this.sizeService.getInputSize();
const width = w || 'auto';
const height = h || 'auto';
let imageEl = this.#element;
// as (block | inline)
if ((this.as === FORMAT_TYPE.BLOCK && !this.#cover) || (this.as === FORMAT_TYPE.INLINE && this.#cover)) {
imageEl = this.figure.convertAsFormat(imageEl, this.as);
}
// --- update image ---
const cover = this.#cover;
const container = this.#container === this.#cover ? null : this.#container;
// check size
let changeSize;
const x = numbers.is(width) ? width + this.state.sizeUnit : width;
const y = numbers.is(height) ? height + this.state.sizeUnit : height;
if (/%$/.test(imageEl.style.width)) {
changeSize = x !== container.style.width || y !== container.style.height;
} else {
changeSize = x !== imageEl.style.width || y !== imageEl.style.height;
}
// alt
imageEl.alt = this.altText.value;
// caption
let modifiedCaption = false;
if (this.captionCheckEl.checked) {
if (!this.#caption) {
this.#caption = Figure.CreateCaption(cover, this.$.lang.caption);
modifiedCaption = true;
}
} else {
if (this.#caption) {
dom.utils.removeItem(this.#caption);
this.#caption = null;
modifiedCaption = true;
}
}
// link
let isNewAnchor = false;
const anchor = this.anchor.create(true);
if (anchor) {
if (this.#linkElement !== anchor || !container.contains(anchor)) {
this.#linkElement = anchor.cloneNode(false);
cover.insertBefore(this.#setAnchor(imageEl, this.#linkElement), this.#caption);
isNewAnchor = true;
}
} else if (this.#linkElement !== null) {
if (cover.contains(this.#linkElement)) {
const newEl = imageEl.cloneNode(true);
cover.removeChild(this.#linkElement);
cover.insertBefore(newEl, this.#caption);
imageEl = newEl;
}
}
if (isNewAnchor) {
dom.utils.removeItem(anchor);
}
// size
if (this.#resizing && changeSize) {
this.sizeService.applySize(width, height);
}
// transform
if (modifiedCaption || (!this.state.onlyPercentage && changeSize)) {
if (/\d+/.test(imageEl.style.height) || (this.figure.isVertical && this.captionCheckEl.checked)) {
if (/auto|%$/.test(width) || /auto|%$/.test(height)) {
this.figure.deleteTransform(imageEl);
} else if (!this.#resizing || !changeSize || !this.figure.isVertical) {
this.figure.setTransform(imageEl, width, height, 0);
}
}
}
// align
this.figure.setAlign(imageEl, this.#align);
// select
imageEl.onload = () => {
this.componentSelect(imageEl);
};
}
/**
* @description Validates the image size and applies necessary transformations.
* @param {string} width - The width of the image.
* @param {string} height - The height of the image.
*/
#fileCheck(width, height) {
const { w, h } = this.sizeService.getInputSize();
width ||= w || 'auto';
height ||= h || 'auto';
let imageEl = this.#element;
let cover = this.#cover;
let inlineCover = null;
let container = this.#container === this.#cover ? null : this.#container;
let isNewContainer = false;
if (!cover || !container) {
isNewContainer = true;
imageEl = this.#element.cloneNode(true);
const figureInfo =
this.pluginOptions.useFormatType && width !== 'auto' && (/^span$/i.test(this.#element.parentElement?.nodeName) || this.$.format.isLine(this.#element.parentElement))
? Figure.CreateInlineContainer(imageEl, 'se-image-container')
: Figure.CreateContainer(imageEl, 'se-image-container');
cover = figureInfo.cover;
container = figureInfo.container;
inlineCover = figureInfo.inlineCover;
this.figure.open(imageEl, { nonResizing: true, nonSizeInfo: false, nonBorder: false, figureTarget: false, infoOnly: true });
}
// alt
imageEl.alt = this.altText.value;
// caption
let modifiedCaption = false;
if (!inlineCover) {
if (this.captionCheckEl.checked) {
if (!this.#caption || isNewContainer) {
this.#caption = Figure.CreateCaption(cover, this.$.lang.caption);
modifiedCaption = true;
}
} else {
if (this.#caption) {
dom.utils.removeItem(this.#caption);
this.#caption = null;
modifiedCaption = true;
}
}
}
// link
let isNewAnchor = null;
const anchor = this.anchor.create(true);
if (anchor) {
if (this.#linkElement !== anchor || (isNewContainer && !container.contains(anchor))) {
this.#linkElement = anchor.cloneNode(false);
cover.insertBefore(this.#setAnchor(imageEl, this.#linkElement), this.#caption);
isNewAnchor = this.#element;
}
} else if (this.#linkElement !== null) {
if (cover.contains(this.#linkElement)) {
const newEl = imageEl.cloneNode(true);
cover.removeChild(this.#linkElement);
cover.insertBefore(newEl, this.#caption);
imageEl = newEl;
}
}
if (isNewContainer) {
imageEl = this.#element;
this.figure.retainFigureFormat(container, this.#element, isNewAnchor ? anchor : null, this.fileManager);
this.#element = imageEl = container.querySelector('img');
this.#cover = cover;
this.#container = container;
}
// size
imageEl.style.width = '';
imageEl.style.height = '';
imageEl.removeAttribute('width');
imageEl.removeAttribute('height');
this.sizeService.applySize(width, height);
if (isNewAnchor) {
if (!isNewContainer) {
dom.utils.removeItem(anchor);
} else {
dom.utils.removeItem(isNewAnchor);
if (dom.query.getListChildren(anchor, (current) => /IMG/i.test(current.tagName), null).length === 0) {
dom.utils.removeItem(anchor);
}
}
}
// transform
if (modifiedCaption || !this.state.onlyPercentage) {
if (/\d+/.test(imageEl.style.height) || (this.figure.isVertical && this.captionCheckEl.checked)) {
if (/auto|%$/.test(width) || /auto|%$/.test(height)) {
this.figure.deleteTransform(imageEl);
} else {
this.figure.setTransform(imageEl, width, height, 0);
}
}
}
// align
this.figure.setAlign(imageEl, this.#align);
}
/**
* @description Wraps an image element with an anchor if provided.
* @param {Node} imgTag - The image element to be wrapped.
* @param {?Node} anchor - The anchor element to wrap around the image. If `null`, returns the image itself.
* @returns {Node} - The wrapped image inside the anchor or the original image element.
*/
#setAnchor(imgTag, anchor) {
if (anchor) {
anchor.appendChild(imgTag);
return anchor;
}
return imgTag;
}
/**
* @description Opens a specific tab inside the modal.
* @param {MouseEvent|string} e - The event object or tab name.
* @returns {boolean} - Whether the tab was successfully opened.
*/
#OpenTab(e) {
const modalForm = this.modal.form;
const targetElement = typeof e === 'string' ? modalForm.querySelector('._se_tab_link') : dom.query.getEventTarget(e);
if (!/^BUTTON$/i.test(targetElement.tagName)) {
return false;
}
// Declare all variables
const tabName = targetElement.getAttribute('data-tab-link');
let i;
// Get all elements with class="tabcontent" and hide them
const tabContent = /** @type {HTMLCollectionOf<HTMLElement>}*/ (modalForm.getElementsByClassName('_se_tab_content'));
for (i = 0; i < tabContent.length; i++) {
tabContent[i].style.display = 'none';
}
// Get all elements with class="tablinks" and remove the class "active"
const tabLinks = modalForm.getElementsByClassName('_se_tab_link');
for (i = 0; i < tabLinks.length; i++) {
dom.utils.removeClass(tabLinks[i], 'active');
}
// Show the current tab, and add an "active" class to the button that opened the tab
/** @type {HTMLElement}*/ (modalForm.querySelector('._se_tab_content_' + tabName)).style.display = 'block';
dom.utils.addClass(targetElement, 'active');
// focus
if (e !== 'init') {
if (tabName === 'image') {
this.focusElement.focus();
} else if (tabName === 'url') {
this.anchor.urlInput.focus();
}
}
return false;
}
#RemoveSelectedFiles() {
this.imgInputFile.value = '';
if (this.imgUrlFile) {
this.imgUrlFile.disabled = false;
this.previewSrc.style.textDecoration = '';
}
// inputFile check
Modal.OnChangeFile(this.fileModalWrapper, []);
}
#OnClickAsButton({ target }) {
this.#activeAsInline(target.getAttribute('data-command') === 'asInline');
}
#OnLinkPreview(e) {
const value = e.target.value.trim();
this.#linkValue = this.previewSrc.textContent = !value
? ''
: this.$.options.get('defaultUrlProtocol') && !value.includes('://') && value.indexOf('#') !== 0
? this.$.options.get('defaultUrlProtocol') + value
: !value.includes('://')
? '/' + value
: value;
}
#OnfileInputChange({ target }) {
if (!this.imgInputFile.value) {
this.imgUrlFile.disabled = false;
this.previewSrc.style.textDecoration = '';
} else {
this.imgUrlFile.disabled = true;
this.previewSrc.style.textDecoration = 'line-through';
}
// inputFile check
Modal.OnChangeFile(this.fileModalWrapper, target.files);
}
#OpenGallery() {
this.$.plugins.imageGallery.open(this.#SetUrlInput.bind(this));
}
#SetUrlInput(target) {
this.altText.value = target.getAttribute('data-value') || target.alt;
this.#linkValue = this.previewSrc.textContent = this.imgUrlFile.value = target.getAttribute('data-command') || target.src;
this.imgUrlFile.focus();
}
#OnloadImg(oImg, _svgDefaultSize, container) {
this.setState('produceIndex', this.state.produceIndex - 1);
delete oImg.onload;
// svg exception handling
if (oImg.offsetWidth === 0) this.sizeService.applySize(_svgDefaultSize, '');
if (this.state.produceIndex === 0) {
this.$.component.applyInsertBehavior(container, null, this.pluginOptions.insertBehavior || this.$.options.get('componentInsertBehavior'));
this.$.ui._iframeAutoHeight(this.$.frameContext);
this.$.history.push(false);
}
}
}
export default Image_;