UNPKG

@ckeditor/ckeditor5-list

Version:

Ordered and unordered lists feature to CKEditor 5.

168 lines (167 loc) 7.73 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 */ /** * @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; }