UNPKG

13.6 kBJavaScriptView Raw
1/**
2 * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4 */
5/**
6 * @module list/listproperties/listpropertiesui
7 */
8import { Plugin } from 'ckeditor5/src/core';
9import { ButtonView, SplitButtonView, createDropdown, focusChildOnDropdownOpen } from 'ckeditor5/src/ui';
10import ListPropertiesView from './ui/listpropertiesview';
11import bulletedListIcon from '../../theme/icons/bulletedlist.svg';
12import numberedListIcon from '../../theme/icons/numberedlist.svg';
13import listStyleDiscIcon from '../../theme/icons/liststyledisc.svg';
14import listStyleCircleIcon from '../../theme/icons/liststylecircle.svg';
15import listStyleSquareIcon from '../../theme/icons/liststylesquare.svg';
16import listStyleDecimalIcon from '../../theme/icons/liststyledecimal.svg';
17import listStyleDecimalWithLeadingZeroIcon from '../../theme/icons/liststyledecimalleadingzero.svg';
18import listStyleLowerRomanIcon from '../../theme/icons/liststylelowerroman.svg';
19import listStyleUpperRomanIcon from '../../theme/icons/liststyleupperroman.svg';
20import listStyleLowerLatinIcon from '../../theme/icons/liststylelowerlatin.svg';
21import listStyleUpperLatinIcon from '../../theme/icons/liststyleupperlatin.svg';
22import '../../theme/liststyles.css';
23/**
24 * The list properties UI plugin. It introduces the extended `'bulletedList'` and `'numberedList'` toolbar
25 * buttons that allow users to control such aspects of list as the marker, start index or order.
26 *
27 * **Note**: Buttons introduced by this plugin override implementations from the {@link module:list/list/listui~ListUI}
28 * (because they share the same names).
29 */
30export default class ListPropertiesUI extends Plugin {
31 /**
32 * @inheritDoc
33 */
34 static get pluginName() {
35 return 'ListPropertiesUI';
36 }
37 init() {
38 const editor = this.editor;
39 const t = editor.locale.t;
40 const enabledProperties = editor.config.get('list.properties');
41 // Note: When this plugin does not register the "bulletedList" dropdown due to properties configuration,
42 // a simple button will be still registered under the same name by ListUI as a fallback. This should happen
43 // in most editor configuration because the List plugin automatically requires ListUI.
44 if (enabledProperties.styles) {
45 editor.ui.componentFactory.add('bulletedList', getDropdownViewCreator({
46 editor,
47 parentCommandName: 'bulletedList',
48 buttonLabel: t('Bulleted List'),
49 buttonIcon: bulletedListIcon,
50 styleGridAriaLabel: t('Bulleted list styles toolbar'),
51 styleDefinitions: [
52 {
53 label: t('Toggle the disc list style'),
54 tooltip: t('Disc'),
55 type: 'disc',
56 icon: listStyleDiscIcon
57 },
58 {
59 label: t('Toggle the circle list style'),
60 tooltip: t('Circle'),
61 type: 'circle',
62 icon: listStyleCircleIcon
63 },
64 {
65 label: t('Toggle the square list style'),
66 tooltip: t('Square'),
67 type: 'square',
68 icon: listStyleSquareIcon
69 }
70 ]
71 }));
72 }
73 // Note: When this plugin does not register the "numberedList" dropdown due to properties configuration,
74 // a simple button will be still registered under the same name by ListUI as a fallback. This should happen
75 // in most editor configuration because the List plugin automatically requires ListUI.
76 if (enabledProperties.styles || enabledProperties.startIndex || enabledProperties.reversed) {
77 editor.ui.componentFactory.add('numberedList', getDropdownViewCreator({
78 editor,
79 parentCommandName: 'numberedList',
80 buttonLabel: t('Numbered List'),
81 buttonIcon: numberedListIcon,
82 styleGridAriaLabel: t('Numbered list styles toolbar'),
83 styleDefinitions: [
84 {
85 label: t('Toggle the decimal list style'),
86 tooltip: t('Decimal'),
87 type: 'decimal',
88 icon: listStyleDecimalIcon
89 },
90 {
91 label: t('Toggle the decimal with leading zero list style'),
92 tooltip: t('Decimal with leading zero'),
93 type: 'decimal-leading-zero',
94 icon: listStyleDecimalWithLeadingZeroIcon
95 },
96 {
97 label: t('Toggle the lower–roman list style'),
98 tooltip: t('Lower–roman'),
99 type: 'lower-roman',
100 icon: listStyleLowerRomanIcon
101 },
102 {
103 label: t('Toggle the upper–roman list style'),
104 tooltip: t('Upper-roman'),
105 type: 'upper-roman',
106 icon: listStyleUpperRomanIcon
107 },
108 {
109 label: t('Toggle the lower–latin list style'),
110 tooltip: t('Lower-latin'),
111 type: 'lower-latin',
112 icon: listStyleLowerLatinIcon
113 },
114 {
115 label: t('Toggle the upper–latin list style'),
116 tooltip: t('Upper-latin'),
117 type: 'upper-latin',
118 icon: listStyleUpperLatinIcon
119 }
120 ]
121 }));
122 }
123 }
124}
125/**
126 * A helper that returns a function that creates a split button with a toolbar in the dropdown,
127 * which in turn contains buttons allowing users to change list styles in the context of the current selection.
128 *
129 * @param options.editor
130 * @param options.parentCommandName The name of the higher-order editor command associated with
131 * the set of particular list styles (e.g. "bulletedList" for "disc", "circle", and "square" styles).
132 * @param options.buttonLabel Label of the main part of the split button.
133 * @param options.buttonIcon The SVG string of an icon for the main part of the split button.
134 * @param options.styleGridAriaLabel The ARIA label for the styles grid in the split button dropdown.
135 * @param options.styleDefinitions Definitions of the style buttons.
136 * @returns A function that can be passed straight into {@link module:ui/componentfactory~ComponentFactory#add}.
137 */
138function getDropdownViewCreator({ editor, parentCommandName, buttonLabel, buttonIcon, styleGridAriaLabel, styleDefinitions }) {
139 const parentCommand = editor.commands.get(parentCommandName);
140 return (locale) => {
141 const dropdownView = createDropdown(locale, SplitButtonView);
142 const mainButtonView = dropdownView.buttonView;
143 dropdownView.bind('isEnabled').to(parentCommand);
144 dropdownView.class = 'ck-list-styles-dropdown';
145 // Main button was clicked.
146 mainButtonView.on('execute', () => {
147 editor.execute(parentCommandName);
148 editor.editing.view.focus();
149 });
150 mainButtonView.set({
151 label: buttonLabel,
152 icon: buttonIcon,
153 tooltip: true,
154 isToggleable: true
155 });
156 mainButtonView.bind('isOn').to(parentCommand, 'value', value => !!value);
157 dropdownView.once('change:isOpen', () => {
158 const listPropertiesView = createListPropertiesView({
159 editor,
160 dropdownView,
161 parentCommandName,
162 styleGridAriaLabel,
163 styleDefinitions
164 });
165 dropdownView.panelView.children.add(listPropertiesView);
166 });
167 // Focus the editable after executing the command.
168 // Overrides a default behaviour where the focus is moved to the dropdown button (#12125).
169 dropdownView.on('execute', () => {
170 editor.editing.view.focus();
171 });
172 return dropdownView;
173 };
174}
175/**
176 * A helper that returns a function (factory) that creates individual buttons used by users to change styles
177 * of lists.
178 *
179 * @param options.editor
180 * @param options.listStyleCommand The instance of the `ListStylesCommand` class.
181 * @param options.parentCommandName The name of the higher-order command associated with a
182 * particular list style (e.g. "bulletedList" is associated with "square" and "numberedList" is associated with "roman").
183 * @returns A function that can be passed straight into {@link module:ui/componentfactory~ComponentFactory#add}.
184 */
185function getStyleButtonCreator({ editor, listStyleCommand, parentCommandName }) {
186 const locale = editor.locale;
187 const parentCommand = editor.commands.get(parentCommandName);
188 return ({ label, type, icon, tooltip }) => {
189 const button = new ButtonView(locale);
190 button.set({ label, icon, tooltip });
191 listStyleCommand.on('change:value', () => {
192 button.isOn = listStyleCommand.value === type;
193 });
194 button.on('execute', () => {
195 // If the content the selection is anchored to is a list, let's change its style.
196 if (parentCommand.value) {
197 // If the current list style is not set in the model or the style is different than the
198 // one to be applied, simply apply the new style.
199 if (listStyleCommand.value !== type) {
200 editor.execute('listStyle', { type });
201 }
202 // If the style was the same, remove it (the button works as an off toggle).
203 else {
204 editor.execute('listStyle', { type: listStyleCommand.defaultType });
205 }
206 }
207 // Otherwise, leave the creation of the styled list to the `ListStyleCommand`.
208 else {
209 editor.model.change(() => {
210 editor.execute('listStyle', { type });
211 });
212 }
213 });
214 return button;
215 };
216}
217/**
218 * A helper that creates the properties view for the individual style dropdown.
219 *
220 * @param options.editor Editor instance.
221 * @param options.dropdownView Styles dropdown view that hosts the properties view.
222 * @param options.parentCommandName The name of the higher-order editor command associated with
223 * the set of particular list styles (e.g. "bulletedList" for "disc", "circle", and "square" styles).
224 * @param options.styleDefinitions Definitions of the style buttons.
225 * @param options.styleGridAriaLabel An assistive technologies label set on the grid of styles (if the grid is rendered).
226 */
227function createListPropertiesView({ editor, dropdownView, parentCommandName, styleDefinitions, styleGridAriaLabel }) {
228 const locale = editor.locale;
229 const enabledProperties = editor.config.get('list.properties');
230 let styleButtonViews = null;
231 if (parentCommandName != 'numberedList') {
232 enabledProperties.startIndex = false;
233 enabledProperties.reversed = false;
234 }
235 if (enabledProperties.styles) {
236 const listStyleCommand = editor.commands.get('listStyle');
237 const styleButtonCreator = getStyleButtonCreator({
238 editor,
239 parentCommandName,
240 listStyleCommand
241 });
242 // The command can be ListStyleCommand or DocumentListStyleCommand.
243 const isStyleTypeSupported = typeof listStyleCommand.isStyleTypeSupported == 'function' ?
244 (styleDefinition) => listStyleCommand.isStyleTypeSupported(styleDefinition.type) :
245 () => true;
246 styleButtonViews = styleDefinitions.filter(isStyleTypeSupported).map(styleButtonCreator);
247 }
248 const listPropertiesView = new ListPropertiesView(locale, {
249 styleGridAriaLabel,
250 enabledProperties,
251 styleButtonViews
252 });
253 if (enabledProperties.styles) {
254 // Accessibility: focus the first active style when opening the dropdown.
255 focusChildOnDropdownOpen(dropdownView, () => {
256 return listPropertiesView.stylesView.children.find((child) => child.isOn);
257 });
258 }
259 if (enabledProperties.startIndex) {
260 const listStartCommand = editor.commands.get('listStart');
261 listPropertiesView.startIndexFieldView.bind('isEnabled').to(listStartCommand);
262 listPropertiesView.startIndexFieldView.fieldView.bind('value').to(listStartCommand);
263 listPropertiesView.on('listStart', (evt, data) => editor.execute('listStart', data));
264 }
265 if (enabledProperties.reversed) {
266 const listReversedCommand = editor.commands.get('listReversed');
267 listPropertiesView.reversedSwitchButtonView.bind('isEnabled').to(listReversedCommand);
268 listPropertiesView.reversedSwitchButtonView.bind('isOn').to(listReversedCommand, 'value', value => !!value);
269 listPropertiesView.on('listReversed', () => {
270 const isReversed = listReversedCommand.value;
271 editor.execute('listReversed', { reversed: !isReversed });
272 });
273 }
274 // Make sure applying styles closes the dropdown.
275 listPropertiesView.delegate('execute').to(dropdownView);
276 return listPropertiesView;
277}