From 3ceff034b8c5dadb73b0f03ddb03ced723c1b813 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 16 Dec 2021 16:17:45 +0100 Subject: [PATCH 01/66] Initial implementation of document list indent command. --- packages/ckeditor5-list/src/documentlist.js | 36 +++++ .../src/documentlist/documentlistediting.js | 23 ++++ .../documentlist/documentlistindentcommand.js | 129 ++++++++++++++++++ .../ckeditor5-list/src/documentlist/utils.js | 36 ++++- .../tests/manual/documentlist.js | 29 ++-- 5 files changed, 231 insertions(+), 22 deletions(-) create mode 100644 packages/ckeditor5-list/src/documentlist.js create mode 100644 packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js diff --git a/packages/ckeditor5-list/src/documentlist.js b/packages/ckeditor5-list/src/documentlist.js new file mode 100644 index 00000000000..02d22b04eec --- /dev/null +++ b/packages/ckeditor5-list/src/documentlist.js @@ -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'; + } +} diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.js b/packages/ckeditor5-list/src/documentlist/documentlistediting.js index a9a0202b41a..2f54d35b364 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.js @@ -12,6 +12,7 @@ import { Enter } from 'ckeditor5/src/enter'; import { Delete } from 'ckeditor5/src/typing'; import { CKEditorError } from 'ckeditor5/src/utils'; +import DocumentListIndentCommand from './documentlistindentcommand'; import { listItemDowncastConverter, listItemParagraphDowncastConverter, @@ -72,6 +73,28 @@ export default class DocumentListEditing extends Plugin { model.on( 'insertContent', createModelIndentPasteFixer( model ), { priority: 'high' } ); this._setupConversion(); + + // Register commands for indenting. + editor.commands.add( 'indentList', new DocumentListIndentCommand( editor, 'forward' ) ); + editor.commands.add( 'outdentList', new DocumentListIndentCommand( editor, 'backward' ) ); + } + + /** + * @inheritDoc + */ + afterInit() { + const commands = this.editor.commands; + + const indent = commands.get( 'indent' ); + const outdent = commands.get( 'outdent' ); + + if ( indent ) { + indent.registerChildCommand( commands.get( 'indentList' ) ); + } + + if ( outdent ) { + outdent.registerChildCommand( commands.get( 'outdentList' ) ); + } } /** diff --git a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js new file mode 100644 index 00000000000..8021962be22 --- /dev/null +++ b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js @@ -0,0 +1,129 @@ +/** + * @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/documentlistindentcommand + */ + +import { Command } from 'ckeditor5/src/core'; +import { first } from 'ckeditor5/src/utils'; +import { getNestedListItems, getSiblingListItem } from './utils'; + +/** + * The document list indent command. It is used by the {@link module:list/documentlist~DocumentList list feature}. + * + * @extends module:core/command~Command + */ +export default class DocumentListIndentCommand extends Command { + /** + * Creates an instance of the command. + * + * @param {module:core/editor/editor~Editor} editor The editor instance. + * @param {'forward'|'backward'} indentDirection The direction of indent. If it is equal to `backward`, the command + * will outdent a list item. + */ + constructor( editor, indentDirection ) { + super( editor ); + + /** + * Determines by how much the command will change the list item's indent attribute. + * + * @readonly + * @private + * @member {Number} + */ + this._indentBy = indentDirection == 'forward' ? 1 : -1; + } + + /** + * @inheritDoc + */ + refresh() { + this.isEnabled = this._checkEnabled(); + } + + /** + * Indents or outdents (depending on the {@link #constructor}'s `indentDirection` parameter) selected list items. + * + * @fires execute + * @fires _executeCleanup + */ + execute() { + const model = this.editor.model; + const doc = model.document; + const blocks = Array.from( doc.selection.getSelectedBlocks() ); + + // TODO outdent of the following block of list item should split it from previous block + // TODO intdent of the following block of list item should do nothing + + model.change( writer => { + // const firstItem = blocks[ 0 ]; + const lastItem = blocks[ blocks.length - 1 ]; + + // Indenting a list item should also indent all the items that are already sub-items of indented item. + for ( const block of getNestedListItems( lastItem ) ) { + blocks.push( block ); + } + + for ( const item of blocks ) { + const indent = item.getAttribute( 'listIndent' ) + this._indentBy; + + if ( indent < 0 ) { + for ( const attributeKey of item.getAttributeKeys() ) { + if ( attributeKey.startsWith( 'list' ) ) { + writer.removeAttribute( attributeKey, item ); + } + } + } else { + writer.setAttribute( 'listIndent', indent, item ); + // TODO alter the listItemId + } + } + + /** + * Event fired by the {@link #execute} method. + * + * It allows to execute an action after executing the {@link ~DocumentListIndentCommand#execute} method, + * for example adjusting attributes of changed list items. + * + * @protected + * @event afterExecute + */ + this.fire( 'afterExecute', blocks ); + } ); + } + + /** + * Checks whether the command can be enabled in the current context. + * + * @private + * @returns {Boolean} Whether the command should be enabled. + */ + _checkEnabled() { + // Check whether any of position's ancestor is a list item. + const listItem = first( this.editor.model.document.selection.getSelectedBlocks() ); + + // If selection is not in a list item, the command is disabled. + if ( !listItem || !listItem.hasAttribute( 'listItemId' ) ) { + return false; + } + + // If we are outdenting it is enough to be in list item. Every list item can always be outdented. + if ( this._indentBy < 0 ) { + return true; + } + + const siblingItem = getSiblingListItem( listItem.previousSibling, { + listIndent: listItem.getAttribute( 'listIndent' ), + sameIndent: true + } ); + + if ( !siblingItem ) { + return false; + } + + return siblingItem.getAttribute( 'listType' ) == listItem.getAttribute( 'listType' ); + } +} diff --git a/packages/ckeditor5-list/src/documentlist/utils.js b/packages/ckeditor5-list/src/documentlist/utils.js index a5ca67bd990..29335e3d68e 100644 --- a/packages/ckeditor5-list/src/documentlist/utils.js +++ b/packages/ckeditor5-list/src/documentlist/utils.js @@ -165,12 +165,16 @@ export function getSiblingListItem( modelElement, options ) { ) { const itemIndent = item.getAttribute( 'listIndent' ); + if ( itemIndent > indent ) { + continue; + } + if ( sameIndent && itemIndent == indent ) { return item; } - if ( smallerIndent && itemIndent < indent ) { - return item; + if ( itemIndent < indent ) { + return smallerIndent ? item : null; } } @@ -216,14 +220,16 @@ export function getListItemElements( listItem, direction ) { item && item.hasAttribute( 'listItemId' ); item = isForward ? item.nextSibling : item.previousSibling ) { + const itemIndent = item.getAttribute( 'listIndent' ); + // If current parsed item has lower indent that element that the element that was a starting point, // it means we left a nested list. Abort searching items. - if ( item.getAttribute( 'listIndent' ) < limitIndent ) { + if ( itemIndent < limitIndent ) { break; } // Ignore nested lists. - if ( item.getAttribute( 'listIndent' ) > limitIndent ) { + if ( itemIndent > limitIndent ) { continue; } @@ -238,6 +244,28 @@ export function getListItemElements( listItem, direction ) { return isForward ? items : items.reverse(); } +/** + * TODO + */ +export function getNestedListItems( listItem ) { + const indent = listItem.getAttribute( 'listIndent' ); + const items = []; + + for ( + let item = listItem.nextSibling; + item && item.hasAttribute( 'listItemId' ); + item = item.nextSibling + ) { + if ( item.getAttribute( 'listIndent' ) <= indent ) { + break; + } + + items.push( item ); + } + + return items; +} + /** * Based on the provided positions looks for the list head and stores it in the provided map. * diff --git a/packages/ckeditor5-list/tests/manual/documentlist.js b/packages/ckeditor5-list/tests/manual/documentlist.js index d9ee03d5ef3..46d36aac529 100644 --- a/packages/ckeditor5-list/tests/manual/documentlist.js +++ b/packages/ckeditor5-list/tests/manual/documentlist.js @@ -14,7 +14,6 @@ import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalli import HtmlEmbed from '@ckeditor/ckeditor5-html-embed/src/htmlembed'; import HtmlComment from '@ckeditor/ckeditor5-html-support/src/htmlcomment'; import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize'; -import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock'; import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage'; import PageBreak from '@ckeditor/ckeditor5-page-break/src/pagebreak'; import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting'; @@ -39,31 +38,25 @@ import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar'; import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; -import DocumentListEditing from '../../src/documentlist/documentlistediting'; +import DocumentList from '../../src/documentlist'; ClassicEditor .create( document.querySelector( '#editor' ), { plugins: [ Essentials, BlockQuote, Bold, Heading, Image, ImageCaption, ImageStyle, ImageToolbar, Indent, Italic, Link, MediaEmbed, Paragraph, Table, TableToolbar, CodeBlock, TableCaption, EasyImage, ImageResize, LinkImage, - AutoImage, HtmlEmbed, HtmlComment, Alignment, IndentBlock, PageBreak, HorizontalLine, ImageUpload, - CloudServices, SourceEditing, DocumentListEditing + AutoImage, HtmlEmbed, HtmlComment, Alignment, PageBreak, HorizontalLine, ImageUpload, + CloudServices, SourceEditing, DocumentList ], toolbar: [ - 'sourceEditing', - '|', - 'heading', - '|', - 'bold', 'italic', 'link', - '|', - 'blockQuote', 'uploadImage', 'insertTable', 'mediaEmbed', 'codeBlock', - '|', - 'htmlEmbed', - '|', - 'alignment', 'outdent', 'indent', - '|', - 'pageBreak', 'horizontalLine', - '|', + 'sourceEditing', '|', + 'outdent', 'indent', '|', + 'heading', '|', + 'bold', 'italic', 'link', '|', + 'blockQuote', 'uploadImage', 'insertTable', 'mediaEmbed', 'codeBlock', '|', + 'htmlEmbed', '|', + 'alignment', '|', + 'pageBreak', 'horizontalLine', '|', 'undo', 'redo' ], cloudServices: CS_CONFIG, From 5d01e24fd1d4773c8fd2d7e09882b1ae9ce20f5f Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 16 Dec 2021 20:56:00 +0100 Subject: [PATCH 02/66] Indent command. Added utils. --- .../src/documentlist/converters.js | 12 +- .../documentlist/documentlistindentcommand.js | 81 ++++++++---- .../ckeditor5-list/src/documentlist/utils.js | 125 ++++++++++++++++-- .../tests/documentlist/utils.js | 62 ++++----- 4 files changed, 207 insertions(+), 73 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/converters.js b/packages/ckeditor5-list/src/documentlist/converters.js index 230016ed7e1..ae0e26aa8b9 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.js +++ b/packages/ckeditor5-list/src/documentlist/converters.js @@ -6,12 +6,12 @@ import { createListElement, createListItemElement, - getAllListItemElements, + getAllListItemBlocks, getIndent, - getSiblingListItem, + getSiblingListBlock, isListView, isListItemView, - getListItemElements, + getListItemBlocks, findAndAddListHeadToMap, getViewElementNameForListType } from './utils'; @@ -178,7 +178,7 @@ export function reconvertItemsOnDataChange( model, editing ) { type: item.getAttribute( 'listType' ) }; - const blocks = getListItemElements( item, 'forward' ); + const blocks = getListItemBlocks( item, 'forward' ); for ( const block of blocks ) { visited.add( block ); @@ -448,7 +448,7 @@ function wrapListItemBlock( listItem, viewRange, writer ) { break; } - currentListItem = getSiblingListItem( currentListItem, { smallerIndent: true, listIndent: indent } ); + currentListItem = getSiblingListBlock( currentListItem, { smallerIndent: true, listIndent: indent } ); // There is no list item with smaller 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. @@ -500,7 +500,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; } diff --git a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js index 8021962be22..dd01f2c9552 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js @@ -8,8 +8,14 @@ */ import { Command } from 'ckeditor5/src/core'; -import { first } from 'ckeditor5/src/utils'; -import { getNestedListItems, getSiblingListItem } from './utils'; +import { + expandListBlocksToCompleteItems, + getNestedListBlocks, + getSiblingListBlock, + indentBlocks, + isFirstBlockOfListItem, + splitListItemBefore +} from './utils'; /** * The document list indent command. It is used by the {@link module:list/documentlist~DocumentList list feature}. @@ -55,32 +61,27 @@ export default class DocumentListIndentCommand extends Command { const doc = model.document; const blocks = Array.from( doc.selection.getSelectedBlocks() ); - // TODO outdent of the following block of list item should split it from previous block - // TODO intdent of the following block of list item should do nothing - model.change( writer => { - // const firstItem = blocks[ 0 ]; - const lastItem = blocks[ blocks.length - 1 ]; + // Handle selection contained in the single list item and starting in the following blocks. + if ( startsInTheMiddleOfTheOnlyOneSelectedListItem( blocks ) ) { + // Do nothing while indenting, but split list item on outdent. + if ( this._indentBy < 0 ) { + splitListItemBefore( blocks[ 0 ], writer ); + } + + return; + } + + // Expand the selected blocks to contain the whole list items. + expandListBlocksToCompleteItems( blocks ); // Indenting a list item should also indent all the items that are already sub-items of indented item. - for ( const block of getNestedListItems( lastItem ) ) { + for ( const block of getNestedListBlocks( blocks[ blocks.length - 1 ] ) ) { blocks.push( block ); } - for ( const item of blocks ) { - const indent = item.getAttribute( 'listIndent' ) + this._indentBy; - - if ( indent < 0 ) { - for ( const attributeKey of item.getAttributeKeys() ) { - if ( attributeKey.startsWith( 'list' ) ) { - writer.removeAttribute( attributeKey, item ); - } - } - } else { - writer.setAttribute( 'listIndent', indent, item ); - // TODO alter the listItemId - } - } + // Now just update the attributes of blocks. + indentBlocks( blocks, this._indentBy, writer ); /** * Event fired by the {@link #execute} method. @@ -103,10 +104,11 @@ export default class DocumentListIndentCommand extends Command { */ _checkEnabled() { // Check whether any of position's ancestor is a list item. - const listItem = first( this.editor.model.document.selection.getSelectedBlocks() ); + const blocks = Array.from( this.editor.model.document.selection.getSelectedBlocks() ); + let firstBlock = blocks[ 0 ]; // If selection is not in a list item, the command is disabled. - if ( !listItem || !listItem.hasAttribute( 'listItemId' ) ) { + if ( !firstBlock || !firstBlock.hasAttribute( 'listItemId' ) ) { return false; } @@ -115,8 +117,17 @@ export default class DocumentListIndentCommand extends Command { return true; } - const siblingItem = getSiblingListItem( listItem.previousSibling, { - listIndent: listItem.getAttribute( 'listIndent' ), + // Indenting of the following blocks of a list item is not allowed. + if ( startsInTheMiddleOfTheOnlyOneSelectedListItem( blocks ) ) { + return false; + } + + expandListBlocksToCompleteItems( blocks ); + firstBlock = blocks[ 0 ]; + + // Check if there is any list item before selected items that could become a parent of selected items. + const siblingItem = getSiblingListBlock( firstBlock.previousSibling, { + listIndent: firstBlock.getAttribute( 'listIndent' ), sameIndent: true } ); @@ -124,6 +135,22 @@ export default class DocumentListIndentCommand extends Command { return false; } - return siblingItem.getAttribute( 'listType' ) == listItem.getAttribute( 'listType' ); + return siblingItem.getAttribute( 'listType' ) == firstBlock.getAttribute( 'listType' ); + } +} + +// Checks whether the given blocks are related to a single list item and does not include the first block of the list item. +function startsInTheMiddleOfTheOnlyOneSelectedListItem( blocks ) { + const firstItem = blocks[ 0 ]; + + // It's not a middle block; + if ( isFirstBlockOfListItem( firstItem ) ) { + return false; } + + const firstItemId = firstItem.getAttribute( 'listItemId' ); + const isSingleListItemSelected = !blocks.some( item => item.getAttribute( 'listItemId' ) != firstItemId ); + + // Is only one list item is selected? + return isSingleListItemSelected; } diff --git a/packages/ckeditor5-list/src/documentlist/utils.js b/packages/ckeditor5-list/src/documentlist/utils.js index 29335e3d68e..6b7fa2cf09c 100644 --- a/packages/ckeditor5-list/src/documentlist/utils.js +++ b/packages/ckeditor5-list/src/documentlist/utils.js @@ -152,7 +152,7 @@ export function getViewElementNameForListType( type ) { * @param {'forward'|'backward'} [options.direction='backward'] The search direction. * @return {module:engine/model/element~Element|null} */ -export function getSiblingListItem( modelElement, options ) { +export function getSiblingListBlock( modelElement, options ) { const sameIndent = !!options.sameIndent; const smallerIndent = !!options.smallerIndent; const indent = options.listIndent; @@ -190,10 +190,10 @@ export function getSiblingListItem( modelElement, options ) { * @param {module:engine/model/element~Element} listItem Starting list item element. * @return {Array.} */ -export function getAllListItemElements( listItem ) { +export function getAllListItemBlocks( listItem ) { return [ - ...getListItemElements( listItem, 'backward' ), - ...getListItemElements( listItem, 'forward' ) + ...getListItemBlocks( listItem, 'backward' ), + ...getListItemBlocks( listItem, 'forward' ) ]; } @@ -206,10 +206,10 @@ export function getAllListItemElements( listItem ) { * * @protected * @param {module:engine/model/element~Element} listItem Starting list item element. - * @param {'forward'|'backward'} direction Walking direction. + * @param {'forward'|'backward'} [direction='backward'] Walking direction. * @returns {Array.} */ -export function getListItemElements( listItem, direction ) { +export function getListItemBlocks( listItem, direction ) { const limitIndent = listItem.getAttribute( 'listIndent' ); const listItemId = listItem.getAttribute( 'listItemId' ); const isForward = direction == 'forward'; @@ -245,9 +245,13 @@ export function getListItemElements( listItem, direction ) { } /** - * TODO + * Returns a list items nested inside the given list item. + * + * @protected + * @param {module:engine/model/element~Element} listItem Starting list item element. + * @returns {Array.} */ -export function getNestedListItems( listItem ) { +export function getNestedListBlocks( listItem ) { const indent = listItem.getAttribute( 'listIndent' ); const items = []; @@ -266,6 +270,109 @@ export function getNestedListItems( listItem ) { return items; } +/** + * Check if the given block is the first in the list item. + * + * @protected + * @param {module:engine/model/element~Element} listBlock The list block element. + * @returns {Boolean} + */ +export function isFirstBlockOfListItem( listBlock ) { + const previousSibling = getSiblingListBlock( listBlock.previousSibling, { + listIndent: listBlock.getAttribute( 'listIndent' ), + sameIndent: true + } ); + + if ( !previousSibling ) { + return true; + } + + return previousSibling.getAttribute( 'listItemId' ) != listBlock.getAttribute( 'listItemId' ); +} + +/** + * Check if the given block is the last in the list item. + * + * @protected + * @param {module:engine/model/element~Element} listBlock The list block element. + * @returns {Boolean} + */ +export function isLastBlockOfListItem( listBlock ) { + const nextSibling = getSiblingListBlock( listBlock.nextSibling, { + listIndent: listBlock.getAttribute( 'listIndent' ), + direction: 'forward', + sameIndent: true + } ); + + if ( !nextSibling ) { + return true; + } + + return nextSibling.getAttribute( 'listItemId' ) != listBlock.getAttribute( 'listItemId' ); +} + +/** + * Expands the given list of selected blocks to include the leading and tailing blocks of partially selected list items. + * + * @protected + * @param {Array.} blocks The list of selected blocks. + */ +export function expandListBlocksToCompleteItems( blocks ) { + const firstBlock = blocks[ 0 ]; + const lastBlock = blocks[ blocks.length - 1 ]; + + // Add missing blocks of the first selected list item. + for ( const item of getListItemBlocks( firstBlock, 'backward' ) ) { + blocks.unshift( item ); + } + + // Add missing blocks of the last selected list item. + for ( const item of getListItemBlocks( lastBlock, 'forward' ) ) { + if ( item != lastBlock ) { + blocks.push( item ); + } + } +} + +/** + * Splits the list item just before the provided list block. + * + * @protected + * @param {module:engine/model/element~Element} listBlock The list block element. + * @param {module:engine/model/writer~Writer} writer The model writer. + */ +export function splitListItemBefore( listBlock, writer ) { + const id = uid(); + + for ( const item of getListItemBlocks( listBlock, 'forward' ) ) { + writer.setAttribute( 'listItemId', id, item ); + } +} + +/** + * Updates indentation of given list blocks. + * + * @protected + * @param {Array.} blocks The list of selected blocks. + * @param {Number} indentBy The indentation level difference. + * @param {module:engine/model/writer~Writer} writer The model writer. + */ +export function indentBlocks( blocks, indentBy, writer ) { + for ( const item of blocks ) { + const indent = item.getAttribute( 'listIndent' ) + indentBy; + + if ( indent < 0 ) { + for ( const attributeKey of item.getAttributeKeys() ) { + if ( attributeKey.startsWith( 'list' ) ) { + writer.removeAttribute( attributeKey, item ); + } + } + } else { + writer.setAttribute( 'listIndent', indent, item ); + } + } +} + /** * Based on the provided positions looks for the list head and stores it in the provided map. * @@ -391,7 +498,7 @@ export function fixListItemIds( listHead, seenIds, writer ) { seenIds.add( listItemId ); - for ( const block of getListItemElements( item, 'forward' ) ) { + for ( const block of getListItemBlocks( item, 'forward' ) ) { visited.add( block ); // Use a new ID if a block of a bigger list item has different type. diff --git a/packages/ckeditor5-list/tests/documentlist/utils.js b/packages/ckeditor5-list/tests/documentlist/utils.js index 3ea2301b67c..62aca398ca6 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils.js +++ b/packages/ckeditor5-list/tests/documentlist/utils.js @@ -9,10 +9,10 @@ import { findAndAddListHeadToMap, fixListIndents, fixListItemIds, - getAllListItemElements, + getAllListItemBlocks, getIndent, - getListItemElements, - getSiblingListItem, + getListItemBlocks, + getSiblingListBlock, getViewElementNameForListType, isListItemView, isListView @@ -330,7 +330,7 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); - const foundElement = getSiblingListItem( listItem, { + const foundElement = getSiblingListBlock( listItem, { sameIndent: true, listIndent: 0 } ); @@ -346,7 +346,7 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); - const foundElement = getSiblingListItem( listItem, { + const foundElement = getSiblingListBlock( listItem, { sameIndent: true, listIndent: 0, direction: 'forward' @@ -355,7 +355,7 @@ describe( 'DocumentList - utils', () => { expect( foundElement ).to.equal( fragment.getChild( 1 ) ); } ); - it( 'should return the first listItem that matches criteria (sameIndent, listIndent=1)', () => { + it( 'should not return the listItem if there is an outdented item before (sameIndent, listIndent=1)', () => { const input = '0.' + '1.' + @@ -367,15 +367,15 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 5 ); - const foundElement = getSiblingListItem( listItem.previousSibling, { + const foundElement = getSiblingListBlock( listItem.previousSibling, { sameIndent: true, listIndent: 1 } ); - expect( foundElement ).to.equal( fragment.getChild( 3 ) ); + expect( foundElement ).to.be.null; } ); - it( 'should return the first listItem that matches criteria (sameIndent, listIndent=1, direction="forward")', () => { + it( 'should not return the listItem if there is an outdented item before (sameIndent, listIndent=1, direction="forward")', () => { const input = '0.' + '1.' + // Starting item. @@ -385,13 +385,13 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); - const foundElement = getSiblingListItem( listItem.nextSibling, { + const foundElement = getSiblingListBlock( listItem.nextSibling, { sameIndent: true, listIndent: 1, direction: 'forward' } ); - expect( foundElement ).to.equal( fragment.getChild( 3 ) ); + expect( foundElement ).to.be.null; } ); it( 'should return the first listItem that matches criteria (smallerIndent, listIndent=1)', () => { @@ -404,7 +404,7 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 4 ); - const foundElement = getSiblingListItem( listItem, { + const foundElement = getSiblingListBlock( listItem, { smallerIndent: true, listIndent: 1 } ); @@ -422,7 +422,7 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); - const foundElement = getSiblingListItem( listItem, { + const foundElement = getSiblingListBlock( listItem, { smallerIndent: true, listIndent: 1, direction: 'forward' @@ -439,7 +439,7 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); - const foundElement = getSiblingListItem( listItem, { + const foundElement = getSiblingListBlock( listItem, { smallerIndent: true, listIndent: 0 } ); @@ -458,7 +458,7 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); - const foundElements = getAllListItemElements( listItem ); + const foundElements = getAllListItemBlocks( listItem ); expect( foundElements.length ).to.equal( 1 ); expect( foundElements[ 0 ] ).to.be.equal( listItem ); @@ -475,7 +475,7 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); - const foundElements = getAllListItemElements( listItem ); + const foundElements = getAllListItemBlocks( listItem ); expect( foundElements.length ).to.equal( 3 ); expect( foundElements[ 0 ] ).to.be.equal( listItem ); @@ -494,7 +494,7 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 3 ); - const foundElements = getAllListItemElements( listItem ); + const foundElements = getAllListItemBlocks( listItem ); expect( foundElements.length ).to.equal( 3 ); expect( foundElements[ 0 ] ).to.be.equal( fragment.getChild( 1 ) ); @@ -513,7 +513,7 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 2 ); - const foundElements = getAllListItemElements( listItem ); + const foundElements = getAllListItemBlocks( listItem ); expect( foundElements.length ).to.equal( 3 ); expect( foundElements[ 0 ] ).to.be.equal( fragment.getChild( 1 ) ); @@ -535,7 +535,7 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 4 ); - const foundElements = getAllListItemElements( listItem ); + const foundElements = getAllListItemBlocks( listItem ); expect( foundElements.length ).to.equal( 3 ); expect( foundElements[ 0 ] ).to.be.equal( fragment.getChild( 2 ) ); @@ -554,8 +554,8 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); - const backwardElements = getListItemElements( listItem, 'backward' ); - const forwardElements = getListItemElements( listItem, 'forward' ); + const backwardElements = getListItemBlocks( listItem, 'backward' ); + const forwardElements = getListItemBlocks( listItem, 'forward' ); expect( backwardElements.length ).to.equal( 0 ); expect( forwardElements.length ).to.equal( 1 ); @@ -573,8 +573,8 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); - const backwardElements = getListItemElements( listItem, 'backward' ); - const forwardElements = getListItemElements( listItem, 'forward' ); + const backwardElements = getListItemBlocks( listItem, 'backward' ); + const forwardElements = getListItemBlocks( listItem, 'forward' ); expect( backwardElements.length ).to.equal( 0 ); expect( forwardElements.length ).to.equal( 3 ); @@ -594,8 +594,8 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 3 ); - const backwardElements = getListItemElements( listItem, 'backward' ); - const forwardElements = getListItemElements( listItem, 'forward' ); + const backwardElements = getListItemBlocks( listItem, 'backward' ); + const forwardElements = getListItemBlocks( listItem, 'forward' ); expect( backwardElements.length ).to.equal( 2 ); expect( backwardElements[ 0 ] ).to.be.equal( fragment.getChild( 1 ) ); @@ -616,8 +616,8 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 2 ); - const backwardElements = getListItemElements( listItem, 'backward' ); - const forwardElements = getListItemElements( listItem, 'forward' ); + const backwardElements = getListItemBlocks( listItem, 'backward' ); + const forwardElements = getListItemBlocks( listItem, 'forward' ); expect( backwardElements.length ).to.equal( 1 ); expect( backwardElements[ 0 ] ).to.be.equal( fragment.getChild( 1 ) ); @@ -641,8 +641,8 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 4 ); - const backwardElements = getListItemElements( listItem, 'backward' ); - const forwardElements = getListItemElements( listItem, 'forward' ); + const backwardElements = getListItemBlocks( listItem, 'backward' ); + const forwardElements = getListItemBlocks( listItem, 'forward' ); expect( backwardElements.length ).to.equal( 1 ); expect( backwardElements[ 0 ] ).to.be.equal( fragment.getChild( 2 ) ); @@ -663,8 +663,8 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 2 ); - const backwardElements = getListItemElements( listItem, 'backward' ); - const forwardElements = getListItemElements( listItem, 'forward' ); + const backwardElements = getListItemBlocks( listItem, 'backward' ); + const forwardElements = getListItemBlocks( listItem, 'forward' ); expect( backwardElements.length ).to.equal( 0 ); From 888dab5709a41260ee4098ac1c0b2a86f224585e Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 16 Dec 2021 21:59:56 +0100 Subject: [PATCH 03/66] Added tests. --- .../src/documentlist/converters.js | 2 +- .../documentlist/documentlistindentcommand.js | 3 +- .../ckeditor5-list/src/documentlist/utils.js | 34 +-- .../tests/documentlist/utils.js | 246 ++++++++++++++++-- 4 files changed, 252 insertions(+), 33 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/converters.js b/packages/ckeditor5-list/src/documentlist/converters.js index ae0e26aa8b9..ff0805524b2 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.js +++ b/packages/ckeditor5-list/src/documentlist/converters.js @@ -178,7 +178,7 @@ export function reconvertItemsOnDataChange( model, editing ) { type: item.getAttribute( 'listType' ) }; - const blocks = getListItemBlocks( item, 'forward' ); + const blocks = getListItemBlocks( item, { direction: 'forward' } ); for ( const block of blocks ) { visited.add( block ); diff --git a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js index dd01f2c9552..80aa32a3a8d 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js @@ -58,8 +58,7 @@ export default class DocumentListIndentCommand extends Command { */ execute() { const model = this.editor.model; - const doc = model.document; - const blocks = Array.from( doc.selection.getSelectedBlocks() ); + const blocks = Array.from( model.document.selection.getSelectedBlocks() ); model.change( writer => { // Handle selection contained in the single list item and starting in the following blocks. diff --git a/packages/ckeditor5-list/src/documentlist/utils.js b/packages/ckeditor5-list/src/documentlist/utils.js index 6b7fa2cf09c..a047a10f5da 100644 --- a/packages/ckeditor5-list/src/documentlist/utils.js +++ b/packages/ckeditor5-list/src/documentlist/utils.js @@ -192,27 +192,29 @@ export function getSiblingListBlock( modelElement, options ) { */ export function getAllListItemBlocks( listItem ) { return [ - ...getListItemBlocks( listItem, 'backward' ), - ...getListItemBlocks( listItem, 'forward' ) + ...getListItemBlocks( listItem, { direction: 'backward' } ), + ...getListItemBlocks( listItem, { direction: 'forward' } ) ]; } /** * Returns an array with elements that represents the same list item in the specified direction. * - * It means that values for `listIndent`, and `listItemId` for all items are equal. + * It means that values for `listIndent` and `listItemId` for all items are equal. * * **Note**: For backward search the provided item is not included, but for forward search it is included in the result. * * @protected * @param {module:engine/model/element~Element} listItem Starting list item element. - * @param {'forward'|'backward'} [direction='backward'] Walking direction. + * @param {Object} [options] + * @param {'forward'|'backward'} [options.direction='backward'] Walking direction. * @returns {Array.} */ -export function getListItemBlocks( listItem, direction ) { +export function getListItemBlocks( listItem, options = {} ) { const limitIndent = listItem.getAttribute( 'listIndent' ); const listItemId = listItem.getAttribute( 'listItemId' ); - const isForward = direction == 'forward'; + const isForward = options.direction == 'forward'; + const includeNested = !!options.includeNested; const items = []; for ( @@ -229,12 +231,12 @@ export function getListItemBlocks( listItem, direction ) { } // Ignore nested lists. - if ( itemIndent > limitIndent ) { + if ( !includeNested && itemIndent > limitIndent ) { continue; } // Abort if item has a different ID. - if ( item.getAttribute( 'listItemId' ) != listItemId ) { + if ( itemIndent == limitIndent && item.getAttribute( 'listItemId' ) != listItemId ) { break; } @@ -287,7 +289,11 @@ export function isFirstBlockOfListItem( listBlock ) { return true; } - return previousSibling.getAttribute( 'listItemId' ) != listBlock.getAttribute( 'listItemId' ); + if ( previousSibling.getAttribute( 'listItemId' ) != listBlock.getAttribute( 'listItemId' ) ) { + return true; + } + + return false; } /** @@ -322,12 +328,10 @@ export function expandListBlocksToCompleteItems( blocks ) { const lastBlock = blocks[ blocks.length - 1 ]; // Add missing blocks of the first selected list item. - for ( const item of getListItemBlocks( firstBlock, 'backward' ) ) { - blocks.unshift( item ); - } + blocks.splice( 0, 0, ...getListItemBlocks( firstBlock, { direction: 'backward', includeNested: true } ) ); // Add missing blocks of the last selected list item. - for ( const item of getListItemBlocks( lastBlock, 'forward' ) ) { + for ( const item of getListItemBlocks( lastBlock, { direction: 'forward', includeNested: true } ) ) { if ( item != lastBlock ) { blocks.push( item ); } @@ -344,7 +348,7 @@ export function expandListBlocksToCompleteItems( blocks ) { export function splitListItemBefore( listBlock, writer ) { const id = uid(); - for ( const item of getListItemBlocks( listBlock, 'forward' ) ) { + for ( const item of getListItemBlocks( listBlock, { direction: 'forward' } ) ) { writer.setAttribute( 'listItemId', id, item ); } } @@ -498,7 +502,7 @@ export function fixListItemIds( listHead, seenIds, writer ) { seenIds.add( listItemId ); - for ( const block of getListItemBlocks( item, 'forward' ) ) { + for ( const block of getListItemBlocks( item, { direction: 'forward' } ) ) { visited.add( block ); // Use a new ID if a block of a bigger list item has different type. diff --git a/packages/ckeditor5-list/tests/documentlist/utils.js b/packages/ckeditor5-list/tests/documentlist/utils.js index 62aca398ca6..45a11de1d5b 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils.js +++ b/packages/ckeditor5-list/tests/documentlist/utils.js @@ -6,14 +6,18 @@ import { createListElement, createListItemElement, + expandListBlocksToCompleteItems, findAndAddListHeadToMap, fixListIndents, fixListItemIds, getAllListItemBlocks, getIndent, getListItemBlocks, + getNestedListBlocks, getSiblingListBlock, getViewElementNameForListType, + isFirstBlockOfListItem, + isLastBlockOfListItem, isListItemView, isListView } from '../../src/documentlist/utils'; @@ -321,7 +325,7 @@ describe( 'DocumentList - utils', () => { } ); } ); - describe( 'getSiblingListItem()', () => { + describe( 'getSiblingListBlock()', () => { it( 'should return the passed element if it matches the criteria (sameIndent, listIndent=0)', () => { const input = '0.' + @@ -448,7 +452,7 @@ describe( 'DocumentList - utils', () => { } ); } ); - describe( 'getAllListItemElements()', () => { + describe( 'getAllListItemBlocks()', () => { it( 'should return a single item if it meets conditions', () => { const input = 'foo' + @@ -544,7 +548,7 @@ describe( 'DocumentList - utils', () => { } ); } ); - describe( 'getListItemElements()', () => { + describe( 'getListItemBlocks()', () => { it( 'should return a single item if it meets conditions', () => { const input = 'foo' + @@ -554,8 +558,8 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); - const backwardElements = getListItemBlocks( listItem, 'backward' ); - const forwardElements = getListItemBlocks( listItem, 'forward' ); + const backwardElements = getListItemBlocks( listItem, { direction: 'backward' } ); + const forwardElements = getListItemBlocks( listItem, { direction: 'forward' } ); expect( backwardElements.length ).to.equal( 0 ); expect( forwardElements.length ).to.equal( 1 ); @@ -573,8 +577,8 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); - const backwardElements = getListItemBlocks( listItem, 'backward' ); - const forwardElements = getListItemBlocks( listItem, 'forward' ); + const backwardElements = getListItemBlocks( listItem, { direction: 'backward' } ); + const forwardElements = getListItemBlocks( listItem, { direction: 'forward' } ); expect( backwardElements.length ).to.equal( 0 ); expect( forwardElements.length ).to.equal( 3 ); @@ -594,8 +598,8 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 3 ); - const backwardElements = getListItemBlocks( listItem, 'backward' ); - const forwardElements = getListItemBlocks( listItem, 'forward' ); + const backwardElements = getListItemBlocks( listItem, { direction: 'backward' } ); + const forwardElements = getListItemBlocks( listItem, { direction: 'forward' } ); expect( backwardElements.length ).to.equal( 2 ); expect( backwardElements[ 0 ] ).to.be.equal( fragment.getChild( 1 ) ); @@ -616,8 +620,8 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 2 ); - const backwardElements = getListItemBlocks( listItem, 'backward' ); - const forwardElements = getListItemBlocks( listItem, 'forward' ); + const backwardElements = getListItemBlocks( listItem, { direction: 'backward' } ); + const forwardElements = getListItemBlocks( listItem, { direction: 'forward' } ); expect( backwardElements.length ).to.equal( 1 ); expect( backwardElements[ 0 ] ).to.be.equal( fragment.getChild( 1 ) ); @@ -641,8 +645,8 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 4 ); - const backwardElements = getListItemBlocks( listItem, 'backward' ); - const forwardElements = getListItemBlocks( listItem, 'forward' ); + const backwardElements = getListItemBlocks( listItem, { direction: 'backward' } ); + const forwardElements = getListItemBlocks( listItem, { direction: 'forward' } ); expect( backwardElements.length ).to.equal( 1 ); expect( backwardElements[ 0 ] ).to.be.equal( fragment.getChild( 2 ) ); @@ -663,8 +667,8 @@ describe( 'DocumentList - utils', () => { const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 2 ); - const backwardElements = getListItemBlocks( listItem, 'backward' ); - const forwardElements = getListItemBlocks( listItem, 'forward' ); + const backwardElements = getListItemBlocks( listItem, { direction: 'backward' } ); + const forwardElements = getListItemBlocks( listItem, { direction: 'forward' } ); expect( backwardElements.length ).to.equal( 0 ); @@ -674,6 +678,218 @@ describe( 'DocumentList - utils', () => { } ); } ); + describe( 'getNestedListBlocks()', () => { + it( 'should return empty array if there is no nested blocks', () => { + const input = + 'a' + + 'b'; + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 0 ); + const blocks = getNestedListBlocks( listItem ); + + expect( blocks.length ).to.equal( 0 ); + } ); + + it( 'should return blocks that have a greater indent than the given item', () => { + const input = + 'a' + + 'b' + + 'c' + + 'd' + + 'e'; + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 0 ); + const blocks = getNestedListBlocks( listItem ); + + expect( blocks.length ).to.equal( 3 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + expect( blocks[ 2 ] ).to.equal( fragment.getChild( 3 ) ); + } ); + + it( 'should return blocks that have a greater indent than the given item (nested one)', () => { + const input = + 'a' + + 'b' + + 'c' + + 'd' + + 'e'; + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 1 ); + const blocks = getNestedListBlocks( listItem ); + + expect( blocks.length ).to.equal( 2 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 2 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 3 ) ); + } ); + + it( 'should not include items from other subtrees', () => { + const input = + 'a' + + 'b' + + 'c' + + 'd' + + 'e'; + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 0 ); + const blocks = getNestedListBlocks( listItem ); + + expect( blocks.length ).to.equal( 2 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + } ); + } ); + + describe( 'isFirstBlockOfListItem()', () => { + it( 'should return true for the first list item', () => { + const input = + 'a' + + 'b'; + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 0 ); + + expect( isFirstBlockOfListItem( listItem ) ).to.be.true; + } ); + + it( 'should return true for the second list item', () => { + const input = + 'a' + + 'b'; + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 1 ); + + expect( isFirstBlockOfListItem( listItem ) ).to.be.true; + } ); + + it( 'should return false for the second block of list item', () => { + const input = + 'a' + + 'b'; + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 1 ); + + expect( isFirstBlockOfListItem( listItem ) ).to.be.false; + } ); + + it( 'should return true if the previous block has smaller indent', () => { + const input = + 'a' + + 'b'; + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 1 ); + + expect( isFirstBlockOfListItem( listItem ) ).to.be.true; + } ); + + it( 'should return false if the previous block has bigger indent but it is a part of bigger list item', () => { + const input = + 'a' + + 'b' + + 'c'; + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 2 ); + + expect( isFirstBlockOfListItem( listItem ) ).to.be.false; + } ); + } ); + + describe( 'isLastBlockOfListItem()', () => { + it( 'should return true for the last list item', () => { + const input = + 'a' + + 'b'; + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 1 ); + + expect( isLastBlockOfListItem( listItem ) ).to.be.true; + } ); + + it( 'should return true for the first list item', () => { + const input = + 'a' + + 'b'; + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 0 ); + + expect( isLastBlockOfListItem( listItem ) ).to.be.true; + } ); + + it( 'should return false for the first block of list item', () => { + const input = + 'a' + + 'b'; + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 0 ); + + expect( isLastBlockOfListItem( listItem ) ).to.be.false; + } ); + + it( 'should return true if the next block has smaller indent', () => { + const input = + 'a' + + 'b' + + 'c'; + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 1 ); + + expect( isLastBlockOfListItem( listItem ) ).to.be.true; + } ); + + it( 'should return false if the next block has bigger indent but it is a part of bigger list item', () => { + const input = + 'a' + + 'b' + + 'c'; + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 0 ); + + expect( isLastBlockOfListItem( listItem ) ).to.be.false; + } ); + } ); + + describe( 'expandListBlocksToCompleteItems()', () => { + it( 'should not modify list for a single block of a single-block list item', () => { + const input = + 'a' + + 'b' + + 'c' + + 'd'; + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 0 ) + ]; + + expandListBlocksToCompleteItems( blocks ); + + expect( blocks.length ).to.equal( 1 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 0 ) ); + } ); + // TODO + } ); + + describe( 'splitListItemBefore()', () => { + // TODO + } ); + + describe( 'indentBlocks()', () => { + // TODO + } ); + describe( 'findAndAddListHeadToMap()', () => { it( 'should find list that starts just after the given position', () => { const input = From 3fb8c15d6d35a1845cb15481f6f0f554d7ed2fc3 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 17 Dec 2021 08:08:50 +0100 Subject: [PATCH 04/66] Added tests. --- .../tests/documentlist/utils.js | 424 +++++++++++++++++- 1 file changed, 420 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-list/tests/documentlist/utils.js b/packages/ckeditor5-list/tests/documentlist/utils.js index 45a11de1d5b..0f58770ec33 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils.js +++ b/packages/ckeditor5-list/tests/documentlist/utils.js @@ -16,10 +16,12 @@ import { getNestedListBlocks, getSiblingListBlock, getViewElementNameForListType, + indentBlocks, isFirstBlockOfListItem, isLastBlockOfListItem, isListItemView, - isListView + isListView, + splitListItemBefore } from '../../src/documentlist/utils'; import stubUid from './_utils/uid'; @@ -676,6 +678,23 @@ describe( 'DocumentList - utils', () => { expect( forwardElements[ 0 ] ).to.be.equal( listItem ); expect( forwardElements[ 1 ] ).to.be.equal( fragment.getChild( 3 ) ); } ); + + it( 'should search backward by default', () => { + const input = + 'foo' + + 'a' + + 'b' + + 'b' + + 'c' + + 'bar'; + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 3 ); + const backwardElements = getListItemBlocks( listItem ); + + expect( backwardElements.length ).to.equal( 1 ); + expect( backwardElements[ 0 ] ).to.equal( fragment.getChild( 2 ) ); + } ); } ); describe( 'getNestedListBlocks()', () => { @@ -879,15 +898,412 @@ describe( 'DocumentList - utils', () => { expect( blocks.length ).to.equal( 1 ); expect( blocks[ 0 ] ).to.equal( fragment.getChild( 0 ) ); } ); - // TODO + + it( 'should include all blocks for single list item', () => { + const input = + '0' + + '1' + + '2'; + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 0 ) + ]; + + expandListBlocksToCompleteItems( blocks ); + + expect( blocks.length ).to.equal( 3 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 0 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 1 ) ); + expect( blocks[ 2 ] ).to.equal( fragment.getChild( 2 ) ); + } ); + + it( 'should include all blocks for only first list item block', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '3'; + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 1 ) + ]; + + expandListBlocksToCompleteItems( blocks ); + + expect( blocks.length ).to.equal( 3 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + expect( blocks[ 2 ] ).to.equal( fragment.getChild( 3 ) ); + } ); + + it( 'should include all blocks for only last list item block', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '3'; + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 3 ) + ]; + + expandListBlocksToCompleteItems( blocks ); + + expect( blocks.length ).to.equal( 3 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + expect( blocks[ 2 ] ).to.equal( fragment.getChild( 3 ) ); + } ); + + it( 'should include all blocks for only middle list item block', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '3'; + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 2 ) + ]; + + expandListBlocksToCompleteItems( blocks ); + + expect( blocks.length ).to.equal( 3 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + expect( blocks[ 2 ] ).to.equal( fragment.getChild( 3 ) ); + } ); + + it( 'should include all blocks in nested list item', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '3'; + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 2 ) + ]; + + expandListBlocksToCompleteItems( blocks ); + + expect( blocks.length ).to.equal( 3 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + expect( blocks[ 2 ] ).to.equal( fragment.getChild( 3 ) ); + } ); + + it( 'should include all blocks including nested items (start from first item)', () => { + const input = + '0' + + '1' + + '2'; + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 0 ) + ]; + + expandListBlocksToCompleteItems( blocks ); + + expect( blocks.length ).to.equal( 3 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 0 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 1 ) ); + expect( blocks[ 2 ] ).to.equal( fragment.getChild( 2 ) ); + } ); + + it( 'should include all blocks including nested items (start from last item)', () => { + const input = + '0' + + '1' + + '2'; + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 2 ) + ]; + + expandListBlocksToCompleteItems( blocks ); + + expect( blocks.length ).to.equal( 3 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 0 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 1 ) ); + expect( blocks[ 2 ] ).to.equal( fragment.getChild( 2 ) ); + } ); + + it( 'should expand first and last items', () => { + const input = + 'x' + + '0' + + '1' + + '2' + + '3' + + 'y'; + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 2 ), + fragment.getChild( 3 ) + ]; + + expandListBlocksToCompleteItems( blocks ); + + expect( blocks.length ).to.equal( 4 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + expect( blocks[ 2 ] ).to.equal( fragment.getChild( 3 ) ); + expect( blocks[ 3 ] ).to.equal( fragment.getChild( 4 ) ); + } ); } ); describe( 'splitListItemBefore()', () => { - // TODO + it( 'should replace all blocks ids for first block given', () => { + const input = + 'a' + + 'b' + + 'c'; + + const fragment = parseModel( input, schema ); + + stubUid(); + model.change( writer => splitListItemBefore( fragment.getChild( 0 ), writer ) ); + + expect( stringifyModel( fragment ) ).to.equal( + 'a' + + 'b' + + 'c' + ); + } ); + + it( 'should replace blocks ids for second block given', () => { + const input = + 'a' + + 'b' + + 'c'; + + const fragment = parseModel( input, schema ); + + stubUid(); + model.change( writer => splitListItemBefore( fragment.getChild( 1 ), writer ) ); + + expect( stringifyModel( fragment ) ).to.equal( + 'a' + + 'b' + + 'c' + ); + } ); + + it( 'should not modify other items', () => { + const input = + 'x' + + 'a' + + 'b' + + 'c' + + 'y'; + + const fragment = parseModel( input, schema ); + + stubUid(); + model.change( writer => splitListItemBefore( fragment.getChild( 2 ), writer ) ); + + expect( stringifyModel( fragment ) ).to.equal( + 'x' + + 'a' + + 'b' + + 'c' + + 'y' + ); + } ); + + it( 'should not modify nested items', () => { + const input = + 'a' + + 'b' + + 'c' + + 'd'; + + const fragment = parseModel( input, schema ); + + stubUid(); + model.change( writer => splitListItemBefore( fragment.getChild( 1 ), writer ) ); + + expect( stringifyModel( fragment ) ).to.equal( + 'a' + + 'b' + + 'c' + + 'd' + ); + } ); + + it( 'should not modify parent items', () => { + const input = + 'a' + + 'b' + + 'c' + + 'd' + + 'e'; + + const fragment = parseModel( input, schema ); + + stubUid(); + model.change( writer => splitListItemBefore( fragment.getChild( 2 ), writer ) ); + + expect( stringifyModel( fragment ) ).to.equal( + 'a' + + 'b' + + 'c' + + 'd' + + 'e' + ); + } ); } ); describe( 'indentBlocks()', () => { - // TODO + it( 'flat items', () => { + const input = + 'a' + + 'b' + + 'c' + + 'd'; + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 1 ), + fragment.getChild( 2 ) + ]; + + stubUid(); + + model.change( writer => indentBlocks( blocks, 1, writer ) ); + + expect( stringifyModel( fragment ) ).to.equal( + 'a' + + 'b' + + 'c' + + 'd' + ); + } ); + + it( 'nested lists should keep structure', () => { + const input = + 'a' + + 'b' + + 'c' + + 'd' + + 'e'; + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 1 ), + fragment.getChild( 2 ), + fragment.getChild( 3 ) + ]; + + stubUid(); + + model.change( writer => indentBlocks( blocks, 1, writer ) ); + + expect( stringifyModel( fragment ) ).to.equal( + 'a' + + 'b' + + 'c' + + 'd' + + 'e' + ); + } ); + + it( 'should handle outdenting', () => { + const input = + 'a' + + 'b' + + 'c' + + 'd' + + 'e'; + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 1 ), + fragment.getChild( 2 ), + fragment.getChild( 3 ) + ]; + + stubUid(); + + model.change( writer => indentBlocks( blocks, -1, writer ) ); + + expect( stringifyModel( fragment ) ).to.equal( + 'a' + + 'b' + + 'c' + + 'd' + + 'e' + ); + } ); + + it( 'should remove list attributes if outdented below 0', () => { + const input = + 'a' + + 'b' + + 'c' + + 'd' + + 'e'; + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 2 ), + fragment.getChild( 3 ), + fragment.getChild( 4 ) + ]; + + stubUid(); + + model.change( writer => indentBlocks( blocks, -2, writer ) ); + + expect( stringifyModel( fragment ) ).to.equal( + 'a' + + 'b' + + 'c' + + 'd' + + 'e' + ); + } ); + + it( 'should not remove attributes other than lists if outdented below 0', () => { + const input = + 'a' + + 'b' + + 'c' + + 'd' + + 'e'; + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 2 ), + fragment.getChild( 3 ), + fragment.getChild( 4 ) + ]; + + stubUid(); + + model.change( writer => indentBlocks( blocks, -2, writer ) ); + + expect( stringifyModel( fragment ) ).to.equal( + 'a' + + 'b' + + 'c' + + 'd' + + 'e' + ); + } ); } ); describe( 'findAndAddListHeadToMap()', () => { From 8ab36aeaec1f688c0a844fac2b1e13ab2a40cc2a Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 17 Dec 2021 08:33:09 +0100 Subject: [PATCH 05/66] Added tests. --- .../tests/documentlist/documentlistediting.js | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistediting.js b/packages/ckeditor5-list/tests/documentlist/documentlistediting.js index 236ad4c5089..c3e6c1436ac 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistediting.js @@ -21,6 +21,7 @@ import { getData as getModelData, parse as parseModel, setData as setModelData } import { parse as parseView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; import ListEditing from '../../src/list/listediting'; +import DocumentListIndentCommand from '../../src/documentlist/documentlistindentcommand'; import stubUid from './_utils/uid'; import { prepareTest } from './_utils/utils'; @@ -31,7 +32,7 @@ describe( 'DocumentListEditing', () => { beforeEach( async () => { editor = await VirtualTestEditor.create( { - plugins: [ Paragraph, IndentEditing, ClipboardPipeline, BoldEditing, DocumentListEditing, UndoEditing, + plugins: [ Paragraph, ClipboardPipeline, BoldEditing, DocumentListEditing, UndoEditing, BlockQuoteEditing, TableEditing, HeadingEditing ] } ); @@ -103,6 +104,56 @@ describe( 'DocumentListEditing', () => { expect( model.schema.checkAttribute( [ '$root', 'tableCell' ], 'listType' ) ).to.be.false; } ); + describe( 'commands', () => { + it( 'should register indent list command', () => { + const command = editor.commands.get( 'indentList' ); + + expect( command ).to.be.instanceOf( DocumentListIndentCommand ); + } ); + + it( 'should register outdent list command', () => { + const command = editor.commands.get( 'outdentList' ); + + expect( command ).to.be.instanceOf( DocumentListIndentCommand ); + } ); + + it( 'should add indent list command to indent command', async () => { + const editor = await VirtualTestEditor.create( { + plugins: [ Paragraph, IndentEditing, DocumentListEditing ] + } ); + + const indentListCommand = editor.commands.get( 'indentList' ); + const indentCommand = editor.commands.get( 'indent' ); + + const spy = sinon.spy( indentListCommand, 'execute' ); + + indentListCommand.isEnabled = true; + indentCommand.execute(); + + sinon.assert.calledOnce( spy ); + + await editor.destroy(); + } ); + + it( 'should add outdent list command to outdent command', async () => { + const editor = await VirtualTestEditor.create( { + plugins: [ Paragraph, IndentEditing, DocumentListEditing ] + } ); + + const outdentListCommand = editor.commands.get( 'outdentList' ); + const outdentCommand = editor.commands.get( 'outdent' ); + + const spy = sinon.spy( outdentListCommand, 'execute' ); + + outdentListCommand.isEnabled = true; + outdentCommand.execute(); + + sinon.assert.calledOnce( spy ); + + await editor.destroy(); + } ); + } ); + describe( 'post fixer', () => { describe( 'insert', () => { function testList( input, inserted, output ) { From 984a4a5bf726030060bcde8c29a8b5e1cde54835 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 17 Dec 2021 09:20:24 +0100 Subject: [PATCH 06/66] Added tests. --- .../ckeditor5-list/src/documentlist/utils.js | 22 ++++++- .../tests/documentlist/utils.js | 62 +++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/utils.js b/packages/ckeditor5-list/src/documentlist/utils.js index a047a10f5da..5226469563a 100644 --- a/packages/ckeditor5-list/src/documentlist/utils.js +++ b/packages/ckeditor5-list/src/documentlist/utils.js @@ -216,6 +216,7 @@ export function getListItemBlocks( listItem, options = {} ) { const isForward = options.direction == 'forward'; const includeNested = !!options.includeNested; const items = []; + const nestedItems = []; for ( let item = isForward ? listItem : listItem.previousSibling; @@ -230,9 +231,18 @@ export function getListItemBlocks( listItem, options = {} ) { break; } - // Ignore nested lists. - if ( !includeNested && itemIndent > limitIndent ) { - continue; + if ( itemIndent > limitIndent ) { + // Ignore nested lists. + if ( !includeNested ) { + continue; + } + + // Collect nested items to verify if they are really nested, or it's a different item. + if ( !isForward ) { + nestedItems.push( item ); + + continue; + } } // Abort if item has a different ID. @@ -240,6 +250,12 @@ export function getListItemBlocks( listItem, options = {} ) { break; } + // There is another block for the same list item so the nested items were in the same list item. + if ( nestedItems.length ) { + items.push( ...nestedItems ); + nestedItems.length = 0; + } + items.push( item ); } diff --git a/packages/ckeditor5-list/tests/documentlist/utils.js b/packages/ckeditor5-list/tests/documentlist/utils.js index 0f58770ec33..b854ee04697 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils.js +++ b/packages/ckeditor5-list/tests/documentlist/utils.js @@ -695,6 +695,48 @@ describe( 'DocumentList - utils', () => { expect( backwardElements.length ).to.equal( 1 ); expect( backwardElements[ 0 ] ).to.equal( fragment.getChild( 2 ) ); } ); + + it( 'should include nested blocks if requested', () => { + const input = + 'a' + + 'b' + + 'c' + + 'd' + + 'e'; + + const fragment = parseModel( input, schema ); + const forwardElements = getListItemBlocks( fragment.getChild( 1 ), { direction: 'forward', includeNested: true } ); + const backwardElements = getListItemBlocks( fragment.getChild( 4 ), { direction: 'backward', includeNested: true } ); + + expect( forwardElements.length ).to.equal( 2 ); + expect( forwardElements[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + expect( forwardElements[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + + expect( backwardElements.length ).to.equal( 1 ); + expect( backwardElements[ 0 ] ).to.equal( fragment.getChild( 3 ) ); + } ); + + it( 'should include nested blocks if requested (multi block item with nested item inside)', () => { + const input = + 'a' + + 'b' + + 'c' + + 'd' + + 'e'; + + const fragment = parseModel( input, schema ); + const forwardElements = getListItemBlocks( fragment.getChild( 1 ), { direction: 'forward', includeNested: true } ); + const backwardElements = getListItemBlocks( fragment.getChild( 3 ), { direction: 'backward', includeNested: true } ); + + expect( forwardElements.length ).to.equal( 3 ); + expect( forwardElements[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + expect( forwardElements[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + expect( forwardElements[ 2 ] ).to.equal( fragment.getChild( 3 ) ); + + expect( backwardElements.length ).to.equal( 2 ); + expect( backwardElements[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + expect( backwardElements[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + } ); } ); describe( 'getNestedListBlocks()', () => { @@ -1063,6 +1105,26 @@ describe( 'DocumentList - utils', () => { expect( blocks[ 2 ] ).to.equal( fragment.getChild( 3 ) ); expect( blocks[ 3 ] ).to.equal( fragment.getChild( 4 ) ); } ); + + it( 'should not include nested items from other item', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4'; + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 2 ) + ]; + + expandListBlocksToCompleteItems( blocks ); + + expect( blocks.length ).to.equal( 2 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 2 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 3 ) ); + } ); } ); describe( 'splitListItemBefore()', () => { From 557924a078b1cdb634b4979a681430ea1374de5c Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 17 Dec 2021 11:38:55 +0100 Subject: [PATCH 07/66] Ported plain list tests. --- .../documentlist/documentlistindentcommand.js | 334 ++++++++++++++++++ 1 file changed, 334 insertions(+) create mode 100644 packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js new file mode 100644 index 00000000000..c9ac312ecf5 --- /dev/null +++ b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js @@ -0,0 +1,334 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import DocumentListIndentCommand from '../../src/documentlist/documentlistindentcommand'; + +import Editor from '@ckeditor/ckeditor5-core/src/editor/editor'; +import Model from '@ckeditor/ckeditor5-engine/src/model/model'; +import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +describe( 'DocumentListIndentCommand', () => { + let editor, model, doc, root; + + beforeEach( () => { + editor = new Editor(); + editor.model = new Model(); + + model = editor.model; + doc = model.document; + root = doc.createRoot(); + + model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); + model.schema.register( 'blockQuote', { inheritAllFrom: '$container' } ); + model.schema.extend( '$container', { allowAttributes: [ 'listType', 'listIndent', 'listItemId' ] } ); + + setData( model, + '0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); + } ); + + describe( 'forward (indent)', () => { + let command; + + beforeEach( () => { + command = new DocumentListIndentCommand( editor, 'forward' ); + } ); + + afterEach( () => { + command.destroy(); + } ); + + describe( 'isEnabled', () => { + it( 'should be true if selection starts in list item', () => { + model.change( writer => { + writer.setSelection( root.getChild( 5 ), 0 ); + } ); + + expect( command.isEnabled ).to.be.true; + } ); + + it( 'should be false if selection starts in first list item', () => { + model.change( writer => { + writer.setSelection( root.getChild( 0 ), 0 ); + } ); + + expect( command.isEnabled ).to.be.false; + } ); + + it( 'should be false if selection starts in first list item #2', () => { + setData( model, + 'a' + + 'b' + + 'c' + + '[]d' + + 'e' + ); + + expect( command.isEnabled ).to.be.false; + } ); + + // Reported in PR #53. + it( 'should be false if selection starts in first list item #3', () => { + setData( + model, + 'a' + + 'b' + + 'c' + + 'd' + + '[]e' + ); + + expect( command.isEnabled ).to.be.false; + } ); + + it( 'should be false if selection starts in first list item of top level list with different type than previous list', () => { + setData( + model, + 'a' + + '[]b' + ); + + expect( command.isEnabled ).to.be.false; + } ); + + it( 'should be false if selection starts in a list item that has bigger indent than it\'s previous sibling', () => { + model.change( writer => { + writer.setSelection( root.getChild( 2 ), 0 ); + } ); + + expect( command.isEnabled ).to.be.false; + } ); + + // Edge case but may happen that some other blocks will also use the indent attribute + // and before we fixed it the command was enabled in such a case. + it( 'should be false if selection starts in a paragraph with indent attribute', () => { + model.schema.extend( 'paragraph', { allowAttributes: 'listIndent' } ); + + setData( model, + 'a' + + 'b[]' + ); + + expect( command.isEnabled ).to.be.false; + } ); + } ); + + describe( 'execute()', () => { + it( 'should use parent batch', () => { + model.change( writer => { + writer.setSelection( root.getChild( 5 ), 0 ); + } ); + + model.change( writer => { + expect( writer.batch.operations.length ).to.equal( 0 ); + + command.execute(); + + expect( writer.batch.operations.length ).to.be.above( 0 ); + } ); + } ); + + it( 'should increment indent attribute by 1', () => { + model.change( writer => { + writer.setSelection( root.getChild( 5 ), 0 ); + } ); + + command.execute(); + + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); + } ); + + it( 'should increment indent of all sub-items of indented item', () => { + model.change( writer => { + writer.setSelection( root.getChild( 1 ), 0 ); + } ); + + command.execute(); + + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); + } ); + + it( 'should increment indent of all selected item when multiple items are selected', () => { + model.change( writer => { + writer.setSelection( writer.createRange( + writer.createPositionFromPath( root.getChild( 1 ), [ 0 ] ), + writer.createPositionFromPath( root.getChild( 3 ), [ 1 ] ) + ) ); + } ); + + command.execute(); + + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); + } ); + + it( 'should fire "afterExecute" event after finish all operations with all changed items', done => { + model.change( writer => { + writer.setSelection( root.getChild( 1 ), 0 ); + } ); + + command.on( 'afterExecute', ( evt, data ) => { + expect( data ).to.deep.equal( [ + root.getChild( 1 ), + root.getChild( 2 ), + root.getChild( 3 ), + root.getChild( 4 ), + root.getChild( 5 ) + ] ); + + done(); + } ); + + command.execute(); + } ); + } ); + } ); + + describe( 'backward (outdent)', () => { + let command; + + beforeEach( () => { + command = new DocumentListIndentCommand( editor, 'backward' ); + } ); + + afterEach( () => { + command.destroy(); + } ); + + describe( 'isEnabled', () => { + it( 'should be true if selection starts in list item', () => { + model.change( writer => { + writer.setSelection( root.getChild( 5 ), 0 ); + } ); + + expect( command.isEnabled ).to.be.true; + } ); + + it( 'should be true if selection starts in first list item', () => { + // This is in contrary to forward indent command. + model.change( writer => { + writer.setSelection( root.getChild( 0 ), 0 ); + } ); + + expect( command.isEnabled ).to.be.true; + } ); + + it( 'should be true if selection starts in a list item that has bigger indent than it\'s previous sibling', () => { + // This is in contrary to forward indent command. + model.change( writer => { + writer.setSelection( root.getChild( 2 ), 0 ); + } ); + + expect( command.isEnabled ).to.be.true; + } ); + } ); + + describe( 'execute()', () => { + it( 'should decrement indent attribute by 1 (if it is bigger than 0)', () => { + model.change( writer => { + writer.setSelection( root.getChild( 5 ), 0 ); + } ); + + command.execute(); + + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); + } ); + + it( 'should remove list attributes (if indent is less than to 0)', () => { + model.change( writer => { + writer.setSelection( root.getChild( 0 ), 0 ); + } ); + + command.execute(); + + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); + } ); + + it( 'should decrement indent of all sub-items of outdented item', () => { + model.change( writer => { + writer.setSelection( root.getChild( 1 ), 0 ); + } ); + + command.execute(); + + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); + } ); + + it( 'should outdent all selected item when multiple items are selected', () => { + model.change( writer => { + writer.setSelection( writer.createRange( + writer.createPositionFromPath( root.getChild( 1 ), [ 0 ] ), + writer.createPositionFromPath( root.getChild( 3 ), [ 1 ] ) + ) ); + } ); + + command.execute(); + + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); + } ); + } ); + } ); +} ); From ddc1d72d6b416615d2b5d236751a2e2f2a568dd3 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 17 Dec 2021 13:16:26 +0100 Subject: [PATCH 08/66] Added tests. --- .../documentlist/documentlistindentcommand.js | 24 +- .../documentlist/documentlistindentcommand.js | 341 ++++++++++++------ 2 files changed, 254 insertions(+), 111 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js index 80aa32a3a8d..edd7569967a 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js @@ -58,7 +58,7 @@ export default class DocumentListIndentCommand extends Command { */ execute() { const model = this.editor.model; - const blocks = Array.from( model.document.selection.getSelectedBlocks() ); + const blocks = getSelectedListBlocks( model.document.selection ); model.change( writer => { // Handle selection contained in the single list item and starting in the following blocks. @@ -103,11 +103,11 @@ export default class DocumentListIndentCommand extends Command { */ _checkEnabled() { // Check whether any of position's ancestor is a list item. - const blocks = Array.from( this.editor.model.document.selection.getSelectedBlocks() ); + const blocks = getSelectedListBlocks( this.editor.model.document.selection ); let firstBlock = blocks[ 0 ]; // If selection is not in a list item, the command is disabled. - if ( !firstBlock || !firstBlock.hasAttribute( 'listItemId' ) ) { + if ( !firstBlock ) { return false; } @@ -134,10 +134,26 @@ export default class DocumentListIndentCommand extends Command { return false; } - return siblingItem.getAttribute( 'listType' ) == firstBlock.getAttribute( 'listType' ); + if ( siblingItem.getAttribute( 'listType' ) == firstBlock.getAttribute( 'listType' ) ) { + return true; + } + + return false; } } +// Returns an array of selected blocks truncated to the first non list block element. +function getSelectedListBlocks( selection ) { + const blocks = Array.from( selection.getSelectedBlocks() ); + const firstNonListBlockIndex = blocks.findIndex( block => !block.hasAttribute( 'listItemId' ) ); + + if ( firstNonListBlockIndex != -1 ) { + blocks.length = firstNonListBlockIndex; + } + + return blocks; +} + // Checks whether the given blocks are related to a single list item and does not include the first block of the list item. function startsInTheMiddleOfTheOnlyOneSelectedListItem( blocks ) { const firstItem = blocks[ 0 ]; diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js index c9ac312ecf5..65303fce832 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js @@ -23,16 +23,6 @@ describe( 'DocumentListIndentCommand', () => { model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); model.schema.register( 'blockQuote', { inheritAllFrom: '$container' } ); model.schema.extend( '$container', { allowAttributes: [ 'listType', 'listIndent', 'listItemId' ] } ); - - setData( model, - '0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); } ); describe( 'forward (indent)', () => { @@ -47,85 +37,164 @@ describe( 'DocumentListIndentCommand', () => { } ); describe( 'isEnabled', () => { - it( 'should be true if selection starts in list item', () => { - model.change( writer => { - writer.setSelection( root.getChild( 5 ), 0 ); + describe( 'single block per list item', () => { + it( 'should be true if selection starts in list item', () => { + setData( model, + '0' + + '1' + + '2' + + '3' + + '4' + + '[]5' + + '6' + ); + + expect( command.isEnabled ).to.be.true; } ); - expect( command.isEnabled ).to.be.true; - } ); + it( 'should be false if selection starts in first list item', () => { + setData( model, + '[]0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); + + expect( command.isEnabled ).to.be.false; + } ); - it( 'should be false if selection starts in first list item', () => { - model.change( writer => { - writer.setSelection( root.getChild( 0 ), 0 ); + it( 'should be false if selection starts in first list item at given indent', () => { + setData( model, + 'a' + + 'b' + + 'c' + + '[]d' + + 'e' + ); + + expect( command.isEnabled ).to.be.false; } ); - expect( command.isEnabled ).to.be.false; - } ); + it( 'should be false if selection starts in first list item (different list type)', () => { + setData( model, + 'a' + + 'b' + + 'c' + + 'd' + + '[]e' + ); - it( 'should be false if selection starts in first list item #2', () => { - setData( model, - 'a' + - 'b' + - 'c' + - '[]d' + - 'e' - ); + expect( command.isEnabled ).to.be.false; + } ); - expect( command.isEnabled ).to.be.false; - } ); + it( 'should be false if selection is in first list item with different type than previous list', () => { + setData( model, + 'a' + + '[]b' + ); - // Reported in PR #53. - it( 'should be false if selection starts in first list item #3', () => { - setData( - model, - 'a' + - 'b' + - 'c' + - 'd' + - '[]e' - ); + expect( command.isEnabled ).to.be.false; + } ); - expect( command.isEnabled ).to.be.false; + it( 'should be false if selection starts in a list item that has bigger indent than it\'s previous sibling', () => { + setData( model, + '0' + + '1' + + '[]2' + + '3' + + '4' + + '5' + + '6' + ); + + expect( command.isEnabled ).to.be.false; + } ); } ); - it( 'should be false if selection starts in first list item of top level list with different type than previous list', () => { - setData( - model, - 'a' + - '[]b' - ); + describe( 'multiple blocks per list item', () => { + it( 'should be true if selection starts in the first block of list item', () => { + setData( model, + '0' + + '[]1' + + '2' + + '3' + ); - expect( command.isEnabled ).to.be.false; - } ); + expect( command.isEnabled ).to.be.true; + } ); - it( 'should be false if selection starts in a list item that has bigger indent than it\'s previous sibling', () => { - model.change( writer => { - writer.setSelection( root.getChild( 2 ), 0 ); + it( 'should be false if selection starts in the second block of list item', () => { + setData( model, + '0' + + '1' + + '[]2' + + '3' + ); + + expect( command.isEnabled ).to.be.false; } ); - expect( command.isEnabled ).to.be.false; - } ); + it( 'should be false if selection starts in the last block of list item', () => { + setData( model, + '0' + + '1' + + '2' + + '[]3' + ); - // Edge case but may happen that some other blocks will also use the indent attribute - // and before we fixed it the command was enabled in such a case. - it( 'should be false if selection starts in a paragraph with indent attribute', () => { - model.schema.extend( 'paragraph', { allowAttributes: 'listIndent' } ); + expect( command.isEnabled ).to.be.false; + } ); - setData( model, - 'a' + - 'b[]' - ); + it( 'should be false if selection starts in first list item', () => { + setData( model, + '[]0' + + '1' + ); + + expect( command.isEnabled ).to.be.false; + } ); - expect( command.isEnabled ).to.be.false; + it( 'should be false if selection starts in the first list item at given indent', () => { + setData( model, + 'a' + + '[]b' + + 'c' + ); + + expect( command.isEnabled ).to.be.false; + } ); + + it( 'should be false if selection is in first list item with different type than previous list', () => { + setData( model, + 'a' + + 'a' + + '[]b' + + 'b' + ); + + expect( command.isEnabled ).to.be.false; + } ); + + describe( 'multiple list items selection', () => { + // TODO + } ); } ); } ); describe( 'execute()', () => { it( 'should use parent batch', () => { - model.change( writer => { - writer.setSelection( root.getChild( 5 ), 0 ); - } ); + setData( model, + '0' + + '1' + + '2' + + '3' + + '4' + + '[]5' + + '6' + ); model.change( writer => { expect( writer.batch.operations.length ).to.equal( 0 ); @@ -137,9 +206,15 @@ describe( 'DocumentListIndentCommand', () => { } ); it( 'should increment indent attribute by 1', () => { - model.change( writer => { - writer.setSelection( root.getChild( 5 ), 0 ); - } ); + setData( model, + '0' + + '1' + + '2' + + '3' + + '4' + + '[]5' + + '6' + ); command.execute(); @@ -155,9 +230,15 @@ describe( 'DocumentListIndentCommand', () => { } ); it( 'should increment indent of all sub-items of indented item', () => { - model.change( writer => { - writer.setSelection( root.getChild( 1 ), 0 ); - } ); + setData( model, + '0' + + '[]1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); command.execute(); @@ -173,12 +254,15 @@ describe( 'DocumentListIndentCommand', () => { } ); it( 'should increment indent of all selected item when multiple items are selected', () => { - model.change( writer => { - writer.setSelection( writer.createRange( - writer.createPositionFromPath( root.getChild( 1 ), [ 0 ] ), - writer.createPositionFromPath( root.getChild( 3 ), [ 1 ] ) - ) ); - } ); + setData( model, + '0' + + '[1' + + '2' + + '3]' + + '4' + + '5' + + '6' + ); command.execute(); @@ -194,9 +278,15 @@ describe( 'DocumentListIndentCommand', () => { } ); it( 'should fire "afterExecute" event after finish all operations with all changed items', done => { - model.change( writer => { - writer.setSelection( root.getChild( 1 ), 0 ); - } ); + setData( model, + '0' + + '[]1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); command.on( 'afterExecute', ( evt, data ) => { expect( data ).to.deep.equal( [ @@ -228,27 +318,43 @@ describe( 'DocumentListIndentCommand', () => { describe( 'isEnabled', () => { it( 'should be true if selection starts in list item', () => { - model.change( writer => { - writer.setSelection( root.getChild( 5 ), 0 ); - } ); + setData( model, + '0' + + '1' + + '2' + + '3' + + '4' + + '[]5' + + '6' + ); expect( command.isEnabled ).to.be.true; } ); it( 'should be true if selection starts in first list item', () => { - // This is in contrary to forward indent command. - model.change( writer => { - writer.setSelection( root.getChild( 0 ), 0 ); - } ); + setData( model, + '[]0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); expect( command.isEnabled ).to.be.true; } ); it( 'should be true if selection starts in a list item that has bigger indent than it\'s previous sibling', () => { - // This is in contrary to forward indent command. - model.change( writer => { - writer.setSelection( root.getChild( 2 ), 0 ); - } ); + setData( model, + '0' + + '1' + + '[]2' + + '3' + + '4' + + '5' + + '6' + ); expect( command.isEnabled ).to.be.true; } ); @@ -256,9 +362,15 @@ describe( 'DocumentListIndentCommand', () => { describe( 'execute()', () => { it( 'should decrement indent attribute by 1 (if it is bigger than 0)', () => { - model.change( writer => { - writer.setSelection( root.getChild( 5 ), 0 ); - } ); + setData( model, + '0' + + '1' + + '2' + + '3' + + '4' + + '[]5' + + '6' + ); command.execute(); @@ -274,9 +386,15 @@ describe( 'DocumentListIndentCommand', () => { } ); it( 'should remove list attributes (if indent is less than to 0)', () => { - model.change( writer => { - writer.setSelection( root.getChild( 0 ), 0 ); - } ); + setData( model, + '[]0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); command.execute(); @@ -292,9 +410,15 @@ describe( 'DocumentListIndentCommand', () => { } ); it( 'should decrement indent of all sub-items of outdented item', () => { - model.change( writer => { - writer.setSelection( root.getChild( 1 ), 0 ); - } ); + setData( model, + '0' + + '[]1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); command.execute(); @@ -310,12 +434,15 @@ describe( 'DocumentListIndentCommand', () => { } ); it( 'should outdent all selected item when multiple items are selected', () => { - model.change( writer => { - writer.setSelection( writer.createRange( - writer.createPositionFromPath( root.getChild( 1 ), [ 0 ] ), - writer.createPositionFromPath( root.getChild( 3 ), [ 1 ] ) - ) ); - } ); + setData( model, + '0' + + '[1' + + '2' + + '3]' + + '4' + + '5' + + '6' + ); command.execute(); From cd91fd4775940f4d5be20133abfb9ad1e2632904 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 17 Dec 2021 16:14:59 +0100 Subject: [PATCH 09/66] Refactored utils file. --- .../src/documentlist/converters.js | 13 +- .../src/documentlist/documentlistediting.js | 2 +- .../documentlist/documentlistindentcommand.js | 9 +- .../ckeditor5-list/src/documentlist/utils.js | 539 ------------ .../src/documentlist/utils/model.js | 267 ++++++ .../src/documentlist/utils/postfixers.js | 156 ++++ .../src/documentlist/utils/view.js | 137 +++ .../tests/documentlist/documentlistediting.js | 4 +- .../documentlist/{utils.js => utils/model.js} | 818 +----------------- .../tests/documentlist/utils/postfixers.js | 549 ++++++++++++ .../tests/documentlist/utils/view.js | 308 +++++++ 11 files changed, 1433 insertions(+), 1369 deletions(-) delete mode 100644 packages/ckeditor5-list/src/documentlist/utils.js create mode 100644 packages/ckeditor5-list/src/documentlist/utils/model.js create mode 100644 packages/ckeditor5-list/src/documentlist/utils/postfixers.js create mode 100644 packages/ckeditor5-list/src/documentlist/utils/view.js rename packages/ckeditor5-list/tests/documentlist/{utils.js => utils/model.js} (60%) create mode 100644 packages/ckeditor5-list/tests/documentlist/utils/postfixers.js create mode 100644 packages/ckeditor5-list/tests/documentlist/utils/view.js diff --git a/packages/ckeditor5-list/src/documentlist/converters.js b/packages/ckeditor5-list/src/documentlist/converters.js index ff0805524b2..83f35e1ef63 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.js +++ b/packages/ckeditor5-list/src/documentlist/converters.js @@ -3,18 +3,21 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ +import { + getAllListItemBlocks, + getSiblingListBlock, + getListItemBlocks +} from './utils/model'; import { createListElement, createListItemElement, - getAllListItemBlocks, getIndent, - getSiblingListBlock, isListView, isListItemView, - getListItemBlocks, - findAndAddListHeadToMap, getViewElementNameForListType -} from './utils'; +} from './utils/view'; +import { findAndAddListHeadToMap } from './utils/postfixers'; + import { uid } from 'ckeditor5/src/utils'; import { UpcastWriter } from 'ckeditor5/src/engine'; diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.js b/packages/ckeditor5-list/src/documentlist/documentlistediting.js index 2f54d35b364..53908728a3d 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.js @@ -25,7 +25,7 @@ import { findAndAddListHeadToMap, fixListIndents, fixListItemIds -} from './utils'; +} from './utils/postfixers'; /** * The editing part of the document-list feature. It handles creating, editing and removing lists and list items. diff --git a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js index edd7569967a..c31cd88ff2a 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js @@ -10,12 +10,11 @@ import { Command } from 'ckeditor5/src/core'; import { expandListBlocksToCompleteItems, - getNestedListBlocks, getSiblingListBlock, indentBlocks, isFirstBlockOfListItem, splitListItemBefore -} from './utils'; +} from './utils/model'; /** * The document list indent command. It is used by the {@link module:list/documentlist~DocumentList list feature}. @@ -74,11 +73,6 @@ export default class DocumentListIndentCommand extends Command { // Expand the selected blocks to contain the whole list items. expandListBlocksToCompleteItems( blocks ); - // Indenting a list item should also indent all the items that are already sub-items of indented item. - for ( const block of getNestedListBlocks( blocks[ blocks.length - 1 ] ) ) { - blocks.push( block ); - } - // Now just update the attributes of blocks. indentBlocks( blocks, this._indentBy, writer ); @@ -155,6 +149,7 @@ function getSelectedListBlocks( selection ) { } // Checks whether the given blocks are related to a single list item and does not include the first block of the list item. +// TODO split into 2 helpers function startsInTheMiddleOfTheOnlyOneSelectedListItem( blocks ) { const firstItem = blocks[ 0 ]; diff --git a/packages/ckeditor5-list/src/documentlist/utils.js b/packages/ckeditor5-list/src/documentlist/utils.js deleted file mode 100644 index 5226469563a..00000000000 --- a/packages/ckeditor5-list/src/documentlist/utils.js +++ /dev/null @@ -1,539 +0,0 @@ -/** - * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -import { uid } from 'ckeditor5/src/utils'; - -/** - * @module list/documentlist/utils - */ - -/** - * Checks if view element is a list type (ul or ol). - * - * @protected - * @param {module:engine/view/element~Element} viewElement - * @returns {Boolean} -*/ -export function isListView( viewElement ) { - return viewElement.is( 'element', 'ol' ) || viewElement.is( 'element', 'ul' ); -} - -/** - * Checks if view element is a list item (li). - * - * @protected - * @param {module:engine/view/element~Element} viewElement - * @returns {Boolean} - */ -export function isListItemView( viewElement ) { - return viewElement.is( 'element', 'li' ); -} - -/** - * Calculates the indent value for a list item. Handles HTML compliant and non-compliant lists. - * - * Also, fixes non HTML compliant lists indents: - * - * before: fixed list: - * OL OL - * |-> LI (parent LIs: 0) |-> LI (indent: 0) - * |-> OL |-> OL - * |-> OL | - * | |-> OL | - * | |-> OL | - * | |-> LI (parent LIs: 1) |-> LI (indent: 1) - * |-> LI (parent LIs: 1) |-> LI (indent: 1) - * - * before: fixed list: - * OL OL - * |-> OL | - * |-> OL | - * |-> OL | - * |-> LI (parent LIs: 0) |-> LI (indent: 0) - * - * before: fixed list: - * OL OL - * |-> LI (parent LIs: 0) |-> LI (indent: 0) - * |-> OL |-> OL - * |-> LI (parent LIs: 0) |-> LI (indent: 1) - * - * @protected - * @param {module:engine/view/element~Element} listItem - * @returns {Number} - */ -export function getIndent( listItem ) { - let indent = 0; - let parent = listItem.parent; - - while ( parent ) { - // Each LI in the tree will result in an increased indent for HTML compliant lists. - if ( isListItemView( parent ) ) { - indent++; - } else { - // If however the list is nested in other list we should check previous sibling of any of the list elements... - const previousSibling = parent.previousSibling; - - // ...because the we might need increase its indent: - // before: fixed list: - // OL OL - // |-> LI (parent LIs: 0) |-> LI (indent: 0) - // |-> OL |-> OL - // |-> LI (parent LIs: 0) |-> LI (indent: 1) - if ( previousSibling && isListItemView( previousSibling ) ) { - indent++; - } - } - - parent = parent.parent; - } - - return indent; -} - -/** - * Creates a list attribute element (ol or ul). - * - * @protected - * @param {module:engine/view/downcastwriter~DowncastWriter} writer The downcast writer. - * @param {Number} indent The list item indent. - * @param {'bulleted'|'numbered'} type The list type. - * @param {String} [id] The list ID. - * @returns {module:engine/view/attributeelement~AttributeElement} - */ -export function createListElement( writer, indent, type, id ) { - // Negative priorities so that restricted editing attribute won't wrap lists. - return writer.createAttributeElement( getViewElementNameForListType( type ), null, { - priority: 2 * indent / 100 - 100, - id - } ); -} - -/** - * Creates a list item attribute element (li). - * - * @protected - * @param {module:engine/view/downcastwriter~DowncastWriter} writer The downcast writer. - * @param {Number} indent The list item indent. - * @param {String} id The list item ID. - * @returns {module:engine/view/attributeelement~AttributeElement} - */ -export function createListItemElement( writer, indent, id ) { - // Negative priorities so that restricted editing attribute won't wrap list items. - return writer.createAttributeElement( 'li', null, { - priority: ( 2 * indent + 1 ) / 100 - 100, - id - } ); -} - -/** - * Returns a view element name for the given list type. - * - * @protected - * @param {'bulleted'|'numbered'} type The list type. - * @returns {String} - */ -export function getViewElementNameForListType( type ) { - return type == 'numbered' ? 'ol' : 'ul'; -} - -/** - * Returns the closest list item model element according to the specified options. - * - * Note that if the provided model element satisfies the provided options then it's returned. - * - * @protected - * @param {module:engine/model/element~Element} modelElement - * @param {Object} options - * @param {Number} options.listIndent The reference list indent. - * @param {Boolean} [options.sameIndent=false] Whether to return list item model element with the same indent as specified. - * @param {Boolean} [options.smallerIndent=false] Whether to return list item model element with the smaller indent as specified. - * @param {'forward'|'backward'} [options.direction='backward'] The search direction. - * @return {module:engine/model/element~Element|null} - */ -export function getSiblingListBlock( modelElement, options ) { - const sameIndent = !!options.sameIndent; - const smallerIndent = !!options.smallerIndent; - const indent = options.listIndent; - const isForward = options.direction == 'forward'; - - for ( - let item = modelElement; - item && item.hasAttribute( 'listItemId' ); - item = isForward ? item.nextSibling : item.previousSibling - ) { - const itemIndent = item.getAttribute( 'listIndent' ); - - if ( itemIndent > indent ) { - continue; - } - - if ( sameIndent && itemIndent == indent ) { - return item; - } - - if ( itemIndent < indent ) { - return smallerIndent ? item : null; - } - } - - return null; -} - -/** - * Returns an array with all elements that represents the same list item. - * - * It means that values for `listIndent`, and `listItemId` for all items are equal. - * - * @protected - * @param {module:engine/model/element~Element} listItem Starting list item element. - * @return {Array.} - */ -export function getAllListItemBlocks( listItem ) { - return [ - ...getListItemBlocks( listItem, { direction: 'backward' } ), - ...getListItemBlocks( listItem, { direction: 'forward' } ) - ]; -} - -/** - * Returns an array with elements that represents the same list item in the specified direction. - * - * It means that values for `listIndent` and `listItemId` for all items are equal. - * - * **Note**: For backward search the provided item is not included, but for forward search it is included in the result. - * - * @protected - * @param {module:engine/model/element~Element} listItem Starting list item element. - * @param {Object} [options] - * @param {'forward'|'backward'} [options.direction='backward'] Walking direction. - * @returns {Array.} - */ -export function getListItemBlocks( listItem, options = {} ) { - const limitIndent = listItem.getAttribute( 'listIndent' ); - const listItemId = listItem.getAttribute( 'listItemId' ); - const isForward = options.direction == 'forward'; - const includeNested = !!options.includeNested; - const items = []; - const nestedItems = []; - - for ( - let item = isForward ? listItem : listItem.previousSibling; - item && item.hasAttribute( 'listItemId' ); - item = isForward ? item.nextSibling : item.previousSibling - ) { - const itemIndent = item.getAttribute( 'listIndent' ); - - // If current parsed item has lower indent that element that the element that was a starting point, - // it means we left a nested list. Abort searching items. - if ( itemIndent < limitIndent ) { - break; - } - - if ( itemIndent > limitIndent ) { - // Ignore nested lists. - if ( !includeNested ) { - continue; - } - - // Collect nested items to verify if they are really nested, or it's a different item. - if ( !isForward ) { - nestedItems.push( item ); - - continue; - } - } - - // Abort if item has a different ID. - if ( itemIndent == limitIndent && item.getAttribute( 'listItemId' ) != listItemId ) { - break; - } - - // There is another block for the same list item so the nested items were in the same list item. - if ( nestedItems.length ) { - items.push( ...nestedItems ); - nestedItems.length = 0; - } - - items.push( item ); - } - - return isForward ? items : items.reverse(); -} - -/** - * Returns a list items nested inside the given list item. - * - * @protected - * @param {module:engine/model/element~Element} listItem Starting list item element. - * @returns {Array.} - */ -export function getNestedListBlocks( listItem ) { - const indent = listItem.getAttribute( 'listIndent' ); - const items = []; - - for ( - let item = listItem.nextSibling; - item && item.hasAttribute( 'listItemId' ); - item = item.nextSibling - ) { - if ( item.getAttribute( 'listIndent' ) <= indent ) { - break; - } - - items.push( item ); - } - - return items; -} - -/** - * Check if the given block is the first in the list item. - * - * @protected - * @param {module:engine/model/element~Element} listBlock The list block element. - * @returns {Boolean} - */ -export function isFirstBlockOfListItem( listBlock ) { - const previousSibling = getSiblingListBlock( listBlock.previousSibling, { - listIndent: listBlock.getAttribute( 'listIndent' ), - sameIndent: true - } ); - - if ( !previousSibling ) { - return true; - } - - if ( previousSibling.getAttribute( 'listItemId' ) != listBlock.getAttribute( 'listItemId' ) ) { - return true; - } - - return false; -} - -/** - * Check if the given block is the last in the list item. - * - * @protected - * @param {module:engine/model/element~Element} listBlock The list block element. - * @returns {Boolean} - */ -export function isLastBlockOfListItem( listBlock ) { - const nextSibling = getSiblingListBlock( listBlock.nextSibling, { - listIndent: listBlock.getAttribute( 'listIndent' ), - direction: 'forward', - sameIndent: true - } ); - - if ( !nextSibling ) { - return true; - } - - return nextSibling.getAttribute( 'listItemId' ) != listBlock.getAttribute( 'listItemId' ); -} - -/** - * Expands the given list of selected blocks to include the leading and tailing blocks of partially selected list items. - * - * @protected - * @param {Array.} blocks The list of selected blocks. - */ -export function expandListBlocksToCompleteItems( blocks ) { - const firstBlock = blocks[ 0 ]; - const lastBlock = blocks[ blocks.length - 1 ]; - - // Add missing blocks of the first selected list item. - blocks.splice( 0, 0, ...getListItemBlocks( firstBlock, { direction: 'backward', includeNested: true } ) ); - - // Add missing blocks of the last selected list item. - for ( const item of getListItemBlocks( lastBlock, { direction: 'forward', includeNested: true } ) ) { - if ( item != lastBlock ) { - blocks.push( item ); - } - } -} - -/** - * Splits the list item just before the provided list block. - * - * @protected - * @param {module:engine/model/element~Element} listBlock The list block element. - * @param {module:engine/model/writer~Writer} writer The model writer. - */ -export function splitListItemBefore( listBlock, writer ) { - const id = uid(); - - for ( const item of getListItemBlocks( listBlock, { direction: 'forward' } ) ) { - writer.setAttribute( 'listItemId', id, item ); - } -} - -/** - * Updates indentation of given list blocks. - * - * @protected - * @param {Array.} blocks The list of selected blocks. - * @param {Number} indentBy The indentation level difference. - * @param {module:engine/model/writer~Writer} writer The model writer. - */ -export function indentBlocks( blocks, indentBy, writer ) { - for ( const item of blocks ) { - const indent = item.getAttribute( 'listIndent' ) + indentBy; - - if ( indent < 0 ) { - for ( const attributeKey of item.getAttributeKeys() ) { - if ( attributeKey.startsWith( 'list' ) ) { - writer.removeAttribute( attributeKey, item ); - } - } - } else { - writer.setAttribute( 'listIndent', indent, item ); - } - } -} - -/** - * Based on the provided positions looks for the list head and stores it in the provided map. - * - * @protected - * @param {module:engine/model/position~Position} position The search starting position. - * @param {Map.} itemToListHead The map from list item element - * to the list head element. - */ -export function findAndAddListHeadToMap( position, itemToListHead ) { - const previousNode = position.nodeBefore; - - if ( !previousNode || !previousNode.hasAttribute( 'listItemId' ) ) { - const item = position.nodeAfter; - - if ( item && item.hasAttribute( 'listItemId' ) ) { - itemToListHead.set( item, item ); - } - } else { - let listHead = previousNode; - - if ( itemToListHead.has( listHead ) ) { - return; - } - - for ( - let previousSibling = listHead.previousSibling; - previousSibling && previousSibling.hasAttribute( 'listItemId' ); - previousSibling = listHead.previousSibling - ) { - listHead = previousSibling; - - if ( itemToListHead.has( listHead ) ) { - return; - } - } - - itemToListHead.set( previousNode, listHead ); - } -} - -/** - * Scans the list starting from the given list head element and fixes items' indentation. - * - * @protected - * @param {module:engine/model/element~Element} listHead The list head model element. - * @param {module:engine/model/writer~Writer} writer The model writer. - * @returns {Boolean} Whether the model was modified. - */ -export function fixListIndents( listHead, writer ) { - let maxIndent = 0; // Guards local sublist max indents that need fixing. - let prevIndent = -1; // Previous item indent. - let fixBy = null; - let applied = false; - - for ( - let item = listHead; - item && item.hasAttribute( 'listItemId' ); - item = item.nextSibling - ) { - const itemIndent = item.getAttribute( 'listIndent' ); - - if ( itemIndent > maxIndent ) { - let newIndent; - - if ( fixBy === null ) { - fixBy = itemIndent - maxIndent; - newIndent = maxIndent; - } else { - if ( fixBy > itemIndent ) { - fixBy = itemIndent; - } - - newIndent = itemIndent - fixBy; - } - - if ( newIndent > prevIndent + 1 ) { - newIndent = prevIndent + 1; - } - - writer.setAttribute( 'listIndent', newIndent, item ); - - applied = true; - prevIndent = newIndent; - } else { - fixBy = null; - maxIndent = itemIndent + 1; - prevIndent = itemIndent; - } - } - - return applied; -} - -/** - * Scans the list starting from the given list head element and fixes items' types. - * - * @protected - * @param {module:engine/model/element~Element} listHead The list head model element. - * @param {Set.} seenIds The set of already known IDs. - * @param {module:engine/model/writer~Writer} writer The model writer. - * @returns {Boolean} Whether the model was modified. - */ -export function fixListItemIds( listHead, seenIds, writer ) { - const visited = new Set(); - let applied = false; - - for ( - let item = listHead; - item && item.hasAttribute( 'listItemId' ); - item = item.nextSibling - ) { - if ( visited.has( item ) ) { - continue; - } - - let listType = item.getAttribute( 'listType' ); - let listItemId = item.getAttribute( 'listItemId' ); - - // Use a new ID if this one was spot earlier (even in other list). - if ( seenIds.has( listItemId ) ) { - listItemId = uid(); - } - - seenIds.add( listItemId ); - - for ( const block of getListItemBlocks( item, { direction: 'forward' } ) ) { - visited.add( block ); - - // Use a new ID if a block of a bigger list item has different type. - if ( block.getAttribute( 'listType' ) != listType ) { - listItemId = uid(); - listType = block.getAttribute( 'listType' ); - } - - if ( block.getAttribute( 'listItemId' ) != listItemId ) { - writer.setAttribute( 'listItemId', listItemId, block ); - - applied = true; - } - } - } - - return applied; -} diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.js b/packages/ckeditor5-list/src/documentlist/utils/model.js new file mode 100644 index 00000000000..5fb634027f6 --- /dev/null +++ b/packages/ckeditor5-list/src/documentlist/utils/model.js @@ -0,0 +1,267 @@ +/** + * @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/utils/model + */ + +import { uid } from 'ckeditor5/src/utils'; + +/** + * Returns the closest list item model element according to the specified options. + * + * Note that if the provided model element satisfies the provided options then it's returned. + * + * @protected + * @param {module:engine/model/element~Element} modelElement + * @param {Object} options + * @param {Number} options.listIndent The reference list indent. + * @param {Boolean} [options.sameIndent=false] Whether to return list item model element with the same indent as specified. + * @param {Boolean} [options.smallerIndent=false] Whether to return list item model element with the smaller indent as specified. + * @param {'forward'|'backward'} [options.direction='backward'] The search direction. + * @return {module:engine/model/element~Element|null} + */ +export function getSiblingListBlock( modelElement, options ) { + const sameIndent = !!options.sameIndent; + const smallerIndent = !!options.smallerIndent; + const indent = options.listIndent; + const isForward = options.direction == 'forward'; + + for ( + let item = modelElement; + item && item.hasAttribute( 'listItemId' ); + item = isForward ? item.nextSibling : item.previousSibling + ) { + const itemIndent = item.getAttribute( 'listIndent' ); + + if ( itemIndent > indent ) { + continue; + } + + if ( sameIndent && itemIndent == indent ) { + return item; + } + + if ( itemIndent < indent ) { + return smallerIndent ? item : null; + } + } + + return null; +} + +/** + * Returns an array with all elements that represents the same list item. + * + * It means that values for `listIndent`, and `listItemId` for all items are equal. + * + * @protected + * @param {module:engine/model/element~Element} listItem Starting list item element. + * @return {Array.} + */ +export function getAllListItemBlocks( listItem ) { + return [ + ...getListItemBlocks( listItem, { direction: 'backward' } ), + ...getListItemBlocks( listItem, { direction: 'forward' } ) + ]; +} + +/** + * Returns an array with elements that represents the same list item in the specified direction. + * + * It means that values for `listIndent` and `listItemId` for all items are equal. + * + * **Note**: For backward search the provided item is not included, but for forward search it is included in the result. + * + * @protected + * @param {module:engine/model/element~Element} listItem Starting list item element. + * @param {Object} [options] + * @param {'forward'|'backward'} [options.direction='backward'] Walking direction. + * @param {Boolean} [options.includeNested=false] Whether nested blocks should be included. + * @returns {Array.} + */ +export function getListItemBlocks( listItem, options = {} ) { + const limitIndent = listItem.getAttribute( 'listIndent' ); + const listItemId = listItem.getAttribute( 'listItemId' ); + const isForward = options.direction == 'forward'; + const includeNested = !!options.includeNested; + const items = []; + const nestedItems = []; + + // TODO use generator instead of for loop (ListWalker) + for ( + let item = isForward ? listItem : listItem.previousSibling; + item && item.hasAttribute( 'listItemId' ); + item = isForward ? item.nextSibling : item.previousSibling + ) { + const itemIndent = item.getAttribute( 'listIndent' ); + + // If current parsed item has lower indent that element that the element that was a starting point, + // it means we left a nested list. Abort searching items. + if ( itemIndent < limitIndent ) { + break; + } + + if ( itemIndent > limitIndent ) { + // Ignore nested lists. + if ( !includeNested ) { + continue; + } + + // Collect nested items to verify if they are really nested, or it's a different item. + if ( !isForward ) { + nestedItems.push( item ); + + continue; + } + } + + // Abort if item has a different ID. + if ( itemIndent == limitIndent && item.getAttribute( 'listItemId' ) != listItemId ) { + break; + } + + // There is another block for the same list item so the nested items were in the same list item. + if ( nestedItems.length ) { + items.push( ...nestedItems ); + nestedItems.length = 0; + } + + items.push( item ); + } + + return isForward ? items : items.reverse(); +} + +/** + * Returns a list items nested inside the given list item. + * + * @protected + * @param {module:engine/model/element~Element} listItem Starting list item element. + * @returns {Array.} + */ +export function getNestedListBlocks( listItem ) { + const indent = listItem.getAttribute( 'listIndent' ); + const items = []; + + for ( + let item = listItem.nextSibling; + item && item.hasAttribute( 'listItemId' ); + item = item.nextSibling + ) { + if ( item.getAttribute( 'listIndent' ) <= indent ) { + break; + } + + items.push( item ); + } + + return items; +} + +/** + * Check if the given block is the first in the list item. + * + * @protected + * @param {module:engine/model/element~Element} listBlock The list block element. + * @returns {Boolean} + */ +export function isFirstBlockOfListItem( listBlock ) { + const previousSibling = getSiblingListBlock( listBlock.previousSibling, { + listIndent: listBlock.getAttribute( 'listIndent' ), + sameIndent: true + } ); + + if ( !previousSibling ) { + return true; + } + + if ( previousSibling.getAttribute( 'listItemId' ) != listBlock.getAttribute( 'listItemId' ) ) { + return true; + } + + return false; +} + +/** + * Check if the given block is the last in the list item. + * + * @protected + * @param {module:engine/model/element~Element} listBlock The list block element. + * @returns {Boolean} + */ +export function isLastBlockOfListItem( listBlock ) { + const nextSibling = getSiblingListBlock( listBlock.nextSibling, { + listIndent: listBlock.getAttribute( 'listIndent' ), + direction: 'forward', + sameIndent: true + } ); + + if ( !nextSibling ) { + return true; + } + + return nextSibling.getAttribute( 'listItemId' ) != listBlock.getAttribute( 'listItemId' ); +} + +/** + * Expands the given list of selected blocks to include the leading and tailing blocks of partially selected list items. + * + * @protected + * @param {Array.} blocks The list of selected blocks. + */ +export function expandListBlocksToCompleteItems( blocks ) { + const firstBlock = blocks[ 0 ]; + const lastBlock = blocks[ blocks.length - 1 ]; + + // Add missing blocks of the first selected list item. + blocks.splice( 0, 0, ...getListItemBlocks( firstBlock, { direction: 'backward', includeNested: true } ) ); + + // Add missing blocks of the last selected list item. + for ( const item of getListItemBlocks( lastBlock, { direction: 'forward', includeNested: true } ) ) { + if ( item != lastBlock ) { + blocks.push( item ); + } + } +} + +/** + * Splits the list item just before the provided list block. + * + * @protected + * @param {module:engine/model/element~Element} listBlock The list block element. + * @param {module:engine/model/writer~Writer} writer The model writer. + */ +export function splitListItemBefore( listBlock, writer ) { + const id = uid(); + + for ( const item of getListItemBlocks( listBlock, { direction: 'forward' } ) ) { + writer.setAttribute( 'listItemId', id, item ); + } +} + +/** + * Updates indentation of given list blocks. + * + * @protected + * @param {Array.} blocks The list of selected blocks. + * @param {Number} indentBy The indentation level difference. + * @param {module:engine/model/writer~Writer} writer The model writer. + */ +export function indentBlocks( blocks, indentBy, writer ) { + for ( const item of blocks ) { + const indent = item.getAttribute( 'listIndent' ) + indentBy; + + if ( indent < 0 ) { + for ( const attributeKey of item.getAttributeKeys() ) { + if ( attributeKey.startsWith( 'list' ) ) { + writer.removeAttribute( attributeKey, item ); + } + } + } else { + writer.setAttribute( 'listIndent', indent, item ); + } + } +} diff --git a/packages/ckeditor5-list/src/documentlist/utils/postfixers.js b/packages/ckeditor5-list/src/documentlist/utils/postfixers.js new file mode 100644 index 00000000000..6e700d8fdcf --- /dev/null +++ b/packages/ckeditor5-list/src/documentlist/utils/postfixers.js @@ -0,0 +1,156 @@ +/** + * @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/utils/postfixers + */ + +import { uid } from 'ckeditor5/src/utils'; +import { getListItemBlocks } from './model'; + +/** + * Based on the provided positions looks for the list head and stores it in the provided map. + * + * @protected + * @param {module:engine/model/position~Position} position The search starting position. + * @param {Map.} itemToListHead The map from list item element + * to the list head element. + */ +export function findAndAddListHeadToMap( position, itemToListHead ) { + const previousNode = position.nodeBefore; + + if ( !previousNode || !previousNode.hasAttribute( 'listItemId' ) ) { + const item = position.nodeAfter; + + if ( item && item.hasAttribute( 'listItemId' ) ) { + itemToListHead.set( item, item ); + } + } else { + let listHead = previousNode; + + if ( itemToListHead.has( listHead ) ) { + return; + } + + for ( + let previousSibling = listHead.previousSibling; + previousSibling && previousSibling.hasAttribute( 'listItemId' ); + previousSibling = listHead.previousSibling + ) { + listHead = previousSibling; + + if ( itemToListHead.has( listHead ) ) { + return; + } + } + + itemToListHead.set( previousNode, listHead ); + } +} + +/** + * Scans the list starting from the given list head element and fixes items' indentation. + * + * @protected + * @param {module:engine/model/element~Element} listHead The list head model element. + * @param {module:engine/model/writer~Writer} writer The model writer. + * @returns {Boolean} Whether the model was modified. + */ +export function fixListIndents( listHead, writer ) { + let maxIndent = 0; // Guards local sublist max indents that need fixing. + let prevIndent = -1; // Previous item indent. + let fixBy = null; + let applied = false; + + for ( + let item = listHead; + item && item.hasAttribute( 'listItemId' ); + item = item.nextSibling + ) { + const itemIndent = item.getAttribute( 'listIndent' ); + + if ( itemIndent > maxIndent ) { + let newIndent; + + if ( fixBy === null ) { + fixBy = itemIndent - maxIndent; + newIndent = maxIndent; + } else { + if ( fixBy > itemIndent ) { + fixBy = itemIndent; + } + + newIndent = itemIndent - fixBy; + } + + if ( newIndent > prevIndent + 1 ) { + newIndent = prevIndent + 1; + } + + writer.setAttribute( 'listIndent', newIndent, item ); + + applied = true; + prevIndent = newIndent; + } else { + fixBy = null; + maxIndent = itemIndent + 1; + prevIndent = itemIndent; + } + } + + return applied; +} + +/** + * Scans the list starting from the given list head element and fixes items' types. + * + * @protected + * @param {module:engine/model/element~Element} listHead The list head model element. + * @param {Set.} seenIds The set of already known IDs. + * @param {module:engine/model/writer~Writer} writer The model writer. + * @returns {Boolean} Whether the model was modified. + */ +export function fixListItemIds( listHead, seenIds, writer ) { + const visited = new Set(); + let applied = false; + + for ( + let item = listHead; + item && item.hasAttribute( 'listItemId' ); + item = item.nextSibling + ) { + if ( visited.has( item ) ) { + continue; + } + + let listType = item.getAttribute( 'listType' ); + let listItemId = item.getAttribute( 'listItemId' ); + + // Use a new ID if this one was spot earlier (even in other list). + if ( seenIds.has( listItemId ) ) { + listItemId = uid(); + } + + seenIds.add( listItemId ); + + for ( const block of getListItemBlocks( item, { direction: 'forward' } ) ) { + visited.add( block ); + + // Use a new ID if a block of a bigger list item has different type. + if ( block.getAttribute( 'listType' ) != listType ) { + listItemId = uid(); + listType = block.getAttribute( 'listType' ); + } + + if ( block.getAttribute( 'listItemId' ) != listItemId ) { + writer.setAttribute( 'listItemId', listItemId, block ); + + applied = true; + } + } + } + + return applied; +} diff --git a/packages/ckeditor5-list/src/documentlist/utils/view.js b/packages/ckeditor5-list/src/documentlist/utils/view.js new file mode 100644 index 00000000000..be7cfd4525c --- /dev/null +++ b/packages/ckeditor5-list/src/documentlist/utils/view.js @@ -0,0 +1,137 @@ +/** + * @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/utils/view + */ + +/** + * Checks if view element is a list type (ul or ol). + * + * @protected + * @param {module:engine/view/element~Element} viewElement + * @returns {Boolean} + */ +export function isListView( viewElement ) { + return viewElement.is( 'element', 'ol' ) || viewElement.is( 'element', 'ul' ); +} + +/** + * Checks if view element is a list item (li). + * + * @protected + * @param {module:engine/view/element~Element} viewElement + * @returns {Boolean} + */ +export function isListItemView( viewElement ) { + return viewElement.is( 'element', 'li' ); +} + +/** + * Calculates the indent value for a list item. Handles HTML compliant and non-compliant lists. + * + * Also, fixes non HTML compliant lists indents: + * + * before: fixed list: + * OL OL + * |-> LI (parent LIs: 0) |-> LI (indent: 0) + * |-> OL |-> OL + * |-> OL | + * | |-> OL | + * | |-> OL | + * | |-> LI (parent LIs: 1) |-> LI (indent: 1) + * |-> LI (parent LIs: 1) |-> LI (indent: 1) + * + * before: fixed list: + * OL OL + * |-> OL | + * |-> OL | + * |-> OL | + * |-> LI (parent LIs: 0) |-> LI (indent: 0) + * + * before: fixed list: + * OL OL + * |-> LI (parent LIs: 0) |-> LI (indent: 0) + * |-> OL |-> OL + * |-> LI (parent LIs: 0) |-> LI (indent: 1) + * + * @protected + * @param {module:engine/view/element~Element} listItem + * @returns {Number} + */ +export function getIndent( listItem ) { + let indent = 0; + let parent = listItem.parent; + + while ( parent ) { + // Each LI in the tree will result in an increased indent for HTML compliant lists. + if ( isListItemView( parent ) ) { + indent++; + } else { + // If however the list is nested in other list we should check previous sibling of any of the list elements... + const previousSibling = parent.previousSibling; + + // ...because the we might need increase its indent: + // before: fixed list: + // OL OL + // |-> LI (parent LIs: 0) |-> LI (indent: 0) + // |-> OL |-> OL + // |-> LI (parent LIs: 0) |-> LI (indent: 1) + if ( previousSibling && isListItemView( previousSibling ) ) { + indent++; + } + } + + parent = parent.parent; + } + + return indent; +} + +/** + * Creates a list attribute element (ol or ul). + * + * @protected + * @param {module:engine/view/downcastwriter~DowncastWriter} writer The downcast writer. + * @param {Number} indent The list item indent. + * @param {'bulleted'|'numbered'} type The list type. + * @param {String} [id] The list ID. + * @returns {module:engine/view/attributeelement~AttributeElement} + */ +export function createListElement( writer, indent, type, id ) { + // Negative priorities so that restricted editing attribute won't wrap lists. + return writer.createAttributeElement( getViewElementNameForListType( type ), null, { + priority: 2 * indent / 100 - 100, + id + } ); +} + +/** + * Creates a list item attribute element (li). + * + * @protected + * @param {module:engine/view/downcastwriter~DowncastWriter} writer The downcast writer. + * @param {Number} indent The list item indent. + * @param {String} id The list item ID. + * @returns {module:engine/view/attributeelement~AttributeElement} + */ +export function createListItemElement( writer, indent, id ) { + // Negative priorities so that restricted editing attribute won't wrap list items. + return writer.createAttributeElement( 'li', null, { + priority: ( 2 * indent + 1 ) / 100 - 100, + id + } ); +} + +/** + * Returns a view element name for the given list type. + * + * @protected + * @param {'bulleted'|'numbered'} type The list type. + * @returns {String} + */ +export function getViewElementNameForListType( type ) { + return type == 'numbered' ? 'ol' : 'ul'; +} diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistediting.js b/packages/ckeditor5-list/tests/documentlist/documentlistediting.js index c3e6c1436ac..e5fae0fce94 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistediting.js @@ -125,7 +125,7 @@ describe( 'DocumentListEditing', () => { const indentListCommand = editor.commands.get( 'indentList' ); const indentCommand = editor.commands.get( 'indent' ); - const spy = sinon.spy( indentListCommand, 'execute' ); + const spy = sinon.stub( indentListCommand, 'execute' ); indentListCommand.isEnabled = true; indentCommand.execute(); @@ -143,7 +143,7 @@ describe( 'DocumentListEditing', () => { const outdentListCommand = editor.commands.get( 'outdentList' ); const outdentCommand = editor.commands.get( 'outdent' ); - const spy = sinon.spy( outdentListCommand, 'execute' ); + const spy = sinon.stub( outdentListCommand, 'execute' ); outdentListCommand.isEnabled = true; outdentCommand.execute(); diff --git a/packages/ckeditor5-list/tests/documentlist/utils.js b/packages/ckeditor5-list/tests/documentlist/utils/model.js similarity index 60% rename from packages/ckeditor5-list/tests/documentlist/utils.js rename to packages/ckeditor5-list/tests/documentlist/utils/model.js index b854ee04697..7290e82e8de 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/model.js @@ -4,47 +4,29 @@ */ import { - createListElement, - createListItemElement, expandListBlocksToCompleteItems, - findAndAddListHeadToMap, - fixListIndents, - fixListItemIds, getAllListItemBlocks, - getIndent, getListItemBlocks, getNestedListBlocks, getSiblingListBlock, - getViewElementNameForListType, indentBlocks, isFirstBlockOfListItem, isLastBlockOfListItem, - isListItemView, - isListView, splitListItemBefore -} from '../../src/documentlist/utils'; -import stubUid from './_utils/uid'; +} from '../../../src/documentlist/utils/model'; +import stubUid from '../_utils/uid'; -import UpcastWriter from '@ckeditor/ckeditor5-engine/src/view/upcastwriter'; -import DowncastWriter from '@ckeditor/ckeditor5-engine/src/view/downcastwriter'; -import StylesProcessor from '@ckeditor/ckeditor5-engine/src/view/stylesmap'; import Model from '@ckeditor/ckeditor5-engine/src/model/model'; -import Document from '@ckeditor/ckeditor5-engine/src/view/document'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { stringify as stringifyModel, parse as parseModel } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import { parse as parseView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; describe( 'DocumentList - utils', () => { - let model, schema, viewUpcastWriter, viewDowncastWriter; + let model, schema; testUtils.createSinonSandbox(); beforeEach( () => { - const viewDocument = new Document( new StylesProcessor() ); - model = new Model(); - viewUpcastWriter = new UpcastWriter( viewDocument ); - viewDowncastWriter = new DowncastWriter( viewDocument ); schema = model.schema; schema.register( 'paragraph', { inheritAllFrom: '$block' } ); @@ -52,281 +34,6 @@ describe( 'DocumentList - utils', () => { schema.extend( '$container', { allowAttributes: [ 'listType', 'listIndent', 'listItemId' ] } ); } ); - describe( 'isListView()', () => { - it( 'should return true for UL element', () => { - expect( isListView( viewUpcastWriter.createElement( 'ul' ) ) ).to.be.true; - } ); - - it( 'should return true for OL element', () => { - expect( isListView( viewUpcastWriter.createElement( 'ol' ) ) ).to.be.true; - } ); - - it( 'should return false for LI element', () => { - expect( isListView( viewUpcastWriter.createElement( 'li' ) ) ).to.be.false; - } ); - - it( 'should return false for other elements', () => { - expect( isListView( viewUpcastWriter.createElement( 'a' ) ) ).to.be.false; - expect( isListView( viewUpcastWriter.createElement( 'p' ) ) ).to.be.false; - expect( isListView( viewUpcastWriter.createElement( 'div' ) ) ).to.be.false; - } ); - } ); - - describe( 'isListItemView()', () => { - it( 'should return true for LI element', () => { - expect( isListItemView( viewUpcastWriter.createElement( 'li' ) ) ).to.be.true; - } ); - - it( 'should return false for UL element', () => { - expect( isListItemView( viewUpcastWriter.createElement( 'ul' ) ) ).to.be.false; - } ); - - it( 'should return false for OL element', () => { - expect( isListItemView( viewUpcastWriter.createElement( 'ol' ) ) ).to.be.false; - } ); - - it( 'should return false for other elements', () => { - expect( isListItemView( viewUpcastWriter.createElement( 'a' ) ) ).to.be.false; - expect( isListItemView( viewUpcastWriter.createElement( 'p' ) ) ).to.be.false; - expect( isListItemView( viewUpcastWriter.createElement( 'div' ) ) ).to.be.false; - } ); - } ); - - describe( 'getIndent()', () => { - it( 'should return 0 for flat list', () => { - const viewElement = parseView( - '
    ' + - '
  • a
  • ' + - '
  • b
  • ' + - '
' - ); - - expect( getIndent( viewElement.getChild( 0 ) ) ).to.equal( 0 ); - expect( getIndent( viewElement.getChild( 1 ) ) ).to.equal( 0 ); - } ); - - it( 'should return 1 for first level nested items', () => { - const viewElement = parseView( - '
    ' + - '
  • ' + - '
      ' + - '
    • a
    • ' + - '
    • b
    • ' + - '
    ' + - '
  • ' + - '
  • ' + - '
      ' + - '
    1. c
    2. ' + - '
    3. d
    4. ' + - '
    ' + - '
  • ' + - '
' - ); - - expect( getIndent( viewElement.getChild( 0 ).getChild( 0 ).getChild( 0 ) ) ).to.equal( 1 ); - expect( getIndent( viewElement.getChild( 0 ).getChild( 0 ).getChild( 1 ) ) ).to.equal( 1 ); - expect( getIndent( viewElement.getChild( 1 ).getChild( 0 ).getChild( 0 ) ) ).to.equal( 1 ); - expect( getIndent( viewElement.getChild( 1 ).getChild( 0 ).getChild( 1 ) ) ).to.equal( 1 ); - } ); - - it( 'should ignore container elements', () => { - const viewElement = parseView( - '
    ' + - '
  • ' + - '
    ' + - '
      ' + - '
    • a
    • ' + - '
    • b
    • ' + - '
    ' + - '
    ' + - '
  • ' + - '
  • ' + - '
      ' + - '
    • c
    • ' + - '
    • d
    • ' + - '
    ' + - '
  • ' + - '
' - ); - - expect( getIndent( viewElement.getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 ) ) ).to.equal( 1 ); - expect( getIndent( viewElement.getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 1 ) ) ).to.equal( 1 ); - expect( getIndent( viewElement.getChild( 1 ).getChild( 0 ).getChild( 0 ) ) ).to.equal( 1 ); - expect( getIndent( viewElement.getChild( 1 ).getChild( 0 ).getChild( 1 ) ) ).to.equal( 1 ); - } ); - - it( 'should handle deep nesting', () => { - const viewElement = parseView( - '
    ' + - '
  • ' + - '
      ' + - '
    1. ' + - '
        ' + - '
      • a
      • ' + - '
      • b
      • ' + - '
      ' + - '
    2. ' + - '
    ' + - '
  • ' + - '
' - ); - - const innerList = viewElement.getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 ); - - expect( getIndent( innerList.getChild( 0 ) ) ).to.equal( 2 ); - expect( getIndent( innerList.getChild( 1 ) ) ).to.equal( 2 ); - } ); - - it( 'should ignore superfluous OLs', () => { - const viewElement = parseView( - '
    ' + - '
  • ' + - '
      ' + - '
        ' + - '
          ' + - '
            ' + - '
          1. a
          2. ' + - '
          ' + - '
        ' + - '
      ' + - '
    1. b
    2. ' + - '
    ' + - '
  • ' + - '
' - ); - - const innerList = viewElement.getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 ); - - expect( getIndent( innerList.getChild( 0 ) ) ).to.equal( 1 ); - expect( getIndent( viewElement.getChild( 0 ).getChild( 0 ).getChild( 1 ) ) ).to.equal( 1 ); - } ); - - it( 'should handle broken structure', () => { - const viewElement = parseView( - '
    ' + - '
  • a
  • ' + - '
      ' + - '
    • b
    • ' + - '
    ' + - '
' - ); - - expect( getIndent( viewElement.getChild( 0 ) ) ).to.equal( 0 ); - expect( getIndent( viewElement.getChild( 1 ).getChild( 0 ) ) ).to.equal( 1 ); - } ); - - it( 'should handle broken deeper structure', () => { - const viewElement = parseView( - '
    ' + - '
  • a
  • ' + - '
      ' + - '
    1. b
    2. ' + - '
        ' + - '
      • c
      • ' + - '
      ' + - '
    ' + - '
' - ); - - expect( getIndent( viewElement.getChild( 0 ) ) ).to.equal( 0 ); - expect( getIndent( viewElement.getChild( 1 ).getChild( 0 ) ) ).to.equal( 1 ); - expect( getIndent( viewElement.getChild( 1 ).getChild( 1 ).getChild( 0 ) ) ).to.equal( 2 ); - } ); - } ); - - describe( 'createListElement()', () => { - it( 'should create an attribute element for numbered list with given ID', () => { - const element = createListElement( viewDowncastWriter, 0, 'numbered', 'abc' ); - - expect( element.is( 'attributeElement', 'ol' ) ).to.be.true; - expect( element.id ).to.equal( 'abc' ); - } ); - - it( 'should create an attribute element for bulleted list with given ID', () => { - const element = createListElement( viewDowncastWriter, 0, 'bulleted', '123' ); - - expect( element.is( 'attributeElement', 'ul' ) ).to.be.true; - expect( element.id ).to.equal( '123' ); - } ); - - it( 'should create an attribute element OL for other list types', () => { - const element = createListElement( viewDowncastWriter, 0, 'something', 'foobar' ); - - expect( element.is( 'attributeElement', 'ul' ) ).to.be.true; - expect( element.id ).to.equal( 'foobar' ); - } ); - - it( 'should use priority related to indent', () => { - let previousPriority = Number.NEGATIVE_INFINITY; - - for ( let i = 0; i < 20; i++ ) { - const element = createListElement( viewDowncastWriter, i, 'abc', '123' ); - - expect( element.priority ).to.be.greaterThan( previousPriority ); - expect( element.priority ).to.be.lessThan( 80 ); - - previousPriority = element.priority; - } - } ); - } ); - - describe( 'createListItemElement()', () => { - it( 'should create an attribute element with given ID', () => { - const element = createListItemElement( viewDowncastWriter, 0, 'abc' ); - - expect( element.is( 'attributeElement', 'li' ) ).to.be.true; - expect( element.id ).to.equal( 'abc' ); - } ); - - it( 'should use priority related to indent', () => { - let previousPriority = Number.NEGATIVE_INFINITY; - - for ( let i = 0; i < 20; i++ ) { - const element = createListItemElement( viewDowncastWriter, i, 'abc' ); - - expect( element.priority ).to.be.greaterThan( previousPriority ); - expect( element.priority ).to.be.lessThan( 80 ); - - previousPriority = element.priority; - } - } ); - - it( 'priorities of LI and UL should interleave between nesting levels', () => { - let previousPriority = Number.NEGATIVE_INFINITY; - - for ( let i = 0; i < 20; i++ ) { - const listElement = createListElement( viewDowncastWriter, i, 'abc', '123' ); - const listItemElement = createListItemElement( viewDowncastWriter, i, 'aaaa' ); - - expect( listElement.priority ).to.be.greaterThan( previousPriority ); - expect( listElement.priority ).to.be.lessThan( 80 ); - - previousPriority = listElement.priority; - - expect( listItemElement.priority ).to.be.greaterThan( previousPriority ); - expect( listItemElement.priority ).to.be.lessThan( 80 ); - - previousPriority = listItemElement.priority; - } - } ); - } ); - - describe( 'getViewElementNameForListType()', () => { - it( 'should return "ol" for numbered type', () => { - expect( getViewElementNameForListType( 'numbered' ) ).to.equal( 'ol' ); - } ); - - it( 'should return "ul" for bulleted type', () => { - expect( getViewElementNameForListType( 'bulleted' ) ).to.equal( 'ul' ); - } ); - - it( 'should return "ul" for other types', () => { - expect( getViewElementNameForListType( 'foo' ) ).to.equal( 'ul' ); - expect( getViewElementNameForListType( 'bar' ) ).to.equal( 'ul' ); - expect( getViewElementNameForListType( 'sth' ) ).to.equal( 'ul' ); - } ); - } ); - describe( 'getSiblingListBlock()', () => { it( 'should return the passed element if it matches the criteria (sameIndent, listIndent=0)', () => { const input = @@ -1367,523 +1074,4 @@ describe( 'DocumentList - utils', () => { ); } ); } ); - - describe( 'findAndAddListHeadToMap()', () => { - it( 'should find list that starts just after the given position', () => { - const input = - 'foo' + - 'a' + - 'b'; - - const fragment = parseModel( input, schema ); - const position = model.createPositionAt( fragment, 1 ); - const itemToListHead = new Map(); - - findAndAddListHeadToMap( position, itemToListHead ); - - const heads = Array.from( itemToListHead.values() ); - - expect( heads.length ).to.equal( 1 ); - expect( heads[ 0 ] ).to.equal( fragment.getChild( 1 ) ); - } ); - - it( 'should find list that starts just before the given position', () => { - const input = - 'foo' + - 'a' + - 'b'; - - const fragment = parseModel( input, schema ); - const position = model.createPositionAt( fragment, 2 ); - const itemToListHead = new Map(); - - findAndAddListHeadToMap( position, itemToListHead ); - - const heads = Array.from( itemToListHead.values() ); - - expect( heads.length ).to.equal( 1 ); - expect( heads[ 0 ] ).to.equal( fragment.getChild( 1 ) ); - } ); - - it( 'should find list that ends just before the given position', () => { - const input = - 'foo' + - 'a' + - 'b'; - - const fragment = parseModel( input, schema ); - const position = model.createPositionAt( fragment, 3 ); - const itemToListHead = new Map(); - - findAndAddListHeadToMap( position, itemToListHead ); - - const heads = Array.from( itemToListHead.values() ); - - expect( heads.length ).to.equal( 1 ); - expect( heads[ 0 ] ).to.equal( fragment.getChild( 1 ) ); - } ); - - it( 'should reuse data from map if first item was previously mapped to head', () => { - const input = - 'foo' + - 'a' + - 'b' + - 'c'; - - const fragment = parseModel( input, schema ); - const position = model.createPositionAt( fragment, 3 ); - const itemToListHead = new Map(); - - itemToListHead.set( fragment.getChild( 2 ), fragment.getChild( 1 ) ); - - findAndAddListHeadToMap( position, itemToListHead ); - - const heads = Array.from( itemToListHead.values() ); - - expect( heads.length ).to.equal( 1 ); - expect( heads[ 0 ] ).to.equal( fragment.getChild( 1 ) ); - } ); - - it( 'should reuse data from map if found some item that was previously mapped to head', () => { - const input = - 'foo' + - 'a' + - 'b' + - 'c'; - - const fragment = parseModel( input, schema ); - const position = model.createPositionAt( fragment, 4 ); - const itemToListHead = new Map(); - - itemToListHead.set( fragment.getChild( 2 ), fragment.getChild( 1 ) ); - - findAndAddListHeadToMap( position, itemToListHead ); - - const heads = Array.from( itemToListHead.values() ); - - expect( heads.length ).to.equal( 1 ); - expect( heads[ 0 ] ).to.equal( fragment.getChild( 1 ) ); - } ); - - it( 'should not mix 2 lists separated by some non-list element', () => { - const input = - 'a' + - 'foo' + - 'b' + - 'c'; - - const fragment = parseModel( input, schema ); - const position = model.createPositionAt( fragment, 4 ); - const itemToListHead = new Map(); - - findAndAddListHeadToMap( position, itemToListHead ); - - const heads = Array.from( itemToListHead.values() ); - - expect( heads.length ).to.equal( 1 ); - expect( heads[ 0 ] ).to.equal( fragment.getChild( 2 ) ); - } ); - - it( 'should find list head even for mixed indents, ids, and types', () => { - const input = - 'foo' + - 'a' + - 'a' + - 'b' + - 'c'; - - const fragment = parseModel( input, schema ); - const position = model.createPositionAt( fragment, 5 ); - const itemToListHead = new Map(); - - findAndAddListHeadToMap( position, itemToListHead ); - - const heads = Array.from( itemToListHead.values() ); - - expect( heads.length ).to.equal( 1 ); - expect( heads[ 0 ] ).to.equal( fragment.getChild( 1 ) ); - } ); - - it( 'should not find a list if position is between plain paragraphs', () => { - const input = - 'a' + - 'b' + - 'foo' + - 'bar' + - 'c' + - 'd'; - - const fragment = parseModel( input, schema ); - const position = model.createPositionAt( fragment, 3 ); - const itemToListHead = new Map(); - - findAndAddListHeadToMap( position, itemToListHead ); - - const heads = Array.from( itemToListHead.values() ); - - expect( heads.length ).to.equal( 0 ); - } ); - } ); - - describe( 'fixListIndents()', () => { - it( 'should fix indentation of first list item', () => { - const input = - 'foo' + - 'a'; - - const fragment = parseModel( input, schema ); - - model.change( writer => { - fixListIndents( fragment.getChild( 1 ), writer ); - } ); - - expect( stringifyModel( fragment ) ).to.equal( - 'foo' + - 'a' - ); - } ); - - it( 'should fix indentation of to deep nested items', () => { - const input = - 'a' + - 'b' + - 'c'; - - const fragment = parseModel( input, schema ); - - model.change( writer => { - fixListIndents( fragment.getChild( 0 ), writer ); - } ); - - expect( stringifyModel( fragment ) ).to.equal( - 'a' + - 'b' + - 'c' - ); - } ); - - it( 'should not affect properly indented items after fixed item', () => { - const input = - 'a' + - 'b' + - 'c'; - - const fragment = parseModel( input, schema ); - - model.change( writer => { - fixListIndents( fragment.getChild( 0 ), writer ); - } ); - - expect( stringifyModel( fragment ) ).to.equal( - 'a' + - 'b' + - 'c' - ); - } ); - - it( 'should fix rapid indent spikes', () => { - const input = - 'a' + - 'b' + - 'c'; - - const fragment = parseModel( input, schema ); - - model.change( writer => { - fixListIndents( fragment.getChild( 0 ), writer ); - } ); - - expect( stringifyModel( fragment ) ).to.equal( - 'a' + - 'b' + - 'c' - ); - } ); - - it( 'should fix rapid indent spikes after some item', () => { - const input = - 'a' + - 'b' + - 'c' + - 'd'; - - const fragment = parseModel( input, schema ); - - model.change( writer => { - fixListIndents( fragment.getChild( 0 ), writer ); - } ); - - expect( stringifyModel( fragment ) ).to.equal( - 'a' + - 'b' + - 'c' + - 'd' - ); - } ); - - it( 'should fix indentation keeping the relative indentations', () => { - const input = - 'a' + - 'b' + - 'c' + - 'd' + - 'e' + - 'f' + - 'g'; - - const fragment = parseModel( input, schema ); - - model.change( writer => { - fixListIndents( fragment.getChild( 0 ), writer ); - } ); - - expect( stringifyModel( fragment ) ).to.equal( - 'a' + - 'b' + - 'c' + - 'd' + - 'e' + - 'f' + - 'g' - ); - } ); - - it( 'should flatten the leading indentation spike', () => { - const input = - 'e' + - 'f' + - 'g' + - 'h' + - 'i' + - 'j'; - - const fragment = parseModel( input, schema ); - - model.change( writer => { - fixListIndents( fragment.getChild( 0 ), writer ); - } ); - - expect( stringifyModel( fragment ) ).to.equal( - 'e' + - 'f' + - 'g' + - 'h' + - 'i' + - 'j' - ); - } ); - - it( 'list nested in blockquote', () => { - const input = - 'foo' + - '
' + - 'foo' + - 'bar' + - '
'; - - const fragment = parseModel( input, schema ); - - model.change( writer => { - fixListIndents( fragment.getChild( 1 ).getChild( 0 ), writer ); - } ); - - expect( stringifyModel( fragment ) ).to.equal( - 'foo' + - '
' + - 'foo' + - 'bar' + - '
' - ); - } ); - } ); - - describe( 'fixListItemIds()', () => { - it( 'should update nested item ID', () => { - const input = - '0' + - '1'; - - const fragment = parseModel( input, model.schema ); - const seenIds = new Set(); - - stubUid(); - - model.change( writer => { - fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); - } ); - - expect( stringifyModel( fragment ) ).to.equal( - '0' + - '1' - ); - } ); - - it( 'should update nested item ID (middle element of bigger list item)', () => { - const input = - '0' + - '1' + - '2'; - - const fragment = parseModel( input, model.schema ); - const seenIds = new Set(); - - stubUid(); - - model.change( writer => { - fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); - } ); - - expect( stringifyModel( fragment ) ).to.equal( - '0' + - '1' + - '2' - ); - } ); - - it( 'should use same new ID if multiple items were indented', () => { - const input = - '0' + - '1' + - '2'; - - const fragment = parseModel( input, model.schema ); - const seenIds = new Set(); - - stubUid(); - - model.change( writer => { - fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); - } ); - - expect( stringifyModel( fragment ) ).to.equal( - '0' + - '1' + - '2' - ); - } ); - - it( 'should update item ID if middle item of bigger block changed type', () => { - const input = - '0' + - '1' + - '2'; - - const fragment = parseModel( input, model.schema ); - const seenIds = new Set(); - - stubUid(); - - model.change( writer => { - fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); - } ); - - expect( stringifyModel( fragment ) ).to.equal( - '0' + - '1' + - '2' - ); - } ); - - it( 'should use same new ID if multiple items changed type', () => { - const input = - '0' + - '1' + - '2'; - - const fragment = parseModel( input, model.schema ); - const seenIds = new Set(); - - stubUid(); - - model.change( writer => { - fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); - } ); - - expect( stringifyModel( fragment ) ).to.equal( - '0' + - '1' + - '2' - ); - } ); - - it( 'should fix ids of list with nested lists', () => { - const input = - '0' + - '1' + - '2' + - '3'; - - const fragment = parseModel( input, model.schema ); - const seenIds = new Set(); - - stubUid(); - - model.change( writer => { - fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); - } ); - - expect( stringifyModel( fragment ) ).to.equal( - '0' + - '1' + - '2' + - '3' - ); - } ); - - it( 'should fix ids of list with altered types of multiple items of a single bigger list item', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' + - '7'; - - const fragment = parseModel( input, model.schema ); - const seenIds = new Set(); - - stubUid(); - - model.change( writer => { - fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); - } ); - - expect( stringifyModel( fragment ) ).to.equal( - '0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' + - '7' - ); - } ); - - it( 'should use new ID if some ID was spot before in the other list', () => { - const input = - '0' + - '1' + - '2'; - - const fragment = parseModel( input, model.schema ); - const seenIds = new Set(); - - stubUid(); - - seenIds.add( 'b' ); - - model.change( writer => { - fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); - } ); - - expect( stringifyModel( fragment ) ).to.equal( - '0' + - '1' + - '2' - ); - } ); - } ); } ); diff --git a/packages/ckeditor5-list/tests/documentlist/utils/postfixers.js b/packages/ckeditor5-list/tests/documentlist/utils/postfixers.js new file mode 100644 index 00000000000..4cc9d3ffe43 --- /dev/null +++ b/packages/ckeditor5-list/tests/documentlist/utils/postfixers.js @@ -0,0 +1,549 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import { + findAndAddListHeadToMap, + fixListIndents, + fixListItemIds +} from '../../../src/documentlist/utils/postfixers'; +import stubUid from '../_utils/uid'; + +import Model from '@ckeditor/ckeditor5-engine/src/model/model'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import { stringify as stringifyModel, parse as parseModel } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +describe( 'DocumentList - utils', () => { + let model, schema; + + testUtils.createSinonSandbox(); + + beforeEach( () => { + model = new Model(); + schema = model.schema; + + schema.register( 'paragraph', { inheritAllFrom: '$block' } ); + schema.register( 'blockQuote', { inheritAllFrom: '$container' } ); + schema.extend( '$container', { allowAttributes: [ 'listType', 'listIndent', 'listItemId' ] } ); + } ); + + describe( 'findAndAddListHeadToMap()', () => { + it( 'should find list that starts just after the given position', () => { + const input = + 'foo' + + 'a' + + 'b'; + + const fragment = parseModel( input, schema ); + const position = model.createPositionAt( fragment, 1 ); + const itemToListHead = new Map(); + + findAndAddListHeadToMap( position, itemToListHead ); + + const heads = Array.from( itemToListHead.values() ); + + expect( heads.length ).to.equal( 1 ); + expect( heads[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + } ); + + it( 'should find list that starts just before the given position', () => { + const input = + 'foo' + + 'a' + + 'b'; + + const fragment = parseModel( input, schema ); + const position = model.createPositionAt( fragment, 2 ); + const itemToListHead = new Map(); + + findAndAddListHeadToMap( position, itemToListHead ); + + const heads = Array.from( itemToListHead.values() ); + + expect( heads.length ).to.equal( 1 ); + expect( heads[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + } ); + + it( 'should find list that ends just before the given position', () => { + const input = + 'foo' + + 'a' + + 'b'; + + const fragment = parseModel( input, schema ); + const position = model.createPositionAt( fragment, 3 ); + const itemToListHead = new Map(); + + findAndAddListHeadToMap( position, itemToListHead ); + + const heads = Array.from( itemToListHead.values() ); + + expect( heads.length ).to.equal( 1 ); + expect( heads[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + } ); + + it( 'should reuse data from map if first item was previously mapped to head', () => { + const input = + 'foo' + + 'a' + + 'b' + + 'c'; + + const fragment = parseModel( input, schema ); + const position = model.createPositionAt( fragment, 3 ); + const itemToListHead = new Map(); + + itemToListHead.set( fragment.getChild( 2 ), fragment.getChild( 1 ) ); + + findAndAddListHeadToMap( position, itemToListHead ); + + const heads = Array.from( itemToListHead.values() ); + + expect( heads.length ).to.equal( 1 ); + expect( heads[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + } ); + + it( 'should reuse data from map if found some item that was previously mapped to head', () => { + const input = + 'foo' + + 'a' + + 'b' + + 'c'; + + const fragment = parseModel( input, schema ); + const position = model.createPositionAt( fragment, 4 ); + const itemToListHead = new Map(); + + itemToListHead.set( fragment.getChild( 2 ), fragment.getChild( 1 ) ); + + findAndAddListHeadToMap( position, itemToListHead ); + + const heads = Array.from( itemToListHead.values() ); + + expect( heads.length ).to.equal( 1 ); + expect( heads[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + } ); + + it( 'should not mix 2 lists separated by some non-list element', () => { + const input = + 'a' + + 'foo' + + 'b' + + 'c'; + + const fragment = parseModel( input, schema ); + const position = model.createPositionAt( fragment, 4 ); + const itemToListHead = new Map(); + + findAndAddListHeadToMap( position, itemToListHead ); + + const heads = Array.from( itemToListHead.values() ); + + expect( heads.length ).to.equal( 1 ); + expect( heads[ 0 ] ).to.equal( fragment.getChild( 2 ) ); + } ); + + it( 'should find list head even for mixed indents, ids, and types', () => { + const input = + 'foo' + + 'a' + + 'a' + + 'b' + + 'c'; + + const fragment = parseModel( input, schema ); + const position = model.createPositionAt( fragment, 5 ); + const itemToListHead = new Map(); + + findAndAddListHeadToMap( position, itemToListHead ); + + const heads = Array.from( itemToListHead.values() ); + + expect( heads.length ).to.equal( 1 ); + expect( heads[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + } ); + + it( 'should not find a list if position is between plain paragraphs', () => { + const input = + 'a' + + 'b' + + 'foo' + + 'bar' + + 'c' + + 'd'; + + const fragment = parseModel( input, schema ); + const position = model.createPositionAt( fragment, 3 ); + const itemToListHead = new Map(); + + findAndAddListHeadToMap( position, itemToListHead ); + + const heads = Array.from( itemToListHead.values() ); + + expect( heads.length ).to.equal( 0 ); + } ); + } ); + + describe( 'fixListIndents()', () => { + it( 'should fix indentation of first list item', () => { + const input = + 'foo' + + 'a'; + + const fragment = parseModel( input, schema ); + + model.change( writer => { + fixListIndents( fragment.getChild( 1 ), writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equal( + 'foo' + + 'a' + ); + } ); + + it( 'should fix indentation of to deep nested items', () => { + const input = + 'a' + + 'b' + + 'c'; + + const fragment = parseModel( input, schema ); + + model.change( writer => { + fixListIndents( fragment.getChild( 0 ), writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equal( + 'a' + + 'b' + + 'c' + ); + } ); + + it( 'should not affect properly indented items after fixed item', () => { + const input = + 'a' + + 'b' + + 'c'; + + const fragment = parseModel( input, schema ); + + model.change( writer => { + fixListIndents( fragment.getChild( 0 ), writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equal( + 'a' + + 'b' + + 'c' + ); + } ); + + it( 'should fix rapid indent spikes', () => { + const input = + 'a' + + 'b' + + 'c'; + + const fragment = parseModel( input, schema ); + + model.change( writer => { + fixListIndents( fragment.getChild( 0 ), writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equal( + 'a' + + 'b' + + 'c' + ); + } ); + + it( 'should fix rapid indent spikes after some item', () => { + const input = + 'a' + + 'b' + + 'c' + + 'd'; + + const fragment = parseModel( input, schema ); + + model.change( writer => { + fixListIndents( fragment.getChild( 0 ), writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equal( + 'a' + + 'b' + + 'c' + + 'd' + ); + } ); + + it( 'should fix indentation keeping the relative indentations', () => { + const input = + 'a' + + 'b' + + 'c' + + 'd' + + 'e' + + 'f' + + 'g'; + + const fragment = parseModel( input, schema ); + + model.change( writer => { + fixListIndents( fragment.getChild( 0 ), writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equal( + 'a' + + 'b' + + 'c' + + 'd' + + 'e' + + 'f' + + 'g' + ); + } ); + + it( 'should flatten the leading indentation spike', () => { + const input = + 'e' + + 'f' + + 'g' + + 'h' + + 'i' + + 'j'; + + const fragment = parseModel( input, schema ); + + model.change( writer => { + fixListIndents( fragment.getChild( 0 ), writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equal( + 'e' + + 'f' + + 'g' + + 'h' + + 'i' + + 'j' + ); + } ); + + it( 'list nested in blockquote', () => { + const input = + 'foo' + + '
' + + 'foo' + + 'bar' + + '
'; + + const fragment = parseModel( input, schema ); + + model.change( writer => { + fixListIndents( fragment.getChild( 1 ).getChild( 0 ), writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equal( + 'foo' + + '
' + + 'foo' + + 'bar' + + '
' + ); + } ); + } ); + + describe( 'fixListItemIds()', () => { + it( 'should update nested item ID', () => { + const input = + '0' + + '1'; + + const fragment = parseModel( input, model.schema ); + const seenIds = new Set(); + + stubUid(); + + model.change( writer => { + fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equal( + '0' + + '1' + ); + } ); + + it( 'should update nested item ID (middle element of bigger list item)', () => { + const input = + '0' + + '1' + + '2'; + + const fragment = parseModel( input, model.schema ); + const seenIds = new Set(); + + stubUid(); + + model.change( writer => { + fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equal( + '0' + + '1' + + '2' + ); + } ); + + it( 'should use same new ID if multiple items were indented', () => { + const input = + '0' + + '1' + + '2'; + + const fragment = parseModel( input, model.schema ); + const seenIds = new Set(); + + stubUid(); + + model.change( writer => { + fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equal( + '0' + + '1' + + '2' + ); + } ); + + it( 'should update item ID if middle item of bigger block changed type', () => { + const input = + '0' + + '1' + + '2'; + + const fragment = parseModel( input, model.schema ); + const seenIds = new Set(); + + stubUid(); + + model.change( writer => { + fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equal( + '0' + + '1' + + '2' + ); + } ); + + it( 'should use same new ID if multiple items changed type', () => { + const input = + '0' + + '1' + + '2'; + + const fragment = parseModel( input, model.schema ); + const seenIds = new Set(); + + stubUid(); + + model.change( writer => { + fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equal( + '0' + + '1' + + '2' + ); + } ); + + it( 'should fix ids of list with nested lists', () => { + const input = + '0' + + '1' + + '2' + + '3'; + + const fragment = parseModel( input, model.schema ); + const seenIds = new Set(); + + stubUid(); + + model.change( writer => { + fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equal( + '0' + + '1' + + '2' + + '3' + ); + } ); + + it( 'should fix ids of list with altered types of multiple items of a single bigger list item', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6' + + '7'; + + const fragment = parseModel( input, model.schema ); + const seenIds = new Set(); + + stubUid(); + + model.change( writer => { + fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equal( + '0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6' + + '7' + ); + } ); + + it( 'should use new ID if some ID was spot before in the other list', () => { + const input = + '0' + + '1' + + '2'; + + const fragment = parseModel( input, model.schema ); + const seenIds = new Set(); + + stubUid(); + + seenIds.add( 'b' ); + + model.change( writer => { + fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equal( + '0' + + '1' + + '2' + ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-list/tests/documentlist/utils/view.js b/packages/ckeditor5-list/tests/documentlist/utils/view.js new file mode 100644 index 00000000000..fc3315611f4 --- /dev/null +++ b/packages/ckeditor5-list/tests/documentlist/utils/view.js @@ -0,0 +1,308 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import { + createListElement, + createListItemElement, + getIndent, + getViewElementNameForListType, + isListItemView, + isListView +} from '../../../src/documentlist/utils/view'; + +import UpcastWriter from '@ckeditor/ckeditor5-engine/src/view/upcastwriter'; +import DowncastWriter from '@ckeditor/ckeditor5-engine/src/view/downcastwriter'; +import StylesProcessor from '@ckeditor/ckeditor5-engine/src/view/stylesmap'; +import Document from '@ckeditor/ckeditor5-engine/src/view/document'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import { parse as parseView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; + +describe( 'DocumentList - utils', () => { + let viewUpcastWriter, viewDowncastWriter; + + testUtils.createSinonSandbox(); + + beforeEach( () => { + const viewDocument = new Document( new StylesProcessor() ); + + viewUpcastWriter = new UpcastWriter( viewDocument ); + viewDowncastWriter = new DowncastWriter( viewDocument ); + } ); + + describe( 'isListView()', () => { + it( 'should return true for UL element', () => { + expect( isListView( viewUpcastWriter.createElement( 'ul' ) ) ).to.be.true; + } ); + + it( 'should return true for OL element', () => { + expect( isListView( viewUpcastWriter.createElement( 'ol' ) ) ).to.be.true; + } ); + + it( 'should return false for LI element', () => { + expect( isListView( viewUpcastWriter.createElement( 'li' ) ) ).to.be.false; + } ); + + it( 'should return false for other elements', () => { + expect( isListView( viewUpcastWriter.createElement( 'a' ) ) ).to.be.false; + expect( isListView( viewUpcastWriter.createElement( 'p' ) ) ).to.be.false; + expect( isListView( viewUpcastWriter.createElement( 'div' ) ) ).to.be.false; + } ); + } ); + + describe( 'isListItemView()', () => { + it( 'should return true for LI element', () => { + expect( isListItemView( viewUpcastWriter.createElement( 'li' ) ) ).to.be.true; + } ); + + it( 'should return false for UL element', () => { + expect( isListItemView( viewUpcastWriter.createElement( 'ul' ) ) ).to.be.false; + } ); + + it( 'should return false for OL element', () => { + expect( isListItemView( viewUpcastWriter.createElement( 'ol' ) ) ).to.be.false; + } ); + + it( 'should return false for other elements', () => { + expect( isListItemView( viewUpcastWriter.createElement( 'a' ) ) ).to.be.false; + expect( isListItemView( viewUpcastWriter.createElement( 'p' ) ) ).to.be.false; + expect( isListItemView( viewUpcastWriter.createElement( 'div' ) ) ).to.be.false; + } ); + } ); + + describe( 'getIndent()', () => { + it( 'should return 0 for flat list', () => { + const viewElement = parseView( + '
    ' + + '
  • a
  • ' + + '
  • b
  • ' + + '
' + ); + + expect( getIndent( viewElement.getChild( 0 ) ) ).to.equal( 0 ); + expect( getIndent( viewElement.getChild( 1 ) ) ).to.equal( 0 ); + } ); + + it( 'should return 1 for first level nested items', () => { + const viewElement = parseView( + '
    ' + + '
  • ' + + '
      ' + + '
    • a
    • ' + + '
    • b
    • ' + + '
    ' + + '
  • ' + + '
  • ' + + '
      ' + + '
    1. c
    2. ' + + '
    3. d
    4. ' + + '
    ' + + '
  • ' + + '
' + ); + + expect( getIndent( viewElement.getChild( 0 ).getChild( 0 ).getChild( 0 ) ) ).to.equal( 1 ); + expect( getIndent( viewElement.getChild( 0 ).getChild( 0 ).getChild( 1 ) ) ).to.equal( 1 ); + expect( getIndent( viewElement.getChild( 1 ).getChild( 0 ).getChild( 0 ) ) ).to.equal( 1 ); + expect( getIndent( viewElement.getChild( 1 ).getChild( 0 ).getChild( 1 ) ) ).to.equal( 1 ); + } ); + + it( 'should ignore container elements', () => { + const viewElement = parseView( + '
    ' + + '
  • ' + + '
    ' + + '
      ' + + '
    • a
    • ' + + '
    • b
    • ' + + '
    ' + + '
    ' + + '
  • ' + + '
  • ' + + '
      ' + + '
    • c
    • ' + + '
    • d
    • ' + + '
    ' + + '
  • ' + + '
' + ); + + expect( getIndent( viewElement.getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 ) ) ).to.equal( 1 ); + expect( getIndent( viewElement.getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 1 ) ) ).to.equal( 1 ); + expect( getIndent( viewElement.getChild( 1 ).getChild( 0 ).getChild( 0 ) ) ).to.equal( 1 ); + expect( getIndent( viewElement.getChild( 1 ).getChild( 0 ).getChild( 1 ) ) ).to.equal( 1 ); + } ); + + it( 'should handle deep nesting', () => { + const viewElement = parseView( + '
    ' + + '
  • ' + + '
      ' + + '
    1. ' + + '
        ' + + '
      • a
      • ' + + '
      • b
      • ' + + '
      ' + + '
    2. ' + + '
    ' + + '
  • ' + + '
' + ); + + const innerList = viewElement.getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 ); + + expect( getIndent( innerList.getChild( 0 ) ) ).to.equal( 2 ); + expect( getIndent( innerList.getChild( 1 ) ) ).to.equal( 2 ); + } ); + + it( 'should ignore superfluous OLs', () => { + const viewElement = parseView( + '
    ' + + '
  • ' + + '
      ' + + '
        ' + + '
          ' + + '
            ' + + '
          1. a
          2. ' + + '
          ' + + '
        ' + + '
      ' + + '
    1. b
    2. ' + + '
    ' + + '
  • ' + + '
' + ); + + const innerList = viewElement.getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 ); + + expect( getIndent( innerList.getChild( 0 ) ) ).to.equal( 1 ); + expect( getIndent( viewElement.getChild( 0 ).getChild( 0 ).getChild( 1 ) ) ).to.equal( 1 ); + } ); + + it( 'should handle broken structure', () => { + const viewElement = parseView( + '
    ' + + '
  • a
  • ' + + '
      ' + + '
    • b
    • ' + + '
    ' + + '
' + ); + + expect( getIndent( viewElement.getChild( 0 ) ) ).to.equal( 0 ); + expect( getIndent( viewElement.getChild( 1 ).getChild( 0 ) ) ).to.equal( 1 ); + } ); + + it( 'should handle broken deeper structure', () => { + const viewElement = parseView( + '
    ' + + '
  • a
  • ' + + '
      ' + + '
    1. b
    2. ' + + '
        ' + + '
      • c
      • ' + + '
      ' + + '
    ' + + '
' + ); + + expect( getIndent( viewElement.getChild( 0 ) ) ).to.equal( 0 ); + expect( getIndent( viewElement.getChild( 1 ).getChild( 0 ) ) ).to.equal( 1 ); + expect( getIndent( viewElement.getChild( 1 ).getChild( 1 ).getChild( 0 ) ) ).to.equal( 2 ); + } ); + } ); + + describe( 'createListElement()', () => { + it( 'should create an attribute element for numbered list with given ID', () => { + const element = createListElement( viewDowncastWriter, 0, 'numbered', 'abc' ); + + expect( element.is( 'attributeElement', 'ol' ) ).to.be.true; + expect( element.id ).to.equal( 'abc' ); + } ); + + it( 'should create an attribute element for bulleted list with given ID', () => { + const element = createListElement( viewDowncastWriter, 0, 'bulleted', '123' ); + + expect( element.is( 'attributeElement', 'ul' ) ).to.be.true; + expect( element.id ).to.equal( '123' ); + } ); + + it( 'should create an attribute element OL for other list types', () => { + const element = createListElement( viewDowncastWriter, 0, 'something', 'foobar' ); + + expect( element.is( 'attributeElement', 'ul' ) ).to.be.true; + expect( element.id ).to.equal( 'foobar' ); + } ); + + it( 'should use priority related to indent', () => { + let previousPriority = Number.NEGATIVE_INFINITY; + + for ( let i = 0; i < 20; i++ ) { + const element = createListElement( viewDowncastWriter, i, 'abc', '123' ); + + expect( element.priority ).to.be.greaterThan( previousPriority ); + expect( element.priority ).to.be.lessThan( 80 ); + + previousPriority = element.priority; + } + } ); + } ); + + describe( 'createListItemElement()', () => { + it( 'should create an attribute element with given ID', () => { + const element = createListItemElement( viewDowncastWriter, 0, 'abc' ); + + expect( element.is( 'attributeElement', 'li' ) ).to.be.true; + expect( element.id ).to.equal( 'abc' ); + } ); + + it( 'should use priority related to indent', () => { + let previousPriority = Number.NEGATIVE_INFINITY; + + for ( let i = 0; i < 20; i++ ) { + const element = createListItemElement( viewDowncastWriter, i, 'abc' ); + + expect( element.priority ).to.be.greaterThan( previousPriority ); + expect( element.priority ).to.be.lessThan( 80 ); + + previousPriority = element.priority; + } + } ); + + it( 'priorities of LI and UL should interleave between nesting levels', () => { + let previousPriority = Number.NEGATIVE_INFINITY; + + for ( let i = 0; i < 20; i++ ) { + const listElement = createListElement( viewDowncastWriter, i, 'abc', '123' ); + const listItemElement = createListItemElement( viewDowncastWriter, i, 'aaaa' ); + + expect( listElement.priority ).to.be.greaterThan( previousPriority ); + expect( listElement.priority ).to.be.lessThan( 80 ); + + previousPriority = listElement.priority; + + expect( listItemElement.priority ).to.be.greaterThan( previousPriority ); + expect( listItemElement.priority ).to.be.lessThan( 80 ); + + previousPriority = listItemElement.priority; + } + } ); + } ); + + describe( 'getViewElementNameForListType()', () => { + it( 'should return "ol" for numbered type', () => { + expect( getViewElementNameForListType( 'numbered' ) ).to.equal( 'ol' ); + } ); + + it( 'should return "ul" for bulleted type', () => { + expect( getViewElementNameForListType( 'bulleted' ) ).to.equal( 'ul' ); + } ); + + it( 'should return "ul" for other types', () => { + expect( getViewElementNameForListType( 'foo' ) ).to.equal( 'ul' ); + expect( getViewElementNameForListType( 'bar' ) ).to.equal( 'ul' ); + expect( getViewElementNameForListType( 'sth' ) ).to.equal( 'ul' ); + } ); + } ); +} ); From 177d443920a842dfec3d67853298503d794e7038 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 17 Dec 2021 19:06:51 +0100 Subject: [PATCH 10/66] Introducing ListWalker. --- .../src/documentlist/converters.js | 7 +- .../documentlist/documentlistindentcommand.js | 5 +- .../src/documentlist/utils/listwalker.js | 101 +++++++++++++ .../src/documentlist/utils/model.js | 135 +++--------------- .../tests/documentlist/utils/model.js | 3 +- 5 files changed, 130 insertions(+), 121 deletions(-) create mode 100644 packages/ckeditor5-list/src/documentlist/utils/listwalker.js diff --git a/packages/ckeditor5-list/src/documentlist/converters.js b/packages/ckeditor5-list/src/documentlist/converters.js index 83f35e1ef63..f442c2fd7ae 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.js +++ b/packages/ckeditor5-list/src/documentlist/converters.js @@ -5,7 +5,6 @@ import { getAllListItemBlocks, - getSiblingListBlock, getListItemBlocks } from './utils/model'; import { @@ -16,6 +15,7 @@ import { isListItemView, getViewElementNameForListType } from './utils/view'; +import ListWalker from './utils/listwalker'; import { findAndAddListHeadToMap } from './utils/postfixers'; import { uid } from 'ckeditor5/src/utils'; @@ -451,7 +451,10 @@ function wrapListItemBlock( listItem, viewRange, writer ) { break; } - currentListItem = getSiblingListBlock( currentListItem, { smallerIndent: true, listIndent: indent } ); + currentListItem = ListWalker.first( currentListItem, { + smallerIndent: true, + includeSelf: false + } ); // There is no list item with smaller 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. diff --git a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js index c31cd88ff2a..2af5bcc3250 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js @@ -10,11 +10,11 @@ import { Command } from 'ckeditor5/src/core'; import { expandListBlocksToCompleteItems, - getSiblingListBlock, indentBlocks, isFirstBlockOfListItem, splitListItemBefore } from './utils/model'; +import ListWalker from './utils/listwalker'; /** * The document list indent command. It is used by the {@link module:list/documentlist~DocumentList list feature}. @@ -119,8 +119,7 @@ export default class DocumentListIndentCommand extends Command { firstBlock = blocks[ 0 ]; // Check if there is any list item before selected items that could become a parent of selected items. - const siblingItem = getSiblingListBlock( firstBlock.previousSibling, { - listIndent: firstBlock.getAttribute( 'listIndent' ), + const siblingItem = ListWalker.first( firstBlock, { sameIndent: true } ); diff --git a/packages/ckeditor5-list/src/documentlist/utils/listwalker.js b/packages/ckeditor5-list/src/documentlist/utils/listwalker.js new file mode 100644 index 00000000000..6bc4505d3bd --- /dev/null +++ b/packages/ckeditor5-list/src/documentlist/utils/listwalker.js @@ -0,0 +1,101 @@ +/** + * @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/utils/listwalker + */ + +import { first } from 'ckeditor5/src/utils'; + +export default class ListWalker { + constructor( startElement, options ) { + this._startElement = startElement; + this._startItemId = startElement.getAttribute( 'listItemId' ); + this._startIndent = startElement.getAttribute( 'listIndent' ); + + this._isForward = options.direction == 'forward'; + this._includeSelf = !!options.includeSelf; + this._sameItem = !!options.sameItem; + this._sameIndent = !!options.sameIndent; + this._smallerIndent = !!options.smallerIndent; + this._biggerIndent = !!options.biggerIndent; + } + + static first( startElement, options ) { + return first( new this( startElement, options )[ Symbol.iterator ]() ); + } + + * [ Symbol.iterator ]() { + const nestedItems = []; + + for ( const node of iterateSiblingListBlocks( this._getStartNode(), this._isForward ) ) { + const indent = node.getAttribute( 'listIndent' ); + + // Leaving a nested list. + if ( indent < this._startIndent ) { + // Abort searching blocks. + if ( !this._smallerIndent ) { + break; + } + } + // Entering a nested list. + else if ( indent > this._startIndent ) { + // Ignore nested blocks. + if ( !this._biggerIndent ) { + continue; + } + + // Collect nested blocks to verify if they are really nested, or it's a different item. + if ( !this._isForward ) { + nestedItems.push( node ); + + continue; + } + } + // Same indent level item. + else /* if ( indent == this._startIndent ) */ { + // Ignore same indent block. + if ( !this._sameIndent ) { + if ( this._smallerIndent ) { + continue; + } else if ( this._biggerIndent ) { + break; + } + } + + // Abort if item has a different ID. + if ( this._sameItem && node.getAttribute( 'listItemId' ) != this._startItemId ) { + break; + } + } + + // There is another block for the same list item so the nested items were in the same list item. + if ( nestedItems.length ) { + yield* nestedItems; + nestedItems.length = 0; + } + + yield node; + } + } + + _getStartNode() { + if ( this._includeSelf ) { + return this._startElement; + } + + return this._isForward ? + this._startElement.nextSibling : + this._startElement.previousSibling; + } +} + +function* iterateSiblingListBlocks( node, isForward ) { + while ( node && node.hasAttribute( 'listItemId' ) ) { + yield node; + + node = isForward ? node.nextSibling : node.previousSibling; + } +} diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.js b/packages/ckeditor5-list/src/documentlist/utils/model.js index 5fb634027f6..41cf2c112b5 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.js +++ b/packages/ckeditor5-list/src/documentlist/utils/model.js @@ -8,49 +8,7 @@ */ import { uid } from 'ckeditor5/src/utils'; - -/** - * Returns the closest list item model element according to the specified options. - * - * Note that if the provided model element satisfies the provided options then it's returned. - * - * @protected - * @param {module:engine/model/element~Element} modelElement - * @param {Object} options - * @param {Number} options.listIndent The reference list indent. - * @param {Boolean} [options.sameIndent=false] Whether to return list item model element with the same indent as specified. - * @param {Boolean} [options.smallerIndent=false] Whether to return list item model element with the smaller indent as specified. - * @param {'forward'|'backward'} [options.direction='backward'] The search direction. - * @return {module:engine/model/element~Element|null} - */ -export function getSiblingListBlock( modelElement, options ) { - const sameIndent = !!options.sameIndent; - const smallerIndent = !!options.smallerIndent; - const indent = options.listIndent; - const isForward = options.direction == 'forward'; - - for ( - let item = modelElement; - item && item.hasAttribute( 'listItemId' ); - item = isForward ? item.nextSibling : item.previousSibling - ) { - const itemIndent = item.getAttribute( 'listIndent' ); - - if ( itemIndent > indent ) { - continue; - } - - if ( sameIndent && itemIndent == indent ) { - return item; - } - - if ( itemIndent < indent ) { - return smallerIndent ? item : null; - } - } - - return null; -} +import ListWalker from './listwalker'; /** * Returns an array with all elements that represents the same list item. @@ -83,54 +41,16 @@ export function getAllListItemBlocks( listItem ) { * @returns {Array.} */ export function getListItemBlocks( listItem, options = {} ) { - const limitIndent = listItem.getAttribute( 'listIndent' ); - const listItemId = listItem.getAttribute( 'listItemId' ); const isForward = options.direction == 'forward'; const includeNested = !!options.includeNested; - const items = []; - const nestedItems = []; - - // TODO use generator instead of for loop (ListWalker) - for ( - let item = isForward ? listItem : listItem.previousSibling; - item && item.hasAttribute( 'listItemId' ); - item = isForward ? item.nextSibling : item.previousSibling - ) { - const itemIndent = item.getAttribute( 'listIndent' ); - // If current parsed item has lower indent that element that the element that was a starting point, - // it means we left a nested list. Abort searching items. - if ( itemIndent < limitIndent ) { - break; - } - - if ( itemIndent > limitIndent ) { - // Ignore nested lists. - if ( !includeNested ) { - continue; - } - - // Collect nested items to verify if they are really nested, or it's a different item. - if ( !isForward ) { - nestedItems.push( item ); - - continue; - } - } - - // Abort if item has a different ID. - if ( itemIndent == limitIndent && item.getAttribute( 'listItemId' ) != listItemId ) { - break; - } - - // There is another block for the same list item so the nested items were in the same list item. - if ( nestedItems.length ) { - items.push( ...nestedItems ); - nestedItems.length = 0; - } - - items.push( item ); - } + const items = Array.from( new ListWalker( listItem, { + direction: options.direction, + biggerIndent: includeNested, + includeSelf: isForward, + sameIndent: true, + sameItem: true + } ) ); return isForward ? items : items.reverse(); } @@ -143,22 +63,10 @@ export function getListItemBlocks( listItem, options = {} ) { * @returns {Array.} */ export function getNestedListBlocks( listItem ) { - const indent = listItem.getAttribute( 'listIndent' ); - const items = []; - - for ( - let item = listItem.nextSibling; - item && item.hasAttribute( 'listItemId' ); - item = item.nextSibling - ) { - if ( item.getAttribute( 'listIndent' ) <= indent ) { - break; - } - - items.push( item ); - } - - return items; + return Array.from( new ListWalker( listItem, { + direction: 'forward', + biggerIndent: true + } ) ); } /** @@ -169,19 +77,16 @@ export function getNestedListBlocks( listItem ) { * @returns {Boolean} */ export function isFirstBlockOfListItem( listBlock ) { - const previousSibling = getSiblingListBlock( listBlock.previousSibling, { - listIndent: listBlock.getAttribute( 'listIndent' ), - sameIndent: true + const previousSibling = ListWalker.first( listBlock, { + direction: 'backward', + sameIndent: true, + sameItem: true } ); if ( !previousSibling ) { return true; } - if ( previousSibling.getAttribute( 'listItemId' ) != listBlock.getAttribute( 'listItemId' ) ) { - return true; - } - return false; } @@ -193,17 +98,17 @@ export function isFirstBlockOfListItem( listBlock ) { * @returns {Boolean} */ export function isLastBlockOfListItem( listBlock ) { - const nextSibling = getSiblingListBlock( listBlock.nextSibling, { - listIndent: listBlock.getAttribute( 'listIndent' ), + const nextSibling = ListWalker.first( listBlock, { direction: 'forward', - sameIndent: true + sameIndent: true, + sameItem: true } ); if ( !nextSibling ) { return true; } - return nextSibling.getAttribute( 'listItemId' ) != listBlock.getAttribute( 'listItemId' ); + return false; } /** diff --git a/packages/ckeditor5-list/tests/documentlist/utils/model.js b/packages/ckeditor5-list/tests/documentlist/utils/model.js index 7290e82e8de..07deb64971c 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/model.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/model.js @@ -34,7 +34,8 @@ describe( 'DocumentList - utils', () => { schema.extend( '$container', { allowAttributes: [ 'listType', 'listIndent', 'listItemId' ] } ); } ); - describe( 'getSiblingListBlock()', () => { + // TODO ListWalker + describe.skip( 'getSiblingListBlock()', () => { it( 'should return the passed element if it matches the criteria (sameIndent, listIndent=0)', () => { const input = '0.' + From ef73a21abb41d7b30a21e65f6699f502536aaf19 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 17 Dec 2021 19:18:04 +0100 Subject: [PATCH 11/66] Small code refactor. --- .../ckeditor5-list/src/documentlist/utils/listwalker.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/utils/listwalker.js b/packages/ckeditor5-list/src/documentlist/utils/listwalker.js index 6bc4505d3bd..05a244047b6 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/listwalker.js +++ b/packages/ckeditor5-list/src/documentlist/utils/listwalker.js @@ -58,11 +58,11 @@ export default class ListWalker { else /* if ( indent == this._startIndent ) */ { // Ignore same indent block. if ( !this._sameIndent ) { - if ( this._smallerIndent ) { - continue; - } else if ( this._biggerIndent ) { + if ( this._biggerIndent ) { break; } + + continue; } // Abort if item has a different ID. From f9ab04ac3674c01f0e10b7d78ca30456fae740d6 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 17 Dec 2021 19:33:58 +0100 Subject: [PATCH 12/66] Code cleanup. --- .../src/documentlist/utils/listwalker.js | 36 ++++++++++++++++--- .../src/documentlist/utils/model.js | 6 ++-- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/utils/listwalker.js b/packages/ckeditor5-list/src/documentlist/utils/listwalker.js index 05a244047b6..70163fd867c 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/listwalker.js +++ b/packages/ckeditor5-list/src/documentlist/utils/listwalker.js @@ -10,21 +10,49 @@ import { first } from 'ckeditor5/src/utils'; export default class ListWalker { + /** + * TODO + * + * @param {module:engine/model/element~Element} startElement Starting list item block element. + * @param {Object} options + * @param {'forward'|'backward'} [options.direction='backward'] + * @param {Boolean} [options.includeSelf=false] + * @param {Boolean} [options.sameItem=false] + * @param {Boolean} [options.sameIndent=false] + * @param {Boolean} [options.smallerIndent=false] + * @param {Boolean} [options.biggerIndent=false] + */ constructor( startElement, options ) { this._startElement = startElement; - this._startItemId = startElement.getAttribute( 'listItemId' ); this._startIndent = startElement.getAttribute( 'listIndent' ); + this._startItemId = startElement.getAttribute( 'listItemId' ); this._isForward = options.direction == 'forward'; this._includeSelf = !!options.includeSelf; - this._sameItem = !!options.sameItem; + this._sameItemId = !!options.sameItemId; this._sameIndent = !!options.sameIndent; this._smallerIndent = !!options.smallerIndent; this._biggerIndent = !!options.biggerIndent; } + /** + * TODO + * + * @param {module:engine/model/element~Element} startElement Starting list item block element. + * @param {Object} options + * @param {'forward'|'backward'} [options.direction='backward'] + * @param {Boolean} [options.includeSelf=false] + * @param {Boolean} [options.sameItem=false] + * @param {Boolean} [options.sameIndent=false] + * @param {Boolean} [options.smallerIndent=false] + * @param {Boolean} [options.biggerIndent=false] + * @returns {module:engine/model/element~Element|null} + */ static first( startElement, options ) { - return first( new this( startElement, options )[ Symbol.iterator ]() ); + const walker = new this( startElement, options ); + const iterator = walker[ Symbol.iterator ](); + + return first( iterator ); } * [ Symbol.iterator ]() { @@ -66,7 +94,7 @@ export default class ListWalker { } // Abort if item has a different ID. - if ( this._sameItem && node.getAttribute( 'listItemId' ) != this._startItemId ) { + if ( this._sameItemId && node.getAttribute( 'listItemId' ) != this._startItemId ) { break; } } diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.js b/packages/ckeditor5-list/src/documentlist/utils/model.js index 41cf2c112b5..aececec34a3 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.js +++ b/packages/ckeditor5-list/src/documentlist/utils/model.js @@ -49,7 +49,7 @@ export function getListItemBlocks( listItem, options = {} ) { biggerIndent: includeNested, includeSelf: isForward, sameIndent: true, - sameItem: true + sameItemId: true } ) ); return isForward ? items : items.reverse(); @@ -80,7 +80,7 @@ export function isFirstBlockOfListItem( listBlock ) { const previousSibling = ListWalker.first( listBlock, { direction: 'backward', sameIndent: true, - sameItem: true + sameItemId: true } ); if ( !previousSibling ) { @@ -101,7 +101,7 @@ export function isLastBlockOfListItem( listBlock ) { const nextSibling = ListWalker.first( listBlock, { direction: 'forward', sameIndent: true, - sameItem: true + sameItemId: true } ); if ( !nextSibling ) { From 66082affe77fce717a0b1987323c7eb093777654 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Sat, 18 Dec 2021 13:03:49 +0100 Subject: [PATCH 13/66] ListWalker tuning. --- .../src/documentlist/converters.js | 5 +- .../documentlist/documentlistindentcommand.js | 4 +- .../src/documentlist/utils/listwalker.js | 121 ++++++- .../src/documentlist/utils/model.js | 30 +- .../tests/documentlist/utils/model.js | 338 +++++++++--------- .../tests/documentlist/utils/postfixers.js | 2 +- .../tests/documentlist/utils/view.js | 2 +- 7 files changed, 295 insertions(+), 207 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/converters.js b/packages/ckeditor5-list/src/documentlist/converters.js index f442c2fd7ae..3d027200423 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.js +++ b/packages/ckeditor5-list/src/documentlist/converters.js @@ -451,10 +451,7 @@ function wrapListItemBlock( listItem, viewRange, writer ) { break; } - currentListItem = ListWalker.first( currentListItem, { - smallerIndent: true, - includeSelf: false - } ); + currentListItem = ListWalker.first( currentListItem, { smallerIndent: true } ); // There is no list item with smaller 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. diff --git a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js index 2af5bcc3250..283d6714eb5 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js @@ -119,9 +119,7 @@ export default class DocumentListIndentCommand extends Command { firstBlock = blocks[ 0 ]; // Check if there is any list item before selected items that could become a parent of selected items. - const siblingItem = ListWalker.first( firstBlock, { - sameIndent: true - } ); + const siblingItem = ListWalker.first( firstBlock, { sameIndent: true } ); if ( !siblingItem ) { return false; diff --git a/packages/ckeditor5-list/src/documentlist/utils/listwalker.js b/packages/ckeditor5-list/src/documentlist/utils/listwalker.js index 70163fd867c..a625787bc8f 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/listwalker.js +++ b/packages/ckeditor5-list/src/documentlist/utils/listwalker.js @@ -9,43 +9,115 @@ import { first } from 'ckeditor5/src/utils'; +/** + * Document list blocks iterator. + */ export default class ListWalker { /** - * TODO + * Creates a document list iterator. * - * @param {module:engine/model/element~Element} startElement Starting list item block element. + * @param {module:engine/model/element~Element} startElement The start list item block element. * @param {Object} options - * @param {'forward'|'backward'} [options.direction='backward'] - * @param {Boolean} [options.includeSelf=false] - * @param {Boolean} [options.sameItem=false] - * @param {Boolean} [options.sameIndent=false] - * @param {Boolean} [options.smallerIndent=false] - * @param {Boolean} [options.biggerIndent=false] + * @param {'forward'|'backward'} [options.direction='backward'] The iterating direction. + * @param {Boolean} [options.includeSelf=false] Whether start block should be included in the result (if it's matching other criteria). + * @param {Boolean} [options.sameItemId=false] Whether should return only blocks with the same `listItemId` attribute + * as the start element. + * @param {Boolean} [options.sameIndent=false] Whether blocks with the same indent level as the start block should be included + * in the result. + * @param {Boolean} [options.smallerIndent=false] Whether blocks with a smaller indent level then the start block should be included + * in the result. + * @param {Boolean} [options.biggerIndent=false] Whether blocks with a bigger indent level then the start block should be included + * in the result. */ constructor( startElement, options ) { + /** + * The start list item block element. + * + * @private + * @type {module:engine/model/element~Element} + */ this._startElement = startElement; + + /** + * The indent of the start block. + * + * @private + * @type {Number} + */ this._startIndent = startElement.getAttribute( 'listIndent' ); + + /** + * The `listItemId` of the start block. + * + * @private + * @type {String} + */ this._startItemId = startElement.getAttribute( 'listItemId' ); + /** + * The iterating direction. + * + * @private + * @type {Boolean} + */ this._isForward = options.direction == 'forward'; + + /** + * Whether should return only blocks with the same `listItemId` attribute as the start element. + * + * @private + * @type {Boolean} + */ this._includeSelf = !!options.includeSelf; + + /** + * Whether only blocks with the `listItemId` attribute same as the start element should be included. + * + * @private + * @type {Boolean} + */ this._sameItemId = !!options.sameItemId; + + /** + * Whether blocks with the same indent level as the start block should be included in the result. + * + * @private + * @type {Boolean} + */ this._sameIndent = !!options.sameIndent; + + /** + * Whether blocks with a smaller indent level then the start block should be included in the result. + * + * @private + * @type {Boolean} + */ this._smallerIndent = !!options.smallerIndent; + + /** + * Whether blocks with a bigger indent level then the start block should be included in the result. + * + * @private + * @type {Boolean} + */ this._biggerIndent = !!options.biggerIndent; } /** - * TODO + * Performs only first step of iteration and returns the result. * - * @param {module:engine/model/element~Element} startElement Starting list item block element. + * @param {module:engine/model/element~Element} startElement The start list item block element. * @param {Object} options - * @param {'forward'|'backward'} [options.direction='backward'] - * @param {Boolean} [options.includeSelf=false] - * @param {Boolean} [options.sameItem=false] - * @param {Boolean} [options.sameIndent=false] - * @param {Boolean} [options.smallerIndent=false] - * @param {Boolean} [options.biggerIndent=false] + * @param {'forward'|'backward'} [options.direction='backward'] The iterating direction. + * @param {Boolean} [options.includeSelf=false] Whether start block should be included in the result (if it's matching other criteria). + * @param {Boolean} [options.sameItemId=false] Whether should return only blocks with the same `listItemId` attribute + * as the start element. + * @param {Boolean} [options.sameIndent=false] Whether blocks with the same indent level as the start block should be included + * in the result. + * @param {Boolean} [options.smallerIndent=false] Whether blocks with a smaller indent level then the start block should be included + * in the result. + * @param {Boolean} [options.biggerIndent=false] Whether blocks with a bigger indent level then the start block should be included + * in the result. * @returns {module:engine/model/element~Element|null} */ static first( startElement, options ) { @@ -55,6 +127,11 @@ export default class ListWalker { return first( iterator ); } + /** + * Iterable interface. + * + * @returns {Iterable.} + */ * [ Symbol.iterator ]() { const nestedItems = []; @@ -82,10 +159,11 @@ export default class ListWalker { continue; } } - // Same indent level item. - else /* if ( indent == this._startIndent ) */ { + // Same indent level block. + else { // Ignore same indent block. if ( !this._sameIndent ) { + // While looking for nested blocks, stop iterating while encountering first same indent block. if ( this._biggerIndent ) { break; } @@ -109,6 +187,12 @@ export default class ListWalker { } } + /** + * Returns the model element to start iterating. + * + * @private + * @returns {module:engine/model/element~Element} + */ _getStartNode() { if ( this._includeSelf ) { return this._startElement; @@ -120,6 +204,7 @@ export default class ListWalker { } } +// Iterates sibling list blocks starting from the given node. function* iterateSiblingListBlocks( node, isForward ) { while ( node && node.hasAttribute( 'listItemId' ) ) { yield node; diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.js b/packages/ckeditor5-list/src/documentlist/utils/model.js index aececec34a3..a40cecd86f7 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.js +++ b/packages/ckeditor5-list/src/documentlist/utils/model.js @@ -37,16 +37,13 @@ export function getAllListItemBlocks( listItem ) { * @param {module:engine/model/element~Element} listItem Starting list item element. * @param {Object} [options] * @param {'forward'|'backward'} [options.direction='backward'] Walking direction. - * @param {Boolean} [options.includeNested=false] Whether nested blocks should be included. * @returns {Array.} */ export function getListItemBlocks( listItem, options = {} ) { const isForward = options.direction == 'forward'; - const includeNested = !!options.includeNested; const items = Array.from( new ListWalker( listItem, { direction: options.direction, - biggerIndent: includeNested, includeSelf: isForward, sameIndent: true, sameItemId: true @@ -78,7 +75,6 @@ export function getNestedListBlocks( listItem ) { */ export function isFirstBlockOfListItem( listBlock ) { const previousSibling = ListWalker.first( listBlock, { - direction: 'backward', sameIndent: true, sameItemId: true } ); @@ -118,17 +114,29 @@ export function isLastBlockOfListItem( listBlock ) { * @param {Array.} blocks The list of selected blocks. */ export function expandListBlocksToCompleteItems( blocks ) { - const firstBlock = blocks[ 0 ]; - const lastBlock = blocks[ blocks.length - 1 ]; + const walkerOptions = { + biggerIndent: true, + sameIndent: true, + sameItemId: true + }; // Add missing blocks of the first selected list item. - blocks.splice( 0, 0, ...getListItemBlocks( firstBlock, { direction: 'backward', includeNested: true } ) ); + const firstBlock = blocks[ 0 ]; + const backwardWalker = new ListWalker( firstBlock, walkerOptions ); + + for ( const block of backwardWalker ) { + blocks.unshift( block ); + } // Add missing blocks of the last selected list item. - for ( const item of getListItemBlocks( lastBlock, { direction: 'forward', includeNested: true } ) ) { - if ( item != lastBlock ) { - blocks.push( item ); - } + const lastBlock = blocks[ blocks.length - 1 ]; + const forwardWalker = new ListWalker( lastBlock, { + ...walkerOptions, + direction: 'forward' + } ); + + for ( const block of forwardWalker ) { + blocks.push( block ); } } diff --git a/packages/ckeditor5-list/tests/documentlist/utils/model.js b/packages/ckeditor5-list/tests/documentlist/utils/model.js index 07deb64971c..f43ae9fd791 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/model.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/model.js @@ -8,7 +8,6 @@ import { getAllListItemBlocks, getListItemBlocks, getNestedListBlocks, - getSiblingListBlock, indentBlocks, isFirstBlockOfListItem, isLastBlockOfListItem, @@ -20,7 +19,7 @@ import Model from '@ckeditor/ckeditor5-engine/src/model/model'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { stringify as stringifyModel, parse as parseModel } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -describe( 'DocumentList - utils', () => { +describe( 'DocumentList - utils - model', () => { let model, schema; testUtils.createSinonSandbox(); @@ -35,132 +34,132 @@ describe( 'DocumentList - utils', () => { } ); // TODO ListWalker - describe.skip( 'getSiblingListBlock()', () => { - it( 'should return the passed element if it matches the criteria (sameIndent, listIndent=0)', () => { - const input = - '0.' + - '1.' + // Starting item, wanted item. - '2.'; - - const fragment = parseModel( input, schema ); - const listItem = fragment.getChild( 1 ); - const foundElement = getSiblingListBlock( listItem, { - sameIndent: true, - listIndent: 0 - } ); - - expect( foundElement ).to.equal( fragment.getChild( 1 ) ); - } ); - - it( 'should return the passed element if it matches the criteria (sameIndent, listIndent=0, direction="forward")', () => { - const input = - '0.' + - '1.' + // Starting item, wanted item. - '2.'; - - const fragment = parseModel( input, schema ); - const listItem = fragment.getChild( 1 ); - const foundElement = getSiblingListBlock( listItem, { - sameIndent: true, - listIndent: 0, - direction: 'forward' - } ); - - expect( foundElement ).to.equal( fragment.getChild( 1 ) ); - } ); - - it( 'should not return the listItem if there is an outdented item before (sameIndent, listIndent=1)', () => { - const input = - '0.' + - '1.' + - '1.1' + - '1.2' + // Wanted item. - '2.' + // Starting item. - '2.1.' + - '2.2.'; - - const fragment = parseModel( input, schema ); - const listItem = fragment.getChild( 5 ); - const foundElement = getSiblingListBlock( listItem.previousSibling, { - sameIndent: true, - listIndent: 1 - } ); - - expect( foundElement ).to.be.null; - } ); - - it( 'should not return the listItem if there is an outdented item before (sameIndent, listIndent=1, direction="forward")', () => { - const input = - '0.' + - '1.' + // Starting item. - '2.' + - '2.1.' + // Wanted item. - '2.2.'; - - const fragment = parseModel( input, schema ); - const listItem = fragment.getChild( 1 ); - const foundElement = getSiblingListBlock( listItem.nextSibling, { - sameIndent: true, - listIndent: 1, - direction: 'forward' - } ); - - expect( foundElement ).to.be.null; - } ); - - it( 'should return the first listItem that matches criteria (smallerIndent, listIndent=1)', () => { - const input = - '0.' + - '1.' + - '2.' + // Wanted item. - '2.1.' + // Starting item. - '2.2.'; - - const fragment = parseModel( input, schema ); - const listItem = fragment.getChild( 4 ); - const foundElement = getSiblingListBlock( listItem, { - smallerIndent: true, - listIndent: 1 - } ); - - expect( foundElement ).to.equal( fragment.getChild( 2 ) ); - } ); - - it( 'should return the first listItem that matches criteria (smallerIndent, listIndent=1, direction="forward")', () => { - const input = - '0.' + - '0.1.' + // Starting item. - '0.2.' + - '0.3.' + - '1.'; // Wanted item. - - const fragment = parseModel( input, schema ); - const listItem = fragment.getChild( 1 ); - const foundElement = getSiblingListBlock( listItem, { - smallerIndent: true, - listIndent: 1, - direction: 'forward' - } ); - - expect( foundElement ).to.equal( fragment.getChild( 4 ) ); - } ); - - it( 'should return null if there were no items matching options', () => { - const input = - 'foo' + - '0.' + - '1.'; - - const fragment = parseModel( input, schema ); - const listItem = fragment.getChild( 1 ); - const foundElement = getSiblingListBlock( listItem, { - smallerIndent: true, - listIndent: 0 - } ); - - expect( foundElement ).to.be.null; - } ); - } ); + // describe( 'getSiblingListBlock()', () => { + // it( 'should return the passed element if it matches the criteria (sameIndent, listIndent=0)', () => { + // const input = + // '0.' + + // '1.' + // Starting item, wanted item. + // '2.'; + // + // const fragment = parseModel( input, schema ); + // const listItem = fragment.getChild( 1 ); + // const foundElement = getSiblingListBlock( listItem, { + // sameIndent: true, + // listIndent: 0 + // } ); + // + // expect( foundElement ).to.equal( fragment.getChild( 1 ) ); + // } ); + // + // it( 'should return the passed element if it matches the criteria (sameIndent, listIndent=0, direction="forward")', () => { + // const input = + // '0.' + + // '1.' + // Starting item, wanted item. + // '2.'; + // + // const fragment = parseModel( input, schema ); + // const listItem = fragment.getChild( 1 ); + // const foundElement = getSiblingListBlock( listItem, { + // sameIndent: true, + // listIndent: 0, + // direction: 'forward' + // } ); + // + // expect( foundElement ).to.equal( fragment.getChild( 1 ) ); + // } ); + // + // it( 'should not return the listItem if there is an outdented item before (sameIndent, listIndent=1)', () => { + // const input = + // '0.' + + // '1.' + + // '1.1' + + // '1.2' + // Wanted item. + // '2.' + // Starting item. + // '2.1.' + + // '2.2.'; + // + // const fragment = parseModel( input, schema ); + // const listItem = fragment.getChild( 5 ); + // const foundElement = getSiblingListBlock( listItem.previousSibling, { + // sameIndent: true, + // listIndent: 1 + // } ); + // + // expect( foundElement ).to.be.null; + // } ); + // + // it( 'should not return the listItem if there is an outdented item before (sameIndent, listIndent=1, direction="forward")', () => { + // const input = + // '0.' + + // '1.' + // Starting item. + // '2.' + + // '2.1.' + // Wanted item. + // '2.2.'; + // + // const fragment = parseModel( input, schema ); + // const listItem = fragment.getChild( 1 ); + // const foundElement = getSiblingListBlock( listItem.nextSibling, { + // sameIndent: true, + // listIndent: 1, + // direction: 'forward' + // } ); + // + // expect( foundElement ).to.be.null; + // } ); + // + // it( 'should return the first listItem that matches criteria (smallerIndent, listIndent=1)', () => { + // const input = + // '0.' + + // '1.' + + // '2.' + // Wanted item. + // '2.1.' + // Starting item. + // '2.2.'; + // + // const fragment = parseModel( input, schema ); + // const listItem = fragment.getChild( 4 ); + // const foundElement = getSiblingListBlock( listItem, { + // smallerIndent: true, + // listIndent: 1 + // } ); + // + // expect( foundElement ).to.equal( fragment.getChild( 2 ) ); + // } ); + // + // it( 'should return the first listItem that matches criteria (smallerIndent, listIndent=1, direction="forward")', () => { + // const input = + // '0.' + + // '0.1.' + // Starting item. + // '0.2.' + + // '0.3.' + + // '1.'; // Wanted item. + // + // const fragment = parseModel( input, schema ); + // const listItem = fragment.getChild( 1 ); + // const foundElement = getSiblingListBlock( listItem, { + // smallerIndent: true, + // listIndent: 1, + // direction: 'forward' + // } ); + // + // expect( foundElement ).to.equal( fragment.getChild( 4 ) ); + // } ); + // + // it( 'should return null if there were no items matching options', () => { + // const input = + // 'foo' + + // '0.' + + // '1.'; + // + // const fragment = parseModel( input, schema ); + // const listItem = fragment.getChild( 1 ); + // const foundElement = getSiblingListBlock( listItem, { + // smallerIndent: true, + // listIndent: 0 + // } ); + // + // expect( foundElement ).to.be.null; + // } ); + // } ); describe( 'getAllListItemBlocks()', () => { it( 'should return a single item if it meets conditions', () => { @@ -404,47 +403,48 @@ describe( 'DocumentList - utils', () => { expect( backwardElements[ 0 ] ).to.equal( fragment.getChild( 2 ) ); } ); - it( 'should include nested blocks if requested', () => { - const input = - 'a' + - 'b' + - 'c' + - 'd' + - 'e'; - - const fragment = parseModel( input, schema ); - const forwardElements = getListItemBlocks( fragment.getChild( 1 ), { direction: 'forward', includeNested: true } ); - const backwardElements = getListItemBlocks( fragment.getChild( 4 ), { direction: 'backward', includeNested: true } ); - - expect( forwardElements.length ).to.equal( 2 ); - expect( forwardElements[ 0 ] ).to.equal( fragment.getChild( 1 ) ); - expect( forwardElements[ 1 ] ).to.equal( fragment.getChild( 2 ) ); - - expect( backwardElements.length ).to.equal( 1 ); - expect( backwardElements[ 0 ] ).to.equal( fragment.getChild( 3 ) ); - } ); - - it( 'should include nested blocks if requested (multi block item with nested item inside)', () => { - const input = - 'a' + - 'b' + - 'c' + - 'd' + - 'e'; - - const fragment = parseModel( input, schema ); - const forwardElements = getListItemBlocks( fragment.getChild( 1 ), { direction: 'forward', includeNested: true } ); - const backwardElements = getListItemBlocks( fragment.getChild( 3 ), { direction: 'backward', includeNested: true } ); - - expect( forwardElements.length ).to.equal( 3 ); - expect( forwardElements[ 0 ] ).to.equal( fragment.getChild( 1 ) ); - expect( forwardElements[ 1 ] ).to.equal( fragment.getChild( 2 ) ); - expect( forwardElements[ 2 ] ).to.equal( fragment.getChild( 3 ) ); - - expect( backwardElements.length ).to.equal( 2 ); - expect( backwardElements[ 0 ] ).to.equal( fragment.getChild( 1 ) ); - expect( backwardElements[ 1 ] ).to.equal( fragment.getChild( 2 ) ); - } ); + // TODO ListWalker + // it( 'should include nested blocks if requested', () => { + // const input = + // 'a' + + // 'b' + + // 'c' + + // 'd' + + // 'e'; + // + // const fragment = parseModel( input, schema ); + // const forwardElements = getListItemBlocks( fragment.getChild( 1 ), { direction: 'forward', includeNested: true } ); + // const backwardElements = getListItemBlocks( fragment.getChild( 4 ), { direction: 'backward', includeNested: true } ); + // + // expect( forwardElements.length ).to.equal( 2 ); + // expect( forwardElements[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + // expect( forwardElements[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + // + // expect( backwardElements.length ).to.equal( 1 ); + // expect( backwardElements[ 0 ] ).to.equal( fragment.getChild( 3 ) ); + // } ); + // + // it( 'should include nested blocks if requested (multi block item with nested item inside)', () => { + // const input = + // 'a' + + // 'b' + + // 'c' + + // 'd' + + // 'e'; + // + // const fragment = parseModel( input, schema ); + // const forwardElements = getListItemBlocks( fragment.getChild( 1 ), { direction: 'forward', includeNested: true } ); + // const backwardElements = getListItemBlocks( fragment.getChild( 3 ), { direction: 'backward', includeNested: true } ); + // + // expect( forwardElements.length ).to.equal( 3 ); + // expect( forwardElements[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + // expect( forwardElements[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + // expect( forwardElements[ 2 ] ).to.equal( fragment.getChild( 3 ) ); + // + // expect( backwardElements.length ).to.equal( 2 ); + // expect( backwardElements[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + // expect( backwardElements[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + // } ); } ); describe( 'getNestedListBlocks()', () => { diff --git a/packages/ckeditor5-list/tests/documentlist/utils/postfixers.js b/packages/ckeditor5-list/tests/documentlist/utils/postfixers.js index 4cc9d3ffe43..bfd4ca16479 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/postfixers.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/postfixers.js @@ -14,7 +14,7 @@ import Model from '@ckeditor/ckeditor5-engine/src/model/model'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { stringify as stringifyModel, parse as parseModel } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -describe( 'DocumentList - utils', () => { +describe( 'DocumentList - utils - postfixers', () => { let model, schema; testUtils.createSinonSandbox(); diff --git a/packages/ckeditor5-list/tests/documentlist/utils/view.js b/packages/ckeditor5-list/tests/documentlist/utils/view.js index fc3315611f4..8fb43eccacb 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/view.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/view.js @@ -19,7 +19,7 @@ import Document from '@ckeditor/ckeditor5-engine/src/view/document'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { parse as parseView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; -describe( 'DocumentList - utils', () => { +describe( 'DocumentList - utils - view', () => { let viewUpcastWriter, viewDowncastWriter; testUtils.createSinonSandbox(); From 5098f12cbc146e49c57458444fde2576efe18fb1 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Sat, 18 Dec 2021 15:00:49 +0100 Subject: [PATCH 14/66] Added tests. --- .../src/documentlist/utils/listwalker.js | 17 +- .../tests/documentlist/utils/listwalker.js | 791 ++++++++++++++++++ .../tests/documentlist/utils/model.js | 171 ---- 3 files changed, 804 insertions(+), 175 deletions(-) create mode 100644 packages/ckeditor5-list/tests/documentlist/utils/listwalker.js diff --git a/packages/ckeditor5-list/src/documentlist/utils/listwalker.js b/packages/ckeditor5-list/src/documentlist/utils/listwalker.js index a625787bc8f..89e7fb305fd 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/listwalker.js +++ b/packages/ckeditor5-list/src/documentlist/utils/listwalker.js @@ -39,12 +39,12 @@ export default class ListWalker { this._startElement = startElement; /** - * The indent of the start block. + * The reference indent. Initialized by the indent of the start block. * * @private * @type {Number} */ - this._startIndent = startElement.getAttribute( 'listIndent' ); + this._referenceIndent = startElement.getAttribute( 'listIndent' ); /** * The `listItemId` of the start block. @@ -139,14 +139,17 @@ export default class ListWalker { const indent = node.getAttribute( 'listIndent' ); // Leaving a nested list. - if ( indent < this._startIndent ) { + if ( indent < this._referenceIndent ) { // Abort searching blocks. if ( !this._smallerIndent ) { break; } + + // While searching for smaller indents, update the reference indent to find another parent in the next step. + this._referenceIndent = indent; } // Entering a nested list. - else if ( indent > this._startIndent ) { + else if ( indent > this._referenceIndent ) { // Ignore nested blocks. if ( !this._biggerIndent ) { continue; @@ -165,6 +168,12 @@ export default class ListWalker { if ( !this._sameIndent ) { // While looking for nested blocks, stop iterating while encountering first same indent block. if ( this._biggerIndent ) { + // No more nested blocks so yield nested items. + if ( nestedItems.length ) { + yield* nestedItems; + nestedItems.length = 0; + } + break; } diff --git a/packages/ckeditor5-list/tests/documentlist/utils/listwalker.js b/packages/ckeditor5-list/tests/documentlist/utils/listwalker.js new file mode 100644 index 00000000000..e53fc0eb51a --- /dev/null +++ b/packages/ckeditor5-list/tests/documentlist/utils/listwalker.js @@ -0,0 +1,791 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import ListWalker from '../../../src/documentlist/utils/listwalker'; + +import Model from '@ckeditor/ckeditor5-engine/src/model/model'; +import { parse as parseModel } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +describe( 'DocumentList - utils - ListWalker', () => { + let model, schema; + + beforeEach( () => { + model = new Model(); + schema = model.schema; + + schema.register( 'paragraph', { inheritAllFrom: '$block' } ); + schema.register( 'blockQuote', { inheritAllFrom: '$container' } ); + schema.extend( '$container', { allowAttributes: [ 'listType', 'listIndent', 'listItemId' ] } ); + } ); + + it( 'should return no blocks (sameIndent = false, smallerIndent = false, biggerIndent = false)', () => { + const input = + '0' + + '1' + + '2'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 0 ), { + direction: 'forward', + includeSelf: true + // sameIndent: false -> default + // smallerIndent: false -> default + // biggerIndent: false -> default + + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 0 ); + } ); + + describe( 'same level iterating (sameIndent = true)', () => { + it( 'should iterate on nodes with `listItemId` attribute', () => { + const input = + '0' + + '1' + + '2'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 0 ), { + direction: 'forward', + sameIndent: true, + includeSelf: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 3 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 0 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 1 ) ); + expect( blocks[ 2 ] ).to.equal( fragment.getChild( 2 ) ); + } ); + + it( 'should stop iterating on first node without `listItemId` attribute', () => { + const input = + '0' + + '1' + + '2'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 0 ), { + direction: 'forward', + sameIndent: true, + includeSelf: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 2 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 0 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 1 ) ); + } ); + + it( 'should not iterate over nodes without `listItemId` attribute', () => { + const input = + 'x' + + '0' + + '1'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 0 ), { + direction: 'forward', + sameIndent: true, + includeSelf: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 0 ); + } ); + + it( 'should skip start block (includeSelf = false, direction = forward)', () => { + const input = + '0' + + '1' + + '2'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 0 ), { + direction: 'forward', + sameIndent: true + // includeSelf: false -> default + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 2 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + } ); + + it( 'should skip start block (includeSelf = false, direction = backward)', () => { + const input = + '0' + + '1' + + '2'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 2 ), { + direction: 'backward', + sameIndent: true + // includeSelf: false -> default + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 2 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 0 ) ); + } ); + + it( 'should return items with the same ID', () => { + const input = + '0' + + '1' + + '2'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 0 ), { + direction: 'forward', + sameIndent: true, + includeSelf: true, + sameItemId: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 2 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 0 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 1 ) ); + } ); + + it( 'should return items while iterating over a nested list', () => { + const input = + '0' + + '1' + + '2' + + '3'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 1 ), { + direction: 'forward', + sameIndent: true, + includeSelf: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 2 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + } ); + + it( 'should skip nested items (biggerIndent = false)', () => { + const input = + '0' + + '1' + + '2' + + '3'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 0 ), { + direction: 'forward', + sameIndent: true, + includeSelf: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 2 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 0 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 3 ) ); + } ); + + it( 'should include nested items (biggerIndent = true)', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 1 ), { + direction: 'forward', + sameIndent: true, + biggerIndent: true, + includeSelf: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 4 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + expect( blocks[ 2 ] ).to.equal( fragment.getChild( 3 ) ); + expect( blocks[ 3 ] ).to.equal( fragment.getChild( 4 ) ); + } ); + + it( 'should include nested items (biggerIndent = true, sameItemId = true, forward)', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 1 ), { + direction: 'forward', + sameIndent: true, + biggerIndent: true, + includeSelf: true, + sameItemId: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 4 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + expect( blocks[ 2 ] ).to.equal( fragment.getChild( 3 ) ); + expect( blocks[ 3 ] ).to.equal( fragment.getChild( 4 ) ); + } ); + + it( 'should include nested items (biggerIndent = true, sameItemId = true, backward)', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 4 ), { + direction: 'backward', + sameIndent: true, + biggerIndent: true, + includeSelf: true, + sameItemId: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 4 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 4 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 3 ) ); + expect( blocks[ 2 ] ).to.equal( fragment.getChild( 2 ) ); + expect( blocks[ 3 ] ).to.equal( fragment.getChild( 1 ) ); + } ); + + it( 'should not include nested items from other item (biggerIndent = true, sameItemId = true, backward)', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 4 ), { + direction: 'backward', + sameIndent: true, + biggerIndent: true, + includeSelf: true, + sameItemId: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 1 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 4 ) ); + } ); + + it( 'should return all list blocks (biggerIndent = true, sameIndent = true, smallerIndent = true)', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 1 ), { + direction: 'forward', + sameIndent: true, + smallerIndent: true, + biggerIndent: true, + includeSelf: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 5 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + expect( blocks[ 2 ] ).to.equal( fragment.getChild( 3 ) ); + expect( blocks[ 3 ] ).to.equal( fragment.getChild( 4 ) ); + expect( blocks[ 4 ] ).to.equal( fragment.getChild( 5 ) ); + } ); + + describe( 'first()', () => { + it( 'should return first sibling block', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5'; + + const fragment = parseModel( input, schema ); + const block = ListWalker.first( fragment.getChild( 2 ), { + direction: 'forward', + sameIndent: true + } ); + + expect( block ).to.equal( fragment.getChild( 3 ) ); + } ); + + it( 'should return first block on the same indent level (forward)', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5'; + + const fragment = parseModel( input, schema ); + const block = ListWalker.first( fragment.getChild( 1 ), { + direction: 'forward', + sameIndent: true + } ); + + expect( block ).to.equal( fragment.getChild( 4 ) ); + } ); + + it( 'should return first block on the same indent level (backward)', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5'; + + const fragment = parseModel( input, schema ); + const block = ListWalker.first( fragment.getChild( 4 ), { + direction: 'backward', + sameIndent: true + } ); + + expect( block ).to.equal( fragment.getChild( 1 ) ); + } ); + } ); + } ); + + describe( 'nested level iterating (biggerIndent = true )', () => { + it( 'should return nested list blocks (biggerIndent = true, sameIndent = false)', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 1 ), { + direction: 'forward', + biggerIndent: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 2 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 2 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 3 ) ); + } ); + + it( 'should return all nested blocks (biggerIndent = true, sameIndent = false)', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 0 ), { + direction: 'forward', + biggerIndent: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 4 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + expect( blocks[ 2 ] ).to.equal( fragment.getChild( 3 ) ); + expect( blocks[ 3 ] ).to.equal( fragment.getChild( 4 ) ); + } ); + + it( 'should return all nested blocks (biggerIndent = true, sameIndent = false, backward)', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 5 ), { + direction: 'backward', + biggerIndent: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 4 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 4 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 3 ) ); + expect( blocks[ 2 ] ).to.equal( fragment.getChild( 2 ) ); + expect( blocks[ 3 ] ).to.equal( fragment.getChild( 1 ) ); + } ); + + it( 'should return nested blocks next to the start element', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 0 ), { + direction: 'forward', + biggerIndent: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 2 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + } ); + + it( 'should return nested blocks next to the start element (backward)', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 5 ), { + direction: 'backward', + biggerIndent: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 2 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 4 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 3 ) ); + } ); + + it( 'should return nothing there is no nested sibling', () => { + const input = + '0' + + '1' + + '2' + + '3'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 1 ), { + direction: 'forward', + biggerIndent: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 0 ); + } ); + + it( 'should return nothing there is no nested sibling (backward)', () => { + const input = + '0' + + '1' + + '2' + + '3'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 2 ), { + direction: 'backward', + biggerIndent: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 0 ); + } ); + + it( 'should return nothing if a the end of nested list', () => { + const input = + '0' + + '1' + + '2' + + '3'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 2 ), { + direction: 'forward', + biggerIndent: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 0 ); + } ); + + it( 'should return nothing if a the start of nested list (backward)', () => { + const input = + '0' + + '1' + + '2' + + '3'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 1 ), { + direction: 'backward', + biggerIndent: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 0 ); + } ); + + describe( 'first()', () => { + it( 'should return nested sibling block', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5'; + + const fragment = parseModel( input, schema ); + const block = ListWalker.first( fragment.getChild( 1 ), { + direction: 'forward', + biggerIndent: true + } ); + + expect( block ).to.equal( fragment.getChild( 2 ) ); + } ); + + it( 'should return nested sibling block (backward)', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5'; + + const fragment = parseModel( input, schema ); + const block = ListWalker.first( fragment.getChild( 4 ), { + direction: 'backward', + biggerIndent: true + } ); + + expect( block ).to.equal( fragment.getChild( 3 ) ); + } ); + } ); + } ); + + describe( 'parent level iterating (smallerIndent = true )', () => { + it( 'should return nothing if at the start of top level list (backward)', () => { + const input = + '0' + + '1' + + '2' + + '3'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 0 ), { + direction: 'backward', + smallerIndent: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 0 ); + } ); + + it( 'should return nothing if at top level list (backward)', () => { + const input = + '0' + + '1' + + '2' + + '3'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 1 ), { + direction: 'backward', + smallerIndent: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 0 ); + } ); + + it( 'should return nothing if at top level list (forward)', () => { + const input = + '0' + + '1' + + '2' + + '3'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 1 ), { + direction: 'forward', + smallerIndent: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 0 ); + } ); + + it( 'should return parent block if at the first block of nested list (backward)', () => { + const input = + '0' + + '1' + + '2' + + '3'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 1 ), { + direction: 'backward', + smallerIndent: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 1 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 0 ) ); + } ); + + it( 'should return parent block if at the following block of nested list (backward)', () => { + const input = + '0' + + '1' + + '2' + + '3'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 2 ), { + direction: 'backward', + smallerIndent: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 1 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 0 ) ); + } ); + + it( 'should return parent block even when there is a nested list (backward)', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 4 ), { + direction: 'backward', + smallerIndent: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 1 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 0 ) ); + } ); + + it( 'should return parent block even when there is a nested list (forward)', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 1 ), { + direction: 'forward', + smallerIndent: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 1 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 5 ) ); + } ); + + it( 'should return parent blocks (backward)', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 4 ), { + direction: 'backward', + smallerIndent: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 3 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 3 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + expect( blocks[ 2 ] ).to.equal( fragment.getChild( 1 ) ); + } ); + + it( 'should return parent blocks (forward)', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6'; + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 3 ), { + direction: 'forward', + smallerIndent: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 2 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 4 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 6 ) ); + } ); + + describe( 'first()', () => { + it( 'should return nested sibling block', () => { + const input = + '0' + + '1' + + '2' + + '3' + + '4' + + '5'; + + const fragment = parseModel( input, schema ); + const block = ListWalker.first( fragment.getChild( 4 ), { + direction: 'backward', + smallerIndent: true + } ); + + expect( block ).to.equal( fragment.getChild( 1 ) ); + } ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-list/tests/documentlist/utils/model.js b/packages/ckeditor5-list/tests/documentlist/utils/model.js index f43ae9fd791..8718c8fc0a9 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/model.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/model.js @@ -33,134 +33,6 @@ describe( 'DocumentList - utils - model', () => { schema.extend( '$container', { allowAttributes: [ 'listType', 'listIndent', 'listItemId' ] } ); } ); - // TODO ListWalker - // describe( 'getSiblingListBlock()', () => { - // it( 'should return the passed element if it matches the criteria (sameIndent, listIndent=0)', () => { - // const input = - // '0.' + - // '1.' + // Starting item, wanted item. - // '2.'; - // - // const fragment = parseModel( input, schema ); - // const listItem = fragment.getChild( 1 ); - // const foundElement = getSiblingListBlock( listItem, { - // sameIndent: true, - // listIndent: 0 - // } ); - // - // expect( foundElement ).to.equal( fragment.getChild( 1 ) ); - // } ); - // - // it( 'should return the passed element if it matches the criteria (sameIndent, listIndent=0, direction="forward")', () => { - // const input = - // '0.' + - // '1.' + // Starting item, wanted item. - // '2.'; - // - // const fragment = parseModel( input, schema ); - // const listItem = fragment.getChild( 1 ); - // const foundElement = getSiblingListBlock( listItem, { - // sameIndent: true, - // listIndent: 0, - // direction: 'forward' - // } ); - // - // expect( foundElement ).to.equal( fragment.getChild( 1 ) ); - // } ); - // - // it( 'should not return the listItem if there is an outdented item before (sameIndent, listIndent=1)', () => { - // const input = - // '0.' + - // '1.' + - // '1.1' + - // '1.2' + // Wanted item. - // '2.' + // Starting item. - // '2.1.' + - // '2.2.'; - // - // const fragment = parseModel( input, schema ); - // const listItem = fragment.getChild( 5 ); - // const foundElement = getSiblingListBlock( listItem.previousSibling, { - // sameIndent: true, - // listIndent: 1 - // } ); - // - // expect( foundElement ).to.be.null; - // } ); - // - // it( 'should not return the listItem if there is an outdented item before (sameIndent, listIndent=1, direction="forward")', () => { - // const input = - // '0.' + - // '1.' + // Starting item. - // '2.' + - // '2.1.' + // Wanted item. - // '2.2.'; - // - // const fragment = parseModel( input, schema ); - // const listItem = fragment.getChild( 1 ); - // const foundElement = getSiblingListBlock( listItem.nextSibling, { - // sameIndent: true, - // listIndent: 1, - // direction: 'forward' - // } ); - // - // expect( foundElement ).to.be.null; - // } ); - // - // it( 'should return the first listItem that matches criteria (smallerIndent, listIndent=1)', () => { - // const input = - // '0.' + - // '1.' + - // '2.' + // Wanted item. - // '2.1.' + // Starting item. - // '2.2.'; - // - // const fragment = parseModel( input, schema ); - // const listItem = fragment.getChild( 4 ); - // const foundElement = getSiblingListBlock( listItem, { - // smallerIndent: true, - // listIndent: 1 - // } ); - // - // expect( foundElement ).to.equal( fragment.getChild( 2 ) ); - // } ); - // - // it( 'should return the first listItem that matches criteria (smallerIndent, listIndent=1, direction="forward")', () => { - // const input = - // '0.' + - // '0.1.' + // Starting item. - // '0.2.' + - // '0.3.' + - // '1.'; // Wanted item. - // - // const fragment = parseModel( input, schema ); - // const listItem = fragment.getChild( 1 ); - // const foundElement = getSiblingListBlock( listItem, { - // smallerIndent: true, - // listIndent: 1, - // direction: 'forward' - // } ); - // - // expect( foundElement ).to.equal( fragment.getChild( 4 ) ); - // } ); - // - // it( 'should return null if there were no items matching options', () => { - // const input = - // 'foo' + - // '0.' + - // '1.'; - // - // const fragment = parseModel( input, schema ); - // const listItem = fragment.getChild( 1 ); - // const foundElement = getSiblingListBlock( listItem, { - // smallerIndent: true, - // listIndent: 0 - // } ); - // - // expect( foundElement ).to.be.null; - // } ); - // } ); - describe( 'getAllListItemBlocks()', () => { it( 'should return a single item if it meets conditions', () => { const input = @@ -402,49 +274,6 @@ describe( 'DocumentList - utils - model', () => { expect( backwardElements.length ).to.equal( 1 ); expect( backwardElements[ 0 ] ).to.equal( fragment.getChild( 2 ) ); } ); - - // TODO ListWalker - // it( 'should include nested blocks if requested', () => { - // const input = - // 'a' + - // 'b' + - // 'c' + - // 'd' + - // 'e'; - // - // const fragment = parseModel( input, schema ); - // const forwardElements = getListItemBlocks( fragment.getChild( 1 ), { direction: 'forward', includeNested: true } ); - // const backwardElements = getListItemBlocks( fragment.getChild( 4 ), { direction: 'backward', includeNested: true } ); - // - // expect( forwardElements.length ).to.equal( 2 ); - // expect( forwardElements[ 0 ] ).to.equal( fragment.getChild( 1 ) ); - // expect( forwardElements[ 1 ] ).to.equal( fragment.getChild( 2 ) ); - // - // expect( backwardElements.length ).to.equal( 1 ); - // expect( backwardElements[ 0 ] ).to.equal( fragment.getChild( 3 ) ); - // } ); - // - // it( 'should include nested blocks if requested (multi block item with nested item inside)', () => { - // const input = - // 'a' + - // 'b' + - // 'c' + - // 'd' + - // 'e'; - // - // const fragment = parseModel( input, schema ); - // const forwardElements = getListItemBlocks( fragment.getChild( 1 ), { direction: 'forward', includeNested: true } ); - // const backwardElements = getListItemBlocks( fragment.getChild( 3 ), { direction: 'backward', includeNested: true } ); - // - // expect( forwardElements.length ).to.equal( 3 ); - // expect( forwardElements[ 0 ] ).to.equal( fragment.getChild( 1 ) ); - // expect( forwardElements[ 1 ] ).to.equal( fragment.getChild( 2 ) ); - // expect( forwardElements[ 2 ] ).to.equal( fragment.getChild( 3 ) ); - // - // expect( backwardElements.length ).to.equal( 2 ); - // expect( backwardElements[ 0 ] ).to.equal( fragment.getChild( 1 ) ); - // expect( backwardElements[ 1 ] ).to.equal( fragment.getChild( 2 ) ); - // } ); } ); describe( 'getNestedListBlocks()', () => { From 81dd56a7552f1fd48be03e9f8420e2fe17022f63 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Sat, 18 Dec 2021 16:05:42 +0100 Subject: [PATCH 15/66] Changed for loops with sibling iterator. --- .../src/documentlist/converters.js | 20 ++++------ .../src/documentlist/documentlistediting.js | 9 ++--- .../src/documentlist/utils/listwalker.js | 19 +++++++-- .../src/documentlist/utils/postfixers.js | 39 ++++++------------- 4 files changed, 38 insertions(+), 49 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/converters.js b/packages/ckeditor5-list/src/documentlist/converters.js index 3d027200423..1200f7485d8 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.js +++ b/packages/ckeditor5-list/src/documentlist/converters.js @@ -15,7 +15,7 @@ import { isListItemView, getViewElementNameForListType } from './utils/view'; -import ListWalker from './utils/listwalker'; +import ListWalker, { iterateSiblingListBlocks } from './utils/listwalker'; import { findAndAddListHeadToMap } from './utils/postfixers'; import { uid } from 'ckeditor5/src/utils'; @@ -161,27 +161,23 @@ export function reconvertItemsOnDataChange( model, editing ) { 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 ) ) { + 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' ) ) { + if ( previous && itemIndent < previous.getAttribute( 'listIndent' ) ) { stack.length = itemIndent + 1; } stack[ itemIndent ] = { - id: item.getAttribute( 'listItemId' ), - type: item.getAttribute( 'listType' ) + id: node.getAttribute( 'listItemId' ), + type: node.getAttribute( 'listType' ) }; - const blocks = getListItemBlocks( item, { direction: 'forward' } ); + const blocks = getListItemBlocks( node, { direction: 'forward' } ); for ( const block of blocks ) { visited.add( block ); diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.js b/packages/ckeditor5-list/src/documentlist/documentlistediting.js index 53908728a3d..3cf74c04c7e 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.js @@ -26,6 +26,7 @@ import { fixListIndents, fixListItemIds } from './utils/postfixers'; +import { iterateSiblingListBlocks } from './utils/listwalker'; /** * The editing part of the document-list feature. It handles creating, editing and removing lists and list items. @@ -242,7 +243,7 @@ function createModelIndentPasteFixer( model ) { // that list will be broken. // Note: we also need to handle singular elements because inserting item with indent 0 into 0,1,[],2 // would create incorrect model. - let item = content.is( 'documentFragment' ) ? content.getChild( 0 ) : content; + const item = content.is( 'documentFragment' ) ? content.getChild( 0 ) : content; if ( !item || !item.hasAttribute( 'listItemId' ) ) { return; @@ -283,10 +284,8 @@ function createModelIndentPasteFixer( model ) { model.change( writer => { // Adjust indent of all "first" list items in inserted data. - while ( item && item.hasAttribute( 'listItemId' ) ) { - writer.setAttribute( 'listIndent', item.getAttribute( 'listIndent' ) + indentChange, item ); - - item = item.nextSibling; + for ( const { node } of iterateSiblingListBlocks( item, 'forward' ) ) { + writer.setAttribute( 'listIndent', node.getAttribute( 'listIndent' ) + indentChange, node ); } } ); }; diff --git a/packages/ckeditor5-list/src/documentlist/utils/listwalker.js b/packages/ckeditor5-list/src/documentlist/utils/listwalker.js index 89e7fb305fd..8dd692e0f2c 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/listwalker.js +++ b/packages/ckeditor5-list/src/documentlist/utils/listwalker.js @@ -135,7 +135,7 @@ export default class ListWalker { * [ Symbol.iterator ]() { const nestedItems = []; - for ( const node of iterateSiblingListBlocks( this._getStartNode(), this._isForward ) ) { + for ( const { node } of iterateSiblingListBlocks( this._getStartNode(), this._isForward ? 'forward' : 'backward' ) ) { const indent = node.getAttribute( 'listIndent' ); // Leaving a nested list. @@ -213,11 +213,22 @@ export default class ListWalker { } } -// Iterates sibling list blocks starting from the given node. -function* iterateSiblingListBlocks( node, isForward ) { +/** + * Iterates sibling list blocks starting from the given node. + * + * @protected + * @param {module:engine/model/node~Node} node The model node. + * @param {'backward'|'forward'} direction Iteration direction. + * @returns {Iterable.} The object with `node` and `previous` {@link module:engine/model/element~Element blocks}. + */ +export function* iterateSiblingListBlocks( node, direction ) { + const isForward = direction == 'forward'; + let previous = null; + while ( node && node.hasAttribute( 'listItemId' ) ) { - yield node; + yield { node, previous }; + previous = node; node = isForward ? node.nextSibling : node.previousSibling; } } diff --git a/packages/ckeditor5-list/src/documentlist/utils/postfixers.js b/packages/ckeditor5-list/src/documentlist/utils/postfixers.js index 6e700d8fdcf..f0bec0fd5f1 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/postfixers.js +++ b/packages/ckeditor5-list/src/documentlist/utils/postfixers.js @@ -7,8 +7,9 @@ * @module list/documentlist/utils/postfixers */ -import { uid } from 'ckeditor5/src/utils'; +import { iterateSiblingListBlocks } from './listwalker'; import { getListItemBlocks } from './model'; +import { uid } from 'ckeditor5/src/utils'; /** * Based on the provided positions looks for the list head and stores it in the provided map. @@ -30,17 +31,7 @@ export function findAndAddListHeadToMap( position, itemToListHead ) { } else { let listHead = previousNode; - if ( itemToListHead.has( listHead ) ) { - return; - } - - for ( - let previousSibling = listHead.previousSibling; - previousSibling && previousSibling.hasAttribute( 'listItemId' ); - previousSibling = listHead.previousSibling - ) { - listHead = previousSibling; - + for ( { node: listHead } of iterateSiblingListBlocks( listHead, 'backward' ) ) { if ( itemToListHead.has( listHead ) ) { return; } @@ -64,12 +55,8 @@ export function fixListIndents( listHead, writer ) { let fixBy = null; let applied = false; - for ( - let item = listHead; - item && item.hasAttribute( 'listItemId' ); - item = item.nextSibling - ) { - const itemIndent = item.getAttribute( 'listIndent' ); + for ( const { node } of iterateSiblingListBlocks( listHead, 'forward' ) ) { + const itemIndent = node.getAttribute( 'listIndent' ); if ( itemIndent > maxIndent ) { let newIndent; @@ -89,7 +76,7 @@ export function fixListIndents( listHead, writer ) { newIndent = prevIndent + 1; } - writer.setAttribute( 'listIndent', newIndent, item ); + writer.setAttribute( 'listIndent', newIndent, node ); applied = true; prevIndent = newIndent; @@ -116,17 +103,13 @@ export function fixListItemIds( listHead, seenIds, writer ) { const visited = new Set(); let applied = false; - for ( - let item = listHead; - item && item.hasAttribute( 'listItemId' ); - item = item.nextSibling - ) { - if ( visited.has( item ) ) { + for ( const { node } of iterateSiblingListBlocks( listHead, 'forward' ) ) { + if ( visited.has( node ) ) { continue; } - let listType = item.getAttribute( 'listType' ); - let listItemId = item.getAttribute( 'listItemId' ); + let listType = node.getAttribute( 'listType' ); + let listItemId = node.getAttribute( 'listItemId' ); // Use a new ID if this one was spot earlier (even in other list). if ( seenIds.has( listItemId ) ) { @@ -135,7 +118,7 @@ export function fixListItemIds( listHead, seenIds, writer ) { seenIds.add( listItemId ); - for ( const block of getListItemBlocks( item, { direction: 'forward' } ) ) { + for ( const block of getListItemBlocks( node, { direction: 'forward' } ) ) { visited.add( block ); // Use a new ID if a block of a bigger list item has different type. From e0266ea39e19f40398b6bf47249b8e2e2cf1dfff Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Sat, 18 Dec 2021 16:17:09 +0100 Subject: [PATCH 16/66] Fixed typos. --- .../src/documentlist/utils/listwalker.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/utils/listwalker.js b/packages/ckeditor5-list/src/documentlist/utils/listwalker.js index 8dd692e0f2c..42acabcac30 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/listwalker.js +++ b/packages/ckeditor5-list/src/documentlist/utils/listwalker.js @@ -24,9 +24,9 @@ export default class ListWalker { * as the start element. * @param {Boolean} [options.sameIndent=false] Whether blocks with the same indent level as the start block should be included * in the result. - * @param {Boolean} [options.smallerIndent=false] Whether blocks with a smaller indent level then the start block should be included + * @param {Boolean} [options.smallerIndent=false] Whether blocks with a smaller indent level than the start block should be included * in the result. - * @param {Boolean} [options.biggerIndent=false] Whether blocks with a bigger indent level then the start block should be included + * @param {Boolean} [options.biggerIndent=false] Whether blocks with a bigger indent level than the start block should be included * in the result. */ constructor( startElement, options ) { @@ -87,7 +87,7 @@ export default class ListWalker { this._sameIndent = !!options.sameIndent; /** - * Whether blocks with a smaller indent level then the start block should be included in the result. + * Whether blocks with a smaller indent level than the start block should be included in the result. * * @private * @type {Boolean} @@ -95,7 +95,7 @@ export default class ListWalker { this._smallerIndent = !!options.smallerIndent; /** - * Whether blocks with a bigger indent level then the start block should be included in the result. + * Whether blocks with a bigger indent level than the start block should be included in the result. * * @private * @type {Boolean} @@ -114,9 +114,9 @@ export default class ListWalker { * as the start element. * @param {Boolean} [options.sameIndent=false] Whether blocks with the same indent level as the start block should be included * in the result. - * @param {Boolean} [options.smallerIndent=false] Whether blocks with a smaller indent level then the start block should be included + * @param {Boolean} [options.smallerIndent=false] Whether blocks with a smaller indent level than the start block should be included * in the result. - * @param {Boolean} [options.biggerIndent=false] Whether blocks with a bigger indent level then the start block should be included + * @param {Boolean} [options.biggerIndent=false] Whether blocks with a bigger indent level than the start block should be included * in the result. * @returns {module:engine/model/element~Element|null} */ From ea4245fba554f33d8a83a1cfb054d32f3dbc10a0 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Sat, 18 Dec 2021 16:25:25 +0100 Subject: [PATCH 17/66] Code refactor. --- .../documentlist/documentlistindentcommand.js | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js index 283d6714eb5..8c64e2cca9e 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js @@ -61,7 +61,7 @@ export default class DocumentListIndentCommand extends Command { model.change( writer => { // Handle selection contained in the single list item and starting in the following blocks. - if ( startsInTheMiddleOfTheOnlyOneSelectedListItem( blocks ) ) { + if ( isOnlyOneListItemSelected( blocks ) && !isFirstBlockOfListItem( blocks[ 0 ] ) ) { // Do nothing while indenting, but split list item on outdent. if ( this._indentBy < 0 ) { splitListItemBefore( blocks[ 0 ], writer ); @@ -111,7 +111,7 @@ export default class DocumentListIndentCommand extends Command { } // Indenting of the following blocks of a list item is not allowed. - if ( startsInTheMiddleOfTheOnlyOneSelectedListItem( blocks ) ) { + if ( isOnlyOneListItemSelected( blocks ) && !isFirstBlockOfListItem( blocks[ 0 ] ) ) { return false; } @@ -145,19 +145,9 @@ function getSelectedListBlocks( selection ) { return blocks; } -// Checks whether the given blocks are related to a single list item and does not include the first block of the list item. -// TODO split into 2 helpers -function startsInTheMiddleOfTheOnlyOneSelectedListItem( blocks ) { - const firstItem = blocks[ 0 ]; +// Checks whether the given blocks are related to a single list item. +function isOnlyOneListItemSelected( blocks ) { + const firstItemId = blocks[ 0 ].getAttribute( 'listItemId' ); - // It's not a middle block; - if ( isFirstBlockOfListItem( firstItem ) ) { - return false; - } - - const firstItemId = firstItem.getAttribute( 'listItemId' ); - const isSingleListItemSelected = !blocks.some( item => item.getAttribute( 'listItemId' ) != firstItemId ); - - // Is only one list item is selected? - return isSingleListItemSelected; + return !blocks.some( item => item.getAttribute( 'listItemId' ) != firstItemId ); } From fdccf87e9118b6a24e62c1370b0133b945907fb7 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 15 Dec 2021 13:26:52 +0100 Subject: [PATCH 18/66] Implemented DocumentListUI. Basic DocumentListCommand. --- .../src/documentlist/documentlistcommand.js | 160 +++++ .../src/documentlist/documentlistediting.js | 5 + .../src/documentlist/documentlistui.js | 40 ++ .../src/documentlist/utils/model.js | 30 + .../tests/documentlist/documentlistcommand.js | 564 ++++++++++++++++++ .../tests/manual/documentlist.js | 1 + 6 files changed, 800 insertions(+) create mode 100644 packages/ckeditor5-list/src/documentlist/documentlistcommand.js create mode 100644 packages/ckeditor5-list/src/documentlist/documentlistui.js create mode 100644 packages/ckeditor5-list/tests/documentlist/documentlistcommand.js diff --git a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js new file mode 100644 index 00000000000..1c364df2741 --- /dev/null +++ b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js @@ -0,0 +1,160 @@ +/** + * @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/documentlistcinnabd + */ + +import { Command } from 'ckeditor5/src/core'; +import { first, uid } from 'ckeditor5/src/utils'; +import { getAllListItemBlocks, getListItemBlocks, getNestedListBlocks, indentBlocks, isFirstBlockOfListItem, mergeListItemBlocksIntoParentListItem } from './utils/model'; + +/** + * The list command. It is used by the {@link TODO document list feature}. + * + * @extends module:core/command~Command + */ +export default class DocumentListCommand extends Command { + /** + * Creates an instance of the command. + * + * @param {module:core/editor/editor~Editor} editor The editor instance. + * @param {'numbered'|'bulleted'} type List type that will be handled by this command. + */ + constructor( editor, type ) { + super( editor ); + + /** + * The type of the list created by the command. + * + * @readonly + * @member {'numbered'|'bulleted'|'todo'} + */ + this.type = type; + + /** + * A flag indicating whether the command is active, which means that the selection starts in a list of the same type. + * + * @observable + * @readonly + * @member {Boolean} #value + */ + } + + /** + * @inheritDoc + */ + refresh() { + this.value = this._getValue(); + this.isEnabled = this._checkEnabled(); + } + + /** + * Executes the list command. + * + * @fires execute + * @param {Object} [options] Command options. + * @param {Boolean} [options.forceValue] If set, it will force the command behavior. If `true`, the command will try to convert the + * selected items and potentially the neighbor elements to the proper list items. If set to `false` it will convert selected elements + * to paragraphs. If not set, the command will toggle selected elements to list items or paragraphs, depending on the selection. + */ + execute( options = {} ) { + const model = this.editor.model; + const document = model.document; + const blocks = Array.from( document.selection.getSelectedBlocks() ) + .filter( block => isListItemOrCanBeListItem( block, model.schema ) ); + + // Whether we are turning off some items. + const turnOff = options.forceValue !== undefined ? !options.forceValue : this.value; + + model.change( writer => { + for ( const block of blocks.reverse() ) { + if ( turnOff ) { + // Blocks in top-level list items simply outdent when turning off. + if ( block.getAttribute( 'listIndent' ) === 0 ) { + console.log( [ block, ...getNestedListBlocks( block ) ].length ); + indentBlocks( [ block, ...getNestedListBlocks( block ) ], -1, writer ); + } else { + mergeListItemBlocksIntoParentListItem( block, writer ); + } + } + // Turning on and the block is not a list item - it should get the full set of necessary attributes. + else if ( !turnOff && !block.hasAttribute( 'listType' ) ) { + writer.setAttributes( { + listType: this.type, + listIndent: 0, + listItemId: uid() + }, block ); + } + // Turning on and the block is already a list items but has different type - change it's type and + // type of it's all siblings that have same indent. + else if ( !turnOff && block.hasAttribute( 'listType' ) && block.getAttribute( 'listType' ) != this.type ) { + writer.setAttributes( { + listType: this.type + }, block ); + } + } + + /** + * Event fired by the {@link #execute} method. + * + * It allows to execute an action after executing the {@link ~ListCommand#execute} method, for example adjusting + * attributes of changed blocks. + * + * @protected + * @event _executeCleanup + */ + this.fire( '_executeCleanup', blocks ); + } ); + } + + /** + * Checks the command's {@link #value}. + * + * @private + * @returns {Boolean} The current value. + */ + _getValue() { + // Check whether closest `listItem` ancestor of the position has a correct type. + const listItem = first( this.editor.model.document.selection.getSelectedBlocks() ); + + return !!listItem && listItem.getAttribute( 'listType' ) == this.type; + } + + /** + * Checks whether the command can be enabled in the current context. + * + * @private + * @returns {Boolean} Whether the command should be enabled. + */ + _checkEnabled() { + // If command value is true it means that we are in list item, so the command should be enabled. + if ( this.value ) { + return true; + } + + const selection = this.editor.model.document.selection; + const schema = this.editor.model.schema; + + const firstBlock = first( selection.getSelectedBlocks() ); + + if ( !firstBlock ) { + return false; + } + + // Otherwise, check if list item can be inserted at the position start. + return isListItemOrCanBeListItem( firstBlock, schema ); + } +} + +// Checks whether the given block can get the `listType` attribute and become a document list item. +// +// @private +// @param {module:engine/model/element~Element} block A block to be tested. +// @param {module:engine/model/schema~Schema} schema The schema of the document. +// @returns {Boolean} +function isListItemOrCanBeListItem( block, schema ) { + return schema.checkAttribute( block, 'listType' ) && !schema.isObject( block ); +} diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.js b/packages/ckeditor5-list/src/documentlist/documentlistediting.js index 3cf74c04c7e..55b79d202e0 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.js @@ -13,6 +13,7 @@ import { Delete } from 'ckeditor5/src/typing'; import { CKEditorError } from 'ckeditor5/src/utils'; import DocumentListIndentCommand from './documentlistindentcommand'; +import DocumentListCommand from './documentlistcommand'; import { listItemDowncastConverter, listItemParagraphDowncastConverter, @@ -69,6 +70,10 @@ export default class DocumentListEditing extends Plugin { allowAttributes: [ 'listType', 'listIndent', 'listItemId' ] } ); + // Register commands for numbered and bulleted list. + editor.commands.add( 'numberedList', new DocumentListCommand( editor, 'numbered' ) ); + editor.commands.add( 'bulletedList', new DocumentListCommand( editor, 'bulleted' ) ); + model.document.registerPostFixer( writer => modelChangePostFixer( model, writer ) ); model.on( 'insertContent', createModelIndentPasteFixer( model ), { priority: 'high' } ); diff --git a/packages/ckeditor5-list/src/documentlist/documentlistui.js b/packages/ckeditor5-list/src/documentlist/documentlistui.js new file mode 100644 index 00000000000..fcae1349038 --- /dev/null +++ b/packages/ckeditor5-list/src/documentlist/documentlistui.js @@ -0,0 +1,40 @@ +/** + * @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/list/listui + */ + +import { createUIComponent } from '../list/utils'; + +import numberedListIcon from '../../theme/icons/numberedlist.svg'; +import bulletedListIcon from '../../theme/icons/bulletedlist.svg'; + +import { Plugin } from 'ckeditor5/src/core'; + +/** + * The document list UI feature. It introduces the `'numberedList'` and `'bulletedList'` buttons. + * + * @extends module:core/plugin~Plugin + */ +export default class DocumentListUI extends Plugin { + /** + * @inheritDoc + */ + static get pluginName() { + return 'DocumentListUI'; + } + + /** + * @inheritDoc + */ + init() { + const t = this.editor.t; + + // Create two buttons and link them with numberedList and bulletedList commands. + createUIComponent( this.editor, 'numberedList', t( 'Numbered List' ), numberedListIcon ); + createUIComponent( this.editor, 'bulletedList', t( 'Bulleted List' ), bulletedListIcon ); + } +} diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.js b/packages/ckeditor5-list/src/documentlist/utils/model.js index a40cecd86f7..2e3e920be1b 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.js +++ b/packages/ckeditor5-list/src/documentlist/utils/model.js @@ -155,6 +155,36 @@ export function splitListItemBefore( listBlock, writer ) { } } +/** + * Splits the list item just before the provided list block. + * + * @protected + * @param {module:engine/model/element~Element} listBlock The list block element. + * @param {module:engine/model/writer~Writer} writer The model writer. + */ +export function mergeListItemBlocksIntoParentListItem( listBlock, writer ) { + const blocks = getAllListItemBlocks( listBlock ); + const firstBlock = blocks[ 0 ]; + const parentListItem = firstBlock.previousSibling; + + // TODO remove paranoid check that should not be necessary. + if ( !parentListItem || !parentListItem.hasAttribute( 'listItemId' ) ) { + throw 'Cannot merge when there is nothing to merge into.'; + } + + const parentListAttributes = {}; + + for ( const attributeKey of parentListItem.getAttributeKeys() ) { + if ( attributeKey.startsWith( 'list' ) ) { + parentListAttributes[ attributeKey ] = parentListItem.getAttribute( attributeKey ); + } + } + + for ( const block of blocks ) { + writer.setAttributes( parentListAttributes, block ); + } +} + /** * Updates indentation of given list blocks. * diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js new file mode 100644 index 00000000000..0d688d32f19 --- /dev/null +++ b/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js @@ -0,0 +1,564 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import Editor from '@ckeditor/ckeditor5-core/src/editor/editor'; +import Model from '@ckeditor/ckeditor5-engine/src/model/model'; +import DocumentListCommand from '../../src/documentlist/documentlistcommand'; + +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import stubUid from './_utils/uid'; + +describe.only( 'DocumentListCommand', () => { + let editor, command, model, doc, root; + + testUtils.createSinonSandbox(); + + beforeEach( () => { + editor = new Editor(); + editor.model = new Model(); + + model = editor.model; + doc = model.document; + root = doc.createRoot(); + + command = new DocumentListCommand( editor, 'bulleted' ); + + // TODO: I don't like it but OTOH I don't want DocumentListEditing here because it introduces + // post-fixers and I'd rather see how the command works on its own. + model.schema.extend( '$container', { + allowAttributes: [ 'listType', 'listIndent', 'listItemId' ] + } ); + + model.schema.register( 'paragraph', { + inheritAllFrom: '$block', + allowIn: 'widget' + } ); + + model.schema.register( 'widget', { inheritAllFrom: '$block' } ); + + setData( + model, + 'foo' + + 'bulleted' + + 'numbered' + + 'bar' + + '' + + 'xyz' + + '' + ); + + model.change( writer => { + writer.setSelection( doc.getRoot().getChild( 0 ), 0 ); + } ); + + stubUid(); + } ); + + afterEach( () => { + command.destroy(); + } ); + + describe( 'DocumentListCommand', () => { + describe( 'constructor()', () => { + it( 'should create list command with given type and value set to false', () => { + expect( command.type ).to.equal( 'bulleted' ); + expect( command.value ).to.be.false; + + const numberedList = new DocumentListCommand( editor, 'numbered' ); + expect( numberedList.type ).to.equal( 'numbered' ); + } ); + } ); + + describe( 'value', () => { + it( 'should be false if first position in selection is not in a list item', () => { + model.change( writer => { + writer.setSelection( doc.getRoot().getChild( 3 ), 0 ); + } ); + + expect( command.value ).to.be.false; + } ); + + it( 'should be false if first position in selection is in a list item of different type', () => { + model.change( writer => { + writer.setSelection( doc.getRoot().getChild( 2 ), 0 ); + } ); + + expect( command.value ).to.be.false; + } ); + + it( 'should be true if first position in selection is in a list item of same type', () => { + model.change( writer => { + writer.setSelection( doc.getRoot().getChild( 1 ), 0 ); + } ); + + expect( command.value ).to.be.true; + } ); + } ); + + describe( 'isEnabled', () => { + it( 'should be true if entire selection is in a list', () => { + setData( model, '[a]' ); + expect( command.isEnabled ).to.be.true; + } ); + + it( 'should be true if entire selection is in a block which can be turned into a list', () => { + setData( model, '[a]' ); + expect( command.isEnabled ).to.be.true; + } ); + + it( 'should be true if selection first position is in a block which can be turned into a list', () => { + setData( model, '[ab]' ); + expect( command.isEnabled ).to.be.true; + } ); + + it( 'should be false if selection first position is in an element which cannot be converted to a list item', () => { + // Disallow document lists in widgets. + model.schema.addAttributeCheck( ( ctx, attributeName ) => { + if ( ctx.endsWith( 'widget paragraph' ) && attributeName === 'listType' ) { + return false; + } + } ); + + setData( model, '[ab]' ); + expect( command.isEnabled ).to.be.false; + } ); + + it( 'should be false in a root which does not allow blocks at all', () => { + doc.createRoot( 'paragraph', 'inlineOnlyRoot' ); + setData( model, 'a[]b', { rootName: 'inlineOnlyRoot' } ); + expect( command.isEnabled ).to.be.false; + } ); + } ); + + describe( 'execute()', () => { + it( 'should use parent batch', () => { + model.change( writer => { + expect( writer.batch.operations.length ).to.equal( 0 ); + + command.execute(); + + expect( writer.batch.operations.length ).to.be.above( 0 ); + } ); + } ); + + describe( 'options.forceValue', () => { + it( 'should force converting into the list if the `options.forceValue` is set to `true`', () => { + setData( model, 'fo[]o' ); + + command.execute( { forceValue: true } ); + + expect( getData( model ) ).to.equal( + 'fo[]o' ); + + command.execute( { forceValue: true } ); + + expect( getData( model ) ).to.equal( + 'fo[]o' ); + } ); + + it( 'should force converting into the paragraph if the `options.forceValue` is set to `false`', () => { + setData( model, 'fo[]o' ); + + command.execute( { forceValue: false } ); + + expect( getData( model ) ).to.equal( 'fo[]o' ); + + command.execute( { forceValue: false } ); + + expect( getData( model ) ).to.equal( 'fo[]o' ); + } ); + } ); + + describe( 'collapsed selection', () => { + describe( 'when turning on', () => { + it( 'should turn the closest block into a list item', () => { + setData( model, 'fo[]o' ); + + command.execute(); + + expect( getData( model ) ).to.equal( + 'fo[]o' + ); + } ); + + it( 'should change the type of an existing (closest) list item', () => { + setData( model, 'fo[]o' ); + + command.execute(); + + expect( getData( model ) ).to.equal( + 'fo[]o' + ); + } ); + } ); + + describe.only( 'when turning off', () => { + it( 'should strip the list attributes from the closest list item (single list item)', () => { + setData( model, 'fo[]o' ); + + command.execute(); + + expect( getData( model ) ).to.equal( 'fo[]o' ); + } ); + + it( 'should strip the list attributes from the closest item (multiple list items, selection in first item)', () => { + setData( model, + 'f[o]o' + + 'bar' + + 'baz' + ); + + command.execute(); + + expect( getData( model ) ).to.equal( + 'f[o]o' + + 'bar' + + 'baz' + ); + } ); + + it( 'should strip the list attributes from the closest item (multiple list items, selection in the middle item)', () => { + setData( model, + 'foo' + + 'b[a]r' + + 'baz' + ); + + command.execute(); + + expect( getData( model ) ).to.equal( + 'foo' + + 'b[a]r' + + 'baz' + ); + } ); + + it( 'should strip the list attributes from the closest item (multiple list items, selection in the last item)', () => { + setData( model, + 'foo' + + 'bar' + + 'b[a]z' + ); + + command.execute(); + + expect( getData( model ) ).to.equal( + 'foo' + + 'bar' + + 'b[a]z' + ); + } ); + + describe( 'with nested lists inside', () => { + it( 'should strip the list attributes from the closest item and decrease indent of children (selection in the first item)', () => { + setData( model, + 'f[o]o' + + 'bar' + + 'baz' + + 'qux' + ); + + command.execute(); + + expect( getData( model ) ).to.equal( + 'f[o]o' + + 'bar' + + 'baz' + + 'qux' + ); + } ); + + it( 'should strip the list attributes from the closest item and decrease indent of children (selection in the middle item)', () => { + setData( model, + 'foo' + + 'b[a]r' + + 'baz' + + 'qux' + ); + + command.execute(); + + expect( getData( model ) ).to.equal( + 'foo' + + 'b[a]r' + + 'baz' + + 'qux' + ); + } ); + } ); + + describe( 'with blocks inside list items', () => { + it( 'should strip the list attributes from the closest list item and all blocks inside (selection in the first item)', () => { + setData( model, + 'fo[]o' + + 'bar' + + 'baz' + ); + + command.execute(); + + expect( getData( model ) ).to.equal( + 'fo[]o' + + 'bar' + + 'baz' + ); + } ); + + describe( 'with nested list items', () => { + + } ); + } ); + } ); + } ); + + describe.skip( 'non-collapsed selection', () => { + beforeEach( () => { + setData( + model, + '---' + + '---' + + '---' + + '---' + + '---' + + '---' + + '---' + + '---' + ); + } ); + + // https://github.com/ckeditor/ckeditor5-list/issues/62 + it( 'should not rename blocks which cannot become listItems (list item is not allowed in their parent)', () => { + model.schema.register( 'restricted' ); + model.schema.extend( 'restricted', { allowIn: '$root' } ); + + model.schema.register( 'fooBlock', { inheritAllFrom: '$block' } ); + model.schema.extend( 'fooBlock', { allowIn: 'restricted' } ); + + setData( + model, + 'a[bc' + + '' + + 'de]f' + ); + + command.execute(); + + expect( getData( model ) ).to.equal( + 'a[bc' + + '' + + 'de]f' + ); + } ); + + it( 'should not rename blocks which cannot become listItems (block is an object)', () => { + model.schema.register( 'imageBlock', { + isBlock: true, + isObject: true, + allowIn: '$root' + } ); + + setData( + model, + 'a[bc' + + '' + + 'de]f' + ); + + command.execute(); + + expect( getData( model ) ).to.equal( + 'a[bc' + + '' + + 'de]f' + ); + } ); + + it( 'should rename closest block to listItem and set correct attributes', () => { + // From first paragraph to second paragraph. + // Command value=false, we are turning on list items. + model.change( writer => { + writer.setSelection( writer.createRange( + writer.createPositionAt( root.getChild( 2 ), 0 ), + writer.createPositionAt( root.getChild( 3 ), 'end' ) + ) ); + } ); + + command.execute(); + + const expectedData = + '---' + + '---' + + '[---' + + '---]' + + '---' + + '---' + + '---' + + '---'; + + expect( getData( model ) ).to.equal( expectedData ); + } ); + + it( 'should rename closest listItem to paragraph', () => { + // From second bullet list item to first numbered list item. + // Command value=true, we are turning off list items. + model.change( writer => { + writer.setSelection( writer.createRange( + writer.createPositionAt( root.getChild( 1 ), 0 ), + writer.createPositionAt( root.getChild( 4 ), 'end' ) + ) ); + } ); + + // Convert paragraphs, leave numbered list items. + command.execute(); + + const expectedData = + '---' + + '[---' + // Attributes will be removed by post fixer. + '---' + + '---' + + '---]' + // Attributes will be removed by post fixer. + '---' + + '---' + + '---'; + + expect( getData( model ) ).to.equal( expectedData ); + } ); + + it( 'should change closest listItem\'s type', () => { + // From first numbered lsit item to third bulleted list item. + model.change( writer => { + writer.setSelection( writer.createRange( + writer.createPositionAt( root.getChild( 4 ), 0 ), + writer.createPositionAt( root.getChild( 6 ), 0 ) + ) ); + } ); + + // Convert paragraphs, leave numbered list items. + command.execute(); + + const expectedData = + '---' + + '---' + + '---' + + '---' + + '[---' + + '---' + + ']---' + + '---'; + + expect( getData( model ) ).to.equal( expectedData ); + } ); + + it( 'should handle outdenting sub-items when list item is turned off', () => { + // From first numbered list item to third bulleted list item. + model.change( writer => { + writer.setSelection( writer.createRange( + writer.createPositionAt( root.getChild( 1 ), 0 ), + writer.createPositionAt( root.getChild( 5 ), 'end' ) + ) ); + } ); + + // Convert paragraphs, leave numbered list items. + command.execute(); + + const expectedData = + '---' + + '[---' + // Attributes will be removed by post fixer. + '---' + + '---' + + '---' + // Attributes will be removed by post fixer. + '---]' + // Attributes will be removed by post fixer. + '---' + + '---'; + + expect( getData( model ) ).to.equal( expectedData ); + } ); + + // Example from docs. + it( 'should change type of all items in nested list if one of items changed', () => { + setData( + model, + '---' + + '---' + + '---' + + '---' + + '---' + + '-[-' + + '---' + + '---' + + '---' + + '-]-' + + '---' + + '---' + + '---' + ); + + // * ------ <-- do not fix, top level item + // * ------ <-- fix, because latter list item of this item's list is changed + // * ------ <-- do not fix, item is not affected (different list) + // * ------ <-- fix, because latter list item of this item's list is changed + // * ------ <-- fix, because latter list item of this item's list is changed + // * ---[-- <-- already in selection + // * ------ <-- already in selection + // * ------ <-- already in selection + // * ------ <-- already in selection, but does not cause other list items to change because is top-level + // * ---]-- <-- already in selection + // * ------ <-- fix, because preceding list item of this item's list is changed + // * ------ <-- do not fix, item is not affected (different list) + // * ------ <-- do not fix, top level item + + command.execute(); + + const expectedData = + '---' + + '---' + + '---' + + '---' + + '---' + + '-[-' + + '---' + + '---' + + '---' + + '-]-' + + '---' + + '---' + + '---'; + + expect( getData( model ) ).to.equal( expectedData ); + } ); + } ); + + it.skip( 'should fire "_executeCleanup" event after finish all operations with all changed items', done => { + setData( model, + 'Foo 1.' + + '[Foo 2.' + + 'Foo 3.]' + + 'Foo 4.' + ); + + command.execute(); + + expect( getData( model ) ).to.equal( + 'Foo 1.' + + '[Foo 2.' + + 'Foo 3.]' + + 'Foo 4.' + ); + + command.on( '_executeCleanup', ( evt, data ) => { + expect( data ).to.deep.equal( [ + root.getChild( 2 ), + root.getChild( 1 ) + ] ); + + done(); + } ); + + command.execute(); + } ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-list/tests/manual/documentlist.js b/packages/ckeditor5-list/tests/manual/documentlist.js index 46d36aac529..09c4eb97365 100644 --- a/packages/ckeditor5-list/tests/manual/documentlist.js +++ b/packages/ckeditor5-list/tests/manual/documentlist.js @@ -50,6 +50,7 @@ ClassicEditor ], toolbar: [ 'sourceEditing', '|', + 'numberedList', 'bulletedList', 'outdent', 'indent', '|', 'heading', '|', 'bold', 'italic', 'link', '|', From a3e32dc3f0819d2eefd331e8acb389a995f6f022 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 21 Dec 2021 13:38:56 +0100 Subject: [PATCH 19/66] Tests --- .../src/documentlist/documentlistcommand.js | 18 ++- .../tests/documentlist/documentlistcommand.js | 112 ++++++++++++++++-- 2 files changed, 115 insertions(+), 15 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js index 1c364df2741..1aec99e7959 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js @@ -9,7 +9,7 @@ import { Command } from 'ckeditor5/src/core'; import { first, uid } from 'ckeditor5/src/utils'; -import { getAllListItemBlocks, getListItemBlocks, getNestedListBlocks, indentBlocks, isFirstBlockOfListItem, mergeListItemBlocksIntoParentListItem } from './utils/model'; +import { getAllListItemBlocks, isLastBlockOfListItem, getListItemBlocks, getNestedListBlocks, indentBlocks, isFirstBlockOfListItem, mergeListItemBlocksIntoParentListItem } from './utils/model'; /** * The list command. It is used by the {@link TODO document list feature}. @@ -74,8 +74,20 @@ export default class DocumentListCommand extends Command { if ( turnOff ) { // Blocks in top-level list items simply outdent when turning off. if ( block.getAttribute( 'listIndent' ) === 0 ) { - console.log( [ block, ...getNestedListBlocks( block ) ].length ); - indentBlocks( [ block, ...getNestedListBlocks( block ) ], -1, writer ); + if ( isFirstBlockOfListItem( block ) ) { + // If the only block in the list item, outdent this block only. + if ( isLastBlockOfListItem( block ) ) { + indentBlocks( [ block, ...getNestedListBlocks( block ) ], -1, writer ); + } + // If the first block in the list item but some follow, outdent them too. + else { + indentBlocks( Array.from( getListItemBlocks( block, { direction: 'forward' } ) ), -1, writer ); + } + } + // If not the first block in the list item, indent all blocks that follow. + else { + indentBlocks( [ block, ...getNestedListBlocks( block ) ], -1, writer ); + } } else { mergeListItemBlocksIntoParentListItem( block, writer ); } diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js index 0d688d32f19..169f88352a4 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js @@ -173,7 +173,7 @@ describe.only( 'DocumentListCommand', () => { } ); describe( 'collapsed selection', () => { - describe( 'when turning on', () => { + describe.only( 'when turning on', () => { it( 'should turn the closest block into a list item', () => { setData( model, 'fo[]o' ); @@ -193,69 +193,139 @@ describe.only( 'DocumentListCommand', () => { 'fo[]o' ); } ); + + describe( 'with blocks inside list items', () => { + it( 'should turn the closest block into a list item (middle block of the list item)', () => { + // * foo + // b[]ar + // baz + setData( model, + 'foo' + + 'b[]ar' + + 'baz' + ); + + command.execute(); + + // * foo + // * b[]ar + // b[]az + expect( getData( model ) ).to.equal( + 'foo' + + 'b[]ar' + + 'baz' + ); + } ); + + it( 'should turn the closest block into a list item (last block of the list item)', () => { + // * foo + // bar + // b[]az + setData( model, + 'foo' + + 'bar' + + 'b[]az' + ); + + command.execute(); + + // * foo + // bar + // * b[]az + expect( getData( model ) ).to.equal( + 'foo' + + 'bar' + + 'b[]az' + ); + } ); + } ); } ); describe.only( 'when turning off', () => { it( 'should strip the list attributes from the closest list item (single list item)', () => { + // * f[]oo setData( model, 'fo[]o' ); command.execute(); + // f[]oo expect( getData( model ) ).to.equal( 'fo[]o' ); } ); it( 'should strip the list attributes from the closest item (multiple list items, selection in first item)', () => { + // * f[]oo + // * bar + // * baz setData( model, - 'f[o]o' + + 'f[]oo' + 'bar' + 'baz' ); command.execute(); + // f[]oo + // * bar + // * baz expect( getData( model ) ).to.equal( - 'f[o]o' + + 'f[]oo' + 'bar' + 'baz' ); } ); it( 'should strip the list attributes from the closest item (multiple list items, selection in the middle item)', () => { + // * foo + // * b[]ar + // * baz setData( model, 'foo' + - 'b[a]r' + + 'b[]ar' + 'baz' ); command.execute(); + // * foo + // b[]ar + // * baz expect( getData( model ) ).to.equal( 'foo' + - 'b[a]r' + + 'b[]ar' + 'baz' ); } ); it( 'should strip the list attributes from the closest item (multiple list items, selection in the last item)', () => { + // * foo + // * bar + // * b[]az setData( model, 'foo' + 'bar' + - 'b[a]z' + 'b[]az' ); command.execute(); + // * foo + // * bar + // b[]az expect( getData( model ) ).to.equal( 'foo' + 'bar' + - 'b[a]z' + 'b[]az' ); } ); describe( 'with nested lists inside', () => { it( 'should strip the list attributes from the closest item and decrease indent of children (selection in the first item)', () => { + // * f[]oo + // * bar + // * baz + // * qux setData( model, - 'f[o]o' + + 'f[]oo' + 'bar' + 'baz' + 'qux' @@ -263,8 +333,12 @@ describe.only( 'DocumentListCommand', () => { command.execute(); + // f[]oo + // * bar + // * baz + // * qux expect( getData( model ) ).to.equal( - 'f[o]o' + + 'f[]oo' + 'bar' + 'baz' + 'qux' @@ -272,18 +346,26 @@ describe.only( 'DocumentListCommand', () => { } ); it( 'should strip the list attributes from the closest item and decrease indent of children (selection in the middle item)', () => { + // * foo + // * b[]ar + // * baz + // * qux setData( model, 'foo' + - 'b[a]r' + + 'b[]ar' + 'baz' + 'qux' ); command.execute(); + // * foo + // b[]ar + // * baz + // * qux expect( getData( model ) ).to.equal( 'foo' + - 'b[a]r' + + 'b[]ar' + 'baz' + 'qux' ); @@ -291,7 +373,10 @@ describe.only( 'DocumentListCommand', () => { } ); describe( 'with blocks inside list items', () => { - it( 'should strip the list attributes from the closest list item and all blocks inside (selection in the first item)', () => { + it( 'should strip the list attributes from the closest list item and all blocks inside (selection in the first block)', () => { + // * fo[]o + // bar + // baz setData( model, 'fo[]o' + 'bar' + @@ -300,6 +385,9 @@ describe.only( 'DocumentListCommand', () => { command.execute(); + // fo[]o + // bar + // baz expect( getData( model ) ).to.equal( 'fo[]o' + 'bar' + From 5b7266577584812f95bcb1e5fb89c067a3f84707 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 21 Dec 2021 17:50:02 +0100 Subject: [PATCH 20/66] Added tests. --- .../documentlist/documentlistindentcommand.js | 475 ++++++++++++++---- 1 file changed, 378 insertions(+), 97 deletions(-) diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js index 65303fce832..f69f8cb7659 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js @@ -4,14 +4,18 @@ */ import DocumentListIndentCommand from '../../src/documentlist/documentlistindentcommand'; +import stubUid from './_utils/uid'; import Editor from '@ckeditor/ckeditor5-core/src/editor/editor'; import Model from '@ckeditor/ckeditor5-engine/src/model/model'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; describe( 'DocumentListIndentCommand', () => { let editor, model, doc, root; + testUtils.createSinonSandbox(); + beforeEach( () => { editor = new Editor(); editor.model = new Model(); @@ -112,6 +116,16 @@ describe( 'DocumentListIndentCommand', () => { expect( command.isEnabled ).to.be.false; } ); + + it( 'should be false if selection starts before a list item', () => { + setData( model, + '[]x' + + '0' + + '1' + ); + + expect( command.isEnabled ).to.be.false; + } ); } ); describe( 'multiple blocks per list item', () => { @@ -179,128 +193,303 @@ describe( 'DocumentListIndentCommand', () => { } ); describe( 'multiple list items selection', () => { - // TODO + it( 'should be true if selection starts in the middle block of list item and spans multiple items', () => { + setData( model, + '0' + + '1' + + '[2' + + '3]' + + '4' + ); + + expect( command.isEnabled ).to.be.true; + } ); } ); } ); } ); describe( 'execute()', () => { - it( 'should use parent batch', () => { - setData( model, - '0' + - '1' + - '2' + - '3' + - '4' + - '[]5' + - '6' - ); + describe( 'single block per list item', () => { + it( 'should use parent batch', () => { + setData( model, + '0' + + '1' + + '2' + + '3' + + '4' + + '[]5' + + '6' + ); - model.change( writer => { - expect( writer.batch.operations.length ).to.equal( 0 ); + model.change( writer => { + expect( writer.batch.operations.length ).to.equal( 0 ); + + command.execute(); + + expect( writer.batch.operations.length ).to.be.above( 0 ); + } ); + } ); + + it( 'should increment indent attribute by 1', () => { + setData( model, + '0' + + '1' + + '2' + + '3' + + '4' + + '[]5' + + '6' + ); command.execute(); - expect( writer.batch.operations.length ).to.be.above( 0 ); + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); } ); - } ); - it( 'should increment indent attribute by 1', () => { - setData( model, - '0' + - '1' + - '2' + - '3' + - '4' + - '[]5' + - '6' - ); + it( 'should increment indent of all sub-items of indented item', () => { + setData( model, + '0' + + '[]1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); - command.execute(); + command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equal( - '0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); - } ); + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); + } ); - it( 'should increment indent of all sub-items of indented item', () => { - setData( model, - '0' + - '[]1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); + it( 'should increment indent of all selected item when multiple items are selected', () => { + setData( model, + '0' + + '[1' + + '2' + + '3]' + + '4' + + '5' + + '6' + ); - command.execute(); + command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equal( - '0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); - } ); + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); + } ); - it( 'should increment indent of all selected item when multiple items are selected', () => { - setData( model, - '0' + - '[1' + - '2' + - '3]' + - '4' + - '5' + - '6' - ); + it( 'should fire "afterExecute" event after finish all operations with all changed items', done => { + setData( model, + '0' + + '[]1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); - command.execute(); + command.on( 'afterExecute', ( evt, data ) => { + expect( data ).to.deep.equal( [ + root.getChild( 1 ), + root.getChild( 2 ), + root.getChild( 3 ), + root.getChild( 4 ), + root.getChild( 5 ) + ] ); - expect( getData( model, { withoutSelection: true } ) ).to.equal( - '0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); + done(); + } ); + + command.execute(); + } ); } ); - it( 'should fire "afterExecute" event after finish all operations with all changed items', done => { - setData( model, - '0' + - '[]1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); + describe( 'multiple blocks per list item', () => { + it( 'should change indent of all blocks of a list item', () => { + setData( model, + '0' + + '[]1' + + '2' + + '3' + + '4' + ); - command.on( 'afterExecute', ( evt, data ) => { - expect( data ).to.deep.equal( [ - root.getChild( 1 ), - root.getChild( 2 ), - root.getChild( 3 ), - root.getChild( 4 ), - root.getChild( 5 ) - ] ); + command.execute(); - done(); + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '0' + + '1' + + '2' + + '3' + + '4' + ); } ); - command.execute(); + it( 'should do nothing if the following block of bigger list item is selected', () => { + setData( model, + '0' + + '1' + + '[]2' + + '3' + + '4' + ); + + command.isEnabled = true; + command.execute(); + + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '0' + + '1' + + '2' + + '3' + + '4' + ); + } ); + + it( 'should increment indent of all sub-items of indented item', () => { + setData( model, + '0' + + '[]1' + + '2' + + '3' + + '4' + + '5' + + '6' + + '6' + ); + + command.execute(); + + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6' + + '6' + ); + } ); + + it( 'should increment indent of all sub-items of indented item (at end of list item)', () => { + setData( model, + '0' + + '[]1' + + '2' + + '3' + + '4' + + '5' + + '6' + + '8' + ); + + command.execute(); + + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6' + + '8' + ); + } ); + + it( 'should increment indent of all selected list items when multiple items are selected partially', () => { + setData( model, + '0' + + '1' + + '[2' + + '3]' + + '4' + + '5' + ); + + command.execute(); + + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '0' + + '1' + + '2' + + '3' + + '4' + + '5' + ); + } ); + + it( 'should not increment indent of items from the following list even if it was selected', () => { + setData( model, + '0' + + '[1' + + '2' + + '3]' + + '4' + ); + + command.execute(); + + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '0' + + '1' + + '2' + + '3' + + '4' + ); + } ); + + it( 'should fire "afterExecute" event after finish all operations with all changed items', done => { + setData( model, + '0' + + '[]1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); + + command.on( 'afterExecute', ( evt, data ) => { + expect( data ).to.deep.equal( [ + root.getChild( 1 ), + root.getChild( 2 ), + root.getChild( 3 ), + root.getChild( 4 ), + root.getChild( 5 ) + ] ); + + done(); + } ); + + command.execute(); + } ); } ); } ); } ); @@ -358,6 +547,36 @@ describe( 'DocumentListIndentCommand', () => { expect( command.isEnabled ).to.be.true; } ); + + it( 'should be false if selection starts before a list', () => { + setData( model, + '[0' + + '1]' + + '2' + ); + + expect( command.isEnabled ).to.be.false; + } ); + + it( 'should be true with selection in the middle block of a list item', () => { + setData( model, + '0' + + '[]1' + + '2' + ); + + expect( command.isEnabled ).to.be.true; + } ); + + it( 'should be true with selection in the last block of a list item', () => { + setData( model, + '0' + + '1' + + '[]2' + ); + + expect( command.isEnabled ).to.be.true; + } ); } ); describe( 'execute()', () => { @@ -456,6 +675,68 @@ describe( 'DocumentListIndentCommand', () => { '6' ); } ); + + it( 'should outdent all blocks of partly selected item when multiple items are selected', () => { + setData( model, + '0' + + '1' + + '[2' + + '3]' + + '4' + + '5' + + '6' + ); + + command.execute(); + + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6' + ); + } ); + + it( 'should split list item if selection is in the following list item block', () => { + setData( model, + '0' + + '[]1' + + '2' + + '3' + ); + + stubUid(); + command.execute(); + + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '0' + + '1' + + '2' + + '3' + ); + } ); + + it( 'should split list item if selection is in the last list item block', () => { + setData( model, + '0' + + '1' + + '[]2' + + '3' + ); + + stubUid(); + command.execute(); + + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '0' + + '1' + + '2' + + '3' + ); + } ); } ); } ); } ); From 1f6e3f6edddcabd64f932b38813b29a443a1e74d Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 21 Dec 2021 19:12:45 +0100 Subject: [PATCH 21/66] Merging sub-list while outdenting. --- .../documentlist/documentlistindentcommand.js | 20 ++++++++++++++++ .../src/documentlist/utils/model.js | 23 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js index 8c64e2cca9e..6af5d288a0b 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js @@ -12,6 +12,7 @@ import { expandListBlocksToCompleteItems, indentBlocks, isFirstBlockOfListItem, + mergeListItemBefore, splitListItemBefore } from './utils/model'; import ListWalker from './utils/listwalker'; @@ -73,6 +74,25 @@ export default class DocumentListIndentCommand extends Command { // Expand the selected blocks to contain the whole list items. expandListBlocksToCompleteItems( blocks ); + // Merge with parent list item while outdenting. + if ( this._indentBy < 0 ) { + const firstBlockIndent = blocks[ 0 ].getAttribute( 'listIndent' ); + + for ( const block of blocks ) { + const blockIndent = block.getAttribute( 'listIndent' ); + + // Don't merge nested lists (those should keep their structure). + // Merge only if there is any parent list item. + if ( blockIndent < 1 || blockIndent > firstBlockIndent ) { + continue; + } + + const parentBlock = ListWalker.first( block, { smallerIndent: true } ); + + mergeListItemBefore( block, parentBlock, writer ); + } + } + // Now just update the attributes of blocks. indentBlocks( blocks, this._indentBy, writer ); diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.js b/packages/ckeditor5-list/src/documentlist/utils/model.js index a40cecd86f7..1c9b8416c30 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.js +++ b/packages/ckeditor5-list/src/documentlist/utils/model.js @@ -155,6 +155,29 @@ export function splitListItemBefore( listBlock, writer ) { } } +/** + * Merges the list item with the parent list item. + * + * @protected + * @param {module:engine/model/element~Element} listBlock The list block element. + * @param {module:engine/model/element~Element} parentBlock The list block element to merge with. + * @param {module:engine/model/writer~Writer} writer The model writer. + */ +export function mergeListItemBefore( listBlock, parentBlock, writer ) { + const attributes = {}; + + for ( const [ key, value ] of parentBlock.getAttributes() ) { + // TODO skip listIndent or exclude blocks from indentation change? + if ( key != 'listIndent' && key.startsWith( 'list' ) ) { + attributes[ key ] = value; + } + } + + for ( const item of getListItemBlocks( listBlock, { direction: 'forward' } ) ) { + writer.setAttributes( attributes, item ); + } +} + /** * Updates indentation of given list blocks. * From 7af112f4c19314e36faac1b012cf102f088cafa7 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 22 Dec 2021 12:32:02 +0100 Subject: [PATCH 22/66] WiP. --- .../documentlist/documentlistindentcommand.js | 10 ++++++++-- .../src/documentlist/utils/model.js | 18 +++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js index 6af5d288a0b..3995bfb259d 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js @@ -74,6 +74,10 @@ export default class DocumentListIndentCommand extends Command { // Expand the selected blocks to contain the whole list items. expandListBlocksToCompleteItems( blocks ); + // The set of all blocks that require indent update. + // Some of those will be handled by merging with parent item. + const blocksToUpdateIndent = new Set( blocks ); + // Merge with parent list item while outdenting. if ( this._indentBy < 0 ) { const firstBlockIndent = blocks[ 0 ].getAttribute( 'listIndent' ); @@ -89,12 +93,14 @@ export default class DocumentListIndentCommand extends Command { const parentBlock = ListWalker.first( block, { smallerIndent: true } ); - mergeListItemBefore( block, parentBlock, writer ); + for ( const updatedBlock of mergeListItemBefore( block, parentBlock, writer ) ) { + blocksToUpdateIndent.delete( updatedBlock ); + } } } // Now just update the attributes of blocks. - indentBlocks( blocks, this._indentBy, writer ); + indentBlocks( blocksToUpdateIndent, this._indentBy, writer ); /** * Event fired by the {@link #execute} method. diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.js b/packages/ckeditor5-list/src/documentlist/utils/model.js index 1c9b8416c30..a9ce269b476 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.js +++ b/packages/ckeditor5-list/src/documentlist/utils/model.js @@ -150,8 +150,8 @@ export function expandListBlocksToCompleteItems( blocks ) { export function splitListItemBefore( listBlock, writer ) { const id = uid(); - for ( const item of getListItemBlocks( listBlock, { direction: 'forward' } ) ) { - writer.setAttribute( 'listItemId', id, item ); + for ( const block of getListItemBlocks( listBlock, { direction: 'forward' } ) ) { + writer.setAttribute( 'listItemId', id, block ); } } @@ -162,27 +162,31 @@ export function splitListItemBefore( listBlock, writer ) { * @param {module:engine/model/element~Element} listBlock The list block element. * @param {module:engine/model/element~Element} parentBlock The list block element to merge with. * @param {module:engine/model/writer~Writer} writer The model writer. + * @returns {Iterable.} The iterable of updated blocks. */ export function mergeListItemBefore( listBlock, parentBlock, writer ) { const attributes = {}; for ( const [ key, value ] of parentBlock.getAttributes() ) { - // TODO skip listIndent or exclude blocks from indentation change? - if ( key != 'listIndent' && key.startsWith( 'list' ) ) { + if ( key.startsWith( 'list' ) ) { attributes[ key ] = value; } } - for ( const item of getListItemBlocks( listBlock, { direction: 'forward' } ) ) { - writer.setAttributes( attributes, item ); + const blocks = new Set( getListItemBlocks( listBlock, { direction: 'forward' } ) ); + + for ( const block of blocks ) { + writer.setAttributes( attributes, block ); } + + return blocks; } /** * Updates indentation of given list blocks. * * @protected - * @param {Array.} blocks The list of selected blocks. + * @param {Iterable.} blocks The iterable of selected blocks. * @param {Number} indentBy The indentation level difference. * @param {module:engine/model/writer~Writer} writer The model writer. */ From 672a1135fb0d037aa1e9634b79f1252f18679ccc Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 22 Dec 2021 19:57:08 +0100 Subject: [PATCH 23/66] Adding test utils. --- .../tests/documentlist/_utils-tests/utils.js | 292 ++++++++++++++++++ .../tests/documentlist/_utils/utils.js | 100 +++++- .../documentlist/documentlistindentcommand.js | 73 ++--- 3 files changed, 428 insertions(+), 37 deletions(-) create mode 100644 packages/ckeditor5-list/tests/documentlist/_utils-tests/utils.js diff --git a/packages/ckeditor5-list/tests/documentlist/_utils-tests/utils.js b/packages/ckeditor5-list/tests/documentlist/_utils-tests/utils.js new file mode 100644 index 00000000000..53b243cb6c6 --- /dev/null +++ b/packages/ckeditor5-list/tests/documentlist/_utils-tests/utils.js @@ -0,0 +1,292 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import Model from '@ckeditor/ckeditor5-engine/src/model/model'; +import { parse as parseModel } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import { modelList, stringifyList } from '../_utils/utils'; + +describe( 'mockList()', () => { + it( 'Single bulleted list item', () => { + expect( modelList( [ + '* foo' + ] ) ).to.equalMarkup( + 'foo' + ); + } ); + + it( 'flat list', () => { + expect( modelList( [ + '* foo', + '* bar' + ] ) ).to.equalMarkup( + 'foo' + + 'bar' + ); + } ); + + it( 'list item after plain paragraph', () => { + expect( modelList( [ + 'foo', + '* bar' + ] ) ).to.equalMarkup( + 'foo' + + 'bar' + ); + } ); + + it( 'should allow leading space in list content', () => { + expect( modelList( [ + '* foo', + ' bar', + '* baz' + ] ) ).to.equalMarkup( + ' foo' + + ' bar' + + ' baz' + ); + } ); + + it( 'list item before plain paragraph', () => { + expect( modelList( [ + '* foo', + 'bar' + ] ) ).to.equalMarkup( + 'foo' + + 'bar' + ); + } ); + + it( 'list item with multiple blocks', () => { + expect( modelList( [ + '* foo', + ' bar', + ' baz' + ] ) ).to.equalMarkup( + 'foo' + + 'bar' + + 'baz' + ); + } ); + + it( 'flat list item with multiple blocks in the first item', () => { + expect( modelList( [ + '* foo', + ' bar', + '* baz' + ] ) ).to.equalMarkup( + 'foo' + + 'bar' + + 'baz' + ); + } ); + + it( 'flat list item with multiple blocks in the last item', () => { + expect( modelList( [ + '* foo', + '* bar', + ' baz' + ] ) ).to.equalMarkup( + 'foo' + + 'bar' + + 'baz' + ); + } ); + + it( 'mixed bulleted with numbered lists', () => { + expect( modelList( [ + '* foo', + '# bar', + '* baz' + ] ) ).to.equalMarkup( + 'foo' + + 'bar' + + 'baz' + ); + } ); + + it( 'numbered lists with blocks', () => { + expect( modelList( [ + '# foo', + '# bar', + ' baz' + ] ) ).to.equalMarkup( + 'foo' + + 'bar' + + 'baz' + ); + } ); + + it( 'list with nested lists', () => { + expect( modelList( [ + '* foo', + ' * bar', + '* baz' + ] ) ).to.equalMarkup( + 'foo' + + 'bar' + + 'baz' + ); + } ); + + it( 'list with nested list inside a single list item', () => { + expect( modelList( [ + '* foo', + ' * bar', + ' baz' + ] ) ).to.equalMarkup( + 'foo' + + 'bar' + + 'baz' + ); + } ); + + it( 'list with deep nested lists', () => { + expect( modelList( [ + '* foo', + ' * bar', + ' * baz' + ] ) ).to.equalMarkup( + 'foo' + + 'bar' + + 'baz' + ); + } ); + + it( 'list with indent drop', () => { + expect( modelList( [ + '* foo', + ' * bar', + ' * baz', + ' * abc', + '* 123' + ] ) ).to.equalMarkup( + 'foo' + + 'bar' + + 'baz' + + 'abc' + + '123' + ); + } ); + + it( 'list with bigger indent drop', () => { + expect( modelList( [ + '* foo', + ' * bar', + ' * baz', + '* abc', + ' * 123' + ] ) ).to.equalMarkup( + 'foo' + + 'bar' + + 'baz' + + 'abc' + + '123' + ); + } ); + + it( 'lists with plain paragraph in the middle', () => { + expect( modelList( [ + '* foo', + ' * bar', + 'baz', + '* abc', + ' * 123' + ] ) ).to.equalMarkup( + 'foo' + + 'bar' + + 'baz' + + 'abc' + + '123' + ); + } ); + + it( 'should not alter selection brackets', () => { + expect( modelList( [ + '* fo[o', + ' * bar', + ' * b]az' + ] ) ).to.equalMarkup( + 'fo[o' + + 'bar' + + 'b]az' + ); + } ); + + it( 'should allow passing custom element', () => { + expect( modelList( [ + '* foo', + '* bar', + '* baz' + ] ) ).to.equalMarkup( + 'foo' + + 'bar' + + 'baz' + ); + } ); + + it( 'should throw when indent is invalid', () => { + expect( () => modelList( [ + '* foo', + ' bar', + ' baz' + ] ) ).to.throw( Error, 'Invalid indent: bar' ); + } ); +} ); + +describe( 'stringifyList()', () => { + let model; + + beforeEach( () => { + model = new Model(); + + model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); + model.schema.extend( '$container', { allowAttributes: [ 'listType', 'listIndent', 'listItemId' ] } ); + } ); + + it( 'flat list', () => { + const input = parseModel( + 'aaa' + + 'bbb', + model.schema + ); + + expect( stringifyList( input ) ).to.equal( [ + '* aaa', + '* bbb' + ].join( '\n' ) ); + } ); + + it( 'flat list with multi-block items', () => { + const input = parseModel( + 'aaa' + + 'bbb' + + 'ccc', + model.schema + ); + + expect( stringifyList( input ) ).to.equal( [ + '* aaa', + ' bbb', + '* ccc' + ].join( '\n' ) ); + } ); + + it( 'nested list with multi-block items', () => { + const input = parseModel( + 'aaa' + + 'bbb' + + 'ccc', + model.schema + ); + + expect( stringifyList( input ) ).to.equal( [ + '* aaa', + ' * bbb', + ' ccc' + ].join( '\n' ) ); + } ); + + // TODO +} ); diff --git a/packages/ckeditor5-list/tests/documentlist/_utils/utils.js b/packages/ckeditor5-list/tests/documentlist/_utils/utils.js index c4dbd649954..a0c3ed66871 100644 --- a/packages/ckeditor5-list/tests/documentlist/_utils/utils.js +++ b/packages/ckeditor5-list/tests/documentlist/_utils/utils.js @@ -3,8 +3,10 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -import { getData as getModelData, parse as parseModel } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import Model from '@ckeditor/ckeditor5-engine/src/model/model'; +import { getData as getModelData, parse as parseModel, stringify as stringifyModel } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; +import ListWalker from '../../../src/documentlist/utils/listwalker'; /** * Sets the editor model according to the specified input string. @@ -200,3 +202,99 @@ export function setupTestHelpers( editor ) { return test; } + +/** + * TODO + * + * @param {Iterable.} lines + * @returns {String} + */ +export function modelList( lines ) { + const items = []; + const stack = []; + + let idx = 0; + let prevIndent = -1; + + for ( const line of lines ) { + const [ , pad, marker, content ] = line.match( /^((?: {2})*(?:([*#]) )?)(.*)/ ); + const listIndent = pad.length / 2 - 1; + + if ( listIndent < 0 ) { + stack.length = 0; + } else if ( prevIndent > listIndent ) { + stack.length = listIndent + 1; + } + + if ( listIndent < 0 ) { + items.push( stringifyElement( content ) ); + } else { + if ( !stack[ listIndent ] && !marker ) { + throw new Error( 'Invalid indent: ' + line ); + } + + if ( !stack[ listIndent ] || marker ) { + stack[ listIndent ] = { + listItemId: String( idx++ ).padStart( 3, '0' ), + listType: marker == '#' ? 'numbered' : 'bulleted' + }; + } + + items.push( stringifyElement( content, { listIndent, ...stack[ listIndent ] } ) ); + } + + prevIndent = listIndent; + } + + return items.join( '' ); +} + +/** + * TODO + * + * @param fragment + * @returns {String} + */ +export function stringifyList( fragment ) { + const model = new Model(); + const lines = []; + + model.change( writer => { + for ( let node = fragment.getChild( 0 ); node; node = node.nextSibling ) { + let pad = ''; + + if ( node.hasAttribute( 'listItemId' ) ) { + const marker = node.getAttribute( 'listType' ) == 'numbered' ? '#' : '*'; + const indentSpaces = ( node.getAttribute( 'listIndent' ) + 1 ) * 2; + const isFollowing = !!ListWalker.first( node, { sameIndent: true, sameItemId: true } ); + + pad = isFollowing ? ' '.repeat( indentSpaces ) : marker.padStart( indentSpaces - 1 ) + ' '; + } + + lines.push( `${ pad }${ stringifyNode( node, writer ) }` ); + } + } ); + + return lines.join( '\n' ); +} + +function stringifyNode( node, writer ) { + const fragment = writer.createDocumentFragment(); + + if ( node.is( 'element', 'paragraph' ) ) { + for ( const child of node.getChildren() ) { + writer.append( child, fragment ); + } + } else { + writer.append( node, fragment ); + } + + return stringifyModel( fragment ); +} + +function stringifyElement( content, attributes = {}, name = 'paragraph' ) { + [ , name, content ] = content.match( /^<([^>]+)>([^<]*)?/ ) || [ null, name, content ]; + attributes = Object.entries( attributes ).map( ( [ key, value ] ) => ` ${ key }="${ value }"` ).join( '' ); + + return `<${ name }${ attributes }>${ content }`; +} diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js index f69f8cb7659..fa1d4b8148f 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js @@ -5,6 +5,7 @@ import DocumentListIndentCommand from '../../src/documentlist/documentlistindentcommand'; import stubUid from './_utils/uid'; +import { modelList } from './_utils/utils'; import Editor from '@ckeditor/ckeditor5-core/src/editor/editor'; import Model from '@ckeditor/ckeditor5-engine/src/model/model'; @@ -222,7 +223,7 @@ describe( 'DocumentListIndentCommand', () => { ); model.change( writer => { - expect( writer.batch.operations.length ).to.equal( 0 ); + expect( writer.batch.operations.length ).to.equalMarkup( 0 ); command.execute(); @@ -231,27 +232,27 @@ describe( 'DocumentListIndentCommand', () => { } ); it( 'should increment indent attribute by 1', () => { - setData( model, - '0' + - '1' + - '2' + - '3' + - '4' + - '[]5' + - '6' - ); + setData( model, modelList( [ + '* 0', + '* 1', + ' * 2', + ' * 3', + ' * 4', + ' * []5', + '* 6' + ] ) ); command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equal( - '0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + ' * 2', + ' * 3', + ' * 4', + ' * []5', + '* 6' + ] ) ); } ); it( 'should increment indent of all sub-items of indented item', () => { @@ -267,7 +268,7 @@ describe( 'DocumentListIndentCommand', () => { command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equal( + expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( '0' + '1' + '2' + @@ -291,7 +292,7 @@ describe( 'DocumentListIndentCommand', () => { command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equal( + expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( '0' + '1' + '2' + @@ -314,7 +315,7 @@ describe( 'DocumentListIndentCommand', () => { ); command.on( 'afterExecute', ( evt, data ) => { - expect( data ).to.deep.equal( [ + expect( data ).to.deep.equalMarkup( [ root.getChild( 1 ), root.getChild( 2 ), root.getChild( 3 ), @@ -341,7 +342,7 @@ describe( 'DocumentListIndentCommand', () => { command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equal( + expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( '0' + '1' + '2' + @@ -362,7 +363,7 @@ describe( 'DocumentListIndentCommand', () => { command.isEnabled = true; command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equal( + expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( '0' + '1' + '2' + @@ -385,7 +386,7 @@ describe( 'DocumentListIndentCommand', () => { command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equal( + expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( '0' + '1' + '2' + @@ -411,7 +412,7 @@ describe( 'DocumentListIndentCommand', () => { command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equal( + expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( '0' + '1' + '2' + @@ -435,7 +436,7 @@ describe( 'DocumentListIndentCommand', () => { command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equal( + expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( '0' + '1' + '2' + @@ -456,7 +457,7 @@ describe( 'DocumentListIndentCommand', () => { command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equal( + expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( '0' + '1' + '2' + @@ -477,7 +478,7 @@ describe( 'DocumentListIndentCommand', () => { ); command.on( 'afterExecute', ( evt, data ) => { - expect( data ).to.deep.equal( [ + expect( data ).to.deep.equalMarkup( [ root.getChild( 1 ), root.getChild( 2 ), root.getChild( 3 ), @@ -593,7 +594,7 @@ describe( 'DocumentListIndentCommand', () => { command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equal( + expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( '0' + '1' + '2' + @@ -617,7 +618,7 @@ describe( 'DocumentListIndentCommand', () => { command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equal( + expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( '0' + '1' + '2' + @@ -641,7 +642,7 @@ describe( 'DocumentListIndentCommand', () => { command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equal( + expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( '0' + '1' + '2' + @@ -665,7 +666,7 @@ describe( 'DocumentListIndentCommand', () => { command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equal( + expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( '0' + '1' + '2' + @@ -689,7 +690,7 @@ describe( 'DocumentListIndentCommand', () => { command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equal( + expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( '0' + '1' + '2' + @@ -711,7 +712,7 @@ describe( 'DocumentListIndentCommand', () => { stubUid(); command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equal( + expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( '0' + '1' + '2' + @@ -730,7 +731,7 @@ describe( 'DocumentListIndentCommand', () => { stubUid(); command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equal( + expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( '0' + '1' + '2' + From 1ce325ab2c7455731aefedc00bf006c1be7fbb28 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 23 Dec 2021 18:53:31 +0100 Subject: [PATCH 24/66] WiP. --- .../src/documentlist/documentlistcommand.js | 141 +-- .../documentlist/documentlistindentcommand.js | 44 +- .../src/documentlist/utils/model.js | 147 ++- .../tests/documentlist/_utils-tests/utils.js | 10 +- .../tests/documentlist/_utils/utils.js | 5 +- .../tests/documentlist/documentlistcommand.js | 6 +- .../documentlist/documentlistindentcommand.js | 859 ++++++++++-------- .../tests/documentlist/utils/model.js | 73 +- 8 files changed, 720 insertions(+), 565 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js index 1aec99e7959..b35a12fa04c 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js @@ -8,8 +8,15 @@ */ import { Command } from 'ckeditor5/src/core'; -import { first, uid } from 'ckeditor5/src/utils'; -import { getAllListItemBlocks, isLastBlockOfListItem, getListItemBlocks, getNestedListBlocks, indentBlocks, isFirstBlockOfListItem, mergeListItemBlocksIntoParentListItem } from './utils/model'; +import { uid } from 'ckeditor5/src/utils'; +import { + indentBlocks, + isFirstBlockOfListItem, + isOnlyOneListItemSelected, + splitListItemBefore, + expandListBlocksToCompleteItems, + getSameIndentBlocks +} from './utils/model'; /** * The list command. It is used by the {@link TODO document list feature}. @@ -64,61 +71,60 @@ export default class DocumentListCommand extends Command { const model = this.editor.model; const document = model.document; const blocks = Array.from( document.selection.getSelectedBlocks() ) - .filter( block => isListItemOrCanBeListItem( block, model.schema ) ); + .filter( block => model.schema.checkAttribute( block, 'listType' ) ); // Whether we are turning off some items. const turnOff = options.forceValue !== undefined ? !options.forceValue : this.value; model.change( writer => { - for ( const block of blocks.reverse() ) { - if ( turnOff ) { - // Blocks in top-level list items simply outdent when turning off. - if ( block.getAttribute( 'listIndent' ) === 0 ) { - if ( isFirstBlockOfListItem( block ) ) { - // If the only block in the list item, outdent this block only. - if ( isLastBlockOfListItem( block ) ) { - indentBlocks( [ block, ...getNestedListBlocks( block ) ], -1, writer ); - } - // If the first block in the list item but some follow, outdent them too. - else { - indentBlocks( Array.from( getListItemBlocks( block, { direction: 'forward' } ) ), -1, writer ); - } - } - // If not the first block in the list item, indent all blocks that follow. - else { - indentBlocks( [ block, ...getNestedListBlocks( block ) ], -1, writer ); + if ( turnOff ) { + // Outdent. + indentBlocks( blocks, -1, { expand: true, alwaysMerge: true }, writer ); + } else { + // Case of selection: + // * a + // * [b + // c] + // Should be treated as only "c" selected to make it: + // * a + // * b + // * c + const completeItemsBlocks = expandListBlocksToCompleteItems( blocks ); + const sameIndentBlocks = getSameIndentBlocks( completeItemsBlocks ); + const originallySelectedBlocks = sameIndentBlocks.filter( block => blocks.includes( block ) ); + + if ( isOnlyOneListItemSelected( originallySelectedBlocks ) && !isFirstBlockOfListItem( originallySelectedBlocks[ 0 ] ) ) { + indentBlocks( originallySelectedBlocks, 1, {}, writer ); + + for ( const block of originallySelectedBlocks.reverse() ) { + splitListItemBefore( block, writer ); + } + } else { + for ( const block of blocks ) { + if ( !block.hasAttribute( 'listType' ) ) { + writer.setAttributes( { + listIndent: 0, + listItemId: uid(), + listType: this.type + }, block ); + } else { + expandListBlocksToCompleteItems( [ block ] ); + writer.setAttribute( 'listType', this.type, block ); } - } else { - mergeListItemBlocksIntoParentListItem( block, writer ); } } - // Turning on and the block is not a list item - it should get the full set of necessary attributes. - else if ( !turnOff && !block.hasAttribute( 'listType' ) ) { - writer.setAttributes( { - listType: this.type, - listIndent: 0, - listItemId: uid() - }, block ); - } - // Turning on and the block is already a list items but has different type - change it's type and - // type of it's all siblings that have same indent. - else if ( !turnOff && block.hasAttribute( 'listType' ) && block.getAttribute( 'listType' ) != this.type ) { - writer.setAttributes( { - listType: this.type - }, block ); - } } /** * Event fired by the {@link #execute} method. * - * It allows to execute an action after executing the {@link ~ListCommand#execute} method, for example adjusting - * attributes of changed blocks. + * It allows to execute an action after executing the {@link ~DocumentListCommand#execute} method, + * for example adjusting attributes of changed list items. * * @protected - * @event _executeCleanup + * @event afterExecute */ - this.fire( '_executeCleanup', blocks ); + this.fire( 'afterExecute', blocks ); } ); } @@ -129,10 +135,25 @@ export default class DocumentListCommand extends Command { * @returns {Boolean} The current value. */ _getValue() { - // Check whether closest `listItem` ancestor of the position has a correct type. - const listItem = first( this.editor.model.document.selection.getSelectedBlocks() ); + const selection = this.editor.model.document.selection; + const blocks = Array.from( selection.getSelectedBlocks() ); - return !!listItem && listItem.getAttribute( 'listType' ) == this.type; + for ( const block of blocks ) { + if ( block.getAttribute( 'listType' ) != this.type ) { + return false; + } + } + + // TODO this is same as in execute + const completeItemsBlocks = expandListBlocksToCompleteItems( blocks ); + const sameIndentBlocks = getSameIndentBlocks( completeItemsBlocks ); + const originallySelectedBlocks = sameIndentBlocks.filter( block => blocks.includes( block ) ); + + if ( isOnlyOneListItemSelected( originallySelectedBlocks ) && !isFirstBlockOfListItem( originallySelectedBlocks[ 0 ] ) ) { + return false; + } + + return true; } /** @@ -142,31 +163,25 @@ export default class DocumentListCommand extends Command { * @returns {Boolean} Whether the command should be enabled. */ _checkEnabled() { + const selection = this.editor.model.document.selection; + const schema = this.editor.model.schema; + const blocks = Array.from( selection.getSelectedBlocks() ); + + if ( !blocks.length ) { + return false; + } + // If command value is true it means that we are in list item, so the command should be enabled. if ( this.value ) { return true; } - const selection = this.editor.model.document.selection; - const schema = this.editor.model.schema; - - const firstBlock = first( selection.getSelectedBlocks() ); - - if ( !firstBlock ) { - return false; + for ( const block of blocks ) { + if ( schema.checkAttribute( block, 'listType' ) ) { + return true; + } } - // Otherwise, check if list item can be inserted at the position start. - return isListItemOrCanBeListItem( firstBlock, schema ); + return false; } } - -// Checks whether the given block can get the `listType` attribute and become a document list item. -// -// @private -// @param {module:engine/model/element~Element} block A block to be tested. -// @param {module:engine/model/schema~Schema} schema The schema of the document. -// @returns {Boolean} -function isListItemOrCanBeListItem( block, schema ) { - return schema.checkAttribute( block, 'listType' ) && !schema.isObject( block ); -} diff --git a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js index 3995bfb259d..460c78ae5eb 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js @@ -12,7 +12,7 @@ import { expandListBlocksToCompleteItems, indentBlocks, isFirstBlockOfListItem, - mergeListItemBefore, + isOnlyOneListItemSelected, splitListItemBefore } from './utils/model'; import ListWalker from './utils/listwalker'; @@ -71,36 +71,8 @@ export default class DocumentListIndentCommand extends Command { return; } - // Expand the selected blocks to contain the whole list items. - expandListBlocksToCompleteItems( blocks ); - - // The set of all blocks that require indent update. - // Some of those will be handled by merging with parent item. - const blocksToUpdateIndent = new Set( blocks ); - - // Merge with parent list item while outdenting. - if ( this._indentBy < 0 ) { - const firstBlockIndent = blocks[ 0 ].getAttribute( 'listIndent' ); - - for ( const block of blocks ) { - const blockIndent = block.getAttribute( 'listIndent' ); - - // Don't merge nested lists (those should keep their structure). - // Merge only if there is any parent list item. - if ( blockIndent < 1 || blockIndent > firstBlockIndent ) { - continue; - } - - const parentBlock = ListWalker.first( block, { smallerIndent: true } ); - - for ( const updatedBlock of mergeListItemBefore( block, parentBlock, writer ) ) { - blocksToUpdateIndent.delete( updatedBlock ); - } - } - } - // Now just update the attributes of blocks. - indentBlocks( blocksToUpdateIndent, this._indentBy, writer ); + const changedBlocks = indentBlocks( blocks, this._indentBy, { expand: true }, writer ); /** * Event fired by the {@link #execute} method. @@ -111,7 +83,7 @@ export default class DocumentListIndentCommand extends Command { * @protected * @event afterExecute */ - this.fire( 'afterExecute', blocks ); + this.fire( 'afterExecute', changedBlocks ); } ); } @@ -123,7 +95,7 @@ export default class DocumentListIndentCommand extends Command { */ _checkEnabled() { // Check whether any of position's ancestor is a list item. - const blocks = getSelectedListBlocks( this.editor.model.document.selection ); + let blocks = getSelectedListBlocks( this.editor.model.document.selection ); let firstBlock = blocks[ 0 ]; // If selection is not in a list item, the command is disabled. @@ -141,7 +113,7 @@ export default class DocumentListIndentCommand extends Command { return false; } - expandListBlocksToCompleteItems( blocks ); + blocks = expandListBlocksToCompleteItems( blocks ); firstBlock = blocks[ 0 ]; // Check if there is any list item before selected items that could become a parent of selected items. @@ -171,9 +143,3 @@ function getSelectedListBlocks( selection ) { return blocks; } -// Checks whether the given blocks are related to a single list item. -function isOnlyOneListItemSelected( blocks ) { - const firstItemId = blocks[ 0 ].getAttribute( 'listItemId' ); - - return !blocks.some( item => item.getAttribute( 'listItemId' ) != firstItemId ); -} diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.js b/packages/ckeditor5-list/src/documentlist/utils/model.js index 30f24ba55ae..4f49d740b65 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.js +++ b/packages/ckeditor5-list/src/documentlist/utils/model.js @@ -17,12 +17,13 @@ import ListWalker from './listwalker'; * * @protected * @param {module:engine/model/element~Element} listItem Starting list item element. + * @param {Object} options TODO * @return {Array.} */ -export function getAllListItemBlocks( listItem ) { +export function getAllListItemBlocks( listItem, options = {} ) { return [ - ...getListItemBlocks( listItem, { direction: 'backward' } ), - ...getListItemBlocks( listItem, { direction: 'forward' } ) + ...getListItemBlocks( listItem, { ...options, direction: 'backward' } ), + ...getListItemBlocks( listItem, { ...options, direction: 'forward' } ) ]; } @@ -37,13 +38,14 @@ export function getAllListItemBlocks( listItem ) { * @param {module:engine/model/element~Element} listItem Starting list item element. * @param {Object} [options] * @param {'forward'|'backward'} [options.direction='backward'] Walking direction. + * TODO all options * @returns {Array.} */ export function getListItemBlocks( listItem, options = {} ) { const isForward = options.direction == 'forward'; const items = Array.from( new ListWalker( listItem, { - direction: options.direction, + ...options, includeSelf: isForward, sameIndent: true, sameItemId: true @@ -112,32 +114,18 @@ export function isLastBlockOfListItem( listBlock ) { * * @protected * @param {Array.} blocks The list of selected blocks. + * @returns {Array.} */ export function expandListBlocksToCompleteItems( blocks ) { - const walkerOptions = { - biggerIndent: true, - sameIndent: true, - sameItemId: true - }; + const allBlocks = new Set(); - // Add missing blocks of the first selected list item. - const firstBlock = blocks[ 0 ]; - const backwardWalker = new ListWalker( firstBlock, walkerOptions ); - - for ( const block of backwardWalker ) { - blocks.unshift( block ); + for ( const block of blocks ) { + for ( const itemBlock of getAllListItemBlocks( block, { biggerIndent: true } ) ) { + allBlocks.add( itemBlock ); + } } - // Add missing blocks of the last selected list item. - const lastBlock = blocks[ blocks.length - 1 ]; - const forwardWalker = new ListWalker( lastBlock, { - ...walkerOptions, - direction: 'forward' - } ); - - for ( const block of forwardWalker ) { - blocks.push( block ); - } + return Array.from( allBlocks.values() ).sort( ( a, b ) => a.index - b.index ); } /** @@ -218,20 +206,113 @@ export function mergeListItemBefore( listBlock, parentBlock, writer ) { * @protected * @param {Iterable.} blocks The iterable of selected blocks. * @param {Number} indentBy The indentation level difference. + * @param {Boolean} expand TODO * @param {module:engine/model/writer~Writer} writer The model writer. */ -export function indentBlocks( blocks, indentBy, writer ) { - for ( const item of blocks ) { - const indent = item.getAttribute( 'listIndent' ) + indentBy; +export function indentBlocks( blocks, indentBy, { expand, alwaysMerge }, writer ) { + // Expand the selected blocks to contain the whole list items. + const allBlocks = expand ? expandListBlocksToCompleteItems( blocks ) : blocks; + const visited = new Set(); + + const referenceIndex = allBlocks.reduce( ( indent, block ) => { + const blockIndent = block.getAttribute( 'listIndent' ); - if ( indent < 0 ) { - for ( const attributeKey of item.getAttributeKeys() ) { + return blockIndent < indent ? blockIndent : indent; + }, Number.POSITIVE_INFINITY ); + + const parentBlocks = new Map(); + + // Collect parent blocks before the list structure gets altered. + if ( indentBy < 0 ) { + for ( const block of allBlocks ) { + parentBlocks.set( block, ListWalker.first( block, { smallerIndent: true } ) ); + } + } + + for ( const block of allBlocks ) { + if ( visited.has( block ) ) { + continue; + } + + visited.add( block ); + + const blockIndent = block.getAttribute( 'listIndent' ) + indentBy; + + if ( blockIndent < 0 ) { + for ( const attributeKey of block.getAttributeKeys() ) { if ( attributeKey.startsWith( 'list' ) ) { - writer.removeAttribute( attributeKey, item ); + writer.removeAttribute( attributeKey, block ); } } - } else { - writer.setAttribute( 'listIndent', indent, item ); + + continue; } + + // Merge with parent list item while outdenting. + if ( indentBy < 0 ) { + const atReferenceIndent = block.getAttribute( 'listIndent' ) == referenceIndex; + + // Merge if the block indent matches reference indent or the block was passed directly with alwaysMerge flag. + if ( atReferenceIndent || alwaysMerge && blocks.includes( block ) ) { + const parentBlock = parentBlocks.get( block ); + + // The parent block could become a non-list block. + if ( parentBlock.hasAttribute( 'listIndent' ) ) { + const parentItemBlocks = getListItemBlocks( parentBlock, { direction: 'forward' } ); + + // Merge with parent only if it wasn't the last item. + // Merge: + // * a + // * b <- outdent + // c + // Don't merge: + // * a + // * b <- outdent + // * c + if ( alwaysMerge || parentItemBlocks.pop().index > block.index ) { + for ( const mergedBlock of mergeListItemBefore( block, parentBlock, writer ) ) { + visited.add( mergedBlock ); + } + + continue; + } + } + } + } + + writer.setAttribute( 'listIndent', blockIndent, block ); } + + return allBlocks; +} + +/** + * Checks whether the given blocks are related to a single list item. + * TODO + */ +export function isOnlyOneListItemSelected( blocks ) { + if ( !blocks.length ) { + return false; + } + + const firstItemId = blocks[ 0 ].getAttribute( 'listItemId' ); + + if ( !firstItemId ) { + return false; + } + + return !blocks.some( item => item.getAttribute( 'listItemId' ) != firstItemId ); +} + +/** + * TODO + */ +export function getSameIndentBlocks( blocks ) { + if ( !blocks.length ) { + return []; + } + + const firstIndent = blocks[ 0 ].getAttribute( 'listIndent' ); + + return blocks.filter( block => block.getAttribute( 'listIndent' ) == firstIndent ); } diff --git a/packages/ckeditor5-list/tests/documentlist/_utils-tests/utils.js b/packages/ckeditor5-list/tests/documentlist/_utils-tests/utils.js index 53b243cb6c6..1d07986e34a 100644 --- a/packages/ckeditor5-list/tests/documentlist/_utils-tests/utils.js +++ b/packages/ckeditor5-list/tests/documentlist/_utils-tests/utils.js @@ -32,7 +32,7 @@ describe( 'mockList()', () => { '* bar' ] ) ).to.equalMarkup( 'foo' + - 'bar' + 'bar' ); } ); @@ -44,7 +44,7 @@ describe( 'mockList()', () => { ] ) ).to.equalMarkup( ' foo' + ' bar' + - ' baz' + ' baz' ); } ); @@ -78,7 +78,7 @@ describe( 'mockList()', () => { ] ) ).to.equalMarkup( 'foo' + 'bar' + - 'baz' + 'baz' ); } ); @@ -197,8 +197,8 @@ describe( 'mockList()', () => { 'foo' + 'bar' + 'baz' + - 'abc' + - '123' + 'abc' + + '123' ); } ); diff --git a/packages/ckeditor5-list/tests/documentlist/_utils/utils.js b/packages/ckeditor5-list/tests/documentlist/_utils/utils.js index a0c3ed66871..5003c0ba75b 100644 --- a/packages/ckeditor5-list/tests/documentlist/_utils/utils.js +++ b/packages/ckeditor5-list/tests/documentlist/_utils/utils.js @@ -213,10 +213,9 @@ export function modelList( lines ) { const items = []; const stack = []; - let idx = 0; let prevIndent = -1; - for ( const line of lines ) { + for ( const [ idx, line ] of lines.entries() ) { const [ , pad, marker, content ] = line.match( /^((?: {2})*(?:([*#]) )?)(.*)/ ); const listIndent = pad.length / 2 - 1; @@ -235,7 +234,7 @@ export function modelList( lines ) { if ( !stack[ listIndent ] || marker ) { stack[ listIndent ] = { - listItemId: String( idx++ ).padStart( 3, '0' ), + listItemId: String( idx ).padStart( 3, '0' ), listType: marker == '#' ? 'numbered' : 'bulleted' }; } diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js index 169f88352a4..e032e023fcd 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js @@ -11,7 +11,7 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import stubUid from './_utils/uid'; -describe.only( 'DocumentListCommand', () => { +describe.skip( 'DocumentListCommand', () => { let editor, command, model, doc, root; testUtils.createSinonSandbox(); @@ -173,7 +173,7 @@ describe.only( 'DocumentListCommand', () => { } ); describe( 'collapsed selection', () => { - describe.only( 'when turning on', () => { + describe( 'when turning on', () => { it( 'should turn the closest block into a list item', () => { setData( model, 'fo[]o' ); @@ -241,7 +241,7 @@ describe.only( 'DocumentListCommand', () => { } ); } ); - describe.only( 'when turning off', () => { + describe( 'when turning off', () => { it( 'should strip the list attributes from the closest list item (single list item)', () => { // * f[]oo setData( model, 'fo[]o' ); diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js index fa1d4b8148f..a00ab001c15 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js @@ -44,86 +44,86 @@ describe( 'DocumentListIndentCommand', () => { describe( 'isEnabled', () => { describe( 'single block per list item', () => { it( 'should be true if selection starts in list item', () => { - setData( model, - '0' + - '1' + - '2' + - '3' + - '4' + - '[]5' + - '6' - ); + setData( model, modelList( [ + '* 0', + '* 1', + ' * 2', + ' * 3', + ' * 4', + ' * []5', + '* 6' + ] ) ); expect( command.isEnabled ).to.be.true; } ); it( 'should be false if selection starts in first list item', () => { - setData( model, - '[]0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); + setData( model, modelList( [ + '* []0', + '* 1', + ' * 2', + ' * 3', + ' * 4', + ' * 5', + '* 6' + ] ) ); expect( command.isEnabled ).to.be.false; } ); it( 'should be false if selection starts in first list item at given indent', () => { - setData( model, - 'a' + - 'b' + - 'c' + - '[]d' + - 'e' - ); + setData( model, modelList( [ + '* 0', + ' * 1', + '* 2', + ' * []3', + ' * 4' + ] ) ); expect( command.isEnabled ).to.be.false; } ); it( 'should be false if selection starts in first list item (different list type)', () => { - setData( model, - 'a' + - 'b' + - 'c' + - 'd' + - '[]e' - ); + setData( model, modelList( [ + '* 0', + ' * 1', + '# 2', + ' * 3', + '* []4' + ] ) ); expect( command.isEnabled ).to.be.false; } ); it( 'should be false if selection is in first list item with different type than previous list', () => { - setData( model, - 'a' + - '[]b' - ); + setData( model, modelList( [ + '* 0', + '# []1' + ] ) ); expect( command.isEnabled ).to.be.false; } ); it( 'should be false if selection starts in a list item that has bigger indent than it\'s previous sibling', () => { - setData( model, - '0' + - '1' + - '[]2' + - '3' + - '4' + - '5' + - '6' - ); + setData( model, modelList( [ + '* 0', + '* 1', + ' * []2', + ' * 3', + ' * 4', + ' * 5', + '* 6' + ] ) ); expect( command.isEnabled ).to.be.false; } ); it( 'should be false if selection starts before a list item', () => { - setData( model, - '[]x' + - '0' + - '1' - ); + setData( model, modelList( [ + '[]0', + '* 1', + '* 2' + ] ) ); expect( command.isEnabled ).to.be.false; } ); @@ -131,77 +131,77 @@ describe( 'DocumentListIndentCommand', () => { describe( 'multiple blocks per list item', () => { it( 'should be true if selection starts in the first block of list item', () => { - setData( model, - '0' + - '[]1' + - '2' + - '3' - ); + setData( model, modelList( [ + '* 0', + '* []1', + ' 2', + ' 3' + ] ) ); expect( command.isEnabled ).to.be.true; } ); it( 'should be false if selection starts in the second block of list item', () => { - setData( model, - '0' + - '1' + - '[]2' + - '3' - ); + setData( model, modelList( [ + '* 0', + '* 1', + ' []2', + ' 3' + ] ) ); expect( command.isEnabled ).to.be.false; } ); it( 'should be false if selection starts in the last block of list item', () => { - setData( model, - '0' + - '1' + - '2' + - '[]3' - ); + setData( model, modelList( [ + '* 0', + '* 1', + ' 2', + ' []3' + ] ) ); expect( command.isEnabled ).to.be.false; } ); it( 'should be false if selection starts in first list item', () => { - setData( model, - '[]0' + - '1' - ); + setData( model, modelList( [ + '* []0', + ' 1' + ] ) ); expect( command.isEnabled ).to.be.false; } ); it( 'should be false if selection starts in the first list item at given indent', () => { - setData( model, - 'a' + - '[]b' + - 'c' - ); + setData( model, modelList( [ + '* 0', + ' * []1', + ' 2' + ] ) ); expect( command.isEnabled ).to.be.false; } ); it( 'should be false if selection is in first list item with different type than previous list', () => { - setData( model, - 'a' + - 'a' + - '[]b' + - 'b' - ); + setData( model, modelList( [ + '* 0', + ' 1', + '# []2', + ' 3' + ] ) ); expect( command.isEnabled ).to.be.false; } ); describe( 'multiple list items selection', () => { it( 'should be true if selection starts in the middle block of list item and spans multiple items', () => { - setData( model, - '0' + - '1' + - '[2' + - '3]' + - '4' - ); + setData( model, modelList( [ + '* 0', + '* 1', + ' [2', + '* 3]', + ' 4' + ] ) ); expect( command.isEnabled ).to.be.true; } ); @@ -212,18 +212,18 @@ describe( 'DocumentListIndentCommand', () => { describe( 'execute()', () => { describe( 'single block per list item', () => { it( 'should use parent batch', () => { - setData( model, - '0' + - '1' + - '2' + - '3' + - '4' + - '[]5' + - '6' - ); + setData( model, modelList( [ + '* 0', + '* 1', + ' * 2', + ' * 3', + ' * 4', + ' * []5', + '* 6' + ] ) ); model.change( writer => { - expect( writer.batch.operations.length ).to.equalMarkup( 0 ); + expect( writer.batch.operations.length ).to.equal( 0 ); command.execute(); @@ -256,66 +256,66 @@ describe( 'DocumentListIndentCommand', () => { } ); it( 'should increment indent of all sub-items of indented item', () => { - setData( model, - '0' + - '[]1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); + setData( model, modelList( [ + '* 0', + '* []1', + ' * 2', + ' * 3', + ' * 4', + ' * 5', + '* 6' + ] ) ); command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( - '0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' * []1', + ' * 2', + ' * 3', + ' * 4', + ' * 5', + '* 6' + ] ) ); } ); it( 'should increment indent of all selected item when multiple items are selected', () => { - setData( model, - '0' + - '[1' + - '2' + - '3]' + - '4' + - '5' + - '6' - ); + setData( model, modelList( [ + '* 0', + '* [1', + ' * 2', + ' * 3]', + ' * 4', + ' * 5', + '* 6' + ] ) ); command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( - '0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' * [1', + ' * 2', + ' * 3]', + ' * 4', + ' * 5', + '* 6' + ] ) ); } ); it( 'should fire "afterExecute" event after finish all operations with all changed items', done => { - setData( model, - '0' + - '[]1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); + setData( model, modelList( [ + '* 0', + '* []1', + ' * 2', + ' * 3', + ' * 4', + ' * 5', + '* 6' + ] ) ); command.on( 'afterExecute', ( evt, data ) => { - expect( data ).to.deep.equalMarkup( [ + expect( data ).to.deep.equal( [ root.getChild( 1 ), root.getChild( 2 ), root.getChild( 3 ), @@ -332,138 +332,138 @@ describe( 'DocumentListIndentCommand', () => { describe( 'multiple blocks per list item', () => { it( 'should change indent of all blocks of a list item', () => { - setData( model, - '0' + - '[]1' + - '2' + - '3' + - '4' - ); + setData( model, modelList( [ + '* 0', + '* []1', + ' 2', + ' 3', + '* 4' + ] ) ); command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( - '0' + - '1' + - '2' + - '3' + - '4' - ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' * []1', + ' 2', + ' 3', + '* 4' + ] ) ); } ); it( 'should do nothing if the following block of bigger list item is selected', () => { - setData( model, - '0' + - '1' + - '[]2' + - '3' + - '4' - ); + setData( model, modelList( [ + '* 0', + '* 1', + ' []2', + ' 3', + '* 4' + ] ) ); command.isEnabled = true; command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( - '0' + - '1' + - '2' + - '3' + - '4' - ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + ' []2', + ' 3', + '* 4' + ] ) ); } ); it( 'should increment indent of all sub-items of indented item', () => { - setData( model, - '0' + - '[]1' + - '2' + - '3' + - '4' + - '5' + - '6' + - '6' - ); + setData( model, modelList( [ + '* 0', + '* []1', + ' * 2', + ' * 3', + ' 4', + ' * 5', + ' 6', + '* 7' + ] ) ); command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( - '0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' + - '6' - ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' * []1', + ' * 2', + ' * 3', + ' 4', + ' * 5', + ' 6', + '* 7' + ] ) ); } ); it( 'should increment indent of all sub-items of indented item (at end of list item)', () => { - setData( model, - '0' + - '[]1' + - '2' + - '3' + - '4' + - '5' + - '6' + - '8' - ); + setData( model, modelList( [ + '* 0', + '* []1', + ' 2', + ' * 3', + ' * 4', + ' 5', + ' * 6', + '* 7' + ] ) ); command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( - '0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' + - '8' - ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' * []1', + ' 2', + ' * 3', + ' * 4', + ' 5', + ' * 6', + '* 7' + ] ) ); } ); it( 'should increment indent of all selected list items when multiple items are selected partially', () => { - setData( model, - '0' + - '1' + - '[2' + - '3]' + - '4' + - '5' - ); + setData( model, modelList( [ + '* 0', + '* 1', + ' [2', + '* 3]', + ' 4', + '* 5' + ] ) ); command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( - '0' + - '1' + - '2' + - '3' + - '4' + - '5' - ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' * 1', + ' [2', + ' * 3]', + ' 4', + '* 5' + ] ) ); } ); it( 'should not increment indent of items from the following list even if it was selected', () => { - setData( model, - '0' + - '[1' + - '2' + - '3]' + - '4' - ); + setData( model, modelList( [ + '* 0', + '* [1', + '2', + '* 3]', + '* 4' + ] ) ); command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( - '0' + - '1' + - '2' + - '3' + - '4' - ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' * [1', + '2', + '* 3]', + '* 4' + ] ) ); } ); it( 'should fire "afterExecute" event after finish all operations with all changed items', done => { @@ -476,9 +476,18 @@ describe( 'DocumentListIndentCommand', () => { '5' + '6' ); + setData( model, modelList( [ + '* 0', + '* []1', + ' * 2', + ' * 3', + ' * 4', + ' * 5', + '* 6' + ] ) ); command.on( 'afterExecute', ( evt, data ) => { - expect( data ).to.deep.equalMarkup( [ + expect( data ).to.deep.equal( [ root.getChild( 1 ), root.getChild( 2 ), root.getChild( 3 ), @@ -508,73 +517,73 @@ describe( 'DocumentListIndentCommand', () => { describe( 'isEnabled', () => { it( 'should be true if selection starts in list item', () => { - setData( model, - '0' + - '1' + - '2' + - '3' + - '4' + - '[]5' + - '6' - ); + setData( model, modelList( [ + '* 0', + '* 1', + ' * 2', + ' * 3', + ' * 4', + ' * []5', + '* 6' + ] ) ); expect( command.isEnabled ).to.be.true; } ); it( 'should be true if selection starts in first list item', () => { - setData( model, - '[]0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); + setData( model, modelList( [ + '* []0', + '* 1', + ' * 2', + ' * 3', + ' * 4', + ' * 5', + '* 6' + ] ) ); expect( command.isEnabled ).to.be.true; } ); it( 'should be true if selection starts in a list item that has bigger indent than it\'s previous sibling', () => { - setData( model, - '0' + - '1' + - '[]2' + - '3' + - '4' + - '5' + - '6' - ); + setData( model, modelList( [ + '* 0', + '* 1', + ' * []2', + ' * 3', + ' * 4', + ' * 5', + '* 6' + ] ) ); expect( command.isEnabled ).to.be.true; } ); it( 'should be false if selection starts before a list', () => { - setData( model, - '[0' + - '1]' + - '2' - ); + setData( model, modelList( [ + '[0', + '* 1]', + ' * 2' + ] ) ); expect( command.isEnabled ).to.be.false; } ); it( 'should be true with selection in the middle block of a list item', () => { - setData( model, - '0' + - '[]1' + - '2' - ); + setData( model, modelList( [ + '* 0', + ' []1', + ' 2' + ] ) ); expect( command.isEnabled ).to.be.true; } ); it( 'should be true with selection in the last block of a list item', () => { - setData( model, - '0' + - '1' + - '[]2' - ); + setData( model, modelList( [ + '* 0', + ' 1', + ' []2' + ] ) ); expect( command.isEnabled ).to.be.true; } ); @@ -582,162 +591,216 @@ describe( 'DocumentListIndentCommand', () => { describe( 'execute()', () => { it( 'should decrement indent attribute by 1 (if it is bigger than 0)', () => { - setData( model, - '0' + - '1' + - '2' + - '3' + - '4' + - '[]5' + - '6' - ); + setData( model, modelList( [ + '* 0', + '* 1', + ' * 2', + ' * 3', + ' * 4', + ' * []5', + '* 6' + ] ) ); command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( - '0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + ' * 2', + ' * 3', + ' * 4', + '* []5', + '* 6' + ] ) ); } ); it( 'should remove list attributes (if indent is less than to 0)', () => { - setData( model, - '[]0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); + setData( model, modelList( [ + '* []0', + '* 1', + ' * 2', + ' * 3', + ' * 4', + ' * 5', + '* 6' + ] ) ); command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( - '0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '[]0', + '* 1', + ' * 2', + ' * 3', + ' * 4', + ' * 5', + '* 6' + ] ) ); } ); it( 'should decrement indent of all sub-items of outdented item', () => { - setData( model, - '0' + - '[]1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); + setData( model, modelList( [ + '* 0', + '* []1', + ' * 2', + ' * 3', + ' * 4', + ' * 5', + '* 6' + ] ) ); command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( - '0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); + expect( getData( model ) ).to.equalMarkup(modelList( [ + '* 0', + '[]1', + '* 2', + ' * 3', + ' * 4', + '* 5', + '* 6' + ] ) ); } ); it( 'should outdent all selected item when multiple items are selected', () => { - setData( model, - '0' + - '[1' + - '2' + - '3]' + - '4' + - '5' + - '6' - ); + setData( model, modelList( [ + '* 0', + '* [1', + ' * 2', + ' * 3]', + ' * 4', + ' * 5', + '* 6' + ] ) ); command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( - '0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + '[1', + '* 2', + ' * 3]', + ' * 4', + '* 5', + '* 6' + ] ) ); } ); it( 'should outdent all blocks of partly selected item when multiple items are selected', () => { - setData( model, - '0' + - '1' + - '[2' + - '3]' + - '4' + - '5' + - '6' - ); + setData( model, modelList( [ + '* 0', + ' * 1', + ' [2', + ' * 3]', + ' 4', + ' * 5', + '* 6' + ] ) ); command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( - '0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + ' [2', + ' * 3]', + ' 4', + ' * 5', + '* 6' + ] ) ); } ); it( 'should split list item if selection is in the following list item block', () => { - setData( model, - '0' + - '[]1' + - '2' + - '3' - ); + setData( model, modelList( [ + '* 0', + ' []1', + ' 2', + '* 3' + ] ) ); stubUid(); command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( - '0' + - '1' + + expect( getData( model ) ).to.equalMarkup( + '0' + + '[]1' + '2' + - '3' + '3' ); } ); it( 'should split list item if selection is in the last list item block', () => { - setData( model, - '0' + - '1' + - '[]2' + - '3' - ); + setData( model, modelList( [ + '* 0', + ' 1', + ' []2', + '* 3' + ] ) ); stubUid(); command.execute(); - expect( getData( model, { withoutSelection: true } ) ).to.equalMarkup( - '0' + - '1' + - '2' + - '3' + expect( getData( model ) ).to.equalMarkup( + '0' + + '1' + + '[]2' + + '3' ); } ); + + it( 'should merge item if parent has more following blocks', () => { + setData( model, modelList( [ + '* 0', + ' * []1', + ' 2' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' []1', + ' 2' + ] ) ); + } ); + + it( 'should not merge item if parent has no more following blocks', () => { + setData( model, modelList( [ + '* 0', + ' * []1', + '* 2' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + '* []1', + '* 2' + ] ) ); + } ); + + it( 'should handle bigger indent drop between items', () => { + setData( model, modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * [3', + ' * 4]', + ' * 5' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * [3', + '* 4]', + ' * 5' + ] ) ); + } ); } ); } ); } ); diff --git a/packages/ckeditor5-list/tests/documentlist/utils/model.js b/packages/ckeditor5-list/tests/documentlist/utils/model.js index 8718c8fc0a9..f29c25fcde1 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/model.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/model.js @@ -13,6 +13,7 @@ import { isLastBlockOfListItem, splitListItemBefore } from '../../../src/documentlist/utils/model'; +import { modelList } from '../_utils/utils'; import stubUid from '../_utils/uid'; import Model from '@ckeditor/ckeditor5-engine/src/model/model'; @@ -459,7 +460,7 @@ describe( 'DocumentList - utils - model', () => { } ); } ); - describe( 'expandListBlocksToCompleteItems()', () => { + describe.only( 'expandListBlocksToCompleteItems()', () => { it( 'should not modify list for a single block of a single-block list item', () => { const input = 'a' + @@ -468,11 +469,11 @@ describe( 'DocumentList - utils - model', () => { 'd'; const fragment = parseModel( input, schema ); - const blocks = [ + let blocks = [ fragment.getChild( 0 ) ]; - expandListBlocksToCompleteItems( blocks ); + blocks = expandListBlocksToCompleteItems( blocks ); expect( blocks.length ).to.equal( 1 ); expect( blocks[ 0 ] ).to.equal( fragment.getChild( 0 ) ); @@ -485,11 +486,11 @@ describe( 'DocumentList - utils - model', () => { '2'; const fragment = parseModel( input, schema ); - const blocks = [ + let blocks = [ fragment.getChild( 0 ) ]; - expandListBlocksToCompleteItems( blocks ); + blocks = expandListBlocksToCompleteItems( blocks ); expect( blocks.length ).to.equal( 3 ); expect( blocks[ 0 ] ).to.equal( fragment.getChild( 0 ) ); @@ -506,11 +507,11 @@ describe( 'DocumentList - utils - model', () => { '3'; const fragment = parseModel( input, schema ); - const blocks = [ + let blocks = [ fragment.getChild( 1 ) ]; - expandListBlocksToCompleteItems( blocks ); + blocks = expandListBlocksToCompleteItems( blocks ); expect( blocks.length ).to.equal( 3 ); expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); @@ -527,11 +528,11 @@ describe( 'DocumentList - utils - model', () => { '3'; const fragment = parseModel( input, schema ); - const blocks = [ + let blocks = [ fragment.getChild( 3 ) ]; - expandListBlocksToCompleteItems( blocks ); + blocks = expandListBlocksToCompleteItems( blocks ); expect( blocks.length ).to.equal( 3 ); expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); @@ -548,11 +549,11 @@ describe( 'DocumentList - utils - model', () => { '3'; const fragment = parseModel( input, schema ); - const blocks = [ + let blocks = [ fragment.getChild( 2 ) ]; - expandListBlocksToCompleteItems( blocks ); + blocks = expandListBlocksToCompleteItems( blocks ); expect( blocks.length ).to.equal( 3 ); expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); @@ -569,11 +570,11 @@ describe( 'DocumentList - utils - model', () => { '3'; const fragment = parseModel( input, schema ); - const blocks = [ + let blocks = [ fragment.getChild( 2 ) ]; - expandListBlocksToCompleteItems( blocks ); + blocks = expandListBlocksToCompleteItems( blocks ); expect( blocks.length ).to.equal( 3 ); expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); @@ -588,11 +589,11 @@ describe( 'DocumentList - utils - model', () => { '2'; const fragment = parseModel( input, schema ); - const blocks = [ + let blocks = [ fragment.getChild( 0 ) ]; - expandListBlocksToCompleteItems( blocks ); + blocks = expandListBlocksToCompleteItems( blocks ); expect( blocks.length ).to.equal( 3 ); expect( blocks[ 0 ] ).to.equal( fragment.getChild( 0 ) ); @@ -607,11 +608,11 @@ describe( 'DocumentList - utils - model', () => { '2'; const fragment = parseModel( input, schema ); - const blocks = [ + let blocks = [ fragment.getChild( 2 ) ]; - expandListBlocksToCompleteItems( blocks ); + blocks = expandListBlocksToCompleteItems( blocks ); expect( blocks.length ).to.equal( 3 ); expect( blocks[ 0 ] ).to.equal( fragment.getChild( 0 ) ); @@ -629,12 +630,12 @@ describe( 'DocumentList - utils - model', () => { 'y'; const fragment = parseModel( input, schema ); - const blocks = [ + let blocks = [ fragment.getChild( 2 ), fragment.getChild( 3 ) ]; - expandListBlocksToCompleteItems( blocks ); + blocks = expandListBlocksToCompleteItems( blocks ); expect( blocks.length ).to.equal( 4 ); expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); @@ -652,16 +653,46 @@ describe( 'DocumentList - utils - model', () => { '4'; const fragment = parseModel( input, schema ); - const blocks = [ + let blocks = [ fragment.getChild( 2 ) ]; - expandListBlocksToCompleteItems( blocks ); + blocks = expandListBlocksToCompleteItems( blocks ); expect( blocks.length ).to.equal( 2 ); expect( blocks[ 0 ] ).to.equal( fragment.getChild( 2 ) ); expect( blocks[ 1 ] ).to.equal( fragment.getChild( 3 ) ); } ); + + it( 'should include all blocks even if not at the same indent level from the edge block', () => { + const fragment = parseModel( modelList( [ + '* 0', + ' * 1', + ' * 2', + ' 3', + ' * 4', + ' * 5', + ' 6', + ' * 7' + ] ), schema ); + + let blocks = [ + fragment.getChild( 2 ), + fragment.getChild( 3 ), + fragment.getChild( 4 ), + fragment.getChild( 5 ) + ]; + + blocks = expandListBlocksToCompleteItems( blocks ); + + expect( blocks.length ).to.equal( 6 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 1 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 2 ) ); + expect( blocks[ 2 ] ).to.equal( fragment.getChild( 3 ) ); + expect( blocks[ 3 ] ).to.equal( fragment.getChild( 4 ) ); + expect( blocks[ 4 ] ).to.equal( fragment.getChild( 5 ) ); + expect( blocks[ 5 ] ).to.equal( fragment.getChild( 6 ) ); + } ); } ); describe( 'splitListItemBefore()', () => { From accba160a9cdb9fc5e24914f952da359d2c48ad4 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 27 Dec 2021 12:22:21 +0100 Subject: [PATCH 25/66] Tuned indent command behavior. --- .../documentlist/documentlistindentcommand.js | 53 ++++++++++++------- .../documentlist/documentlistindentcommand.js | 11 +--- .../tests/documentlist/utils/model.js | 2 +- 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js index 460c78ae5eb..4a95df2218c 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js @@ -63,30 +63,45 @@ export default class DocumentListIndentCommand extends Command { model.change( writer => { // Handle selection contained in the single list item and starting in the following blocks. if ( isOnlyOneListItemSelected( blocks ) && !isFirstBlockOfListItem( blocks[ 0 ] ) ) { - // Do nothing while indenting, but split list item on outdent. - if ( this._indentBy < 0 ) { - splitListItemBefore( blocks[ 0 ], writer ); + // Allow increasing indent of following list item blocks. + if ( this._indentBy > 0 ) { + indentBlocks( blocks, this._indentBy, {}, writer ); } - return; + // For indent make sure that indented blocks have a new ID. + // For outdent just split blocks from the list item (give them a new IDs). + splitListItemBefore( blocks[ 0 ], writer ); + + this._fireAfterExecute( blocks ); } + // More than a single list item is selected, or the first block of list item is selected. + else { + // Now just update the attributes of blocks. + const changedBlocks = indentBlocks( blocks, this._indentBy, { expand: true }, writer ); - // Now just update the attributes of blocks. - const changedBlocks = indentBlocks( blocks, this._indentBy, { expand: true }, writer ); - - /** - * Event fired by the {@link #execute} method. - * - * It allows to execute an action after executing the {@link ~DocumentListIndentCommand#execute} method, - * for example adjusting attributes of changed list items. - * - * @protected - * @event afterExecute - */ - this.fire( 'afterExecute', changedBlocks ); + this._fireAfterExecute( changedBlocks ); + } } ); } + /** + * TODO + * @private + * @param {Array.} changedBlocks The changed list elements. + */ + _fireAfterExecute( changedBlocks ) { + /** + * Event fired by the {@link #execute} method. + * + * It allows to execute an action after executing the {@link ~DocumentListIndentCommand#execute} method, + * for example adjusting attributes of changed list items. + * + * @protected + * @event afterExecute + */ + this.fire( 'afterExecute', changedBlocks ); + } + /** * Checks whether the command can be enabled in the current context. * @@ -108,9 +123,9 @@ export default class DocumentListIndentCommand extends Command { return true; } - // Indenting of the following blocks of a list item is not allowed. + // A single block of a list item is selected, so it could be indented as a sublist. if ( isOnlyOneListItemSelected( blocks ) && !isFirstBlockOfListItem( blocks[ 0 ] ) ) { - return false; + return true; } blocks = expandListBlocksToCompleteItems( blocks ); diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js index a00ab001c15..b45955d900a 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js @@ -467,15 +467,6 @@ describe( 'DocumentListIndentCommand', () => { } ); it( 'should fire "afterExecute" event after finish all operations with all changed items', done => { - setData( model, - '0' + - '[]1' + - '2' + - '3' + - '4' + - '5' + - '6' - ); setData( model, modelList( [ '* 0', '* []1', @@ -651,7 +642,7 @@ describe( 'DocumentListIndentCommand', () => { command.execute(); - expect( getData( model ) ).to.equalMarkup(modelList( [ + expect( getData( model ) ).to.equalMarkup( modelList( [ '* 0', '[]1', '* 2', diff --git a/packages/ckeditor5-list/tests/documentlist/utils/model.js b/packages/ckeditor5-list/tests/documentlist/utils/model.js index f29c25fcde1..65ee1c4c5a8 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/model.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/model.js @@ -460,7 +460,7 @@ describe( 'DocumentList - utils - model', () => { } ); } ); - describe.only( 'expandListBlocksToCompleteItems()', () => { + describe( 'expandListBlocksToCompleteItems()', () => { it( 'should not modify list for a single block of a single-block list item', () => { const input = 'a' + From 4fbe7ba279e98315a7fda2c8825af714f3a92e28 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 27 Dec 2021 16:36:55 +0100 Subject: [PATCH 26/66] WiP. --- .../src/documentlist/documentlistcommand.js | 126 +++---- .../src/documentlist/documentlistediting.js | 2 + .../documentlist/documentlistindentcommand.js | 14 +- .../src/documentlist/utils/listwalker.js | 23 ++ .../src/documentlist/utils/model.js | 308 +++++++++++++----- .../ckeditor5-list/theme/documentlist.css | 17 + 6 files changed, 340 insertions(+), 150 deletions(-) create mode 100644 packages/ckeditor5-list/theme/documentlist.css diff --git a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js index b35a12fa04c..230e7fbde66 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js @@ -10,12 +10,12 @@ import { Command } from 'ckeditor5/src/core'; import { uid } from 'ckeditor5/src/utils'; import { - indentBlocks, - isFirstBlockOfListItem, - isOnlyOneListItemSelected, splitListItemBefore, expandListBlocksToCompleteItems, - getSameIndentBlocks + getListItemBlocks, + getListItems, + removeListAttributes, + outdentItemsAfterItemRemoved } from './utils/model'; /** @@ -78,56 +78,81 @@ export default class DocumentListCommand extends Command { model.change( writer => { if ( turnOff ) { - // Outdent. - indentBlocks( blocks, -1, { expand: true, alwaysMerge: true }, writer ); - } else { - // Case of selection: - // * a - // * [b - // c] - // Should be treated as only "c" selected to make it: - // * a - // * b - // * c - const completeItemsBlocks = expandListBlocksToCompleteItems( blocks ); - const sameIndentBlocks = getSameIndentBlocks( completeItemsBlocks ); - const originallySelectedBlocks = sameIndentBlocks.filter( block => blocks.includes( block ) ); - - if ( isOnlyOneListItemSelected( originallySelectedBlocks ) && !isFirstBlockOfListItem( originallySelectedBlocks[ 0 ] ) ) { - indentBlocks( originallySelectedBlocks, 1, {}, writer ); - - for ( const block of originallySelectedBlocks.reverse() ) { - splitListItemBefore( block, writer ); + const lastBlock = blocks[ blocks.length - 1 ]; + + // Split the first block from the list item. + const itemBlocks = getListItemBlocks( lastBlock, { direction: 'forward' } ); + + if ( itemBlocks.length > 1 ) { + splitListItemBefore( itemBlocks[ 1 ], writer ); + } + + // Convert list blocks to plain blocks. + const changedBlocks = removeListAttributes( blocks, writer ); + + // Outdent items following the selected list item. + changedBlocks.push( ...outdentItemsAfterItemRemoved( lastBlock, writer ) ); + + this._fireAfterExecute( changedBlocks ); + } + // Turning on the list items for a collapsed selection inside a list item. + else if ( document.selection.isCollapsed && blocks[ 0 ].hasAttribute( 'listType' ) ) { + const changedBlocks = getListItems( blocks[ 0 ] ); + + for ( const block of changedBlocks ) { + writer.setAttribute( 'listType', this.type, block ); + } + + this._fireAfterExecute( changedBlocks ); + } + // Turning on the list items for a non-collapsed selection. + else { + const changedBlocks = []; + + for ( const block of blocks ) { + // Promote the given block to the list item. + if ( !block.hasAttribute( 'listType' ) ) { + writer.setAttributes( { + listIndent: 0, + listItemId: uid(), + listType: this.type + }, block ); + + changedBlocks.push( block ); } - } else { - for ( const block of blocks ) { - if ( !block.hasAttribute( 'listType' ) ) { - writer.setAttributes( { - listIndent: 0, - listItemId: uid(), - listType: this.type - }, block ); - } else { - expandListBlocksToCompleteItems( [ block ] ); - writer.setAttribute( 'listType', this.type, block ); + // Change the type of list item. + else { + for ( const node of expandListBlocksToCompleteItems( block, { withNested: false } ) ) { + writer.setAttribute( 'listType', this.type, node ); + changedBlocks.push( node ); } } } - } - /** - * Event fired by the {@link #execute} method. - * - * It allows to execute an action after executing the {@link ~DocumentListCommand#execute} method, - * for example adjusting attributes of changed list items. - * - * @protected - * @event afterExecute - */ - this.fire( 'afterExecute', blocks ); + this._fireAfterExecute( changedBlocks ); + } } ); } + /** + * TODO + * + * @private + * @param {Array.} changedBlocks The changed list elements. + */ + _fireAfterExecute( changedBlocks ) { + /** + * Event fired by the {@link #execute} method. + * + * It allows to execute an action after executing the {@link ~DocumentListCommand#execute} method, + * for example adjusting attributes of changed list items. + * + * @protected + * @event afterExecute + */ + this.fire( 'afterExecute', changedBlocks ); + } + /** * Checks the command's {@link #value}. * @@ -144,15 +169,6 @@ export default class DocumentListCommand extends Command { } } - // TODO this is same as in execute - const completeItemsBlocks = expandListBlocksToCompleteItems( blocks ); - const sameIndentBlocks = getSameIndentBlocks( completeItemsBlocks ); - const originallySelectedBlocks = sameIndentBlocks.filter( block => blocks.includes( block ) ); - - if ( isOnlyOneListItemSelected( originallySelectedBlocks ) && !isFirstBlockOfListItem( originallySelectedBlocks[ 0 ] ) ) { - return false; - } - return true; } diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.js b/packages/ckeditor5-list/src/documentlist/documentlistediting.js index 55b79d202e0..f04ffe30c51 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.js @@ -29,6 +29,8 @@ import { } from './utils/postfixers'; import { iterateSiblingListBlocks } from './utils/listwalker'; +import '../../theme/documentlist.css'; + /** * The editing part of the document-list feature. It handles creating, editing and removing lists and list items. * diff --git a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js index 4a95df2218c..9347c27ad19 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js @@ -13,6 +13,7 @@ import { indentBlocks, isFirstBlockOfListItem, isOnlyOneListItemSelected, + outdentBlocks, splitListItemBefore } from './utils/model'; import ListWalker from './utils/listwalker'; @@ -40,7 +41,7 @@ export default class DocumentListIndentCommand extends Command { * @private * @member {Number} */ - this._indentBy = indentDirection == 'forward' ? 1 : -1; + this._direction = indentDirection; } /** @@ -64,8 +65,8 @@ export default class DocumentListIndentCommand extends Command { // Handle selection contained in the single list item and starting in the following blocks. if ( isOnlyOneListItemSelected( blocks ) && !isFirstBlockOfListItem( blocks[ 0 ] ) ) { // Allow increasing indent of following list item blocks. - if ( this._indentBy > 0 ) { - indentBlocks( blocks, this._indentBy, {}, writer ); + if ( this._direction == 'forward' ) { + indentBlocks( blocks, writer ); } // For indent make sure that indented blocks have a new ID. @@ -77,7 +78,9 @@ export default class DocumentListIndentCommand extends Command { // More than a single list item is selected, or the first block of list item is selected. else { // Now just update the attributes of blocks. - const changedBlocks = indentBlocks( blocks, this._indentBy, { expand: true }, writer ); + const changedBlocks = this._direction == 'forward' ? + indentBlocks( blocks, writer, { expand: true } ) : + outdentBlocks( blocks, writer, { expand: true } ); this._fireAfterExecute( changedBlocks ); } @@ -86,6 +89,7 @@ export default class DocumentListIndentCommand extends Command { /** * TODO + * * @private * @param {Array.} changedBlocks The changed list elements. */ @@ -119,7 +123,7 @@ export default class DocumentListIndentCommand extends Command { } // If we are outdenting it is enough to be in list item. Every list item can always be outdented. - if ( this._indentBy < 0 ) { + if ( this._direction == 'backward' ) { return true; } diff --git a/packages/ckeditor5-list/src/documentlist/utils/listwalker.js b/packages/ckeditor5-list/src/documentlist/utils/listwalker.js index 42acabcac30..08b9193b751 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/listwalker.js +++ b/packages/ckeditor5-list/src/documentlist/utils/listwalker.js @@ -22,6 +22,7 @@ export default class ListWalker { * @param {Boolean} [options.includeSelf=false] Whether start block should be included in the result (if it's matching other criteria). * @param {Boolean} [options.sameItemId=false] Whether should return only blocks with the same `listItemId` attribute * as the start element. + * @param {Boolean} [options.sameItemType=false] Whether should return only blocks wit the same `listType` attribute. * @param {Boolean} [options.sameIndent=false] Whether blocks with the same indent level as the start block should be included * in the result. * @param {Boolean} [options.smallerIndent=false] Whether blocks with a smaller indent level than the start block should be included @@ -54,6 +55,14 @@ export default class ListWalker { */ this._startItemId = startElement.getAttribute( 'listItemId' ); + /** + * The `listType` of the start block. + * + * @private + * @type {String} + */ + this._startItemType = startElement.getAttribute( 'listType' ); + /** * The iterating direction. * @@ -78,6 +87,14 @@ export default class ListWalker { */ this._sameItemId = !!options.sameItemId; + /** + * Whether should return only blocks wit the same `listType` attribute. + * + * @private + * @type {Boolean} + */ + this._sameItemType = !!options.sameItemType; + /** * Whether blocks with the same indent level as the start block should be included in the result. * @@ -112,6 +129,7 @@ export default class ListWalker { * @param {Boolean} [options.includeSelf=false] Whether start block should be included in the result (if it's matching other criteria). * @param {Boolean} [options.sameItemId=false] Whether should return only blocks with the same `listItemId` attribute * as the start element. + * @param {Boolean} [options.sameItemType=false] Whether should return only blocks wit the same `listType` attribute. * @param {Boolean} [options.sameIndent=false] Whether blocks with the same indent level as the start block should be included * in the result. * @param {Boolean} [options.smallerIndent=false] Whether blocks with a smaller indent level than the start block should be included @@ -184,6 +202,11 @@ export default class ListWalker { if ( this._sameItemId && node.getAttribute( 'listItemId' ) != this._startItemId ) { break; } + + // Abort if item has a different type. + if ( this._sameItemType && node.getAttribute( 'listType' ) != this._startItemType ) { + break; + } } // There is another block for the same list item so the nested items were in the same list item. diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.js b/packages/ckeditor5-list/src/documentlist/utils/model.js index 4f49d740b65..49c556ac35f 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.js +++ b/packages/ckeditor5-list/src/documentlist/utils/model.js @@ -7,8 +7,8 @@ * @module list/documentlist/utils/model */ -import { uid } from 'ckeditor5/src/utils'; -import ListWalker from './listwalker'; +import { uid, toArray } from 'ckeditor5/src/utils'; +import ListWalker, { iterateSiblingListBlocks } from './listwalker'; /** * Returns an array with all elements that represents the same list item. @@ -68,6 +68,32 @@ export function getNestedListBlocks( listItem ) { } ) ); } +/** + * Returns array of all blocks/items of the same list as given block (same indent, same type). + * + * @protected + * @param {module:engine/model/element~Element} listItem Starting list item element. + * @returns {Array.} + */ +export function getListItems( listItem ) { + const backwardBlocks = new ListWalker( listItem, { + sameIndent: true, + sameItemType: true + } ); + + const forwardBlocks = new ListWalker( listItem, { + sameIndent: true, + sameItemType: true, + includeSelf: true, + direction: 'forward' + } ); + + return [ + ...Array.from( backwardBlocks ).reverse(), + ...forwardBlocks + ]; +} + /** * Check if the given block is the first in the list item. * @@ -113,19 +139,24 @@ export function isLastBlockOfListItem( listBlock ) { * Expands the given list of selected blocks to include the leading and tailing blocks of partially selected list items. * * @protected - * @param {Array.} blocks The list of selected blocks. + * @param {module:engine/model/element~Element|Array.} blocks The list of selected blocks. + * @param {Object} [options] + * @param {Boolean} [options.withNested=true] Whether should include nested list items. * @returns {Array.} */ -export function expandListBlocksToCompleteItems( blocks ) { +export function expandListBlocksToCompleteItems( blocks, options = {} ) { + blocks = toArray( blocks ); + + const biggerIndent = options.withNested !== false; const allBlocks = new Set(); for ( const block of blocks ) { - for ( const itemBlock of getAllListItemBlocks( block, { biggerIndent: true } ) ) { + for ( const itemBlock of getAllListItemBlocks( block, { biggerIndent } ) ) { allBlocks.add( itemBlock ); } } - return Array.from( allBlocks.values() ).sort( ( a, b ) => a.index - b.index ); + return sortBlocks( allBlocks ); } /** @@ -143,36 +174,6 @@ export function splitListItemBefore( listBlock, writer ) { } } -/** - * Splits the list item just before the provided list block. - * - * @protected - * @param {module:engine/model/element~Element} listBlock The list block element. - * @param {module:engine/model/writer~Writer} writer The model writer. - */ -export function mergeListItemBlocksIntoParentListItem( listBlock, writer ) { - const blocks = getAllListItemBlocks( listBlock ); - const firstBlock = blocks[ 0 ]; - const parentListItem = firstBlock.previousSibling; - - // TODO remove paranoid check that should not be necessary. - if ( !parentListItem || !parentListItem.hasAttribute( 'listItemId' ) ) { - throw 'Cannot merge when there is nothing to merge into.'; - } - - const parentListAttributes = {}; - - for ( const attributeKey of parentListItem.getAttributeKeys() ) { - if ( attributeKey.startsWith( 'list' ) ) { - parentListAttributes[ attributeKey ] = parentListItem.getAttribute( attributeKey ); - } - } - - for ( const block of blocks ) { - writer.setAttributes( parentListAttributes, block ); - } -} - /** * Merges the list item with the parent list item. * @@ -201,32 +202,51 @@ export function mergeListItemBefore( listBlock, parentBlock, writer ) { } /** - * Updates indentation of given list blocks. + * Increases indentation of given list blocks. * * @protected - * @param {Iterable.} blocks The iterable of selected blocks. - * @param {Number} indentBy The indentation level difference. - * @param {Boolean} expand TODO + * @param {module:engine/model/element~Element|Iterable.} blocks The block or iterable of blocks. * @param {module:engine/model/writer~Writer} writer The model writer. + * @param {Object} [options] + * @param {Boolean} [options.expand=false] Whether should expand the list of blocks to include complete list items + * (all blocks of given list items). */ -export function indentBlocks( blocks, indentBy, { expand, alwaysMerge }, writer ) { +export function indentBlocks( blocks, writer, { expand } = {} ) { + blocks = toArray( blocks ); + // Expand the selected blocks to contain the whole list items. const allBlocks = expand ? expandListBlocksToCompleteItems( blocks ) : blocks; - const visited = new Set(); - const referenceIndex = allBlocks.reduce( ( indent, block ) => { - const blockIndent = block.getAttribute( 'listIndent' ); + for ( const block of allBlocks ) { + writer.setAttribute( 'listIndent', block.getAttribute( 'listIndent' ) + 1, block ); + } - return blockIndent < indent ? blockIndent : indent; - }, Number.POSITIVE_INFINITY ); + return allBlocks; +} +/** + * Decreases indentation of given list blocks. + * + * @protected + * @param {module:engine/model/element~Element|Iterable.} blocks The block or iterable of blocks. + * @param {module:engine/model/writer~Writer} writer The model writer. + * @param {Object} [options] + * @param {Boolean} [options.expand=false] Whether should expand the list of blocks to include complete list items + * (all blocks of given list items). + */ +export function outdentBlocks( blocks, writer, { expand } = {} ) { + blocks = toArray( blocks ); + + // Expand the selected blocks to contain the whole list items. + const allBlocks = expand ? expandListBlocksToCompleteItems( blocks ) : blocks; + const visited = new Set(); + + const referenceIndex = Math.min( ...allBlocks.map( block => block.getAttribute( 'listIndent' ) ) ); const parentBlocks = new Map(); // Collect parent blocks before the list structure gets altered. - if ( indentBy < 0 ) { - for ( const block of allBlocks ) { - parentBlocks.set( block, ListWalker.first( block, { smallerIndent: true } ) ); - } + for ( const block of allBlocks ) { + parentBlocks.set( block, ListWalker.first( block, { smallerIndent: true } ) ); } for ( const block of allBlocks ) { @@ -236,59 +256,61 @@ export function indentBlocks( blocks, indentBy, { expand, alwaysMerge }, writer visited.add( block ); - const blockIndent = block.getAttribute( 'listIndent' ) + indentBy; + const blockIndent = block.getAttribute( 'listIndent' ) - 1; if ( blockIndent < 0 ) { - for ( const attributeKey of block.getAttributeKeys() ) { - if ( attributeKey.startsWith( 'list' ) ) { - writer.removeAttribute( attributeKey, block ); - } - } + removeListAttributes( block, writer ); continue; } - // Merge with parent list item while outdenting. - if ( indentBy < 0 ) { - const atReferenceIndent = block.getAttribute( 'listIndent' ) == referenceIndex; - - // Merge if the block indent matches reference indent or the block was passed directly with alwaysMerge flag. - if ( atReferenceIndent || alwaysMerge && blocks.includes( block ) ) { - const parentBlock = parentBlocks.get( block ); - - // The parent block could become a non-list block. - if ( parentBlock.hasAttribute( 'listIndent' ) ) { - const parentItemBlocks = getListItemBlocks( parentBlock, { direction: 'forward' } ); - - // Merge with parent only if it wasn't the last item. - // Merge: - // * a - // * b <- outdent - // c - // Don't merge: - // * a - // * b <- outdent - // * c - if ( alwaysMerge || parentItemBlocks.pop().index > block.index ) { - for ( const mergedBlock of mergeListItemBefore( block, parentBlock, writer ) ) { - visited.add( mergedBlock ); - } - - continue; - } - } + // Merge with parent list item while outdenting and indent matches reference indent. + if ( block.getAttribute( 'listIndent' ) == referenceIndex ) { + const mergedBlocks = mergeListItemIfNotLast( block, parentBlocks.get( block ), writer ); + + // All list item blocks are updated while merging so add those to visited set. + for ( const mergedBlock of mergedBlocks ) { + visited.add( mergedBlock ); + } + + // The indent level was updated while merging so continue to next block. + if ( mergedBlocks.length ) { + continue; } } writer.setAttribute( 'listIndent', blockIndent, block ); } - return allBlocks; + return sortBlocks( visited ); +} + +/** + * Removes list attributes from the fiven blocks. + * + * @protected + * @param {module:engine/model/element~Element|Iterable.} blocks The block or iterable of blocks. + * @param {module:engine/model/writer~Writer} writer The model writer. + * @returns {Array.} Array of altered blocks. + */ +export function removeListAttributes( blocks, writer ) { + blocks = toArray( blocks ); + + for ( const block of blocks ) { + for ( const attributeKey of block.getAttributeKeys() ) { + if ( attributeKey.startsWith( 'list' ) ) { + writer.removeAttribute( attributeKey, block ); + } + } + } + + return blocks; } /** * Checks whether the given blocks are related to a single list item. * TODO + * @protected */ export function isOnlyOneListItemSelected( blocks ) { if ( !blocks.length ) { @@ -306,13 +328,119 @@ export function isOnlyOneListItemSelected( blocks ) { /** * TODO + * + * @protected */ -export function getSameIndentBlocks( blocks ) { - if ( !blocks.length ) { +export function outdentItemsAfterItemRemoved( lastBlock, writer ) { + const changedBlocks = []; + + // Start from the model item that is just after the last turned-off item. + let currentIndent = Number.POSITIVE_INFINITY; + + // Correct indent of all items after the last turned off item. + // Rules that should be followed: + // 1. All direct sub-items of turned-off item should become indent 0, because the first item after it + // will be the first item of a new list. Other items are at the same level, so should have same 0 index. + // 2. All items with indent lower than indent of turned-off item should become indent 0, because they + // should not end up as a child of any of list items that they were not children of before. + // 3. All other items should have their indent changed relatively to it's parent. + // + // For example: + // 1 * -------- + // 2 * -------- + // 3 * -------- <-- this is turned off. + // 4 * -------- <-- this has to become indent = 0, because it will be first item on a new list. + // 5 * -------- <-- this should be still be a child of item above, so indent = 1. + // 6 * -------- <-- this has to become indent = 0, because it should not be a child of any of items above. + // 7 * -------- <-- this should be still be a child of item above, so indent = 1. + // 8 * -------- <-- this has to become indent = 0. + // 9 * -------- <-- this should still be a child of item above, so indent = 1. + // 10 * -------- <-- this should still be a child of item above, so indent = 2. + // 11 * -------- <-- this should still be at the same level as item above, so indent = 2. + // 12 * -------- <-- this and all below are left unchanged. + // 13 * -------- + // 14 * -------- + // + // After turning off 3 the list becomes: + // + // 1 * -------- + // 2 * -------- + // + // 3 -------- + // + // 4 * -------- + // 5 * -------- + // 6 * -------- + // 7 * -------- + // 8 * -------- + // 9 * -------- + // 10 * -------- + // 11 * -------- + // 12 * -------- + // 13 * -------- + // 14 * -------- + // + // Thanks to this algorithm no lists are mismatched and no items get unexpected children/parent, while + // those parent-child connection which are possible to maintain are still maintained. It's worth noting + // that this is the same effect that we would be get by multiple use of outdent command. However doing + // it like this is much more efficient because it's less operation (less memory usage, easier OT) and + // less conversion (faster). + for ( const { node } of iterateSiblingListBlocks( lastBlock.nextSibling, 'forward' ) ) { + // Check each next list item, as long as its indent is bigger than 0. + const indent = node.getAttribute( 'listIndent' ); + + // If the indent is 0 we are not going to change anything anyway. + if ( indent == 0 ) { + break; + } + + // We check if that's item indent is lower as current relative indent. + if ( indent < currentIndent ) { + // If it is, current relative indent becomes that indent. + currentIndent = indent; + } + + // Fix indent relatively to current relative indent. + // Note, that if we just changed the current relative indent, the newIndent will be equal to 0. + const newIndent = indent - currentIndent; + + // Save the entry in changes array. We do not apply it at the moment, because we will need to + // reverse the changes so the last item is changed first. + // This is to keep model in correct state all the time. + writer.setAttribute( 'listIndent', newIndent, node ); + changedBlocks.push( node ); + } + + return changedBlocks; +} + +// 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. +function mergeListItemIfNotLast( block, parentBlock, writer ) { + // The parent block could become a non-list block. + if ( !parentBlock.hasAttribute( 'listIndent' ) ) { return []; } - const firstIndent = blocks[ 0 ].getAttribute( 'listIndent' ); + const parentItemBlocks = getListItemBlocks( parentBlock, { direction: 'forward' } ); + + // Merge with parent only if outdented item wasn't the last one in its parent. + // Merge: + // * a -> * a + // * [b] -> b + // c -> c + // Don't merge: + // * a -> * a + // * [b] -> * b + // * c -> * c + if ( parentItemBlocks.pop().index > block.index ) { + return mergeListItemBefore( block, parentBlock, writer ); + } - return blocks.filter( block => block.getAttribute( 'listIndent' ) == firstIndent ); + return []; } + +// TODO +function sortBlocks( blocks ) { + return Array.from( blocks ).sort( ( a, b ) => a.index - b.index ); +} + diff --git a/packages/ckeditor5-list/theme/documentlist.css b/packages/ckeditor5-list/theme/documentlist.css new file mode 100644 index 00000000000..b81200285d0 --- /dev/null +++ b/packages/ckeditor5-list/theme/documentlist.css @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +.ck-editor__editable .ck-list-bogus-paragraph { + /* + * Use display:inline-block to force Chrome/Safari to limit text mutations to this element. + * See https://github.com/ckeditor/ckeditor5/issues/6062. + */ + display: inline-block; + + /* + * Make it possible to click the whole line to put selection inside it. + */ + width: 100%; +} From 166c7614017ab48d0bb60b8006a784eea33acf48 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 28 Dec 2021 14:39:45 +0100 Subject: [PATCH 27/66] Fix tests inentation. --- .../tests/documentlist/utils/view.js | 122 +++++++++--------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/packages/ckeditor5-list/tests/documentlist/utils/view.js b/packages/ckeditor5-list/tests/documentlist/utils/view.js index 8fb43eccacb..89411455a74 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/view.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/view.js @@ -75,8 +75,8 @@ describe( 'DocumentList - utils - view', () => { it( 'should return 0 for flat list', () => { const viewElement = parseView( '
    ' + - '
  • a
  • ' + - '
  • b
  • ' + + '
  • a
  • ' + + '
  • b
  • ' + '
' ); @@ -87,18 +87,18 @@ describe( 'DocumentList - utils - view', () => { it( 'should return 1 for first level nested items', () => { const viewElement = parseView( '
    ' + - '
  • ' + - '
      ' + - '
    • a
    • ' + - '
    • b
    • ' + - '
    ' + - '
  • ' + - '
  • ' + - '
      ' + - '
    1. c
    2. ' + - '
    3. d
    4. ' + - '
    ' + - '
  • ' + + '
  • ' + + '
      ' + + '
    • a
    • ' + + '
    • b
    • ' + + '
    ' + + '
  • ' + + '
  • ' + + '
      ' + + '
    1. c
    2. ' + + '
    3. d
    4. ' + + '
    ' + + '
  • ' + '
' ); @@ -111,20 +111,20 @@ describe( 'DocumentList - utils - view', () => { it( 'should ignore container elements', () => { const viewElement = parseView( '
    ' + - '
  • ' + - '
    ' + - '
      ' + - '
    • a
    • ' + - '
    • b
    • ' + - '
    ' + - '
    ' + - '
  • ' + - '
  • ' + - '
      ' + - '
    • c
    • ' + - '
    • d
    • ' + - '
    ' + - '
  • ' + + '
  • ' + + '
    ' + + '
      ' + + '
    • a
    • ' + + '
    • b
    • ' + + '
    ' + + '
    ' + + '
  • ' + + '
  • ' + + '
      ' + + '
    • c
    • ' + + '
    • d
    • ' + + '
    ' + + '
  • ' + '
' ); @@ -137,16 +137,16 @@ describe( 'DocumentList - utils - view', () => { it( 'should handle deep nesting', () => { const viewElement = parseView( '
    ' + - '
  • ' + - '
      ' + - '
    1. ' + - '
        ' + - '
      • a
      • ' + - '
      • b
      • ' + - '
      ' + - '
    2. ' + - '
    ' + - '
  • ' + + '
  • ' + + '
      ' + + '
    1. ' + + '
        ' + + '
      • a
      • ' + + '
      • b
      • ' + + '
      ' + + '
    2. ' + + '
    ' + + '
  • ' + '
' ); @@ -159,18 +159,18 @@ describe( 'DocumentList - utils - view', () => { it( 'should ignore superfluous OLs', () => { const viewElement = parseView( '
    ' + - '
  • ' + - '
      ' + - '
        ' + - '
          ' + - '
            ' + - '
          1. a
          2. ' + - '
          ' + - '
        ' + - '
      ' + - '
    1. b
    2. ' + - '
    ' + - '
  • ' + + '
  • ' + + '
      ' + + '
        ' + + '
          ' + + '
            ' + + '
          1. a
          2. ' + + '
          ' + + '
        ' + + '
      ' + + '
    1. b
    2. ' + + '
    ' + + '
  • ' + '
' ); @@ -183,10 +183,10 @@ describe( 'DocumentList - utils - view', () => { it( 'should handle broken structure', () => { const viewElement = parseView( '
    ' + - '
  • a
  • ' + - '
      ' + - '
    • b
    • ' + - '
    ' + + '
  • a
  • ' + + '
      ' + + '
    • b
    • ' + + '
    ' + '
' ); @@ -197,13 +197,13 @@ describe( 'DocumentList - utils - view', () => { it( 'should handle broken deeper structure', () => { const viewElement = parseView( '
    ' + - '
  • a
  • ' + - '
      ' + - '
    1. b
    2. ' + - '
        ' + - '
      • c
      • ' + - '
      ' + - '
    ' + + '
  • a
  • ' + + '
      ' + + '
    1. b
    2. ' + + '
        ' + + '
      • c
      • ' + + '
      ' + + '
    ' + '
' ); From 2bc8aa75bd5bf916737ab1258ef50532bb391e13 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 28 Dec 2021 14:39:45 +0100 Subject: [PATCH 28/66] Updating tests. --- .../documentlist/documentlistindentcommand.js | 25 +-- .../tests/documentlist/utils/model.js | 165 +++++++++++++----- .../tests/documentlist/utils/view.js | 122 ++++++------- 3 files changed, 197 insertions(+), 115 deletions(-) diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js index b45955d900a..ee3f3c78d99 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js @@ -141,7 +141,7 @@ describe( 'DocumentListIndentCommand', () => { expect( command.isEnabled ).to.be.true; } ); - it( 'should be false if selection starts in the second block of list item', () => { + it( 'should be true if selection starts in the second block of list item', () => { setData( model, modelList( [ '* 0', '* 1', @@ -149,10 +149,10 @@ describe( 'DocumentListIndentCommand', () => { ' 3' ] ) ); - expect( command.isEnabled ).to.be.false; + expect( command.isEnabled ).to.be.true; } ); - it( 'should be false if selection starts in the last block of list item', () => { + it( 'should be true if selection starts in the last block of list item', () => { setData( model, modelList( [ '* 0', '* 1', @@ -160,7 +160,7 @@ describe( 'DocumentListIndentCommand', () => { ' []3' ] ) ); - expect( command.isEnabled ).to.be.false; + expect( command.isEnabled ).to.be.true; } ); it( 'should be false if selection starts in first list item', () => { @@ -351,7 +351,7 @@ describe( 'DocumentListIndentCommand', () => { ] ) ); } ); - it( 'should do nothing if the following block of bigger list item is selected', () => { + it( 'should change indent (with new ID) if the following block of bigger list item is selected', () => { setData( model, modelList( [ '* 0', '* 1', @@ -360,16 +360,17 @@ describe( 'DocumentListIndentCommand', () => { '* 4' ] ) ); + stubUid(); command.isEnabled = true; command.execute(); - expect( getData( model ) ).to.equalMarkup( modelList( [ - '* 0', - '* 1', - ' []2', - ' 3', - '* 4' - ] ) ); + expect( getData( model ) ).to.equalMarkup( + '0' + + '1' + + '[]2' + + '3' + + '4' + ); } ); it( 'should increment indent of all sub-items of indented item', () => { diff --git a/packages/ckeditor5-list/tests/documentlist/utils/model.js b/packages/ckeditor5-list/tests/documentlist/utils/model.js index 65ee1c4c5a8..80b32bab8b3 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/model.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/model.js @@ -11,6 +11,7 @@ import { indentBlocks, isFirstBlockOfListItem, isLastBlockOfListItem, + outdentBlocks, splitListItemBefore } from '../../../src/documentlist/utils/model'; import { modelList } from '../_utils/utils'; @@ -343,6 +344,10 @@ describe( 'DocumentList - utils - model', () => { } ); } ); + describe( 'getListItems()', () => { + // TODO + } ); + describe( 'isFirstBlockOfListItem()', () => { it( 'should return true for the first list item', () => { const input = @@ -797,6 +802,10 @@ describe( 'DocumentList - utils - model', () => { } ); } ); + describe( 'mergeListItemBefore()', () => { + // TODO + } ); + describe( 'indentBlocks()', () => { it( 'flat items', () => { const input = @@ -813,7 +822,7 @@ describe( 'DocumentList - utils - model', () => { stubUid(); - model.change( writer => indentBlocks( blocks, 1, writer ) ); + model.change( writer => indentBlocks( blocks, writer ) ); expect( stringifyModel( fragment ) ).to.equal( 'a' + @@ -840,7 +849,7 @@ describe( 'DocumentList - utils - model', () => { stubUid(); - model.change( writer => indentBlocks( blocks, 1, writer ) ); + model.change( writer => indentBlocks( blocks, writer ) ); expect( stringifyModel( fragment ) ).to.equal( 'a' + @@ -851,13 +860,44 @@ describe( 'DocumentList - utils - model', () => { ); } ); + it( 'should apply indentation on all blocks of given items (expand = true)', () => { + const input = modelList( [ + '* 0', + '* 1', + ' 2', + '* 3', + ' 4', + '* 5' + ] ); + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 2 ), + fragment.getChild( 3 ) + ]; + + model.change( writer => indentBlocks( blocks, writer, { expand: true } ) ); + + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* 0', + ' * 1', + ' 2', + ' * 3', + ' 4', + '* 5' + ] ) ); + } ); + } ); + + describe( 'outdentBlocks()', () => { it( 'should handle outdenting', () => { - const input = - 'a' + - 'b' + - 'c' + - 'd' + - 'e'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * 3', + '* 4' + ] ); const fragment = parseModel( input, schema ); const blocks = [ @@ -868,24 +908,25 @@ describe( 'DocumentList - utils - model', () => { stubUid(); - model.change( writer => indentBlocks( blocks, -1, writer ) ); + model.change( writer => outdentBlocks( blocks, writer ) ); - expect( stringifyModel( fragment ) ).to.equal( - 'a' + - 'b' + - 'c' + - 'd' + - 'e' - ); + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + ' * 2', + '* 3', + '* 4' + ] ) ); } ); it( 'should remove list attributes if outdented below 0', () => { - const input = - 'a' + - 'b' + - 'c' + - 'd' + - 'e'; + const input = modelList( [ + '* 0', + '* 1', + '* 2', + ' * 3', + '* 4' + ] ); const fragment = parseModel( input, schema ); const blocks = [ @@ -896,24 +937,24 @@ describe( 'DocumentList - utils - model', () => { stubUid(); - model.change( writer => indentBlocks( blocks, -2, writer ) ); + model.change( writer => outdentBlocks( blocks, writer ) ); - expect( stringifyModel( fragment ) ).to.equal( - 'a' + - 'b' + - 'c' + - 'd' + - 'e' - ); + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + '2', + '* 3', + '4' + ] ) ); } ); it( 'should not remove attributes other than lists if outdented below 0', () => { const input = - 'a' + - 'b' + - 'c' + - 'd' + - 'e'; + '0' + + '1' + + '2' + + '3' + + '4'; const fragment = parseModel( input, schema ); const blocks = [ @@ -924,15 +965,55 @@ describe( 'DocumentList - utils - model', () => { stubUid(); - model.change( writer => indentBlocks( blocks, -2, writer ) ); + model.change( writer => outdentBlocks( blocks, writer ) ); - expect( stringifyModel( fragment ) ).to.equal( - 'a' + - 'b' + - 'c' + - 'd' + - 'e' + expect( stringifyModel( fragment ) ).to.equalMarkup( + '0' + + '1' + + '2' + + '3' + + '4' ); } ); + + it( 'should apply indentation on all blocks of given items (expand = true)', () => { + const input = modelList( [ + '* 0', + ' * 1', + ' 2', + ' * 3', + ' 4', + ' * 5' + ] ); + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 2 ), + fragment.getChild( 3 ) + ]; + + model.change( writer => outdentBlocks( blocks, writer, { expand: true } ) ); + + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + ' 2', + '* 3', + ' 4', + ' * 5' + ] ) ); + } ); + } ); + + describe( 'removeListAttributes()', () => { + // TODO + } ); + + describe( 'isOnlyOneListItemSelected()', () => { + // TODO + } ); + + describe( 'outdentItemsAfterItemRemoved()', () => { + // TODO } ); } ); diff --git a/packages/ckeditor5-list/tests/documentlist/utils/view.js b/packages/ckeditor5-list/tests/documentlist/utils/view.js index 8fb43eccacb..89411455a74 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/view.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/view.js @@ -75,8 +75,8 @@ describe( 'DocumentList - utils - view', () => { it( 'should return 0 for flat list', () => { const viewElement = parseView( '
    ' + - '
  • a
  • ' + - '
  • b
  • ' + + '
  • a
  • ' + + '
  • b
  • ' + '
' ); @@ -87,18 +87,18 @@ describe( 'DocumentList - utils - view', () => { it( 'should return 1 for first level nested items', () => { const viewElement = parseView( '
    ' + - '
  • ' + - '
      ' + - '
    • a
    • ' + - '
    • b
    • ' + - '
    ' + - '
  • ' + - '
  • ' + - '
      ' + - '
    1. c
    2. ' + - '
    3. d
    4. ' + - '
    ' + - '
  • ' + + '
  • ' + + '
      ' + + '
    • a
    • ' + + '
    • b
    • ' + + '
    ' + + '
  • ' + + '
  • ' + + '
      ' + + '
    1. c
    2. ' + + '
    3. d
    4. ' + + '
    ' + + '
  • ' + '
' ); @@ -111,20 +111,20 @@ describe( 'DocumentList - utils - view', () => { it( 'should ignore container elements', () => { const viewElement = parseView( '
    ' + - '
  • ' + - '
    ' + - '
      ' + - '
    • a
    • ' + - '
    • b
    • ' + - '
    ' + - '
    ' + - '
  • ' + - '
  • ' + - '
      ' + - '
    • c
    • ' + - '
    • d
    • ' + - '
    ' + - '
  • ' + + '
  • ' + + '
    ' + + '
      ' + + '
    • a
    • ' + + '
    • b
    • ' + + '
    ' + + '
    ' + + '
  • ' + + '
  • ' + + '
      ' + + '
    • c
    • ' + + '
    • d
    • ' + + '
    ' + + '
  • ' + '
' ); @@ -137,16 +137,16 @@ describe( 'DocumentList - utils - view', () => { it( 'should handle deep nesting', () => { const viewElement = parseView( '
    ' + - '
  • ' + - '
      ' + - '
    1. ' + - '
        ' + - '
      • a
      • ' + - '
      • b
      • ' + - '
      ' + - '
    2. ' + - '
    ' + - '
  • ' + + '
  • ' + + '
      ' + + '
    1. ' + + '
        ' + + '
      • a
      • ' + + '
      • b
      • ' + + '
      ' + + '
    2. ' + + '
    ' + + '
  • ' + '
' ); @@ -159,18 +159,18 @@ describe( 'DocumentList - utils - view', () => { it( 'should ignore superfluous OLs', () => { const viewElement = parseView( '
    ' + - '
  • ' + - '
      ' + - '
        ' + - '
          ' + - '
            ' + - '
          1. a
          2. ' + - '
          ' + - '
        ' + - '
      ' + - '
    1. b
    2. ' + - '
    ' + - '
  • ' + + '
  • ' + + '
      ' + + '
        ' + + '
          ' + + '
            ' + + '
          1. a
          2. ' + + '
          ' + + '
        ' + + '
      ' + + '
    1. b
    2. ' + + '
    ' + + '
  • ' + '
' ); @@ -183,10 +183,10 @@ describe( 'DocumentList - utils - view', () => { it( 'should handle broken structure', () => { const viewElement = parseView( '
    ' + - '
  • a
  • ' + - '
      ' + - '
    • b
    • ' + - '
    ' + + '
  • a
  • ' + + '
      ' + + '
    • b
    • ' + + '
    ' + '
' ); @@ -197,13 +197,13 @@ describe( 'DocumentList - utils - view', () => { it( 'should handle broken deeper structure', () => { const viewElement = parseView( '
    ' + - '
  • a
  • ' + - '
      ' + - '
    1. b
    2. ' + - '
        ' + - '
      • c
      • ' + - '
      ' + - '
    ' + + '
  • a
  • ' + + '
      ' + + '
    1. b
    2. ' + + '
        ' + + '
      • c
      • ' + + '
      ' + + '
    ' + '
' ); From 94f4308e1fb2e8960bb4198575dd99b2d2bc849a Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 28 Dec 2021 16:20:15 +0100 Subject: [PATCH 29/66] Added tests. --- .../src/documentlist/utils/model.js | 9 +- .../tests/documentlist/utils/model.js | 140 +++++++++++++++++- 2 files changed, 135 insertions(+), 14 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.js b/packages/ckeditor5-list/src/documentlist/utils/model.js index 49c556ac35f..88a5a26f7d2 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.js +++ b/packages/ckeditor5-list/src/documentlist/utils/model.js @@ -181,7 +181,7 @@ export function splitListItemBefore( listBlock, writer ) { * @param {module:engine/model/element~Element} listBlock The list block element. * @param {module:engine/model/element~Element} parentBlock The list block element to merge with. * @param {module:engine/model/writer~Writer} writer The model writer. - * @returns {Iterable.} The iterable of updated blocks. + * @returns {Array.} The array of updated blocks. */ export function mergeListItemBefore( listBlock, parentBlock, writer ) { const attributes = {}; @@ -192,7 +192,7 @@ export function mergeListItemBefore( listBlock, parentBlock, writer ) { } } - const blocks = new Set( getListItemBlocks( listBlock, { direction: 'forward' } ) ); + const blocks = getListItemBlocks( listBlock, { direction: 'forward' } ); for ( const block of blocks ) { writer.setAttributes( attributes, block ); @@ -416,11 +416,6 @@ export function outdentItemsAfterItemRemoved( lastBlock, writer ) { // 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. function mergeListItemIfNotLast( block, parentBlock, writer ) { - // The parent block could become a non-list block. - if ( !parentBlock.hasAttribute( 'listIndent' ) ) { - return []; - } - const parentItemBlocks = getListItemBlocks( parentBlock, { direction: 'forward' } ); // Merge with parent only if outdented item wasn't the last one in its parent. diff --git a/packages/ckeditor5-list/tests/documentlist/utils/model.js b/packages/ckeditor5-list/tests/documentlist/utils/model.js index 80b32bab8b3..ee6d301af25 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/model.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/model.js @@ -906,9 +906,11 @@ describe( 'DocumentList - utils - model', () => { fragment.getChild( 3 ) ]; - stubUid(); + let changedBlocks; - model.change( writer => outdentBlocks( blocks, writer ) ); + model.change( writer => { + changedBlocks = outdentBlocks( blocks, writer ); + } ); expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ '* 0', @@ -917,6 +919,8 @@ describe( 'DocumentList - utils - model', () => { '* 3', '* 4' ] ) ); + + expect( changedBlocks ).to.deep.equal( blocks ); } ); it( 'should remove list attributes if outdented below 0', () => { @@ -935,9 +939,11 @@ describe( 'DocumentList - utils - model', () => { fragment.getChild( 4 ) ]; - stubUid(); + let changedBlocks; - model.change( writer => outdentBlocks( blocks, writer ) ); + model.change( writer => { + changedBlocks = outdentBlocks( blocks, writer ); + } ); expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ '* 0', @@ -946,6 +952,8 @@ describe( 'DocumentList - utils - model', () => { '* 3', '4' ] ) ); + + expect( changedBlocks ).to.deep.equal( blocks ); } ); it( 'should not remove attributes other than lists if outdented below 0', () => { @@ -963,9 +971,11 @@ describe( 'DocumentList - utils - model', () => { fragment.getChild( 4 ) ]; - stubUid(); + let changedBlocks; - model.change( writer => outdentBlocks( blocks, writer ) ); + model.change( writer => { + changedBlocks = outdentBlocks( blocks, writer ); + } ); expect( stringifyModel( fragment ) ).to.equalMarkup( '0' + @@ -974,6 +984,8 @@ describe( 'DocumentList - utils - model', () => { '3' + '4' ); + + expect( changedBlocks ).to.deep.equal( blocks ); } ); it( 'should apply indentation on all blocks of given items (expand = true)', () => { @@ -992,7 +1004,11 @@ describe( 'DocumentList - utils - model', () => { fragment.getChild( 3 ) ]; - model.change( writer => outdentBlocks( blocks, writer, { expand: true } ) ); + let changedBlocks; + + model.change( writer => { + changedBlocks = outdentBlocks( blocks, writer, { expand: true } ); + } ); expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ '* 0', @@ -1002,6 +1018,116 @@ describe( 'DocumentList - utils - model', () => { ' 4', ' * 5' ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + fragment.getChild( 1 ), + fragment.getChild( 2 ), + fragment.getChild( 3 ), + fragment.getChild( 4 ) + ] ); + } ); + + it( 'should merge nested items to the parent item if nested block is not the last block of parent list item', () => { + const input = modelList( [ + '* 0', + ' * 1', + ' 2', + ' 3', + '* 4' + ] ); + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 1 ) + ]; + + let changedBlocks; + + model.change( writer => { + changedBlocks = outdentBlocks( blocks, writer, { expand: true } ); + } ); + + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* 0', + ' 1', + ' 2', + ' 3', + '* 4' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + fragment.getChild( 1 ), + fragment.getChild( 2 ) + ] ); + } ); + + it( 'should not merge nested items to the parent item if nested block is the last block of parent list item', () => { + const input = modelList( [ + '* 0', + ' * 1', + ' 2', + '* 3', + '* 4' + ] ); + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 1 ) + ]; + + let changedBlocks; + + model.change( writer => { + changedBlocks = outdentBlocks( blocks, writer, { expand: true } ); + } ); + + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + ' 2', + '* 3', + '* 4' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + fragment.getChild( 1 ), + fragment.getChild( 2 ) + ] ); + } ); + + it( 'should merge nested items but not deeper nested lists', () => { + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * 3', + '* 4' + ] ); + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 1 ) + ]; + + let changedBlocks; + + model.change( writer => { + changedBlocks = outdentBlocks( blocks, writer, { expand: true } ); + } ); + + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + ' * 2', + ' * 3', + '* 4' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + fragment.getChild( 1 ), + fragment.getChild( 2 ), + fragment.getChild( 3 ) + ] ); } ); } ); From 68a74bad00b1500ee2161c1a4b6412bae07b8d00 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 28 Dec 2021 16:31:13 +0100 Subject: [PATCH 30/66] Added tests. --- .../tests/documentlist/utils/model.js | 135 +++++++++++++++++- 1 file changed, 134 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-list/tests/documentlist/utils/model.js b/packages/ckeditor5-list/tests/documentlist/utils/model.js index ee6d301af25..8dea9695f6a 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/model.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/model.js @@ -7,6 +7,7 @@ import { expandListBlocksToCompleteItems, getAllListItemBlocks, getListItemBlocks, + getListItems, getNestedListBlocks, indentBlocks, isFirstBlockOfListItem, @@ -345,7 +346,139 @@ describe( 'DocumentList - utils - model', () => { } ); describe( 'getListItems()', () => { - // TODO + it( 'should return all list items for a single flat list (when given the first list item)', () => { + const input = modelList( [ + '0', + '* 1', + '* 2', + '* 3', + '4' + ] ); + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 1 ); + + expect( getListItems( listItem ) ).to.deep.equal( [ + fragment.getChild( 1 ), + fragment.getChild( 2 ), + fragment.getChild( 3 ) + ] ); + } ); + + it( 'should return all list items for a single flat list (when given the last list item)', () => { + const input = modelList( [ + '0', + '* 1', + '* 2', + '* 3', + '4' + ] ); + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 3 ); + + expect( getListItems( listItem ) ).to.deep.equal( [ + fragment.getChild( 1 ), + fragment.getChild( 2 ), + fragment.getChild( 3 ) + ] ); + } ); + + it( 'should return all list items for a single flat list (when given the middle list item)', () => { + const input = modelList( [ + '0', + '* 1', + '* 2', + '* 3', + '4' + ] ); + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 2 ); + + expect( getListItems( listItem ) ).to.deep.equal( [ + fragment.getChild( 1 ), + fragment.getChild( 2 ), + fragment.getChild( 3 ) + ] ); + } ); + + it( 'should return all list items for a nested list', () => { + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * 3', + '* 4' + ] ); + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 2 ); + + expect( getListItems( listItem ) ).to.deep.equal( [ + fragment.getChild( 1 ), + fragment.getChild( 2 ), + fragment.getChild( 3 ) + ] ); + } ); + + it( 'should return all list items of the same type', () => { + const input = modelList( [ + '# 0', + '* 1', + '* 2', + '* 3', + '# 4' + ] ); + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 2 ); + + expect( getListItems( listItem ) ).to.deep.equal( [ + fragment.getChild( 1 ), + fragment.getChild( 2 ), + fragment.getChild( 3 ) + ] ); + } ); + + it( 'should return all list items and ignore nested lists', () => { + const input = modelList( [ + '0', + '* 1', + ' * 2', + '* 3', + '4' + ] ); + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 1 ); + + expect( getListItems( listItem ) ).to.deep.equal( [ + fragment.getChild( 1 ), + fragment.getChild( 3 ) + ] ); + } ); + + it( 'should return all list items with following blocks belonging to the same item', () => { + const input = modelList( [ + '0', + '* 1', + ' 2', + '* 3', + ' 4', + '5' + ] ); + + const fragment = parseModel( input, schema ); + const listItem = fragment.getChild( 2 ); + + expect( getListItems( listItem ) ).to.deep.equal( [ + fragment.getChild( 1 ), + fragment.getChild( 2 ), + fragment.getChild( 3 ), + fragment.getChild( 4 ) + ] ); + } ); } ); describe( 'isFirstBlockOfListItem()', () => { From 5268746ca8c17ef0c16045d47459193c6ff04549 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 28 Dec 2021 16:49:47 +0100 Subject: [PATCH 31/66] Added tests. --- .../tests/documentlist/utils/model.js | 78 ++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-list/tests/documentlist/utils/model.js b/packages/ckeditor5-list/tests/documentlist/utils/model.js index 8dea9695f6a..03d02a45857 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/model.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/model.js @@ -12,6 +12,7 @@ import { indentBlocks, isFirstBlockOfListItem, isLastBlockOfListItem, + mergeListItemBefore, outdentBlocks, splitListItemBefore } from '../../../src/documentlist/utils/model'; @@ -936,7 +937,82 @@ describe( 'DocumentList - utils - model', () => { } ); describe( 'mergeListItemBefore()', () => { - // TODO + it( 'should apply parent list attributes to the given list block', () => { + const input = modelList( [ + '* 0', + ' # 1', + '* 2' + ] ); + + const fragment = parseModel( input, schema ); + let changedBlocks; + + model.change( writer => { + changedBlocks = mergeListItemBefore( fragment.getChild( 1 ), fragment.getChild( 0 ), writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* 0', + ' 1', + '* 2' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + fragment.getChild( 1 ) + ] ); + } ); + + it( 'should apply parent list attributes to the given list block and all blocks of the same item', () => { + const input = modelList( [ + '* 0', + ' # 1', + ' 2', + '* 3' + ] ); + + const fragment = parseModel( input, schema ); + let changedBlocks; + + model.change( writer => { + changedBlocks = mergeListItemBefore( fragment.getChild( 1 ), fragment.getChild( 0 ), writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* 0', + ' 1', + ' 2', + '* 3' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + fragment.getChild( 1 ), + fragment.getChild( 2 ) + ] ); + } ); + + it( 'should not apply non-list attributes', () => { + const input = + '0' + + '1' + + '2'; + + const fragment = parseModel( input, schema ); + let changedBlocks; + + model.change( writer => { + changedBlocks = mergeListItemBefore( fragment.getChild( 1 ), fragment.getChild( 0 ), writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equalMarkup( + '0' + + '1' + + '2' + ); + + expect( changedBlocks ).to.deep.equal( [ + fragment.getChild( 1 ) + ] ); + } ); } ); describe( 'indentBlocks()', () => { From 5baf3028b03a04e842116e281147d7d5ef05bcfb Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 28 Dec 2021 17:05:21 +0100 Subject: [PATCH 32/66] Code cleanup. --- .../src/documentlist/converters.js | 8 +- .../src/documentlist/documentlistediting.js | 97 +++++++++---------- 2 files changed, 50 insertions(+), 55 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/converters.js b/packages/ckeditor5-list/src/documentlist/converters.js index 1200f7485d8..112c35d40ed 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.js +++ b/packages/ckeditor5-list/src/documentlist/converters.js @@ -3,6 +3,10 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ +/** + * @module list/documentlist/converters + */ + import { getAllListItemBlocks, getListItemBlocks @@ -21,10 +25,6 @@ import { findAndAddListHeadToMap } from './utils/postfixers'; import { uid } from 'ckeditor5/src/utils'; import { UpcastWriter } from 'ckeditor5/src/engine'; -/** - * @module list/documentlist/converters - */ - /** * Returns the upcast converter for list items. It's supposed to work after the block converters (content inside list items) is converted. * diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.js b/packages/ckeditor5-list/src/documentlist/documentlistediting.js index f04ffe30c51..5c09a8ded1d 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.js @@ -72,17 +72,16 @@ export default class DocumentListEditing extends Plugin { allowAttributes: [ 'listType', 'listIndent', 'listItemId' ] } ); - // Register commands for numbered and bulleted list. - editor.commands.add( 'numberedList', new DocumentListCommand( editor, 'numbered' ) ); - editor.commands.add( 'bulletedList', new DocumentListCommand( editor, 'bulleted' ) ); - model.document.registerPostFixer( writer => modelChangePostFixer( model, writer ) ); model.on( 'insertContent', createModelIndentPasteFixer( model ), { priority: 'high' } ); this._setupConversion(); - // Register commands for indenting. + // Register commands. + editor.commands.add( 'numberedList', new DocumentListCommand( editor, 'numbered' ) ); + editor.commands.add( 'bulletedList', new DocumentListCommand( editor, 'bulleted' ) ); + editor.commands.add( 'indentList', new DocumentListIndentCommand( editor, 'forward' ) ); editor.commands.add( 'outdentList', new DocumentListIndentCommand( editor, 'backward' ) ); } @@ -138,31 +137,29 @@ export default class DocumentListEditing extends Plugin { } } -/** - * Post-fixer that reacts to changes on document and fixes incorrect model states. - * - * In the example below, there is a correct list structure. - * Then the middle element is removed so the list structure will become incorrect: - * - * Item 1 - * Item 2 <--- this is removed. - * Item 3 - * - * The list structure after the middle element is removed: - * - * Item 1 - * Item 3 - * - * Should become: - * - * Item 1 - * Item 3 <--- note that indent got post-fixed. - * - * @param {module:engine/model/model~Model} model The data model. - * @param {module:engine/model/writer~Writer} writer The writer to do changes with. - * @returns {Boolean} `true` if any change has been applied, `false` otherwise. - */ -export function modelChangePostFixer( model, writer ) { +// Post-fixer that reacts to changes on document and fixes incorrect model states. +// +// In the example below, there is a correct list structure. +// Then the middle element is removed so the list structure will become incorrect: +// +// Item 1 +// Item 2 <--- this is removed. +// Item 3 +// +// The list structure after the middle element is removed: +// +// Item 1 +// Item 3 +// +// Should become: +// +// Item 1 +// Item 3 <--- note that indent got post-fixed. +// +// @param {module:engine/model/model~Model} model The data model. +// @param {module:engine/model/writer~Writer} writer The writer to do changes with. +// @returns {Boolean} `true` if any change has been applied, `false` otherwise. +function modelChangePostFixer( model, writer ) { const changes = model.document.differ.getChanges(); const itemToListHead = new Map(); @@ -222,27 +219,25 @@ export function modelChangePostFixer( model, writer ) { return applied; } -/** - * A fixer for pasted content that includes list items. - * - * It fixes indentation of pasted list items so the pasted items match correctly to the context they are pasted into. - * - * Example: - * - * A - * B^ - * // At ^ paste: X - * // Y - * C - * - * Should become: - * - * A - * BX - * Y/paragraph> - * C - * - */ +// A fixer for pasted content that includes list items. +// +// It fixes indentation of pasted list items so the pasted items match correctly to the context they are pasted into. +// +// Example: +// +// A +// B^ +// // At ^ paste: X +// // Y +// C +// +// Should become: +// +// A +// BX +// Y/paragraph> +// C +// function createModelIndentPasteFixer( model ) { return ( evt, [ content, selectable ] ) => { // Check whether inserted content starts from a `listItem`. If it does not, it means that there are some other From b32a2b9e32a76634d3d4c6bb74b5bacc24c9519a Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 28 Dec 2021 17:09:56 +0100 Subject: [PATCH 33/66] Added tests. --- .../tests/documentlist/utils/model.js | 123 +++++++++++++++++- 1 file changed, 121 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-list/tests/documentlist/utils/model.js b/packages/ckeditor5-list/tests/documentlist/utils/model.js index 03d02a45857..493d6b2cf8f 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/model.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/model.js @@ -12,8 +12,10 @@ import { indentBlocks, isFirstBlockOfListItem, isLastBlockOfListItem, + isOnlyOneListItemSelected, mergeListItemBefore, outdentBlocks, + removeListAttributes, splitListItemBefore } from '../../../src/documentlist/utils/model'; import { modelList } from '../_utils/utils'; @@ -1341,11 +1343,128 @@ describe( 'DocumentList - utils - model', () => { } ); describe( 'removeListAttributes()', () => { - // TODO + it( 'should remove all list attributes on a given blocks', () => { + const input = modelList( [ + '* 0', + '* 1', + ' * 2', + ' 3', + ' * 4', + '* 5' + ] ); + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 2 ), + fragment.getChild( 3 ), + fragment.getChild( 4 ) + ]; + + let changedBlocks; + + model.change( writer => { + changedBlocks = removeListAttributes( blocks, writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + '2', + '3', + '4', + '* 5' + ] ) ); + + expect( changedBlocks ).to.deep.equal( blocks ); + } ); + + it( 'should not remove non-list attributes', () => { + const input = modelList( [ + '* 0', + '* 1', + ' * 2', + ' 3', + ' * 4', + '* 5' + ] ); + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 2 ), + fragment.getChild( 3 ), + fragment.getChild( 4 ) + ]; + + let changedBlocks; + + model.change( writer => { + changedBlocks = removeListAttributes( blocks, writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + '2', + '3', + '4', + '* 5' + ] ) ); + + expect( changedBlocks ).to.deep.equal( blocks ); + } ); } ); describe( 'isOnlyOneListItemSelected()', () => { - // TODO + it( 'should return false if no blocks are given', () => { + expect( isOnlyOneListItemSelected( [] ) ).to.be.false; + } ); + + it( 'should return false if first block is not a list item', () => { + const input = modelList( [ + '0', + '1' + ] ); + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 1 ) + ]; + + expect( isOnlyOneListItemSelected( blocks ) ).to.be.false; + } ); + + it( 'should return false if any block has a different ID', () => { + const input = modelList( [ + '* 0', + ' 1', + '* 2' + ] ); + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 0 ), + fragment.getChild( 1 ), + fragment.getChild( 2 ) + ]; + + expect( isOnlyOneListItemSelected( blocks ) ).to.be.false; + } ); + + it( 'should return true if all block has the same ID', () => { + const input = modelList( [ + '* 0', + ' 1', + '* 2' + ] ); + + const fragment = parseModel( input, schema ); + const blocks = [ + fragment.getChild( 0 ), + fragment.getChild( 1 ) + ]; + + expect( isOnlyOneListItemSelected( blocks ) ).to.be.true; + } ); } ); describe( 'outdentItemsAfterItemRemoved()', () => { From b29f0fcf1e179265422216a20f2fa2d377374c61 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 28 Dec 2021 17:46:46 +0100 Subject: [PATCH 34/66] Added tests. --- .../tests/documentlist/utils/model.js | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-list/tests/documentlist/utils/model.js b/packages/ckeditor5-list/tests/documentlist/utils/model.js index 493d6b2cf8f..462f200a3a8 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/model.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/model.js @@ -15,6 +15,7 @@ import { isOnlyOneListItemSelected, mergeListItemBefore, outdentBlocks, + outdentItemsAfterItemRemoved, removeListAttributes, splitListItemBefore } from '../../../src/documentlist/utils/model'; @@ -1468,6 +1469,60 @@ describe( 'DocumentList - utils - model', () => { } ); describe( 'outdentItemsAfterItemRemoved()', () => { - // TODO + it( 'should outdent all items and keep nesting structure where possible', () => { + const input = modelList( [ + '0', + '* 1', + ' * 2', + ' * 3', // <- this is turned off. + ' * 4', // <- this has to become indent = 0, because it will be first item on a new list. + ' * 5', // <- this should be still be a child of item above, so indent = 1. + ' * 6', // <- this has to become indent = 0, because it should not be a child of any of items above. + ' * 7', // <- this should be still be a child of item above, so indent = 1. + ' * 8', // <- this has to become indent = 0. + ' * 9', // <- this should still be a child of item above, so indent = 1. + ' * 10', // <- this should still be a child of item above, so indent = 2. + ' * 11', // <- this should still be at the same level as item above, so indent = 2. + '* 12', // <- this and all below are left unchanged. + ' * 13', + ' * 14' + ] ); + + const fragment = parseModel( input, schema ); + let changedBlocks; + + model.change( writer => { + changedBlocks = outdentItemsAfterItemRemoved( fragment.getChild( 3 ), writer ); + } ); + + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '0', + '* 1', + ' * 2', + ' * 3', + '* 4', + ' * 5', + '* 6', + ' * 7', + '* 8', + ' * 9', + ' * 10', + ' * 11', + '* 12', + ' * 13', + ' * 14' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + fragment.getChild( 4 ), + fragment.getChild( 5 ), + fragment.getChild( 6 ), + fragment.getChild( 7 ), + fragment.getChild( 8 ), + fragment.getChild( 9 ), + fragment.getChild( 10 ), + fragment.getChild( 11 ) + ] ); + } ); } ); } ); From b48ea1845f954500250465ab0bec5555fe72b427 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 28 Dec 2021 18:11:14 +0100 Subject: [PATCH 35/66] Rename smaller -> lower and bigger -> higher. --- .../src/documentlist/converters.js | 4 +- .../src/documentlist/utils/listwalker.js | 24 ++-- .../src/documentlist/utils/model.js | 10 +- .../tests/documentlist/_utils-tests/utils.js | 2 +- .../tests/documentlist/converters-changes.js | 36 +++--- .../documentlist/documentlistindentcommand.js | 8 +- .../tests/documentlist/utils/listwalker.js | 104 +++++++++++------- .../tests/documentlist/utils/model.js | 8 +- 8 files changed, 109 insertions(+), 87 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/converters.js b/packages/ckeditor5-list/src/documentlist/converters.js index 112c35d40ed..a004dc8ead7 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.js +++ b/packages/ckeditor5-list/src/documentlist/converters.js @@ -447,9 +447,9 @@ function wrapListItemBlock( listItem, viewRange, writer ) { break; } - currentListItem = ListWalker.first( currentListItem, { smallerIndent: true } ); + 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; diff --git a/packages/ckeditor5-list/src/documentlist/utils/listwalker.js b/packages/ckeditor5-list/src/documentlist/utils/listwalker.js index 08b9193b751..4493bc2d5f1 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/listwalker.js +++ b/packages/ckeditor5-list/src/documentlist/utils/listwalker.js @@ -25,9 +25,9 @@ export default class ListWalker { * @param {Boolean} [options.sameItemType=false] Whether should return only blocks wit the same `listType` attribute. * @param {Boolean} [options.sameIndent=false] Whether blocks with the same indent level as the start block should be included * in the result. - * @param {Boolean} [options.smallerIndent=false] Whether blocks with a smaller indent level than the start block should be included + * @param {Boolean} [options.lowerIndent=false] Whether blocks with a lower indent level than the start block should be included * in the result. - * @param {Boolean} [options.biggerIndent=false] Whether blocks with a bigger indent level than the start block should be included + * @param {Boolean} [options.higherIndent=false] Whether blocks with a higher indent level than the start block should be included * in the result. */ constructor( startElement, options ) { @@ -104,20 +104,20 @@ export default class ListWalker { this._sameIndent = !!options.sameIndent; /** - * Whether blocks with a smaller indent level than the start block should be included in the result. + * Whether blocks with a lower indent level than the start block should be included in the result. * * @private * @type {Boolean} */ - this._smallerIndent = !!options.smallerIndent; + this._lowerIndent = !!options.lowerIndent; /** - * Whether blocks with a bigger indent level than the start block should be included in the result. + * Whether blocks with a higher indent level than the start block should be included in the result. * * @private * @type {Boolean} */ - this._biggerIndent = !!options.biggerIndent; + this._higherIndent = !!options.higherIndent; } /** @@ -132,9 +132,9 @@ export default class ListWalker { * @param {Boolean} [options.sameItemType=false] Whether should return only blocks wit the same `listType` attribute. * @param {Boolean} [options.sameIndent=false] Whether blocks with the same indent level as the start block should be included * in the result. - * @param {Boolean} [options.smallerIndent=false] Whether blocks with a smaller indent level than the start block should be included + * @param {Boolean} [options.lowerIndent=false] Whether blocks with a lower indent level than the start block should be included * in the result. - * @param {Boolean} [options.biggerIndent=false] Whether blocks with a bigger indent level than the start block should be included + * @param {Boolean} [options.higherIndent=false] Whether blocks with a higher indent level than the start block should be included * in the result. * @returns {module:engine/model/element~Element|null} */ @@ -159,17 +159,17 @@ export default class ListWalker { // Leaving a nested list. if ( indent < this._referenceIndent ) { // Abort searching blocks. - if ( !this._smallerIndent ) { + if ( !this._lowerIndent ) { break; } - // While searching for smaller indents, update the reference indent to find another parent in the next step. + // While searching for lower indents, update the reference indent to find another parent in the next step. this._referenceIndent = indent; } // Entering a nested list. else if ( indent > this._referenceIndent ) { // Ignore nested blocks. - if ( !this._biggerIndent ) { + if ( !this._higherIndent ) { continue; } @@ -185,7 +185,7 @@ export default class ListWalker { // Ignore same indent block. if ( !this._sameIndent ) { // While looking for nested blocks, stop iterating while encountering first same indent block. - if ( this._biggerIndent ) { + if ( this._higherIndent ) { // No more nested blocks so yield nested items. if ( nestedItems.length ) { yield* nestedItems; diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.js b/packages/ckeditor5-list/src/documentlist/utils/model.js index 88a5a26f7d2..c4d301a7d55 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.js +++ b/packages/ckeditor5-list/src/documentlist/utils/model.js @@ -64,7 +64,7 @@ export function getListItemBlocks( listItem, options = {} ) { export function getNestedListBlocks( listItem ) { return Array.from( new ListWalker( listItem, { direction: 'forward', - biggerIndent: true + higherIndent: true } ) ); } @@ -147,11 +147,11 @@ export function isLastBlockOfListItem( listBlock ) { export function expandListBlocksToCompleteItems( blocks, options = {} ) { blocks = toArray( blocks ); - const biggerIndent = options.withNested !== false; + const higherIndent = options.withNested !== false; const allBlocks = new Set(); for ( const block of blocks ) { - for ( const itemBlock of getAllListItemBlocks( block, { biggerIndent } ) ) { + for ( const itemBlock of getAllListItemBlocks( block, { higherIndent } ) ) { allBlocks.add( itemBlock ); } } @@ -246,7 +246,7 @@ export function outdentBlocks( blocks, writer, { expand } = {} ) { // Collect parent blocks before the list structure gets altered. for ( const block of allBlocks ) { - parentBlocks.set( block, ListWalker.first( block, { smallerIndent: true } ) ); + parentBlocks.set( block, ListWalker.first( block, { lowerIndent: true } ) ); } for ( const block of allBlocks ) { @@ -386,7 +386,7 @@ export function outdentItemsAfterItemRemoved( lastBlock, writer ) { // it like this is much more efficient because it's less operation (less memory usage, easier OT) and // less conversion (faster). for ( const { node } of iterateSiblingListBlocks( lastBlock.nextSibling, 'forward' ) ) { - // Check each next list item, as long as its indent is bigger than 0. + // Check each next list item, as long as its indent is higher than 0. const indent = node.getAttribute( 'listIndent' ); // If the indent is 0 we are not going to change anything anyway. diff --git a/packages/ckeditor5-list/tests/documentlist/_utils-tests/utils.js b/packages/ckeditor5-list/tests/documentlist/_utils-tests/utils.js index 1d07986e34a..b66901858b8 100644 --- a/packages/ckeditor5-list/tests/documentlist/_utils-tests/utils.js +++ b/packages/ckeditor5-list/tests/documentlist/_utils-tests/utils.js @@ -170,7 +170,7 @@ describe( 'mockList()', () => { ); } ); - it( 'list with bigger indent drop', () => { + it( 'list with higher indent drop', () => { expect( modelList( [ '* foo', ' * bar', diff --git a/packages/ckeditor5-list/tests/documentlist/converters-changes.js b/packages/ckeditor5-list/tests/documentlist/converters-changes.js index 728e52c5fc5..255a19052fd 100644 --- a/packages/ckeditor5-list/tests/documentlist/converters-changes.js +++ b/packages/ckeditor5-list/tests/documentlist/converters-changes.js @@ -1336,7 +1336,7 @@ describe( 'DocumentListEditing - converters - changes', () => { describe( 'nested lists', () => { describe( 'insert', () => { describe( 'same list type', () => { - it( 'after smaller indent', () => { + it( 'after lower indent', () => { test.insert( 'p' + '1' + @@ -1356,7 +1356,7 @@ describe( 'DocumentListEditing - converters - changes', () => { expect( test.reconvertSpy.callCount ).to.equal( 0 ); } ); - it( 'after smaller indent (multi block)', () => { + it( 'after lower indent (multi block)', () => { test.insert( 'p' + '1a' + @@ -1382,7 +1382,7 @@ describe( 'DocumentListEditing - converters - changes', () => { expect( test.reconvertSpy.callCount ).to.equal( 0 ); } ); - it( 'after smaller indent, before same indent', () => { + it( 'after lower indent, before same indent', () => { test.insert( 'p' + '1' + @@ -1404,7 +1404,7 @@ describe( 'DocumentListEditing - converters - changes', () => { expect( test.reconvertSpy.callCount ).to.equal( 0 ); } ); - it( 'after smaller indent, before same indent (multi block)', () => { + it( 'after lower indent, before same indent (multi block)', () => { test.insert( 'p' + '1a' + @@ -1436,7 +1436,7 @@ describe( 'DocumentListEditing - converters - changes', () => { expect( test.reconvertSpy.callCount ).to.equal( 0 ); } ); - it( 'after smaller indent, before smaller indent', () => { + it( 'after lower indent, before lower indent', () => { test.insert( 'p' + '1' + @@ -1458,7 +1458,7 @@ describe( 'DocumentListEditing - converters - changes', () => { expect( test.reconvertSpy.callCount ).to.equal( 0 ); } ); - it( 'after smaller indent, before smaller indent (multi block)', () => { + it( 'after lower indent, before lower indent (multi block)', () => { test.insert( 'p' + '1a' + @@ -1544,7 +1544,7 @@ describe( 'DocumentListEditing - converters - changes', () => { expect( test.reconvertSpy.callCount ).to.equal( 0 ); } ); - it( 'after same indent, before bigger indent', () => { + it( 'after same indent, before higher indent', () => { test.insert( 'p' + '1' + @@ -1567,7 +1567,7 @@ describe( 'DocumentListEditing - converters - changes', () => { expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 3 ) ); } ); - it( 'after same indent, before bigger indent (multi block)', () => { + it( 'after same indent, before higher indent (multi block)', () => { test.insert( 'p' + '1a' + @@ -1601,7 +1601,7 @@ describe( 'DocumentListEditing - converters - changes', () => { expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 6 ) ); } ); - it( 'after bigger indent, before bigger indent', () => { + it( 'after higher indent, before higher indent', () => { test.insert( 'p' + '1' + @@ -1630,7 +1630,7 @@ describe( 'DocumentListEditing - converters - changes', () => { expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 4 ) ); } ); - it( 'after bigger indent, before bigger indent( multi block)', () => { + it( 'after higher indent, before higher indent( multi block)', () => { test.insert( 'p' + '1' + @@ -1702,7 +1702,7 @@ describe( 'DocumentListEditing - converters - changes', () => { ); } ); - it( 'additional block before bigger indent', () => { + it( 'additional block before higher indent', () => { test.insert( 'p' + '1' + @@ -1727,7 +1727,7 @@ describe( 'DocumentListEditing - converters - changes', () => { } ); describe( 'different list type', () => { - it( 'after smaller indent, before same indent', () => { + it( 'after lower indent, before same indent', () => { test.insert( 'p' + '1' + @@ -1771,7 +1771,7 @@ describe( 'DocumentListEditing - converters - changes', () => { ); } ); - it( 'after same indent, before bigger indent', () => { + it( 'after same indent, before higher indent', () => { test.insert( 'p' + '1' + @@ -1793,7 +1793,7 @@ describe( 'DocumentListEditing - converters - changes', () => { ); } ); - it( 'after bigger indent, before bigger indent', () => { + it( 'after higher indent, before higher indent', () => { test.insert( 'p' + '1' + @@ -1821,7 +1821,7 @@ describe( 'DocumentListEditing - converters - changes', () => { ); } ); - it( 'after bigger indent, in nested list, different type', () => { + it( 'after higher indent, in nested list, different type', () => { test.insert( 'a' + 'b' + @@ -2088,7 +2088,7 @@ describe( 'DocumentListEditing - converters - changes', () => { ); } ); - it( 'item that has nested lists, previous item has smaller indent', () => { + it( 'item that has nested lists, previous item has lower indent', () => { test.remove( 'a' + '[b]' + @@ -2107,7 +2107,7 @@ describe( 'DocumentListEditing - converters - changes', () => { ); } ); - it( 'item that has nested lists, previous item has bigger indent by 1', () => { + it( 'item that has nested lists, previous item has higher indent by 1', () => { test.remove( 'a' + 'b' + @@ -2132,7 +2132,7 @@ describe( 'DocumentListEditing - converters - changes', () => { ); } ); - it( 'item that has nested lists, previous item has bigger indent by 2', () => { + it( 'item that has nested lists, previous item has higher indent by 2', () => { test.remove( 'a' + 'b' + diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js index ee3f3c78d99..109d1be8b7a 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js @@ -104,7 +104,7 @@ describe( 'DocumentListIndentCommand', () => { expect( command.isEnabled ).to.be.false; } ); - it( 'should be false if selection starts in a list item that has bigger indent than it\'s previous sibling', () => { + it( 'should be false if selection starts in a list item that has higher indent than it\'s previous sibling', () => { setData( model, modelList( [ '* 0', '* 1', @@ -536,7 +536,7 @@ describe( 'DocumentListIndentCommand', () => { expect( command.isEnabled ).to.be.true; } ); - it( 'should be true if selection starts in a list item that has bigger indent than it\'s previous sibling', () => { + it( 'should be true if selection starts in a list item that has higher indent than it\'s previous sibling', () => { setData( model, modelList( [ '* 0', '* 1', @@ -582,7 +582,7 @@ describe( 'DocumentListIndentCommand', () => { } ); describe( 'execute()', () => { - it( 'should decrement indent attribute by 1 (if it is bigger than 0)', () => { + it( 'should decrement indent attribute by 1 (if it is higher than 0)', () => { setData( model, modelList( [ '* 0', '* 1', @@ -772,7 +772,7 @@ describe( 'DocumentListIndentCommand', () => { ] ) ); } ); - it( 'should handle bigger indent drop between items', () => { + it( 'should handle higher indent drop between items', () => { setData( model, modelList( [ '* 0', ' * 1', diff --git a/packages/ckeditor5-list/tests/documentlist/utils/listwalker.js b/packages/ckeditor5-list/tests/documentlist/utils/listwalker.js index e53fc0eb51a..cb7a818254b 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/listwalker.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/listwalker.js @@ -4,6 +4,7 @@ */ import ListWalker from '../../../src/documentlist/utils/listwalker'; +import { modelList } from '../_utils/utils'; import Model from '@ckeditor/ckeditor5-engine/src/model/model'; import { parse as parseModel } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; @@ -20,7 +21,7 @@ describe( 'DocumentList - utils - ListWalker', () => { schema.extend( '$container', { allowAttributes: [ 'listType', 'listIndent', 'listItemId' ] } ); } ); - it( 'should return no blocks (sameIndent = false, smallerIndent = false, biggerIndent = false)', () => { + it( 'should return no blocks (sameIndent = false, lowerIndent = false, higherIndent = false)', () => { const input = '0' + '1' + @@ -31,8 +32,8 @@ describe( 'DocumentList - utils - ListWalker', () => { direction: 'forward', includeSelf: true // sameIndent: false -> default - // smallerIndent: false -> default - // biggerIndent: false -> default + // lowerIndent: false -> default + // higherIndent: false -> default } ); const blocks = Array.from( walker ); @@ -155,6 +156,27 @@ describe( 'DocumentList - utils - ListWalker', () => { expect( blocks[ 1 ] ).to.equal( fragment.getChild( 1 ) ); } ); + it( 'should return items of the same type', () => { + const input = modelList( [ + '* 0', + '* 1', + '# 2' + ] ); + + const fragment = parseModel( input, schema ); + const walker = new ListWalker( fragment.getChild( 0 ), { + direction: 'forward', + sameIndent: true, + includeSelf: true, + sameItemType: true + } ); + const blocks = Array.from( walker ); + + expect( blocks.length ).to.equal( 2 ); + expect( blocks[ 0 ] ).to.equal( fragment.getChild( 0 ) ); + expect( blocks[ 1 ] ).to.equal( fragment.getChild( 1 ) ); + } ); + it( 'should return items while iterating over a nested list', () => { const input = '0' + @@ -175,7 +197,7 @@ describe( 'DocumentList - utils - ListWalker', () => { expect( blocks[ 1 ] ).to.equal( fragment.getChild( 2 ) ); } ); - it( 'should skip nested items (biggerIndent = false)', () => { + it( 'should skip nested items (higherIndent = false)', () => { const input = '0' + '1' + @@ -195,7 +217,7 @@ describe( 'DocumentList - utils - ListWalker', () => { expect( blocks[ 1 ] ).to.equal( fragment.getChild( 3 ) ); } ); - it( 'should include nested items (biggerIndent = true)', () => { + it( 'should include nested items (higherIndent = true)', () => { const input = '0' + '1' + @@ -208,7 +230,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const walker = new ListWalker( fragment.getChild( 1 ), { direction: 'forward', sameIndent: true, - biggerIndent: true, + higherIndent: true, includeSelf: true } ); const blocks = Array.from( walker ); @@ -220,7 +242,7 @@ describe( 'DocumentList - utils - ListWalker', () => { expect( blocks[ 3 ] ).to.equal( fragment.getChild( 4 ) ); } ); - it( 'should include nested items (biggerIndent = true, sameItemId = true, forward)', () => { + it( 'should include nested items (higherIndent = true, sameItemId = true, forward)', () => { const input = '0' + '1' + @@ -233,7 +255,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const walker = new ListWalker( fragment.getChild( 1 ), { direction: 'forward', sameIndent: true, - biggerIndent: true, + higherIndent: true, includeSelf: true, sameItemId: true } ); @@ -246,7 +268,7 @@ describe( 'DocumentList - utils - ListWalker', () => { expect( blocks[ 3 ] ).to.equal( fragment.getChild( 4 ) ); } ); - it( 'should include nested items (biggerIndent = true, sameItemId = true, backward)', () => { + it( 'should include nested items (higherIndent = true, sameItemId = true, backward)', () => { const input = '0' + '1' + @@ -259,7 +281,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const walker = new ListWalker( fragment.getChild( 4 ), { direction: 'backward', sameIndent: true, - biggerIndent: true, + higherIndent: true, includeSelf: true, sameItemId: true } ); @@ -272,7 +294,7 @@ describe( 'DocumentList - utils - ListWalker', () => { expect( blocks[ 3 ] ).to.equal( fragment.getChild( 1 ) ); } ); - it( 'should not include nested items from other item (biggerIndent = true, sameItemId = true, backward)', () => { + it( 'should not include nested items from other item (higherIndent = true, sameItemId = true, backward)', () => { const input = '0' + '1' + @@ -285,7 +307,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const walker = new ListWalker( fragment.getChild( 4 ), { direction: 'backward', sameIndent: true, - biggerIndent: true, + higherIndent: true, includeSelf: true, sameItemId: true } ); @@ -295,7 +317,7 @@ describe( 'DocumentList - utils - ListWalker', () => { expect( blocks[ 0 ] ).to.equal( fragment.getChild( 4 ) ); } ); - it( 'should return all list blocks (biggerIndent = true, sameIndent = true, smallerIndent = true)', () => { + it( 'should return all list blocks (higherIndent = true, sameIndent = true, lowerIndent = true)', () => { const input = '0' + '1' + @@ -308,8 +330,8 @@ describe( 'DocumentList - utils - ListWalker', () => { const walker = new ListWalker( fragment.getChild( 1 ), { direction: 'forward', sameIndent: true, - smallerIndent: true, - biggerIndent: true, + lowerIndent: true, + higherIndent: true, includeSelf: true } ); const blocks = Array.from( walker ); @@ -379,8 +401,8 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); } ); - describe( 'nested level iterating (biggerIndent = true )', () => { - it( 'should return nested list blocks (biggerIndent = true, sameIndent = false)', () => { + describe( 'nested level iterating (higherIndent = true )', () => { + it( 'should return nested list blocks (higherIndent = true, sameIndent = false)', () => { const input = '0' + '1' + @@ -392,7 +414,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 1 ), { direction: 'forward', - biggerIndent: true + higherIndent: true } ); const blocks = Array.from( walker ); @@ -401,7 +423,7 @@ describe( 'DocumentList - utils - ListWalker', () => { expect( blocks[ 1 ] ).to.equal( fragment.getChild( 3 ) ); } ); - it( 'should return all nested blocks (biggerIndent = true, sameIndent = false)', () => { + it( 'should return all nested blocks (higherIndent = true, sameIndent = false)', () => { const input = '0' + '1' + @@ -413,7 +435,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 0 ), { direction: 'forward', - biggerIndent: true + higherIndent: true } ); const blocks = Array.from( walker ); @@ -424,7 +446,7 @@ describe( 'DocumentList - utils - ListWalker', () => { expect( blocks[ 3 ] ).to.equal( fragment.getChild( 4 ) ); } ); - it( 'should return all nested blocks (biggerIndent = true, sameIndent = false, backward)', () => { + it( 'should return all nested blocks (higherIndent = true, sameIndent = false, backward)', () => { const input = '0' + '1' + @@ -436,7 +458,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 5 ), { direction: 'backward', - biggerIndent: true + higherIndent: true } ); const blocks = Array.from( walker ); @@ -459,7 +481,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 0 ), { direction: 'forward', - biggerIndent: true + higherIndent: true } ); const blocks = Array.from( walker ); @@ -480,7 +502,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 5 ), { direction: 'backward', - biggerIndent: true + higherIndent: true } ); const blocks = Array.from( walker ); @@ -499,7 +521,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 1 ), { direction: 'forward', - biggerIndent: true + higherIndent: true } ); const blocks = Array.from( walker ); @@ -516,7 +538,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 2 ), { direction: 'backward', - biggerIndent: true + higherIndent: true } ); const blocks = Array.from( walker ); @@ -533,7 +555,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 2 ), { direction: 'forward', - biggerIndent: true + higherIndent: true } ); const blocks = Array.from( walker ); @@ -550,7 +572,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 1 ), { direction: 'backward', - biggerIndent: true + higherIndent: true } ); const blocks = Array.from( walker ); @@ -570,7 +592,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const block = ListWalker.first( fragment.getChild( 1 ), { direction: 'forward', - biggerIndent: true + higherIndent: true } ); expect( block ).to.equal( fragment.getChild( 2 ) ); @@ -588,7 +610,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const block = ListWalker.first( fragment.getChild( 4 ), { direction: 'backward', - biggerIndent: true + higherIndent: true } ); expect( block ).to.equal( fragment.getChild( 3 ) ); @@ -596,7 +618,7 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); } ); - describe( 'parent level iterating (smallerIndent = true )', () => { + describe( 'parent level iterating (lowerIndent = true )', () => { it( 'should return nothing if at the start of top level list (backward)', () => { const input = '0' + @@ -607,7 +629,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 0 ), { direction: 'backward', - smallerIndent: true + lowerIndent: true } ); const blocks = Array.from( walker ); @@ -624,7 +646,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 1 ), { direction: 'backward', - smallerIndent: true + lowerIndent: true } ); const blocks = Array.from( walker ); @@ -641,7 +663,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 1 ), { direction: 'forward', - smallerIndent: true + lowerIndent: true } ); const blocks = Array.from( walker ); @@ -658,7 +680,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 1 ), { direction: 'backward', - smallerIndent: true + lowerIndent: true } ); const blocks = Array.from( walker ); @@ -676,7 +698,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 2 ), { direction: 'backward', - smallerIndent: true + lowerIndent: true } ); const blocks = Array.from( walker ); @@ -696,7 +718,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 4 ), { direction: 'backward', - smallerIndent: true + lowerIndent: true } ); const blocks = Array.from( walker ); @@ -716,7 +738,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 1 ), { direction: 'forward', - smallerIndent: true + lowerIndent: true } ); const blocks = Array.from( walker ); @@ -736,7 +758,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 4 ), { direction: 'backward', - smallerIndent: true + lowerIndent: true } ); const blocks = Array.from( walker ); @@ -759,7 +781,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 3 ), { direction: 'forward', - smallerIndent: true + lowerIndent: true } ); const blocks = Array.from( walker ); @@ -781,7 +803,7 @@ describe( 'DocumentList - utils - ListWalker', () => { const fragment = parseModel( input, schema ); const block = ListWalker.first( fragment.getChild( 4 ), { direction: 'backward', - smallerIndent: true + lowerIndent: true } ); expect( block ).to.equal( fragment.getChild( 1 ) ); diff --git a/packages/ckeditor5-list/tests/documentlist/utils/model.js b/packages/ckeditor5-list/tests/documentlist/utils/model.js index 462f200a3a8..b4bee095b20 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/model.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/model.js @@ -519,7 +519,7 @@ describe( 'DocumentList - utils - model', () => { expect( isFirstBlockOfListItem( listItem ) ).to.be.false; } ); - it( 'should return true if the previous block has smaller indent', () => { + it( 'should return true if the previous block has lower indent', () => { const input = 'a' + 'b'; @@ -530,7 +530,7 @@ describe( 'DocumentList - utils - model', () => { expect( isFirstBlockOfListItem( listItem ) ).to.be.true; } ); - it( 'should return false if the previous block has bigger indent but it is a part of bigger list item', () => { + it( 'should return false if the previous block has higher indent but it is a part of bigger list item', () => { const input = 'a' + 'b' + @@ -577,7 +577,7 @@ describe( 'DocumentList - utils - model', () => { expect( isLastBlockOfListItem( listItem ) ).to.be.false; } ); - it( 'should return true if the next block has smaller indent', () => { + it( 'should return true if the next block has lower indent', () => { const input = 'a' + 'b' + @@ -589,7 +589,7 @@ describe( 'DocumentList - utils - model', () => { expect( isLastBlockOfListItem( listItem ) ).to.be.true; } ); - it( 'should return false if the next block has bigger indent but it is a part of bigger list item', () => { + it( 'should return false if the next block has higher indent but it is a part of bigger list item', () => { const input = 'a' + 'b' + From 30b9f38bb335531e2f08a3021d2b22cbd35d85ca Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 28 Dec 2021 19:26:30 +0100 Subject: [PATCH 36/66] ULs and OLs view elements have an ID to make sure merging sibling attributes elements won't break if some feature would add any attribute to the AttributeElement. Code refactoring in refresh handler. --- .../src/documentlist/converters.js | 178 +++++++++++------- .../src/documentlist/utils/view.js | 13 +- 2 files changed, 117 insertions(+), 74 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/converters.js b/packages/ckeditor5-list/src/documentlist/converters.js index a004dc8ead7..d7f19f65194 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.js +++ b/packages/ckeditor5-list/src/documentlist/converters.js @@ -17,7 +17,8 @@ import { getIndent, isListView, isListItemView, - getViewElementNameForListType + getViewElementNameForListType, + getViewElementIdForListType } from './utils/view'; import ListWalker, { iterateSiblingListBlocks } from './utils/listwalker'; import { findAndAddListHeadToMap } from './utils/postfixers'; @@ -111,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(); @@ -139,112 +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 = []; + 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; - } + for ( const { node, previous } of iterateSiblingListBlocks( listHead, 'forward' ) ) { + if ( visited.has( node ) ) { + continue; + } - const itemIndent = node.getAttribute( 'listIndent' ); + const itemIndent = node.getAttribute( 'listIndent' ); - if ( previous && itemIndent < previous.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: node.getAttribute( 'listItemId' ), - type: node.getAttribute( 'listType' ) - }; + // Update the stack for the current indent level. + stack[ itemIndent ] = { + id: node.getAttribute( 'listItemId' ), + type: node.getAttribute( 'listType' ) + }; - const blocks = getListItemBlocks( node, { direction: '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; + } } /** @@ -379,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. diff --git a/packages/ckeditor5-list/src/documentlist/utils/view.js b/packages/ckeditor5-list/src/documentlist/utils/view.js index be7cfd4525c..11abcb6674a 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/view.js +++ b/packages/ckeditor5-list/src/documentlist/utils/view.js @@ -100,7 +100,7 @@ export function getIndent( listItem ) { * @param {String} [id] The list ID. * @returns {module:engine/view/attributeelement~AttributeElement} */ -export function createListElement( writer, indent, type, id ) { +export function createListElement( writer, indent, type, id = getViewElementIdForListType( type, indent ) ) { // Negative priorities so that restricted editing attribute won't wrap lists. return writer.createAttributeElement( getViewElementNameForListType( type ), null, { priority: 2 * indent / 100 - 100, @@ -135,3 +135,14 @@ export function createListItemElement( writer, indent, id ) { export function getViewElementNameForListType( type ) { return type == 'numbered' ? 'ol' : 'ul'; } + +/** + * Returns a view element ID for the given list type and indent. + * + * @param {'bulleted'|'numbered'} type The list type. + * @param {Number} indent The list indent level. + * @returns {String} + */ +export function getViewElementIdForListType( type, indent ) { + return `list-${ type }-${ indent }`; +} From 80724fa0ed4a694b75b936c443fad3c6331a6ef1 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 29 Dec 2021 10:36:57 +0100 Subject: [PATCH 37/66] Updated docs. --- packages/ckeditor5-list/src/documentlist/utils/view.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ckeditor5-list/src/documentlist/utils/view.js b/packages/ckeditor5-list/src/documentlist/utils/view.js index 11abcb6674a..800be5a3b89 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/view.js +++ b/packages/ckeditor5-list/src/documentlist/utils/view.js @@ -139,6 +139,7 @@ export function getViewElementNameForListType( type ) { /** * Returns a view element ID for the given list type and indent. * + * @protected * @param {'bulleted'|'numbered'} type The list type. * @param {Number} indent The list indent level. * @returns {String} From ff0e25337c65ea9ac175b4eea072e92ee28a01b0 Mon Sep 17 00:00:00 2001 From: Andrzej Stanek Date: Wed, 29 Dec 2021 10:46:10 +0100 Subject: [PATCH 38/66] Manual test for mocking list --- .../tests/manual/listmocking.html | 36 +++++++++++++ .../tests/manual/listmocking.js | 54 +++++++++++++++++++ .../tests/manual/listmocking.md | 53 ++++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 packages/ckeditor5-list/tests/manual/listmocking.html create mode 100644 packages/ckeditor5-list/tests/manual/listmocking.js create mode 100644 packages/ckeditor5-list/tests/manual/listmocking.md diff --git a/packages/ckeditor5-list/tests/manual/listmocking.html b/packages/ckeditor5-list/tests/manual/listmocking.html new file mode 100644 index 00000000000..e41c68f81be --- /dev/null +++ b/packages/ckeditor5-list/tests/manual/listmocking.html @@ -0,0 +1,36 @@ + + +
+
+

Model data

+ +
+
+

 

+ +
+
+

ASCII

+

+	
+
+ +
+ +
\ No newline at end of file diff --git a/packages/ckeditor5-list/tests/manual/listmocking.js b/packages/ckeditor5-list/tests/manual/listmocking.js new file mode 100644 index 00000000000..cad071e69bc --- /dev/null +++ b/packages/ckeditor5-list/tests/manual/listmocking.js @@ -0,0 +1,54 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals console, window, document */ + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import Enter from '@ckeditor/ckeditor5-enter/src/enter'; +import Typing from '@ckeditor/ckeditor5-typing/src/typing'; +import Heading from '@ckeditor/ckeditor5-heading/src/heading'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import Undo from '@ckeditor/ckeditor5-undo/src/undo'; +import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; +import { parse as parseModel, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +import List from '../../src/list'; +import { stringifyList } from './../documentlist/_utils/utils'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ Enter, Typing, Heading, Paragraph, Undo, List, Clipboard ], + toolbar: [ 'heading', '|', 'bulletedList', 'numberedList', 'undo', 'redo' ] + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); + +const asciifyModelData = event => { + const paste = ( event.clipboardData || window.clipboardData ).getData( 'text' ); + let modelDataString = paste || document.getElementById( 'model-data' ).value; + modelDataString = modelDataString.replace( /[+|'|\t|\r|\n]/g, '' ); + modelDataString = modelDataString.replace( /> <' ); + const parsedModel = parseModel( modelDataString, window.editor.model.schema ); + document.getElementById( 'ascii-art' ).innerText = stringifyList( parsedModel ); + setModelData( window.editor.model, modelDataString ); +}; + +document.getElementById( 'asciify' ).addEventListener( 'click', asciifyModelData ); +document.getElementById( 'model-data' ).addEventListener( 'paste', asciifyModelData ); +// document.getElementById( 'model-data' ).innerText = stringifyList( ); + +// document.getElementById( 'ascii-art' ).innerText = [ +// '* 0', +// '* 1', +// ' * 2', +// ' * 3', +// ' * 4', +// ' * []5', +// '* 6' +// ].join( '\n' ); diff --git a/packages/ckeditor5-list/tests/manual/listmocking.md b/packages/ckeditor5-list/tests/manual/listmocking.md new file mode 100644 index 00000000000..d58bd9c2c16 --- /dev/null +++ b/packages/ckeditor5-list/tests/manual/listmocking.md @@ -0,0 +1,53 @@ +### Loading + +1. The data should be loaded with: + * two paragraphs, + * bulleted list with eight items, + * two paragraphs, + * numbered list with one item, + * bullet list with one item. +2. Toolbar should have two buttons: for bullet and for numbered list. + +### Testing + +After each step test undo (whole stack) -> redo (whole stack) -> undo (whole stack). + +Creating: + +1. Convert first paragraph to list item +2. Create empty paragraph and convert to list item +3. Enter in the middle of item +4. Enter at the start of item +5. Enter at the end of item + +Removing: + +1. Delete all contents from list item and then the list item +2. Press enter in empty list item +3. Click on highlighted button ("turn off" list feature) +4. Do it for first, second and last list item + +Changing type: + +1. Change type from bulleted to numbered +2. Do it for first, second and last item +3. Do it for multiple items at once + +Merging: + +1. Convert paragraph before list to same type of list +2. Convert paragraph after list to same type of list +3. Convert paragraph before list to different type of list +4. Convert paragraph after list to different type of list +5. Convert first paragraph to bulleted list, then convert second paragraph to bulleted list +6. Convert multiple items and paragraphs at once + +Selection deletion. Make selection between items and press delete button: + +1. two items from the same list +2. all items in a list +3. paragraph before list and second item of list +4. paragraph after list and one-but-last item of list +5. two paragraphs that have list between them +6. two items from different lists of same type +7. two items from different lists of different type From 61aaec5a04ce93474a99d23c63443989aa8fde87 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 29 Dec 2021 11:47:43 +0100 Subject: [PATCH 39/66] Added tests. --- .../ckeditor5-list/tests/documentlist/utils/view.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/ckeditor5-list/tests/documentlist/utils/view.js b/packages/ckeditor5-list/tests/documentlist/utils/view.js index 89411455a74..2703829ff1a 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/view.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/view.js @@ -7,6 +7,7 @@ import { createListElement, createListItemElement, getIndent, + getViewElementIdForListType, getViewElementNameForListType, isListItemView, isListView @@ -305,4 +306,15 @@ describe( 'DocumentList - utils - view', () => { expect( getViewElementNameForListType( 'sth' ) ).to.equal( 'ul' ); } ); } ); + + describe( 'getViewElementIdForListType()', () => { + it( 'should generate view element ID for the given list type and indent', () => { + expect( getViewElementIdForListType( 'bulleted', 0 ) ).to.equal( 'list-bulleted-0' ); + expect( getViewElementIdForListType( 'bulleted', 1 ) ).to.equal( 'list-bulleted-1' ); + expect( getViewElementIdForListType( 'bulleted', 2 ) ).to.equal( 'list-bulleted-2' ); + expect( getViewElementIdForListType( 'numbered', 0 ) ).to.equal( 'list-numbered-0' ); + expect( getViewElementIdForListType( 'numbered', 1 ) ).to.equal( 'list-numbered-1' ); + expect( getViewElementIdForListType( 'numbered', 2 ) ).to.equal( 'list-numbered-2' ); + } ); + } ); } ); From 5cb608fc3a36279a7fda9ce9329c85085d39560c Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 29 Dec 2021 12:53:05 +0100 Subject: [PATCH 40/66] Added support for custom ID in modelList() helper. --- .../tests/documentlist/_utils-tests/utils.js | 36 +++++++++++++++++++ .../tests/documentlist/_utils/utils.js | 12 +++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-list/tests/documentlist/_utils-tests/utils.js b/packages/ckeditor5-list/tests/documentlist/_utils-tests/utils.js index b66901858b8..e92ee80da96 100644 --- a/packages/ckeditor5-list/tests/documentlist/_utils-tests/utils.js +++ b/packages/ckeditor5-list/tests/documentlist/_utils-tests/utils.js @@ -226,6 +226,42 @@ describe( 'mockList()', () => { ); } ); + it( 'should allow to customize the list item id (suffix)', () => { + expect( modelList( [ + '* foo{abc}', + ' bar', + '* baz' + ] ) ).to.equalMarkup( + 'foo' + + 'bar' + + 'baz' + ); + } ); + + it( 'should allow to customize the list item id (prefix)', () => { + expect( modelList( [ + '* foo', + '* {abc}bar', + ' baz' + ] ) ).to.equalMarkup( + 'foo' + + 'bar' + + 'baz' + ); + } ); + + it( 'should not parse the custom list item ID if provided in the following block of a list item', () => { + expect( modelList( [ + '* foo', + ' {abc}bar', + '* baz' + ] ) ).to.equalMarkup( + 'foo' + + '{abc}bar' + + 'baz' + ); + } ); + it( 'should throw when indent is invalid', () => { expect( () => modelList( [ '* foo', diff --git a/packages/ckeditor5-list/tests/documentlist/_utils/utils.js b/packages/ckeditor5-list/tests/documentlist/_utils/utils.js index 5003c0ba75b..59bdc16a369 100644 --- a/packages/ckeditor5-list/tests/documentlist/_utils/utils.js +++ b/packages/ckeditor5-list/tests/documentlist/_utils/utils.js @@ -216,7 +216,7 @@ export function modelList( lines ) { let prevIndent = -1; for ( const [ idx, line ] of lines.entries() ) { - const [ , pad, marker, content ] = line.match( /^((?: {2})*(?:([*#]) )?)(.*)/ ); + let [ , pad, marker, content ] = line.match( /^((?: {2})*(?:([*#]) )?)(.*)/ ); const listIndent = pad.length / 2 - 1; if ( listIndent < 0 ) { @@ -233,8 +233,16 @@ export function modelList( lines ) { } if ( !stack[ listIndent ] || marker ) { + let listItemId = String( idx ).padStart( 3, '0' ); + + content = content.replace( /{([^}]+)}/, ( match, id ) => { + listItemId = id; + + return ''; + } ); + stack[ listIndent ] = { - listItemId: String( idx ).padStart( 3, '0' ), + listItemId, listType: marker == '#' ? 'numbered' : 'bulleted' }; } From 6bdb29b1bf2662b0d041984e504e91201d0d8616 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 29 Dec 2021 13:51:31 +0100 Subject: [PATCH 41/66] New list item ID generator. --- .../src/documentlist/converters.js | 6 +- .../src/documentlist/documentlistcommand.js | 6 +- .../src/documentlist/utils/model.js | 19 +- .../src/documentlist/utils/postfixers.js | 7 +- .../tests/documentlist/_utils-tests/uid.js | 56 +- .../tests/documentlist/_utils/uid.js | 26 +- .../tests/documentlist/_utils/utils.js | 14 +- .../tests/documentlist/converters-data.js | 586 +++++++++--------- .../tests/documentlist/converters.js | 10 +- .../tests/documentlist/documentlistediting.js | 98 +-- .../documentlist/documentlistindentcommand.js | 8 +- .../tests/documentlist/utils/model.js | 22 +- .../tests/documentlist/utils/postfixers.js | 46 +- 13 files changed, 454 insertions(+), 450 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/converters.js b/packages/ckeditor5-list/src/documentlist/converters.js index d7f19f65194..37c2c2cce9d 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.js +++ b/packages/ckeditor5-list/src/documentlist/converters.js @@ -9,7 +9,8 @@ import { getAllListItemBlocks, - getListItemBlocks + getListItemBlocks, + ListItemUid } from './utils/model'; import { createListElement, @@ -23,7 +24,6 @@ import { import ListWalker, { iterateSiblingListBlocks } from './utils/listwalker'; import { findAndAddListHeadToMap } from './utils/postfixers'; -import { uid } from 'ckeditor5/src/utils'; import { UpcastWriter } from 'ckeditor5/src/engine'; /** @@ -48,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' }; diff --git a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js index 230e7fbde66..2bcfac8e39a 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js @@ -8,14 +8,14 @@ */ import { Command } from 'ckeditor5/src/core'; -import { uid } from 'ckeditor5/src/utils'; import { splitListItemBefore, expandListBlocksToCompleteItems, getListItemBlocks, getListItems, removeListAttributes, - outdentItemsAfterItemRemoved + outdentItemsAfterItemRemoved, + ListItemUid } from './utils/model'; /** @@ -114,7 +114,7 @@ export default class DocumentListCommand extends Command { if ( !block.hasAttribute( 'listType' ) ) { writer.setAttributes( { listIndent: 0, - listItemId: uid(), + listItemId: ListItemUid.next(), listType: this.type }, block ); diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.js b/packages/ckeditor5-list/src/documentlist/utils/model.js index c4d301a7d55..5f04d836ec8 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.js +++ b/packages/ckeditor5-list/src/documentlist/utils/model.js @@ -10,6 +10,23 @@ import { uid, toArray } from 'ckeditor5/src/utils'; import ListWalker, { iterateSiblingListBlocks } from './listwalker'; +/** + * The list item ID generator. + * + * @class + */ +export class ListItemUid { + /** + * Returns the next ID. + * + * @protected + * @returns {String} + */ + static next() { + return uid(); + } +} + /** * Returns an array with all elements that represents the same list item. * @@ -167,7 +184,7 @@ export function expandListBlocksToCompleteItems( blocks, options = {} ) { * @param {module:engine/model/writer~Writer} writer The model writer. */ export function splitListItemBefore( listBlock, writer ) { - const id = uid(); + const id = ListItemUid.next(); for ( const block of getListItemBlocks( listBlock, { direction: 'forward' } ) ) { writer.setAttribute( 'listItemId', id, block ); diff --git a/packages/ckeditor5-list/src/documentlist/utils/postfixers.js b/packages/ckeditor5-list/src/documentlist/utils/postfixers.js index f0bec0fd5f1..6abd834dce2 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/postfixers.js +++ b/packages/ckeditor5-list/src/documentlist/utils/postfixers.js @@ -8,8 +8,7 @@ */ import { iterateSiblingListBlocks } from './listwalker'; -import { getListItemBlocks } from './model'; -import { uid } from 'ckeditor5/src/utils'; +import { getListItemBlocks, ListItemUid } from './model'; /** * Based on the provided positions looks for the list head and stores it in the provided map. @@ -113,7 +112,7 @@ export function fixListItemIds( listHead, seenIds, writer ) { // Use a new ID if this one was spot earlier (even in other list). if ( seenIds.has( listItemId ) ) { - listItemId = uid(); + listItemId = ListItemUid.next(); } seenIds.add( listItemId ); @@ -123,7 +122,7 @@ export function fixListItemIds( listHead, seenIds, writer ) { // Use a new ID if a block of a bigger list item has different type. if ( block.getAttribute( 'listType' ) != listType ) { - listItemId = uid(); + listItemId = ListItemUid.next(); listType = block.getAttribute( 'listType' ); } diff --git a/packages/ckeditor5-list/tests/documentlist/_utils-tests/uid.js b/packages/ckeditor5-list/tests/documentlist/_utils-tests/uid.js index a9aa93b03f2..06912fdfccd 100644 --- a/packages/ckeditor5-list/tests/documentlist/_utils-tests/uid.js +++ b/packages/ckeditor5-list/tests/documentlist/_utils-tests/uid.js @@ -4,44 +4,44 @@ */ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; -import { uid } from '@ckeditor/ckeditor5-utils'; import stubUid from '../_utils/uid'; +import { ListItemUid } from '../../../src/documentlist/utils/model'; describe( 'stubUid()', () => { testUtils.createSinonSandbox(); it( 'Should start from 0', () => { - stubUid(); + stubUid( 0 ); - expect( uid() ).to.equal( 'e00000000000000000000000000000000' ); - expect( uid() ).to.equal( 'e00000000000000000000000000000001' ); - expect( uid() ).to.equal( 'e00000000000000000000000000000002' ); - expect( uid() ).to.equal( 'e00000000000000000000000000000003' ); - expect( uid() ).to.equal( 'e00000000000000000000000000000004' ); - expect( uid() ).to.equal( 'e00000000000000000000000000000005' ); - expect( uid() ).to.equal( 'e00000000000000000000000000000006' ); - expect( uid() ).to.equal( 'e00000000000000000000000000000007' ); - expect( uid() ).to.equal( 'e00000000000000000000000000000008' ); - expect( uid() ).to.equal( 'e00000000000000000000000000000009' ); - expect( uid() ).to.equal( 'e0000000000000000000000000000000a' ); - expect( uid() ).to.equal( 'e0000000000000000000000000000000b' ); + expect( ListItemUid.next() ).to.equal( '000' ); + expect( ListItemUid.next() ).to.equal( '001' ); + expect( ListItemUid.next() ).to.equal( '002' ); + expect( ListItemUid.next() ).to.equal( '003' ); + expect( ListItemUid.next() ).to.equal( '004' ); + expect( ListItemUid.next() ).to.equal( '005' ); + expect( ListItemUid.next() ).to.equal( '006' ); + expect( ListItemUid.next() ).to.equal( '007' ); + expect( ListItemUid.next() ).to.equal( '008' ); + expect( ListItemUid.next() ).to.equal( '009' ); + expect( ListItemUid.next() ).to.equal( '00a' ); + expect( ListItemUid.next() ).to.equal( '00b' ); } ); - it( 'Should start from 0x10000', () => { - stubUid( 0x10000 ); + it( 'Should start from 0xa00 (default)', () => { + stubUid(); - expect( uid() ).to.equal( 'e00000000000000000000000000010000' ); - expect( uid() ).to.equal( 'e00000000000000000000000000010001' ); - expect( uid() ).to.equal( 'e00000000000000000000000000010002' ); - expect( uid() ).to.equal( 'e00000000000000000000000000010003' ); - expect( uid() ).to.equal( 'e00000000000000000000000000010004' ); - expect( uid() ).to.equal( 'e00000000000000000000000000010005' ); - expect( uid() ).to.equal( 'e00000000000000000000000000010006' ); - expect( uid() ).to.equal( 'e00000000000000000000000000010007' ); - expect( uid() ).to.equal( 'e00000000000000000000000000010008' ); - expect( uid() ).to.equal( 'e00000000000000000000000000010009' ); - expect( uid() ).to.equal( 'e0000000000000000000000000001000a' ); - expect( uid() ).to.equal( 'e0000000000000000000000000001000b' ); + expect( ListItemUid.next() ).to.equal( 'a00' ); + expect( ListItemUid.next() ).to.equal( 'a01' ); + expect( ListItemUid.next() ).to.equal( 'a02' ); + expect( ListItemUid.next() ).to.equal( 'a03' ); + expect( ListItemUid.next() ).to.equal( 'a04' ); + expect( ListItemUid.next() ).to.equal( 'a05' ); + expect( ListItemUid.next() ).to.equal( 'a06' ); + expect( ListItemUid.next() ).to.equal( 'a07' ); + expect( ListItemUid.next() ).to.equal( 'a08' ); + expect( ListItemUid.next() ).to.equal( 'a09' ); + expect( ListItemUid.next() ).to.equal( 'a0a' ); + expect( ListItemUid.next() ).to.equal( 'a0b' ); } ); } ); diff --git a/packages/ckeditor5-list/tests/documentlist/_utils/uid.js b/packages/ckeditor5-list/tests/documentlist/_utils/uid.js index 916d6eadda7..1d8f435a50c 100644 --- a/packages/ckeditor5-list/tests/documentlist/_utils/uid.js +++ b/packages/ckeditor5-list/tests/documentlist/_utils/uid.js @@ -3,33 +3,21 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ +import { ListItemUid } from '../../../src/documentlist/utils/model'; + /** * Mocks the `uid()` with sequential numbers. * - * @param {Number} [start=0] The uid start number. + * @param {Number} [start=0xa00] The uid start number. */ -export default function stubUid( start = 0 ) { +export default function stubUid( start = 0xa00 ) { const seq = sequence( start ); - sinon.stub( Math, 'random' ).callsFake( () => seq.next().value ); + sinon.stub( ListItemUid, 'next' ).callsFake( () => seq.next().value ); } -function* sequence( start ) { - let num = start << 2; - +function* sequence( num ) { while ( true ) { - if ( num % 4 == 3 ) { - const flipped = - ( num >> 2 & 0xff000000 ) >>> 24 | - ( num >> 2 & 0x00ff0000 ) >> 8 | - ( num >> 2 & 0x0000ff00 ) << 8 | - ( num >> 2 & 0x000000ff ) << 24; - - yield flipped / 0xffffffff; - } else { - yield 0; - } - - num++; + yield ( num++ ).toString( 16 ).padStart( 3, '000' ); } } diff --git a/packages/ckeditor5-list/tests/documentlist/_utils/utils.js b/packages/ckeditor5-list/tests/documentlist/_utils/utils.js index 59bdc16a369..6f993fbe09d 100644 --- a/packages/ckeditor5-list/tests/documentlist/_utils/utils.js +++ b/packages/ckeditor5-list/tests/documentlist/_utils/utils.js @@ -70,7 +70,7 @@ export function setupTestHelpers( editor ) { actionCallback( callbackSelection ); test.reconvertSpy.restore(); - expect( getViewData( view, { withoutSelection: true } ) ).to.equal( output ); + expect( getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( output ); if ( testUndo ) { const modelAfter = getModelData( model ); @@ -78,13 +78,13 @@ export function setupTestHelpers( editor ) { editor.execute( 'undo' ); - expect( getModelData( model ), 'after undo' ).to.equal( modelBefore ); - expect( getViewData( view, { withoutSelection: true } ), 'after undo' ).to.equal( viewBefore ); + expect( getModelData( model ), 'after undo' ).to.equalMarkup( modelBefore ); + expect( getViewData( view, { withoutSelection: true } ), 'after undo' ).to.equalMarkup( viewBefore ); editor.execute( 'redo' ); - expect( getModelData( model ), 'after redo' ).to.equal( modelAfter ); - expect( getViewData( view, { withoutSelection: true } ), 'after redo' ).to.equal( viewAfter ); + expect( getModelData( model ), 'after redo' ).to.equalMarkup( modelAfter ); + expect( getViewData( view, { withoutSelection: true } ), 'after redo' ).to.equalMarkup( viewAfter ); } }, @@ -195,8 +195,8 @@ export function setupTestHelpers( editor ) { data( input, modelData, output = input ) { editor.setData( input ); - expect( editor.getData(), 'output data' ).to.equal( output ); - expect( getModelData( model, { withoutSelection: true } ), 'model data' ).to.equal( modelData ); + expect( editor.getData(), 'output data' ).to.equalMarkup( output ); + expect( getModelData( model, { withoutSelection: true } ), 'model data' ).to.equalMarkup( modelData ); } }; diff --git a/packages/ckeditor5-list/tests/documentlist/converters-data.js b/packages/ckeditor5-list/tests/documentlist/converters-data.js index 3533931b8ec..93ed2a7b5a8 100644 --- a/packages/ckeditor5-list/tests/documentlist/converters-data.js +++ b/packages/ckeditor5-list/tests/documentlist/converters-data.js @@ -56,14 +56,14 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { it( 'single item', () => { test.data( '
  • x
', - 'x' + 'x' ); } ); it( 'single item with spaces', () => { test.data( '
  •  x 
', - ' x ' + ' x ' ); } ); @@ -75,9 +75,9 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
  • c
  • ' + '', - 'a' + - 'b' + - 'c' + 'a' + + 'b' + + 'c' ); } ); @@ -90,8 +90,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '', - 'a' + - 'b' + 'a' + + 'b' ); } ); @@ -108,10 +108,10 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '', - 'a' + - 'b' + - 'c' + - 'd' + 'a' + + 'b' + + 'c' + + 'd' ); } ); @@ -131,13 +131,13 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '', - 'a' + - 'b' + - 'c' + - 'd' + - 'e' + - 'f' + - 'g' + 'a' + + 'b' + + 'c' + + 'd' + + 'e' + + 'f' + + 'g' ); } ); @@ -149,9 +149,9 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
  • c
  • ' + '', - ' a' + - 'b' + - 'c' + ' a' + + 'b' + + 'c' ); } ); @@ -163,9 +163,9 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
  • ' + '', - 'a' + - 'b' + - 'c ' + 'a' + + 'b' + + 'c ' ); } ); @@ -183,11 +183,11 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '', 'xxx' + - 'a' + - 'b' + + 'a' + + 'b' + 'yyy' + - 'c' + - 'd' + 'c' + + 'd' ); } ); @@ -198,8 +198,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
  • b
  • ' + '', - 'a' + - 'b' + 'a' + + 'b' ); } ); @@ -217,10 +217,10 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '

    yyy

    ', 'xxx' + - 'a' + - 'b' + - 'c' + - 'd' + + 'a' + + 'b' + + 'c' + + 'd' + 'yyy' ); } ); @@ -240,12 +240,12 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
  • d
  • ' + '', - 'a' + + 'a' + 'xxx' + - 'b' + - 'c' + + 'b' + + 'c' + 'yyy' + - 'd' + 'd' ); } ); @@ -260,8 +260,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '

    c

    ', - 'a' + - 'b' + + 'a' + + 'b' + 'c', '
      ' + @@ -281,8 +281,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    ', 'foo' + - 'xxx' + - 'yyy', + 'xxx' + + 'yyy', '

    foo

    ' + '
      ' + @@ -298,7 +298,7 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • foo
    • ' + '', - '' + + '' + '<$text bold="true">foo' + '', @@ -323,12 +323,12 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • d
    • ' + '
    ', - 'a' + + 'a' + 'xxx' + - 'b' + - 'c' + + 'b' + + 'c' + 'yyy' + - 'd' + 'd' ); } ); @@ -343,7 +343,7 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '', - '
    ' + + '
    ' + 'foo' + 'bar' + '
    ' @@ -358,7 +358,7 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '', - '' + + '' + 'abc' + '' ); @@ -380,7 +380,7 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '', - '' + + '
    ' + '' + '' + 'foo' + @@ -399,7 +399,7 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '', 'text' + - 'foo', + 'foo', '

    text

    ' + '
      ' + @@ -416,7 +416,7 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    ', 'text' + - 'foo', + 'foo', '

    text

    ' + '
      ' + @@ -432,7 +432,7 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    ' + 'text', - 'foo' + + 'foo' + 'text', '
      ' + @@ -449,7 +449,7 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    ' + 'text', - 'foo' + + 'foo' + 'text', '
      ' + @@ -467,7 +467,7 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    ', 'text' + - 'foo', + 'foo', '

    text

    ' + '
      ' + @@ -486,9 +486,9 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '
    ', - 'foo' + - 'bar' + - 'baz', + 'foo' + + 'bar' + + 'baz', '
      ' + '
    • ' + @@ -506,7 +506,7 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { it( 'single item', () => { test.data( '
      • Foo

      ', - 'Foo', + 'Foo', '
      • Foo
      ' ); } ); @@ -518,8 +518,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • Bar

    • ' + '
    ', - 'Foo' + - 'Bar', + 'Foo' + + 'Bar', '
      ' + '
    • Foo
    • ' + @@ -539,8 +539,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '
    ', - 'Foo' + - 'Bar', + 'Foo' + + 'Bar', '
      ' + '
    • ' + @@ -564,8 +564,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • ' + '
    ', - 'Foo' + - 'Bar' + 'Foo' + + 'Bar' ); } ); @@ -583,9 +583,9 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '', - '123' + - 'Foo' + - 'Bar', + '123' + + 'Foo' + + 'Bar', '
      ' + '
    1. ' + @@ -608,9 +608,9 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    2. abc
    3. ' + '', - 'Foo' + - 'Bar' + - 'abc', + 'Foo' + + 'Bar' + + 'abc', '
        ' + '
      • Foo

        Bar

      • ' + @@ -634,10 +634,10 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '
    ', - '123' + - '456' + - 'Foo' + - 'Bar' + '123' + + '456' + + 'Foo' + + 'Bar' ); } ); @@ -656,10 +656,10 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '', - '123' + - 'Foo' + - 'Bar' + - '456' + '123' + + 'Foo' + + 'Bar' + + '456' ); } ); } ); @@ -674,8 +674,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '', - 'Foo' + - 'Bar', + 'Foo' + + 'Bar', '
      ' + '
    • ' + @@ -699,10 +699,10 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • ' + '
    ', - 'Foo' + - 'Bar' + - 'Foz' + - 'Baz', + 'Foo' + + 'Bar' + + 'Foz' + + 'Baz', '
      ' + '
    • ' + @@ -724,8 +724,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • Bar

    • ' + '
    ', - 'Foo' + - 'Bar', + 'Foo' + + 'Bar', '
      ' + '
    • Foo
    • ' + @@ -745,8 +745,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '
    ', - 'Foo' + - 'Bar', + 'Foo' + + 'Bar', '
      ' + '
    • ' + @@ -774,10 +774,10 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • ' + '', - 'Foo' + - 'Bar' + - '123' + - '456', + 'Foo' + + 'Bar' + + '123' + + '456', '
        ' + '
      1. ' + @@ -813,12 +813,12 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
      2. ' + '
      ', - 'Foo' + - 'Bar' + - '123' + - '456' + - 'abc' + - 'def', + 'Foo' + + 'Bar' + + '123' + + '456' + + 'abc' + + 'def', '
        ' + '
      1. ' + @@ -850,8 +850,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
      2. ' + '
    ', - 'Foo' + - 'Bar', + 'Foo' + + 'Bar', '
      ' + '
    • ' + @@ -875,10 +875,10 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • ' + '
    ', - 'Foo' + - 'Bar' + - 'Foz' + - 'Baz', + 'Foo' + + 'Bar' + + 'Foz' + + 'Baz', '
      ' + '
    • ' + @@ -902,8 +902,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • ' + '
    ', - 'Bar' + - 'Foo', + 'Bar' + + 'Foo', '
      ' + '
    • Bar
    • ' + @@ -923,8 +923,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '
    ', - 'Bar' + - 'Foo', + 'Bar' + + 'Foo', '
      ' + '
    • ' + @@ -952,10 +952,10 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • ' + '', - 'Foo' + - 'Bar' + - '123' + - '456', + 'Foo' + + 'Bar' + + '123' + + '456', '
        ' + '
      1. ' + @@ -991,12 +991,12 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
      2. ' + '
      ', - 'Foo' + - 'Bar' + - '123' + - '456' + - 'abc' + - 'def', + 'Foo' + + 'Bar' + + '123' + + '456' + + 'abc' + + 'def', '
        ' + '
      1. ' + @@ -1029,9 +1029,9 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
      2. ' + '
    ', - 'Foo' + - 'Bar' + - 'Baz', + 'Foo' + + 'Bar' + + 'Baz', '
      ' + '
    • ' + @@ -1053,9 +1053,9 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • ' + '
    ', - 'Txt' + - 'Foo' + - 'Bar', + 'Txt' + + 'Foo' + + 'Bar', '
      ' + '
    • ' + @@ -1077,9 +1077,9 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • ' + '
    ', - 'Foo' + - 'Bar' + - 'Text', + 'Foo' + + 'Bar' + + 'Text', '
      ' + '
    • ' + @@ -1101,9 +1101,9 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • ' + '
    ', - 'Foo' + - 'Bar' + - 'Baz' + 'Foo' + + 'Bar' + + 'Baz' ); } ); @@ -1117,9 +1117,9 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '', - 'Foo' + - 'Bar' + - 'Baz', + 'Foo' + + 'Bar' + + 'Baz', '
      ' + '
    • Foo
    • ' + @@ -1141,9 +1141,9 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • Baz
    • ' + '
    ', - 'Foo' + - 'Bar' + - 'Baz', + 'Foo' + + 'Bar' + + 'Baz', '
      ' + '
    • ' + @@ -1169,11 +1169,11 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • ' + '
    ', - 'Txt' + - 'Foo' + - 'Bar' + - 'Baz' + - '123', + 'Txt' + + 'Foo' + + 'Bar' + + 'Baz' + + '123', '
      ' + '
    • ' + @@ -1210,14 +1210,14 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • ' + '
    ', - 'Foo' + - 'Bar' + - 'Baz' + - '123' + - '456' + - 'ABC' + - 'DEF' + - 'GHI', + 'Foo' + + 'Bar' + + 'Baz' + + '123' + + '456' + + 'ABC' + + 'DEF' + + 'GHI', '
      ' + '
    • ' + @@ -1249,8 +1249,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • ' + '
    ', - 'Foo' + - 'Bar', + 'Foo' + + 'Bar', '
      ' + '
    • ' + @@ -1278,9 +1278,9 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • ' + '
    ', - 'Foo' + + 'Foo' + 'Bar' + - 'Baz', + 'Baz', '
      ' + '
    • Foo
    • ' + @@ -1343,7 +1343,7 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    ', 'foo' + - 'bar', + 'bar', '

    foo

    ' + '
      ' + @@ -1361,7 +1361,7 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '
    ', - 'foo' + + 'foo' + 'bar', '
      ' + @@ -1381,9 +1381,9 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '
    ', - 'foo' + + 'foo' + 'bar' + - 'baz', + 'baz', '
      ' + '
    • foo
    • ' + @@ -1407,7 +1407,7 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    ', '' + - 'x', + 'x', '

     

    ' + '
      ' + @@ -1429,7 +1429,7 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    ' + '', - '1.1', + '1.1', '
      ' + '
    • 1.1
    • ' + @@ -1445,7 +1445,7 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    ' + '', - '1.1', + '1.1', '
      ' + '
    • 1.1
    • ' + @@ -1462,8 +1462,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    ' + '', - '1' + - '2.1', + '1' + + '2.1', '
      ' + '
    • 1' + @@ -1492,9 +1492,9 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • ' + '
    ', - '1.1' + - '1.2' + - '2.1', + '1.1' + + '1.2' + + '2.1', '
      ' + '
    • 1.1
    • ' + @@ -1526,11 +1526,11 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '
    ', - '1.1' + - '1.2' + - '2.1' + - '3.1' + - '2.2', + '1.1' + + '1.2' + + '2.1' + + '3.1' + + '2.2', '
      ' + '
    • 1.1
    • ' + @@ -1565,9 +1565,9 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '
    ', - 'A' + - 'B' + - 'C', + 'A' + + 'B' + + 'C', '
      ' + '
    • A' + @@ -1597,9 +1597,9 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • ' + '
    ', - 'A' + - 'B' + - 'C', + 'A' + + 'B' + + 'C', '
      ' + '
    • A' + @@ -1639,16 +1639,16 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '
    ', - '1' + - 'A' + - '1' + - '2' + - '3' + - 'A' + - 'B' + - 'A' + - '1' + - '2', + '1' + + 'A' + + '1' + + '2' + + '3' + + 'A' + + 'B' + + 'A' + + '1' + + '2', '
      ' + '
    1. 1' + @@ -1705,11 +1705,11 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    ' + '', - 'A1' + - 'B8' + - 'C3' + - 'D4' + - 'E2', + 'A1' + + 'B8' + + 'C3' + + 'D4' + + 'E2', '
      ' + '
    1. A1' + @@ -1742,8 +1742,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '', 'text before' + - 'text' + - 'inner', + 'text' + + 'inner', '

      text before

      ' + '
        ' + @@ -1792,20 +1792,20 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
      • g
      • ' + '
      ', - 'a' + - '
    ' + + 'a' + + '
    ' + '' + '' + - '' + + '' + 'b' + '' + - '' + + '' + 'c' + '' + - '' + + '' + 'd' + '' + - '
    ' + + '
    ' + '' + '' + 'e' + @@ -1815,8 +1815,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '' + '
    ' + - 'f' + - 'g', + 'f' + + 'g', '
      ' + '
    • ' + @@ -1880,13 +1880,13 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • g
    • ' + '
    ', - 'a' + - 'b' + - 'c' + - 'd' + - 'e' + - 'f' + - 'g', + 'a' + + 'b' + + 'c' + + 'd' + + 'e' + + 'f' + + 'g', '
      ' + '
    • ' + @@ -1925,8 +1925,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '

      bar

      ', 'foo' + - '1' + - '1.1' + + '1' + + '1.1' + 'bar' ); } ); @@ -1949,10 +1949,10 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '

      bar

      ', 'foo' + - '1' + - '1.1a' + - '1.1b' + - '1a' + + '1' + + '1.1a' + + '1.1b' + + '1a' + 'bar' ); } ); @@ -1994,20 +1994,20 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '

      bar

      ', 'foo' + - '1' + - '1.1' + - '1.1.1' + - '1.1.2' + - '1.1.3' + - '1.1.4' + - '1.2' + - '1.2.1' + - '2' + - '3' + - '3.1' + - '3.1.1' + - '3.1.1.1' + - '3.1.2' + + '1' + + '1.1' + + '1.1.1' + + '1.1.2' + + '1.1.3' + + '1.1.4' + + '1.2' + + '1.2.1' + + '2' + + '3' + + '3.1' + + '3.1.1' + + '3.1.1.1' + + '3.1.2' + 'bar' ); } ); @@ -2055,23 +2055,23 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '

      bar

      ', 'foo' + - '1' + - '1.1' + - '1.1.1' + - '1.1.2' + - '1.1.3' + - '1.1.4' + - '1.2' + - '1.2.1' + - '2' + - '3' + - '3.1' + - '3.1.1' + - '3.1.1.1' + - '3.1.1.2' + - '3.1.2' + - '3.2' + - '3.3' + + '1' + + '1.1' + + '1.1.1' + + '1.1.2' + + '1.1.3' + + '1.1.4' + + '1.2' + + '1.2.1' + + '2' + + '3' + + '3.1' + + '3.1.1' + + '3.1.1.1' + + '3.1.1.2' + + '3.1.2' + + '3.2' + + '3.3' + 'bar' ); } ); @@ -2122,26 +2122,26 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '

      bar

      ', 'foo' + - '1' + - '' + - '' + - '1.1.2' + - '1.1.3' + - '1.1.4' + - '' + - '1.2.1' + - '2' + - '' + - '' + + '1' + + '' + + '' + + '1.1.2' + + '1.1.3' + + '1.1.4' + + '' + + '1.2.1' + + '2' + + '' + + '' + '3<$text bold="true">.1' + '' + - '3.1.1' + - '3.1.1.1' + - '3.1.1.2' + - '3.1.2' + - 'xxx' + - '3.2' + - '3.3' + + '3.1.1' + + '3.1.1.1' + + '3.1.1.2' + + '3.1.2' + + 'xxx' + + '3.2' + + '3.3' + 'bar', '

      foo

      ' + @@ -2197,9 +2197,9 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • ' + '
    ', - '
    ' + - 'foo' + - 'bar' + + '
    ' + + 'foo' + + 'bar' + '
    ' ); } ); @@ -2225,11 +2225,11 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '', - '' + + '
    ' + '' + '' + - 'foo' + - 'bar' + + 'foo' + + 'bar' + '' + '' + '
    ' @@ -2247,8 +2247,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '', - '' + - 'foo', + '' + + 'foo', '
      ' + '
    • ' + @@ -2271,8 +2271,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • ' + '
    ', - 'foo' + - '', + 'foo' + + '', '
      ' + '
    • ' + @@ -2297,8 +2297,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    ', 'foo' + - '' + - '', + '' + + '', '

    foo

    ' + '
      ' + @@ -2329,9 +2329,9 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { 'e', 'a' + - 'b' + - 'c' + - 'd' + + 'b' + + 'c' + + 'd' + 'e', '

      a

      ' + @@ -2375,12 +2375,12 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '

      bar

      ', 'foo' + - '1' + - '1.1' + - '1.2' + - '1.2.1' + - '1.3' + - '2' + + '1' + + '1.1' + + '1.2' + + '1.2.1' + + '1.3' + + '2' + 'bar', '

      foo

      ' + @@ -2439,20 +2439,20 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '
    • g
    • ' + '
    ', - 'a' + - '' + + 'a' + + '
    ' + '' + '' + - '' + + '' + 'b' + '' + - '' + + '' + 'c' + '' + - '' + + '' + 'd' + '' + - '
    ' + + '
    ' + '' + '' + 'e' + @@ -2462,8 +2462,8 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { '' + '' + '
    ' + - 'f' + - 'g', + 'f' + + 'g', '
      ' + '
    • ' + diff --git a/packages/ckeditor5-list/tests/documentlist/converters.js b/packages/ckeditor5-list/tests/documentlist/converters.js index c43dff36d64..a40063ef399 100644 --- a/packages/ckeditor5-list/tests/documentlist/converters.js +++ b/packages/ckeditor5-list/tests/documentlist/converters.js @@ -990,7 +990,7 @@ describe( 'DocumentListEditing - converters', () => { expect( getModelData( model, { withoutSelection: true } ) ).to.equal( '
      abc
      ' + - 'foo' + + 'foo' + '
      def
      ' ); } ); @@ -1010,7 +1010,7 @@ describe( 'DocumentListEditing - converters', () => { expect( getModelData( model, { withoutSelection: true } ) ).to.equal( '
      abc
      ' + - 'foo' + 'foo' ); } ); @@ -1028,7 +1028,7 @@ describe( 'DocumentListEditing - converters', () => { ); expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - 'foo' + + 'foo' + '
      def
      ' ); } ); @@ -1044,8 +1044,8 @@ describe( 'DocumentListEditing - converters', () => { ); expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - 'a' + - 'b' + + 'a' + + 'b' + 'c' ); } ); diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistediting.js b/packages/ckeditor5-list/tests/documentlist/documentlistediting.js index e5fae0fce94..9f18ed0f59b 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistediting.js @@ -313,8 +313,8 @@ describe( 'DocumentListEditing', () => { 'a' + 'x' + 'x' + - 'b' + - 'c' + + 'b' + + 'c' + 'd'; setModelData( model, input ); @@ -325,7 +325,7 @@ describe( 'DocumentListEditing', () => { writer.insert( parseModel( item, model.schema ), modelRoot, 1 ); } ); - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( output ); + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( output ); } ); } ); @@ -602,7 +602,7 @@ describe( 'DocumentListEditing', () => { writer.removeAttribute( 'listType', element ); } ); - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( expectedModel ); + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( expectedModel ); } ); it( 'add list attributes', () => { @@ -634,7 +634,7 @@ describe( 'DocumentListEditing', () => { writer.setAttribute( 'listIndent', 2, element.nextSibling ); } ); - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( expectedModel ); + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( expectedModel ); } ); it( 'middle block indent', () => { @@ -645,7 +645,7 @@ describe( 'DocumentListEditing', () => { const expectedModel = 'a' + - 'b' + + 'b' + 'c'; const selection = prepareTest( model, modelBefore ); @@ -655,7 +655,7 @@ describe( 'DocumentListEditing', () => { writer.setAttribute( 'listIndent', 1, element ); } ); - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( expectedModel ); + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( expectedModel ); } ); it( 'middle blocks indent', () => { @@ -667,8 +667,8 @@ describe( 'DocumentListEditing', () => { const expectedModel = 'a' + - 'b' + - 'c' + + 'b' + + 'c' + 'd'; const selection = prepareTest( model, modelBefore ); @@ -679,7 +679,7 @@ describe( 'DocumentListEditing', () => { } } ); - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( expectedModel ); + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( expectedModel ); } ); it( 'middle block outdent', () => { @@ -691,7 +691,7 @@ describe( 'DocumentListEditing', () => { const expectedModel = 'a' + 'b' + - 'c'; + 'c'; const selection = prepareTest( model, modelBefore ); const element = selection.getFirstPosition().nodeAfter; @@ -700,7 +700,7 @@ describe( 'DocumentListEditing', () => { writer.setAttribute( 'listIndent', 0, element ); } ); - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( expectedModel ); + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( expectedModel ); } ); } ); } ); @@ -773,7 +773,7 @@ describe( 'DocumentListEditing', () => { ) ); - expect( getModelData( model ) ).to.equal( + expect( getModelData( model ) ).to.equalMarkup( 'A' + 'BX' + 'Y[]' + @@ -800,7 +800,7 @@ describe( 'DocumentListEditing', () => { ) ); - expect( getModelData( model ) ).to.equal( + expect( getModelData( model ) ).to.equalMarkup( 'A' + 'B[]X' + 'Y' + @@ -823,7 +823,7 @@ describe( 'DocumentListEditing', () => { model.insertContent( paragraph ); } ); - expect( getModelData( model ) ).to.equal( + expect( getModelData( model ) ).to.equalMarkup( 'A' + 'BX[]' + 'C' @@ -842,7 +842,7 @@ describe( 'DocumentListEditing', () => { model.insertContent( writer.createText( 'X' ) ); } ); - expect( getModelData( model ) ).to.equal( + expect( getModelData( model ) ).to.equalMarkup( 'A' + 'BX[]' + 'C' @@ -862,10 +862,10 @@ describe( 'DocumentListEditing', () => { content: parseView( '
      • X
        • Y
      ' ) } ); - expect( getModelData( model ) ).to.equal( + expect( getModelData( model ) ).to.equalMarkup( 'A' + 'BX' + - 'Y[]' + + 'Y[]' + 'C' ); } ); @@ -883,12 +883,12 @@ describe( 'DocumentListEditing', () => { content: parseView( '
      • W
        • X

      Y

      • Z
      ' ) } ); - expect( getModelData( model ) ).to.equal( + expect( getModelData( model ) ).to.equalMarkup( 'A' + 'BW' + - 'X' + + 'X' + 'Y' + - 'Z[]' + + 'Z[]' + 'C' ); } ); @@ -906,10 +906,10 @@ describe( 'DocumentListEditing', () => { content: parseView( '

      X

      • Y
      ' ) } ); - expect( getModelData( model ) ).to.equal( + expect( getModelData( model ) ).to.equalMarkup( 'A' + 'BX' + - 'Y[]' + + 'Y[]' + 'C' ); } ); @@ -931,11 +931,11 @@ describe( 'DocumentListEditing', () => { } ); } ); - expect( getModelData( model ) ).to.equal( + expect( getModelData( model ) ).to.equalMarkup( 'A' + 'B' + - 'X' + - 'Y[]' + + 'X' + + 'Y[]' + 'C' ); } ); @@ -952,9 +952,9 @@ describe( 'DocumentListEditing', () => { content: parseView( '
      • X
        • Y
      ' ) } ); - expect( getModelData( model ) ).to.equal( + expect( getModelData( model ) ).to.equalMarkup( 'AX' + - 'Y[]' + + 'Y[]' + 'B' ); } ); @@ -971,9 +971,9 @@ describe( 'DocumentListEditing', () => { content: parseView( '
      • X
        • Y
      ' ) } ); - expect( getModelData( model ) ).to.equal( + expect( getModelData( model ) ).to.equalMarkup( 'AX' + - 'Y[]' + + 'Y[]' + 'B' ); } ); @@ -1007,11 +1007,11 @@ describe( 'DocumentListEditing', () => { } ); } ); - expect( getModelData( model ) ).to.equal( + expect( getModelData( model ) ).to.equalMarkup( 'Foo' + 'A' + 'B' + - 'X[]' + + 'X[]' + 'Bar' ); } ); @@ -1035,12 +1035,12 @@ describe( 'DocumentListEditing', () => { } ); } ); - expect( getModelData( model ) ).to.equal( + expect( getModelData( model ) ).to.equalMarkup( 'Foo' + 'A' + 'B' + - 'X' + - 'Y[]' + + 'X' + + 'Y[]' + 'Bar' ); } ); @@ -1058,12 +1058,12 @@ describe( 'DocumentListEditing', () => { content: parseView( '
      • W
        • X

          Y

          Z
      ' ) } ); - expect( getModelData( model ) ).to.equal( + expect( getModelData( model ) ).to.equalMarkup( 'A' + 'BW' + - 'X' + - 'Y' + - 'Z[]' + + 'X' + + 'Y' + + 'Z[]' + 'C' ); } ); @@ -1081,11 +1081,11 @@ describe( 'DocumentListEditing', () => { content: parseView( '
      • W
        • X

          Y

          Z
      ' ) } ); - expect( getModelData( model ) ).to.equal( + expect( getModelData( model ) ).to.equalMarkup( 'AW' + - 'X' + - 'Y' + - 'Z[]' + + 'X' + + 'Y' + + 'Z[]' + 'B' + 'C' ); @@ -1104,11 +1104,11 @@ describe( 'DocumentListEditing', () => { content: parseView( '
      • W

        X

        Y

      • Z
      ' ) } ); - expect( getModelData( model ) ).to.equal( + expect( getModelData( model ) ).to.equalMarkup( 'AW' + - 'X' + - 'Y' + - 'Z[]' + + 'X' + + 'Y' + + 'Z[]' + 'B' + 'C' ); @@ -1138,10 +1138,10 @@ describe( 'DocumentListEditing', () => { content: parseView( '
      • ab
      ' ) } ); - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( 'Aa' + '' + - 'b' + + 'b' + 'B' + 'C' ); diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js index 109d1be8b7a..cf03ab312c9 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js @@ -367,7 +367,7 @@ describe( 'DocumentListIndentCommand', () => { expect( getData( model ) ).to.equalMarkup( '0' + '1' + - '[]2' + + '[]2' + '3' + '4' ); @@ -715,8 +715,8 @@ describe( 'DocumentListIndentCommand', () => { expect( getData( model ) ).to.equalMarkup( '0' + - '[]1' + - '2' + + '[]1' + + '2' + '3' ); } ); @@ -735,7 +735,7 @@ describe( 'DocumentListIndentCommand', () => { expect( getData( model ) ).to.equalMarkup( '0' + '1' + - '[]2' + + '[]2' + '3' ); } ); diff --git a/packages/ckeditor5-list/tests/documentlist/utils/model.js b/packages/ckeditor5-list/tests/documentlist/utils/model.js index b4bee095b20..904ba479843 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/model.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/model.js @@ -850,9 +850,9 @@ describe( 'DocumentList - utils - model', () => { model.change( writer => splitListItemBefore( fragment.getChild( 0 ), writer ) ); expect( stringifyModel( fragment ) ).to.equal( - 'a' + - 'b' + - 'c' + 'a' + + 'b' + + 'c' ); } ); @@ -869,8 +869,8 @@ describe( 'DocumentList - utils - model', () => { expect( stringifyModel( fragment ) ).to.equal( 'a' + - 'b' + - 'c' + 'b' + + 'c' ); } ); @@ -890,8 +890,8 @@ describe( 'DocumentList - utils - model', () => { expect( stringifyModel( fragment ) ).to.equal( 'x' + 'a' + - 'b' + - 'c' + + 'b' + + 'c' + 'y' ); } ); @@ -910,9 +910,9 @@ describe( 'DocumentList - utils - model', () => { expect( stringifyModel( fragment ) ).to.equal( 'a' + - 'b' + + 'b' + 'c' + - 'd' + 'd' ); } ); @@ -932,8 +932,8 @@ describe( 'DocumentList - utils - model', () => { expect( stringifyModel( fragment ) ).to.equal( 'a' + 'b' + - 'c' + - 'd' + + 'c' + + 'd' + 'e' ); } ); diff --git a/packages/ckeditor5-list/tests/documentlist/utils/postfixers.js b/packages/ckeditor5-list/tests/documentlist/utils/postfixers.js index bfd4ca16479..dc004fee80e 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/postfixers.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/postfixers.js @@ -336,9 +336,9 @@ describe( 'DocumentList - utils - postfixers', () => { it( 'list nested in blockquote', () => { const input = 'foo' + - '
      ' + - 'foo' + - 'bar' + + '
      ' + + 'foo' + + 'bar' + '
      '; const fragment = parseModel( input, schema ); @@ -349,9 +349,9 @@ describe( 'DocumentList - utils - postfixers', () => { expect( stringifyModel( fragment ) ).to.equal( 'foo' + - '
      ' + - 'foo' + - 'bar' + + '
      ' + + 'foo' + + 'bar' + '
      ' ); } ); @@ -374,7 +374,7 @@ describe( 'DocumentList - utils - postfixers', () => { expect( stringifyModel( fragment ) ).to.equal( '0' + - '1' + '1' ); } ); @@ -395,7 +395,7 @@ describe( 'DocumentList - utils - postfixers', () => { expect( stringifyModel( fragment ) ).to.equal( '0' + - '1' + + '1' + '2' ); } ); @@ -417,8 +417,8 @@ describe( 'DocumentList - utils - postfixers', () => { expect( stringifyModel( fragment ) ).to.equal( '0' + - '1' + - '2' + '1' + + '2' ); } ); @@ -439,8 +439,8 @@ describe( 'DocumentList - utils - postfixers', () => { expect( stringifyModel( fragment ) ).to.equal( '0' + - '1' + - '2' + '1' + + '2' ); } ); @@ -461,8 +461,8 @@ describe( 'DocumentList - utils - postfixers', () => { expect( stringifyModel( fragment ) ).to.equal( '0' + - '1' + - '2' + '1' + + '2' ); } ); @@ -484,9 +484,9 @@ describe( 'DocumentList - utils - postfixers', () => { expect( stringifyModel( fragment ) ).to.equal( '0' + - '1' + + '1' + '2' + - '3' + '3' ); } ); @@ -513,12 +513,12 @@ describe( 'DocumentList - utils - postfixers', () => { expect( stringifyModel( fragment ) ).to.equal( '0' + '1' + - '2' + - '3' + - '4' + - '5' + - '6' + - '7' + '2' + + '3' + + '4' + + '5' + + '6' + + '7' ); } ); @@ -541,7 +541,7 @@ describe( 'DocumentList - utils - postfixers', () => { expect( stringifyModel( fragment ) ).to.equal( '0' + - '1' + + '1' + '2' ); } ); From f14bd50edee85d12eb0928f2aa4df30133a34f53 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 29 Dec 2021 13:52:53 +0100 Subject: [PATCH 42/66] Updated docs. --- packages/ckeditor5-list/src/documentlist/utils/model.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.js b/packages/ckeditor5-list/src/documentlist/utils/model.js index 5f04d836ec8..b8985efeda5 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.js +++ b/packages/ckeditor5-list/src/documentlist/utils/model.js @@ -12,8 +12,6 @@ import ListWalker, { iterateSiblingListBlocks } from './listwalker'; /** * The list item ID generator. - * - * @class */ export class ListItemUid { /** From a91bcacc739e74dce8819218fe7a4c88d0bbd14b Mon Sep 17 00:00:00 2001 From: Andrzej Stanek Date: Wed, 29 Dec 2021 13:56:55 +0100 Subject: [PATCH 43/66] Fix tests --- .../tests/documentlist/utils/listwalker.js | 486 +++++++++-------- .../tests/documentlist/utils/model.js | 512 ++++++++++-------- .../tests/manual/listmocking.html | 3 +- .../tests/manual/listmocking.js | 49 +- 4 files changed, 574 insertions(+), 476 deletions(-) diff --git a/packages/ckeditor5-list/tests/documentlist/utils/listwalker.js b/packages/ckeditor5-list/tests/documentlist/utils/listwalker.js index cb7a818254b..a227038367a 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/listwalker.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/listwalker.js @@ -22,10 +22,11 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return no blocks (sameIndent = false, lowerIndent = false, higherIndent = false)', () => { - const input = - '0' + - '1' + - '2'; + const input = modelList( [ + '* 0', + '* 1', + '* 2' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 0 ), { @@ -43,10 +44,11 @@ describe( 'DocumentList - utils - ListWalker', () => { describe( 'same level iterating (sameIndent = true)', () => { it( 'should iterate on nodes with `listItemId` attribute', () => { - const input = - '0' + - '1' + - '2'; + const input = modelList( [ + '* 0', + '* 1', + '* 2' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 0 ), { @@ -63,10 +65,11 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should stop iterating on first node without `listItemId` attribute', () => { - const input = - '0' + - '1' + - '2'; + const input = modelList( [ + '* 0', + '* 1', + '2' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 0 ), { @@ -82,10 +85,11 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should not iterate over nodes without `listItemId` attribute', () => { - const input = - 'x' + - '0' + - '1'; + const input = modelList( [ + 'x', + '* 0', + '* 1' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 0 ), { @@ -99,10 +103,11 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should skip start block (includeSelf = false, direction = forward)', () => { - const input = - '0' + - '1' + - '2'; + const input = modelList( [ + '* 0', + '* 1', + '* 2' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 0 ), { @@ -118,10 +123,11 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should skip start block (includeSelf = false, direction = backward)', () => { - const input = - '0' + - '1' + - '2'; + const input = modelList( [ + '* 0', + '* 1', + '* 2' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 2 ), { @@ -137,10 +143,11 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return items with the same ID', () => { - const input = - '0' + - '1' + - '2'; + const input = modelList( [ + '* 0', + ' 1', + '* 2' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 0 ), { @@ -178,11 +185,12 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return items while iterating over a nested list', () => { - const input = - '0' + - '1' + - '2' + - '3'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + '* 3' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 1 ), { @@ -198,11 +206,12 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should skip nested items (higherIndent = false)', () => { - const input = - '0' + - '1' + - '2' + - '3'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + '* 3' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 0 ), { @@ -218,13 +227,14 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should include nested items (higherIndent = true)', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * 3', + ' * 4', + '* 5' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 1 ), { @@ -243,13 +253,14 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should include nested items (higherIndent = true, sameItemId = true, forward)', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * 3', + ' 4', + ' * 5' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 1 ), { @@ -269,13 +280,14 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should include nested items (higherIndent = true, sameItemId = true, backward)', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * 3', + ' 4', + ' * 5' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 4 ), { @@ -295,13 +307,14 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should not include nested items from other item (higherIndent = true, sameItemId = true, backward)', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * 3', + ' * 4', + ' * 5' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 4 ), { @@ -318,13 +331,14 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return all list blocks (higherIndent = true, sameIndent = true, lowerIndent = true)', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * 3', + ' * 4', + '* 5' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 1 ), { @@ -346,13 +360,14 @@ describe( 'DocumentList - utils - ListWalker', () => { describe( 'first()', () => { it( 'should return first sibling block', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * 3', + ' * 4', + '* 5' + ] ); const fragment = parseModel( input, schema ); const block = ListWalker.first( fragment.getChild( 2 ), { @@ -364,13 +379,14 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return first block on the same indent level (forward)', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * 3', + ' * 4', + '* 5' + ] ); const fragment = parseModel( input, schema ); const block = ListWalker.first( fragment.getChild( 1 ), { @@ -382,13 +398,14 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return first block on the same indent level (backward)', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * 3', + ' * 4', + '* 5' + ] ); const fragment = parseModel( input, schema ); const block = ListWalker.first( fragment.getChild( 4 ), { @@ -403,13 +420,14 @@ describe( 'DocumentList - utils - ListWalker', () => { describe( 'nested level iterating (higherIndent = true )', () => { it( 'should return nested list blocks (higherIndent = true, sameIndent = false)', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * 3', + ' * 4', + '* 5' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 1 ), { @@ -424,13 +442,14 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return all nested blocks (higherIndent = true, sameIndent = false)', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * 3', + ' * 4', + '* 5' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 0 ), { @@ -447,13 +466,14 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return all nested blocks (higherIndent = true, sameIndent = false, backward)', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * 3', + ' * 4', + '* 5' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 5 ), { @@ -470,13 +490,14 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return nested blocks next to the start element', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + '* 3', + ' * 4', + ' * 5' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 0 ), { @@ -491,13 +512,14 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return nested blocks next to the start element (backward)', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5'; + const input = modelList( [ + '* 0', + ' * 1', + '* 2', + ' * 3', + ' * 4', + '* 5' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 5 ), { @@ -512,11 +534,12 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return nothing there is no nested sibling', () => { - const input = - '0' + - '1' + - '2' + - '3'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + '* 3' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 1 ), { @@ -529,11 +552,12 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return nothing there is no nested sibling (backward)', () => { - const input = - '0' + - '1' + - '2' + - '3'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + '* 3' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 2 ), { @@ -546,11 +570,12 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return nothing if a the end of nested list', () => { - const input = - '0' + - '1' + - '2' + - '3'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + '* 3' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 2 ), { @@ -563,11 +588,12 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return nothing if a the start of nested list (backward)', () => { - const input = - '0' + - '1' + - '2' + - '3'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + '* 3' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 1 ), { @@ -581,13 +607,14 @@ describe( 'DocumentList - utils - ListWalker', () => { describe( 'first()', () => { it( 'should return nested sibling block', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * 3', + ' * 4', + '* 5' + ] ); const fragment = parseModel( input, schema ); const block = ListWalker.first( fragment.getChild( 1 ), { @@ -599,13 +626,14 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return nested sibling block (backward)', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * 3', + ' * 4', + '* 5' + ] ); const fragment = parseModel( input, schema ); const block = ListWalker.first( fragment.getChild( 4 ), { @@ -620,11 +648,12 @@ describe( 'DocumentList - utils - ListWalker', () => { describe( 'parent level iterating (lowerIndent = true )', () => { it( 'should return nothing if at the start of top level list (backward)', () => { - const input = - '0' + - '1' + - '2' + - '3'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + '* 3' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 0 ), { @@ -637,11 +666,12 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return nothing if at top level list (backward)', () => { - const input = - '0' + - '1' + - '2' + - '3'; + const input = modelList( [ + '* 0', + '* 1', + '* 2', + '* 3' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 1 ), { @@ -654,11 +684,12 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return nothing if at top level list (forward)', () => { - const input = - '0' + - '1' + - '2' + - '3'; + const input = modelList( [ + '* 0', + '* 1', + '* 2', + '* 3' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 1 ), { @@ -671,11 +702,12 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return parent block if at the first block of nested list (backward)', () => { - const input = - '0' + - '1' + - '2' + - '3'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + '* 3' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 1 ), { @@ -689,11 +721,12 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return parent block if at the following block of nested list (backward)', () => { - const input = - '0' + - '1' + - '2' + - '3'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + '* 3' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 2 ), { @@ -707,13 +740,14 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return parent block even when there is a nested list (backward)', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * 3', + ' * 4', + '* 5' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 4 ), { @@ -727,13 +761,14 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return parent block even when there is a nested list (forward)', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * 3', + ' * 4', + '* 5' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 1 ), { @@ -747,13 +782,14 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return parent blocks (backward)', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5'; + const input = modelList( [ + '* 0', + '* 1', + ' * 2', + ' * 3', + ' * 4', + '* 5' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 4 ), { @@ -769,14 +805,15 @@ describe( 'DocumentList - utils - ListWalker', () => { } ); it( 'should return parent blocks (forward)', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6'; + const input = modelList( [ + '* 0', + ' * 1', + ' * 2', + ' * 3', + ' * 4', + ' * 5', + '* 6' + ] ); const fragment = parseModel( input, schema ); const walker = new ListWalker( fragment.getChild( 3 ), { @@ -792,13 +829,14 @@ describe( 'DocumentList - utils - ListWalker', () => { describe( 'first()', () => { it( 'should return nested sibling block', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5'; + const input = modelList( [ + '* 0', + '* 1', + ' * 2', + ' * 3', + ' * 4', + '* 5' + ] ); const fragment = parseModel( input, schema ); const block = ListWalker.first( fragment.getChild( 4 ), { diff --git a/packages/ckeditor5-list/tests/documentlist/utils/model.js b/packages/ckeditor5-list/tests/documentlist/utils/model.js index b4bee095b20..b14eb8084d7 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/model.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/model.js @@ -42,11 +42,12 @@ describe( 'DocumentList - utils - model', () => { describe( 'getAllListItemBlocks()', () => { it( 'should return a single item if it meets conditions', () => { - const input = - 'foo' + - '0.' + - '1.' + - 'bar'; + const input = modelList( [ + 'foo', + '* 0.', + '* 1.', + 'bar' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); @@ -57,13 +58,14 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should return a items if started looking from the first list item block', () => { - const input = - 'foo' + - '0a.' + - '1b.' + - '1c.' + - '2.' + - 'bar'; + const input = modelList( [ + 'foo', + '* 0a.', + ' 1b.', + ' 1c.', + '* 2.', + 'bar' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); @@ -76,13 +78,14 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should return a items if started looking from the last list item block', () => { - const input = - 'foo' + - '0a.' + - '1b.' + - '1c.' + - '2.' + - 'bar'; + const input = modelList( [ + 'foo', + '* 0a.', + ' 1b.', + ' 1c.', + '* 2.', + 'bar' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 3 ); @@ -95,13 +98,14 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should return a items if started looking from the middle list item block', () => { - const input = - 'foo' + - '0a.' + - '1b.' + - '1c.' + - '2.' + - 'bar'; + const input = modelList( [ + 'foo', + '* 0a.', + ' 1b.', + ' 1c.', + '* 2.', + 'bar' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 2 ); @@ -114,16 +118,17 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should ignore nested list blocks', () => { - const input = - 'foo' + - 'a' + - 'b1' + - 'b1.c' + - 'b2' + - 'b2.d' + - 'b3' + - 'e' + - 'bar'; + const input = modelList( [ + 'foo', + '* a', + '* b1', + ' * b1.c', + ' b2', + ' * b2.d', + ' b3', + '* e', + 'bar' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 4 ); @@ -138,11 +143,12 @@ describe( 'DocumentList - utils - model', () => { describe( 'getListItemBlocks()', () => { it( 'should return a single item if it meets conditions', () => { - const input = - 'foo' + - '0.' + - '1.' + - 'bar'; + const input = modelList( [ + 'foo', + '* 0.', + '* 1.', + 'bar' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); @@ -155,13 +161,14 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should return a items if started looking from the first list item block', () => { - const input = - 'foo' + - '0a.' + - '1b.' + - '1c.' + - '2.' + - 'bar'; + const input = modelList( [ + 'foo', + '* 0a.', + ' 1b.', + ' 1c.', + '* 2.', + 'bar' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); @@ -176,13 +183,14 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should return a items if started looking from the last list item block', () => { - const input = - 'foo' + - '0a.' + - '1b.' + - '1c.' + - '2.' + - 'bar'; + const input = modelList( [ + 'foo', + '* 0a.', + ' 1b.', + ' 1c.', + '* 2.', + 'bar' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 3 ); @@ -198,13 +206,14 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should return a items if started looking from the middle list item block', () => { - const input = - 'foo' + - '0a.' + - '1b.' + - '1c.' + - '2.' + - 'bar'; + const input = modelList( [ + 'foo', + '* 0a.', + ' 1b.', + ' 1c.', + '* 2.', + 'bar' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 2 ); @@ -220,16 +229,17 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should ignore nested list blocks', () => { - const input = - 'foo' + - 'a' + - 'b1' + - 'b1.c' + - 'b2' + - 'b2.d' + - 'b3' + - 'e' + - 'bar'; + const input = modelList( [ + 'foo', + '* a', + '* b1', + ' * b1.c', + ' b2', + ' * b2.d', + ' b3', + '* e', + 'bar' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 4 ); @@ -245,13 +255,14 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should break if exited nested list', () => { - const input = - 'foo' + - 'a' + - 'b' + - 'b' + - 'c' + - 'bar'; + const input = modelList( [ + 'foo', + '* a', + ' * b', + ' b', + '* c', + 'bar' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 2 ); @@ -266,13 +277,14 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should search backward by default', () => { - const input = - 'foo' + - 'a' + - 'b' + - 'b' + - 'c' + - 'bar'; + const input = modelList( [ + 'foo', + '* a', + '* b', + ' b', + '* c', + 'bar' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 3 ); @@ -285,9 +297,10 @@ describe( 'DocumentList - utils - model', () => { describe( 'getNestedListBlocks()', () => { it( 'should return empty array if there is no nested blocks', () => { - const input = - 'a' + - 'b'; + const input = modelList( [ + '* a', + '* b' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 0 ); @@ -297,12 +310,13 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should return blocks that have a greater indent than the given item', () => { - const input = - 'a' + - 'b' + - 'c' + - 'd' + - 'e'; + const input = modelList( [ + '* a', + ' * b', + ' * c', + ' * d', + '* e' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 0 ); @@ -315,12 +329,13 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should return blocks that have a greater indent than the given item (nested one)', () => { - const input = - 'a' + - 'b' + - 'c' + - 'd' + - 'e'; + const input = modelList( [ + '* a', + ' * b', + ' * c', + ' * d', + '* e' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); @@ -332,12 +347,13 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should not include items from other subtrees', () => { - const input = - 'a' + - 'b' + - 'c' + - 'd' + - 'e'; + const input = modelList( [ + '* a', + ' * b', + ' * c', + '* d', + ' * e' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 0 ); @@ -487,9 +503,10 @@ describe( 'DocumentList - utils - model', () => { describe( 'isFirstBlockOfListItem()', () => { it( 'should return true for the first list item', () => { - const input = - 'a' + - 'b'; + const input = modelList( [ + '* a', + '* b' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 0 ); @@ -498,9 +515,10 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should return true for the second list item', () => { - const input = - 'a' + - 'b'; + const input = modelList( [ + '* a', + '* b' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); @@ -509,9 +527,10 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should return false for the second block of list item', () => { - const input = - 'a' + - 'b'; + const input = modelList( [ + '* a', + ' b' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); @@ -520,9 +539,10 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should return true if the previous block has lower indent', () => { - const input = - 'a' + - 'b'; + const input = modelList( [ + '* a', + ' * b' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); @@ -531,10 +551,11 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should return false if the previous block has higher indent but it is a part of bigger list item', () => { - const input = - 'a' + - 'b' + - 'c'; + const input = modelList( [ + '* a', + ' * b', + ' c' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 2 ); @@ -545,9 +566,10 @@ describe( 'DocumentList - utils - model', () => { describe( 'isLastBlockOfListItem()', () => { it( 'should return true for the last list item', () => { - const input = - 'a' + - 'b'; + const input = modelList( [ + '* a', + '* b' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); @@ -556,9 +578,10 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should return true for the first list item', () => { - const input = - 'a' + - 'b'; + const input = modelList( [ + '* a', + '* b' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 0 ); @@ -567,9 +590,10 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should return false for the first block of list item', () => { - const input = - 'a' + - 'b'; + const input = modelList( [ + '* a', + ' b' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 0 ); @@ -578,10 +602,11 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should return true if the next block has lower indent', () => { - const input = - 'a' + - 'b' + - 'c'; + const input = modelList( [ + '* a', + ' * b', + '* c' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 1 ); @@ -590,10 +615,11 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should return false if the next block has higher indent but it is a part of bigger list item', () => { - const input = - 'a' + - 'b' + - 'c'; + const input = modelList( [ + '* a', + ' * b', + ' c' + ] ); const fragment = parseModel( input, schema ); const listItem = fragment.getChild( 0 ); @@ -604,11 +630,12 @@ describe( 'DocumentList - utils - model', () => { describe( 'expandListBlocksToCompleteItems()', () => { it( 'should not modify list for a single block of a single-block list item', () => { - const input = - 'a' + - 'b' + - 'c' + - 'd'; + const input = modelList( [ + '* a', + '* b', + '* c', + '* d' + ] ); const fragment = parseModel( input, schema ); let blocks = [ @@ -622,10 +649,11 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should include all blocks for single list item', () => { - const input = - '0' + - '1' + - '2'; + const input = modelList( [ + '* 0', + ' 1', + ' 2' + ] ); const fragment = parseModel( input, schema ); let blocks = [ @@ -641,12 +669,13 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should include all blocks for only first list item block', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '3'; + const input = modelList( [ + '* 0', + '* 1', + ' 2', + ' 3', + '* 3' + ] ); const fragment = parseModel( input, schema ); let blocks = [ @@ -662,12 +691,13 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should include all blocks for only last list item block', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '3'; + const input = modelList( [ + '* 0', + '* 1', + ' 2', + ' 3', + '* 3' + ] ); const fragment = parseModel( input, schema ); let blocks = [ @@ -683,12 +713,13 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should include all blocks for only middle list item block', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '3'; + const input = modelList( [ + '* 0', + '* 1', + ' 2', + ' 3', + '* 3' + ] ); const fragment = parseModel( input, schema ); let blocks = [ @@ -704,12 +735,13 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should include all blocks in nested list item', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '3'; + const input = modelList( [ + '* 0', + ' * 1', + ' 2', + ' 3', + '* 3' + ] ); const fragment = parseModel( input, schema ); let blocks = [ @@ -725,10 +757,11 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should include all blocks including nested items (start from first item)', () => { - const input = - '0' + - '1' + - '2'; + const input = modelList( [ + '* 0', + ' * 1', + ' 2' + ] ); const fragment = parseModel( input, schema ); let blocks = [ @@ -744,10 +777,11 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should include all blocks including nested items (start from last item)', () => { - const input = - '0' + - '1' + - '2'; + const input = modelList( [ + '* 0', + ' * 1', + ' 2' + ] ); const fragment = parseModel( input, schema ); let blocks = [ @@ -763,13 +797,14 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should expand first and last items', () => { - const input = - 'x' + - '0' + - '1' + - '2' + - '3' + - 'y'; + const input = modelList( [ + '* x', + '* 0', + ' 1', + '* 2', + ' 3', + '* y' + ] ); const fragment = parseModel( input, schema ); let blocks = [ @@ -787,12 +822,13 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should not include nested items from other item', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4'; + const input = modelList( [ + '* 0', + ' * 1', + '* 2', + ' * 3', + '* 4' + ] ); const fragment = parseModel( input, schema ); let blocks = [ @@ -839,10 +875,11 @@ describe( 'DocumentList - utils - model', () => { describe( 'splitListItemBefore()', () => { it( 'should replace all blocks ids for first block given', () => { - const input = - 'a' + - 'b' + - 'c'; + const input = modelList( [ + '* a', + ' b', + ' c' + ] ); const fragment = parseModel( input, schema ); @@ -857,10 +894,11 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should replace blocks ids for second block given', () => { - const input = - 'a' + - 'b' + - 'c'; + const input = modelList( [ + '* a', + ' b', + ' c' + ] ); const fragment = parseModel( input, schema ); @@ -875,12 +913,13 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should not modify other items', () => { - const input = - 'x' + - 'a' + - 'b' + - 'c' + - 'y'; + const input = modelList( [ + '* x', + '* a', + ' b', + ' c', + '* y' + ] ); const fragment = parseModel( input, schema ); @@ -897,11 +936,12 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should not modify nested items', () => { - const input = - 'a' + - 'b' + - 'c' + - 'd'; + const input = modelList( [ + '* a', + ' b', + ' * c', + ' d' + ] ); const fragment = parseModel( input, schema ); @@ -994,10 +1034,11 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should not apply non-list attributes', () => { - const input = - '0' + - '1' + - '2'; + const input = modelList( [ + '* 0', + ' * 1', + '* 2' + ] ); const fragment = parseModel( input, schema ); let changedBlocks; @@ -1020,11 +1061,12 @@ describe( 'DocumentList - utils - model', () => { describe( 'indentBlocks()', () => { it( 'flat items', () => { - const input = - 'a' + - 'b' + - 'c' + - 'd'; + const input = modelList( [ + '* a', + ' b', + '* c', + ' d' + ] ); const fragment = parseModel( input, schema ); const blocks = [ @@ -1045,12 +1087,13 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'nested lists should keep structure', () => { - const input = - 'a' + - 'b' + - 'c' + - 'd' + - 'e'; + const input = modelList( [ + '* a', + ' * b', + ' * c', + ' * d', + '* e' + ] ); const fragment = parseModel( input, schema ); const blocks = [ @@ -1169,12 +1212,13 @@ describe( 'DocumentList - utils - model', () => { } ); it( 'should not remove attributes other than lists if outdented below 0', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4'; + const input = modelList( [ + '* 0', + '* 1', + ' * 2', + '* 3', + ' * 4' + ] ); const fragment = parseModel( input, schema ); const blocks = [ diff --git a/packages/ckeditor5-list/tests/manual/listmocking.html b/packages/ckeditor5-list/tests/manual/listmocking.html index e41c68f81be..7085c64ec8f 100644 --- a/packages/ckeditor5-list/tests/manual/listmocking.html +++ b/packages/ckeditor5-list/tests/manual/listmocking.html @@ -23,7 +23,8 @@

      Model data

       

      - + +

      ASCII

      diff --git a/packages/ckeditor5-list/tests/manual/listmocking.js b/packages/ckeditor5-list/tests/manual/listmocking.js index cad071e69bc..313f7002d97 100644 --- a/packages/ckeditor5-list/tests/manual/listmocking.js +++ b/packages/ckeditor5-list/tests/manual/listmocking.js @@ -29,26 +29,41 @@ ClassicEditor console.error( err.stack ); } ); -const asciifyModelData = event => { - const paste = ( event.clipboardData || window.clipboardData ).getData( 'text' ); - let modelDataString = paste || document.getElementById( 'model-data' ).value; - modelDataString = modelDataString.replace( /[+|'|\t|\r|\n]/g, '' ); +const copyAscii = () => { + const ascii = document.getElementById( 'ascii-art' ).innerText; + window.navigator.clipboard.writeText( ascii ); +}; + +const asciifyModelData = () => { + let modelDataString = document.getElementById( 'model-data' ).value; + + modelDataString = modelDataString.replace( /[+|'|\t|\r|\n|;]/g, '' ); modelDataString = modelDataString.replace( /> <' ); + const parsedModel = parseModel( modelDataString, window.editor.model.schema ); - document.getElementById( 'ascii-art' ).innerText = stringifyList( parsedModel ); + const listArray = stringifyList( parsedModel ).split( '\n' ); + + const test = listArray.map( ( element, index ) => { + if ( index === listArray.length - 1 ) { + return `'${ element }'`; + } + + return `'${ element }',`; + } ); + + document.getElementById( 'ascii-art' ).innerText = 'modelList( [\n\t' + + test.join( '\n\t' ) + + '\n] );'; setModelData( window.editor.model, modelDataString ); + copyAscii(); +}; + +const onPaste = () => { + window.setTimeout( () => { + asciifyModelData(); + }, 0 ); }; document.getElementById( 'asciify' ).addEventListener( 'click', asciifyModelData ); -document.getElementById( 'model-data' ).addEventListener( 'paste', asciifyModelData ); -// document.getElementById( 'model-data' ).innerText = stringifyList( ); - -// document.getElementById( 'ascii-art' ).innerText = [ -// '* 0', -// '* 1', -// ' * 2', -// ' * 3', -// ' * 4', -// ' * []5', -// '* 6' -// ].join( '\n' ); +document.getElementById( 'btn-copy-ascii' ).addEventListener( 'click', copyAscii ); +document.getElementById( 'model-data' ).addEventListener( 'paste', onPaste ); From 518d9d9a7d10c73e9193e86b92f6a22e3fc3e126 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 29 Dec 2021 18:20:18 +0100 Subject: [PATCH 44/66] Added tests. --- .../src/documentlist/documentlistcommand.js | 10 +- .../tests/documentlist/_utils-tests/utils.js | 12 + .../tests/documentlist/_utils/utils.js | 2 +- .../tests/documentlist/documentlistcommand.js | 1113 ++++++++++------- .../documentlist/documentlistindentcommand.js | 3 + 5 files changed, 701 insertions(+), 439 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js index 2bcfac8e39a..c30353a3bb4 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js @@ -123,8 +123,10 @@ export default class DocumentListCommand extends Command { // Change the type of list item. else { for ( const node of expandListBlocksToCompleteItems( block, { withNested: false } ) ) { - writer.setAttribute( 'listType', this.type, node ); - changedBlocks.push( node ); + if ( node.getAttribute( 'listType' ) != this.type ) { + writer.setAttribute( 'listType', this.type, node ); + changedBlocks.push( node ); + } } } } @@ -163,6 +165,10 @@ export default class DocumentListCommand extends Command { const selection = this.editor.model.document.selection; const blocks = Array.from( selection.getSelectedBlocks() ); + if ( !blocks.length ) { + return false; + } + for ( const block of blocks ) { if ( block.getAttribute( 'listType' ) != this.type ) { return false; diff --git a/packages/ckeditor5-list/tests/documentlist/_utils-tests/utils.js b/packages/ckeditor5-list/tests/documentlist/_utils-tests/utils.js index e92ee80da96..e224ff1ff68 100644 --- a/packages/ckeditor5-list/tests/documentlist/_utils-tests/utils.js +++ b/packages/ckeditor5-list/tests/documentlist/_utils-tests/utils.js @@ -250,6 +250,18 @@ describe( 'mockList()', () => { ); } ); + it( 'should allow to customize the list item id (with prefix)', () => { + expect( modelList( [ + '* foo', + '* bar{id:abc}', + ' baz' + ] ) ).to.equalMarkup( + 'foo' + + 'bar' + + 'baz' + ); + } ); + it( 'should not parse the custom list item ID if provided in the following block of a list item', () => { expect( modelList( [ '* foo', diff --git a/packages/ckeditor5-list/tests/documentlist/_utils/utils.js b/packages/ckeditor5-list/tests/documentlist/_utils/utils.js index 6f993fbe09d..3bb0ff9374a 100644 --- a/packages/ckeditor5-list/tests/documentlist/_utils/utils.js +++ b/packages/ckeditor5-list/tests/documentlist/_utils/utils.js @@ -235,7 +235,7 @@ export function modelList( lines ) { if ( !stack[ listIndent ] || marker ) { let listItemId = String( idx ).padStart( 3, '0' ); - content = content.replace( /{([^}]+)}/, ( match, id ) => { + content = content.replace( /\s*{(?:id:)?([^}]+)}\s*/, ( match, id ) => { listItemId = id; return ''; diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js index e032e023fcd..580a1be21f2 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js @@ -3,16 +3,18 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ +import DocumentListCommand from '../../src/documentlist/documentlistcommand'; +import stubUid from './_utils/uid'; +import { modelList } from './_utils/utils'; + import Editor from '@ckeditor/ckeditor5-core/src/editor/editor'; import Model from '@ckeditor/ckeditor5-engine/src/model/model'; -import DocumentListCommand from '../../src/documentlist/documentlistcommand'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import stubUid from './_utils/uid'; -describe.skip( 'DocumentListCommand', () => { - let editor, command, model, doc, root; +describe.only( 'DocumentListCommand', () => { + let editor, command, model, doc, root, changedBlocks; testUtils.createSinonSandbox(); @@ -24,75 +26,125 @@ describe.skip( 'DocumentListCommand', () => { doc = model.document; root = doc.createRoot(); - command = new DocumentListCommand( editor, 'bulleted' ); + model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); + model.schema.register( 'blockQuote', { inheritAllFrom: '$container' } ); + model.schema.extend( '$container', { allowAttributes: [ 'listType', 'listIndent', 'listItemId' ] } ); - // TODO: I don't like it but OTOH I don't want DocumentListEditing here because it introduces - // post-fixers and I'd rather see how the command works on its own. - model.schema.extend( '$container', { - allowAttributes: [ 'listType', 'listIndent', 'listItemId' ] - } ); + stubUid(); + } ); - model.schema.register( 'paragraph', { - inheritAllFrom: '$block', - allowIn: 'widget' - } ); + describe( 'bulleted', () => { + beforeEach( () => { + command = new DocumentListCommand( editor, 'bulleted' ); - model.schema.register( 'widget', { inheritAllFrom: '$block' } ); - - setData( - model, - 'foo' + - 'bulleted' + - 'numbered' + - 'bar' + - '' + - 'xyz' + - '' - ); - - model.change( writer => { - writer.setSelection( doc.getRoot().getChild( 0 ), 0 ); + command.on( 'afterExecute', ( evt, data ) => { + changedBlocks = data; + } ); } ); - stubUid(); - } ); - - afterEach( () => { - command.destroy(); - } ); + afterEach( () => { + command.destroy(); + } ); - describe( 'DocumentListCommand', () => { describe( 'constructor()', () => { it( 'should create list command with given type and value set to false', () => { + setData( model, '[]' ); + expect( command.type ).to.equal( 'bulleted' ); expect( command.value ).to.be.false; - - const numberedList = new DocumentListCommand( editor, 'numbered' ); - expect( numberedList.type ).to.equal( 'numbered' ); } ); } ); describe( 'value', () => { it( 'should be false if first position in selection is not in a list item', () => { - model.change( writer => { - writer.setSelection( doc.getRoot().getChild( 3 ), 0 ); - } ); + setData( model, modelList( [ + '0[]', + '* 1' + ] ) ); expect( command.value ).to.be.false; } ); it( 'should be false if first position in selection is in a list item of different type', () => { - model.change( writer => { - writer.setSelection( doc.getRoot().getChild( 2 ), 0 ); + setData( model, modelList( [ + '# 0[]', + '# 1' + ] ) ); + + expect( command.value ).to.be.false; + } ); + + it( 'should be false if any of selected blocks is not a list item (non-list after list)', () => { + setData( model, modelList( [ + '* [0', + '1]' + ] ) ); + + expect( command.value ).to.be.false; + } ); + + it( 'should be false if any of selected blocks is not a list item (non-list before list)', () => { + setData( model, modelList( [ + '[0', + '* 1]' + ] ) ); + + expect( command.value ).to.be.false; + } ); + + it( 'should be false if any of selected blocks is not a list item (non-list between lists)', () => { + setData( model, modelList( [ + '* [0', + '1', + '* 2]' + ] ) ); + + expect( command.value ).to.be.false; + } ); + + it( 'should be false if any of selected blocks is not a same type list item', () => { + setData( model, modelList( [ + '* [0', + '# 1]' + ] ) ); + + expect( command.value ).to.be.false; + } ); + + it( 'should be false if there is no blocks in the selection', () => { + model.schema.register( 'table', { + allowWhere: '$block', + allowAttributesOf: '$container', + isObject: true, + isBlock: true } ); + model.schema.register( 'tableCell', { + allowContentOf: '$container', + allowIn: 'table', + isLimit: true, + isSelectable: true + } ); + + setData( model, '[]
      ' ); + expect( command.value ).to.be.false; } ); it( 'should be true if first position in selection is in a list item of same type', () => { - model.change( writer => { - writer.setSelection( doc.getRoot().getChild( 1 ), 0 ); - } ); + setData( model, modelList( [ + '* 0[]', + '* 1' + ] ) ); + + expect( command.value ).to.be.true; + } ); + + it( 'should be true if first position in selection is in a following block of the list item', () => { + setData( model, modelList( [ + '* 0', + ' 1[]' + ] ) ); expect( command.value ).to.be.true; } ); @@ -100,7 +152,7 @@ describe.skip( 'DocumentListCommand', () => { describe( 'isEnabled', () => { it( 'should be true if entire selection is in a list', () => { - setData( model, '[a]' ); + setData( model, modelList( [ '* [a]' ] ) ); expect( command.isEnabled ).to.be.true; } ); @@ -109,179 +161,444 @@ describe.skip( 'DocumentListCommand', () => { expect( command.isEnabled ).to.be.true; } ); - it( 'should be true if selection first position is in a block which can be turned into a list', () => { - setData( model, '[ab]' ); + it( 'should be true if any of the selected blocks allows list attributes (the last element does not allow)', () => { + model.schema.register( 'heading1', { inheritAllFrom: '$block' } ); + model.schema.addAttributeCheck( ( ctx, attributeName ) => { + if ( ctx.endsWith( 'heading1' ) && attributeName === 'listType' ) { + return false; + } + } ); + + setData( model, + '[a' + + 'b]' + ); + + expect( command.isEnabled ).to.be.true; + } ); + + it( 'should be true if any of the selected blocks allows list attributes (the first element does not allow)', () => { + model.schema.register( 'heading1', { inheritAllFrom: '$block' } ); + model.schema.addAttributeCheck( ( ctx, attributeName ) => { + if ( ctx.endsWith( 'heading1' ) && attributeName === 'listType' ) { + return false; + } + } ); + + setData( model, + '[a' + + 'b]' + ); + expect( command.isEnabled ).to.be.true; } ); - it( 'should be false if selection first position is in an element which cannot be converted to a list item', () => { - // Disallow document lists in widgets. + it( 'should be false if all of the selected blocks do not allow list attributes', () => { + model.schema.register( 'heading1', { inheritAllFrom: '$block' } ); model.schema.addAttributeCheck( ( ctx, attributeName ) => { - if ( ctx.endsWith( 'widget paragraph' ) && attributeName === 'listType' ) { + if ( ctx.endsWith( 'heading1' ) && attributeName === 'listType' ) { return false; } } ); - setData( model, '[ab]' ); + setData( model, + 'a[]' + + 'b' + ); + expect( command.isEnabled ).to.be.false; } ); - it( 'should be false in a root which does not allow blocks at all', () => { - doc.createRoot( 'paragraph', 'inlineOnlyRoot' ); - setData( model, 'a[]b', { rootName: 'inlineOnlyRoot' } ); + it( 'should be false if there is no blocks in the selection', () => { + model.schema.register( 'table', { + allowWhere: '$block', + allowAttributesOf: '$container', + isObject: true, + isBlock: true + } ); + + model.schema.register( 'tableCell', { + allowContentOf: '$container', + allowIn: 'table', + isLimit: true, + isSelectable: true + } ); + + setData( model, '[]
      ' ); + expect( command.isEnabled ).to.be.false; } ); } ); - describe( 'execute()', () => { + describe.only( 'execute()', () => { it( 'should use parent batch', () => { + setData( model, '[0]' ); + model.change( writer => { - expect( writer.batch.operations.length ).to.equal( 0 ); + expect( writer.batch.operations.length, 'before' ).to.equal( 0 ); command.execute(); - expect( writer.batch.operations.length ).to.be.above( 0 ); + expect( writer.batch.operations.length, 'after' ).to.be.above( 0 ); } ); } ); describe( 'options.forceValue', () => { it( 'should force converting into the list if the `options.forceValue` is set to `true`', () => { - setData( model, 'fo[]o' ); + setData( model, modelList( [ + 'fo[]o' + ] ) ); command.execute( { forceValue: true } ); - expect( getData( model ) ).to.equal( - 'fo[]o' ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* fo[]o {id:a00}' + ] ) ); + } ); + + it( 'should not modify list item if not needed if the list if the `options.forceValue` is set to `true`', () => { + setData( model, modelList( [ + '* fo[]o' + ] ) ); command.execute( { forceValue: true } ); - expect( getData( model ) ).to.equal( - 'fo[]o' ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* fo[]o' + ] ) ); } ); it( 'should force converting into the paragraph if the `options.forceValue` is set to `false`', () => { - setData( model, 'fo[]o' ); + setData( model, modelList( [ + '* fo[]o' + ] ) ); command.execute( { forceValue: false } ); - expect( getData( model ) ).to.equal( 'fo[]o' ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + 'fo[]o' + ] ) ); + } ); + + it( 'should not modify list item if not needed if the `options.forceValue` is set to `false`', () => { + setData( model, modelList( [ + 'fo[]o' + ] ) ); command.execute( { forceValue: false } ); - expect( getData( model ) ).to.equal( 'fo[]o' ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + 'fo[]o' + ] ) ); } ); } ); - describe( 'collapsed selection', () => { - describe( 'when turning on', () => { - it( 'should turn the closest block into a list item', () => { - setData( model, 'fo[]o' ); + describe( 'when turning on', () => { + it( 'should turn the closest block into a list item', () => { + setData( model, 'fo[]o' ); - command.execute(); + command.execute(); - expect( getData( model ) ).to.equal( - 'fo[]o' - ); - } ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* fo[]o {id:a00}' + ] ) ); - it( 'should change the type of an existing (closest) list item', () => { - setData( model, 'fo[]o' ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 0 ) + ] ); + } ); - command.execute(); + it( 'should change the type of an existing (closest) list item', () => { + setData( model, modelList( [ + '# fo[]o' + ] ) ); - expect( getData( model ) ).to.equal( - 'fo[]o' - ); - } ); + command.execute(); - describe( 'with blocks inside list items', () => { - it( 'should turn the closest block into a list item (middle block of the list item)', () => { - // * foo - // b[]ar - // baz - setData( model, - 'foo' + - 'b[]ar' + - 'baz' - ); - - command.execute(); - - // * foo - // * b[]ar - // b[]az - expect( getData( model ) ).to.equal( - 'foo' + - 'b[]ar' + - 'baz' - ); - } ); - - it( 'should turn the closest block into a list item (last block of the list item)', () => { - // * foo - // bar - // b[]az - setData( model, - 'foo' + - 'bar' + - 'b[]az' - ); - - command.execute(); - - // * foo - // bar - // * b[]az - expect( getData( model ) ).to.equal( - 'foo' + - 'bar' + - 'b[]az' - ); - } ); - } ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* fo[]o' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 0 ) + ] ); } ); - describe( 'when turning off', () => { - it( 'should strip the list attributes from the closest list item (single list item)', () => { - // * f[]oo - setData( model, 'fo[]o' ); + it( 'should make a list items from multiple paragraphs', () => { + setData( model, modelList( [ + 'fo[o', + 'ba]r' + ] ) ); - command.execute(); + command.execute(); - // f[]oo - expect( getData( model ) ).to.equal( 'fo[]o' ); - } ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* fo[o {id:a00}', + '* ba]r {id:a01}' + ] ) ); + + expect( changedBlocks.length ).to.equal( 2 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 0 ), + root.getChild( 1 ) + ] ); + } ); + + it( 'should make a list items from multiple paragraphs mixed with list items', () => { + setData( model, modelList( [ + 'a', + '[b', + '* c', + 'd]', + 'e' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + 'a', + '* [b {id:a00}', + '* c', + '* d] {id:a01}', + 'e' + ] ) ); + + expect( changedBlocks.length ).to.equal( 2 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 1 ), + root.getChild( 3 ) + ] ); + } ); + + it( 'should change type of the whole list items if only some blocks of a list item are selected', () => { + setData( model, modelList( [ + '# a', + ' [b', + 'c', + '# d]', + ' e', + '# f' + ] ) ); + + command.execute(); - it( 'should strip the list attributes from the closest item (multiple list items, selection in first item)', () => { + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' [b', + '* c {id:a00}', + '* d]', + ' e', + '# f' + ] ) ); + + expect( changedBlocks.length ).to.equal( 5 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 0 ), + root.getChild( 1 ), + root.getChild( 2 ), + root.getChild( 3 ), + root.getChild( 4 ) + ] ); + } ); + + it( 'should not change type of nested list if parent is selected', () => { + setData( model, modelList( [ + '# [a', + '# b]', + ' # c', + '# d' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* [a', + '* b]', + ' # c', + '# d' + ] ) ); + + expect( changedBlocks.length ).to.equal( 2 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 0 ), + root.getChild( 1 ) + ] ); + } ); + + it( 'should change the type of the whole list if the selection is collapsed (bulleted lists at the boundaries)', () => { + setData( model, modelList( [ + '* a', + '# b[]', + ' # c', + '# d', + '* e' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* a', + '* b[]', + ' # c', + '* d', + '* e' + ] ) ); + + expect( changedBlocks.length ).to.equal( 2 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 1 ), + root.getChild( 3 ) + ] ); + } ); + + it( 'should change the type of the whole list if the selection is collapsed (paragraphs at the boundaries)', () => { + setData( model, modelList( [ + 'a', + '# b', + ' c[]', + ' # d', + ' e', + '# f', + 'g' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + 'a', + '* b', + ' c[]', + ' # d', + ' e', + '* f', + 'g' + ] ) ); + + expect( changedBlocks.length ).to.equal( 4 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 1 ), + root.getChild( 2 ), + root.getChild( 4 ), + root.getChild( 5 ) + ] ); + } ); + } ); + + // TODO + describe.only( 'when turning off', () => { + it( 'should strip the list attributes from the closest list item (single list item)', () => { + // * f[]oo + setData( model, 'fo[]o' ); + + command.execute(); + + // f[]oo + expect( getData( model ) ).to.equalMarkup( 'fo[]o' ); + } ); + + it( 'should strip the list attributes from the closest item (multiple list items, selection in first item)', () => { + // * f[]oo + // * bar + // * baz + setData( model, + 'f[]oo' + + 'bar' + + 'baz' + ); + + command.execute(); + + // f[]oo + // * bar + // * baz + expect( getData( model ) ).to.equalMarkup( + 'f[]oo' + + 'bar' + + 'baz' + ); + } ); + + it( 'should strip the list attributes from the closest item (multiple list items, selection in the middle item)', () => { + // * foo + // * b[]ar + // * baz + setData( model, + 'foo' + + 'b[]ar' + + 'baz' + ); + + command.execute(); + + // * foo + // b[]ar + // * baz + expect( getData( model ) ).to.equalMarkup( + 'foo' + + 'b[]ar' + + 'baz' + ); + } ); + + it( 'should strip the list attributes from the closest item (multiple list items, selection in the last item)', () => { + // * foo + // * bar + // * b[]az + setData( model, + 'foo' + + 'bar' + + 'b[]az' + ); + + command.execute(); + + // * foo + // * bar + // b[]az + expect( getData( model ) ).to.equalMarkup( + 'foo' + + 'bar' + + 'b[]az' + ); + } ); + + describe( 'with nested lists inside', () => { + it( 'should strip the list attributes from the closest item and decrease indent of children (selection in the first item)', () => { // * f[]oo // * bar - // * baz + // * baz + // * qux setData( model, 'f[]oo' + - 'bar' + - 'baz' + 'bar' + + 'baz' + + 'qux' ); command.execute(); // f[]oo // * bar - // * baz - expect( getData( model ) ).to.equal( + // * baz + // * qux + expect( getData( model ) ).to.equalMarkup( 'f[]oo' + - 'bar' + - 'baz' + 'bar' + + 'baz' + + 'qux' ); } ); - it( 'should strip the list attributes from the closest item (multiple list items, selection in the middle item)', () => { + it( 'should strip the list attributes from the closest item and decrease indent of children (selection in the middle item)', () => { // * foo // * b[]ar - // * baz + // * baz + // * qux setData( model, 'foo' + - 'b[]ar' + - 'baz' + 'b[]ar' + + 'baz' + + 'qux' ); command.execute(); @@ -289,334 +606,258 @@ describe.skip( 'DocumentListCommand', () => { // * foo // b[]ar // * baz - expect( getData( model ) ).to.equal( + // * qux + expect( getData( model ) ).to.equalMarkup( 'foo' + 'b[]ar' + - 'baz' + 'baz' + + 'qux' ); } ); + } ); - it( 'should strip the list attributes from the closest item (multiple list items, selection in the last item)', () => { - // * foo - // * bar - // * b[]az + describe( 'with blocks inside list items', () => { + it( 'should strip the list attributes from the closest list item and all blocks inside (selection in the first block)', () => { + // * fo[]o + // bar + // baz setData( model, - 'foo' + - 'bar' + - 'b[]az' + 'fo[]o' + + 'bar' + + 'baz' ); command.execute(); - // * foo - // * bar - // b[]az - expect( getData( model ) ).to.equal( - 'foo' + - 'bar' + - 'b[]az' + // fo[]o + // bar + // baz + expect( getData( model ) ).to.equalMarkup( + 'fo[]o' + + 'bar' + + 'baz' ); } ); - describe( 'with nested lists inside', () => { - it( 'should strip the list attributes from the closest item and decrease indent of children (selection in the first item)', () => { - // * f[]oo - // * bar - // * baz - // * qux - setData( model, - 'f[]oo' + - 'bar' + - 'baz' + - 'qux' - ); - - command.execute(); - - // f[]oo - // * bar - // * baz - // * qux - expect( getData( model ) ).to.equal( - 'f[]oo' + - 'bar' + - 'baz' + - 'qux' - ); - } ); - - it( 'should strip the list attributes from the closest item and decrease indent of children (selection in the middle item)', () => { - // * foo - // * b[]ar - // * baz - // * qux - setData( model, - 'foo' + - 'b[]ar' + - 'baz' + - 'qux' - ); - - command.execute(); - - // * foo - // b[]ar - // * baz - // * qux - expect( getData( model ) ).to.equal( - 'foo' + - 'b[]ar' + - 'baz' + - 'qux' - ); - } ); - } ); + describe( 'with nested list items', () => { - describe( 'with blocks inside list items', () => { - it( 'should strip the list attributes from the closest list item and all blocks inside (selection in the first block)', () => { - // * fo[]o - // bar - // baz - setData( model, - 'fo[]o' + - 'bar' + - 'baz' - ); - - command.execute(); - - // fo[]o - // bar - // baz - expect( getData( model ) ).to.equal( - 'fo[]o' + - 'bar' + - 'baz' - ); - } ); - - describe( 'with nested list items', () => { - - } ); } ); } ); } ); - describe.skip( 'non-collapsed selection', () => { - beforeEach( () => { - setData( - model, - '---' + - '---' + - '---' + - '---' + - '---' + - '---' + - '---' + - '---' - ); - } ); + beforeEach( () => { + setData( + model, + '---' + + '---' + + '---' + + '---' + + '---' + + '---' + + '---' + + '---' + ); + } ); - // https://github.com/ckeditor/ckeditor5-list/issues/62 - it( 'should not rename blocks which cannot become listItems (list item is not allowed in their parent)', () => { - model.schema.register( 'restricted' ); - model.schema.extend( 'restricted', { allowIn: '$root' } ); + // https://github.com/ckeditor/ckeditor5-list/issues/62 + it( 'should not rename blocks which cannot become listItems (list item is not allowed in their parent)', () => { + model.schema.register( 'restricted' ); + model.schema.extend( 'restricted', { allowIn: '$root' } ); - model.schema.register( 'fooBlock', { inheritAllFrom: '$block' } ); - model.schema.extend( 'fooBlock', { allowIn: 'restricted' } ); + model.schema.register( 'fooBlock', { inheritAllFrom: '$block' } ); + model.schema.extend( 'fooBlock', { allowIn: 'restricted' } ); - setData( - model, - 'a[bc' + - '' + - 'de]f' - ); + setData( + model, + 'a[bc' + + '' + + 'de]f' + ); - command.execute(); + command.execute(); - expect( getData( model ) ).to.equal( - 'a[bc' + - '' + - 'de]f' - ); + expect( getData( model ) ).to.equalMarkup( + 'a[bc' + + '' + + 'de]f' + ); + } ); + + it( 'should not rename blocks which cannot become listItems (block is an object)', () => { + model.schema.register( 'imageBlock', { + isBlock: true, + isObject: true, + allowIn: '$root' } ); - it( 'should not rename blocks which cannot become listItems (block is an object)', () => { - model.schema.register( 'imageBlock', { - isBlock: true, - isObject: true, - allowIn: '$root' - } ); + setData( + model, + 'a[bc' + + '' + + 'de]f' + ); - setData( - model, - 'a[bc' + - '' + - 'de]f' - ); + command.execute(); - command.execute(); + expect( getData( model ) ).to.equalMarkup( + 'a[bc' + + '' + + 'de]f' + ); + } ); - expect( getData( model ) ).to.equal( - 'a[bc' + - '' + - 'de]f' - ); + it( 'should rename closest block to listItem and set correct attributes', () => { + // From first paragraph to second paragraph. + // Command value=false, we are turning on list items. + model.change( writer => { + writer.setSelection( writer.createRange( + writer.createPositionAt( root.getChild( 2 ), 0 ), + writer.createPositionAt( root.getChild( 3 ), 'end' ) + ) ); } ); - it( 'should rename closest block to listItem and set correct attributes', () => { - // From first paragraph to second paragraph. - // Command value=false, we are turning on list items. - model.change( writer => { - writer.setSelection( writer.createRange( - writer.createPositionAt( root.getChild( 2 ), 0 ), - writer.createPositionAt( root.getChild( 3 ), 'end' ) - ) ); - } ); + command.execute(); - command.execute(); + const expectedData = + '---' + + '---' + + '[---' + + '---]' + + '---' + + '---' + + '---' + + '---'; + + expect( getData( model ) ).to.equalMarkup( expectedData ); + } ); - const expectedData = - '---' + - '---' + - '[---' + - '---]' + - '---' + - '---' + - '---' + - '---'; - - expect( getData( model ) ).to.equal( expectedData ); - } ); - - it( 'should rename closest listItem to paragraph', () => { - // From second bullet list item to first numbered list item. - // Command value=true, we are turning off list items. - model.change( writer => { - writer.setSelection( writer.createRange( - writer.createPositionAt( root.getChild( 1 ), 0 ), - writer.createPositionAt( root.getChild( 4 ), 'end' ) - ) ); - } ); + it( 'should rename closest listItem to paragraph', () => { + // From second bullet list item to first numbered list item. + // Command value=true, we are turning off list items. + model.change( writer => { + writer.setSelection( writer.createRange( + writer.createPositionAt( root.getChild( 1 ), 0 ), + writer.createPositionAt( root.getChild( 4 ), 'end' ) + ) ); + } ); - // Convert paragraphs, leave numbered list items. - command.execute(); + // Convert paragraphs, leave numbered list items. + command.execute(); - const expectedData = - '---' + - '[---' + // Attributes will be removed by post fixer. - '---' + - '---' + - '---]' + // Attributes will be removed by post fixer. - '---' + - '---' + - '---'; - - expect( getData( model ) ).to.equal( expectedData ); - } ); - - it( 'should change closest listItem\'s type', () => { - // From first numbered lsit item to third bulleted list item. - model.change( writer => { - writer.setSelection( writer.createRange( - writer.createPositionAt( root.getChild( 4 ), 0 ), - writer.createPositionAt( root.getChild( 6 ), 0 ) - ) ); - } ); + const expectedData = + '---' + + '[---' + // Attributes will be removed by post fixer. + '---' + + '---' + + '---]' + // Attributes will be removed by post fixer. + '---' + + '---' + + '---'; + + expect( getData( model ) ).to.equalMarkup( expectedData ); + } ); - // Convert paragraphs, leave numbered list items. - command.execute(); + it( 'should change closest listItem\'s type', () => { + // From first numbered lsit item to third bulleted list item. + model.change( writer => { + writer.setSelection( writer.createRange( + writer.createPositionAt( root.getChild( 4 ), 0 ), + writer.createPositionAt( root.getChild( 6 ), 0 ) + ) ); + } ); - const expectedData = - '---' + - '---' + - '---' + - '---' + - '[---' + - '---' + - ']---' + - '---'; - - expect( getData( model ) ).to.equal( expectedData ); - } ); - - it( 'should handle outdenting sub-items when list item is turned off', () => { - // From first numbered list item to third bulleted list item. - model.change( writer => { - writer.setSelection( writer.createRange( - writer.createPositionAt( root.getChild( 1 ), 0 ), - writer.createPositionAt( root.getChild( 5 ), 'end' ) - ) ); - } ); + // Convert paragraphs, leave numbered list items. + command.execute(); - // Convert paragraphs, leave numbered list items. - command.execute(); + const expectedData = + '---' + + '---' + + '---' + + '---' + + '[---' + + '---' + + ']---' + + '---'; + + expect( getData( model ) ).to.equalMarkup( expectedData ); + } ); - const expectedData = - '---' + - '[---' + // Attributes will be removed by post fixer. - '---' + - '---' + - '---' + // Attributes will be removed by post fixer. - '---]' + // Attributes will be removed by post fixer. - '---' + - '---'; - - expect( getData( model ) ).to.equal( expectedData ); - } ); - - // Example from docs. - it( 'should change type of all items in nested list if one of items changed', () => { - setData( - model, - '---' + - '---' + - '---' + - '---' + - '---' + - '-[-' + - '---' + - '---' + - '---' + - '-]-' + - '---' + - '---' + - '---' - ); + it( 'should handle outdenting sub-items when list item is turned off', () => { + // From first numbered list item to third bulleted list item. + model.change( writer => { + writer.setSelection( writer.createRange( + writer.createPositionAt( root.getChild( 1 ), 0 ), + writer.createPositionAt( root.getChild( 5 ), 'end' ) + ) ); + } ); - // * ------ <-- do not fix, top level item - // * ------ <-- fix, because latter list item of this item's list is changed - // * ------ <-- do not fix, item is not affected (different list) - // * ------ <-- fix, because latter list item of this item's list is changed - // * ------ <-- fix, because latter list item of this item's list is changed - // * ---[-- <-- already in selection - // * ------ <-- already in selection - // * ------ <-- already in selection - // * ------ <-- already in selection, but does not cause other list items to change because is top-level - // * ---]-- <-- already in selection - // * ------ <-- fix, because preceding list item of this item's list is changed - // * ------ <-- do not fix, item is not affected (different list) - // * ------ <-- do not fix, top level item + // Convert paragraphs, leave numbered list items. + command.execute(); - command.execute(); + const expectedData = + '---' + + '[---' + // Attributes will be removed by post fixer. + '---' + + '---' + + '---' + // Attributes will be removed by post fixer. + '---]' + // Attributes will be removed by post fixer. + '---' + + '---'; + + expect( getData( model ) ).to.equalMarkup( expectedData ); + } ); - const expectedData = - '---' + - '---' + - '---' + - '---' + - '---' + - '-[-' + - '---' + - '---' + - '---' + - '-]-' + - '---' + - '---' + - '---'; - - expect( getData( model ) ).to.equal( expectedData ); - } ); + // Example from docs. + it( 'should change type of all items in nested list if one of items changed', () => { + setData( + model, + '---' + + '---' + + '---' + + '---' + + '---' + + '-[-' + + '---' + + '---' + + '---' + + '-]-' + + '---' + + '---' + + '---' + ); + + // * ------ <-- do not fix, top level item + // * ------ <-- fix, because latter list item of this item's list is changed + // * ------ <-- do not fix, item is not affected (different list) + // * ------ <-- fix, because latter list item of this item's list is changed + // * ------ <-- fix, because latter list item of this item's list is changed + // * ---[-- <-- already in selection + // * ------ <-- already in selection + // * ------ <-- already in selection + // * ------ <-- already in selection, but does not cause other list items to change because is top-level + // * ---]-- <-- already in selection + // * ------ <-- fix, because preceding list item of this item's list is changed + // * ------ <-- do not fix, item is not affected (different list) + // * ------ <-- do not fix, top level item + + command.execute(); + + const expectedData = + '---' + + '---' + + '---' + + '---' + + '---' + + '-[-' + + '---' + + '---' + + '---' + + '-]-' + + '---' + + '---' + + '---'; + + expect( getData( model ) ).to.equalMarkup( expectedData ); } ); it.skip( 'should fire "_executeCleanup" event after finish all operations with all changed items', done => { diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js index cf03ab312c9..69dc2512931 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js @@ -9,12 +9,15 @@ import { modelList } from './_utils/utils'; import Editor from '@ckeditor/ckeditor5-core/src/editor/editor'; import Model from '@ckeditor/ckeditor5-engine/src/model/model'; + import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; describe( 'DocumentListIndentCommand', () => { let editor, model, doc, root; + // TODO check changed blocks (afterExecute event) + testUtils.createSinonSandbox(); beforeEach( () => { From 361394245088b7d6abd9d36438ff20ebfe968e98 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 29 Dec 2021 19:20:26 +0100 Subject: [PATCH 45/66] Added tests. --- .../src/documentlist/documentlistcommand.js | 10 +- .../documentlist/documentlistindentcommand.js | 1 + .../src/documentlist/utils/model.js | 26 +- .../tests/documentlist/documentlistcommand.js | 762 ++++++++++-------- .../tests/documentlist/utils/model.js | 14 + 5 files changed, 446 insertions(+), 367 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js index c30353a3bb4..ab5cb5dacb7 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js @@ -15,7 +15,8 @@ import { getListItems, removeListAttributes, outdentItemsAfterItemRemoved, - ListItemUid + ListItemUid, + sortBlocks } from './utils/model'; /** @@ -82,18 +83,19 @@ export default class DocumentListCommand extends Command { // Split the first block from the list item. const itemBlocks = getListItemBlocks( lastBlock, { direction: 'forward' } ); + const changedBlocks = []; if ( itemBlocks.length > 1 ) { - splitListItemBefore( itemBlocks[ 1 ], writer ); + changedBlocks.push( ...splitListItemBefore( itemBlocks[ 1 ], writer ) ); } // Convert list blocks to plain blocks. - const changedBlocks = removeListAttributes( blocks, writer ); + changedBlocks.push( ...removeListAttributes( blocks, writer ) ); // Outdent items following the selected list item. changedBlocks.push( ...outdentItemsAfterItemRemoved( lastBlock, writer ) ); - this._fireAfterExecute( changedBlocks ); + this._fireAfterExecute( sortBlocks( new Set( changedBlocks ) ) ); } // Turning on the list items for a collapsed selection inside a list item. else if ( document.selection.isCollapsed && blocks[ 0 ].hasAttribute( 'listType' ) ) { diff --git a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js index 9347c27ad19..a8ffe0b75f6 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js @@ -72,6 +72,7 @@ export default class DocumentListIndentCommand extends Command { // For indent make sure that indented blocks have a new ID. // For outdent just split blocks from the list item (give them a new IDs). splitListItemBefore( blocks[ 0 ], writer ); + // TODO add split result to changed blocks. this._fireAfterExecute( blocks ); } diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.js b/packages/ckeditor5-list/src/documentlist/utils/model.js index b8985efeda5..0891a635294 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.js +++ b/packages/ckeditor5-list/src/documentlist/utils/model.js @@ -20,6 +20,7 @@ export class ListItemUid { * @protected * @returns {String} */ + /* istanbul ignore next: static function definition */ static next() { return uid(); } @@ -180,13 +181,18 @@ export function expandListBlocksToCompleteItems( blocks, options = {} ) { * @protected * @param {module:engine/model/element~Element} listBlock The list block element. * @param {module:engine/model/writer~Writer} writer The model writer. + * @returns {Array.} The array of updated blocks. */ +// TODO add test for return value. export function splitListItemBefore( listBlock, writer ) { + const blocks = getListItemBlocks( listBlock, { direction: 'forward' } ); const id = ListItemUid.next(); - for ( const block of getListItemBlocks( listBlock, { direction: 'forward' } ) ) { + for ( const block of blocks ) { writer.setAttribute( 'listItemId', id, block ); } + + return blocks; } /** @@ -429,6 +435,18 @@ export function outdentItemsAfterItemRemoved( lastBlock, writer ) { return changedBlocks; } +/** + * Returns the sorted array of given blocks. + * + * @protected + * @param {Iterable.} blocks The array of blocks. + * @returns {Array.} The sorted array of blocks. + */ +// TODO add tests. +export function sortBlocks( blocks ) { + return Array.from( blocks ).sort( ( a, b ) => a.index - b.index ); +} + // 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. function mergeListItemIfNotLast( block, parentBlock, writer ) { const parentItemBlocks = getListItemBlocks( parentBlock, { direction: 'forward' } ); @@ -448,9 +466,3 @@ function mergeListItemIfNotLast( block, parentBlock, writer ) { return []; } - -// TODO -function sortBlocks( blocks ) { - return Array.from( blocks ).sort( ( a, b ) => a.index - b.index ); -} - diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js index 580a1be21f2..798eeac5aeb 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js @@ -13,7 +13,7 @@ import Model from '@ckeditor/ckeditor5-engine/src/model/model'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -describe.only( 'DocumentListCommand', () => { +describe( 'DocumentListCommand', () => { let editor, command, model, doc, root, changedBlocks; testUtils.createSinonSandbox(); @@ -230,7 +230,7 @@ describe.only( 'DocumentListCommand', () => { } ); } ); - describe.only( 'execute()', () => { + describe( 'execute()', () => { it( 'should use parent batch', () => { setData( model, '[0]' ); @@ -484,410 +484,460 @@ describe.only( 'DocumentListCommand', () => { } ); } ); - // TODO - describe.only( 'when turning off', () => { + describe( 'when turning off', () => { it( 'should strip the list attributes from the closest list item (single list item)', () => { - // * f[]oo - setData( model, 'fo[]o' ); + setData( model, modelList( [ + '* fo[]o' + ] ) ); command.execute(); - // f[]oo - expect( getData( model ) ).to.equalMarkup( 'fo[]o' ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + 'fo[]o' + ] ) ); + + expect( changedBlocks.length ).to.equal( 1 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 0 ) + ] ); } ); it( 'should strip the list attributes from the closest item (multiple list items, selection in first item)', () => { - // * f[]oo - // * bar - // * baz - setData( model, - 'f[]oo' + - 'bar' + - 'baz' - ); + setData( model, modelList( [ + '* f[]oo', + '* bar', + '* baz' + ] ) ); command.execute(); - // f[]oo - // * bar - // * baz - expect( getData( model ) ).to.equalMarkup( - 'f[]oo' + - 'bar' + - 'baz' - ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + 'f[]oo', + '* bar', + '* baz' + ] ) ); + + expect( changedBlocks.length ).to.equal( 1 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 0 ) + ] ); } ); it( 'should strip the list attributes from the closest item (multiple list items, selection in the middle item)', () => { - // * foo - // * b[]ar - // * baz - setData( model, - 'foo' + - 'b[]ar' + - 'baz' - ); + setData( model, modelList( [ + '* foo', + '* b[]ar', + '* baz' + ] ) ); command.execute(); - // * foo - // b[]ar - // * baz - expect( getData( model ) ).to.equalMarkup( - 'foo' + - 'b[]ar' + - 'baz' - ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* foo', + 'b[]ar', + '* baz' + ] ) ); + + expect( changedBlocks.length ).to.equal( 1 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 1 ) + ] ); } ); it( 'should strip the list attributes from the closest item (multiple list items, selection in the last item)', () => { - // * foo - // * bar - // * b[]az - setData( model, - 'foo' + - 'bar' + - 'b[]az' - ); + setData( model, modelList( [ + '* foo', + '* bar', + '* b[]az' + ] ) ); command.execute(); - // * foo - // * bar - // b[]az - expect( getData( model ) ).to.equalMarkup( - 'foo' + - 'bar' + - 'b[]az' - ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* foo', + '* bar', + 'b[]az' + ] ) ); + + expect( changedBlocks.length ).to.equal( 1 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 2 ) + ] ); } ); describe( 'with nested lists inside', () => { - it( 'should strip the list attributes from the closest item and decrease indent of children (selection in the first item)', () => { - // * f[]oo - // * bar - // * baz - // * qux - setData( model, - 'f[]oo' + - 'bar' + - 'baz' + - 'qux' - ); + it( 'should strip the list attributes from the closest item and decrease indent of children (first item)', () => { + setData( model, modelList( [ + '* f[]oo', + ' * bar', + ' * baz', + ' * qux' + ] ) ); command.execute(); - // f[]oo - // * bar - // * baz - // * qux - expect( getData( model ) ).to.equalMarkup( - 'f[]oo' + - 'bar' + - 'baz' + - 'qux' - ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + 'f[]oo', + '* bar', + '* baz', + ' * qux' + ] ) ); + + expect( changedBlocks.length ).to.equal( 4 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 0 ), + root.getChild( 1 ), + root.getChild( 2 ), + root.getChild( 3 ) + ] ); } ); - it( 'should strip the list attributes from the closest item and decrease indent of children (selection in the middle item)', () => { - // * foo - // * b[]ar - // * baz - // * qux - setData( model, - 'foo' + - 'b[]ar' + - 'baz' + - 'qux' - ); + it( 'should strip the list attributes from the closest item and decrease indent of children (middle item)', () => { + setData( model, modelList( [ + '* foo', + '* b[]ar', + ' * baz', + ' * qux' + ] ) ); command.execute(); - // * foo - // b[]ar - // * baz - // * qux - expect( getData( model ) ).to.equalMarkup( - 'foo' + - 'b[]ar' + - 'baz' + - 'qux' - ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* foo', + 'b[]ar', + '* baz', + ' * qux' + ] ) ); + + expect( changedBlocks.length ).to.equal( 3 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 1 ), + root.getChild( 2 ), + root.getChild( 3 ) + ] ); } ); - } ); - describe( 'with blocks inside list items', () => { - it( 'should strip the list attributes from the closest list item and all blocks inside (selection in the first block)', () => { - // * fo[]o - // bar - // baz - setData( model, - 'fo[]o' + - 'bar' + - 'baz' - ); + it( 'should strip the list attributes from the selected items and decrease indent of nested list', () => { + setData( model, modelList( [ + '0', + '* 1', + ' * 2', + ' * 3[]', // <- this is turned off. + ' * 4', // <- this has to become indent = 0, because it will be first item on a new list. + ' * 5', // <- this should be still be a child of item above, so indent = 1. + ' * 6', // <- this has to become indent = 0, because it should not be a child of any of items above. + ' * 7', // <- this should be still be a child of item above, so indent = 1. + ' * 8', // <- this has to become indent = 0. + ' * 9', // <- this should still be a child of item above, so indent = 1. + ' * 10', // <- this should still be a child of item above, so indent = 2. + ' * 11', // <- this should still be at the same level as item above, so indent = 2. + '* 12', // <- this and all below are left unchanged. + ' * 13', + ' * 14' + ] ) ); command.execute(); - // fo[]o - // bar - // baz - expect( getData( model ) ).to.equalMarkup( - 'fo[]o' + - 'bar' + - 'baz' - ); - } ); - - describe( 'with nested list items', () => { - + expect( getData( model ) ).to.equalMarkup( modelList( [ + '0', + '* 1', + ' * 2', + '3[]', + '* 4', + ' * 5', + '* 6', + ' * 7', + '* 8', + ' * 9', + ' * 10', + ' * 11', + '* 12', + ' * 13', + ' * 14' + ] ) ); + + expect( changedBlocks.length ).to.equal( 9 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 3 ), + root.getChild( 4 ), + root.getChild( 5 ), + root.getChild( 6 ), + root.getChild( 7 ), + root.getChild( 8 ), + root.getChild( 9 ), + root.getChild( 10 ), + root.getChild( 11 ) + ] ); } ); } ); - } ); - - beforeEach( () => { - setData( - model, - '---' + - '---' + - '---' + - '---' + - '---' + - '---' + - '---' + - '---' - ); - } ); - // https://github.com/ckeditor/ckeditor5-list/issues/62 - it( 'should not rename blocks which cannot become listItems (list item is not allowed in their parent)', () => { - model.schema.register( 'restricted' ); - model.schema.extend( 'restricted', { allowIn: '$root' } ); - - model.schema.register( 'fooBlock', { inheritAllFrom: '$block' } ); - model.schema.extend( 'fooBlock', { allowIn: 'restricted' } ); - - setData( - model, - 'a[bc' + - '' + - 'de]f' - ); - - command.execute(); - - expect( getData( model ) ).to.equalMarkup( - 'a[bc' + - '' + - 'de]f' - ); - } ); - - it( 'should not rename blocks which cannot become listItems (block is an object)', () => { - model.schema.register( 'imageBlock', { - isBlock: true, - isObject: true, - allowIn: '$root' - } ); - - setData( - model, - 'a[bc' + - '' + - 'de]f' - ); - - command.execute(); - - expect( getData( model ) ).to.equalMarkup( - 'a[bc' + - '' + - 'de]f' - ); - } ); - - it( 'should rename closest block to listItem and set correct attributes', () => { - // From first paragraph to second paragraph. - // Command value=false, we are turning on list items. - model.change( writer => { - writer.setSelection( writer.createRange( - writer.createPositionAt( root.getChild( 2 ), 0 ), - writer.createPositionAt( root.getChild( 3 ), 'end' ) - ) ); - } ); - - command.execute(); - - const expectedData = - '---' + - '---' + - '[---' + - '---]' + - '---' + - '---' + - '---' + - '---'; - - expect( getData( model ) ).to.equalMarkup( expectedData ); - } ); - - it( 'should rename closest listItem to paragraph', () => { - // From second bullet list item to first numbered list item. - // Command value=true, we are turning off list items. - model.change( writer => { - writer.setSelection( writer.createRange( - writer.createPositionAt( root.getChild( 1 ), 0 ), - writer.createPositionAt( root.getChild( 4 ), 'end' ) - ) ); - } ); - - // Convert paragraphs, leave numbered list items. - command.execute(); - - const expectedData = - '---' + - '[---' + // Attributes will be removed by post fixer. - '---' + - '---' + - '---]' + // Attributes will be removed by post fixer. - '---' + - '---' + - '---'; + describe( 'with blocks inside list items', () => { + it( 'should strip the list attributes from the first list item block', () => { + setData( model, modelList( [ + '* fo[]o', + ' bar', + ' baz' + ] ) ); - expect( getData( model ) ).to.equalMarkup( expectedData ); - } ); + command.execute(); - it( 'should change closest listItem\'s type', () => { - // From first numbered lsit item to third bulleted list item. - model.change( writer => { - writer.setSelection( writer.createRange( - writer.createPositionAt( root.getChild( 4 ), 0 ), - writer.createPositionAt( root.getChild( 6 ), 0 ) - ) ); - } ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + 'fo[]o', + '* bar {id:a00}', + ' baz' + ] ) ); + + expect( changedBlocks.length ).to.equal( 3 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 0 ), + root.getChild( 1 ), + root.getChild( 2 ) + ] ); + } ); - // Convert paragraphs, leave numbered list items. - command.execute(); + it( 'should strip the list attributes from the middle list item block', () => { + setData( model, modelList( [ + '* foo', + ' ba[]r', + ' baz' + ] ) ); - const expectedData = - '---' + - '---' + - '---' + - '---' + - '[---' + - '---' + - ']---' + - '---'; + command.execute(); - expect( getData( model ) ).to.equalMarkup( expectedData ); - } ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* foo', + 'ba[]r', + '* baz {id:a00}' + ] ) ); + + expect( changedBlocks.length ).to.equal( 2 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 1 ), + root.getChild( 2 ) + ] ); + } ); - it( 'should handle outdenting sub-items when list item is turned off', () => { - // From first numbered list item to third bulleted list item. - model.change( writer => { - writer.setSelection( writer.createRange( - writer.createPositionAt( root.getChild( 1 ), 0 ), - writer.createPositionAt( root.getChild( 5 ), 'end' ) - ) ); + describe( 'with nested list items', () => { + // TODO + } ); } ); - - // Convert paragraphs, leave numbered list items. - command.execute(); - - const expectedData = - '---' + - '[---' + // Attributes will be removed by post fixer. - '---' + - '---' + - '---' + // Attributes will be removed by post fixer. - '---]' + // Attributes will be removed by post fixer. - '---' + - '---'; - - expect( getData( model ) ).to.equalMarkup( expectedData ); - } ); - - // Example from docs. - it( 'should change type of all items in nested list if one of items changed', () => { - setData( - model, - '---' + - '---' + - '---' + - '---' + - '---' + - '-[-' + - '---' + - '---' + - '---' + - '-]-' + - '---' + - '---' + - '---' - ); - - // * ------ <-- do not fix, top level item - // * ------ <-- fix, because latter list item of this item's list is changed - // * ------ <-- do not fix, item is not affected (different list) - // * ------ <-- fix, because latter list item of this item's list is changed - // * ------ <-- fix, because latter list item of this item's list is changed - // * ---[-- <-- already in selection - // * ------ <-- already in selection - // * ------ <-- already in selection - // * ------ <-- already in selection, but does not cause other list items to change because is top-level - // * ---]-- <-- already in selection - // * ------ <-- fix, because preceding list item of this item's list is changed - // * ------ <-- do not fix, item is not affected (different list) - // * ------ <-- do not fix, top level item - - command.execute(); - - const expectedData = - '---' + - '---' + - '---' + - '---' + - '---' + - '-[-' + - '---' + - '---' + - '---' + - '-]-' + - '---' + - '---' + - '---'; - - expect( getData( model ) ).to.equalMarkup( expectedData ); } ); - it.skip( 'should fire "_executeCleanup" event after finish all operations with all changed items', done => { - setData( model, - 'Foo 1.' + - '[Foo 2.' + - 'Foo 3.]' + - 'Foo 4.' - ); - - command.execute(); - - expect( getData( model ) ).to.equal( - 'Foo 1.' + - '[Foo 2.' + - 'Foo 3.]' + - 'Foo 4.' - ); - - command.on( '_executeCleanup', ( evt, data ) => { - expect( data ).to.deep.equal( [ - root.getChild( 2 ), - root.getChild( 1 ) - ] ); - - done(); - } ); - - command.execute(); - } ); + // TODO + // beforeEach( () => { + // setData( + // model, + // '---' + + // '---' + + // '---' + + // '---' + + // '---' + + // '---' + + // '---' + + // '---' + // ); + // } ); + // + // // https://github.com/ckeditor/ckeditor5-list/issues/62 + // it( 'should not rename blocks which cannot become listItems (list item is not allowed in their parent)', () => { + // model.schema.register( 'restricted' ); + // model.schema.extend( 'restricted', { allowIn: '$root' } ); + // + // model.schema.register( 'fooBlock', { inheritAllFrom: '$block' } ); + // model.schema.extend( 'fooBlock', { allowIn: 'restricted' } ); + // + // setData( + // model, + // 'a[bc' + + // '' + + // 'de]f' + // ); + // + // command.execute(); + // + // expect( getData( model ) ).to.equalMarkup( + // 'a[bc' + + // '' + + // 'de]f' + // ); + // } ); + // + // it( 'should not rename blocks which cannot become listItems (block is an object)', () => { + // model.schema.register( 'imageBlock', { + // isBlock: true, + // isObject: true, + // allowIn: '$root' + // } ); + // + // setData( + // model, + // 'a[bc' + + // '' + + // 'de]f' + // ); + // + // command.execute(); + // + // expect( getData( model ) ).to.equalMarkup( + // 'a[bc' + + // '' + + // 'de]f' + // ); + // } ); + // + // it( 'should rename closest block to listItem and set correct attributes', () => { + // // From first paragraph to second paragraph. + // // Command value=false, we are turning on list items. + // model.change( writer => { + // writer.setSelection( writer.createRange( + // writer.createPositionAt( root.getChild( 2 ), 0 ), + // writer.createPositionAt( root.getChild( 3 ), 'end' ) + // ) ); + // } ); + // + // command.execute(); + // + // const expectedData = + // '---' + + // '---' + + // '[---' + + // '---]' + + // '---' + + // '---' + + // '---' + + // '---'; + // + // expect( getData( model ) ).to.equalMarkup( expectedData ); + // } ); + // + // it( 'should rename closest listItem to paragraph', () => { + // // From second bullet list item to first numbered list item. + // // Command value=true, we are turning off list items. + // model.change( writer => { + // writer.setSelection( writer.createRange( + // writer.createPositionAt( root.getChild( 1 ), 0 ), + // writer.createPositionAt( root.getChild( 4 ), 'end' ) + // ) ); + // } ); + // + // // Convert paragraphs, leave numbered list items. + // command.execute(); + // + // const expectedData = + // '---' + + // '[---' + // Attributes will be removed by post fixer. + // '---' + + // '---' + + // '---]' + // Attributes will be removed by post fixer. + // '---' + + // '---' + + // '---'; + // + // expect( getData( model ) ).to.equalMarkup( expectedData ); + // } ); + // + // it( 'should change closest listItem\'s type', () => { + // // From first numbered lsit item to third bulleted list item. + // model.change( writer => { + // writer.setSelection( writer.createRange( + // writer.createPositionAt( root.getChild( 4 ), 0 ), + // writer.createPositionAt( root.getChild( 6 ), 0 ) + // ) ); + // } ); + // + // // Convert paragraphs, leave numbered list items. + // command.execute(); + // + // const expectedData = + // '---' + + // '---' + + // '---' + + // '---' + + // '[---' + + // '---' + + // ']---' + + // '---'; + // + // expect( getData( model ) ).to.equalMarkup( expectedData ); + // } ); + // + // it( 'should handle outdenting sub-items when list item is turned off', () => { + // // From first numbered list item to third bulleted list item. + // model.change( writer => { + // writer.setSelection( writer.createRange( + // writer.createPositionAt( root.getChild( 1 ), 0 ), + // writer.createPositionAt( root.getChild( 5 ), 'end' ) + // ) ); + // } ); + // + // // Convert paragraphs, leave numbered list items. + // command.execute(); + // + // const expectedData = + // '---' + + // '[---' + // Attributes will be removed by post fixer. + // '---' + + // '---' + + // '---' + // Attributes will be removed by post fixer. + // '---]' + // Attributes will be removed by post fixer. + // '---' + + // '---'; + // + // expect( getData( model ) ).to.equalMarkup( expectedData ); + // } ); + // + // // Example from docs. + // it( 'should change type of all items in nested list if one of items changed', () => { + // setData( + // model, + // '---' + + // '---' + + // '---' + + // '---' + + // '---' + + // '-[-' + + // '---' + + // '---' + + // '---' + + // '-]-' + + // '---' + + // '---' + + // '---' + // ); + // + // // * ------ <-- do not fix, top level item + // // * ------ <-- fix, because latter list item of this item's list is changed + // // * ------ <-- do not fix, item is not affected (different list) + // // * ------ <-- fix, because latter list item of this item's list is changed + // // * ------ <-- fix, because latter list item of this item's list is changed + // // * ---[-- <-- already in selection + // // * ------ <-- already in selection + // // * ------ <-- already in selection + // // * ------ <-- already in selection, but does not cause other list items to change because is top-level + // // * ---]-- <-- already in selection + // // * ------ <-- fix, because preceding list item of this item's list is changed + // // * ------ <-- do not fix, item is not affected (different list) + // // * ------ <-- do not fix, top level item + // + // command.execute(); + // + // const expectedData = + // '---' + + // '---' + + // '---' + + // '---' + + // '---' + + // '-[-' + + // '---' + + // '---' + + // '---' + + // '-]-' + + // '---' + + // '---' + + // '---'; + // + // expect( getData( model ) ).to.equalMarkup( expectedData ); + // } ); } ); } ); } ); diff --git a/packages/ckeditor5-list/tests/documentlist/utils/model.js b/packages/ckeditor5-list/tests/documentlist/utils/model.js index 904ba479843..e2efab0cac3 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/model.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/model.js @@ -13,6 +13,7 @@ import { isFirstBlockOfListItem, isLastBlockOfListItem, isOnlyOneListItemSelected, + ListItemUid, mergeListItemBefore, outdentBlocks, outdentItemsAfterItemRemoved, @@ -40,6 +41,19 @@ describe( 'DocumentList - utils - model', () => { schema.extend( '$container', { allowAttributes: [ 'listType', 'listIndent', 'listItemId' ] } ); } ); + describe( 'ListItemUid.next()', () => { + it( 'should generate UIDs', () => { + stubUid( 0 ); + + expect( ListItemUid.next() ).to.equal( '000' ); + expect( ListItemUid.next() ).to.equal( '001' ); + expect( ListItemUid.next() ).to.equal( '002' ); + expect( ListItemUid.next() ).to.equal( '003' ); + expect( ListItemUid.next() ).to.equal( '004' ); + expect( ListItemUid.next() ).to.equal( '005' ); + } ); + } ); + describe( 'getAllListItemBlocks()', () => { it( 'should return a single item if it meets conditions', () => { const input = From c8882360382c456055592afe7e380f65ff26cddb Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 30 Dec 2021 13:56:08 +0100 Subject: [PATCH 46/66] Added tests. --- .../tests/documentlist/documentlistcommand.js | 964 ++++++++++++++---- 1 file changed, 747 insertions(+), 217 deletions(-) diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js index 798eeac5aeb..0326479186d 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistcommand.js @@ -717,227 +717,757 @@ describe( 'DocumentListCommand', () => { ] ); } ); - describe( 'with nested list items', () => { - // TODO + it( 'should strip the list attributes from blocks with nested list', () => { + setData( model, modelList( [ + '* a[]', + ' b', + ' * c', + ' d', + ' * e', + ' f', + '* g' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + 'a[]', + '* b {id:a00}', + ' * c', + ' d', + ' * e', + ' f', + '* g' + ] ) ); + + expect( changedBlocks.length ).to.equal( 3 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 0 ), + root.getChild( 1 ), + root.getChild( 5 ) + ] ); } ); } ); } ); + } ); + } ); + + describe( 'numbered', () => { + beforeEach( () => { + command = new DocumentListCommand( editor, 'numbered' ); + + command.on( 'afterExecute', ( evt, data ) => { + changedBlocks = data; + } ); + } ); + + afterEach( () => { + command.destroy(); + } ); + + describe( 'constructor()', () => { + it( 'should create list command with given type and value set to false', () => { + setData( model, '[]' ); + + expect( command.type ).to.equal( 'numbered' ); + expect( command.value ).to.be.false; + } ); + } ); + + describe( 'value', () => { + it( 'should be false if first position in selection is not in a list item', () => { + setData( model, modelList( [ + '0[]', + '# 1' + ] ) ); + + expect( command.value ).to.be.false; + } ); + + it( 'should be false if first position in selection is in a list item of different type', () => { + setData( model, modelList( [ + '* 0[]', + '* 1' + ] ) ); + + expect( command.value ).to.be.false; + } ); + + it( 'should be false if any of selected blocks is not a list item (non-list after list)', () => { + setData( model, modelList( [ + '# [0', + '1]' + ] ) ); + + expect( command.value ).to.be.false; + } ); + + it( 'should be false if any of selected blocks is not a list item (non-list before list)', () => { + setData( model, modelList( [ + '[0', + '# 1]' + ] ) ); + + expect( command.value ).to.be.false; + } ); + + it( 'should be false if any of selected blocks is not a list item (non-list between lists)', () => { + setData( model, modelList( [ + '# [0', + '1', + '# 2]' + ] ) ); + + expect( command.value ).to.be.false; + } ); + + it( 'should be false if any of selected blocks is not a same type list item', () => { + setData( model, modelList( [ + '# [0', + '* 1]' + ] ) ); + + expect( command.value ).to.be.false; + } ); + + it( 'should be false if there is no blocks in the selection', () => { + model.schema.register( 'table', { + allowWhere: '$block', + allowAttributesOf: '$container', + isObject: true, + isBlock: true + } ); + + model.schema.register( 'tableCell', { + allowContentOf: '$container', + allowIn: 'table', + isLimit: true, + isSelectable: true + } ); + + setData( model, '[]
      ' ); + + expect( command.value ).to.be.false; + } ); + + it( 'should be true if first position in selection is in a list item of same type', () => { + setData( model, modelList( [ + '# 0[]', + '# 1' + ] ) ); + + expect( command.value ).to.be.true; + } ); + + it( 'should be true if first position in selection is in a following block of the list item', () => { + setData( model, modelList( [ + '# 0', + ' 1[]' + ] ) ); + + expect( command.value ).to.be.true; + } ); + } ); + + describe( 'isEnabled', () => { + it( 'should be true if entire selection is in a list', () => { + setData( model, modelList( [ '# [a]' ] ) ); + expect( command.isEnabled ).to.be.true; + } ); + + it( 'should be true if entire selection is in a block which can be turned into a list', () => { + setData( model, '[a]' ); + expect( command.isEnabled ).to.be.true; + } ); + + it( 'should be true if any of the selected blocks allows list attributes (the last element does not allow)', () => { + model.schema.register( 'heading1', { inheritAllFrom: '$block' } ); + model.schema.addAttributeCheck( ( ctx, attributeName ) => { + if ( ctx.endsWith( 'heading1' ) && attributeName === 'listType' ) { + return false; + } + } ); + + setData( model, + '[a' + + 'b]' + ); + + expect( command.isEnabled ).to.be.true; + } ); + + it( 'should be true if any of the selected blocks allows list attributes (the first element does not allow)', () => { + model.schema.register( 'heading1', { inheritAllFrom: '$block' } ); + model.schema.addAttributeCheck( ( ctx, attributeName ) => { + if ( ctx.endsWith( 'heading1' ) && attributeName === 'listType' ) { + return false; + } + } ); + + setData( model, + '[a' + + 'b]' + ); + + expect( command.isEnabled ).to.be.true; + } ); + + it( 'should be false if all of the selected blocks do not allow list attributes', () => { + model.schema.register( 'heading1', { inheritAllFrom: '$block' } ); + model.schema.addAttributeCheck( ( ctx, attributeName ) => { + if ( ctx.endsWith( 'heading1' ) && attributeName === 'listType' ) { + return false; + } + } ); + + setData( model, + 'a[]' + + 'b' + ); + + expect( command.isEnabled ).to.be.false; + } ); + + it( 'should be false if there is no blocks in the selection', () => { + model.schema.register( 'table', { + allowWhere: '$block', + allowAttributesOf: '$container', + isObject: true, + isBlock: true + } ); + + model.schema.register( 'tableCell', { + allowContentOf: '$container', + allowIn: 'table', + isLimit: true, + isSelectable: true + } ); + + setData( model, '[]
      ' ); + + expect( command.isEnabled ).to.be.false; + } ); + } ); + + describe( 'execute()', () => { + it( 'should use parent batch', () => { + setData( model, '[0]' ); + + model.change( writer => { + expect( writer.batch.operations.length, 'before' ).to.equal( 0 ); + + command.execute(); + + expect( writer.batch.operations.length, 'after' ).to.be.above( 0 ); + } ); + } ); + + describe( 'options.forceValue', () => { + it( 'should force converting into the list if the `options.forceValue` is set to `true`', () => { + setData( model, modelList( [ + 'fo[]o' + ] ) ); + + command.execute( { forceValue: true } ); - // TODO - // beforeEach( () => { - // setData( - // model, - // '---' + - // '---' + - // '---' + - // '---' + - // '---' + - // '---' + - // '---' + - // '---' - // ); - // } ); - // - // // https://github.com/ckeditor/ckeditor5-list/issues/62 - // it( 'should not rename blocks which cannot become listItems (list item is not allowed in their parent)', () => { - // model.schema.register( 'restricted' ); - // model.schema.extend( 'restricted', { allowIn: '$root' } ); - // - // model.schema.register( 'fooBlock', { inheritAllFrom: '$block' } ); - // model.schema.extend( 'fooBlock', { allowIn: 'restricted' } ); - // - // setData( - // model, - // 'a[bc' + - // '' + - // 'de]f' - // ); - // - // command.execute(); - // - // expect( getData( model ) ).to.equalMarkup( - // 'a[bc' + - // '' + - // 'de]f' - // ); - // } ); - // - // it( 'should not rename blocks which cannot become listItems (block is an object)', () => { - // model.schema.register( 'imageBlock', { - // isBlock: true, - // isObject: true, - // allowIn: '$root' - // } ); - // - // setData( - // model, - // 'a[bc' + - // '' + - // 'de]f' - // ); - // - // command.execute(); - // - // expect( getData( model ) ).to.equalMarkup( - // 'a[bc' + - // '' + - // 'de]f' - // ); - // } ); - // - // it( 'should rename closest block to listItem and set correct attributes', () => { - // // From first paragraph to second paragraph. - // // Command value=false, we are turning on list items. - // model.change( writer => { - // writer.setSelection( writer.createRange( - // writer.createPositionAt( root.getChild( 2 ), 0 ), - // writer.createPositionAt( root.getChild( 3 ), 'end' ) - // ) ); - // } ); - // - // command.execute(); - // - // const expectedData = - // '---' + - // '---' + - // '[---' + - // '---]' + - // '---' + - // '---' + - // '---' + - // '---'; - // - // expect( getData( model ) ).to.equalMarkup( expectedData ); - // } ); - // - // it( 'should rename closest listItem to paragraph', () => { - // // From second bullet list item to first numbered list item. - // // Command value=true, we are turning off list items. - // model.change( writer => { - // writer.setSelection( writer.createRange( - // writer.createPositionAt( root.getChild( 1 ), 0 ), - // writer.createPositionAt( root.getChild( 4 ), 'end' ) - // ) ); - // } ); - // - // // Convert paragraphs, leave numbered list items. - // command.execute(); - // - // const expectedData = - // '---' + - // '[---' + // Attributes will be removed by post fixer. - // '---' + - // '---' + - // '---]' + // Attributes will be removed by post fixer. - // '---' + - // '---' + - // '---'; - // - // expect( getData( model ) ).to.equalMarkup( expectedData ); - // } ); - // - // it( 'should change closest listItem\'s type', () => { - // // From first numbered lsit item to third bulleted list item. - // model.change( writer => { - // writer.setSelection( writer.createRange( - // writer.createPositionAt( root.getChild( 4 ), 0 ), - // writer.createPositionAt( root.getChild( 6 ), 0 ) - // ) ); - // } ); - // - // // Convert paragraphs, leave numbered list items. - // command.execute(); - // - // const expectedData = - // '---' + - // '---' + - // '---' + - // '---' + - // '[---' + - // '---' + - // ']---' + - // '---'; - // - // expect( getData( model ) ).to.equalMarkup( expectedData ); - // } ); - // - // it( 'should handle outdenting sub-items when list item is turned off', () => { - // // From first numbered list item to third bulleted list item. - // model.change( writer => { - // writer.setSelection( writer.createRange( - // writer.createPositionAt( root.getChild( 1 ), 0 ), - // writer.createPositionAt( root.getChild( 5 ), 'end' ) - // ) ); - // } ); - // - // // Convert paragraphs, leave numbered list items. - // command.execute(); - // - // const expectedData = - // '---' + - // '[---' + // Attributes will be removed by post fixer. - // '---' + - // '---' + - // '---' + // Attributes will be removed by post fixer. - // '---]' + // Attributes will be removed by post fixer. - // '---' + - // '---'; - // - // expect( getData( model ) ).to.equalMarkup( expectedData ); - // } ); - // - // // Example from docs. - // it( 'should change type of all items in nested list if one of items changed', () => { - // setData( - // model, - // '---' + - // '---' + - // '---' + - // '---' + - // '---' + - // '-[-' + - // '---' + - // '---' + - // '---' + - // '-]-' + - // '---' + - // '---' + - // '---' - // ); - // - // // * ------ <-- do not fix, top level item - // // * ------ <-- fix, because latter list item of this item's list is changed - // // * ------ <-- do not fix, item is not affected (different list) - // // * ------ <-- fix, because latter list item of this item's list is changed - // // * ------ <-- fix, because latter list item of this item's list is changed - // // * ---[-- <-- already in selection - // // * ------ <-- already in selection - // // * ------ <-- already in selection - // // * ------ <-- already in selection, but does not cause other list items to change because is top-level - // // * ---]-- <-- already in selection - // // * ------ <-- fix, because preceding list item of this item's list is changed - // // * ------ <-- do not fix, item is not affected (different list) - // // * ------ <-- do not fix, top level item - // - // command.execute(); - // - // const expectedData = - // '---' + - // '---' + - // '---' + - // '---' + - // '---' + - // '-[-' + - // '---' + - // '---' + - // '---' + - // '-]-' + - // '---' + - // '---' + - // '---'; - // - // expect( getData( model ) ).to.equalMarkup( expectedData ); - // } ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '# fo[]o {id:a00}' + ] ) ); + } ); + + it( 'should not modify list item if not needed if the list if the `options.forceValue` is set to `true`', () => { + setData( model, modelList( [ + '# fo[]o' + ] ) ); + + command.execute( { forceValue: true } ); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '# fo[]o' + ] ) ); + } ); + + it( 'should force converting into the paragraph if the `options.forceValue` is set to `false`', () => { + setData( model, modelList( [ + '# fo[]o' + ] ) ); + + command.execute( { forceValue: false } ); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + 'fo[]o' + ] ) ); + } ); + + it( 'should not modify list item if not needed if the `options.forceValue` is set to `false`', () => { + setData( model, modelList( [ + 'fo[]o' + ] ) ); + + command.execute( { forceValue: false } ); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + 'fo[]o' + ] ) ); + } ); + } ); + + describe( 'when turning on', () => { + it( 'should turn the closest block into a list item', () => { + setData( model, 'fo[]o' ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '# fo[]o {id:a00}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 0 ) + ] ); + } ); + + it( 'should change the type of an existing (closest) list item', () => { + setData( model, modelList( [ + '* fo[]o' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '# fo[]o' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 0 ) + ] ); + } ); + + it( 'should make a list items from multiple paragraphs', () => { + setData( model, modelList( [ + 'fo[o', + 'ba]r' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '# fo[o {id:a00}', + '# ba]r {id:a01}' + ] ) ); + + expect( changedBlocks.length ).to.equal( 2 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 0 ), + root.getChild( 1 ) + ] ); + } ); + + it( 'should make a list items from multiple paragraphs mixed with list items', () => { + setData( model, modelList( [ + 'a', + '[b', + '# c', + 'd]', + 'e' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + 'a', + '# [b {id:a00}', + '# c', + '# d] {id:a01}', + 'e' + ] ) ); + + expect( changedBlocks.length ).to.equal( 2 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 1 ), + root.getChild( 3 ) + ] ); + } ); + + it( 'should change type of the whole list items if only some blocks of a list item are selected', () => { + setData( model, modelList( [ + '* a', + ' [b', + 'c', + '* d]', + ' e', + '* f' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '# a', + ' [b', + '# c {id:a00}', + '# d]', + ' e', + '* f' + ] ) ); + + expect( changedBlocks.length ).to.equal( 5 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 0 ), + root.getChild( 1 ), + root.getChild( 2 ), + root.getChild( 3 ), + root.getChild( 4 ) + ] ); + } ); + + it( 'should not change type of nested list if parent is selected', () => { + setData( model, modelList( [ + '* [a', + '* b]', + ' * c', + '* d' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '# [a', + '# b]', + ' * c', + '* d' + ] ) ); + + expect( changedBlocks.length ).to.equal( 2 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 0 ), + root.getChild( 1 ) + ] ); + } ); + + it( 'should change the type of the whole list if the selection is collapsed (bulleted lists at the boundaries)', () => { + setData( model, modelList( [ + '# a', + '* b[]', + ' * c', + '* d', + '# e' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '# a', + '# b[]', + ' * c', + '# d', + '# e' + ] ) ); + + expect( changedBlocks.length ).to.equal( 2 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 1 ), + root.getChild( 3 ) + ] ); + } ); + + it( 'should change the type of the whole list if the selection is collapsed (paragraphs at the boundaries)', () => { + setData( model, modelList( [ + 'a', + '* b', + ' c[]', + ' * d', + ' e', + '* f', + 'g' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + 'a', + '# b', + ' c[]', + ' * d', + ' e', + '# f', + 'g' + ] ) ); + + expect( changedBlocks.length ).to.equal( 4 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 1 ), + root.getChild( 2 ), + root.getChild( 4 ), + root.getChild( 5 ) + ] ); + } ); + } ); + + describe( 'when turning off', () => { + it( 'should strip the list attributes from the closest list item (single list item)', () => { + setData( model, modelList( [ + '# fo[]o' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + 'fo[]o' + ] ) ); + + expect( changedBlocks.length ).to.equal( 1 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 0 ) + ] ); + } ); + + it( 'should strip the list attributes from the closest item (multiple list items, selection in first item)', () => { + setData( model, modelList( [ + '# f[]oo', + '# bar', + '# baz' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + 'f[]oo', + '# bar', + '# baz' + ] ) ); + + expect( changedBlocks.length ).to.equal( 1 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 0 ) + ] ); + } ); + + it( 'should strip the list attributes from the closest item (multiple list items, selection in the middle item)', () => { + setData( model, modelList( [ + '# foo', + '# b[]ar', + '# baz' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '# foo', + 'b[]ar', + '# baz' + ] ) ); + + expect( changedBlocks.length ).to.equal( 1 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 1 ) + ] ); + } ); + + it( 'should strip the list attributes from the closest item (multiple list items, selection in the last item)', () => { + setData( model, modelList( [ + '# foo', + '# bar', + '# b[]az' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '# foo', + '# bar', + 'b[]az' + ] ) ); + + expect( changedBlocks.length ).to.equal( 1 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 2 ) + ] ); + } ); + + describe( 'with nested lists inside', () => { + it( 'should strip the list attributes from the closest item and decrease indent of children (first item)', () => { + setData( model, modelList( [ + '# f[]oo', + ' # bar', + ' # baz', + ' # qux' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + 'f[]oo', + '# bar', + '# baz', + ' # qux' + ] ) ); + + expect( changedBlocks.length ).to.equal( 4 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 0 ), + root.getChild( 1 ), + root.getChild( 2 ), + root.getChild( 3 ) + ] ); + } ); + + it( 'should strip the list attributes from the closest item and decrease indent of children (middle item)', () => { + setData( model, modelList( [ + '# foo', + '# b[]ar', + ' # baz', + ' # qux' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '# foo', + 'b[]ar', + '# baz', + ' # qux' + ] ) ); + + expect( changedBlocks.length ).to.equal( 3 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 1 ), + root.getChild( 2 ), + root.getChild( 3 ) + ] ); + } ); + + it( 'should strip the list attributes from the selected items and decrease indent of nested list', () => { + setData( model, modelList( [ + '0', + '# 1', + ' # 2', + ' # 3[]', // <- this is turned off. + ' # 4', // <- this has to become indent = 0, because it will be first item on a new list. + ' # 5', // <- this should be still be a child of item above, so indent = 1. + ' # 6', // <- this has to become indent = 0, because it should not be a child of any of items above. + ' # 7', // <- this should be still be a child of item above, so indent = 1. + ' # 8', // <- this has to become indent = 0. + ' # 9', // <- this should still be a child of item above, so indent = 1. + ' # 10', // <- this should still be a child of item above, so indent = 2. + ' # 11', // <- this should still be at the same level as item above, so indent = 2. + '# 12', // <- this and all below are left unchanged. + ' # 13', + ' # 14' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '0', + '# 1', + ' # 2', + '3[]', + '# 4', + ' # 5', + '# 6', + ' # 7', + '# 8', + ' # 9', + ' # 10', + ' # 11', + '# 12', + ' # 13', + ' # 14' + ] ) ); + + expect( changedBlocks.length ).to.equal( 9 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 3 ), + root.getChild( 4 ), + root.getChild( 5 ), + root.getChild( 6 ), + root.getChild( 7 ), + root.getChild( 8 ), + root.getChild( 9 ), + root.getChild( 10 ), + root.getChild( 11 ) + ] ); + } ); + } ); + + describe( 'with blocks inside list items', () => { + it( 'should strip the list attributes from the first list item block', () => { + setData( model, modelList( [ + '# fo[]o', + ' bar', + ' baz' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + 'fo[]o', + '# bar {id:a00}', + ' baz' + ] ) ); + + expect( changedBlocks.length ).to.equal( 3 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 0 ), + root.getChild( 1 ), + root.getChild( 2 ) + ] ); + } ); + + it( 'should strip the list attributes from the middle list item block', () => { + setData( model, modelList( [ + '# foo', + ' ba[]r', + ' baz' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '# foo', + 'ba[]r', + '# baz {id:a00}' + ] ) ); + + expect( changedBlocks.length ).to.equal( 2 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 1 ), + root.getChild( 2 ) + ] ); + } ); + + it( 'should strip the list attributes from blocks with nested list', () => { + setData( model, modelList( [ + '# a[]', + ' b', + ' * c', + ' d', + ' * e', + ' f', + '# g' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + 'a[]', + '# b {id:a00}', + ' * c', + ' d', + ' * e', + ' f', + '# g' + ] ) ); + + expect( changedBlocks.length ).to.equal( 3 ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 0 ), + root.getChild( 1 ), + root.getChild( 5 ) + ] ); + } ); + } ); + } ); } ); } ); } ); From bf56228664218b44e017d470cb896818a648a81c Mon Sep 17 00:00:00 2001 From: Andrzej Stanek Date: Thu, 30 Dec 2021 15:22:25 +0100 Subject: [PATCH 47/66] Replace string model with ASCII lists in utils tests --- .../tests/documentlist/utils/model.js | 139 +++--- .../tests/documentlist/utils/postfixers.js | 434 ++++++++++-------- 2 files changed, 301 insertions(+), 272 deletions(-) diff --git a/packages/ckeditor5-list/tests/documentlist/utils/model.js b/packages/ckeditor5-list/tests/documentlist/utils/model.js index 1ed08094418..742aee74df2 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/model.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/model.js @@ -876,7 +876,7 @@ describe( 'DocumentList - utils - model', () => { describe( 'splitListItemBefore()', () => { it( 'should replace all blocks ids for first block given', () => { const input = modelList( [ - '* a', + '* a{a}', ' b', ' c' ] ); @@ -886,16 +886,16 @@ describe( 'DocumentList - utils - model', () => { stubUid(); model.change( writer => splitListItemBefore( fragment.getChild( 0 ), writer ) ); - expect( stringifyModel( fragment ) ).to.equal( - 'a' + - 'b' + - 'c' - ); + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* a{a00}', + ' b', + ' c' + ] ) ); } ); it( 'should replace blocks ids for second block given', () => { const input = modelList( [ - '* a', + '* a{a}', ' b', ' c' ] ); @@ -905,11 +905,11 @@ describe( 'DocumentList - utils - model', () => { stubUid(); model.change( writer => splitListItemBefore( fragment.getChild( 1 ), writer ) ); - expect( stringifyModel( fragment ) ).to.equal( - 'a' + - 'b' + - 'c' - ); + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* a{a}', + '* b{a00}', + ' c' + ] ) ); } ); it( 'should not modify other items', () => { @@ -926,13 +926,13 @@ describe( 'DocumentList - utils - model', () => { stubUid(); model.change( writer => splitListItemBefore( fragment.getChild( 2 ), writer ) ); - expect( stringifyModel( fragment ) ).to.equal( - 'x' + - 'a' + - 'b' + - 'c' + - 'y' - ); + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* x', + '* a', + '* b{a00}', + ' c', + '* y' + ] ) ); } ); it( 'should not modify nested items', () => { @@ -948,34 +948,35 @@ describe( 'DocumentList - utils - model', () => { stubUid(); model.change( writer => splitListItemBefore( fragment.getChild( 1 ), writer ) ); - expect( stringifyModel( fragment ) ).to.equal( - 'a' + - 'b' + - 'c' + - 'd' - ); + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* a', + '* b{a00}', + ' * c', + ' d' + ] ) ); } ); it( 'should not modify parent items', () => { - const input = - 'a' + - 'b' + - 'c' + - 'd' + - 'e'; + const input = modelList( [ + '* a', + ' * b', + ' c', + ' d', + ' e' + ] ); const fragment = parseModel( input, schema ); stubUid(); model.change( writer => splitListItemBefore( fragment.getChild( 2 ), writer ) ); - expect( stringifyModel( fragment ) ).to.equal( - 'a' + - 'b' + - 'c' + - 'd' + - 'e' - ); + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* a', + ' * b', + ' * c{a00}', + ' d', + ' e' + ] ) ); } ); } ); @@ -1035,7 +1036,7 @@ describe( 'DocumentList - utils - model', () => { it( 'should not apply non-list attributes', () => { const input = modelList( [ - '* 0', + '* 0{a}', ' * 1', '* 2' ] ); @@ -1047,11 +1048,11 @@ describe( 'DocumentList - utils - model', () => { changedBlocks = mergeListItemBefore( fragment.getChild( 1 ), fragment.getChild( 0 ), writer ); } ); - expect( stringifyModel( fragment ) ).to.equalMarkup( - '0' + - '1' + - '2' - ); + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* 0{a}', + '* 1{a}', + '* 2' + ] ) ); expect( changedBlocks ).to.deep.equal( [ fragment.getChild( 1 ) @@ -1078,12 +1079,12 @@ describe( 'DocumentList - utils - model', () => { model.change( writer => indentBlocks( blocks, writer ) ); - expect( stringifyModel( fragment ) ).to.equal( - 'a' + - 'b' + - 'c' + - 'd' - ); + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* a', + ' * b{000}', + ' * c', + '* d{002}' + ] ) ); } ); it( 'nested lists should keep structure', () => { @@ -1106,13 +1107,13 @@ describe( 'DocumentList - utils - model', () => { model.change( writer => indentBlocks( blocks, writer ) ); - expect( stringifyModel( fragment ) ).to.equal( - 'a' + - 'b' + - 'c' + - 'd' + - 'e' - ); + expect( stringifyModel( fragment ) ).to.equal( modelList( [ + '* a', + ' * b', + ' * c', + ' * d', + '* e' + ] ) ); } ); it( 'should apply indentation on all blocks of given items (expand = true)', () => { @@ -1213,11 +1214,11 @@ describe( 'DocumentList - utils - model', () => { it( 'should not remove attributes other than lists if outdented below 0', () => { const input = modelList( [ - '* 0', - '* 1', - ' * 2', - '* 3', - ' * 4' + '* 0', + '* 1', + ' * 2', + '* 3', + ' * 4' ] ); const fragment = parseModel( input, schema ); @@ -1233,13 +1234,13 @@ describe( 'DocumentList - utils - model', () => { changedBlocks = outdentBlocks( blocks, writer ); } ); - expect( stringifyModel( fragment ) ).to.equalMarkup( - '0' + - '1' + - '2' + - '3' + - '4' - ); + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + '* 2', + '3', + '* 4' + ] ) ); expect( changedBlocks ).to.deep.equal( blocks ); } ); diff --git a/packages/ckeditor5-list/tests/documentlist/utils/postfixers.js b/packages/ckeditor5-list/tests/documentlist/utils/postfixers.js index dc004fee80e..4e03e95d593 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/postfixers.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/postfixers.js @@ -9,6 +9,7 @@ import { fixListItemIds } from '../../../src/documentlist/utils/postfixers'; import stubUid from '../_utils/uid'; +import { modelList } from '../_utils/utils'; import Model from '@ckeditor/ckeditor5-engine/src/model/model'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; @@ -30,10 +31,11 @@ describe( 'DocumentList - utils - postfixers', () => { describe( 'findAndAddListHeadToMap()', () => { it( 'should find list that starts just after the given position', () => { - const input = - 'foo' + - 'a' + - 'b'; + const input = modelList( [ + 'foo', + '* a', + '* b' + ] ); const fragment = parseModel( input, schema ); const position = model.createPositionAt( fragment, 1 ); @@ -48,10 +50,11 @@ describe( 'DocumentList - utils - postfixers', () => { } ); it( 'should find list that starts just before the given position', () => { - const input = - 'foo' + - 'a' + - 'b'; + const input = modelList( [ + 'foo', + '* a', + '* b' + ] ); const fragment = parseModel( input, schema ); const position = model.createPositionAt( fragment, 2 ); @@ -66,10 +69,11 @@ describe( 'DocumentList - utils - postfixers', () => { } ); it( 'should find list that ends just before the given position', () => { - const input = - 'foo' + - 'a' + - 'b'; + const input = modelList( [ + 'foo', + '* a', + '* b' + ] ); const fragment = parseModel( input, schema ); const position = model.createPositionAt( fragment, 3 ); @@ -84,11 +88,12 @@ describe( 'DocumentList - utils - postfixers', () => { } ); it( 'should reuse data from map if first item was previously mapped to head', () => { - const input = - 'foo' + - 'a' + - 'b' + - 'c'; + const input = modelList( [ + 'foo', + '* a', + '* b', + '* c' + ] ); const fragment = parseModel( input, schema ); const position = model.createPositionAt( fragment, 3 ); @@ -105,11 +110,12 @@ describe( 'DocumentList - utils - postfixers', () => { } ); it( 'should reuse data from map if found some item that was previously mapped to head', () => { - const input = - 'foo' + - 'a' + - 'b' + - 'c'; + const input = modelList( [ + 'foo', + '* a', + '* b', + '* c' + ] ); const fragment = parseModel( input, schema ); const position = model.createPositionAt( fragment, 4 ); @@ -126,11 +132,12 @@ describe( 'DocumentList - utils - postfixers', () => { } ); it( 'should not mix 2 lists separated by some non-list element', () => { - const input = - 'a' + - 'foo' + - 'b' + - 'c'; + const input = modelList( [ + '* a', + 'foo', + '* b', + '* c' + ] ); const fragment = parseModel( input, schema ); const position = model.createPositionAt( fragment, 4 ); @@ -145,12 +152,13 @@ describe( 'DocumentList - utils - postfixers', () => { } ); it( 'should find list head even for mixed indents, ids, and types', () => { - const input = - 'foo' + - 'a' + - 'a' + - 'b' + - 'c'; + const input = modelList( [ + 'foo', + '* a', + ' a', + ' # b', + '* c' + ] ); const fragment = parseModel( input, schema ); const position = model.createPositionAt( fragment, 5 ); @@ -165,13 +173,14 @@ describe( 'DocumentList - utils - postfixers', () => { } ); it( 'should not find a list if position is between plain paragraphs', () => { - const input = - 'a' + - 'b' + - 'foo' + - 'bar' + - 'c' + - 'd'; + const input = modelList( [ + '* a', + '* b', + 'foo', + 'bar', + '* c', + '* d' + ] ); const fragment = parseModel( input, schema ); const position = model.createPositionAt( fragment, 3 ); @@ -187,9 +196,10 @@ describe( 'DocumentList - utils - postfixers', () => { describe( 'fixListIndents()', () => { it( 'should fix indentation of first list item', () => { - const input = - 'foo' + - 'a'; + const input = modelList( [ + 'foo', + ' * a' + ] ); const fragment = parseModel( input, schema ); @@ -197,17 +207,18 @@ describe( 'DocumentList - utils - postfixers', () => { fixListIndents( fragment.getChild( 1 ), writer ); } ); - expect( stringifyModel( fragment ) ).to.equal( - 'foo' + - 'a' - ); + expect( stringifyModel( fragment ) ).to.equal( modelList( [ + 'foo', + '* a' + ] ) ); } ); it( 'should fix indentation of to deep nested items', () => { - const input = - 'a' + - 'b' + - 'c'; + const input = modelList( [ + '* a', + ' * b', + ' * c' + ] ); const fragment = parseModel( input, schema ); @@ -215,18 +226,19 @@ describe( 'DocumentList - utils - postfixers', () => { fixListIndents( fragment.getChild( 0 ), writer ); } ); - expect( stringifyModel( fragment ) ).to.equal( - 'a' + - 'b' + - 'c' - ); + expect( stringifyModel( fragment ) ).to.equal( modelList( [ + '* a', + ' * b', + ' * c' + ] ) ); } ); it( 'should not affect properly indented items after fixed item', () => { - const input = - 'a' + - 'b' + - 'c'; + const input = modelList( [ + '* a', + ' * b', + ' * c' + ] ); const fragment = parseModel( input, schema ); @@ -234,18 +246,19 @@ describe( 'DocumentList - utils - postfixers', () => { fixListIndents( fragment.getChild( 0 ), writer ); } ); - expect( stringifyModel( fragment ) ).to.equal( - 'a' + - 'b' + - 'c' - ); + expect( stringifyModel( fragment ) ).to.equal( modelList( [ + '* a', + ' * b', + ' * c' + ] ) ); } ); it( 'should fix rapid indent spikes', () => { - const input = - 'a' + - 'b' + - 'c'; + const input = modelList( [ + ' * a', + ' * b', + ' * c' + ] ); const fragment = parseModel( input, schema ); @@ -253,19 +266,20 @@ describe( 'DocumentList - utils - postfixers', () => { fixListIndents( fragment.getChild( 0 ), writer ); } ); - expect( stringifyModel( fragment ) ).to.equal( - 'a' + - 'b' + - 'c' - ); + expect( stringifyModel( fragment ) ).to.equal( modelList( [ + '* a', + '* b', + ' * c' + ] ) ); } ); it( 'should fix rapid indent spikes after some item', () => { - const input = - 'a' + - 'b' + - 'c' + - 'd'; + const input = modelList( [ + ' * a', + ' * b', + ' * c', + ' * d' + ] ); const fragment = parseModel( input, schema ); @@ -273,23 +287,24 @@ describe( 'DocumentList - utils - postfixers', () => { fixListIndents( fragment.getChild( 0 ), writer ); } ); - expect( stringifyModel( fragment ) ).to.equal( - 'a' + - 'b' + - 'c' + - 'd' - ); + expect( stringifyModel( fragment ) ).to.equal( modelList( [ + '* a', + ' * b', + ' * c', + ' * d' + ] ) ); } ); it( 'should fix indentation keeping the relative indentations', () => { - const input = - 'a' + - 'b' + - 'c' + - 'd' + - 'e' + - 'f' + - 'g'; + const input = modelList( [ + ' * a', + ' * b', + ' * c', + ' * d', + ' * e', + ' * f', + ' * g' + ] ); const fragment = parseModel( input, schema ); @@ -297,25 +312,26 @@ describe( 'DocumentList - utils - postfixers', () => { fixListIndents( fragment.getChild( 0 ), writer ); } ); - expect( stringifyModel( fragment ) ).to.equal( - 'a' + - 'b' + - 'c' + - 'd' + - 'e' + - 'f' + - 'g' - ); + expect( stringifyModel( fragment ) ).to.equal( modelList( [ + '* a', + ' * b', + ' * c', + ' * d', + ' * e', + ' * f', + '* g' + ] ) ); } ); it( 'should flatten the leading indentation spike', () => { - const input = - 'e' + - 'f' + - 'g' + - 'h' + - 'i' + - 'j'; + const input = modelList( [ + ' # e', + ' * f', + ' * g', + ' * h', + ' # i', + '# j' + ] ); const fragment = parseModel( input, schema ); @@ -323,22 +339,24 @@ describe( 'DocumentList - utils - postfixers', () => { fixListIndents( fragment.getChild( 0 ), writer ); } ); - expect( stringifyModel( fragment ) ).to.equal( - 'e' + - 'f' + - 'g' + - 'h' + - 'i' + - 'j' - ); + expect( stringifyModel( fragment ) ).to.equal( modelList( [ + '# e', + '* f', + ' * g', + '* h', + ' # i', + '# j' + ] ) ); } ); it( 'list nested in blockquote', () => { const input = 'foo' + '
      ' + - 'foo' + - 'bar' + + modelList( [ + ' * foo', + ' * bar' + ] ) + '
      '; const fragment = parseModel( input, schema ); @@ -350,8 +368,10 @@ describe( 'DocumentList - utils - postfixers', () => { expect( stringifyModel( fragment ) ).to.equal( 'foo' + '
      ' + - 'foo' + - 'bar' + + modelList( [ + '* foo', + '* bar' + ] ) + '
      ' ); } ); @@ -359,9 +379,10 @@ describe( 'DocumentList - utils - postfixers', () => { describe( 'fixListItemIds()', () => { it( 'should update nested item ID', () => { - const input = - '0' + - '1'; + const input = modelList( [ + '* 0', + ' * 1' + ] ); const fragment = parseModel( input, model.schema ); const seenIds = new Set(); @@ -372,17 +393,18 @@ describe( 'DocumentList - utils - postfixers', () => { fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); } ); - expect( stringifyModel( fragment ) ).to.equal( - '0' + - '1' - ); + expect( stringifyModel( fragment ) ).to.equal( modelList( [ + '* 0', + ' * 1' + ] ) ); } ); it( 'should update nested item ID (middle element of bigger list item)', () => { - const input = - '0' + - '1' + - '2'; + const input = modelList( [ + '* 0', + ' * 1', + ' 2' + ] ); const fragment = parseModel( input, model.schema ); const seenIds = new Set(); @@ -393,18 +415,19 @@ describe( 'DocumentList - utils - postfixers', () => { fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); } ); - expect( stringifyModel( fragment ) ).to.equal( - '0' + - '1' + - '2' - ); + expect( stringifyModel( fragment ) ).to.equal( modelList( [ + '* 0', + ' * 1', + ' 2' + ] ) ); } ); it( 'should use same new ID if multiple items were indented', () => { - const input = - '0' + - '1' + - '2'; + const input = modelList( [ + '* 0', + ' * 1', + ' 2' + ] ); const fragment = parseModel( input, model.schema ); const seenIds = new Set(); @@ -415,18 +438,19 @@ describe( 'DocumentList - utils - postfixers', () => { fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); } ); - expect( stringifyModel( fragment ) ).to.equal( - '0' + - '1' + - '2' - ); + expect( stringifyModel( fragment ) ).to.equal( modelList( [ + '* 0', + ' * 1', + ' 2' + ] ) ); } ); it( 'should update item ID if middle item of bigger block changed type', () => { - const input = - '0' + - '1' + - '2'; + const input = modelList( [ + '* 0{a}', + '# 1{a}', + '* 2{a}' + ] ); const fragment = parseModel( input, model.schema ); const seenIds = new Set(); @@ -437,18 +461,19 @@ describe( 'DocumentList - utils - postfixers', () => { fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); } ); - expect( stringifyModel( fragment ) ).to.equal( - '0' + - '1' + - '2' - ); + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* 0{a}', + '# 1{a00}', + '* 2{a01}' + ] ) ); } ); it( 'should use same new ID if multiple items changed type', () => { - const input = - '0' + - '1' + - '2'; + const input = modelList( [ + '* 0{a}', + '# 1{a}', + '# 2{a}' + ] ); const fragment = parseModel( input, model.schema ); const seenIds = new Set(); @@ -459,19 +484,20 @@ describe( 'DocumentList - utils - postfixers', () => { fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); } ); - expect( stringifyModel( fragment ) ).to.equal( - '0' + - '1' + - '2' - ); + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* 0{a}', + '# 1{a00}', + ' 2' + ] ) ); } ); it( 'should fix ids of list with nested lists', () => { - const input = - '0' + - '1' + - '2' + - '3'; + const input = modelList( [ + '* 0{a}', + '# 1{a}', + ' * 2{b}', + '# 3{a}' + ] ); const fragment = parseModel( input, model.schema ); const seenIds = new Set(); @@ -482,24 +508,25 @@ describe( 'DocumentList - utils - postfixers', () => { fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); } ); - expect( stringifyModel( fragment ) ).to.equal( - '0' + - '1' + - '2' + - '3' - ); + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* 0{a}', + '# 1{a00}', + ' * 2{b}', + ' 3' + ] ) ); } ); it( 'should fix ids of list with altered types of multiple items of a single bigger list item', () => { - const input = - '0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' + - '7'; + const input = modelList( [ + '* 0{a}', + ' 1', + '# 2{a}', + ' 3', + '* 4{a}', + ' 5', + '# 6{a}', + ' 7' + ] ); const fragment = parseModel( input, model.schema ); const seenIds = new Set(); @@ -510,23 +537,24 @@ describe( 'DocumentList - utils - postfixers', () => { fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); } ); - expect( stringifyModel( fragment ) ).to.equal( - '0' + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' + - '7' - ); + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* 0{a}', + ' 1', + '# 2{a00}', + ' 3', + '* 4{a01}', + ' 5', + '# 6{a02}', + ' 7' + ] ) ); } ); it( 'should use new ID if some ID was spot before in the other list', () => { - const input = - '0' + - '1' + - '2'; + const input = modelList( [ + '* 0{a}', + ' * 1{b}', + ' 2' + ] ); const fragment = parseModel( input, model.schema ); const seenIds = new Set(); @@ -539,11 +567,11 @@ describe( 'DocumentList - utils - postfixers', () => { fixListItemIds( fragment.getChild( 0 ), seenIds, writer ); } ); - expect( stringifyModel( fragment ) ).to.equal( - '0' + - '1' + - '2' - ); + expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ + '* 0{a}', + ' * 1{a00}', + ' 2' + ] ) ); } ); } ); } ); From 5a00db8e7a7c398965cf10368be54ec78d59e439 Mon Sep 17 00:00:00 2001 From: Andrzej Stanek Date: Tue, 4 Jan 2022 10:49:58 +0100 Subject: [PATCH 48/66] Added list mocking manual test --- .../tests/manual/listmocking.html | 64 ++++++--- .../tests/manual/listmocking.js | 131 ++++++++++++++---- .../tests/manual/listmocking.md | 82 ++++------- 3 files changed, 181 insertions(+), 96 deletions(-) diff --git a/packages/ckeditor5-list/tests/manual/listmocking.html b/packages/ckeditor5-list/tests/manual/listmocking.html index 7085c64ec8f..ec1738b7a9e 100644 --- a/packages/ckeditor5-list/tests/manual/listmocking.html +++ b/packages/ckeditor5-list/tests/manual/listmocking.html @@ -1,5 +1,5 @@ -
      -
      -

      Model data

      - -
      -
      -

       

      - - -
      -
      -

      ASCII

      -
      
      -	
      +

      Input

      +
      + + + + + +
      + +
      + + + + + +
      +
      -
      \ No newline at end of file +

      Output

      +
      + +
      +
      
      \ No newline at end of file
      diff --git a/packages/ckeditor5-list/tests/manual/listmocking.js b/packages/ckeditor5-list/tests/manual/listmocking.js
      index 313f7002d97..b3fce715070 100644
      --- a/packages/ckeditor5-list/tests/manual/listmocking.js
      +++ b/packages/ckeditor5-list/tests/manual/listmocking.js
      @@ -12,58 +12,137 @@ import Heading from '@ckeditor/ckeditor5-heading/src/heading';
       import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
       import Undo from '@ckeditor/ckeditor5-undo/src/undo';
       import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
      -import { parse as parseModel, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
      +import {
      +	parse as parseModel,
      +	setData as setModelData,
      +	getData as getModelData
      +} from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
       
      -import List from '../../src/list';
      -import { stringifyList } from './../documentlist/_utils/utils';
      +import { modelList, stringifyList } from './../documentlist/_utils/utils';
      +import DocumentList from '../../src/documentlist';
       
       ClassicEditor
       	.create( document.querySelector( '#editor' ), {
      -		plugins: [ Enter, Typing, Heading, Paragraph, Undo, List, Clipboard ],
      +		plugins: [ Enter, Typing, Heading, Paragraph, Undo, Clipboard, DocumentList ],
       		toolbar: [ 'heading', '|', 'bulletedList', 'numberedList', 'undo', 'redo' ]
       	} )
       	.then( editor => {
       		window.editor = editor;
      +
      +		const model = 'A\n' +
      +		'B\n' +
      +		'C\n' +
      +		'D\n' +
      +		'E\n' +
      +		'F';
      +
      +		document.getElementById( 'data-input' ).value = model;
      +		document.getElementById( 'btn-process-input' ).click();
       	} )
       	.catch( err => {
       		console.error( err.stack );
       	} );
       
      -const copyAscii = () => {
      -	const ascii = document.getElementById( 'ascii-art' ).innerText;
      -	window.navigator.clipboard.writeText( ascii );
      +const copyOutput = async () => {
      +	const output = document.getElementById( 'data-output' ).innerText;
      +
      +	await window.navigator.clipboard.writeText( output );
      +
      +	const copyButton = document.getElementById( 'btn-copy-output' );
      +	const label = document.createElement( 'span' );
      +
      +	label.id = 'btn-copy-label';
      +	label.innerText = 'Copied!';
      +
      +	copyButton.appendChild( label );
      +
      +	window.setTimeout( () => {
      +		label.className = 'hide'; }
      +	, 0 );
      +
      +	window.setTimeout( () => {
      +		label.remove();
      +	}, 1000 );
       };
       
      -const asciifyModelData = () => {
      -	let modelDataString = document.getElementById( 'model-data' ).value;
      +const getListModelWithNewLines = stringifiedModel => {
      +	return stringifiedModel.replace( /<\/paragraph>/g, '\n' );
      +};
       
      -	modelDataString = modelDataString.replace( /[+|'|\t|\r|\n|;]/g, '' );
      -	modelDataString = modelDataString.replace( /> <' );
      +const setModelDataFromAscii = () => {
      +	const asciiList = document.getElementById( 'data-input' ).value;
      +	const cleanedAsciiList = asciiList.replace( /[+|'|\t|;|,]/g, '' );
      +	const modelDataArray = cleanedAsciiList.split( '\n' );
      +	const editorModelString = modelList( modelDataArray );
       
      -	const parsedModel = parseModel( modelDataString, window.editor.model.schema );
      -	const listArray = stringifyList( parsedModel ).split( '\n' );
      +	setModelData( window.editor.model, editorModelString );
      +	document.getElementById( 'data-output' ).innerText = getListModelWithNewLines( editorModelString );
      +};
      +
      +const setAsciiListFromModel = () => {
      +	const editorModelString = document.getElementById( 'data-input' ).value;
      +	const cleanedEditorModelString = editorModelString.replace( /[+|'|\t|\r|\n|;|,]/g, '' ).replace( /> <' );
      +	const editorModel = parseModel( cleanedEditorModelString, window.editor.model.schema );
      +	const asciiList = stringifyList( editorModel ).split( '\n' );
       
      -	const test = listArray.map( ( element, index ) => {
      -		if ( index === listArray.length - 1 ) {
      +	const asciiListToInsertInArray = asciiList.map( ( element, index ) => {
      +		if ( index === asciiList.length - 1 ) {
       			return `'${ element }'`;
       		}
       
       		return `'${ element }',`;
       	} );
       
      -	document.getElementById( 'ascii-art' ).innerText = 'modelList( [\n\t' +
      -														test.join( '\n\t' ) +
      -														'\n] );';
      -	setModelData( window.editor.model, modelDataString );
      -	copyAscii();
      +	const asciiListCodeSnippet = 'modelList( [\n\t' +
      +									asciiListToInsertInArray.join( '\n\t' ) +
      +								'\n] );';
      +
      +	document.getElementById( 'data-output' ).innerText = asciiListCodeSnippet;
      +	setModelData( window.editor.model, cleanedEditorModelString );
      +};
      +
      +const processInput = () => {
      +	const dataType = document.querySelector( 'input[name="input-type"]:checked' ).value;
      +
      +	if ( dataType === 'model' ) {
      +		setAsciiListFromModel();
      +	}
      +
      +	if ( dataType === 'ascii' ) {
      +		setModelDataFromAscii();
      +	}
      +
      +	if ( document.getElementById( 'chbx-should-copy' ).checked ) {
      +		copyOutput();
      +	}
      +};
      +
      +const processEditorModel = () => {
      +	const dataType = document.querySelector( 'input[name="input-type"]:checked' ).value;
      +
      +	if ( dataType === 'model' ) {
      +		const editorModelStringWithNewLines = getListModelWithNewLines( getModelData( window.editor.model, { withoutSelection: true } ) );
      +
      +		document.getElementById( 'data-input' ).value = editorModelStringWithNewLines;
      +	}
      +
      +	if ( dataType === 'ascii' ) {
      +		const stringifiedEditorModel = getModelData( window.editor.model, { withoutSelection: true } );
      +		const editorModel = parseModel( stringifiedEditorModel, window.editor.model.schema );
      +
      +		document.getElementById( 'data-input' ).value = stringifyList( editorModel );
      +	}
      +
      +	processInput();
       };
       
       const onPaste = () => {
      -	window.setTimeout( () => {
      -		asciifyModelData();
      -	}, 0 );
      +	if ( document.getElementById( 'chbx-process-on-paste' ).checked ) {
      +		window.setTimeout( processInput, 0 );
      +	}
       };
       
      -document.getElementById( 'asciify' ).addEventListener( 'click', asciifyModelData );
      -document.getElementById( 'btn-copy-ascii' ).addEventListener( 'click', copyAscii );
      -document.getElementById( 'model-data' ).addEventListener( 'paste', onPaste );
      +document.getElementById( 'btn-process-input' ).addEventListener( 'click', processInput );
      +document.getElementById( 'btn-process-editor-model' ).addEventListener( 'click', processEditorModel );
      +document.getElementById( 'btn-copy-output' ).addEventListener( 'click', copyOutput );
      +document.getElementById( 'data-input' ).addEventListener( 'paste', onPaste );
      diff --git a/packages/ckeditor5-list/tests/manual/listmocking.md b/packages/ckeditor5-list/tests/manual/listmocking.md
      index d58bd9c2c16..91f5f76691e 100644
      --- a/packages/ckeditor5-list/tests/manual/listmocking.md
      +++ b/packages/ckeditor5-list/tests/manual/listmocking.md
      @@ -1,53 +1,29 @@
      -### Loading
      -
      -1. The data should be loaded with:
      -  * two paragraphs,
      -  * bulleted list with eight items,
      -  * two paragraphs,
      -  * numbered list with one item,
      -  * bullet list with one item.
      -2. Toolbar should have two buttons: for bullet and for numbered list.
      -
      -### Testing
      -
      -After each step test undo (whole stack) -> redo (whole stack) -> undo (whole stack).
      -
      -Creating:
      -
      -1. Convert first paragraph to list item
      -2. Create empty paragraph and convert to list item
      -3. Enter in the middle of item
      -4. Enter at the start of item
      -5. Enter at the end of item
      -
      -Removing:
      -
      -1. Delete all contents from list item and then the list item
      -2. Press enter in empty list item
      -3. Click on highlighted button ("turn off" list feature)
      -4. Do it for first, second and last list item
      -
      -Changing type:
      -
      -1. Change type from bulleted to numbered
      -2. Do it for first, second and last item
      -3. Do it for multiple items at once
      -
      -Merging:
      -
      -1. Convert paragraph before list to same type of list
      -2. Convert paragraph after list to same type of list
      -3. Convert paragraph before list to different type of list
      -4. Convert paragraph after list to different type of list
      -5. Convert first paragraph to bulleted list, then convert second paragraph to bulleted list
      -6. Convert multiple items and paragraphs at once
      -
      -Selection deletion. Make selection between items and press delete button:
      -
      -1. two items from the same list
      -2. all items in a list
      -3. paragraph before list and second item of list
      -4. paragraph after list and one-but-last item of list
      -5. two paragraphs that have list between them
      -6. two items from different lists of same type
      -7. two items from different lists of different type
      +## Description
      +Main purpose of this tool is to process editor's model to ASCII art that can be used in automatic tests so they are more readable.
      +It also allows to process ASCII art back to model data. You can provide your own editor's model/ASCII art to the input and parse it or you can create list in an editor and get model/ASCII from it.
      +
      +### ASCII Tree
      +
      +* A
      +  B
      +  # C
      +    # D
      +* E
      +* F
      +
      +* - bulleted list
      +# - numbered list
      +Each indentation is two spaces before list type.
      +
      + +## Input +Input should be valid editor's model or an ASCII art created in this tool. Processing function tries to be a little bit smart (naively) and cleans input so it can be copied and pasted from code - it will get rid of spaces, new lines and other characters not allowed in model. + +## Editor +Editor allows to inspect how the processed data renders in the editor. You can also create your list in the editor and create model/ASCII from it with 'Process editor model' button. + +## Output +### When input is model +It should create ASCII tree as a code ready to be pasted in tests. +### When input is ASCII +It should create correct editor's model. \ No newline at end of file From f6d94bba420ef4f8adadbc881becf0b361e9420d Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 4 Jan 2022 12:31:21 +0100 Subject: [PATCH 49/66] Updated docs. Cleanup. --- .../src/documentlist/documentlistcommand.js | 8 ++++---- .../documentlist/documentlistindentcommand.js | 14 ++++++++------ .../src/documentlist/utils/model.js | 16 ++++++++++------ 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js index ab5cb5dacb7..dec52e8902e 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js @@ -20,7 +20,7 @@ import { } from './utils/model'; /** - * The list command. It is used by the {@link TODO document list feature}. + * The list command. It is used by the {@link module:list/documentlist~DocumentList document list feature}. * * @extends module:core/command~Command */ @@ -95,7 +95,7 @@ export default class DocumentListCommand extends Command { // Outdent items following the selected list item. changedBlocks.push( ...outdentItemsAfterItemRemoved( lastBlock, writer ) ); - this._fireAfterExecute( sortBlocks( new Set( changedBlocks ) ) ); + this._fireAfterExecute( changedBlocks ); } // Turning on the list items for a collapsed selection inside a list item. else if ( document.selection.isCollapsed && blocks[ 0 ].hasAttribute( 'listType' ) ) { @@ -139,7 +139,7 @@ export default class DocumentListCommand extends Command { } /** - * TODO + * Fires the `afterExecute` event. * * @private * @param {Array.} changedBlocks The changed list elements. @@ -154,7 +154,7 @@ export default class DocumentListCommand extends Command { * @protected * @event afterExecute */ - this.fire( 'afterExecute', changedBlocks ); + this.fire( 'afterExecute', sortBlocks( new Set( changedBlocks ) ) ); } /** diff --git a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js index a8ffe0b75f6..f296d93978e 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js @@ -14,6 +14,7 @@ import { isFirstBlockOfListItem, isOnlyOneListItemSelected, outdentBlocks, + sortBlocks, splitListItemBefore } from './utils/model'; import ListWalker from './utils/listwalker'; @@ -64,17 +65,18 @@ export default class DocumentListIndentCommand extends Command { model.change( writer => { // Handle selection contained in the single list item and starting in the following blocks. if ( isOnlyOneListItemSelected( blocks ) && !isFirstBlockOfListItem( blocks[ 0 ] ) ) { + const changedBlocks = []; + // Allow increasing indent of following list item blocks. if ( this._direction == 'forward' ) { - indentBlocks( blocks, writer ); + changedBlocks.push( ...indentBlocks( blocks, writer ) ); } // For indent make sure that indented blocks have a new ID. // For outdent just split blocks from the list item (give them a new IDs). - splitListItemBefore( blocks[ 0 ], writer ); - // TODO add split result to changed blocks. + changedBlocks.push( ...splitListItemBefore( blocks[ 0 ], writer ) ); - this._fireAfterExecute( blocks ); + this._fireAfterExecute( changedBlocks ); } // More than a single list item is selected, or the first block of list item is selected. else { @@ -89,7 +91,7 @@ export default class DocumentListIndentCommand extends Command { } /** - * TODO + * Fires the `afterExecute` event. * * @private * @param {Array.} changedBlocks The changed list elements. @@ -104,7 +106,7 @@ export default class DocumentListIndentCommand extends Command { * @protected * @event afterExecute */ - this.fire( 'afterExecute', changedBlocks ); + this.fire( 'afterExecute', sortBlocks( new Set( changedBlocks ) ) ); } /** diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.js b/packages/ckeditor5-list/src/documentlist/utils/model.js index 0891a635294..78e45b9cf35 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.js +++ b/packages/ckeditor5-list/src/documentlist/utils/model.js @@ -33,7 +33,9 @@ export class ListItemUid { * * @protected * @param {module:engine/model/element~Element} listItem Starting list item element. - * @param {Object} options TODO + * @param {Object} [options] + * @param {Boolean} [options.higherIndent=false] Whether blocks with a higher indent level than the start block should be included + * in the result. * @return {Array.} */ export function getAllListItemBlocks( listItem, options = {} ) { @@ -54,7 +56,8 @@ export function getAllListItemBlocks( listItem, options = {} ) { * @param {module:engine/model/element~Element} listItem Starting list item element. * @param {Object} [options] * @param {'forward'|'backward'} [options.direction='backward'] Walking direction. - * TODO all options + * @param {Boolean} [options.higherIndent=false] Whether blocks with a higher indent level than the start block should be included + * in the result. * @returns {Array.} */ export function getListItemBlocks( listItem, options = {} ) { @@ -183,7 +186,6 @@ export function expandListBlocksToCompleteItems( blocks, options = {} ) { * @param {module:engine/model/writer~Writer} writer The model writer. * @returns {Array.} The array of updated blocks. */ -// TODO add test for return value. export function splitListItemBefore( listBlock, writer ) { const blocks = getListItemBlocks( listBlock, { direction: 'forward' } ); const id = ListItemUid.next(); @@ -330,8 +332,10 @@ export function removeListAttributes( blocks, writer ) { /** * Checks whether the given blocks are related to a single list item. - * TODO + * * @protected + * @param {Array.} blocks The list block elements. + * @returns {Boolean} */ export function isOnlyOneListItemSelected( blocks ) { if ( !blocks.length ) { @@ -348,7 +352,8 @@ export function isOnlyOneListItemSelected( blocks ) { } /** - * TODO + * Modifies the indents of list blocks following the given list block so the indentation is valid after + * the given block is no longer a list item. * * @protected */ @@ -442,7 +447,6 @@ export function outdentItemsAfterItemRemoved( lastBlock, writer ) { * @param {Iterable.} blocks The array of blocks. * @returns {Array.} The sorted array of blocks. */ -// TODO add tests. export function sortBlocks( blocks ) { return Array.from( blocks ).sort( ( a, b ) => a.index - b.index ); } From 800a9272b795ff3765e676bb3b4faff8cab91581 Mon Sep 17 00:00:00 2001 From: Andrzej Stanek Date: Tue, 4 Jan 2022 12:48:20 +0100 Subject: [PATCH 50/66] Fix lost mocking manual test --- packages/ckeditor5-list/tests/manual/listmocking.html | 4 ++-- packages/ckeditor5-list/tests/manual/listmocking.js | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-list/tests/manual/listmocking.html b/packages/ckeditor5-list/tests/manual/listmocking.html index ec1738b7a9e..d7875b1569d 100644 --- a/packages/ckeditor5-list/tests/manual/listmocking.html +++ b/packages/ckeditor5-list/tests/manual/listmocking.html @@ -49,8 +49,8 @@

      Input

      - - + + diff --git a/packages/ckeditor5-list/tests/manual/listmocking.js b/packages/ckeditor5-list/tests/manual/listmocking.js index b3fce715070..357c9044fe8 100644 --- a/packages/ckeditor5-list/tests/manual/listmocking.js +++ b/packages/ckeditor5-list/tests/manual/listmocking.js @@ -12,6 +12,7 @@ import Heading from '@ckeditor/ckeditor5-heading/src/heading'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Undo from '@ckeditor/ckeditor5-undo/src/undo'; import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; +import Indent from '@ckeditor/ckeditor5-indent/src/indent'; import { parse as parseModel, setData as setModelData, @@ -23,8 +24,8 @@ import DocumentList from '../../src/documentlist'; ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ Enter, Typing, Heading, Paragraph, Undo, Clipboard, DocumentList ], - toolbar: [ 'heading', '|', 'bulletedList', 'numberedList', 'undo', 'redo' ] + plugins: [ Enter, Typing, Heading, Paragraph, Undo, Clipboard, DocumentList, Indent ], + toolbar: [ 'heading', '|', 'bulletedList', 'numberedList', 'outdent', 'indent', '|', 'undo', 'redo' ] } ) .then( editor => { window.editor = editor; From b34058777c4bfb459b77a56540e553eb37b2be88 Mon Sep 17 00:00:00 2001 From: Andrzej Stanek Date: Tue, 4 Jan 2022 13:01:18 +0100 Subject: [PATCH 51/66] Fix list mocking manual test label --- packages/ckeditor5-list/tests/manual/listmocking.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-list/tests/manual/listmocking.html b/packages/ckeditor5-list/tests/manual/listmocking.html index d7875b1569d..da893870135 100644 --- a/packages/ckeditor5-list/tests/manual/listmocking.html +++ b/packages/ckeditor5-list/tests/manual/listmocking.html @@ -52,7 +52,7 @@

      Input

      - +
      From ceb31f1faecbffcc758cb9bfea3b72fdee257065 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 4 Jan 2022 14:44:42 +0100 Subject: [PATCH 52/66] Code refactoring. --- .../src/documentlist/documentlistui.js | 40 ------------------- .../tests/manual/listmocking.js | 4 +- .../tests/manual/listmocking.md | 7 ++-- 3 files changed, 6 insertions(+), 45 deletions(-) delete mode 100644 packages/ckeditor5-list/src/documentlist/documentlistui.js diff --git a/packages/ckeditor5-list/src/documentlist/documentlistui.js b/packages/ckeditor5-list/src/documentlist/documentlistui.js deleted file mode 100644 index fcae1349038..00000000000 --- a/packages/ckeditor5-list/src/documentlist/documentlistui.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @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/list/listui - */ - -import { createUIComponent } from '../list/utils'; - -import numberedListIcon from '../../theme/icons/numberedlist.svg'; -import bulletedListIcon from '../../theme/icons/bulletedlist.svg'; - -import { Plugin } from 'ckeditor5/src/core'; - -/** - * The document list UI feature. It introduces the `'numberedList'` and `'bulletedList'` buttons. - * - * @extends module:core/plugin~Plugin - */ -export default class DocumentListUI extends Plugin { - /** - * @inheritDoc - */ - static get pluginName() { - return 'DocumentListUI'; - } - - /** - * @inheritDoc - */ - init() { - const t = this.editor.t; - - // Create two buttons and link them with numberedList and bulletedList commands. - createUIComponent( this.editor, 'numberedList', t( 'Numbered List' ), numberedListIcon ); - createUIComponent( this.editor, 'bulletedList', t( 'Bulleted List' ), bulletedListIcon ); - } -} diff --git a/packages/ckeditor5-list/tests/manual/listmocking.js b/packages/ckeditor5-list/tests/manual/listmocking.js index 357c9044fe8..6baa158d361 100644 --- a/packages/ckeditor5-list/tests/manual/listmocking.js +++ b/packages/ckeditor5-list/tests/manual/listmocking.js @@ -95,8 +95,8 @@ const setAsciiListFromModel = () => { } ); const asciiListCodeSnippet = 'modelList( [\n\t' + - asciiListToInsertInArray.join( '\n\t' ) + - '\n] );'; + asciiListToInsertInArray.join( '\n\t' ) + + '\n] );'; document.getElementById( 'data-output' ).innerText = asciiListCodeSnippet; setModelData( window.editor.model, cleanedEditorModelString ); diff --git a/packages/ckeditor5-list/tests/manual/listmocking.md b/packages/ckeditor5-list/tests/manual/listmocking.md index 91f5f76691e..f0581eda14a 100644 --- a/packages/ckeditor5-list/tests/manual/listmocking.md +++ b/packages/ckeditor5-list/tests/manual/listmocking.md @@ -3,7 +3,8 @@ Main purpose of this tool is to process editor's model to ASCII art that can be It also allows to process ASCII art back to model data. You can provide your own editor's model/ASCII art to the input and parse it or you can create list in an editor and get model/ASCII from it. ### ASCII Tree -
      +
      +```
       * A
         B
         # C
      @@ -14,7 +15,7 @@ It also allows to process ASCII art back to model data. You can provide your own
       * - bulleted list
       # - numbered list
       Each indentation is two spaces before list type.
      -
      +``` ## Input Input should be valid editor's model or an ASCII art created in this tool. Processing function tries to be a little bit smart (naively) and cleans input so it can be copied and pasted from code - it will get rid of spaces, new lines and other characters not allowed in model. @@ -26,4 +27,4 @@ Editor allows to inspect how the processed data renders in the editor. You can a ### When input is model It should create ASCII tree as a code ready to be pasted in tests. ### When input is ASCII -It should create correct editor's model. \ No newline at end of file +It should create correct editor's model. From e3e2c9bcae8711c18a41ae5c49c503a669cee235 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> Date: Tue, 4 Jan 2022 15:39:14 +0100 Subject: [PATCH 53/66] Apply review comment. Co-authored-by: Aleksander Nowodzinski --- packages/ckeditor5-list/src/documentlist/utils/model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.js b/packages/ckeditor5-list/src/documentlist/utils/model.js index 78e45b9cf35..0dfeba8da3e 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.js +++ b/packages/ckeditor5-list/src/documentlist/utils/model.js @@ -309,7 +309,7 @@ export function outdentBlocks( blocks, writer, { expand } = {} ) { } /** - * Removes list attributes from the fiven blocks. + * Removes all list attributes from the given blocks. * * @protected * @param {module:engine/model/element~Element|Iterable.} blocks The block or iterable of blocks. From 162d0fdeacf27e7c704176fda5ab596f46f3417a Mon Sep 17 00:00:00 2001 From: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> Date: Tue, 4 Jan 2022 15:42:52 +0100 Subject: [PATCH 54/66] Apply review comment. Co-authored-by: Aleksander Nowodzinski --- packages/ckeditor5-list/src/documentlist/utils/model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.js b/packages/ckeditor5-list/src/documentlist/utils/model.js index 0dfeba8da3e..25aec10575c 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.js +++ b/packages/ckeditor5-list/src/documentlist/utils/model.js @@ -441,7 +441,7 @@ export function outdentItemsAfterItemRemoved( lastBlock, writer ) { } /** - * Returns the sorted array of given blocks. + * Returns the array of given blocks sorted by model indexes (document order). * * @protected * @param {Iterable.} blocks The array of blocks. From 2dc9e984cf1f908fe64bfbcdbe8051fada2fe89f Mon Sep 17 00:00:00 2001 From: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> Date: Tue, 4 Jan 2022 15:46:38 +0100 Subject: [PATCH 55/66] Apply review comment. Co-authored-by: Aleksander Nowodzinski --- packages/ckeditor5-list/src/documentlist/documentlistcommand.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js index dec52e8902e..5a51788cc53 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js @@ -4,7 +4,7 @@ */ /** - * @module list/documentlist/documentlistcinnabd + * @module list/documentlist/documentlistcommand */ import { Command } from 'ckeditor5/src/core'; From 31b6aa33c9ea39f779b88bdea60047d2eef3b9c1 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> Date: Tue, 4 Jan 2022 15:47:06 +0100 Subject: [PATCH 56/66] Apply review comment. Co-authored-by: Aleksander Nowodzinski --- packages/ckeditor5-list/src/documentlist/documentlistcommand.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js index 5a51788cc53..31100d06f89 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js @@ -38,7 +38,7 @@ export default class DocumentListCommand extends Command { * The type of the list created by the command. * * @readonly - * @member {'numbered'|'bulleted'|'todo'} + * @member {'numbered'|'bulleted'} */ this.type = type; From 76fcdfcfc23f9a9e0bd80dd00377b8f70cd812b3 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> Date: Tue, 4 Jan 2022 15:47:21 +0100 Subject: [PATCH 57/66] Apply review comment. Co-authored-by: Aleksander Nowodzinski --- packages/ckeditor5-list/tests/documentlist/_utils/uid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-list/tests/documentlist/_utils/uid.js b/packages/ckeditor5-list/tests/documentlist/_utils/uid.js index 1d8f435a50c..cb92217bd3a 100644 --- a/packages/ckeditor5-list/tests/documentlist/_utils/uid.js +++ b/packages/ckeditor5-list/tests/documentlist/_utils/uid.js @@ -6,7 +6,7 @@ import { ListItemUid } from '../../../src/documentlist/utils/model'; /** - * Mocks the `uid()` with sequential numbers. + * Mocks the `ListItemUid.next()` with sequential numbers. * * @param {Number} [start=0xa00] The uid start number. */ From 77b7684110d127f6037520fed4819d93b6941b2a Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 4 Jan 2022 15:57:35 +0100 Subject: [PATCH 58/66] Updated docs. --- .../src/documentlist/documentlistcommand.js | 1 + .../documentlist/documentlistindentcommand.js | 2 +- .../tests/documentlist/_utils/utils.js | 16 +++++++++++++--- .../documentlist/documentlistindentcommand.js | 2 -- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js index 31100d06f89..c474dfdc71f 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js @@ -63,6 +63,7 @@ export default class DocumentListCommand extends Command { * Executes the list command. * * @fires execute + * @fires afterExecute * @param {Object} [options] Command options. * @param {Boolean} [options.forceValue] If set, it will force the command behavior. If `true`, the command will try to convert the * selected items and potentially the neighbor elements to the proper list items. If set to `false` it will convert selected elements diff --git a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js index f296d93978e..a8a35687a6b 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js @@ -56,7 +56,7 @@ export default class DocumentListIndentCommand extends Command { * Indents or outdents (depending on the {@link #constructor}'s `indentDirection` parameter) selected list items. * * @fires execute - * @fires _executeCleanup + * @fires afterExecute */ execute() { const model = this.editor.model; diff --git a/packages/ckeditor5-list/tests/documentlist/_utils/utils.js b/packages/ckeditor5-list/tests/documentlist/_utils/utils.js index 3bb0ff9374a..6356340b232 100644 --- a/packages/ckeditor5-list/tests/documentlist/_utils/utils.js +++ b/packages/ckeditor5-list/tests/documentlist/_utils/utils.js @@ -204,7 +204,17 @@ export function setupTestHelpers( editor ) { } /** - * TODO + * Returns a model representation of a document list pseudo markdown notation: + * + * modelList( [ + * '* foo', + * '* bar' + * ] ); + * + * will output: + * + * 'foo' + + * 'bar' * * @param {Iterable.} lines * @returns {String} @@ -257,9 +267,9 @@ export function modelList( lines ) { } /** - * TODO + * Returns document list pseudo markdown notation for a given document fragment. * - * @param fragment + * @param {module:engine/model/documentfragment~DocumentFragment} fragment The document fragment to stringify to pseudo markdown notation. * @returns {String} */ export function stringifyList( fragment ) { diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js index 69dc2512931..db69dc9a993 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js @@ -16,8 +16,6 @@ import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model describe( 'DocumentListIndentCommand', () => { let editor, model, doc, root; - // TODO check changed blocks (afterExecute event) - testUtils.createSinonSandbox(); beforeEach( () => { From 7cfad2c1a6d0ccd8a18a979833f1b7f534a42c5e Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 4 Jan 2022 15:58:52 +0100 Subject: [PATCH 59/66] Updated docs. --- packages/ckeditor5-list/src/documentlist/utils/model.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.js b/packages/ckeditor5-list/src/documentlist/utils/model.js index 25aec10575c..f65796c2e38 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.js +++ b/packages/ckeditor5-list/src/documentlist/utils/model.js @@ -12,6 +12,8 @@ import ListWalker, { iterateSiblingListBlocks } from './listwalker'; /** * The list item ID generator. + * + * @protected */ export class ListItemUid { /** @@ -356,6 +358,9 @@ export function isOnlyOneListItemSelected( blocks ) { * the given block is no longer a list item. * * @protected + * @param {module:engine/model/element~Element} lastBlock The last list block that has become a non-list element. + * @param {module:engine/model/writer~Writer} writer The model writer. + * @returns {Array.} Array of altered blocks. */ export function outdentItemsAfterItemRemoved( lastBlock, writer ) { const changedBlocks = []; From 842c89b656d34c38c08b8e598d2d16e8ea44cf0a Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 4 Jan 2022 16:14:17 +0100 Subject: [PATCH 60/66] Util functions renaming. --- .../src/documentlist/documentlistcommand.js | 4 ++-- .../documentlist/documentlistindentcommand.js | 6 +++--- .../src/documentlist/utils/model.js | 4 ++-- .../tests/documentlist/utils/model.js | 18 +++++++++--------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js index c474dfdc71f..08d16999a7f 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistcommand.js @@ -14,7 +14,7 @@ import { getListItemBlocks, getListItems, removeListAttributes, - outdentItemsAfterItemRemoved, + outdentFollowingItems, ListItemUid, sortBlocks } from './utils/model'; @@ -94,7 +94,7 @@ export default class DocumentListCommand extends Command { changedBlocks.push( ...removeListAttributes( blocks, writer ) ); // Outdent items following the selected list item. - changedBlocks.push( ...outdentItemsAfterItemRemoved( lastBlock, writer ) ); + changedBlocks.push( ...outdentFollowingItems( lastBlock, writer ) ); this._fireAfterExecute( changedBlocks ); } diff --git a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js index a8a35687a6b..ed38b9f57e6 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js @@ -12,7 +12,7 @@ import { expandListBlocksToCompleteItems, indentBlocks, isFirstBlockOfListItem, - isOnlyOneListItemSelected, + isSingleListItem, outdentBlocks, sortBlocks, splitListItemBefore @@ -64,7 +64,7 @@ export default class DocumentListIndentCommand extends Command { model.change( writer => { // Handle selection contained in the single list item and starting in the following blocks. - if ( isOnlyOneListItemSelected( blocks ) && !isFirstBlockOfListItem( blocks[ 0 ] ) ) { + if ( isSingleListItem( blocks ) && !isFirstBlockOfListItem( blocks[ 0 ] ) ) { const changedBlocks = []; // Allow increasing indent of following list item blocks. @@ -131,7 +131,7 @@ export default class DocumentListIndentCommand extends Command { } // A single block of a list item is selected, so it could be indented as a sublist. - if ( isOnlyOneListItemSelected( blocks ) && !isFirstBlockOfListItem( blocks[ 0 ] ) ) { + if ( isSingleListItem( blocks ) && !isFirstBlockOfListItem( blocks[ 0 ] ) ) { return true; } diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.js b/packages/ckeditor5-list/src/documentlist/utils/model.js index f65796c2e38..358615219d3 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.js +++ b/packages/ckeditor5-list/src/documentlist/utils/model.js @@ -339,7 +339,7 @@ export function removeListAttributes( blocks, writer ) { * @param {Array.} blocks The list block elements. * @returns {Boolean} */ -export function isOnlyOneListItemSelected( blocks ) { +export function isSingleListItem( blocks ) { if ( !blocks.length ) { return false; } @@ -362,7 +362,7 @@ export function isOnlyOneListItemSelected( blocks ) { * @param {module:engine/model/writer~Writer} writer The model writer. * @returns {Array.} Array of altered blocks. */ -export function outdentItemsAfterItemRemoved( lastBlock, writer ) { +export function outdentFollowingItems( lastBlock, writer ) { const changedBlocks = []; // Start from the model item that is just after the last turned-off item. diff --git a/packages/ckeditor5-list/tests/documentlist/utils/model.js b/packages/ckeditor5-list/tests/documentlist/utils/model.js index 28fa3978f5e..ae72a03ccb0 100644 --- a/packages/ckeditor5-list/tests/documentlist/utils/model.js +++ b/packages/ckeditor5-list/tests/documentlist/utils/model.js @@ -12,11 +12,11 @@ import { indentBlocks, isFirstBlockOfListItem, isLastBlockOfListItem, - isOnlyOneListItemSelected, + isSingleListItem, ListItemUid, mergeListItemBefore, outdentBlocks, - outdentItemsAfterItemRemoved, + outdentFollowingItems, removeListAttributes, splitListItemBefore } from '../../../src/documentlist/utils/model'; @@ -1474,9 +1474,9 @@ describe( 'DocumentList - utils - model', () => { } ); } ); - describe( 'isOnlyOneListItemSelected()', () => { + describe( 'isSingleListItem()', () => { it( 'should return false if no blocks are given', () => { - expect( isOnlyOneListItemSelected( [] ) ).to.be.false; + expect( isSingleListItem( [] ) ).to.be.false; } ); it( 'should return false if first block is not a list item', () => { @@ -1490,7 +1490,7 @@ describe( 'DocumentList - utils - model', () => { fragment.getChild( 1 ) ]; - expect( isOnlyOneListItemSelected( blocks ) ).to.be.false; + expect( isSingleListItem( blocks ) ).to.be.false; } ); it( 'should return false if any block has a different ID', () => { @@ -1507,7 +1507,7 @@ describe( 'DocumentList - utils - model', () => { fragment.getChild( 2 ) ]; - expect( isOnlyOneListItemSelected( blocks ) ).to.be.false; + expect( isSingleListItem( blocks ) ).to.be.false; } ); it( 'should return true if all block has the same ID', () => { @@ -1523,11 +1523,11 @@ describe( 'DocumentList - utils - model', () => { fragment.getChild( 1 ) ]; - expect( isOnlyOneListItemSelected( blocks ) ).to.be.true; + expect( isSingleListItem( blocks ) ).to.be.true; } ); } ); - describe( 'outdentItemsAfterItemRemoved()', () => { + describe( 'outdentFollowingItems()', () => { it( 'should outdent all items and keep nesting structure where possible', () => { const input = modelList( [ '0', @@ -1551,7 +1551,7 @@ describe( 'DocumentList - utils - model', () => { let changedBlocks; model.change( writer => { - changedBlocks = outdentItemsAfterItemRemoved( fragment.getChild( 3 ), writer ); + changedBlocks = outdentFollowingItems( fragment.getChild( 3 ), writer ); } ); expect( stringifyModel( fragment ) ).to.equalMarkup( modelList( [ From e89652c6e705b948066f9991c825a9084d36ed46 Mon Sep 17 00:00:00 2001 From: Andrzej Stanek Date: Fri, 7 Jan 2022 14:24:11 +0100 Subject: [PATCH 61/66] Fix list mocking manual test --- .../tests/manual/listmocking.html | 2 + .../tests/manual/listmocking.js | 50 +++++++++++++------ .../tests/manual/listmocking.md | 9 +++- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/packages/ckeditor5-list/tests/manual/listmocking.html b/packages/ckeditor5-list/tests/manual/listmocking.html index da893870135..c4b9c0e06cb 100644 --- a/packages/ckeditor5-list/tests/manual/listmocking.html +++ b/packages/ckeditor5-list/tests/manual/listmocking.html @@ -55,6 +55,8 @@

      Input

      + +
      diff --git a/packages/ckeditor5-list/tests/manual/listmocking.js b/packages/ckeditor5-list/tests/manual/listmocking.js index 6baa158d361..b40d7d0e3c7 100644 --- a/packages/ckeditor5-list/tests/manual/listmocking.js +++ b/packages/ckeditor5-list/tests/manual/listmocking.js @@ -31,11 +31,11 @@ ClassicEditor window.editor = editor; const model = 'A\n' + - 'B\n' + - 'C\n' + - 'D\n' + - 'E\n' + - 'F'; + 'B\n' + + 'C\n' + + 'D\n' + + 'E\n' + + 'F'; document.getElementById( 'data-input' ).value = model; document.getElementById( 'btn-process-input' ).click(); @@ -58,7 +58,8 @@ const copyOutput = async () => { copyButton.appendChild( label ); window.setTimeout( () => { - label.className = 'hide'; } + label.className = 'hide'; + } , 0 ); window.setTimeout( () => { @@ -72,19 +73,20 @@ const getListModelWithNewLines = stringifiedModel => { const setModelDataFromAscii = () => { const asciiList = document.getElementById( 'data-input' ).value; - const cleanedAsciiList = asciiList.replace( /[+|'|\t|;|,]/g, '' ); - const modelDataArray = cleanedAsciiList.split( '\n' ); + const modelDataArray = []; + + asciiList.replace( /[^']*'(.*)'.*$/gm, ( match, content ) => { + modelDataArray.push( content ); + } ); + const editorModelString = modelList( modelDataArray ); setModelData( window.editor.model, editorModelString ); document.getElementById( 'data-output' ).innerText = getListModelWithNewLines( editorModelString ); }; -const setAsciiListFromModel = () => { - const editorModelString = document.getElementById( 'data-input' ).value; - const cleanedEditorModelString = editorModelString.replace( /[+|'|\t|\r|\n|;|,]/g, '' ).replace( /> <' ); - const editorModel = parseModel( cleanedEditorModelString, window.editor.model.schema ); - const asciiList = stringifyList( editorModel ).split( '\n' ); +const createAsciiListCodeSnippet = stringifiedAsciiList => { + const asciiList = stringifiedAsciiList.split( '\n' ); const asciiListToInsertInArray = asciiList.map( ( element, index ) => { if ( index === asciiList.length - 1 ) { @@ -96,7 +98,21 @@ const setAsciiListFromModel = () => { const asciiListCodeSnippet = 'modelList( [\n\t' + asciiListToInsertInArray.join( '\n\t' ) + - '\n] );'; + '\n] );'; + + return asciiListCodeSnippet; +}; + +const setAsciiListFromModel = () => { + const editorModelString = document.getElementById( 'data-input' ).value; + let cleanedEditorModelString = ''; + + editorModelString.replace( /()/gm, ( match, content ) => { + cleanedEditorModelString += content; + } ); + + const editorModel = parseModel( cleanedEditorModelString, window.editor.model.schema ); + const asciiListCodeSnippet = createAsciiListCodeSnippet( stringifyList( editorModel ) ); document.getElementById( 'data-output' ).innerText = asciiListCodeSnippet; setModelData( window.editor.model, cleanedEditorModelString ); @@ -113,6 +129,10 @@ const processInput = () => { setModelDataFromAscii(); } + if ( document.getElementById( 'chbx-should-focus-editor' ).checked ) { + window.editor.editing.view.focus(); + } + if ( document.getElementById( 'chbx-should-copy' ).checked ) { copyOutput(); } @@ -131,7 +151,7 @@ const processEditorModel = () => { const stringifiedEditorModel = getModelData( window.editor.model, { withoutSelection: true } ); const editorModel = parseModel( stringifiedEditorModel, window.editor.model.schema ); - document.getElementById( 'data-input' ).value = stringifyList( editorModel ); + document.getElementById( 'data-input' ).value = createAsciiListCodeSnippet( stringifyList( editorModel ) ); } processInput(); diff --git a/packages/ckeditor5-list/tests/manual/listmocking.md b/packages/ckeditor5-list/tests/manual/listmocking.md index f0581eda14a..72f2a685043 100644 --- a/packages/ckeditor5-list/tests/manual/listmocking.md +++ b/packages/ckeditor5-list/tests/manual/listmocking.md @@ -7,14 +7,19 @@ It also allows to process ASCII art back to model data. You can provide your own ``` * A B - # C + # C{id:50} # D * E * F * - bulleted list # - numbered list -Each indentation is two spaces before list type. +--- +{id:fixedId} - force given id as listItemId +attribute in model. +--- +Each indentation is two spaces before list +type. ``` ## Input From a4d26247254e7d1566ee537381bd7a7a53bb5b71 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 10 Jan 2022 11:58:22 +0100 Subject: [PATCH 62/66] Tests: Indent/Outdent refactoring WIP. --- .../documentlist/documentlistindentcommand.js | 238 +++++++++++++++++- 1 file changed, 226 insertions(+), 12 deletions(-) diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js index db69dc9a993..4840170b57f 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js @@ -280,30 +280,224 @@ describe( 'DocumentListIndentCommand', () => { ] ) ); } ); - it( 'should increment indent of all selected item when multiple items are selected', () => { + it( 'should TODO 1', () => { setData( model, modelList( [ '* 0', - '* [1', - ' * 2', - ' * 3]', - ' * 4', - ' * 5', - '* 6' + '* 1[]', + ' # 2', + '* 3' ] ) ); command.execute(); expect( getData( model ) ).to.equalMarkup( modelList( [ '* 0', - ' * [1', + ' * 1[]', + ' # 2', + '* 3' + ] ) ); + } ); + + it( 'should TODO 1a', () => { + setData( model, modelList( [ + '# 0', + '# 1[]', + ' * 2', + '# 3' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '# 0', + ' # 1[]', ' * 2', - ' * 3]', - ' * 4', - ' * 5', - '* 6' + '# 3' + ] ) ); + } ); + + it( 'should TODO 2', () => { + setData( model, modelList( [ + '* 0', + '* 1', + ' # 2', + '* []3' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + ' # 2', + ' # []3' + ] ) ); + } ); + + it( 'should TODO 2a', () => { + setData( model, modelList( [ + '* 0', + ' # 1', + '* []2', + '* 3' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' # 1', + ' # []2', + '* 3' + ] ) ); + } ); + + it( 'should TODO 5', () => { + setData( model, modelList( [ + '* 0', + '* []1', + ' # 2', + ' * 3', + ' # 4' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' * []1', + ' # 2', + ' * 3', + ' # 4' ] ) ); } ); + it( 'should TODO 5a', () => { + setData( model, modelList( [ + '* 0', + '* []1', + ' # 2', + ' * 3', + ' # 4', + '* 5', + ' # 6' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' * []1', + ' # 2', + ' * 3', + ' # 4', + '* 5', + ' # 6' + ] ) ); + } ); + + describe( 'non-collapsed selection', () => { + it( 'should increment indent of all selected item when multiple items are selected', () => { + setData( model, modelList( [ + '* 0', + '* [1', + ' * 2', + ' * 3]', + ' * 4', + ' * 5', + '* 6' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' * [1', + ' * 2', + ' * 3]', + ' * 4', + ' * 5', + '* 6' + ] ) ); + } ); + + it( 'should TODO ex1', () => { + setData( model, modelList( [ + '* 0', + '* [1', + ' # 2]', + ' * 3' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' * [1', + ' # 2]', + ' * 3' + ] ) ); + } ); + + it( 'should TODO ex2', () => { + setData( model, modelList( [ + '* 0', + '* [1', + ' # 2]', + '* 3' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' * [1', + ' # 2]', + '* 3' + ] ) ); + } ); + + it( 'should TODO ex3', () => { + setData( model, modelList( [ + '* 0', + '* 1', + ' # 2', + '* [3', + '* 4]' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + ' # 2', + ' # [3', + ' # 4]' + ] ) ); + } ); + + it( 'should TODO ex4', () => { + setData( model, modelList( [ + '* 0', + '* 1', + ' # 2', + ' # [3', + '* 4]' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + ' # 2', + ' # [3', + ' # 4]' + ] ) ); + } ); + } ); + it( 'should fire "afterExecute" event after finish all operations with all changed items', done => { setData( model, modelList( [ '* 0', @@ -493,6 +687,26 @@ describe( 'DocumentListIndentCommand', () => { command.execute(); } ); + + it( 'TODO 1', () => { + setData( model, modelList( [ + '* 0', + ' # 1', + ' * 2', + ' 3[]', + '* 4' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' # 1', + ' * 2', + ' * 3[]', + '* 4' + ] ) ); + } ); } ); } ); } ); From c0bedaa7fdd0586667d207643ae43869d9f1338d Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 10 Jan 2022 14:27:48 +0100 Subject: [PATCH 63/66] Outdent/indent aligns list item types. --- .../documentlist/documentlistindentcommand.js | 30 +- .../documentlist/documentlistindentcommand.js | 498 ++++++++++++------ 2 files changed, 360 insertions(+), 168 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js index ed38b9f57e6..b9f3fa8b5e9 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistindentcommand.js @@ -63,10 +63,10 @@ export default class DocumentListIndentCommand extends Command { const blocks = getSelectedListBlocks( model.document.selection ); model.change( writer => { + const changedBlocks = []; + // Handle selection contained in the single list item and starting in the following blocks. if ( isSingleListItem( blocks ) && !isFirstBlockOfListItem( blocks[ 0 ] ) ) { - const changedBlocks = []; - // Allow increasing indent of following list item blocks. if ( this._direction == 'forward' ) { changedBlocks.push( ...indentBlocks( blocks, writer ) ); @@ -75,18 +75,32 @@ export default class DocumentListIndentCommand extends Command { // For indent make sure that indented blocks have a new ID. // For outdent just split blocks from the list item (give them a new IDs). changedBlocks.push( ...splitListItemBefore( blocks[ 0 ], writer ) ); - - this._fireAfterExecute( changedBlocks ); } // More than a single list item is selected, or the first block of list item is selected. else { // Now just update the attributes of blocks. - const changedBlocks = this._direction == 'forward' ? - indentBlocks( blocks, writer, { expand: true } ) : - outdentBlocks( blocks, writer, { expand: true } ); + if ( this._direction == 'forward' ) { + changedBlocks.push( ...indentBlocks( blocks, writer, { expand: true } ) ); + } else { + changedBlocks.push( ...outdentBlocks( blocks, writer, { expand: true } ) ); + } + } - this._fireAfterExecute( changedBlocks ); + // Align the list item type to match the previous list item (from the same list). + for ( const block of changedBlocks ) { + // This block become a plain block (for example a paragraph). + if ( !block.hasAttribute( 'listType' ) ) { + continue; + } + + const previousItemBlock = ListWalker.first( block, { sameIndent: true } ); + + if ( previousItemBlock ) { + writer.setAttribute( 'listType', previousItemBlock.getAttribute( 'listType' ), block ); + } } + + this._fireAfterExecute( changedBlocks ); } ); } diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js index 4840170b57f..b18c159e16e 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistindentcommand.js @@ -280,222 +280,234 @@ describe( 'DocumentListIndentCommand', () => { ] ) ); } ); - it( 'should TODO 1', () => { - setData( model, modelList( [ - '* 0', - '* 1[]', - ' # 2', - '* 3' - ] ) ); - - command.execute(); - - expect( getData( model ) ).to.equalMarkup( modelList( [ - '* 0', - ' * 1[]', - ' # 2', - '* 3' - ] ) ); - } ); - - it( 'should TODO 1a', () => { - setData( model, modelList( [ - '# 0', - '# 1[]', - ' * 2', - '# 3' - ] ) ); - - command.execute(); - - expect( getData( model ) ).to.equalMarkup( modelList( [ - '# 0', - ' # 1[]', - ' * 2', - '# 3' - ] ) ); - } ); - - it( 'should TODO 2', () => { - setData( model, modelList( [ - '* 0', - '* 1', - ' # 2', - '* []3' - ] ) ); - - command.execute(); - - expect( getData( model ) ).to.equalMarkup( modelList( [ - '* 0', - '* 1', - ' # 2', - ' # []3' - ] ) ); - } ); - - it( 'should TODO 2a', () => { - setData( model, modelList( [ - '* 0', - ' # 1', - '* []2', - '* 3' - ] ) ); - - command.execute(); - - expect( getData( model ) ).to.equalMarkup( modelList( [ - '* 0', - ' # 1', - ' # []2', - '* 3' - ] ) ); - } ); - - it( 'should TODO 5', () => { - setData( model, modelList( [ - '* 0', - '* []1', - ' # 2', - ' * 3', - ' # 4' - ] ) ); - - command.execute(); - - expect( getData( model ) ).to.equalMarkup( modelList( [ - '* 0', - ' * []1', - ' # 2', - ' * 3', - ' # 4' - ] ) ); - } ); - - it( 'should TODO 5a', () => { - setData( model, modelList( [ - '* 0', - '* []1', - ' # 2', - ' * 3', - ' # 4', - '* 5', - ' # 6' - ] ) ); + describe( 'mixed list types', () => { + it( 'should not change list item type if the indented list item is the first one in the nested list (bulleted)', () => { + setData( model, modelList( [ + '* 0', + '* 1[]', + ' # 2', + '* 3' + ] ) ); - command.execute(); + command.execute(); - expect( getData( model ) ).to.equalMarkup( modelList( [ - '* 0', - ' * []1', - ' # 2', - ' * 3', - ' # 4', - '* 5', - ' # 6' - ] ) ); - } ); + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' * 1[]', + ' # 2', + '* 3' + ] ) ); + } ); - describe( 'non-collapsed selection', () => { - it( 'should increment indent of all selected item when multiple items are selected', () => { + it( 'should not change list item type if the indented list item is the first one in the nested list (numbered)', () => { setData( model, modelList( [ - '* 0', - '* [1', + '# 0', + '# 1[]', ' * 2', - ' * 3]', - ' * 4', - ' * 5', - '* 6' + '# 3' ] ) ); command.execute(); expect( getData( model ) ).to.equalMarkup( modelList( [ - '* 0', - ' * [1', + '# 0', + ' # 1[]', ' * 2', - ' * 3]', - ' * 4', - ' * 5', - '* 6' + '# 3' ] ) ); } ); - it( 'should TODO ex1', () => { + it( 'should adjust list type to the previous list item (numbered)', () => { setData( model, modelList( [ '* 0', - '* [1', - ' # 2]', - ' * 3' + '* 1', + ' # 2', + '* []3' ] ) ); command.execute(); expect( getData( model ) ).to.equalMarkup( modelList( [ '* 0', - ' * [1', - ' # 2]', - ' * 3' + '* 1', + ' # 2', + ' # []3' ] ) ); } ); - it( 'should TODO ex2', () => { + it( 'should not change list item type if the indented list item is the first one in the nested list', () => { setData( model, modelList( [ '* 0', - '* [1', - ' # 2]', - '* 3' + '* []1', + ' # 2', + ' * 3', + ' # 4' ] ) ); command.execute(); expect( getData( model ) ).to.equalMarkup( modelList( [ '* 0', - ' * [1', - ' # 2]', - '* 3' + ' * []1', + ' # 2', + ' * 3', + ' # 4' ] ) ); } ); - it( 'should TODO ex3', () => { + it( 'should not change list item type if the first item in the nested list (has more items)', () => { setData( model, modelList( [ '* 0', - '* 1', + '* []1', ' # 2', - '* [3', - '* 4]' + ' * 3', + ' # 4', + '* 5', + ' # 6' ] ) ); command.execute(); expect( getData( model ) ).to.equalMarkup( modelList( [ '* 0', - '* 1', - ' # 2', - ' # [3', - ' # 4]' + ' * []1', + ' # 2', + ' * 3', + ' # 4', + '* 5', + ' # 6' ] ) ); } ); + } ); - it( 'should TODO ex4', () => { + describe( 'non-collapsed selection', () => { + it( 'should increment indent of all selected item when multiple items are selected', () => { setData( model, modelList( [ '* 0', - '* 1', - ' # 2', - ' # [3', - '* 4]' + '* [1', + ' * 2', + ' * 3]', + ' * 4', + ' * 5', + '* 6' ] ) ); command.execute(); expect( getData( model ) ).to.equalMarkup( modelList( [ '* 0', - '* 1', - ' # 2', - ' # [3', - ' # 4]' + ' * [1', + ' * 2', + ' * 3]', + ' * 4', + ' * 5', + '* 6' ] ) ); } ); + + describe( 'mixed list types', () => { + it( 'should not change list types for the first list items', () => { + setData( model, modelList( [ + '* 0', + '* [1', + ' # 2]', + ' * 3' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' * [1', + ' # 2]', + ' * 3' + ] ) ); + } ); + + it( 'should not change list types for the first list items (with nested lists)', () => { + setData( model, modelList( [ + '* 0', + '* [1', + ' # 2]', + '* 3' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' * [1', + ' # 2]', + '* 3' + ] ) ); + } ); + + it( 'should align the list type if become a part of other list (bulleted)', () => { + setData( model, modelList( [ + '* 0', + '* 1', + ' # 2', + '* [3', + '* 4]' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + ' # 2', + ' # [3', + ' # 4]' + ] ) ); + } ); + + it( 'should align the list type if become a part of other list (numbered)', () => { + setData( model, modelList( [ + '* 0', + '* 1', + ' # 2', + ' # [3', + '* 4]' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + ' # 2', + ' # [3', + ' # 4]' + ] ) ); + } ); + + it( 'should align the list type (bigger structure)', () => { + setData( model, modelList( [ + '* 0', + '* 1', + ' # 2', + ' * 3', + ' * [4', + ' # 5', + ' * 6', + ' # 7]' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + ' # 2', + ' * 3', + ' * [4', + ' * 5', + ' * 6', + ' * 7]' + ] ) ); + } ); + } ); } ); it( 'should fire "afterExecute" event after finish all operations with all changed items', done => { @@ -688,7 +700,7 @@ describe( 'DocumentListIndentCommand', () => { command.execute(); } ); - it( 'TODO 1', () => { + it( 'should align the list item type after indenting a following block of a list item (numbered)', () => { setData( model, modelList( [ '* 0', ' # 1', @@ -697,16 +709,76 @@ describe( 'DocumentListIndentCommand', () => { '* 4' ] ) ); + stubUid(); command.execute(); expect( getData( model ) ).to.equalMarkup( modelList( [ '* 0', ' # 1', ' * 2', - ' * 3[]', + ' * 3[] {id:a00}', '* 4' ] ) ); } ); + + it( 'should align the list item type after indenting a following block of a list item (bulleted)', () => { + setData( model, modelList( [ + '# 0', + ' * 1', + ' # 2', + ' 3[]', + '# 4' + ] ) ); + + stubUid(); + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '# 0', + ' * 1', + ' # 2', + ' # 3[] {id:a00}', + '# 4' + ] ) ); + } ); + + it( 'should align the list item type after indenting a following block of a list item (bigger structure)', () => { + setData( model, modelList( [ + '* 0', + ' # 1', + ' * 2', + ' 3', + ' 4[]' + ] ) ); + + stubUid(); + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' # 1', + ' * 2', + ' 3', + ' # 4[] {id:a00}' + ] ) ); + } ); + + it( 'should align the list item type after indenting the last block of a list item', () => { + setData( model, modelList( [ + '* 0', + ' # 1', + ' 2[]' + ] ) ); + + stubUid(); + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + ' # 1', + ' # 2[] {id:a00}' + ] ) ); + } ); } ); } ); } ); @@ -1008,6 +1080,112 @@ describe( 'DocumentListIndentCommand', () => { ' * 5' ] ) ); } ); + + it( 'should align a list item type after outdenting item', () => { + setData( model, modelList( [ + '* 0', + '* 1', + ' # 2[]', + '* 3' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + '* 2[]', + '* 3' + ] ) ); + } ); + + it( 'should align a list item type after outdenting the last list item', () => { + setData( model, modelList( [ + '# 0', + ' * 1', + ' * 2[]', + '# 3' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '# 0', + ' * 1', + '# 2[]', + '# 3' + ] ) ); + } ); + + it( 'should align the list item type after the more indented item', () => { + setData( model, modelList( [ + '* 0', + '* 1', + ' # 2', + ' * 3', + ' # 4[]' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + ' # 2', + ' * 3', + '* 4[]' + ] ) ); + } ); + + it( 'should outdent the whole nested list (and align appropriate list item types)', () => { + setData( model, modelList( [ + '* 0', + ' # []1', + ' # 2', + ' * 3', + ' # 4', + '* 5', + ' # 6' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + '* []1', + ' # 2', + ' * 3', + ' # 4', + '* 5', + ' # 6' + ] ) ); + } ); + + it( 'should align list item typed after outdenting a bigger structure', () => { + setData( model, modelList( [ + '* 0', + '* 1', + ' # 2', + ' * 3', + ' # [4', + ' * 5', + ' # 6', + ' * 7]' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* 0', + '* 1', + ' # 2', + ' * 3', + ' * [4', + ' # 5', + ' # 6', + ' # 7]' + ] ) ); + } ); } ); } ); } ); From b2bb0557962ae2560ee61972a5317ef830f43857 Mon Sep 17 00:00:00 2001 From: Andrzej Stanek Date: Mon, 10 Jan 2022 14:39:12 +0100 Subject: [PATCH 64/66] Fix copyoutput mockinglist --- packages/ckeditor5-list/tests/manual/listmocking.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/ckeditor5-list/tests/manual/listmocking.js b/packages/ckeditor5-list/tests/manual/listmocking.js index b40d7d0e3c7..4c1d3bd7180 100644 --- a/packages/ckeditor5-list/tests/manual/listmocking.js +++ b/packages/ckeditor5-list/tests/manual/listmocking.js @@ -45,6 +45,11 @@ ClassicEditor } ); const copyOutput = async () => { + if ( !window.navigator.clipboard ) { + console.warn( 'Cannot copy output. Clipboard API requires HTTPS or localhost.' ); + return; + } + const output = document.getElementById( 'data-output' ).innerText; await window.navigator.clipboard.writeText( output ); From 51584621e7fcffa3f555297788f8d9e678295046 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 10 Jan 2022 15:22:09 +0100 Subject: [PATCH 65/66] Cleaning. --- .../tests/documentlist/_utils/utils.js | 4 +-- .../tests/manual/listmocking.html | 35 +++++++++++++++---- .../tests/manual/listmocking.js | 26 ++++++-------- 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/packages/ckeditor5-list/tests/documentlist/_utils/utils.js b/packages/ckeditor5-list/tests/documentlist/_utils/utils.js index 6356340b232..f520003834a 100644 --- a/packages/ckeditor5-list/tests/documentlist/_utils/utils.js +++ b/packages/ckeditor5-list/tests/documentlist/_utils/utils.js @@ -300,10 +300,10 @@ function stringifyNode( node, writer ) { if ( node.is( 'element', 'paragraph' ) ) { for ( const child of node.getChildren() ) { - writer.append( child, fragment ); + writer.append( writer.cloneElement( child ), fragment ); } } else { - writer.append( node, fragment ); + writer.append( writer.cloneElement( node ), fragment ); } return stringifyModel( fragment ); diff --git a/packages/ckeditor5-list/tests/manual/listmocking.html b/packages/ckeditor5-list/tests/manual/listmocking.html index c4b9c0e06cb..4efb7417aac 100644 --- a/packages/ckeditor5-list/tests/manual/listmocking.html +++ b/packages/ckeditor5-list/tests/manual/listmocking.html @@ -1,4 +1,14 @@

      Input

      - + - +
      @@ -55,8 +76,8 @@

      Input

      - - + +
      @@ -66,4 +87,4 @@

      Output

      -
      
      \ No newline at end of file
      +
      
      diff --git a/packages/ckeditor5-list/tests/manual/listmocking.js b/packages/ckeditor5-list/tests/manual/listmocking.js
      index 4c1d3bd7180..c19c0fe98db 100644
      --- a/packages/ckeditor5-list/tests/manual/listmocking.js
      +++ b/packages/ckeditor5-list/tests/manual/listmocking.js
      @@ -19,7 +19,7 @@ import {
       	getData as getModelData
       } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
       
      -import { modelList, stringifyList } from './../documentlist/_utils/utils';
      +import { modelList, stringifyList } from '../documentlist/_utils/utils';
       import DocumentList from '../../src/documentlist';
       
       ClassicEditor
      @@ -73,16 +73,12 @@ const copyOutput = async () => {
       };
       
       const getListModelWithNewLines = stringifiedModel => {
      -	return stringifiedModel.replace( /<\/paragraph>/g, '\n' );
      +	return stringifiedModel.replace( /<\/(paragraph|heading\d)>/g, '\n' );
       };
       
       const setModelDataFromAscii = () => {
       	const asciiList = document.getElementById( 'data-input' ).value;
      -	const modelDataArray = [];
      -
      -	asciiList.replace( /[^']*'(.*)'.*$/gm, ( match, content ) => {
      -		modelDataArray.push( content );
      -	} );
      +	const modelDataArray = asciiList.replace( /^[^']*'|'[^']*$/gm, '' ).split( '\n' );
       
       	const editorModelString = modelList( modelDataArray );
       
      @@ -110,11 +106,7 @@ const createAsciiListCodeSnippet = stringifiedAsciiList => {
       
       const setAsciiListFromModel = () => {
       	const editorModelString = document.getElementById( 'data-input' ).value;
      -	let cleanedEditorModelString = '';
      -
      -	editorModelString.replace( /()/gm, ( match, content ) => {
      -		cleanedEditorModelString += content;
      -	} );
      +	const cleanedEditorModelString = editorModelString.replace( /^[^']*'|'[^']*$|\n|\r/gm, '' );
       
       	const editorModel = parseModel( cleanedEditorModelString, window.editor.model.schema );
       	const asciiListCodeSnippet = createAsciiListCodeSnippet( stringifyList( editorModel ) );
      @@ -134,9 +126,7 @@ const processInput = () => {
       		setModelDataFromAscii();
       	}
       
      -	if ( document.getElementById( 'chbx-should-focus-editor' ).checked ) {
      -		window.editor.editing.view.focus();
      -	}
      +	window.editor.focus();
       
       	if ( document.getElementById( 'chbx-should-copy' ).checked ) {
       		copyOutput();
      @@ -168,7 +158,13 @@ const onPaste = () => {
       	}
       };
       
      +const onHighlighChange = () => {
      +	document.querySelector( '.ck-editor' ).classList.toggle( 'highlight-lists' );
      +};
      +
       document.getElementById( 'btn-process-input' ).addEventListener( 'click', processInput );
       document.getElementById( 'btn-process-editor-model' ).addEventListener( 'click', processEditorModel );
       document.getElementById( 'btn-copy-output' ).addEventListener( 'click', copyOutput );
       document.getElementById( 'data-input' ).addEventListener( 'paste', onPaste );
      +document.getElementById( 'chbx-highlight-lists' ).addEventListener( 'change', onHighlighChange );
      +
      
      From 97e5aebac49b6fe3a99e9abd2e10b39cbffc5012 Mon Sep 17 00:00:00 2001
      From: Kuba Niegowski 
      Date: Mon, 10 Jan 2022 15:49:49 +0100
      Subject: [PATCH 66/66] Fixed model list stringify.
      
      ---
       .../ckeditor5-list/tests/documentlist/_utils/utils.js  | 10 +++++++++-
       1 file changed, 9 insertions(+), 1 deletion(-)
      
      diff --git a/packages/ckeditor5-list/tests/documentlist/_utils/utils.js b/packages/ckeditor5-list/tests/documentlist/_utils/utils.js
      index f520003834a..2162490a50c 100644
      --- a/packages/ckeditor5-list/tests/documentlist/_utils/utils.js
      +++ b/packages/ckeditor5-list/tests/documentlist/_utils/utils.js
      @@ -303,7 +303,15 @@ function stringifyNode( node, writer ) {
       			writer.append( writer.cloneElement( child ), fragment );
       		}
       	} else {
      -		writer.append( writer.cloneElement( node ), fragment );
      +		const contentNode = writer.cloneElement( node );
      +
      +		for ( const key of contentNode.getAttributeKeys() ) {
      +			if ( key.startsWith( 'list' ) ) {
      +				writer.removeAttribute( key, contentNode );
      +			}
      +		}
      +
      +		writer.append( contentNode, fragment );
       	}
       
       	return stringifyModel( fragment );