UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

534 lines (533 loc) • 19.7 kB
/** * DevExtreme (esm/ui/html_editor/ui.html_editor.js) * Version: 21.1.4 * Build date: Mon Jun 21 2021 * * Copyright (c) 2012 - 2021 Developer Express Inc. ALL RIGHTS RESERVED * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/ */ import $ from "../../core/renderer"; import { extend } from "../../core/utils/extend"; import { isDefined, isFunction } from "../../core/utils/type"; import { getPublicElement } from "../../core/element"; import { executeAsync, noop, ensureDefined, deferRender } from "../../core/utils/common"; import registerComponent from "../../core/component_registrator"; import { EmptyTemplate } from "../../core/templates/empty_template"; import Editor from "../editor/editor"; import Errors from "../widget/ui.errors"; import Callbacks from "../../core/utils/callbacks"; import { Deferred } from "../../core/utils/deferred"; import eventsEngine from "../../events/core/events_engine"; import { addNamespace } from "../../events/utils/index"; import { Event as dxEvent } from "../../events/index"; import scrollEvents from "../scroll_view/ui.events.emitter.gesture.scroll"; import { prepareScrollData } from "../text_box/utils.scroll"; import QuillRegistrator from "./quill_registrator"; import "./converters/delta"; import ConverterController from "./converterController"; import getWordMatcher from "./matchers/wordLists"; import FormDialog from "./ui/formDialog"; var HTML_EDITOR_CLASS = "dx-htmleditor"; var QUILL_CONTAINER_CLASS = "dx-quill-container"; var QUILL_CLIPBOARD_CLASS = "ql-clipboard"; var HTML_EDITOR_SUBMIT_ELEMENT_CLASS = "dx-htmleditor-submit-element"; var HTML_EDITOR_CONTENT_CLASS = "dx-htmleditor-content"; var MARKDOWN_VALUE_TYPE = "markdown"; var ANONYMOUS_TEMPLATE_NAME = "htmlContent"; var HtmlEditor = Editor.inherit({ _getDefaultOptions: function() { return extend(this.callBase(), { focusStateEnabled: true, valueType: "html", placeholder: "", toolbar: null, variables: null, mediaResizing: null, mentions: null, customizeModules: null, formDialogOptions: null, stylingMode: "outlined" }) }, _init: function() { this.callBase(); this._cleanCallback = Callbacks(); this._contentInitializedCallback = Callbacks() }, _getAnonymousTemplateName: function() { return ANONYMOUS_TEMPLATE_NAME }, _initTemplates: function() { this._templateManager.addDefaultTemplates({ [ANONYMOUS_TEMPLATE_NAME]: new EmptyTemplate }); this.callBase() }, _focusTarget: function() { return this._getContent() }, _getContent: function() { return this.$element().find(".".concat(HTML_EDITOR_CONTENT_CLASS)) }, _focusInHandler: function(_ref) { var { relatedTarget: relatedTarget } = _ref; if (this._shouldSkipFocusEvent(relatedTarget)) { return } this._toggleFocusClass(true, this.$element()); this.callBase.apply(this, arguments) }, _focusOutHandler: function(_ref2) { var { relatedTarget: relatedTarget } = _ref2; if (this._shouldSkipFocusEvent(relatedTarget)) { return } this._toggleFocusClass(false, this.$element()); this.callBase.apply(this, arguments) }, _shouldSkipFocusEvent: function(relatedTarget) { return $(relatedTarget).hasClass(QUILL_CLIPBOARD_CLASS) }, _initMarkup: function() { this._$htmlContainer = $("<div>").addClass(QUILL_CONTAINER_CLASS); this.$element().attr("role", "application").addClass(HTML_EDITOR_CLASS).wrapInner(this._$htmlContainer); this._renderStylingMode(); var template = this._getTemplate(ANONYMOUS_TEMPLATE_NAME); this._$templateResult = template && template.render({ container: getPublicElement(this._$htmlContainer), noModel: true, transclude: true }); this._renderSubmitElement(); this.callBase(); this._updateContainerMarkup() }, _renderSubmitElement: function() { this._$submitElement = $("<textarea>").addClass(HTML_EDITOR_SUBMIT_ELEMENT_CLASS).attr("hidden", true).appendTo(this.$element()); this._setSubmitValue(this.option("value")) }, _setSubmitValue: function(value) { this._getSubmitElement().val(value) }, _getSubmitElement: function() { return this._$submitElement }, _updateContainerMarkup: function() { var markup = this.option("value"); if (this._isMarkdownValue()) { this._prepareMarkdownConverter(); markup = this._markdownConverter.toHtml(markup) } if (markup) { this._$htmlContainer.html(markup) } }, _prepareMarkdownConverter: function() { var MarkdownConverter = ConverterController.getConverter("markdown"); if (MarkdownConverter) { this._markdownConverter = new MarkdownConverter } else { throw Errors.Error("E1051", "markdown") } }, _render: function() { this._prepareConverters(); this.callBase() }, _prepareQuillRegistrator: function() { if (!this._quillRegistrator) { this._quillRegistrator = new QuillRegistrator } }, _getRegistrator: function() { this._prepareQuillRegistrator(); return this._quillRegistrator }, _prepareConverters: function() { if (!this._deltaConverter) { var DeltaConverter = ConverterController.getConverter("delta"); if (DeltaConverter) { this._deltaConverter = new DeltaConverter } } if (this.option("valueType") === MARKDOWN_VALUE_TYPE && !this._markdownConverter) { this._prepareMarkdownConverter() } }, _renderContentImpl: function() { this._contentRenderedDeferred = new Deferred; var renderContentPromise = this._contentRenderedDeferred.promise(); this.callBase(); this._renderHtmlEditor(); this._renderFormDialog(); this._addKeyPressHandler(); return renderContentPromise }, _attachFocusEvents: function() { deferRender(this.callBase.bind(this)) }, _addKeyPressHandler: function() { var keyDownEvent = addNamespace("keydown", "".concat(this.NAME, "TextChange")); eventsEngine.on(this._$htmlContainer, keyDownEvent, this._keyDownHandler.bind(this)) }, _keyDownHandler: function(e) { this._saveValueChangeEvent(e) }, _renderHtmlEditor: function() { var customizeModules = this.option("customizeModules"); var modulesConfig = this._getModulesConfig(); if (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._deltaConverter.setQuillInstance(this._quillInstance); this._textChangeHandlerWithContext = this._textChangeHandler.bind(this); this._quillInstance.on("text-change", this._textChangeHandlerWithContext); this._renderScrollHandler(); if (this._hasTranscludedContent()) { this._updateContentTask = executeAsync(() => { this._applyTranscludedContent() }) } else { this._finalizeContentRendering() } }, _renderScrollHandler: function() { var $scrollContainer = this._getContent(); var initScrollData = prepareScrollData($scrollContainer); eventsEngine.on($scrollContainer, addNamespace(scrollEvents.init, this.NAME), initScrollData, noop) }, _applyTranscludedContent: function() { var valueOption = this.option("value"); if (!isDefined(valueOption)) { var html = this._deltaConverter.toHtml(); var newDelta = this._quillInstance.clipboard.convert({ html: html }); if (newDelta.ops.length) { this._quillInstance.setContents(newDelta); return } } this._finalizeContentRendering() }, _hasTranscludedContent: function() { return this._$templateResult && this._$templateResult.length }, _getModulesConfig: function() { var quill = this._getRegistrator().getQuill(); var wordListMatcher = getWordMatcher(quill); var modulesConfig = extend({}, { table: true, toolbar: this._getModuleConfigByOption("toolbar"), variables: this._getModuleConfigByOption("variables"), resizing: this._getModuleConfigByOption("mediaResizing"), mentions: this._getModuleConfigByOption("mentions"), uploader: { onDrop: e => this._saveValueChangeEvent(dxEvent(e)), imageBlot: "extendedImage" }, keyboard: { onKeydown: e => this._saveValueChangeEvent(dxEvent(e)) }, clipboard: { onPaste: e => this._saveValueChangeEvent(dxEvent(e)), onCut: e => this._saveValueChangeEvent(dxEvent(e)), matchers: [ ["p.MsoListParagraphCxSpFirst", wordListMatcher], ["p.MsoListParagraphCxSpMiddle", wordListMatcher], ["p.MsoListParagraphCxSpLast", wordListMatcher] ] } }, this._getCustomModules()); return modulesConfig }, _getModuleConfigByOption: function(userOptionName) { var optionValue = this.option(userOptionName); var config = {}; if (!isDefined(optionValue)) { return } if (Array.isArray(optionValue)) { config[userOptionName] = optionValue } else { config = optionValue } return extend(this._getBaseModuleConfig(), config) }, _getBaseModuleConfig: function() { return { editorInstance: this } }, _getCustomModules: function() { var modules = {}; var moduleNames = this._getRegistrator().getRegisteredModuleNames(); moduleNames.forEach(modulePath => { modules[modulePath] = this._getBaseModuleConfig() }); return modules }, _textChangeHandler: function(newDelta, oldDelta, source) { var htmlMarkup = this._deltaConverter.toHtml(); var convertedValue = this._isMarkdownValue() ? this._updateValueByType(MARKDOWN_VALUE_TYPE, htmlMarkup) : htmlMarkup; var currentValue = this.option("value"); if (currentValue !== convertedValue && !this._isNullValueConverted(currentValue, convertedValue)) { this._isEditorUpdating = true; this.option("value", convertedValue) } this._finalizeContentRendering() }, _isNullValueConverted: function(currentValue, convertedValue) { return null === currentValue && "" === convertedValue }, _finalizeContentRendering: function() { if (this._contentRenderedDeferred) { this.clearHistory(); this._contentInitializedCallback.fire(); this._contentRenderedDeferred.resolve(); this._contentRenderedDeferred = void 0 } }, _updateValueByType: function(valueType, value) { var converter = this._markdownConverter; if (!isDefined(converter)) { return } var currentValue = ensureDefined(value, this.option("value")); return valueType === MARKDOWN_VALUE_TYPE ? converter.toMarkdown(currentValue) : converter.toHtml(currentValue) }, _isMarkdownValue: function() { return this.option("valueType") === MARKDOWN_VALUE_TYPE }, _resetEnabledState: function() { if (this._quillInstance) { var isEnabled = !(this.option("readOnly") || this.option("disabled")); this._quillInstance.enable(isEnabled) } }, _renderFormDialog: function() { var userOptions = extend(true, { width: "auto", height: "auto", closeOnOutsideClick: true }, this.option("formDialogOptions")); this._formDialog = new FormDialog(this, userOptions) }, _getStylingModePrefix: function() { return "dx-htmleditor-" }, _getQuillContainer: function() { return this._$htmlContainer }, _optionChanged: function(args) { switch (args.name) { case "value": if (this._quillInstance) { if (this._isEditorUpdating) { this._isEditorUpdating = false } else { var updatedValue = this._isMarkdownValue() ? this._updateValueByType("HTML", args.value) : args.value; this._updateHtmlContent(updatedValue) } } else { this._$htmlContainer.html(args.value) } this._setSubmitValue(args.value); this.callBase(args); break; case "placeholder": case "variables": case "toolbar": case "mentions": case "customizeModules": this._invalidate(); break; case "valueType": this._prepareConverters(); var newValue = this._updateValueByType(args.value); if ("html" === args.value && this._quillInstance) { this._updateHtmlContent(newValue) } else { this.option("value", newValue) } break; case "stylingMode": this._renderStylingMode(); break; case "readOnly": case "disabled": this.callBase(args); this._resetEnabledState(); break; case "formDialogOptions": this._renderFormDialog(); break; case "mediaResizing": if (!args.previousValue || !args.value) { this._invalidate() } else { this._quillInstance.getModule("resizing").option(args.name, args.value) } break; case "width": this.callBase(args); this._repaintToolbar(); break; default: this.callBase(args) } }, _repaintToolbar: function() { var toolbar = this._quillInstance.getModule("toolbar"); toolbar && toolbar.repaint() }, _updateHtmlContent: function(html) { var newDelta = this._quillInstance.clipboard.convert({ html: html }); this._quillInstance.setContents(newDelta) }, _clean: function() { if (this._quillInstance) { eventsEngine.off(this._getContent(), ".".concat(this.NAME)); this._quillInstance.off("text-change", this._textChangeHandlerWithContext); this._cleanCallback.fire() } this._abortUpdateContentTask(); this._cleanCallback.empty(); this._contentInitializedCallback.empty(); this.callBase() }, _abortUpdateContentTask: function() { 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]() } }, addCleanCallback(callback) { this._cleanCallback.add(callback) }, addContentInitializedCallback(callback) { this._contentInitializedCallback.add(callback) }, register: function(components) { this._getRegistrator().registerModules(components); if (this._quillInstance) { this.repaint() } }, get: function(modulePath) { return this._getRegistrator().getQuill().import(modulePath) }, getModule: function(moduleName) { return this._applyQuillMethod("getModule", arguments) }, getQuillInstance: function() { return this._quillInstance }, getSelection: function(focus) { return this._applyQuillMethod("getSelection", arguments) }, setSelection: function(index, length) { this._applyQuillMethod("setSelection", arguments) }, getText: function(index, length) { return this._applyQuillMethod("getText", arguments) }, format: function(formatName, formatValue) { this._applyQuillMethod("format", arguments) }, formatText: function(index, length, formatName, formatValue) { this._applyQuillMethod("formatText", arguments) }, formatLine: function(index, length, formatName, formatValue) { this._applyQuillMethod("formatLine", arguments) }, getFormat: function(index, length) { return this._applyQuillMethod("getFormat", arguments) }, removeFormat: function(index, length) { return this._applyQuillMethod("removeFormat", arguments) }, clearHistory: function() { this._applyQuillHistoryMethod("clear") }, undo: function() { this._applyQuillHistoryMethod("undo") }, redo: function() { this._applyQuillHistoryMethod("redo") }, getLength: function() { return this._applyQuillMethod("getLength") }, getBounds: function(index, length) { return this._applyQuillMethod("getBounds", arguments) }, delete: function(index, length) { this._applyQuillMethod("deleteText", arguments) }, insertText: function(index, text, formats) { this._applyQuillMethod("insertText", arguments) }, insertEmbed: function(index, type, config) { this._applyQuillMethod("insertEmbed", arguments) }, showFormDialog: function(formConfig) { return this._formDialog.show(formConfig) }, formDialogOption: function(optionName, optionValue) { return this._formDialog.popupOption.apply(this._formDialog, arguments) }, focus: function() { this.callBase(); this._applyQuillMethod("focus") }, blur: function() { this._applyQuillMethod("blur") } }); registerComponent("dxHtmlEditor", HtmlEditor); export default HtmlEditor;