UNPKG

30.8 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/listproperties/listpropertiesediting
7 */
8import { Plugin } from 'ckeditor5/src/core';
9import ListEditing from '../list/listediting';
10import ListStyleCommand from './liststylecommand';
11import ListReversedCommand from './listreversedcommand';
12import ListStartCommand from './liststartcommand';
13import { getSiblingListItem, getSiblingNodes } from '../list/utils';
14const DEFAULT_LIST_TYPE = 'default';
15/**
16 * The engine of the list properties feature.
17 *
18 * It sets the value for the `listItem` attribute of the {@link module:list/list~List `<listItem>`} element that
19 * allows modifying the list style type.
20 *
21 * It registers the `'listStyle'`, `'listReversed'` and `'listStart'` commands if they are enabled in the configuration.
22 * Read more in {@link module:list/listconfig~ListPropertiesConfig}.
23 */
24export default class ListPropertiesEditing extends Plugin {
25 /**
26 * @inheritDoc
27 */
28 static get requires() {
29 return [ListEditing];
30 }
31 /**
32 * @inheritDoc
33 */
34 static get pluginName() {
35 return 'ListPropertiesEditing';
36 }
37 /**
38 * @inheritDoc
39 */
40 constructor(editor) {
41 super(editor);
42 editor.config.define('list', {
43 properties: {
44 styles: true,
45 startIndex: false,
46 reversed: false
47 }
48 });
49 }
50 /**
51 * @inheritDoc
52 */
53 init() {
54 const editor = this.editor;
55 const model = editor.model;
56 const enabledProperties = editor.config.get('list.properties');
57 const strategies = createAttributeStrategies(enabledProperties);
58 // Extend schema.
59 model.schema.extend('listItem', {
60 allowAttributes: strategies.map(s => s.attributeName)
61 });
62 for (const strategy of strategies) {
63 strategy.addCommand(editor);
64 }
65 // Fix list attributes when modifying their nesting levels (the `listIndent` attribute).
66 this.listenTo(editor.commands.get('indentList'), '_executeCleanup', fixListAfterIndentListCommand(editor, strategies));
67 this.listenTo(editor.commands.get('outdentList'), '_executeCleanup', fixListAfterOutdentListCommand(editor, strategies));
68 this.listenTo(editor.commands.get('bulletedList'), '_executeCleanup', restoreDefaultListStyle(editor));
69 this.listenTo(editor.commands.get('numberedList'), '_executeCleanup', restoreDefaultListStyle(editor));
70 // Register a post-fixer that ensures that the attributes is specified in each `listItem` element.
71 model.document.registerPostFixer(fixListAttributesOnListItemElements(editor, strategies));
72 // Set up conversion.
73 editor.conversion.for('upcast').add(upcastListItemAttributes(strategies));
74 editor.conversion.for('downcast').add(downcastListItemAttributes(strategies));
75 // Handle merging two separated lists into the single one.
76 this._mergeListAttributesWhileMergingLists(strategies);
77 }
78 /**
79 * @inheritDoc
80 */
81 afterInit() {
82 const editor = this.editor;
83 // Enable post-fixer that removes the attributes from to-do list items only if the "TodoList" plugin is on.
84 // We need to registry the hook here since the `TodoList` plugin can be added after the `ListPropertiesEditing`.
85 if (editor.commands.get('todoList')) {
86 editor.model.document.registerPostFixer(removeListItemAttributesFromTodoList(editor));
87 }
88 }
89 /**
90 * Starts listening to {@link module:engine/model/model~Model#deleteContent} and checks whether two lists will be merged into a single
91 * one after deleting the content.
92 *
93 * The purpose of this action is to adjust the `listStyle`, `listReversed` and `listStart` values
94 * for the list that was merged.
95 *
96 * Consider the following model's content:
97 *
98 * ```xml
99 * <listItem listIndent="0" listType="bulleted" listStyle="square">UL List item 1</listItem>
100 * <listItem listIndent="0" listType="bulleted" listStyle="square">UL List item 2</listItem>
101 * <paragraph>[A paragraph.]</paragraph>
102 * <listItem listIndent="0" listType="bulleted" listStyle="circle">UL List item 1</listItem>
103 * <listItem listIndent="0" listType="bulleted" listStyle="circle">UL List item 2</listItem>
104 * ```
105 *
106 * After removing the paragraph element, the second list will be merged into the first one.
107 * We want to inherit the `listStyle` attribute for the second list from the first one.
108 *
109 * ```xml
110 * <listItem listIndent="0" listType="bulleted" listStyle="square">UL List item 1</listItem>
111 * <listItem listIndent="0" listType="bulleted" listStyle="square">UL List item 2</listItem>
112 * <listItem listIndent="0" listType="bulleted" listStyle="square">UL List item 1</listItem>
113 * <listItem listIndent="0" listType="bulleted" listStyle="square">UL List item 2</listItem>
114 * ```
115 *
116 * See https://github.com/ckeditor/ckeditor5/issues/7879.
117 *
118 * @param attributeStrategies Strategies for the enabled attributes.
119 */
120 _mergeListAttributesWhileMergingLists(attributeStrategies) {
121 const editor = this.editor;
122 const model = editor.model;
123 // First the outer-most`listItem` in the first list reference.
124 // If found, the lists should be merged and this `listItem` provides the attributes
125 // and it is also a starting point when searching for items in the second list.
126 let firstMostOuterItem;
127 // Check whether the removed content is between two lists.
128 this.listenTo(model, 'deleteContent', (evt, [selection]) => {
129 const firstPosition = selection.getFirstPosition();
130 const lastPosition = selection.getLastPosition();
131 // Typing or removing content in a single item. Aborting.
132 if (firstPosition.parent === lastPosition.parent) {
133 return;
134 }
135 // An element before the content that will be removed is not a list.
136 if (!firstPosition.parent.is('element', 'listItem')) {
137 return;
138 }
139 const nextSibling = lastPosition.parent.nextSibling;
140 // An element after the content that will be removed is not a list.
141 if (!nextSibling || !nextSibling.is('element', 'listItem')) {
142 return;
143 }
144 // Find the outermost list item based on the `listIndent` attribute. We can't assume that `listIndent=0`
145 // because the selection can be hooked in nested lists.
146 //
147 // <listItem listIndent="0" listType="bulleted" listStyle="square">UL List item 1</listItem>
148 // <listItem listIndent="1" listType="bulleted" listStyle="square">UL List [item 1.1</listItem>
149 // <listItem listIndent="0" listType="bulleted" listStyle="circle">[]UL List item 1.</listItem>
150 // <listItem listIndent="1" listType="bulleted" listStyle="circle">UL List ]item 1.1</listItem>
151 //
152 // After deleting the content, we would like to inherit the "square" attribute for the last element:
153 //
154 // <listItem listIndent="0" listType="bulleted" listStyle="square">UL List item 1</listItem>
155 // <listItem listIndent="1" listType="bulleted" listStyle="square">UL List []item 1.1</listItem>
156 const mostOuterItemList = getSiblingListItem(firstPosition.parent, {
157 sameIndent: true,
158 listIndent: nextSibling.getAttribute('listIndent')
159 });
160 // The outermost list item may not exist while removing elements between lists with different value
161 // of the `listIndent` attribute. In such a case we don't want to update anything. See: #8073.
162 if (!mostOuterItemList) {
163 return;
164 }
165 if (mostOuterItemList.getAttribute('listType') === nextSibling.getAttribute('listType')) {
166 firstMostOuterItem = mostOuterItemList;
167 }
168 }, { priority: 'high' });
169 // If so, update the `listStyle` attribute for the second list.
170 this.listenTo(model, 'deleteContent', () => {
171 if (!firstMostOuterItem) {
172 return;
173 }
174 model.change(writer => {
175 // Find the first most-outer item list in the merged list.
176 // A case when the first list item in the second list was merged into the last item in the first list.
177 //
178 // <listItem listIndent="0" listType="bulleted" listStyle="square">UL List item 1</listItem>
179 // <listItem listIndent="0" listType="bulleted" listStyle="square">UL List item 2</listItem>
180 // <listItem listIndent="0" listType="bulleted" listStyle="circle">[]UL List item 1</listItem>
181 // <listItem listIndent="0" listType="bulleted" listStyle="circle">UL List item 2</listItem>
182 const secondListMostOuterItem = getSiblingListItem(firstMostOuterItem.nextSibling, {
183 sameIndent: true,
184 listIndent: firstMostOuterItem.getAttribute('listIndent'),
185 direction: 'forward'
186 });
187 // If the selection ends in a non-list element, there are no <listItem>s that would require adjustments.
188 // See: #8642.
189 if (!secondListMostOuterItem) {
190 firstMostOuterItem = null;
191 return;
192 }
193 const items = [
194 secondListMostOuterItem,
195 ...getSiblingNodes(writer.createPositionAt(secondListMostOuterItem, 0), 'forward')
196 ];
197 for (const listItem of items) {
198 for (const strategy of attributeStrategies) {
199 if (strategy.appliesToListItem(listItem)) {
200 const attributeName = strategy.attributeName;
201 const value = firstMostOuterItem.getAttribute(attributeName);
202 writer.setAttribute(attributeName, value, listItem);
203 }
204 }
205 }
206 });
207 firstMostOuterItem = null;
208 }, { priority: 'low' });
209 }
210}
211/**
212 * Creates an array of strategies for dealing with enabled listItem attributes.
213 */
214function createAttributeStrategies(enabledProperties) {
215 const strategies = [];
216 if (enabledProperties.styles) {
217 strategies.push({
218 attributeName: 'listStyle',
219 defaultValue: DEFAULT_LIST_TYPE,
220 addCommand(editor) {
221 editor.commands.add('listStyle', new ListStyleCommand(editor, DEFAULT_LIST_TYPE));
222 },
223 appliesToListItem() {
224 return true;
225 },
226 setAttributeOnDowncast(writer, listStyle, element) {
227 if (listStyle && listStyle !== DEFAULT_LIST_TYPE) {
228 writer.setStyle('list-style-type', listStyle, element);
229 }
230 else {
231 writer.removeStyle('list-style-type', element);
232 }
233 },
234 getAttributeOnUpcast(listParent) {
235 return listParent.getStyle('list-style-type') || DEFAULT_LIST_TYPE;
236 }
237 });
238 }
239 if (enabledProperties.reversed) {
240 strategies.push({
241 attributeName: 'listReversed',
242 defaultValue: false,
243 addCommand(editor) {
244 editor.commands.add('listReversed', new ListReversedCommand(editor));
245 },
246 appliesToListItem(item) {
247 return item.getAttribute('listType') == 'numbered';
248 },
249 setAttributeOnDowncast(writer, listReversed, element) {
250 if (listReversed) {
251 writer.setAttribute('reversed', 'reversed', element);
252 }
253 else {
254 writer.removeAttribute('reversed', element);
255 }
256 },
257 getAttributeOnUpcast(listParent) {
258 return listParent.hasAttribute('reversed');
259 }
260 });
261 }
262 if (enabledProperties.startIndex) {
263 strategies.push({
264 attributeName: 'listStart',
265 defaultValue: 1,
266 addCommand(editor) {
267 editor.commands.add('listStart', new ListStartCommand(editor));
268 },
269 appliesToListItem(item) {
270 return item.getAttribute('listType') == 'numbered';
271 },
272 setAttributeOnDowncast(writer, listStart, element) {
273 if (listStart == 0 || listStart > 1) {
274 writer.setAttribute('start', listStart, element);
275 }
276 else {
277 writer.removeAttribute('start', element);
278 }
279 },
280 getAttributeOnUpcast(listParent) {
281 const startAttributeValue = listParent.getAttribute('start');
282 return startAttributeValue >= 0 ? startAttributeValue : 1;
283 }
284 });
285 }
286 return strategies;
287}
288/**
289 * Returns a converter consumes the `style`, `reversed` and `start` attribute.
290 * In `style` it searches for the `list-style-type` definition.
291 * If not found, the `"default"` value will be used.
292 */
293function upcastListItemAttributes(attributeStrategies) {
294 return (dispatcher) => {
295 dispatcher.on('element:li', (evt, data, conversionApi) => {
296 // https://github.com/ckeditor/ckeditor5/issues/13858
297 if (!data.modelRange) {
298 return;
299 }
300 const listParent = data.viewItem.parent;
301 const listItem = data.modelRange.start.nodeAfter || data.modelRange.end.nodeBefore;
302 for (const strategy of attributeStrategies) {
303 if (strategy.appliesToListItem(listItem)) {
304 const listStyle = strategy.getAttributeOnUpcast(listParent);
305 conversionApi.writer.setAttribute(strategy.attributeName, listStyle, listItem);
306 }
307 }
308 }, { priority: 'low' });
309 };
310}
311/**
312 * Returns a converter that adds `reversed`, `start` attributes and adds `list-style-type` definition as a value for the `style` attribute.
313 * The `"default"` values are removed and not present in the view/data.
314 */
315function downcastListItemAttributes(attributeStrategies) {
316 return (dispatcher) => {
317 for (const strategy of attributeStrategies) {
318 dispatcher.on(`attribute:${strategy.attributeName}:listItem`, (evt, data, conversionApi) => {
319 const viewWriter = conversionApi.writer;
320 const currentElement = data.item;
321 const previousElement = getSiblingListItem(currentElement.previousSibling, {
322 sameIndent: true,
323 listIndent: currentElement.getAttribute('listIndent'),
324 direction: 'backward'
325 });
326 const viewItem = conversionApi.mapper.toViewElement(currentElement);
327 // A case when elements represent different lists. We need to separate their container.
328 if (!areRepresentingSameList(currentElement, previousElement)) {
329 viewWriter.breakContainer(viewWriter.createPositionBefore(viewItem));
330 }
331 strategy.setAttributeOnDowncast(viewWriter, data.attributeNewValue, viewItem.parent);
332 }, { priority: 'low' });
333 }
334 };
335 /**
336 * Checks whether specified list items belong to the same list.
337 */
338 function areRepresentingSameList(listItem1, listItem2) {
339 return listItem2 &&
340 listItem1.getAttribute('listType') === listItem2.getAttribute('listType') &&
341 listItem1.getAttribute('listIndent') === listItem2.getAttribute('listIndent') &&
342 listItem1.getAttribute('listStyle') === listItem2.getAttribute('listStyle') &&
343 listItem1.getAttribute('listReversed') === listItem2.getAttribute('listReversed') &&
344 listItem1.getAttribute('listStart') === listItem2.getAttribute('listStart');
345 }
346}
347/**
348 * When indenting list, nested list should clear its value for the attributes or inherit from nested lists.
349 *
350 * ■ List item 1.
351 * ■ List item 2.[]
352 * ■ List item 3.
353 * editor.execute( 'indentList' );
354 *
355 * ■ List item 1.
356 * ○ List item 2.[]
357 * ■ List item 3.
358 */
359function fixListAfterIndentListCommand(editor, attributeStrategies) {
360 return (evt, changedItems) => {
361 const root = changedItems[0];
362 const rootIndent = root.getAttribute('listIndent');
363 const itemsToUpdate = changedItems.filter(item => item.getAttribute('listIndent') === rootIndent);
364 // A case where a few list items are indented must be checked separately
365 // since `getSiblingListItem()` returns the first changed element.
366 // ■ List item 1.
367 // ○ [List item 2.
368 // ○ List item 3.]
369 // ■ List item 4.
370 //
371 // List items: `2` and `3` should be adjusted.
372 let previousSibling = null;
373 if (root.previousSibling.getAttribute('listIndent') + 1 !== rootIndent) {
374 previousSibling = getSiblingListItem(root.previousSibling, {
375 sameIndent: true, direction: 'backward', listIndent: rootIndent
376 });
377 }
378 editor.model.change(writer => {
379 for (const item of itemsToUpdate) {
380 for (const strategy of attributeStrategies) {
381 if (strategy.appliesToListItem(item)) {
382 const valueToSet = previousSibling == null ?
383 strategy.defaultValue :
384 previousSibling.getAttribute(strategy.attributeName);
385 writer.setAttribute(strategy.attributeName, valueToSet, item);
386 }
387 }
388 }
389 });
390 };
391}
392/**
393 * When outdenting a list, a nested list should copy attribute values
394 * from the previous sibling list item including the same value for the `listIndent` value.
395 *
396 * ■ List item 1.
397 * ○ List item 2.[]
398 * ■ List item 3.
399 *
400 * editor.execute( 'outdentList' );
401 *
402 * ■ List item 1.
403 * ■ List item 2.[]
404 * ■ List item 3.
405 */
406function fixListAfterOutdentListCommand(editor, attributeStrategies) {
407 return (evt, changedItems) => {
408 changedItems = changedItems.reverse().filter(item => item.is('element', 'listItem'));
409 if (!changedItems.length) {
410 return;
411 }
412 const indent = changedItems[0].getAttribute('listIndent');
413 const listType = changedItems[0].getAttribute('listType');
414 let listItem = changedItems[0].previousSibling;
415 // ■ List item 1.
416 // ○ List item 2.
417 // ○ List item 3.[]
418 // ■ List item 4.
419 //
420 // After outdenting a list, `List item 3` should inherit the `listStyle` attribute from `List item 1`.
421 //
422 // ■ List item 1.
423 // ○ List item 2.
424 // ■ List item 3.[]
425 // ■ List item 4.
426 if (listItem.is('element', 'listItem')) {
427 while (listItem.getAttribute('listIndent') !== indent) {
428 listItem = listItem.previousSibling;
429 }
430 }
431 else {
432 listItem = null;
433 }
434 // Outdenting such a list should restore values based on `List item 4`.
435 // ■ List item 1.[]
436 // ○ List item 2.
437 // ○ List item 3.
438 // ■ List item 4.
439 if (!listItem) {
440 listItem = changedItems[changedItems.length - 1].nextSibling;
441 }
442 // And such a list should not modify anything.
443 // However, `listItem` can indicate a node below the list. Be sure that we have the `listItem` element.
444 // ■ List item 1.[]
445 // ○ List item 2.
446 // ○ List item 3.
447 // <paragraph>The later if check.</paragraph>
448 if (!listItem || !listItem.is('element', 'listItem')) {
449 return;
450 }
451 // Do not modify the list if found `listItem` represents other type of list than outdented list items.
452 if (listItem.getAttribute('listType') !== listType) {
453 return;
454 }
455 editor.model.change(writer => {
456 const itemsToUpdate = changedItems.filter(item => item.getAttribute('listIndent') === indent);
457 for (const item of itemsToUpdate) {
458 for (const strategy of attributeStrategies) {
459 if (strategy.appliesToListItem(item)) {
460 const attributeName = strategy.attributeName;
461 const valueToSet = listItem.getAttribute(attributeName);
462 writer.setAttribute(attributeName, valueToSet, item);
463 }
464 }
465 }
466 });
467 };
468}
469/**
470 * Each `listItem` element must have specified the `listStyle`, `listReversed` and `listStart` attributes
471 * if they are enabled and supported by its `listType`.
472 * This post-fixer checks whether inserted elements `listItem` elements should inherit the attribute values from
473 * their sibling nodes or should use the default values.
474 *
475 * Paragraph[]
476 * ■ List item 1. // [listStyle="square", listType="bulleted"]
477 * ■ List item 2. // ...
478 * ■ List item 3. // ...
479 *
480 * editor.execute( 'bulletedList' )
481 *
482 * ■ Paragraph[] // [listStyle="square", listType="bulleted"]
483 * ■ List item 1. // [listStyle="square", listType="bulleted"]
484 * ■ List item 2.
485 * ■ List item 3.
486 *
487 * It also covers a such change:
488 *
489 * [Paragraph 1
490 * Paragraph 2]
491 * ■ List item 1. // [listStyle="square", listType="bulleted"]
492 * ■ List item 2. // ...
493 * ■ List item 3. // ...
494 *
495 * editor.execute( 'numberedList' )
496 *
497 * 1. [Paragraph 1 // [listStyle="default", listType="numbered"]
498 * 2. Paragraph 2] // [listStyle="default", listType="numbered"]
499 * ■ List item 1. // [listStyle="square", listType="bulleted"]
500 * ■ List item 2. // ...
501 * ■ List item 3. // ...
502 */
503function fixListAttributesOnListItemElements(editor, attributeStrategies) {
504 return (writer) => {
505 let wasFixed = false;
506 const insertedListItems = getChangedListItems(editor.model.document.differ.getChanges())
507 .filter(item => {
508 // Don't touch todo lists. They are handled in another post-fixer.
509 return item.getAttribute('listType') !== 'todo';
510 });
511 if (!insertedListItems.length) {
512 return wasFixed;
513 }
514 // Check whether the last inserted element is next to the `listItem` element.
515 //
516 // ■ Paragraph[] // <-- The inserted item.
517 // ■ List item 1.
518 let existingListItem = insertedListItems[insertedListItems.length - 1].nextSibling;
519 // If it doesn't, maybe the `listItem` was inserted at the end of the list.
520 //
521 // ■ List item 1.
522 // ■ Paragraph[] // <-- The inserted item.
523 if (!existingListItem || !existingListItem.is('element', 'listItem')) {
524 existingListItem = insertedListItems[0].previousSibling;
525 if (existingListItem) {
526 const indent = insertedListItems[0].getAttribute('listIndent');
527 // But we need to find a `listItem` with the `listIndent=0` attribute.
528 // If doesn't, maybe the `listItem` was inserted at the end of the list.
529 //
530 // ■ List item 1.
531 // ○ List item 2.
532 // ■ Paragraph[] // <-- The inserted item.
533 while (existingListItem.is('element', 'listItem') && existingListItem.getAttribute('listIndent') !== indent) {
534 existingListItem = existingListItem.previousSibling;
535 // If the item does not exist, most probably there is no other content in the editor. See: #8072.
536 if (!existingListItem) {
537 break;
538 }
539 }
540 }
541 }
542 for (const strategy of attributeStrategies) {
543 const attributeName = strategy.attributeName;
544 for (const item of insertedListItems) {
545 if (!strategy.appliesToListItem(item)) {
546 writer.removeAttribute(attributeName, item);
547 continue;
548 }
549 if (!item.hasAttribute(attributeName)) {
550 if (shouldInheritListType(existingListItem, item, strategy)) {
551 writer.setAttribute(attributeName, existingListItem.getAttribute(attributeName), item);
552 }
553 else {
554 writer.setAttribute(attributeName, strategy.defaultValue, item);
555 }
556 wasFixed = true;
557 }
558 else {
559 // Adjust the `listStyle`, `listReversed` and `listStart`
560 // attributes for inserted (pasted) items. See #8160.
561 //
562 // ■ List item 1. // [listStyle="square", listType="bulleted"]
563 // ○ List item 1.1. // [listStyle="circle", listType="bulleted"]
564 // ○ [] (selection is here)
565 //
566 // Then, pasting a list with different attributes (listStyle, listType):
567 //
568 // 1. First. // [listStyle="decimal", listType="numbered"]
569 // 2. Second // [listStyle="decimal", listType="numbered"]
570 //
571 // The `listType` attribute will be corrected by the `ListEditing` converters.
572 // We need to adjust the `listStyle` attribute. Expected structure:
573 //
574 // ■ List item 1. // [listStyle="square", listType="bulleted"]
575 // ○ List item 1.1. // [listStyle="circle", listType="bulleted"]
576 // ○ First. // [listStyle="circle", listType="bulleted"]
577 // ○ Second // [listStyle="circle", listType="bulleted"]
578 const previousSibling = item.previousSibling;
579 if (shouldInheritListTypeFromPreviousItem(previousSibling, item, strategy.attributeName)) {
580 writer.setAttribute(attributeName, previousSibling.getAttribute(attributeName), item);
581 wasFixed = true;
582 }
583 }
584 }
585 }
586 return wasFixed;
587 };
588}
589/**
590 * Checks whether the `listStyle`, `listReversed` and `listStart` attributes
591 * should be copied from the `baseItem` element.
592 *
593 * The attribute should be copied if the inserted element does not have defined it and
594 * the value for the element is other than default in the base element.
595 */
596function shouldInheritListType(baseItem, itemToChange, attributeStrategy) {
597 if (!baseItem) {
598 return false;
599 }
600 const baseListAttribute = baseItem.getAttribute(attributeStrategy.attributeName);
601 if (!baseListAttribute) {
602 return false;
603 }
604 if (baseListAttribute == attributeStrategy.defaultValue) {
605 return false;
606 }
607 if (baseItem.getAttribute('listType') !== itemToChange.getAttribute('listType')) {
608 return false;
609 }
610 return true;
611}
612/**
613 * Checks whether the `listStyle`, `listReversed` and `listStart` attributes
614 * should be copied from previous list item.
615 *
616 * The attribute should be copied if there's a mismatch of styles of the pasted list into a nested list.
617 * Top-level lists are not normalized as we allow side-by-side list of different types.
618 */
619function shouldInheritListTypeFromPreviousItem(previousItem, itemToChange, attributeName) {
620 if (!previousItem || !previousItem.is('element', 'listItem')) {
621 return false;
622 }
623 if (itemToChange.getAttribute('listType') !== previousItem.getAttribute('listType')) {
624 return false;
625 }
626 const previousItemIndent = previousItem.getAttribute('listIndent');
627 if (previousItemIndent < 1 || previousItemIndent !== itemToChange.getAttribute('listIndent')) {
628 return false;
629 }
630 const previousItemListAttribute = previousItem.getAttribute(attributeName);
631 if (!previousItemListAttribute || previousItemListAttribute === itemToChange.getAttribute(attributeName)) {
632 return false;
633 }
634 return true;
635}
636/**
637 * Removes the `listStyle`, `listReversed` and `listStart` attributes from "todo" list items.
638 */
639function removeListItemAttributesFromTodoList(editor) {
640 return (writer) => {
641 const todoListItems = getChangedListItems(editor.model.document.differ.getChanges())
642 .filter(item => {
643 // Handle the todo lists only. The rest is handled in another post-fixer.
644 return item.getAttribute('listType') === 'todo' && (item.hasAttribute('listStyle') ||
645 item.hasAttribute('listReversed') ||
646 item.hasAttribute('listStart'));
647 });
648 if (!todoListItems.length) {
649 return false;
650 }
651 for (const item of todoListItems) {
652 writer.removeAttribute('listStyle', item);
653 writer.removeAttribute('listReversed', item);
654 writer.removeAttribute('listStart', item);
655 }
656 return true;
657 };
658}
659/**
660 * Restores the `listStyle` attribute after changing the list type.
661 */
662function restoreDefaultListStyle(editor) {
663 return (evt, changedItems) => {
664 changedItems = changedItems.filter(item => item.is('element', 'listItem'));
665 editor.model.change(writer => {
666 for (const item of changedItems) {
667 // Remove the attribute. Post-fixer will restore the proper value.
668 writer.removeAttribute('listStyle', item);
669 }
670 });
671 };
672}
673/**
674 * Returns the `listItem` that was inserted or changed.
675 *
676 * @param changes The changes list returned by the differ.
677 */
678function getChangedListItems(changes) {
679 const items = [];
680 for (const change of changes) {
681 const item = getItemFromChange(change);
682 if (item && item.is('element', 'listItem')) {
683 items.push(item);
684 }
685 }
686 return items;
687}
688function getItemFromChange(change) {
689 if (change.type === 'attribute') {
690 return change.range.start.nodeAfter;
691 }
692 if (change.type === 'insert') {
693 return change.position.nodeAfter;
694 }
695 return null;
696}