UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

372 lines (371 loc) • 14.4 kB
/** * DevExtreme (esm/ui/html_editor/modules/mentions.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 Quill from "devextreme-quill"; import { compileGetter } from "../../../core/utils/data"; import { isString } from "../../../core/utils/type"; import { extend } from "../../../core/utils/extend"; import { getPublicElement } from "../../../core/element"; import eventsEngine from "../../../events/core/events_engine"; import BaseModule from "./base"; import PopupModule from "./popup"; import Mention from "../formats/mention"; var MentionModule = BaseModule; if (Quill) { var USER_ACTION = "user"; var SILENT_ACTION = "silent"; var DEFAULT_MARKER = "@"; var KEYS = { ARROW_UP: "upArrow", ARROW_DOWN: "downArrow", ARROW_LEFT: "leftArrow", ARROW_RIGHT: "rightArrow", ENTER: "enter", ESCAPE: "escape", SPACE: "space", PAGE_UP: "pageUp", PAGE_DOWN: "pageDown", END: "end", HOME: "home" }; var NAVIGATION_KEYS = [KEYS.ARROW_LEFT, KEYS.ARROW_RIGHT, KEYS.PAGE_UP, KEYS.PAGE_DOWN, KEYS.END, KEYS.HOME]; var ALLOWED_PREFIX_CHARS = [" ", "\n"]; var DISABLED_STATE_CLASS = "dx-state-disabled"; Quill.register({ "formats/mention": Mention }, true); MentionModule = class extends PopupModule { _getDefaultOptions() { var baseConfig = super._getDefaultOptions(); return extend(baseConfig, { itemTemplate: "item", valueExpr: "this", displayExpr: "this", template: null, searchExpr: null, searchTimeout: 500, minSearchLength: 0 }) } constructor(quill, options) { super(quill, options); this._mentions = {}; options.mentions.forEach(item => { var marker = item.marker; if (!marker) { item.marker = marker = DEFAULT_MARKER } var template = item.template; if (template) { var preparedTemplate = this.editorInstance._getTemplate(template); preparedTemplate && Mention.addTemplate(marker, preparedTemplate) } this._mentions[marker] = extend({}, this._getDefaultOptions(), item) }); this._attachKeyboardHandlers(); this.addCleanCallback(this.clean.bind(this)); this.quill.on("text-change", this.onTextChange.bind(this)) } _attachKeyboardHandlers() { this.quill.keyboard.addBinding({ key: KEYS.ARROW_UP }, this._moveToItem.bind(this, "prev")); this.quill.keyboard.addBinding({ key: KEYS.ARROW_DOWN }, this._moveToItem.bind(this, "next")); this.quill.keyboard.addBinding({ key: [KEYS.ENTER, KEYS.SPACE] }, this._selectItemHandler.bind(this)); var enterBindings = this.quill.keyboard.bindings[KEYS.ENTER]; enterBindings.unshift(enterBindings.pop()); this.quill.keyboard.addBinding({ key: KEYS.ESCAPE }, this._escapeKeyHandler.bind(this)); this.quill.keyboard.addBinding({ key: [KEYS.ARROW_LEFT, KEYS.ARROW_RIGHT], shiftKey: true }, this._ignoreKeyHandler.bind(this)); this.quill.keyboard.addBinding({ key: NAVIGATION_KEYS }, this._ignoreKeyHandler.bind(this)) } _moveToItem(direction) { var dataSource = this._list.getDataSource(); if (this._isMentionActive && !dataSource.isLoading()) { var $focusedItem = $(this._list.option("focusedElement")); var defaultItemPosition = "next" === direction ? "first" : "last"; var $nextItem = $focusedItem[direction](); $nextItem = $nextItem.length ? $nextItem : this._activeListItems[defaultItemPosition](); this._list.option("focusedElement", getPublicElement($nextItem)); this._list.scrollToItem($nextItem) } return !this._isMentionActive } _ignoreKeyHandler() { return !this._isMentionActive } _fitIntoRange(value, start, end) { if (value > end) { return start } if (value < start) { return end } return value } _selectItemHandler() { if (this._isMentionActive) { this._list.selectItem(this._list.option("focusedElement")) } return !this._isMentionActive } _escapeKeyHandler() { if (this._isMentionActive) { this._popup.hide() } return !this._isMentionActive } renderList($container, options) { this.compileGetters(this.options); super.renderList($container, options) } compileGetters(_ref) { var { displayExpr: displayExpr, valueExpr: valueExpr } = _ref; this._valueGetter = compileGetter(displayExpr); this._idGetter = compileGetter(valueExpr) } _getListConfig(options) { var baseConfig = super._getListConfig(options); return extend(baseConfig, { itemTemplate: this.options.itemTemplate, onContentReady: () => { if (this._hasSearch) { this._popup.repaint(); this._focusFirstElement(); this._hasSearch = false } } }) } insertEmbedContent() { var markerLength = this._activeMentionConfig.marker.length; var textLength = markerLength + this._searchValue.length; var caretPosition = this.getPosition(); var startIndex = Math.max(0, caretPosition - markerLength); var selectedItem = this._list.option("selectedItem"); var value = { value: this._valueGetter(selectedItem), id: this._idGetter(selectedItem), marker: this._activeMentionConfig.marker }; setTimeout(function() { this.quill.insertText(startIndex, " ", SILENT_ACTION); this.quill.deleteText(startIndex + 1, textLength, SILENT_ACTION); this.quill.insertEmbed(startIndex, "mention", value); this.quill.setSelection(startIndex + 2) }.bind(this)) } _getLastInsertOperation(ops) { var lastOperation = ops[ops.length - 1]; var isLastOperationInsert = "insert" in lastOperation; if (isLastOperationInsert) { return lastOperation } var isLastOperationDelete = "delete" in lastOperation; if (isLastOperationDelete && ops.length >= 2) { var penultOperation = ops[ops.length - 2]; var isPenultOperationInsert = "insert" in penultOperation; var isSelectionReplacing = isLastOperationDelete && isPenultOperationInsert; if (isSelectionReplacing) { return penultOperation } } return null } onTextChange(newDelta, oldDelta, source) { if (source === USER_ACTION) { var lastOperation = newDelta.ops[newDelta.ops.length - 1]; if (this._isMentionActive && this._isPopupVisible) { this._processSearchValue(lastOperation) && this._filterList(this._searchValue) } else { var { ops: ops } = newDelta; var lastInsertOperation = this._getLastInsertOperation(ops); if (lastInsertOperation) { this.checkMentionRequest(lastInsertOperation, ops) } } } } get _isPopupVisible() { var _this$_popup; return null === (_this$_popup = this._popup) || void 0 === _this$_popup ? void 0 : _this$_popup.option("visible") } _processSearchValue(operation) { var isInsertOperation = "insert" in operation; if (isInsertOperation) { this._searchValue += operation.insert } else if (!this._searchValue.length || operation.delete > 1) { this._popup.hide(); return false } else { this._searchValue = this._searchValue.slice(0, -1) } return true } checkMentionRequest(_ref2, ops) { var { insert: insert } = _ref2; var caret = this.quill.getSelection(); if (!insert || !isString(insert) || !caret || this._isMarkerPartOfText(ops[0].retain)) { return } this._activeMentionConfig = this._mentions[insert]; if (this._activeMentionConfig) { this._updateList(this._activeMentionConfig); this.savePosition(caret.index); this._popup.option("position", this._popupPosition); this._searchValue = ""; this._popup.show() } } _isMarkerPartOfText(retain) { if (!retain || -1 !== ALLOWED_PREFIX_CHARS.indexOf(this._getCharByIndex(retain - 1))) { return false } return true } _getCharByIndex(index) { return this.quill.getContents(index, 1).ops[0].insert } _updateList(_ref3) { var { dataSource: dataSource, displayExpr: displayExpr, valueExpr: valueExpr, itemTemplate: itemTemplate, searchExpr: searchExpr } = _ref3; this.compileGetters({ displayExpr: displayExpr, valueExpr: valueExpr }); this._list.unselectAll(); this._list.option({ dataSource: dataSource, displayExpr: displayExpr, itemTemplate: itemTemplate, searchExpr: searchExpr }) } _filterList(searchValue) { if (!this._isMinSearchLengthExceeded(searchValue)) { this._resetFilter(); return } var searchTimeout = this._activeMentionConfig.searchTimeout; if (searchTimeout) { clearTimeout(this._searchTimer); this._searchTimer = setTimeout(() => { this._search(searchValue) }, searchTimeout) } else { this._search(searchValue) } } _isMinSearchLengthExceeded(searchValue) { return searchValue.length >= this._activeMentionConfig.minSearchLength } _resetFilter() { clearTimeout(this._searchTimer); this._search(null) } _search(searchValue) { this._hasSearch = true; this._list.option("searchValue", searchValue) } _focusFirstElement() { if (!this._list) { return } var $firstItem = this._activeListItems.first(); this._list.option("focusedElement", getPublicElement($firstItem)); this._list.scrollToItem($firstItem) } get _popupPosition() { var position = this.getPosition(); var { left: mentionLeft, top: mentionTop, height: mentionHeight } = this.quill.getBounds(position ? position - 1 : position); var { left: leftOffset, top: topOffset } = $(this.quill.root).offset(); var positionEvent = eventsEngine.Event("positionEvent", { pageX: leftOffset + mentionLeft, pageY: topOffset + mentionTop }); return { of: positionEvent, offset: { v: mentionHeight }, my: "top left", at: "top left", collision: { y: "flip", x: "flipfit" } } } _getPopupConfig() { return extend(super._getPopupConfig(), { closeOnTargetScroll: false, onShown: () => { this._isMentionActive = true; this._hasSearch = false; this._focusFirstElement() }, onHidden: () => { this._list.unselectAll(); this._list.option("focusedElement", null); this._isMentionActive = false; this._search(null) }, focusStateEnabled: false }) } get _activeListItems() { return this._list.itemElements().filter(":not(.".concat(DISABLED_STATE_CLASS, ")")) } clean() { Object.keys(this._mentions).forEach(marker => { if (this._mentions[marker].template) { Mention.removeTemplate(marker) } }) } } } export default MentionModule;