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/converters
|
8 | */
|
9 |
|
10 | import { TreeWalker } from 'ckeditor5/src/engine';
|
11 |
|
12 | import {
|
13 | generateLiInUl,
|
14 | injectViewList,
|
15 | mergeViewLists,
|
16 | getSiblingListItem,
|
17 | positionAfterUiElements
|
18 | } from './utils';
|
19 |
|
20 | /**
|
21 | * A model-to-view converter for the `listItem` model element insertion.
|
22 | *
|
23 | * It creates a `<ul><li></li><ul>` (or `<ol>`) view structure out of a `listItem` model element, inserts it at the correct
|
24 | * position, and merges the list with surrounding lists (if available).
|
25 | *
|
26 | * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert
|
27 | * @param {module:engine/model/model~Model} model Model instance.
|
28 | * @returns {Function} Returns a conversion callback.
|
29 | */
|
30 | export function modelViewInsertion( model ) {
|
31 | return ( evt, data, conversionApi ) => {
|
32 | const consumable = conversionApi.consumable;
|
33 |
|
34 | if ( !consumable.test( data.item, 'insert' ) ||
|
35 | !consumable.test( data.item, 'attribute:listType' ) ||
|
36 | !consumable.test( data.item, 'attribute:listIndent' )
|
37 | ) {
|
38 | return;
|
39 | }
|
40 |
|
41 | consumable.consume( data.item, 'insert' );
|
42 | consumable.consume( data.item, 'attribute:listType' );
|
43 | consumable.consume( data.item, 'attribute:listIndent' );
|
44 |
|
45 | const modelItem = data.item;
|
46 | const viewItem = generateLiInUl( modelItem, conversionApi );
|
47 |
|
48 | injectViewList( modelItem, viewItem, conversionApi, model );
|
49 | };
|
50 | }
|
51 |
|
52 | /**
|
53 | * A model-to-view converter for the `listItem` model element removal.
|
54 | *
|
55 | * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:remove
|
56 | * @param {module:engine/model/model~Model} model Model instance.
|
57 | * @returns {Function} Returns a conversion callback.
|
58 | */
|
59 | export function modelViewRemove( model ) {
|
60 | return ( evt, data, conversionApi ) => {
|
61 | const viewPosition = conversionApi.mapper.toViewPosition( data.position );
|
62 | const viewStart = viewPosition.getLastMatchingPosition( value => !value.item.is( 'element', 'li' ) );
|
63 | const viewItem = viewStart.nodeAfter;
|
64 | const viewWriter = conversionApi.writer;
|
65 |
|
66 | // 1. Break the container after and before the list item.
|
67 | // This will create a view list with one view list item - the one to remove.
|
68 | viewWriter.breakContainer( viewWriter.createPositionBefore( viewItem ) );
|
69 | viewWriter.breakContainer( viewWriter.createPositionAfter( viewItem ) );
|
70 |
|
71 | // 2. Remove the list with the item to remove.
|
72 | const viewList = viewItem.parent;
|
73 | const viewListPrev = viewList.previousSibling;
|
74 | const removeRange = viewWriter.createRangeOn( viewList );
|
75 | const removed = viewWriter.remove( removeRange );
|
76 |
|
77 | // 3. Merge the whole created by breaking and removing the list.
|
78 | if ( viewListPrev && viewListPrev.nextSibling ) {
|
79 | mergeViewLists( viewWriter, viewListPrev, viewListPrev.nextSibling );
|
80 | }
|
81 |
|
82 | // 4. Bring back nested list that was in the removed <li>.
|
83 | const modelItem = conversionApi.mapper.toModelElement( viewItem );
|
84 |
|
85 | hoistNestedLists( modelItem.getAttribute( 'listIndent' ) + 1, data.position, removeRange.start, viewItem, conversionApi, model );
|
86 |
|
87 | // 5. Unbind removed view item and all children.
|
88 | for ( const child of viewWriter.createRangeIn( removed ).getItems() ) {
|
89 | conversionApi.mapper.unbindViewElement( child );
|
90 | }
|
91 |
|
92 | evt.stop();
|
93 | };
|
94 | }
|
95 |
|
96 | /**
|
97 | * A model-to-view converter for the `type` attribute change on the `listItem` model element.
|
98 | *
|
99 | * This change means that the `<li>` element parent changes from `<ul>` to `<ol>` (or vice versa). This is accomplished
|
100 | * by breaking view elements and changing their name. The next {@link module:list/list/converters~modelViewMergeAfterChangeType}
|
101 | * converter will attempt to merge split nodes.
|
102 | *
|
103 | * Splitting this conversion into 2 steps makes it possible to add an additional conversion in the middle.
|
104 | * Check {@link module:list/todolist/todolistconverters~modelViewChangeType} to see an example of it.
|
105 | *
|
106 | * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute
|
107 | * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
|
108 | * @param {Object} data Additional information about the change.
|
109 | * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface.
|
110 | */
|
111 | export function modelViewChangeType( evt, data, conversionApi ) {
|
112 | if ( !conversionApi.consumable.test( data.item, evt.name ) ) {
|
113 | return;
|
114 | }
|
115 |
|
116 | const viewItem = conversionApi.mapper.toViewElement( data.item );
|
117 | const viewWriter = conversionApi.writer;
|
118 |
|
119 | // Break the container after and before the list item.
|
120 | // This will create a view list with one view list item -- the one that changed type.
|
121 | viewWriter.breakContainer( viewWriter.createPositionBefore( viewItem ) );
|
122 | viewWriter.breakContainer( viewWriter.createPositionAfter( viewItem ) );
|
123 |
|
124 | // Change name of the view list that holds the changed view item.
|
125 | // We cannot just change name property, because that would not render properly.
|
126 | const viewList = viewItem.parent;
|
127 | const listName = data.attributeNewValue == 'numbered' ? 'ol' : 'ul';
|
128 |
|
129 | viewWriter.rename( listName, viewList );
|
130 | }
|
131 |
|
132 | /**
|
133 | * A model-to-view converter that attempts to merge nodes split by {@link module:list/list/converters~modelViewChangeType}.
|
134 | *
|
135 | * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute
|
136 | * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
|
137 | * @param {Object} data Additional information about the change.
|
138 | * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface.
|
139 | */
|
140 | export function modelViewMergeAfterChangeType( evt, data, conversionApi ) {
|
141 | conversionApi.consumable.consume( data.item, evt.name );
|
142 |
|
143 | const viewItem = conversionApi.mapper.toViewElement( data.item );
|
144 | const viewList = viewItem.parent;
|
145 | const viewWriter = conversionApi.writer;
|
146 |
|
147 | // Merge the changed view list with other lists, if possible.
|
148 | mergeViewLists( viewWriter, viewList, viewList.nextSibling );
|
149 | mergeViewLists( viewWriter, viewList.previousSibling, viewList );
|
150 | }
|
151 |
|
152 | /**
|
153 | * A model-to-view converter for the `listIndent` attribute change on the `listItem` model element.
|
154 | *
|
155 | * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute
|
156 | * @param {module:engine/model/model~Model} model Model instance.
|
157 | * @returns {Function} Returns a conversion callback.
|
158 | */
|
159 | export function modelViewChangeIndent( model ) {
|
160 | return ( evt, data, conversionApi ) => {
|
161 | if ( !conversionApi.consumable.consume( data.item, 'attribute:listIndent' ) ) {
|
162 | return;
|
163 | }
|
164 |
|
165 | const viewItem = conversionApi.mapper.toViewElement( data.item );
|
166 | const viewWriter = conversionApi.writer;
|
167 |
|
168 | // 1. Break the container after and before the list item.
|
169 | // This will create a view list with one view list item -- the one that changed type.
|
170 | viewWriter.breakContainer( viewWriter.createPositionBefore( viewItem ) );
|
171 | viewWriter.breakContainer( viewWriter.createPositionAfter( viewItem ) );
|
172 |
|
173 | // 2. Extract view list with changed view list item and merge "hole" possibly created by breaking and removing elements.
|
174 | const viewList = viewItem.parent;
|
175 | const viewListPrev = viewList.previousSibling;
|
176 | const removeRange = viewWriter.createRangeOn( viewList );
|
177 | viewWriter.remove( removeRange );
|
178 |
|
179 | if ( viewListPrev && viewListPrev.nextSibling ) {
|
180 | mergeViewLists( viewWriter, viewListPrev, viewListPrev.nextSibling );
|
181 | }
|
182 |
|
183 | // 3. Bring back nested list that was in the removed <li>.
|
184 | hoistNestedLists( data.attributeOldValue + 1, data.range.start, removeRange.start, viewItem, conversionApi, model );
|
185 |
|
186 | // 4. Inject view list like it is newly inserted.
|
187 | injectViewList( data.item, viewItem, conversionApi, model );
|
188 |
|
189 | // 5. Consume insertion of children inside the item. They are already handled by re-building the item in view.
|
190 | for ( const child of data.item.getChildren() ) {
|
191 | conversionApi.consumable.consume( child, 'insert' );
|
192 | }
|
193 | };
|
194 | }
|
195 |
|
196 | /**
|
197 | * A special model-to-view converter introduced by the {@link module:list/list~List list feature}. This converter is fired for
|
198 | * insert change of every model item, and should be fired before the actual converter. The converter checks whether the inserted
|
199 | * model item is a non-`listItem` element. If it is, and it is inserted inside a view list, the converter breaks the
|
200 | * list so the model element is inserted to the view parent element corresponding to its model parent element.
|
201 | *
|
202 | * The converter prevents such situations:
|
203 | *
|
204 | * // Model: // View:
|
205 | * <listItem>foo</listItem> <ul>
|
206 | * <listItem>bar</listItem> <li>foo</li>
|
207 | * <li>bar</li>
|
208 | * </ul>
|
209 | *
|
210 | * // After change: // Correct view guaranteed by this converter:
|
211 | * <listItem>foo</listItem> <ul><li>foo</li></ul><p>xxx</p><ul><li>bar</li></ul>
|
212 | * <paragraph>xxx</paragraph> // Instead of this wrong view state:
|
213 | * <listItem>bar</listItem> <ul><li>foo</li><p>xxx</p><li>bar</li></ul>
|
214 | *
|
215 | * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert
|
216 | * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
|
217 | * @param {Object} data Additional information about the change.
|
218 | * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface.
|
219 | */
|
220 | export function modelViewSplitOnInsert( evt, data, conversionApi ) {
|
221 | if ( !conversionApi.consumable.test( data.item, evt.name ) ) {
|
222 | return;
|
223 | }
|
224 |
|
225 | if ( data.item.name != 'listItem' ) {
|
226 | let viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
|
227 |
|
228 | const viewWriter = conversionApi.writer;
|
229 | const lists = [];
|
230 |
|
231 | // Break multiple ULs/OLs if there are.
|
232 | //
|
233 | // Imagine following list:
|
234 | //
|
235 | // 1 --------
|
236 | // 1.1 --------
|
237 | // 1.1.1 --------
|
238 | // 1.1.2 --------
|
239 | // 1.1.3 --------
|
240 | // 1.1.3.1 --------
|
241 | // 1.2 --------
|
242 | // 1.2.1 --------
|
243 | // 2 --------
|
244 | //
|
245 | // Insert paragraph after item 1.1.1:
|
246 | //
|
247 | // 1 --------
|
248 | // 1.1 --------
|
249 | // 1.1.1 --------
|
250 | //
|
251 | // Lorem ipsum.
|
252 | //
|
253 | // 1.1.2 --------
|
254 | // 1.1.3 --------
|
255 | // 1.1.3.1 --------
|
256 | // 1.2 --------
|
257 | // 1.2.1 --------
|
258 | // 2 --------
|
259 | //
|
260 | // In this case 1.1.2 has to become beginning of a new list.
|
261 | // We need to break list before 1.1.2 (obvious), then we need to break list also before 1.2.
|
262 | // Then we need to move those broken pieces one after another and merge:
|
263 | //
|
264 | // 1 --------
|
265 | // 1.1 --------
|
266 | // 1.1.1 --------
|
267 | //
|
268 | // Lorem ipsum.
|
269 | //
|
270 | // 1.1.2 --------
|
271 | // 1.1.3 --------
|
272 | // 1.1.3.1 --------
|
273 | // 1.2 --------
|
274 | // 1.2.1 --------
|
275 | // 2 --------
|
276 | //
|
277 | while ( viewPosition.parent.name == 'ul' || viewPosition.parent.name == 'ol' ) {
|
278 | viewPosition = viewWriter.breakContainer( viewPosition );
|
279 |
|
280 | if ( viewPosition.parent.name != 'li' ) {
|
281 | break;
|
282 | }
|
283 |
|
284 | // Remove lists that are after inserted element.
|
285 | // They will be brought back later, below the inserted element.
|
286 | const removeStart = viewPosition;
|
287 | const removeEnd = viewWriter.createPositionAt( viewPosition.parent, 'end' );
|
288 |
|
289 | // Don't remove if there is nothing to remove.
|
290 | if ( !removeStart.isEqual( removeEnd ) ) {
|
291 | const removed = viewWriter.remove( viewWriter.createRange( removeStart, removeEnd ) );
|
292 | lists.push( removed );
|
293 | }
|
294 |
|
295 | viewPosition = viewWriter.createPositionAfter( viewPosition.parent );
|
296 | }
|
297 |
|
298 | // Bring back removed lists.
|
299 | if ( lists.length > 0 ) {
|
300 | for ( let i = 0; i < lists.length; i++ ) {
|
301 | const previousList = viewPosition.nodeBefore;
|
302 | const insertedRange = viewWriter.insert( viewPosition, lists[ i ] );
|
303 | viewPosition = insertedRange.end;
|
304 |
|
305 | // Don't merge first list! We want a split in that place (this is why this converter is introduced).
|
306 | if ( i > 0 ) {
|
307 | const mergePos = mergeViewLists( viewWriter, previousList, previousList.nextSibling );
|
308 |
|
309 | // If `mergePos` is in `previousList` it means that the lists got merged.
|
310 | // In this case, we need to fix insert position.
|
311 | if ( mergePos && mergePos.parent == previousList ) {
|
312 | viewPosition.offset--;
|
313 | }
|
314 | }
|
315 | }
|
316 |
|
317 | // Merge last inserted list with element after it.
|
318 | mergeViewLists( viewWriter, viewPosition.nodeBefore, viewPosition.nodeAfter );
|
319 | }
|
320 | }
|
321 | }
|
322 |
|
323 | /**
|
324 | * A special model-to-view converter introduced by the {@link module:list/list~List list feature}. This converter takes care of
|
325 | * merging view lists after something is removed or moved from near them.
|
326 | *
|
327 | * Example:
|
328 | *
|
329 | * // Model: // View:
|
330 | * <listItem>foo</listItem> <ul><li>foo</li></ul>
|
331 | * <paragraph>xxx</paragraph> <p>xxx</p>
|
332 | * <listItem>bar</listItem> <ul><li>bar</li></ul>
|
333 | *
|
334 | * // After change: // Correct view guaranteed by this converter:
|
335 | * <listItem>foo</listItem> <ul>
|
336 | * <listItem>bar</listItem> <li>foo</li>
|
337 | * <li>bar</li>
|
338 | * </ul>
|
339 | *
|
340 | * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:remove
|
341 | * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
|
342 | * @param {Object} data Additional information about the change.
|
343 | * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface.
|
344 | */
|
345 | export function modelViewMergeAfter( evt, data, conversionApi ) {
|
346 | const viewPosition = conversionApi.mapper.toViewPosition( data.position );
|
347 | const viewItemPrev = viewPosition.nodeBefore;
|
348 | const viewItemNext = viewPosition.nodeAfter;
|
349 |
|
350 | // Merge lists if something (remove, move) was done from inside of list.
|
351 | // Merging will be done only if both items are view lists of the same type.
|
352 | // The check is done inside the helper function.
|
353 | mergeViewLists( conversionApi.writer, viewItemPrev, viewItemNext );
|
354 | }
|
355 |
|
356 | /**
|
357 | * A view-to-model converter that converts the `<li>` view elements into the `listItem` model elements.
|
358 | *
|
359 | * To set correct values of the `listType` and `listIndent` attributes the converter:
|
360 | * * checks `<li>`'s parent,
|
361 | * * stores and increases the `conversionApi.store.indent` value when `<li>`'s sub-items are converted.
|
362 | *
|
363 | * @see module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
|
364 | * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
|
365 | * @param {Object} data An object containing conversion input and a placeholder for conversion output and possibly other values.
|
366 | * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion interface to be used by the callback.
|
367 | */
|
368 | export function viewModelConverter( evt, data, conversionApi ) {
|
369 | if ( conversionApi.consumable.consume( data.viewItem, { name: true } ) ) {
|
370 | const writer = conversionApi.writer;
|
371 |
|
372 | // 1. Create `listItem` model element.
|
373 | const listItem = writer.createElement( 'listItem' );
|
374 |
|
375 | // 2. Handle `listItem` model element attributes.
|
376 | const indent = getIndent( data.viewItem );
|
377 |
|
378 | writer.setAttribute( 'listIndent', indent, listItem );
|
379 |
|
380 | // Set 'bulleted' as default. If this item is pasted into a context,
|
381 | const type = data.viewItem.parent && data.viewItem.parent.name == 'ol' ? 'numbered' : 'bulleted';
|
382 | writer.setAttribute( 'listType', type, listItem );
|
383 |
|
384 | if ( !conversionApi.safeInsert( listItem, data.modelCursor ) ) {
|
385 | return;
|
386 | }
|
387 |
|
388 | const nextPosition = viewToModelListItemChildrenConverter( listItem, data.viewItem.getChildren(), conversionApi );
|
389 |
|
390 | // Result range starts before the first item and ends after the last.
|
391 | data.modelRange = writer.createRange( data.modelCursor, nextPosition );
|
392 |
|
393 | conversionApi.updateConversionResult( listItem, data );
|
394 | }
|
395 | }
|
396 |
|
397 | /**
|
398 | * A view-to-model converter for the `<ul>` and `<ol>` view elements that cleans the input view of garbage.
|
399 | * This is mostly to clean whitespaces from between the `<li>` view elements inside the view list element, however, also
|
400 | * incorrect data can be cleared if the view was incorrect.
|
401 | *
|
402 | * @see module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
|
403 | * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
|
404 | * @param {Object} data An object containing conversion input and a placeholder for conversion output and possibly other values.
|
405 | * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion interface to be used by the callback.
|
406 | */
|
407 | export function cleanList( evt, data, conversionApi ) {
|
408 | if ( conversionApi.consumable.test( data.viewItem, { name: true } ) ) {
|
409 | // Caching children because when we start removing them iterating fails.
|
410 | const children = Array.from( data.viewItem.getChildren() );
|
411 |
|
412 | for ( const child of children ) {
|
413 | const isWrongElement = !( child.is( 'element', 'li' ) || isList( child ) );
|
414 |
|
415 | if ( isWrongElement ) {
|
416 | child._remove();
|
417 | }
|
418 | }
|
419 | }
|
420 | }
|
421 |
|
422 | /**
|
423 | * A view-to-model converter for the `<li>` elements that cleans whitespace formatting from the input view.
|
424 | *
|
425 | * @see module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
|
426 | * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
|
427 | * @param {Object} data An object containing conversion input and a placeholder for conversion output and possibly other values.
|
428 | * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion interface to be used by the callback.
|
429 | */
|
430 | export function cleanListItem( evt, data, conversionApi ) {
|
431 | if ( conversionApi.consumable.test( data.viewItem, { name: true } ) ) {
|
432 | if ( data.viewItem.childCount === 0 ) {
|
433 | return;
|
434 | }
|
435 |
|
436 | const children = [ ...data.viewItem.getChildren() ];
|
437 |
|
438 | let foundList = false;
|
439 |
|
440 | for ( const child of children ) {
|
441 | if ( foundList && !isList( child ) ) {
|
442 | child._remove();
|
443 | }
|
444 |
|
445 | if ( isList( child ) ) {
|
446 | // If this is a <ul> or <ol>, do not process it, just mark that we already visited list element.
|
447 | foundList = true;
|
448 | }
|
449 | }
|
450 | }
|
451 | }
|
452 |
|
453 | /**
|
454 | * Returns a callback for model position to view position mapping for {@link module:engine/conversion/mapper~Mapper}. The callback fixes
|
455 | * positions between the `listItem` elements that would be incorrectly mapped because of how list items are represented in the model
|
456 | * and in the view.
|
457 | *
|
458 | * @see module:engine/conversion/mapper~Mapper#event:modelToViewPosition
|
459 | * @param {module:engine/view/view~View} view A view instance.
|
460 | * @returns {Function}
|
461 | */
|
462 | export function modelToViewPosition( view ) {
|
463 | return ( evt, data ) => {
|
464 | if ( data.isPhantom ) {
|
465 | return;
|
466 | }
|
467 |
|
468 | const modelItem = data.modelPosition.nodeBefore;
|
469 |
|
470 | if ( modelItem && modelItem.is( 'element', 'listItem' ) ) {
|
471 | const viewItem = data.mapper.toViewElement( modelItem );
|
472 | const topmostViewList = viewItem.getAncestors().find( isList );
|
473 | const walker = view.createPositionAt( viewItem, 0 ).getWalker();
|
474 |
|
475 | for ( const value of walker ) {
|
476 | if ( value.type == 'elementStart' && value.item.is( 'element', 'li' ) ) {
|
477 | data.viewPosition = value.previousPosition;
|
478 |
|
479 | break;
|
480 | } else if ( value.type == 'elementEnd' && value.item == topmostViewList ) {
|
481 | data.viewPosition = value.nextPosition;
|
482 |
|
483 | break;
|
484 | }
|
485 | }
|
486 | }
|
487 | };
|
488 | }
|
489 |
|
490 | /**
|
491 | * The callback for view position to model position mapping for {@link module:engine/conversion/mapper~Mapper}. The callback fixes
|
492 | * positions between the `<li>` elements that would be incorrectly mapped because of how list items are represented in the model
|
493 | * and in the view.
|
494 | *
|
495 | * @see module:engine/conversion/mapper~Mapper#event:viewToModelPosition
|
496 | * @param {module:engine/model/model~Model} model Model instance.
|
497 | * @returns {Function} Returns a conversion callback.
|
498 | */
|
499 | export function viewToModelPosition( model ) {
|
500 | return ( evt, data ) => {
|
501 | const viewPos = data.viewPosition;
|
502 | const viewParent = viewPos.parent;
|
503 | const mapper = data.mapper;
|
504 |
|
505 | if ( viewParent.name == 'ul' || viewParent.name == 'ol' ) {
|
506 | // Position is directly in <ul> or <ol>.
|
507 | if ( !viewPos.isAtEnd ) {
|
508 | // If position is not at the end, it must be before <li>.
|
509 | // Get that <li>, map it to `listItem` and set model position before that `listItem`.
|
510 | const modelNode = mapper.toModelElement( viewPos.nodeAfter );
|
511 |
|
512 | data.modelPosition = model.createPositionBefore( modelNode );
|
513 | } else {
|
514 | // Position is at the end of <ul> or <ol>, so there is no <li> after it to be mapped.
|
515 | // There is <li> before the position, but we cannot just map it to `listItem` and set model position after it,
|
516 | // because that <li> may contain nested items.
|
517 | // We will check "model length" of that <li>, in other words - how many `listItem`s are in that <li>.
|
518 | const modelNode = mapper.toModelElement( viewPos.nodeBefore );
|
519 | const modelLength = mapper.getModelLength( viewPos.nodeBefore );
|
520 |
|
521 | // Then we get model position before mapped `listItem` and shift it accordingly.
|
522 | data.modelPosition = model.createPositionBefore( modelNode ).getShiftedBy( modelLength );
|
523 | }
|
524 |
|
525 | evt.stop();
|
526 | } else if (
|
527 | viewParent.name == 'li' &&
|
528 | viewPos.nodeBefore &&
|
529 | ( viewPos.nodeBefore.name == 'ul' || viewPos.nodeBefore.name == 'ol' )
|
530 | ) {
|
531 | // In most cases when view position is in <li> it is in text and this is a correct position.
|
532 | // However, if position is after <ul> or <ol> we have to fix it -- because in model <ul>/<ol> are not in the `listItem`.
|
533 | const modelNode = mapper.toModelElement( viewParent );
|
534 |
|
535 | // Check all <ul>s and <ol>s that are in the <li> but before mapped position.
|
536 | // Get model length of those elements and then add it to the offset of `listItem` mapped to the original <li>.
|
537 | let modelLength = 1; // Starts from 1 because the original <li> has to be counted in too.
|
538 | let viewList = viewPos.nodeBefore;
|
539 |
|
540 | while ( viewList && isList( viewList ) ) {
|
541 | modelLength += mapper.getModelLength( viewList );
|
542 |
|
543 | viewList = viewList.previousSibling;
|
544 | }
|
545 |
|
546 | data.modelPosition = model.createPositionBefore( modelNode ).getShiftedBy( modelLength );
|
547 |
|
548 | evt.stop();
|
549 | }
|
550 | };
|
551 | }
|
552 |
|
553 | /**
|
554 | * Post-fixer that reacts to changes on document and fixes incorrect model states.
|
555 | *
|
556 | * In the example below, there is a correct list structure.
|
557 | * Then the middle element is removed so the list structure will become incorrect:
|
558 | *
|
559 | * <listItem listType="bulleted" listIndent=0>Item 1</listItem>
|
560 | * <listItem listType="bulleted" listIndent=1>Item 2</listItem> <--- this is removed.
|
561 | * <listItem listType="bulleted" listIndent=2>Item 3</listItem>
|
562 | *
|
563 | * The list structure after the middle element is removed:
|
564 | *
|
565 | * <listItem listType="bulleted" listIndent=0>Item 1</listItem>
|
566 | * <listItem listType="bulleted" listIndent=2>Item 3</listItem>
|
567 | *
|
568 | * Should become:
|
569 | *
|
570 | * <listItem listType="bulleted" listIndent=0>Item 1</listItem>
|
571 | * <listItem listType="bulleted" listIndent=1>Item 3</listItem> <--- note that indent got post-fixed.
|
572 | *
|
573 | * @param {module:engine/model/model~Model} model The data model.
|
574 | * @param {module:engine/model/writer~Writer} writer The writer to do changes with.
|
575 | * @returns {Boolean} `true` if any change has been applied, `false` otherwise.
|
576 | */
|
577 | export function modelChangePostFixer( model, writer ) {
|
578 | const changes = model.document.differ.getChanges();
|
579 | const itemToListHead = new Map();
|
580 |
|
581 | let applied = false;
|
582 |
|
583 | for ( const entry of changes ) {
|
584 | if ( entry.type == 'insert' && entry.name == 'listItem' ) {
|
585 | _addListToFix( entry.position );
|
586 | } else if ( entry.type == 'insert' && entry.name != 'listItem' ) {
|
587 | if ( entry.name != '$text' ) {
|
588 | // In case of renamed element.
|
589 | const item = entry.position.nodeAfter;
|
590 |
|
591 | if ( item.hasAttribute( 'listIndent' ) ) {
|
592 | writer.removeAttribute( 'listIndent', item );
|
593 |
|
594 | applied = true;
|
595 | }
|
596 |
|
597 | if ( item.hasAttribute( 'listType' ) ) {
|
598 | writer.removeAttribute( 'listType', item );
|
599 |
|
600 | applied = true;
|
601 | }
|
602 |
|
603 | if ( item.hasAttribute( 'listStyle' ) ) {
|
604 | writer.removeAttribute( 'listStyle', item );
|
605 |
|
606 | applied = true;
|
607 | }
|
608 |
|
609 | if ( item.hasAttribute( 'listReversed' ) ) {
|
610 | writer.removeAttribute( 'listReversed', item );
|
611 |
|
612 | applied = true;
|
613 | }
|
614 |
|
615 | if ( item.hasAttribute( 'listStart' ) ) {
|
616 | writer.removeAttribute( 'listStart', item );
|
617 |
|
618 | applied = true;
|
619 | }
|
620 |
|
621 | for ( const innerItem of Array.from( model.createRangeIn( item ) ).filter( e => e.item.is( 'element', 'listItem' ) ) ) {
|
622 | _addListToFix( innerItem.previousPosition );
|
623 | }
|
624 | }
|
625 |
|
626 | const posAfter = entry.position.getShiftedBy( entry.length );
|
627 |
|
628 | _addListToFix( posAfter );
|
629 | } else if ( entry.type == 'remove' && entry.name == 'listItem' ) {
|
630 | _addListToFix( entry.position );
|
631 | } else if ( entry.type == 'attribute' && entry.attributeKey == 'listIndent' ) {
|
632 | _addListToFix( entry.range.start );
|
633 | } else if ( entry.type == 'attribute' && entry.attributeKey == 'listType' ) {
|
634 | _addListToFix( entry.range.start );
|
635 | }
|
636 | }
|
637 |
|
638 | for ( const listHead of itemToListHead.values() ) {
|
639 | _fixListIndents( listHead );
|
640 | _fixListTypes( listHead );
|
641 | }
|
642 |
|
643 | return applied;
|
644 |
|
645 | function _addListToFix( position ) {
|
646 | const previousNode = position.nodeBefore;
|
647 |
|
648 | if ( !previousNode || !previousNode.is( 'element', 'listItem' ) ) {
|
649 | const item = position.nodeAfter;
|
650 |
|
651 | if ( item && item.is( 'element', 'listItem' ) ) {
|
652 | itemToListHead.set( item, item );
|
653 | }
|
654 | } else {
|
655 | let listHead = previousNode;
|
656 |
|
657 | if ( itemToListHead.has( listHead ) ) {
|
658 | return;
|
659 | }
|
660 |
|
661 | for (
|
662 | // Cache previousSibling and reuse for performance reasons. See #6581.
|
663 | let previousSibling = listHead.previousSibling;
|
664 | previousSibling && previousSibling.is( 'element', 'listItem' );
|
665 | previousSibling = listHead.previousSibling
|
666 | ) {
|
667 | listHead = previousSibling;
|
668 |
|
669 | if ( itemToListHead.has( listHead ) ) {
|
670 | return;
|
671 | }
|
672 | }
|
673 |
|
674 | itemToListHead.set( previousNode, listHead );
|
675 | }
|
676 | }
|
677 |
|
678 | function _fixListIndents( item ) {
|
679 | let maxIndent = 0;
|
680 | let fixBy = null;
|
681 |
|
682 | while ( item && item.is( 'element', 'listItem' ) ) {
|
683 | const itemIndent = item.getAttribute( 'listIndent' );
|
684 |
|
685 | if ( itemIndent > maxIndent ) {
|
686 | let newIndent;
|
687 |
|
688 | if ( fixBy === null ) {
|
689 | fixBy = itemIndent - maxIndent;
|
690 | newIndent = maxIndent;
|
691 | } else {
|
692 | if ( fixBy > itemIndent ) {
|
693 | fixBy = itemIndent;
|
694 | }
|
695 |
|
696 | newIndent = itemIndent - fixBy;
|
697 | }
|
698 |
|
699 | writer.setAttribute( 'listIndent', newIndent, item );
|
700 |
|
701 | applied = true;
|
702 | } else {
|
703 | fixBy = null;
|
704 | maxIndent = item.getAttribute( 'listIndent' ) + 1;
|
705 | }
|
706 |
|
707 | item = item.nextSibling;
|
708 | }
|
709 | }
|
710 |
|
711 | function _fixListTypes( item ) {
|
712 | let typesStack = [];
|
713 | let prev = null;
|
714 |
|
715 | while ( item && item.is( 'element', 'listItem' ) ) {
|
716 | const itemIndent = item.getAttribute( 'listIndent' );
|
717 |
|
718 | if ( prev && prev.getAttribute( 'listIndent' ) > itemIndent ) {
|
719 | typesStack = typesStack.slice( 0, itemIndent + 1 );
|
720 | }
|
721 |
|
722 | if ( itemIndent != 0 ) {
|
723 | if ( typesStack[ itemIndent ] ) {
|
724 | const type = typesStack[ itemIndent ];
|
725 |
|
726 | if ( item.getAttribute( 'listType' ) != type ) {
|
727 | writer.setAttribute( 'listType', type, item );
|
728 |
|
729 | applied = true;
|
730 | }
|
731 | } else {
|
732 | typesStack[ itemIndent ] = item.getAttribute( 'listType' );
|
733 | }
|
734 | }
|
735 |
|
736 | prev = item;
|
737 | item = item.nextSibling;
|
738 | }
|
739 | }
|
740 | }
|
741 |
|
742 | /**
|
743 | * A fixer for pasted content that includes list items.
|
744 | *
|
745 | * It fixes indentation of pasted list items so the pasted items match correctly to the context they are pasted into.
|
746 | *
|
747 | * Example:
|
748 | *
|
749 | * <listItem listType="bulleted" listIndent=0>A</listItem>
|
750 | * <listItem listType="bulleted" listIndent=1>B^</listItem>
|
751 | * // At ^ paste: <listItem listType="bulleted" listIndent=4>X</listItem>
|
752 | * // <listItem listType="bulleted" listIndent=5>Y</listItem>
|
753 | * <listItem listType="bulleted" listIndent=2>C</listItem>
|
754 | *
|
755 | * Should become:
|
756 | *
|
757 | * <listItem listType="bulleted" listIndent=0>A</listItem>
|
758 | * <listItem listType="bulleted" listIndent=1>BX</listItem>
|
759 | * <listItem listType="bulleted" listIndent=2>Y/listItem>
|
760 | * <listItem listType="bulleted" listIndent=2>C</listItem>
|
761 | *
|
762 | * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
|
763 | * @param {Array} args Arguments of {@link module:engine/model/model~Model#insertContent}.
|
764 | */
|
765 | export function modelIndentPasteFixer( evt, [ content, selectable ] ) {
|
766 | // Check whether inserted content starts from a `listItem`. If it does not, it means that there are some other
|
767 | // elements before it and there is no need to fix indents, because even if we insert that content into a list,
|
768 | // that list will be broken.
|
769 | // Note: we also need to handle singular elements because inserting item with indent 0 into 0,1,[],2
|
770 | // would create incorrect model.
|
771 | let item = content.is( 'documentFragment' ) ? content.getChild( 0 ) : content;
|
772 |
|
773 | let selection;
|
774 |
|
775 | if ( !selectable ) {
|
776 | selection = this.document.selection;
|
777 | } else {
|
778 | selection = this.createSelection( selectable );
|
779 | }
|
780 |
|
781 | if ( item && item.is( 'element', 'listItem' ) ) {
|
782 | // Get a reference list item. Inserted list items will be fixed according to that item.
|
783 | const pos = selection.getFirstPosition();
|
784 | let refItem = null;
|
785 |
|
786 | if ( pos.parent.is( 'element', 'listItem' ) ) {
|
787 | refItem = pos.parent;
|
788 | } else if ( pos.nodeBefore && pos.nodeBefore.is( 'element', 'listItem' ) ) {
|
789 | refItem = pos.nodeBefore;
|
790 | }
|
791 |
|
792 | // If there is `refItem` it means that we do insert list items into an existing list.
|
793 | if ( refItem ) {
|
794 | // First list item in `data` has indent equal to 0 (it is a first list item). It should have indent equal
|
795 | // to the indent of reference item. We have to fix the first item and all of it's children and following siblings.
|
796 | // Indent of all those items has to be adjusted to reference item.
|
797 | const indentChange = refItem.getAttribute( 'listIndent' );
|
798 |
|
799 | // Fix only if there is anything to fix.
|
800 | if ( indentChange > 0 ) {
|
801 | // Adjust indent of all "first" list items in inserted data.
|
802 | while ( item && item.is( 'element', 'listItem' ) ) {
|
803 | item._setAttribute( 'listIndent', item.getAttribute( 'listIndent' ) + indentChange );
|
804 |
|
805 | item = item.nextSibling;
|
806 | }
|
807 | }
|
808 | }
|
809 | }
|
810 | }
|
811 |
|
812 | // Helper function that converts children of a given `<li>` view element into corresponding model elements.
|
813 | // The function maintains proper order of elements if model `listItem` is split during the conversion
|
814 | // due to block children conversion.
|
815 | //
|
816 | // @param {module:engine/model/element~Element} listItemModel List item model element to which converted children will be inserted.
|
817 | // @param {Iterable.<module:engine/view/node~Node>} viewChildren View elements which will be converted.
|
818 | // @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion interface to be used by the callback.
|
819 | // @returns {module:engine/model/position~Position} Position on which next elements should be inserted after children conversion.
|
820 | function viewToModelListItemChildrenConverter( listItemModel, viewChildren, conversionApi ) {
|
821 | const { writer, schema } = conversionApi;
|
822 |
|
823 | // A position after the last inserted `listItem`.
|
824 | let nextPosition = writer.createPositionAfter( listItemModel );
|
825 |
|
826 | // Check all children of the converted `<li>`. At this point we assume there are no "whitespace" view text nodes
|
827 | // in view list, between view list items. This should be handled by `<ul>` and `<ol>` converters.
|
828 | for ( const child of viewChildren ) {
|
829 | if ( child.name == 'ul' || child.name == 'ol' ) {
|
830 | // If the children is a list, we will insert its conversion result after currently handled `listItem`.
|
831 | // Then, next insertion position will be set after all the new list items (and maybe other elements if
|
832 | // something split list item).
|
833 | //
|
834 | // If this is a list, we expect that some `listItem`s and possibly other blocks will be inserted, however `.modelCursor`
|
835 | // should be set after last `listItem` (or block). This is why it feels safe to use it as `nextPosition`
|
836 | nextPosition = conversionApi.convertItem( child, nextPosition ).modelCursor;
|
837 | } else {
|
838 | // If this is not a list, try inserting content at the end of the currently handled `listItem`.
|
839 | const result = conversionApi.convertItem( child, writer.createPositionAt( listItemModel, 'end' ) );
|
840 |
|
841 | // It may end up that the current `listItem` becomes split (if that content cannot be inside `listItem`). For example:
|
842 | //
|
843 | // <li><p>Foo</p></li>
|
844 | //
|
845 | // will be converted to:
|
846 | //
|
847 | // <listItem></listItem><paragraph>Foo</paragraph><listItem></listItem>
|
848 | //
|
849 | const convertedChild = result.modelRange.start.nodeAfter;
|
850 | const wasSplit = convertedChild && convertedChild.is( 'element' ) && !schema.checkChild( listItemModel, convertedChild.name );
|
851 |
|
852 | if ( wasSplit ) {
|
853 | // As `lastListItem` got split, we need to update it to the second part of the split `listItem` element.
|
854 | //
|
855 | // `modelCursor` should be set to a position where the conversion should continue. There are multiple possible scenarios
|
856 | // that may happen. Usually, `modelCursor` (marked as `#` below) would point to the second list item after conversion:
|
857 | //
|
858 | // `<li><p>Foo</p></li>` -> `<listItem></listItem><paragraph>Foo</paragraph><listItem>#</listItem>`
|
859 | //
|
860 | // However, in some cases, like auto-paragraphing, the position is placed at the end of the block element:
|
861 | //
|
862 | // `<li><div>Foo</div></li>` -> `<listItem></listItem><paragraph>Foo#</paragraph><listItem></listItem>`
|
863 | //
|
864 | // or after an element if another element broken auto-paragraphed element:
|
865 | //
|
866 | // `<li><div><h2>Foo</h2></div></li>` -> `<listItem></listItem><heading1>Foo</heading1>#<listItem></listItem>`
|
867 | //
|
868 | // We need to check for such cases and use proper list item and position based on it.
|
869 | //
|
870 | if ( result.modelCursor.parent.is( 'element', 'listItem' ) ) {
|
871 | // (1).
|
872 | listItemModel = result.modelCursor.parent;
|
873 | } else {
|
874 | // (2), (3).
|
875 | listItemModel = findNextListItem( result.modelCursor );
|
876 | }
|
877 |
|
878 | nextPosition = writer.createPositionAfter( listItemModel );
|
879 | }
|
880 | }
|
881 | }
|
882 |
|
883 | return nextPosition;
|
884 | }
|
885 |
|
886 | // Helper function that seeks for a next list item starting from given `startPosition`.
|
887 | function findNextListItem( startPosition ) {
|
888 | const treeWalker = new TreeWalker( { startPosition } );
|
889 |
|
890 | let value;
|
891 |
|
892 | do {
|
893 | value = treeWalker.next();
|
894 | } while ( !value.value.item.is( 'element', 'listItem' ) );
|
895 |
|
896 | return value.value.item;
|
897 | }
|
898 |
|
899 | // Helper function that takes all children of given `viewRemovedItem` and moves them in a correct place, according
|
900 | // to other given parameters.
|
901 | function hoistNestedLists( nextIndent, modelRemoveStartPosition, viewRemoveStartPosition, viewRemovedItem, conversionApi, model ) {
|
902 | // Find correct previous model list item element.
|
903 | // The element has to have either same or smaller indent than given reference indent.
|
904 | // This will be the model element which will get nested items (if it has smaller indent) or sibling items (if it has same indent).
|
905 | // Keep in mind that such element might not be found, if removed item was the first item.
|
906 | const prevModelItem = getSiblingListItem( modelRemoveStartPosition.nodeBefore, {
|
907 | sameIndent: true,
|
908 | smallerIndent: true,
|
909 | listIndent: nextIndent,
|
910 | foo: 'b'
|
911 | } );
|
912 |
|
913 | const mapper = conversionApi.mapper;
|
914 | const viewWriter = conversionApi.writer;
|
915 |
|
916 | // Indent of found element or `null` if the element has not been found.
|
917 | const prevIndent = prevModelItem ? prevModelItem.getAttribute( 'listIndent' ) : null;
|
918 |
|
919 | let insertPosition;
|
920 |
|
921 | if ( !prevModelItem ) {
|
922 | // If element has not been found, simply insert lists at the position where the removed item was:
|
923 | //
|
924 | // Lorem ipsum.
|
925 | // 1 -------- <--- this is removed, no previous list item, put nested items in place of removed item.
|
926 | // 1.1 -------- <--- this is reference indent.
|
927 | // 1.1.1 --------
|
928 | // 1.1.2 --------
|
929 | // 1.2 --------
|
930 | //
|
931 | // Becomes:
|
932 | //
|
933 | // Lorem ipsum.
|
934 | // 1.1 --------
|
935 | // 1.1.1 --------
|
936 | // 1.1.2 --------
|
937 | // 1.2 --------
|
938 | insertPosition = viewRemoveStartPosition;
|
939 | } else if ( prevIndent == nextIndent ) {
|
940 | // If element has been found and has same indent as reference indent it means that nested items should
|
941 | // become siblings of found element:
|
942 | //
|
943 | // 1 --------
|
944 | // 1.1 --------
|
945 | // 1.2 -------- <--- this is `prevModelItem`.
|
946 | // 2 -------- <--- this is removed, previous list item has indent same as reference indent.
|
947 | // 2.1 -------- <--- this is reference indent, this and 2.2 should become siblings of 1.2.
|
948 | // 2.2 --------
|
949 | //
|
950 | // Becomes:
|
951 | //
|
952 | // 1 --------
|
953 | // 1.1 --------
|
954 | // 1.2 --------
|
955 | // 2.1 --------
|
956 | // 2.2 --------
|
957 | const prevViewList = mapper.toViewElement( prevModelItem ).parent;
|
958 | insertPosition = viewWriter.createPositionAfter( prevViewList );
|
959 | } else {
|
960 | // If element has been found and has smaller indent as reference indent it means that nested items
|
961 | // should become nested items of found item:
|
962 | //
|
963 | // 1 -------- <--- this is `prevModelItem`.
|
964 | // 1.1 -------- <--- this is removed, previous list item has indent smaller than reference indent.
|
965 | // 1.1.1 -------- <--- this is reference indent, this and 1.1.1 should become nested items of 1.
|
966 | // 1.1.2 --------
|
967 | // 1.2 --------
|
968 | //
|
969 | // Becomes:
|
970 | //
|
971 | // 1 --------
|
972 | // 1.1.1 --------
|
973 | // 1.1.2 --------
|
974 | // 1.2 --------
|
975 | //
|
976 | // Note: in this case 1.1.1 have indent 2 while 1 have indent 0. In model that should not be possible,
|
977 | // because following item may have indent bigger only by one. But this is fixed by postfixer.
|
978 | const modelPosition = model.createPositionAt( prevModelItem, 'end' );
|
979 | insertPosition = mapper.toViewPosition( modelPosition );
|
980 | }
|
981 |
|
982 | insertPosition = positionAfterUiElements( insertPosition );
|
983 |
|
984 | // Handle multiple lists. This happens if list item has nested numbered and bulleted lists. Following lists
|
985 | // are inserted after the first list (no need to recalculate insertion position for them).
|
986 | for ( const child of [ ...viewRemovedItem.getChildren() ] ) {
|
987 | if ( isList( child ) ) {
|
988 | insertPosition = viewWriter.move( viewWriter.createRangeOn( child ), insertPosition ).end;
|
989 |
|
990 | mergeViewLists( viewWriter, child, child.nextSibling );
|
991 | mergeViewLists( viewWriter, child.previousSibling, child );
|
992 | }
|
993 | }
|
994 | }
|
995 |
|
996 | // Checks if view element is a list type (ul or ol).
|
997 | //
|
998 | // @param {module:engine/view/element~Element} viewElement
|
999 | // @returns {Boolean}
|
1000 | function isList( viewElement ) {
|
1001 | return viewElement.is( 'element', 'ol' ) || viewElement.is( 'element', 'ul' );
|
1002 | }
|
1003 |
|
1004 | // Calculates the indent value for a list item. Handles HTML compliant and non-compliant lists.
|
1005 | //
|
1006 | // Also, fixes non HTML compliant lists indents:
|
1007 | //
|
1008 | // before: fixed list:
|
1009 | // OL OL
|
1010 | // |-> LI (parent LIs: 0) |-> LI (indent: 0)
|
1011 | // |-> OL |-> OL
|
1012 | // |-> OL |
|
1013 | // | |-> OL |
|
1014 | // | |-> OL |
|
1015 | // | |-> LI (parent LIs: 1) |-> LI (indent: 1)
|
1016 | // |-> LI (parent LIs: 1) |-> LI (indent: 1)
|
1017 | //
|
1018 | // before: fixed list:
|
1019 | // OL OL
|
1020 | // |-> OL |
|
1021 | // |-> OL |
|
1022 | // |-> OL |
|
1023 | // |-> LI (parent LIs: 0) |-> LI (indent: 0)
|
1024 | //
|
1025 | // before: fixed list:
|
1026 | // OL OL
|
1027 | // |-> LI (parent LIs: 0) |-> LI (indent: 0)
|
1028 | // |-> OL |-> OL
|
1029 | // |-> LI (parent LIs: 0) |-> LI (indent: 1)
|
1030 | //
|
1031 | // @param {module:engine/view/element~Element} listItem
|
1032 | // @param {Object} conversionStore
|
1033 | // @returns {Number}
|
1034 | function getIndent( listItem ) {
|
1035 | let indent = 0;
|
1036 |
|
1037 | let parent = listItem.parent;
|
1038 |
|
1039 | while ( parent ) {
|
1040 | // Each LI in the tree will result in an increased indent for HTML compliant lists.
|
1041 | if ( parent.is( 'element', 'li' ) ) {
|
1042 | indent++;
|
1043 | } else {
|
1044 | // If however the list is nested in other list we should check previous sibling of any of the list elements...
|
1045 | const previousSibling = parent.previousSibling;
|
1046 |
|
1047 | // ...because the we might need increase its indent:
|
1048 | // before: fixed list:
|
1049 | // OL OL
|
1050 | // |-> LI (parent LIs: 0) |-> LI (indent: 0)
|
1051 | // |-> OL |-> OL
|
1052 | // |-> LI (parent LIs: 0) |-> LI (indent: 1)
|
1053 | if ( previousSibling && previousSibling.is( 'element', 'li' ) ) {
|
1054 | indent++;
|
1055 | }
|
1056 | }
|
1057 |
|
1058 | parent = parent.parent;
|
1059 | }
|
1060 |
|
1061 | return indent;
|
1062 | }
|