devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
649 lines (646 loc) • 25 kB
JavaScript
/**
* DevExtreme (cjs/__internal/ui/html_editor/m_html_editor.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.default = void 0;
require("../../ui/html_editor/converters/m_delta");
var _events = require("../../../common/core/events");
var _events_engine = _interopRequireDefault(require("../../../common/core/events/core/events_engine"));
var _emitterGesture = _interopRequireDefault(require("../../../common/core/events/gesture/emitter.gesture.scroll"));
var _pointer = _interopRequireDefault(require("../../../common/core/events/pointer"));
var _index = require("../../../common/core/events/utils/index");
var _component_registrator = _interopRequireDefault(require("../../../core/component_registrator"));
var _config = _interopRequireDefault(require("../../../core/config"));
var _devices = _interopRequireDefault(require("../../../core/devices"));
var _element = require("../../../core/element");
var _renderer = _interopRequireDefault(require("../../../core/renderer"));
var _empty_template = require("../../../core/templates/empty_template");
var _callbacks = _interopRequireDefault(require("../../../core/utils/callbacks"));
var _common = require("../../../core/utils/common");
var _deferred = require("../../../core/utils/deferred");
var _extend = require("../../../core/utils/extend");
var _type = require("../../../core/utils/type");
var _editor = _interopRequireDefault(require("../../ui/editor/editor"));
var _m_converterController = _interopRequireDefault(require("../../ui/html_editor/m_converterController"));
var _m_quill_importer = require("../../ui/html_editor/m_quill_importer");
var _m_quill_registrator = _interopRequireDefault(require("../../ui/html_editor/m_quill_registrator"));
var _m_wordLists = _interopRequireDefault(require("../../ui/html_editor/matchers/m_wordLists"));
var _m_formDialog = _interopRequireDefault(require("../../ui/html_editor/ui/m_formDialog"));
var _m_utils = require("../../ui/text_box/m_utils.scroll");
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)
}
const HTML_EDITOR_CLASS = "dx-htmleditor";
const QUILL_CONTAINER_CLASS = "dx-quill-container";
const QUILL_CLIPBOARD_CLASS = "ql-clipboard";
const HTML_EDITOR_SUBMIT_ELEMENT_CLASS = "dx-htmleditor-submit-element";
const HTML_EDITOR_CONTENT_CLASS = "dx-htmleditor-content";
const ANONYMOUS_TEMPLATE_NAME = "htmlContent";
const isIos = "ios" === _devices.default.current().platform;
let editorsCount = 0;
class HtmlEditor extends _editor.default {
_getDefaultOptions() {
return _extends({}, super._getDefaultOptions(), {
focusStateEnabled: true,
placeholder: "",
toolbar: null,
variables: null,
mediaResizing: null,
tableResizing: null,
mentions: null,
customizeModules: null,
tableContextMenu: null,
allowSoftLineBreak: false,
formDialogOptions: null,
imageUpload: null,
stylingMode: (0, _config.default)().editorStylingMode || "outlined",
converter: null
})
}
_init() {
this._mentionKeyInTemplateStorage = editorsCount++;
super._init();
this._cleanCallback = (0, _callbacks.default)();
this._contentInitializedCallback = (0, _callbacks.default)();
this._prepareHtmlConverter()
}
_prepareHtmlConverter() {
const {
converter: converter
} = this.option();
if (converter) {
this._htmlConverter = converter
}
}
_getAnonymousTemplateName() {
return "htmlContent"
}
_initTemplates() {
this._templateManager.addDefaultTemplates({
[ANONYMOUS_TEMPLATE_NAME]: new _empty_template.EmptyTemplate
});
super._initTemplates()
}
_focusTarget() {
return this._getContent()
}
_getContent() {
return this.$element().find(".dx-htmleditor-content")
}
_focusInHandler(_ref) {
let {
relatedTarget: relatedTarget
} = _ref;
if (this._shouldSkipFocusEvent(relatedTarget)) {
return
}
this._toggleFocusClass(true, this.$element());
super._focusInHandler.apply(this, arguments)
}
_focusOutHandler(_ref2) {
let {
relatedTarget: relatedTarget
} = _ref2;
if (this._shouldSkipFocusEvent(relatedTarget)) {
return
}
this._toggleFocusClass(false, this.$element());
super._focusOutHandler.apply(this, arguments)
}
_shouldSkipFocusEvent(relatedTarget) {
return (0, _renderer.default)(relatedTarget).hasClass("ql-clipboard")
}
_initMarkup() {
this._$htmlContainer = (0, _renderer.default)("<div>").addClass("dx-quill-container");
this.$element().attr("role", "application").addClass("dx-htmleditor").wrapInner(this._$htmlContainer);
this._renderStylingMode();
const template = this._getTemplate("htmlContent");
this._$templateResult = template && template.render({
container: (0, _element.getPublicElement)(this._$htmlContainer),
noModel: true,
transclude: true
});
this._renderSubmitElement();
super._initMarkup();
this._updateContainerMarkup()
}
_renderValidationState() {
const $content = this._getContent();
if (1 === $content.length) {
super._renderValidationState()
}
}
_renderSubmitElement() {
this._$submitElement = (0, _renderer.default)("<textarea>").addClass("dx-htmleditor-submit-element").attr("hidden", true).appendTo(this.$element());
this._setSubmitValue(this.option("value"))
}
_setSubmitValue(value) {
this._getSubmitElement().val(value)
}
_getSubmitElement() {
return this._$submitElement
}
_createNoScriptFrame() {
return (0, _renderer.default)("<iframe>").css("display", "none").attr({
srcdoc: "",
id: "xss-frame",
sandbox: "allow-same-origin"
})
}
_removeXSSVulnerableHtml(value) {
const $frame = this._createNoScriptFrame().appendTo("body");
const frame = $frame.get(0);
const frameWindow = frame.contentWindow;
const frameDocument = frameWindow.document;
const frameDocumentBody = frameDocument.body;
const quill = (0, _m_quill_importer.getQuill)();
const valueWithoutStyles = quill.replaceStyleAttribute(value);
frameDocumentBody.innerHTML = valueWithoutStyles;
const removeInlineHandlers = element => {
if (element.attributes) {
for (let i = 0; i < element.attributes.length; i++) {
const {
name: name
} = element.attributes[i];
if (name.startsWith("on")) {
element.removeAttribute(name)
}
}
}
if (element.childNodes) {
for (let i = 0; i < element.childNodes.length; i++) {
removeInlineHandlers(element.childNodes[i])
}
}
};
removeInlineHandlers(frameDocumentBody);
frameDocumentBody.querySelectorAll("script").forEach((scriptNode => {
scriptNode.remove()
}));
const sanitizedHtml = frameDocumentBody.innerHTML;
$frame.remove();
return sanitizedHtml
}
_convertToHtml(value) {
var _this$_htmlConverter;
const result = (0, _type.isFunction)(null === (_this$_htmlConverter = this._htmlConverter) || void 0 === _this$_htmlConverter ? void 0 : _this$_htmlConverter.toHtml) ? String(this._htmlConverter.toHtml(value ?? "") ?? "") : value;
return result
}
_convertFromHtml(value) {
var _this$_htmlConverter2;
const result = (0, _type.isFunction)(null === (_this$_htmlConverter2 = this._htmlConverter) || void 0 === _this$_htmlConverter2 ? void 0 : _this$_htmlConverter2.fromHtml) ? String(this._htmlConverter.fromHtml(value) ?? "") : value;
return result
}
_updateContainerMarkup() {
const {
value: value
} = this.option();
const html = this._convertToHtml(value);
if (!html) {
return
}
const sanitizedHtml = this._removeXSSVulnerableHtml(html);
this._$htmlContainer.html(sanitizedHtml)
}
_render() {
this._prepareConverters();
super._render();
this._toggleReadOnlyState()
}
_prepareQuillRegistrator() {
if (!this._quillRegistrator) {
this._quillRegistrator = new _m_quill_registrator.default
}
}
_getRegistrator() {
this._prepareQuillRegistrator();
return this._quillRegistrator
}
_prepareConverters() {
if (!this._deltaConverter) {
const DeltaConverter = _m_converterController.default.getConverter("delta");
if (DeltaConverter) {
this._deltaConverter = new DeltaConverter
}
}
}
_renderContentImpl() {
this._contentRenderedDeferred = (0, _deferred.Deferred)();
const renderContentPromise = this._contentRenderedDeferred.promise();
super._renderContentImpl();
this._renderHtmlEditor();
this._renderFormDialog();
this._addKeyPressHandler();
return renderContentPromise
}
_pointerMoveHandler(e) {
if (isIos) {
e.stopPropagation()
}
}
_attachFocusEvents() {
(0, _common.deferRender)(super._attachFocusEvents.bind(this))
}
_addKeyPressHandler() {
const keyDownEvent = (0, _index.addNamespace)("keydown", `${this.NAME}TextChange`);
_events_engine.default.on(this._$htmlContainer, keyDownEvent, this._keyDownHandler.bind(this))
}
_keyDownHandler(e) {
this._saveValueChangeEvent(e)
}
_renderHtmlEditor() {
const customizeModules = this.option("customizeModules");
const modulesConfig = this._getModulesConfig();
if ((0, _type.isFunction)(customizeModules)) {
customizeModules(modulesConfig)
}
this._quillInstance = this._getRegistrator().createEditor(this._$htmlContainer[0], {
placeholder: this.option("placeholder"),
readOnly: this.option("readOnly") || this.option("disabled"),
modules: modulesConfig,
theme: "basic"
});
this._renderValidationState();
this._deltaConverter.setQuillInstance(this._quillInstance);
this._textChangeHandlerWithContext = this._textChangeHandler.bind(this);
this._quillInstance.on("text-change", this._textChangeHandlerWithContext);
this._renderScrollHandler();
if (this._hasTranscludedContent()) {
this._updateContentTask = (0, _common.executeAsync)((() => {
this._applyTranscludedContent()
}))
} else {
this._finalizeContentRendering()
}
}
_renderScrollHandler() {
const $scrollContainer = this._getContent();
const initScrollData = (0, _m_utils.prepareScrollData)($scrollContainer);
_events_engine.default.on($scrollContainer, (0, _index.addNamespace)(_emitterGesture.default.init, this.NAME), initScrollData, _common.noop);
_events_engine.default.on($scrollContainer, (0, _index.addNamespace)(_pointer.default.move, this.NAME), this._pointerMoveHandler.bind(this))
}
_applyTranscludedContent() {
const valueOption = this.option("value");
if (!(0, _type.isDefined)(valueOption)) {
const html = this._deltaConverter.toHtml();
const newDelta = this._quillInstance.clipboard.convert({
html: html
});
if (newDelta.ops.length) {
this._quillInstance.setContents(newDelta);
return
}
}
this._finalizeContentRendering()
}
_hasTranscludedContent() {
return this._$templateResult && this._$templateResult.length
}
_getModulesConfig() {
const quill = this._getRegistrator().getQuill();
const wordListMatcher = (0, _m_wordLists.default)(quill);
const modulesConfig = (0, _extend.extend)({}, {
table: true,
toolbar: this._getModuleConfigByOption("toolbar"),
variables: this._getModuleConfigByOption("variables"),
resizing: this._getModuleConfigByOption("mediaResizing"),
tableResizing: this._getModuleConfigByOption("tableResizing"),
tableContextMenu: this._getModuleConfigByOption("tableContextMenu"),
imageUpload: this._getModuleConfigByOption("imageUpload"),
imageCursor: this._getBaseModuleConfig(),
mentions: this._getModuleConfigByOption("mentions"),
uploader: {
onDrop: e => this._saveValueChangeEvent((0, _events.Event)(e)),
imageBlot: "extendedImage"
},
keyboard: {
onKeydown: e => this._saveValueChangeEvent((0, _events.Event)(e))
},
clipboard: {
onPaste: e => this._saveValueChangeEvent((0, _events.Event)(e)),
onCut: e => this._saveValueChangeEvent((0, _events.Event)(e)),
matchers: [
["p.MsoListParagraphCxSpFirst", wordListMatcher],
["p.MsoListParagraphCxSpMiddle", wordListMatcher],
["p.MsoListParagraphCxSpLast", wordListMatcher]
]
},
multiline: Boolean(this.option("allowSoftLineBreak"))
}, this._getCustomModules());
return modulesConfig
}
_getModuleConfigByOption(userOptionName) {
const optionValue = this.option(userOptionName);
let config = {};
if (!(0, _type.isDefined)(optionValue)) {
return
}
if (Array.isArray(optionValue)) {
config[userOptionName] = optionValue
} else {
config = optionValue
}
return (0, _extend.extend)(this._getBaseModuleConfig(), config)
}
_getBaseModuleConfig() {
return {
editorInstance: this
}
}
_getCustomModules() {
const modules = {};
const moduleNames = this._getRegistrator().getRegisteredModuleNames();
moduleNames.forEach((modulePath => {
modules[modulePath] = this._getBaseModuleConfig()
}));
return modules
}
_textChangeHandler() {
const {
value: currentValue
} = this.option();
const html = this._deltaConverter.toHtml();
const convertedValue = this._convertFromHtml(html);
if (currentValue !== convertedValue && !this._isNullValueConverted(currentValue, convertedValue)) {
this._isEditorUpdating = true;
this.option("value", convertedValue)
}
this._finalizeContentRendering()
}
_isNullValueConverted(currentValue, convertedValue) {
return null === currentValue && "" === convertedValue
}
_finalizeContentRendering() {
if (this._contentRenderedDeferred) {
this.clearHistory();
this._contentInitializedCallback.fire();
this._contentRenderedDeferred.resolve();
this._contentRenderedDeferred = void 0
}
}
_resetEnabledState() {
if (this._quillInstance) {
const isEnabled = !(this.option("readOnly") || this.option("disabled"));
this._quillInstance.enable(isEnabled)
}
}
_renderFormDialog() {
const userOptions = (0, _extend.extend)(true, {
width: "auto",
height: "auto",
hideOnOutsideClick: true
}, this.option("formDialogOptions"));
this._formDialog = new _m_formDialog.default(this, userOptions)
}
_getStylingModePrefix() {
return "dx-htmleditor-"
}
_getQuillContainer() {
return this._$htmlContainer
}
_prepareModuleOptions(args) {
var _args$fullName;
const optionData = null === (_args$fullName = args.fullName) || void 0 === _args$fullName ? void 0 : _args$fullName.split(".");
let {
value: value
} = args;
const optionName = optionData.length >= 2 ? optionData[1] : args.name;
if (3 === optionData.length) {
value = {
[optionData[2]]: value
}
}
return [optionName, value]
}
_moduleOptionChanged(moduleName, args) {
const moduleInstance = this.getModule(moduleName);
const shouldPassOptionsToModule = Boolean(moduleInstance);
if (shouldPassOptionsToModule) {
moduleInstance.option(...this._prepareModuleOptions(args))
} else {
this._invalidate()
}
}
_processHtmlContentUpdating(value) {
if (this._quillInstance) {
if (this._isEditorUpdating) {
this._isEditorUpdating = false
} else {
const html = this._convertToHtml(value);
this._suppressValueChangeAction();
this._updateHtmlContent(html);
this._resumeValueChangeAction()
}
} else {
this._$htmlContainer.html(value)
}
}
_optionChanged(args) {
switch (args.name) {
case "converter": {
this._htmlConverter = args.value;
const {
value: value
} = this.option();
this._processHtmlContentUpdating(value);
break
}
case "value": {
this._processHtmlContentUpdating(args.value);
const value = this.option("value");
if (value !== args.previousValue) {
this._setSubmitValue(value);
super._optionChanged(_extends({}, args, {
value: value
}))
}
break
}
case "placeholder":
case "variables":
case "toolbar":
case "mentions":
case "customizeModules":
case "allowSoftLineBreak":
this._invalidate();
break;
case "tableResizing":
this._moduleOptionChanged("tableResizing", args);
break;
case "stylingMode":
this._renderStylingMode();
break;
case "readOnly":
case "disabled":
super._optionChanged(args);
this._resetEnabledState();
break;
case "formDialogOptions":
this._renderFormDialog();
break;
case "tableContextMenu":
this._moduleOptionChanged("tableContextMenu", args);
break;
case "mediaResizing":
this._moduleOptionChanged("resizing", args);
break;
case "width":
super._optionChanged(args);
this._repaintToolbar();
break;
case "imageUpload":
this._moduleOptionChanged("imageUpload", args);
break;
default:
super._optionChanged(args)
}
}
_repaintToolbar() {
this._applyToolbarMethod("repaint")
}
_updateHtmlContent(html) {
const newDelta = this._quillInstance.clipboard.convert({
html: html
});
this._quillInstance.setContents(newDelta)
}
_clean() {
if (this._quillInstance) {
_events_engine.default.off(this._getContent(), `.${this.NAME}`);
this._quillInstance.off("text-change", this._textChangeHandlerWithContext);
this._cleanCallback.fire()
}
this._abortUpdateContentTask();
this._cleanCallback.empty();
this._contentInitializedCallback.empty();
super._clean()
}
_abortUpdateContentTask() {
if (this._updateContentTask) {
this._updateContentTask.abort();
this._updateContentTask = void 0
}
}
_applyQuillMethod(methodName, args) {
if (this._quillInstance) {
return this._quillInstance[methodName].apply(this._quillInstance, args)
}
}
_applyQuillHistoryMethod(methodName) {
if (this._quillInstance && this._quillInstance.history) {
this._quillInstance.history[methodName]()
}
}
_applyToolbarMethod(methodName) {
var _this$getModule;
null === (_this$getModule = this.getModule("toolbar")) || void 0 === _this$getModule || _this$getModule[methodName]()
}
addCleanCallback(callback) {
this._cleanCallback.add(callback)
}
addContentInitializedCallback(callback) {
this._contentInitializedCallback.add(callback)
}
register(components) {
this._getRegistrator().registerModules(components);
if (this._quillInstance) {
this.repaint()
}
}
get(modulePath) {
return this._getRegistrator().getQuill().import(modulePath)
}
getModule(moduleName) {
return this._applyQuillMethod("getModule", arguments)
}
getQuillInstance() {
return this._quillInstance
}
getSelection(focus) {
return this._applyQuillMethod("getSelection", arguments)
}
setSelection(index, length) {
this._applyQuillMethod("setSelection", arguments)
}
getText(index, length) {
return this._applyQuillMethod("getText", arguments)
}
format(formatName, formatValue) {
this._applyQuillMethod("format", arguments)
}
formatText(index, length, formatName, formatValue) {
this._applyQuillMethod("formatText", arguments)
}
formatLine(index, length, formatName, formatValue) {
this._applyQuillMethod("formatLine", arguments)
}
getFormat(index, length) {
return this._applyQuillMethod("getFormat", arguments)
}
removeFormat(index, length) {
return this._applyQuillMethod("removeFormat", arguments)
}
clearHistory() {
this._applyQuillHistoryMethod("clear");
this._applyToolbarMethod("updateHistoryWidgets")
}
undo() {
this._applyQuillHistoryMethod("undo")
}
redo() {
this._applyQuillHistoryMethod("redo")
}
getLength() {
return this._applyQuillMethod("getLength")
}
getBounds(index, length) {
return this._applyQuillMethod("getBounds", arguments)
}
delete(index, length) {
this._applyQuillMethod("deleteText", arguments)
}
insertText(index, text, formats) {
this._applyQuillMethod("insertText", arguments)
}
insertEmbed(index, type, config) {
this._applyQuillMethod("insertEmbed", arguments)
}
showFormDialog(formConfig) {
return this._formDialog.show(formConfig)
}
formDialogOption(optionName, optionValue) {
return this._formDialog.popupOption.apply(this._formDialog, arguments)
}
focus() {
super.focus();
this._applyQuillMethod("focus")
}
blur() {
this._applyQuillMethod("blur")
}
getMentionKeyInTemplateStorage() {
return this._mentionKeyInTemplateStorage
}
}(0, _component_registrator.default)("dxHtmlEditor", HtmlEditor);
var _default = exports.default = HtmlEditor;