Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ck/10975 document list commands #11057

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
3ceff03
Initial implementation of document list indent command.
niegowski Dec 16, 2021
5d01e24
Indent command. Added utils.
niegowski Dec 16, 2021
888dab5
Added tests.
niegowski Dec 16, 2021
3fb8c15
Added tests.
niegowski Dec 17, 2021
8ab36ae
Added tests.
niegowski Dec 17, 2021
984a4a5
Added tests.
niegowski Dec 17, 2021
557924a
Ported plain list tests.
niegowski Dec 17, 2021
ddc1d72
Added tests.
niegowski Dec 17, 2021
cd91fd4
Refactored utils file.
niegowski Dec 17, 2021
177d443
Introducing ListWalker.
niegowski Dec 17, 2021
ef73a21
Small code refactor.
niegowski Dec 17, 2021
f9ab04a
Code cleanup.
niegowski Dec 17, 2021
66082af
ListWalker tuning.
niegowski Dec 18, 2021
5098f12
Added tests.
niegowski Dec 18, 2021
81dd56a
Changed for loops with sibling iterator.
niegowski Dec 18, 2021
e0266ea
Fixed typos.
niegowski Dec 18, 2021
ea4245f
Code refactor.
niegowski Dec 18, 2021
fdccf87
Implemented DocumentListUI. Basic DocumentListCommand.
oleq Dec 15, 2021
a3e32dc
Tests
oleq Dec 21, 2021
5b72665
Added tests.
niegowski Dec 21, 2021
1f6e3f6
Merging sub-list while outdenting.
niegowski Dec 21, 2021
7af112f
WiP.
niegowski Dec 22, 2021
672a113
Adding test utils.
niegowski Dec 22, 2021
20f1a3a
Merge branch 'ck/10974-document-list-indentcommand' into ck/10975-doc…
oleq Dec 23, 2021
1ce325a
WiP.
niegowski Dec 23, 2021
accba16
Tuned indent command behavior.
niegowski Dec 27, 2021
4fbe7ba
WiP.
niegowski Dec 27, 2021
166c761
Fix tests inentation.
niegowski Dec 28, 2021
2bc8aa7
Updating tests.
niegowski Dec 28, 2021
730597a
Merge remote-tracking branch 'origin/ck/10975-document-list-command' …
niegowski Dec 28, 2021
94f4308
Added tests.
niegowski Dec 28, 2021
68a74ba
Added tests.
niegowski Dec 28, 2021
5268746
Added tests.
niegowski Dec 28, 2021
5baf302
Code cleanup.
niegowski Dec 28, 2021
b32a2b9
Added tests.
niegowski Dec 28, 2021
b29f0fc
Added tests.
niegowski Dec 28, 2021
b48ea18
Rename smaller -> lower and bigger -> higher.
niegowski Dec 28, 2021
30b9f38
ULs and OLs view elements have an ID to make sure merging sibling att…
niegowski Dec 28, 2021
80724fa
Updated docs.
niegowski Dec 29, 2021
ff0e253
Manual test for mocking list
Dec 29, 2021
61aaec5
Added tests.
niegowski Dec 29, 2021
5cb608f
Added support for custom ID in modelList() helper.
niegowski Dec 29, 2021
6bdb29b
New list item ID generator.
niegowski Dec 29, 2021
f14bd50
Updated docs.
niegowski Dec 29, 2021
a91bcac
Fix tests
CatStrategist Dec 29, 2021
a5462a1
Merge branch 'ck/10975-document-list-command' of github.com:ckeditor/…
CatStrategist Dec 29, 2021
518d9d9
Added tests.
niegowski Dec 29, 2021
3613942
Added tests.
niegowski Dec 29, 2021
c888236
Added tests.
niegowski Dec 30, 2021
d902a81
Merge branch 'ck/10812-document-list-editing' into ck/10975-document-…
niegowski Dec 30, 2021
bf56228
Replace string model with ASCII lists in utils tests
CatStrategist Dec 30, 2021
3f9861e
Merge branch 'ck/10975-document-list-command' of github.com:ckeditor/…
CatStrategist Dec 30, 2021
5a00db8
Added list mocking manual test
CatStrategist Jan 4, 2022
f6d94bb
Updated docs. Cleanup.
niegowski Jan 4, 2022
800a927
Fix lost mocking manual test
CatStrategist Jan 4, 2022
0d27ada
Merge branch 'ck/10975-document-list-command' of github.com:ckeditor/…
CatStrategist Jan 4, 2022
b340587
Fix list mocking manual test label
CatStrategist Jan 4, 2022
ceb31f1
Code refactoring.
oleq Jan 4, 2022
e3e2c9b
Apply review comment.
niegowski Jan 4, 2022
162d0fd
Apply review comment.
niegowski Jan 4, 2022
2dc9e98
Apply review comment.
niegowski Jan 4, 2022
31b6aa3
Apply review comment.
niegowski Jan 4, 2022
76fcdfc
Apply review comment.
niegowski Jan 4, 2022
77b7684
Updated docs.
niegowski Jan 4, 2022
7cfad2c
Updated docs.
niegowski Jan 4, 2022
842c89b
Util functions renaming.
niegowski Jan 4, 2022
e89652c
Fix list mocking manual test
CatStrategist Jan 7, 2022
a4d2624
Tests: Indent/Outdent refactoring WIP.
oleq Jan 10, 2022
c0bedaa
Outdent/indent aligns list item types.
niegowski Jan 10, 2022
b2bb055
Fix copyoutput mockinglist
CatStrategist Jan 10, 2022
a32f75d
Merge branch 'ck/10975-document-list-command' of github.com:ckeditor/…
CatStrategist Jan 10, 2022
5158462
Cleaning.
niegowski Jan 10, 2022
97e5aeb
Fixed model list stringify.
niegowski Jan 10, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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