UNPKG

suneditor

Version:

Vanilla JavaScript based WYSIWYG web editor

205 lines (180 loc) 6.85 kB
import { dom, numbers } from '../../../helper'; /** * @description Service class managing the selection state and toolbar updates. * - Handles activating toolbar buttons based on the current selection. * - Manages the `active` state of plugins and commands. */ export default class SelectionState { #eventOrchestrator; #$; /** @type {RegExp} */ #onButtonsCheck; /** * @constructor * @param {import('../eventOrchestrator').default} eventOrchestrator */ constructor(eventOrchestrator) { this.#eventOrchestrator = eventOrchestrator; this.#$ = eventOrchestrator.$; this.#onButtonsCheck = new RegExp(`^(${Object.keys(this.#$.options.get('_defaultStyleTagMap')).join('|')})$`, 'i'); } /** * @description Updates the toolbar state based on the current selection. * - Traverses the DOM from the selection to the root. * - Checks for active tags and styles. * - Activates corresponding toolbar buttons. * @param {Node} [selectionNode] The node where the selection is currently located. * @returns {Node|undefined} The processed selection node. */ update(selectionNode) { selectionNode ||= this.#$.selection.getNode(); if (selectionNode === this.#$.store.get('_lastSelectionNode')) return; this.#$.store.set('_lastSelectionNode', selectionNode); const marginDir = this.#$.options.get('_rtl') ? 'marginRight' : 'marginLeft'; const plugins = this.#$.plugins; const commandTargets = this.#$.commandDispatcher.targets; const classOnCheck = this.#onButtonsCheck; const styleCommand = this.#$.options.get('_styleCommandMap'); const commandMapNodes = []; const currentNodes = []; const styleTags = this.#$.options.get('_textStyleTags'); const styleNodes = []; const ignoreCommands = []; const activeCommands = this.#$.commandDispatcher.activeCommands; const cLen = activeCommands.length; let nodeName = ''; if (this.#$.component.is(selectionNode) && !this.#$.component.__selectionSelected) { const component = this.#$.component.get(selectionNode); if (!component) return; this.#$.store.set('_lastSelectionNode', null); this.#$.component.select(component.target, component.pluginName); return; } while (selectionNode.firstChild) { selectionNode = selectionNode.firstChild; } const fc = this.#$.frameContext; const notReadonly = !fc.get('isReadOnly'); for (let element = selectionNode; !dom.check.isWysiwygFrame(element); element = element.parentElement) { if (!element) break; if (element.nodeType !== 1 || dom.check.isBreak(element)) continue; if (this.#isNonFocusNode(element)) { this.#$.focusManager.blur(); return; } nodeName = element.nodeName.toLowerCase(); currentNodes.push(nodeName); if (styleTags.includes(nodeName) && !this.#$.format.isLine(nodeName)) styleNodes.push(element); /* Active plugins */ if (notReadonly) { for (let c = 0, name; c < cLen; c++) { name = activeCommands[c]; if ( !commandMapNodes.includes(name) && !ignoreCommands.includes(name) && commandTargets.get(name) && commandTargets.get(name).filter((e) => { const r = plugins[name]?.active(element, e); if (r === undefined) { ignoreCommands.push(name); } return r; }).length > 0 ) { commandMapNodes.push(name); } } } /** indent, outdent */ if (this.#$.format.isLine(element)) { /* Outdent */ if (!commandMapNodes.includes('outdent') && commandTargets.has('outdent') && (dom.check.isListCell(element) || (element.style[marginDir] && numbers.get(element.style[marginDir], 0) > 0))) { if ( commandTargets.get('outdent').filter((e) => { if (dom.check.isImportantDisabled(e)) return false; e.disabled = false; return true; }).length > 0 ) { commandMapNodes.push('outdent'); } } /* Indent */ if (!commandMapNodes.includes('indent') && commandTargets.has('indent')) { const indentDisable = dom.check.isListCell(element) && !element.previousElementSibling; if ( commandTargets.get('indent').filter((e) => { if (dom.check.isImportantDisabled(e)) return false; e.disabled = indentDisable; return true; }).length > 0 ) { commandMapNodes.push('indent'); } } continue; } /** default active buttons [strong, ins, em, del, sub, sup] */ if (classOnCheck.test(nodeName)) { nodeName = styleCommand[nodeName] || nodeName; commandMapNodes.push(nodeName); dom.utils.addClass(commandTargets.get(nodeName), 'active'); } } this.#setKeyEffect(commandMapNodes); // cache style nodes this.#eventOrchestrator.__cacheStyleNodes = styleNodes.reverse(); /** save current nodes */ this.#$.store.set('currentNodes', currentNodes.reverse()); this.#$.store.set('currentNodesMap', commandMapNodes); /** Displays the current node structure to statusbar */ if (this.#$.frameOptions.get('statusbar_showPathLabel') && fc.get('navigation')) { fc.get('navigation').textContent = this.#$.options.get('_rtl') ? this.#$.store.get('currentNodes').reverse().join(' < ') : this.#$.store.get('currentNodes').join(' > '); } return selectionNode; } /** * @description Resets the toolbar state. * - Deactivates all buttons and clears the effect. * - Equivalent to calling `setKeyEffect([])`. */ reset() { this.#setKeyEffect([]); } /** * @description Internal logic to update the visual state of buttons. * - Checks the list of `active` commands and updates the DOM classes (`active`/inactive). * @param {Array<string>} ignoredList List of formatting commands to keep `active` (others will be deactivated). */ #setKeyEffect(ignoredList) { const activeCommands = this.#$.commandDispatcher.activeCommands; const commandTargets = this.#$.commandDispatcher.targets; const plugins = this.#$.plugins; for (let i = 0, len = activeCommands.length, k, c, p; i < len; i++) { k = activeCommands[i]; if (ignoredList.includes(k) || !(c = commandTargets.get(k))) continue; p = plugins[k]; for (let j = 0, jLen = c.length, e; j < jLen; j++) { e = c[j]; if (!e) continue; if (p) { p.active(null, e); } else if (/^outdent$/i.test(k)) { if (!dom.check.isImportantDisabled(e)) e.disabled = true; } else if (/^indent$/i.test(k)) { if (!dom.check.isImportantDisabled(e)) e.disabled = false; } else { dom.utils.removeClass(e, 'active'); } } } } /** * @description Checks if a node is a non-focusable element(`.data-se-non-focus`). (e.g. fileUpload.component > span) * @param {Node} node Node to check * @returns {boolean} `true` if the node is non-focusable, otherwise `false` */ #isNonFocusNode(node) { return dom.check.isElement(node) && node.getAttribute('data-se-non-focus') === 'true'; } }