UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

649 lines (646 loc) • 25 kB
/** * 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;