diff --git a/packages/ckeditor5-list/src/listpropertiesui.js b/packages/ckeditor5-list/src/listpropertiesui.js index 085afb0baf8..0decea62bcf 100644 --- a/packages/ckeditor5-list/src/listpropertiesui.js +++ b/packages/ckeditor5-list/src/listpropertiesui.js @@ -228,10 +228,9 @@ function getStyleButtonCreator( { editor, listStyleCommand, parentCommandName } editor.execute( 'listStyle', { type: listStyleCommand._defaultType } ); } } - // If the content the selection is anchored to is not a list, let's create a list of a desired style. + // Otherwise, leave the creation of the styled list to the `ListStyleCommand`. else { editor.model.change( () => { - editor.execute( parentCommandName ); editor.execute( 'listStyle', { type } ); } ); } diff --git a/packages/ckeditor5-list/src/liststylecommand.js b/packages/ckeditor5-list/src/liststylecommand.js index a5b43a4b6c0..483fd212ffc 100644 --- a/packages/ckeditor5-list/src/liststylecommand.js +++ b/packages/ckeditor5-list/src/liststylecommand.js @@ -8,10 +8,13 @@ */ import { Command } from 'ckeditor5/src/core'; -import { getSelectedListItems } from './utils'; +import { getListTypeFromListStyleType, getSelectedListItems } from './utils'; /** * The list style command. It changes the `listStyle` attribute of the selected list items. + * + * If the list type (numbered or bulleted) can be inferred from the passed style type, + * the command tries to convert selected items to a list of that type. * It is used by the {@link module:list/listproperties~ListProperties list properties feature}. * * @extends module:core/command~Command @@ -48,11 +51,13 @@ export default class ListStyleCommand extends Command { * Executes the command. * * @param {Object} options - * @param {String|null} options.type The type of the list style, e.g. `'disc'` or `'square'`. If `null` is specified, the default + * @param {String|null} [options.type] The type of the list style, e.g. `'disc'` or `'square'`. If `null` is specified, the default * style will be applied. * @protected */ execute( options = {} ) { + this._tryToConvertItemsToList( options ); + const model = this.editor.model; const listItems = getSelectedListItems( model ); @@ -97,4 +102,31 @@ export default class ListStyleCommand extends Command { return numberedList.isEnabled || bulletedList.isEnabled; } + + /** + * Check if the provided list style is valid. Also change the selection to a list if it's not set yet. + * + * @param {Object} options + * @param {String|null} [options.type] The type of the list style. If `null` is specified, the function does nothing. + * @private + */ + _tryToConvertItemsToList( options ) { + if ( !options.type ) { + return; + } + + const listType = getListTypeFromListStyleType( options.type ); + + if ( !listType ) { + return; + } + + const editor = this.editor; + const commandName = listType + 'List'; + const command = editor.commands.get( commandName ); + + if ( !command.value ) { + editor.execute( commandName ); + } + } } diff --git a/packages/ckeditor5-list/src/utils.js b/packages/ckeditor5-list/src/utils.js index 9cce1e8a220..48481e71983 100644 --- a/packages/ckeditor5-list/src/utils.js +++ b/packages/ckeditor5-list/src/utils.js @@ -410,6 +410,37 @@ export function getSelectedListItems( model ) { return listItems; } +const BULLETED_LIST_STYLE_TYPES = [ 'disc', 'circle', 'square' ]; + +// There's a lot of them (https://www.w3.org/TR/css-counter-styles-3/#typedef-counter-style). +// Let's support only those that can be selected by ListPropertiesUI. +const NUMBERED_LIST_STYLE_TYPES = [ + 'decimal', + 'decimal-leading-zero', + 'lower-roman', + 'upper-roman', + 'lower-latin', + 'upper-latin' +]; + +/** + * Checks whether the given list-style-type is supported by numbered or bulleted list. + * + * @param {String} listStyleType + * @returns {'bulleted'|'numbered'|null} + */ +export function getListTypeFromListStyleType( listStyleType ) { + if ( BULLETED_LIST_STYLE_TYPES.includes( listStyleType ) ) { + return 'bulleted'; + } + + if ( NUMBERED_LIST_STYLE_TYPES.includes( listStyleType ) ) { + return 'numbered'; + } + + return null; +} + // Implementation of getFillerOffset for view list item element. // // @returns {Number|null} Block filler offset or `null` if block filler is not needed. diff --git a/packages/ckeditor5-list/tests/liststylecommand.js b/packages/ckeditor5-list/tests/liststylecommand.js index 3947bbf0b62..6cd4157a230 100644 --- a/packages/ckeditor5-list/tests/liststylecommand.js +++ b/packages/ckeditor5-list/tests/liststylecommand.js @@ -253,7 +253,7 @@ describe( 'ListStyleCommand', () => { ); } ); - it( 'should start searching for the list items from starting position (collapsed selection)', () => { + it( 'should start searching for the list items from starting position (non-collapsed selection)', () => { setData( model, '1.' + '2.' + @@ -271,7 +271,7 @@ describe( 'ListStyleCommand', () => { ); } ); - it( 'should start searching for the list items from ending position (collapsed selection)', () => { + it( 'should start searching for the list items from ending position (non-collapsed selection)', () => { setData( model, '[Foo.' + '1.]' + @@ -282,7 +282,7 @@ describe( 'ListStyleCommand', () => { listStyleCommand.execute( { type: 'circle' } ); expect( getData( model ) ).to.equal( - '[Foo.' + + '[Foo.' + '1.]' + '2.' + '3.' @@ -325,24 +325,108 @@ describe( 'ListStyleCommand', () => { ); } ); - it( 'should not update anything if no listItem found in the selection', () => { + it( 'should not update anything if no listItem found in the selection (default style)', () => { setData( model, '[Foo.]' + - '1.' + '1.' ); const modelChangeStub = sinon.stub( model, 'change' ).named( 'model#change' ); - listStyleCommand.execute( { type: 'circle' } ); + listStyleCommand.execute(); expect( getData( model ) ).to.equal( '[Foo.]' + - '1.' + '1.' ); expect( modelChangeStub.called ).to.equal( false ); } ); + it( 'should create a list list if no listItem found in the selection (circle, non-collapsed selection)', () => { + setData( model, + '[Foo.' + + 'Bar.]' + ); + + const listCommand = editor.commands.get( 'bulletedList' ); + const spy = sinon.spy( listCommand, 'execute' ); + + listStyleCommand.execute( { type: 'circle' } ); + + expect( getData( model ) ).to.equal( + '[Foo.' + + 'Bar.]' + ); + + expect( spy.called ).to.be.true; + + spy.restore(); + } ); + + it( 'should create a list list if no listItem found in the selection (square, collapsed selection)', () => { + setData( model, + 'Fo[]o.' + + 'Bar.' + ); + + const listCommand = editor.commands.get( 'bulletedList' ); + const spy = sinon.spy( listCommand, 'execute' ); + + listStyleCommand.execute( { type: 'circle' } ); + + expect( getData( model ) ).to.equal( + 'Fo[]o.' + + 'Bar.' + ); + + expect( spy.called ).to.be.true; + + spy.restore(); + } ); + + it( 'should create a list list if no listItem found in the selection (decimal, non-collapsed selection)', () => { + setData( model, + '[Foo.' + + 'Bar.]' + ); + + const listCommand = editor.commands.get( 'numberedList' ); + const spy = sinon.spy( listCommand, 'execute' ); + + listStyleCommand.execute( { type: 'decimal' } ); + + expect( getData( model ) ).to.equal( + '[Foo.' + + 'Bar.]' + ); + + expect( spy.called ).to.be.true; + + spy.restore(); + } ); + + it( 'should create a list list if no listItem found in the selection (upper-roman, collapsed selection)', () => { + setData( model, + 'Fo[]o.' + + 'Bar.' + ); + + const listCommand = editor.commands.get( 'numberedList' ); + const spy = sinon.spy( listCommand, 'execute' ); + + listStyleCommand.execute( { type: 'upper-roman' } ); + + expect( getData( model ) ).to.equal( + 'Fo[]o.' + + 'Bar.' + ); + + expect( spy.called ).to.be.true; + + spy.restore(); + } ); + it( 'should update all items that belong to selected elements', () => { // [x] = items that should be updated. // All list items that belong to the same lists that selected items should be updated. diff --git a/packages/ckeditor5-list/tests/utils.js b/packages/ckeditor5-list/tests/utils.js index 6f7157b6a1f..e28be9124c1 100644 --- a/packages/ckeditor5-list/tests/utils.js +++ b/packages/ckeditor5-list/tests/utils.js @@ -11,7 +11,7 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtest import ListEditing from '../src/listediting'; import ListPropertiesEditing from '../src/listpropertiesediting'; -import { createViewListItemElement, getSiblingListItem, getSiblingNodes } from '../src/utils'; +import { createViewListItemElement, getListTypeFromListStyleType, getSiblingListItem, getSiblingNodes } from '../src/utils'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; describe( 'utils', () => { @@ -449,4 +449,26 @@ describe( 'utils', () => { ] ); } ); } ); + + describe( 'getListTypeFromListStyleType()', () => { + const testData = [ + [ 'decimal', 'numbered' ], + [ 'decimal-leading-zero', 'numbered' ], + [ 'lower-roman', 'numbered' ], + [ 'upper-roman', 'numbered' ], + [ 'lower-latin', 'numbered' ], + [ 'upper-latin', 'numbered' ], + [ 'disc', 'bulleted' ], + [ 'circle', 'bulleted' ], + [ 'square', 'bulleted' ], + [ 'default', null ], + [ 'style-type-that-is-not-possibly-supported-by-css', null ] + ]; + + for ( const [ style, type ] of testData ) { + it( `shoud return "${ type }" for "${ style }" style`, () => { + expect( getListTypeFromListStyleType( style ) ).to.equal( type ); + } ); + } + } ); } );