UNPKG

@ckeditor/ckeditor5-style

Version:

Style feature for CKEditor 5.

209 lines (208 loc) • 9.13 kB
/** * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options */ import { Command } from 'ckeditor5/src/core.js'; import { logWarning, first } from 'ckeditor5/src/utils.js'; import StyleUtils from './styleutils.js'; /** * Style command. * * Applies and removes styles from selection and elements. */ export default class StyleCommand extends Command { /** * Normalized definitions of the styles. */ _styleDefinitions; /** * The StyleUtils plugin. */ _styleUtils; /** * Creates an instance of the command. * * @param editor Editor on which this command will be used. * @param styleDefinitions Normalized definitions of the styles. */ constructor(editor, styleDefinitions) { super(editor); this.set('value', []); this.set('enabledStyles', []); this._styleDefinitions = styleDefinitions; this._styleUtils = this.editor.plugins.get(StyleUtils); } /** * @inheritDoc */ refresh() { const model = this.editor.model; const selection = model.document.selection; const value = new Set(); const enabledStyles = new Set(); // Inline styles. for (const definition of this._styleDefinitions.inline) { // Check if this inline style is enabled. if (this._styleUtils.isStyleEnabledForInlineSelection(definition, selection)) { enabledStyles.add(definition.name); } // Check if this inline style is active. if (this._styleUtils.isStyleActiveForInlineSelection(definition, selection)) { value.add(definition.name); } } // Block styles. const firstBlock = first(selection.getSelectedBlocks()) || selection.getFirstPosition().parent; if (firstBlock) { const ancestorBlocks = firstBlock.getAncestors({ includeSelf: true, parentFirst: true }); for (const block of ancestorBlocks) { if (block.is('rootElement')) { break; } for (const definition of this._styleDefinitions.block) { // Check if this block style is enabled. if (!this._styleUtils.isStyleEnabledForBlock(definition, block)) { continue; } enabledStyles.add(definition.name); // Check if this block style is active. if (this._styleUtils.isStyleActiveForBlock(definition, block)) { value.add(definition.name); } } // E.g. reached a model table when the selection is in a cell. The command should not modify // ancestors of a table. if (model.schema.isObject(block)) { break; } } } this.enabledStyles = Array.from(enabledStyles).sort(); this.isEnabled = this.enabledStyles.length > 0; this.value = this.isEnabled ? Array.from(value).sort() : []; } /** * Executes the command – applies the style classes to the selection or removes it from the selection. * * If the command value already contains the requested style, it will remove the style classes. Otherwise, it will set it. * * The execution result differs, depending on the {@link module:engine/model/document~Document#selection} and the * style type (inline or block): * * * When applying inline styles: * * If the selection is on a range, the command applies the style classes to all nodes in that range. * * If the selection is collapsed in a non-empty node, the command applies the style classes to the * {@link module:engine/model/document~Document#selection}. * * * When applying block styles: * * If the selection is on a range, the command applies the style classes to the nearest block parent element. * * @fires execute * @param options Command options. * @param options.styleName Style name matching the one defined in the * {@link module:style/styleconfig~StyleConfig#definitions configuration}. * @param options.forceValue Whether the command should add given style (`true`) or remove it (`false`) from the selection. * If not set (default), the command will toggle the style basing on the first selected node. Note, that this will not force * setting a style on an element that cannot receive given style. */ execute({ styleName, forceValue }) { if (!this.enabledStyles.includes(styleName)) { /** * Style command can be executed only with a correct style name. * * This warning may be caused by: * * * passing a name that is not specified in the {@link module:style/styleconfig~StyleConfig#definitions configuration} * (e.g. a CSS class name), * * when trying to apply a style that is not allowed on a given element. * * @error style-command-executed-with-incorrect-style-name */ logWarning('style-command-executed-with-incorrect-style-name'); return; } const model = this.editor.model; const selection = model.document.selection; const htmlSupport = this.editor.plugins.get('GeneralHtmlSupport'); const allDefinitions = [ ...this._styleDefinitions.inline, ...this._styleDefinitions.block ]; const activeDefinitions = allDefinitions.filter(({ name }) => this.value.includes(name)); const definition = allDefinitions.find(({ name }) => name == styleName); const shouldAddStyle = forceValue === undefined ? !this.value.includes(definition.name) : forceValue; model.change(() => { let selectables; if (isBlockStyleDefinition(definition)) { selectables = this._findAffectedBlocks(getBlocksFromSelection(selection), definition); } else { selectables = [this._styleUtils.getAffectedInlineSelectable(definition, selection)]; } for (const selectable of selectables) { if (shouldAddStyle) { htmlSupport.addModelHtmlClass(definition.element, definition.classes, selectable); } else { htmlSupport.removeModelHtmlClass(definition.element, getDefinitionExclusiveClasses(activeDefinitions, definition), selectable); } } }); } /** * Returns a set of elements that should be affected by the block-style change. */ _findAffectedBlocks(selectedBlocks, definition) { const blocks = new Set(); for (const selectedBlock of selectedBlocks) { const ancestorBlocks = selectedBlock.getAncestors({ includeSelf: true, parentFirst: true }); for (const block of ancestorBlocks) { if (block.is('rootElement')) { break; } const affectedBlocks = this._styleUtils.getAffectedBlocks(definition, block); if (affectedBlocks) { for (const affectedBlock of affectedBlocks) { blocks.add(affectedBlock); } break; } } } return blocks; } } /** * Returns classes that are defined only in the supplied definition and not in any other active definition. It's used * to ensure that classes used by other definitions are preserved when a style is removed. See #11748. * * @param activeDefinitions All currently active definitions affecting selected element(s). * @param definition Definition whose classes will be compared with all other active definition classes. * @returns Array of classes exclusive to the supplied definition. */ function getDefinitionExclusiveClasses(activeDefinitions, definition) { return activeDefinitions.reduce((classes, currentDefinition) => { if (currentDefinition.name === definition.name) { return classes; } return classes.filter(className => !currentDefinition.classes.includes(className)); }, definition.classes); } /** * Checks if provided style definition is of type block. */ function isBlockStyleDefinition(definition) { return 'isBlock' in definition; } /** * Gets block elements from selection. If there are none, returns first selected element. * @param selection Current document's selection. * @returns Selected blocks if there are any, first selected element otherwise. */ function getBlocksFromSelection(selection) { const blocks = Array.from(selection.getSelectedBlocks()); if (blocks.length) { return blocks; } return [selection.getFirstPosition().parent]; }