Forráskód Böngészése

Merge branch 'master' into t/ckeditor5-core/90

Piotrek Koszuliński 8 éve
szülő
commit
4ce8d78e9e

+ 111 - 0
packages/ckeditor5-basic-styles/src/attributecommand.js

@@ -0,0 +1,111 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module basic-styles/attributecommand
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+
+/**
+ * A extension of the base {@link module:core/command~Command} class, which provides utilities for a command
+ * which toggles a single attribute on a text or an element.
+ *
+ * `AttributeCommand` uses {@link module:engine/model/document~Document#selection}
+ * to decide which nodes (if any) should be changed, and applies or removes the attribute from them.
+ *
+ * The command checks {@link module:engine/model/document~Document#schema} to decide if it can be enabled
+ * for the current selection and to which nodes the attribute can be applied.
+ *
+ * @extends module:core/command~Command
+ */
+export default class AttributeCommand extends Command {
+	/**
+	 * @param {module:core/editor/editor~Editor} editor
+	 * @param {String} attributeKey Attribute that will be set by the command.
+	 */
+	constructor( editor, attributeKey ) {
+		super( editor );
+
+		/**
+		 * The attribute that will be set by the command.
+		 *
+		 * @readonly
+		 * @member {String}
+		 */
+		this.attributeKey = attributeKey;
+
+		/**
+		 * Flag indicating whether the command is active. The command is active when the
+		 * {@link module:engine/model/selection~Selection#hasAttribute selection has the attribute} which means that:
+		 *
+		 * * if the selection is not empty that it starts in a text (or other node) which has the attribute set,
+		 * * if the selection is empty that the selection has the attribute itself (which means that newly typed
+		 * text will have this attribute too).
+		 *
+		 * @observable
+		 * @readonly
+		 * @member {Boolean} #value
+		 */
+	}
+
+	/**
+	 * Updates command's {@link #value} based on the current selection.
+	 */
+	refresh() {
+		const doc = this.editor.document;
+
+		this.value = doc.selection.hasAttribute( this.attributeKey );
+		this.isEnabled = doc.schema.checkAttributeInSelection( doc.selection, this.attributeKey );
+	}
+
+	/**
+	 * Executes the command – applies or removes the attribute from the selection.
+	 *
+	 * If the command is active (`value == true`), it will remove attributes. Otherwise, it will set attributes.
+	 *
+	 * The execution result differs, depending on the {@link module:engine/model/document~Document#selection}:
+	 *
+	 * * if selection is on a range, the command applies the attribute on all nodes in that range
+	 * (if they are allowed to have this attribute by the {@link module:engine/model/schema~Schema schema}),
+	 * * if selection is collapsed in non-empty node, the command applies attribute to the
+	 * {@link module:engine/model/document~Document#selection} itself (note that typed characters copy attributes from selection),
+	 * * if selection is collapsed in empty node, the command applies attribute to the parent node of selection (note
+	 * that selection inherits all attributes from a node if it is in empty node).
+	 *
+	 * @fires execute
+	 * @param {Object} [options] Options of command.
+	 * @param {Boolean} [options.forceValue] If set it will force the command behavior. If `true`, command will apply the attribute,
+	 * otherwise the command will remove the attribute.
+	 * If not set, the command will look for it's current value to decide what it should do.
+	 * @param {module:engine/model/batch~Batch} [options.batch] Batch to group undo steps.
+	 */
+	execute( options = {} ) {
+		const doc = this.editor.document;
+		const selection = doc.selection;
+		const value = ( options.forceValue === undefined ) ? !this.value : options.forceValue;
+
+		doc.enqueueChanges( () => {
+			if ( selection.isCollapsed ) {
+				if ( value ) {
+					selection.setAttribute( this.attributeKey, true );
+				} else {
+					selection.removeAttribute( this.attributeKey );
+				}
+			} else {
+				const ranges = doc.schema.getValidRanges( selection.getRanges(), this.attributeKey );
+				const batch = options.batch || doc.batch();
+
+				for ( const range of ranges ) {
+					if ( value ) {
+						batch.setAttribute( range, this.attributeKey, value );
+					} else {
+						batch.removeAttribute( range, this.attributeKey );
+					}
+				}
+			}
+		} );
+	}
+}

+ 2 - 2
packages/ckeditor5-basic-styles/src/boldengine.js

@@ -10,7 +10,7 @@
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
 import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
-import ToggleAttributeCommand from '@ckeditor/ckeditor5-core/src/command/toggleattributecommand';
+import AttributeCommand from './attributecommand';
 
 const BOLD = 'bold';
 
@@ -47,6 +47,6 @@ export default class BoldEngine extends Plugin {
 			.toAttribute( BOLD, true );
 
 		// Create bold command.
-		editor.commands.add( BOLD, new ToggleAttributeCommand( editor, BOLD ) );
+		editor.commands.add( BOLD, new AttributeCommand( editor, BOLD ) );
 	}
 }

+ 2 - 2
packages/ckeditor5-basic-styles/src/italicengine.js

@@ -10,7 +10,7 @@
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
 import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
-import ToggleAttributeCommand from '@ckeditor/ckeditor5-core/src/command/toggleattributecommand';
+import AttributeCommand from './attributecommand';
 
 const ITALIC = 'italic';
 
@@ -47,6 +47,6 @@ export default class ItalicEngine extends Plugin {
 			.toAttribute( ITALIC, true );
 
 		// Create italic command.
-		editor.commands.add( ITALIC, new ToggleAttributeCommand( editor, ITALIC ) );
+		editor.commands.add( ITALIC, new AttributeCommand( editor, ITALIC ) );
 	}
 }

+ 285 - 0
packages/ckeditor5-basic-styles/tests/attributecommand.js

@@ -0,0 +1,285 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import AttributeCommand from '../src/attributecommand';
+
+import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+
+import Document from '@ckeditor/ckeditor5-engine/src/model/document';
+import Batch from '@ckeditor/ckeditor5-engine/src/model/batch';
+import Range from '@ckeditor/ckeditor5-engine/src/model/range';
+import Position from '@ckeditor/ckeditor5-engine/src/model/position';
+import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+describe( 'AttributeCommand', () => {
+	const attrKey = 'bold';
+	let editor, command, doc, root;
+
+	beforeEach( () => {
+		return ModelTestEditor
+			.create()
+			.then( newEditor => {
+				editor = newEditor;
+				doc = editor.document;
+				root = doc.getRoot();
+
+				command = new AttributeCommand( editor, attrKey );
+
+				doc.schema.registerItem( 'p', '$block' );
+				doc.schema.registerItem( 'h1', '$block' );
+				doc.schema.registerItem( 'img', '$inline' );
+
+				// Allow block in "root" (DIV)
+				doc.schema.allow( { name: '$block', inside: '$root' } );
+
+				// Bold text is allowed only in P.
+				doc.schema.allow( { name: '$text', attributes: 'bold', inside: 'p' } );
+				doc.schema.allow( { name: 'p', attributes: 'bold', inside: '$root' } );
+
+				// Disallow bold on image.
+				doc.schema.disallow( { name: 'img', attributes: 'bold', inside: '$root' } );
+			} );
+	} );
+
+	afterEach( () => {
+		command.destroy();
+
+		return editor.destroy();
+	} );
+
+	describe( 'value', () => {
+		it( 'is true when selection has the attribute', () => {
+			doc.enqueueChanges( () => {
+				doc.selection.setAttribute( attrKey, true );
+			} );
+
+			expect( command.value ).to.be.true;
+		} );
+
+		it( 'is false when selection does not have the attribute', () => {
+			doc.enqueueChanges( () => {
+				doc.selection.removeAttribute( attrKey );
+			} );
+
+			expect( command.value ).to.be.false;
+		} );
+	} );
+
+	describe( 'isEnabled', () => {
+		// This test doesn't tests every possible case.
+		// Method `refresh()` uses `checkAttributeInSelection()` which is fully tested in its own test.
+
+		beforeEach( () => {
+			doc.schema.registerItem( 'x', '$block' );
+			doc.schema.disallow( { name: '$text', inside: 'x', attributes: 'link' } );
+		} );
+
+		describe( 'when selection is collapsed', () => {
+			it( 'should return true if characters with the attribute can be placed at caret position', () => {
+				setData( doc, '<p>f[]oo</p>' );
+				expect( command.isEnabled ).to.be.true;
+			} );
+
+			it( 'should return false if characters with the attribute cannot be placed at caret position', () => {
+				setData( doc, '<x>fo[]o</x>' );
+				expect( command.isEnabled ).to.be.false;
+			} );
+		} );
+
+		describe( 'when selection is not collapsed', () => {
+			it( 'should return true if there is at least one node in selection that can have the attribute', () => {
+				setData( doc, '<p>[foo]</p>' );
+				expect( command.isEnabled ).to.be.true;
+			} );
+
+			it( 'should return false if there are no nodes in selection that can have the attribute', () => {
+				setData( doc, '<x>[foo]</x>' );
+				expect( command.isEnabled ).to.be.false;
+			} );
+		} );
+	} );
+
+	describe( 'execute()', () => {
+		it( 'should do nothing if the command is disabled', () => {
+			setData( doc, '<p>fo[ob]ar</p>' );
+
+			command.isEnabled = false;
+
+			command.execute();
+
+			expect( getData( doc ) ).to.equal( '<p>fo[ob]ar</p>' );
+		} );
+
+		it( 'should add attribute on selected nodes if the command value was false', () => {
+			setData( doc, '<p>a[bc<$text bold="true">fo]obar</$text>xyz</p>' );
+
+			expect( command.value ).to.be.false;
+
+			command.execute();
+
+			expect( command.value ).to.be.true;
+			expect( getData( doc ) ).to.equal( '<p>a[<$text bold="true">bcfo]obar</$text>xyz</p>' );
+		} );
+
+		it( 'should remove attribute from selected nodes if the command value was true', () => {
+			setData( doc, '<p>abc[<$text bold="true">foo]bar</$text>xyz</p>' );
+
+			expect( command.value ).to.be.true;
+
+			command.execute();
+
+			expect( getData( doc ) ).to.equal( '<p>abc[foo]<$text bold="true">bar</$text>xyz</p>' );
+			expect( command.value ).to.be.false;
+		} );
+
+		it( 'should add attribute on selected nodes if execute parameter was set to true', () => {
+			setData( doc, '<p>abc<$text bold="true">foob[ar</$text>x]yz</p>' );
+
+			expect( command.value ).to.be.true;
+
+			command.execute( { forceValue: true } );
+
+			expect( command.value ).to.be.true;
+			expect( getData( doc ) ).to.equal( '<p>abc<$text bold="true">foob[arx</$text>]yz</p>' );
+		} );
+
+		it( 'should remove attribute on selected nodes if execute parameter was set to false', () => {
+			setData( doc, '<p>a[bc<$text bold="true">fo]obar</$text>xyz</p>' );
+
+			command.execute( { forceValue: false } );
+
+			expect( command.value ).to.be.false;
+			expect( getData( doc ) ).to.equal( '<p>a[bcfo]<$text bold="true">obar</$text>xyz</p>' );
+		} );
+
+		it( 'should change selection attribute if selection is collapsed in non-empty parent', () => {
+			setData( doc, '<p>a[]bc<$text bold="true">foobar</$text>xyz</p><p></p>' );
+
+			expect( command.value ).to.be.false;
+
+			command.execute();
+
+			expect( command.value ).to.be.true;
+			expect( doc.selection.hasAttribute( 'bold' ) ).to.be.true;
+
+			command.execute();
+
+			expect( command.value ).to.be.false;
+			expect( doc.selection.hasAttribute( 'bold' ) ).to.be.false;
+		} );
+
+		it( 'should not store attribute change on selection if selection is collapsed in non-empty parent', () => {
+			setData( doc, '<p>a[]bc<$text bold="true">foobar</$text>xyz</p>' );
+
+			command.execute();
+
+			// It should not save that bold was executed at position ( root, [ 0, 1 ] ).
+
+			doc.enqueueChanges( () => {
+				// Simulate clicking right arrow key by changing selection ranges.
+				doc.selection.setRanges( [ new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 0, 2 ] ) ) ] );
+
+				// Get back to previous selection.
+				doc.selection.setRanges( [ new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 0, 1 ] ) ) ] );
+			} );
+
+			expect( command.value ).to.be.false;
+		} );
+
+		it( 'should change selection attribute and store it if selection is collapsed in empty parent', () => {
+			setData( doc, '<p>abc<$text bold="true">foobar</$text>xyz</p><p>[]</p>' );
+
+			expect( command.value ).to.be.false;
+
+			command.execute();
+
+			expect( command.value ).to.be.true;
+			expect( doc.selection.hasAttribute( 'bold' ) ).to.be.true;
+
+			// Attribute should be stored.
+			// Simulate clicking somewhere else in the editor.
+			doc.enqueueChanges( () => {
+				doc.selection.setRanges( [ new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 0, 2 ] ) ) ] );
+			} );
+
+			expect( command.value ).to.be.false;
+
+			// Go back to where attribute was stored.
+			doc.enqueueChanges( () => {
+				doc.selection.setRanges( [ new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 1, 0 ] ) ) ] );
+			} );
+
+			// Attribute should be restored.
+			expect( command.value ).to.be.true;
+
+			command.execute();
+
+			expect( command.value ).to.be.false;
+			expect( doc.selection.hasAttribute( 'bold' ) ).to.be.false;
+		} );
+
+		it( 'should not apply attribute change where it would invalid schema', () => {
+			doc.schema.registerItem( 'image', '$block' );
+			setData( doc, '<p>ab[c<img></img><$text bold="true">foobar</$text>xy<img></img>]z</p>' );
+
+			expect( command.isEnabled ).to.be.true;
+
+			command.execute();
+
+			expect( getData( doc ) )
+				.to.equal( '<p>ab[<$text bold="true">c</$text><img></img><$text bold="true">foobarxy</$text><img></img>]z</p>' );
+		} );
+
+		it( 'should use provided batch for storing undo steps', () => {
+			const batch = new Batch( new Document() );
+			setData( doc, '<p>a[bc<$text bold="true">fo]obar</$text>xyz</p>' );
+
+			expect( batch.deltas.length ).to.equal( 0 );
+
+			command.execute( { batch } );
+
+			expect( batch.deltas.length ).to.equal( 1 );
+			expect( getData( doc ) ).to.equal( '<p>a[<$text bold="true">bcfo]obar</$text>xyz</p>' );
+		} );
+
+		describe( 'should cause firing model document changesDone event', () => {
+			let spy;
+
+			beforeEach( () => {
+				spy = sinon.spy();
+			} );
+
+			it( 'collapsed selection in non-empty parent', () => {
+				setData( doc, '<p>x[]y</p>' );
+
+				doc.on( 'changesDone', spy );
+
+				command.execute();
+
+				expect( spy.calledOnce ).to.be.true;
+			} );
+
+			it( 'non-collapsed selection', () => {
+				setData( doc, '<p>[xy]</p>' );
+
+				doc.on( 'changesDone', spy );
+
+				command.execute();
+
+				expect( spy.calledOnce ).to.be.true;
+			} );
+
+			it( 'in empty parent', () => {
+				setData( doc, '<p>[]</p>' );
+
+				doc.on( 'changesDone', spy );
+
+				command.execute();
+
+				expect( spy.calledOnce ).to.be.true;
+			} );
+		} );
+	} );
+} );

+ 2 - 2
packages/ckeditor5-basic-styles/tests/boldengine.js

@@ -7,7 +7,7 @@ import BoldEngine from '../src/boldengine';
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import ToggleAttributeCommand from '@ckeditor/ckeditor5-core/src/command/toggleattributecommand';
+import AttributeCommand from '../src/attributecommand';
 
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
@@ -39,7 +39,7 @@ describe( 'BoldEngine', () => {
 		it( 'should register bold command', () => {
 			const command = editor.commands.get( 'bold' );
 
-			expect( command ).to.be.instanceOf( ToggleAttributeCommand );
+			expect( command ).to.be.instanceOf( AttributeCommand );
 			expect( command ).to.have.property( 'attributeKey', 'bold' );
 		} );
 	} );

+ 2 - 2
packages/ckeditor5-basic-styles/tests/italicengine.js

@@ -7,7 +7,7 @@ import ItalicEngine from '../src/italicengine';
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import ToggleAttributeCommand from '@ckeditor/ckeditor5-core/src/command/toggleattributecommand';
+import AttributeCommand from '../src/attributecommand';
 
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
@@ -43,7 +43,7 @@ describe( 'ItalicEngine', () => {
 		it( 'should register italic command', () => {
 			const command = editor.commands.get( 'italic' );
 
-			expect( command ).to.be.instanceOf( ToggleAttributeCommand );
+			expect( command ).to.be.instanceOf( AttributeCommand );
 			expect( command ).to.have.property( 'attributeKey', 'italic' );
 		} );
 	} );

+ 1 - 1
packages/ckeditor5-basic-styles/tests/manual/basic-styles.js

@@ -5,7 +5,7 @@
 
 /* globals console:false, window, document */
 
-import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import Enter from '@ckeditor/ckeditor5-enter/src/enter';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';