@ckeditor/ckeditor5-list 48.1.0 → 48.2.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ckeditor5-metadata.json +6 -0
- package/dist/index.css.map +1 -1
- package/dist/index.js +351 -48
- package/dist/index.js.map +1 -1
- package/dist/list/converters.d.ts +19 -1
- package/dist/list/utils/model.d.ts +33 -3
- package/dist/listconfig.d.ts +20 -2
- package/package.json +10 -10
package/dist/index.js
CHANGED
|
@@ -476,7 +476,11 @@ import { DomEventObserver, Matcher, ModelTreeWalker, getViewFillerOffset } from
|
|
|
476
476
|
continue;
|
|
477
477
|
}
|
|
478
478
|
// Merge with parent list item while outdenting and indent matches reference indent.
|
|
479
|
-
if (
|
|
479
|
+
// The parent block may be null if the block has no ancestor with a lower indent (e.g. when skip-level
|
|
480
|
+
// lists are enabled and the first item starts at indent > 0). In that case, skip the merge and just
|
|
481
|
+
// decrease the indent. Without skip-level lists, the post-fixer guarantees sequential indents, so
|
|
482
|
+
// a parent with a lower indent always exists and this guard has no effect.
|
|
483
|
+
if (block.getAttribute('listIndent') == referenceIndent && parentBlocks.get(block)) {
|
|
480
484
|
const mergedBlocks = mergeListItemIfNotLast(block, parentBlocks.get(block), writer);
|
|
481
485
|
// All list item blocks are updated while merging so add those to visited set.
|
|
482
486
|
for (const mergedBlock of mergedBlocks){
|
|
@@ -655,16 +659,65 @@ import { DomEventObserver, Matcher, ModelTreeWalker, getViewFillerOffset } from
|
|
|
655
659
|
return listType == 'numbered' || listType == 'customNumbered';
|
|
656
660
|
}
|
|
657
661
|
/**
|
|
658
|
-
* Checks if the given list item is the first item in the
|
|
662
|
+
* Checks if the given list item block is the first block of the first item in its list at the given indent.
|
|
659
663
|
*
|
|
660
|
-
*
|
|
661
|
-
* at
|
|
664
|
+
* Walks back over previous siblings and returns:
|
|
665
|
+
* - `true` if it reaches a non-list block or a list block at a lower indent (a new list begins here),
|
|
666
|
+
* - `false` if it finds a same-indent block of the same `listItemId` (a continuation of the current item) or of the same
|
|
667
|
+
* `listType` (the visible list already has earlier items),
|
|
668
|
+
* - `true` if it finds a same-indent block of a different `listType` and a different `listItemId` (a different list ends; ours
|
|
669
|
+
* starts here),
|
|
670
|
+
* - `false` if the loop ends (it reaches the first non-list-item block, or no more previous siblings) while passing only
|
|
671
|
+
* higher-indent blocks (those blocks live inside an intermediate skip-level `<li style="list-style-type:none">` wrapper
|
|
672
|
+
* at our indent).
|
|
673
|
+
*
|
|
674
|
+
* For example, in the model:
|
|
675
|
+
*
|
|
676
|
+
* ```
|
|
677
|
+
* ' # aaa'
|
|
678
|
+
* '# bbb'
|
|
679
|
+
* ```
|
|
680
|
+
*
|
|
681
|
+
* `bbb` is preceded by a higher-indent block `aaa`, which in the view is rendered inside an intermediate
|
|
682
|
+
* skip-level wrapper at indent 0:
|
|
683
|
+
*
|
|
684
|
+
* ```html
|
|
685
|
+
* <ol>
|
|
686
|
+
* <li style="list-style-type:none">
|
|
687
|
+
* <ol>
|
|
688
|
+
* <li>aaa</li>
|
|
689
|
+
* </ol>
|
|
690
|
+
* </li>
|
|
691
|
+
* <li>bbb</li>
|
|
692
|
+
* </ol>
|
|
693
|
+
* ```
|
|
694
|
+
*
|
|
695
|
+
* So `bbb` is the second visible item in the outer list and the function returns `false`.
|
|
662
696
|
*/ function isFirstListItemInList(listItem) {
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
697
|
+
const itemIndent = listItem.getAttribute('listIndent');
|
|
698
|
+
const itemListType = listItem.getAttribute('listType');
|
|
699
|
+
const itemListItemId = listItem.getAttribute('listItemId');
|
|
700
|
+
let previous = listItem.previousSibling;
|
|
701
|
+
let sawHigherIndent = false;
|
|
702
|
+
while(isListItemBlock(previous)){
|
|
703
|
+
const previousIndent = previous.getAttribute('listIndent');
|
|
704
|
+
if (previousIndent < itemIndent) {
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
if (previousIndent === itemIndent) {
|
|
708
|
+
// The previous block belongs to the current item — we are a continuation block, not the first block of
|
|
709
|
+
// the list item, so item-block indent should not apply (a normal list indent should fall through instead).
|
|
710
|
+
if (previous.getAttribute('listItemId') === itemListItemId) {
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
return previous.getAttribute('listType') !== itemListType;
|
|
714
|
+
}
|
|
715
|
+
sawHigherIndent = true;
|
|
716
|
+
previous = previous.previousSibling;
|
|
717
|
+
}
|
|
718
|
+
// Reached the first non-list-item block (or no more previous siblings). If only higher-indent blocks were on
|
|
719
|
+
// the way, they live inside an intermediate skip-level wrapper at our indent — we are not first.
|
|
720
|
+
return !sawHigherIndent;
|
|
668
721
|
}
|
|
669
722
|
/**
|
|
670
723
|
* Merges a given block to the given parent block if parent is a list item and there is no more blocks in the same item.
|
|
@@ -788,6 +841,10 @@ import { DomEventObserver, Matcher, ModelTreeWalker, getViewFillerOffset } from
|
|
|
788
841
|
if (isSingleListItem(blocks) && !isFirstBlockOfListItem(blocks[0])) {
|
|
789
842
|
return true;
|
|
790
843
|
}
|
|
844
|
+
// When skip levels are allowed, any list item can always be indented further.
|
|
845
|
+
if (this.editor.config.get('list.enableSkipLevelLists')) {
|
|
846
|
+
return true;
|
|
847
|
+
}
|
|
791
848
|
blocks = expandListBlocksToCompleteItems(blocks);
|
|
792
849
|
firstBlock = blocks[0];
|
|
793
850
|
// Check if there is any list item before selected items that could become a parent of selected items.
|
|
@@ -1011,6 +1068,12 @@ import { DomEventObserver, Matcher, ModelTreeWalker, getViewFillerOffset } from
|
|
|
1011
1068
|
const attributeNames = listEditing.getListAttributeNames();
|
|
1012
1069
|
model.change((writer)=>{
|
|
1013
1070
|
const { firstElement, lastElement } = this._getMergeSubjectElements(selection, shouldMergeOnBlocksContentLevel);
|
|
1071
|
+
// A defensive guard. When `_getMergeSubjectElements()` cannot determine a valid pair of elements
|
|
1072
|
+
// to merge (for example because there is no list block before/after the current one),
|
|
1073
|
+
// the command should be a no-op instead of throwing on `null.getAttribute()`.
|
|
1074
|
+
if (!firstElement || !lastElement) {
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1014
1077
|
const firstIndent = firstElement.getAttribute('listIndent') || 0;
|
|
1015
1078
|
const lastIndent = lastElement.getAttribute('listIndent');
|
|
1016
1079
|
const lastElementId = lastElement.getAttribute('listItemId');
|
|
@@ -1123,6 +1186,11 @@ import { DomEventObserver, Matcher, ModelTreeWalker, getViewFillerOffset } from
|
|
|
1123
1186
|
sameIndent: true,
|
|
1124
1187
|
lowerIndent: true
|
|
1125
1188
|
});
|
|
1189
|
+
// Fallback for "skip-level" structures where the previous list block in the model
|
|
1190
|
+
// happens to live at a *higher* indent (no same-or-lower-indent block precedes it).
|
|
1191
|
+
if (!firstElement && isListItemBlock(positionParent.previousSibling)) {
|
|
1192
|
+
firstElement = positionParent.previousSibling;
|
|
1193
|
+
}
|
|
1126
1194
|
} else {
|
|
1127
1195
|
firstElement = positionParent.previousSibling;
|
|
1128
1196
|
}
|
|
@@ -2139,6 +2207,55 @@ import { DomEventObserver, Matcher, ModelTreeWalker, getViewFillerOffset } from
|
|
|
2139
2207
|
return applied;
|
|
2140
2208
|
}
|
|
2141
2209
|
|
|
2210
|
+
/**
|
|
2211
|
+
* Returns a consuming upcast converter for skip-level list item wrappers. It detects intermediate `<li>` elements
|
|
2212
|
+
* with `list-style-type:none` (generated by the skip-level downcast or by external sources) and consumes them
|
|
2213
|
+
* without producing a model element, so they don't end up as empty list items in the model.
|
|
2214
|
+
*
|
|
2215
|
+
* The wrapper `<li>` is consumed, but its children (nested lists) are converted normally. Because `getIndent()`
|
|
2216
|
+
* counts all ancestor `<li>` elements (including the consumed wrapper), nested items receive the correct indent
|
|
2217
|
+
* values that reflect the skip-level gap.
|
|
2218
|
+
*
|
|
2219
|
+
* Only `<li>` elements whose sole meaningful content is a nested `<ul>`/`<ol>` are treated as intermediate wrappers.
|
|
2220
|
+
* Anything else (text, paragraphs, custom elements, even an empty `<li>` with `list-style-type:none` carrying only
|
|
2221
|
+
* attributes) falls through to the regular list item upcast, so its data and attributes can be preserved by GHS
|
|
2222
|
+
* or other plugins.
|
|
2223
|
+
*
|
|
2224
|
+
* @internal
|
|
2225
|
+
*/ function listItemSkipLevelConsumer() {
|
|
2226
|
+
return (evt, data, conversionApi)=>{
|
|
2227
|
+
const viewItem = data.viewItem;
|
|
2228
|
+
if (viewItem.getStyle('list-style-type') !== 'none') {
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
if (!isSkipLevelWrapper(viewItem)) {
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
if (!conversionApi.consumable.consume(viewItem, {
|
|
2235
|
+
name: true
|
|
2236
|
+
})) {
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
const { modelRange, modelCursor } = conversionApi.convertChildren(viewItem, data.modelCursor);
|
|
2240
|
+
data.modelRange = modelRange;
|
|
2241
|
+
data.modelCursor = modelCursor;
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
/**
|
|
2245
|
+
* Checks whether a `<li>` view element is a skip-level intermediate wrapper, i.e. its only child is a nested
|
|
2246
|
+
* `<ul>`/`<ol>`. Any other content (text, `<br>`, `<p>`, custom elements, NBSP, etc.) disqualifies the element,
|
|
2247
|
+
* so it is upcast as a regular list item.
|
|
2248
|
+
*/ function isSkipLevelWrapper(viewItem) {
|
|
2249
|
+
let hasNestedList = false;
|
|
2250
|
+
for (const child of viewItem.getChildren()){
|
|
2251
|
+
if (child.is('element', 'ul') || child.is('element', 'ol')) {
|
|
2252
|
+
hasNestedList = true;
|
|
2253
|
+
continue;
|
|
2254
|
+
}
|
|
2255
|
+
return false;
|
|
2256
|
+
}
|
|
2257
|
+
return hasNestedList;
|
|
2258
|
+
}
|
|
2142
2259
|
/**
|
|
2143
2260
|
* Returns the upcast converter for list items. It's supposed to work after the block converters (content inside list items) are converted.
|
|
2144
2261
|
*
|
|
@@ -2155,6 +2272,11 @@ import { DomEventObserver, Matcher, ModelTreeWalker, getViewFillerOffset } from
|
|
|
2155
2272
|
if (!items.length) {
|
|
2156
2273
|
return;
|
|
2157
2274
|
}
|
|
2275
|
+
// All items already have list attributes set by a recursive conversion (e.g. children of a skip-level
|
|
2276
|
+
// wrapper <li> that was consumed without creating a model element). Nothing left to do here.
|
|
2277
|
+
if (items.every((item)=>item.hasAttribute('listItemId'))) {
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2158
2280
|
const listItemId = data.viewItem.getAttribute('data-list-item-id') || ListItemUid.next();
|
|
2159
2281
|
conversionApi.consumable.consume(data.viewItem, {
|
|
2160
2282
|
attributes: 'data-list-item-id'
|
|
@@ -2263,9 +2385,10 @@ import { DomEventObserver, Matcher, ModelTreeWalker, getViewFillerOffset } from
|
|
|
2263
2385
|
}
|
|
2264
2386
|
// Update the stack for the current indent level.
|
|
2265
2387
|
stack[itemIndent] = {
|
|
2266
|
-
modelAttributes:
|
|
2388
|
+
modelAttributes: getListModelAttributes(node),
|
|
2267
2389
|
modelElement: node
|
|
2268
2390
|
};
|
|
2391
|
+
fillStackForIntermediates(node, itemIndent, stack);
|
|
2269
2392
|
// Find all blocks of the current node.
|
|
2270
2393
|
const blocks = getListItemBlocks(node, {
|
|
2271
2394
|
direction: 'forward'
|
|
@@ -2282,6 +2405,42 @@ import { DomEventObserver, Matcher, ModelTreeWalker, getViewFillerOffset } from
|
|
|
2282
2405
|
}
|
|
2283
2406
|
return itemsToRefresh;
|
|
2284
2407
|
}
|
|
2408
|
+
// Returns model list attributes (those tracked by `attributeNames`) of the given item as a plain object.
|
|
2409
|
+
function getListModelAttributes(item) {
|
|
2410
|
+
return Object.fromEntries(Array.from(item.getAttributes()).filter(([key])=>attributeNames.includes(key)));
|
|
2411
|
+
}
|
|
2412
|
+
// Skip-level lists have view wrappers (<ol>/<ul>) at indent levels with no matching
|
|
2413
|
+
// model item. Each such wrapper inherits its attributes (listStart, listStyle, etc.)
|
|
2414
|
+
// from a nearby model item - the first sibling found at that indent, or the closest
|
|
2415
|
+
// lower-indent ancestor (mirroring the downcast's fallback). Remember that item here
|
|
2416
|
+
// so later we can compare the wrapper against the current model and tell whether it's
|
|
2417
|
+
// still up to date.
|
|
2418
|
+
function fillStackForIntermediates(node, itemIndent, stack) {
|
|
2419
|
+
for(let i = itemIndent - 1; i >= 0; i--){
|
|
2420
|
+
if (stack[i]) {
|
|
2421
|
+
break;
|
|
2422
|
+
}
|
|
2423
|
+
const siblingAtIndent = findSiblingListItemAt(node, i);
|
|
2424
|
+
let ancestorAtLowerIndent = null;
|
|
2425
|
+
if (!siblingAtIndent) {
|
|
2426
|
+
for(let k = i - 1; k >= 0; k--){
|
|
2427
|
+
if (stack[k]) {
|
|
2428
|
+
ancestorAtLowerIndent = stack[k].modelElement;
|
|
2429
|
+
break;
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
const referenceItem = siblingAtIndent || ancestorAtLowerIndent || node;
|
|
2434
|
+
stack[i] = {
|
|
2435
|
+
modelAttributes: {
|
|
2436
|
+
...getListModelAttributes(referenceItem),
|
|
2437
|
+
listItemId: `list-item-skip-${i}`,
|
|
2438
|
+
listIndent: i
|
|
2439
|
+
},
|
|
2440
|
+
modelElement: referenceItem
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2285
2444
|
function doesItemBlockRequiresRefresh(item, blocks) {
|
|
2286
2445
|
const viewElement = editing.mapper.toViewElement(item);
|
|
2287
2446
|
if (!viewElement) {
|
|
@@ -2338,14 +2497,20 @@ import { DomEventObserver, Matcher, ModelTreeWalker, getViewFillerOffset } from
|
|
|
2338
2497
|
if (!isListElement && !isListItemElement) {
|
|
2339
2498
|
continue;
|
|
2340
2499
|
}
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2500
|
+
// The stack is indexed by listIndent, so with skip-level lists it may have empty slots
|
|
2501
|
+
// for skipped indent levels. Skip attribute checking for those but still track the indent
|
|
2502
|
+
// level below. Without skip-level lists, the post-fixer ensures sequential indents, so the
|
|
2503
|
+
// stack is always fully populated and this guard has no effect.
|
|
2504
|
+
if (stack[indent]) {
|
|
2505
|
+
const eventName = `checkAttributes:${isListItemElement ? 'item' : 'list'}`;
|
|
2506
|
+
const needsRefresh = listEditing.fire(eventName, {
|
|
2507
|
+
viewElement: element,
|
|
2508
|
+
modelAttributes: stack[indent].modelAttributes,
|
|
2509
|
+
modelReferenceElement: stack[indent].modelElement
|
|
2510
|
+
});
|
|
2511
|
+
if (needsRefresh) {
|
|
2512
|
+
break;
|
|
2513
|
+
}
|
|
2349
2514
|
}
|
|
2350
2515
|
if (isListElement) {
|
|
2351
2516
|
indent--;
|
|
@@ -2365,7 +2530,7 @@ import { DomEventObserver, Matcher, ModelTreeWalker, getViewFillerOffset } from
|
|
|
2365
2530
|
* @param attributeNames A list of attribute names that should be converted if they are set.
|
|
2366
2531
|
* @param strategies The strategies.
|
|
2367
2532
|
* @param model The model.
|
|
2368
|
-
*/ function listItemDowncastConverter(attributeNames, strategies, model, { dataPipeline
|
|
2533
|
+
*/ function listItemDowncastConverter(attributeNames, strategies, model, { dataPipeline, enableSkipLevelLists }) {
|
|
2369
2534
|
const consumer = createAttributesConsumer(attributeNames, strategies);
|
|
2370
2535
|
return (evt, data, conversionApi)=>{
|
|
2371
2536
|
const { writer, mapper, consumable } = conversionApi;
|
|
@@ -2379,7 +2544,8 @@ import { DomEventObserver, Matcher, ModelTreeWalker, getViewFillerOffset } from
|
|
|
2379
2544
|
}
|
|
2380
2545
|
const options = {
|
|
2381
2546
|
...conversionApi.options,
|
|
2382
|
-
dataPipeline
|
|
2547
|
+
dataPipeline,
|
|
2548
|
+
enableSkipLevelLists
|
|
2383
2549
|
};
|
|
2384
2550
|
// Use positions mapping instead of mapper.toViewElement( listItem ) to find outermost view element.
|
|
2385
2551
|
// This is for cases when mapping is using inner view element like in the code blocks (pre > code).
|
|
@@ -2593,35 +2759,109 @@ import { DomEventObserver, Matcher, ModelTreeWalker, getViewFillerOffset } from
|
|
|
2593
2759
|
}
|
|
2594
2760
|
/**
|
|
2595
2761
|
* Wraps the given list item with appropriate attribute elements for ul, ol, and li.
|
|
2762
|
+
*
|
|
2763
|
+
* For skip-level lists (where indent gaps exist, e.g. indent 0 → indent 2), this function
|
|
2764
|
+
* generates intermediate wrapper pairs (ul/ol + li) at each missing level. These intermediate
|
|
2765
|
+
* wrappers are invisible (`list-style-type: none` on the li). Scope `'list'` strategies are
|
|
2766
|
+
* applied so the wrapper element (ul/ol) carries the same classes and styles as real list
|
|
2767
|
+
* wrappers, while scope `'item'` strategies are skipped since there is no model element
|
|
2768
|
+
* backing the intermediate level.
|
|
2596
2769
|
*/ function wrapListItemBlock(listItem, viewRange, strategies, writer, options) {
|
|
2597
2770
|
if (!listItem.hasAttribute('listIndent')) {
|
|
2598
2771
|
return;
|
|
2599
2772
|
}
|
|
2600
2773
|
const listItemIndent = listItem.getAttribute('listIndent');
|
|
2774
|
+
const enableSkipLevelLists = options.enableSkipLevelLists;
|
|
2601
2775
|
let currentListItem = listItem;
|
|
2602
2776
|
for(let indent = listItemIndent; indent >= 0; indent--){
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2777
|
+
// When ListWalker jumps over indent levels (e.g. from indent 2 to indent 0, either because
|
|
2778
|
+
// enableSkipLevelLists is enabled or because the item is at the start of a fragment and its
|
|
2779
|
+
// nearest ancestor is further up), the levels in between have no corresponding model element.
|
|
2780
|
+
// We detect these "intermediate" levels by checking if currentListItem's indent doesn't match
|
|
2781
|
+
// the current loop indent. Handling this regardless of the enableSkipLevelLists config makes the
|
|
2782
|
+
// downcast resilient to unexpected skip-level states in the model.
|
|
2783
|
+
const isIntermediate = currentListItem.getAttribute('listIndent') !== indent;
|
|
2784
|
+
if (isIntermediate) {
|
|
2785
|
+
// Intermediate levels get invisible wrappers: list-style-type:none hides the marker on <li>,
|
|
2786
|
+
// and scope:'item' strategies are not applied (no data-list-item-id, etc.) since there is no
|
|
2787
|
+
// model element backing this level.
|
|
2788
|
+
//
|
|
2789
|
+
// The <li> uses a fixed ID per indent (`list-item-skip-N`) so that sibling items sharing
|
|
2790
|
+
// the same skipped level merge into one <li> (e.g. two items at indent 1 with no parent
|
|
2791
|
+
// at indent 0 share one intermediate <li> at indent 0).
|
|
2792
|
+
//
|
|
2793
|
+
// The <ul/ol> type is chosen to ensure correct merging at this indent:
|
|
2794
|
+
// - If a real list item exists at this indent (found by walking forward), use its type
|
|
2795
|
+
// so the intermediate wrapper merges with that item's real wrapper.
|
|
2796
|
+
// - Otherwise, use the ancestor's type so all intermediates at this indent share the
|
|
2797
|
+
// same type and merge with each other.
|
|
2798
|
+
const siblingAtIndent = findSiblingListItemAt(listItem, indent);
|
|
2799
|
+
const referenceItem = siblingAtIndent || currentListItem;
|
|
2800
|
+
const listType = referenceItem.getAttribute('listType');
|
|
2801
|
+
const listItemViewElement = createListItemElement(writer, indent, `list-item-skip-${indent}`);
|
|
2802
|
+
const listViewElement = createListElement(writer, indent, listType);
|
|
2803
|
+
writer.setStyle('list-style-type', 'none', listItemViewElement);
|
|
2804
|
+
// Apply scope:'list' strategies so that intermediate <ol>/<ul> wrappers carry the same
|
|
2805
|
+
// classes and styles as real list wrappers (e.g. multi-level-list class, todo-list class,
|
|
2806
|
+
// list-style-type:none). Without this, intermediate and real list elements at the same
|
|
2807
|
+
// indent would have mismatched attributes and could not merge correctly, causing browsers
|
|
2808
|
+
// to render unwanted default markers.
|
|
2809
|
+
//
|
|
2810
|
+
// Use the reference item (sibling or ancestor) as the strategy source so the attributes
|
|
2811
|
+
// match whatever this intermediate is supposed to merge with.
|
|
2812
|
+
for (const strategy of strategies){
|
|
2813
|
+
if (strategy.scope == 'list' && referenceItem.hasAttribute(strategy.attributeName)) {
|
|
2814
|
+
strategy.setAttributeOnDowncast(writer, referenceItem.getAttribute(strategy.attributeName), listViewElement, options, referenceItem);
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
viewRange = writer.wrap(viewRange, listItemViewElement);
|
|
2818
|
+
viewRange = writer.wrap(viewRange, listViewElement);
|
|
2819
|
+
} else {
|
|
2820
|
+
const listItemViewElement = createListItemElement(writer, indent, currentListItem.getAttribute('listItemId'));
|
|
2821
|
+
const listViewElement = createListElement(writer, indent, currentListItem.getAttribute('listType'));
|
|
2822
|
+
for (const strategy of strategies){
|
|
2823
|
+
if ((strategy.scope == 'list' || strategy.scope == 'item') && currentListItem.hasAttribute(strategy.attributeName)) {
|
|
2824
|
+
strategy.setAttributeOnDowncast(writer, currentListItem.getAttribute(strategy.attributeName), strategy.scope == 'list' ? listViewElement : listItemViewElement, options, currentListItem);
|
|
2825
|
+
}
|
|
2608
2826
|
}
|
|
2827
|
+
viewRange = writer.wrap(viewRange, listItemViewElement);
|
|
2828
|
+
viewRange = writer.wrap(viewRange, listViewElement);
|
|
2609
2829
|
}
|
|
2610
|
-
viewRange = writer.wrap(viewRange, listItemViewElement);
|
|
2611
|
-
viewRange = writer.wrap(viewRange, listViewElement);
|
|
2612
2830
|
if (indent == 0) {
|
|
2613
2831
|
break;
|
|
2614
2832
|
}
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2833
|
+
// Only look for a parent when at a real (non-intermediate) level. At intermediate levels
|
|
2834
|
+
// currentListItem already points to the nearest found ancestor and won't change.
|
|
2835
|
+
if (!isIntermediate) {
|
|
2836
|
+
const nextListItem = ListWalker.first(currentListItem, {
|
|
2837
|
+
lowerIndent: true
|
|
2838
|
+
});
|
|
2839
|
+
if (nextListItem) {
|
|
2840
|
+
currentListItem = nextListItem;
|
|
2841
|
+
} else if (!enableSkipLevelLists) {
|
|
2842
|
+
break;
|
|
2843
|
+
}
|
|
2622
2844
|
}
|
|
2623
2845
|
}
|
|
2624
2846
|
}
|
|
2847
|
+
/**
|
|
2848
|
+
* Walks forward from the given list item through model siblings to find the first list item block
|
|
2849
|
+
* at exactly the specified indent level. Stops when it encounters a non-list block or a list item
|
|
2850
|
+
* at a lower indent (which means we left the current subtree).
|
|
2851
|
+
*/ function findSiblingListItemAt(listItem, targetIndent) {
|
|
2852
|
+
let node = listItem.nextSibling;
|
|
2853
|
+
while(node && isListItemBlock(node)){
|
|
2854
|
+
const indent = node.getAttribute('listIndent');
|
|
2855
|
+
if (indent < targetIndent) {
|
|
2856
|
+
return null;
|
|
2857
|
+
}
|
|
2858
|
+
if (indent === targetIndent) {
|
|
2859
|
+
return node;
|
|
2860
|
+
}
|
|
2861
|
+
node = node.nextSibling;
|
|
2862
|
+
}
|
|
2863
|
+
return null;
|
|
2864
|
+
}
|
|
2625
2865
|
// Returns the function that is responsible for consuming attributes that are set on the model node.
|
|
2626
2866
|
function createAttributesConsumer(attributeNames, strategies) {
|
|
2627
2867
|
const nonConsumingAttributes = strategies.filter((strategy)=>strategy.consume === false).map((strategy)=>strategy.attributeName);
|
|
@@ -2951,6 +3191,7 @@ function shouldUseBogusParagraph(item, attributeNames, blocks = getAllListItemBl
|
|
|
2951
3191
|
const attributeNames = this.getListAttributeNames();
|
|
2952
3192
|
const multiBlock = editor.config.get('list.multiBlock');
|
|
2953
3193
|
const elementName = multiBlock ? 'paragraph' : 'listItem';
|
|
3194
|
+
const enableSkipLevelLists = !!editor.config.get('list.enableSkipLevelLists');
|
|
2954
3195
|
editor.conversion.for('upcast')// Convert <li> to a generic paragraph (or listItem element) so the content of <li> is always inside a block.
|
|
2955
3196
|
// Setting the listType attribute to let other features (to-do list) know that this is part of a list item.
|
|
2956
3197
|
// This is also important to properly handle simple lists so that paragraphs inside a list item won't break the list item.
|
|
@@ -2978,6 +3219,60 @@ function shouldUseBogusParagraph(item, attributeNames, blocks = getAllListItemBl
|
|
|
2978
3219
|
},
|
|
2979
3220
|
converterPriority: 'high'
|
|
2980
3221
|
}).add((dispatcher)=>{
|
|
3222
|
+
// A `<p>` that is the only `<p>` child of its `<li>` and carries no own attributes is
|
|
3223
|
+
// a bogus wrapper around the list item's content. Consume its name and convert its
|
|
3224
|
+
// children directly into the surrounding `<li>`'s paragraph instead of letting
|
|
3225
|
+
// `elementToElement('p')` above create a separate paragraph and force an autobreak.
|
|
3226
|
+
// Otherwise we would end up with two paragraphs (one of them empty), instead of one paragraph with content.
|
|
3227
|
+
dispatcher.on('element:p', (evt, data, conversionApi)=>{
|
|
3228
|
+
const viewElement = data.viewItem;
|
|
3229
|
+
if (!viewElement.parent || !viewElement.parent.is('element', 'li')) {
|
|
3230
|
+
return;
|
|
3231
|
+
}
|
|
3232
|
+
// Empty <p> (truly empty, e.g. `<li><p></p></li>`) must go through the standard
|
|
3233
|
+
// `elementToElement('p')` path so the EmptyBlock upcast (priority 'lowest') takes care of it.
|
|
3234
|
+
if (viewElement.isEmpty) {
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
3237
|
+
// Bogus only when `<p>` carries no own attributes.
|
|
3238
|
+
if (!viewElement.getAttributeKeys().next().done) {
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3241
|
+
// And the surrounding <li> has no other block-level content besides nested lists
|
|
3242
|
+
// (any text or non-list element sibling means a multi-block list item where this
|
|
3243
|
+
// <p> must keep its own paragraph in the model).
|
|
3244
|
+
for (const sibling of viewElement.parent.getChildren()){
|
|
3245
|
+
if (sibling === viewElement) {
|
|
3246
|
+
continue;
|
|
3247
|
+
}
|
|
3248
|
+
if (sibling.is('element', 'ol') || sibling.is('element', 'ul')) {
|
|
3249
|
+
continue;
|
|
3250
|
+
}
|
|
3251
|
+
return;
|
|
3252
|
+
}
|
|
3253
|
+
// Mark the <p> as consumed so the lower-priority `elementToElement('p')` (and the
|
|
3254
|
+
// Paragraph plugin's default `<p>` converter) skip it — otherwise they would
|
|
3255
|
+
// create a separate model paragraph and trigger the auto-break empty paragraph
|
|
3256
|
+
// we are working around.
|
|
3257
|
+
conversionApi.consumable.consume(viewElement, {
|
|
3258
|
+
name: true
|
|
3259
|
+
});
|
|
3260
|
+
const { modelRange, modelCursor } = conversionApi.convertChildren(viewElement, data.modelCursor);
|
|
3261
|
+
data.modelRange = modelRange;
|
|
3262
|
+
data.modelCursor = modelCursor;
|
|
3263
|
+
}, {
|
|
3264
|
+
priority: 'highest'
|
|
3265
|
+
});
|
|
3266
|
+
if (enableSkipLevelLists) {
|
|
3267
|
+
// A consuming converter for intermediate `<li>` wrappers produced by the skip-level downcast
|
|
3268
|
+
// (or by external sources). It is registered with 'high' priority so it reliably runs before
|
|
3269
|
+
// any other `element:li` upcast converter. Registration order inside this `.add()` is already
|
|
3270
|
+
// enough for our own converters, but `high` guards against other plugins that may attach their
|
|
3271
|
+
// own `element:li` listeners at the default priority in a separate `.add()` block.
|
|
3272
|
+
dispatcher.on('element:li', listItemSkipLevelConsumer(), {
|
|
3273
|
+
priority: 'high'
|
|
3274
|
+
});
|
|
3275
|
+
}
|
|
2981
3276
|
dispatcher.on('element:li', listItemUpcastConverter());
|
|
2982
3277
|
});
|
|
2983
3278
|
if (!multiBlock) {
|
|
@@ -2991,7 +3286,9 @@ function shouldUseBogusParagraph(item, attributeNames, blocks = getAllListItemBl
|
|
|
2991
3286
|
view: bogusParagraphCreator(attributeNames),
|
|
2992
3287
|
converterPriority: 'high'
|
|
2993
3288
|
}).add((dispatcher)=>{
|
|
2994
|
-
dispatcher.on('attribute', listItemDowncastConverter(attributeNames, this._downcastStrategies, model
|
|
3289
|
+
dispatcher.on('attribute', listItemDowncastConverter(attributeNames, this._downcastStrategies, model, {
|
|
3290
|
+
enableSkipLevelLists
|
|
3291
|
+
}));
|
|
2995
3292
|
dispatcher.on('remove', listItemDowncastRemoveConverter(model.schema));
|
|
2996
3293
|
});
|
|
2997
3294
|
editor.conversion.for('dataDowncast').elementToElement({
|
|
@@ -3002,7 +3299,8 @@ function shouldUseBogusParagraph(item, attributeNames, blocks = getAllListItemBl
|
|
|
3002
3299
|
converterPriority: 'high'
|
|
3003
3300
|
}).add((dispatcher)=>{
|
|
3004
3301
|
dispatcher.on('attribute', listItemDowncastConverter(attributeNames, this._downcastStrategies, model, {
|
|
3005
|
-
dataPipeline: true
|
|
3302
|
+
dataPipeline: true,
|
|
3303
|
+
enableSkipLevelLists
|
|
3006
3304
|
}));
|
|
3007
3305
|
});
|
|
3008
3306
|
const modelToViewPositionMapper = createModelToViewPositionMapper(this._downcastStrategies, editor.editing.view);
|
|
@@ -3035,12 +3333,14 @@ function shouldUseBogusParagraph(item, attributeNames, blocks = getAllListItemBl
|
|
|
3035
3333
|
// First the low level handler.
|
|
3036
3334
|
model.document.registerPostFixer((writer)=>modelChangePostFixer$1(model, writer, attributeNames, this));
|
|
3037
3335
|
// Then the callbacks for the specific lists.
|
|
3038
|
-
// The indentation fixing must be the first one...
|
|
3039
|
-
this.
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3336
|
+
// The indentation fixing must be the first one (but only when skip levels are not allowed)...
|
|
3337
|
+
if (!this.editor.config.get('list.enableSkipLevelLists')) {
|
|
3338
|
+
this.on('postFixer', (evt, { listNodes, writer })=>{
|
|
3339
|
+
evt.return = fixListIndents(listNodes, writer) || evt.return;
|
|
3340
|
+
}, {
|
|
3341
|
+
priority: 'high'
|
|
3342
|
+
});
|
|
3343
|
+
}
|
|
3044
3344
|
// ...then the item ids... and after that other fixers that rely on the correct indentation and ids.
|
|
3045
3345
|
this.on('postFixer', (evt, { listNodes, writer, seenIds })=>{
|
|
3046
3346
|
evt.return = fixListItemIds(listNodes, seenIds, writer) || evt.return;
|
|
@@ -4651,7 +4951,8 @@ const DEFAULT_LIST_TYPE$1 = 'default';
|
|
|
4651
4951
|
dropdownView.panelView.children.add(listPropertiesView);
|
|
4652
4952
|
});
|
|
4653
4953
|
// Focus the editable after executing the command.
|
|
4654
|
-
// Overrides a default behaviour where the focus is moved to the dropdown button
|
|
4954
|
+
// Overrides a default behaviour where the focus is moved to the dropdown button.
|
|
4955
|
+
// See https://github.com/ckeditor/ckeditor5/issues/12125.
|
|
4655
4956
|
dropdownView.on('execute', ()=>{
|
|
4656
4957
|
editor.editing.view.focus();
|
|
4657
4958
|
});
|
|
@@ -6743,7 +7044,7 @@ const NUMBERED_LIST_STYLE_TYPES = [
|
|
|
6743
7044
|
if (itemToListHead.has(listHead)) {
|
|
6744
7045
|
return;
|
|
6745
7046
|
}
|
|
6746
|
-
for(// Cache previousSibling and reuse for performance reasons. See
|
|
7047
|
+
for(// Cache previousSibling and reuse for performance reasons. See https://github.com/ckeditor/ckeditor5/issues/6581.
|
|
6747
7048
|
let previousSibling = listHead.previousSibling; previousSibling && previousSibling.is('element', 'listItem'); previousSibling = listHead.previousSibling){
|
|
6748
7049
|
listHead = previousSibling;
|
|
6749
7050
|
if (itemToListHead.has(listHead)) {
|
|
@@ -7611,7 +7912,8 @@ const DEFAULT_LIST_TYPE = 'default';
|
|
|
7611
7912
|
listIndent: nextSibling.getAttribute('listIndent')
|
|
7612
7913
|
});
|
|
7613
7914
|
// The outermost list item may not exist while removing elements between lists with different value
|
|
7614
|
-
// of the `listIndent` attribute. In such a case we don't want to update anything.
|
|
7915
|
+
// of the `listIndent` attribute. In such a case we don't want to update anything.
|
|
7916
|
+
// See: https://github.com/ckeditor/ckeditor5/issues/8073.
|
|
7615
7917
|
if (!mostOuterItemList) {
|
|
7616
7918
|
return;
|
|
7617
7919
|
}
|
|
@@ -7640,7 +7942,7 @@ const DEFAULT_LIST_TYPE = 'default';
|
|
|
7640
7942
|
direction: 'forward'
|
|
7641
7943
|
});
|
|
7642
7944
|
// If the selection ends in a non-list element, there are no <listItem>s that would require adjustments.
|
|
7643
|
-
// See:
|
|
7945
|
+
// See: https://github.com/ckeditor/ckeditor5/issues/8642.
|
|
7644
7946
|
if (!secondListMostOuterItem) {
|
|
7645
7947
|
firstMostOuterItem = null;
|
|
7646
7948
|
return;
|
|
@@ -7976,7 +8278,8 @@ const DEFAULT_LIST_TYPE = 'default';
|
|
|
7976
8278
|
// ■ Paragraph[] // <-- The inserted item.
|
|
7977
8279
|
while(existingListItem.is('element', 'listItem') && existingListItem.getAttribute('listIndent') !== indent){
|
|
7978
8280
|
existingListItem = existingListItem.previousSibling;
|
|
7979
|
-
// If the item does not exist, most probably there is no other content in the editor.
|
|
8281
|
+
// If the item does not exist, most probably there is no other content in the editor.
|
|
8282
|
+
// See: https://github.com/ckeditor/ckeditor5/issues/8072.
|
|
7980
8283
|
if (!existingListItem) {
|
|
7981
8284
|
break;
|
|
7982
8285
|
}
|
|
@@ -7999,7 +8302,7 @@ const DEFAULT_LIST_TYPE = 'default';
|
|
|
7999
8302
|
wasFixed = true;
|
|
8000
8303
|
} else {
|
|
8001
8304
|
// Adjust the `listStyle`, `listReversed` and `listStart`
|
|
8002
|
-
// attributes for inserted (pasted) items. See
|
|
8305
|
+
// attributes for inserted (pasted) items. See https://github.com/ckeditor/ckeditor5/issues/8160.
|
|
8003
8306
|
//
|
|
8004
8307
|
// ■ List item 1. // [listStyle="square", listType="bulleted"]
|
|
8005
8308
|
// ○ List item 1.1. // [listStyle="circle", listType="bulleted"]
|