UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

642 lines (640 loc) • 27.9 kB
/** * DevExtreme (cjs/__internal/ui/html_editor/modules/m_toolbar.js) * Version: 25.1.3 * Build date: Wed Jun 25 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/select_box"); require("../../../ui/color_box/m_color_view"); require("../../../../ui/number_box"); require("../../../../ui/menu"); var _events_engine = _interopRequireDefault(require("../../../../common/core/events/core/events_engine")); var _index = require("../../../../common/core/events/utils/index"); var _message = _interopRequireDefault(require("../../../../common/core/localization/message")); var _renderer = _interopRequireDefault(require("../../../../core/renderer")); var _extend = require("../../../../core/utils/extend"); var _inflector = require("../../../../core/utils/inflector"); var _iterator = require("../../../../core/utils/iterator"); var _type = require("../../../../core/utils/type"); var _toolbar = _interopRequireDefault(require("../../../../ui/toolbar")); var _ui = _interopRequireDefault(require("../../../../ui/widget/ui.errors")); var _capitalize = require("../../../core/utils/capitalize"); var _m_menu = require("../../../ui/menu/m_menu"); var _devextremeQuill = _interopRequireDefault(require("devextreme-quill")); var _ai = require("../utils/ai"); var _m_table_helper = require("../utils/m_table_helper"); var _m_toolbar_helper = require("../utils/m_toolbar_helper"); var _m_base = _interopRequireDefault(require("./m_base")); var _m_widget_collector = _interopRequireDefault(require("./m_widget_collector")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e } } let ToolbarModule = _m_base.default; if (_devextremeQuill.default) { const TOOLBAR_WRAPPER_CLASS = "dx-htmleditor-toolbar-wrapper"; const TOOLBAR_CLASS = "dx-htmleditor-toolbar"; const TOOLBAR_FORMAT_WIDGET_CLASS = "dx-htmleditor-toolbar-format"; const TOOLBAR_SEPARATOR_CLASS = "dx-htmleditor-toolbar-separator"; const TOOLBAR_MENU_SEPARATOR_CLASS = "dx-htmleditor-toolbar-menu-separator"; const ACTIVE_FORMAT_CLASS = "dx-format-active"; const SELECTED_STATE_CLASS = "dx-state-selected"; const ICON_CLASS = "dx-icon"; const SELECTION_CHANGE_EVENT = "selection-change"; const USER_ACTION = "user"; const SILENT_ACTION = "silent"; const FORMAT_HOTKEYS = { 66: "bold", 73: "italic", 85: "underline" }; const KEY_CODES = { b: 66, i: 73, u: 85 }; const TOOLBAR_AI_ITEM_NAME = "ai"; const localize = name => _message.default.format(`dxHtmlEditor-${(0,_inflector.camelize)(name)}`); const localizeValue = (value, name) => { if ("header" === name) { const isHeaderValue = (0, _type.isDefined)(value) && false !== value; return isHeaderValue ? `${localize("heading")} ${value}` : localize("normalText") } return localize(value) || value }; ToolbarModule = class extends _m_base.default { constructor(quill, options) { var _this; super(quill, options); _this = this; this._toolbarWidgets = new _m_widget_collector.default; this._formatHandlers = (0, _m_toolbar_helper.getFormatHandlers)(this); this._tableFormats = (0, _m_table_helper.getTableFormats)(quill); if ((0, _type.isDefined)(options.items)) { this._addCallbacks(); this._renderToolbar(); const toolbarMenu = this.toolbarInstance._layoutStrategy._menu; if (toolbarMenu) { const { _renderPopup: _renderPopup } = toolbarMenu; toolbarMenu._renderPopup = function() { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key] } _renderPopup.apply(toolbarMenu, ...args); toolbarMenu._popup.on("showing", (() => { _this._updateToolbar(true) })) } } this.quill.on("editor-change", ((eventName, newValue, oldValue, eventSource) => { const isSilentMode = eventSource === SILENT_ACTION && (0, _type.isEmptyObject)(this.quill.getFormat()); if (!isSilentMode) { const isSelectionChanged = eventName === SELECTION_CHANGE_EVENT; this._updateToolbar(isSelectionChanged) } })) } } _addCallbacks() { this.addCleanCallback(this.clean.bind(this)); this.editorInstance.addContentInitializedCallback(this.updateHistoryWidgets.bind(this)) } _updateToolbar(isSelectionChanged) { this.updateFormatWidgets(isSelectionChanged); this.updateHistoryWidgets(); this.updateTableWidgets() } _updateFormatWidget(name, isApplied, formats) { const widget = this._toolbarWidgets.getByName(name); if (!widget) { return } if (isApplied) { this._markActiveFormatWidget(name, widget, formats) } else { this._resetFormatWidget(name, widget); if (Object.prototype.hasOwnProperty.call(name)) { delete formats[name] } } this._toggleClearFormatting(isApplied || !(0, _type.isEmptyObject)(formats)) } _renderToolbar() { const container = this.options.container || this._getContainer(); this._$toolbar = (0, _renderer.default)("<div>").addClass(TOOLBAR_CLASS).appendTo(container); this._$toolbarContainer = (0, _renderer.default)(container).addClass(TOOLBAR_WRAPPER_CLASS); _events_engine.default.on(this._$toolbarContainer, (0, _index.addNamespace)("mousedown", this.editorInstance.NAME), (e => { e.target.focus(); e.preventDefault() })); this._subscribeFormatHotKeys(); this.toolbarInstance = this.editorInstance._createComponent(this._$toolbar, _toolbar.default, this.toolbarConfig); this.editorInstance.on("optionChanged", (_ref => { let { name: name } = _ref; if ("readOnly" === name || "disabled" === name) { this.toolbarInstance.option("disabled", this.isInteractionDisabled) } })) } get toolbarConfig() { return { dataSource: this._prepareToolbarItems(), disabled: this.isInteractionDisabled, menuContainer: this._$toolbarContainer, multiline: this.isMultilineMode() } } get isInteractionDisabled() { return this.editorInstance.option("readOnly") || this.editorInstance.option("disabled") } isMultilineMode() { return this.options.multiline ?? true } clean() { this._toolbarWidgets.clear(); if (this._$toolbarContainer) { this._$toolbarContainer.empty().removeClass(TOOLBAR_WRAPPER_CLASS) } } repaint() { this.toolbarInstance && this.toolbarInstance.repaint() } _getContainer() { const $container = (0, _renderer.default)("<div>"); this.editorInstance.$element().prepend($container); return $container } _subscribeFormatHotKeys() { this.quill.keyboard.addBinding({ which: KEY_CODES.b, shortKey: true }, this._handleFormatHotKey.bind(this)); this.quill.keyboard.addBinding({ which: KEY_CODES.i, shortKey: true }, this._handleFormatHotKey.bind(this)); this.quill.keyboard.addBinding({ which: KEY_CODES.u, shortKey: true }, this._handleFormatHotKey.bind(this)) } _handleFormatHotKey(range, context, _ref2) { let { which: which } = _ref2; const formatName = FORMAT_HOTKEYS[which]; this._updateButtonState(formatName) } _updateButtonState(formatName) { const formatWidget = this._toolbarWidgets.getByName(formatName); const currentFormat = this.quill.getFormat(); const formatValue = currentFormat[formatName]; if (formatValue) { this._markActiveFormatWidget(formatName, formatWidget, currentFormat) } else { this._resetFormatWidget(formatName, formatWidget) } } _prepareToolbarItems() { const resultItems = []; (0, _iterator.each)(this.options.items, ((index, item) => { let newItem; if ((0, _type.isObject)(item)) { newItem = this._handleObjectItem(item) } else if (item === TOOLBAR_AI_ITEM_NAME) { resultItems.push(this._getToolbarItem(this._prepareAIMenuItemConfig(item))) } else if ((0, _type.isString)(item)) { const buttonItemConfig = this._prepareButtonItemConfig(item); newItem = this._getToolbarItem(buttonItemConfig) } if (newItem) { resultItems.push(newItem) } })); return resultItems } _handleObjectItem(item) { if (item.name === TOOLBAR_AI_ITEM_NAME) { return this._getToolbarItem(this._prepareAIMenuItemConfig(item)) } if (item.name && item.acceptedValues && this._isAcceptableItem(item.widget, "dxSelectBox")) { const selectItemConfig = this._prepareSelectItemConfig(item); return this._getToolbarItem(selectItemConfig) } if (item.name && this._isAcceptableItem(item.widget, "dxButton")) { const defaultButtonItemConfig = this._prepareButtonItemConfig(item.name); const buttonItemConfig = (0, _extend.extend)(true, defaultButtonItemConfig, item); return this._getToolbarItem(buttonItemConfig) } return this._getToolbarItem(item) } _isAcceptableItem(widget, acceptableWidgetName) { return !widget || widget === acceptableWidgetName } _prepareButtonItemConfig(name) { const iconName = _m_toolbar_helper.ICON_MAP[name] ?? name; const buttonText = (0, _inflector.titleize)(name); return { widget: "dxButton", name: name, options: { hint: localize(buttonText), text: localize(buttonText), icon: iconName.toLowerCase(), onClick: this._formatHandlers[name] || (0, _m_toolbar_helper.getDefaultClickHandler)(this, name), stylingMode: "text" }, showText: "inMenu" } } _prepareSelectItemConfig(item) { const { name: name, acceptedValues: acceptedValues } = item; return (0, _extend.extend)(true, { widget: "dxSelectBox", name: name, options: { stylingMode: "filled", dataSource: acceptedValues, displayExpr: value => localizeValue(value, name), placeholder: localize(name), onValueChanged: e => { if (!this._isReset) { this._hideAdaptiveMenu(); (0, _m_toolbar_helper.applyFormat)(this, [name, e.value, USER_ACTION], e.event); this._setValueSilent(e.component, e.value) } } } }, item) } _createCommandMenuItem(command, text, commandOptions) { var _getDefaultOptionsByC; const options = (null === commandOptions || void 0 === commandOptions ? void 0 : commandOptions.map(_capitalize.capitalize)) ?? (null === (_getDefaultOptionsByC = (0, _ai.getDefaultOptionsByCommand)(command)) || void 0 === _getDefaultOptionsByC ? void 0 : _getDefaultOptionsByC.map(_capitalize.capitalize)); const item = { id: command, name: command, text: text ?? _ai.defaultCommandNames[command], items: null === options || void 0 === options ? void 0 : options.map((option => ({ id: option, text: option, parentCommand: command, options: null === options || void 0 === options ? void 0 : options.map(_capitalize.capitalize) }))) }; return item } _buildMenuItems(commands) { let customCommandIndex = 0; const items = null === commands || void 0 === commands ? void 0 : commands.map((command => { if ("object" === typeof command) { if ("custom" === command.name) { var _command$options; const id = `custom${customCommandIndex}`; const { prompt: prompt, options: options } = command; const capitalized = null === options || void 0 === options ? void 0 : options.map(_capitalize.capitalize); const item = { id: id, name: "custom", text: command.text, items: null === (_command$options = command.options) || void 0 === _command$options ? void 0 : _command$options.map((rawOptionName => { const option = (0, _capitalize.capitalize)(rawOptionName); const result = { parentCommand: id, id: option, text: option, options: capitalized, prompt: prompt }; return result })), disabled: !prompt, prompt: prompt }; customCommandIndex += 1; return item } return this._createCommandMenuItem(command.name, command.text, command.options) } return this._createCommandMenuItem(command) })); return items } _validateAIToolbarItemConfig(commandsMap) { const { aiIntegration: aiIntegration } = this.editorInstance.option(); if (!aiIntegration) { _ui.default.log("W1026") } if ((0, _ai.hasInvalidCustomCommand)(commandsMap)) { _ui.default.log("W1027") } } _prepareAIMenuItemConfig(item) { var _dataSource$0$items; const { name: name = TOOLBAR_AI_ITEM_NAME, commands: commands = Object.keys(_ai.defaultCommandNames) } = item; const commandsMap = (0, _ai.buildCommandsMap)(commands); const menuItems = this._buildMenuItems(commands); this._validateAIToolbarItemConfig(commandsMap); const dataSource = [{ id: "root", icon: "sparkle", items: menuItems }]; const { aiIntegration: aiIntegration } = this.editorInstance.option(); const isMenuDisabled = !(null !== (_dataSource$0$items = dataSource[0].items) && void 0 !== _dataSource$0$items && _dataSource$0$items.length) || !aiIntegration; const options = { dataSource: dataSource, disabled: isMenuDisabled, onContentReady: e => { const $item = (0, _renderer.default)(e.element).find(`.${_m_menu.DX_MENU_ITEM_CLASS}`).first(); $item.attr("aria-label", _message.default.format("dxHtmlEditor-aiToolbarItemAriaLabel")) }, onItemClick: e => { var _itemData$items; const { itemData: itemData } = e; if (!itemData || null !== (_itemData$items = itemData.items) && void 0 !== _itemData$items && _itemData$items.length) { return } const aiDialogOptions = { command: itemData.id, parentCommand: itemData.parentCommand, commandsMap: commandsMap, prompt: itemData.prompt }; this._formatHandlers[name](aiDialogOptions) } }; return (0, _extend.extend)(true, { widget: "dxMenu", name: name, options: options }, "string" === typeof item ? {} : item) } _hideAdaptiveMenu() { if (this.toolbarInstance.option("overflowMenuVisible")) { this.toolbarInstance.option("overflowMenuVisible", false) } } _getToolbarItem(item) { const baseItem = { options: { onInitialized: e => { if (item.name) { e.component.$element().addClass(TOOLBAR_FORMAT_WIDGET_CLASS); e.component.$element().toggleClass(`dx-${item.name.toLowerCase()}-format`, !!item.name); this._toolbarWidgets.add(item.name, e.component) } }, onDisposing: () => { this._toolbarWidgets.remove(item.name) } } }; return (0, _extend.extend)(true, { location: "before", locateInMenu: "auto" }, this._getDefaultConfig(item.name), item, baseItem) } _getDefaultItemsConfig() { return { clear: { options: { disabled: true } }, undo: { options: { disabled: true } }, redo: { options: { disabled: true } }, insertRowAbove: { options: { disabled: true } }, insertRowBelow: { options: { disabled: true } }, insertHeaderRow: { options: { disabled: true } }, insertColumnLeft: { options: { disabled: true } }, insertColumnRight: { options: { disabled: true } }, deleteRow: { options: { disabled: true } }, deleteColumn: { options: { disabled: true } }, deleteTable: { options: { disabled: true } }, cellProperties: { options: { disabled: true } }, tableProperties: { options: { disabled: true } }, separator: { template: (data, index, element) => { (0, _renderer.default)(element).addClass(TOOLBAR_SEPARATOR_CLASS) }, menuItemTemplate: (data, index, element) => { (0, _renderer.default)(element).addClass(TOOLBAR_MENU_SEPARATOR_CLASS) } } } } _getDefaultConfig(name) { return this._getDefaultItemsConfig()[name] } updateHistoryWidgets() { const historyModule = this.quill.history; if (!historyModule) { return } const { undo: undoOps, redo: redoOps } = historyModule.stack; this._updateManipulationWidget(this._toolbarWidgets.getByName("undo"), Boolean(undoOps.length)); this._updateManipulationWidget(this._toolbarWidgets.getByName("redo"), Boolean(redoOps.length)) } updateTableWidgets() { const table = this.quill.getModule("table"); if (!table) { return } const selection = this.quill.getSelection(); const formats = selection && this.quill.getFormat(selection) || {}; const isTableOperationsEnabled = this._tableFormats.some((format => Boolean(formats[format]))); _m_table_helper.TABLE_OPERATIONS.forEach((operationName => { const isInsertTable = "insertTable" === operationName; const widget = this._toolbarWidgets.getByName(operationName); this._updateManipulationWidget(widget, isInsertTable ? !isTableOperationsEnabled : isTableOperationsEnabled) })) } _updateManipulationWidget(widget, isOperationEnabled) { if (!widget) { return } widget.option("disabled", !isOperationEnabled) } updateFormatWidgets(isResetRequired) { const selection = this.quill.getSelection(); if (!selection) { return } const formats = this.quill.getFormat(selection); const hasFormats = !(0, _type.isEmptyObject)(formats); if (!hasFormats || isResetRequired) { this._resetFormatWidgets() } for (const formatName in formats) { const widgetName = this._getFormatWidgetName(formatName, formats); const formatWidget = this._toolbarWidgets.getByName(widgetName) ?? this._toolbarWidgets.getByName(formatName); if (!formatWidget) { continue } this._markActiveFormatWidget(formatName, formatWidget, formats) } this._toggleClearFormatting(hasFormats || selection.length > 1) } _markActiveFormatWidget(name, widget, formats) { if (this._isColorFormat(name)) { this._updateColorWidget(name, formats[name]) } if ("value" in widget.option()) { this._setValueSilent(widget, formats[name]) } else { widget.$element().addClass(ACTIVE_FORMAT_CLASS); widget.$element().addClass(SELECTED_STATE_CLASS); widget.$element().attr("aria-pressed", true) } } _toggleClearFormatting(hasFormats) { const clearWidget = this._toolbarWidgets.getByName("clear"); if (clearWidget) { clearWidget.option("disabled", !hasFormats) } } _isColorFormat(name) { return "color" === name || "background" === name } _updateColorWidget(name, color) { const formatWidget = this._toolbarWidgets.getByName(name); if (!formatWidget) { return } formatWidget.$element().find(`.${ICON_CLASS}`).css("borderBottomColor", color || "transparent") } _getFormatWidgetName(name, formats) { let widgetName; switch (name) { case "align": widgetName = name + (0, _inflector.titleize)(formats[name]); break; case "list": widgetName = formats[name] + (0, _inflector.titleize)(name); break; case "code-block": widgetName = "codeBlock"; break; case "script": widgetName = formats[name] + name; break; case "imageSrc": widgetName = "image"; break; default: widgetName = name } return widgetName } _setValueSilent(widget, value) { this._isReset = true; widget.option("value", value); this._isReset = false } _resetFormatWidgets() { this._toolbarWidgets.each(((name, widget) => { this._resetFormatWidget(name, widget) })) } _resetFormatWidget(name, widget) { widget.$element().removeClass(ACTIVE_FORMAT_CLASS); widget.$element().removeClass(SELECTED_STATE_CLASS); widget.$element().removeAttr("aria-pressed"); if (this._isColorFormat(name)) { this._updateColorWidget(name) } if ("clear" === name) { widget.option("disabled", true) } if ("dxSelectBox" === widget.NAME) { this._setValueSilent(widget, null) } } addClickHandler(name, handler) { this._formatHandlers[name] = handler; const formatWidget = this._toolbarWidgets.getByName(name); if (formatWidget && "dxButton" === formatWidget.NAME) { formatWidget.option("onClick", handler) } } } } var _default = exports.default = ToolbarModule;