Skip to content

Commit

Permalink
Merge pull request #11057 from ckeditor/ck/10975-document-list-command
Browse files Browse the repository at this point in the history
Internal: Added `DocumentListCommand` and `DocumentListIndentCommand`. Closes #10974. Closes #10975.
  • Loading branch information
niegowski authored Jan 10, 2022
2 parents 60de4ad + 97e5aeb commit ba75822
Show file tree
Hide file tree
Showing 30 changed files with 8,896 additions and 2,163 deletions.
36 changes: 36 additions & 0 deletions packages/ckeditor5-list/src/documentlist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module list/documentlist
*/

import { Plugin } from 'ckeditor5/src/core';
import DocumentListEditing from './documentlist/documentlistediting';
import ListUI from './list/listui';

/**
* The document list feature.
*
* This is a "glue" plugin that loads the {@link module:list/documentlist/documentlistediting~DocumentListEditing document list
* editing feature} and {@link module:list/list/listui~ListUI list UI feature}.
*
* @extends module:core/plugin~Plugin
*/
export default class DocumentList extends Plugin {
/**
* @inheritDoc
*/
static get requires() {
return [ DocumentListEditing, ListUI ];
}

/**
* @inheritDoc
*/
static get pluginName() {
return 'DocumentList';
}
}
215 changes: 123 additions & 92 deletions packages/ckeditor5-list/src/documentlist/converters.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,28 @@
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module list/documentlist/converters
*/

import {
getAllListItemBlocks,
getListItemBlocks,
ListItemUid
} from './utils/model';
import {
createListElement,
createListItemElement,
getAllListItemElements,
getIndent,
getSiblingListItem,
isListView,
isListItemView,
getListItemElements,
findAndAddListHeadToMap,
getViewElementNameForListType
} from './utils';
import { uid } from 'ckeditor5/src/utils';
import { UpcastWriter } from 'ckeditor5/src/engine';
getViewElementNameForListType,
getViewElementIdForListType
} from './utils/view';
import ListWalker, { iterateSiblingListBlocks } from './utils/listwalker';
import { findAndAddListHeadToMap } from './utils/postfixers';

/**
* @module list/documentlist/converters
*/
import { UpcastWriter } from 'ckeditor5/src/engine';

/**
* Returns the upcast converter for list items. It's supposed to work after the block converters (content inside list items) is converted.
Expand All @@ -44,7 +48,7 @@ export function listItemUpcastConverter() {
}

const attributes = {
listItemId: uid(),
listItemId: ListItemUid.next(),
listIndent: getIndent( data.viewItem ),
listType: data.viewItem.parent && data.viewItem.parent.name == 'ol' ? 'numbered' : 'bulleted'
};
Expand Down Expand Up @@ -108,7 +112,7 @@ export function listUpcastCleanList() {
export function reconvertItemsOnDataChange( model, editing ) {
return () => {
const changes = model.document.differ.getChanges();
const itemsToRefresh = new Set();
const itemsToRefresh = [];
const itemToListHead = new Map();
const changedItems = new Set();

Expand Down Expand Up @@ -136,116 +140,142 @@ export function reconvertItemsOnDataChange( model, editing ) {

if ( entry.attributeNewValue === null ) {
findAndAddListHeadToMap( entry.range.start.getShiftedBy( 1 ), itemToListHead );
refreshItemParagraphIfNeeded( item, [] );

// Check if paragraph should be converted from bogus to plain paragraph.
// Passing empty array to not look for other blocks because it's already gone from the model.
if ( doesItemParagraphRequiresRefresh( item, [] ) ) {
itemsToRefresh.push( item );
}
} else {
changedItems.add( item );
}
} else if ( item.hasAttribute( 'listItemId' ) ) {
refreshItemParagraphIfNeeded( item );
// Some other attribute was changed on the list item,
// check if paragraph does not need to be converted to bogus or back.
if ( doesItemParagraphRequiresRefresh( item ) ) {
itemsToRefresh.push( item );
}
}
}
}

for ( const listHead of itemToListHead.values() ) {
checkList( listHead );
itemsToRefresh.push( ...collectListItemsToRefresh( listHead, changedItems ) );
}

for ( const item of itemsToRefresh ) {
for ( const item of new Set( itemsToRefresh ) ) {
editing.reconvertItem( item );
}
};

function checkList( listHead ) {
const visited = new Set();
const stack = [];

for (
let prev = null, item = listHead;
item && item.hasAttribute( 'listItemId' );
prev = item, item = item.nextSibling
) {
if ( visited.has( item ) ) {
continue;
}
function collectListItemsToRefresh( listHead, changedItems ) {
const itemsToRefresh = [];
const visited = new Set();
const stack = [];

for ( const { node, previous } of iterateSiblingListBlocks( listHead, 'forward' ) ) {
if ( visited.has( node ) ) {
continue;
}

const itemIndent = item.getAttribute( 'listIndent' );
const itemIndent = node.getAttribute( 'listIndent' );

if ( prev && itemIndent < prev.getAttribute( 'listIndent' ) ) {
stack.length = itemIndent + 1;
}
// Current node is at the lower indent so trim the stack.
if ( previous && itemIndent < previous.getAttribute( 'listIndent' ) ) {
stack.length = itemIndent + 1;
}

stack[ itemIndent ] = {
id: item.getAttribute( 'listItemId' ),
type: item.getAttribute( 'listType' )
};
// Update the stack for the current indent level.
stack[ itemIndent ] = {
id: node.getAttribute( 'listItemId' ),
type: node.getAttribute( 'listType' )
};

const blocks = getListItemElements( item, 'forward' );
// Find all blocks of the current node.
const blocks = getListItemBlocks( node, { direction: 'forward' } );

for ( const block of blocks ) {
visited.add( block );
for ( const block of blocks ) {
visited.add( block );

refreshItemParagraphIfNeeded( block, blocks );
refreshItemWrappingIfNeeded( block, stack );
// Check if bogus vs plain paragraph needs refresh.
if ( doesItemParagraphRequiresRefresh( block, blocks ) ) {
itemsToRefresh.push( block );
}
// Check if wrapping with UL, OL, LIs needs refresh.
else if ( doesItemWrappingRequiresRefresh( block, stack, changedItems ) ) {
itemsToRefresh.push( block );
}
}
}

function refreshItemParagraphIfNeeded( item, blocks ) {
if ( !item.is( 'element', 'paragraph' ) ) {
return;
}
return itemsToRefresh;
}

const viewElement = editing.mapper.toViewElement( item );
function doesItemParagraphRequiresRefresh( item, blocks ) {
if ( !item.is( 'element', 'paragraph' ) ) {
return false;
}

if ( !viewElement ) {
return;
}
const viewElement = editing.mapper.toViewElement( item );

const useBogus = shouldUseBogusParagraph( item, blocks );
if ( !viewElement ) {
return false;
}

if ( useBogus && viewElement.is( 'element', 'p' ) ) {
itemsToRefresh.add( item );
} else if ( !useBogus && viewElement.is( 'element', 'span' ) ) {
itemsToRefresh.add( item );
}
const useBogus = shouldUseBogusParagraph( item, blocks );

if ( useBogus && viewElement.is( 'element', 'p' ) ) {
return true;
} else if ( !useBogus && viewElement.is( 'element', 'span' ) ) {
return true;
}

function refreshItemWrappingIfNeeded( item, stack ) {
// Items directly affected by some "change" don't need a refresh, they will be converted by their own changes.
if ( changedItems.has( item ) ) {
return;
}
return false;
}

const viewElement = editing.mapper.toViewElement( item );
let stackIdx = stack.length - 1;

for (
let element = viewElement.parent;
!element.is( 'editableElement' );
element = element.parent
) {
if ( isListItemView( element ) ) {
if ( element.id != stack[ stackIdx ].id ) {
break;
}
} else if ( isListView( element ) ) {
const expectedElementName = getViewElementNameForListType( stack[ stackIdx ].type );
function doesItemWrappingRequiresRefresh( item, stack, changedItems ) {
// Items directly affected by some "change" don't need a refresh, they will be converted by their own changes.
if ( changedItems.has( item ) ) {
return false;
}

if ( element.name != expectedElementName ) {
break;
}
const viewElement = editing.mapper.toViewElement( item );
let indent = stack.length - 1;

// Traverse down the stack to the root to verify if all ULs, OLs, and LIs are as expected.
for (
let element = viewElement.parent;
!element.is( 'editableElement' );
element = element.parent
) {
if ( isListItemView( element ) ) {
const expectedElementId = stack[ indent ].id;

// For LI verify if an ID of the attribute element is correct.
if ( element.id != expectedElementId ) {
break;
}
} else if ( isListView( element ) ) {
const type = stack[ indent ].type;
const expectedElementName = getViewElementNameForListType( type );
const expectedElementId = getViewElementIdForListType( type, indent );

// For UL and OL check if the name and ID of element is correct.
if ( element.name != expectedElementName || element.id != expectedElementId ) {
break;
}

stackIdx--;
indent--;

// Don't need to iterate further if we already know that the item is wrapped appropriately.
if ( stackIdx < 0 ) {
return;
}
// Don't need to iterate further if we already know that the item is wrapped appropriately.
if ( indent < 0 ) {
return false;
}
}

itemsToRefresh.add( item );
}
};

return true;
}
}

/**
Expand Down Expand Up @@ -380,14 +410,15 @@ export function listItemParagraphDowncastConverter( attributes, model, { dataPip
// Find the range over the bogus paragraph (or just an inline content in the data pipeline).
let viewRange;

if ( !dataPipeline ) {
viewRange = writer.createRangeOn( paragraphElement );
} else {
if ( dataPipeline ) {
// Unwrap paragraph content from bogus paragraph.
viewRange = writer.move( writer.createRangeIn( paragraphElement ), viewPosition );

writer.remove( paragraphElement );
mapper.unbindViewElement( paragraphElement );
} else {
// Use range on the bogus paragraph to wrap it with ULs and LIs.
viewRange = writer.createRangeOn( paragraphElement );
}

// Then wrap it with the list wrappers.
Expand Down Expand Up @@ -448,9 +479,9 @@ function wrapListItemBlock( listItem, viewRange, writer ) {
break;
}

currentListItem = getSiblingListItem( currentListItem, { smallerIndent: true, listIndent: indent } );
currentListItem = ListWalker.first( currentListItem, { lowerIndent: true } );

// There is no list item with smaller indent, this means this is a document fragment containing
// There is no list item with lower indent, this means this is a document fragment containing
// only a part of nested list (like copy to clipboard) so we don't need to try to wrap it further.
if ( !currentListItem ) {
break;
Expand Down Expand Up @@ -500,7 +531,7 @@ function getListItemFillerOffset() {
}

// Whether the given item should be rendered as a bogus paragraph.
function shouldUseBogusParagraph( item, blocks = getAllListItemElements( item ) ) {
function shouldUseBogusParagraph( item, blocks = getAllListItemBlocks( item ) ) {
if ( !item.hasAttribute( 'listItemId' ) ) {
return false;
}
Expand Down
Loading

0 comments on commit ba75822

Please sign in to comment.