8
0
Piotrek Koszuliński 9 лет назад
Родитель
Сommit
6d8927e669

+ 1 - 1
packages/ckeditor5-enter/src/enter.js

@@ -45,6 +45,6 @@ export default class Enter extends Feature {
  *
  * Note: This event is fired by the {@link enter.Enter enter feature}.
  *
- * @event engine.treeView#enter
+ * @event engine.treeView.TreeView#enter
  * @param {engine.treeView.observer.DomEventData} data
  */

+ 10 - 0
packages/ckeditor5-enter/src/entercommand.js

@@ -26,6 +26,16 @@ export default class EnterCommand extends Command {
 	}
 }
 
+/**
+ * Enters new block in the way how <kbd>Enter</kbd> is expected to work.
+ *
+ * @param {engine.treeModel.Batch} batch Batch to which the deltas will be added.
+ * @param {engine.treeModel.Selection} selection
+ * @param {Object} options
+ * @param {Boolean} options.defaultBlockName Name of the block which should be created when enter leaves
+ * another block. Usuall set to `'paragraph'`. E.g. when entering block in `<heading>foo^</heading>` the result will be
+ * `<heading>foo</heading><paragraph>^</paragraph>`.
+ */
 export function enterBlock( batch, selection, options = {} ) {
 	const defaultBlockName = options.defaultBlockName;
 	const doc = batch.doc;

+ 80 - 0
packages/ckeditor5-enter/tests/enter.js

@@ -0,0 +1,80 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Editor from '/ckeditor5/editor.js';
+import StandardCreator from '/ckeditor5/creator/standardcreator.js';
+import Enter from '/ckeditor5/enter/enter.js';
+import EnterCommand from '/ckeditor5/enter/entercommand.js';
+import DomEventData from '/ckeditor5/engine/treeview/observer/domeventdata.js';
+import { getCode } from '/ckeditor5/utils/keyboard.js';
+
+describe( 'Enter feature', () => {
+	let editor, editingView;
+
+	beforeEach( () => {
+		editor = new Editor( null, {
+			creator: StandardCreator,
+			features: [ Enter ]
+		} );
+
+		return editor.init()
+			.then( () => {
+				editor.document.createRoot( 'main' );
+				editingView = editor.editing.view;
+			} );
+	} );
+
+	it( 'creates the commands', () => {
+		expect( editor.commands.get( 'enter' ) ).to.be.instanceof( EnterCommand );
+	} );
+
+	it( 'listens to the editing view enter event', () => {
+		const spy = editor.execute = sinon.spy();
+		const view = editor.editing.view;
+		const domEvt = getDomEvent();
+
+		view.fire( 'enter', new DomEventData( editingView, domEvt ) );
+
+		expect( spy.calledOnce ).to.be.true;
+		expect( spy.calledWithExactly( 'enter' ) ).to.be.true;
+
+		expect( domEvt.preventDefault.calledOnce ).to.be.true;
+	} );
+
+	describe( 'enter event', () => {
+		it( 'is fired on keydown', () => {
+			const view = editor.editing.view;
+			const spy = sinon.spy();
+
+			view.on( 'enter', spy );
+
+			view.fire( 'keydown', new DomEventData( editingView, getDomEvent(), {
+				keyCode: getCode( 'enter' )
+			} ) );
+
+			expect( spy.calledOnce ).to.be.true;
+		} );
+		it( 'is not fired on keydown when keyCode does not match enter', () => {
+			const view = editor.editing.view;
+			const spy = sinon.spy();
+
+			view.on( 'enter', spy );
+
+			view.fire( 'keydown', new DomEventData( editingView, getDomEvent(), {
+				keyCode: 1
+			} ) );
+
+			expect( spy.calledOnce ).to.be.false;
+		} );
+	} );
+
+	function getDomEvent() {
+		return {
+			preventDefault: sinon.spy()
+		};
+	}
+} );

+ 173 - 162
packages/ckeditor5-enter/tests/entercommand.js

@@ -10,178 +10,189 @@ import Document from '/ckeditor5/engine/treemodel/document.js';
 import { default as EnterCommand, enterBlock } from '/ckeditor5/enter/entercommand.js';
 import { getData, setData } from '/tests/engine/_utils/model.js';
 
-describe( 'EnterCommand', () => {
-	let editor, document;
+let editor, doc;
+
+beforeEach( () => {
+	editor = new Editor();
+	doc = editor.document = new Document();
+
+	doc.createRoot( 'main', '$root' );
+
+	const command = new EnterCommand( editor );
+	editor.commands.set( 'enter', command );
 
-	beforeEach( () => {
-		editor = new Editor();
-		document = editor.document = new Document();
+	const schema = doc.schema;
 
-		editor.document.createRoot( 'main', '$root' );
+	// Note: We could use real names like 'paragraph', but that would make test patterns too long.
+	// Plus, this is actually a good test that the algorithm can be used for any model.
+	schema.registerItem( 'img', '$inline' );
+	schema.registerItem( 'p', '$block' );
+	schema.registerItem( 'h', '$block' );
+} );
+
+describe( 'EnterCommand', () => {
+	it( 'enters a block using enqueueChanges', () => {
+		const spy = sinon.spy( doc, 'enqueueChanges' );
 
-		const command = new EnterCommand( editor );
-		editor.commands.set( 'enter', command );
+		setData( doc, 'main', '<p>foo<selection /></p>' );
 
-		const schema = document.schema;
+		editor.execute( 'enter' );
 
-		// Note: We could use real names like 'paragraph', but that would make test patterns too long.
-		// Plus, this is actually a good test that the algorithm can be used for any model.
-		schema.registerItem( 'img', '$inline' );
-		schema.registerItem( 'p', '$block' );
-		schema.registerItem( 'h', '$block' );
+		expect( getData( doc, 'main' ) ).to.equal( '<p>foo</p><p></p>' );
+		expect( spy.calledOnce ).to.be.true;
 	} );
+} );
 
-	describe( 'enterBlock', () => {
-		describe( 'collapsed selection', () => {
-			test(
-				'does nothing in the root',
-				'foo<selection />bar',
-				'foo<selection />bar',
-				{ defaultBlockName: 'p' }
-			);
-
-			test(
-				'splits block',
-				'<p>x</p><p>foo<selection />bar</p><p>y</p>',
-				'<p>x</p><p>foo</p><p><selection />bar</p><p>y</p>',
-				{ defaultBlockName: 'p' }
-			);
-
-			test(
-				'splits block (other than default)',
-				'<p>x</p><h>foo<selection />bar</h><p>y</p>',
-				'<p>x</p><h>foo</h><h><selection />bar</h><p>y</p>',
-				{ defaultBlockName: 'p' }
-			);
-
-			test(
-				'splits block at the end',
-				'<p>x</p><p>foo<selection /></p><p>y</p>',
-				'<p>x</p><p>foo</p><p><selection /></p><p>y</p>',
-				{ defaultBlockName: 'p' }
-			);
-
-			test(
-				'splits block at the beginning',
-				'<p>x</p><p><selection />foo</p><p>y</p>',
-				'<p>x</p><p></p><p><selection />foo</p><p>y</p>',
-				{ defaultBlockName: 'p' }
-			);
-
-			test(
-				'splits block at the beginning (other than default)',
-				'<p>x</p><h><selection />foo</h><p>y</p>',
-				'<p>x</p><h></h><h><selection />foo</h><p>y</p>',
-				{ defaultBlockName: 'p' }
-			);
-
-			test(
-				'creates default block when leaving other block',
-				'<h>foo<selection /></h><p>x</p>',
-				'<h>foo</h><p><selection /></p><p>x</p>',
-				{ defaultBlockName: 'p' }
-			);
-
-			test(
-				'does not rename when default block is not allowed',
-				'<h>foo<selection /></h><p>x</p>',
-				'<h>foo</h><h><selection /></h><p>x</p>',
-				{ defaultBlockName: 'xxx' }
-			);
-
-			test(
-				'inserts new block after empty one',
-				'<p>x</p><p><selection /></p><p>y</p>',
-				'<p>x</p><p></p><p><selection /></p><p>y</p>',
-				{ defaultBlockName: 'p' }
-			);
-
-			test(
-				'inserts new block after empty one (other than default)',
-				'<p>x</p><h><selection /></h><p>y</p>',
-				'<p>x</p><h></h><p><selection /></p><p>y</p>',
-				{ defaultBlockName: 'p' }
-			);
-		} );
+describe( 'enterBlock', () => {
+	describe( 'collapsed selection', () => {
+		test(
+			'does nothing in the root',
+			'foo<selection />bar',
+			'foo<selection />bar',
+			{ defaultBlockName: 'p' }
+		);
+
+		test(
+			'splits block',
+			'<p>x</p><p>foo<selection />bar</p><p>y</p>',
+			'<p>x</p><p>foo</p><p><selection />bar</p><p>y</p>',
+			{ defaultBlockName: 'p' }
+		);
+
+		test(
+			'splits block (other than default)',
+			'<p>x</p><h>foo<selection />bar</h><p>y</p>',
+			'<p>x</p><h>foo</h><h><selection />bar</h><p>y</p>',
+			{ defaultBlockName: 'p' }
+		);
+
+		test(
+			'splits block at the end',
+			'<p>x</p><p>foo<selection /></p><p>y</p>',
+			'<p>x</p><p>foo</p><p><selection /></p><p>y</p>',
+			{ defaultBlockName: 'p' }
+		);
+
+		test(
+			'splits block at the beginning',
+			'<p>x</p><p><selection />foo</p><p>y</p>',
+			'<p>x</p><p></p><p><selection />foo</p><p>y</p>',
+			{ defaultBlockName: 'p' }
+		);
+
+		test(
+			'splits block at the beginning (other than default)',
+			'<p>x</p><h><selection />foo</h><p>y</p>',
+			'<p>x</p><h></h><h><selection />foo</h><p>y</p>',
+			{ defaultBlockName: 'p' }
+		);
+
+		test(
+			'creates default block when leaving other block',
+			'<h>foo<selection /></h><p>x</p>',
+			'<h>foo</h><p><selection /></p><p>x</p>',
+			{ defaultBlockName: 'p' }
+		);
+
+		test(
+			'does not rename when default block is not allowed',
+			'<h>foo<selection /></h><p>x</p>',
+			'<h>foo</h><h><selection /></h><p>x</p>',
+			{ defaultBlockName: 'xxx' }
+		);
+
+		test(
+			'inserts new block after empty one',
+			'<p>x</p><p><selection /></p><p>y</p>',
+			'<p>x</p><p></p><p><selection /></p><p>y</p>',
+			{ defaultBlockName: 'p' }
+		);
+
+		test(
+			'inserts new block after empty one (other than default)',
+			'<p>x</p><h><selection /></h><p>y</p>',
+			'<p>x</p><h></h><p><selection /></p><p>y</p>',
+			{ defaultBlockName: 'p' }
+		);
+	} );
 
-		describe( 'non-collapsed selection', () => {
-			test(
-				'only deletes the content when directly in the root',
-				'fo<selection>ob</selection>ar',
-				'fo<selection />ar',
-				{ defaultBlockName: 'p' }
-			);
-
-			test(
-				'deletes text and splits',
-				'<p>ab<selection>cd</selection>ef</p><p>ghi</p>',
-				'<p>ab</p><p><selection />ef</p><p>ghi</p>',
-				{ defaultBlockName: 'p' }
-			);
-
-			test(
-				'deletes text and splits (other than default)',
-				'<h>ab<selection>cd</selection>ef</h>',
-				'<h>ab</h><h><selection />ef</h>',
-				{ defaultBlockName: 'p' }
-			);
-
-			test(
-				'places selection in the 2nd element',
-				'<h>ab<selection>c</h><p>d</selection>ef</p><p>ghi</p>',
-				'<h>ab</h><p><selection />ef</p><p>ghi</p>',
-				{ defaultBlockName: 'p' }
-			);
-
-			test(
-				'leaves one empty element after one was fully selected',
-				'<p>x</p><p><selection>abcdef</selection></p><p>y</p>',
-				'<p>x</p><p><selection /></p><p>y</p>',
-				{ defaultBlockName: 'p' }
-			);
-
-			test(
-				'leaves one (default) empty element after one was fully selected',
-				'<h><selection>abcdef</selection></h>',
-				'<p><selection /></p>',
-				{ defaultBlockName: 'p' }
-			);
-
-			test(
-				'leaves one (default) empty element after two were fully selected',
-				'<h><selection>abc</h><p>def</selection></p>',
-				'<p><selection /></p>',
-				{ defaultBlockName: 'p' }
-			);
-
-			test(
-				'leaves one (default) empty element after two were fully selected (backward)',
-				'<h><selection backward>abc</h><p>def</selection></p>',
-				'<p><selection /></p>',
-				{ defaultBlockName: 'p' }
-			);
-
-			it( 'uses composer.deleteContents', () => {
-				const spy = sinon.spy();
-
-				document.composer.on( 'deleteContents', spy );
-
-				setData( document, 'main', '<p><selection>x</selection></p>' );
-
-				enterBlock( document.batch(), document.selection, { defaultBlockName: 'p' } );
-
-				expect( spy.calledOnce ).to.be.true;
-			} );
+	describe( 'non-collapsed selection', () => {
+		test(
+			'only deletes the content when directly in the root',
+			'fo<selection>ob</selection>ar',
+			'fo<selection />ar',
+			{ defaultBlockName: 'p' }
+		);
+
+		test(
+			'deletes text and splits',
+			'<p>ab<selection>cd</selection>ef</p><p>ghi</p>',
+			'<p>ab</p><p><selection />ef</p><p>ghi</p>',
+			{ defaultBlockName: 'p' }
+		);
+
+		test(
+			'deletes text and splits (other than default)',
+			'<h>ab<selection>cd</selection>ef</h>',
+			'<h>ab</h><h><selection />ef</h>',
+			{ defaultBlockName: 'p' }
+		);
+
+		test(
+			'places selection in the 2nd element',
+			'<h>ab<selection>c</h><p>d</selection>ef</p><p>ghi</p>',
+			'<h>ab</h><p><selection />ef</p><p>ghi</p>',
+			{ defaultBlockName: 'p' }
+		);
+
+		test(
+			'leaves one empty element after one was fully selected',
+			'<p>x</p><p><selection>abcdef</selection></p><p>y</p>',
+			'<p>x</p><p><selection /></p><p>y</p>',
+			{ defaultBlockName: 'p' }
+		);
+
+		test(
+			'leaves one (default) empty element after one was fully selected',
+			'<h><selection>abcdef</selection></h>',
+			'<p><selection /></p>',
+			{ defaultBlockName: 'p' }
+		);
+
+		test(
+			'leaves one (default) empty element after two were fully selected',
+			'<h><selection>abc</h><p>def</selection></p>',
+			'<p><selection /></p>',
+			{ defaultBlockName: 'p' }
+		);
+
+		test(
+			'leaves one (default) empty element after two were fully selected (backward)',
+			'<h><selection backward>abc</h><p>def</selection></p>',
+			'<p><selection /></p>',
+			{ defaultBlockName: 'p' }
+		);
+
+		it( 'uses composer.deleteContents', () => {
+			const spy = sinon.spy();
+
+			doc.composer.on( 'deleteContents', spy );
+
+			setData( doc, 'main', '<p><selection>x</selection></p>' );
+
+			enterBlock( doc.batch(), doc.selection, { defaultBlockName: 'p' } );
+
+			expect( spy.calledOnce ).to.be.true;
 		} );
+	} );
 
-		function test( title, input, output, options ) {
-			it( title, () => {
-				setData( document, 'main', input );
+	function test( title, input, output, options ) {
+		it( title, () => {
+			setData( doc, 'main', input );
 
-				enterBlock( document.batch(), document.selection, options );
+			enterBlock( doc.batch(), doc.selection, options );
 
-				expect( getData( document, 'main', { selection: true } ) ).to.equal( output );
-			} );
-		}
-	} );
+			expect( getData( doc, 'main', { selection: true } ) ).to.equal( output );
+		} );
+	}
 } );