UNPKG

@ckeditor/ckeditor5-list

Version:

Ordered and unordered lists feature to CKEditor 5.

363 lines (362 loc) 16.9 kB
/** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @module list/listproperties/listpropertiesui */ import { icons, Plugin } from 'ckeditor5/src/core.js'; import { ButtonView, SplitButtonView, createDropdown, focusChildOnDropdownOpen, MenuBarMenuView } from 'ckeditor5/src/ui.js'; import ListPropertiesView from './ui/listpropertiesview.js'; import listStyleDiscIcon from '../../theme/icons/liststyledisc.svg'; import listStyleCircleIcon from '../../theme/icons/liststylecircle.svg'; import listStyleSquareIcon from '../../theme/icons/liststylesquare.svg'; import listStyleDecimalIcon from '../../theme/icons/liststyledecimal.svg'; import listStyleDecimalWithLeadingZeroIcon from '../../theme/icons/liststyledecimalleadingzero.svg'; import listStyleLowerRomanIcon from '../../theme/icons/liststylelowerroman.svg'; import listStyleUpperRomanIcon from '../../theme/icons/liststyleupperroman.svg'; import listStyleLowerLatinIcon from '../../theme/icons/liststylelowerlatin.svg'; import listStyleUpperLatinIcon from '../../theme/icons/liststyleupperlatin.svg'; import '../../theme/liststyles.css'; /** * The list properties UI plugin. It introduces the extended `'bulletedList'` and `'numberedList'` toolbar * buttons that allow users to control such aspects of list as the marker, start index or order. * * **Note**: Buttons introduced by this plugin override implementations from the {@link module:list/list/listui~ListUI} * (because they share the same names). */ export default class ListPropertiesUI extends Plugin { /** * @inheritDoc */ static get pluginName() { return 'ListPropertiesUI'; } init() { const editor = this.editor; const t = editor.locale.t; const propertiesConfig = editor.config.get('list.properties'); // Note: When this plugin does not register the "bulletedList" dropdown due to properties configuration, // a simple button will be still registered under the same name by ListUI as a fallback. This should happen // in most editor configuration because the List plugin automatically requires ListUI. if (propertiesConfig.styles) { const styleDefinitions = [ { label: t('Toggle the disc list style'), tooltip: t('Disc'), type: 'disc', icon: listStyleDiscIcon }, { label: t('Toggle the circle list style'), tooltip: t('Circle'), type: 'circle', icon: listStyleCircleIcon }, { label: t('Toggle the square list style'), tooltip: t('Square'), type: 'square', icon: listStyleSquareIcon } ]; const buttonLabel = t('Bulleted List'); const styleGridAriaLabel = t('Bulleted list styles toolbar'); const commandName = 'bulletedList'; editor.ui.componentFactory.add(commandName, getDropdownViewCreator({ editor, propertiesConfig, parentCommandName: commandName, buttonLabel, buttonIcon: icons.bulletedList, styleGridAriaLabel, styleDefinitions })); // Add the menu bar item for bulleted list. editor.ui.componentFactory.add(`menuBar:${commandName}`, getMenuBarStylesMenuCreator({ editor, propertiesConfig, parentCommandName: commandName, buttonLabel, styleGridAriaLabel, styleDefinitions })); } // Note: When this plugin does not register the "numberedList" dropdown due to properties configuration, // a simple button will be still registered under the same name by ListUI as a fallback. This should happen // in most editor configuration because the List plugin automatically requires ListUI. if (propertiesConfig.styles || propertiesConfig.startIndex || propertiesConfig.reversed) { const styleDefinitions = [ { label: t('Toggle the decimal list style'), tooltip: t('Decimal'), type: 'decimal', icon: listStyleDecimalIcon }, { label: t('Toggle the decimal with leading zero list style'), tooltip: t('Decimal with leading zero'), type: 'decimal-leading-zero', icon: listStyleDecimalWithLeadingZeroIcon }, { label: t('Toggle the lower–roman list style'), tooltip: t('Lower–roman'), type: 'lower-roman', icon: listStyleLowerRomanIcon }, { label: t('Toggle the upper–roman list style'), tooltip: t('Upper-roman'), type: 'upper-roman', icon: listStyleUpperRomanIcon }, { label: t('Toggle the lower–latin list style'), tooltip: t('Lower-latin'), type: 'lower-latin', icon: listStyleLowerLatinIcon }, { label: t('Toggle the upper–latin list style'), tooltip: t('Upper-latin'), type: 'upper-latin', icon: listStyleUpperLatinIcon } ]; const buttonLabel = t('Numbered List'); const styleGridAriaLabel = t('Numbered list styles toolbar'); const commandName = 'numberedList'; editor.ui.componentFactory.add(commandName, getDropdownViewCreator({ editor, propertiesConfig, parentCommandName: commandName, buttonLabel, buttonIcon: icons.numberedList, styleGridAriaLabel, styleDefinitions })); // Menu bar menu does not display list start index or reverse UI. If there are no styles enabled, // the menu makes no sense and should be omitted. if (propertiesConfig.styles) { editor.ui.componentFactory.add(`menuBar:${commandName}`, getMenuBarStylesMenuCreator({ editor, propertiesConfig, parentCommandName: commandName, buttonLabel, styleGridAriaLabel, styleDefinitions })); } } } } /** * A helper that returns a function that creates a split button with a toolbar in the dropdown, * which in turn contains buttons allowing users to change list styles in the context of the current selection. * * @param options.editor * @param options.propertiesConfig List properties configuration. * @param options.parentCommandName The name of the higher-order editor command associated with * the set of particular list styles (e.g. "bulletedList" for "disc", "circle", and "square" styles). * @param options.buttonLabel Label of the main part of the split button. * @param options.buttonIcon The SVG string of an icon for the main part of the split button. * @param options.styleGridAriaLabel The ARIA label for the styles grid in the split button dropdown. * @param options.styleDefinitions Definitions of the style buttons. * @returns A function that can be passed straight into {@link module:ui/componentfactory~ComponentFactory#add}. */ function getDropdownViewCreator({ editor, propertiesConfig, parentCommandName, buttonLabel, buttonIcon, styleGridAriaLabel, styleDefinitions }) { const parentCommand = editor.commands.get(parentCommandName); return (locale) => { const dropdownView = createDropdown(locale, SplitButtonView); const mainButtonView = dropdownView.buttonView; dropdownView.bind('isEnabled').to(parentCommand); dropdownView.class = 'ck-list-styles-dropdown'; // Main button was clicked. mainButtonView.on('execute', () => { editor.execute(parentCommandName); editor.editing.view.focus(); }); mainButtonView.set({ label: buttonLabel, icon: buttonIcon, tooltip: true, isToggleable: true }); mainButtonView.bind('isOn').to(parentCommand, 'value', value => !!value); dropdownView.once('change:isOpen', () => { const listPropertiesView = createListPropertiesView({ editor, propertiesConfig, dropdownView, parentCommandName, styleGridAriaLabel, styleDefinitions }); dropdownView.panelView.children.add(listPropertiesView); }); // Focus the editable after executing the command. // Overrides a default behaviour where the focus is moved to the dropdown button (#12125). dropdownView.on('execute', () => { editor.editing.view.focus(); }); return dropdownView; }; } /** * A helper that returns a function (factory) that creates individual buttons used by users to change styles * of lists. * * @param options.editor * @param options.listStyleCommand The instance of the `ListStylesCommand` class. * @param options.parentCommandName The name of the higher-order command associated with a * particular list style (e.g. "bulletedList" is associated with "square" and "numberedList" is associated with "roman"). * @returns A function that can be passed straight into {@link module:ui/componentfactory~ComponentFactory#add}. */ function getStyleButtonCreator({ editor, listStyleCommand, parentCommandName }) { const locale = editor.locale; const parentCommand = editor.commands.get(parentCommandName); return ({ label, type, icon, tooltip }) => { const button = new ButtonView(locale); button.set({ label, icon, tooltip }); listStyleCommand.on('change:value', () => { button.isOn = listStyleCommand.value === type; }); button.on('execute', () => { // If the content the selection is anchored to is a list, let's change its style. if (parentCommand.value) { // Remove the list when the current list style is the same as the one that would normally be applied. if (listStyleCommand.value === type) { editor.execute(parentCommandName); } // If the current list style is not set in the model or the style is different than the // one to be applied, simply apply the new style. else if (listStyleCommand.value !== type) { editor.execute('listStyle', { type }); } } // Otherwise, leave the creation of the styled list to the `ListStyleCommand`. else { editor.model.change(() => { editor.execute('listStyle', { type }); }); } }); return button; }; } /** * A helper that creates the properties view for the individual style dropdown. * * @param options.editor Editor instance. * @param options.propertiesConfig List properties configuration. * @param options.dropdownView Styles dropdown view that hosts the properties view. * @param options.parentCommandName The name of the higher-order editor command associated with * the set of particular list styles (e.g. "bulletedList" for "disc", "circle", and "square" styles). * @param options.styleDefinitions Definitions of the style buttons. * @param options.styleGridAriaLabel An assistive technologies label set on the grid of styles (if the grid is rendered). */ function createListPropertiesView({ editor, propertiesConfig, dropdownView, parentCommandName, styleDefinitions, styleGridAriaLabel }) { const locale = editor.locale; const enabledProperties = { ...propertiesConfig }; if (parentCommandName != 'numberedList') { enabledProperties.startIndex = false; enabledProperties.reversed = false; } let styleButtonViews = null; if (enabledProperties.styles) { const listStyleCommand = editor.commands.get('listStyle'); const styleButtonCreator = getStyleButtonCreator({ editor, parentCommandName, listStyleCommand }); // The command can be ListStyleCommand or DocumentListStyleCommand. const isStyleTypeSupported = getStyleTypeSupportChecker(listStyleCommand); styleButtonViews = styleDefinitions.filter(isStyleTypeSupported).map(styleButtonCreator); } const listPropertiesView = new ListPropertiesView(locale, { styleGridAriaLabel, enabledProperties, styleButtonViews }); if (enabledProperties.styles) { // Accessibility: focus the first active style when opening the dropdown. focusChildOnDropdownOpen(dropdownView, () => { return listPropertiesView.stylesView.children.find((child) => child.isOn); }); } if (enabledProperties.startIndex) { const listStartCommand = editor.commands.get('listStart'); listPropertiesView.startIndexFieldView.bind('isEnabled').to(listStartCommand); listPropertiesView.startIndexFieldView.fieldView.bind('value').to(listStartCommand); listPropertiesView.on('listStart', (evt, data) => editor.execute('listStart', data)); } if (enabledProperties.reversed) { const listReversedCommand = editor.commands.get('listReversed'); listPropertiesView.reversedSwitchButtonView.bind('isEnabled').to(listReversedCommand); listPropertiesView.reversedSwitchButtonView.bind('isOn').to(listReversedCommand, 'value', value => !!value); listPropertiesView.on('listReversed', () => { const isReversed = listReversedCommand.value; editor.execute('listReversed', { reversed: !isReversed }); }); } // Make sure applying styles closes the dropdown. listPropertiesView.delegate('execute').to(dropdownView); return listPropertiesView; } /** * A helper that creates the list style submenu for menu bar. * * @param editor Editor instance. * @param propertiesConfig List properties configuration. * @param parentCommandName Name of the list command. * @param buttonLabel Label of the menu button. * @param styleGridAriaLabel ARIA label of the styles grid. * @param styleDefinitions Array of available styles for processed list type. */ function getMenuBarStylesMenuCreator({ editor, propertiesConfig, parentCommandName, buttonLabel, styleGridAriaLabel, styleDefinitions }) { return (locale) => { const menuView = new MenuBarMenuView(locale); const listCommand = editor.commands.get(parentCommandName); const listStyleCommand = editor.commands.get('listStyle'); const isStyleTypeSupported = getStyleTypeSupportChecker(listStyleCommand); const styleButtonCreator = getStyleButtonCreator({ editor, parentCommandName, listStyleCommand }); const styleButtonViews = styleDefinitions.filter(isStyleTypeSupported).map(styleButtonCreator); const listPropertiesView = new ListPropertiesView(locale, { styleGridAriaLabel, enabledProperties: { ...propertiesConfig, // Disable list start index and reversed in the menu bar. startIndex: false, reversed: false }, styleButtonViews }); listPropertiesView.delegate('execute').to(menuView); menuView.buttonView.set({ label: buttonLabel, icon: icons[parentCommandName] }); menuView.panelView.children.add(listPropertiesView); menuView.bind('isEnabled').to(listCommand, 'isEnabled'); menuView.on('execute', () => { editor.editing.view.focus(); }); return menuView; }; } function getStyleTypeSupportChecker(listStyleCommand) { if (typeof listStyleCommand.isStyleTypeSupported == 'function') { return (styleDefinition) => listStyleCommand.isStyleTypeSupported(styleDefinition.type); } else { return () => true; } }