8
0
Эх сурвалжийг харах

Handle a case when remove a content between two lists.

Kamil Piechaczek 5 жил өмнө
parent
commit
f7dc25ea6f

+ 103 - 1
packages/ckeditor5-list/src/liststyleediting.js

@@ -10,7 +10,7 @@
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import ListEditing from './listediting';
 import ListStyleCommand from './liststylecommand';
-import { getSiblingListItem } from './utils';
+import { getSiblingListItem, getSiblingNodes } from './utils';
 
 const DEFAULT_LIST_TYPE = 'default';
 
@@ -66,6 +66,9 @@ export default class ListStyleEditing extends Plugin {
 		// Set up conversion.
 		editor.conversion.for( 'upcast' ).add( upcastListItemStyle() );
 		editor.conversion.for( 'downcast' ).add( downcastListStyleAttribute() );
+
+		// Handle merging two separated lists into the single oen.
+		this._mergeListStyleAttributeWhileMergingLists();
 	}
 
 	/**
@@ -80,6 +83,105 @@ export default class ListStyleEditing extends Plugin {
 			editor.model.document.registerPostFixer( removeListStyleAttributeFromTodoList( editor ) );
 		}
 	}
+
+	/**
+	 * Starts listening to {@link module:engine/model/model~Model#deleteContent} checks whether two lists will be merged into a single one
+	 * after deleting the content.
+	 *
+	 * The purpose of this action is to adjust the `listStyle` value for the list that was merged.
+	 *
+	 * Consider the following model's content:
+	 *
+	 *     <listItem listIndent="0" listType="bulleted" listStyle="square">UL List item 1</listItem>
+	 *     <listItem listIndent="0" listType="bulleted" listStyle="square">UL List item 2</listItem>
+	 *     <paragraph>[A paragraph.]</paragraph>
+	 *     <listItem listIndent="0" listType="bulleted" listStyle="circle">UL List item 1</listItem>
+	 *     <listItem listIndent="0" listType="bulleted" listStyle="circle">UL List item 2</listItem>
+	 *
+	 * After removing the paragraph element, the second list will be merged into the first one.
+	 * We want to inherit the `listStyle` attribute for the second list from the first one.
+	 *
+	 *     <listItem listIndent="0" listType="bulleted" listStyle="square">UL List item 1</listItem>
+	 *     <listItem listIndent="0" listType="bulleted" listStyle="square">UL List item 2</listItem>
+	 *     <listItem listIndent="0" listType="bulleted" listStyle="square">UL List item 1</listItem>
+	 *     <listItem listIndent="0" listType="bulleted" listStyle="square">UL List item 2</listItem>
+	 *
+	 * See https://github.com/ckeditor/ckeditor5/issues/7879.
+	 *
+	 * @private
+	 */
+	_mergeListStyleAttributeWhileMergingLists() {
+		const editor = this.editor;
+		const model = editor.model;
+
+		// First the most-outer `listItem` in the first list reference.
+		// If found, lists should be merged and this `listItem` provides the `listStyle` attribute
+		// and it' also a starting point when searching for items in the second list.
+		let firstMostOuterItem;
+
+		this.listenTo( model, 'deleteContent', ( evt, [ selection ] ) => {
+			const firstPosition = selection.getFirstPosition();
+			const lastPosition = selection.getLastPosition();
+
+			// Typing or removing content in a single item. Aborting.
+			if ( firstPosition.parent === lastPosition.parent ) {
+				return;
+			}
+
+			// An element before the content that will be removed is not a list.
+			if ( !firstPosition.parent.is( 'element', 'listItem' ) ) {
+				return;
+			}
+
+			const nextSibling = lastPosition.parent.nextSibling;
+
+			// An element after the content that will be removed is not a list.
+			if ( !nextSibling || !nextSibling.is( 'element', 'listItem' ) ) {
+				return;
+			}
+
+			const mostOuterItemList = getSiblingListItem( firstPosition.parent, {
+				sameIndent: true,
+				listIndent: 0
+			} );
+
+			if ( mostOuterItemList.getAttribute( 'listType' ) === nextSibling.getAttribute( 'listType' ) ) {
+				firstMostOuterItem = mostOuterItemList;
+			}
+		}, { priority: 'high' } );
+
+		this.listenTo( model, 'deleteContent', () => {
+			if ( !firstMostOuterItem ) {
+				return;
+			}
+
+			model.change( writer => {
+				// Find the first most-outer item list in the merged list.
+				// A case when the first list item in the second list was merged into the last item in the first list.
+				//
+				// <listItem listIndent="0" listType="bulleted" listStyle="square">UL List item 1</listItem>
+				// <listItem listIndent="0" listType="bulleted" listStyle="square">UL List item 2</listItem>
+				// <listItem listIndent="0" listType="bulleted" listStyle="circle">[]UL List item 1</listItem>
+				// <listItem listIndent="0" listType="bulleted" listStyle="circle">UL List item 2</listItem>
+				const secondListMostOuterItem = getSiblingListItem( firstMostOuterItem.nextSibling, {
+					sameIndent: true,
+					listIndent: 0,
+					direction: 'forward'
+				} );
+
+				const items = [
+					secondListMostOuterItem,
+					...getSiblingNodes( writer.createPositionAt( secondListMostOuterItem, 0 ), 'forward' )
+				];
+
+				for ( const listItem of items ) {
+					writer.setAttribute( 'listStyle', firstMostOuterItem.getAttribute( 'listStyle' ), listItem );
+				}
+			} );
+
+			firstMostOuterItem = null;
+		}, { priority: 'low' } );
+	}
 }
 
 // Returns a converter that consumes the `style` attribute and search for `list-style-type` definition.

+ 261 - 0
packages/ckeditor5-list/tests/liststyleediting.js

@@ -11,6 +11,7 @@ import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
 import ListStyleEditing from '../src/liststyleediting';
 import TodoListEditing from '../src/todolistediting';
 import ListStyleCommand from '../src/liststylecommand';
+import Typing from '@ckeditor/ckeditor5-typing/src/typing';
 
 describe( 'ListStyleEditing', () => {
 	let editor, model, view;
@@ -892,5 +893,265 @@ describe( 'ListStyleEditing', () => {
 				);
 			} );
 		} );
+
+		describe( 'removing content between two lists', () => {
+			let editor, model;
+
+			beforeEach( () => {
+				return VirtualTestEditor
+					.create( {
+						plugins: [ Paragraph, ListStyleEditing, Typing ]
+					} )
+					.then( newEditor => {
+						editor = newEditor;
+						model = editor.model;
+					} );
+			} );
+
+			afterEach( () => {
+				return editor.destroy();
+			} );
+
+			it( 'should not do anything while removing a letter inside a listItem', () => {
+				setModelData( model,
+					'<listItem listIndent="0" listStyle="square" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="square" listType="bulleted">2.[]</listItem>' +
+					'<paragraph></paragraph>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>'
+				);
+
+				editor.execute( 'delete' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="square" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="square" listType="bulleted">2[]</listItem>' +
+					'<paragraph></paragraph>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>'
+				);
+			} );
+
+			it( 'should not do anything if there is a non-listItem before the removed content', () => {
+				setModelData( model,
+					'<paragraph>Foo</paragraph>' +
+					'<paragraph>[]</paragraph>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>'
+				);
+
+				editor.execute( 'delete' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>Foo[]</paragraph>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>'
+				);
+			} );
+
+			it( 'should not do anything if there is a non-listItem after the removed content', () => {
+				setModelData( model,
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>' +
+					'<paragraph>[]</paragraph>' +
+					'<paragraph>Foo</paragraph>'
+				);
+
+				editor.execute( 'delete' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.[]</listItem>' +
+					'<paragraph>Foo</paragraph>'
+				);
+			} );
+
+			it( 'should not do anything if there is no element after the removed content', () => {
+				setModelData( model,
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>' +
+					'<paragraph>[]</paragraph>'
+				);
+
+				editor.execute( 'delete' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.[]</listItem>'
+				);
+			} );
+
+			it(
+				'should modify the the `listStyle` attribute for the merged (second) list when removing content between those lists',
+				() => {
+					setModelData( model,
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">1.</listItem>' +
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">2.</listItem>' +
+						'<paragraph>[]</paragraph>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>'
+					);
+
+					editor.execute( 'delete' );
+
+					expect( getModelData( model ) ).to.equal(
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">1.</listItem>' +
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">2.[]</listItem>' +
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">1.</listItem>' +
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">2.</listItem>'
+					);
+				}
+			);
+
+			it( 'should read the `listStyle` attribute from the most outer list', () => {
+				setModelData( model,
+					'<listItem listIndent="0" listStyle="square" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="square" listType="bulleted">2.</listItem>' +
+					'<listItem listIndent="1" listStyle="numbered" listType="decimal">2.1.</listItem>' +
+					'<listItem listIndent="2" listStyle="default" listType="default">2.1.1</listItem>' +
+					'<paragraph>[]</paragraph>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>'
+				);
+
+				editor.execute( 'delete' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="square" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="square" listType="bulleted">2.</listItem>' +
+					'<listItem listIndent="1" listStyle="numbered" listType="decimal">2.1.</listItem>' +
+					'<listItem listIndent="2" listStyle="default" listType="default">2.1.1[]</listItem>' +
+					'<listItem listIndent="0" listStyle="square" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="square" listType="bulleted">2.</listItem>'
+				);
+			} );
+
+			it(
+				'should not modify the the `listStyle` attribute for the merged (second) list if merging different `listType` attribute',
+				() => {
+					setModelData( model,
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">1.</listItem>' +
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">2.</listItem>' +
+						'<paragraph>[]</paragraph>' +
+						'<listItem listIndent="0" listStyle="decimal" listType="numbered">1.</listItem>' +
+						'<listItem listIndent="0" listStyle="decimal" listType="numbered">2.</listItem>'
+					);
+
+					editor.execute( 'delete' );
+
+					expect( getModelData( model ) ).to.equal(
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">1.</listItem>' +
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">2.[]</listItem>' +
+						'<listItem listIndent="0" listStyle="decimal" listType="numbered">1.</listItem>' +
+						'<listItem listIndent="0" listStyle="decimal" listType="numbered">2.</listItem>'
+					);
+				}
+			);
+
+			it(
+				'should modify the the `listStyle` attribute for the merged (second) list when removing content from both lists',
+				() => {
+					setModelData( model,
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">1.</listItem>' +
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">2.</listItem>' +
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">[3.</listItem>' +
+						'<paragraph>Foo</paragraph>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.]</listItem>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>'
+					);
+
+					editor.execute( 'delete' );
+
+					expect( getModelData( model ) ).to.equal(
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">1.</listItem>' +
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">2.</listItem>' +
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">[]</listItem>' +
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">2.</listItem>'
+					);
+				}
+			);
+
+			it(
+				'should modify the the `listStyle` attribute for the merged (second) list when typing over content from both lists',
+				() => {
+					setModelData( model,
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">1.</listItem>' +
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">2.</listItem>' +
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">[3.</listItem>' +
+						'<paragraph>Foo</paragraph>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.]</listItem>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>'
+					);
+
+					editor.execute( 'input', { text: 'Foo' } );
+
+					expect( getModelData( model ) ).to.equal(
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">1.</listItem>' +
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">2.</listItem>' +
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">Foo[]</listItem>' +
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">2.</listItem>'
+					);
+				}
+			);
+
+			it(
+				'should not modify the the `listStyle` if lists were not merged but the content was partially removed',
+				() => {
+					setModelData( model,
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">111.</listItem>' +
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">222.</listItem>' +
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">[333.</listItem>' +
+						'<paragraph>Foo</paragraph>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">1]11.</listItem>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>'
+					);
+
+					editor.execute( 'delete' );
+
+					expect( getModelData( model ) ).to.equal(
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">111.</listItem>' +
+						'<listItem listIndent="0" listStyle="square" listType="bulleted">222.</listItem>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">[]11.</listItem>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>'
+					);
+				}
+			);
+
+			it( 'should not do anything while typing in a list item', () => {
+				setModelData( model,
+					'<listItem listIndent="0" listStyle="square" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="square" listType="bulleted">2.[]</listItem>' +
+					'<listItem listIndent="0" listStyle="square" listType="bulleted">3.</listItem>' +
+					'<paragraph></paragraph>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>'
+				);
+
+				const modelChangeStub = sinon.stub( model, 'change' ).callThrough();
+
+				simulateTyping( ' Foo' );
+
+				// Each character calls `editor.model.change()`.
+				expect( modelChangeStub.callCount ).to.equal( 4 );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="square" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="square" listType="bulleted">2. Foo[]</listItem>' +
+					'<listItem listIndent="0" listStyle="square" listType="bulleted">3.</listItem>' +
+					'<paragraph></paragraph>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>'
+				);
+			} );
+
+			function simulateTyping( text ) {
+				// While typing, every character is an atomic change.
+				text.split( '' ).forEach( character => {
+					editor.execute( 'input', {
+						text: character
+					} );
+				} );
+			}
+		} );
 	} );
 } );