@ckeditor/ckeditor5-list
Version:
Ordered and unordered lists feature to CKEditor 5.
168 lines (167 loc) • 7.73 kB
JavaScript
/**
* @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
*/
/**
* @module list/legacylist/legacylistediting
*/
import LegacyListCommand from './legacylistcommand.js';
import LegacyIndentCommand from './legacyindentcommand.js';
import LegacyListUtils from './legacylistutils.js';
import { Plugin } from 'ckeditor5/src/core.js';
import { Enter } from 'ckeditor5/src/enter.js';
import { Delete } from 'ckeditor5/src/typing.js';
import { cleanList, cleanListItem, modelViewInsertion, modelViewChangeType, modelViewMergeAfterChangeType, modelViewMergeAfter, modelViewRemove, modelViewSplitOnInsert, modelViewChangeIndent, modelChangePostFixer, modelIndentPasteFixer, viewModelConverter, modelToViewPosition, viewToModelPosition } from './legacyconverters.js';
import '../../theme/list.css';
/**
* The engine of the list feature. It handles creating, editing and removing lists and list items.
*
* It registers the `'numberedList'`, `'bulletedList'`, `'indentList'` and `'outdentList'` commands.
*/
export default class LegacyListEditing extends Plugin {
/**
* @inheritDoc
*/
static get pluginName() {
return 'LegacyListEditing';
}
/**
* @inheritDoc
*/
static get isOfficialPlugin() {
return true;
}
/**
* @inheritDoc
*/
static get requires() {
return [Enter, Delete, LegacyListUtils];
}
/**
* @inheritDoc
*/
init() {
const editor = this.editor;
// Schema.
// Note: in case `$block` will ever be allowed in `listItem`, keep in mind that this feature
// uses `Selection#getSelectedBlocks()` without any additional processing to obtain all selected list items.
// If there are blocks allowed inside list item, algorithms using `getSelectedBlocks()` will have to be modified.
editor.model.schema.register('listItem', {
inheritAllFrom: '$block',
allowAttributes: ['listType', 'listIndent']
});
// Converters.
const data = editor.data;
const editing = editor.editing;
editor.model.document.registerPostFixer(writer => modelChangePostFixer(editor.model, writer));
editing.mapper.registerViewToModelLength('li', getViewListItemLength);
data.mapper.registerViewToModelLength('li', getViewListItemLength);
editing.mapper.on('modelToViewPosition', modelToViewPosition(editing.view));
editing.mapper.on('viewToModelPosition', viewToModelPosition(editor.model));
data.mapper.on('modelToViewPosition', modelToViewPosition(editing.view));
editor.conversion.for('editingDowncast')
.add(dispatcher => {
dispatcher.on('insert', modelViewSplitOnInsert, { priority: 'high' });
dispatcher.on('insert:listItem', modelViewInsertion(editor.model));
dispatcher.on('attribute:listType:listItem', modelViewChangeType, { priority: 'high' });
dispatcher.on('attribute:listType:listItem', modelViewMergeAfterChangeType, { priority: 'low' });
dispatcher.on('attribute:listIndent:listItem', modelViewChangeIndent(editor.model));
dispatcher.on('remove:listItem', modelViewRemove(editor.model));
dispatcher.on('remove', modelViewMergeAfter, { priority: 'low' });
});
editor.conversion.for('dataDowncast')
.add(dispatcher => {
dispatcher.on('insert', modelViewSplitOnInsert, { priority: 'high' });
dispatcher.on('insert:listItem', modelViewInsertion(editor.model));
});
editor.conversion.for('upcast')
.add(dispatcher => {
dispatcher.on('element:ul', cleanList, { priority: 'high' });
dispatcher.on('element:ol', cleanList, { priority: 'high' });
dispatcher.on('element:li', cleanListItem, { priority: 'high' });
dispatcher.on('element:li', viewModelConverter);
});
// Fix indentation of pasted items.
editor.model.on('insertContent', modelIndentPasteFixer, { priority: 'high' });
// Register commands for numbered and bulleted list.
editor.commands.add('numberedList', new LegacyListCommand(editor, 'numbered'));
editor.commands.add('bulletedList', new LegacyListCommand(editor, 'bulleted'));
// Register commands for indenting.
editor.commands.add('indentList', new LegacyIndentCommand(editor, 'forward'));
editor.commands.add('outdentList', new LegacyIndentCommand(editor, 'backward'));
const viewDocument = editing.view.document;
// Overwrite default Enter key behavior.
// If Enter key is pressed with selection collapsed in empty list item, outdent it instead of breaking it.
this.listenTo(viewDocument, 'enter', (evt, data) => {
const doc = this.editor.model.document;
const positionParent = doc.selection.getLastPosition().parent;
if (doc.selection.isCollapsed && positionParent.name == 'listItem' && positionParent.isEmpty) {
this.editor.execute('outdentList');
data.preventDefault();
evt.stop();
}
}, { context: 'li' });
// Overwrite default Backspace key behavior.
// If Backspace key is pressed with selection collapsed on first position in first list item, outdent it. #83
this.listenTo(viewDocument, 'delete', (evt, data) => {
// Check conditions from those that require less computations like those immediately available.
if (data.direction !== 'backward') {
return;
}
const selection = this.editor.model.document.selection;
if (!selection.isCollapsed) {
return;
}
const firstPosition = selection.getFirstPosition();
if (!firstPosition.isAtStart) {
return;
}
const positionParent = firstPosition.parent;
if (positionParent.name !== 'listItem') {
return;
}
const previousIsAListItem = positionParent.previousSibling && positionParent.previousSibling.name === 'listItem';
if (previousIsAListItem) {
return;
}
this.editor.execute('outdentList');
data.preventDefault();
evt.stop();
}, { context: 'li' });
this.listenTo(editor.editing.view.document, 'tab', (evt, data) => {
const commandName = data.shiftKey ? 'outdentList' : 'indentList';
const command = this.editor.commands.get(commandName);
if (command.isEnabled) {
editor.execute(commandName);
data.stopPropagation();
data.preventDefault();
evt.stop();
}
}, { context: 'li' });
}
/**
* @inheritDoc
*/
afterInit() {
const commands = this.editor.commands;
const indent = commands.get('indent');
const outdent = commands.get('outdent');
if (indent) {
indent.registerChildCommand(commands.get('indentList'));
}
if (outdent) {
outdent.registerChildCommand(commands.get('outdentList'));
}
}
}
function getViewListItemLength(element) {
let length = 1;
for (const child of element.getChildren()) {
if (child.name == 'ul' || child.name == 'ol') {
for (const item of child.getChildren()) {
length += getViewListItemLength(item);
}
}
}
return length;
}