UNPKG

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