@ckeditor/ckeditor5-list
Version:
Ordered and unordered lists feature to CKEditor 5.
302 lines (301 loc) • 13.1 kB
JavaScript
/**
* @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/ui/listpropertiesview
*/
import { View, ViewCollection, FocusCycler, SwitchButtonView, LabeledFieldView, createLabeledInputNumber, addKeyboardHandlingForGrid, CollapsibleView } from 'ckeditor5/src/ui.js';
import { FocusTracker, KeystrokeHandler, global } from 'ckeditor5/src/utils.js';
import '../../../theme/listproperties.css';
/**
* The list properties view to be displayed in the list dropdown.
*
* Contains a grid of available list styles and, for numbered list, also the list start index and reversed fields.
*
* @internal
*/
export default class ListPropertiesView extends View {
/**
* Creates an instance of the list properties view.
*
* @param locale The {@link module:core/editor/editor~Editor#locale} instance.
* @param options Options of the view.
* @param options.enabledProperties An object containing the configuration of enabled list property names.
* Allows conditional rendering the sub-components of the properties view.
* @param options.styleButtonViews A list of style buttons to be rendered
* inside the styles grid. The grid will not be rendered when `enabledProperties` does not include the `'styles'` key.
* @param options.styleGridAriaLabel An assistive technologies label set on the grid of styles (if the grid is rendered).
*/
constructor(locale, { enabledProperties, styleButtonViews, styleGridAriaLabel }) {
super(locale);
/**
* A view that renders the grid of list styles.
*/
this.stylesView = null;
/**
* A collapsible view that hosts additional list property fields ({@link #startIndexFieldView} and
* {@link #reversedSwitchButtonView}) to visually separate them from the {@link #stylesView grid of styles}.
*
* **Note**: Only present when:
* * the view represents **numbered** list properties,
* * and the {@link #stylesView} is rendered,
* * and either {@link #startIndexFieldView} or {@link #reversedSwitchButtonView} is rendered.
*
* @readonly
*/
this.additionalPropertiesCollapsibleView = null;
/**
* A labeled number field allowing the user to set the start index of the list.
*
* **Note**: Only present when the view represents **numbered** list properties.
*
* @readonly
*/
this.startIndexFieldView = null;
/**
* A switch button allowing the user to make the edited list reversed.
*
* **Note**: Only present when the view represents **numbered** list properties.
*
* @readonly
*/
this.reversedSwitchButtonView = null;
/**
* Tracks information about the DOM focus in the view.
*/
this.focusTracker = new FocusTracker();
/**
* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
*/
this.keystrokes = new KeystrokeHandler();
/**
* A collection of views that can be focused in the properties view.
*/
this.focusables = new ViewCollection();
const elementCssClasses = [
'ck',
'ck-list-properties'
];
this.children = this.createCollection();
this.focusCycler = new FocusCycler({
focusables: this.focusables,
focusTracker: this.focusTracker,
keystrokeHandler: this.keystrokes,
actions: {
// Navigate #children backwards using the <kbd>Shift</kbd> + <kbd>Tab</kbd> keystroke.
focusPrevious: 'shift + tab',
// Navigate #children forwards using the <kbd>Tab</kbd> key.
focusNext: 'tab'
}
});
// The rendering of the styles grid is conditional. When there is no styles grid, the view will render without collapsible
// for numbered list properties, hence simplifying the layout.
if (enabledProperties.styles) {
this.stylesView = this._createStylesView(styleButtonViews, styleGridAriaLabel);
this.children.add(this.stylesView);
}
else {
elementCssClasses.push('ck-list-properties_without-styles');
}
// The rendering of the numbered list property views is also conditional. It only makes sense for the numbered list
// dropdown. The unordered list does not have such properties.
if (enabledProperties.startIndex || enabledProperties.reversed) {
this._addNumberedListPropertyViews(enabledProperties);
elementCssClasses.push('ck-list-properties_with-numbered-properties');
}
this.setTemplate({
tag: 'div',
attributes: {
class: elementCssClasses
},
children: this.children
});
}
/**
* @inheritDoc
*/
render() {
super.render();
if (this.stylesView) {
this.focusables.add(this.stylesView);
this.focusTracker.add(this.stylesView.element);
// Register the collapsible toggle button to the focus system.
if (this.startIndexFieldView || this.reversedSwitchButtonView) {
this.focusables.add(this.children.last.buttonView);
this.focusTracker.add(this.children.last.buttonView.element);
}
for (const item of this.stylesView.children) {
this.stylesView.focusTracker.add(item.element);
}
addKeyboardHandlingForGrid({
keystrokeHandler: this.stylesView.keystrokes,
focusTracker: this.stylesView.focusTracker,
gridItems: this.stylesView.children,
// Note: The styles view has a different number of columns depending on whether the other properties
// are enabled in the dropdown or not (https://github.com/ckeditor/ckeditor5/issues/12340)
numberOfColumns: () => global.window
.getComputedStyle(this.stylesView.element)
.getPropertyValue('grid-template-columns')
.split(' ')
.length,
uiLanguageDirection: this.locale && this.locale.uiLanguageDirection
});
}
if (this.startIndexFieldView) {
this.focusables.add(this.startIndexFieldView);
this.focusTracker.add(this.startIndexFieldView.element);
const stopPropagation = (data) => data.stopPropagation();
// Since the form is in the dropdown panel which is a child of the toolbar, the toolbar's
// keystroke handler would take over the key management in the input. We need to prevent
// this ASAP. Otherwise, the basic caret movement using the arrow keys will be impossible.
this.keystrokes.set('arrowright', stopPropagation);
this.keystrokes.set('arrowleft', stopPropagation);
this.keystrokes.set('arrowup', stopPropagation);
this.keystrokes.set('arrowdown', stopPropagation);
}
if (this.reversedSwitchButtonView) {
this.focusables.add(this.reversedSwitchButtonView);
this.focusTracker.add(this.reversedSwitchButtonView.element);
}
// Start listening for the keystrokes coming from #element.
this.keystrokes.listenTo(this.element);
}
/**
* @inheritDoc
*/
focus() {
this.focusCycler.focusFirst();
}
/**
* @inheritDoc
*/
focusLast() {
this.focusCycler.focusLast();
}
/**
* @inheritDoc
*/
destroy() {
super.destroy();
this.focusTracker.destroy();
this.keystrokes.destroy();
}
/**
* Creates the list styles grid.
*
* @param styleButtons Buttons to be placed in the grid.
* @param styleGridAriaLabel The assistive technology label of the grid.
*/
_createStylesView(styleButtons, styleGridAriaLabel) {
const stylesView = new View(this.locale);
stylesView.children = stylesView.createCollection();
stylesView.children.addMany(styleButtons);
stylesView.setTemplate({
tag: 'div',
attributes: {
'aria-label': styleGridAriaLabel,
class: [
'ck',
'ck-list-styles-list'
]
},
children: stylesView.children
});
stylesView.children.delegate('execute').to(this);
stylesView.focus = function () {
this.children.first.focus();
};
stylesView.focusTracker = new FocusTracker();
stylesView.keystrokes = new KeystrokeHandler();
stylesView.render();
stylesView.keystrokes.listenTo(stylesView.element);
return stylesView;
}
/**
* Renders {@link #startIndexFieldView} and/or {@link #reversedSwitchButtonView} depending on the configuration of the properties view.
*
* @param enabledProperties An object containing the configuration of enabled list property names
* (see {@link #constructor}).
*/
_addNumberedListPropertyViews(enabledProperties) {
const t = this.locale.t;
const numberedPropertyViews = [];
if (enabledProperties.startIndex) {
this.startIndexFieldView = this._createStartIndexField();
numberedPropertyViews.push(this.startIndexFieldView);
}
if (enabledProperties.reversed) {
this.reversedSwitchButtonView = this._createReversedSwitchButton();
numberedPropertyViews.push(this.reversedSwitchButtonView);
}
// When there are some style buttons, pack the numbered list properties into a collapsible to separate them.
if (enabledProperties.styles) {
this.additionalPropertiesCollapsibleView = new CollapsibleView(this.locale, numberedPropertyViews);
this.additionalPropertiesCollapsibleView.set({
label: t('List properties'),
isCollapsed: true
});
// Don't enable the collapsible view unless either start index or reversed field is enabled (e.g. when no list is selected).
this.additionalPropertiesCollapsibleView.buttonView.bind('isEnabled').toMany(numberedPropertyViews, 'isEnabled', (...areEnabled) => areEnabled.some(isEnabled => isEnabled));
// Automatically collapse the additional properties collapsible when either start index or reversed field gets disabled.
this.additionalPropertiesCollapsibleView.buttonView.on('change:isEnabled', (evt, data, isEnabled) => {
if (!isEnabled) {
this.additionalPropertiesCollapsibleView.isCollapsed = true;
}
});
this.children.add(this.additionalPropertiesCollapsibleView);
}
else {
this.children.addMany(numberedPropertyViews);
}
}
/**
* Creates the list start index labeled field.
*/
_createStartIndexField() {
const t = this.locale.t;
const startIndexFieldView = new LabeledFieldView(this.locale, createLabeledInputNumber);
startIndexFieldView.set({
label: t('Start at'),
class: 'ck-numbered-list-properties__start-index'
});
startIndexFieldView.fieldView.set({
min: 0,
step: 1,
value: 1,
inputMode: 'numeric'
});
startIndexFieldView.fieldView.on('input', () => {
const inputElement = startIndexFieldView.fieldView.element;
const startIndex = inputElement.valueAsNumber;
if (Number.isNaN(startIndex)) {
// Number inputs allow for the entry of characters that may result in NaN,
// such as 'e', '+', '123e', '2-'.
startIndexFieldView.errorText = t('Invalid start index value.');
return;
}
if (!inputElement.checkValidity()) {
startIndexFieldView.errorText = t('Start index must be greater than 0.');
}
else {
this.fire('listStart', { startIndex });
}
});
return startIndexFieldView;
}
/**
* Creates the reversed list switch button.
*/
_createReversedSwitchButton() {
const t = this.locale.t;
const reversedButtonView = new SwitchButtonView(this.locale);
reversedButtonView.set({
withText: true,
label: t('Reversed order'),
class: 'ck-numbered-list-properties__reversed-order'
});
reversedButtonView.delegate('execute').to(this, 'listReversed');
return reversedButtonView;
}
}