浏览代码

Align feature class naming to a new scheme.

Maciej Gołaszewski 7 年之前
父节点
当前提交
e445bc49b7

+ 6 - 116
packages/ckeditor5-list/src/list.js

@@ -7,19 +7,16 @@
  * @module list/list
  */
 
-import ListEngine from './listengine';
-
-import numberedListIcon from '../theme/icons/numberedlist.svg';
-import bulletedListIcon from '../theme/icons/bulletedlist.svg';
+import ListEditing from './listediting';
+import ListUI from './listui';
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 
 /**
- * The list feature. It introduces the `numberedList` and `bulletedList` buttons that
- * allow to convert paragraphs to and from list items and indent or outdent them.
+ * The list feature.
  *
- * See also {@link module:list/listengine~ListEngine}.
+ * It loads the {@link module:list/listediting~ListEditing list editing feature}
+ * and {@link module:list/listui~ListUI list UI feature}.
  *
  * @extends module:core/plugin~Plugin
  */
@@ -28,7 +25,7 @@ export default class List extends Plugin {
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ ListEngine ];
+		return [ ListEditing, ListUI ];
 	}
 
 	/**
@@ -37,111 +34,4 @@ export default class List extends Plugin {
 	static get pluginName() {
 		return 'List';
 	}
-
-	/**
-	 * @inheritDoc
-	 */
-	init() {
-		// Create two buttons and link them with numberedList and bulletedList commands.
-		const t = this.editor.t;
-		this._addButton( 'numberedList', t( 'Numbered List' ), numberedListIcon );
-		this._addButton( 'bulletedList', t( 'Bulleted List' ), bulletedListIcon );
-
-		// Overwrite default Enter key behavior.
-		// If Enter key is pressed with selection collapsed in empty list item, outdent it instead of breaking it.
-		this.listenTo( this.editor.editing.view, 'enter', ( evt, data ) => {
-			const doc = this.editor.model.document;
-			const positionParent = doc.selection.getLastPosition().parent;
-
-			if ( doc.selection.isCollapsed && positionParent.name == 'listItem' && positionParent.isEmpty ) {
-				this.editor.execute( 'outdentList' );
-
-				data.preventDefault();
-				evt.stop();
-			}
-		} );
-
-		// Overwrite default Backspace key behavior.
-		// If Backspace key is pressed with selection collapsed on first position in first list item, outdent it. #83
-		this.listenTo( this.editor.editing.view, 'delete', ( evt, data ) => {
-			// Check conditions from those that require less computations like those immediately available.
-			if ( data.direction !== 'backward' ) {
-				return;
-			}
-
-			const selection = this.editor.model.document.selection;
-
-			if ( !selection.isCollapsed ) {
-				return;
-			}
-
-			const firstPosition = selection.getFirstPosition();
-
-			if ( !firstPosition.isAtStart ) {
-				return;
-			}
-
-			const positionParent = firstPosition.parent;
-
-			if ( positionParent.name !== 'listItem' ) {
-				return;
-			}
-
-			const previousIsAListItem = positionParent.previousSibling && positionParent.previousSibling.name === 'listItem';
-
-			if ( previousIsAListItem ) {
-				return;
-			}
-
-			this.editor.execute( 'outdentList' );
-
-			data.preventDefault();
-			evt.stop();
-		}, { priority: 'high' } );
-
-		const getCommandExecuter = commandName => {
-			return ( data, cancel ) => {
-				const command = this.editor.commands.get( commandName );
-
-				if ( command.isEnabled ) {
-					this.editor.execute( commandName );
-					cancel();
-				}
-			};
-		};
-
-		this.editor.keystrokes.set( 'Tab', getCommandExecuter( 'indentList' ) );
-		this.editor.keystrokes.set( 'Shift+Tab', getCommandExecuter( 'outdentList' ) );
-	}
-
-	/**
-	 * Helper method for initializing a button and linking it with an appropriate command.
-	 *
-	 * @private
-	 * @param {String} commandName The name of the command.
-	 * @param {Object} label The button label.
-	 * @param {String} icon The source of the icon.
-	 */
-	_addButton( commandName, label, icon ) {
-		const editor = this.editor;
-		const command = editor.commands.get( commandName );
-
-		editor.ui.componentFactory.add( commandName, locale => {
-			const buttonView = new ButtonView( locale );
-
-			buttonView.set( {
-				label,
-				icon,
-				tooltip: true
-			} );
-
-			// Bind button model to command.
-			buttonView.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );
-
-			// Execute command.
-			this.listenTo( buttonView, 'execute', () => editor.execute( commandName ) );
-
-			return buttonView;
-		} );
-	}
 }

+ 16 - 2
packages/ckeditor5-list/src/listengine.js

@@ -4,7 +4,7 @@
  */
 
 /**
- * @module list/listengine
+ * @module list/listediting
  */
 
 import ListCommand from './listcommand';
@@ -35,7 +35,7 @@ import {
  *
  * @extends module:core/plugin~Plugin
  */
-export default class ListEngine extends Plugin {
+export default class ListEditing extends Plugin {
 	/**
 	 * @inheritDoc
 	 */
@@ -101,6 +101,20 @@ export default class ListEngine extends Plugin {
 		// Register commands for indenting.
 		editor.commands.add( 'indentList', new IndentCommand( editor, 'forward' ) );
 		editor.commands.add( 'outdentList', new IndentCommand( editor, 'backward' ) );
+
+		const getCommandExecuter = commandName => {
+			return ( data, cancel ) => {
+				const command = this.editor.commands.get( commandName );
+
+				if ( command.isEnabled ) {
+					this.editor.execute( commandName );
+					cancel();
+				}
+			};
+		};
+
+		this.editor.keystrokes.set( 'Tab', getCommandExecuter( 'indentList' ) );
+		this.editor.keystrokes.set( 'Shift+Tab', getCommandExecuter( 'outdentList' ) );
 	}
 }
 

+ 116 - 0
packages/ckeditor5-list/src/listui.js

@@ -0,0 +1,116 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module list/listui
+ */
+
+import numberedListIcon from '../theme/icons/numberedlist.svg';
+import bulletedListIcon from '../theme/icons/bulletedlist.svg';
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+
+/**
+ * The list UI feature. It introduces the `numberedList` and `bulletedList` buttons that
+ * allow to convert paragraphs to and from list items and indent or outdent them.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class ListUI extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		// Create two buttons and link them with numberedList and bulletedList commands.
+		const t = this.editor.t;
+		this._addButton( 'numberedList', t( 'Numbered List' ), numberedListIcon );
+		this._addButton( 'bulletedList', t( 'Bulleted List' ), bulletedListIcon );
+
+		// Overwrite default Enter key behavior.
+		// If Enter key is pressed with selection collapsed in empty list item, outdent it instead of breaking it.
+		this.listenTo( this.editor.editing.view, 'enter', ( evt, data ) => {
+			const doc = this.editor.model.document;
+			const positionParent = doc.selection.getLastPosition().parent;
+
+			if ( doc.selection.isCollapsed && positionParent.name == 'listItem' && positionParent.isEmpty ) {
+				this.editor.execute( 'outdentList' );
+
+				data.preventDefault();
+				evt.stop();
+			}
+		} );
+
+		// Overwrite default Backspace key behavior.
+		// If Backspace key is pressed with selection collapsed on first position in first list item, outdent it. #83
+		this.listenTo( this.editor.editing.view, 'delete', ( evt, data ) => {
+			// Check conditions from those that require less computations like those immediately available.
+			if ( data.direction !== 'backward' ) {
+				return;
+			}
+
+			const selection = this.editor.model.document.selection;
+
+			if ( !selection.isCollapsed ) {
+				return;
+			}
+
+			const firstPosition = selection.getFirstPosition();
+
+			if ( !firstPosition.isAtStart ) {
+				return;
+			}
+
+			const positionParent = firstPosition.parent;
+
+			if ( positionParent.name !== 'listItem' ) {
+				return;
+			}
+
+			const previousIsAListItem = positionParent.previousSibling && positionParent.previousSibling.name === 'listItem';
+
+			if ( previousIsAListItem ) {
+				return;
+			}
+
+			this.editor.execute( 'outdentList' );
+
+			data.preventDefault();
+			evt.stop();
+		}, { priority: 'high' } );
+	}
+
+	/**
+	 * Helper method for initializing a button and linking it with an appropriate command.
+	 *
+	 * @private
+	 * @param {String} commandName The name of the command.
+	 * @param {Object} label The button label.
+	 * @param {String} icon The source of the icon.
+	 */
+	_addButton( commandName, label, icon ) {
+		const editor = this.editor;
+
+		editor.ui.componentFactory.add( commandName, locale => {
+			const command = editor.commands.get( commandName );
+
+			const buttonView = new ButtonView( locale );
+
+			buttonView.set( {
+				label,
+				icon,
+				tooltip: true
+			} );
+
+			// Bind button model to command.
+			buttonView.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );
+
+			// Execute command.
+			this.listenTo( buttonView, 'execute', () => editor.execute( commandName ) );
+
+			return buttonView;
+		} );
+	}
+}

+ 6 - 305
packages/ckeditor5-list/tests/list.js

@@ -3,315 +3,16 @@
  * For licensing, see LICENSE.md.
  */
 
-/* globals document */
-
 import List from '../src/list';
-import ListEngine from '../src/listengine';
-
-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 { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
-import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import ListEditing from '../src/listediting';
+import ListUI from '../src/listui';
 
 describe( 'List', () => {
-	let editor, model, bulletedListButton, numberedListButton;
-
-	beforeEach( () => {
-		const editorElement = document.createElement( 'div' );
-		document.body.appendChild( editorElement );
-
-		return ClassicTestEditor.create( editorElement, { plugins: [ Paragraph, BlockQuote, List ] } )
-			.then( newEditor => {
-				editor = newEditor;
-				model = editor.model;
-
-				bulletedListButton = editor.ui.componentFactory.create( 'bulletedList' );
-				numberedListButton = editor.ui.componentFactory.create( 'numberedList' );
-			} );
-	} );
-
-	afterEach( () => {
-		return editor.destroy();
-	} );
-
-	it( 'should be loaded', () => {
-		expect( editor.plugins.get( List ) ).to.be.instanceOf( List );
-	} );
-
-	it( 'should load ListEngine', () => {
-		expect( editor.plugins.get( ListEngine ) ).to.be.instanceOf( ListEngine );
-	} );
-
-	it( 'should set up keys for bulleted list and numbered list', () => {
-		expect( bulletedListButton ).to.be.instanceOf( ButtonView );
-		expect( numberedListButton ).to.be.instanceOf( ButtonView );
-	} );
-
-	it( 'should execute proper commands when buttons are used', () => {
-		sinon.spy( editor, 'execute' );
-
-		bulletedListButton.fire( 'execute' );
-		expect( editor.execute.calledWithExactly( 'bulletedList' ) );
-
-		numberedListButton.fire( 'execute' );
-		expect( editor.execute.calledWithExactly( 'numberedList' ) );
-	} );
-
-	it( 'should bind bulleted list button model to bulledList command', () => {
-		setData( model, '<listItem type="bulleted" indent="0">[]foo</listItem>' );
-
-		const command = editor.commands.get( 'bulletedList' );
-
-		expect( bulletedListButton.isOn ).to.be.true;
-		expect( bulletedListButton.isEnabled ).to.be.true;
-
-		command.value = false;
-		expect( bulletedListButton.isOn ).to.be.false;
-
-		command.isEnabled = false;
-		expect( bulletedListButton.isEnabled ).to.be.false;
-	} );
-
-	it( 'should bind numbered list button model to numberedList command', () => {
-		setData( model, '<listItem type="bulleted" indent="0">[]foo</listItem>' );
-
-		const command = editor.commands.get( 'numberedList' );
-
-		// We are in UL, so numbered list is off.
-		expect( numberedListButton.isOn ).to.be.false;
-		expect( numberedListButton.isEnabled ).to.be.true;
-
-		command.value = true;
-		expect( numberedListButton.isOn ).to.be.true;
-
-		command.isEnabled = false;
-		expect( numberedListButton.isEnabled ).to.be.false;
-	} );
-
-	describe( 'enter key handling callback', () => {
-		it( 'should execute outdentList command on enter key in empty list', () => {
-			const domEvtDataStub = { preventDefault() {} };
-
-			sinon.spy( editor, 'execute' );
-
-			setData( model, '<listItem type="bulleted" indent="0">[]</listItem>' );
-
-			editor.editing.view.fire( 'enter', domEvtDataStub );
-
-			expect( editor.execute.calledOnce ).to.be.true;
-			expect( editor.execute.calledWithExactly( 'outdentList' ) );
-		} );
-
-		it( 'should not execute outdentList command on enter key in non-empty list', () => {
-			const domEvtDataStub = { preventDefault() {} };
-
-			sinon.spy( editor, 'execute' );
-
-			setData( model, '<listItem type="bulleted" indent="0">foo[]</listItem>' );
-
-			editor.editing.view.fire( 'enter', domEvtDataStub );
-
-			expect( editor.execute.called ).to.be.false;
-		} );
-	} );
-
-	describe( 'delete key handling callback', () => {
-		it( 'should execute outdentList command on backspace key in first item of list', () => {
-			const domEvtDataStub = { preventDefault() {}, direction: 'backward' };
-
-			sinon.spy( editor, 'execute' );
-
-			setData( model, '<listItem type="bulleted" indent="0">[]foo</listItem>' );
-
-			editor.editing.view.fire( 'delete', domEvtDataStub );
-
-			expect( editor.execute.calledWithExactly( 'outdentList' ) );
-		} );
-
-		it( 'should execute outdentList command on backspace key in first item of list', () => {
-			const domEvtDataStub = { preventDefault() {}, direction: 'backward' };
-
-			sinon.spy( editor, 'execute' );
-
-			setData( model, '<paragraph>foo</paragraph><listItem type="bulleted" indent="0">[]foo</listItem>' );
-
-			editor.editing.view.fire( 'delete', domEvtDataStub );
-
-			expect( editor.execute.calledWithExactly( 'outdentList' ) );
-		} );
-
-		it( 'should not execute outdentList command on delete key in first item of list', () => {
-			const domEvtDataStub = { preventDefault() {}, direction: 'forward' };
-
-			sinon.spy( editor, 'execute' );
-
-			setData( model, '<listItem type="bulleted" indent="0">[]foo</listItem>' );
-
-			editor.editing.view.fire( 'delete', domEvtDataStub );
-
-			sinon.assert.notCalled( editor.execute );
-		} );
-
-		it( 'should not execute outdentList command when selection is not collapsed', () => {
-			const domEvtDataStub = { preventDefault() {}, direction: 'backward' };
-
-			sinon.spy( editor, 'execute' );
-
-			setData( model, '<listItem type="bulleted" indent="0">[fo]o</listItem>' );
-
-			editor.editing.view.fire( 'delete', domEvtDataStub );
-
-			sinon.assert.notCalled( editor.execute );
-		} );
-
-		it( 'should not execute outdentList command if not in list item', () => {
-			const domEvtDataStub = { preventDefault() {}, direction: 'backward' };
-
-			sinon.spy( editor, 'execute' );
-
-			setData( model, '<paragraph>[]foo</paragraph>' );
-
-			editor.editing.view.fire( 'delete', domEvtDataStub );
-
-			sinon.assert.notCalled( editor.execute );
-		} );
-
-		it( 'should not execute outdentList command if not in first list item', () => {
-			const domEvtDataStub = { preventDefault() {}, direction: 'backward' };
-
-			sinon.spy( editor, 'execute' );
-
-			setData( model, '<listItem type="bulleted" indent="0">foo</listItem><listItem type="bulleted" indent="0">[]foo</listItem>' );
-
-			editor.editing.view.fire( 'delete', domEvtDataStub );
-
-			sinon.assert.notCalled( editor.execute );
-		} );
-
-		it( 'should not execute outdentList command when selection is not on first position', () => {
-			const domEvtDataStub = { preventDefault() {}, direction: 'backward' };
-
-			sinon.spy( editor, 'execute' );
-
-			setData( model, '<listItem type="bulleted" indent="0">fo[]o</listItem>' );
-
-			editor.editing.view.fire( 'delete', domEvtDataStub );
-
-			sinon.assert.notCalled( editor.execute );
-		} );
-
-		it( 'should not execute outdentList command when selection is not on first position', () => {
-			const domEvtDataStub = { preventDefault() {}, direction: 'backward' };
-
-			sinon.spy( editor, 'execute' );
-
-			setData( model, '<listItem type="bulleted" indent="0">fo[]o</listItem>' );
-
-			editor.editing.view.fire( 'delete', domEvtDataStub );
-
-			sinon.assert.notCalled( editor.execute );
-		} );
-
-		it( 'should outdent list when previous element is nested in block quote', () => {
-			const domEvtDataStub = { preventDefault() {}, direction: 'backward' };
-
-			sinon.spy( editor, 'execute' );
-
-			setData( model, '<blockQuote><paragraph>x</paragraph></blockQuote><listItem type="bulleted" indent="0">[]foo</listItem>' );
-
-			editor.editing.view.fire( 'delete', domEvtDataStub );
-
-			expect( editor.execute.calledWithExactly( 'outdentList' ) );
-		} );
-
-		it( 'should outdent list when list is nested in block quote', () => {
-			const domEvtDataStub = { preventDefault() {}, direction: 'backward' };
-
-			sinon.spy( editor, 'execute' );
-
-			setData( model, '<paragraph>x</paragraph><blockQuote><listItem type="bulleted" indent="0">[]foo</listItem></blockQuote>' );
-
-			editor.editing.view.fire( 'delete', domEvtDataStub );
-
-			expect( editor.execute.calledWithExactly( 'outdentList' ) );
-		} );
+	it( 'should be named', () => {
+		expect( List.pluginName ).to.equal( 'List' );
 	} );
 
-	describe( 'tab key handling callback', () => {
-		let domEvtDataStub;
-
-		beforeEach( () => {
-			domEvtDataStub = {
-				keyCode: getCode( 'Tab' ),
-				preventDefault: sinon.spy(),
-				stopPropagation: sinon.spy()
-			};
-
-			sinon.spy( editor, 'execute' );
-		} );
-
-		afterEach( () => {
-			editor.execute.restore();
-		} );
-
-		it( 'should execute indentList command on tab key', () => {
-			setData(
-				model,
-				'<listItem type="bulleted" indent="0">foo</listItem>' +
-				'<listItem type="bulleted" indent="0">[]bar</listItem>'
-			);
-
-			editor.editing.view.fire( 'keydown', domEvtDataStub );
-
-			expect( editor.execute.calledOnce ).to.be.true;
-			expect( editor.execute.calledWithExactly( 'indentList' ) ).to.be.true;
-			sinon.assert.calledOnce( domEvtDataStub.preventDefault );
-			sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
-		} );
-
-		it( 'should execute outdentList command on Shift+Tab keystroke', () => {
-			domEvtDataStub.keyCode += getCode( 'Shift' );
-
-			setData(
-				model,
-				'<listItem type="bulleted" indent="0">foo</listItem>' +
-				'<listItem type="bulleted" indent="1">[]bar</listItem>'
-			);
-
-			editor.editing.view.fire( 'keydown', domEvtDataStub );
-
-			expect( editor.execute.calledOnce ).to.be.true;
-			expect( editor.execute.calledWithExactly( 'outdentList' ) ).to.be.true;
-			sinon.assert.calledOnce( domEvtDataStub.preventDefault );
-			sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
-		} );
-
-		it( 'should not indent if command is disabled', () => {
-			setData( model, '<listItem type="bulleted" indent="0">[]foo</listItem>' );
-
-			editor.editing.view.fire( 'keydown', domEvtDataStub );
-
-			expect( editor.execute.called ).to.be.false;
-			sinon.assert.notCalled( domEvtDataStub.preventDefault );
-			sinon.assert.notCalled( domEvtDataStub.stopPropagation );
-		} );
-
-		it( 'should not indent or outdent if alt+tab is pressed', () => {
-			domEvtDataStub.keyCode += getCode( 'alt' );
-
-			setData(
-				model,
-				'<listItem type="bulleted" indent="0">foo</listItem>' +
-				'<listItem type="bulleted" indent="0">[]bar</listItem>'
-			);
-
-			editor.editing.view.fire( 'keydown', domEvtDataStub );
-
-			expect( editor.execute.called ).to.be.false;
-			sinon.assert.notCalled( domEvtDataStub.preventDefault );
-			sinon.assert.notCalled( domEvtDataStub.stopPropagation );
-		} );
+	it( 'should require ListEditing and ListUI', () => {
+		expect( List.requires ).to.deep.equal( [ ListEditing, ListUI ] );
 	} );
 } );

+ 7 - 7
packages/ckeditor5-list/tests/listengine.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import ListEngine from '../src/listengine';
+import ListEditing from '../src/listediting';
 import ListCommand from '../src/listcommand';
 
 import ModelDocumentFragment from '@ckeditor/ckeditor5-engine/src/model/documentfragment';
@@ -15,10 +15,10 @@ import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
 import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement';
 import ViewUIElement from '@ckeditor/ckeditor5-engine/src/view/uielement';
 
-import BoldEngine from '@ckeditor/ckeditor5-basic-styles/src/boldengine';
-import UndoEngine from '@ckeditor/ckeditor5-undo/src/undoengine';
+import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
+import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
 import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
-import BlockQuoteEngine from '@ckeditor/ckeditor5-block-quote/src/blockquoteengine';
+import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting';
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import { getData as getModelData, setData as setModelData, parse as parseModel } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
@@ -27,13 +27,13 @@ import { getData as getViewData, parse as parseView } from '@ckeditor/ckeditor5-
 import { insertElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
 import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
 
-describe( 'ListEngine', () => {
+describe( 'ListEditing', () => {
 	let editor, model, modelDoc, modelRoot, viewDoc, viewRoot;
 
 	beforeEach( () => {
 		return VirtualTestEditor
 			.create( {
-				plugins: [ Clipboard, BoldEngine, ListEngine, UndoEngine, BlockQuoteEngine ]
+				plugins: [ Clipboard, BoldEditing, ListEditing, UndoEditing, BlockQuoteEditing ]
 			} )
 			.then( newEditor => {
 				editor = newEditor;
@@ -48,7 +48,7 @@ describe( 'ListEngine', () => {
 	} );
 
 	it( 'should be loaded', () => {
-		expect( editor.plugins.get( ListEngine ) ).to.be.instanceOf( ListEngine );
+		expect( editor.plugins.get( ListEditing ) ).to.be.instanceOf( ListEditing );
 	} );
 
 	it( 'should set proper schema rules', () => {

+ 313 - 0
packages/ckeditor5-list/tests/listui.js

@@ -0,0 +1,313 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document */
+
+import ListEditing from '../src/listediting';
+import ListUI from '../src/listui';
+
+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 { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+describe( 'ListUI', () => {
+	let editor, model, bulletedListButton, numberedListButton;
+
+	beforeEach( () => {
+		const editorElement = document.createElement( 'div' );
+		document.body.appendChild( editorElement );
+
+		return ClassicTestEditor.create( editorElement, { plugins: [ Paragraph, BlockQuote, ListEditing, ListUI ] } )
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+
+				bulletedListButton = editor.ui.componentFactory.create( 'bulletedList' );
+				numberedListButton = editor.ui.componentFactory.create( 'numberedList' );
+			} );
+	} );
+
+	afterEach( () => {
+		return editor.destroy();
+	} );
+
+	it( 'should be loaded', () => {
+		expect( editor.plugins.get( ListUI ) ).to.be.instanceOf( ListUI );
+	} );
+
+	it( 'should set up keys for bulleted list and numbered list', () => {
+		expect( bulletedListButton ).to.be.instanceOf( ButtonView );
+		expect( numberedListButton ).to.be.instanceOf( ButtonView );
+	} );
+
+	it( 'should execute proper commands when buttons are used', () => {
+		sinon.spy( editor, 'execute' );
+
+		bulletedListButton.fire( 'execute' );
+		expect( editor.execute.calledWithExactly( 'bulletedList' ) );
+
+		numberedListButton.fire( 'execute' );
+		expect( editor.execute.calledWithExactly( 'numberedList' ) );
+	} );
+
+	it( 'should bind bulleted list button model to bulledList command', () => {
+		setData( model, '<listItem type="bulleted" indent="0">[]foo</listItem>' );
+
+		const command = editor.commands.get( 'bulletedList' );
+
+		expect( bulletedListButton.isOn ).to.be.true;
+		expect( bulletedListButton.isEnabled ).to.be.true;
+
+		command.value = false;
+		expect( bulletedListButton.isOn ).to.be.false;
+
+		command.isEnabled = false;
+		expect( bulletedListButton.isEnabled ).to.be.false;
+	} );
+
+	it( 'should bind numbered list button model to numberedList command', () => {
+		setData( model, '<listItem type="bulleted" indent="0">[]foo</listItem>' );
+
+		const command = editor.commands.get( 'numberedList' );
+
+		// We are in UL, so numbered list is off.
+		expect( numberedListButton.isOn ).to.be.false;
+		expect( numberedListButton.isEnabled ).to.be.true;
+
+		command.value = true;
+		expect( numberedListButton.isOn ).to.be.true;
+
+		command.isEnabled = false;
+		expect( numberedListButton.isEnabled ).to.be.false;
+	} );
+
+	describe( 'enter key handling callback', () => {
+		it( 'should execute outdentList command on enter key in empty list', () => {
+			const domEvtDataStub = { preventDefault() {} };
+
+			sinon.spy( editor, 'execute' );
+
+			setData( model, '<listItem type="bulleted" indent="0">[]</listItem>' );
+
+			editor.editing.view.fire( 'enter', domEvtDataStub );
+
+			expect( editor.execute.calledOnce ).to.be.true;
+			expect( editor.execute.calledWithExactly( 'outdentList' ) );
+		} );
+
+		it( 'should not execute outdentList command on enter key in non-empty list', () => {
+			const domEvtDataStub = { preventDefault() {} };
+
+			sinon.spy( editor, 'execute' );
+
+			setData( model, '<listItem type="bulleted" indent="0">foo[]</listItem>' );
+
+			editor.editing.view.fire( 'enter', domEvtDataStub );
+
+			expect( editor.execute.called ).to.be.false;
+		} );
+	} );
+
+	describe( 'delete key handling callback', () => {
+		it( 'should execute outdentList command on backspace key in first item of list', () => {
+			const domEvtDataStub = { preventDefault() {}, direction: 'backward' };
+
+			sinon.spy( editor, 'execute' );
+
+			setData( model, '<listItem type="bulleted" indent="0">[]foo</listItem>' );
+
+			editor.editing.view.fire( 'delete', domEvtDataStub );
+
+			expect( editor.execute.calledWithExactly( 'outdentList' ) );
+		} );
+
+		it( 'should execute outdentList command on backspace key in first item of list', () => {
+			const domEvtDataStub = { preventDefault() {}, direction: 'backward' };
+
+			sinon.spy( editor, 'execute' );
+
+			setData( model, '<paragraph>foo</paragraph><listItem type="bulleted" indent="0">[]foo</listItem>' );
+
+			editor.editing.view.fire( 'delete', domEvtDataStub );
+
+			expect( editor.execute.calledWithExactly( 'outdentList' ) );
+		} );
+
+		it( 'should not execute outdentList command on delete key in first item of list', () => {
+			const domEvtDataStub = { preventDefault() {}, direction: 'forward' };
+
+			sinon.spy( editor, 'execute' );
+
+			setData( model, '<listItem type="bulleted" indent="0">[]foo</listItem>' );
+
+			editor.editing.view.fire( 'delete', domEvtDataStub );
+
+			sinon.assert.notCalled( editor.execute );
+		} );
+
+		it( 'should not execute outdentList command when selection is not collapsed', () => {
+			const domEvtDataStub = { preventDefault() {}, direction: 'backward' };
+
+			sinon.spy( editor, 'execute' );
+
+			setData( model, '<listItem type="bulleted" indent="0">[fo]o</listItem>' );
+
+			editor.editing.view.fire( 'delete', domEvtDataStub );
+
+			sinon.assert.notCalled( editor.execute );
+		} );
+
+		it( 'should not execute outdentList command if not in list item', () => {
+			const domEvtDataStub = { preventDefault() {}, direction: 'backward' };
+
+			sinon.spy( editor, 'execute' );
+
+			setData( model, '<paragraph>[]foo</paragraph>' );
+
+			editor.editing.view.fire( 'delete', domEvtDataStub );
+
+			sinon.assert.notCalled( editor.execute );
+		} );
+
+		it( 'should not execute outdentList command if not in first list item', () => {
+			const domEvtDataStub = { preventDefault() {}, direction: 'backward' };
+
+			sinon.spy( editor, 'execute' );
+
+			setData( model, '<listItem type="bulleted" indent="0">foo</listItem><listItem type="bulleted" indent="0">[]foo</listItem>' );
+
+			editor.editing.view.fire( 'delete', domEvtDataStub );
+
+			sinon.assert.notCalled( editor.execute );
+		} );
+
+		it( 'should not execute outdentList command when selection is not on first position', () => {
+			const domEvtDataStub = { preventDefault() {}, direction: 'backward' };
+
+			sinon.spy( editor, 'execute' );
+
+			setData( model, '<listItem type="bulleted" indent="0">fo[]o</listItem>' );
+
+			editor.editing.view.fire( 'delete', domEvtDataStub );
+
+			sinon.assert.notCalled( editor.execute );
+		} );
+
+		it( 'should not execute outdentList command when selection is not on first position', () => {
+			const domEvtDataStub = { preventDefault() {}, direction: 'backward' };
+
+			sinon.spy( editor, 'execute' );
+
+			setData( model, '<listItem type="bulleted" indent="0">fo[]o</listItem>' );
+
+			editor.editing.view.fire( 'delete', domEvtDataStub );
+
+			sinon.assert.notCalled( editor.execute );
+		} );
+
+		it( 'should outdent list when previous element is nested in block quote', () => {
+			const domEvtDataStub = { preventDefault() {}, direction: 'backward' };
+
+			sinon.spy( editor, 'execute' );
+
+			setData( model, '<blockQuote><paragraph>x</paragraph></blockQuote><listItem type="bulleted" indent="0">[]foo</listItem>' );
+
+			editor.editing.view.fire( 'delete', domEvtDataStub );
+
+			expect( editor.execute.calledWithExactly( 'outdentList' ) );
+		} );
+
+		it( 'should outdent list when list is nested in block quote', () => {
+			const domEvtDataStub = { preventDefault() {}, direction: 'backward' };
+
+			sinon.spy( editor, 'execute' );
+
+			setData( model, '<paragraph>x</paragraph><blockQuote><listItem type="bulleted" indent="0">[]foo</listItem></blockQuote>' );
+
+			editor.editing.view.fire( 'delete', domEvtDataStub );
+
+			expect( editor.execute.calledWithExactly( 'outdentList' ) );
+		} );
+	} );
+
+	describe( 'tab key handling callback', () => {
+		let domEvtDataStub;
+
+		beforeEach( () => {
+			domEvtDataStub = {
+				keyCode: getCode( 'Tab' ),
+				preventDefault: sinon.spy(),
+				stopPropagation: sinon.spy()
+			};
+
+			sinon.spy( editor, 'execute' );
+		} );
+
+		afterEach( () => {
+			editor.execute.restore();
+		} );
+
+		it( 'should execute indentList command on tab key', () => {
+			setData(
+				model,
+				'<listItem type="bulleted" indent="0">foo</listItem>' +
+				'<listItem type="bulleted" indent="0">[]bar</listItem>'
+			);
+
+			editor.editing.view.fire( 'keydown', domEvtDataStub );
+
+			expect( editor.execute.calledOnce ).to.be.true;
+			expect( editor.execute.calledWithExactly( 'indentList' ) ).to.be.true;
+			sinon.assert.calledOnce( domEvtDataStub.preventDefault );
+			sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
+		} );
+
+		it( 'should execute outdentList command on Shift+Tab keystroke', () => {
+			domEvtDataStub.keyCode += getCode( 'Shift' );
+
+			setData(
+				model,
+				'<listItem type="bulleted" indent="0">foo</listItem>' +
+				'<listItem type="bulleted" indent="1">[]bar</listItem>'
+			);
+
+			editor.editing.view.fire( 'keydown', domEvtDataStub );
+
+			expect( editor.execute.calledOnce ).to.be.true;
+			expect( editor.execute.calledWithExactly( 'outdentList' ) ).to.be.true;
+			sinon.assert.calledOnce( domEvtDataStub.preventDefault );
+			sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
+		} );
+
+		it( 'should not indent if command is disabled', () => {
+			setData( model, '<listItem type="bulleted" indent="0">[]foo</listItem>' );
+
+			editor.editing.view.fire( 'keydown', domEvtDataStub );
+
+			expect( editor.execute.called ).to.be.false;
+			sinon.assert.notCalled( domEvtDataStub.preventDefault );
+			sinon.assert.notCalled( domEvtDataStub.stopPropagation );
+		} );
+
+		it( 'should not indent or outdent if alt+tab is pressed', () => {
+			domEvtDataStub.keyCode += getCode( 'alt' );
+
+			setData(
+				model,
+				'<listItem type="bulleted" indent="0">foo</listItem>' +
+				'<listItem type="bulleted" indent="0">[]bar</listItem>'
+			);
+
+			editor.editing.view.fire( 'keydown', domEvtDataStub );
+
+			expect( editor.execute.called ).to.be.false;
+			sinon.assert.notCalled( domEvtDataStub.preventDefault );
+			sinon.assert.notCalled( domEvtDataStub.stopPropagation );
+		} );
+	} );
+} );