devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
544 lines (536 loc) • 20.1 kB
JavaScript
/**
* DevExtreme (cjs/__internal/ui/html_editor/utils/m_image_uploader_helper.js)
* Version: 24.2.6
* Build date: Mon Mar 17 2025
*
* Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED
* Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
*/
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ImageUploader = void 0;
exports.correctSlashesInUrl = correctSlashesInUrl;
exports.getFileUploaderBaseOptions = getFileUploaderBaseOptions;
exports.serverUpload = serverUpload;
exports.urlUpload = urlUpload;
var _message = _interopRequireDefault(require("../../../../common/core/localization/message"));
var _devices = _interopRequireDefault(require("../../../../core/devices"));
var _renderer = _interopRequireDefault(require("../../../../core/renderer"));
var _extend = require("../../../../core/utils/extend");
var _iterator = require("../../../../core/utils/iterator");
var _size = require("../../../../core/utils/size");
var _type = require("../../../../core/utils/type");
var _button_group = _interopRequireDefault(require("../../../../ui/button_group"));
var _file_uploader = _interopRequireDefault(require("../../../../ui/file_uploader"));
var _text_box = _interopRequireDefault(require("../../../../ui/text_box"));
var _themes = require("../../../../ui/themes");
const _excluded = ["imageSrc", "src"];
function _interopRequireDefault(e) {
return e && e.__esModule ? e : {
default: e
}
}
function _extends() {
return _extends = Object.assign ? Object.assign.bind() : function(n) {
for (var e = 1; e < arguments.length; e++) {
var t = arguments[e];
for (var r in t) {
({}).hasOwnProperty.call(t, r) && (n[r] = t[r])
}
}
return n
}, _extends.apply(null, arguments)
}
function _objectWithoutPropertiesLoose(r, e) {
if (null == r) {
return {}
}
var t = {};
for (var n in r) {
if ({}.hasOwnProperty.call(r, n)) {
if (e.includes(n)) {
continue
}
t[n] = r[n]
}
}
return t
}
const isMobile = "phone" === _devices.default.current().deviceType;
const DIALOG_IMAGE_CAPTION = "dxHtmlEditor-dialogImageCaption";
const DIALOG_UPDATE_IMAGE_CAPTION = "dxHtmlEditor-dialogUpdateImageCaption";
const DIALOG_IMAGE_FIELD_URL = "dxHtmlEditor-dialogImageUrlField";
const DIALOG_IMAGE_FIELD_ALT = "dxHtmlEditor-dialogImageAltField";
const DIALOG_IMAGE_FIELD_WIDTH = "dxHtmlEditor-dialogImageWidthField";
const DIALOG_IMAGE_FIELD_HEIGHT = "dxHtmlEditor-dialogImageHeightField";
const DIALOG_IMAGE_ADD_BUTTON = "dxHtmlEditor-dialogImageAddButton";
const DIALOG_IMAGE_UPDATE_BUTTON = "dxHtmlEditor-dialogImageUpdateButton";
const DIALOG_IMAGE_SPECIFY_URL = "dxHtmlEditor-dialogImageSpecifyUrl";
const DIALOG_IMAGE_SELECT_FILE = "dxHtmlEditor-dialogImageSelectFile";
const DIALOG_IMAGE_KEEP_ASPECT_RATIO = "dxHtmlEditor-dialogImageKeepAspectRatio";
const DIALOG_IMAGE_ENCODE_TO_BASE64 = "dxHtmlEditor-dialogImageEncodeToBase64";
const DIALOG_IMAGE_POPUP_CLASS = "dx-htmleditor-add-image-popup";
const DIALOG_IMAGE_POPUP_WITH_TABS_CLASS = "dx-htmleditor-add-image-popup-with-tabs";
const DIALOG_IMAGE_FIX_RATIO_CONTAINER = "dx-fix-ratio-container";
const FORM_DIALOG_CLASS = "dx-formdialog";
const USER_ACTION = "user";
const SILENT_ACTION = "silent";
const FILE_UPLOADER_NAME = "dx-htmleditor-image";
class ImageUploader {
constructor(module, config) {
this.module = module;
this.config = config ?? {};
this.quill = this.module.quill;
this.editorInstance = this.module.editorInstance
}
render() {
if (this.editorInstance._formDialog) {
this.editorInstance._formDialog.beforeAddButtonAction = () => this.getCurrentTab().upload()
}
this.tabPanelIndex = 0;
this.formData = this.getFormData();
this.isUpdating = this.isImageUpdating();
this.tabsModel = this.createTabsModel(this.config.tabs);
this.tabs = this.createTabs(this.formData);
const formConfig = this.getFormConfig();
this.updatePopupConfig();
this.updateAddButtonState();
this.editorInstance.showFormDialog(formConfig).done(((formData, event) => {
this.tabs[this.getActiveTabIndex()].strategy.pasteImage(formData, event)
})).always((() => {
this.resetDialogPopupOptions();
this.quill.focus()
}))
}
getCurrentTab() {
return this.tabs[this.tabPanelIndex]
}
updateAddButtonState() {
const isDisabled = this.getCurrentTab().isDisableButton();
this.setAddButtonDisabled(isDisabled)
}
setAddButtonDisabled(value) {
this.editorInstance.formDialogOption({
"toolbarItems[0].options.disabled": value
})
}
getActiveTabIndex() {
return this.isUpdating ? 0 : this.tabPanelIndex
}
getFormData() {
return this.getUpdateDialogFormData(this.quill.getFormat())
}
getUpdateDialogFormData(formData) {
const {
imageSrc: imageSrc,
src: src
} = formData, props = _objectWithoutPropertiesLoose(formData, _excluded);
return _extends({
src: imageSrc ?? src
}, props)
}
createUrlTab(formData) {
return new UrlTab(this.module, {
config: this.config,
formData: formData,
isUpdating: this.isUpdating
}, (() => this.updateAddButtonState()))
}
createFileTab() {
return new FileTab(this.module, {
config: this.config
}, (() => this.updateAddButtonState()))
}
createTabsModel() {
let model = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : [];
if (0 === model.length || this.isUpdating) {
return ["url"]
}
return model.map((tab => "object" === typeof tab ? tab.name : tab))
}
createTabs(formData) {
return this.tabsModel.map((tabName => {
const isUrlTab = "url" === tabName;
return isUrlTab ? this.createUrlTab(formData) : this.createFileTab()
}))
}
isImageUpdating() {
return Object.prototype.hasOwnProperty.call(this.module.quill.getFormat() ?? {}, "imageSrc")
}
updatePopupConfig() {
let wrapperClasses = `${DIALOG_IMAGE_POPUP_CLASS} dx-formdialog`;
if (this.useTabbedItems()) {
wrapperClasses += ` ${DIALOG_IMAGE_POPUP_WITH_TABS_CLASS}`
}
const titleKey = this.isUpdating ? DIALOG_UPDATE_IMAGE_CAPTION : DIALOG_IMAGE_CAPTION;
const addButtonTextKey = this.isUpdating ? DIALOG_IMAGE_UPDATE_BUTTON : DIALOG_IMAGE_ADD_BUTTON;
this.editorInstance.formDialogOption({
title: _message.default.format(titleKey),
"toolbarItems[0].options.text": _message.default.format(addButtonTextKey),
wrapperAttr: {
class: wrapperClasses
}
})
}
resetDialogPopupOptions() {
this.editorInstance.formDialogOption({
"toolbarItems[0].options.text": _message.default.format("OK"),
"toolbarItems[0].options.visible": true,
"toolbarItems[0].options.disabled": false,
wrapperAttr: {
class: "dx-formdialog"
}
})
}
useTabbedItems() {
return this.tabsModel.length > 1
}
getFormWidth() {
return isMobile ? "100%" : 493
}
getFormConfig() {
return {
formData: this.formData,
width: this.getFormWidth(),
labelLocation: "top",
colCount: this.useTabbedItems() ? 1 : 11,
items: this.getItemsConfig()
}
}
getItemsConfig() {
if (this.useTabbedItems()) {
const tabsConfig = (0, _iterator.map)(this.tabs, (tabController => ({
title: tabController.getTabName(),
colCount: 11,
items: tabController.getItemsConfig()
})));
return [{
itemType: "tabbed",
tabPanelOptions: {
onSelectionChanged: e => {
this.tabPanelIndex = e.component.option("selectedIndex");
this.updateAddButtonState()
}
},
tabs: tabsConfig
}]
}
return this.tabs[0].getItemsConfig()
}
}
exports.ImageUploader = ImageUploader;
class BaseTab {
constructor(module, _ref, onFileSelected) {
let {
config: config,
formData: formData,
isUpdating: isUpdating
} = _ref;
this.module = module;
this.config = config;
this.formData = formData;
this.isUpdating = isUpdating;
this.onFileSelected = onFileSelected;
this.strategy = this.createStrategy()
}
getItemsConfig() {
return this.strategy.getItemsConfig()
}
createStrategy() {
return this.isUpdating ? new UpdateUrlStrategy(this.module, this.config, this.formData) : new AddUrlStrategy(this.module, this.config, this.onFileSelected)
}
isDisableButton() {
return false
}
upload() {
return this.strategy.upload()
}
}
class UrlTab extends BaseTab {
getTabName() {
return _message.default.format(DIALOG_IMAGE_SPECIFY_URL)
}
}
class FileTab extends BaseTab {
getTabName() {
return _message.default.format(DIALOG_IMAGE_SELECT_FILE)
}
createStrategy() {
return new FileStrategy(this.module, this.config, this.onFileSelected)
}
isDisableButton() {
return !this.strategy.isValid()
}
}
class BaseStrategy {
constructor(module, config) {
this.module = module;
this.config = config;
this.editorInstance = module.editorInstance;
this.quill = module.quill;
this.selection = this.getQuillSelection()
}
getQuillSelection() {
const selection = this.quill.getSelection();
return selection ?? {
index: this.quill.getLength(),
length: 0
}
}
pasteImage(formData, event) {}
isValid() {
return true
}
upload() {}
}
class AddUrlStrategy extends BaseStrategy {
constructor(module, config, onFileSelected) {
super(module, config, onFileSelected);
this.shouldKeepAspectRatio = true
}
pasteImage(formData, event) {
this.module.saveValueChangeEvent(event);
urlUpload(this.quill, this.selection.index, formData)
}
keepAspectRatio(data, _ref2) {
let {
dependentEditor: dependentEditor,
e: e
} = _ref2;
const newValue = parseInt(e.value);
const previousValue = parseInt(e.previousValue);
const previousDependentEditorValue = parseInt(dependentEditor.option("value"));
data.component.updateData(data.dataField, newValue);
if (this.shouldKeepAspectRatio && previousDependentEditorValue && previousValue && !this.preventRecalculating) {
this.preventRecalculating = true;
dependentEditor.option("value", Math.round(newValue * previousDependentEditorValue / parseInt(previousValue)).toString())
}
this.preventRecalculating = false
}
createKeepAspectRatioEditor($container, data, dependentEditorDataField) {
return this.editorInstance._createComponent($container, _text_box.default, (0, _extend.extend)(true, data.editorOptions, {
value: data.component.option("formData")[data.dataField],
onEnterKey: data.component.option("onEditorEnterKey").bind(this.editorInstance._formDialog, data),
onValueChanged: e => {
this.keepAspectRatio(data, {
dependentEditor: this[`${dependentEditorDataField}Editor`],
e: e
})
}
}))
}
upload() {
const result = this.editorInstance._formDialog._form.validate();
return result.isValid
}
getItemsConfig() {
const stylingMode = (0, _themes.isFluent)() ? "text" : "outlined";
return [{
dataField: "src",
colSpan: 11,
label: {
text: _message.default.format(DIALOG_IMAGE_FIELD_URL)
},
validationRules: [{
type: "required"
}, {
type: "stringLength",
min: 1
}]
}, {
dataField: "width",
colSpan: 6,
label: {
text: _message.default.format(DIALOG_IMAGE_FIELD_WIDTH)
},
template: data => {
const $content = (0, _renderer.default)("<div>").addClass("dx-fix-ratio-container");
const $widthEditor = (0, _renderer.default)("<div>").appendTo($content);
this.widthEditor = this.createKeepAspectRatioEditor($widthEditor, data, "height");
const $ratioEditor = (0, _renderer.default)("<div>").appendTo($content);
this.editorInstance._createComponent($ratioEditor, _button_group.default, {
items: [{
icon: "imgarlock",
value: "keepRatio"
}],
hint: _message.default.format(DIALOG_IMAGE_KEEP_ASPECT_RATIO),
focusStateEnabled: false,
keyExpr: "value",
stylingMode: stylingMode,
selectionMode: "multiple",
selectedItemKeys: ["keepRatio"],
onSelectionChanged: e => {
this.shouldKeepAspectRatio = !!e.component.option("selectedItems").length
}
});
return $content
}
}, {
dataField: "height",
colSpan: 5,
label: {
text: _message.default.format(DIALOG_IMAGE_FIELD_HEIGHT)
},
template: data => {
const $content = (0, _renderer.default)("<div>");
this.heightEditor = this.createKeepAspectRatioEditor($content, data, "width");
return $content
}
}, {
dataField: "alt",
colSpan: 11,
label: {
text: _message.default.format(DIALOG_IMAGE_FIELD_ALT)
}
}]
}
}
class UpdateUrlStrategy extends AddUrlStrategy {
constructor(module, config, formData, onFileSelected) {
super(module, config, onFileSelected);
this.formData = formData;
this.modifyFormData()
}
modifyFormData() {
const {
imageSrc: imageSrc
} = this.quill.getFormat(this.selection.index - 1, 1);
if (!imageSrc || 0 === this.selection.index) {
this.selection = {
index: this.selection.index + 1,
length: 0
};
this.quill.setSelection(this.selection.index, this.selection.length, "silent")
}
const imgElement = this.quill.getLeaf(this.selection.index)[0].domNode;
if (imgElement) {
this.formData.width = this.formData.width ?? (0, _size.getWidth)((0, _renderer.default)(imgElement));
this.formData.height = this.formData.height ?? (0, _size.getHeight)((0, _renderer.default)(imgElement))
}
}
pasteImage(formData, event) {
this.quill.deleteText(this.embedFormatIndex(), 1, "silent");
this.selection.index -= 1;
super.pasteImage(formData, event)
}
embedFormatIndex() {
const selection = this.selection ?? this.quill.getSelection();
if (selection) {
if (selection.length) {
return selection.index
}
return selection.index - 1
}
return this.quill.getLength()
}
}
class FileStrategy extends BaseStrategy {
constructor(module, config, onFileSelected) {
super(module, config, onFileSelected);
this.useBase64 = !(0, _type.isDefined)(this.config.fileUploadMode) || "base64" === this.config.fileUploadMode;
this.isValidInternal = false;
this.onFileSelected = onFileSelected;
this.data = null
}
upload() {
if (this.useBase64) {
this.base64Upload(this.data)
} else if (this.data.value.length) {
this.data.component.upload()
}
return true
}
isValid() {
return this.isValidInternal
}
onUploaded(data) {
serverUpload(this.config.uploadDirectory, data.file.name, this.quill, this.selection.index)
}
base64Upload(data) {
this.quill.getModule("uploader").upload(this.selection, data.value, true)
}
pasteImage(formData, event) {
if (this.useBase64) {
super.pasteImage(formData, event)
}
}
isBase64Editable() {
return "both" === this.config.fileUploadMode
}
validate(e) {
const fileUploader = e.component;
this.isValidInternal = !fileUploader._files.some((file => !file.isValid()));
if (0 === fileUploader._files.length) {
this.isValidInternal = false
}
}
getFileUploaderOptions() {
const fileUploaderOptions = {
uploadUrl: this.config.uploadUrl,
onValueChanged: data => {
this.validate(data);
this.data = data;
this.onFileSelected()
},
onUploaded: e => this.onUploaded(e)
};
return (0, _extend.extend)({}, getFileUploaderBaseOptions(), fileUploaderOptions, this.config.fileUploaderOptions)
}
getItemsConfig() {
return [{
itemType: "simple",
dataField: "files",
colSpan: 11,
label: {
visible: false
},
template: () => {
const $content = (0, _renderer.default)("<div>");
this.module.editorInstance._createComponent($content, _file_uploader.default, this.getFileUploaderOptions());
return $content
}
}, {
itemType: "simple",
colSpan: 11,
label: {
visible: false
},
editorType: "dxCheckBox",
editorOptions: {
value: this.useBase64,
visible: this.isBase64Editable(),
text: _message.default.format(DIALOG_IMAGE_ENCODE_TO_BASE64),
onValueChanged: e => {
if (this.isBase64Editable()) {
this.useBase64 = e.value
}
}
}
}]
}
}
function correctSlashesInUrl(url) {
return "/" !== url[url.length - 1] ? `${url}/` : url
}
function getFileUploaderBaseOptions() {
return {
value: [],
name: FILE_UPLOADER_NAME,
accept: "image/*",
uploadMode: "useButtons"
}
}
function urlUpload(quill, index, attributes) {
quill.insertEmbed(index, "extendedImage", attributes, USER_ACTION);
quill.setSelection(index + 1, 0, USER_ACTION)
}
function serverUpload(url, fileName, quill, pasteIndex) {
if (url) {
const imageUrl = correctSlashesInUrl(url) + fileName;
urlUpload(quill, pasteIndex, {
src: imageUrl
})
}
}