UNPKG

7.51 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/list/listediting
7 */
8import ListCommand from './listcommand';
9import IndentCommand from './indentcommand';
10import ListUtils from './listutils';
11import { Plugin } from 'ckeditor5/src/core';
12import { Enter } from 'ckeditor5/src/enter';
13import { Delete } from 'ckeditor5/src/typing';
14import { cleanList, cleanListItem, modelViewInsertion, modelViewChangeType, modelViewMergeAfterChangeType, modelViewMergeAfter, modelViewRemove, modelViewSplitOnInsert, modelViewChangeIndent, modelChangePostFixer, modelIndentPasteFixer, viewModelConverter, modelToViewPosition, viewToModelPosition } from './converters';
15import '../../theme/list.css';
16/**
17 * The engine of the list feature. It handles creating, editing and removing lists and list items.
18 *
19 * It registers the `'numberedList'`, `'bulletedList'`, `'indentList'` and `'outdentList'` commands.
20 */
21export default class ListEditing extends Plugin {
22 /**
23 * @inheritDoc
24 */
25 static get pluginName() {
26 return 'ListEditing';
27 }
28 /**
29 * @inheritDoc
30 */
31 static get requires() {
32 return [Enter, Delete, ListUtils];
33 }
34 /**
35 * @inheritDoc
36 */
37 init() {
38 const editor = this.editor;
39 // Schema.
40 // Note: in case `$block` will ever be allowed in `listItem`, keep in mind that this feature
41 // uses `Selection#getSelectedBlocks()` without any additional processing to obtain all selected list items.
42 // If there are blocks allowed inside list item, algorithms using `getSelectedBlocks()` will have to be modified.
43 editor.model.schema.register('listItem', {
44 inheritAllFrom: '$block',
45 allowAttributes: ['listType', 'listIndent']
46 });
47 // Converters.
48 const data = editor.data;
49 const editing = editor.editing;
50 editor.model.document.registerPostFixer(writer => modelChangePostFixer(editor.model, writer));
51 editing.mapper.registerViewToModelLength('li', getViewListItemLength);
52 data.mapper.registerViewToModelLength('li', getViewListItemLength);
53 editing.mapper.on('modelToViewPosition', modelToViewPosition(editing.view));
54 editing.mapper.on('viewToModelPosition', viewToModelPosition(editor.model));
55 data.mapper.on('modelToViewPosition', modelToViewPosition(editing.view));
56 editor.conversion.for('editingDowncast')
57 .add(dispatcher => {
58 dispatcher.on('insert', modelViewSplitOnInsert, { priority: 'high' });
59 dispatcher.on('insert:listItem', modelViewInsertion(editor.model));
60 dispatcher.on('attribute:listType:listItem', modelViewChangeType, { priority: 'high' });
61 dispatcher.on('attribute:listType:listItem', modelViewMergeAfterChangeType, { priority: 'low' });
62 dispatcher.on('attribute:listIndent:listItem', modelViewChangeIndent(editor.model));
63 dispatcher.on('remove:listItem', modelViewRemove(editor.model));
64 dispatcher.on('remove', modelViewMergeAfter, { priority: 'low' });
65 });
66 editor.conversion.for('dataDowncast')
67 .add(dispatcher => {
68 dispatcher.on('insert', modelViewSplitOnInsert, { priority: 'high' });
69 dispatcher.on('insert:listItem', modelViewInsertion(editor.model));
70 });
71 editor.conversion.for('upcast')
72 .add(dispatcher => {
73 dispatcher.on('element:ul', cleanList, { priority: 'high' });
74 dispatcher.on('element:ol', cleanList, { priority: 'high' });
75 dispatcher.on('element:li', cleanListItem, { priority: 'high' });
76 dispatcher.on('element:li', viewModelConverter);
77 });
78 // Fix indentation of pasted items.
79 editor.model.on('insertContent', modelIndentPasteFixer, { priority: 'high' });
80 // Register commands for numbered and bulleted list.
81 editor.commands.add('numberedList', new ListCommand(editor, 'numbered'));
82 editor.commands.add('bulletedList', new ListCommand(editor, 'bulleted'));
83 // Register commands for indenting.
84 editor.commands.add('indentList', new IndentCommand(editor, 'forward'));
85 editor.commands.add('outdentList', new IndentCommand(editor, 'backward'));
86 const viewDocument = editing.view.document;
87 // Overwrite default Enter key behavior.
88 // If Enter key is pressed with selection collapsed in empty list item, outdent it instead of breaking it.
89 this.listenTo(viewDocument, 'enter', (evt, data) => {
90 const doc = this.editor.model.document;
91 const positionParent = doc.selection.getLastPosition().parent;
92 if (doc.selection.isCollapsed && positionParent.name == 'listItem' && positionParent.isEmpty) {
93 this.editor.execute('outdentList');
94 data.preventDefault();
95 evt.stop();
96 }
97 }, { context: 'li' });
98 // Overwrite default Backspace key behavior.
99 // If Backspace key is pressed with selection collapsed on first position in first list item, outdent it. #83
100 this.listenTo(viewDocument, 'delete', (evt, data) => {
101 // Check conditions from those that require less computations like those immediately available.
102 if (data.direction !== 'backward') {
103 return;
104 }
105 const selection = this.editor.model.document.selection;
106 if (!selection.isCollapsed) {
107 return;
108 }
109 const firstPosition = selection.getFirstPosition();
110 if (!firstPosition.isAtStart) {
111 return;
112 }
113 const positionParent = firstPosition.parent;
114 if (positionParent.name !== 'listItem') {
115 return;
116 }
117 const previousIsAListItem = positionParent.previousSibling && positionParent.previousSibling.name === 'listItem';
118 if (previousIsAListItem) {
119 return;
120 }
121 this.editor.execute('outdentList');
122 data.preventDefault();
123 evt.stop();
124 }, { context: 'li' });
125 this.listenTo(editor.editing.view.document, 'tab', (evt, data) => {
126 const commandName = data.shiftKey ? 'outdentList' : 'indentList';
127 const command = this.editor.commands.get(commandName);
128 if (command.isEnabled) {
129 editor.execute(commandName);
130 data.stopPropagation();
131 data.preventDefault();
132 evt.stop();
133 }
134 }, { context: 'li' });
135 }
136 /**
137 * @inheritDoc
138 */
139 afterInit() {
140 const commands = this.editor.commands;
141 const indent = commands.get('indent');
142 const outdent = commands.get('outdent');
143 if (indent) {
144 indent.registerChildCommand(commands.get('indentList'));
145 }
146 if (outdent) {
147 outdent.registerChildCommand(commands.get('outdentList'));
148 }
149 }
150}
151function getViewListItemLength(element) {
152 let length = 1;
153 for (const child of element.getChildren()) {
154 if (child.name == 'ul' || child.name == 'ol') {
155 for (const item of child.getChildren()) {
156 length += getViewListItemLength(item);
157 }
158 }
159 }
160 return length;
161}