diff --git a/packages/ckeditor5-list/lang/contexts.json b/packages/ckeditor5-list/lang/contexts.json
index 5ee754657e0..33cd5248b91 100644
--- a/packages/ckeditor5-list/lang/contexts.json
+++ b/packages/ckeditor5-list/lang/contexts.json
@@ -1,5 +1,25 @@
{
"Numbered List": "Toolbar button tooltip for the Numbered List feature.",
"Bulleted List": "Toolbar button tooltip for the Bulleted List feature.",
- "To-do List": "Toolbar button tooltip for the To-do List feature."
+ "To-do List": "Toolbar button tooltip for the To-do List feature.",
+ "Bulleted list styles toolbar": "The ARIA label of the toolbar displaying buttons allowing users to change the bulleted list style.",
+ "Numbered list styles toolbar": "The ARIA label of the toolbar displaying buttons allowing users to change the numbered list style.",
+ "Toggle the disc list style": "The ARIA label of the button that toggles the \"disc\" list style.",
+ "Toggle the circle list style": "The ARIA label of the button that toggles the \"circle\" list style.",
+ "Toggle the square list style": "The ARIA label of the button that toggles the \"square\" list style.",
+ "Toggle the decimal list style": "The ARIA label of the button that toggles the \"decimal\" list style.",
+ "Toggle the decimal with leading zero list style": "The ARIA label of the button that toggles the \"decimal with leading zero\" list style.",
+ "Toggle the lower–roman list style": "The ARIA label of the button that toggles the \"lower–roman\" list style.",
+ "Toggle the upper–roman list style": "The ARIA label of the button that toggles the \"upper–roman\" list style.",
+ "Toggle the lower–latin list style": "The ARIA label of the button that toggles the \"lower–latin\" list style.",
+ "Toggle the upper–latin list style": "The ARIA label of the button that toggles the \"upper–latin\" list style.",
+ "Disc": "The tooltip text of the button that toggles the \"disc\" list style.",
+ "Circle": "The tooltip text of the button that toggles the \"circle\" list style.",
+ "Square": "The tooltip text of the button that toggles the \"square\" list style.",
+ "Decimal": "The tooltip text of the button that toggles the \"decimal\" list style.",
+ "Decimal with leading zero": "The tooltip text of the button that toggles the \"decimal with leading zero\" list style.",
+ "Lower–roman": "The tooltip text of the button that toggles the \"lower–roman\" list style.",
+ "Upper-roman": "The tooltip text of the button that toggles the \"upper–roman\" list style.",
+ "Lower-latin": "The tooltip text of the button that toggles the \"lower–latin\" list style.",
+ "Upper-latin": "The tooltip text of the button that toggles the \"upper–latin\" list style."
}
diff --git a/packages/ckeditor5-list/package.json b/packages/ckeditor5-list/package.json
index ab7c31c2ed8..9ddf0879424 100644
--- a/packages/ckeditor5-list/package.json
+++ b/packages/ckeditor5-list/package.json
@@ -20,7 +20,7 @@
"@ckeditor/ckeditor5-basic-styles": "^21.0.0",
"@ckeditor/ckeditor5-block-quote": "^21.0.0",
"@ckeditor/ckeditor5-clipboard": "^21.0.0",
- "@ckeditor/ckeditor5-essential": "^21.0.0",
+ "@ckeditor/ckeditor5-essentials": "^21.0.0",
"@ckeditor/ckeditor5-editor-classic": "^21.0.0",
"@ckeditor/ckeditor5-enter": "^21.0.0",
"@ckeditor/ckeditor5-font": "^21.0.0",
diff --git a/packages/ckeditor5-list/src/liststylesui.js b/packages/ckeditor5-list/src/liststylesui.js
index dbd4919f594..1d01d596114 100644
--- a/packages/ckeditor5-list/src/liststylesui.js
+++ b/packages/ckeditor5-list/src/liststylesui.js
@@ -8,8 +8,35 @@
*/
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import SplitButtonView from '@ckeditor/ckeditor5-ui/src/dropdown/button/splitbuttonview';
+import {
+ createDropdown,
+ addToolbarToDropdown
+} from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
+
+import bulletedListIcon from '../theme/icons/bulletedlist.svg';
+import numberedListIcon from '../theme/icons/numberedlist.svg';
+
+import listStyleDiscIcon from '../theme/icons/liststyledisc.svg';
+import listStyleCircleIcon from '../theme/icons/liststylecircle.svg';
+import listStyleSquareIcon from '../theme/icons/liststylesquare.svg';
+import listStyleDecimalIcon from '../theme/icons/liststyledecimal.svg';
+import listStyleDecimalWithLeadingZeroIcon from '../theme/icons/liststyledecimalleadingzero.svg';
+import listStyleLowerRomanIcon from '../theme/icons/liststylelowerroman.svg';
+import listStyleUpperRomanIcon from '../theme/icons/liststyleupperroman.svg';
+import listStyleLowerLatinIcon from '../theme/icons/liststylelowerlatin.svg';
+import listStyleUpperLatinIcon from '../theme/icons/liststyleupperlatin.svg';
+
+import '../theme/liststyles.css';
/**
+ * The list styles UI plugin. It introduces the extended `'bulletedList'` and `'numberedList'` toolbar
+ * buttons that allow users change styles of individual lists in the content.
+ *
+ * **Note**: Buttons introduces by this plugin override implementations from the {@link module:list/listui~ListUI}
+ * (because they share the same names).
+ *
* @extends module:core/plugin~Plugin
*/
export default class ListStylesUI extends Plugin {
@@ -21,6 +48,181 @@ export default class ListStylesUI extends Plugin {
}
init() {
- return null;
+ const editor = this.editor;
+ const t = editor.locale.t;
+
+ editor.ui.componentFactory.add( 'bulletedList', getSplitButtonCreator( {
+ editor,
+ parentCommandName: 'bulletedList',
+ buttonLabel: t( 'Bulleted List' ),
+ buttonIcon: bulletedListIcon,
+ toolbarAriaLabel: t( 'Bulleted list styles toolbar' ),
+ styleDefinitions: [
+ {
+ label: t( 'Toggle the disc list style' ),
+ tooltip: t( 'Disc' ),
+ type: 'disc',
+ icon: listStyleDiscIcon
+ },
+ {
+ label: t( 'Toggle the circle list style' ),
+ tooltip: t( 'Circle' ),
+ type: 'circle',
+ icon: listStyleCircleIcon
+ },
+ {
+ label: t( 'Toggle the square list style' ),
+ tooltip: t( 'Square' ),
+ type: 'square',
+ icon: listStyleSquareIcon
+ }
+ ]
+ } ) );
+
+ editor.ui.componentFactory.add( 'numberedList', getSplitButtonCreator( {
+ editor,
+ parentCommandName: 'numberedList',
+ buttonLabel: t( 'Numbered List' ),
+ buttonIcon: numberedListIcon,
+ toolbarAriaLabel: t( 'Numbered list styles toolbar' ),
+ styleDefinitions: [
+ {
+ label: t( 'Toggle the decimal list style' ),
+ tooltip: t( 'Decimal' ),
+ type: 'decimal',
+ icon: listStyleDecimalIcon
+ },
+ {
+ label: t( 'Toggle the decimal with leading zero list style' ),
+ tooltip: t( 'Decimal with leading zero' ),
+ type: 'decimal-leading-zero',
+ icon: listStyleDecimalWithLeadingZeroIcon
+ },
+ {
+ label: t( 'Toggle the lower–roman list style' ),
+ tooltip: t( 'Lower–roman' ),
+ type: 'lower-roman',
+ icon: listStyleLowerRomanIcon
+ },
+ {
+ label: t( 'Toggle the upper–roman list style' ),
+ tooltip: t( 'Upper-roman' ),
+ type: 'upper-roman',
+ icon: listStyleUpperRomanIcon
+ },
+ {
+ label: t( 'Toggle the lower–latin list style' ),
+ tooltip: t( 'Lower-latin' ),
+ type: 'lower-latin',
+ icon: listStyleLowerLatinIcon
+ },
+ {
+ label: t( 'Toggle the upper–latin list style' ),
+ tooltip: t( 'Upper-latin' ),
+ type: 'upper-latin',
+ icon: listStyleUpperLatinIcon
+ }
+ ]
+ } ) );
}
}
+
+// A helper that returns a function that creates a split button with a toolbar in the dropdown,
+// which in turn contains buttons allowing users to change list styles in the context of the current selection.
+//
+// @param {Object} options
+// @param {module:core/editor/editor~Editor} options.editor
+// @param {'bulletedList'|'numberedList'} options.parentCommandName The name of the higher-order editor command associated with
+// the set of particular list styles (e.g. "bulletedList" for "disc", "circle", and "square" styles).
+// @param {String} options.buttonLabel Label of the main part of the split button.
+// @param {String} options.buttonIcon The SVG string of an icon for the main part of the split button.
+// @param {String} options.toolbarAriaLabel The ARIA label for the toolbar in the split button dropdown.
+// @param {Object} options.styleDefinitions Definitions of the style buttons.
+// @returns {Function} A function that can be passed straight into {@link module:ui/componentfactory~ComponentFactory#add}.
+function getSplitButtonCreator( { editor, parentCommandName, buttonLabel, buttonIcon, toolbarAriaLabel, styleDefinitions } ) {
+ const parentCommand = editor.commands.get( parentCommandName );
+ const listStylesCommand = editor.commands.get( 'listStyles' );
+
+ // @param {module:utils/locale~Locale} locale
+ // @returns {module:ui/dropdown/dropdownview~DropdownView}
+ return locale => {
+ const dropdownView = createDropdown( locale, SplitButtonView );
+ const splitButtonView = dropdownView.buttonView;
+ const styleButtonCreator = getStyleButtonCreator( { editor, parentCommandName, listStylesCommand } );
+
+ addToolbarToDropdown( dropdownView, styleDefinitions.map( styleButtonCreator ) );
+
+ dropdownView.bind( 'isEnabled' ).to( parentCommand );
+ dropdownView.toolbarView.ariaLabel = toolbarAriaLabel;
+ dropdownView.class = 'ck-list-styles-dropdown';
+
+ splitButtonView.on( 'execute', () => {
+ editor.execute( parentCommandName );
+ editor.editing.view.focus();
+ } );
+
+ splitButtonView.set( {
+ label: buttonLabel,
+ icon: buttonIcon,
+ tooltip: true,
+ isToggleable: true
+ } );
+
+ splitButtonView.bind( 'isOn' ).to( parentCommand, 'value', value => !!value );
+
+ return dropdownView;
+ };
+}
+
+// A helper that returns a function (factory) that creates individual buttons used by users to change styles
+// of lists.
+//
+// @param {Object} options
+// @param {module:core/editor/editor~Editor} options.editor
+// @param {module:list/liststylescommand~ListStylesCommand} options.listStylesCommand The instance of the `ListStylesCommand` class.
+// @param {'bulletedList'|'numberedList'} options.parentCommandName The name of the higher-order command associated with a
+// particular list style (e.g. "bulletedList" is associated with "square" and "numberedList" is associated with "roman").
+// @returns {Function} A function that can be passed straight into {@link module:ui/componentfactory~ComponentFactory#add}.
+function getStyleButtonCreator( { editor, listStylesCommand, parentCommandName } ) {
+ const locale = editor.locale;
+ const parentCommand = editor.commands.get( parentCommandName );
+
+ // @param {String} label The label of the style button.
+ // @param {String} type The type of the style button (e.g. "roman" or "circle").
+ // @param {String} icon The SVG string of an icon of the style button.
+ // @param {String} tooltip The tooltip text of the button (shorter than verbose label).
+ // @returns {module:ui/button/buttonview~ButtonView}
+ return ( { label, type, icon, tooltip } ) => {
+ const button = new ButtonView( locale );
+
+ button.set( { label, icon, tooltip } );
+
+ listStylesCommand.on( 'change:value', () => {
+ button.isOn = listStylesCommand.value === type;
+ } );
+
+ button.on( 'execute', () => {
+ // If the content the selection is anchored to is a list, let's change its style.
+ if ( parentCommand.value ) {
+ // If the current list style is not set in the model or the style is different than the
+ // one to be applied, simply apply the new style.
+ if ( listStylesCommand.value !== type ) {
+ editor.execute( 'listStyles', { type } );
+ }
+ // If the style was the same, remove it (the button works as an off toggle).
+ else {
+ editor.execute( 'listStyles', { type: 'default' } );
+ }
+ }
+ // If the content the selection is anchored to is not a list, let's create a list of a desired style.
+ else {
+ editor.execute( parentCommandName );
+ editor.execute( 'listStyles', { type } );
+ }
+
+ editor.editing.view.focus();
+ } );
+
+ return button;
+ };
+}
diff --git a/packages/ckeditor5-list/src/utils.js b/packages/ckeditor5-list/src/utils.js
index e14d0b9eb2e..6b75110c2c4 100644
--- a/packages/ckeditor5-list/src/utils.js
+++ b/packages/ckeditor5-list/src/utils.js
@@ -207,7 +207,6 @@ export function positionAfterUiElements( viewPosition ) {
* @param {Boolean} [options.sameIndent=false] Whether the sought sibling should have the same indentation.
* @param {Boolean} [options.smallerIndent=false] Whether the sought sibling should have a smaller indentation.
* @param {Number} [options.listIndent] The reference indentation.
- * @param {'forward'|'backward'} [options.direction='backward'] Walking direction.
* @returns {module:engine/model/item~Item|null}
*/
export function getSiblingListItem( modelItem, options ) {
@@ -224,11 +223,7 @@ export function getSiblingListItem( modelItem, options ) {
return item;
}
- if ( options.direction === 'forward' ) {
- item = item.nextSibling;
- } else {
- item = item.previousSibling;
- }
+ item = item.previousSibling;
}
return null;
diff --git a/packages/ckeditor5-list/tests/liststylesui.js b/packages/ckeditor5-list/tests/liststylesui.js
index d54b7cef704..a7afacaaea4 100644
--- a/packages/ckeditor5-list/tests/liststylesui.js
+++ b/packages/ckeditor5-list/tests/liststylesui.js
@@ -3,18 +3,443 @@
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
+/* globals document */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import ListStyles from '../src/liststyles';
import ListStylesUI from '../src/liststylesui';
+import DropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/dropdownview';
+
+import bulletedListIcon from '../theme/icons/bulletedlist.svg';
+import numberedListIcon from '../theme/icons/numberedlist.svg';
+
+import listStyleDiscIcon from '../theme/icons/liststyledisc.svg';
+import listStyleCircleIcon from '../theme/icons/liststylecircle.svg';
+import listStyleSquareIcon from '../theme/icons/liststylesquare.svg';
+import listStyleDecimalIcon from '../theme/icons/liststyledecimal.svg';
+import listStyleDecimalWithLeadingZeroIcon from '../theme/icons/liststyledecimalleadingzero.svg';
+import listStyleLowerRomanIcon from '../theme/icons/liststylelowerroman.svg';
+import listStyleUpperRomanIcon from '../theme/icons/liststyleupperroman.svg';
+import listStyleLowerLatinIcon from '../theme/icons/liststylelowerlatin.svg';
+import listStyleUpperLatinIcon from '../theme/icons/liststyleupperlatin.svg';
describe( 'ListStylesUI', () => {
+ let editorElement, editor, model, listStylesCommand;
+
+ beforeEach( () => {
+ editorElement = document.createElement( 'div' );
+ document.body.appendChild( editorElement );
+
+ return ClassicTestEditor.create( editorElement, { plugins: [ Paragraph, BlockQuote, ListStyles ] } )
+ .then( newEditor => {
+ editor = newEditor;
+ model = editor.model;
+
+ listStylesCommand = editor.commands.get( 'listStyles' );
+ } );
+ } );
+
+ afterEach( () => {
+ editorElement.remove();
+
+ return editor.destroy();
+ } );
+
it( 'should be named', () => {
expect( ListStylesUI.pluginName ).to.equal( 'ListStylesUI' );
} );
+ it( 'should be loaded', () => {
+ expect( editor.plugins.get( ListStylesUI ) ).to.be.instanceOf( ListStylesUI );
+ } );
+
describe( 'init()', () => {
- it( 'returns null', () => {
- const plugin = new ListStylesUI( {} );
+ describe( 'bulleted list dropdown', () => {
+ let bulletedListCommand, bulletedListDropdown;
+
+ beforeEach( () => {
+ bulletedListCommand = editor.commands.get( 'bulletedList' );
+ bulletedListDropdown = editor.ui.componentFactory.create( 'bulletedList' );
+ } );
+
+ it( 'should registered as "bulletedList" in the component factory', () => {
+ expect( bulletedListDropdown ).to.be.instanceOf( DropdownView );
+ } );
+
+ it( 'should have #isEnabled bound to the "bulletedList" command state', () => {
+ expect( bulletedListDropdown.isEnabled ).to.be.true;
+
+ bulletedListCommand.isEnabled = true;
+ expect( bulletedListDropdown.isEnabled ).to.be.true;
+
+ bulletedListCommand.isEnabled = false;
+ expect( bulletedListDropdown.isEnabled ).to.be.false;
+ } );
+
+ it( 'should have a specific CSS class', () => {
+ expect( bulletedListDropdown.class ).to.equal( 'ck-list-styles-dropdown' );
+ } );
+
+ describe( 'main split button', () => {
+ let mainButtonView;
+
+ beforeEach( () => {
+ mainButtonView = bulletedListDropdown.buttonView;
+ } );
+
+ it( 'should have a #label', () => {
+ expect( mainButtonView.label ).to.equal( 'Bulleted List' );
+ } );
+
+ it( 'should have an #icon', () => {
+ expect( mainButtonView.icon ).to.equal( bulletedListIcon );
+ } );
+
+ it( 'should have a #tooltip based on a label', () => {
+ expect( mainButtonView.tooltip ).to.be.true;
+ } );
+
+ it( 'should be toggleable', () => {
+ expect( mainButtonView.isToggleable ).to.be.true;
+ } );
+
+ it( 'should have the #isOn state bound to the value of the "bulletedList" command', () => {
+ expect( mainButtonView.isOn ).to.be.false;
+
+ bulletedListCommand.value = 'foo';
+ expect( mainButtonView.isOn ).to.be.true;
+
+ bulletedListCommand.value = null;
+ expect( mainButtonView.isOn ).to.be.false;
+ } );
+
+ it( 'should execute the "bulletedList" command and focus the editing view when clicked', () => {
+ sinon.spy( editor, 'execute' );
+ sinon.spy( editor.editing.view, 'focus' );
+
+ mainButtonView.fire( 'execute' );
+ sinon.assert.calledWithExactly( editor.execute, 'bulletedList' );
+ sinon.assert.calledOnce( editor.editing.view.focus );
+ sinon.assert.callOrder( editor.execute, editor.editing.view.focus );
+ } );
+ } );
+
+ describe( 'toolbar with style buttons', () => {
+ let toolbarView;
+
+ beforeEach( () => {
+ toolbarView = bulletedListDropdown.toolbarView;
+ } );
+
+ it( 'should be in the dropdown panel', () => {
+ expect( bulletedListDropdown.panelView.children.get( 0 ) ).to.equal( toolbarView );
+ } );
+
+ it( 'should have a proper ARIA label', () => {
+ expect( toolbarView.ariaLabel ).to.equal( 'Bulleted list styles toolbar' );
+ } );
+
+ it( 'should bring the "disc" list style button', () => {
+ const buttonView = toolbarView.items.get( 0 );
+
+ expect( buttonView.label ).to.equal( 'Toggle the disc list style' );
+ expect( buttonView.tooltip ).to.equal( 'Disc' );
+ expect( buttonView.icon ).to.equal( listStyleDiscIcon );
+ } );
+
+ it( 'should bring the "circle" list style button', () => {
+ const buttonView = toolbarView.items.get( 1 );
+
+ expect( buttonView.label ).to.equal( 'Toggle the circle list style' );
+ expect( buttonView.tooltip ).to.equal( 'Circle' );
+ expect( buttonView.icon ).to.equal( listStyleCircleIcon );
+ } );
+
+ it( 'should bring the "square" list style button', () => {
+ const buttonView = toolbarView.items.get( 2 );
+
+ expect( buttonView.label ).to.equal( 'Toggle the square list style' );
+ expect( buttonView.tooltip ).to.equal( 'Square' );
+ expect( buttonView.icon ).to.equal( listStyleSquareIcon );
+ } );
+
+ describe( 'style button', () => {
+ let styleButtonView;
+
+ beforeEach( () => {
+ // "circle"
+ styleButtonView = toolbarView.items.get( 1 );
+
+ sinon.spy( editor, 'execute' );
+ sinon.spy( editor.editing.view, 'focus' );
+ } );
+
+ it( 'should be instances of ButtonView', () => {
+ expect( styleButtonView ).to.be.instanceOf( ButtonView );
+ } );
+
+ it( 'should change its #isOn state when the value of the "listStylesCommand" command changes', () => {
+ expect( styleButtonView.isOn ).to.be.false;
+
+ listStylesCommand.value = 'foo';
+ expect( styleButtonView.isOn ).to.be.false;
+
+ listStylesCommand.value = 'circle';
+ expect( styleButtonView.isOn ).to.be.true;
+
+ listStylesCommand.value = null;
+ expect( styleButtonView.isOn ).to.be.false;
+ } );
+
+ it( 'should apply the new style if none was set', () => {
+ setData( model, '[]foo' );
+
+ styleButtonView.fire( 'execute' );
+
+ sinon.assert.calledWithExactly( editor.execute, 'listStyles', { type: 'circle' } );
+ sinon.assert.calledOnce( editor.editing.view.focus );
+ sinon.assert.callOrder( editor.execute, editor.editing.view.focus );
+ } );
+
+ it( 'should apply the new style if a different one was set', () => {
+ setData( model, '[]foo' );
+
+ styleButtonView.fire( 'execute' );
+
+ sinon.assert.calledWithExactly( editor.execute, 'listStyles', { type: 'circle' } );
+ sinon.assert.calledOnce( editor.editing.view.focus );
+ sinon.assert.callOrder( editor.execute, editor.editing.view.focus );
+ } );
+
+ it( 'should remove (toggle) the style if the same style was set', () => {
+ setData( model, '[]foo' );
+
+ styleButtonView.fire( 'execute' );
+
+ sinon.assert.calledWithExactly( editor.execute, 'listStyles', { type: 'default' } );
+ sinon.assert.calledOnce( editor.editing.view.focus );
+ sinon.assert.callOrder( editor.execute, editor.editing.view.focus );
+ } );
+
+ it( 'should execute the "bulletedList" command and apply the style if selection was not anchored in a list', () => {
+ setData( model, 'foo[]' );
+
+ styleButtonView.fire( 'execute' );
+
+ sinon.assert.calledWithExactly( editor.execute, 'listStyles', { type: 'circle' } );
+ sinon.assert.calledOnce( editor.editing.view.focus );
+ sinon.assert.callOrder( editor.execute, editor.editing.view.focus );
+ } );
+ } );
+ } );
+ } );
+
+ describe( 'numbered list dropdown', () => {
+ let numberedListCommand, numberedListDropdown;
+
+ beforeEach( () => {
+ numberedListCommand = editor.commands.get( 'numberedList' );
+ numberedListDropdown = editor.ui.componentFactory.create( 'numberedList' );
+ } );
+
+ it( 'should registered as "numberedList" in the component factory', () => {
+ expect( numberedListDropdown ).to.be.instanceOf( DropdownView );
+ } );
+
+ it( 'should have #isEnabled bound to the "numberedList" command state', () => {
+ expect( numberedListDropdown.isEnabled ).to.be.true;
+
+ numberedListCommand.isEnabled = true;
+ expect( numberedListDropdown.isEnabled ).to.be.true;
+
+ numberedListCommand.isEnabled = false;
+ expect( numberedListDropdown.isEnabled ).to.be.false;
+ } );
+
+ it( 'should have a specific CSS class', () => {
+ expect( numberedListDropdown.class ).to.equal( 'ck-list-styles-dropdown' );
+ } );
+
+ describe( 'main split button', () => {
+ let mainButtonView;
+
+ beforeEach( () => {
+ mainButtonView = numberedListDropdown.buttonView;
+ } );
+
+ it( 'should have a #label', () => {
+ expect( mainButtonView.label ).to.equal( 'Numbered List' );
+ } );
+
+ it( 'should have an #icon', () => {
+ expect( mainButtonView.icon ).to.equal( numberedListIcon );
+ } );
+
+ it( 'should have a #tooltip based on a label', () => {
+ expect( mainButtonView.tooltip ).to.be.true;
+ } );
+
+ it( 'should be toggleable', () => {
+ expect( mainButtonView.isToggleable ).to.be.true;
+ } );
+
+ it( 'should have the #isOn state bound to the value of the "numberedList" command', () => {
+ expect( mainButtonView.isOn ).to.be.false;
+
+ numberedListCommand.value = 'foo';
+ expect( mainButtonView.isOn ).to.be.true;
+
+ numberedListCommand.value = null;
+ expect( mainButtonView.isOn ).to.be.false;
+ } );
+
+ it( 'should execute the "numberedList" command and focus the editing view when clicked', () => {
+ sinon.spy( editor, 'execute' );
+ sinon.spy( editor.editing.view, 'focus' );
+
+ mainButtonView.fire( 'execute' );
+ sinon.assert.calledWithExactly( editor.execute, 'numberedList' );
+ sinon.assert.calledOnce( editor.editing.view.focus );
+ sinon.assert.callOrder( editor.execute, editor.editing.view.focus );
+ } );
+ } );
+
+ describe( 'toolbar with style buttons', () => {
+ let toolbarView;
+
+ beforeEach( () => {
+ toolbarView = numberedListDropdown.toolbarView;
+ } );
+
+ it( 'should be in the dropdown panel', () => {
+ expect( numberedListDropdown.panelView.children.get( 0 ) ).to.equal( toolbarView );
+ } );
+
+ it( 'should have a proper ARIA label', () => {
+ expect( toolbarView.ariaLabel ).to.equal( 'Numbered list styles toolbar' );
+ } );
+
+ it( 'should bring the "decimal" list style button', () => {
+ const buttonView = toolbarView.items.get( 0 );
+
+ expect( buttonView.label ).to.equal( 'Toggle the decimal list style' );
+ expect( buttonView.tooltip ).to.equal( 'Decimal' );
+ expect( buttonView.icon ).to.equal( listStyleDecimalIcon );
+ } );
+
+ it( 'should bring the "decimal-leading-zero" list style button', () => {
+ const buttonView = toolbarView.items.get( 1 );
+
+ expect( buttonView.label ).to.equal( 'Toggle the decimal with leading zero list style' );
+ expect( buttonView.tooltip ).to.equal( 'Decimal with leading zero' );
+ expect( buttonView.icon ).to.equal( listStyleDecimalWithLeadingZeroIcon );
+ } );
+
+ it( 'should bring the "lower-roman" list style button', () => {
+ const buttonView = toolbarView.items.get( 2 );
+
+ expect( buttonView.label ).to.equal( 'Toggle the lower–roman list style' );
+ expect( buttonView.tooltip ).to.equal( 'Lower–roman' );
+ expect( buttonView.icon ).to.equal( listStyleLowerRomanIcon );
+ } );
+
+ it( 'should bring the "upper-roman" list style button', () => {
+ const buttonView = toolbarView.items.get( 3 );
+
+ expect( buttonView.label ).to.equal( 'Toggle the upper–roman list style' );
+ expect( buttonView.tooltip ).to.equal( 'Upper-roman' );
+ expect( buttonView.icon ).to.equal( listStyleUpperRomanIcon );
+ } );
+
+ it( 'should bring the "lower–latin" list style button', () => {
+ const buttonView = toolbarView.items.get( 4 );
+
+ expect( buttonView.label ).to.equal( 'Toggle the lower–latin list style' );
+ expect( buttonView.tooltip ).to.equal( 'Lower-latin' );
+ expect( buttonView.icon ).to.equal( listStyleLowerLatinIcon );
+ } );
+
+ it( 'should bring the "upper–latin" list style button', () => {
+ const buttonView = toolbarView.items.get( 5 );
+
+ expect( buttonView.label ).to.equal( 'Toggle the upper–latin list style' );
+ expect( buttonView.tooltip ).to.equal( 'Upper-latin' );
+ expect( buttonView.icon ).to.equal( listStyleUpperLatinIcon );
+ } );
+
+ describe( 'style button', () => {
+ let styleButtonView;
+
+ beforeEach( () => {
+ // "decimal-leading-zero""
+ styleButtonView = toolbarView.items.get( 1 );
+
+ sinon.spy( editor, 'execute' );
+ sinon.spy( editor.editing.view, 'focus' );
+ } );
+
+ it( 'should be instances of ButtonView', () => {
+ expect( styleButtonView ).to.be.instanceOf( ButtonView );
+ } );
+
+ it( 'should change its #isOn state when the value of the "listStylesCommand" command changes', () => {
+ expect( styleButtonView.isOn ).to.be.false;
+
+ listStylesCommand.value = 'foo';
+ expect( styleButtonView.isOn ).to.be.false;
+
+ listStylesCommand.value = 'decimal-leading-zero';
+ expect( styleButtonView.isOn ).to.be.true;
+
+ listStylesCommand.value = null;
+ expect( styleButtonView.isOn ).to.be.false;
+ } );
+
+ it( 'should apply the new style if none was set', () => {
+ setData( model, '[]foo' );
+
+ styleButtonView.fire( 'execute' );
+
+ sinon.assert.calledWithExactly( editor.execute, 'listStyles', { type: 'decimal-leading-zero' } );
+ sinon.assert.calledOnce( editor.editing.view.focus );
+ sinon.assert.callOrder( editor.execute, editor.editing.view.focus );
+ } );
+
+ it( 'should apply the new style if a different one was set', () => {
+ setData( model, '[]foo' );
+
+ styleButtonView.fire( 'execute' );
+
+ sinon.assert.calledWithExactly( editor.execute, 'listStyles', { type: 'decimal-leading-zero' } );
+ sinon.assert.calledOnce( editor.editing.view.focus );
+ sinon.assert.callOrder( editor.execute, editor.editing.view.focus );
+ } );
+
+ it( 'should remove (toggle) the style if the same style was set', () => {
+ setData( model, '[]foo' );
+
+ styleButtonView.fire( 'execute' );
+
+ sinon.assert.calledWithExactly( editor.execute, 'listStyles', { type: 'default' } );
+ sinon.assert.calledOnce( editor.editing.view.focus );
+ sinon.assert.callOrder( editor.execute, editor.editing.view.focus );
+ } );
+
+ it( 'should execute the "numberedList" command and apply the style if selection was not anchored in a list', () => {
+ setData( model, 'foo[]' );
+
+ styleButtonView.fire( 'execute' );
- expect( plugin.init() ).to.equal( null );
+ sinon.assert.calledWithExactly( editor.execute, 'listStyles', { type: 'decimal-leading-zero' } );
+ sinon.assert.calledOnce( editor.editing.view.focus );
+ sinon.assert.callOrder( editor.execute, editor.editing.view.focus );
+ } );
+ } );
+ } );
} );
} );
} );
diff --git a/packages/ckeditor5-list/theme/icons/liststylecircle.svg b/packages/ckeditor5-list/theme/icons/liststylecircle.svg
new file mode 100644
index 00000000000..13a23c93ceb
--- /dev/null
+++ b/packages/ckeditor5-list/theme/icons/liststylecircle.svg
@@ -0,0 +1 @@
+
diff --git a/packages/ckeditor5-list/theme/icons/liststyledecimal.svg b/packages/ckeditor5-list/theme/icons/liststyledecimal.svg
new file mode 100644
index 00000000000..cdaf9d557fe
--- /dev/null
+++ b/packages/ckeditor5-list/theme/icons/liststyledecimal.svg
@@ -0,0 +1 @@
+
diff --git a/packages/ckeditor5-list/theme/icons/liststyledecimalleadingzero.svg b/packages/ckeditor5-list/theme/icons/liststyledecimalleadingzero.svg
new file mode 100644
index 00000000000..63dac91fea0
--- /dev/null
+++ b/packages/ckeditor5-list/theme/icons/liststyledecimalleadingzero.svg
@@ -0,0 +1 @@
+
diff --git a/packages/ckeditor5-list/theme/icons/liststyledisc.svg b/packages/ckeditor5-list/theme/icons/liststyledisc.svg
new file mode 100644
index 00000000000..e4fc7e8ac6e
--- /dev/null
+++ b/packages/ckeditor5-list/theme/icons/liststyledisc.svg
@@ -0,0 +1 @@
+
diff --git a/packages/ckeditor5-list/theme/icons/liststylelowerlatin.svg b/packages/ckeditor5-list/theme/icons/liststylelowerlatin.svg
new file mode 100644
index 00000000000..756de456f2e
--- /dev/null
+++ b/packages/ckeditor5-list/theme/icons/liststylelowerlatin.svg
@@ -0,0 +1 @@
+
diff --git a/packages/ckeditor5-list/theme/icons/liststylelowerroman.svg b/packages/ckeditor5-list/theme/icons/liststylelowerroman.svg
new file mode 100644
index 00000000000..dcc6e10b1fe
--- /dev/null
+++ b/packages/ckeditor5-list/theme/icons/liststylelowerroman.svg
@@ -0,0 +1 @@
+
diff --git a/packages/ckeditor5-list/theme/icons/liststylesquare.svg b/packages/ckeditor5-list/theme/icons/liststylesquare.svg
new file mode 100644
index 00000000000..8c8b50ca579
--- /dev/null
+++ b/packages/ckeditor5-list/theme/icons/liststylesquare.svg
@@ -0,0 +1 @@
+
diff --git a/packages/ckeditor5-list/theme/icons/liststyleupperlatin.svg b/packages/ckeditor5-list/theme/icons/liststyleupperlatin.svg
new file mode 100644
index 00000000000..b963bf6b9fc
--- /dev/null
+++ b/packages/ckeditor5-list/theme/icons/liststyleupperlatin.svg
@@ -0,0 +1 @@
+
diff --git a/packages/ckeditor5-list/theme/icons/liststyleupperroman.svg b/packages/ckeditor5-list/theme/icons/liststyleupperroman.svg
new file mode 100644
index 00000000000..4c40f3e82d1
--- /dev/null
+++ b/packages/ckeditor5-list/theme/icons/liststyleupperroman.svg
@@ -0,0 +1 @@
+
diff --git a/packages/ckeditor5-list/theme/liststyles.css b/packages/ckeditor5-list/theme/liststyles.css
new file mode 100644
index 00000000000..119de648756
--- /dev/null
+++ b/packages/ckeditor5-list/theme/liststyles.css
@@ -0,0 +1,12 @@
+/*
+ * Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+.ck.ck-list-styles-dropdown > .ck-dropdown__panel > .ck-toolbar > .ck-toolbar__items {
+ /*
+ * Use the benefits of the toolbar (e.g. out-of-the-box keyboard navigation) but make it look
+ * like panel with thumbnails (previews).
+ */
+ display: grid;
+}
diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-list/liststyles.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-list/liststyles.css
new file mode 100644
index 00000000000..a9236105999
--- /dev/null
+++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-list/liststyles.css
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+:root {
+ --ck-list-style-button-size: 44px;
+}
+
+.ck.ck-list-styles-dropdown > .ck-dropdown__panel > .ck-toolbar {
+ background: none;
+ padding: 0;
+
+ & > .ck-toolbar__items {
+ grid-template-columns: repeat( 3, auto );
+ row-gap: var(--ck-spacing-medium);
+ column-gap: var(--ck-spacing-medium);
+ padding: var(--ck-spacing-medium);
+
+ & .ck-button {
+ /* Make the button look like a thumbnail (the icon "takes it all"). */
+ width: var(--ck-list-style-button-size);
+ height: var(--ck-list-style-button-size);
+ padding: 0;
+
+ /*
+ * Buttons are aligned by the grid so disable default button margins to not collide with the
+ * gaps in the grid.
+ */
+ margin: 0;
+
+ /*
+ * Make sure the button border (which is displayed on focus, BTW) does not steal pixels
+ * from the button dimensions and, as a result, decrease the size of the icon
+ * (which becomes blurry as it scales down).
+ */
+ box-sizing: content-box;
+
+ & .ck-icon {
+ width: var(--ck-list-style-button-size);
+ height: var(--ck-list-style-button-size);
+ }
+ }
+ }
+}
diff --git a/packages/ckeditor5-ui/src/componentfactory.js b/packages/ckeditor5-ui/src/componentfactory.js
index fbc7da2198f..738a098bceb 100644
--- a/packages/ckeditor5-ui/src/componentfactory.js
+++ b/packages/ckeditor5-ui/src/componentfactory.js
@@ -77,20 +77,6 @@ export default class ComponentFactory {
* @param {Function} callback The callback that returns the component.
*/
add( name, callback ) {
- if ( this.has( name ) ) {
- /**
- * The item already exists in the component factory.
- *
- * @error componentfactory-item-exists
- * @param {String} name The name of the component.
- */
- throw new CKEditorError(
- 'componentfactory-item-exists: The item already exists in the component factory.',
- this,
- { name }
- );
- }
-
this._components.set( getNormalized( name ), { callback, originalName: name } );
}
diff --git a/packages/ckeditor5-ui/tests/componentfactory.js b/packages/ckeditor5-ui/tests/componentfactory.js
index 423cc18440a..4ef8a4ac805 100644
--- a/packages/ckeditor5-ui/tests/componentfactory.js
+++ b/packages/ckeditor5-ui/tests/componentfactory.js
@@ -39,26 +39,24 @@ describe( 'ComponentFactory', () => {
} );
describe( 'add()', () => {
- it( 'throws when trying to override already registered component', () => {
- factory.add( 'foo', () => {} );
+ it( 'does not normalize component names', () => {
+ factory.add( 'FoO', () => {} );
- expectToThrowCKEditorError( () => {
- factory.add( 'foo', () => {} );
- }, /^componentfactory-item-exists/, editor );
+ expect( Array.from( factory.names() ) ).to.have.members( [ 'FoO' ] );
} );
- it( 'throws when trying to override already registered component added with different case', () => {
- factory.add( 'Foo', () => {} );
+ it( 'should allow overriding already registered components', () => {
+ factory.add( 'foo', () => 'old' );
+ factory.add( 'foo', () => 'new' );
- expectToThrowCKEditorError( () => {
- factory.add( 'foo', () => {} );
- }, /^componentfactory-item-exists/, editor );
+ expect( factory.create( 'foo' ) ).to.equal( 'new' );
} );
- it( 'does not normalize component names', () => {
- factory.add( 'FoO', () => {} );
+ it( 'should allow overriding already registered components (same name, different case)', () => {
+ factory.add( 'foo', () => 'old' );
+ factory.add( 'Foo', () => 'new' );
- expect( Array.from( factory.names() ) ).to.have.members( [ 'FoO' ] );
+ expect( factory.create( 'foo' ) ).to.equal( 'new' );
} );
} );