From 3fe0d1cce805cfcab09e71105d3b52caacb95285 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 31 Jul 2023 19:30:36 +0200 Subject: [PATCH 01/54] Initial PoC of to-do lists in document lists. --- .../src/integrations/documentlist.ts | 10 +- packages/ckeditor5-list/src/augmentation.ts | 2 +- .../src/documentlist/converters.ts | 7 +- .../src/documentlist/documentlistcommand.ts | 4 +- .../src/documentlist/documentlistediting.ts | 2 +- .../src/documentlist/utils/model.ts | 2 +- .../src/documentlist/utils/view.ts | 6 +- .../documentlistpropertiesediting.ts | 8 +- packages/ckeditor5-list/src/todo.ts | 36 +++ .../ckeditor5-list/src/todo/todoediting.ts | 225 ++++++++++++++++++ .../src/utils/formathtml.ts | 1 - 11 files changed, 286 insertions(+), 17 deletions(-) create mode 100644 packages/ckeditor5-list/src/todo.ts create mode 100644 packages/ckeditor5-list/src/todo/todoediting.ts diff --git a/packages/ckeditor5-html-support/src/integrations/documentlist.ts b/packages/ckeditor5-html-support/src/integrations/documentlist.ts index e7c304286fa..022a4bf7a6b 100644 --- a/packages/ckeditor5-html-support/src/integrations/documentlist.ts +++ b/packages/ckeditor5-html-support/src/integrations/documentlist.ts @@ -174,7 +174,7 @@ export default class DocumentListElementSupport extends Plugin { for ( const { node } of listNodes ) { const listType = node.getAttribute( 'listType' ); - if ( listType === 'bulleted' && node.getAttribute( 'htmlOlAttributes' ) ) { + if ( listType !== 'numbered' && node.getAttribute( 'htmlOlAttributes' ) ) { writer.removeAttribute( 'htmlOlAttributes', node ); evt.return = true; } @@ -256,8 +256,8 @@ function viewToModelListAttributeConverter( attributeName: string, dataFilter: D /** * Returns HTML attribute name based on provided list type. */ -function getAttributeFromListType( listType: 'bulleted' | 'numbered' ) { - return listType === 'bulleted' ? - 'htmlUlAttributes' : - 'htmlOlAttributes'; +function getAttributeFromListType( listType: string ) { + return listType === 'numbered' ? + 'htmlOlAttributes' : + 'htmlUlAttributes'; } diff --git a/packages/ckeditor5-list/src/augmentation.ts b/packages/ckeditor5-list/src/augmentation.ts index 7e579551e18..bfe1b69c01e 100644 --- a/packages/ckeditor5-list/src/augmentation.ts +++ b/packages/ckeditor5-list/src/augmentation.ts @@ -83,7 +83,7 @@ declare module '@ckeditor/ckeditor5-core' { listStyle: ListStyleCommand | DocumentListStyleCommand; listStart: ListStartCommand | DocumentListStartCommand; listReversed: ListReversedCommand | DocumentListReversedCommand; - todoList: ListCommand; + todoList: ListCommand | DocumentListCommand; checkTodoList: CheckTodoListCommand; } } diff --git a/packages/ckeditor5-list/src/documentlist/converters.ts b/packages/ckeditor5-list/src/documentlist/converters.ts index 751e4307b71..c559f3d9c59 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.ts +++ b/packages/ckeditor5-list/src/documentlist/converters.ts @@ -81,7 +81,12 @@ export function listItemUpcastConverter(): GetCallback { for ( const item of items ) { // Set list attributes only on same level items, those nested deeper are already handled by the recursive conversion. if ( !isListItemBlock( item ) ) { - writer.setAttributes( attributes, item ); + // Preserve list type if was already set (for example by to-do list feature). + if ( item.hasAttribute( 'listType' ) ) { + writer.setAttributes( { ...attributes, listType: item.getAttribute( 'listType' ) }, item ); + } else { + writer.setAttributes( attributes, item ); + } } } diff --git a/packages/ckeditor5-list/src/documentlist/documentlistcommand.ts b/packages/ckeditor5-list/src/documentlist/documentlistcommand.ts index e9b51287f56..4ca5ababf13 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistcommand.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistcommand.ts @@ -29,7 +29,7 @@ export default class DocumentListCommand extends Command { /** * The type of the list created by the command. */ - public readonly type: 'numbered' | 'bulleted'; + public readonly type: 'numbered' | 'bulleted' | 'todo'; /** * A flag indicating whether the command is active, which means that the selection starts in a list of the same type. @@ -45,7 +45,7 @@ export default class DocumentListCommand extends Command { * @param editor The editor instance. * @param type List type that will be handled by this command. */ - constructor( editor: Editor, type: 'numbered' | 'bulleted' ) { + constructor( editor: Editor, type: 'numbered' | 'bulleted' | 'todo' ) { super( editor ); this.type = type; diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts index 3046084f42f..e51f63f362c 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts @@ -79,7 +79,7 @@ const LIST_BASE_ATTRIBUTES = [ 'listType', 'listIndent', 'listItemId' ]; * Map of model attributes applicable to list blocks. */ export interface ListItemAttributesMap { - listType?: 'numbered' | 'bulleted'; + listType?: 'numbered' | 'bulleted' | string; listIndent?: number; listItemId?: string; } diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.ts b/packages/ckeditor5-list/src/documentlist/utils/model.ts index b9a34845488..899c404173c 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.ts +++ b/packages/ckeditor5-list/src/documentlist/utils/model.ts @@ -45,7 +45,7 @@ export class ListItemUid { export interface ListElement extends Element { getAttribute( key: 'listItemId' ): string; getAttribute( key: 'listIndent' ): number; - getAttribute( key: 'listType' ): 'numbered' | 'bulleted'; + getAttribute( key: 'listType' ): 'numbered' | 'bulleted' | string; getAttribute( key: string ): unknown; } diff --git a/packages/ckeditor5-list/src/documentlist/utils/view.ts b/packages/ckeditor5-list/src/documentlist/utils/view.ts index ad25eeb8254..15d0ed93430 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/view.ts +++ b/packages/ckeditor5-list/src/documentlist/utils/view.ts @@ -96,7 +96,7 @@ export function getIndent( listItem: ViewElement ): number { export function createListElement( writer: DowncastWriter, indent: number, - type: 'bulleted' | 'numbered', + type: string, id = getViewElementIdForListType( type, indent ) ): ViewAttributeElement { // Negative priorities so that restricted editing attribute won't wrap lists. @@ -128,7 +128,7 @@ export function createListItemElement( * * @internal */ -export function getViewElementNameForListType( type?: 'bulleted' | 'numbered' ): 'ol' | 'ul' { +export function getViewElementNameForListType( type?: string ): 'ol' | 'ul' { return type == 'numbered' ? 'ol' : 'ul'; } @@ -137,6 +137,6 @@ export function getViewElementNameForListType( type?: 'bulleted' | 'numbered' ): * * @internal */ -export function getViewElementIdForListType( type?: 'bulleted' | 'numbered', indent?: number ): string { +export function getViewElementIdForListType( type?: string, indent?: number ): string { return `list-${ type }-${ indent }`; } diff --git a/packages/ckeditor5-list/src/documentlistproperties/documentlistpropertiesediting.ts b/packages/ckeditor5-list/src/documentlistproperties/documentlistpropertiesediting.ts index 8d6dabe3de7..8fb2670b142 100644 --- a/packages/ckeditor5-list/src/documentlistproperties/documentlistpropertiesediting.ts +++ b/packages/ckeditor5-list/src/documentlistproperties/documentlistpropertiesediting.ts @@ -302,11 +302,15 @@ function createAttributeStrategies( enabledProperties: ListPropertiesConfig ) { editor.commands.add( 'listStyle', new DocumentListStyleCommand( editor, DEFAULT_LIST_TYPE, supportedTypes ) ); }, - appliesToListItem() { - return true; + appliesToListItem( item ) { + return item.getAttribute( 'listType' ) == 'numbered' || item.getAttribute( 'listType' ) == 'bulleted'; }, hasValidAttribute( item ) { + if ( !this.appliesToListItem( item ) ) { + return !item.hasAttribute( 'listStyle' ); + } + if ( !item.hasAttribute( 'listStyle' ) ) { return false; } diff --git a/packages/ckeditor5-list/src/todo.ts b/packages/ckeditor5-list/src/todo.ts new file mode 100644 index 00000000000..e75ab5da115 --- /dev/null +++ b/packages/ckeditor5-list/src/todo.ts @@ -0,0 +1,36 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module list/todo + */ + +import TodoEditing from './todo/todoediting'; +import TodoListUI from './todolist/todolistui'; +import { Plugin } from 'ckeditor5/src/core'; + +import '../theme/todolist.css'; + +/** + * The to-do list feature. + * + * This is a "glue" plugin that loads the {@link module:list/todolist/todolistediting~TodoListEditing to-do list editing feature} + * and the {@link module:list/todolist/todolistui~TodoListUI to-do list UI feature}. + */ +export default class Todo extends Plugin { + /** + * @inheritDoc + */ + public static get requires() { + return [ TodoEditing, TodoListUI ] as const; + } + + /** + * @inheritDoc + */ + public static get pluginName() { + return 'Todo' as const; + } +} diff --git a/packages/ckeditor5-list/src/todo/todoediting.ts b/packages/ckeditor5-list/src/todo/todoediting.ts new file mode 100644 index 00000000000..a0ca4fcd764 --- /dev/null +++ b/packages/ckeditor5-list/src/todo/todoediting.ts @@ -0,0 +1,225 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module list/todo/todoediting + */ + +import type { + View, + ViewElement, + MapperModelToViewPositionEvent, + ElementCreatorFunction, + UpcastElementEvent, + Element +} from 'ckeditor5/src/engine'; + +import { Plugin } from 'ckeditor5/src/core'; +import type { GetCallback } from 'ckeditor5/src/utils'; +import { isListItemBlock } from '../documentlist/utils/model'; +import DocumentListEditing from '../documentlist/documentlistediting'; +import DocumentListCommand from '../documentlist/documentlistcommand'; + +/** + * TODO + */ +export default class TodoEditing extends Plugin { + /** + * @inheritDoc + */ + public init(): void { + const editor = this.editor; + const model = editor.model; + + editor.commands.add( 'todoList', new DocumentListCommand( editor, 'todo' ) ); + + model.schema.extend( 'paragraph', { + allowAttributes: 'todoItemChecked' + } ); + + model.schema.addAttributeCheck( ( context: any, attributeName ) => { + const item = context.last; + + if ( attributeName == 'todoItemChecked' && isListItemBlock( item ) && item.getAttribute( 'listType' ) != 'todo' ) { + return false; + } + } ); + + editor.conversion.for( 'upcast' ).add( dispatcher => { + dispatcher.on( 'element:input', todoItemInputConverter() ); + } ); + + editor.conversion.for( 'dataDowncast' ).elementToElement( { + model: { + name: 'paragraph', + attributes: 'todoItemChecked' + }, + view: todoItemViewCreator( { dataPipeline: true } ), + converterPriority: 'highest' + } ); + + editor.conversion.for( 'editingDowncast' ).elementToElement( { + model: { + name: 'paragraph', + attributes: 'todoItemChecked' + }, + view: todoItemViewCreator(), + converterPriority: 'highest' + } ); + + editor.editing.mapper.on( 'modelToViewPosition', mapModelToViewPosition( editor.editing.view ) ); + editor.data.mapper.on( 'modelToViewPosition', mapModelToViewPosition( editor.editing.view ) ); + + const documentListEditing = editor.plugins.get( DocumentListEditing ); + + documentListEditing.registerDowncastStrategy( { + attributeName: 'listType', + scope: 'list', + setAttributeOnDowncast( writer, value, element ) { + if ( value == 'todo' ) { + writer.addClass( 'todo-list', element ); + } else { + writer.removeClass( 'todo-list', element ); + } + } + } ); + + model.document.registerPostFixer( writer => { + const changes = model.document.differ.getChanges(); + let wasFixed = false; + + for ( const change of changes ) { + if ( change.type != 'attribute' || change.attributeKey != 'listType' ) { + continue; + } + + const element = change.range.start.nodeAfter!; + + if ( change.attributeNewValue == 'todo' ) { + if ( !element.hasAttribute( 'todoItemChecked' ) ) { + writer.setAttribute( 'todoItemChecked', false, element ); + wasFixed = true; + } + } else if ( change.attributeOldValue == 'todo' ) { + if ( element.hasAttribute( 'todoItemChecked' ) ) { + writer.removeAttribute( 'todoItemChecked', element ); + wasFixed = true; + } + } + } + + return wasFixed; + } ); + } +} + +function todoItemInputConverter(): GetCallback { + return ( evt, data, conversionApi ) => { + const modelCursor = data.modelCursor; + const modelItem = modelCursor.parent as Element; + const viewItem = data.viewItem; + + // TODO detect if this is a to-do list + if ( viewItem.getAttribute( 'type' ) != 'checkbox' /* || !isListItemBlock( modelItem )*/ || !modelCursor.isAtStart ) { + return; + } + + if ( !conversionApi.consumable.consume( viewItem, { name: true } ) ) { + return; + } + + const writer = conversionApi.writer; + + writer.setAttribute( 'listType', 'todo', modelItem ); + + if ( data.viewItem.hasAttribute( 'checked' ) ) { + writer.setAttribute( 'todoItemChecked', true, modelItem ); + } + + data.modelRange = writer.createRange( modelCursor ); + }; +} + +/** + * TODO + */ +function todoItemViewCreator( { dataPipeline }: { dataPipeline?: boolean } = {} ): ElementCreatorFunction { + return ( modelElement, { writer } ) => { + if ( modelElement.getAttribute( 'listType' ) != 'todo' ) { + return null; + } + + // Using `

` in data pipeline in case there are some markers on it and transparentRendering will render it anyway. + const viewElement = dataPipeline ? + writer.createContainerElement( 'p' ) : + writer.createContainerElement( 'span', { class: 'ck-list-bogus-paragraph' } ); + + if ( dataPipeline ) { + writer.setCustomProperty( 'dataPipeline:transparentRendering', true, viewElement ); + } + + const labelWithCheckbox = writer.createContainerElement( 'label', { + class: 'todo-list__label' + }, [ + writer.createEmptyElement( 'input', { + type: 'checkbox', + ...( modelElement.getAttribute( 'todoItemChecked' ) ? { checked: 'checked' } : null ) + } ) + ] ); + + const descriptionSpan = writer.createContainerElement( 'span', { + class: 'todo-list__label__description' + } ); + + writer.insert( writer.createPositionAt( viewElement, 0 ), labelWithCheckbox ); + + // The `

  • to a generic paragraph so the content of
  • is always inside a block. + // Setting the listType attribute to let other features (to-do list) know that this is part of a list item. + .elementToElement( { + view: 'li', + model: ( viewElement, { writer } ) => writer.createElement( 'paragraph', { listType: '' } ) + } ) .add( dispatcher => { dispatcher.on( 'element:li', listItemUpcastConverter() ); dispatcher.on( 'element:ul', listUpcastCleanList(), { priority: 'high' } ); diff --git a/packages/ckeditor5-list/src/todo/todoediting.ts b/packages/ckeditor5-list/src/todo/todoediting.ts index a0ca4fcd764..8949e364f78 100644 --- a/packages/ckeditor5-list/src/todo/todoediting.ts +++ b/packages/ckeditor5-list/src/todo/todoediting.ts @@ -7,17 +7,20 @@ * @module list/todo/todoediting */ -import type { - View, - ViewElement, - MapperModelToViewPositionEvent, - ElementCreatorFunction, - UpcastElementEvent, - Element +import { + Matcher, + type View, + type ViewElement, + type MapperModelToViewPositionEvent, + type ElementCreatorFunction, + type UpcastElementEvent, + type Element, + type MatcherPattern } from 'ckeditor5/src/engine'; import { Plugin } from 'ckeditor5/src/core'; import type { GetCallback } from 'ckeditor5/src/utils'; + import { isListItemBlock } from '../documentlist/utils/model'; import DocumentListEditing from '../documentlist/documentlistediting'; import DocumentListCommand from '../documentlist/documentlistcommand'; @@ -48,7 +51,19 @@ export default class TodoEditing extends Plugin { } ); editor.conversion.for( 'upcast' ).add( dispatcher => { + // Upcast of to-do list item is based on a checkbox at the beginning of a
  • to keep compatibility with markdown input. dispatcher.on( 'element:input', todoItemInputConverter() ); + + // Consume other elements that are normally generated in data downcast, so they won't get captured by GHS. + dispatcher.on( 'element:label', elementUpcastConsumingConverter( + { name: 'label', classes: 'todo-list__label' } + ) ); + dispatcher.on( 'element:span', elementUpcastConsumingConverter( + { name: 'span', classes: 'todo-list__label__description' } + ) ); + dispatcher.on( 'element:ul', attributeUpcastConsumingConverter( + { name: 'ul', classes: 'todo-list' } + ) ); } ); editor.conversion.for( 'dataDowncast' ).elementToElement( { @@ -115,14 +130,16 @@ export default class TodoEditing extends Plugin { } } +/** + * TODO + */ function todoItemInputConverter(): GetCallback { return ( evt, data, conversionApi ) => { const modelCursor = data.modelCursor; const modelItem = modelCursor.parent as Element; const viewItem = data.viewItem; - // TODO detect if this is a to-do list - if ( viewItem.getAttribute( 'type' ) != 'checkbox' /* || !isListItemBlock( modelItem )*/ || !modelCursor.isAtStart ) { + if ( viewItem.getAttribute( 'type' ) != 'checkbox' || !modelCursor.isAtStart || !modelItem.hasAttribute( 'listType' ) ) { return; } @@ -142,6 +159,47 @@ function todoItemInputConverter(): GetCallback { }; } +/** + * TODO + */ +function elementUpcastConsumingConverter( matcherPattern: MatcherPattern ): GetCallback { + const matcher = new Matcher( matcherPattern ); + + return ( evt, data, conversionApi ) => { + const matcherResult = matcher.match( data.viewItem ); + + if ( !matcherResult ) { + return; + } + + if ( !conversionApi.consumable.consume( data.viewItem, matcherResult.match ) ) { + return; + } + + Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) ); + }; +} + +/** + * TODO + */ +function attributeUpcastConsumingConverter( matcherPattern: MatcherPattern ): GetCallback { + const matcher = new Matcher( matcherPattern ); + + return ( evt, data, conversionApi ) => { + const matcherResult = matcher.match( data.viewItem ); + + if ( !matcherResult ) { + return; + } + + const match = matcherResult.match; + + match.name = false; + conversionApi.consumable.consume( data.viewItem, match ); + }; +} + /** * TODO */ @@ -165,7 +223,8 @@ function todoItemViewCreator( { dataPipeline }: { dataPipeline?: boolean } = {} }, [ writer.createEmptyElement( 'input', { type: 'checkbox', - ...( modelElement.getAttribute( 'todoItemChecked' ) ? { checked: 'checked' } : null ) + ...( modelElement.getAttribute( 'todoItemChecked' ) ? { checked: 'checked' } : null ), + ... ( dataPipeline ? { disabled: 'disabled' } : null ) } ) ] ); diff --git a/packages/ckeditor5-list/tests/manual/todo-documentlist.html b/packages/ckeditor5-list/tests/manual/todo-documentlist.html new file mode 100644 index 00000000000..4efbb16981d --- /dev/null +++ b/packages/ckeditor5-list/tests/manual/todo-documentlist.html @@ -0,0 +1,50 @@ +

    Editor

    +
    +
      +
    • + +
    • +
    + +

    This is a test for list feature.

    +

    Some more text for testing.

    +
      +
    • To-do list item 1
    • +
    • To-do list item 2
    • +
    • To-do list item 3
    • +
    • To-do list item 4
    • +
    • To-do list item 5
    • +
    • + To-do list item 6 +
        +
      • + +
      • +
      +
    • +
    • To-do list item 7
    • +
    • To-do list item 8
    • +
    +

    Paragraph.

    +

    Another testing paragraph.

    +
      +
    1. Numbered list item
    2. +
    +
      +
    • To-do list item
    • +
    +
      +
    • Bullet list
    • +
    +

    Checkbox in paragraph

    +
    + +

    Editor content preview

    +
    + + diff --git a/packages/ckeditor5-list/tests/manual/todo-documentlist.js b/packages/ckeditor5-list/tests/manual/todo-documentlist.js new file mode 100644 index 00000000000..5a7920d1551 --- /dev/null +++ b/packages/ckeditor5-list/tests/manual/todo-documentlist.js @@ -0,0 +1,81 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. 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 { Essentials } from '@ckeditor/ckeditor5-essentials'; +import { Autoformat } from '@ckeditor/ckeditor5-autoformat'; +import { BlockQuote } from '@ckeditor/ckeditor5-block-quote'; +import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles'; +import { Heading } from '@ckeditor/ckeditor5-heading'; +import { Link } from '@ckeditor/ckeditor5-link'; +import { MediaEmbed } from '@ckeditor/ckeditor5-media-embed'; +import { Paragraph } from '@ckeditor/ckeditor5-paragraph'; +import { Table, TableToolbar } from '@ckeditor/ckeditor5-table'; +import { FontSize } from '@ckeditor/ckeditor5-font'; +import { Indent } from '@ckeditor/ckeditor5-indent'; +import { SourceEditing } from '@ckeditor/ckeditor5-source-editing'; +import { GeneralHtmlSupport } from '@ckeditor/ckeditor5-html-support'; + +import DocumentList from '../../src/documentlist'; +import Todo from '../../src/todo'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ + Essentials, + Autoformat, + BlockQuote, + Bold, + Heading, + Italic, + Link, + MediaEmbed, + Paragraph, + Table, + TableToolbar, + FontSize, + Indent, + DocumentList, + Todo, + SourceEditing, + GeneralHtmlSupport + ], + toolbar: [ + 'heading', + '|', + 'bulletedList', 'numberedList', 'todoList', 'outdent', 'indent', + '|', + 'bold', 'link', 'insertTable', 'fontSize', + '|', + 'undo', 'redo', '|', 'sourceEditing' + ], + table: { + contentToolbar: [ + 'tableColumn', + 'tableRow', + 'mergeTableCells' + ] + }, + htmlSupport: { + allow: [ { name: /.*/, attributes: true, classes: true, styles: true } ] + } + } ) + .then( editor => { + window.editor = editor; + + const contentPreviewBox = document.getElementById( 'preview' ); + + contentPreviewBox.innerHTML = editor.getData(); + + editor.model.document.on( 'change:data', () => { + contentPreviewBox.innerHTML = editor.getData(); + } ); + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-list/tests/manual/todo-documentlist.md b/packages/ckeditor5-list/tests/manual/todo-documentlist.md new file mode 100644 index 00000000000..84b93d007fc --- /dev/null +++ b/packages/ckeditor5-list/tests/manual/todo-documentlist.md @@ -0,0 +1,92 @@ +## Loading + +1. The data should be loaded with: + * two paragraphs, + * to-do list with eight items, where 2,4 and 7 are checked, + * two paragraphs, + * numbered list with one item, + * to-do list with one unchecked item, + * bullet list with one item. +2. Toolbar should have three buttons: for bullet, numbered and to-do list. + +## Testing + +### Creating: + +1. Convert first paragraph to to-do list item +2. Create empty paragraph and convert to to-do list item +3. Press `Enter` in the middle of item +4. Press `Enter` at the start of item +5. Press `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 todo to numbered for checked and unchecked list item +3. Do it for multiple items at once + +### Merging: + +1. Convert paragraph before to-do list to same type of list +2. Convert paragraph after to-do list to same type of list +3. Convert paragraph before to-do list to different type of list +4. Convert paragraph after to-do list to different type of list +5. Convert first paragraph to to-do list, then convert second paragraph to to-do list +6. Convert multiple items and paragraphs at once + +### Toggling check state: + +1. Put selection in the middle of unchecked the to-do list item +2. Check list item (selection should not move) + +--- + +1. Select multiple to-do list items +2. Check or uncheck to-do list item (selection should not move) + +--- + +1. Check to-do list item +2. Convert checked list item to other list item +3. Convert this list item once again to to-do list item ()should be unchecked) + +--- + +1. Put collapsed selection to to-do list item +2. Press `Ctrl+Space` (check state should toggle) + +### Toggling check state for multiple items: + +1. Select two unchecked list items +2. Press `Ctrl+Space` (both should be checked) +3. Press `Ctrl+Space` once again (both should be unchecked) + +--- + +1. Select checked and unchecked list item +2. Press `Ctrl+Space` (both should be checked) + +--- + +1. Select the entire content +2. Press `Ctrl+Space` (all to-do list items should be checked) +3. Press `Ctrl+Space` once again (all to-do list items should be unchecked) + +### Integration with attribute elements: + +1. Select multiple to-do list items +2. Highlight selected text +3. Check or uncheck highlighted to-do list item +4. Type inside highlighted to-do list item + +### Content styles + +1. Inspect list styles in the editor and in the content preview (below). +2. There should be no major visual difference between them. +3. Check marks in the content preview should be rich custom components (no native checkboxes). From d3d8e8c68871d83e4886c22dca07c4316df3705a Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 10 Aug 2023 12:57:46 +0200 Subject: [PATCH 03/54] The to-do list item should be properly upcasted even if wrapped with a paragraph. --- .../src/documentlist/documentlistediting.ts | 16 ++++++++++++++++ .../tests/manual/todo-documentlist.html | 5 ++++- .../tests/manual/todo-documentlist.js | 6 ++++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts index 2237f40c2b2..9aae8aa99af 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts @@ -397,6 +397,22 @@ export default class DocumentListEditing extends Plugin { view: 'li', model: ( viewElement, { writer } ) => writer.createElement( 'paragraph', { listType: '' } ) } ) + // Convert paragraph to the list block (without list type defined yet). + // This is important to properly handle bogus paragraph and to-do lists. + // Most of the time the bogus paragraph should not appear in the data of to-do list, + // but if there is any marker or an attribute on the paragraph then the bogus paragraph + // is preserved in the data, and we need to be able to detect this case. + .elementToElement( { + view: 'p', + model: ( viewElement, { writer } ) => { + if ( viewElement.parent && viewElement.parent.is( 'element', 'li' ) ) { + return writer.createElement( 'paragraph', { listType: '' } ); + } + + return null; + }, + converterPriority: 'high' + } ) .add( dispatcher => { dispatcher.on( 'element:li', listItemUpcastConverter() ); dispatcher.on( 'element:ul', listUpcastCleanList(), { priority: 'high' } ); diff --git a/packages/ckeditor5-list/tests/manual/todo-documentlist.html b/packages/ckeditor5-list/tests/manual/todo-documentlist.html index 4efbb16981d..56218b0fc18 100644 --- a/packages/ckeditor5-list/tests/manual/todo-documentlist.html +++ b/packages/ckeditor5-list/tests/manual/todo-documentlist.html @@ -2,7 +2,10 @@

    Editor

    • - + +
    • +
    • +
    diff --git a/packages/ckeditor5-list/tests/manual/todo-documentlist.js b/packages/ckeditor5-list/tests/manual/todo-documentlist.js index 5a7920d1551..31aa77aed29 100644 --- a/packages/ckeditor5-list/tests/manual/todo-documentlist.js +++ b/packages/ckeditor5-list/tests/manual/todo-documentlist.js @@ -23,6 +23,7 @@ import { GeneralHtmlSupport } from '@ckeditor/ckeditor5-html-support'; import DocumentList from '../../src/documentlist'; import Todo from '../../src/todo'; +import { Alignment } from '@ckeditor/ckeditor5-alignment'; ClassicEditor .create( document.querySelector( '#editor' ), { @@ -43,14 +44,15 @@ ClassicEditor DocumentList, Todo, SourceEditing, - GeneralHtmlSupport + GeneralHtmlSupport, + Alignment ], toolbar: [ 'heading', '|', 'bulletedList', 'numberedList', 'todoList', 'outdent', 'indent', '|', - 'bold', 'link', 'insertTable', 'fontSize', + 'bold', 'link', 'insertTable', 'fontSize', 'alignment', '|', 'undo', 'redo', '|', 'sourceEditing' ], From 4705bf36548bce73f179abac4c28c013ec0d0f45 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 17 Aug 2023 19:38:17 +0200 Subject: [PATCH 04/54] Renamed Todo plugin to TodoDocumentList. --- .../src/{todo.ts => tododocumentlist.ts} | 10 +++++----- .../tododocumentlistediting.ts} | 11 +++++++++-- .../ckeditor5-list/tests/manual/todo-documentlist.js | 4 ++-- 3 files changed, 16 insertions(+), 9 deletions(-) rename packages/ckeditor5-list/src/{todo.ts => tododocumentlist.ts} (72%) rename packages/ckeditor5-list/src/{todo/todoediting.ts => tododocumentlist/tododocumentlistediting.ts} (97%) diff --git a/packages/ckeditor5-list/src/todo.ts b/packages/ckeditor5-list/src/tododocumentlist.ts similarity index 72% rename from packages/ckeditor5-list/src/todo.ts rename to packages/ckeditor5-list/src/tododocumentlist.ts index e75ab5da115..0f52cde2d17 100644 --- a/packages/ckeditor5-list/src/todo.ts +++ b/packages/ckeditor5-list/src/tododocumentlist.ts @@ -4,10 +4,10 @@ */ /** - * @module list/todo + * @module list/tododocumentlist */ -import TodoEditing from './todo/todoediting'; +import TodoDocumentListEditing from './tododocumentlist/tododocumentlistediting'; import TodoListUI from './todolist/todolistui'; import { Plugin } from 'ckeditor5/src/core'; @@ -19,18 +19,18 @@ import '../theme/todolist.css'; * This is a "glue" plugin that loads the {@link module:list/todolist/todolistediting~TodoListEditing to-do list editing feature} * and the {@link module:list/todolist/todolistui~TodoListUI to-do list UI feature}. */ -export default class Todo extends Plugin { +export default class TodoDocumentlist extends Plugin { /** * @inheritDoc */ public static get requires() { - return [ TodoEditing, TodoListUI ] as const; + return [ TodoDocumentListEditing, TodoListUI ] as const; } /** * @inheritDoc */ public static get pluginName() { - return 'Todo' as const; + return 'TodoDocumentlist' as const; } } diff --git a/packages/ckeditor5-list/src/todo/todoediting.ts b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts similarity index 97% rename from packages/ckeditor5-list/src/todo/todoediting.ts rename to packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts index 8949e364f78..66af593289d 100644 --- a/packages/ckeditor5-list/src/todo/todoediting.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts @@ -4,7 +4,7 @@ */ /** - * @module list/todo/todoediting + * @module list/tododocumentlist/tododocumentlistediting */ import { @@ -28,7 +28,14 @@ import DocumentListCommand from '../documentlist/documentlistcommand'; /** * TODO */ -export default class TodoEditing extends Plugin { +export default class TodoDocumentListEditing extends Plugin { + /** + * @inheritDoc + */ + public static get pluginName() { + return 'TodoDocumentListEditing' as const; + } + /** * @inheritDoc */ diff --git a/packages/ckeditor5-list/tests/manual/todo-documentlist.js b/packages/ckeditor5-list/tests/manual/todo-documentlist.js index 31aa77aed29..f87e888f0e0 100644 --- a/packages/ckeditor5-list/tests/manual/todo-documentlist.js +++ b/packages/ckeditor5-list/tests/manual/todo-documentlist.js @@ -22,7 +22,7 @@ import { SourceEditing } from '@ckeditor/ckeditor5-source-editing'; import { GeneralHtmlSupport } from '@ckeditor/ckeditor5-html-support'; import DocumentList from '../../src/documentlist'; -import Todo from '../../src/todo'; +import TodoDocumentlist from '../../src/tododocumentlist'; import { Alignment } from '@ckeditor/ckeditor5-alignment'; ClassicEditor @@ -42,7 +42,7 @@ ClassicEditor FontSize, Indent, DocumentList, - Todo, + TodoDocumentlist, SourceEditing, GeneralHtmlSupport, Alignment From 56b225124fbdb25c439c5ab0263bc90522d0666c Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 17 Aug 2023 21:06:06 +0200 Subject: [PATCH 05/54] Added command to toggle to-do list items. --- packages/ckeditor5-list/src/augmentation.ts | 9 +- .../src/documentlist/converters.ts | 2 +- packages/ckeditor5-list/src/index.ts | 3 + .../ckeditor5-list/src/tododocumentlist.ts | 4 +- .../checktododocumentlistcommand.ts | 97 ++++++++++++ .../tododocumentlist/inputchangeobserver.ts | 47 ++++++ .../tododocumentlistediting.ts | 138 ++++++++++++++++-- 7 files changed, 280 insertions(+), 20 deletions(-) create mode 100644 packages/ckeditor5-list/src/tododocumentlist/checktododocumentlistcommand.ts create mode 100644 packages/ckeditor5-list/src/tododocumentlist/inputchangeobserver.ts diff --git a/packages/ckeditor5-list/src/augmentation.ts b/packages/ckeditor5-list/src/augmentation.ts index bfe1b69c01e..357b4cdfb1d 100644 --- a/packages/ckeditor5-list/src/augmentation.ts +++ b/packages/ckeditor5-list/src/augmentation.ts @@ -23,6 +23,8 @@ import type { TodoList, TodoListEditing, TodoListUI, + TodoDocumentList, + TodoDocumentListEditing, ListCommand, DocumentListCommand, @@ -36,7 +38,8 @@ import type { DocumentListStartCommand, ListReversedCommand, DocumentListReversedCommand, - CheckTodoListCommand + CheckTodoListCommand, + CheckTodoDocumentListCommand } from '.'; declare module '@ckeditor/ckeditor5-core' { @@ -69,6 +72,8 @@ declare module '@ckeditor/ckeditor5-core' { [ TodoList.pluginName ]: TodoList; [ TodoListEditing.pluginName ]: TodoListEditing; [ TodoListUI.pluginName ]: TodoListUI; + [ TodoDocumentList.pluginName ]: TodoDocumentList; + [ TodoDocumentListEditing.pluginName ]: TodoDocumentListEditing; } interface CommandsMap { @@ -84,6 +89,6 @@ declare module '@ckeditor/ckeditor5-core' { listStart: ListStartCommand | DocumentListStartCommand; listReversed: ListReversedCommand | DocumentListReversedCommand; todoList: ListCommand | DocumentListCommand; - checkTodoList: CheckTodoListCommand; + checkTodoList: CheckTodoListCommand | CheckTodoDocumentListCommand; } } diff --git a/packages/ckeditor5-list/src/documentlist/converters.ts b/packages/ckeditor5-list/src/documentlist/converters.ts index 1cdc17a0570..c2c8716a79e 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.ts +++ b/packages/ckeditor5-list/src/documentlist/converters.ts @@ -80,7 +80,7 @@ export function listItemUpcastConverter(): GetCallback { for ( const item of items ) { // Set list attributes only on same level items, those nested deeper are already handled by the recursive conversion. - if ( !isListItemBlock( item ) ) { + if ( !item.hasAttribute( 'listItemId' ) ) { // Preserve list type if was already set (for example by to-do list feature). if ( item.getAttribute( 'listType' ) ) { writer.setAttributes( { ...attributes, listType: item.getAttribute( 'listType' ) }, item ); diff --git a/packages/ckeditor5-list/src/index.ts b/packages/ckeditor5-list/src/index.ts index 5ff7baa1c15..c6dc8eb719c 100644 --- a/packages/ckeditor5-list/src/index.ts +++ b/packages/ckeditor5-list/src/index.ts @@ -26,6 +26,8 @@ export { default as ListPropertiesUI } from './listproperties/listpropertiesui'; export { default as TodoList } from './todolist'; export { default as TodoListEditing } from './todolist/todolistediting'; export { default as TodoListUI } from './todolist/todolistui'; +export { default as TodoDocumentList } from './tododocumentlist'; +export { default as TodoDocumentListEditing } from './tododocumentlist/tododocumentlistediting'; export type { ListConfig, ListPropertiesConfig } from './listconfig'; export type { default as ListStyle } from './liststyle'; @@ -40,5 +42,6 @@ export type { default as ListReversedCommand } from './listproperties/listrevers export type { default as ListStartCommand } from './listproperties/liststartcommand'; export type { default as ListStyleCommand } from './listproperties/liststylecommand'; export type { default as CheckTodoListCommand } from './todolist/checktodolistcommand'; +export type { default as CheckTodoDocumentListCommand } from './tododocumentlist/checktododocumentlistcommand'; import './augmentation'; diff --git a/packages/ckeditor5-list/src/tododocumentlist.ts b/packages/ckeditor5-list/src/tododocumentlist.ts index 0f52cde2d17..129b3d36abd 100644 --- a/packages/ckeditor5-list/src/tododocumentlist.ts +++ b/packages/ckeditor5-list/src/tododocumentlist.ts @@ -19,7 +19,7 @@ import '../theme/todolist.css'; * This is a "glue" plugin that loads the {@link module:list/todolist/todolistediting~TodoListEditing to-do list editing feature} * and the {@link module:list/todolist/todolistui~TodoListUI to-do list UI feature}. */ -export default class TodoDocumentlist extends Plugin { +export default class TodoDocumentList extends Plugin { /** * @inheritDoc */ @@ -31,6 +31,6 @@ export default class TodoDocumentlist extends Plugin { * @inheritDoc */ public static get pluginName() { - return 'TodoDocumentlist' as const; + return 'TodoDocumentList' as const; } } diff --git a/packages/ckeditor5-list/src/tododocumentlist/checktododocumentlistcommand.ts b/packages/ckeditor5-list/src/tododocumentlist/checktododocumentlistcommand.ts new file mode 100644 index 00000000000..826deec38ec --- /dev/null +++ b/packages/ckeditor5-list/src/tododocumentlist/checktododocumentlistcommand.ts @@ -0,0 +1,97 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module list/tododocumentlist/checktododocumentlistcommand + */ + +import { Command, type Editor } from 'ckeditor5/src/core'; +import type { + Element, + DocumentSelection, + Selection +} from 'ckeditor5/src/engine'; + +const attributeKey = 'todoListChecked'; + +/** + * The check to-do command. + * + * TODO + * + * The command is registered by the {@link module:list/todolist/todolistediting~TodoListEditing} as + * the `checkTodoList` editor command and it is also available via aliased `todoListCheck` name. + */ +export default class CheckTodoDocumentListCommand extends Command { + /** + * A list of to-do list items selected by the {@link module:engine/model/selection~Selection}. + * + * @observable + * @readonly + */ + declare public value: boolean; + + /** + * Updates the command's {@link #value} and {@link #isEnabled} properties based on the current selection. + */ + public override refresh(): void { + const selectedElements = this._getSelectedItems( this.editor.model.document.selection ); + + this.value = this._getValue( selectedElements ); + this.isEnabled = !!selectedElements.length; + } + + /** + * Executes the command. + * + * @param options.forceValue If set, it will force the command behavior. If `true`, the command will apply + * the attribute. Otherwise, the command will remove the attribute. If not set, the command will look for its current + * value to decide what it should do. + */ + public override execute( options: { + forceValue?: boolean; + selection?: Selection | DocumentSelection; + } = {} ): void { + this.editor.model.change( writer => { + const selectedElements = this._getSelectedItems( options.selection || this.editor.model.document.selection ); + const value = ( options.forceValue === undefined ) ? !this._getValue( selectedElements ) : options.forceValue; + + for ( const element of selectedElements ) { + writer.setAttribute( attributeKey, value, element ); + } + } ); + } + + /** + * TODO + */ + private _getValue( selectedElements: Array ): boolean { + return selectedElements.every( element => !!element.getAttribute( attributeKey ) ); + } + + /** + * Gets all to-do list items selected by the {@link module:engine/model/selection~Selection}. + */ + private _getSelectedItems( selection: Selection | DocumentSelection ) { + const model = this.editor.model; + const schema = model.schema; + + const selectionRange = selection.getFirstRange()!; + const startElement = selectionRange.start.parent as Element; + const elements: Array = []; + + if ( schema.checkAttribute( startElement, attributeKey ) ) { + elements.push( startElement ); + } + + for ( const item of selectionRange.getItems() as Iterable ) { + if ( schema.checkAttribute( item, attributeKey ) && !elements.includes( item ) ) { + elements.push( item ); + } + } + + return elements; + } +} diff --git a/packages/ckeditor5-list/src/tododocumentlist/inputchangeobserver.ts b/packages/ckeditor5-list/src/tododocumentlist/inputchangeobserver.ts new file mode 100644 index 00000000000..7d0dd859369 --- /dev/null +++ b/packages/ckeditor5-list/src/tododocumentlist/inputchangeobserver.ts @@ -0,0 +1,47 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module list/tododocumentlist/inputchangebserver + */ + +import { DomEventObserver, type DomEventData } from 'ckeditor5/src/engine'; + +/** + * TODO + * + * Note that this observer is not available by default. To make it available it needs to be added to + * {@link module:engine/view/view~View} by {@link module:engine/view/view~View#addObserver} method. + */ +export default class InputChangeObserver extends DomEventObserver<'change'> { + /** + * @inheritDoc + */ + public readonly domEventType = [ 'change' ] as const; + + /** + * @inheritDoc + */ + public onDomEvent( domEvent: Event ): void { + this.fire( 'inputChange', domEvent ); + } +} + +/** + * Fired when the TODO + * + * Introduced by TODO + * + * Note that this event is not available by default. To make it available, TODO + * needs to be added to {@link module:engine/view/view~View} by the {@link module:engine/view/view~View#addObserver} method. + * + * @see module:list/tododocumentlist/inputchangebserver~InputChangeObserver + * @eventName module:engine/view/document~Document#inputchange + * @param data The event data. + */ +export type ViewDocumentInputChangeEvent = { + name: 'inputChange'; + args: [ data: DomEventData ]; +}; diff --git a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts index 66af593289d..3284d75f572 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts @@ -15,15 +15,29 @@ import { type ElementCreatorFunction, type UpcastElementEvent, type Element, - type MatcherPattern + type MatcherPattern, + type Model, + type ViewDocumentArrowKeyEvent, + type ViewDocumentKeyDownEvent } from 'ckeditor5/src/engine'; +import { + getCode, + getLocalizedArrowKeyCodeDirection, + parseKeystroke, + type Locale, + type GetCallback +} from 'ckeditor5/src/utils'; + import { Plugin } from 'ckeditor5/src/core'; -import type { GetCallback } from 'ckeditor5/src/utils'; import { isListItemBlock } from '../documentlist/utils/model'; import DocumentListEditing from '../documentlist/documentlistediting'; import DocumentListCommand from '../documentlist/documentlistcommand'; +import CheckTodoDocumentListCommand from './checktododocumentlistcommand'; +import InputChangeObserver, { type ViewDocumentInputChangeEvent } from './inputchangeobserver'; + +const ITEM_TOGGLE_KEYSTROKE = parseKeystroke( 'Ctrl+Enter' ); /** * TODO @@ -45,14 +59,26 @@ export default class TodoDocumentListEditing extends Plugin { editor.commands.add( 'todoList', new DocumentListCommand( editor, 'todo' ) ); + const checkTodoListCommand = new CheckTodoDocumentListCommand( editor ); + + // Register `checkTodoList` command and add `todoListCheck` command as an alias for backward compatibility. + editor.commands.add( 'checkTodoList', checkTodoListCommand ); + editor.commands.add( 'todoListCheck', checkTodoListCommand ); + + editor.editing.view.addObserver( InputChangeObserver ); + model.schema.extend( 'paragraph', { - allowAttributes: 'todoItemChecked' + allowAttributes: 'todoListChecked' } ); - model.schema.addAttributeCheck( ( context: any, attributeName ) => { + model.schema.addAttributeCheck( ( context, attributeName ) => { const item = context.last; - if ( attributeName == 'todoItemChecked' && isListItemBlock( item ) && item.getAttribute( 'listType' ) != 'todo' ) { + if ( attributeName != 'todoListChecked' ) { + return; + } + + if ( !item.getAttribute( 'listItemId' ) || item.getAttribute( 'listType' ) != 'todo' ) { return false; } } ); @@ -76,7 +102,7 @@ export default class TodoDocumentListEditing extends Plugin { editor.conversion.for( 'dataDowncast' ).elementToElement( { model: { name: 'paragraph', - attributes: 'todoItemChecked' + attributes: 'todoListChecked' }, view: todoItemViewCreator( { dataPipeline: true } ), converterPriority: 'highest' @@ -85,7 +111,7 @@ export default class TodoDocumentListEditing extends Plugin { editor.conversion.for( 'editingDowncast' ).elementToElement( { model: { name: 'paragraph', - attributes: 'todoItemChecked' + attributes: 'todoListChecked' }, view: todoItemViewCreator(), converterPriority: 'highest' @@ -120,13 +146,13 @@ export default class TodoDocumentListEditing extends Plugin { const element = change.range.start.nodeAfter!; if ( change.attributeNewValue == 'todo' ) { - if ( !element.hasAttribute( 'todoItemChecked' ) ) { - writer.setAttribute( 'todoItemChecked', false, element ); + if ( !element.hasAttribute( 'todoListChecked' ) ) { + writer.setAttribute( 'todoListChecked', false, element ); wasFixed = true; } } else if ( change.attributeOldValue == 'todo' ) { - if ( element.hasAttribute( 'todoItemChecked' ) ) { - writer.removeAttribute( 'todoItemChecked', element ); + if ( element.hasAttribute( 'todoListChecked' ) ) { + writer.removeAttribute( 'todoListChecked', element ); wasFixed = true; } } @@ -134,6 +160,48 @@ export default class TodoDocumentListEditing extends Plugin { return wasFixed; } ); + + // Jump at the end of the previous node on left arrow key press, when selection is after the checkbox. + // + //

    Foo

    + //
    • {}Bar
    + // + // press: `<-` + // + //

    Foo{}

    + //
    • Bar
    + // + this.listenTo( + editor.editing.view.document, + 'arrowKey', + jumpOverCheckmarkOnSideArrowKeyPress( model, editor.locale ), + { context: 'li' } + ); + + // Toggle check state of selected to-do list items on keystroke. + this.listenTo( editor.editing.view.document, 'keydown', ( evt, data ) => { + if ( getCode( data ) === ITEM_TOGGLE_KEYSTROKE ) { + editor.execute( 'checkTodoList' ); + evt.stop(); + } + }, { priority: 'high' } ); + + this.listenTo( editor.editing.view.document, 'inputChange', ( evt, data ) => { + const viewTarget = data.target; + + if ( !viewTarget || !viewTarget.is( 'element', 'input' ) ) { + return; + } + + const viewElement = editor.editing.mapper.findMappedViewAncestor( editor.editing.view.createPositionBefore( data.target ) ); + const modelElement = editor.editing.mapper.toModelElement( viewElement ); + + if ( modelElement && isListItemBlock( modelElement ) && modelElement.getAttribute( 'listType' ) == 'todo' ) { + editor.execute( 'checkTodoList', { + selection: editor.model.createSelection( modelElement, 'end' ) + } ); + } + } ); } } @@ -159,7 +227,7 @@ function todoItemInputConverter(): GetCallback { writer.setAttribute( 'listType', 'todo', modelItem ); if ( data.viewItem.hasAttribute( 'checked' ) ) { - writer.setAttribute( 'todoItemChecked', true, modelItem ); + writer.setAttribute( 'todoListChecked', true, modelItem ); } data.modelRange = writer.createRange( modelCursor ); @@ -226,12 +294,13 @@ function todoItemViewCreator( { dataPipeline }: { dataPipeline?: boolean } = {} } const labelWithCheckbox = writer.createContainerElement( 'label', { - class: 'todo-list__label' + class: 'todo-list__label', + ...( !dataPipeline ? { contenteditable: false } : null ) }, [ writer.createEmptyElement( 'input', { type: 'checkbox', - ...( modelElement.getAttribute( 'todoItemChecked' ) ? { checked: 'checked' } : null ), - ... ( dataPipeline ? { disabled: 'disabled' } : null ) + ...( modelElement.getAttribute( 'todoListChecked' ) ? { checked: 'checked' } : null ), + ... ( dataPipeline ? { disabled: 'disabled' } : { tabindex: '-1' } ) } ) ] ); @@ -289,3 +358,42 @@ function findDescription( viewItem: ViewElement, view: View ) { } } } + +/** + * Handles the left/right (LTR/RTL content) arrow key and moves the selection at the end of the previous block element + * if the selection is just after the checkbox element. In other words, it jumps over the checkbox element when + * moving the selection to the left/right (LTR/RTL). + * + * @returns Callback for 'keydown' events. + */ +function jumpOverCheckmarkOnSideArrowKeyPress( model: Model, locale: Locale ): GetCallback { + return ( eventInfo, domEventData ) => { + const direction = getLocalizedArrowKeyCodeDirection( domEventData.keyCode, locale.contentLanguageDirection ); + + if ( direction != 'left' ) { + return; + } + + const schema = model.schema; + const selection = model.document.selection; + + if ( !selection.isCollapsed ) { + return; + } + + const position = selection.getFirstPosition()!; + const parent = position.parent; + + if ( isListItemBlock( parent ) && parent.getAttribute( 'listType' ) == 'todo' && position.isAtStart ) { + const newRange = schema.getNearestSelectionRange( model.createPositionBefore( parent ), 'backward' ); + + if ( newRange ) { + model.change( writer => writer.setSelection( newRange ) ); + } + + domEventData.preventDefault(); + domEventData.stopPropagation(); + eventInfo.stop(); + } + }; +} From 76a59fbc4127d6dbe52a3beb083463a2abf58e7c Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 18 Aug 2023 18:57:14 +0200 Subject: [PATCH 06/54] Allow converting any block type to a to-do list item. --- .../ckeditor5-html-support/src/datafilter.ts | 88 ++++++++++++------- .../src/documentlist/documentlistcommand.ts | 27 +++++- .../src/documentlist/utils/model.ts | 13 ++- .../src/documentlist/utils/postfixers.ts | 11 +++ .../checktododocumentlistcommand.ts | 6 +- .../tododocumentlistediting.ts | 49 +++++++---- 6 files changed, 143 insertions(+), 51 deletions(-) diff --git a/packages/ckeditor5-html-support/src/datafilter.ts b/packages/ckeditor5-html-support/src/datafilter.ts index de681af2e7a..9943e63fa7e 100644 --- a/packages/ckeditor5-html-support/src/datafilter.ts +++ b/packages/ckeditor5-html-support/src/datafilter.ts @@ -109,7 +109,7 @@ export default class DataFilter extends Plugin { /** * Allowed element definitions by {@link module:html-support/datafilter~DataFilter#allowElement} method. */ - private readonly _allowedElements: Set; + private readonly _allowedElements: Set; /** * Disallowed element names by {@link module:html-support/datafilter~DataFilter#disallowElement} method. @@ -128,22 +128,22 @@ export default class DataFilter extends Plugin { */ private _coupledAttributes: Map> | null; + /** + * TODO + */ + private _coupledGHSAttributes: Map | null; + constructor( editor: Editor ) { super( editor ); this._dataSchema = editor.plugins.get( 'DataSchema' ); - this._allowedAttributes = new Matcher(); - this._disallowedAttributes = new Matcher(); - this._allowedElements = new Set(); - this._disallowedElements = new Set(); - this._dataInitialized = false; - this._coupledAttributes = null; + this._coupledGHSAttributes = null; this._registerElementsAfterInit(); this._registerElementHandlers(); @@ -449,27 +449,44 @@ export default class DataFilter extends Plugin { const changes = model.document.differ.getChanges(); let changed = false; - const coupledAttributes = this._getCoupledAttributesMap(); + const [ coupledAttributes, coupledGHSAttributes ] = this._getCoupledAttributesMaps(); for ( const change of changes ) { - // Handle only attribute removals. - if ( change.type != 'attribute' || change.attributeNewValue !== null ) { - continue; - } + // Handle attribute removals. + if ( change.type == 'attribute' && change.attributeNewValue === null ) { + // Find a list of coupled GHS attributes. + const attributeKeys = coupledAttributes.get( change.attributeKey ); - // Find a list of coupled GHS attributes. - const attributeKeys = coupledAttributes.get( change.attributeKey ); + if ( !attributeKeys ) { + continue; + } - if ( !attributeKeys ) { - continue; + // Remove the coupled GHS attributes on the same range as the feature attribute was removed. + for ( const { item } of change.range.getWalker( { shallow: true } ) ) { + for ( const attributeKey of attributeKeys ) { + if ( item.hasAttribute( attributeKey ) ) { + writer.removeAttribute( attributeKey, item ); + changed = true; + } + } + } } + // Handle element rename (and attribute remove on it). + // Example: -> + // The GHS element attributes can't exist without coupled feature attribute (for example in lists). + else if ( change.type == 'insert' && change.name != '$text' ) { + for ( const { item } of writer.createRangeOn( change.position.nodeAfter! ) ) { + if ( !item.is( 'element' ) ) { + continue; + } + + for ( const [ key ] of item.getAttributes() ) { + const coupledAttribute = coupledGHSAttributes.get( key ); - // Remove the coupled GHS attributes on the same range as the feature attribute was removed. - for ( const { item } of change.range.getWalker( { shallow: true } ) ) { - for ( const attributeKey of attributeKeys ) { - if ( item.hasAttribute( attributeKey ) ) { - writer.removeAttribute( attributeKey, item ); - changed = true; + if ( coupledAttribute && !item.hasAttribute( coupledAttribute ) ) { + writer.removeAttribute( key, item ); + changed = true; + } } } } @@ -540,26 +557,37 @@ export default class DataFilter extends Plugin { * Collects the map of coupled attributes. The returned map is keyed by the feature attribute name * and coupled GHS attribute names are stored in the value array. */ - private _getCoupledAttributesMap(): Map> { - if ( this._coupledAttributes ) { - return this._coupledAttributes; + private _getCoupledAttributesMaps(): [ Map>, Map ] { + if ( this._coupledAttributes && this._coupledGHSAttributes ) { + return [ this._coupledAttributes, this._coupledGHSAttributes ]; } this._coupledAttributes = new Map>(); + this._coupledGHSAttributes = new Map(); for ( const definition of this._allowedElements ) { - if ( definition.coupledAttribute && definition.model ) { - const attributeNames = this._coupledAttributes.get( definition.coupledAttribute ); + if ( !definition.isInline ) { + continue; + } + + const inlineDefinition = definition as DataSchemaInlineElementDefinition; + + if ( inlineDefinition.coupledAttribute && inlineDefinition.model ) { + const attributeNames = this._coupledAttributes.get( inlineDefinition.coupledAttribute ); if ( attributeNames ) { - attributeNames.push( definition.model ); + attributeNames.push( inlineDefinition.model ); } else { - this._coupledAttributes.set( definition.coupledAttribute, [ definition.model ] ); + this._coupledAttributes.set( inlineDefinition.coupledAttribute, [ inlineDefinition.model ] ); + } + + if ( inlineDefinition.appliesToBlock ) { + this._coupledGHSAttributes.set( inlineDefinition.model, inlineDefinition.coupledAttribute ); } } } - return this._coupledAttributes; + return [ this._coupledAttributes, this._coupledGHSAttributes ]; } /** diff --git a/packages/ckeditor5-list/src/documentlist/documentlistcommand.ts b/packages/ckeditor5-list/src/documentlist/documentlistcommand.ts index 4ca5ababf13..87b6221303c 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistcommand.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistcommand.ts @@ -19,7 +19,8 @@ import { ListItemUid, sortBlocks, getSelectedBlockObject, - isListItemBlock + isListItemBlock, + checkCanBeRenamed } from './utils/model'; /** @@ -39,16 +40,22 @@ export default class DocumentListCommand extends Command { */ public declare value: boolean; + /** + * TODO + */ + private _requiredElementName?: string; + /** * Creates an instance of the command. * * @param editor The editor instance. * @param type List type that will be handled by this command. */ - constructor( editor: Editor, type: 'numbered' | 'bulleted' | 'todo' ) { + constructor( editor: Editor, type: 'numbered' | 'bulleted' | 'todo', requiredElementName?: string ) { super( editor ); this.type = type; + this._requiredElementName = requiredElementName; } /** @@ -75,7 +82,10 @@ export default class DocumentListCommand extends Command { const selectedBlockObject = getSelectedBlockObject( model ); const blocks = Array.from( document.selection.getSelectedBlocks() ) - .filter( block => model.schema.checkAttribute( block, 'listType' ) ); + .filter( block => ( + model.schema.checkAttribute( block, 'listType' ) || + this._requiredElementName && checkCanBeRenamed( block, model.schema, this._requiredElementName ) + ) ); // Whether we are turning off some items. const turnOff = options.forceValue !== undefined ? !options.forceValue : this.value; @@ -100,7 +110,7 @@ export default class DocumentListCommand extends Command { this._fireAfterExecute( changedBlocks ); } - // Turning on the list items for a collapsed selection inside a list item. + // Changing type of list items for a collapsed selection inside a list item. else if ( ( selectedBlockObject || document.selection.isCollapsed ) && isListItemBlock( blocks[ 0 ] ) ) { const changedBlocks = getListItems( selectedBlockObject || blocks[ 0 ] ); @@ -115,6 +125,15 @@ export default class DocumentListCommand extends Command { const changedBlocks = []; for ( const block of blocks ) { + // Rename block to a required element name if type of the list requires it. + if ( + this._requiredElementName && + !block.is( 'element', this._requiredElementName ) && + checkCanBeRenamed( block, model.schema, this._requiredElementName ) + ) { + writer.rename( block, this._requiredElementName ); + } + // Promote the given block to the list item. if ( !block.hasAttribute( 'listType' ) ) { writer.setAttributes( { diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.ts b/packages/ckeditor5-list/src/documentlist/utils/model.ts index 899c404173c..2b2910d71d5 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.ts +++ b/packages/ckeditor5-list/src/documentlist/utils/model.ts @@ -13,7 +13,8 @@ import type { Model, Node, Writer, - Item + Item, + Schema } from 'ckeditor5/src/engine'; import { uid, toArray, type ArrayOrItem } from 'ckeditor5/src/utils'; @@ -545,6 +546,16 @@ export function getSelectedBlockObject( model: Model ): Element | null { return null; } +/** + * Checks whether the given block can be replaced by a paragraph. + * + * @param block A block to be tested. + * @param schema The schema of the document. + */ +export function checkCanBeRenamed( block: Element, schema: Schema, elementName: string ): boolean { + return schema.checkChild( block.parent as Element, elementName ) && !schema.isObject( block ); +} + // 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: ListElement, diff --git a/packages/ckeditor5-list/src/documentlist/utils/postfixers.ts b/packages/ckeditor5-list/src/documentlist/utils/postfixers.ts index 34ccfb64f3a..ff8d0c5bf51 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/postfixers.ts +++ b/packages/ckeditor5-list/src/documentlist/utils/postfixers.ts @@ -135,6 +135,17 @@ export function fixListItemIds( seenIds.add( listItemId ); + // Make sure that all items in a to-do list have unique IDs. + if ( listType == 'todo' ) { + if ( node.getAttribute( 'listItemId' ) != listItemId ) { + writer.setAttribute( 'listItemId', listItemId, node ); + + applied = true; + } + + continue; + } + for ( const block of getListItemBlocks( node, { direction: 'forward' } ) ) { visited.add( block ); diff --git a/packages/ckeditor5-list/src/tododocumentlist/checktododocumentlistcommand.ts b/packages/ckeditor5-list/src/tododocumentlist/checktododocumentlistcommand.ts index 826deec38ec..aa868aee49f 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/checktododocumentlistcommand.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/checktododocumentlistcommand.ts @@ -59,7 +59,11 @@ export default class CheckTodoDocumentListCommand extends Command { const value = ( options.forceValue === undefined ) ? !this._getValue( selectedElements ) : options.forceValue; for ( const element of selectedElements ) { - writer.setAttribute( attributeKey, value, element ); + if ( value ) { + writer.setAttribute( attributeKey, true, element ); + } else { + writer.removeAttribute( attributeKey, element ); + } } } ); } diff --git a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts index 3284d75f572..af66b00c2ef 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts @@ -31,8 +31,8 @@ import { import { Plugin } from 'ckeditor5/src/core'; -import { isListItemBlock } from '../documentlist/utils/model'; -import DocumentListEditing from '../documentlist/documentlistediting'; +import { isListItemBlock, removeListAttributes } from '../documentlist/utils/model'; +import DocumentListEditing, { type DocumentListEditingPostFixerEvent } from '../documentlist/documentlistediting'; import DocumentListCommand from '../documentlist/documentlistcommand'; import CheckTodoDocumentListCommand from './checktododocumentlistcommand'; import InputChangeObserver, { type ViewDocumentInputChangeEvent } from './inputchangeobserver'; @@ -50,6 +50,13 @@ export default class TodoDocumentListEditing extends Plugin { return 'TodoDocumentListEditing' as const; } + /** + * @inheritDoc + */ + public static get requires() { + return [ DocumentListEditing ] as const; + } + /** * @inheritDoc */ @@ -57,7 +64,7 @@ export default class TodoDocumentListEditing extends Plugin { const editor = this.editor; const model = editor.model; - editor.commands.add( 'todoList', new DocumentListCommand( editor, 'todo' ) ); + editor.commands.add( 'todoList', new DocumentListCommand( editor, 'todo', 'paragraph' ) ); const checkTodoListCommand = new CheckTodoDocumentListCommand( editor ); @@ -139,28 +146,40 @@ export default class TodoDocumentListEditing extends Plugin { let wasFixed = false; for ( const change of changes ) { - if ( change.type != 'attribute' || change.attributeKey != 'listType' ) { - continue; - } + if ( change.type == 'attribute' && change.attributeKey == 'listType' ) { + const element = change.range.start.nodeAfter!; - const element = change.range.start.nodeAfter!; - - if ( change.attributeNewValue == 'todo' ) { - if ( !element.hasAttribute( 'todoListChecked' ) ) { - writer.setAttribute( 'todoListChecked', false, element ); - wasFixed = true; - } - } else if ( change.attributeOldValue == 'todo' ) { - if ( element.hasAttribute( 'todoListChecked' ) ) { + if ( change.attributeOldValue == 'todo' && element.hasAttribute( 'todoListChecked' ) ) { writer.removeAttribute( 'todoListChecked', element ); wasFixed = true; } + } else if ( change.type == 'insert' && change.name != '$text' ) { + for ( const { item } of writer.createRangeOn( change.position.nodeAfter! ) ) { + if ( item.is( 'element' ) && item.getAttribute( 'listType' ) != 'todo' && item.hasAttribute( 'todoListChecked' ) ) { + writer.removeAttribute( 'todoListChecked', item ); + wasFixed = true; + } + } } } return wasFixed; } ); + // Make sure that only paragraphs can be a to-do list item. + this.listenTo( documentListEditing, 'postFixer', ( evt, { listNodes, writer } ) => { + let applied = false; + + for ( const { node } of listNodes ) { + if ( node.getAttribute( 'listType' ) == 'todo' && node.name != 'paragraph' ) { + removeListAttributes( node, writer ); + applied = true; + } + } + + evt.return = applied || evt.return; + } ); + // Jump at the end of the previous node on left arrow key press, when selection is after the checkbox. // //

    Foo

    From 8205c7f0ed42fe3dea47d55803791d1454535f81 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 18 Aug 2023 19:32:46 +0200 Subject: [PATCH 07/54] WiP. --- .../ckeditor5-list/src/documentlist/documentlistcommand.ts | 1 + packages/ckeditor5-list/tests/manual/todo-documentlist.html | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistcommand.ts b/packages/ckeditor5-list/src/documentlist/documentlistcommand.ts index 87b6221303c..e56a342a8b2 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistcommand.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistcommand.ts @@ -132,6 +132,7 @@ export default class DocumentListCommand extends Command { checkCanBeRenamed( block, model.schema, this._requiredElementName ) ) { writer.rename( block, this._requiredElementName ); + changedBlocks.push( block ); } // Promote the given block to the list item. diff --git a/packages/ckeditor5-list/tests/manual/todo-documentlist.html b/packages/ckeditor5-list/tests/manual/todo-documentlist.html index 56218b0fc18..e2e6747d479 100644 --- a/packages/ckeditor5-list/tests/manual/todo-documentlist.html +++ b/packages/ckeditor5-list/tests/manual/todo-documentlist.html @@ -4,8 +4,8 @@

    Editor

  • -
  • - +
  • +
  • From 3df3875f10fd2013343e74de5127516a0f98af05 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 25 Aug 2023 21:32:55 +0200 Subject: [PATCH 08/54] Introducing a generic list item marker downcast strategy (PoC). --- .../src/documentlist/converters.ts | 44 ++++++-- .../src/documentlist/documentlistcommand.ts | 19 ++-- .../src/documentlist/documentlistediting.ts | 15 ++- .../src/documentlist/utils/postfixers.ts | 11 -- .../ckeditor5-list/src/tododocumentlist.ts | 2 +- .../tododocumentlistediting.ts | 85 ++++++++------- .../ckeditor5-list/theme/tododocumentlist.css | 100 ++++++++++++++++++ 7 files changed, 202 insertions(+), 74 deletions(-) create mode 100644 packages/ckeditor5-list/theme/tododocumentlist.css diff --git a/packages/ckeditor5-list/src/documentlist/converters.ts b/packages/ckeditor5-list/src/documentlist/converters.ts index c2c8716a79e..260c95b4f37 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.ts +++ b/packages/ckeditor5-list/src/documentlist/converters.ts @@ -30,6 +30,7 @@ import { getAllListItemBlocks, getListItemBlocks, isListItemBlock, + isFirstBlockOfListItem, ListItemUid, type ListElement } from './utils/model'; @@ -72,21 +73,19 @@ export function listItemUpcastConverter(): GetCallback { return; } + // Preserve list type if was already set (for example by to-do list feature). + const firstItemListType = items[ 0 ].getAttribute( 'listType' ); + const attributes = { listItemId: ListItemUid.next(), listIndent: getIndent( data.viewItem ), - listType: data.viewItem.parent && data.viewItem.parent.is( 'element', 'ol' ) ? 'numbered' : 'bulleted' + listType: firstItemListType || ( data.viewItem.parent && data.viewItem.parent.is( 'element', 'ol' ) ? 'numbered' : 'bulleted' ) }; for ( const item of items ) { // Set list attributes only on same level items, those nested deeper are already handled by the recursive conversion. if ( !item.hasAttribute( 'listItemId' ) ) { - // Preserve list type if was already set (for example by to-do list feature). - if ( item.getAttribute( 'listType' ) ) { - writer.setAttributes( { ...attributes, listType: item.getAttribute( 'listType' ) }, item ); - } else { - writer.setAttributes( attributes, item ); - } + writer.setAttributes( attributes, item ); } } @@ -328,7 +327,8 @@ export function reconvertItemsOnDataChange( export function listItemDowncastConverter( attributeNames: Array, strategies: Array, - model: Model + model: Model, + { dataPipeline }: { dataPipeline?: boolean } = {} ): GetCallback> { const consumer = createAttributesConsumer( attributeNames ); @@ -349,12 +349,34 @@ export function listItemDowncastConverter( // Use positions mapping instead of mapper.toViewElement( listItem ) to find outermost view element. // This is for cases when mapping is using inner view element like in the code blocks (pre > code). const viewElement = findMappedViewElement( listItem, mapper, model )!; + const previousSibling = viewElement.previousSibling; + + if ( previousSibling && previousSibling.is( 'element', 'input' ) ) { + writer.remove( previousSibling ); + } // Unwrap element from current list wrappers. unwrapListItemBlock( viewElement, writer ); + let viewRange = writer.createRangeOn( viewElement ); + + if ( isFirstBlockOfListItem( listItem ) && listItem.getAttribute( 'listType' ) == 'todo' ) { + const markerElement = writer.createEmptyElement( 'input', { + type: 'checkbox', + ...( listItem.getAttribute( 'todoListChecked' ) ? { checked: 'checked' } : null ), + ... ( dataPipeline ? { disabled: 'disabled' } : { tabindex: '-1', contenteditable: 'false' } ) + } ); + + writer.insert( viewRange.start, markerElement ); + + viewRange = writer.createRange( + writer.createPositionBefore( markerElement ), + writer.createPositionAfter( viewElement ) + ); + } + // Then wrap them with the new list wrappers. - wrapListItemBlock( listItem, writer.createRangeOn( viewElement ), strategies, writer ); + wrapListItemBlock( listItem, viewRange, strategies, writer ); }; } @@ -400,7 +422,9 @@ export function findMappedViewElement( element: Element, mapper: Mapper, model: const modelRange = model.createRangeOn( element ); const viewRange = mapper.toViewRange( modelRange ).getTrimmed(); - return viewRange.getContainedElement(); + // TODO trim the custom marker (empty element without mapping and model length = 0). + // return viewRange.getContainedElement(); + return viewRange.end.nodeBefore as ViewElement | null; } // Unwraps all ol, ul, and li attribute elements that are wrapping the provided view element. diff --git a/packages/ckeditor5-list/src/documentlist/documentlistcommand.ts b/packages/ckeditor5-list/src/documentlist/documentlistcommand.ts index e56a342a8b2..3b199c15663 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistcommand.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistcommand.ts @@ -82,10 +82,10 @@ export default class DocumentListCommand extends Command { const selectedBlockObject = getSelectedBlockObject( model ); const blocks = Array.from( document.selection.getSelectedBlocks() ) - .filter( block => ( - model.schema.checkAttribute( block, 'listType' ) || - this._requiredElementName && checkCanBeRenamed( block, model.schema, this._requiredElementName ) - ) ); + .filter( block => this._requiredElementName ? + checkCanBeRenamed( block, model.schema, this._requiredElementName ) : + model.schema.checkAttribute( block, 'listType' ) + ); // Whether we are turning off some items. const turnOff = options.forceValue !== undefined ? !options.forceValue : this.value; @@ -198,8 +198,9 @@ export default class DocumentListCommand extends Command { * @returns Whether the command should be enabled. */ private _checkEnabled(): boolean { - const selection = this.editor.model.document.selection; - const schema = this.editor.model.schema; + const model = this.editor.model; + const selection = model.document.selection; + const blocks = Array.from( selection.getSelectedBlocks() ); if ( !blocks.length ) { @@ -212,7 +213,11 @@ export default class DocumentListCommand extends Command { } for ( const block of blocks ) { - if ( schema.checkAttribute( block, 'listType' ) ) { + const isEnabled = this._requiredElementName ? + checkCanBeRenamed( block, model.schema, this._requiredElementName ) : + model.schema.checkAttribute( block, 'listType' ); + + if ( isEnabled ) { return true; } } diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts index 9aae8aa99af..2bcc44772f2 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts @@ -73,7 +73,7 @@ import '../../theme/list.css'; /** * A list of base list model attributes. */ -const LIST_BASE_ATTRIBUTES = [ 'listType', 'listIndent', 'listItemId' ]; +const LIST_BASE_ATTRIBUTES = [ 'listType', 'listIndent', 'listItemId', 'todoListChecked' ]; /** * Map of model attributes applicable to list blocks. @@ -424,20 +424,25 @@ export default class DocumentListEditing extends Plugin { model: 'paragraph', view: bogusParagraphCreator( attributeNames ), converterPriority: 'high' + } ) + .add( dispatcher => { + dispatcher.on>( + 'attribute', + listItemDowncastConverter( attributeNames, this._downcastStrategies, model ) + ); } ); + editor.conversion.for( 'dataDowncast' ) .elementToElement( { model: 'paragraph', view: bogusParagraphCreator( attributeNames, { dataPipeline: true } ), converterPriority: 'high' - } ); - - editor.conversion.for( 'downcast' ) + } ) .add( dispatcher => { dispatcher.on>( 'attribute', - listItemDowncastConverter( attributeNames, this._downcastStrategies, model ) + listItemDowncastConverter( attributeNames, this._downcastStrategies, model, { dataPipeline: true } ) ); } ); diff --git a/packages/ckeditor5-list/src/documentlist/utils/postfixers.ts b/packages/ckeditor5-list/src/documentlist/utils/postfixers.ts index ff8d0c5bf51..34ccfb64f3a 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/postfixers.ts +++ b/packages/ckeditor5-list/src/documentlist/utils/postfixers.ts @@ -135,17 +135,6 @@ export function fixListItemIds( seenIds.add( listItemId ); - // Make sure that all items in a to-do list have unique IDs. - if ( listType == 'todo' ) { - if ( node.getAttribute( 'listItemId' ) != listItemId ) { - writer.setAttribute( 'listItemId', listItemId, node ); - - applied = true; - } - - continue; - } - for ( const block of getListItemBlocks( node, { direction: 'forward' } ) ) { visited.add( block ); diff --git a/packages/ckeditor5-list/src/tododocumentlist.ts b/packages/ckeditor5-list/src/tododocumentlist.ts index 129b3d36abd..61bba2867d9 100644 --- a/packages/ckeditor5-list/src/tododocumentlist.ts +++ b/packages/ckeditor5-list/src/tododocumentlist.ts @@ -11,7 +11,7 @@ import TodoDocumentListEditing from './tododocumentlist/tododocumentlistediting' import TodoListUI from './todolist/todolistui'; import { Plugin } from 'ckeditor5/src/core'; -import '../theme/todolist.css'; +import '../theme/tododocumentlist.css'; /** * The to-do list feature. diff --git a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts index af66b00c2ef..31419124224 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts @@ -64,7 +64,7 @@ export default class TodoDocumentListEditing extends Plugin { const editor = this.editor; const model = editor.model; - editor.commands.add( 'todoList', new DocumentListCommand( editor, 'todo', 'paragraph' ) ); + editor.commands.add( 'todoList', new DocumentListCommand( editor, 'todo' ) ); const checkTodoListCommand = new CheckTodoDocumentListCommand( editor ); @@ -106,26 +106,26 @@ export default class TodoDocumentListEditing extends Plugin { ) ); } ); - editor.conversion.for( 'dataDowncast' ).elementToElement( { - model: { - name: 'paragraph', - attributes: 'todoListChecked' - }, - view: todoItemViewCreator( { dataPipeline: true } ), - converterPriority: 'highest' - } ); - - editor.conversion.for( 'editingDowncast' ).elementToElement( { - model: { - name: 'paragraph', - attributes: 'todoListChecked' - }, - view: todoItemViewCreator(), - converterPriority: 'highest' - } ); - - editor.editing.mapper.on( 'modelToViewPosition', mapModelToViewPosition( editor.editing.view ) ); - editor.data.mapper.on( 'modelToViewPosition', mapModelToViewPosition( editor.editing.view ) ); + // editor.conversion.for( 'dataDowncast' ).elementToElement( { + // model: { + // name: 'paragraph', + // attributes: 'todoListChecked' + // }, + // view: todoItemViewCreator( { dataPipeline: true } ), + // converterPriority: 'highest' + // } ); + + // editor.conversion.for( 'editingDowncast' ).elementToElement( { + // model: { + // name: 'paragraph', + // attributes: 'todoListChecked' + // }, + // view: todoItemViewCreator(), + // converterPriority: 'highest' + // } ); + + // editor.editing.mapper.on( 'modelToViewPosition', mapModelToViewPosition( editor.editing.view ) ); + // editor.data.mapper.on( 'modelToViewPosition', mapModelToViewPosition( editor.editing.view ) ); const documentListEditing = editor.plugins.get( DocumentListEditing ); @@ -167,18 +167,18 @@ export default class TodoDocumentListEditing extends Plugin { } ); // Make sure that only paragraphs can be a to-do list item. - this.listenTo( documentListEditing, 'postFixer', ( evt, { listNodes, writer } ) => { - let applied = false; - - for ( const { node } of listNodes ) { - if ( node.getAttribute( 'listType' ) == 'todo' && node.name != 'paragraph' ) { - removeListAttributes( node, writer ); - applied = true; - } - } - - evt.return = applied || evt.return; - } ); + // this.listenTo( documentListEditing, 'postFixer', ( evt, { listNodes, writer } ) => { + // let applied = false; + // + // for ( const { node } of listNodes ) { + // if ( node.getAttribute( 'listType' ) == 'todo' && node.name != 'paragraph' ) { + // removeListAttributes( node, writer ); + // applied = true; + // } + // } + // + // evt.return = applied || evt.return; + // } ); // Jump at the end of the previous node on left arrow key press, when selection is after the checkbox. // @@ -190,12 +190,12 @@ export default class TodoDocumentListEditing extends Plugin { //

    Foo{}

    //
    • Bar
    // - this.listenTo( - editor.editing.view.document, - 'arrowKey', - jumpOverCheckmarkOnSideArrowKeyPress( model, editor.locale ), - { context: 'li' } - ); + // this.listenTo( + // editor.editing.view.document, + // 'arrowKey', + // jumpOverCheckmarkOnSideArrowKeyPress( model, editor.locale ), + // { context: 'li' } + // ); // Toggle check state of selected to-do list items on keystroke. this.listenTo( editor.editing.view.document, 'keydown', ( evt, data ) => { @@ -212,7 +212,7 @@ export default class TodoDocumentListEditing extends Plugin { return; } - const viewElement = editor.editing.mapper.findMappedViewAncestor( editor.editing.view.createPositionBefore( data.target ) ); + const viewElement = editor.editing.mapper.findMappedViewAncestor( editor.editing.view.createPositionAt( data.target.nextSibling!, 0 ) ); const modelElement = editor.editing.mapper.toModelElement( viewElement ); if ( modelElement && isListItemBlock( modelElement ) && modelElement.getAttribute( 'listType' ) == 'todo' ) { @@ -221,6 +221,11 @@ export default class TodoDocumentListEditing extends Plugin { } ); } } ); + + editor.editing.mapper.registerViewToModelLength( 'input', viewElement => { + // TODO verify if this is a to-do list checkbox + return 0; + } ); } } diff --git a/packages/ckeditor5-list/theme/tododocumentlist.css b/packages/ckeditor5-list/theme/tododocumentlist.css new file mode 100644 index 00000000000..dca441e366e --- /dev/null +++ b/packages/ckeditor5-list/theme/tododocumentlist.css @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +:root { + --ck-todo-list-checkmark-size: 16px; +} + +.ck-content .todo-list { + list-style: none; + + & > li { + position: relative; + margin-bottom: 5px; + + & .todo-list { + margin-top: 5px; + } + + & > input { + -webkit-appearance: none; + display: inline-block; + position: absolute; + width: var(--ck-todo-list-checkmark-size); + height: var(--ck-todo-list-checkmark-size); + vertical-align: middle; + + /* Needed on iOS */ + border: 0; + + /* LTR styles */ + left: -25px; + margin-right: -15px; + right: 0; + margin-left: 0; + + &::before { + display: block; + position: absolute; + box-sizing: border-box; + content: ''; + width: 100%; + height: 100%; + border: 1px solid hsl(0, 0%, 20%); + border-radius: 2px; + transition: 250ms ease-in-out box-shadow, 250ms ease-in-out background, 250ms ease-in-out border; + } + + &::after { + display: block; + position: absolute; + box-sizing: content-box; + pointer-events: none; + content: ''; + + /* Calculate tick position, size and border-width proportional to the checkmark size. */ + left: calc(var(--ck-todo-list-checkmark-size) / 3); + top: calc(var(--ck-todo-list-checkmark-size) / 5.3); + width: calc(var(--ck-todo-list-checkmark-size) / 5.3); + height: calc(var(--ck-todo-list-checkmark-size) / 2.6); + border-style: solid; + border-color: transparent; + border-width: 0 calc(var(--ck-todo-list-checkmark-size) / 8) calc(var(--ck-todo-list-checkmark-size) / 8) 0; + transform: rotate(45deg); + } + + &[checked] { + &::before { + background: hsl(126, 64%, 41%); + border-color: hsl(126, 64%, 41%); + } + + &::after { + border-color: hsl(0, 0%, 100%); + } + } + } + } +} + +/* RTL styles */ +[dir="rtl"] .todo-list li > input { + left: 0; + margin-right: 0; + right: -25px; + margin-left: -15px; +} + +/* + * To-do list should be interactive only during the editing + * (https://github.com/ckeditor/ckeditor5/issues/2090). + */ +.ck-editor__editable .todo-list .todo-list__label > input { + cursor: pointer; + + &:hover::before { + box-shadow: 0 0 0 5px hsla(0, 0%, 0%, 0.1); + } +} From 6386b7caa86d2970705e3c899954897dde1e56e3 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 28 Aug 2023 10:26:42 +0200 Subject: [PATCH 09/54] Review fixes. --- .../ckeditor5-list/src/documentlist/documentlistediting.ts | 2 +- packages/ckeditor5-list/src/documentlist/utils/model.ts | 2 +- packages/ckeditor5-list/src/documentlist/utils/view.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts index 2bcc44772f2..9709eb705cc 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts @@ -79,7 +79,7 @@ const LIST_BASE_ATTRIBUTES = [ 'listType', 'listIndent', 'listItemId', 'todoList * Map of model attributes applicable to list blocks. */ export interface ListItemAttributesMap { - listType?: 'numbered' | 'bulleted' | string; + listType?: 'numbered' | 'bulleted' | 'todo'; listIndent?: number; listItemId?: string; } diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.ts b/packages/ckeditor5-list/src/documentlist/utils/model.ts index 2b2910d71d5..9a41a2ae2d3 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.ts +++ b/packages/ckeditor5-list/src/documentlist/utils/model.ts @@ -46,7 +46,7 @@ export class ListItemUid { export interface ListElement extends Element { getAttribute( key: 'listItemId' ): string; getAttribute( key: 'listIndent' ): number; - getAttribute( key: 'listType' ): 'numbered' | 'bulleted' | string; + getAttribute( key: 'listType' ): 'numbered' | 'bulleted' | 'todo'; getAttribute( key: string ): unknown; } diff --git a/packages/ckeditor5-list/src/documentlist/utils/view.ts b/packages/ckeditor5-list/src/documentlist/utils/view.ts index 15d0ed93430..20326aaebee 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/view.ts +++ b/packages/ckeditor5-list/src/documentlist/utils/view.ts @@ -96,7 +96,7 @@ export function getIndent( listItem: ViewElement ): number { export function createListElement( writer: DowncastWriter, indent: number, - type: string, + type: 'bulleted' | 'numbered' | 'todo', id = getViewElementIdForListType( type, indent ) ): ViewAttributeElement { // Negative priorities so that restricted editing attribute won't wrap lists. @@ -128,7 +128,7 @@ export function createListItemElement( * * @internal */ -export function getViewElementNameForListType( type?: string ): 'ol' | 'ul' { +export function getViewElementNameForListType( type?: 'bulleted' | 'numbered' | 'todo' ): 'ol' | 'ul' { return type == 'numbered' ? 'ol' : 'ul'; } @@ -137,6 +137,6 @@ export function getViewElementNameForListType( type?: string ): 'ol' | 'ul' { * * @internal */ -export function getViewElementIdForListType( type?: string, indent?: number ): string { +export function getViewElementIdForListType( type?: 'bulleted' | 'numbered' | 'todo', indent?: number ): string { return `list-${ type }-${ indent }`; } From 2045beadcec4a91109fa4c5b1fb6fa8971128f6d Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 28 Aug 2023 12:11:05 +0200 Subject: [PATCH 10/54] Code cleanup and refactoring. --- .../src/integrations/documentlist.ts | 30 +-- .../src/documentlist/converters.ts | 18 +- .../src/documentlist/documentlistcommand.ts | 32 +-- .../src/documentlist/utils/listwalker.ts | 32 ++- .../src/documentlist/utils/model.ts | 10 - .../documentlistpropertiesediting.ts | 31 +-- .../checktododocumentlistcommand.ts | 7 +- .../tododocumentlistediting.ts | 245 ++++-------------- 8 files changed, 99 insertions(+), 306 deletions(-) diff --git a/packages/ckeditor5-html-support/src/integrations/documentlist.ts b/packages/ckeditor5-html-support/src/integrations/documentlist.ts index 022a4bf7a6b..f570b8e69d7 100644 --- a/packages/ckeditor5-html-support/src/integrations/documentlist.ts +++ b/packages/ckeditor5-html-support/src/integrations/documentlist.ts @@ -107,36 +107,8 @@ export default class DocumentListElementSupport extends Plugin { } ); // Make sure that all items in a single list (items at the same level & listType) have the same properties. - // Note: This is almost an exact copy from DocumentListPropertiesEditing. documentListEditing.on( 'postFixer', ( evt, { listNodes, writer } ) => { - const previousNodesByIndent = []; // Last seen nodes of lower indented lists. - - for ( const { node, previous } of listNodes ) { - // For the first list block there is nothing to compare with. - if ( !previous ) { - continue; - } - - const nodeIndent = node.getAttribute( 'listIndent' ); - const previousNodeIndent = previous.getAttribute( 'listIndent' ); - - let previousNodeInList = null; // It's like `previous` but has the same indent as current node. - - // Let's find previous node for the same indent. - // We're going to need that when we get back to previous indent. - if ( nodeIndent > previousNodeIndent ) { - previousNodesByIndent[ previousNodeIndent ] = previous; - } - // Restore the one for given indent. - else if ( nodeIndent < previousNodeIndent ) { - previousNodeInList = previousNodesByIndent[ nodeIndent ]; - previousNodesByIndent.length = nodeIndent; - } - // Same indent. - else { - previousNodeInList = previous; - } - + for ( const { node, previousNodeInList } of listNodes ) { // This is a first item of a nested list. if ( !previousNodeInList ) { continue; diff --git a/packages/ckeditor5-list/src/documentlist/converters.ts b/packages/ckeditor5-list/src/documentlist/converters.ts index 260c95b4f37..4f790dba770 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.ts +++ b/packages/ckeditor5-list/src/documentlist/converters.ts @@ -73,13 +73,21 @@ export function listItemUpcastConverter(): GetCallback { return; } + const listItemId = ListItemUid.next(); + const listIndent = getIndent( data.viewItem ); + let listType = data.viewItem.parent && data.viewItem.parent.is( 'element', 'ol' ) ? 'numbered' : 'bulleted'; + // Preserve list type if was already set (for example by to-do list feature). - const firstItemListType = items[ 0 ].getAttribute( 'listType' ); + const firstItemListType = items[ 0 ].getAttribute( 'listType' ) as string; + + if ( firstItemListType ) { + listType = firstItemListType; + } const attributes = { - listItemId: ListItemUid.next(), - listIndent: getIndent( data.viewItem ), - listType: firstItemListType || ( data.viewItem.parent && data.viewItem.parent.is( 'element', 'ol' ) ? 'numbered' : 'bulleted' ) + listItemId, + listIndent, + listType }; for ( const item of items ) { @@ -422,8 +430,6 @@ export function findMappedViewElement( element: Element, mapper: Mapper, model: const modelRange = model.createRangeOn( element ); const viewRange = mapper.toViewRange( modelRange ).getTrimmed(); - // TODO trim the custom marker (empty element without mapping and model length = 0). - // return viewRange.getContainedElement(); return viewRange.end.nodeBefore as ViewElement | null; } diff --git a/packages/ckeditor5-list/src/documentlist/documentlistcommand.ts b/packages/ckeditor5-list/src/documentlist/documentlistcommand.ts index 3b199c15663..968d345b0db 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistcommand.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistcommand.ts @@ -19,8 +19,7 @@ import { ListItemUid, sortBlocks, getSelectedBlockObject, - isListItemBlock, - checkCanBeRenamed + isListItemBlock } from './utils/model'; /** @@ -40,22 +39,16 @@ export default class DocumentListCommand extends Command { */ public declare value: boolean; - /** - * TODO - */ - private _requiredElementName?: string; - /** * Creates an instance of the command. * * @param editor The editor instance. * @param type List type that will be handled by this command. */ - constructor( editor: Editor, type: 'numbered' | 'bulleted' | 'todo', requiredElementName?: string ) { + constructor( editor: Editor, type: 'numbered' | 'bulleted' | 'todo' ) { super( editor ); this.type = type; - this._requiredElementName = requiredElementName; } /** @@ -82,10 +75,7 @@ export default class DocumentListCommand extends Command { const selectedBlockObject = getSelectedBlockObject( model ); const blocks = Array.from( document.selection.getSelectedBlocks() ) - .filter( block => this._requiredElementName ? - checkCanBeRenamed( block, model.schema, this._requiredElementName ) : - model.schema.checkAttribute( block, 'listType' ) - ); + .filter( block => model.schema.checkAttribute( block, 'listType' ) ); // Whether we are turning off some items. const turnOff = options.forceValue !== undefined ? !options.forceValue : this.value; @@ -125,16 +115,6 @@ export default class DocumentListCommand extends Command { const changedBlocks = []; for ( const block of blocks ) { - // Rename block to a required element name if type of the list requires it. - if ( - this._requiredElementName && - !block.is( 'element', this._requiredElementName ) && - checkCanBeRenamed( block, model.schema, this._requiredElementName ) - ) { - writer.rename( block, this._requiredElementName ); - changedBlocks.push( block ); - } - // Promote the given block to the list item. if ( !block.hasAttribute( 'listType' ) ) { writer.setAttributes( { @@ -213,11 +193,7 @@ export default class DocumentListCommand extends Command { } for ( const block of blocks ) { - const isEnabled = this._requiredElementName ? - checkCanBeRenamed( block, model.schema, this._requiredElementName ) : - model.schema.checkAttribute( block, 'listType' ); - - if ( isEnabled ) { + if ( model.schema.checkAttribute( block, 'listType' ) ) { return true; } } diff --git a/packages/ckeditor5-list/src/documentlist/utils/listwalker.ts b/packages/ckeditor5-list/src/documentlist/utils/listwalker.ts index 745ff8ad6b0..f9745e83aa6 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/listwalker.ts +++ b/packages/ckeditor5-list/src/documentlist/utils/listwalker.ts @@ -10,7 +10,7 @@ import { first, toArray, type ArrayOrItem } from 'ckeditor5/src/utils'; import { isListItemBlock, type ListElement } from './model'; -import type { DocumentFragment, Element, Node } from 'ckeditor5/src/engine'; +import type { Element, Node } from 'ckeditor5/src/engine'; /** * Document list blocks iterator. @@ -216,10 +216,33 @@ export function* iterateSiblingListBlocks( direction: 'forward' | 'backward' = 'forward' ): IterableIterator { const isForward = direction == 'forward'; + const previousNodesByIndent = []; // Last seen nodes of lower indented lists. let previous = null; while ( isListItemBlock( node ) ) { - yield { node, previous }; + let previousNodeInList = null; // It's like `previous` but has the same indent as current node. + + if ( previous ) { + const nodeIndent = node.getAttribute( 'listIndent' ); + const previousNodeIndent = previous.getAttribute( 'listIndent' ); + + // Let's find previous node for the same indent. + // We're going to need that when we get back to previous indent. + if ( nodeIndent > previousNodeIndent ) { + previousNodesByIndent[ previousNodeIndent ] = previous; + } + // Restore the one for given indent. + else if ( nodeIndent < previousNodeIndent ) { + previousNodeInList = previousNodesByIndent[ nodeIndent ]; + previousNodesByIndent.length = nodeIndent; + } + // Same indent. + else { + previousNodeInList = previous; + } + } + + yield { node, previous, previousNodeInList }; previous = node; node = isForward ? node.nextSibling : node.previousSibling; @@ -267,4 +290,9 @@ export interface ListIteratorValue { * The previous list node. */ previous: ListElement | null; + + /** + * The previous list node at the same indent as current node. + */ + previousNodeInList: ListElement | null; } diff --git a/packages/ckeditor5-list/src/documentlist/utils/model.ts b/packages/ckeditor5-list/src/documentlist/utils/model.ts index 9a41a2ae2d3..3096212aca5 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/model.ts +++ b/packages/ckeditor5-list/src/documentlist/utils/model.ts @@ -546,16 +546,6 @@ export function getSelectedBlockObject( model: Model ): Element | null { return null; } -/** - * Checks whether the given block can be replaced by a paragraph. - * - * @param block A block to be tested. - * @param schema The schema of the document. - */ -export function checkCanBeRenamed( block: Element, schema: Schema, elementName: string ): boolean { - return schema.checkChild( block.parent as Element, elementName ) && !schema.isObject( block ); -} - // 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: ListElement, diff --git a/packages/ckeditor5-list/src/documentlistproperties/documentlistpropertiesediting.ts b/packages/ckeditor5-list/src/documentlistproperties/documentlistpropertiesediting.ts index 8fb2670b142..251fca2d653 100644 --- a/packages/ckeditor5-list/src/documentlistproperties/documentlistpropertiesediting.ts +++ b/packages/ckeditor5-list/src/documentlistproperties/documentlistpropertiesediting.ts @@ -171,35 +171,8 @@ export default class DocumentListPropertiesEditing extends Plugin { } ); // Make sure that all items in a single list (items at the same level & listType) have the same properties. - documentListEditing.on( 'postFixer', ( evt, { listNodes, writer } ) => { - const previousNodesByIndent = []; // Last seen nodes of lower indented lists. - - for ( const { node, previous } of listNodes ) { - // For the first list block there is nothing to compare with. - if ( !previous ) { - continue; - } - - const nodeIndent = node.getAttribute( 'listIndent' ); - const previousNodeIndent = previous.getAttribute( 'listIndent' ); - - let previousNodeInList = null; // It's like `previous` but has the same indent as current node. - - // Let's find previous node for the same indent. - // We're going to need that when we get back to previous indent. - if ( nodeIndent > previousNodeIndent ) { - previousNodesByIndent[ previousNodeIndent ] = previous; - } - // Restore the one for given indent. - else if ( nodeIndent < previousNodeIndent ) { - previousNodeInList = previousNodesByIndent[ nodeIndent ]; - previousNodesByIndent.length = nodeIndent; - } - // Same indent. - else { - previousNodeInList = previous; - } - + documentListEditing.on( 'postFixer', ( evt, { listNodes, writer } ) => { + for ( const { node, previousNodeInList } of listNodes ) { // This is a first item of a nested list. if ( !previousNodeInList ) { continue; diff --git a/packages/ckeditor5-list/src/tododocumentlist/checktododocumentlistcommand.ts b/packages/ckeditor5-list/src/tododocumentlist/checktododocumentlistcommand.ts index aa868aee49f..ec84b30fbf2 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/checktododocumentlistcommand.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/checktododocumentlistcommand.ts @@ -7,12 +7,13 @@ * @module list/tododocumentlist/checktododocumentlistcommand */ -import { Command, type Editor } from 'ckeditor5/src/core'; +import { Command } from 'ckeditor5/src/core'; import type { Element, DocumentSelection, Selection } from 'ckeditor5/src/engine'; +import { getAllListItemBlocks } from '../documentlist/utils/model'; const attributeKey = 'todoListChecked'; @@ -87,12 +88,12 @@ export default class CheckTodoDocumentListCommand extends Command { const elements: Array = []; if ( schema.checkAttribute( startElement, attributeKey ) ) { - elements.push( startElement ); + elements.push( ...getAllListItemBlocks( startElement ) ); } for ( const item of selectionRange.getItems() as Iterable ) { if ( schema.checkAttribute( item, attributeKey ) && !elements.includes( item ) ) { - elements.push( item ); + elements.push( ...getAllListItemBlocks( item ) ); } } diff --git a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts index 31419124224..99e44563495 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts @@ -9,29 +9,21 @@ import { Matcher, - type View, - type ViewElement, - type MapperModelToViewPositionEvent, - type ElementCreatorFunction, type UpcastElementEvent, type Element, type MatcherPattern, - type Model, - type ViewDocumentArrowKeyEvent, type ViewDocumentKeyDownEvent } from 'ckeditor5/src/engine'; import { getCode, - getLocalizedArrowKeyCodeDirection, parseKeystroke, - type Locale, type GetCallback } from 'ckeditor5/src/utils'; import { Plugin } from 'ckeditor5/src/core'; -import { isListItemBlock, removeListAttributes } from '../documentlist/utils/model'; +import { isListItemBlock } from '../documentlist/utils/model'; import DocumentListEditing, { type DocumentListEditingPostFixerEvent } from '../documentlist/documentlistediting'; import DocumentListCommand from '../documentlist/documentlistcommand'; import CheckTodoDocumentListCommand from './checktododocumentlistcommand'; @@ -63,16 +55,12 @@ export default class TodoDocumentListEditing extends Plugin { public init(): void { const editor = this.editor; const model = editor.model; + const editing = editor.editing; editor.commands.add( 'todoList', new DocumentListCommand( editor, 'todo' ) ); + editor.commands.add( 'checkTodoList', new CheckTodoDocumentListCommand( editor ) ); - const checkTodoListCommand = new CheckTodoDocumentListCommand( editor ); - - // Register `checkTodoList` command and add `todoListCheck` command as an alias for backward compatibility. - editor.commands.add( 'checkTodoList', checkTodoListCommand ); - editor.commands.add( 'todoListCheck', checkTodoListCommand ); - - editor.editing.view.addObserver( InputChangeObserver ); + editing.view.addObserver( InputChangeObserver ); model.schema.extend( 'paragraph', { allowAttributes: 'todoListChecked' @@ -106,27 +94,6 @@ export default class TodoDocumentListEditing extends Plugin { ) ); } ); - // editor.conversion.for( 'dataDowncast' ).elementToElement( { - // model: { - // name: 'paragraph', - // attributes: 'todoListChecked' - // }, - // view: todoItemViewCreator( { dataPipeline: true } ), - // converterPriority: 'highest' - // } ); - - // editor.conversion.for( 'editingDowncast' ).elementToElement( { - // model: { - // name: 'paragraph', - // attributes: 'todoListChecked' - // }, - // view: todoItemViewCreator(), - // converterPriority: 'highest' - // } ); - - // editor.editing.mapper.on( 'modelToViewPosition', mapModelToViewPosition( editor.editing.view ) ); - // editor.data.mapper.on( 'modelToViewPosition', mapModelToViewPosition( editor.editing.view ) ); - const documentListEditing = editor.plugins.get( DocumentListEditing ); documentListEditing.registerDowncastStrategy( { @@ -141,6 +108,33 @@ export default class TodoDocumentListEditing extends Plugin { } } ); + // Make sure that all blocks of the same list item have the same todoListChecked. + documentListEditing.on( 'postFixer', ( evt, { listNodes, writer } ) => { + for ( const { node, previousNodeInList } of listNodes ) { + // This is a first item of a nested list. + if ( !previousNodeInList ) { + continue; + } + + if ( previousNodeInList.getAttribute( 'listItemId' ) != node.getAttribute( 'listItemId' ) ) { + continue; + } + + const previousHasAttribute = previousNodeInList.hasAttribute( 'todoListChecked' ); + const nodeHasAttribute = node.hasAttribute( 'todoListChecked' ); + + if ( nodeHasAttribute && !previousHasAttribute ) { + writer.removeAttribute( 'todoListChecked', node ); + evt.return = true; + } + else if ( !nodeHasAttribute && previousHasAttribute ) { + writer.setAttribute( 'todoListChecked', true, node ); + evt.return = true; + } + } + } ); + + // Make sure that todoListChecked attribute is only present for to-do list items. model.document.registerPostFixer( writer => { const changes = model.document.differ.getChanges(); let wasFixed = false; @@ -166,65 +160,41 @@ export default class TodoDocumentListEditing extends Plugin { return wasFixed; } ); - // Make sure that only paragraphs can be a to-do list item. - // this.listenTo( documentListEditing, 'postFixer', ( evt, { listNodes, writer } ) => { - // let applied = false; - // - // for ( const { node } of listNodes ) { - // if ( node.getAttribute( 'listType' ) == 'todo' && node.name != 'paragraph' ) { - // removeListAttributes( node, writer ); - // applied = true; - // } - // } - // - // evt.return = applied || evt.return; - // } ); - - // Jump at the end of the previous node on left arrow key press, when selection is after the checkbox. - // - //

    Foo

    - //
    • {}Bar
    - // - // press: `<-` - // - //

    Foo{}

    - //
    • Bar
    - // - // this.listenTo( - // editor.editing.view.document, - // 'arrowKey', - // jumpOverCheckmarkOnSideArrowKeyPress( model, editor.locale ), - // { context: 'li' } - // ); - // Toggle check state of selected to-do list items on keystroke. - this.listenTo( editor.editing.view.document, 'keydown', ( evt, data ) => { + this.listenTo( editing.view.document, 'keydown', ( evt, data ) => { if ( getCode( data ) === ITEM_TOGGLE_KEYSTROKE ) { editor.execute( 'checkTodoList' ); evt.stop(); } }, { priority: 'high' } ); - this.listenTo( editor.editing.view.document, 'inputChange', ( evt, data ) => { + this.listenTo( editing.view.document, 'inputChange', ( evt, data ) => { const viewTarget = data.target; if ( !viewTarget || !viewTarget.is( 'element', 'input' ) ) { return; } - const viewElement = editor.editing.mapper.findMappedViewAncestor( editor.editing.view.createPositionAt( data.target.nextSibling!, 0 ) ); - const modelElement = editor.editing.mapper.toModelElement( viewElement ); + const viewPositionInNextSibling = editing.view.createPositionAt( viewTarget.nextSibling!, 0 ); + const viewElement = editing.mapper.findMappedViewAncestor( viewPositionInNextSibling ); + const modelElement = editing.mapper.toModelElement( viewElement ); if ( modelElement && isListItemBlock( modelElement ) && modelElement.getAttribute( 'listType' ) == 'todo' ) { editor.execute( 'checkTodoList', { - selection: editor.model.createSelection( modelElement, 'end' ) + selection: model.createSelection( modelElement, 'end' ) } ); } } ); - editor.editing.mapper.registerViewToModelLength( 'input', viewElement => { - // TODO verify if this is a to-do list checkbox - return 0; + editing.mapper.registerViewToModelLength( 'input', viewElement => { + if ( + viewElement.getAttribute( 'type' ) == 'checkbox' && + viewElement.parent!.name == 'li' + ) { + return 0; + } + + return editing.mapper.toModelElement( viewElement ) ? 1 : 0; } ); } } @@ -298,126 +268,3 @@ function attributeUpcastConsumingConverter( matcherPattern: MatcherPattern ): Ge conversionApi.consumable.consume( data.viewItem, match ); }; } - -/** - * TODO - */ -function todoItemViewCreator( { dataPipeline }: { dataPipeline?: boolean } = {} ): ElementCreatorFunction { - return ( modelElement, { writer } ) => { - if ( modelElement.getAttribute( 'listType' ) != 'todo' ) { - return null; - } - - // Using `

    ` in data pipeline in case there are some markers on it and transparentRendering will render it anyway. - const viewElement = dataPipeline ? - writer.createContainerElement( 'p' ) : - writer.createContainerElement( 'span', { class: 'ck-list-bogus-paragraph' } ); - - if ( dataPipeline ) { - writer.setCustomProperty( 'dataPipeline:transparentRendering', true, viewElement ); - } - - const labelWithCheckbox = writer.createContainerElement( 'label', { - class: 'todo-list__label', - ...( !dataPipeline ? { contenteditable: false } : null ) - }, [ - writer.createEmptyElement( 'input', { - type: 'checkbox', - ...( modelElement.getAttribute( 'todoListChecked' ) ? { checked: 'checked' } : null ), - ... ( dataPipeline ? { disabled: 'disabled' } : { tabindex: '-1' } ) - } ) - ] ); - - const descriptionSpan = writer.createContainerElement( 'span', { - class: 'todo-list__label__description' - } ); - - writer.insert( writer.createPositionAt( viewElement, 0 ), labelWithCheckbox ); - - // The `

    Editor

    -
      -
    • - -
    • -
    • - -
    • -
    -

    This is a test for list feature.

    Some more text for testing.

      From 3a0ffeb84a5e7141a65f43c28911c618b7d4f98f Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 28 Aug 2023 18:44:48 +0200 Subject: [PATCH 15/54] Fixed Blink glitch. --- .../src/tododocumentlist/tododocumentlistediting.ts | 2 +- packages/ckeditor5-list/theme/tododocumentlist.css | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts index 1e4b8164282..fce08d66870 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts @@ -124,7 +124,7 @@ export default class TodoDocumentListEditing extends Plugin { ), ... ( dataPipeline ? { disabled: 'disabled' } : - { tabindex: '-1', contenteditable: 'false' } + { tabindex: '-1' } ) } ); } diff --git a/packages/ckeditor5-list/theme/tododocumentlist.css b/packages/ckeditor5-list/theme/tododocumentlist.css index dca441e366e..3e8d5ff9eb7 100644 --- a/packages/ckeditor5-list/theme/tododocumentlist.css +++ b/packages/ckeditor5-list/theme/tododocumentlist.css @@ -80,7 +80,7 @@ } /* RTL styles */ -[dir="rtl"] .todo-list li > input { +[dir="rtl"] .todo-list > li > input { left: 0; margin-right: 0; right: -25px; @@ -91,7 +91,7 @@ * To-do list should be interactive only during the editing * (https://github.com/ckeditor/ckeditor5/issues/2090). */ -.ck-editor__editable .todo-list .todo-list__label > input { +.ck-editor__editable .todo-list > li > input { cursor: pointer; &:hover::before { From f4a7309d967ecb3d4b26157619e17719abbe0b5b Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 28 Aug 2023 19:18:34 +0200 Subject: [PATCH 16/54] Adding tests. --- .../ckeditor5-list/tests/tododocumentlist.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 packages/ckeditor5-list/tests/tododocumentlist.js diff --git a/packages/ckeditor5-list/tests/tododocumentlist.js b/packages/ckeditor5-list/tests/tododocumentlist.js new file mode 100644 index 00000000000..c8a4df15897 --- /dev/null +++ b/packages/ckeditor5-list/tests/tododocumentlist.js @@ -0,0 +1,18 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import TodoDocumentList from '../src/tododocumentlist'; +import TodoDocumentListEditing from '../src/tododocumentlist/tododocumentlistediting'; +import TodoListUI from '../src/todolist/todolistui'; + +describe( 'TodoDocumentList', () => { + it( 'should be named', () => { + expect( TodoDocumentList.pluginName ).to.equal( 'TodoDocumentList' ); + } ); + + it( 'should require TodoDocumentListEditing and TodoListUI', () => { + expect( TodoDocumentList.requires ).to.deep.equal( [ TodoDocumentListEditing, TodoListUI ] ); + } ); +} ); From 717e5bb3b40b442c05d60934f5ff82bb0a25748a Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 29 Aug 2023 19:32:54 +0200 Subject: [PATCH 17/54] First block of to-do list item should be wrapped in description span and label. --- .../src/documentlist/converters.ts | 37 ++++++++++++++++++- .../src/documentlist/documentlistediting.ts | 21 +++++++++++ .../tododocumentlistediting.ts | 35 ++++++++++++++++-- .../ckeditor5-list/theme/tododocumentlist.css | 25 +++++++++++-- 4 files changed, 110 insertions(+), 8 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/converters.ts b/packages/ckeditor5-list/src/documentlist/converters.ts index c68ca4fe91c..835c2619be3 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.ts +++ b/packages/ckeditor5-list/src/documentlist/converters.ts @@ -49,6 +49,7 @@ import { findAndAddListHeadToMap } from './utils/postfixers'; import type { default as DocumentListEditing, DocumentListEditingCheckAttributesEvent, + DocumentListEditingCheckParagraphEvent, ListItemAttributesMap, DowncastStrategy } from './documentlistediting'; @@ -263,6 +264,15 @@ export function reconvertItemsOnDataChange( return false; } + const needsRefresh = documentListEditing.fire( 'checkParagraph', { + modelElement: item, + viewElement + } ); + + if ( needsRefresh ) { + return true; + } + const useBogus = shouldUseBogusParagraph( item, attributeNames, blocks ); if ( useBogus && viewElement.is( 'element', 'p' ) ) { @@ -358,14 +368,26 @@ export function listItemDowncastConverter( // This is for cases when mapping is using inner view element like in the code blocks (pre > code). const viewElement = findMappedViewElement( listItem, mapper, model )!; + // Remove item wrapper. + if ( viewElement.parent!.is( 'attributeElement' ) && viewElement.parent!.getCustomProperty( 'listItemWrapper' ) ) { + writer.unwrap( writer.createRangeIn( viewElement.parent ), viewElement.parent ); + } + // Remove custom item markers. let previousSibling = viewElement.previousSibling; while ( previousSibling ) { - if ( !previousSibling.is( 'element' ) || !previousSibling.getCustomProperty( 'listItemMarker' ) ) { + // Remove item wrapper if was only wrapping a marker. + if ( previousSibling.is( 'attributeElement' ) && previousSibling.getCustomProperty( 'listItemWrapper' ) ) { + writer.unwrap( writer.createRangeIn( previousSibling ), previousSibling ); + previousSibling = viewElement.previousSibling; + } + + if ( !previousSibling || !previousSibling.is( 'element' ) || !previousSibling.getCustomProperty( 'listItemMarker' ) ) { break; } + // Remove marker itself. writer.remove( previousSibling ); previousSibling = viewElement.previousSibling; } @@ -397,6 +419,19 @@ export function listItemDowncastConverter( writer.createPositionBefore( markerElement ), writer.createPositionAfter( viewElement ) ); + + // TODO move to downcast strategy + const wrapper = writer.createAttributeElement( 'label', { class: 'todo-list__label' } ); + + writer.setCustomProperty( 'listItemWrapper', true, wrapper ); + + // TODO move to downcast strategy (part of this?) + if ( viewElement.is( 'element', 'span' ) && viewElement.hasClass( 'todo-list__label__description' ) ) { + viewRange = writer.wrap( viewRange, wrapper ); + } else { + viewRange = writer.wrap( writer.createRangeOn( markerElement ), wrapper ); + viewRange = writer.createRange( viewRange.start, writer.createPositionAfter( viewElement ) ); + } } } } diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts index bffbb8e6be2..d3cc53f5bcf 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts @@ -848,3 +848,24 @@ export type DocumentListEditingCheckAttributesEvent = { } ]; return: boolean; }; + +/** + * TODO + * Event fired on changes detected on the model list element to verify if the view representation of a list element + * is representing those attributes. + * + * It allows triggering a re-wrapping of a list item. + * + * **Note**: For convenience this event is namespaced and could be captured as `checkAttributes:list` or `checkAttributes:item`. + * + * @internal + * @eventName ~DocumentListEditing#checkAttributes + */ +export type DocumentListEditingCheckParagraphEvent = { + name: 'checkParagraph'; + args: [ { + viewElement: ViewElement & { id?: string }; + modelElement: Element; + } ]; + return: boolean; +}; diff --git a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts index fce08d66870..ade52f7c4ab 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts @@ -23,8 +23,11 @@ import { import { Plugin } from 'ckeditor5/src/core'; -import { isListItemBlock } from '../documentlist/utils/model'; -import DocumentListEditing, { type DocumentListEditingPostFixerEvent } from '../documentlist/documentlistediting'; +import { isFirstBlockOfListItem, isListItemBlock } from '../documentlist/utils/model'; +import DocumentListEditing, { + type DocumentListEditingCheckParagraphEvent, + type DocumentListEditingPostFixerEvent +} from '../documentlist/documentlistediting'; import DocumentListCommand from '../documentlist/documentlistcommand'; import CheckTodoDocumentListCommand from './checktododocumentlistcommand'; import InputChangeObserver, { type ViewDocumentInputChangeEvent } from './inputchangeobserver'; @@ -94,6 +97,16 @@ export default class TodoDocumentListEditing extends Plugin { ) ); } ); + editor.conversion.for( 'downcast' ).elementToElement( { + model: 'paragraph', + view: ( element, { writer } ) => { + if ( isFirstBlockOfListItem( element ) && element.getAttribute( 'listType' ) == 'todo' ) { + return writer.createContainerElement( 'span', { class: 'todo-list__label__description' } ); + } + }, + converterPriority: 'highest' + } ); + const documentListEditing = editor.plugins.get( DocumentListEditing ); documentListEditing.registerDowncastStrategy( { @@ -116,7 +129,7 @@ export default class TodoDocumentListEditing extends Plugin { return null; } - return writer.createEmptyElement( 'input', { + const viewElement = writer.createEmptyElement( 'input', { type: 'checkbox', ...( element.getAttribute( 'todoListChecked' ) ? { checked: 'checked' } : @@ -127,6 +140,12 @@ export default class TodoDocumentListEditing extends Plugin { { tabindex: '-1' } ) } ); + + if ( dataPipeline ) { + return viewElement; + } + + return writer.createContainerElement( 'span', { contenteditable: 'false' }, viewElement ); } } ); @@ -139,6 +158,16 @@ export default class TodoDocumentListEditing extends Plugin { } } ); + documentListEditing.on( 'checkParagraph', ( evt, { modelElement, viewElement } ) => { + const hasViewClass = viewElement.hasClass( 'todo-list__label__description' ); + const isModelTodoList = modelElement.getAttribute( 'listType' ) == 'todo'; + + if ( hasViewClass != isModelTodoList ) { + evt.return = true; + evt.stop(); + } + } ); + // Make sure that all blocks of the same list item have the same todoListChecked. documentListEditing.on( 'postFixer', ( evt, { listNodes, writer } ) => { for ( const { node, previousNodeInList } of listNodes ) { diff --git a/packages/ckeditor5-list/theme/tododocumentlist.css b/packages/ckeditor5-list/theme/tododocumentlist.css index 3e8d5ff9eb7..e7568c9a327 100644 --- a/packages/ckeditor5-list/theme/tododocumentlist.css +++ b/packages/ckeditor5-list/theme/tododocumentlist.css @@ -10,7 +10,7 @@ .ck-content .todo-list { list-style: none; - & > li { + & li { position: relative; margin-bottom: 5px; @@ -18,10 +18,16 @@ margin-top: 5px; } - & > input { + & label { + pointer-events: none; + } + + & input { + pointer-events: auto; -webkit-appearance: none; display: inline-block; position: absolute; + /*position: relative;*/ width: var(--ck-todo-list-checkmark-size); height: var(--ck-todo-list-checkmark-size); vertical-align: middle; @@ -76,11 +82,15 @@ } } } + + & .todo-list__label__description { + vertical-align: middle; + } } } /* RTL styles */ -[dir="rtl"] .todo-list > li > input { +[dir="rtl"] .todo-list li input { left: 0; margin-right: 0; right: -25px; @@ -91,10 +101,17 @@ * To-do list should be interactive only during the editing * (https://github.com/ckeditor/ckeditor5/issues/2090). */ -.ck-editor__editable .todo-list > li > input { +.ck-editor__editable .todo-list li input { cursor: pointer; &:hover::before { box-shadow: 0 0 0 5px hsla(0, 0%, 0%, 0.1); } } + +/* + * Workaround for Chrome and Safari glitch... TODO + */ +.ck-editor__editable .todo-list__label { + display: block; +} From 262146254810d0ce7f068e94cb5fb1302a88e13c Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 30 Aug 2023 15:04:43 +0200 Subject: [PATCH 18/54] Review fixes. --- .../checktododocumentlistcommand.ts | 12 +++++------ ...erver.ts => todocheckboxchangeobserver.ts} | 21 ++++++++++++++----- .../tododocumentlistediting.ts | 6 +++--- 3 files changed, 24 insertions(+), 15 deletions(-) rename packages/ckeditor5-list/src/tododocumentlist/{inputchangeobserver.ts => todocheckboxchangeobserver.ts} (64%) diff --git a/packages/ckeditor5-list/src/tododocumentlist/checktododocumentlistcommand.ts b/packages/ckeditor5-list/src/tododocumentlist/checktododocumentlistcommand.ts index 0c2049cf37e..e7e9d6c8a19 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/checktododocumentlistcommand.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/checktododocumentlistcommand.ts @@ -11,8 +11,6 @@ import { Command, type Editor } from 'ckeditor5/src/core'; import type { Element } from 'ckeditor5/src/engine'; import { getAllListItemBlocks } from '../documentlist/utils/model'; -const attributeKey = 'todoListChecked'; - /** * The check to-do command. * @@ -67,9 +65,9 @@ export default class CheckTodoDocumentListCommand extends Command { for ( const element of selectedElements ) { if ( value ) { - writer.setAttribute( attributeKey, true, element ); + writer.setAttribute( 'todoListChecked', true, element ); } else { - writer.removeAttribute( attributeKey, element ); + writer.removeAttribute( 'todoListChecked', element ); } } } ); @@ -79,7 +77,7 @@ export default class CheckTodoDocumentListCommand extends Command { * TODO */ private _getValue( selectedElements: Array ): boolean { - return selectedElements.every( element => !!element.getAttribute( attributeKey ) ); + return selectedElements.every( element => element.getAttribute( 'todoListChecked' ) ); } /** @@ -93,12 +91,12 @@ export default class CheckTodoDocumentListCommand extends Command { const startElement = selectionRange.start.parent as Element; const elements: Array = []; - if ( schema.checkAttribute( startElement, attributeKey ) ) { + if ( schema.checkAttribute( startElement, 'todoListChecked' ) ) { elements.push( ...getAllListItemBlocks( startElement ) ); } for ( const item of selectionRange.getItems( { shallow: true } ) as Iterable ) { - if ( schema.checkAttribute( item, attributeKey ) && !elements.includes( item ) ) { + if ( schema.checkAttribute( item, 'todoListChecked' ) && !elements.includes( item ) ) { elements.push( ...getAllListItemBlocks( item ) ); } } diff --git a/packages/ckeditor5-list/src/tododocumentlist/inputchangeobserver.ts b/packages/ckeditor5-list/src/tododocumentlist/todocheckboxchangeobserver.ts similarity index 64% rename from packages/ckeditor5-list/src/tododocumentlist/inputchangeobserver.ts rename to packages/ckeditor5-list/src/tododocumentlist/todocheckboxchangeobserver.ts index 7d0dd859369..c0019c2cc8f 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/inputchangeobserver.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/todocheckboxchangeobserver.ts @@ -4,7 +4,7 @@ */ /** - * @module list/tododocumentlist/inputchangebserver + * @module list/tododocumentlist/todocheckboxchangeobserver */ import { DomEventObserver, type DomEventData } from 'ckeditor5/src/engine'; @@ -15,7 +15,7 @@ import { DomEventObserver, type DomEventData } from 'ckeditor5/src/engine'; * Note that this observer is not available by default. To make it available it needs to be added to * {@link module:engine/view/view~View} by {@link module:engine/view/view~View#addObserver} method. */ -export default class InputChangeObserver extends DomEventObserver<'change'> { +export default class TodoCheckboxChangeObserver extends DomEventObserver<'change'> { /** * @inheritDoc */ @@ -25,7 +25,18 @@ export default class InputChangeObserver extends DomEventObserver<'change'> { * @inheritDoc */ public onDomEvent( domEvent: Event ): void { - this.fire( 'inputChange', domEvent ); + if ( domEvent.target ) { + const viewTarget = this.view.domConverter.mapDomToView( domEvent.target as HTMLElement ); + + if ( + viewTarget && + viewTarget.is( 'element', 'input' ) && + viewTarget.getAttribute( 'type' ) == 'checkbox' && + viewTarget.findAncestor( { name: 'label', classes: 'todo-list__label' } ) + ) { + this.fire( 'todoCheckboxChange', domEvent ); + } + } } } @@ -41,7 +52,7 @@ export default class InputChangeObserver extends DomEventObserver<'change'> { * @eventName module:engine/view/document~Document#inputchange * @param data The event data. */ -export type ViewDocumentInputChangeEvent = { - name: 'inputChange'; +export type ViewDocumentTodoCheckboxChangeEvent = { + name: 'todoCheckboxChange'; args: [ data: DomEventData ]; }; diff --git a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts index ade52f7c4ab..93acffa6a36 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts @@ -30,7 +30,7 @@ import DocumentListEditing, { } from '../documentlist/documentlistediting'; import DocumentListCommand from '../documentlist/documentlistcommand'; import CheckTodoDocumentListCommand from './checktododocumentlistcommand'; -import InputChangeObserver, { type ViewDocumentInputChangeEvent } from './inputchangeobserver'; +import TodoCheckboxChangeObserver, { type ViewDocumentTodoCheckboxChangeEvent } from './todocheckboxchangeobserver'; const ITEM_TOGGLE_KEYSTROKE = parseKeystroke( 'Ctrl+Enter' ); @@ -63,7 +63,7 @@ export default class TodoDocumentListEditing extends Plugin { editor.commands.add( 'todoList', new DocumentListCommand( editor, 'todo' ) ); editor.commands.add( 'checkTodoList', new CheckTodoDocumentListCommand( editor ) ); - editing.view.addObserver( InputChangeObserver ); + editing.view.addObserver( TodoCheckboxChangeObserver ); model.schema.extend( '$container', { allowAttributes: 'todoListChecked' } ); model.schema.extend( '$block', { allowAttributes: 'todoListChecked' } ); @@ -228,7 +228,7 @@ export default class TodoDocumentListEditing extends Plugin { } }, { priority: 'high' } ); - this.listenTo( editing.view.document, 'inputChange', ( evt, data ) => { + this.listenTo( editing.view.document, 'todoCheckboxChange', ( evt, data ) => { const viewTarget = data.target; if ( !viewTarget || !viewTarget.is( 'element', 'input' ) ) { From a1d530fed3191a2b8be6ddb54f1320ff548f0d27 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 31 Aug 2023 18:48:34 +0200 Subject: [PATCH 19/54] Checkbox should not be allowed between blocks of a single list item. --- .../src/documentlist/converters.ts | 59 ++++++++--------- .../src/documentlist/documentlistediting.ts | 25 +++++-- .../tododocumentlistediting.ts | 65 ++++++++++++++----- 3 files changed, 98 insertions(+), 51 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/converters.ts b/packages/ckeditor5-list/src/documentlist/converters.ts index 835c2619be3..44f24e0b4ab 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.ts +++ b/packages/ckeditor5-list/src/documentlist/converters.ts @@ -49,7 +49,7 @@ import { findAndAddListHeadToMap } from './utils/postfixers'; import type { default as DocumentListEditing, DocumentListEditingCheckAttributesEvent, - DocumentListEditingCheckParagraphEvent, + DocumentListEditingCheckElementEvent, ListItemAttributesMap, DowncastStrategy } from './documentlistediting'; @@ -185,7 +185,7 @@ export function reconvertItemsOnDataChange( findAndAddListHeadToMap( entry.range.start.getShiftedBy( 1 ), itemToListHead ); // Check if paragraph should be converted from bogus to plain paragraph. - if ( doesItemParagraphRequiresRefresh( item ) ) { + if ( doesItemBlockRequiresRefresh( item ) ) { itemsToRefresh.push( item ); } } else { @@ -194,7 +194,7 @@ export function reconvertItemsOnDataChange( } else if ( isListItemBlock( 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 ) ) { + if ( doesItemBlockRequiresRefresh( item ) ) { itemsToRefresh.push( item ); } } @@ -240,7 +240,7 @@ export function reconvertItemsOnDataChange( visited.add( block ); // Check if bogus vs plain paragraph needs refresh. - if ( doesItemParagraphRequiresRefresh( block, blocks ) ) { + if ( doesItemBlockRequiresRefresh( block, blocks ) ) { itemsToRefresh.push( block ); } // Check if wrapping with UL, OL, LIs needs refresh. @@ -253,8 +253,8 @@ export function reconvertItemsOnDataChange( return itemsToRefresh; } - function doesItemParagraphRequiresRefresh( item: Node, blocks?: Array ) { - if ( !item.is( 'element', 'paragraph' ) ) { + function doesItemBlockRequiresRefresh( item: Node, blocks?: Array ) { + if ( !item.is( 'element' ) ) { return false; } @@ -264,7 +264,7 @@ export function reconvertItemsOnDataChange( return false; } - const needsRefresh = documentListEditing.fire( 'checkParagraph', { + const needsRefresh = documentListEditing.fire( 'checkElement', { modelElement: item, viewElement } ); @@ -273,6 +273,10 @@ export function reconvertItemsOnDataChange( return true; } + if ( !item.is( 'element', 'paragraph' ) ) { + return false; + } + const useBogus = shouldUseBogusParagraph( item, attributeNames, blocks ); if ( useBogus && viewElement.is( 'element', 'p' ) ) { @@ -374,22 +378,21 @@ export function listItemDowncastConverter( } // Remove custom item markers. - let previousSibling = viewElement.previousSibling; + const viewWalker = writer.createPositionBefore( viewElement ).getWalker( { direction: 'backward' } ); + const markersToRemove = []; - while ( previousSibling ) { - // Remove item wrapper if was only wrapping a marker. - if ( previousSibling.is( 'attributeElement' ) && previousSibling.getCustomProperty( 'listItemWrapper' ) ) { - writer.unwrap( writer.createRangeIn( previousSibling ), previousSibling ); - previousSibling = viewElement.previousSibling; + for ( const { item } of viewWalker ) { + if ( item.is( 'element' ) && mapper.toModelElement( item ) ) { + break; } - if ( !previousSibling || !previousSibling.is( 'element' ) || !previousSibling.getCustomProperty( 'listItemMarker' ) ) { - break; + if ( item.is( 'element' ) && item.getCustomProperty( 'listItemMarker' ) ) { + markersToRemove.push( item ); } + } - // Remove marker itself. - writer.remove( previousSibling ); - previousSibling = viewElement.previousSibling; + for ( const marker of markersToRemove ) { + writer.remove( marker ); } // Unwrap element from current list wrappers. @@ -400,13 +403,8 @@ export function listItemDowncastConverter( // Insert custom item markers. if ( isFirstBlockOfListItem( listItem ) ) { for ( const strategy of strategies ) { - if ( strategy.scope == 'itemMarker' && listItem.hasAttribute( strategy.attributeName ) ) { - const markerElement = strategy.createElement( - writer, - listItem.getAttribute( strategy.attributeName ), - listItem, - { dataPipeline } - ); + if ( strategy.scope == 'itemMarker' ) { + const markerElement = strategy.createElement( writer, listItem, { dataPipeline } ); if ( !markerElement ) { continue; @@ -420,13 +418,16 @@ export function listItemDowncastConverter( writer.createPositionAfter( viewElement ) ); - // TODO move to downcast strategy - const wrapper = writer.createAttributeElement( 'label', { class: 'todo-list__label' } ); + if ( !strategy.createWrapperElement || !strategy.canWrapElement ) { + continue; + } + + // TODO make this nicer + const wrapper = strategy.createWrapperElement( writer, { dataPipeline } ); writer.setCustomProperty( 'listItemWrapper', true, wrapper ); - // TODO move to downcast strategy (part of this?) - if ( viewElement.is( 'element', 'span' ) && viewElement.hasClass( 'todo-list__label__description' ) ) { + if ( strategy.canWrapElement( listItem ) ) { viewRange = writer.wrap( viewRange, wrapper ); } else { viewRange = writer.wrap( writer.createRangeOn( markerElement ), wrapper ); diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts index d3cc53f5bcf..070c5e76967 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts @@ -23,6 +23,7 @@ import type { UpcastElementEvent, ViewDocumentTabEvent, ViewElement, + ViewAttributeElement, Writer } from 'ckeditor5/src/engine'; @@ -583,10 +584,22 @@ export interface ItemMarkerDowncastStrategy { */ createElement( writer: DowncastWriter, - value: unknown, - element: Element, + modelElement: Element, { dataPipeline }: { dataPipeline?: boolean } ): ViewElement | null; + + /** + * TODO + */ + canWrapElement?( modelElement: Element ): boolean; + + /** + * TODO + */ + createWrapperElement?( + writer: DowncastWriter, + { dataPipeline }: { dataPipeline?: boolean } + ): ViewAttributeElement; } /** @@ -859,12 +872,12 @@ export type DocumentListEditingCheckAttributesEvent = { * **Note**: For convenience this event is namespaced and could be captured as `checkAttributes:list` or `checkAttributes:item`. * * @internal - * @eventName ~DocumentListEditing#checkAttributes + * @eventName ~DocumentListEditing#checkElement */ -export type DocumentListEditingCheckParagraphEvent = { - name: 'checkParagraph'; +export type DocumentListEditingCheckElementEvent = { + name: 'checkElement'; args: [ { - viewElement: ViewElement & { id?: string }; + viewElement: ViewElement; modelElement: Element; } ]; return: boolean; diff --git a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts index 93acffa6a36..cf6b96800cb 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts @@ -25,7 +25,7 @@ import { Plugin } from 'ckeditor5/src/core'; import { isFirstBlockOfListItem, isListItemBlock } from '../documentlist/utils/model'; import DocumentListEditing, { - type DocumentListEditingCheckParagraphEvent, + type DocumentListEditingCheckElementEvent, type DocumentListEditingPostFixerEvent } from '../documentlist/documentlistediting'; import DocumentListCommand from '../documentlist/documentlistcommand'; @@ -69,6 +69,8 @@ export default class TodoDocumentListEditing extends Plugin { model.schema.extend( '$block', { allowAttributes: 'todoListChecked' } ); model.schema.extend( '$blockObject', { allowAttributes: 'todoListChecked' } ); + // TODO fix arrow keys navigation + model.schema.addAttributeCheck( ( context, attributeName ) => { const item = context.last; @@ -122,16 +124,17 @@ export default class TodoDocumentListEditing extends Plugin { } ); documentListEditing.registerDowncastStrategy( { - attributeName: 'listType', + attributeName: 'todoListChecked', scope: 'itemMarker', - createElement( writer, value, element, { dataPipeline } ) { - if ( value != 'todo' ) { + + createElement( writer, modelElement, { dataPipeline } ) { + if ( modelElement.getAttribute( 'listType' ) != 'todo' ) { return null; } const viewElement = writer.createEmptyElement( 'input', { type: 'checkbox', - ...( element.getAttribute( 'todoListChecked' ) ? + ...( modelElement.getAttribute( 'todoListChecked' ) ? { checked: 'checked' } : null ), @@ -146,23 +149,44 @@ export default class TodoDocumentListEditing extends Plugin { } return writer.createContainerElement( 'span', { contenteditable: 'false' }, viewElement ); + }, + + canWrapElement( modelElement ) { + return isDescriptionBlock( modelElement ); + }, + + createWrapperElement( writer ) { + return writer.createAttributeElement( 'label', { class: 'todo-list__label' } ); } } ); - // Just mark the todoListChecked attribute as a list attribute that could trigger conversion. - documentListEditing.registerDowncastStrategy( { - attributeName: 'todoListChecked', - scope: 'itemMarker', - createElement() { - return null; + documentListEditing.on( 'checkElement', ( evt, { modelElement, viewElement } ) => { + const isFirstTodoModelParagraphBlock = isDescriptionBlock( modelElement ); + const hasViewClass = viewElement.hasClass( 'todo-list__label__description' ); + + if ( hasViewClass != isFirstTodoModelParagraphBlock ) { + evt.return = true; + evt.stop(); } } ); - documentListEditing.on( 'checkParagraph', ( evt, { modelElement, viewElement } ) => { - const hasViewClass = viewElement.hasClass( 'todo-list__label__description' ); - const isModelTodoList = modelElement.getAttribute( 'listType' ) == 'todo'; + documentListEditing.on( 'checkElement', ( evt, { modelElement, viewElement } ) => { + const isFirstTodoModelItemBlock = modelElement.getAttribute( 'listType' ) == 'todo' && isFirstBlockOfListItem( modelElement ); - if ( hasViewClass != isModelTodoList ) { + let hasViewItemMarker = false; + const viewWalker = editor.editing.view.createPositionBefore( viewElement ).getWalker( { direction: 'backward' } ); + + for ( const { item } of viewWalker ) { + if ( item.is( 'element' ) && editor.editing.mapper.toModelElement( item ) ) { + break; + } + + if ( item.is( 'element', 'input' ) && item.getAttribute( 'type' ) == 'checkbox' ) { + hasViewItemMarker = true; + } + } + + if ( hasViewItemMarker != isFirstTodoModelItemBlock ) { evt.return = true; evt.stop(); } @@ -247,7 +271,7 @@ export default class TodoDocumentListEditing extends Plugin { editing.mapper.registerViewToModelLength( 'input', viewElement => { if ( viewElement.getAttribute( 'type' ) == 'checkbox' && - viewElement.parent!.name == 'li' + viewElement.findAncestor( { name: 'label', classes: 'todo-list__label' } ) ) { return 0; } @@ -346,3 +370,12 @@ function attributeUpcastConsumingConverter( matcherPattern: MatcherPattern ): Ge conversionApi.consumable.consume( data.viewItem, match ); }; } + +/** + * TODO + */ +function isDescriptionBlock( modelElement: Element ): boolean { + return modelElement.is( 'element', 'paragraph' ) && + modelElement.getAttribute( 'listType' ) == 'todo' && + isFirstBlockOfListItem( modelElement ); +} From 8e83b68f008b94fe04fcdbfa1a2819354dc4a411 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 1 Sep 2023 17:09:06 +0200 Subject: [PATCH 20/54] Code cleaning. --- .../src/documentlist/converters.ts | 158 +++++++++++------- 1 file changed, 97 insertions(+), 61 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/converters.ts b/packages/ckeditor5-list/src/documentlist/converters.ts index 44f24e0b4ab..7c0009460b2 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.ts +++ b/packages/ckeditor5-list/src/documentlist/converters.ts @@ -372,72 +372,16 @@ export function listItemDowncastConverter( // This is for cases when mapping is using inner view element like in the code blocks (pre > code). const viewElement = findMappedViewElement( listItem, mapper, model )!; - // Remove item wrapper. - if ( viewElement.parent!.is( 'attributeElement' ) && viewElement.parent!.getCustomProperty( 'listItemWrapper' ) ) { - writer.unwrap( writer.createRangeIn( viewElement.parent ), viewElement.parent ); - } - - // Remove custom item markers. - const viewWalker = writer.createPositionBefore( viewElement ).getWalker( { direction: 'backward' } ); - const markersToRemove = []; - - for ( const { item } of viewWalker ) { - if ( item.is( 'element' ) && mapper.toModelElement( item ) ) { - break; - } - - if ( item.is( 'element' ) && item.getCustomProperty( 'listItemMarker' ) ) { - markersToRemove.push( item ); - } - } - - for ( const marker of markersToRemove ) { - writer.remove( marker ); - } + // Remove custom item marker. + removeCustomMarkerElements( viewElement, writer, mapper ); // Unwrap element from current list wrappers. unwrapListItemBlock( viewElement, writer ); - let viewRange = writer.createRangeOn( viewElement ); - - // Insert custom item markers. - if ( isFirstBlockOfListItem( listItem ) ) { - for ( const strategy of strategies ) { - if ( strategy.scope == 'itemMarker' ) { - const markerElement = strategy.createElement( writer, listItem, { dataPipeline } ); - - if ( !markerElement ) { - continue; - } - - writer.setCustomProperty( 'listItemMarker', true, markerElement ); - writer.insert( viewRange.start, markerElement ); - - viewRange = writer.createRange( - writer.createPositionBefore( markerElement ), - writer.createPositionAfter( viewElement ) - ); - - if ( !strategy.createWrapperElement || !strategy.canWrapElement ) { - continue; - } + // Insert custom item marker. + const viewRange = insertCustomMarkerElements( listItem, viewElement, strategies, writer, { dataPipeline } ); - // TODO make this nicer - const wrapper = strategy.createWrapperElement( writer, { dataPipeline } ); - - writer.setCustomProperty( 'listItemWrapper', true, wrapper ); - - if ( strategy.canWrapElement( listItem ) ) { - viewRange = writer.wrap( viewRange, wrapper ); - } else { - viewRange = writer.wrap( writer.createRangeOn( markerElement ), wrapper ); - viewRange = writer.createRange( viewRange.start, writer.createPositionAfter( viewElement ) ); - } - } - } - } - - // Then wrap them with the new list wrappers. + // Then wrap them with the new list wrappers (UL, OL, LI). wrapListItemBlock( listItem, viewRange, strategies, writer ); }; } @@ -487,6 +431,98 @@ export function findMappedViewElement( element: Element, mapper: Mapper, model: return viewRange.end.nodeBefore as ViewElement | null; } +/** + * TODO + */ +function removeCustomMarkerElements( viewElement: ViewElement, viewWriter: DowncastWriter, mapper: Mapper ): void { + // Remove item wrapper. + while ( viewElement.parent!.is( 'attributeElement' ) && viewElement.parent!.getCustomProperty( 'listItemWrapper' ) ) { + viewWriter.unwrap( viewWriter.createRangeIn( viewElement.parent ), viewElement.parent ); + } + + // Remove custom item markers. + const viewWalker = viewWriter.createPositionBefore( viewElement ).getWalker( { direction: 'backward' } ); + const markersToRemove = []; + + for ( const { item } of viewWalker ) { + // Walk only over the non-mapped elements between list item blocks. + if ( item.is( 'element' ) && mapper.toModelElement( item ) ) { + break; + } + + if ( item.is( 'element' ) && item.getCustomProperty( 'listItemMarker' ) ) { + markersToRemove.push( item ); + } + } + + for ( const marker of markersToRemove ) { + viewWriter.remove( marker ); + } +} + +/** + * TODO + */ +function insertCustomMarkerElements( + listItem: Element, + viewElement: ViewElement, + strategies: Array, + writer: DowncastWriter, + { dataPipeline }: { dataPipeline?: boolean } +): ViewRange { + let viewRange = writer.createRangeOn( viewElement ); + + // Marker can be inserted only before the first block of a list item. + if ( !isFirstBlockOfListItem( listItem ) ) { + return viewRange; + } + + for ( const strategy of strategies ) { + if ( strategy.scope != 'itemMarker' ) { + continue; + } + + // Create the custom marker element and inject it before the first block of the list item. + const markerElement = strategy.createElement( writer, listItem, { dataPipeline } ); + + if ( !markerElement ) { + continue; + } + + writer.setCustomProperty( 'listItemMarker', true, markerElement ); + writer.insert( viewRange.start, markerElement ); + + viewRange = writer.createRange( + writer.createPositionBefore( markerElement ), + writer.createPositionAfter( viewElement ) + ); + + // Wrap the marker and optionally the first block with an attribute element (label for to-do lists). + if ( !strategy.createWrapperElement || !strategy.canWrapElement ) { + continue; + } + + const wrapper = strategy.createWrapperElement( writer, { dataPipeline } ); + + writer.setCustomProperty( 'listItemWrapper', true, wrapper ); + + // The whole block can be wrapped... + if ( strategy.canWrapElement( listItem ) ) { + viewRange = writer.wrap( viewRange, wrapper ); + } else { + // ... or only the marker element (if the block is downcasted to heading or block widget). + viewRange = writer.wrap( writer.createRangeOn( markerElement ), wrapper ); + + viewRange = writer.createRange( + viewRange.start, + writer.createPositionAfter( viewElement ) + ); + } + } + + return viewRange; +} + // Unwraps all ol, ul, and li attribute elements that are wrapping the provided view element. function unwrapListItemBlock( viewElement: ViewElement, viewWriter: DowncastWriter ) { let attributeElement: ViewElement | ViewDocumentFragment = viewElement.parent!; From 97bec248140f03fc70c16237088bbe5f208d5db9 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 1 Sep 2023 17:32:54 +0200 Subject: [PATCH 21/54] Handle arrow keys before a checkbox. --- .../src/documentlist/converters.ts | 4 + .../tododocumentlistediting.ts | 101 +++++++++++++++--- 2 files changed, 89 insertions(+), 16 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/converters.ts b/packages/ckeditor5-list/src/documentlist/converters.ts index 7c0009460b2..3531b2d25bd 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.ts +++ b/packages/ckeditor5-list/src/documentlist/converters.ts @@ -440,6 +440,8 @@ function removeCustomMarkerElements( viewElement: ViewElement, viewWriter: Downc viewWriter.unwrap( viewWriter.createRangeIn( viewElement.parent ), viewElement.parent ); } + // TODO how to handle this is RTL? + // Remove custom item markers. const viewWalker = viewWriter.createPositionBefore( viewElement ).getWalker( { direction: 'backward' } ); const markersToRemove = []; @@ -489,6 +491,8 @@ function insertCustomMarkerElements( continue; } + // TODO how to handle this is RTL? + writer.setCustomProperty( 'listItemMarker', true, markerElement ); writer.insert( viewRange.start, markerElement ); diff --git a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts index cf6b96800cb..2b09244e41a 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts @@ -10,15 +10,19 @@ import { Matcher, type UpcastElementEvent, + type Model, type Element, type MatcherPattern, - type ViewDocumentKeyDownEvent + type ViewDocumentKeyDownEvent, + type ViewDocumentArrowKeyEvent } from 'ckeditor5/src/engine'; import { getCode, parseKeystroke, - type GetCallback + getLocalizedArrowKeyCodeDirection, + type GetCallback, + type Locale } from 'ckeditor5/src/utils'; import { Plugin } from 'ckeditor5/src/core'; @@ -69,8 +73,6 @@ export default class TodoDocumentListEditing extends Plugin { model.schema.extend( '$block', { allowAttributes: 'todoListChecked' } ); model.schema.extend( '$blockObject', { allowAttributes: 'todoListChecked' } ); - // TODO fix arrow keys navigation - model.schema.addAttributeCheck( ( context, attributeName ) => { const item = context.last; @@ -83,6 +85,18 @@ export default class TodoDocumentListEditing extends Plugin { } } ); + // TODO + editing.mapper.registerViewToModelLength( 'input', viewElement => { + if ( + viewElement.getAttribute( 'type' ) == 'checkbox' && + viewElement.findAncestor( { name: 'label', classes: 'todo-list__label' } ) + ) { + return 0; + } + + return editing.mapper.toModelElement( viewElement ) ? 1 : 0; + } ); + editor.conversion.for( 'upcast' ).add( dispatcher => { // Upcast of to-do list item is based on a checkbox at the beginning of a
    • to keep compatibility with markdown input. dispatcher.on( 'element:input', todoItemInputConverter() ); @@ -112,8 +126,9 @@ export default class TodoDocumentListEditing extends Plugin { const documentListEditing = editor.plugins.get( DocumentListEditing ); documentListEditing.registerDowncastStrategy( { - attributeName: 'listType', scope: 'list', + attributeName: 'listType', + setAttributeOnDowncast( writer, value, element ) { if ( value == 'todo' ) { writer.addClass( 'todo-list', element ); @@ -124,8 +139,8 @@ export default class TodoDocumentListEditing extends Plugin { } ); documentListEditing.registerDowncastStrategy( { - attributeName: 'todoListChecked', scope: 'itemMarker', + attributeName: 'todoListChecked', createElement( writer, modelElement, { dataPipeline } ) { if ( modelElement.getAttribute( 'listType' ) != 'todo' ) { @@ -268,16 +283,22 @@ export default class TodoDocumentListEditing extends Plugin { } } ); - editing.mapper.registerViewToModelLength( 'input', viewElement => { - if ( - viewElement.getAttribute( 'type' ) == 'checkbox' && - viewElement.findAncestor( { name: 'label', classes: 'todo-list__label' } ) - ) { - return 0; - } - - return editing.mapper.toModelElement( viewElement ) ? 1 : 0; - } ); + // Jump at the start of the next node on right arrow key press, when selection is before the checkbox. + // + //

      Foo{}

      + //
      • Bar
      + // + // press: `->` + // + //

      Foo

      + //
      • {}Bar
      + // + this.listenTo( + editing.view.document, + 'arrowKey', + jumpOverCheckmarkOnSideArrowKeyPress( model, editor.locale ), + { context: '$text' } + ); } /** @@ -379,3 +400,51 @@ function isDescriptionBlock( modelElement: Element ): boolean { modelElement.getAttribute( 'listType' ) == 'todo' && isFirstBlockOfListItem( modelElement ); } + +/** + * TODO + * Handles the left/right (LTR/RTL content) arrow key and moves the selection at the end of the previous block element + * if the selection is just after the checkbox element. In other words, it jumps over the checkbox element when + * moving the selection to the left/right (LTR/RTL). + * + * @returns Callback for 'keydown' events. + */ +function jumpOverCheckmarkOnSideArrowKeyPress( model: Model, locale: Locale ): GetCallback { + return ( eventInfo, domEventData ) => { + const direction = getLocalizedArrowKeyCodeDirection( domEventData.keyCode, locale.contentLanguageDirection ); + + if ( direction != 'right' ) { + return; + } + + const schema = model.schema; + const selection = model.document.selection; + + if ( !selection.isCollapsed ) { + return; + } + + const position = selection.getFirstPosition()!; + const parent = position.parent as Element; + + if ( !position.isAtEnd ) { + return; + } + + const newRange = schema.getNearestSelectionRange( model.createPositionAfter( parent ), 'forward' ); + + if ( !newRange ) { + return; + } + + const newRangeParent = newRange.start.parent; + + if ( isListItemBlock( newRangeParent ) && newRangeParent.getAttribute( 'listType' ) == 'todo' ) { + model.change( writer => writer.setSelection( newRange ) ); + + domEventData.preventDefault(); + domEventData.stopPropagation(); + eventInfo.stop(); + } + }; +} From e89e4c61631eb36a0159cdc10a26722d90636f14 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 1 Sep 2023 19:16:18 +0200 Subject: [PATCH 22/54] Added code comments and doclets. --- .../src/documentlist/converters.ts | 16 ++--- .../src/documentlist/documentlistediting.ts | 28 ++++----- .../checktododocumentlistcommand.ts | 8 +-- .../todocheckboxchangeobserver.ts | 13 ++-- .../tododocumentlistediting.ts | 60 ++++++++++--------- 5 files changed, 64 insertions(+), 61 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/converters.ts b/packages/ckeditor5-list/src/documentlist/converters.ts index 3531b2d25bd..c3788c418a3 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.ts +++ b/packages/ckeditor5-list/src/documentlist/converters.ts @@ -432,7 +432,7 @@ export function findMappedViewElement( element: Element, mapper: Mapper, model: } /** - * TODO + * Removes a custom marker elements and item wrappers related to that marker. */ function removeCustomMarkerElements( viewElement: ViewElement, viewWriter: DowncastWriter, mapper: Mapper ): void { // Remove item wrapper. @@ -440,8 +440,6 @@ function removeCustomMarkerElements( viewElement: ViewElement, viewWriter: Downc viewWriter.unwrap( viewWriter.createRangeIn( viewElement.parent ), viewElement.parent ); } - // TODO how to handle this is RTL? - // Remove custom item markers. const viewWalker = viewWriter.createPositionBefore( viewElement ).getWalker( { direction: 'backward' } ); const markersToRemove = []; @@ -463,7 +461,7 @@ function removeCustomMarkerElements( viewElement: ViewElement, viewWriter: Downc } /** - * TODO + * Inserts a custom marker elements and wraps first block of a list item if marker requires it. */ function insertCustomMarkerElements( listItem: Element, @@ -491,8 +489,6 @@ function insertCustomMarkerElements( continue; } - // TODO how to handle this is RTL? - writer.setCustomProperty( 'listItemMarker', true, markerElement ); writer.insert( viewRange.start, markerElement ); @@ -527,7 +523,9 @@ function insertCustomMarkerElements( return viewRange; } -// Unwraps all ol, ul, and li attribute elements that are wrapping the provided view element. +/** + * Unwraps all ol, ul, and li attribute elements that are wrapping the provided view element. + */ function unwrapListItemBlock( viewElement: ViewElement, viewWriter: DowncastWriter ) { let attributeElement: ViewElement | ViewDocumentFragment = viewElement.parent!; @@ -540,7 +538,9 @@ function unwrapListItemBlock( viewElement: ViewElement, viewWriter: DowncastWrit } } -// Wraps the given list item with appropriate attribute elements for ul, ol, and li. +/** + * Wraps the given list item with appropriate attribute elements for ul, ol, and li. + */ function wrapListItemBlock( listItem: ListElement, viewRange: ViewRange, diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts index 070c5e76967..fe62c6f47f1 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts @@ -544,7 +544,7 @@ export default class DocumentListEditing extends Plugin { } /** - * The downcast strategy. + * The attribute to attribute downcast strategy for UL, OL, LI elements. */ export interface AttributeDowncastStrategy { @@ -565,7 +565,7 @@ export interface AttributeDowncastStrategy { } /** - * TODO + * The custom marker downcast strategy. */ export interface ItemMarkerDowncastStrategy { @@ -580,7 +580,7 @@ export interface ItemMarkerDowncastStrategy { attributeName: string; /** - * TODO + * Creates a view element for a custom item marker. */ createElement( writer: DowncastWriter, @@ -589,21 +589,22 @@ export interface ItemMarkerDowncastStrategy { ): ViewElement | null; /** - * TODO - */ - canWrapElement?( modelElement: Element ): boolean; - - /** - * TODO + * Creates an AttributeElement to be used for wrapping a first block of a list item. */ createWrapperElement?( writer: DowncastWriter, { dataPipeline }: { dataPipeline?: boolean } ): ViewAttributeElement; + + /** + * Should return true if the given list block can be wrapped with the wrapper created by `createWrapperElement()` + * or only the marker element should be wrapped. + */ + canWrapElement?( modelElement: Element ): boolean; } /** - * TODO + * The downcast strategy. */ export type DowncastStrategy = AttributeDowncastStrategy | ItemMarkerDowncastStrategy; @@ -863,13 +864,10 @@ export type DocumentListEditingCheckAttributesEvent = { }; /** - * TODO - * Event fired on changes detected on the model list element to verify if the view representation of a list element + * Event fired on changes detected on the model list element to verify if the view representation of a list block element * is representing those attributes. * - * It allows triggering a re-wrapping of a list item. - * - * **Note**: For convenience this event is namespaced and could be captured as `checkAttributes:list` or `checkAttributes:item`. + * It allows triggering a reconversion of a list item block. * * @internal * @eventName ~DocumentListEditing#checkElement diff --git a/packages/ckeditor5-list/src/tododocumentlist/checktododocumentlistcommand.ts b/packages/ckeditor5-list/src/tododocumentlist/checktododocumentlistcommand.ts index e7e9d6c8a19..18c94ac20f3 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/checktododocumentlistcommand.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/checktododocumentlistcommand.ts @@ -14,10 +14,8 @@ import { getAllListItemBlocks } from '../documentlist/utils/model'; /** * The check to-do command. * - * TODO - * - * The command is registered by the {@link module:list/todolist/todolistediting~TodoListEditing} as - * the `checkTodoList` editor command and it is also available via aliased `todoListCheck` name. + * The command is registered by the {@link module:list/tododocumentlist/tododocumentlistediting~TodoDocumentListEditing} as + * the `checkTodoList` editor command. */ export default class CheckTodoDocumentListCommand extends Command { /** @@ -74,7 +72,7 @@ export default class CheckTodoDocumentListCommand extends Command { } /** - * TODO + * Returns a value for the command. */ private _getValue( selectedElements: Array ): boolean { return selectedElements.every( element => element.getAttribute( 'todoListChecked' ) ); diff --git a/packages/ckeditor5-list/src/tododocumentlist/todocheckboxchangeobserver.ts b/packages/ckeditor5-list/src/tododocumentlist/todocheckboxchangeobserver.ts index c0019c2cc8f..fbffa58ed50 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/todocheckboxchangeobserver.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/todocheckboxchangeobserver.ts @@ -10,7 +10,7 @@ import { DomEventObserver, type DomEventData } from 'ckeditor5/src/engine'; /** - * TODO + * Observes all to-do list checkboxes state changes. * * Note that this observer is not available by default. To make it available it needs to be added to * {@link module:engine/view/view~View} by {@link module:engine/view/view~View#addObserver} method. @@ -41,15 +41,16 @@ export default class TodoCheckboxChangeObserver extends DomEventObserver<'change } /** - * Fired when the TODO + * Fired when the to-do list checkbox is toggled. * - * Introduced by TODO + * Introduced by {@link module:list/tododocumentlist/todocheckboxchangeobserver~TodoCheckboxChangeObserver}. * - * Note that this event is not available by default. To make it available, TODO + * Note that this event is not available by default. To make it available, + * {@link module:list/tododocumentlist/todocheckboxchangeobserver~TodoCheckboxChangeObserver} * needs to be added to {@link module:engine/view/view~View} by the {@link module:engine/view/view~View#addObserver} method. * - * @see module:list/tododocumentlist/inputchangebserver~InputChangeObserver - * @eventName module:engine/view/document~Document#inputchange + * @see module:list/tododocumentlist/todocheckboxchangeobserver~TodoCheckboxChangeObserver + * @eventName module:engine/view/document~Document#todoCheckboxChange * @param data The event data. */ export type ViewDocumentTodoCheckboxChangeEvent = { diff --git a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts index 2b09244e41a..a5f45b00a15 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts @@ -39,7 +39,13 @@ import TodoCheckboxChangeObserver, { type ViewDocumentTodoCheckboxChangeEvent } const ITEM_TOGGLE_KEYSTROKE = parseKeystroke( 'Ctrl+Enter' ); /** - * TODO + * The engine of the to-do list feature. It handles creating, editing and removing to-do lists and their items. + * + * It registers the entire functionality of the {@link module:list/documentlist/documentlistediting~DocumentListEditing list editing plugin} + * and extends it with the commands: + * + * - `'todoList'`, + * - `'checkTodoList'`, */ export default class TodoDocumentListEditing extends Plugin { /** @@ -63,6 +69,7 @@ export default class TodoDocumentListEditing extends Plugin { const editor = this.editor; const model = editor.model; const editing = editor.editing; + const documentListEditing = editor.plugins.get( DocumentListEditing ); editor.commands.add( 'todoList', new DocumentListCommand( editor, 'todo' ) ); editor.commands.add( 'checkTodoList', new CheckTodoDocumentListCommand( editor ) ); @@ -85,18 +92,6 @@ export default class TodoDocumentListEditing extends Plugin { } } ); - // TODO - editing.mapper.registerViewToModelLength( 'input', viewElement => { - if ( - viewElement.getAttribute( 'type' ) == 'checkbox' && - viewElement.findAncestor( { name: 'label', classes: 'todo-list__label' } ) - ) { - return 0; - } - - return editing.mapper.toModelElement( viewElement ) ? 1 : 0; - } ); - editor.conversion.for( 'upcast' ).add( dispatcher => { // Upcast of to-do list item is based on a checkbox at the beginning of a
    • to keep compatibility with markdown input. dispatcher.on( 'element:input', todoItemInputConverter() ); @@ -116,15 +111,13 @@ export default class TodoDocumentListEditing extends Plugin { editor.conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: ( element, { writer } ) => { - if ( isFirstBlockOfListItem( element ) && element.getAttribute( 'listType' ) == 'todo' ) { + if ( isDescriptionBlock( element ) ) { return writer.createContainerElement( 'span', { class: 'todo-list__label__description' } ); } }, converterPriority: 'highest' } ); - const documentListEditing = editor.plugins.get( DocumentListEditing ); - documentListEditing.registerDowncastStrategy( { scope: 'list', attributeName: 'listType', @@ -175,6 +168,21 @@ export default class TodoDocumentListEditing extends Plugin { } } ); + // We need to register the model length callback for the view checkbox input because it has no mapped model element. + // The to-do list item checkbox does not use the UIElement because it would be trimmed by ViewRange#getTrimmed() + // and removing the default remove converter would not include checkbox in the range to remove. + editing.mapper.registerViewToModelLength( 'input', viewElement => { + if ( + viewElement.getAttribute( 'type' ) == 'checkbox' && + viewElement.findAncestor( { name: 'label', classes: 'todo-list__label' } ) + ) { + return 0; + } + + return editing.mapper.toModelElement( viewElement ) ? 1 : 0; + } ); + + // Verifies if a to-do list block requires reconversion of a first item downcasted as an item description. documentListEditing.on( 'checkElement', ( evt, { modelElement, viewElement } ) => { const isFirstTodoModelParagraphBlock = isDescriptionBlock( modelElement ); const hasViewClass = viewElement.hasClass( 'todo-list__label__description' ); @@ -185,6 +193,8 @@ export default class TodoDocumentListEditing extends Plugin { } } ); + // Verifies if a to-do list block requires reconversion of a checkbox element + // (for example there is a new paragraph inserted as a first block of a list item). documentListEditing.on( 'checkElement', ( evt, { modelElement, viewElement } ) => { const isFirstTodoModelItemBlock = modelElement.getAttribute( 'listType' ) == 'todo' && isFirstBlockOfListItem( modelElement ); @@ -207,7 +217,7 @@ export default class TodoDocumentListEditing extends Plugin { } } ); - // Make sure that all blocks of the same list item have the same todoListChecked. + // Make sure that all blocks of the same list item have the same todoListChecked attribute. documentListEditing.on( 'postFixer', ( evt, { listNodes, writer } ) => { for ( const { node, previousNodeInList } of listNodes ) { // This is a first item of a nested list. @@ -267,6 +277,7 @@ export default class TodoDocumentListEditing extends Plugin { } }, { priority: 'high' } ); + // Toggle check state of a to-do list item clicked on the checkbox. this.listenTo( editing.view.document, 'todoCheckboxChange', ( evt, data ) => { const viewTarget = data.target; @@ -323,7 +334,7 @@ export default class TodoDocumentListEditing extends Plugin { } /** - * TODO + * Returns an upcast converter that detects a to-do list checkbox and marks the list item as a to-do list. */ function todoItemInputConverter(): GetCallback { return ( evt, data, conversionApi ) => { @@ -352,7 +363,7 @@ function todoItemInputConverter(): GetCallback { } /** - * TODO + * Returns an upcast converter that consumes element matching the given matcher pattern. */ function elementUpcastConsumingConverter( matcherPattern: MatcherPattern ): GetCallback { const matcher = new Matcher( matcherPattern ); @@ -373,7 +384,7 @@ function elementUpcastConsumingConverter( matcherPattern: MatcherPattern ): GetC } /** - * TODO + * Returns an upcast converter that consumes attributes matching the given matcher pattern. */ function attributeUpcastConsumingConverter( matcherPattern: MatcherPattern ): GetCallback { const matcher = new Matcher( matcherPattern ); @@ -393,7 +404,7 @@ function attributeUpcastConsumingConverter( matcherPattern: MatcherPattern ): Ge } /** - * TODO + * Returns true if the given list item block should be converted as a description block of a to-do list item. */ function isDescriptionBlock( modelElement: Element ): boolean { return modelElement.is( 'element', 'paragraph' ) && @@ -402,12 +413,7 @@ function isDescriptionBlock( modelElement: Element ): boolean { } /** - * TODO - * Handles the left/right (LTR/RTL content) arrow key and moves the selection at the end of the previous block element - * if the selection is just after the checkbox element. In other words, it jumps over the checkbox element when - * moving the selection to the left/right (LTR/RTL). - * - * @returns Callback for 'keydown' events. + * Jump at the start of the next node on right arrow key press, when selection is before the checkbox. */ function jumpOverCheckmarkOnSideArrowKeyPress( model: Model, locale: Locale ): GetCallback { return ( eventInfo, domEventData ) => { From 05289c22e57a7974e17396cfae2c2aa21c4cefc7 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 4 Sep 2023 11:07:33 +0200 Subject: [PATCH 23/54] Updated manual tests. --- .../tests/manual/todo-documentlist-rtl.html | 35 +++++++ .../tests/manual/todo-documentlist-rtl.js | 79 ++++++++++++++++ .../tests/manual/todo-documentlist-rtl.md | 0 .../tests/manual/todo-documentlist.js | 8 +- .../tests/manual/todo-documentlist.md | 92 ------------------- 5 files changed, 118 insertions(+), 96 deletions(-) create mode 100644 packages/ckeditor5-list/tests/manual/todo-documentlist-rtl.html create mode 100644 packages/ckeditor5-list/tests/manual/todo-documentlist-rtl.js create mode 100644 packages/ckeditor5-list/tests/manual/todo-documentlist-rtl.md diff --git a/packages/ckeditor5-list/tests/manual/todo-documentlist-rtl.html b/packages/ckeditor5-list/tests/manual/todo-documentlist-rtl.html new file mode 100644 index 00000000000..303a2b50a1c --- /dev/null +++ b/packages/ckeditor5-list/tests/manual/todo-documentlist-rtl.html @@ -0,0 +1,35 @@ +

      Editor

      +
      +

      مرحبا

      +
        +
      • مرحبا
      • +
      • مرحبا
      • +
      • مرحبا
      • +
      • مرحبا
      • +
      • مرحبا
      • +
      • مرحبا
      • +
      • مرحبا
      • +
      • مرحبا
      • +
      +

      مرحبا.

      +

      مرحبا

      +
        +
      1. مرحبا
      2. +
      +
        +
      • مرحبا
      • +
      +
        +
      • مرحبا
      • +
      +
      + +

      Editor content preview

      +
      + + diff --git a/packages/ckeditor5-list/tests/manual/todo-documentlist-rtl.js b/packages/ckeditor5-list/tests/manual/todo-documentlist-rtl.js new file mode 100644 index 00000000000..84c0d516386 --- /dev/null +++ b/packages/ckeditor5-list/tests/manual/todo-documentlist-rtl.js @@ -0,0 +1,79 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. 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'; + +import { Essentials } from '@ckeditor/ckeditor5-essentials'; +import { Autoformat } from '@ckeditor/ckeditor5-autoformat'; +import { BlockQuote } from '@ckeditor/ckeditor5-block-quote'; +import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles'; +import { Heading } from '@ckeditor/ckeditor5-heading'; +import { Link } from '@ckeditor/ckeditor5-link'; +import { MediaEmbed } from '@ckeditor/ckeditor5-media-embed'; +import { Paragraph } from '@ckeditor/ckeditor5-paragraph'; +import { Table, TableToolbar } from '@ckeditor/ckeditor5-table'; +import { FontSize } from '@ckeditor/ckeditor5-font'; +import { Indent } from '@ckeditor/ckeditor5-indent'; +import { SourceEditing } from '@ckeditor/ckeditor5-source-editing'; +import { Alignment } from '@ckeditor/ckeditor5-alignment'; + +import Documentlist from '../../src/documentlist'; +import TodoDocumentlist from '../../src/tododocumentlist'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ + Essentials, + Autoformat, + BlockQuote, + Bold, + Heading, + Italic, + Link, + MediaEmbed, + Paragraph, + Table, + TableToolbar, + FontSize, + Indent, + Documentlist, + TodoDocumentlist, + SourceEditing, + Alignment + ], + language: 'ar', + toolbar: [ + 'heading', + '|', + 'bulletedList', 'numberedList', 'todoList', 'outdent', 'indent', + '|', + 'bold', 'link', 'insertTable', 'fontSize', 'alignment', + '|', + 'undo', 'redo', '|', 'sourceEditing' + ], + table: { + contentToolbar: [ + 'tableColumn', + 'tableRow', + 'mergeTableCells' + ] + } + } ) + .then( editor => { + window.editor = editor; + + const contentPreviewBox = document.getElementById( 'preview' ); + + contentPreviewBox.innerHTML = editor.getData(); + + editor.model.document.on( 'change:data', () => { + contentPreviewBox.innerHTML = editor.getData(); + } ); + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-list/tests/manual/todo-documentlist-rtl.md b/packages/ckeditor5-list/tests/manual/todo-documentlist-rtl.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/ckeditor5-list/tests/manual/todo-documentlist.js b/packages/ckeditor5-list/tests/manual/todo-documentlist.js index f87e888f0e0..341238ccc66 100644 --- a/packages/ckeditor5-list/tests/manual/todo-documentlist.js +++ b/packages/ckeditor5-list/tests/manual/todo-documentlist.js @@ -5,7 +5,7 @@ /* globals console, window, document */ -import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic'; import { Essentials } from '@ckeditor/ckeditor5-essentials'; import { Autoformat } from '@ckeditor/ckeditor5-autoformat'; @@ -20,10 +20,10 @@ import { FontSize } from '@ckeditor/ckeditor5-font'; import { Indent } from '@ckeditor/ckeditor5-indent'; import { SourceEditing } from '@ckeditor/ckeditor5-source-editing'; import { GeneralHtmlSupport } from '@ckeditor/ckeditor5-html-support'; +import { Alignment } from '@ckeditor/ckeditor5-alignment'; -import DocumentList from '../../src/documentlist'; +import Documentlist from '../../src/documentlist'; import TodoDocumentlist from '../../src/tododocumentlist'; -import { Alignment } from '@ckeditor/ckeditor5-alignment'; ClassicEditor .create( document.querySelector( '#editor' ), { @@ -41,7 +41,7 @@ ClassicEditor TableToolbar, FontSize, Indent, - DocumentList, + Documentlist, TodoDocumentlist, SourceEditing, GeneralHtmlSupport, diff --git a/packages/ckeditor5-list/tests/manual/todo-documentlist.md b/packages/ckeditor5-list/tests/manual/todo-documentlist.md index 84b93d007fc..e69de29bb2d 100644 --- a/packages/ckeditor5-list/tests/manual/todo-documentlist.md +++ b/packages/ckeditor5-list/tests/manual/todo-documentlist.md @@ -1,92 +0,0 @@ -## Loading - -1. The data should be loaded with: - * two paragraphs, - * to-do list with eight items, where 2,4 and 7 are checked, - * two paragraphs, - * numbered list with one item, - * to-do list with one unchecked item, - * bullet list with one item. -2. Toolbar should have three buttons: for bullet, numbered and to-do list. - -## Testing - -### Creating: - -1. Convert first paragraph to to-do list item -2. Create empty paragraph and convert to to-do list item -3. Press `Enter` in the middle of item -4. Press `Enter` at the start of item -5. Press `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 todo to numbered for checked and unchecked list item -3. Do it for multiple items at once - -### Merging: - -1. Convert paragraph before to-do list to same type of list -2. Convert paragraph after to-do list to same type of list -3. Convert paragraph before to-do list to different type of list -4. Convert paragraph after to-do list to different type of list -5. Convert first paragraph to to-do list, then convert second paragraph to to-do list -6. Convert multiple items and paragraphs at once - -### Toggling check state: - -1. Put selection in the middle of unchecked the to-do list item -2. Check list item (selection should not move) - ---- - -1. Select multiple to-do list items -2. Check or uncheck to-do list item (selection should not move) - ---- - -1. Check to-do list item -2. Convert checked list item to other list item -3. Convert this list item once again to to-do list item ()should be unchecked) - ---- - -1. Put collapsed selection to to-do list item -2. Press `Ctrl+Space` (check state should toggle) - -### Toggling check state for multiple items: - -1. Select two unchecked list items -2. Press `Ctrl+Space` (both should be checked) -3. Press `Ctrl+Space` once again (both should be unchecked) - ---- - -1. Select checked and unchecked list item -2. Press `Ctrl+Space` (both should be checked) - ---- - -1. Select the entire content -2. Press `Ctrl+Space` (all to-do list items should be checked) -3. Press `Ctrl+Space` once again (all to-do list items should be unchecked) - -### Integration with attribute elements: - -1. Select multiple to-do list items -2. Highlight selected text -3. Check or uncheck highlighted to-do list item -4. Type inside highlighted to-do list item - -### Content styles - -1. Inspect list styles in the editor and in the content preview (below). -2. There should be no major visual difference between them. -3. Check marks in the content preview should be rich custom components (no native checkboxes). From 0cb7df63274eaf6ec43c50405d0d14c2dfbaef2a Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 5 Sep 2023 16:21:56 +0200 Subject: [PATCH 24/54] WiP css. --- .../ckeditor5-list/src/tododocumentlist.ts | 2 +- .../ckeditor5-list/theme/tododocumentlist.css | 20 ++- packages/ckeditor5-list/theme/todolist.css | 157 +++++++++++------- 3 files changed, 112 insertions(+), 67 deletions(-) diff --git a/packages/ckeditor5-list/src/tododocumentlist.ts b/packages/ckeditor5-list/src/tododocumentlist.ts index 61bba2867d9..129b3d36abd 100644 --- a/packages/ckeditor5-list/src/tododocumentlist.ts +++ b/packages/ckeditor5-list/src/tododocumentlist.ts @@ -11,7 +11,7 @@ import TodoDocumentListEditing from './tododocumentlist/tododocumentlistediting' import TodoListUI from './todolist/todolistui'; import { Plugin } from 'ckeditor5/src/core'; -import '../theme/tododocumentlist.css'; +import '../theme/todolist.css'; /** * The to-do list feature. diff --git a/packages/ckeditor5-list/theme/tododocumentlist.css b/packages/ckeditor5-list/theme/tododocumentlist.css index e7568c9a327..af9ddfcea58 100644 --- a/packages/ckeditor5-list/theme/tododocumentlist.css +++ b/packages/ckeditor5-list/theme/tododocumentlist.css @@ -2,6 +2,7 @@ * Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ +/* :root { --ck-todo-list-checkmark-size: 16px; @@ -27,15 +28,15 @@ -webkit-appearance: none; display: inline-block; position: absolute; - /*position: relative;*/ + !*position: relative;*! width: var(--ck-todo-list-checkmark-size); height: var(--ck-todo-list-checkmark-size); vertical-align: middle; - /* Needed on iOS */ + !* Needed on iOS *! border: 0; - /* LTR styles */ + !* LTR styles *! left: -25px; margin-right: -15px; right: 0; @@ -60,7 +61,7 @@ pointer-events: none; content: ''; - /* Calculate tick position, size and border-width proportional to the checkmark size. */ + !* Calculate tick position, size and border-width proportional to the checkmark size. *! left: calc(var(--ck-todo-list-checkmark-size) / 3); top: calc(var(--ck-todo-list-checkmark-size) / 5.3); width: calc(var(--ck-todo-list-checkmark-size) / 5.3); @@ -89,7 +90,7 @@ } } -/* RTL styles */ +!* RTL styles *! [dir="rtl"] .todo-list li input { left: 0; margin-right: 0; @@ -97,10 +98,10 @@ margin-left: -15px; } -/* +!* * To-do list should be interactive only during the editing * (https://github.com/ckeditor/ckeditor5/issues/2090). - */ + *! .ck-editor__editable .todo-list li input { cursor: pointer; @@ -109,9 +110,10 @@ } } -/* +!* * Workaround for Chrome and Safari glitch... TODO - */ + *! .ck-editor__editable .todo-list__label { display: block; } +*/ diff --git a/packages/ckeditor5-list/theme/todolist.css b/packages/ckeditor5-list/theme/todolist.css index 6723fc1aae2..abedce492cb 100644 --- a/packages/ckeditor5-list/theme/todolist.css +++ b/packages/ckeditor5-list/theme/todolist.css @@ -7,6 +7,91 @@ --ck-todo-list-checkmark-size: 16px; } +@define-mixin todo-list-checkbox { + pointer-events: auto; + + -webkit-appearance: none; + display: inline-block; + position: absolute; + width: var(--ck-todo-list-checkmark-size); + height: var(--ck-todo-list-checkmark-size); + vertical-align: middle; + + /* Needed on iOS */ + border: 0; + + /* LTR styles */ + left: -25px; + margin-right: -15px; + right: 0; + margin-left: 0; + + &::before { + display: block; + position: absolute; + box-sizing: border-box; + content: ''; + width: 100%; + height: 100%; + border: 1px solid hsl(0, 0%, 20%); + border-radius: 2px; + transition: 250ms ease-in-out box-shadow, 250ms ease-in-out background, 250ms ease-in-out border; + } + + &::after { + display: block; + position: absolute; + box-sizing: content-box; + pointer-events: none; + content: ''; + + /* Calculate tick position, size and border-width proportional to the checkmark size. */ + left: calc( var(--ck-todo-list-checkmark-size) / 3 ); + top: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); + width: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); + height: calc( var(--ck-todo-list-checkmark-size) / 2.6 ); + border-style: solid; + border-color: transparent; + border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0; + transform: rotate(45deg); + } + + &[checked] { + &::before { + background: hsl(126, 64%, 41%); + border-color: hsl(126, 64%, 41%); + } + + &::after { + border-color: hsl(0, 0%, 100%); + } + } +} + +/* + * Document Lists - editing view has an additional span around checkbox. + */ +.ck-editor__editable .todo-list .todo-list__label > span[contenteditable=false] > input { + @mixin todo-list-checkbox; +} + +/* + * Workaround for Chrome and Safari glitch... TODO + */ +.ck-editor__editable .todo-list__label:has( > span[contenteditable=false] ) { + display: block; +} + +/* TODO does not work in Firefox */ +.ck-editor__editable li:has( .todo-list__label + .todo-list__label__description ) { + & input[type=checkbox] { + position: relative; + } +} + +/* + * TODO + */ .ck-content .todo-list { list-style: none; @@ -19,68 +104,23 @@ } & .todo-list__label { - & > input { - -webkit-appearance: none; - display: inline-block; - position: relative; - width: var(--ck-todo-list-checkmark-size); - height: var(--ck-todo-list-checkmark-size); - vertical-align: middle; - - /* Needed on iOS */ - border: 0; - - /* LTR styles */ - left: -25px; - margin-right: -15px; - right: 0; - margin-left: 0; - - &::before { - display: block; - position: absolute; - box-sizing: border-box; - content: ''; - width: 100%; - height: 100%; - border: 1px solid hsl(0, 0%, 20%); - border-radius: 2px; - transition: 250ms ease-in-out box-shadow, 250ms ease-in-out background, 250ms ease-in-out border; - } + pointer-events: none; + position: relative; - &::after { - display: block; - position: absolute; - box-sizing: content-box; - pointer-events: none; - content: ''; - - /* Calculate tick position, size and border-width proportional to the checkmark size. */ - left: calc( var(--ck-todo-list-checkmark-size) / 3 ); - top: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); - width: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); - height: calc( var(--ck-todo-list-checkmark-size) / 2.6 ); - border-style: solid; - border-color: transparent; - border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0; - transform: rotate(45deg); - } - - &[checked] { - &::before { - background: hsl(126, 64%, 41%); - border-color: hsl(126, 64%, 41%); - } - - &::after { - border-color: hsl(0, 0%, 100%); - } - } + & > input { + @mixin todo-list-checkbox; } & .todo-list__label__description { vertical-align: middle; } + + /* TODO does not work in Firefox */ + &:has( .todo-list__label__description ) { + & input[type=checkbox] { + position: relative; + } + } } } @@ -96,10 +136,13 @@ * To-do list should be interactive only during the editing * (https://github.com/ckeditor/ckeditor5/issues/2090). */ -.ck-editor__editable .todo-list .todo-list__label > input { +.ck-editor__editable .todo-list .todo-list__label > input, +.ck-editor__editable .todo-list .todo-list__label > span[contenteditable=false] > input { cursor: pointer; &:hover::before { box-shadow: 0 0 0 5px hsla(0, 0%, 0%, 0.1); } } + +.ck-content { line-height: 5em } From 2fabd69eeb716e9e6795b58faf290a74fab75a3c Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 6 Sep 2023 16:36:13 +0200 Subject: [PATCH 25/54] Unified to-do list styles. --- .../src/documentlist/converters.ts | 2 +- .../src/documentlist/documentlistediting.ts | 1 + .../tododocumentlistediting.ts | 12 +- .../ckeditor5-list/theme/tododocumentlist.css | 119 ------------------ packages/ckeditor5-list/theme/todolist.css | 99 ++++++++------- 5 files changed, 63 insertions(+), 170 deletions(-) delete mode 100644 packages/ckeditor5-list/theme/tododocumentlist.css diff --git a/packages/ckeditor5-list/src/documentlist/converters.ts b/packages/ckeditor5-list/src/documentlist/converters.ts index c3788c418a3..1992d1746b6 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.ts +++ b/packages/ckeditor5-list/src/documentlist/converters.ts @@ -502,7 +502,7 @@ function insertCustomMarkerElements( continue; } - const wrapper = strategy.createWrapperElement( writer, { dataPipeline } ); + const wrapper = strategy.createWrapperElement( writer, listItem, { dataPipeline } ); writer.setCustomProperty( 'listItemWrapper', true, wrapper ); diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts index fe62c6f47f1..1d4538913b1 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts @@ -593,6 +593,7 @@ export interface ItemMarkerDowncastStrategy { */ createWrapperElement?( writer: DowncastWriter, + modelElement: Element, { dataPipeline }: { dataPipeline?: boolean } ): ViewAttributeElement; diff --git a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts index a5f45b00a15..a5f57ea4e52 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts @@ -163,8 +163,16 @@ export default class TodoDocumentListEditing extends Plugin { return isDescriptionBlock( modelElement ); }, - createWrapperElement( writer ) { - return writer.createAttributeElement( 'label', { class: 'todo-list__label' } ); + createWrapperElement( writer, modelElement ) { + const classes = [ 'todo-list__label' ]; + + if ( !isDescriptionBlock( modelElement ) ) { + classes.push( 'todo-list__label_without-description' ); + } + + return writer.createAttributeElement( 'label', { + class: classes.join( ' ' ) + } ); } } ); diff --git a/packages/ckeditor5-list/theme/tododocumentlist.css b/packages/ckeditor5-list/theme/tododocumentlist.css deleted file mode 100644 index af9ddfcea58..00000000000 --- a/packages/ckeditor5-list/theme/tododocumentlist.css +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ -/* - -:root { - --ck-todo-list-checkmark-size: 16px; -} - -.ck-content .todo-list { - list-style: none; - - & li { - position: relative; - margin-bottom: 5px; - - & .todo-list { - margin-top: 5px; - } - - & label { - pointer-events: none; - } - - & input { - pointer-events: auto; - -webkit-appearance: none; - display: inline-block; - position: absolute; - !*position: relative;*! - width: var(--ck-todo-list-checkmark-size); - height: var(--ck-todo-list-checkmark-size); - vertical-align: middle; - - !* Needed on iOS *! - border: 0; - - !* LTR styles *! - left: -25px; - margin-right: -15px; - right: 0; - margin-left: 0; - - &::before { - display: block; - position: absolute; - box-sizing: border-box; - content: ''; - width: 100%; - height: 100%; - border: 1px solid hsl(0, 0%, 20%); - border-radius: 2px; - transition: 250ms ease-in-out box-shadow, 250ms ease-in-out background, 250ms ease-in-out border; - } - - &::after { - display: block; - position: absolute; - box-sizing: content-box; - pointer-events: none; - content: ''; - - !* Calculate tick position, size and border-width proportional to the checkmark size. *! - left: calc(var(--ck-todo-list-checkmark-size) / 3); - top: calc(var(--ck-todo-list-checkmark-size) / 5.3); - width: calc(var(--ck-todo-list-checkmark-size) / 5.3); - height: calc(var(--ck-todo-list-checkmark-size) / 2.6); - border-style: solid; - border-color: transparent; - border-width: 0 calc(var(--ck-todo-list-checkmark-size) / 8) calc(var(--ck-todo-list-checkmark-size) / 8) 0; - transform: rotate(45deg); - } - - &[checked] { - &::before { - background: hsl(126, 64%, 41%); - border-color: hsl(126, 64%, 41%); - } - - &::after { - border-color: hsl(0, 0%, 100%); - } - } - } - - & .todo-list__label__description { - vertical-align: middle; - } - } -} - -!* RTL styles *! -[dir="rtl"] .todo-list li input { - left: 0; - margin-right: 0; - right: -25px; - margin-left: -15px; -} - -!* - * To-do list should be interactive only during the editing - * (https://github.com/ckeditor/ckeditor5/issues/2090). - *! -.ck-editor__editable .todo-list li input { - cursor: pointer; - - &:hover::before { - box-shadow: 0 0 0 5px hsla(0, 0%, 0%, 0.1); - } -} - -!* - * Workaround for Chrome and Safari glitch... TODO - *! -.ck-editor__editable .todo-list__label { - display: block; -} -*/ diff --git a/packages/ckeditor5-list/theme/todolist.css b/packages/ckeditor5-list/theme/todolist.css index abedce492cb..065f1960e40 100644 --- a/packages/ckeditor5-list/theme/todolist.css +++ b/packages/ckeditor5-list/theme/todolist.css @@ -3,6 +3,8 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ +@import "@ckeditor/ckeditor5-ui/theme/mixins/_dir.css"; + :root { --ck-todo-list-checkmark-size: 16px; } @@ -12,7 +14,7 @@ -webkit-appearance: none; display: inline-block; - position: absolute; + position: relative; width: var(--ck-todo-list-checkmark-size); height: var(--ck-todo-list-checkmark-size); vertical-align: middle; @@ -21,10 +23,20 @@ border: 0; /* LTR styles */ - left: -25px; - margin-right: -15px; - right: 0; - margin-left: 0; + @mixin ck-dir ltr { + left: -25px; + margin-right: -15px; + right: 0; + margin-left: 0; + } + + /* RTL styles */ + @mixin ck-dir rtl { + left: 0; + margin-right: 0; + right: -25px; + margin-left: -15px; + } &::before { display: block; @@ -68,27 +80,6 @@ } } -/* - * Document Lists - editing view has an additional span around checkbox. - */ -.ck-editor__editable .todo-list .todo-list__label > span[contenteditable=false] > input { - @mixin todo-list-checkbox; -} - -/* - * Workaround for Chrome and Safari glitch... TODO - */ -.ck-editor__editable .todo-list__label:has( > span[contenteditable=false] ) { - display: block; -} - -/* TODO does not work in Firefox */ -.ck-editor__editable li:has( .todo-list__label + .todo-list__label__description ) { - & input[type=checkbox] { - position: relative; - } -} - /* * TODO */ @@ -96,6 +87,7 @@ list-style: none; & li { + position: relative; margin-bottom: 5px; & .todo-list { @@ -105,7 +97,6 @@ & .todo-list__label { pointer-events: none; - position: relative; & > input { @mixin todo-list-checkbox; @@ -115,34 +106,46 @@ vertical-align: middle; } - /* TODO does not work in Firefox */ - &:has( .todo-list__label__description ) { - & input[type=checkbox] { - position: relative; - } + &.todo-list__label_without-description input[type=checkbox] { + position: absolute; } } } -/* RTL styles */ -[dir="rtl"] .todo-list .todo-list__label > input { - left: 0; - margin-right: 0; - right: -25px; - margin-left: -15px; -} - /* - * To-do list should be interactive only during the editing - * (https://github.com/ckeditor/ckeditor5/issues/2090). + * */ -.ck-editor__editable .todo-list .todo-list__label > input, -.ck-editor__editable .todo-list .todo-list__label > span[contenteditable=false] > input { - cursor: pointer; +.ck-editor__editable.ck-content .todo-list .todo-list__label { + /* + * To-do list should be interactive only during the editing + * (https://github.com/ckeditor/ckeditor5/issues/2090). + */ + & > input, + & > span[contenteditable=false] > input { + cursor: pointer; + + &:hover::before { + box-shadow: 0 0 0 5px hsla(0, 0%, 0%, 0.1); + } + } - &:hover::before { - box-shadow: 0 0 0 5px hsla(0, 0%, 0%, 0.1); + /* + * Document Lists - editing view has an additional span around checkbox. + */ + & > span[contenteditable=false] > input { + @mixin todo-list-checkbox; + } + + &.todo-list__label_without-description { + & input[type=checkbox] { + position: absolute; + } } } -.ck-content { line-height: 5em } +/* + * Workaround for Chrome and Safari glitch... TODO + */ +.ck-editor__editable .todo-list__label:has( > span[contenteditable=false] ) { + display: block; +} From 75f0c75611a85f06cc55c6716cb4fd531bfb9d35 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 7 Sep 2023 15:31:06 +0200 Subject: [PATCH 26/54] The label element can't be rendered in the editing view because it intercepts an inline widget behaviors. The css pointer-events workaround helps only partially. --- .../todocheckboxchangeobserver.ts | 2 +- .../tododocumentlistediting.ts | 6 +++--- .../tests/manual/todo-documentlist.js | 20 ++++++++++++++++--- packages/ckeditor5-list/theme/todolist.css | 11 ---------- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/ckeditor5-list/src/tododocumentlist/todocheckboxchangeobserver.ts b/packages/ckeditor5-list/src/tododocumentlist/todocheckboxchangeobserver.ts index fbffa58ed50..cdf721287d5 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/todocheckboxchangeobserver.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/todocheckboxchangeobserver.ts @@ -32,7 +32,7 @@ export default class TodoCheckboxChangeObserver extends DomEventObserver<'change viewTarget && viewTarget.is( 'element', 'input' ) && viewTarget.getAttribute( 'type' ) == 'checkbox' && - viewTarget.findAncestor( { name: 'label', classes: 'todo-list__label' } ) + viewTarget.findAncestor( { classes: 'todo-list__label' } ) ) { this.fire( 'todoCheckboxChange', domEvent ); } diff --git a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts index a5f57ea4e52..e54e4ccb68e 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts @@ -163,14 +163,14 @@ export default class TodoDocumentListEditing extends Plugin { return isDescriptionBlock( modelElement ); }, - createWrapperElement( writer, modelElement ) { + createWrapperElement( writer, modelElement, { dataPipeline } ) { const classes = [ 'todo-list__label' ]; if ( !isDescriptionBlock( modelElement ) ) { classes.push( 'todo-list__label_without-description' ); } - return writer.createAttributeElement( 'label', { + return writer.createAttributeElement( dataPipeline ? 'label' : 'span', { class: classes.join( ' ' ) } ); } @@ -182,7 +182,7 @@ export default class TodoDocumentListEditing extends Plugin { editing.mapper.registerViewToModelLength( 'input', viewElement => { if ( viewElement.getAttribute( 'type' ) == 'checkbox' && - viewElement.findAncestor( { name: 'label', classes: 'todo-list__label' } ) + viewElement.findAncestor( { classes: 'todo-list__label' } ) ) { return 0; } diff --git a/packages/ckeditor5-list/tests/manual/todo-documentlist.js b/packages/ckeditor5-list/tests/manual/todo-documentlist.js index 341238ccc66..68d9384abe9 100644 --- a/packages/ckeditor5-list/tests/manual/todo-documentlist.js +++ b/packages/ckeditor5-list/tests/manual/todo-documentlist.js @@ -12,7 +12,7 @@ import { Autoformat } from '@ckeditor/ckeditor5-autoformat'; import { BlockQuote } from '@ckeditor/ckeditor5-block-quote'; import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles'; import { Heading } from '@ckeditor/ckeditor5-heading'; -import { Link } from '@ckeditor/ckeditor5-link'; +import { Link, LinkImage } from '@ckeditor/ckeditor5-link'; import { MediaEmbed } from '@ckeditor/ckeditor5-media-embed'; import { Paragraph } from '@ckeditor/ckeditor5-paragraph'; import { Table, TableToolbar } from '@ckeditor/ckeditor5-table'; @@ -21,6 +21,11 @@ import { Indent } from '@ckeditor/ckeditor5-indent'; import { SourceEditing } from '@ckeditor/ckeditor5-source-editing'; import { GeneralHtmlSupport } from '@ckeditor/ckeditor5-html-support'; import { Alignment } from '@ckeditor/ckeditor5-alignment'; +import { CloudServices } from '@ckeditor/ckeditor5-cloud-services'; +import { EasyImage } from '@ckeditor/ckeditor5-easy-image'; +import { Image, ImageResize, ImageInsert } from '@ckeditor/ckeditor5-image'; + +import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; import Documentlist from '../../src/documentlist'; import TodoDocumentlist from '../../src/tododocumentlist'; @@ -45,17 +50,26 @@ ClassicEditor TodoDocumentlist, SourceEditing, GeneralHtmlSupport, - Alignment + Alignment, + Image, + CloudServices, + EasyImage, + ImageResize, + ImageInsert, + LinkImage ], toolbar: [ 'heading', '|', 'bulletedList', 'numberedList', 'todoList', 'outdent', 'indent', '|', - 'bold', 'link', 'insertTable', 'fontSize', 'alignment', + 'bold', 'link', 'fontSize', 'alignment', + '|', + 'insertTable', 'insertImage', '|', 'undo', 'redo', '|', 'sourceEditing' ], + cloudServices: CS_CONFIG, table: { contentToolbar: [ 'tableColumn', diff --git a/packages/ckeditor5-list/theme/todolist.css b/packages/ckeditor5-list/theme/todolist.css index 065f1960e40..14acdb0270a 100644 --- a/packages/ckeditor5-list/theme/todolist.css +++ b/packages/ckeditor5-list/theme/todolist.css @@ -10,8 +10,6 @@ } @define-mixin todo-list-checkbox { - pointer-events: auto; - -webkit-appearance: none; display: inline-block; position: relative; @@ -96,8 +94,6 @@ } & .todo-list__label { - pointer-events: none; - & > input { @mixin todo-list-checkbox; } @@ -142,10 +138,3 @@ } } } - -/* - * Workaround for Chrome and Safari glitch... TODO - */ -.ck-editor__editable .todo-list__label:has( > span[contenteditable=false] ) { - display: block; -} From 1be5de4b96b02c959e14872b5e844828b801f32c Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 8 Sep 2023 17:29:03 +0200 Subject: [PATCH 27/54] Fixed RTL styles. --- packages/ckeditor5-list/theme/todolist.css | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/ckeditor5-list/theme/todolist.css b/packages/ckeditor5-list/theme/todolist.css index 14acdb0270a..5d0a4928978 100644 --- a/packages/ckeditor5-list/theme/todolist.css +++ b/packages/ckeditor5-list/theme/todolist.css @@ -3,8 +3,6 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -@import "@ckeditor/ckeditor5-ui/theme/mixins/_dir.css"; - :root { --ck-todo-list-checkmark-size: 16px; } @@ -21,15 +19,13 @@ border: 0; /* LTR styles */ - @mixin ck-dir ltr { - left: -25px; - margin-right: -15px; - right: 0; - margin-left: 0; - } + left: -25px; + margin-right: -15px; + right: 0; + margin-left: 0; /* RTL styles */ - @mixin ck-dir rtl { + @nest [dir=rtl]& { left: 0; margin-right: 0; right: -25px; From b993cc7ac766fcd22f0d2e05e10ef6cb7eb25da1 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 12 Sep 2023 15:21:05 +0200 Subject: [PATCH 28/54] Handling view positions inside the checkbox wrappers. --- .../tododocumentlistediting.ts | 79 +++++++++++++++---- 1 file changed, 64 insertions(+), 15 deletions(-) diff --git a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts index e54e4ccb68e..585897c020d 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts @@ -13,8 +13,11 @@ import { type Model, type Element, type MatcherPattern, + type ViewElement, type ViewDocumentKeyDownEvent, - type ViewDocumentArrowKeyEvent + type ViewDocumentArrowKeyEvent, + type MapperViewToModelPositionEvent, + type ViewDocumentFragment } from 'ckeditor5/src/engine'; import { @@ -302,7 +305,7 @@ export default class TodoDocumentListEditing extends Plugin { } } ); - // Jump at the start of the next node on right arrow key press, when selection is before the checkbox. + // Jump at the start/end of the next node on right arrow key press, when selection is before the checkbox. // //

      Foo{}

      //
      • Bar
      @@ -318,6 +321,36 @@ export default class TodoDocumentListEditing extends Plugin { jumpOverCheckmarkOnSideArrowKeyPress( model, editor.locale ), { context: '$text' } ); + + // Map view positions inside the checkbox and wrappers to the position in the first block of the list item. + this.listenTo( editing.mapper, 'viewToModelPosition', ( evt, data ) => { + if ( !data.modelPosition ) { + return; + } + + if ( data.viewPosition.offset > 0 ) { + return; + } + + const viewParent = data.viewPosition.parent as ViewElement; + + const isInListItem = viewParent.is( 'attributeElement', 'li' ); + const isInListLabel = isLabelElement( viewParent ); + + const isInInputWrapper = viewParent.is( 'element', 'span' ) && + viewParent.getAttribute( 'contenteditable' ) == 'false' && + isLabelElement( viewParent.parent ); + + if ( !isInListItem && !isInListLabel && !isInInputWrapper ) { + return; + } + + const nodeAfter = data.modelPosition.nodeAfter; + + if ( nodeAfter && nodeAfter.getAttribute( 'listType' ) == 'todo' ) { + data.modelPosition = model.createPositionAt( nodeAfter, 0 ); + } + }, { priority: 'low' } ); } /** @@ -421,16 +454,12 @@ function isDescriptionBlock( modelElement: Element ): boolean { } /** - * Jump at the start of the next node on right arrow key press, when selection is before the checkbox. + * Jump at the start and end of a to-do list item. */ function jumpOverCheckmarkOnSideArrowKeyPress( model: Model, locale: Locale ): GetCallback { return ( eventInfo, domEventData ) => { const direction = getLocalizedArrowKeyCodeDirection( domEventData.keyCode, locale.contentLanguageDirection ); - if ( direction != 'right' ) { - return; - } - const schema = model.schema; const selection = model.document.selection; @@ -441,19 +470,32 @@ function jumpOverCheckmarkOnSideArrowKeyPress( model: Model, locale: Locale ): G const position = selection.getFirstPosition()!; const parent = position.parent as Element; - if ( !position.isAtEnd ) { - return; - } + // Right arrow before a to-do list item. + if ( direction == 'right' && position.isAtEnd ) { + const newRange = schema.getNearestSelectionRange( model.createPositionAfter( parent ), 'forward' ); - const newRange = schema.getNearestSelectionRange( model.createPositionAfter( parent ), 'forward' ); + if ( !newRange ) { + return; + } - if ( !newRange ) { - return; + const newRangeParent = newRange.start.parent; + + if ( newRangeParent && isListItemBlock( newRangeParent ) && newRangeParent.getAttribute( 'listType' ) == 'todo' ) { + model.change( writer => writer.setSelection( newRange ) ); + + domEventData.preventDefault(); + domEventData.stopPropagation(); + eventInfo.stop(); + } } + // Left arrow at the beginning of a to-do list item. + else if ( direction == 'left' && position.isAtStart && isListItemBlock( parent ) && parent.getAttribute( 'listType' ) == 'todo' ) { + const newRange = schema.getNearestSelectionRange( model.createPositionBefore( parent ), 'backward' ); - const newRangeParent = newRange.start.parent; + if ( !newRange ) { + return; + } - if ( isListItemBlock( newRangeParent ) && newRangeParent.getAttribute( 'listType' ) == 'todo' ) { model.change( writer => writer.setSelection( newRange ) ); domEventData.preventDefault(); @@ -462,3 +504,10 @@ function jumpOverCheckmarkOnSideArrowKeyPress( model: Model, locale: Locale ): G } }; } + +/** + * Returns true if the given element is a label element of a to-do list item. + */ +function isLabelElement( viewElement: ViewElement | ViewDocumentFragment | null ): boolean { + return !!viewElement && viewElement.is( 'attributeElement' ) && viewElement.hasClass( 'todo-list__label' ); +} From 60c913d700e13af039589e2753c46abfe94b30bb Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Tue, 12 Sep 2023 16:39:46 +0200 Subject: [PATCH 29/54] Tests: todo document list conversion (wip). --- .../tododocumentlistediting.js | 618 ++++++++++++++++++ 1 file changed, 618 insertions(+) create mode 100644 packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js diff --git a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js new file mode 100644 index 00000000000..916340bec77 --- /dev/null +++ b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js @@ -0,0 +1,618 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting'; +import HeadingEditing from '@ckeditor/ckeditor5-heading/src/headingediting'; +import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import TableEditing from '@ckeditor/ckeditor5-table/src/tableediting'; + +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; + +import TodoDocumentListEditing from '../../src/tododocumentlist/tododocumentlistediting'; +import DocumentListEditing from '../../src/documentlist/documentlistediting'; +import DocumentListCommand from '../../src/documentlist/documentlistcommand'; +import CheckTodoDocumentListCommand from '../../src/tododocumentlist/checktododocumentlistcommand'; +import TodoCheckboxChangeObserver from '../../src/tododocumentlist/todocheckboxchangeobserver'; + +import stubUid from '../documentlist/_utils/uid'; + +describe( 'TodoDocumentListEditing', () => { + let editor, model, view; + + testUtils.createSinonSandbox(); + + beforeEach( async () => { + editor = await VirtualTestEditor.create( { + plugins: [ Paragraph, TodoDocumentListEditing, BlockQuoteEditing, TableEditing, HeadingEditing ] + } ); + + model = editor.model; + view = editor.editing.view; + + stubUid(); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + it( 'should have pluginName', () => { + expect( TodoDocumentListEditing.pluginName ).to.equal( 'TodoDocumentListEditing' ); + } ); + + it( 'should load DocumentListEditing', () => { + expect( TodoDocumentListEditing.requires ).to.have.members( [ DocumentListEditing ] ); + } ); + + describe( 'commands', () => { + it( 'should register todoList command', () => { + const command = editor.commands.get( 'todoList' ); + + expect( command ).to.be.instanceOf( DocumentListCommand ); + expect( command ).to.have.property( 'type', 'todo' ); + } ); + + it( 'should register checkTodoList command', () => { + const command = editor.commands.get( 'checkTodoList' ); + + expect( command ).to.be.instanceOf( CheckTodoDocumentListCommand ); + } ); + } ); + + it( 'should register TodoCheckboxChangeObserver', () => { + expect( view.getObserver( TodoCheckboxChangeObserver ) ).to.be.instanceOf( TodoCheckboxChangeObserver ); + } ); + + it( 'should set proper schema rules', () => { + const paragraph = new ModelElement( 'paragraph', { listItemId: 'foo', listType: 'todo' } ); + const heading = new ModelElement( 'heading1', { listItemId: 'foo', listType: 'todo' } ); + const blockQuote = new ModelElement( 'blockQuote', { listItemId: 'foo', listType: 'todo' } ); + const table = new ModelElement( 'table', { listItemId: 'foo', listType: 'todo' }, [ ] ); + + expect( model.schema.checkAttribute( [ '$root', paragraph ], 'todoListChecked' ) ).to.be.true; + expect( model.schema.checkAttribute( [ '$root', heading ], 'todoListChecked' ) ).to.be.true; + expect( model.schema.checkAttribute( [ '$root', blockQuote ], 'todoListChecked' ) ).to.be.true; + expect( model.schema.checkAttribute( [ '$root', table ], 'todoListChecked' ) ).to.be.true; + } ); + + describe( 'upcast', () => { + it( 'should convert li with a checkbox before the first text node as a to-do list item', () => { + testUpcast( + '
      • foo
      ', + 'foo' + ); + } ); + + it( 'should convert the full markup generated by the editor', () => { + testUpcast( + '
      • foo
      ', + 'foo' + ); + + testUpcast( + editor.getData(), + 'foo' + ); + } ); + + it( 'should convert li with checked checkbox as checked to-do list item', () => { + testUpcast( + '
        ' + + '
      • a
      • ' + + '
      • b
      • ' + + '
      • c
      • ' + + '
      ', + 'a' + + 'b' + + 'c' + ); + } ); + + it( 'should not convert li with checkbox in the middle of the text', () => { + testUpcast( + '
      • FooBar
      ', + 'FooBar' + ); + } ); + + it( 'should split items with checkboxes - bulleted list', () => { + testUpcast( + '
        ' + + '
      • foo
      • ' + + '
      • bar
      • ' + + '
      • baz
      • ' + + '
      ', + 'foo' + + 'bar' + + 'baz' + ); + } ); + + it( 'should split items with checkboxes - numbered list', () => { + testUpcast( + '
        ' + + '
      1. foo
      2. ' + + '
      3. bar
      4. ' + + '
      5. baz
      6. ' + + '
      ', + 'foo' + + 'bar' + + 'baz' + ); + } ); + + it( 'should convert li with a checkbox in a nested list', () => { + testUpcast( + '
        ' + + '
      • ' + + '' + + 'foo' + + '
        • foo
        ' + + '
      • ' + + '
      ', + 'foo' + + 'foo' + ); + } ); + + it( 'should convert li with checkboxes in a nested lists (bulleted > todo > todo)', () => { + testUpcast( + '
        ' + + '
      • ' + + '
          ' + + '
        • ' + + 'foo
        • ' + + '
          • foo
          ' + + '
        ' + + '
      • ' + + '
      ', + '' + + 'foo' + + 'foo' + ); + } ); + + it( 'should convert li with a checkbox and a paragraph', () => { + testUpcast( + '
        ' + + '
      • ' + + '' + + '

        foo

        ' + + '
      • ' + + '
      ', + 'foo' + ); + } ); + + it( 'should convert li with a checkbox and two paragraphs', () => { + testUpcast( + '
        ' + + '
      • ' + + '' + + '

        foo

        ' + + '

        bar

        ' + + '
      • ' + + '
      ', + 'foo' + + 'bar' + ); + } ); + + it( 'should convert li with a checkbox and a blockquote', () => { + testUpcast( + '
        ' + + '
      • ' + + '' + + '
        foo
        ' + + '
      • ' + + '
      ', + '
      ' + + 'foo' + + '
      ' + ); + } ); + + it( 'should convert li with a checkbox and a heading', () => { + testUpcast( + '
        ' + + '
      • ' + + '' + + '

        foo

        ' + + '
      • ' + + '
      ', + 'foo' + ); + } ); + + it( 'should convert li with a checkbox and a table', () => { + testUpcast( + '
        ' + + '
      • ' + + '' + + '
        foo
        ' + + '
      • ' + + '
      ', + '' + + '' + + '' + + 'foo' + + '' + + '' + + '
      ' + ); + } ); + } ); + + describe( 'downcast - editing', () => { + it( 'should convert a todo list item', () => { + testEditing( + 'foo', + '
        ' + + '
      • ' + + '' + + '' + + '' + + '' + + '' + + 'foo' + + '' + + '' + + '
      • ' + + '
      ' + ); + } ); + + it( 'should convert a nested todo list item', () => { + testEditing( + 'foo' + + 'foo', + '
        ' + + '
      • ' + + '' + + '' + + '' + + '' + + '' + + 'foo' + + '' + + '' + + '
          ' + + '
        • ' + + '' + + '' + + '' + + '' + + '' + + 'foo' + + '' + + '' + + '
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'should convert to-do list item mixed with bulleted list items', () => { + testEditing( + 'foo' + + 'bar' + + 'baz', + '
        ' + + '
      • ' + + 'foo' + + '
      • ' + + '
      ' + + '
        ' + + '
      • ' + + '' + + '' + + '' + + '' + + 'bar' + + '' + + '
      • ' + + '
      ' + + '
        ' + + '
      • baz
      • ' + + '
      ' + ); + } ); + + it( 'should convert to-do list item mixed with numbered list items', () => { + testEditing( + 'foo' + + 'bar' + + 'baz', + '
        ' + + '
      1. ' + + 'foo' + + '
      2. ' + + '
      ' + + '
        ' + + '
      • ' + + '' + + '' + + '' + + '' + + 'bar' + + '' + + '
      • ' + + '
      ' + + '
        ' + + '
      1. baz
      2. ' + + '
      ' + ); + } ); + + it( 'should wrap a checkbox and first paragraph in a span with a special label class', () => { + testEditing( + 'foo' + + 'bar', + '
        ' + + '
      • ' + + '' + + '' + + '' + + '' + + '' + + 'foo' + + '' + + '' + + '

        bar

        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'should wrap only a checkbox in a span if first element is a blockquote', () => { + testEditing( + '
      ' + + 'foo' + + '
      ', + '
        ' + + '
      • ' + + '' + + '' + + '' + + '' + + '' + + '
        ' + + '

        foo

        ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'should wrap only a checkbox in a span if first element is a heading', () => { + testEditing( + 'foo', + '
        ' + + '
      • ' + + '' + + '' + + '' + + '' + + '' + + '

        foo

        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'should wrap only a checkbox in a span if first element is a table', () => { + testEditing( + '' + + '' + + '' + + 'foo' + + '' + + '' + + '
      ', + '
        ' + + '
      • ' + + '' + + '' + + '' + + '' + + '' + + '
        ' + + '
        ' + + '' + + '' + + '' + + '' + + '' + + '' + + '
        ' + + 'foo' + + '
        ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + } ); + + describe( 'downcast - data', () => { + it( 'should convert a todo list item', () => { + testData( + 'foo', + '
        ' + + '
      • ' + + '' + + '
      • ' + + '
      ' + ); + } ); + + it( 'should convert a nested todo list item', () => { + testData( + 'foo' + + 'foo', + '
        ' + + '
      • ' + + '' + + '
          ' + + '
        • ' + + '' + + '
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'should convert to-do list item mixed with bulleted list items', () => { + testData( + 'foo' + + 'bar' + + 'baz', + '
        ' + + '
      • foo
      • ' + + '
      ' + + '
        ' + + '
      • ' + + '' + + '
      • ' + + '
      ' + + '
        ' + + '
      • baz
      • ' + + '
      ' + ); + } ); + + it( 'should convert to-do list item mixed with numbered list items', () => { + testData( + 'foo' + + 'bar' + + 'baz', + '
        ' + + '
      1. foo
      2. ' + + '
      ' + + '
        ' + + '
      • ' + + '' + + '
      • ' + + '
      ' + + '
        ' + + '
      1. baz
      2. ' + + '
      ' + ); + } ); + + it( 'should wrap a checkbox and first paragraph in a label element', () => { + testData( + 'foo' + + 'bar', + '
        ' + + '
      • ' + + '' + + '

        bar

        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'should wrap only a checkbox in a label element if first element is a blockquote', () => { + testData( + '
      ' + + 'foo' + + '
      ', + '
        ' + + '
      • ' + + '' + + '

        foo

        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'should wrap only a checkbox in a label element if first element is a heading', () => { + testData( + 'foo', + '
        ' + + '
      • ' + + '' + + '

        foo

        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'should wrap only a checkbox in a label element if first element is a table', () => { + testData( + '' + + '' + + '' + + 'foo' + + '' + + '' + + '
      ', + '
        ' + + '
      • ' + + '' + + '
        ' + + '' + + '' + + '' + + '' + + '' + + '' + + '
        foo
        ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + } ); + + function testUpcast( input, output ) { + editor.setData( input ); + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( output ); + } + + function testEditing( input, output ) { + setModelData( model, input ); + expect( getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( output ); + } + + function testData( input, output ) { + setModelData( model, input ); + expect( editor.getData() ).to.equalMarkup( output ); + } +} ); From 9fcd90e94ba1fa6b2ae16c7fa003160c918d3078 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 13 Sep 2023 19:01:53 +0200 Subject: [PATCH 30/54] Adding tests. --- .../tests/documentlist/converters-data.js | 84 +++++++ .../tests/documentlist/documentlistediting.js | 213 +++++++++++++++++- 2 files changed, 296 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-list/tests/documentlist/converters-data.js b/packages/ckeditor5-list/tests/documentlist/converters-data.js index ffa0bed9e79..45b096a030d 100644 --- a/packages/ckeditor5-list/tests/documentlist/converters-data.js +++ b/packages/ckeditor5-list/tests/documentlist/converters-data.js @@ -2506,4 +2506,88 @@ describe( 'DocumentListEditing - converters - data pipeline', () => { } ); } ); } ); + + describe( 'list item content should be able to detect if it is inside some list item', () => { + beforeEach( () => { + model.schema.register( 'obj', { inheritAllFrom: '$inlineObject' } ); + + editor.conversion.for( 'upcast' ).add( dispatcher => { + dispatcher.on( 'element:obj', ( evt, data, conversionApi ) => { + const modelCursor = data.modelCursor; + const modelItem = modelCursor.parent; + const viewItem = data.viewItem; + + // This is the main part. + if ( !modelItem.hasAttribute( 'listType' ) ) { + return; + } + + if ( !conversionApi.consumable.consume( viewItem, { name: true } ) ) { + return; + } + + const writer = conversionApi.writer; + + writer.setAttribute( 'listType', 'todo', modelItem ); + + data.modelRange = writer.createRange( modelCursor ); + } ); + } ); + + editor.plugins.get( DocumentListEditing ).registerDowncastStrategy( { + scope: 'list', + attributeName: 'listType', + + setAttributeOnDowncast( writer, value, element ) { + if ( value === 'todo' ) { + writer.addClass( 'todo-list', element ); + } + } + } ); + + editor.conversion.elementToElement( { model: 'obj', view: 'obj' } ); + } ); + + it( 'content directly inside LI element', () => { + test.data( + '
        ' + + '
      • foo
      • ' + + '
      ' + + '

      bar

      ', + + 'foo' + + 'bar', + + '
        ' + + '
      • foo
      • ' + + '
      ' + + '

       bar

      ' + ); + } ); + + it( 'content inside a P in LI element', () => { + test.data( + '
        ' + + '
      • ' + + '' + + '

        foo

        ' + + '

        123

        ' + + '
      • ' + + '
      ' + + '

      bar

      ', + + 'foo' + + '123' + + 'bar', + + '
        ' + + '
      • ' + + '

        foo

        ' + + '

        123

        ' + + '
      • ' + + '
      ' + + '

       bar

      ' + ); + } ); + } ); } ); diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistediting.js b/packages/ckeditor5-list/tests/documentlist/documentlistediting.js index a03eb25ebec..3f6deb5903e 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistediting.js @@ -24,6 +24,7 @@ import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils import ListEditing from '../../src/list/listediting'; import DocumentListIndentCommand from '../../src/documentlist/documentlistindentcommand'; import DocumentListSplitCommand from '../../src/documentlist/documentlistsplitcommand'; +import { isFirstBlockOfListItem } from '../../src/documentlist/utils/model'; import stubUid from './_utils/uid'; import { modelList, prepareTest } from './_utils/utils'; @@ -782,9 +783,219 @@ describe( 'DocumentListEditing - registerDowncastStrategy()', () => { ); } ); + describe( 'should allow registering strategy for list items markers', () => { + describe( 'without first block wrapper', () => { + beforeEach( async () => { + await createEditor( class CustomPlugin extends Plugin { + init() { + this.editor.plugins.get( 'DocumentListEditing' ).registerDowncastStrategy( { + scope: 'itemMarker', + attributeName: 'someFoo', + + createElement( writer, modelElement, { dataPipeline } ) { + return writer.createEmptyElement( 'input', { + type: 'checkbox', + value: modelElement.getAttribute( 'someFoo' ), + ...( dataPipeline ? { disabled: 'disabled' } : null ) + } ); + } + } ); + } + } ); + } ); + + it( 'single block in a list item', () => { + setModelData( model, modelList( ` + * foo + * bar + ` ) ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( + '
        ' + + '
      • foo
      • ' + + '
      • bar
      • ' + + '
      ' + ); + + expect( editor.getData() ).to.equalMarkup( + '
        ' + + '
      • foo
      • ' + + '
      • bar
      • ' + + '
      ' + ); + } ); + + it( 'multiple blocks in a single list item', () => { + setModelData( model, modelList( ` + * foo + bar + ` ) ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( + '
        ' + + '
      • ' + + '' + + '

        foo

        ' + + '

        bar

        ' + + '
      • ' + + '
      ' + ); + + expect( editor.getData() ).to.equalMarkup( + '
        ' + + '
      • ' + + '' + + '

        foo

        ' + + '

        bar

        ' + + '
      • ' + + '
      ' + ); + } ); + } ); + + describe( 'with first block wrapper', () => { + beforeEach( async () => { + await createEditor( class CustomPlugin extends Plugin { + init() { + this.editor.plugins.get( 'DocumentListEditing' ).registerDowncastStrategy( { + scope: 'itemMarker', + attributeName: 'someFoo', + + createElement( writer, modelElement, { dataPipeline } ) { + return writer.createEmptyElement( 'input', { + type: 'checkbox', + value: modelElement.getAttribute( 'someFoo' ), + ...( dataPipeline ? { disabled: 'disabled' } : null ) + } ); + }, + + canWrapElement( modelElement ) { + return isFirstBlockOfListItem( modelElement ) && modelElement.is( 'element', 'paragraph' ); + }, + + createWrapperElement( writer, modelElement, { dataPipeline } ) { + return writer.createAttributeElement( dataPipeline ? 'label' : 'span', { class: 'label' } ); + } + } ); + + this.editor.conversion.for( 'downcast' ).elementToElement( { + model: 'paragraph', + view: ( element, { writer } ) => { + if ( isFirstBlockOfListItem( element ) ) { + return writer.createContainerElement( 'span', { class: 'description' } ); + } + }, + converterPriority: 'highest' + } ); + } + } ); + } ); + + it( 'single block in a list item', () => { + setModelData( model, modelList( ` + * foo + * bar + ` ) ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( + '
        ' + + '
      • ' + + '' + + '' + + 'foo' + + '' + + '
      • ' + + '
      • ' + + '' + + '' + + 'bar' + + '' + + '
      • ' + + '
      ' + ); + + expect( editor.getData() ).to.equalMarkup( + '
        ' + + '
      • ' + + '' + + '
      • ' + + '
      • ' + + '' + + '
      • ' + + '
      ' + ); + } ); + + it( 'multiple blocks in a single list item', () => { + setModelData( model, modelList( ` + * foo + bar + ` ) ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( + '
        ' + + '
      • ' + + '' + + '' + + 'foo' + + '' + + '

        bar

        ' + + '
      • ' + + '
      ' + ); + + expect( editor.getData() ).to.equalMarkup( + '
        ' + + '
      • ' + + '' + + '

        bar

        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'single block (non paragraph) in a list item', () => { + setModelData( model, modelList( ` + * foo + ` ) ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( + '
        ' + + '
      • ' + + '' + + '' + + '' + + '

        foo

        ' + + '
      • ' + + '
      ' + ); + + expect( editor.getData() ).to.equalMarkup( + '
        ' + + '
      • ' + + '' + + '

        foo

        ' + + '
      • ' + + '
      ' + ); + } ); + } ); + } ); + async function createEditor( extraPlugin ) { editor = await VirtualTestEditor.create( { - plugins: [ extraPlugin, Paragraph, DocumentListEditing, UndoEditing ] + plugins: [ extraPlugin, Paragraph, DocumentListEditing, UndoEditing, HeadingEditing ] } ); model = editor.model; From f163b70cfefff378ad1d829d291a19f05278079a Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Thu, 14 Sep 2023 20:55:24 +0200 Subject: [PATCH 31/54] Tests: todo document list command (wip). --- .../checktododocumentlistcommand.js | 269 ++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 packages/ckeditor5-list/tests/tododocumentlist/checktododocumentlistcommand.js diff --git a/packages/ckeditor5-list/tests/tododocumentlist/checktododocumentlistcommand.js b/packages/ckeditor5-list/tests/tododocumentlist/checktododocumentlistcommand.js new file mode 100644 index 00000000000..5b1d9a8c8a1 --- /dev/null +++ b/packages/ckeditor5-list/tests/tododocumentlist/checktododocumentlistcommand.js @@ -0,0 +1,269 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; + +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +import TodoDocumentListEditing from '../../src/tododocumentlist/tododocumentlistediting'; +import CheckTodoDocumentListCommand from '../../src/tododocumentlist/checktododocumentlistcommand'; + +describe( 'CheckTodoListCommand', () => { + let editor, model, command; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, TodoDocumentListEditing ] + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + command = new CheckTodoDocumentListCommand( editor ); + } ); + } ); + + afterEach( () => { + command.destroy(); + + return editor.destroy(); + } ); + + describe( 'isEnabled', () => { + it( 'should be enabled when selection is inside to-do list item', () => { + setModelData( model, 'f[]oo' ); + + expect( command.isEnabled ).to.equal( true ); + } ); + + it( 'should be disabled when selection is not inside to-do list item', () => { + setModelData( model, 'f[]oo' ); + + expect( command.isEnabled ).to.equal( false ); + } ); + + it( 'should be enabled when at least one to-do list item is selected', () => { + setModelData( model, + 'f[oo' + + 'bar' + + 'ba]z' + ); + + expect( command.isEnabled ).to.equal( true ); + } ); + + it( 'should be disabled when no to-do list item is selected', () => { + setModelData( model, + 'foo' + + 'b[ar' + + 'baz' + + 'b]ax' + ); + + expect( command.isEnabled ).to.equal( false ); + } ); + } ); + + describe( 'value', () => { + it( 'should be false when selection is in not checked element', () => { + setModelData( model, 'f[]oo' ); + + expect( command.value ).to.equal( false ); + } ); + + it( 'should be true when selection is in checked element', () => { + setModelData( model, 'f[]oo' ); + + expect( command.value ).to.equal( true ); + } ); + + it( 'should be false when at least one selected element is not checked', () => { + setModelData( model, + 'f[oo' + + 'bar' + + 'b]az' + ); + + expect( command.value ).to.equal( false ); + } ); + + it( 'should be true when all selected elements are checked', () => { + setModelData( model, + 'f[oo' + + 'bar' + + 'b]az' + ); + + expect( command.value ).to.equal( true ); + } ); + } ); + + describe( 'execute()', () => { + it( 'should toggle checked state on to-do list item when collapsed selection is inside this item', () => { + testCommandToggle( + 'f[]oo', + 'f[]oo' + ); + } ); + + it( 'should toggle checked state on to-do list item when non-collapsed selection is inside this item', () => { + testCommandToggle( + 'f[o]o', + 'f[o]o' + ); + } ); + + it( 'should toggle state on multiple items', () => { + testCommandToggle( + 'foo[' + + 'bar' + + ']baz', + + 'foo[' + + 'bar' + + ']baz' + ); + } ); + + it( 'should toggle state on multiple items in nested list', () => { + testCommandToggle( + 'foo[' + + 'bar' + + ']baz', + + 'foo[' + + 'bar' + + ']baz' + ); + } ); + + it( 'should toggle state on multiple items mixed with none to-do list items', () => { + testCommandToggle( + 'a[bc' + + 'foo' + + 'bar' + + 'baz' + + 'xy]z', + + 'a[bc' + + 'foo' + + 'bar' + + 'baz' + + 'xy]z' + ); + } ); + + it( 'should toggle state on multiple items mixed with none to-do list items in nested list', () => { + testCommandToggle( + 'a[bc' + + 'foo' + + 'bar' + + 'baz' + + 'xy]z', + + 'a[bc' + + 'foo' + + 'bar' + + 'baz' + + 'xy]z' + ); + } ); + + it( 'should toggle state on items if selection is in paragraph', () => { + testCommandToggle( + 'foo' + + 'b[]ar', + + 'foo' + + 'b[]ar' + ); + } ); + + it( 'should toggle state items at the same level if selection is in paragraph', () => { + testCommandToggle( + 'foo' + + 'bar' + + 'b[]az', + + 'foo' + + 'bar' + + 'b[]az' + ); + } ); + + it( 'should mark all selected items as checked when at least one selected item is not checked', () => { + setModelData( model, + 'foo[' + + 'bar' + + ']baz' + ); + + command.execute(); + + expect( getModelData( model ) ).to.equal( + 'foo[' + + 'bar' + + ']baz' + ); + } ); + + it( 'should mark all selected items as checked when at least one selected item is not checked in nested list', () => { + setModelData( model, + 'foo[' + + 'bar' + + ']baz' + ); + + command.execute(); + + expect( getModelData( model ) ).to.equal( + 'foo[' + + 'bar' + + ']baz' + ); + } ); + + it( 'should do nothing when there are no elements to toggle attribute', () => { + setModelData( model, 'b[]ar' ); + + command.execute(); + + expect( getModelData( model ) ).to.equal( 'b[]ar' ); + } ); + + it( 'should set attribute if `forceValue` parameter is set to `true`', () => { + setModelData( model, 'f[]oo' ); + + command.execute( { forceValue: true } ); + + expect( getModelData( model ) ).to.equal( + 'f[]oo' + ); + } ); + + it( 'should remove attribute if `forceValue` parameter is set to `false`', () => { + setModelData( model, 'f[]oo' ); + + command.execute( { forceValue: false } ); + + expect( getModelData( model ) ).to.equal( + 'f[]oo' + ); + } ); + + function testCommandToggle( initialData, changedData ) { + setModelData( model, initialData ); + + command.execute(); + + expect( getModelData( model ) ).to.equal( changedData ); + + command.execute(); + + expect( getModelData( model ) ).to.equal( initialData ); + } + } ); +} ); From 0ec1f70ecdd851906144ae709977e4062a2e7691 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 14 Sep 2023 20:59:13 +0200 Subject: [PATCH 32/54] Adding tests. --- .../src/documentlist/converters.ts | 8 +- .../tests/documentlist/_utils/utils.js | 4 +- ...odocumentlistediting-conversion-changes.js | 3363 +++++++++++++++++ 3 files changed, 3367 insertions(+), 8 deletions(-) create mode 100644 packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting-conversion-changes.js diff --git a/packages/ckeditor5-list/src/documentlist/converters.ts b/packages/ckeditor5-list/src/documentlist/converters.ts index 1992d1746b6..2497cc7b2b9 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.ts +++ b/packages/ckeditor5-list/src/documentlist/converters.ts @@ -185,7 +185,7 @@ export function reconvertItemsOnDataChange( findAndAddListHeadToMap( entry.range.start.getShiftedBy( 1 ), itemToListHead ); // Check if paragraph should be converted from bogus to plain paragraph. - if ( doesItemBlockRequiresRefresh( item ) ) { + if ( doesItemBlockRequiresRefresh( item as Element ) ) { itemsToRefresh.push( item ); } } else { @@ -253,11 +253,7 @@ export function reconvertItemsOnDataChange( return itemsToRefresh; } - function doesItemBlockRequiresRefresh( item: Node, blocks?: Array ) { - if ( !item.is( 'element' ) ) { - return false; - } - + function doesItemBlockRequiresRefresh( item: Element, blocks?: Array ) { const viewElement = editing.mapper.toViewElement( item ); if ( !viewElement ) { diff --git a/packages/ckeditor5-list/tests/documentlist/_utils/utils.js b/packages/ckeditor5-list/tests/documentlist/_utils/utils.js index e3cbc42c3cf..d84e2de1329 100644 --- a/packages/ckeditor5-list/tests/documentlist/_utils/utils.js +++ b/packages/ckeditor5-list/tests/documentlist/_utils/utils.js @@ -116,10 +116,10 @@ export function setupTestHelpers( editor ) { test.test( input, output, actionCallback ); }, - changeType( input, output ) { + changeType( input, output, newTypeFunc = type => type == 'numbered' ? 'bulleted' : 'numbered' ) { const actionCallback = selection => { const element = selection.getFirstPosition().nodeAfter; - const newType = element.getAttribute( 'listType' ) == 'numbered' ? 'bulleted' : 'numbered'; + const newType = newTypeFunc( element.getAttribute( 'listType' ) ); model.change( writer => { const itemsToChange = Array.from( selection.getSelectedBlocks() ); diff --git a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting-conversion-changes.js b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting-conversion-changes.js new file mode 100644 index 00000000000..58ffc03bb61 --- /dev/null +++ b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting-conversion-changes.js @@ -0,0 +1,3363 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting'; +import HeadingEditing from '@ckeditor/ckeditor5-heading/src/headingediting'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import TableEditing from '@ckeditor/ckeditor5-table/src/tableediting'; + +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import { parse as parseModel } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; + +import TodoDocumentListEditing from '../../src/tododocumentlist/tododocumentlistediting'; +import { setupTestHelpers } from '../documentlist/_utils/utils'; + +import stubUid from '../documentlist/_utils/uid'; +import { UndoEditing } from '@ckeditor/ckeditor5-undo'; + +describe( 'TodoDocumentListEditing - conversion - changes', () => { + let editor, model, test, modelRoot; + + testUtils.createSinonSandbox(); + + beforeEach( async () => { + editor = await VirtualTestEditor.create( { + plugins: [ Paragraph, TodoDocumentListEditing, BlockQuoteEditing, TableEditing, HeadingEditing, UndoEditing ] + } ); + + model = editor.model; + modelRoot = model.document.getRoot(); + + stubUid(); + + test = setupTestHelpers( editor ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + describe( 'flat lists', () => { + describe( 'insert', () => { + it( 'list item at the beginning of same list type', () => { + test.insert( + 'p' + + '[x]' + + 'a', + + '

      p

      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'x' + + '' + + '
      • ' + + '
      • ' + + '' + + '' + + 'a' + + '' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'list item in the middle of same list type', () => { + test.insert( + 'p' + + 'a' + + '[x]' + + 'b', + + '

      p

      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'a' + + '' + + '
      • ' + + '
      • ' + + '' + + '' + + 'x' + + '' + + '
      • ' + + '
      • ' + + '' + + '' + + 'b' + + '' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'list item at the end of same list type', () => { + test.insert( + 'p' + + 'a' + + '[x]', + + '

      p

      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'a' + + '' + + '
      • ' + + '
      • ' + + '' + + '' + + 'x' + + '' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'list item at the beginning of different list type', () => { + test.insert( + 'p' + + '[x]' + + 'a', + + '

      p

      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'x' + + '' + + '
      • ' + + '
      ' + + '
        ' + + '
      • a
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'list item in the middle of different list type', () => { + test.insert( + 'p' + + 'a' + + '[x]' + + 'b', + + '

      p

      ' + + '
        ' + + '
      • a
      • ' + + '
      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'x' + + '' + + '
      • ' + + '
      ' + + '
        ' + + '
      • b
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'list item at the end of different list type', () => { + test.insert( + 'p' + + 'a' + + '[x]', + + '

      p

      ' + + '
        ' + + '
      • a
      • ' + + '
      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'x' + + '' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'element between list items', () => { + test.insert( + 'a' + + '[x]' + + 'b', + + '
        ' + + '
      • ' + + '' + + '' + + 'a' + + '' + + '
      • ' + + '
      ' + + '

      x

      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'b' + + '' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'list item that is not a paragraph', () => { + test.insert( + 'p' + + 'a' + + '[x]' + + 'b', + + '

      p

      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'a' + + '' + + '
      • ' + + '
      • ' + + '' + + '' + + '' + + '

        x

        ' + + '
      • ' + + '
      • ' + + '' + + '' + + 'b' + + '' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'new block at the start of list item', () => { + test.insert( + 'p' + + 'a' + + '[x]' + + 'b', + + '

      p

      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'a' + + '' + + '
      • ' + + '
      • ' + + '' + + '' + + 'x' + + '' + + '

        b

        ' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 3 ) ); + } ); + + it( 'new block at the start of list item that contains other element than paragraph', () => { + test.insert( + 'p' + + 'a' + + '[x]' + + 'b', + + '

      p

      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'a' + + '' + + '
      • ' + + '
      • ' + + '' + + '' + + 'x' + + '' + + '

        b

        ' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 3 ) ); + } ); + + it( 'new block at the end of list item', () => { + test.insert( + 'p' + + 'a' + + '[x]' + + 'b', + + '

      p

      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'a' + + '' + + '

        x

        ' + + '
      • ' + + '
      • ' + + '' + + '' + + 'b' + + '' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + } ); + + it( 'new block at the middle of list item', () => { + test.insert( + 'p' + + 'a' + + 'x1' + + '[x]' + + 'x2' + + 'b', + + '

      p

      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'a' + + '' + + '
      • ' + + '
      • ' + + '' + + '' + + 'x1' + + '' + + '

        x

        ' + + '

        x2

        ' + + '
      • ' + + '
      • ' + + '' + + '' + + 'b' + + '' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + } ); + + it( 'new list item in the middle of list item', () => { + test.insert( + 'p' + + 'a' + + 'x1' + + '[y]' + + 'x2' + + 'b', + + '

      p

      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'a' + + '' + + '
      • ' + + '
      • ' + + '' + + '' + + 'x1' + + '' + + '
      • ' + + '
      • ' + + '' + + '' + + 'y' + + '' + + '
      • ' + + '
      • ' + + '' + + '' + + 'x2' + + '' + + '
      • ' + + '
      • ' + + '' + + '' + + 'b' + + '' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 4 ) ); + } ); + } ); + + describe( 'remove', () => { + it( 'remove the first list item', () => { + test.remove( + 'p' + + '[a]' + + 'b' + + 'c', + + '

      p

      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'b' + + '' + + '
      • ' + + '
      • ' + + '' + + '' + + 'c' + + '' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'remove list item from the middle', () => { + test.remove( + 'p' + + 'a' + + '[b]' + + 'c', + + '

      p

      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'a' + + '' + + '
      • ' + + '
      • ' + + '' + + '' + + 'c' + + '' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'remove the last list item', () => { + test.remove( + 'p' + + 'a' + + 'b' + + '[c]', + + '

      p

      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'a' + + '' + + '
      • ' + + '
      • ' + + '' + + '' + + 'b' + + '' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'remove the only list item', () => { + test.remove( + 'p' + + '[x]' + + 'p', + + '

      p

      ' + + '

      p

      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'remove element from between lists of same type', () => { + test.remove( + 'p' + + 'a' + + '[x]' + + 'b' + + 'p', + + '

      p

      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'a' + + '' + + '
      • ' + + '
      • ' + + '' + + '' + + 'b' + + '' + + '
      • ' + + '
      ' + + '

      p

      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'remove element from between lists of different type', () => { + test.remove( + 'p' + + 'a' + + '[x]' + + 'b' + + 'p', + + '

      p

      ' + + '
        ' + + '
      • a
      • ' + + '
      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'b' + + '' + + '
      • ' + + '
      ' + + '

      p

      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'remove the first block of a list item', () => { + test.remove( + 'p' + + 'a' + + '[b1]' + + 'b2', + + '

      p

      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'a' + + '' + + '
      • ' + + '
      • ' + + '' + + '' + + 'b2' + + '' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 2 ) ); + } ); + + it( 'remove the last block of a list item', () => { + test.remove( + 'p' + + 'a1' + + '[a2]' + + 'b', + + '

      p

      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'a1' + + '' + + '
      • ' + + '
      • ' + + '' + + '' + + 'b' + + '' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'remove the middle block of a list item', () => { + test.remove( + 'p' + + 'a1' + + '[a2]' + + 'a3', + + '

      p

      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'a1' + + '' + + '

        a3

        ' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + } ); + } ); + + describe( 'change type', () => { + it( 'change first list item into bulleted', () => { + test.changeType( + 'p' + + '[a]' + + 'b' + + 'c', + + '

      p

      ' + + '
        ' + + '
      • a
      • ' + + '
      ' + + '
        ' + + '
      • ' + + '' + + '' + + 'b' + + '' + + '
      • ' + + '
      • ' + + '' + + '' + + 'c' + + '' + + '
      • ' + + '
      ', + () => 'bulleted' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + } ); + + it.skip( 'change middle list item', () => { + test.changeType( + 'p' + + 'a' + + '[b]' + + 'c', + + '

      p

      ' + + '
        ' + + '
      • a
      • ' + + '
      ' + + '
        ' + + '
      1. b
      2. ' + + '
      ' + + '
        ' + + '
      • c
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it.skip( 'change last list item', () => { + test.changeType( + 'p' + + 'a' + + 'b' + + '[c]', + + '

      p

      ' + + '
        ' + + '
      • a
      • ' + + '
      • b
      • ' + + '
      ' + + '
        ' + + '
      1. c
      2. ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it.skip( 'change only list item', () => { + test.changeType( + 'p' + + '[a]' + + 'p', + + '

      p

      ' + + '
        ' + + '
      1. a
      2. ' + + '
      ' + + '

      p

      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it.skip( 'change element at the edge of two different lists #1', () => { + test.changeType( + 'a' + + 'b' + + '[c]' + + 'd', + + '
        ' + + '
      • a
      • ' + + '
      • b
      • ' + + '
      ' + + '
        ' + + '
      1. c
      2. ' + + '
      3. d
      4. ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it.skip( 'change element at the edge of two different lists #2', () => { + test.changeType( + 'a' + + '[b]' + + 'c' + + 'd', + + '
        ' + + '
      1. a
      2. ' + + '
      3. b
      4. ' + + '
      ' + + '
        ' + + '
      • c
      • ' + + '
      • d
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it.skip( 'change multiple elements - to other type', () => { + test.changeType( + 'a' + + '[b' + + 'c]' + + 'd', + + '
        ' + + '
      • a
      • ' + + '
      ' + + '
        ' + + '
      1. b
      2. ' + + '
      3. c
      4. ' + + '
      ' + + '
        ' + + '
      • d
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it.skip( 'change multiple elements - to same type', () => { + test.changeType( + 'a' + + '[b' + + 'c]' + + 'd', + + '
        ' + + '
      1. a
      2. ' + + '
      3. b
      4. ' + + '
      5. c
      6. ' + + '
      7. d
      8. ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it.skip( 'change of the first block of a list item', () => { + test.changeType( + 'a' + + '[b1]' + + 'b2' + + 'c', + + '
        ' + + '
      • a
      • ' + + '
      ' + + '
        ' + + '
      1. b1
      2. ' + + '
      ' + + '
        ' + + '
      • b2
      • ' + + '
      • c
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 2 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 2 ) ); + } ); + + it.skip( 'change of the last block of a list item', () => { + test.changeType( + 'a' + + 'b1' + + '[b2]' + + 'c', + + '
        ' + + '
      • a
      • ' + + '
      • b1
      • ' + + '
      ' + + '
        ' + + '
      1. b2
      2. ' + + '
      ' + + '
        ' + + '
      • c
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 2 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 2 ) ); + } ); + + it.skip( 'change of the middle block of a list item', () => { + test.changeType( + 'a' + + 'b1' + + '[b2]' + + 'b3' + + 'c', + + '
        ' + + '
      • a
      • ' + + '
      • b1
      • ' + + '
      ' + + '
        ' + + '
      1. b2
      2. ' + + '
      ' + + '
        ' + + '
      • b3
      • ' + + '
      • c
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 3 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 2 ) ); + expect( test.reconvertSpy.thirdCall.firstArg ).to.equal( modelRoot.getChild( 3 ) ); + } ); + + it.skip( 'change outer list type with nested blockquote', () => { + test.changeType( + '[a]' + + '
      ' + + 'b' + + 'c' + + '
      ', + + '
        ' + + '
      1. ' + + 'a' + + '
          ' + + '
        • ' + + '
          ' + + '
            ' + + '
          • ' + + 'b' + + '
              ' + + '
            • c
            • ' + + '
            ' + + '
          • ' + + '
          ' + + '
          ' + + '
        • ' + + '
        ' + + '
      2. ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + } ); + + it.skip( 'change outer list type with nested code block', () => { + test.changeType( + '[a]' + + '' + + 'abc' + + '', + + '
        ' + + '
      1. ' + + 'a' + + '
          ' + + '
        • ' + + '
          ' +
          +					'abc' +
          +					'
          ' + + '
        • ' + + '
        ' + + '
      2. ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + } ); + } ); + + describe.skip( 'rename list item element', () => { + it( 'rename first list item', () => { + test.renameElement( + '[a]' + + 'b', + + '
        ' + + '
      • a

      • ' + + '
      • b
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'rename middle list item', () => { + test.renameElement( + 'a' + + '[b]' + + 'c', + + '
        ' + + '
      • a
      • ' + + '
      • b

      • ' + + '
      • c
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'rename last list item', () => { + test.renameElement( + 'a' + + '[b]', + + '
        ' + + '
      • a
      • ' + + '
      • b

      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'rename first list item to paragraph', () => { + test.renameElement( + '[a]' + + 'b', + + '
        ' + + '
      • a
      • ' + + '
      • b
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'rename middle list item to paragraph', () => { + test.renameElement( + 'a' + + '[b]' + + 'c', + + '
        ' + + '
      • a
      • ' + + '
      • b
      • ' + + '
      • c
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'rename last list item to paragraph', () => { + test.renameElement( + 'a' + + '[b]', + + '
        ' + + '
      • a
      • ' + + '
      • b
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'rename first block of list item', () => { + test.renameElement( + 'a' + + '[b1]' + + 'b2' + + 'c', + + '
        ' + + '
      • a
      • ' + + '
      • ' + + '

        b1

        ' + + '

        b2

        ' + + '
      • ' + + '
      • c
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'rename last block of list item', () => { + test.renameElement( + 'a' + + 'b1' + + '[b2]' + + 'c', + + '
        ' + + '
      • a
      • ' + + '
      • ' + + '

        b1

        ' + + '

        b2

        ' + + '
      • ' + + '
      • c
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'rename first block of list item to paragraph', () => { + test.renameElement( + 'a' + + '[b1]' + + 'b2' + + 'c', + + '
        ' + + '
      • a
      • ' + + '
      • ' + + '

        b1

        ' + + '

        b2

        ' + + '
      • ' + + '
      • c
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'rename last block of list item to paragraph', () => { + test.renameElement( + 'a' + + 'b1' + + '[b2]' + + 'c', + + '
        ' + + '
      • a
      • ' + + '
      • ' + + '

        b1

        ' + + '

        b2

        ' + + '
      • ' + + '
      • c
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + } ); + + describe.skip( 'remove list item attributes', () => { + it( 'first list item', () => { + test.removeListAttributes( + '[a]' + + 'b', + + '

      a

      ' + + '
        ' + + '
      • b
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 0 ) ); + } ); + + it( 'middle list item', () => { + test.removeListAttributes( + 'a' + + '[b]' + + 'c', + + '
        ' + + '
      • a
      • ' + + '
      ' + + '

      b

      ' + + '
        ' + + '
      • c
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + } ); + + it( 'last list item', () => { + test.removeListAttributes( + 'a' + + '[b]', + + '
        ' + + '
      • a
      • ' + + '
      ' + + '

      b

      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + } ); + + it( 'only list item', () => { + test.removeListAttributes( + 'p' + + '[x]' + + 'p', + + '

      p

      ' + + '

      x

      ' + + '

      p

      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + } ); + + it( 'on non paragraph', () => { + test.removeListAttributes( + '[a]' + + 'b', + + '

      a

      ' + + '
        ' + + '
      • b
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'first block of list item', () => { + test.removeListAttributes( + '[a1]' + + 'a2', + + '

      a1

      ' + + '
        ' + + '
      • a2
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + } ); + + it( 'last block of list item', () => { + test.removeListAttributes( + 'a1' + + '[a2]', + + '
        ' + + '
      • a1
      • ' + + '
      ' + + '

      a2

      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 0 ) ); + } ); + + it( 'middle block of list item', () => { + test.removeListAttributes( + 'a1' + + '[a2]' + + 'a3', + + '
        ' + + '
      • a1
      • ' + + '
      ' + + '

      a2

      ' + + '
        ' + + '
      • a3
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 2 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 0 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 2 ) ); + } ); + } ); + + describe.skip( 'set list item attributes', () => { + it( 'only paragraph', () => { + test.setListAttributes( 0, + '[a]', + + '
        ' + + '
      • a
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 0 ) ); + } ); + + it( 'on paragraph between paragraphs', () => { + test.setListAttributes( 0, + 'x' + + '[a]' + + 'x', + + '

      x

      ' + + '
        ' + + '
      • a
      • ' + + '
      ' + + '

      x

      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + } ); + + it( 'on element before list of same type', () => { + test.setListAttributes( 0, + '[x]' + + 'a', + + '
        ' + + '
      • x
      • ' + + '
      • a
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 0 ) ); + } ); + + it( 'on element after list of same type', () => { + test.setListAttributes( 0, + 'a' + + '[x]', + + '
        ' + + '
      • a
      • ' + + '
      • x
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + } ); + + it( 'on element before list of different type', () => { + test.setListAttributes( 0, + '[x]' + + 'a', + + '
        ' + + '
      • x
      • ' + + '
      ' + + '
        ' + + '
      1. a
      2. ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 0 ) ); + } ); + + it( 'on element after list of different type', () => { + test.setListAttributes( 0, + 'a' + + '[x]', + + '
        ' + + '
      1. a
      2. ' + + '
      ' + + '
        ' + + '
      • x
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + } ); + + it( 'on element between lists of same type', () => { + test.setListAttributes( 0, + 'a' + + '[x]' + + 'b', + + '
        ' + + '
      • a
      • ' + + '
      • x
      • ' + + '
      • b
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + } ); + + it( 'before list item with the same id', () => { + test.setListAttributes( 0, + '[x]' + + 'a' + + 'b', + + '
        ' + + '
      • ' + + '

        x

        ' + + '

        a

        ' + + '
      • ' + + '
      • b
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + } ); + + it( 'after list item with the same id', () => { + test.setListAttributes( 0, + 'a' + + '[x]' + + 'b', + + '
        ' + + '
      • ' + + '

        a

        ' + + '

        x

        ' + + '
      • ' + + '
      • b
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 0 ) ); + } ); + } ); + + describe.skip( 'move', () => { + it( 'list item inside same list', () => { + test.move( + 'p' + + 'a' + + '[b]' + + 'c', + + 4, // Move after last item. + + '

      p

      ' + + '
        ' + + '
      • a
      • ' + + '
      • c
      • ' + + '
      • b
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'out list item from list', () => { + test.move( + 'p' + + 'a' + + '[b]' + + 'p', + + 4, // Move after second paragraph. + + '

      p

      ' + + '
        ' + + '
      • a
      • ' + + '
      ' + + '

      p

      ' + + '
        ' + + '
      • b
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'the only list item', () => { + test.move( + 'p' + + '[a]' + + 'p', + + 3, // Move after second paragraph. + + '

      p

      ' + + '

      p

      ' + + '
        ' + + '
      • a
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'list item between two lists of same type', () => { + test.move( + 'a' + + '[b]' + + 'p' + + 'c' + + 'd', + + 4, // Move between list item "c" and list item "d'. + + '
        ' + + '
      • a
      • ' + + '
      ' + + '

      p

      ' + + '
        ' + + '
      • c
      • ' + + '
      • b
      • ' + + '
      • d
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'list item between two lists of different type', () => { + test.move( + 'a' + + '[b]' + + 'p' + + 'c' + + 'd', + + 4, // Move between list item "c" and list item "d'. + + '
        ' + + '
      • a
      • ' + + '
      ' + + '

      p

      ' + + '
        ' + + '
      1. c
      2. ' + + '
      ' + + '
        ' + + '
      • b
      • ' + + '
      ' + + '
        ' + + '
      1. d
      2. ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'element between list items', () => { + test.move( + 'a' + + 'b' + + '[p]', + + 1, // Move between list item "a" and list item "b'. + + '
        ' + + '
      • a
      • ' + + '
      ' + + '

      p

      ' + + '
        ' + + '
      • b
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + } ); + } ); + + describe.skip( 'nested lists', () => { + describe( 'insert', () => { + describe( 'same list type', () => { + it( 'after lower indent', () => { + test.insert( + 'p' + + '1' + + '[x]', + + '

      p

      ' + + '
        ' + + '
      • ' + + '1' + + '
          ' + + '
        • x
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'after lower indent (multi block)', () => { + test.insert( + 'p' + + '1a' + + '1b' + + '[xa' + + 'xb]', + + '

      p

      ' + + '
        ' + + '
      • ' + + '

        1a

        ' + + '

        1b

        ' + + '
          ' + + '
        • ' + + '

          xa

          ' + + '

          xb

          ' + + '
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'after lower indent, before same indent', () => { + test.insert( + 'p' + + '1' + + '[x]' + + '1.1', + + '

      p

      ' + + '
        ' + + '
      • ' + + '1' + + '
          ' + + '
        • x
        • ' + + '
        • 1.1
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'after lower indent, before same indent (multi block)', () => { + test.insert( + 'p' + + '1a' + + '1b' + + '[xa' + + 'xb]' + + '1.1a' + + '1.1b', + + '

      p

      ' + + '
        ' + + '
      • ' + + '

        1a

        ' + + '

        1b

        ' + + '
          ' + + '
        • ' + + '

          xa

          ' + + '

          xb

          ' + + '
        • ' + + '
        • ' + + '

          1.1a

          ' + + '

          1.1b

          ' + + '
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'after lower indent, before lower indent', () => { + test.insert( + 'p' + + '1' + + '[x]' + + '2', + + '

      p

      ' + + '
        ' + + '
      • ' + + '1' + + '
          ' + + '
        • x
        • ' + + '
        ' + + '
      • ' + + '
      • 2
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'after lower indent, before lower indent (multi block)', () => { + test.insert( + 'p' + + '1a' + + '1b' + + '[xa' + + 'xb]' + + '2a' + + '2b', + + '

      p

      ' + + '
        ' + + '
      • ' + + '

        1a

        ' + + '

        1b

        ' + + '
          ' + + '
        • ' + + '

          xa

          ' + + '

          xb

          ' + + '
        • ' + + '
        ' + + '
      • ' + + '
      • ' + + '

        2a

        ' + + '

        2b

        ' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'after same indent', () => { + test.insert( + 'p' + + '1' + + '1.1' + + '[x]', + + '

      p

      ' + + '
        ' + + '
      • ' + + '1' + + '
          ' + + '
        • 1.1
        • ' + + '
        • x
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'after same indent (multi block)', () => { + test.insert( + 'p' + + '1a' + + '1b' + + '1.1a' + + '1.1b' + + '[xa' + + 'xb]', + + '

      p

      ' + + '
        ' + + '
      • ' + + '

        1a

        ' + + '

        1b

        ' + + '
          ' + + '
        • ' + + '

          1.1a

          ' + + '

          1.1b

          ' + + '
        • ' + + '
        • ' + + '

          xa

          ' + + '

          xb

          ' + + '
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); + + it( 'after same indent, before higher indent', () => { + test.insert( + 'p' + + '1' + + '[x]' + + '1.1', + + '

      p

      ' + + '
        ' + + '
      • 1
      • ' + + '
      • ' + + 'x' + + '
          ' + + '
        • 1.1
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 3 ) ); + } ); + + it( 'after same indent, before higher indent (multi block)', () => { + test.insert( + 'p' + + '1a' + + '1b' + + '[xa' + + 'xb]' + + '1.1a' + + '1.1b', + + '

      p

      ' + + '
        ' + + '
      • ' + + '

        1a

        ' + + '

        1b

        ' + + '
      • ' + + '
      • ' + + '

        xa

        ' + + '

        xb

        ' + + '
          ' + + '
        • ' + + '

          1.1a

          ' + + '

          1.1b

          ' + + '
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 2 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 5 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 6 ) ); + } ); + + it( 'after higher indent, before higher indent', () => { + test.insert( + 'p' + + '1' + + '1.1' + + '[x]' + + '1.2', + + '

      p

      ' + + '
        ' + + '
      • ' + + '1' + + '
          ' + + '
        • 1.1
        • ' + + '
        ' + + '
      • ' + + '
      • ' + + 'x' + + '
          ' + + '
        • 1.2
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 4 ) ); + } ); + + it( 'after higher indent, before higher indent( multi block)', () => { + test.insert( + 'p' + + '1' + + '1.1' + + '1.1' + + '[x' + + 'x]' + + '1.2' + + '1.2', + + '

      p

      ' + + '
        ' + + '
      • ' + + '1' + + '
          ' + + '
        • ' + + '

          1.1

          ' + + '

          1.1

          ' + + '
        • ' + + '
        ' + + '
      • ' + + '
      • ' + + '

        x

        ' + + '

        x

        ' + + '
          ' + + '
        • ' + + '

          1.2

          ' + + '

          1.2

          ' + + '
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 2 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 6 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 7 ) ); + } ); + + it( 'list items with too big indent', () => { + test.insert( + 'a' + + 'b' + + '[x' + + 'x' + + 'x]' + + 'c', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • ' + + 'b' + + '
            ' + + '
          • ' + + 'x' + + '
              ' + + '
            • x
            • ' + + '
            ' + + '
          • ' + + '
          • x
          • ' + + '
          ' + + '
        • ' + + '
        • c
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'additional block before higher indent', () => { + test.insert( + 'p' + + '1' + + '[x]' + + '2', + + '

      p

      ' + + '
        ' + + '
      • ' + + '

        1

        ' + + '

        x

        ' + + '
          ' + + '
        • 2
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + } ); + } ); + + describe( 'different list type', () => { + it( 'after lower indent, before same indent', () => { + test.insert( + 'p' + + '1' + + '[x]' + + '1.1', + + '

      p

      ' + + '
        ' + + '
      • ' + + '1' + + '
          ' + + '
        1. x
        2. ' + + '
        ' + + '
          ' + + '
        • 1.1
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'after same indent', () => { + test.insert( + 'p' + + '1' + + '1.1' + + '[x]', + + '

      p

      ' + + '
        ' + + '
      • ' + + '1' + + '
          ' + + '
        • 1.1
        • ' + + '
        ' + + '
          ' + + '
        1. x
        2. ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'after same indent, before higher indent', () => { + test.insert( + 'p' + + '1' + + '[x]' + + '1.1', + + '

      p

      ' + + '
        ' + + '
      • 1
      • ' + + '
      ' + + '
        ' + + '
      1. ' + + 'x' + + '
          ' + + '
        • 1.1
        • ' + + '
        ' + + '
      2. ' + + '
      ' + ); + } ); + + it( 'after higher indent, before higher indent', () => { + test.insert( + 'p' + + '1' + + '1.1' + + '[x]' + + '1.2', + + '

      p

      ' + + '
        ' + + '
      • ' + + '1' + + '
          ' + + '
        • 1.1
        • ' + + '
        ' + + '
      • ' + + '
      ' + + '
        ' + + '
      1. ' + + 'x' + + '
          ' + + '
        • 1.2
        • ' + + '
        ' + + '
      2. ' + + '
      ' + ); + } ); + + it( 'after higher indent, in nested list, different type', () => { + test.insert( + 'a' + + 'b' + + 'c' + + '[x]', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • ' + + 'b' + + '
            ' + + '
          • c
          • ' + + '
          ' + + '
        • ' + + '
        ' + + '
          ' + + '
        1. x
        2. ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + } ); + + // This case is pretty complex but it tests various edge cases concerning splitting lists. + it( 'element between nested list items - complex', () => { + test.insert( + 'a' + + 'b' + + 'c' + + 'd' + + '[x]' + + 'e' + + 'f' + + 'g' + + 'h' + + 'i' + + 'j' + + 'p', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • ' + + 'b' + + '
            ' + + '
          • ' + + 'c' + + '
              ' + + '
            1. d
            2. ' + + '
            ' + + '
          • ' + + '
          ' + + '
        • ' + + '
        ' + + '
      • ' + + '
      ' + + '

      x

      ' + + '
        ' + + '
      1. e
      2. ' + + '
      ' + + '
        ' + + '
      • ' + + 'f' + + '
          ' + + '
        • g
        • ' + + '
        ' + + '
      • ' + + '
      • ' + + 'h' + + '
          ' + + '
        1. i
        2. ' + + '
        ' + + '
      • ' + + '
      ' + + '
        ' + + '
      1. j
      2. ' + + '
      ' + + '

      p

      ' + ); + } ); + + it( 'element before indent "hole"', () => { + test.insert( + '1' + + '1.1' + + '[x]' + + '1.1.1' + + '2', + + '
        ' + + '
      • ' + + '1' + + '
          ' + + '
        • 1.1
        • ' + + '
        ' + + '
      • ' + + '
      ' + + '

      x

      ' + + '
        ' + + '
      • 1.1.1
      • ' + + '
      • 2
      • ' + + '
      ' + ); + } ); + + it( 'two list items with mismatched types inserted in one batch', () => { + test.test( + 'a' + + 'b[]', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        ' + + '
          ' + + '
        1. c
        2. ' + + '
        ' + + '
          ' + + '
        • d
        • ' + + '
        ' + + '
      • ' + + '
      ', + + () => { + const item1 = 'c'; + const item2 = 'd'; + + model.change( writer => { + writer.append( parseModel( item1, model.schema ), modelRoot ); + writer.append( parseModel( item2, model.schema ), modelRoot ); + } ); + } + ); + } ); + } ); + + describe( 'remove', () => { + it( 'the first nested item', () => { + test.remove( + 'a' + + '[b]' + + 'c', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • c
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'nested item from the middle', () => { + test.remove( + 'a' + + 'b' + + '[c]' + + 'd', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        • d
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'the last nested item', () => { + test.remove( + 'a' + + 'b' + + '[c]', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'the only nested item', () => { + test.remove( + 'a' + + '[c]', + + '
        ' + + '
      • a
      • ' + + '
      ' + ); + } ); + + it( 'list item that separates two nested lists of same type', () => { + test.remove( + 'a' + + 'b' + + '[c]' + + 'd', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        1. b
        2. ' + + '
        3. d
        4. ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'list item that separates two nested lists of different type', () => { + test.remove( + 'a' + + 'b' + + '[c]' + + 'd', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        1. b
        2. ' + + '
        ' + + '
          ' + + '
        • d
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'item that has nested lists, previous item has same indent', () => { + test.remove( + 'a' + + '[b]' + + 'c' + + 'd', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • c
        • ' + + '
        • d
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'item that has nested lists, previous item has lower indent', () => { + test.remove( + 'a' + + '[b]' + + 'c' + + 'd', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • c
        • ' + + '
        • d
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'item that has nested lists, previous item has higher indent by 1', () => { + test.remove( + 'a' + + 'b' + + '[c]' + + 'd' + + 'e', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        • ' + + 'd' + + '
            ' + + '
          1. e
          2. ' + + '
          ' + + '
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'item that has nested lists, previous item has higher indent by 2', () => { + test.remove( + 'a' + + 'b' + + 'c' + + '[d]' + + 'e', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • ' + + 'b' + + '
            ' + + '
          • c
          • ' + + '
          ' + + '
        • ' + + '
        • e
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'first list item that has nested list', () => { + test.remove( + '[a]' + + 'b' + + 'c', + + '
        ' + + '
      • ' + + 'b' + + '
          ' + + '
        • c
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + } ); + + describe( 'change type', () => { + it( 'list item that has nested items', () => { + test.changeType( + '[a]' + + 'b' + + 'c', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        • c
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + // The change will be "prevented" by post fixer. + it( 'list item that is a nested item', () => { + test.changeType( + 'a' + + 'b' + + '[c]' + + 'd', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        1. b
        2. ' + + '
        ' + + '
          ' + + '
        • c
        • ' + + '
        ' + + '
          ' + + '
        1. d
        2. ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'changed list type at the same time as adding nested items', () => { + test.test( + 'a[]', + + '
        ' + + '
      1. ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        • c
        • ' + + '
        ' + + '
      2. ' + + '
      ', + + () => { + const item1 = 'b'; + const item2 = 'c'; + + model.change( writer => { + writer.setAttribute( 'listType', 'numbered', modelRoot.getChild( 0 ) ); + writer.append( parseModel( item1, model.schema ), modelRoot ); + writer.append( parseModel( item2, model.schema ), modelRoot ); + } ); + } + ); + } ); + } ); + + describe( 'change indent', () => { + describe( 'same list type', () => { + it( 'indent last item of flat list', () => { + test.changeIndent( + 1, + + 'a' + + '[b]', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'indent middle item of flat list', () => { + test.changeIndent( + 1, + + 'a' + + '[b]' + + 'c', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        ' + + '
      • ' + + '
      • c
      • ' + + '
      ' + ); + } ); + + it( 'indent last item in nested list', () => { + test.changeIndent( + 2, + + 'a' + + 'b' + + '[c]', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • ' + + 'b' + + '
            ' + + '
          • c
          • ' + + '
          ' + + '
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'indent middle item in nested list', () => { + test.changeIndent( + 2, + + 'a' + + 'b' + + '[c]' + + 'd', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • ' + + 'b' + + '
            ' + + '
          • c
          • ' + + '
          ' + + '
        • ' + + '
        • d
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + // Keep in mind that this test is different than "executing command on item that has nested list". + // A command is automatically indenting nested items so the hierarchy is preserved. + // Here we test conversion and the change is simple changing indent of one item. + // This may be true also for other tests in this suite, keep this in mind. + it( 'indent item that has nested list', () => { + test.changeIndent( + 1, + + 'a' + + '[b]' + + 'c', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        • c
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'indent item that in view is a next sibling of item that has nested list', () => { + test.changeIndent( + 1, + + 'a' + + 'b' + + '[c]' + + 'd', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        • c
        • ' + + '
        • d
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'outdent the first item of nested list', () => { + test.changeIndent( + 0, + + 'a' + + '[b]' + + 'c' + + 'd', + + '
        ' + + '
      • a
      • ' + + '
      • ' + + 'b' + + '
          ' + + '
        • c
        • ' + + '
        • d
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'outdent item from the middle of nested list', () => { + test.changeIndent( + 0, + + 'a' + + 'b' + + '[c]' + + 'd', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        ' + + '
      • ' + + '
      • ' + + 'c' + + '
          ' + + '
        • d
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'outdent the last item of nested list', () => { + test.changeIndent( + 0, + + 'a' + + 'b' + + '[c]', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        ' + + '
      • ' + + '
      • c
      • ' + + '
      ' + ); + } ); + + it( 'outdent the only item of nested list', () => { + test.changeIndent( + 1, + + 'a' + + 'b' + + '[c]' + + 'd', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        • c
        • ' + + '
        • d
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'outdent item by two', () => { + test.changeIndent( + 0, + + 'a' + + 'b' + + '[c]' + + 'd', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        ' + + '
      • ' + + '
      • c
      • ' + + '
      • d
      • ' + + '
      ' + ); + } ); + } ); + + describe( 'different list type', () => { + it( 'indent middle item of flat list', () => { + test.changeIndent( + 1, + + 'a' + + '[b]' + + 'c', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        1. b
        2. ' + + '
        ' + + '
      • ' + + '
      • c
      • ' + + '
      ' + ); + } ); + + it( 'indent item that has nested list', () => { + test.changeIndent( + 1, + + 'a' + + '[b]' + + 'c', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        1. b
        2. ' + + '
        ' + + '
          ' + + '
        • c
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'indent item that in view is a next sibling of item that has nested list #1', () => { + test.changeIndent( + 1, + + 'a' + + 'b' + + '[c]' + + 'd', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        ' + + '
          ' + + '
        1. c
        2. ' + + '
        ' + + '
          ' + + '
        • d
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'outdent the first item of nested list', () => { + test.changeIndent( + 0, + + 'a' + + '[b]' + + 'c' + + 'd', + + '
        ' + + '
      • a
      • ' + + '
      • ' + + 'b' + + '
          ' + + '
        • c
        • ' + + '
        • d
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'outdent the only item of nested list', () => { + test.changeIndent( + 1, + + 'a' + + 'b' + + '[c]' + + 'd', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        • c
        • ' + + '
        • d
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'outdent item by two', () => { + test.changeIndent( + 0, + + 'a' + + 'b' + + '[c]' + + 'd', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        ' + + '
      • ' + + '
      ' + + '
        ' + + '
      1. c
      2. ' + + '
      ' + + '
        ' + + '
      • d
      • ' + + '
      ' + ); + } ); + } ); + } ); + + describe( 'rename list item element', () => { + it( 'rename top list item', () => { + test.renameElement( + '[a]' + + 'b', + + '
        ' + + '
      • ' + + '

        a

        ' + + '
          ' + + '
        • ' + + 'b' + + + '
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'rename nested list item', () => { + test.renameElement( + 'a' + + '[b]', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • ' + + '

          b

          ' + + '
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + } ); + + describe( 'remove list item attributes', () => { + it( 'rename nested item from the middle #1', () => { + test.removeListAttributes( + 'a' + + 'b' + + '[c]' + + 'd', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        ' + + '
      • ' + + '
      ' + + '

      c

      ' + + '
        ' + + '
      • d
      • ' + + '
      ' + ); + } ); + + it( 'rename nested item from the middle #2 - nightmare example', () => { + test.removeListAttributes( + // Indents in this example should be fixed by post fixer. + // This nightmare example checks if structure of the list is kept as intact as possible. + 'a' + + 'b' + + '[c]' + + 'd' + + 'e' + + 'f' + + 'g' + + 'h' + + 'i' + + 'j' + + 'k' + + 'l' + + 'm', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        ' + + '
      • ' + + '
      ' + + '

      c

      ' + + '
        ' + + '
      • d
      • ' + + '
      • ' + + 'e' + + '
          ' + + '
        • f
        • ' + + '
        ' + + '
      • ' + + '
      • ' + + 'g' + + '
          ' + + '
        • ' + + 'h' + + '
            ' + + '
          • i
          • ' + + '
          ' + + '
        • ' + + '
        ' + + '
      • ' + + '
      • ' + + 'j' + + '
          ' + + '
        • k
        • ' + + '
        ' + + '
      • ' + + '
      • ' + + 'l' + + '
          ' + + '
        • m
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'rename nested item from the middle #3 - manual test example', () => { + test.removeListAttributes( + // Indents in this example should be fixed by post fixer. + // This example checks a bug found by testing manual test. + 'a' + + 'b' + + '[c]' + + 'd' + + 'e' + + 'f' + + 'g' + + 'h' + + '' + + '' + + 'k' + + 'l', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        ' + + '
      • ' + + '
      ' + + '

      c

      ' + + '
        ' + + '
      • ' + + 'd' + + '
          ' + + '
        • e
        • ' + + '
        • f
        • ' + + '
        • g
        • ' + + '
        • h
        • ' + + '
        ' + + '
      • ' + + '
      • ' + + '' + + '
          ' + + '
        • ' + + '' + + '
            ' + + '
          1. k
          2. ' + + '
          3. l
          4. ' + + '
          ' + + '
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'rename the only nested item', () => { + test.removeListAttributes( + 'a' + + '[b]', + + '
        ' + + '
      • a
      • ' + + '
      ' + + '

      b

      ' + ); + } ); + } ); + + describe( 'set list item attributes', () => { + it( 'element into first item in nested list', () => { + test.setListAttributes( + 1, + + 'a' + + '[b]', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'element into last item in nested list', () => { + test.setListAttributes( + 1, + + 'a' + + 'b' + + '[c]', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        • c
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'element into a first item in deeply nested list', () => { + test.setListAttributes( + 2, + + 'a' + + 'b' + + '[c]' + + 'd', + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • ' + + 'b' + + '
            ' + + '
          • c
          • ' + + '
          ' + + '
        • ' + + '
        ' + + '
      • ' + + '
      • d
      • ' + + '
      ' + ); + } ); + } ); + + describe( 'move', () => { + // Since move is in fact remove + insert and does not event have its own converter, only a few cases will be tested here. + it( 'out nested list items', () => { + test.move( + 'a' + + '[b' + + 'c]' + + 'd' + + 'e' + + 'x', + + 6, + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • ' + + 'd' + + '
            ' + + '
          • e
          • ' + + '
          ' + + '
        • ' + + '
        ' + + '
      • ' + + '
      ' + + '

      x

      ' + + '
        ' + + '
      • ' + + 'b' + + '
          ' + + '
        • c
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'nested list items between lists of same type', () => { + test.move( + 'a' + + 'b' + + '[c' + + 'd]' + + 'e' + + 'x' + + 'f' + + 'g', + + 7, + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • ' + + 'b' + + '
            ' + + '
          • e
          • ' + + '
          ' + + '
        • ' + + '
        ' + + '
      • ' + + '
      ' + + '

      x

      ' + + '
        ' + + '
      • ' + + 'f' + + '
          ' + + '
        • ' + + 'c' + + '
            ' + + '
          • d
          • ' + + '
          ' + + '
        • ' + + '
        ' + + '
      • ' + + '
      • g
      • ' + + '
      ' + ); + } ); + + it( 'nested list items between lists of different type', () => { + test.move( + 'a' + + 'b' + + '[c' + + 'd]' + + 'e' + + 'x' + + 'f' + + 'g', + + 7, + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • ' + + 'b' + + '
            ' + + '
          • e
          • ' + + '
          ' + + '
        • ' + + '
        ' + + '
      • ' + + '
      ' + + '

      x

      ' + + '
        ' + + '
      1. ' + + 'f' + + '
          ' + + '
        • ' + + 'c' + + '
            ' + + '
          • d
          • ' + + '
          ' + + '
        • ' + + '
        ' + + '
          ' + + '
        1. g
        2. ' + + '
        ' + + '
      2. ' + + '
      ' + ); + } ); + + it( 'element between nested list', () => { + test.move( + 'a' + + 'b' + + 'c' + + 'd' + + '[x]', + + 2, + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        ' + + '
      • ' + + '
      ' + + '

      x

      ' + + '
        ' + + '
      • ' + + 'c' + + '
          ' + + '
        • d
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'multiple nested list items of different types #1 - fix at start', () => { + test.move( + 'a' + + 'b' + + '[c' + + 'd' + + 'e]' + + 'f' + + 'g' + + 'h' + + 'i', + + 8, + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        ' + + '
          ' + + '
        1. f
        2. ' + + '
        ' + + '
      • ' + + '
      • ' + + 'g' + + '
          ' + + '
        1. h
        2. ' + + '
        ' + + '
          ' + + '
        • c
        • ' + + '
        ' + + '
      • ' + + '
      • ' + + 'd' + + '
          ' + + '
        1. e
        2. ' + + '
        3. i
        4. ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + + it( 'multiple nested list items of different types #2 - fix at end', () => { + test.move( + 'a' + + 'b' + + '[c' + + 'd' + + 'e]' + + 'f' + + 'g' + + 'h' + + 'i', + + 8, + + '
        ' + + '
      • ' + + 'a' + + '
          ' + + '
        • b
        • ' + + '
        ' + + '
          ' + + '
        1. f
        2. ' + + '
        ' + + '
      • ' + + '
      • ' + + 'g' + + '
          ' + + '
        • h
        • ' + + '
        • c
        • ' + + '
        ' + + '
      • ' + + '
      • ' + + 'd' + + '
          ' + + '
        1. e
        2. ' + + '
        ' + + '
          ' + + '
        • i
        • ' + + '
        ' + + '
      • ' + + '
      ' + ); + } ); + } ); + } ); +} ); From d641ec9913b82f287042f2f4613cf34597882e16 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 15 Sep 2023 16:48:10 +0200 Subject: [PATCH 33/54] Adding tests. --- .../tests/documentlist/_utils/utils.js | 11 +- ...odocumentlistediting-conversion-changes.js | 1427 ++++++++++++----- 2 files changed, 1075 insertions(+), 363 deletions(-) diff --git a/packages/ckeditor5-list/tests/documentlist/_utils/utils.js b/packages/ckeditor5-list/tests/documentlist/_utils/utils.js index d84e2de1329..5c15f135b73 100644 --- a/packages/ckeditor5-list/tests/documentlist/_utils/utils.js +++ b/packages/ckeditor5-list/tests/documentlist/_utils/utils.js @@ -116,10 +116,10 @@ export function setupTestHelpers( editor ) { test.test( input, output, actionCallback ); }, - changeType( input, output, newTypeFunc = type => type == 'numbered' ? 'bulleted' : 'numbered' ) { + changeType( input, output, type ) { const actionCallback = selection => { const element = selection.getFirstPosition().nodeAfter; - const newType = newTypeFunc( element.getAttribute( 'listType' ) ); + const newType = type || ( element.getAttribute( 'listType' ) == 'numbered' ? 'bulleted' : 'numbered' ); model.change( writer => { const itemsToChange = Array.from( selection.getSelectedBlocks() ); @@ -159,12 +159,15 @@ export function setupTestHelpers( editor ) { test.test( input, output, actionCallback, testUndo ); }, - setListAttributes( newIndent, input, output ) { + setListAttributes( newIndentOrType, input, output ) { + const newIndent = typeof newIndentOrType == 'number' ? newIndentOrType : 0; + const newType = typeof newIndentOrType == 'string' ? newIndentOrType : 'bulleted'; + const actionCallback = selection => { const element = selection.getFirstPosition().nodeAfter; model.change( writer => { - writer.setAttributes( { listType: 'bulleted', listIndent: newIndent, listItemId: 'x' }, element ); + writer.setAttributes( { listType: newType, listIndent: newIndent, listItemId: 'x' }, element ); } ); }; diff --git a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting-conversion-changes.js b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting-conversion-changes.js index 58ffc03bb61..8d28daf4aac 100644 --- a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting-conversion-changes.js +++ b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting-conversion-changes.js @@ -7,6 +7,8 @@ import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteedi import HeadingEditing from '@ckeditor/ckeditor5-heading/src/headingediting'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import TableEditing from '@ckeditor/ckeditor5-table/src/tableediting'; +import { UndoEditing } from '@ckeditor/ckeditor5-undo'; +import { CodeBlockEditing } from '@ckeditor/ckeditor5-code-block'; import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import { parse as parseModel } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; @@ -16,7 +18,6 @@ import TodoDocumentListEditing from '../../src/tododocumentlist/tododocumentlist import { setupTestHelpers } from '../documentlist/_utils/utils'; import stubUid from '../documentlist/_utils/uid'; -import { UndoEditing } from '@ckeditor/ckeditor5-undo'; describe( 'TodoDocumentListEditing - conversion - changes', () => { let editor, model, test, modelRoot; @@ -25,7 +26,7 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { beforeEach( async () => { editor = await VirtualTestEditor.create( { - plugins: [ Paragraph, TodoDocumentListEditing, BlockQuoteEditing, TableEditing, HeadingEditing, UndoEditing ] + plugins: [ Paragraph, TodoDocumentListEditing, BlockQuoteEditing, TableEditing, CodeBlockEditing, HeadingEditing, UndoEditing ] } ); model = editor.model; @@ -672,9 +673,9 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { 'c', '

      p

      ' + - '
        ' + + '
          ' + '
        1. a
        2. ' + - '
      ' + + '' + '
        ' + '
      • ' + '' + @@ -688,15 +689,14 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { 'c' + '' + '
      • ' + - '
      ', - () => 'bulleted' + '
    ' ); expect( test.reconvertSpy.callCount ).to.equal( 1 ); expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); } ); - it.skip( 'change middle list item', () => { + it( 'change middle list item', () => { test.changeType( 'p' + 'a' + @@ -704,21 +704,32 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { 'c', '

    p

    ' + - '
      ' + - '
    • a
    • ' + + '
        ' + + '
      • ' + + '' + + '' + + 'a' + + '' + + '
      • ' + '
      ' + '
        ' + - '
      1. b
      2. ' + + '
      3. b
      4. ' + '
      ' + - '
        ' + - '
      • c
      • ' + + '
          ' + + '
        • ' + + '' + + '' + + 'c' + + '' + + '
        • ' + '
        ' ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 2 ) ); } ); - it.skip( 'change last list item', () => { + it( 'change last list item', () => { test.changeType( 'p' + 'a' + @@ -726,19 +737,30 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { '[c]', '

        p

        ' + - '
          ' + - '
        • a
        • ' + - '
        • b
        • ' + + '
            ' + + '
          • ' + + '' + + '' + + 'a' + + '' + + '
          • ' + + '
          • ' + + '' + + '' + + 'b' + + '' + + '
          • ' + '
          ' + '
            ' + - '
          1. c
          2. ' + + '
          3. c
          4. ' + '
          ' ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 3 ) ); } ); - it.skip( 'change only list item', () => { + it( 'change only list item', () => { test.changeType( 'p' + '[a]' + @@ -746,110 +768,183 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { '

          p

          ' + '
            ' + - '
          1. a
          2. ' + + '
          3. a
          4. ' + '
          ' + '

          p

          ' ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); } ); - it.skip( 'change element at the edge of two different lists #1', () => { + it( 'change element into to-do list at the edge of two different lists (after to-do list)', () => { test.changeType( 'a' + 'b' + '[c]' + 'd', - '
            ' + - '
          • a
          • ' + - '
          • b
          • ' + + '
              ' + + '
            • ' + + '' + + '' + + 'a' + + '' + + '
            • ' + + '
            • ' + + '' + + '' + + 'b' + + '' + + '
            • ' + + '
            • ' + + '' + + '' + + 'c' + + '' + + '
            • ' + '
            ' + - '
              ' + - '
            1. c
            2. ' + - '
            3. d
            4. ' + - '
            ' + '
              ' + + '
            • d
            • ' + + '
            ', + + 'todo' ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 2 ) ); } ); - it.skip( 'change element at the edge of two different lists #2', () => { + it( 'change element into to-do list at the edge of two different lists (before to-do list)', () => { test.changeType( 'a' + '[b]' + 'c' + 'd', - '
              ' + - '
            1. a
            2. ' + - '
            3. b
            4. ' + - '
            ' + '
              ' + - '
            • c
            • ' + - '
            • d
            • ' + - '
            ' + '
          • a
          • ' + + '
          ' + + '
            ' + + '
          • ' + + '' + + '' + + 'b' + + '' + + '
          • ' + + '
          • ' + + '' + + '' + + 'c' + + '' + + '
          • ' + + '
          • ' + + '' + + '' + + 'd' + + '' + + '
          • ' + + '
          ', + + 'todo' ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); } ); - it.skip( 'change multiple elements - to other type', () => { + it( 'change element into other list at the edge of two different lists (after to-do list)', () => { test.changeType( 'a' + - '[b' + - 'c]' + - 'd', + 'b' + + '[c]' + + 'd', - '
            ' + - '
          • a
          • ' + + '
              ' + + '
            • ' + + '' + + '' + + 'a' + + '' + + '
            • ' + + '
            • ' + + '' + + '' + + 'b' + + '' + + '
            • ' + '
            ' + - '
              ' + - '
            1. b
            2. ' + - '
            3. c
            4. ' + - '
            ' + '
              ' + - '
            • d
            • ' + - '
            ' + '
          • c
          • ' + + '
          • d
          • ' + + '
          ', + + 'bulleted' ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 2 ) ); } ); - it.skip( 'change multiple elements - to same type', () => { + it( 'change element into other list at the edge of two different lists (before to-do list)', () => { test.changeType( - 'a' + - '[b' + - 'c]' + - 'd', + 'a' + + '[b]' + + 'c' + + 'd', - '
            ' + - '
          1. a
          2. ' + - '
          3. b
          4. ' + - '
          5. c
          6. ' + - '
          7. d
          8. ' + - '
          ' + '
            ' + + '
          • a
          • ' + + '
          • b
          • ' + + '
          ' + + '
            ' + + '
          • ' + + '' + + '' + + 'c' + + '' + + '
          • ' + + '
          • ' + + '' + + '' + + 'd' + + '' + + '
          • ' + + '
          ', + + 'bulleted' ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); } ); - it.skip( 'change of the first block of a list item', () => { + it( 'change multiple elements - to other type', () => { test.changeType( 'a' + - '[b1]' + - 'b2' + - 'c', + '[b' + + 'c]' + + 'd', - '
            ' + - '
          • a
          • ' + + '
              ' + + '
            • ' + + '' + + '' + + 'a' + + '' + + '
            • ' + '
            ' + '
              ' + - '
            1. b1
            2. ' + + '
            3. b
            4. ' + + '
            5. c
            6. ' + '
            ' + - '
              ' + - '
            • b2
            • ' + - '
            • c
            • ' + + '
                ' + + '
              • ' + + '' + + '' + + 'd' + + '' + + '
              • ' + '
              ' ); @@ -858,23 +953,41 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 2 ) ); } ); - it.skip( 'change of the last block of a list item', () => { + it( 'change multiple elements - to same type', () => { test.changeType( 'a' + - 'b1' + - '[b2]' + - 'c', + '[b' + + 'c]' + + 'd', - '
                ' + - '
              • a
              • ' + - '
              • b1
              • ' + - '
              ' + - '
                ' + - '
              1. b2
              2. ' + - '
              ' + - '
                ' + - '
              • c
              • ' + - '
              ' + '
                ' + + '
              • ' + + '' + + '' + + 'a' + + '' + + '
              • ' + + '
              • ' + + '' + + '' + + 'b' + + '' + + '
              • ' + + '
              • ' + + '' + + '' + + 'c' + + '' + + '
              • ' + + '
              • ' + + '' + + '' + + 'd' + + '' + + '
              • ' + + '
              ', + + 'todo' ); expect( test.reconvertSpy.callCount ).to.equal( 2 ); @@ -882,271 +995,685 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 2 ) ); } ); - it.skip( 'change of the middle block of a list item', () => { + it( 'change of the first block of a list item (from todo)', () => { test.changeType( 'a' + - 'b1' + - '[b2]' + - 'b3' + + '[b1]' + + 'b2' + 'c', + '
                ' + + '
              • ' + + '' + + '' + + 'a' + + '' + + '
              • ' + + '
              ' + + '
                ' + + '
              1. b1
              2. ' + + '
              ' + + '
                ' + + '
              • ' + + '' + + '' + + 'b2' + + '' + + '
              • ' + + '
              • ' + + '' + + '' + + 'c' + + '' + + '
              • ' + + '
              ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 2 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 2 ) ); + } ); + + it( 'change of the first block of a list item (into todo)', () => { + test.changeType( + 'a' + + '[b1]' + + 'b2' + + 'c', + '
                ' + '
              • a
              • ' + - '
              • b1
              • ' + + '
              ' + + '
                ' + + '
              • ' + + '' + + '' + + 'b1' + + '' + + '
              • ' + + + '
              ' + + '
                ' + + '
              • b2
              • ' + + '
              • c
              • ' + + '
              ', + + 'todo' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 2 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 2 ) ); + } ); + + it( 'change of the last block of a list item (from todo)', () => { + test.changeType( + 'a' + + 'b1' + + '[b2]' + + 'c', + + '
                ' + + '
              • ' + + '' + + '' + + 'a' + + '' + + '
              • ' + + '
              • ' + + '' + + '' + + 'b1' + + '' + + '
              • ' + '
              ' + '
                ' + - '
              1. b2
              2. ' + + '
              3. b2
              4. ' + '
              ' + + '
                ' + + '
              • ' + + '' + + '' + + 'c' + + '' + + '
              • ' + + '
              ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 2 ) ); + } ); + + it( 'change of the last block of a list item (into todo)', () => { + test.changeType( + 'a' + + 'b1' + + '[b2]' + + 'c', + '
                ' + - '
              • b3
              • ' + - '
              • c
              • ' + + '
              • a
              • ' + + '
              • b1
              • ' + + '
              ' + + '
                ' + + '
              • ' + + '' + + '' + + 'b2' + + '' + + '
              • ' + + '
              ' + + '
                ' + + '
              • c
              • ' + + '
              ', + + 'todo' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 2 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 2 ) ); + } ); + + it( 'change of the middle block of a list item (from todo)', () => { + test.changeType( + 'a' + + 'b1' + + '[b2]' + + 'b3' + + 'c', + + '
                ' + + '
              • ' + + '' + + '' + + 'a' + + '' + + '
              • ' + + '
              • ' + + '' + + '' + + 'b1' + + '' + + '
              • ' + + '
              ' + + '
                ' + + '
              1. b2
              2. ' + + '
              ' + + '
                ' + + '
              • ' + + '' + + '' + + 'b3' + + '' + + '
              • ' + + '
              • ' + + '' + + '' + + 'c' + + '' + + '
              • ' + '
              ' ); + expect( test.reconvertSpy.callCount ).to.equal( 2 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 2 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 3 ) ); + } ); + + it( 'change of the middle block of a list item (into todo)', () => { + test.changeType( + 'a' + + 'b1' + + '[b2]' + + 'b3' + + 'c', + + '
                ' + + '
              • a
              • ' + + '
              • b1
              • ' + + '
              ' + + '
                ' + + '
              • ' + + '' + + '' + + 'b2' + + '' + + '
              • ' + + '
              ' + + '
                ' + + '
              • b3
              • ' + + '
              • c
              • ' + + '
              ', + + 'todo' + ); + expect( test.reconvertSpy.callCount ).to.equal( 3 ); expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 2 ) ); expect( test.reconvertSpy.thirdCall.firstArg ).to.equal( modelRoot.getChild( 3 ) ); } ); - it.skip( 'change outer list type with nested blockquote', () => { + it( 'change outer list type with nested blockquote (from todo)', () => { test.changeType( '[a]' + '
              ' + - 'b' + - 'c' + + 'b' + + 'c' + '
              ', '
                ' + - '
              1. ' + - 'a' + - '
                  ' + - '
                • ' + - '
                  ' + - '
                    ' + - '
                  • ' + - 'b' + - '
                      ' + - '
                    • c
                    • ' + - '
                    ' + - '
                  • ' + - '
                  ' + - '
                  ' + - '
                • ' + - '
                ' + - '
              2. ' + + '
              3. ' + + 'a' + + '
                  ' + + '
                • ' + + '' + + '' + + '' + + '
                  ' + + '
                    ' + + '
                  • ' + + '' + + '' + + 'b' + + '' + + '
                      ' + + '
                    • ' + + '' + + '' + + '' + + 'c' + + '' + + '
                    • ' + + '
                    ' + + '
                  • ' + + '
                  ' + + '
                  ' + + '
                • ' + + '
                ' + + '
              4. ' + '
              ' ); - expect( test.reconvertSpy.callCount ).to.equal( 1 ); - expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + expect( test.reconvertSpy.callCount ).to.equal( 2 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 0 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + } ); + + it( 'change outer list type with nested blockquote (into todo)', () => { + test.changeType( + '[a]' + + '
              ' + + 'b' + + 'c' + + '
              ', + + '
                ' + + '
              • ' + + '' + + '' + + 'a' + + '' + + '
                  ' + + '
                • ' + + '
                  ' + + '
                    ' + + '
                  • ' + + 'b' + + '
                      ' + + '
                    • c
                    • ' + + '
                    ' + + '
                  • ' + + '
                  ' + + '
                  ' + + '
                • ' + + '
                ' + + '
              • ' + + '
              ', + + 'todo' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 2 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 0 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); } ); - it.skip( 'change outer list type with nested code block', () => { + it( 'change outer list type with nested code block (from todo)', () => { test.changeType( '[a]' + '' + - 'abc' + + 'abc' + '', '
                ' + - '
              1. ' + - 'a' + - '
                  ' + - '
                • ' + - '
                  ' +
                  -					'abc' +
                  -					'
                  ' + - '
                • ' + - '
                ' + - '
              2. ' + + '
              3. ' + + 'a' + + '
                  ' + + '
                • ' + + '
                  ' +
                  +										'abc' +
                  +									'
                  ' + + '
                • ' + + '
                ' + + '
              4. ' + '
              ' ); - expect( test.reconvertSpy.callCount ).to.equal( 1 ); - expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + expect( test.reconvertSpy.callCount ).to.equal( 2 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 0 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + } ); + + it( 'change outer list type with nested code block (into todo)', () => { + test.changeType( + '[a]' + + '' + + 'abc' + + '', + + '
                ' + + '
              • ' + + '' + + '' + + 'a' + + '' + + '
                  ' + + '
                • ' + + '
                  ' +
                  +										'abc' +
                  +									'
                  ' + + '
                • ' + + '
                ' + + '
              • ' + + '
              ', + + 'todo' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 2 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 0 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); } ); } ); - describe.skip( 'rename list item element', () => { + describe( 'rename list item element', () => { it( 'rename first list item', () => { test.renameElement( - '[a]' + - 'b', + '[a]' + + 'b', - '
                ' + - '
              • a

              • ' + - '
              • b
              • ' + + '
                  ' + + '
                • ' + + '' + + '' + + '' + + '

                  a

                  ' + + '
                • ' + + '
                • ' + + '' + + '' + + 'b' + + '' + + '
                • ' + '
                ' ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 0 ) ); } ); it( 'rename middle list item', () => { test.renameElement( - 'a' + - '[b]' + - 'c', + 'a' + + '[b]' + + 'c', - '
                  ' + - '
                • a
                • ' + - '
                • b

                • ' + - '
                • c
                • ' + + '
                    ' + + '
                  • ' + + '' + + '' + + 'a' + + '' + + '
                  • ' + + '
                  • ' + + '' + + '' + + '' + + '

                    b

                    ' + + '
                  • ' + + '
                  • ' + + '' + + '' + + 'c' + + '' + + '
                  • ' + '
                  ' ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); } ); it( 'rename last list item', () => { test.renameElement( - 'a' + - '[b]', + 'a' + + '[b]', - '
                    ' + - '
                  • a
                  • ' + - '
                  • b

                  • ' + + '
                      ' + + '
                    • ' + + '' + + '' + + 'a' + + '' + + '
                    • ' + + '
                    • ' + + '' + + '' + + '' + + '

                      b

                      ' + + '
                    • ' + '
                    ' ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); } ); it( 'rename first list item to paragraph', () => { test.renameElement( - '[a]' + - 'b', + '[a]' + + 'b', - '
                      ' + - '
                    • a
                    • ' + - '
                    • b
                    • ' + + '
                        ' + + '
                      • ' + + '' + + '' + + 'a' + + '' + + '
                      • ' + + '
                      • ' + + '' + + '' + + 'b' + + '' + + '
                      • ' + '
                      ' ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 0 ) ); } ); it( 'rename middle list item to paragraph', () => { test.renameElement( - 'a' + - '[b]' + - 'c', + 'a' + + '[b]' + + 'c', - '
                        ' + - '
                      • a
                      • ' + - '
                      • b
                      • ' + - '
                      • c
                      • ' + + '
                          ' + + '
                        • ' + + '' + + '' + + 'a' + + '' + + '
                        • ' + + '
                        • ' + + '' + + '' + + 'b' + + '' + + '
                        • ' + + '
                        • ' + + '' + + '' + + 'c' + + '' + + '
                        • ' + '
                        ' ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); } ); it( 'rename last list item to paragraph', () => { test.renameElement( - 'a' + - '[b]', + 'a' + + '[b]', - '
                          ' + - '
                        • a
                        • ' + - '
                        • b
                        • ' + + '
                            ' + + '
                          • ' + + '' + + '' + + 'a' + + '' + + '
                          • ' + + '
                          • ' + + '' + + '' + + 'b' + + '' + + '
                          • ' + '
                          ' ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); } ); it( 'rename first block of list item', () => { test.renameElement( - 'a' + - '[b1]' + - 'b2' + - 'c', + 'a' + + '[b1]' + + 'b2' + + 'c', - '
                            ' + - '
                          • a
                          • ' + - '
                          • ' + - '

                            b1

                            ' + - '

                            b2

                            ' + - '
                          • ' + - '
                          • c
                          • ' + + '
                              ' + + '
                            • ' + + '' + + '' + + 'a' + + '' + + '
                            • ' + + '
                            • ' + + '' + + '' + + '' + + '

                              b1

                              ' + + '

                              b2

                              ' + + '
                            • ' + + '
                            • ' + + '' + + '' + + 'c' + + '' + + '
                            • ' + '
                            ' ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); } ); it( 'rename last block of list item', () => { test.renameElement( - 'a' + - 'b1' + - '[b2]' + - 'c', + 'a' + + 'b1' + + '[b2]' + + 'c', - '
                              ' + - '
                            • a
                            • ' + - '
                            • ' + - '

                              b1

                              ' + - '

                              b2

                              ' + - '
                            • ' + - '
                            • c
                            • ' + + '
                                ' + + '
                              • ' + + '' + + '' + + 'a' + + '' + + '
                              • ' + + '
                              • ' + + '' + + '' + + 'b1' + + '' + + '

                                b2

                                ' + + '
                              • ' + + '
                              • ' + + '' + + '' + + 'c' + + '' + + '
                              • ' + '
                              ' ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); } ); it( 'rename first block of list item to paragraph', () => { test.renameElement( - 'a' + - '[b1]' + - 'b2' + - 'c', + 'a' + + '[b1]' + + 'b2' + + 'c', - '
                                ' + - '
                              • a
                              • ' + - '
                              • ' + - '

                                b1

                                ' + - '

                                b2

                                ' + - '
                              • ' + - '
                              • c
                              • ' + + '
                                  ' + + '
                                • ' + + '' + + '' + + 'a' + + '' + + '
                                • ' + + '
                                • ' + + '' + + '' + + 'b1' + + '' + + '

                                  b2

                                  ' + + '
                                • ' + + '
                                • ' + + '' + + '' + + 'c' + + '' + + '
                                • ' + '
                                ' ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); } ); it( 'rename last block of list item to paragraph', () => { test.renameElement( - 'a' + - 'b1' + - '[b2]' + - 'c', + 'a' + + 'b1' + + '[b2]' + + 'c', - '
                                  ' + - '
                                • a
                                • ' + - '
                                • ' + - '

                                  b1

                                  ' + - '

                                  b2

                                  ' + - '
                                • ' + - '
                                • c
                                • ' + + '
                                    ' + + '
                                  • ' + + '' + + '' + + 'a' + + '' + + '
                                  • ' + + '
                                  • ' + + '' + + '' + + 'b1' + + '' + + '

                                    b2

                                    ' + + '
                                  • ' + + '
                                  • ' + + '' + + '' + + 'c' + + '' + + '
                                  • ' + '
                                  ' ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); } ); } ); - describe.skip( 'remove list item attributes', () => { + describe( 'remove list item attributes', () => { it( 'first list item', () => { test.removeListAttributes( - '[a]' + - 'b', + '[a]' + + 'b', '

                                  a

                                  ' + - '
                                    ' + - '
                                  • b
                                  • ' + + '
                                      ' + + '
                                    • ' + + '' + + '' + + 'b' + + '' + + '
                                    • ' + '
                                    ' ); @@ -1156,16 +1683,26 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { it( 'middle list item', () => { test.removeListAttributes( - 'a' + - '[b]' + - 'c', + 'a' + + '[b]' + + 'c', - '
                                      ' + - '
                                    • a
                                    • ' + + '
                                        ' + + '
                                      • ' + + '' + + '' + + 'a' + + '' + + '
                                      • ' + '
                                      ' + '

                                      b

                                      ' + - '
                                        ' + - '
                                      • c
                                      • ' + + '
                                          ' + + '
                                        • ' + + '' + + '' + + 'c' + + '' + + '
                                        • ' + '
                                        ' ); @@ -1175,11 +1712,16 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { it( 'last list item', () => { test.removeListAttributes( - 'a' + - '[b]', + 'a' + + '[b]', - '
                                          ' + - '
                                        • a
                                        • ' + + '
                                            ' + + '
                                          • ' + + '' + + '' + + 'a' + + '' + + '
                                          • ' + '
                                          ' + '

                                          b

                                          ' ); @@ -1191,7 +1733,7 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { it( 'only list item', () => { test.removeListAttributes( 'p' + - '[x]' + + '[x]' + 'p', '

                                          p

                                          ' + @@ -1205,76 +1747,106 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { it( 'on non paragraph', () => { test.removeListAttributes( - '[a]' + - 'b', + '[a]' + + 'b', '

                                          a

                                          ' + - '
                                            ' + - '
                                          • b
                                          • ' + + '
                                              ' + + '
                                            • ' + + '' + + '' + + 'b' + + '' + + '
                                            • ' + '
                                            ' ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 0 ) ); } ); it( 'first block of list item', () => { test.removeListAttributes( - '[a1]' + - 'a2', + '[a1]' + + 'a2', '

                                            a1

                                            ' + - '
                                              ' + - '
                                            • a2
                                            • ' + + '
                                                ' + + '
                                              • ' + + '' + + '' + + 'a2' + + '' + + '
                                              • ' + '
                                              ' ); - expect( test.reconvertSpy.callCount ).to.equal( 1 ); - expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + expect( test.reconvertSpy.callCount ).to.equal( 2 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 0 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); } ); it( 'last block of list item', () => { test.removeListAttributes( - 'a1' + - '[a2]', + 'a1' + + '[a2]', - '
                                                ' + - '
                                              • a1
                                              • ' + + '
                                                  ' + + '
                                                • ' + + '' + + '' + + 'a1' + + '' + + '
                                                • ' + '
                                                ' + '

                                                a2

                                                ' ); - expect( test.reconvertSpy.callCount ).to.equal( 1 ); - expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 0 ) ); + expect( test.reconvertSpy.callCount ).to.equal( 0 ); } ); it( 'middle block of list item', () => { test.removeListAttributes( - 'a1' + - '[a2]' + - 'a3', + 'a1' + + '[a2]' + + 'a3', - '
                                                  ' + - '
                                                • a1
                                                • ' + + '
                                                    ' + + '
                                                  • ' + + '' + + '' + + 'a1' + + '' + + '
                                                  • ' + '
                                                  ' + '

                                                  a2

                                                  ' + - '
                                                    ' + - '
                                                  • a3
                                                  • ' + + '
                                                      ' + + '
                                                    • ' + + '' + + '' + + 'a3' + + '' + + '
                                                    • ' + '
                                                    ' ); - expect( test.reconvertSpy.callCount ).to.equal( 2 ); - expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 0 ) ); - expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 2 ) ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 2 ) ); } ); } ); - describe.skip( 'set list item attributes', () => { + describe( 'set list item attributes', () => { it( 'only paragraph', () => { - test.setListAttributes( 0, + test.setListAttributes( 'todo', '[a]', - '
                                                      ' + - '
                                                    • a
                                                    • ' + + '
                                                        ' + + '
                                                      • ' + + '' + + '' + + 'a' + + '' + + '
                                                      • ' + '
                                                      ' ); @@ -1283,14 +1855,19 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { } ); it( 'on paragraph between paragraphs', () => { - test.setListAttributes( 0, + test.setListAttributes( 'todo', 'x' + '[a]' + 'x', '

                                                      x

                                                      ' + - '
                                                        ' + - '
                                                      • a
                                                      • ' + + '
                                                          ' + + '
                                                        • ' + + '' + + '' + + 'a' + + '' + + '
                                                        • ' + '
                                                        ' + '

                                                        x

                                                        ' ); @@ -1300,13 +1877,23 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { } ); it( 'on element before list of same type', () => { - test.setListAttributes( 0, + test.setListAttributes( 'todo', '[x]' + - 'a', + 'a', - '
                                                          ' + - '
                                                        • x
                                                        • ' + - '
                                                        • a
                                                        • ' + + '
                                                            ' + + '
                                                          • ' + + '' + + '' + + 'x' + + '' + + '
                                                          • ' + + '
                                                          • ' + + '' + + '' + + 'a' + + '' + + '
                                                          • ' + '
                                                          ' ); @@ -1315,13 +1902,23 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { } ); it( 'on element after list of same type', () => { - test.setListAttributes( 0, - 'a' + + test.setListAttributes( 'todo', + 'a' + '[x]', - '
                                                            ' + - '
                                                          • a
                                                          • ' + - '
                                                          • x
                                                          • ' + + '
                                                              ' + + '
                                                            • ' + + '' + + '' + + 'a' + + '' + + '
                                                            • ' + + '
                                                            • ' + + '' + + '' + + 'x' + + '' + + '
                                                            • ' + '
                                                            ' ); @@ -1330,15 +1927,20 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { } ); it( 'on element before list of different type', () => { - test.setListAttributes( 0, + test.setListAttributes( 'todo', '[x]' + 'a', - '
                                                              ' + - '
                                                            • x
                                                            • ' + + '
                                                                ' + + '
                                                              • ' + + '' + + '' + + 'x' + + '' + + '
                                                              • ' + '
                                                              ' + '
                                                                ' + - '
                                                              1. a
                                                              2. ' + + '
                                                              3. a
                                                              4. ' + '
                                                              ' ); @@ -1347,15 +1949,20 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { } ); it( 'on element after list of different type', () => { - test.setListAttributes( 0, + test.setListAttributes( 'todo', 'a' + '[x]', '
                                                                ' + - '
                                                              1. a
                                                              2. ' + + '
                                                              3. a
                                                              4. ' + '
                                                              ' + - '
                                                                ' + - '
                                                              • x
                                                              • ' + + '
                                                                  ' + + '
                                                                • ' + + '' + + '' + + 'x' + + '' + + '
                                                                • ' + '
                                                                ' ); @@ -1364,15 +1971,30 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { } ); it( 'on element between lists of same type', () => { - test.setListAttributes( 0, - 'a' + + test.setListAttributes( 'todo', + 'a' + '[x]' + - 'b', + 'b', - '
                                                                  ' + - '
                                                                • a
                                                                • ' + - '
                                                                • x
                                                                • ' + - '
                                                                • b
                                                                • ' + + '
                                                                    ' + + '
                                                                  • ' + + '' + + '' + + 'a' + + '' + + '
                                                                  • ' + + '
                                                                  • ' + + '' + + '' + + 'x' + + '' + + '
                                                                  • ' + + '
                                                                  • ' + + '' + + '' + + 'b' + + '' + + '
                                                                  • ' + '
                                                                  ' ); @@ -1381,36 +2003,53 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { } ); it( 'before list item with the same id', () => { - test.setListAttributes( 0, + test.setListAttributes( 'todo', '[x]' + - 'a' + - 'b', + 'a' + + 'b', - '
                                                                    ' + - '
                                                                  • ' + - '

                                                                    x

                                                                    ' + - '

                                                                    a

                                                                    ' + - '
                                                                  • ' + - '
                                                                  • b
                                                                  • ' + + '
                                                                      ' + + '
                                                                    • ' + + '' + + '' + + 'x' + + '' + + '

                                                                      a

                                                                      ' + + '
                                                                    • ' + + '
                                                                    • ' + + '' + + '' + + 'b' + + '' + + '
                                                                    • ' + '
                                                                    ' ); - expect( test.reconvertSpy.callCount ).to.equal( 1 ); - expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + expect( test.reconvertSpy.callCount ).to.equal( 2 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 0 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); } ); it( 'after list item with the same id', () => { - test.setListAttributes( 0, - 'a' + + test.setListAttributes( 'todo', + 'a' + '[x]' + - 'b', + 'b', - '
                                                                      ' + - '
                                                                    • ' + - '

                                                                      a

                                                                      ' + - '

                                                                      x

                                                                      ' + - '
                                                                    • ' + - '
                                                                    • b
                                                                    • ' + + '
                                                                        ' + + '
                                                                      • ' + + '' + + '' + + 'a' + + '' + + '

                                                                        x

                                                                        ' + + '
                                                                      • ' + + '
                                                                      • ' + + '' + + '' + + 'b' + + '' + + '
                                                                      • ' + '
                                                                      ' ); @@ -1419,21 +2058,36 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { } ); } ); - describe.skip( 'move', () => { + describe( 'move', () => { it( 'list item inside same list', () => { test.move( 'p' + - 'a' + - '[b]' + - 'c', + 'a' + + '[b]' + + 'c', 4, // Move after last item. '

                                                                      p

                                                                      ' + - '
                                                                        ' + - '
                                                                      • a
                                                                      • ' + - '
                                                                      • c
                                                                      • ' + - '
                                                                      • b
                                                                      • ' + + '
                                                                          ' + + '
                                                                        • ' + + '' + + '' + + 'a' + + '' + + '
                                                                        • ' + + '
                                                                        • ' + + '' + + '' + + 'c' + + '' + + '
                                                                        • ' + + '
                                                                        • ' + + '' + + '' + + 'b' + + '' + + '
                                                                        • ' + '
                                                                        ' ); @@ -1443,19 +2097,29 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { it( 'out list item from list', () => { test.move( 'p' + - 'a' + - '[b]' + + 'a' + + '[b]' + 'p', 4, // Move after second paragraph. '

                                                                        p

                                                                        ' + - '
                                                                          ' + - '
                                                                        • a
                                                                        • ' + + '
                                                                            ' + + '
                                                                          • ' + + '' + + '' + + 'a' + + '' + + '
                                                                          • ' + '
                                                                          ' + '

                                                                          p

                                                                          ' + - '
                                                                            ' + - '
                                                                          • b
                                                                          • ' + + '
                                                                              ' + + '
                                                                            • ' + + '' + + '' + + 'b' + + '' + + '
                                                                            • ' + '
                                                                            ' ); @@ -1465,15 +2129,20 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { it( 'the only list item', () => { test.move( 'p' + - '[a]' + + '[a]' + 'p', 3, // Move after second paragraph. '

                                                                            p

                                                                            ' + '

                                                                            p

                                                                            ' + - '
                                                                              ' + - '
                                                                            • a
                                                                            • ' + + '
                                                                                ' + + '
                                                                              • ' + + '' + + '' + + 'a' + + '' + + '
                                                                              • ' + '
                                                                              ' ); @@ -1482,22 +2151,42 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { it( 'list item between two lists of same type', () => { test.move( - 'a' + - '[b]' + + 'a' + + '[b]' + 'p' + - 'c' + - 'd', + 'c' + + 'd', 4, // Move between list item "c" and list item "d'. - '
                                                                                ' + - '
                                                                              • a
                                                                              • ' + + '
                                                                                  ' + + '
                                                                                • ' + + '' + + '' + + 'a' + + '' + + '
                                                                                • ' + '
                                                                                ' + '

                                                                                p

                                                                                ' + - '
                                                                                  ' + - '
                                                                                • c
                                                                                • ' + - '
                                                                                • b
                                                                                • ' + - '
                                                                                • d
                                                                                • ' + + '
                                                                                    ' + + '
                                                                                  • ' + + '' + + '' + + 'c' + + '' + + '
                                                                                  • ' + + '
                                                                                  • ' + + '' + + '' + + 'b' + + '' + + '
                                                                                  • ' + + '
                                                                                  • ' + + '' + + '' + + 'd' + + '' + + '
                                                                                  • ' + '
                                                                                  ' ); @@ -1506,26 +2195,36 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { it( 'list item between two lists of different type', () => { test.move( - 'a' + - '[b]' + + 'a' + + '[b]' + 'p' + 'c' + 'd', 4, // Move between list item "c" and list item "d'. - '
                                                                                    ' + - '
                                                                                  • a
                                                                                  • ' + + '
                                                                                      ' + + '
                                                                                    • ' + + '' + + '' + + 'a' + + '' + + '
                                                                                    • ' + '
                                                                                    ' + '

                                                                                    p

                                                                                    ' + '
                                                                                      ' + - '
                                                                                    1. c
                                                                                    2. ' + + '
                                                                                    3. c
                                                                                    4. ' + '
                                                                                    ' + - '
                                                                                      ' + - '
                                                                                    • b
                                                                                    • ' + + '
                                                                                        ' + + '
                                                                                      • ' + + '' + + '' + + 'b' + + '' + + '
                                                                                      • ' + '
                                                                                      ' + '
                                                                                        ' + - '
                                                                                      1. d
                                                                                      2. ' + + '
                                                                                      3. d
                                                                                      4. ' + '
                                                                                      ' ); @@ -1534,18 +2233,28 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { it( 'element between list items', () => { test.move( - 'a' + - 'b' + + 'a' + + 'b' + '[p]', 1, // Move between list item "a" and list item "b'. - '
                                                                                        ' + - '
                                                                                      • a
                                                                                      • ' + + '
                                                                                          ' + + '
                                                                                        • ' + + '' + + '' + + 'a' + + '' + + '
                                                                                        • ' + '
                                                                                        ' + '

                                                                                        p

                                                                                        ' + - '
                                                                                          ' + - '
                                                                                        • b
                                                                                        • ' + + '
                                                                                            ' + + '
                                                                                          • ' + + '' + + '' + + 'b' + + '' + + '
                                                                                          • ' + '
                                                                                          ' ); From b44ed267f79ebc211defe8a50d6ac05c5505fb1b Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Fri, 15 Sep 2023 18:51:01 +0200 Subject: [PATCH 34/54] Tests: check todo document list command. --- .../checktododocumentlistcommand.js | 150 +++++++++++++++++- 1 file changed, 146 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-list/tests/tododocumentlist/checktododocumentlistcommand.js b/packages/ckeditor5-list/tests/tododocumentlist/checktododocumentlistcommand.js index 5b1d9a8c8a1..bff8ed6a9ae 100644 --- a/packages/ckeditor5-list/tests/tododocumentlist/checktododocumentlistcommand.js +++ b/packages/ckeditor5-list/tests/tododocumentlist/checktododocumentlistcommand.js @@ -3,6 +3,7 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ +import HeadingEditing from '@ckeditor/ckeditor5-heading/src/headingediting'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; @@ -17,7 +18,7 @@ describe( 'CheckTodoListCommand', () => { beforeEach( () => { return VirtualTestEditor .create( { - plugins: [ Paragraph, TodoDocumentListEditing ] + plugins: [ Paragraph, HeadingEditing, TodoDocumentListEditing ] } ) .then( newEditor => { editor = newEditor; @@ -33,12 +34,18 @@ describe( 'CheckTodoListCommand', () => { } ); describe( 'isEnabled', () => { - it( 'should be enabled when selection is inside to-do list item', () => { + it( 'should be enabled when collapsed selection is inside to-do list item', () => { setModelData( model, 'f[]oo' ); expect( command.isEnabled ).to.equal( true ); } ); + it( 'should be enabled when non-collapsed selection is inside to-do list item', () => { + setModelData( model, 'f[o]o' ); + + expect( command.isEnabled ).to.equal( true ); + } ); + it( 'should be disabled when selection is not inside to-do list item', () => { setModelData( model, 'f[]oo' ); @@ -65,21 +72,80 @@ describe( 'CheckTodoListCommand', () => { expect( command.isEnabled ).to.equal( false ); } ); + + it( 'should be enabled when a to-do list item is selected together with other list items', () => { + setModelData( model, + 'fo[o' + + 'bar' + + 'b]az' + ); + + expect( command.isEnabled ).to.equal( true ); + } ); + + it( 'should be enabled when a to-do list item is selected together with other list items in nested list', () => { + setModelData( model, + 'fo[o' + + 'bar' + + 'b]az' + ); + + expect( command.isEnabled ).to.equal( true ); + } ); + + it( 'should be enabled when selection is in paragraph in list item', () => { + setModelData( model, + 'foo' + + 'b[]ar' + ); + + expect( command.isEnabled ).to.equal( true ); + } ); + + it( 'should be enabled when selection is in heading as a first child of list item', () => { + setModelData( model, + 'f[]oo' + + 'bar' + ); + + expect( command.isEnabled ).to.equal( true ); + } ); + + it( 'should be enabled when selection is in heading as a second child of list item', () => { + setModelData( model, + 'bar' + + 'f[]oo' + ); + + expect( command.isEnabled ).to.equal( true ); + } ); } ); describe( 'value', () => { - it( 'should be false when selection is in not checked element', () => { + it( 'should be false when collapsed selection is in not checked element', () => { setModelData( model, 'f[]oo' ); expect( command.value ).to.equal( false ); } ); - it( 'should be true when selection is in checked element', () => { + it( 'should be false when non-collapsed selection is in not checked element', () => { + setModelData( model, 'f[o]o' ); + + expect( command.value ).to.equal( false ); + } ); + + it( 'should be true when collapsed selection is in checked element', () => { setModelData( model, 'f[]oo' ); expect( command.value ).to.equal( true ); } ); + it( 'should be true when non-collapsed selection is in checked element', () => { + setModelData( model, 'f[o]o' ); + + expect( command.value ).to.equal( true ); + } ); + it( 'should be false when at least one selected element is not checked', () => { setModelData( model, 'f[oo' + @@ -99,6 +165,53 @@ describe( 'CheckTodoListCommand', () => { expect( command.value ).to.equal( true ); } ); + + it( 'should be true when a checked to-do list items are selected together with other list items', () => { + setModelData( model, + 'fo[o' + + 'bar' + + 'b]az' + ); + + expect( command.value ).to.equal( true ); + } ); + + it( 'should be true when a to-do list item is selected together with other list items in nested list', () => { + setModelData( model, + 'fo[o' + + 'bar' + + 'b]az' + ); + + expect( command.value ).to.equal( true ); + } ); + + it( 'should be true when selection is in paragraph', () => { + setModelData( model, + 'foo' + + 'b[]ar' + ); + + expect( command.value ).to.equal( true ); + } ); + + it( 'should be true when selection is in heading as a first child of checkked list item', () => { + setModelData( model, + 'f[]oo' + + 'bar' + ); + + expect( command.value ).to.equal( true ); + } ); + + it( 'should be true when selection is in heading as a second child of checkked list item', () => { + setModelData( model, + 'bar' + + 'f[]oo' + ); + + expect( command.value ).to.equal( true ); + } ); } ); describe( 'execute()', () => { @@ -194,6 +307,35 @@ describe( 'CheckTodoListCommand', () => { ); } ); + it( 'should toggle state items when selection is in heading as a first child of list item', () => { + testCommandToggle( + 'f[]oo' + + 'bar', + 'f[]oo' + + 'bar' + ); + } ); + + it( 'should toggle state items when selection is in heading as a second child of list item', () => { + testCommandToggle( + 'bar' + + 'f[]oo', + 'bar' + + 'f[]oo' + ); + } ); + + it( 'should toggle state items when selection is in heading as a second child of nested list item', () => { + testCommandToggle( + 'foo' + + 'bar' + + 'b[]az', + 'foo' + + 'bar' + + 'b[]az' + ); + } ); + it( 'should mark all selected items as checked when at least one selected item is not checked', () => { setModelData( model, 'foo[' + From 153877a463a5869f169e1e670c381700901eff8a Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Fri, 15 Sep 2023 18:53:26 +0200 Subject: [PATCH 35/54] Tests: todo checkbox change observer. --- .../todocheckboxchangeobserver.js | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 packages/ckeditor5-list/tests/tododocumentlist/todocheckboxchangeobserver.js diff --git a/packages/ckeditor5-list/tests/tododocumentlist/todocheckboxchangeobserver.js b/packages/ckeditor5-list/tests/tododocumentlist/todocheckboxchangeobserver.js new file mode 100644 index 00000000000..f0e22889983 --- /dev/null +++ b/packages/ckeditor5-list/tests/tododocumentlist/todocheckboxchangeobserver.js @@ -0,0 +1,112 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals document */ + +import View from '@ckeditor/ckeditor5-engine/src/view/view'; +import DomEventObserver from '@ckeditor/ckeditor5-engine/src/view/observer/domeventobserver'; +import createViewRoot from '@ckeditor/ckeditor5-engine/tests/view/_utils/createroot'; +import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; + +import TodoCheckboxChangeObserver from '../../src/tododocumentlist/todocheckboxchangeobserver'; + +describe( 'TodoCheckboxChangeObserver', () => { + let view, viewDocument, observer, domRoot; + + beforeEach( () => { + domRoot = document.createElement( 'div' ); + view = new View(); + viewDocument = view.document; + createViewRoot( viewDocument ); + view.attachDomRoot( domRoot ); + observer = view.addObserver( TodoCheckboxChangeObserver ); + } ); + + afterEach( () => { + view.destroy(); + } ); + + it( 'should extend DomEventObserver', () => { + expect( observer ).instanceof( DomEventObserver ); + } ); + + it( 'should define domEventType', () => { + expect( observer.domEventType ).to.deep.equal( [ 'change' ] ); + } ); + + it( 'should fire `todoCheckboxChange` for a checkbox in a span with "todo-list__label" class', () => { + const spy = sinon.spy(); + + viewDocument.on( 'todoCheckboxChange', spy ); + + setData( view, + '' + + '' + + '' + + '' + + '' + ); + + sinon.assert.notCalled( spy ); + + observer.onDomEvent( { type: 'change', target: domRoot.querySelector( 'input' ) } ); + + sinon.assert.calledOnce( spy ); + } ); + + it( 'should not fire `todoCheckboxChange` for an input without type checkbox', () => { + const spy = sinon.spy(); + + viewDocument.on( 'todoCheckboxChange', spy ); + + setData( view, + '' + + '' + + '' + + '' + + '' + ); + + observer.onDomEvent( { type: 'change', target: domRoot.querySelector( 'input' ) } ); + + sinon.assert.notCalled( spy ); + } ); + + it( 'should not fire `todoCheckboxChange` for a checkbox in a span without a class', () => { + const spy = sinon.spy(); + + viewDocument.on( 'todoCheckboxChange', spy ); + + setData( view, + '' + + '' + + '' + + '' + + '' + ); + + observer.onDomEvent( { type: 'change', target: domRoot.querySelector( 'input' ) } ); + + sinon.assert.notCalled( spy ); + } ); + + it( 'should not fire `todoCheckboxChange` for a span in a span with "todo-list__label" class', () => { + const spy = sinon.spy(); + + viewDocument.on( 'todoCheckboxChange', spy ); + + setData( view, + '' + + '' + + '' + + '' + + '' + ); + + observer.onDomEvent( { type: 'change', target: domRoot.querySelector( 'span[contenteditable=false]' ) } ); + + sinon.assert.notCalled( spy ); + } ); +} ); From 40c7511858eb06e79227d3a83eb89547a13c6cad Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 18 Sep 2023 14:03:44 +0200 Subject: [PATCH 36/54] Adding tests. --- .../documentlistreversedcommand.js | 6 +++ .../documentliststartcommand.js | 6 +++ .../documentliststylecommand.js | 8 +++ .../tododocumentlistediting.js | 51 +++++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/packages/ckeditor5-list/tests/documentlistproperties/documentlistreversedcommand.js b/packages/ckeditor5-list/tests/documentlistproperties/documentlistreversedcommand.js index 9088387228a..a644a21b86d 100644 --- a/packages/ckeditor5-list/tests/documentlistproperties/documentlistreversedcommand.js +++ b/packages/ckeditor5-list/tests/documentlistproperties/documentlistreversedcommand.js @@ -63,6 +63,12 @@ describe( 'DocumentListReversedCommand', () => { expect( listReversedCommand.isEnabled ).to.be.false; } ); + it( 'should be false if selection is inside a to-do list item', () => { + setData( model, 'foo[]' ); + + expect( listReversedCommand.isEnabled ).to.be.false; + } ); + it( 'should be true if selection is inside a listItem (collapsed selection)', () => { setData( model, modelList( [ '# Foo[] {reversed:true}' ] ) ); diff --git a/packages/ckeditor5-list/tests/documentlistproperties/documentliststartcommand.js b/packages/ckeditor5-list/tests/documentlistproperties/documentliststartcommand.js index 5c588af7095..08ce81aa31a 100644 --- a/packages/ckeditor5-list/tests/documentlistproperties/documentliststartcommand.js +++ b/packages/ckeditor5-list/tests/documentlistproperties/documentliststartcommand.js @@ -63,6 +63,12 @@ describe( 'DocumentListStartCommand', () => { expect( listStartCommand.isEnabled ).to.be.false; } ); + it( 'should be false if selection is inside a listItem (listType: todo)', () => { + setData( model, 'foo[]' ); + + expect( listStartCommand.isEnabled ).to.be.false; + } ); + it( 'should be true if selection is inside a listItem (collapsed selection)', () => { setData( model, modelList( [ '# Foo[] {start:2}' ] ) ); diff --git a/packages/ckeditor5-list/tests/documentlistproperties/documentliststylecommand.js b/packages/ckeditor5-list/tests/documentlistproperties/documentliststylecommand.js index 6fac94e7684..0a8120ec89e 100644 --- a/packages/ckeditor5-list/tests/documentlistproperties/documentliststylecommand.js +++ b/packages/ckeditor5-list/tests/documentlistproperties/documentliststylecommand.js @@ -75,6 +75,14 @@ describe( 'DocumentListStyleCommand', () => { expect( listStyleCommand.isEnabled ).to.equal( false ); } ); + + it( 'should be true if selection is inside a to-do list item', () => { + setData( model, 'foo[]' ); + + listStyleCommand.refresh(); + + expect( listStyleCommand.isEnabled ).to.be.true; + } ); } ); describe( '#value', () => { diff --git a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js index 916340bec77..005af8128bd 100644 --- a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js +++ b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js @@ -19,6 +19,7 @@ import DocumentListEditing from '../../src/documentlist/documentlistediting'; import DocumentListCommand from '../../src/documentlist/documentlistcommand'; import CheckTodoDocumentListCommand from '../../src/tododocumentlist/checktododocumentlistcommand'; import TodoCheckboxChangeObserver from '../../src/tododocumentlist/todocheckboxchangeobserver'; +import DocumentListPropertiesEditing from '../../src/documentlistproperties/documentlistpropertiesediting'; import stubUid from '../documentlist/_utils/uid'; @@ -249,6 +250,56 @@ describe( 'TodoDocumentListEditing', () => { } ); } ); + describe( 'upcast - list properties integration', () => { + let editor, model; + + beforeEach( async () => { + editor = await VirtualTestEditor.create( { + plugins: [ Paragraph, TodoDocumentListEditing, DocumentListPropertiesEditing ], + list: { + properties: { + startIndex: true + } + } + } ); + + model = editor.model; + view = editor.editing.view; + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + it( 'should not convert list style on to-do list', () => { + editor.setData( + '
                                                                                            ' + + '
                                                                                          • Foo
                                                                                          • ' + + '
                                                                                          • Bar
                                                                                          • ' + + '
                                                                                          ' + ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( + 'Foo' + + 'Bar' + ); + } ); + + it( 'should not convert list start on to-do list', () => { + editor.setData( + '
                                                                                            ' + + '
                                                                                          1. Foo
                                                                                          2. ' + + '
                                                                                          3. Bar
                                                                                          4. ' + + '
                                                                                          ' + ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( + 'Foo' + + 'Bar' + ); + } ); + } ); + describe( 'downcast - editing', () => { it( 'should convert a todo list item', () => { testEditing( From da290f59114e001459eb6d58723bfdd4debecced Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 18 Sep 2023 18:27:51 +0200 Subject: [PATCH 37/54] Added tests. --- .../tododocumentlistediting.ts | 16 +- ...odocumentlistediting-conversion-changes.js | 2494 ++++++----------- 2 files changed, 898 insertions(+), 1612 deletions(-) diff --git a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts index 585897c020d..0cd746fd3b3 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts @@ -324,28 +324,20 @@ export default class TodoDocumentListEditing extends Plugin { // Map view positions inside the checkbox and wrappers to the position in the first block of the list item. this.listenTo( editing.mapper, 'viewToModelPosition', ( evt, data ) => { - if ( !data.modelPosition ) { - return; - } - - if ( data.viewPosition.offset > 0 ) { - return; - } - const viewParent = data.viewPosition.parent as ViewElement; - const isInListItem = viewParent.is( 'attributeElement', 'li' ); - const isInListLabel = isLabelElement( viewParent ); + const isStartOfListItem = viewParent.is( 'attributeElement', 'li' ) && data.viewPosition.offset == 0; + const isStartOfListLabel = isLabelElement( viewParent ) && data.viewPosition.offset <= 1; const isInInputWrapper = viewParent.is( 'element', 'span' ) && viewParent.getAttribute( 'contenteditable' ) == 'false' && isLabelElement( viewParent.parent ); - if ( !isInListItem && !isInListLabel && !isInInputWrapper ) { + if ( !isStartOfListItem && !isStartOfListLabel && !isInInputWrapper ) { return; } - const nodeAfter = data.modelPosition.nodeAfter; + const nodeAfter = data.modelPosition!.nodeAfter; if ( nodeAfter && nodeAfter.getAttribute( 'listType' ) == 'todo' ) { data.modelPosition = model.createPositionAt( nodeAfter, 0 ); diff --git a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting-conversion-changes.js b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting-conversion-changes.js index 8d28daf4aac..63eda05d97c 100644 --- a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting-conversion-changes.js +++ b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting-conversion-changes.js @@ -11,7 +11,8 @@ import { UndoEditing } from '@ckeditor/ckeditor5-undo'; import { CodeBlockEditing } from '@ckeditor/ckeditor5-code-block'; import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; -import { parse as parseModel } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import TodoDocumentListEditing from '../../src/tododocumentlist/tododocumentlistediting'; @@ -2263,1810 +2264,1103 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { } ); } ); - describe.skip( 'nested lists', () => { + describe( 'nested lists', () => { describe( 'insert', () => { - describe( 'same list type', () => { - it( 'after lower indent', () => { - test.insert( - 'p' + - '1' + - '[x]', + it( 'after lower indent', () => { + test.insert( + 'p' + + '1' + + '[x]', - '

                                                                                          p

                                                                                          ' + - '
                                                                                            ' + + '

                                                                                            p

                                                                                            ' + + '
                                                                                              ' + '
                                                                                            • ' + - '1' + - '
                                                                                                ' + - '
                                                                                              • x
                                                                                              • ' + - '
                                                                                              ' + + '' + + '' + + '1' + + '' + + '
                                                                                                ' + + '
                                                                                              • ' + + '' + + '' + + 'x' + + '' + + '
                                                                                              • ' + + '
                                                                                              ' + '
                                                                                            • ' + - '
                                                                                            ' - ); - - expect( test.reconvertSpy.callCount ).to.equal( 0 ); - } ); + '
                                                                                          ' + ); - it( 'after lower indent (multi block)', () => { - test.insert( - 'p' + - '1a' + - '1b' + - '[xa' + - 'xb]', - - '

                                                                                          p

                                                                                          ' + - '
                                                                                            ' + - '
                                                                                          • ' + - '

                                                                                            1a

                                                                                            ' + - '

                                                                                            1b

                                                                                            ' + - '
                                                                                              ' + - '
                                                                                            • ' + - '

                                                                                              xa

                                                                                              ' + - '

                                                                                              xb

                                                                                              ' + - '
                                                                                            • ' + - '
                                                                                            ' + - '
                                                                                          • ' + - '
                                                                                          ' - ); - - expect( test.reconvertSpy.callCount ).to.equal( 0 ); - } ); + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); - it( 'after lower indent, before same indent', () => { - test.insert( - 'p' + - '1' + - '[x]' + - '1.1', + it( 'after lower indent (multi block)', () => { + test.insert( + 'p' + + '1a' + + '1b' + + '[xa' + + 'xb]', - '

                                                                                          p

                                                                                          ' + - '
                                                                                            ' + + '

                                                                                            p

                                                                                            ' + + '
                                                                                              ' + '
                                                                                            • ' + - '1' + - '
                                                                                                ' + - '
                                                                                              • x
                                                                                              • ' + - '
                                                                                              • 1.1
                                                                                              • ' + - '
                                                                                              ' + + '' + + '' + + '1a' + + '' + + '

                                                                                              1b

                                                                                              ' + + '
                                                                                                ' + + '
                                                                                              • ' + + '' + + '' + + 'xa' + + '' + + '

                                                                                                xb

                                                                                                ' + + '
                                                                                              • ' + + '
                                                                                              ' + '
                                                                                            • ' + - '
                                                                                            ' - ); + '
                                                                                          ' + ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); - } ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + } ); - it( 'after lower indent, before same indent (multi block)', () => { - test.insert( - 'p' + - '1a' + - '1b' + - '[xa' + - 'xb]' + - '1.1a' + - '1.1b', + it( 'after lower indent, before same indent', () => { + test.insert( + 'p' + + '1' + + '[x]' + + '1.1', - '

                                                                                          p

                                                                                          ' + - '
                                                                                            ' + - '
                                                                                          • ' + - '

                                                                                            1a

                                                                                            ' + - '

                                                                                            1b

                                                                                            ' + - '
                                                                                              ' + - '
                                                                                            • ' + - '

                                                                                              xa

                                                                                              ' + - '

                                                                                              xb

                                                                                              ' + - '
                                                                                            • ' + + '

                                                                                              p

                                                                                              ' + + '
                                                                                                ' + '
                                                                                              • ' + - '

                                                                                                1.1a

                                                                                                ' + - '

                                                                                                1.1b

                                                                                                ' + - '
                                                                                              • ' + - '
                                                                                              ' + + '' + + '' + + '1' + + '' + + '
                                                                                                ' + + '
                                                                                              • ' + + '' + + '' + + 'x' + + '' + + '
                                                                                              • ' + + '
                                                                                              • ' + + '' + + '' + + '1.1' + + '' + + '
                                                                                              • ' + + '
                                                                                              ' + '' + - '
                                                                                            ' - ); + '
                                                                                          ' + ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); - } ); + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); - it( 'after lower indent, before lower indent', () => { - test.insert( - 'p' + - '1' + - '[x]' + - '2', + it( 'after lower indent, before same indent (multi block)', () => { + test.insert( + 'p' + + '1a' + + '1b' + + '[xa' + + 'xb]' + + '1.1a' + + '1.1b', - '

                                                                                          p

                                                                                          ' + - '
                                                                                            ' + + '

                                                                                            p

                                                                                            ' + + '
                                                                                              ' + '
                                                                                            • ' + - '1' + - '
                                                                                                ' + - '
                                                                                              • x
                                                                                              • ' + - '
                                                                                              ' + + '' + + '' + + '1a' + + '' + + '

                                                                                              1b

                                                                                              ' + + '
                                                                                                ' + + '
                                                                                              • ' + + '' + + '' + + 'xa' + + '' + + '

                                                                                                xb

                                                                                                ' + + '
                                                                                              • ' + + '
                                                                                              • ' + + '' + + '' + + '1.1a' + + '' + + '

                                                                                                1.1b

                                                                                                ' + + '
                                                                                              • ' + + '
                                                                                              ' + '
                                                                                            • ' + - '
                                                                                            • 2
                                                                                            • ' + - '
                                                                                            ' - ); + '
                                                                                          ' + ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); - } ); + expect( test.reconvertSpy.callCount ).to.equal( 2 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 5 ) ); + } ); - it( 'after lower indent, before lower indent (multi block)', () => { - test.insert( - 'p' + - '1a' + - '1b' + - '[xa' + - 'xb]' + - '2a' + - '2b', + it( 'after lower indent, before lower indent', () => { + test.insert( + 'p' + + '1' + + '[x]' + + '2', - '

                                                                                          p

                                                                                          ' + - '
                                                                                            ' + - '
                                                                                          • ' + - '

                                                                                            1a

                                                                                            ' + - '

                                                                                            1b

                                                                                            ' + - '
                                                                                              ' + + '

                                                                                              p

                                                                                              ' + + '
                                                                                                ' + '
                                                                                              • ' + - '

                                                                                                xa

                                                                                                ' + - '

                                                                                                xb

                                                                                                ' + - '
                                                                                              • ' + - '
                                                                                              ' + + '' + + '' + + '1' + + '' + + '
                                                                                                ' + + '
                                                                                              • ' + + '' + + '' + + 'x' + + '' + + '
                                                                                              • ' + + '
                                                                                              ' + '' + '
                                                                                            • ' + - '

                                                                                              2a

                                                                                              ' + - '

                                                                                              2b

                                                                                              ' + + '' + + '' + + '2' + + '' + '
                                                                                            • ' + - '
                                                                                            ' - ); + '
                                                                                          ' + ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); - } ); + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); - it( 'after same indent', () => { - test.insert( - 'p' + - '1' + - '1.1' + - '[x]', + it( 'after lower indent, before lower indent (multi block)', () => { + test.insert( + 'p' + + '1a' + + '1b' + + '[xa' + + 'xb]' + + '2a' + + '2b', - '

                                                                                          p

                                                                                          ' + - '
                                                                                            ' + + '

                                                                                            p

                                                                                            ' + + '
                                                                                              ' + + '
                                                                                            • ' + + '' + + '' + + '1a' + + '' + + '

                                                                                              1b

                                                                                              ' + + '
                                                                                                ' + + '
                                                                                              • ' + + '' + + '' + + 'xa' + + '' + + '

                                                                                                xb

                                                                                                ' + + '
                                                                                              • ' + + '
                                                                                              ' + + '
                                                                                            • ' + '
                                                                                            • ' + - '1' + - '
                                                                                                ' + - '
                                                                                              • 1.1
                                                                                              • ' + - '
                                                                                              • x
                                                                                              • ' + - '
                                                                                              ' + + '' + + '' + + '2a' + + '' + + '

                                                                                              2b

                                                                                              ' + '
                                                                                            • ' + - '
                                                                                            ' - ); + '
                                                                                          ' + ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); - } ); + expect( test.reconvertSpy.callCount ).to.equal( 2 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 5 ) ); + } ); - it( 'after same indent (multi block)', () => { - test.insert( - 'p' + - '1a' + - '1b' + - '1.1a' + - '1.1b' + - '[xa' + - 'xb]', + it( 'after same indent', () => { + test.insert( + 'p' + + '1' + + '1.1' + + '[x]', - '

                                                                                          p

                                                                                          ' + - '
                                                                                            ' + - '
                                                                                          • ' + - '

                                                                                            1a

                                                                                            ' + - '

                                                                                            1b

                                                                                            ' + - '
                                                                                              ' + - '
                                                                                            • ' + - '

                                                                                              1.1a

                                                                                              ' + - '

                                                                                              1.1b

                                                                                              ' + - '
                                                                                            • ' + + '

                                                                                              p

                                                                                              ' + + '
                                                                                                ' + '
                                                                                              • ' + - '

                                                                                                xa

                                                                                                ' + - '

                                                                                                xb

                                                                                                ' + + '' + + '' + + '1' + + '' + + '
                                                                                                  ' + + '
                                                                                                • ' + + '' + + '' + + '1.1' + + '' + + '
                                                                                                • ' + + '
                                                                                                • ' + + '' + + '' + + 'x' + + '' + + '
                                                                                                • ' + + '
                                                                                                ' + '
                                                                                              • ' + - '
                                                                                              ' + - '' + - '
                                                                                            ' - ); + '
                                                                                          ' + ); - expect( test.reconvertSpy.callCount ).to.equal( 0 ); - } ); + expect( test.reconvertSpy.callCount ).to.equal( 0 ); + } ); - it( 'after same indent, before higher indent', () => { - test.insert( - 'p' + - '1' + - '[x]' + - '1.1', + it( 'after same indent (multi block)', () => { + test.insert( + 'p' + + '1a' + + '1b' + + '1.1a' + + '1.1b' + + '[xa' + + 'xb]', - '

                                                                                          p

                                                                                          ' + - '
                                                                                            ' + - '
                                                                                          • 1
                                                                                          • ' + + '

                                                                                            p

                                                                                            ' + + '
                                                                                              ' + '
                                                                                            • ' + - 'x' + - '
                                                                                                ' + - '
                                                                                              • 1.1
                                                                                              • ' + - '
                                                                                              ' + + '' + + '' + + '1a' + + '' + + '

                                                                                              1b

                                                                                              ' + + '
                                                                                                ' + + '
                                                                                              • ' + + '' + + '' + + '1.1a' + + '' + + '

                                                                                                1.1b

                                                                                                ' + + '
                                                                                              • ' + + '
                                                                                              • ' + + '' + + '' + + 'xa' + + '' + + '

                                                                                                xb

                                                                                                ' + + '
                                                                                              • ' + + '
                                                                                              ' + '
                                                                                            • ' + - '
                                                                                            ' - ); + '
                                                                                          ' + ); - expect( test.reconvertSpy.callCount ).to.equal( 1 ); - expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 3 ) ); - } ); + expect( test.reconvertSpy.callCount ).to.equal( 2 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 3 ) ); + } ); - it( 'after same indent, before higher indent (multi block)', () => { - test.insert( - 'p' + - '1a' + - '1b' + - '[xa' + - 'xb]' + - '1.1a' + - '1.1b', + it( 'after same indent, before higher indent', () => { + test.insert( + 'p' + + '1' + + '[x]' + + '1.1', - '

                                                                                          p

                                                                                          ' + - '
                                                                                            ' + + '

                                                                                            p

                                                                                            ' + + '
                                                                                              ' + '
                                                                                            • ' + - '

                                                                                              1a

                                                                                              ' + - '

                                                                                              1b

                                                                                              ' + + '' + + '' + + '1' + + '' + '
                                                                                            • ' + '
                                                                                            • ' + - '

                                                                                              xa

                                                                                              ' + - '

                                                                                              xb

                                                                                              ' + - '
                                                                                                ' + - '
                                                                                              • ' + - '

                                                                                                1.1a

                                                                                                ' + - '

                                                                                                1.1b

                                                                                                ' + - '
                                                                                              • ' + - '
                                                                                              ' + + '' + + '' + + 'x' + + '' + + '
                                                                                                ' + + '
                                                                                              • ' + + '' + + '' + + '1.1' + + '' + + '
                                                                                              • ' + + '
                                                                                              ' + '
                                                                                            • ' + - '
                                                                                            ' - ); + '
                                                                                          ' + ); - expect( test.reconvertSpy.callCount ).to.equal( 2 ); - expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 5 ) ); - expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 6 ) ); - } ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 3 ) ); + } ); - it( 'after higher indent, before higher indent', () => { - test.insert( - 'p' + - '1' + - '1.1' + - '[x]' + - '1.2', + it( 'after same indent, before higher indent (multi block)', () => { + test.insert( + 'p' + + '1a' + + '1b' + + '[xa' + + 'xb]' + + '1.1a' + + '1.1b', - '

                                                                                          p

                                                                                          ' + - '
                                                                                            ' + + '

                                                                                            p

                                                                                            ' + + '
                                                                                              ' + '
                                                                                            • ' + - '1' + - '
                                                                                                ' + - '
                                                                                              • 1.1
                                                                                              • ' + - '
                                                                                              ' + + '' + + '' + + '1a' + + '' + + '

                                                                                              1b

                                                                                              ' + '
                                                                                            • ' + '
                                                                                            • ' + - 'x' + - '
                                                                                                ' + - '
                                                                                              • 1.2
                                                                                              • ' + - '
                                                                                              ' + + '' + + '' + + 'xa' + + '' + + '

                                                                                              xb

                                                                                              ' + + '
                                                                                                ' + + '
                                                                                              • ' + + '' + + '' + + '1.1a' + + '' + + '

                                                                                                1.1b

                                                                                                ' + + '
                                                                                              • ' + + '
                                                                                              ' + '
                                                                                            • ' + - '
                                                                                            ' - ); + '
                                                                                          ' + ); - expect( test.reconvertSpy.callCount ).to.equal( 1 ); - expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 4 ) ); - } ); + expect( test.reconvertSpy.callCount ).to.equal( 3 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 5 ) ); + } ); - it( 'after higher indent, before higher indent( multi block)', () => { - test.insert( - 'p' + - '1' + - '1.1' + - '1.1' + - '[x' + - 'x]' + - '1.2' + - '1.2', + it( 'after higher indent, before higher indent', () => { + test.insert( + 'p' + + '1' + + '1.1' + + '[x]' + + '1.2', - '

                                                                                          p

                                                                                          ' + - '
                                                                                            ' + - '
                                                                                          • ' + - '1' + - '
                                                                                              ' + + '

                                                                                              p

                                                                                              ' + + '
                                                                                                ' + '
                                                                                              • ' + - '

                                                                                                1.1

                                                                                                ' + - '

                                                                                                1.1

                                                                                                ' + - '
                                                                                              • ' + - '
                                                                                              ' + + '' + + '' + + '1' + + '' + + '
                                                                                                ' + + '
                                                                                              • ' + + '' + + '' + + '1.1' + + '' + + '
                                                                                              • ' + + '
                                                                                              ' + '' + '
                                                                                            • ' + - '

                                                                                              x

                                                                                              ' + - '

                                                                                              x

                                                                                              ' + - '
                                                                                                ' + - '
                                                                                              • ' + - '

                                                                                                1.2

                                                                                                ' + - '

                                                                                                1.2

                                                                                                ' + - '
                                                                                              • ' + - '
                                                                                              ' + + '' + + '' + + 'x' + + '' + + '
                                                                                                ' + + '
                                                                                              • ' + + '' + + '' + + '1.2' + + '' + + '
                                                                                              • ' + + '
                                                                                              ' + '
                                                                                            • ' + - '
                                                                                            ' - ); + '
                                                                                          ' + ); - expect( test.reconvertSpy.callCount ).to.equal( 2 ); - expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 6 ) ); - expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 7 ) ); - } ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 4 ) ); + } ); - it( 'list items with too big indent', () => { - test.insert( - 'a' + - 'b' + - '[x' + - 'x' + - 'x]' + - 'c', + it( 'after higher indent, before higher indent( multi block)', () => { + test.insert( + 'p' + + '1' + + '1.1' + + '1.1' + + '[x' + + 'x]' + + '1.2' + + '1.2', - '
                                                                                            ' + - '
                                                                                          • ' + - 'a' + - '
                                                                                              ' + - '
                                                                                            • ' + - 'b' + - '
                                                                                                ' + + '

                                                                                                p

                                                                                                ' + + '
                                                                                                  ' + '
                                                                                                • ' + - 'x' + - '
                                                                                                    ' + - '
                                                                                                  • x
                                                                                                  • ' + - '
                                                                                                  ' + - '
                                                                                                • ' + - '
                                                                                                • x
                                                                                                • ' + - '
                                                                                                ' + - '' + - '
                                                                                              • c
                                                                                              • ' + - '
                                                                                              ' + - '
                                                                                            • ' + - '
                                                                                            ' - ); - } ); - - it( 'additional block before higher indent', () => { - test.insert( - 'p' + - '1' + - '[x]' + - '2', - - '

                                                                                            p

                                                                                            ' + - '
                                                                                              ' + - '
                                                                                            • ' + - '

                                                                                              1

                                                                                              ' + - '

                                                                                              x

                                                                                              ' + - '
                                                                                                ' + - '
                                                                                              • 2
                                                                                              • ' + - '
                                                                                              ' + - '
                                                                                            • ' + - '
                                                                                            ' - ); - - expect( test.reconvertSpy.callCount ).to.equal( 1 ); - expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); - } ); - } ); - - describe( 'different list type', () => { - it( 'after lower indent, before same indent', () => { - test.insert( - 'p' + - '1' + - '[x]' + - '1.1', - - '

                                                                                            p

                                                                                            ' + - '
                                                                                              ' + - '
                                                                                            • ' + - '1' + - '
                                                                                                ' + - '
                                                                                              1. x
                                                                                              2. ' + - '
                                                                                              ' + - '
                                                                                                ' + - '
                                                                                              • 1.1
                                                                                              • ' + - '
                                                                                              ' + - '
                                                                                            • ' + - '
                                                                                            ' - ); - } ); - - it( 'after same indent', () => { - test.insert( - 'p' + - '1' + - '1.1' + - '[x]', - - '

                                                                                            p

                                                                                            ' + - '
                                                                                              ' + - '
                                                                                            • ' + - '1' + - '
                                                                                                ' + - '
                                                                                              • 1.1
                                                                                              • ' + - '
                                                                                              ' + - '
                                                                                                ' + - '
                                                                                              1. x
                                                                                              2. ' + - '
                                                                                              ' + - '
                                                                                            • ' + - '
                                                                                            ' - ); - } ); - - it( 'after same indent, before higher indent', () => { - test.insert( - 'p' + - '1' + - '[x]' + - '1.1', - - '

                                                                                            p

                                                                                            ' + - '
                                                                                              ' + - '
                                                                                            • 1
                                                                                            • ' + - '
                                                                                            ' + - '
                                                                                              ' + - '
                                                                                            1. ' + - 'x' + - '
                                                                                                ' + - '
                                                                                              • 1.1
                                                                                              • ' + - '
                                                                                              ' + - '
                                                                                            2. ' + - '
                                                                                            ' - ); - } ); - - it( 'after higher indent, before higher indent', () => { - test.insert( - 'p' + - '1' + - '1.1' + - '[x]' + - '1.2', - - '

                                                                                            p

                                                                                            ' + - '
                                                                                              ' + - '
                                                                                            • ' + - '1' + - '
                                                                                                ' + - '
                                                                                              • 1.1
                                                                                              • ' + - '
                                                                                              ' + - '
                                                                                            • ' + - '
                                                                                            ' + - '
                                                                                              ' + - '
                                                                                            1. ' + - 'x' + - '
                                                                                                ' + - '
                                                                                              • 1.2
                                                                                              • ' + - '
                                                                                              ' + - '
                                                                                            2. ' + - '
                                                                                            ' - ); - } ); - - it( 'after higher indent, in nested list, different type', () => { - test.insert( - 'a' + - 'b' + - 'c' + - '[x]', - - '
                                                                                              ' + - '
                                                                                            • ' + - 'a' + - '
                                                                                                ' + - '
                                                                                              • ' + - 'b' + - '
                                                                                                  ' + - '
                                                                                                • c
                                                                                                • ' + - '
                                                                                                ' + + '' + + '' + + '1' + + '' + + '
                                                                                                  ' + + '
                                                                                                • ' + + '' + + '' + + '1.1' + + '' + + '

                                                                                                  1.1

                                                                                                  ' + + '
                                                                                                • ' + + '
                                                                                                ' + '
                                                                                              • ' + - '
                                                                                              ' + - '
                                                                                                ' + - '
                                                                                              1. x
                                                                                              2. ' + - '
                                                                                              ' + + '
                                                                                            • ' + + '' + + '' + + 'x' + + '' + + '

                                                                                              x

                                                                                              ' + + '
                                                                                                ' + + '
                                                                                              • ' + + '' + + '' + + '1.2' + + '' + + '

                                                                                                1.2

                                                                                                ' + + '
                                                                                              • ' + + '
                                                                                              ' + '
                                                                                            • ' + - '
                                                                                            ' - ); - } ); + '
                                                                                          ' + ); + + expect( test.reconvertSpy.callCount ).to.equal( 3 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 2 ) ); + expect( test.reconvertSpy.secondCall.firstArg ).to.equal( modelRoot.getChild( 6 ) ); + expect( test.reconvertSpy.thirdCall.firstArg ).to.equal( modelRoot.getChild( 7 ) ); } ); - // This case is pretty complex but it tests various edge cases concerning splitting lists. - it( 'element between nested list items - complex', () => { + it( 'list items with too big indent', () => { test.insert( - 'a' + - 'b' + - 'c' + - 'd' + - '[x]' + - 'e' + - 'f' + - 'g' + - 'h' + - 'i' + - 'j' + - 'p', + 'a' + + 'b' + + '[x' + + 'x' + + 'x]' + + 'c', - '
                                                                                            ' + - '
                                                                                          • ' + - 'a' + - '
                                                                                              ' + - '
                                                                                            • ' + - 'b' + - '
                                                                                                ' + - '
                                                                                              • ' + - 'c' + - '
                                                                                                  ' + - '
                                                                                                1. d
                                                                                                2. ' + - '
                                                                                                ' + - '
                                                                                              • ' + - '
                                                                                              ' + - '
                                                                                            • ' + - '
                                                                                            ' + - '
                                                                                          • ' + - '
                                                                                          ' + - '

                                                                                          x

                                                                                          ' + - '
                                                                                            ' + - '
                                                                                          1. e
                                                                                          2. ' + - '
                                                                                          ' + - '
                                                                                            ' + - '
                                                                                          • ' + - 'f' + - '
                                                                                              ' + - '
                                                                                            • g
                                                                                            • ' + - '
                                                                                            ' + - '
                                                                                          • ' + - '
                                                                                          • ' + - 'h' + - '
                                                                                              ' + - '
                                                                                            1. i
                                                                                            2. ' + - '
                                                                                            ' + - '
                                                                                          • ' + - '
                                                                                          ' + - '
                                                                                            ' + - '
                                                                                          1. j
                                                                                          2. ' + - '
                                                                                          ' + - '

                                                                                          p

                                                                                          ' + '
                                                                                            ' + + '
                                                                                          • ' + + '' + + '' + + 'a' + + '' + + '
                                                                                              ' + + '
                                                                                            • ' + + '' + + '' + + 'b' + + '' + + '
                                                                                                ' + + '
                                                                                              • ' + + '' + + '' + + 'x' + + '' + + '
                                                                                                  ' + + '
                                                                                                • ' + + '' + + '' + + '' + + '' + + 'x' + + '' + + '
                                                                                                • ' + + '
                                                                                                ' + + '
                                                                                              • ' + + '
                                                                                              • ' + + '' + + '' + + 'x' + + '' + + '
                                                                                              • ' + + '
                                                                                              ' + + '
                                                                                            • ' + + '
                                                                                            • ' + + '' + + '' + + 'c' + + '' + + '
                                                                                            • ' + + '
                                                                                            ' + + '
                                                                                          • ' + + '
                                                                                          ' ); } ); - it( 'element before indent "hole"', () => { + it( 'additional block before higher indent', () => { test.insert( - '1' + - '1.1' + - '[x]' + - '1.1.1' + - '2', + 'p' + + '1' + + '[x]' + + '2', - '
                                                                                            ' + - '
                                                                                          • ' + - '1' + - '
                                                                                              ' + - '
                                                                                            • 1.1
                                                                                            • ' + - '
                                                                                            ' + - '
                                                                                          • ' + - '
                                                                                          ' + - '

                                                                                          x

                                                                                          ' + - '
                                                                                            ' + - '
                                                                                          • 1.1.1
                                                                                          • ' + - '
                                                                                          • 2
                                                                                          • ' + + '

                                                                                            p

                                                                                            ' + + '
                                                                                              ' + + '
                                                                                            • ' + + '' + + '' + + '1' + + '' + + '

                                                                                              x

                                                                                              ' + + '
                                                                                                ' + + '
                                                                                              • ' + + '' + + '' + + '2' + + '' + + '
                                                                                              • ' + + '
                                                                                              ' + + '
                                                                                            • ' + '
                                                                                            ' ); - } ); - - it( 'two list items with mismatched types inserted in one batch', () => { - test.test( - 'a' + - 'b[]', - - '
                                                                                              ' + - '
                                                                                            • ' + - 'a' + - '
                                                                                                ' + - '
                                                                                              • b
                                                                                              • ' + - '
                                                                                              ' + - '
                                                                                                ' + - '
                                                                                              1. c
                                                                                              2. ' + - '
                                                                                              ' + - '
                                                                                                ' + - '
                                                                                              • d
                                                                                              • ' + - '
                                                                                              ' + - '
                                                                                            • ' + - '
                                                                                            ', - () => { - const item1 = 'c'; - const item2 = 'd'; - - model.change( writer => { - writer.append( parseModel( item1, model.schema ), modelRoot ); - writer.append( parseModel( item2, model.schema ), modelRoot ); - } ); - } - ); + expect( test.reconvertSpy.callCount ).to.equal( 1 ); + expect( test.reconvertSpy.firstCall.firstArg ).to.equal( modelRoot.getChild( 1 ) ); } ); } ); describe( 'remove', () => { it( 'the first nested item', () => { test.remove( - 'a' + - '[b]' + - 'c', + 'a' + + '[b]' + + 'c', - '
                                                                                              ' + - '
                                                                                            • ' + - 'a' + - '
                                                                                                ' + - '
                                                                                              • c
                                                                                              • ' + - '
                                                                                              ' + - '
                                                                                            • ' + + '
                                                                                                ' + + '
                                                                                              • ' + + '' + + '' + + 'a' + + '' + + '
                                                                                                  ' + + '
                                                                                                • ' + + '' + + '' + + 'c' + + '' + + '
                                                                                                • ' + + '
                                                                                                ' + + '
                                                                                              • ' + '
                                                                                              ' ); } ); - it( 'nested item from the middle', () => { + it( 'the last nested item', () => { test.remove( - 'a' + - 'b' + - '[c]' + - 'd', + 'a' + + 'b' + + '[c]', - '
                                                                                                ' + - '
                                                                                              • ' + - 'a' + - '
                                                                                                  ' + - '
                                                                                                • b
                                                                                                • ' + - '
                                                                                                • d
                                                                                                • ' + - '
                                                                                                ' + - '
                                                                                              • ' + + '
                                                                                                  ' + + '
                                                                                                • ' + + '' + + '' + + 'a' + + '' + + '
                                                                                                    ' + + '
                                                                                                  • ' + + '' + + '' + + 'b' + + '' + + '
                                                                                                  • ' + + '
                                                                                                  ' + + '
                                                                                                • ' + '
                                                                                                ' ); } ); - it( 'the last nested item', () => { + it( 'the only nested item', () => { test.remove( - 'a' + - 'b' + - '[c]', + 'a' + + '[c]', - '
                                                                                                  ' + - '
                                                                                                • ' + - 'a' + - '
                                                                                                    ' + - '
                                                                                                  • b
                                                                                                  • ' + - '
                                                                                                  ' + - '
                                                                                                • ' + + '
                                                                                                    ' + + '
                                                                                                  • ' + + '' + + '' + + 'a' + + '' + + '
                                                                                                  • ' + '
                                                                                                  ' ); } ); - it( 'the only nested item', () => { + it( 'first list item that has nested list', () => { test.remove( - 'a' + - '[c]', + '[a]' + + 'b' + + 'c', - '
                                                                                                    ' + - '
                                                                                                  • a
                                                                                                  • ' + + '
                                                                                                      ' + + '
                                                                                                    • ' + + '' + + '' + + 'b' + + '' + + '
                                                                                                        ' + + '
                                                                                                      • ' + + '' + + '' + + 'c' + + '' + + '
                                                                                                      • ' + + '
                                                                                                      ' + + '
                                                                                                    • ' + '
                                                                                                    ' ); } ); + } ); - it( 'list item that separates two nested lists of same type', () => { - test.remove( - 'a' + - 'b' + - '[c]' + - 'd', + describe( 'change indent', () => { + it( 'indent last item of flat list', () => { + test.changeIndent( + 1, - '
                                                                                                      ' + - '
                                                                                                    • ' + - 'a' + - '
                                                                                                        ' + - '
                                                                                                      1. b
                                                                                                      2. ' + - '
                                                                                                      3. d
                                                                                                      4. ' + - '
                                                                                                      ' + - '
                                                                                                    • ' + + 'a' + + '[b]', + + '
                                                                                                        ' + + '
                                                                                                      • ' + + '' + + '' + + 'a' + + '' + + '
                                                                                                          ' + + '
                                                                                                        • ' + + '' + + '' + + 'b' + + '' + + '
                                                                                                        • ' + + '
                                                                                                        ' + + '
                                                                                                      • ' + '
                                                                                                      ' ); } ); - it( 'list item that separates two nested lists of different type', () => { - test.remove( - 'a' + - 'b' + - '[c]' + - 'd', + it( 'indent last item in nested list', () => { + test.changeIndent( + 2, - '
                                                                                                        ' + - '
                                                                                                      • ' + - 'a' + - '
                                                                                                          ' + - '
                                                                                                        1. b
                                                                                                        2. ' + - '
                                                                                                        ' + - '
                                                                                                          ' + - '
                                                                                                        • d
                                                                                                        • ' + - '
                                                                                                        ' + - '
                                                                                                      • ' + + 'a' + + 'b' + + '[c]', + + '
                                                                                                          ' + + '
                                                                                                        • ' + + '' + + '' + + 'a' + + '' + + '
                                                                                                            ' + + '
                                                                                                          • ' + + '' + + '' + + 'b' + + '' + + '
                                                                                                              ' + + '
                                                                                                            • ' + + '' + + '' + + 'c' + + '' + + '
                                                                                                            • ' + + '
                                                                                                            ' + + '
                                                                                                          • ' + + '
                                                                                                          ' + + '
                                                                                                        • ' + '
                                                                                                        ' ); } ); - it( 'item that has nested lists, previous item has same indent', () => { - test.remove( - 'a' + - '[b]' + - 'c' + - 'd', - - '
                                                                                                          ' + - '
                                                                                                        • ' + - 'a' + - '
                                                                                                            ' + - '
                                                                                                          • c
                                                                                                          • ' + - '
                                                                                                          • d
                                                                                                          • ' + - '
                                                                                                          ' + - '
                                                                                                        • ' + - '
                                                                                                        ' - ); - } ); - - it( 'item that has nested lists, previous item has lower indent', () => { - test.remove( - 'a' + - '[b]' + - 'c' + - 'd', - - '
                                                                                                          ' + - '
                                                                                                        • ' + - 'a' + - '
                                                                                                            ' + - '
                                                                                                          • c
                                                                                                          • ' + - '
                                                                                                          • d
                                                                                                          • ' + - '
                                                                                                          ' + - '
                                                                                                        • ' + - '
                                                                                                        ' - ); - } ); - - it( 'item that has nested lists, previous item has higher indent by 1', () => { - test.remove( - 'a' + - 'b' + - '[c]' + - 'd' + - 'e', - - '
                                                                                                          ' + - '
                                                                                                        • ' + - 'a' + - '
                                                                                                            ' + - '
                                                                                                          • b
                                                                                                          • ' + - '
                                                                                                          • ' + - 'd' + - '
                                                                                                              ' + - '
                                                                                                            1. e
                                                                                                            2. ' + - '
                                                                                                            ' + - '
                                                                                                          • ' + - '
                                                                                                          ' + - '
                                                                                                        • ' + - '
                                                                                                        ' - ); - } ); - - it( 'item that has nested lists, previous item has higher indent by 2', () => { - test.remove( - 'a' + - 'b' + - 'c' + - '[d]' + - 'e', - - '
                                                                                                          ' + - '
                                                                                                        • ' + - 'a' + - '
                                                                                                            ' + - '
                                                                                                          • ' + - 'b' + - '
                                                                                                              ' + - '
                                                                                                            • c
                                                                                                            • ' + - '
                                                                                                            ' + - '
                                                                                                          • ' + - '
                                                                                                          • e
                                                                                                          • ' + - '
                                                                                                          ' + - '
                                                                                                        • ' + - '
                                                                                                        ' - ); - } ); + it( 'indent item that has nested list', () => { + test.changeIndent( + 1, - it( 'first list item that has nested list', () => { - test.remove( - '[a]' + - 'b' + - 'c', + 'a' + + '[b]' + + 'c', - '
                                                                                                          ' + - '
                                                                                                        • ' + - 'b' + - '
                                                                                                            ' + - '
                                                                                                          • c
                                                                                                          • ' + - '
                                                                                                          ' + - '
                                                                                                        • ' + + '
                                                                                                            ' + + '
                                                                                                          • ' + + '' + + '' + + 'a' + + '' + + '
                                                                                                              ' + + '
                                                                                                            • ' + + '' + + '' + + 'b' + + '' + + '
                                                                                                            • ' + + '
                                                                                                            • ' + + '' + + '' + + 'c' + + '' + + '
                                                                                                            • ' + + '
                                                                                                            ' + + '
                                                                                                          • ' + '
                                                                                                          ' ); } ); - } ); - describe( 'change type', () => { - it( 'list item that has nested items', () => { - test.changeType( - '[a]' + - 'b' + - 'c', - - '
                                                                                                            ' + - '
                                                                                                          • ' + - 'a' + - '
                                                                                                              ' + - '
                                                                                                            • b
                                                                                                            • ' + - '
                                                                                                            • c
                                                                                                            • ' + - '
                                                                                                            ' + - '
                                                                                                          • ' + - '
                                                                                                          ' - ); - } ); + it( 'indent item that in view is a next sibling of item that has nested list', () => { + test.changeIndent( + 1, - // The change will be "prevented" by post fixer. - it( 'list item that is a nested item', () => { - test.changeType( - 'a' + - 'b' + - '[c]' + - 'd', + 'a' + + 'b' + + '[c]' + + 'd', - '
                                                                                                            ' + - '
                                                                                                          • ' + - 'a' + - '
                                                                                                              ' + - '
                                                                                                            1. b
                                                                                                            2. ' + - '
                                                                                                            ' + - '
                                                                                                              ' + - '
                                                                                                            • c
                                                                                                            • ' + - '
                                                                                                            ' + - '
                                                                                                              ' + - '
                                                                                                            1. d
                                                                                                            2. ' + - '
                                                                                                            ' + - '
                                                                                                          • ' + + '
                                                                                                              ' + + '
                                                                                                            • ' + + '' + + '' + + 'a' + + '' + + '
                                                                                                                ' + + '
                                                                                                              • ' + + '' + + '' + + 'b' + + '' + + '
                                                                                                              • ' + + '
                                                                                                              • ' + + '' + + '' + + 'c' + + '' + + '
                                                                                                              • ' + + '
                                                                                                              • ' + + '' + + '' + + 'd' + + '' + + '
                                                                                                              • ' + + '
                                                                                                              ' + + '
                                                                                                            • ' + '
                                                                                                            ' ); } ); - it( 'changed list type at the same time as adding nested items', () => { - test.test( - 'a[]', - - '
                                                                                                              ' + - '
                                                                                                            1. ' + - 'a' + - '
                                                                                                                ' + - '
                                                                                                              • b
                                                                                                              • ' + - '
                                                                                                              • c
                                                                                                              • ' + - '
                                                                                                              ' + - '
                                                                                                            2. ' + - '
                                                                                                            ', - - () => { - const item1 = 'b'; - const item2 = 'c'; - - model.change( writer => { - writer.setAttribute( 'listType', 'numbered', modelRoot.getChild( 0 ) ); - writer.append( parseModel( item1, model.schema ), modelRoot ); - writer.append( parseModel( item2, model.schema ), modelRoot ); - } ); - } - ); - } ); - } ); - - describe( 'change indent', () => { - describe( 'same list type', () => { - it( 'indent last item of flat list', () => { - test.changeIndent( - 1, - - 'a' + - '[b]', - - '
                                                                                                              ' + - '
                                                                                                            • ' + - 'a' + - '
                                                                                                                ' + - '
                                                                                                              • b
                                                                                                              • ' + - '
                                                                                                              ' + - '
                                                                                                            • ' + - '
                                                                                                            ' - ); - } ); - - it( 'indent middle item of flat list', () => { - test.changeIndent( - 1, - - 'a' + - '[b]' + - 'c', - - '
                                                                                                              ' + - '
                                                                                                            • ' + - 'a' + - '
                                                                                                                ' + - '
                                                                                                              • b
                                                                                                              • ' + - '
                                                                                                              ' + - '
                                                                                                            • ' + - '
                                                                                                            • c
                                                                                                            • ' + - '
                                                                                                            ' - ); - } ); - - it( 'indent last item in nested list', () => { - test.changeIndent( - 2, - - 'a' + - 'b' + - '[c]', - - '
                                                                                                              ' + - '
                                                                                                            • ' + - 'a' + - '
                                                                                                                ' + - '
                                                                                                              • ' + - 'b' + - '
                                                                                                                  ' + - '
                                                                                                                • c
                                                                                                                • ' + - '
                                                                                                                ' + - '
                                                                                                              • ' + - '
                                                                                                              ' + - '
                                                                                                            • ' + - '
                                                                                                            ' - ); - } ); - - it( 'indent middle item in nested list', () => { - test.changeIndent( - 2, - - 'a' + - 'b' + - '[c]' + - 'd', - - '
                                                                                                              ' + - '
                                                                                                            • ' + - 'a' + - '
                                                                                                                ' + - '
                                                                                                              • ' + - 'b' + - '
                                                                                                                  ' + - '
                                                                                                                • c
                                                                                                                • ' + - '
                                                                                                                ' + - '
                                                                                                              • ' + - '
                                                                                                              • d
                                                                                                              • ' + - '
                                                                                                              ' + - '
                                                                                                            • ' + - '
                                                                                                            ' - ); - } ); - - // Keep in mind that this test is different than "executing command on item that has nested list". - // A command is automatically indenting nested items so the hierarchy is preserved. - // Here we test conversion and the change is simple changing indent of one item. - // This may be true also for other tests in this suite, keep this in mind. - it( 'indent item that has nested list', () => { - test.changeIndent( - 1, - - 'a' + - '[b]' + - 'c', - - '
                                                                                                              ' + - '
                                                                                                            • ' + - 'a' + - '
                                                                                                                ' + - '
                                                                                                              • b
                                                                                                              • ' + - '
                                                                                                              • c
                                                                                                              • ' + - '
                                                                                                              ' + - '
                                                                                                            • ' + - '
                                                                                                            ' - ); - } ); - - it( 'indent item that in view is a next sibling of item that has nested list', () => { - test.changeIndent( - 1, - - 'a' + - 'b' + - '[c]' + - 'd', - - '
                                                                                                              ' + - '
                                                                                                            • ' + - 'a' + - '
                                                                                                                ' + - '
                                                                                                              • b
                                                                                                              • ' + - '
                                                                                                              • c
                                                                                                              • ' + - '
                                                                                                              • d
                                                                                                              • ' + - '
                                                                                                              ' + - '
                                                                                                            • ' + - '
                                                                                                            ' - ); - } ); - - it( 'outdent the first item of nested list', () => { - test.changeIndent( - 0, + it( 'outdent the first item of nested list', () => { + test.changeIndent( + 0, - 'a' + - '[b]' + - 'c' + - 'd', - - '
                                                                                                              ' + - '
                                                                                                            • a
                                                                                                            • ' + - '
                                                                                                            • ' + - 'b' + - '
                                                                                                                ' + - '
                                                                                                              • c
                                                                                                              • ' + - '
                                                                                                              • d
                                                                                                              • ' + - '
                                                                                                              ' + - '
                                                                                                            • ' + - '
                                                                                                            ' - ); - } ); - - it( 'outdent item from the middle of nested list', () => { - test.changeIndent( - 0, - - 'a' + - 'b' + - '[c]' + - 'd', - - '
                                                                                                              ' + - '
                                                                                                            • ' + - 'a' + - '
                                                                                                                ' + - '
                                                                                                              • b
                                                                                                              • ' + - '
                                                                                                              ' + - '
                                                                                                            • ' + - '
                                                                                                            • ' + - 'c' + - '
                                                                                                                ' + - '
                                                                                                              • d
                                                                                                              • ' + - '
                                                                                                              ' + - '
                                                                                                            • ' + - '
                                                                                                            ' - ); - } ); - - it( 'outdent the last item of nested list', () => { - test.changeIndent( - 0, - - 'a' + - 'b' + - '[c]', - - '
                                                                                                              ' + - '
                                                                                                            • ' + - 'a' + - '
                                                                                                                ' + - '
                                                                                                              • b
                                                                                                              • ' + - '
                                                                                                              ' + - '
                                                                                                            • ' + - '
                                                                                                            • c
                                                                                                            • ' + - '
                                                                                                            ' - ); - } ); - - it( 'outdent the only item of nested list', () => { - test.changeIndent( - 1, - - 'a' + - 'b' + - '[c]' + - 'd', + 'a' + + '[b]' + + 'c' + + 'd', - '
                                                                                                              ' + + '
                                                                                                                ' + '
                                                                                                              • ' + - 'a' + - '
                                                                                                                  ' + - '
                                                                                                                • b
                                                                                                                • ' + - '
                                                                                                                • c
                                                                                                                • ' + - '
                                                                                                                • d
                                                                                                                • ' + - '
                                                                                                                ' + + '' + + '' + + 'a' + + '' + '
                                                                                                              • ' + - '
                                                                                                              ' - ); - } ); - - it( 'outdent item by two', () => { - test.changeIndent( - 0, - - 'a' + - 'b' + - '[c]' + - 'd', - - '
                                                                                                                ' + '
                                                                                                              • ' + - 'a' + - '
                                                                                                                  ' + - '
                                                                                                                • b
                                                                                                                • ' + - '
                                                                                                                ' + + '' + + '' + + 'b' + + '' + + '
                                                                                                                  ' + + '
                                                                                                                • ' + + '' + + '' + + 'c' + + '' + + '
                                                                                                                • ' + + '
                                                                                                                • ' + + '' + + '' + + 'd' + + '' + + '
                                                                                                                • ' + + '
                                                                                                                ' + '
                                                                                                              • ' + - '
                                                                                                              • c
                                                                                                              • ' + - '
                                                                                                              • d
                                                                                                              • ' + - '
                                                                                                              ' - ); - } ); + '
                                                                                                            ' + ); } ); - describe( 'different list type', () => { - it( 'indent middle item of flat list', () => { - test.changeIndent( - 1, - - 'a' + - '[b]' + - 'c', - - '
                                                                                                              ' + - '
                                                                                                            • ' + - 'a' + - '
                                                                                                                ' + - '
                                                                                                              1. b
                                                                                                              2. ' + - '
                                                                                                              ' + - '
                                                                                                            • ' + - '
                                                                                                            • c
                                                                                                            • ' + - '
                                                                                                            ' - ); - } ); - - it( 'indent item that has nested list', () => { - test.changeIndent( - 1, - - 'a' + - '[b]' + - 'c', - - '
                                                                                                              ' + - '
                                                                                                            • ' + - 'a' + - '
                                                                                                                ' + - '
                                                                                                              1. b
                                                                                                              2. ' + - '
                                                                                                              ' + - '
                                                                                                                ' + - '
                                                                                                              • c
                                                                                                              • ' + - '
                                                                                                              ' + - '
                                                                                                            • ' + - '
                                                                                                            ' - ); - } ); - - it( 'indent item that in view is a next sibling of item that has nested list #1', () => { - test.changeIndent( - 1, - - 'a' + - 'b' + - '[c]' + - 'd', - - '
                                                                                                              ' + - '
                                                                                                            • ' + - 'a' + - '
                                                                                                                ' + - '
                                                                                                              • b
                                                                                                              • ' + - '
                                                                                                              ' + - '
                                                                                                                ' + - '
                                                                                                              1. c
                                                                                                              2. ' + - '
                                                                                                              ' + - '
                                                                                                                ' + - '
                                                                                                              • d
                                                                                                              • ' + - '
                                                                                                              ' + - '
                                                                                                            • ' + - '
                                                                                                            ' - ); - } ); - - it( 'outdent the first item of nested list', () => { - test.changeIndent( - 0, - - 'a' + - '[b]' + - 'c' + - 'd', - - '
                                                                                                              ' + - '
                                                                                                            • a
                                                                                                            • ' + - '
                                                                                                            • ' + - 'b' + - '
                                                                                                                ' + - '
                                                                                                              • c
                                                                                                              • ' + - '
                                                                                                              • d
                                                                                                              • ' + - '
                                                                                                              ' + - '
                                                                                                            • ' + - '
                                                                                                            ' - ); - } ); - - it( 'outdent the only item of nested list', () => { - test.changeIndent( - 1, + it( 'outdent the last item of nested list', () => { + test.changeIndent( + 0, - 'a' + - 'b' + - '[c]' + - 'd', + 'a' + + 'b' + + '[c]', - '
                                                                                                              ' + + '
                                                                                                                ' + '
                                                                                                              • ' + - 'a' + - '
                                                                                                                  ' + - '
                                                                                                                • b
                                                                                                                • ' + - '
                                                                                                                • c
                                                                                                                • ' + - '
                                                                                                                • d
                                                                                                                • ' + - '
                                                                                                                ' + + '' + + '' + + 'a' + + '' + + '
                                                                                                                  ' + + '
                                                                                                                • ' + + '' + + '' + + 'b' + + '' + + '
                                                                                                                • ' + + '
                                                                                                                ' + '
                                                                                                              • ' + - '
                                                                                                              ' - ); - } ); - - it( 'outdent item by two', () => { - test.changeIndent( - 0, - - 'a' + - 'b' + - '[c]' + - 'd', - - '
                                                                                                                ' + '
                                                                                                              • ' + - 'a' + - '
                                                                                                                  ' + - '
                                                                                                                • b
                                                                                                                • ' + - '
                                                                                                                ' + + '' + + '' + + 'c' + + '' + '
                                                                                                              • ' + - '
                                                                                                              ' + - '
                                                                                                                ' + - '
                                                                                                              1. c
                                                                                                              2. ' + - '
                                                                                                              ' + - '
                                                                                                                ' + - '
                                                                                                              • d
                                                                                                              • ' + - '
                                                                                                              ' - ); - } ); - } ); - } ); - - describe( 'rename list item element', () => { - it( 'rename top list item', () => { - test.renameElement( - '[a]' + - 'b', - - '
                                                                                                                ' + - '
                                                                                                              • ' + - '

                                                                                                                a

                                                                                                                ' + - '
                                                                                                                  ' + - '
                                                                                                                • ' + - 'b' + - - '
                                                                                                                • ' + - '
                                                                                                                ' + - '
                                                                                                              • ' + - '
                                                                                                              ' - ); - } ); - - it( 'rename nested list item', () => { - test.renameElement( - 'a' + - '[b]', - - '
                                                                                                                ' + - '
                                                                                                              • ' + - 'a' + - '
                                                                                                                  ' + - '
                                                                                                                • ' + - '

                                                                                                                  b

                                                                                                                  ' + - '
                                                                                                                • ' + - '
                                                                                                                ' + - '
                                                                                                              • ' + '
                                                                                                              ' ); } ); - } ); - describe( 'remove list item attributes', () => { - it( 'rename nested item from the middle #1', () => { - test.removeListAttributes( - 'a' + - 'b' + - '[c]' + - 'd', - - '
                                                                                                                ' + - '
                                                                                                              • ' + - 'a' + - '
                                                                                                                  ' + - '
                                                                                                                • b
                                                                                                                • ' + - '
                                                                                                                ' + - '
                                                                                                              • ' + - '
                                                                                                              ' + - '

                                                                                                              c

                                                                                                              ' + - '
                                                                                                                ' + - '
                                                                                                              • d
                                                                                                              • ' + - '
                                                                                                              ' - ); - } ); + it( 'outdent the only item of nested list', () => { + test.changeIndent( + 1, - it( 'rename nested item from the middle #2 - nightmare example', () => { - test.removeListAttributes( - // Indents in this example should be fixed by post fixer. - // This nightmare example checks if structure of the list is kept as intact as possible. - 'a' + - 'b' + - '[c]' + - 'd' + - 'e' + - 'f' + - 'g' + - 'h' + - 'i' + - 'j' + - 'k' + - 'l' + - 'm', + 'a' + + 'b' + + '[c]' + + 'd', - '
                                                                                                                ' + - '
                                                                                                              • ' + - 'a' + - '
                                                                                                                  ' + - '
                                                                                                                • b
                                                                                                                • ' + - '
                                                                                                                ' + - '
                                                                                                              • ' + - '
                                                                                                              ' + - '

                                                                                                              c

                                                                                                              ' + - '
                                                                                                                ' + - '
                                                                                                              • d
                                                                                                              • ' + - '
                                                                                                              • ' + - 'e' + - '
                                                                                                                  ' + - '
                                                                                                                • f
                                                                                                                • ' + - '
                                                                                                                ' + - '
                                                                                                              • ' + - '
                                                                                                              • ' + - 'g' + - '
                                                                                                                  ' + - '
                                                                                                                • ' + - 'h' + - '
                                                                                                                    ' + - '
                                                                                                                  • i
                                                                                                                  • ' + - '
                                                                                                                  ' + - '
                                                                                                                • ' + - '
                                                                                                                ' + - '
                                                                                                              • ' + - '
                                                                                                              • ' + - 'j' + - '
                                                                                                                  ' + - '
                                                                                                                • k
                                                                                                                • ' + - '
                                                                                                                ' + - '
                                                                                                              • ' + - '
                                                                                                              • ' + - 'l' + - '
                                                                                                                  ' + - '
                                                                                                                • m
                                                                                                                • ' + - '
                                                                                                                ' + - '
                                                                                                              • ' + + '
                                                                                                                  ' + + '
                                                                                                                • ' + + '' + + '' + + 'a' + + '' + + '
                                                                                                                    ' + + '
                                                                                                                  • ' + + '' + + '' + + 'b' + + '' + + '
                                                                                                                  • ' + + '
                                                                                                                  • ' + + '' + + '' + + 'c' + + '' + + '
                                                                                                                  • ' + + '
                                                                                                                  • ' + + '' + + '' + + 'd' + + '' + + '
                                                                                                                  • ' + + '
                                                                                                                  ' + + '
                                                                                                                • ' + '
                                                                                                                ' ); } ); + } ); - it( 'rename nested item from the middle #3 - manual test example', () => { + describe( 'remove list item attributes', () => { + it( 'rename nested item from the middle #1', () => { test.removeListAttributes( - // Indents in this example should be fixed by post fixer. - // This example checks a bug found by testing manual test. - 'a' + - 'b' + - '[c]' + - 'd' + - 'e' + - 'f' + - 'g' + - 'h' + - '' + - '' + - 'k' + - 'l', + 'a' + + 'b' + + '[c]' + + 'd', - '
                                                                                                                  ' + - '
                                                                                                                • ' + - 'a' + - '
                                                                                                                    ' + - '
                                                                                                                  • b
                                                                                                                  • ' + - '
                                                                                                                  ' + - '
                                                                                                                • ' + + '
                                                                                                                    ' + + '
                                                                                                                  • ' + + '' + + '' + + 'a' + + '' + + '
                                                                                                                      ' + + '
                                                                                                                    • ' + + '' + + '' + + 'b' + + '' + + '
                                                                                                                    • ' + + '
                                                                                                                    ' + + '
                                                                                                                  • ' + '
                                                                                                                  ' + '

                                                                                                                  c

                                                                                                                  ' + - '
                                                                                                                    ' + - '
                                                                                                                  • ' + - 'd' + - '
                                                                                                                      ' + - '
                                                                                                                    • e
                                                                                                                    • ' + - '
                                                                                                                    • f
                                                                                                                    • ' + - '
                                                                                                                    • g
                                                                                                                    • ' + - '
                                                                                                                    • h
                                                                                                                    • ' + - '
                                                                                                                    ' + - '
                                                                                                                  • ' + - '
                                                                                                                  • ' + - '' + - '
                                                                                                                      ' + - '
                                                                                                                    • ' + - '' + - '
                                                                                                                        ' + - '
                                                                                                                      1. k
                                                                                                                      2. ' + - '
                                                                                                                      3. l
                                                                                                                      4. ' + - '
                                                                                                                      ' + - '
                                                                                                                    • ' + - '
                                                                                                                    ' + - '
                                                                                                                  • ' + + '
                                                                                                                      ' + + '
                                                                                                                    • ' + + '' + + '' + + 'd' + + '' + + '
                                                                                                                    • ' + '
                                                                                                                    ' ); } ); it( 'rename the only nested item', () => { test.removeListAttributes( - 'a' + - '[b]', + 'a' + + '[b]', - '
                                                                                                                      ' + - '
                                                                                                                    • a
                                                                                                                    • ' + + '
                                                                                                                        ' + + '
                                                                                                                      • ' + + '' + + '' + + 'a' + + '' + + '
                                                                                                                      • ' + '
                                                                                                                      ' + '

                                                                                                                      b

                                                                                                                      ' ); } ); } ); + } ); - describe( 'set list item attributes', () => { - it( 'element into first item in nested list', () => { - test.setListAttributes( - 1, + describe( 'position mapping', () => { + let mapper, view, viewRoot; - 'a' + - '[b]', + beforeEach( () => { + mapper = editor.editing.mapper; + view = editor.editing.view; + viewRoot = view.document.getRoot(); - '
                                                                                                                        ' + + setModelData( model, + '0' + + '1' + + '2' + ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( + '

                                                                                                                        0

                                                                                                                        ' + + '
                                                                                                                          ' + '
                                                                                                                        • ' + - 'a' + - '
                                                                                                                            ' + - '
                                                                                                                          • b
                                                                                                                          • ' + - '
                                                                                                                          ' + + '' + + '' + + '1' + + '' + + '

                                                                                                                          2

                                                                                                                          ' + '
                                                                                                                        • ' + - '
                                                                                                                        ' - ); - } ); + '
                                                                                                                      ' + ); + } ); - it( 'element into last item in nested list', () => { - test.setListAttributes( - 1, + describe( 'view to model', () => { + function testList( viewPath, modelPath ) { + const viewPos = getViewPosition( viewRoot, viewPath, view ); + const modelPos = mapper.toModelPosition( viewPos ); - 'a' + - 'b' + - '[c]', + expect( modelPos.root ).to.equal( modelRoot ); + expect( modelPos.path ).to.deep.equal( modelPath ); + } - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - 'a' + - '
                                                                                                                          ' + - '
                                                                                                                        • b
                                                                                                                        • ' + - '
                                                                                                                        • c
                                                                                                                        • ' + - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - '
                                                                                                                      ' - ); + it( 'before ul --> before first list item', () => { + testList( [ 1 ], [ 1 ] ); } ); - it( 'element into a first item in deeply nested list', () => { - test.setListAttributes( - 2, + it( 'before first li --> before first list item', () => { + testList( [ 1, 0 ], [ 1 ] ); + } ); - 'a' + - 'b' + - '[c]' + - 'd', + it( 'before label --> inside list item block', () => { + testList( [ 1, 0, 0 ], [ 1, 0 ] ); + } ); - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - 'a' + - '
                                                                                                                          ' + - '
                                                                                                                        • ' + - 'b' + - '
                                                                                                                            ' + - '
                                                                                                                          • c
                                                                                                                          • ' + - '
                                                                                                                          ' + - '
                                                                                                                        • ' + - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - '
                                                                                                                      • d
                                                                                                                      • ' + - '
                                                                                                                      ' - ); + it( 'before checkbox wrapper --> inside list item block', () => { + testList( [ 1, 0, 0, 0 ], [ 1, 0 ] ); } ); - } ); - describe( 'move', () => { - // Since move is in fact remove + insert and does not event have its own converter, only a few cases will be tested here. - it( 'out nested list items', () => { - test.move( - 'a' + - '[b' + - 'c]' + - 'd' + - 'e' + - 'x', + it( 'before checkbox --> inside list item block', () => { + testList( [ 1, 0, 0, 0, 0 ], [ 1, 0 ] ); + } ); - 6, + it( 'after checkbox --> inside list item block', () => { + testList( [ 1, 0, 0, 0, 1 ], [ 1, 0 ] ); + } ); - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - 'a' + - '
                                                                                                                          ' + - '
                                                                                                                        • ' + - 'd' + - '
                                                                                                                            ' + - '
                                                                                                                          • e
                                                                                                                          • ' + - '
                                                                                                                          ' + - '
                                                                                                                        • ' + - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - '
                                                                                                                      ' + - '

                                                                                                                      x

                                                                                                                      ' + - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - 'b' + - '
                                                                                                                          ' + - '
                                                                                                                        • c
                                                                                                                        • ' + - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - '
                                                                                                                      ' - ); + it( 'before description --> inside list item block', () => { + testList( [ 1, 0, 0, 1 ], [ 1, 0 ] ); } ); - it( 'nested list items between lists of same type', () => { - test.move( - 'a' + - 'b' + - '[c' + - 'd]' + - 'e' + - 'x' + - 'f' + - 'g', + it( 'start of description --> inside list item block', () => { + testList( [ 1, 0, 0, 1, 0 ], [ 1, 0 ] ); + } ); - 7, + it( 'end of description --> inside list item block', () => { + testList( [ 1, 0, 0, 1, 1 ], [ 1, 1 ] ); + } ); - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - 'a' + - '
                                                                                                                          ' + - '
                                                                                                                        • ' + - 'b' + - '
                                                                                                                            ' + - '
                                                                                                                          • e
                                                                                                                          • ' + - '
                                                                                                                          ' + - '
                                                                                                                        • ' + - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - '
                                                                                                                      ' + - '

                                                                                                                      x

                                                                                                                      ' + - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - 'f' + - '
                                                                                                                          ' + - '
                                                                                                                        • ' + - 'c' + - '
                                                                                                                            ' + - '
                                                                                                                          • d
                                                                                                                          • ' + - '
                                                                                                                          ' + - '
                                                                                                                        • ' + - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - '
                                                                                                                      • g
                                                                                                                      • ' + - '
                                                                                                                      ' - ); + it( 'after description --> after first block', () => { + testList( [ 1, 0, 0, 2 ], [ 2 ] ); } ); - it( 'nested list items between lists of different type', () => { - test.move( - 'a' + - 'b' + - '[c' + - 'd]' + - 'e' + - 'x' + - 'f' + - 'g', + it( 'after label --> after first block', () => { + testList( [ 1, 0, 1 ], [ 2 ] ); + } ); + } ); - 7, + describe( 'model to view', () => { + function testList( modelPath, viewPath ) { + const modelPos = model.createPositionFromPath( modelRoot, modelPath ); + const viewPos = mapper.toViewPosition( modelPos ); - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - 'a' + - '
                                                                                                                          ' + - '
                                                                                                                        • ' + - 'b' + - '
                                                                                                                            ' + - '
                                                                                                                          • e
                                                                                                                          • ' + - '
                                                                                                                          ' + - '
                                                                                                                        • ' + - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - '
                                                                                                                      ' + - '

                                                                                                                      x

                                                                                                                      ' + - '
                                                                                                                        ' + - '
                                                                                                                      1. ' + - 'f' + - '
                                                                                                                          ' + - '
                                                                                                                        • ' + - 'c' + - '
                                                                                                                            ' + - '
                                                                                                                          • d
                                                                                                                          • ' + - '
                                                                                                                          ' + - '
                                                                                                                        • ' + - '
                                                                                                                        ' + - '
                                                                                                                          ' + - '
                                                                                                                        1. g
                                                                                                                        2. ' + - '
                                                                                                                        ' + - '
                                                                                                                      2. ' + - '
                                                                                                                      ' - ); + expect( viewPos.root ).to.equal( viewRoot ); + expect( getViewPath( viewPos ) ).to.deep.equal( viewPath ); + } + + it( 'before list item --> before ul', () => { + testList( [ 1 ], [ 1 ] ); } ); - it( 'element between nested list', () => { - test.move( - 'a' + - 'b' + - 'c' + - 'd' + - '[x]', + it( 'start of list item --> start of description', () => { + testList( [ 1, 0 ], [ 1, 0, 0, 1, 0, 0 ] ); + } ); - 2, + it( 'end of list item --> start of description', () => { + testList( [ 1, 1 ], [ 1, 0, 0, 1, 0, 1 ] ); + } ); - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - 'a' + - '
                                                                                                                          ' + - '
                                                                                                                        • b
                                                                                                                        • ' + - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - '
                                                                                                                      ' + - '

                                                                                                                      x

                                                                                                                      ' + - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - 'c' + - '
                                                                                                                          ' + - '
                                                                                                                        • d
                                                                                                                        • ' + - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - '
                                                                                                                      ' - ); + it( 'after list item --> after a description', () => { + testList( [ 2 ], [ 1, 0, 1 ] ); } ); - it( 'multiple nested list items of different types #1 - fix at start', () => { - test.move( - 'a' + - 'b' + - '[c' + - 'd' + - 'e]' + - 'f' + - 'g' + - 'h' + - 'i', + it( 'start of second list item block --> start of paragraph', () => { + testList( [ 2, 0 ], [ 1, 0, 1, 0, 0 ] ); + } ); + } ); - 8, + function getViewPosition( root, path, view ) { + let parent = root; - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - 'a' + - '
                                                                                                                          ' + - '
                                                                                                                        • b
                                                                                                                        • ' + - '
                                                                                                                        ' + - '
                                                                                                                          ' + - '
                                                                                                                        1. f
                                                                                                                        2. ' + - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - '
                                                                                                                      • ' + - 'g' + - '
                                                                                                                          ' + - '
                                                                                                                        1. h
                                                                                                                        2. ' + - '
                                                                                                                        ' + - '
                                                                                                                          ' + - '
                                                                                                                        • c
                                                                                                                        • ' + - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - '
                                                                                                                      • ' + - 'd' + - '
                                                                                                                          ' + - '
                                                                                                                        1. e
                                                                                                                        2. ' + - '
                                                                                                                        3. i
                                                                                                                        4. ' + - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - '
                                                                                                                      ' - ); - } ); + while ( path.length > 1 ) { + parent = parent.getChild( path.shift() ); + } - it( 'multiple nested list items of different types #2 - fix at end', () => { - test.move( - 'a' + - 'b' + - '[c' + - 'd' + - 'e]' + - 'f' + - 'g' + - 'h' + - 'i', + if ( !parent ) { + throw new Error( 'Invalid view path' ); + } - 8, + return view.createPositionAt( parent, path[ 0 ] ); + } - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - 'a' + - '
                                                                                                                          ' + - '
                                                                                                                        • b
                                                                                                                        • ' + - '
                                                                                                                        ' + - '
                                                                                                                          ' + - '
                                                                                                                        1. f
                                                                                                                        2. ' + - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - '
                                                                                                                      • ' + - 'g' + - '
                                                                                                                          ' + - '
                                                                                                                        • h
                                                                                                                        • ' + - '
                                                                                                                        • c
                                                                                                                        • ' + - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - '
                                                                                                                      • ' + - 'd' + - '
                                                                                                                          ' + - '
                                                                                                                        1. e
                                                                                                                        2. ' + - '
                                                                                                                        ' + - '
                                                                                                                          ' + - '
                                                                                                                        • i
                                                                                                                        • ' + - '
                                                                                                                        ' + - '
                                                                                                                      • ' + - '
                                                                                                                      ' - ); - } ); - } ); + function getViewPath( position ) { + const path = [ position.offset ]; + let parent = position.parent; + + while ( parent.parent ) { + path.unshift( parent.index ); + parent = parent.parent; + } + + return path; + } } ); } ); From 2419eb2b3acf30f7f360755a018c5a3ef89d01e5 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 18 Sep 2023 18:45:32 +0200 Subject: [PATCH 38/54] Adding tests. --- ...odocumentlistediting-conversion-changes.js | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting-conversion-changes.js b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting-conversion-changes.js index 63eda05d97c..93f0c546838 100644 --- a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting-conversion-changes.js +++ b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting-conversion-changes.js @@ -3335,6 +3335,60 @@ describe( 'TodoDocumentListEditing - conversion - changes', () => { it( 'start of second list item block --> start of paragraph', () => { testList( [ 2, 0 ], [ 1, 0, 1, 0, 0 ] ); } ); + + it( 'should not affect other input elements', () => { + model.schema.register( 'input', { inheritAllFrom: '$inlineObject' } ); + editor.conversion.for( 'downcast' ).elementToElement( { + model: 'input', + view: 'input', + converterPriority: 'low' + } ); + + setModelData( model, 'foobar' ); + + testList( [ 0, 7 ], [ 0, 0, 0, 1, 2, 3 ] ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( + '
                                                                                                                        ' + + '
                                                                                                                      • ' + + '' + + '' + + 'foobar' + + '' + + '
                                                                                                                      • ' + + '
                                                                                                                      ' + ); + } ); + + it( 'should not affect other input UI elements', () => { + editor.conversion.for( 'downcast' ).markerToElement( { + model: 'input', + view: ( data, { writer } ) => writer.createUIElement( 'input' ) + } ); + + setModelData( model, 'foo[]bar' ); + + model.change( writer => { + writer.addMarker( 'input', { + range: model.document.selection.getFirstRange(), + usingOperation: false, + affectsData: false + } ); + } ); + + testList( [ 0, 6 ], [ 0, 0, 0, 1, 2, 3 ] ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( + '
                                                                                                                        ' + + '
                                                                                                                      • ' + + '' + + '' + + 'foobar' + + '' + + '
                                                                                                                      • ' + + '
                                                                                                                      ' + ); + } ); } ); function getViewPosition( root, path, view ) { From 7b2094579dcf1f209a70dc63f8c8a18bd166541b Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Mon, 18 Sep 2023 20:39:09 +0200 Subject: [PATCH 39/54] Tests: todo list item postfixers and user interactions. --- .../tododocumentlistediting.js | 147 +++++++++++++++++- 1 file changed, 143 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js index 005af8128bd..51b8fae3eb8 100644 --- a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js +++ b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js @@ -10,8 +10,11 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import TableEditing from '@ckeditor/ckeditor5-table/src/tableediting'; import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; +import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard'; +import { env } from '@ckeditor/ckeditor5-utils'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import TodoDocumentListEditing from '../../src/tododocumentlist/tododocumentlistediting'; @@ -23,13 +26,18 @@ import DocumentListPropertiesEditing from '../../src/documentlistproperties/docu import stubUid from '../documentlist/_utils/uid'; +/* global document */ + describe( 'TodoDocumentListEditing', () => { - let editor, model, view; + let editor, model, view, editorElement; testUtils.createSinonSandbox(); beforeEach( async () => { - editor = await VirtualTestEditor.create( { + editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + editor = await ClassicTestEditor.create( editorElement, { plugins: [ Paragraph, TodoDocumentListEditing, BlockQuoteEditing, TableEditing, HeadingEditing ] } ); @@ -39,8 +47,10 @@ describe( 'TodoDocumentListEditing', () => { stubUid(); } ); - afterEach( () => { - return editor.destroy(); + afterEach( async () => { + editorElement.remove(); + + await editor.destroy(); } ); it( 'should have pluginName', () => { @@ -652,6 +662,130 @@ describe( 'TodoDocumentListEditing', () => { } ); } ); + describe( 'postfixers', () => { + describe( '`todoListChecked` attribute should be the same in all blocks of a single list item', () => { + it( 'should add missing `todoListChecked` attribute to other blocks', () => { + testPostfixer( + 'foo' + + 'bar' + + 'baz', + + 'foo' + + 'bar' + + 'baz' + ); + } ); + + it( 'should add missing `todoListChecked` attribute to other blocks excluding nested list items', () => { + testPostfixer( + 'foo' + + 'bar' + + 'nested 1' + + 'nested 2' + + 'baz', + + 'foo' + + 'bar' + + 'nested 1' + + 'nested 2' + + 'baz' + ); + } ); + + it( 'should remove `todoListChecked` from other blocks', () => { + testPostfixer( + 'foo' + + 'bar' + + 'baz', + + 'foo' + + 'bar' + + 'baz' + ); + } ); + + it( 'should remove `todoListChecked` from other blocks excluding nested list items', () => { + testPostfixer( + 'foo' + + 'bar' + + 'nested 1' + + 'nested 2' + + 'baz', + + 'foo' + + 'bar' + + 'nested 1' + + 'nested 2' + + 'baz' + ); + } ); + } ); + + describe( '`todoListChecked` attribute should be applied only to todo list items', () => { + it( 'should remove `todoListChecked` from elements other than todo list items', () => { + testPostfixer( + 'foo' + + 'foo' + + 'baz', + + 'foo' + + 'foo' + + 'baz' + ); + } ); + + it( 'should remove `todoListChecked` attribute from list items that are converted from todo to bulleted type', () => { + setModelData( model, + '[foo' + + 'foo' + + 'baz]' + ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( + 'foo' + + 'foo' + + 'baz' + ); + + editor.execute( 'bulletedList' ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( + 'foo' + + 'foo' + + 'baz' + ); + } ); + } ); + } ); + + describe( 'user interaction events', () => { + it( 'should toggle check state of selected to-do list item on keystroke', () => { + const command = editor.commands.get( 'checkTodoList' ); + + sinon.spy( command, 'execute' ); + + const domEvtDataStub = { + keyCode: getCode( 'enter' ), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + + if ( env.isMac ) { + domEvtDataStub.metaKey = true; + } else { + domEvtDataStub.ctrlKey = true; + } + + view.document.fire( 'keydown', domEvtDataStub ); + + sinon.assert.calledOnce( command.execute ); + + view.document.fire( 'keydown', domEvtDataStub ); + + sinon.assert.calledTwice( command.execute ); + } ); + } ); + function testUpcast( input, output ) { editor.setData( input ); expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( output ); @@ -666,4 +800,9 @@ describe( 'TodoDocumentListEditing', () => { setModelData( model, input ); expect( editor.getData() ).to.equalMarkup( output ); } + + function testPostfixer( input, output ) { + setModelData( model, input ); + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( output ); + } } ); From 00d0db9ecaeca5483f6c41be653b432a9ad8562f Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 19 Sep 2023 10:29:51 +0200 Subject: [PATCH 40/54] Fixed checkbox toggling. --- .../src/tododocumentlist/tododocumentlistediting.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts index 0cd746fd3b3..87f7facc72b 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts @@ -298,7 +298,7 @@ export default class TodoDocumentListEditing extends Plugin { const viewPositionAfter = editing.view.createPositionAfter( viewTarget ); const modelPositionAfter = editing.mapper.toModelPosition( viewPositionAfter ); - const modelElement = modelPositionAfter.nodeAfter; + const modelElement = modelPositionAfter.parent; if ( modelElement && isListItemBlock( modelElement ) && modelElement.getAttribute( 'listType' ) == 'todo' ) { this._handleCheckmarkChange( modelElement ); From f13f3287c9e7798f155d6646a0e118d8bb319817 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Tue, 19 Sep 2023 15:18:18 +0200 Subject: [PATCH 41/54] Tests: arrow keys in todo list items. --- .../tododocumentlistediting.js | 175 +++++++++++++++++- 1 file changed, 174 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js index 51b8fae3eb8..4604be1e171 100644 --- a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js +++ b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js @@ -26,7 +26,7 @@ import DocumentListPropertiesEditing from '../../src/documentlistproperties/docu import stubUid from '../documentlist/_utils/uid'; -/* global document */ +/* global document, Event */ describe( 'TodoDocumentListEditing', () => { let editor, model, view, editorElement; @@ -784,6 +784,179 @@ describe( 'TodoDocumentListEditing', () => { sinon.assert.calledTwice( command.execute ); } ); + + it( 'should toggle check state of a to-do list item on clicking the checkbox', () => { + setModelData( model, + 'foo' + ); + + const command = editor.commands.get( 'checkTodoList' ); + + sinon.spy( command, 'execute' ); + + view.getDomRoot().querySelector( 'input' ).dispatchEvent( new Event( 'change', { 'bubbles': true } ) ); + + sinon.assert.calledOnce( command.execute ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( + 'foo' + ); + } ); + + describe( 'arrow keys', () => { + it( 'should move collapsed selection at start of following todo list item on right arrow in todo list item', () => { + setModelData( model, + 'foo[]' + + 'bar' + ); + + const eventData = { + keyCode: getCode( 'arrowRight' ), + preventDefault: () => {}, + stopPropagation: () => {} + }; + + view.document.fire( 'keydown', eventData ); + + expect( getModelData( model ) ).to.equalMarkup( + 'foo' + + '[]bar' + ); + } ); + + it( 'should move collapsed selection at start of following todo list item on right arrow in paragraph', () => { + setModelData( model, + 'foo[]' + + 'bar' + ); + + const eventData = { + keyCode: getCode( 'arrowRight' ), + preventDefault: () => {}, + stopPropagation: () => {} + }; + + view.document.fire( 'keydown', eventData ); + + expect( getModelData( model ) ).to.equalMarkup( + 'foo' + + '[]bar' + ); + } ); + + it( 'should do nothing if selection is at end of the last todo list item and right arrow is pressed', () => { + setModelData( model, + 'foo[]' + ); + + const eventData = { + keyCode: getCode( 'arrowRight' ), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy(), + domTarget: view.getDomRoot() + }; + + view.document.fire( 'keydown', eventData ); + + sinon.assert.notCalled( eventData.preventDefault ); + sinon.assert.notCalled( eventData.stopPropagation ); + + expect( getModelData( model ) ).to.equalMarkup( + 'foo[]' + ); + } ); + + it( 'should not move non-collapsed selection at start of following todo list item on right arrow key in todo list item', () => { + setModelData( model, + 'fo[o]' + + 'bar' + ); + + const eventData = { + keyCode: getCode( 'arrowRight' ), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy(), + domTarget: view.getDomRoot() + }; + + view.document.fire( 'keydown', eventData ); + + sinon.assert.notCalled( eventData.preventDefault ); + sinon.assert.notCalled( eventData.stopPropagation ); + + expect( getModelData( model ) ).to.equalMarkup( + 'fo[o]' + + 'bar' + ); + } ); + + it( 'should not move non-collapsed selection at start of following todo list item on right arrow key in paragraph', () => { + setModelData( model, + 'fo[o]' + + 'bar' + ); + + const eventData = { + keyCode: getCode( 'arrowRight' ), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy(), + domTarget: view.getDomRoot() + }; + + view.document.fire( 'keydown', eventData ); + + sinon.assert.notCalled( eventData.preventDefault ); + sinon.assert.notCalled( eventData.stopPropagation ); + + expect( getModelData( model ) ).to.equalMarkup( + 'fo[o]' + + 'bar' + ); + } ); + + it( 'should move a collapsed selection to the end of the preceding todo list item on left arrow', () => { + setModelData( model, + 'foo' + + '[]bar' + ); + + const eventData = { + keyCode: getCode( 'arrowLeft' ), + preventDefault: () => {}, + stopPropagation: () => {}, + domTarget: view.getDomRoot() + }; + + view.document.fire( 'keydown', eventData ); + + expect( getModelData( model ) ).to.equalMarkup( + 'foo[]' + + 'bar' + ); + } ); + + it( 'should do nothing if selection is at start of first element which is a todo list item and left arrow is pressed', () => { + setModelData( model, + '[]foo' + ); + + const eventData = { + keyCode: getCode( 'arrowLeft' ), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy(), + domTarget: view.getDomRoot() + }; + + view.document.fire( 'keydown', eventData ); + + sinon.assert.notCalled( eventData.preventDefault ); + sinon.assert.notCalled( eventData.stopPropagation ); + + expect( getModelData( model ) ).to.equalMarkup( + '[]foo' + ); + } ); + } ); } ); function testUpcast( input, output ) { From 02a8e6156dcb54f25ec0bd48b927552ca1b2cf07 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 19 Sep 2023 16:30:27 +0200 Subject: [PATCH 42/54] Added test. --- .../tododocumentlistediting.ts | 6 +- .../tododocumentlistediting.js | 62 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts index 87f7facc72b..7f1bda89806 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts @@ -375,14 +375,16 @@ function todoItemInputConverter(): GetCallback { const modelItem = modelCursor.parent as Element; const viewItem = data.viewItem; - if ( viewItem.getAttribute( 'type' ) != 'checkbox' || !modelCursor.isAtStart || !modelItem.hasAttribute( 'listType' ) ) { + if ( !conversionApi.consumable.test( viewItem, { name: true } ) ) { return; } - if ( !conversionApi.consumable.consume( viewItem, { name: true } ) ) { + if ( viewItem.getAttribute( 'type' ) != 'checkbox' || !modelCursor.isAtStart || !modelItem.hasAttribute( 'listType' ) ) { return; } + conversionApi.consumable.consume( viewItem, { name: true } ); + const writer = conversionApi.writer; writer.setAttribute( 'listType', 'todo', modelItem ); diff --git a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js index 4604be1e171..785c3c8277f 100644 --- a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js +++ b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js @@ -258,6 +258,18 @@ describe( 'TodoDocumentListEditing', () => { '' ); } ); + + it( 'should not convert checkbox if consumed by other converter', () => { + model.schema.register( 'input', { inheritAllFrom: '$inlineObject' } ); + editor.conversion.elementToElement( { model: 'input', view: 'input', converterPriority: 'high' } ); + + testUpcast( + '
                                                                                                                        ' + + '
                                                                                                                      • foo
                                                                                                                      • ' + + '
                                                                                                                      ', + 'foo' + ); + } ); } ); describe( 'upcast - list properties integration', () => { @@ -310,6 +322,56 @@ describe( 'TodoDocumentListEditing', () => { } ); } ); + describe.skip( 'upcast - GHS integration', () => { + let editor, model; + + beforeEach( async () => { + editor = await VirtualTestEditor.create( { + plugins: [ Paragraph, TodoDocumentListEditing, DocumentListPropertiesEditing ], + list: { + properties: { + startIndex: true + } + } + } ); + + model = editor.model; + view = editor.editing.view; + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + it( 'should not convert list style on to-do list', () => { + editor.setData( + '
                                                                                                                        ' + + '
                                                                                                                      • Foo
                                                                                                                      • ' + + '
                                                                                                                      • Bar
                                                                                                                      • ' + + '
                                                                                                                      ' + ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( + 'Foo' + + 'Bar' + ); + } ); + + it( 'should not convert list start on to-do list', () => { + editor.setData( + '
                                                                                                                        ' + + '
                                                                                                                      1. Foo
                                                                                                                      2. ' + + '
                                                                                                                      3. Bar
                                                                                                                      4. ' + + '
                                                                                                                      ' + ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( + 'Foo' + + 'Bar' + ); + } ); + } ); + describe( 'downcast - editing', () => { it( 'should convert a todo list item', () => { testEditing( From 636ec2cebf5b2e73ed953c3acc443671bf29786a Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 20 Sep 2023 13:29:58 +0200 Subject: [PATCH 43/54] Adding tests. --- .../tododocumentlistediting.ts | 3 + .../tododocumentlistediting.js | 90 +++++++++++++------ 2 files changed, 68 insertions(+), 25 deletions(-) diff --git a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts index 7f1bda89806..acceb638682 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts @@ -103,6 +103,9 @@ export default class TodoDocumentListEditing extends Plugin { dispatcher.on( 'element:label', elementUpcastConsumingConverter( { name: 'label', classes: 'todo-list__label' } ) ); + dispatcher.on( 'element:label', elementUpcastConsumingConverter( + { name: 'label', classes: [ 'todo-list__label', 'todo-list__label_without-description' ] } + ) ); dispatcher.on( 'element:span', elementUpcastConsumingConverter( { name: 'span', classes: 'todo-list__label__description' } ) ); diff --git a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js index 785c3c8277f..15cd691a07f 100644 --- a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js +++ b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js @@ -3,11 +3,14 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ +/* global document, Event */ + import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting'; import HeadingEditing from '@ckeditor/ckeditor5-heading/src/headingediting'; import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import TableEditing from '@ckeditor/ckeditor5-table/src/tableediting'; +import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtmlsupport'; import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; @@ -26,8 +29,6 @@ import DocumentListPropertiesEditing from '../../src/documentlistproperties/docu import stubUid from '../documentlist/_utils/uid'; -/* global document, Event */ - describe( 'TodoDocumentListEditing', () => { let editor, model, view, editorElement; @@ -270,6 +271,18 @@ describe( 'TodoDocumentListEditing', () => { 'foo' ); } ); + + it( 'should not convert label element if already consumed', () => { + model.schema.register( 'label', { inheritAllFrom: '$inlineObject', allowChildren: '$text' } ); + editor.conversion.elementToElement( { model: 'label', view: 'label', converterPriority: 'high' } ); + + testUpcast( + '
                                                                                                                        ' + + '
                                                                                                                      • ' + + '
                                                                                                                      ', + '' + ); + } ); } ); describe( 'upcast - list properties integration', () => { @@ -322,16 +335,24 @@ describe( 'TodoDocumentListEditing', () => { } ); } ); - describe.skip( 'upcast - GHS integration', () => { - let editor, model; + describe( 'upcast - GHS integration', () => { + let element, editor, model; beforeEach( async () => { - editor = await VirtualTestEditor.create( { - plugins: [ Paragraph, TodoDocumentListEditing, DocumentListPropertiesEditing ], - list: { - properties: { - startIndex: true - } + element = document.createElement( 'div' ); + document.body.appendChild( element ); + + editor = await ClassicTestEditor.create( element, { + plugins: [ Paragraph, TodoDocumentListEditing, GeneralHtmlSupport ], + htmlSupport: { + allow: [ + { + name: /./, + styles: true, + attributes: true, + classes: true + } + ] } } ); @@ -339,35 +360,54 @@ describe( 'TodoDocumentListEditing', () => { view = editor.editing.view; } ); - afterEach( () => { - return editor.destroy(); + afterEach( async () => { + element.remove(); + await editor.destroy(); } ); - it( 'should not convert list style on to-do list', () => { + it( 'should consume all to-do list related elements and attributes so GHS will not handle them (with description)', () => { editor.setData( - '
                                                                                                                        ' + - '
                                                                                                                      • Foo
                                                                                                                      • ' + - '
                                                                                                                      • Bar
                                                                                                                      • ' + + '
                                                                                                                          ' + + '
                                                                                                                        • ' + + '' + + '
                                                                                                                        • ' + '
                                                                                                                        ' ); expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( - 'Foo' + - 'Bar' + '' + + 'foo' + + '' ); } ); - it( 'should not convert list start on to-do list', () => { + it( 'should consume all to-do list related elements and attributes so GHS will not handle them (without description)', () => { editor.setData( - '
                                                                                                                          ' + - '
                                                                                                                        1. Foo
                                                                                                                        2. ' + - '
                                                                                                                        3. Bar
                                                                                                                        4. ' + - '
                                                                                                                        ' + '
                                                                                                                          ' + + '
                                                                                                                        • ' + + '' + + '

                                                                                                                          foo

                                                                                                                          ' + + '
                                                                                                                        • ' + + '
                                                                                                                        ' ); expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( - 'Foo' + - 'Bar' + 'foo' + ); + } ); + + it( 'should not consume other label elements', () => { + editor.setData( '

                                                                                                                        ' ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( + '<$text htmlLabel="{}">foo' ); } ); } ); From 17c31edc20508b43b8056f70faad0df9f4933aab Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 20 Sep 2023 14:03:20 +0200 Subject: [PATCH 44/54] Adding tests. --- .../tododocumentlistediting.js | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js index 15cd691a07f..a45da277098 100644 --- a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js +++ b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js @@ -905,6 +905,64 @@ describe( 'TodoDocumentListEditing', () => { ); } ); + it( 'should toggle check state of a to-do list item on todoCheckboxChange event with input target element', () => { + setModelData( model, + 'foo' + ); + + const command = editor.commands.get( 'checkTodoList' ); + + sinon.spy( command, 'execute' ); + + view.document.fire( 'todoCheckboxChange', { + target: view.document.getRoot().getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 ) + } ); + + sinon.assert.calledOnce( command.execute ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( + 'foo' + ); + } ); + + it( 'should not toggle check state of a to-do list item on todoCheckboxChange event without target element', () => { + setModelData( model, + 'foo' + ); + + const command = editor.commands.get( 'checkTodoList' ); + + sinon.spy( command, 'execute' ); + + view.document.fire( 'todoCheckboxChange', {} ); + + sinon.assert.notCalled( command.execute ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( + 'foo' + ); + } ); + + it( 'should not toggle check state of a to-do list item on todoCheckboxChange event with target element', () => { + setModelData( model, + 'foo' + ); + + const command = editor.commands.get( 'checkTodoList' ); + + sinon.spy( command, 'execute' ); + + view.document.fire( 'todoCheckboxChange', { + target: view.document.getRoot() + } ); + + sinon.assert.notCalled( command.execute ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( + 'foo' + ); + } ); + describe( 'arrow keys', () => { it( 'should move collapsed selection at start of following todo list item on right arrow in todo list item', () => { setModelData( model, From f10cda7a8bc4c3ed0f61e82247672a8c5ac31e7e Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 20 Sep 2023 14:19:29 +0200 Subject: [PATCH 45/54] Adding tests. --- .../tests/tododocumentlist/checktododocumentlistcommand.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/ckeditor5-list/tests/tododocumentlist/checktododocumentlistcommand.js b/packages/ckeditor5-list/tests/tododocumentlist/checktododocumentlistcommand.js index bff8ed6a9ae..4b46eb0cf89 100644 --- a/packages/ckeditor5-list/tests/tododocumentlist/checktododocumentlistcommand.js +++ b/packages/ckeditor5-list/tests/tododocumentlist/checktododocumentlistcommand.js @@ -40,6 +40,12 @@ describe( 'CheckTodoListCommand', () => { expect( command.isEnabled ).to.equal( true ); } ); + it( 'should be enabled when item is already checked', () => { + setModelData( model, 'f[]oo' ); + + expect( command.isEnabled ).to.equal( true ); + } ); + it( 'should be enabled when non-collapsed selection is inside to-do list item', () => { setModelData( model, 'f[o]o' ); From 379514327dcd4f36d970645da2c9670d8666bdd0 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 20 Sep 2023 14:33:06 +0200 Subject: [PATCH 46/54] Added missing comments. --- packages/ckeditor5-list/theme/todolist.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-list/theme/todolist.css b/packages/ckeditor5-list/theme/todolist.css index 5d0a4928978..73b0c517ba7 100644 --- a/packages/ckeditor5-list/theme/todolist.css +++ b/packages/ckeditor5-list/theme/todolist.css @@ -75,7 +75,7 @@ } /* - * TODO + * To-do list content styles. */ .ck-content .todo-list { list-style: none; @@ -105,7 +105,7 @@ } /* - * + * To-do list editing view styles. */ .ck-editor__editable.ck-content .todo-list .todo-list__label { /* From f00e6fcf5405d76cf285b18b361b590c180f5224 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 20 Sep 2023 14:59:25 +0200 Subject: [PATCH 47/54] Fixed test. --- .../tests/tododocumentlist/tododocumentlistediting.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js index a45da277098..8ebf29822fd 100644 --- a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js +++ b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js @@ -179,14 +179,15 @@ describe( 'TodoDocumentListEditing', () => { '
                                                                                                                      • ' + '
                                                                                                                          ' + '
                                                                                                                        • ' + - 'foo
                                                                                                                        • ' + - '
                                                                                                                          • foo
                                                                                                                          ' + - '
                                                                                                                        ' + + 'foo' + + '
                                                                                                                        • bar
                                                                                                                        ' + + '
                                                                                                                      • ' + + '
                                                                                                                      ' + '' + '
                                                                                                                    ', '' + - 'foo' + - 'foo' + 'foo' + + 'bar' ); } ); From 9724e172fefe61f1edfdd0ceb90878cfde125c90 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 20 Sep 2023 15:40:48 +0200 Subject: [PATCH 48/54] Fixed white-list of allowed attributes on code-block. --- .../ckeditor5-code-block/src/codeblockediting.ts | 14 ++++++++------ .../src/documentlist/documentlistediting.ts | 6 +++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/ckeditor5-code-block/src/codeblockediting.ts b/packages/ckeditor5-code-block/src/codeblockediting.ts index 562ee2f3647..6514c63bac3 100644 --- a/packages/ckeditor5-code-block/src/codeblockediting.ts +++ b/packages/ckeditor5-code-block/src/codeblockediting.ts @@ -22,6 +22,8 @@ import { type Element } from 'ckeditor5/src/engine'; +import type { DocumentListEditing } from '@ckeditor/ckeditor5-list'; + import CodeBlockCommand from './codeblockcommand'; import IndentCodeBlockCommand from './indentcodeblockcommand'; import OutdentCodeBlockCommand from './outdentcodeblockcommand'; @@ -97,7 +99,8 @@ export default class CodeBlockEditing extends Plugin { const schema = editor.model.schema; const model = editor.model; const view = editor.editing.view; - const isDocumentListEditingLoaded = editor.plugins.has( 'DocumentListEditing' ); + const documentListEditing: DocumentListEditing | null = editor.plugins.has( 'DocumentListEditing' ) ? + editor.plugins.get( 'DocumentListEditing' ) : null; const normalizedLanguagesDefs = getNormalizedAndLocalizedLanguageDefinitions( editor ); @@ -133,11 +136,10 @@ export default class CodeBlockEditing extends Plugin { // Allow all list* attributes on `codeBlock` (integration with DocumentList). // Disallow all attributes on $text inside `codeBlock`. schema.addAttributeCheck( ( context, attributeName ) => { - const isDocumentListAttributeOnCodeBlock = context.endsWith( 'codeBlock' ) && - attributeName.startsWith( 'list' ) && - attributeName !== 'list'; - - if ( isDocumentListEditingLoaded && isDocumentListAttributeOnCodeBlock ) { + if ( + context.endsWith( 'codeBlock' ) && + documentListEditing && documentListEditing.getListAttributeNames().includes( attributeName ) + ) { return true; } diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts index 1d4538913b1..aa706dbb16f 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts @@ -195,7 +195,7 @@ export default class DocumentListEditing extends Plugin { /** * Returns list of model attribute names that should affect downcast conversion. */ - private _getListAttributeNames() { + public getListAttributeNames(): Array { return [ ...LIST_BASE_ATTRIBUTES, ...this._downcastStrategies.map( strategy => strategy.attributeName ) @@ -389,7 +389,7 @@ export default class DocumentListEditing extends Plugin { private _setupConversion() { const editor = this.editor; const model = editor.model; - const attributeNames = this._getListAttributeNames(); + const attributeNames = this.getListAttributeNames(); editor.conversion.for( 'upcast' ) // Convert
                                                                                                                  • to a generic paragraph so the content of
                                                                                                                  • is always inside a block. @@ -478,7 +478,7 @@ export default class DocumentListEditing extends Plugin { */ private _setupModelPostFixing() { const model = this.editor.model; - const attributeNames = this._getListAttributeNames(); + const attributeNames = this.getListAttributeNames(); // Register list fixing. // First the low level handler. From 23434fce8cc1da79f5c4fb5dfce2f2185532ae87 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 20 Sep 2023 15:42:29 +0200 Subject: [PATCH 49/54] Fixed to-do list item alignment. --- .../tododocumentlistediting.ts | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts index acceb638682..7beca563929 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts @@ -117,7 +117,7 @@ export default class TodoDocumentListEditing extends Plugin { editor.conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: ( element, { writer } ) => { - if ( isDescriptionBlock( element ) ) { + if ( isDescriptionBlock( element, documentListEditing.getListAttributeNames() ) ) { return writer.createContainerElement( 'span', { class: 'todo-list__label__description' } ); } }, @@ -166,13 +166,13 @@ export default class TodoDocumentListEditing extends Plugin { }, canWrapElement( modelElement ) { - return isDescriptionBlock( modelElement ); + return isDescriptionBlock( modelElement, documentListEditing.getListAttributeNames() ); }, createWrapperElement( writer, modelElement, { dataPipeline } ) { const classes = [ 'todo-list__label' ]; - if ( !isDescriptionBlock( modelElement ) ) { + if ( !isDescriptionBlock( modelElement, documentListEditing.getListAttributeNames() ) ) { classes.push( 'todo-list__label_without-description' ); } @@ -198,7 +198,7 @@ export default class TodoDocumentListEditing extends Plugin { // Verifies if a to-do list block requires reconversion of a first item downcasted as an item description. documentListEditing.on( 'checkElement', ( evt, { modelElement, viewElement } ) => { - const isFirstTodoModelParagraphBlock = isDescriptionBlock( modelElement ); + const isFirstTodoModelParagraphBlock = isDescriptionBlock( modelElement, documentListEditing.getListAttributeNames() ); const hasViewClass = viewElement.hasClass( 'todo-list__label__description' ); if ( hasViewClass != isFirstTodoModelParagraphBlock ) { @@ -444,10 +444,29 @@ function attributeUpcastConsumingConverter( matcherPattern: MatcherPattern ): Ge /** * Returns true if the given list item block should be converted as a description block of a to-do list item. */ -function isDescriptionBlock( modelElement: Element ): boolean { +function isDescriptionBlock( modelElement: Element, listAttributeNames: Array ): boolean { return modelElement.is( 'element', 'paragraph' ) && modelElement.getAttribute( 'listType' ) == 'todo' && - isFirstBlockOfListItem( modelElement ); + isFirstBlockOfListItem( modelElement ) && + hasOnlyListAttributes( modelElement, listAttributeNames ); +} + +/** + * Returns true if only attributes from the given list are present on the model element. + */ +function hasOnlyListAttributes( modelElement: Element, attributeNames: Array ): boolean { + for ( const attributeKey of modelElement.getAttributeKeys() ) { + // Ignore selection attributes stored on block elements. + if ( attributeKey.startsWith( 'selection:' ) ) { + continue; + } + + if ( !attributeNames.includes( attributeKey ) ) { + return false; + } + } + + return true; } /** From 7b183f3f1ecda1e06d0548d80e5ccf9199562dd7 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 20 Sep 2023 16:47:08 +0200 Subject: [PATCH 50/54] Adding tests. --- .../tododocumentlistediting.js | 94 ++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js index 8ebf29822fd..c6035d2c03a 100644 --- a/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js +++ b/packages/ckeditor5-list/tests/tododocumentlist/tododocumentlistediting.js @@ -11,6 +11,7 @@ import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import TableEditing from '@ckeditor/ckeditor5-table/src/tableediting'; import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtmlsupport'; +import AlignmentEditing from '@ckeditor/ckeditor5-alignment/src/alignmentediting'; import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; @@ -39,7 +40,7 @@ describe( 'TodoDocumentListEditing', () => { document.body.appendChild( editorElement ); editor = await ClassicTestEditor.create( editorElement, { - plugins: [ Paragraph, TodoDocumentListEditing, BlockQuoteEditing, TableEditing, HeadingEditing ] + plugins: [ Paragraph, TodoDocumentListEditing, BlockQuoteEditing, TableEditing, HeadingEditing, AlignmentEditing ] } ); model = editor.model; @@ -604,6 +605,82 @@ describe( 'TodoDocumentListEditing', () => { '
                                                                                                                  ' ); } ); + + it( 'should not use description span if there is an alignment set on the paragraph', () => { + setModelData( model, + 'foo' + ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( + '
                                                                                                                    ' + + '
                                                                                                                  • ' + + '' + + '' + + '' + + '' + + 'foo' + + '' + + '
                                                                                                                  • ' + + '
                                                                                                                  ' + ); + + editor.execute( 'alignment', { value: 'right' } ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( + '
                                                                                                                    ' + + '
                                                                                                                  • ' + + '' + + '' + + '' + + '' + + '' + + '

                                                                                                                    ' + + 'foo' + + '

                                                                                                                    ' + + '
                                                                                                                  • ' + + '
                                                                                                                  ' + ); + + editor.execute( 'alignment', { value: 'left' } ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( + '
                                                                                                                    ' + + '
                                                                                                                  • ' + + '' + + '' + + '' + + '' + + 'foo' + + '' + + '
                                                                                                                  • ' + + '
                                                                                                                  ' + ); + } ); + + it( 'should use description span even if there is an selection attribute on block', () => { + setModelData( model, + '[]' + ); + + model.change( writer => writer.setSelectionAttribute( 'bold', true ) ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( + '' + ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( + '
                                                                                                                    ' + + '
                                                                                                                  • ' + + '' + + '' + + '' + + '' + + '' + + '' + + '
                                                                                                                  • ' + + '
                                                                                                                  ' + ); + } ); } ); describe( 'downcast - data', () => { @@ -763,6 +840,21 @@ describe( 'TodoDocumentListEditing', () => { '
                                                                                                                ' ); } ); + + it( 'should convert a todo list item with alignment set', () => { + testData( + 'foo', + + '
                                                                                                                  ' + + '
                                                                                                                • ' + + '' + + '

                                                                                                                  foo

                                                                                                                  ' + + '
                                                                                                                • ' + + '
                                                                                                                ' + ); + } ); } ); describe( 'postfixers', () => { From 037489377f4b449c65735525b43147107328def9 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 20 Sep 2023 17:06:27 +0200 Subject: [PATCH 51/54] Removed the checkbox toggling animation to avoid glitches while reusing DOM elements (by renderer). --- packages/ckeditor5-list/theme/todolist.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-list/theme/todolist.css b/packages/ckeditor5-list/theme/todolist.css index 73b0c517ba7..915c28085f6 100644 --- a/packages/ckeditor5-list/theme/todolist.css +++ b/packages/ckeditor5-list/theme/todolist.css @@ -41,7 +41,7 @@ height: 100%; border: 1px solid hsl(0, 0%, 20%); border-radius: 2px; - transition: 250ms ease-in-out box-shadow, 250ms ease-in-out background, 250ms ease-in-out border; + transition: 250ms ease-in-out box-shadow; } &::after { From aa4b2bff917f053e1daea9746c8581e6effb2512 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 20 Sep 2023 17:15:30 +0200 Subject: [PATCH 52/54] Fixed tests. --- packages/ckeditor5-code-block/package.json | 3 ++- .../tests/codeblock-integration.js | 23 +++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-code-block/package.json b/packages/ckeditor5-code-block/package.json index 36f0c79d7c6..6de7a881ec9 100644 --- a/packages/ckeditor5-code-block/package.json +++ b/packages/ckeditor5-code-block/package.json @@ -22,9 +22,10 @@ "@ckeditor/ckeditor5-clipboard": "39.0.2", "@ckeditor/ckeditor5-core": "39.0.2", "@ckeditor/ckeditor5-dev-utils": "^38.0.0", + "@ckeditor/ckeditor5-editor-classic": "39.0.2", "@ckeditor/ckeditor5-engine": "39.0.2", "@ckeditor/ckeditor5-enter": "39.0.2", - "@ckeditor/ckeditor5-editor-classic": "39.0.2", + "@ckeditor/ckeditor5-html-support": "39.0.2", "@ckeditor/ckeditor5-image": "39.0.2", "@ckeditor/ckeditor5-indent": "39.0.2", "@ckeditor/ckeditor5-list": "39.0.2", diff --git a/packages/ckeditor5-code-block/tests/codeblock-integration.js b/packages/ckeditor5-code-block/tests/codeblock-integration.js index 365aed21d9c..7cb219ac1f2 100644 --- a/packages/ckeditor5-code-block/tests/codeblock-integration.js +++ b/packages/ckeditor5-code-block/tests/codeblock-integration.js @@ -12,6 +12,8 @@ import GFMDataProcessor from '@ckeditor/ckeditor5-markdown-gfm/src/gfmdataproces import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import ImageInlineEditing from '@ckeditor/ckeditor5-image/src/image/imageinlineediting'; import DocumentListEditing from '@ckeditor/ckeditor5-list/src/documentlist/documentlistediting'; +import DocumentListPropertiesEditing from '@ckeditor/ckeditor5-list/src/documentlistproperties/documentlistpropertiesediting'; +import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtmlsupport'; import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import CodeBlockUI from '../src/codeblockui'; @@ -178,7 +180,14 @@ describe( 'CodeBlock - integration', () => { beforeEach( async () => { editor = await ClassicTestEditor .create( '', { - plugins: [ CodeBlockEditing, DocumentListEditing, Enter, Paragraph ] + plugins: [ + CodeBlockEditing, DocumentListEditing, DocumentListPropertiesEditing, Enter, Paragraph, GeneralHtmlSupport + ], + htmlSupport: { + allow: [ + { name: /./, attributes: true, styles: true, classes: true } + ] + } } ); model = editor.model; @@ -188,22 +197,26 @@ describe( 'CodeBlock - integration', () => { await editor.destroy(); } ); - it( 'should allow all attributes starting with list* in the schema', () => { + it( 'should allow all list attributes in the schema', () => { setData( model, '[]foo' ); const codeBlock = model.document.getRoot().getChild( 0 ); expect( model.schema.checkAttribute( codeBlock, 'listItemId' ), 'listItemId' ).to.be.true; expect( model.schema.checkAttribute( codeBlock, 'listType' ), 'listType' ).to.be.true; - expect( model.schema.checkAttribute( codeBlock, 'listStart' ), 'listStart' ).to.be.true; - expect( model.schema.checkAttribute( codeBlock, 'listFoo' ), 'listFoo' ).to.be.true; + expect( model.schema.checkAttribute( codeBlock, 'listStyle' ), 'listStyle' ).to.be.true; + expect( model.schema.checkAttribute( codeBlock, 'htmlLiAttributes' ), 'htmlLiAttributes' ).to.be.true; + expect( model.schema.checkAttribute( codeBlock, 'htmlUlAttributes' ), 'htmlUlAttributes' ).to.be.true; + expect( model.schema.checkAttribute( codeBlock, 'htmlOlAttributes' ), 'htmlOlAttributes' ).to.be.true; } ); - it( 'should disallow attributes that do not start with "list" in the schema but include the sequence', () => { + it( 'should disallow attributes that are not registered as list attributes', () => { setData( model, '[]foo' ); const codeBlock = model.document.getRoot().getChild( 0 ); + expect( model.schema.checkAttribute( codeBlock, 'listReversed' ), 'listReversed' ).to.be.false; + expect( model.schema.checkAttribute( codeBlock, 'listStart' ), 'listStart' ).to.be.false; expect( model.schema.checkAttribute( codeBlock, 'list' ), 'list' ).to.be.false; expect( model.schema.checkAttribute( codeBlock, 'fooList' ), 'fooList' ).to.be.false; expect( model.schema.checkAttribute( codeBlock, 'alist' ), 'alist' ).to.be.false; From 960220d5fb70c614031fc6c4defd9458b77b4d83 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 20 Sep 2023 17:31:10 +0200 Subject: [PATCH 53/54] The inline html elements should not be formatted in source editing (input and textarea). --- .../src/utils/formathtml.ts | 1 - .../tests/utils/formathtml.js | 15 +++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/ckeditor5-source-editing/src/utils/formathtml.ts b/packages/ckeditor5-source-editing/src/utils/formathtml.ts index 4bf74674be3..6c7a21e8578 100644 --- a/packages/ckeditor5-source-editing/src/utils/formathtml.ts +++ b/packages/ckeditor5-source-editing/src/utils/formathtml.ts @@ -60,7 +60,6 @@ export function formatHtml( input: string ): string { { name: 'table', isVoid: false }, { name: 'tbody', isVoid: false }, { name: 'td', isVoid: false }, - { name: 'textarea', isVoid: false }, { name: 'th', isVoid: false }, { name: 'thead', isVoid: false }, { name: 'tr', isVoid: false }, diff --git a/packages/ckeditor5-source-editing/tests/utils/formathtml.js b/packages/ckeditor5-source-editing/tests/utils/formathtml.js index 57b551f59e0..316472dcbb7 100644 --- a/packages/ckeditor5-source-editing/tests/utils/formathtml.js +++ b/packages/ckeditor5-source-editing/tests/utils/formathtml.js @@ -179,14 +179,13 @@ describe( 'SourceEditing utils', () => { '
                                                                                                                \n' + '
                                                                                                                \n' + '
                                                                                                                \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + + ' ' + + '' + + '' + + '' + + '' + + '' + + '\n' + '
                                                                                                                \n' + '
                                                                                                                \n' + '
                                                                                                                '; From 7e4b6f625a19d02c28cec5e3f6055676936073c0 Mon Sep 17 00:00:00 2001 From: Arkadiusz Filipczak Date: Thu, 21 Sep 2023 14:35:24 +0200 Subject: [PATCH 54/54] Apply suggestions from code review. --- packages/ckeditor5-list/src/documentlist/utils/listwalker.ts | 2 +- .../src/tododocumentlist/tododocumentlistediting.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/utils/listwalker.ts b/packages/ckeditor5-list/src/documentlist/utils/listwalker.ts index f9745e83aa6..95130c29732 100644 --- a/packages/ckeditor5-list/src/documentlist/utils/listwalker.ts +++ b/packages/ckeditor5-list/src/documentlist/utils/listwalker.ts @@ -216,7 +216,7 @@ export function* iterateSiblingListBlocks( direction: 'forward' | 'backward' = 'forward' ): IterableIterator { const isForward = direction == 'forward'; - const previousNodesByIndent = []; // Last seen nodes of lower indented lists. + const previousNodesByIndent: Array = []; // Last seen nodes of lower indented lists. let previous = null; while ( isListItemBlock( node ) ) { diff --git a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts index 7beca563929..570facf16fc 100644 --- a/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts +++ b/packages/ckeditor5-list/src/tododocumentlist/tododocumentlistediting.ts @@ -152,7 +152,7 @@ export default class TodoDocumentListEditing extends Plugin { { checked: 'checked' } : null ), - ... ( dataPipeline ? + ...( dataPipeline ? { disabled: 'disabled' } : { tabindex: '-1' } )