浏览代码

Updated core.AttributeCommand to use proper core.Schema API. Added treeModel.Document#schema.

Szymon Cofalik 9 年之前
父节点
当前提交
b4d7649458

+ 43 - 31
packages/ckeditor5-engine/src/attributecommand.js

@@ -6,7 +6,7 @@
 'use strict';
 
 import Command from './command.js';
-import Position from './treemodel/position.js';
+import TreeWalker from './treemodel/treewalker.js';
 
 /**
  * An extension of basic {@link core.Command} class, which provides utilities typical of commands that sets an
@@ -27,50 +27,62 @@ export default class AttributeCommand extends Command {
 	}
 
 	checkSchema() {
-		let position = this.editor.document.selection.anchor;
+		const selection = this.editor.document.selection;
+		const schema = this.editor.document.schema;
 
-		if ( position ) {
-			return this.editor.document.schema.checkAtPosition( { attribute: this.attributeKey }, position );
+		if ( selection.isCollapsed ) {
+			return schema.checkAtPosition( { name: 'inline', attribute: this.attributeKey }, selection.getFirstPosition() );
+		} else if ( selection.hasAnyRange ) {
+			const ranges = selection.getRanges();
+
+			for ( let range of ranges ) {
+				const walker = new TreeWalker( { boundaries: range } );
+				let step = walker.next();
+
+				while ( !step.done ) {
+					const query = {
+						name: step.value.item.name || 'inline',
+						attribute: this.attributeKey
+					};
+
+					if ( schema.checkAtPosition( query, walker.position ) ) {
+						return true;
+					}
+
+					step = walker.next();
+				}
+			}
 		}
 
-		return true;
+		return false;
 	}
 
 	execute( param ) {
-		let document = this.editor.document;
-		let selection = document.selection;
-		let value = ( param === undefined ) ? !this.value : param;
-
-		if ( selection.isCollapsed ) {
-			// If selection is collapsed we might face one of two scenarios:
-			// - selection is at the beginning of an element - we change that element's attribute, or
-			// - selection is not at the beginning of an element - we change only selection attribute.
+		if ( this.isEnabled ) {
+			let document = this.editor.document;
+			let selection = document.selection;
+			let value = ( param === undefined ) ? !this.value : param;
 
-			let position = Position.createFromPosition( selection.anchor );
-
-			if ( position.parent.getChildCount() === 0 ) {
-				document.enqueueChanges( () => {
-					document.batch().setAttr( this.attributeKey, value || null, position.parent );
-				} );
-			} else {
+			if ( selection.isCollapsed ) {
+				// If selection is collapsed change only selection attribute.
 				if ( value ) {
 					selection.setAttribute( this.attributeKey, true );
 				} else {
 					selection.removeAttribute( this.attributeKey );
 				}
-			}
-		} else if ( selection.hasAnyRange ) {
-			// If selection is not collapsed and has ranges, we change attribute on those ranges.
-			document.enqueueChanges( () => {
-				let ranges = selection.getRanges();
+			} else if ( selection.hasAnyRange ) {
+				// If selection is not collapsed and has ranges, we change attribute on those ranges.
+				document.enqueueChanges( () => {
+					let ranges = selection.getRanges();
 
-				// Keep it as one undo step.
-				let batch = document.batch();
+					// Keep it as one undo step.
+					let batch = document.batch();
 
-				for ( let range of ranges ) {
-					batch.setAttr( this.attributeKey, value || null, range );
-				}
-			} );
+					for ( let range of ranges ) {
+						batch.setAttr( this.attributeKey, value || null, range );
+					}
+				} );
+			}
 		}
 	}
 }

+ 3 - 0
packages/ckeditor5-engine/src/treemodel/document.js

@@ -17,6 +17,7 @@ import EmitterMixin from '../emittermixin.js';
 import CKEditorError from '../ckeditorerror.js';
 import utils from '../utils.js';
 import CharacterProxy from './characterproxy.js';
+import Schema from '../schema.js';
 
 const graveyardSymbol = Symbol( 'graveyard' );
 
@@ -83,6 +84,8 @@ export default class Document {
 		this.on( 'changesDone', () => {
 			this._setSelectionAttributes();
 		} );
+
+		this.schema = new Schema();
 	}
 
 	/**

+ 103 - 12
packages/ckeditor5-engine/tests/attributecommand.js

@@ -93,7 +93,7 @@ describe( 'execute', () => {
 		expect( root.getChild( 4 ).hasAttribute( attrKey ) ).to.be.false;
 	} );
 
-	it( 'should change selection attribute if selection is collapsed and is not at the beginning of node', () => {
+	it( 'should change selection attribute if selection is collapsed', () => {
 		modelDoc.selection.addRange( new Range( new Position( root, [ 1 ] ), new Position( root, [ 1 ] ) ) );
 
 		expect( command.value ).to.be.false;
@@ -103,6 +103,16 @@ describe( 'execute', () => {
 		expect( command.value ).to.be.true;
 		expect( modelDoc.selection.hasAttribute( 'bold' ) ).to.be.true;
 
+		command.execute();
+
+		expect( command.value ).to.be.false;
+		expect( modelDoc.selection.hasAttribute( 'bold' ) ).to.be.false;
+	} );
+
+	it( 'should not save that attribute was changed on selection when selection changes', () => {
+		modelDoc.selection.addRange( new Range( new Position( root, [ 1 ] ), new Position( root, [ 1 ] ) ) );
+		command.execute();
+
 		// It should not save that bold was executed at position ( root, [ 1 ] ).
 
 		// Simulate clicking right arrow key by changing selection ranges.
@@ -114,25 +124,106 @@ describe( 'execute', () => {
 		expect( command.value ).to.be.false;
 	} );
 
-	it( 'should change selection parent element attribute if selection is collapsed and in empty node', () => {
-		root.insertChildren( 0, new Element( 'p' ) );
-		modelDoc.selection.addRange( new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 0 ] ) ) );
+	it( 'should not throw and do nothing if selection has no ranges', () => {
+		let spy = sinon.spy();
+		modelDoc.on( 'change', spy );
+
+		modelDoc.selection.removeAllRanges();
+		command.execute();
+
+		expect( spy.called ).to.be.false;
+		expect( Array.from( modelDoc.selection.getAttributes() ) ).to.deep.equal( [ ] );
+	} );
+
+	it( 'should do nothing if command is disabled', () => {
+		let spy = sinon.spy();
+		modelDoc.on( 'change', spy );
+
+		modelDoc.selection.addRange( new Range( new Position( root, [ 1 ] ), new Position( root, [ 1 ] ) ) );
+		command.checkEnabled();
 
 		expect( command.value ).to.be.false;
+		expect( command.isEnabled ).to.be.false;
 
 		command.execute();
 
-		expect( command.value ).to.be.true;
-		expect( root.getChild( 0 ).hasAttribute( 'bold' ) ).to.be.true;
+		expect( spy.called ).to.be.false;
+		expect( Array.from( modelDoc.selection.getAttributes() ) ).to.deep.equal( [ ] );
+	} );
+} );
 
-		// It should save that bold was set on selection's parent node.
+describe( 'checkSchema', () => {
+	beforeEach( () => {
+		modelDoc.schema.registerItem( 'p', 'block' );
+		modelDoc.schema.registerItem( 'div', 'block' );
+		modelDoc.schema.registerItem( 'img', 'inline' );
+
+		// Bold text is allowed only in P.
+		modelDoc.schema.allow( { name: 'inline', attribute: 'bold', inside: 'p' } );
+		modelDoc.schema.allow( { name: 'p', attribute: 'bold', inside: 'root' } );
+
+		// Disallow bold on image.
+		modelDoc.schema.disallow( { name: 'img', attribute: 'bold', inside: 'root' } );
+
+		root.insertChildren( 0, [
+			new Element( 'p', [], [
+				'foo',
+				new Element( 'img' ),
+				new Element( 'img' ),
+				'bar'
+			] ),
+			new Element( 'div' ),
+			new Element( 'p' )
+		] );
+	} );
 
-		// Simulate clicking right arrow key by changing selection ranges.
-		modelDoc.selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) ] );
+	describe( 'when selection is collapsed', () => {
+		it( 'should return true if characters with the attribute can be placed at caret position', () => {
+			modelDoc.selection.setRanges( [ new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 0, 1 ] ) ) ] );
+			expect( command.checkSchema() ).to.be.true;
+		} );
 
-		// Get back to previous selection.
-		modelDoc.selection.setRanges( [ new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 0 ] ) ) ] );
+		it( 'should return false if characters with the attribute cannot be placed at caret position', () => {
+			modelDoc.selection.setRanges( [ new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 1, 0 ] ) ) ] );
+			expect( command.checkSchema() ).to.be.false;
 
-		expect( command.value ).to.be.true;
+			modelDoc.selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) ] );
+			expect( command.checkSchema() ).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', () => {
+			// Simple selection on a few characters.
+			modelDoc.selection.setRanges( [ new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 3 ] ) ) ] );
+			expect( command.checkSchema() ).to.be.true;
+
+			// Selection spans over characters but also include nodes that can't have attribute.
+			modelDoc.selection.setRanges( [ new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 0, 6 ] ) ) ] );
+			expect( command.checkSchema() ).to.be.true;
+
+			// Selection on whole root content. Characters in P can have an attribute so it's valid.
+			modelDoc.selection.setRanges( [ new Range( new Position( root, [ 0 ] ), new Position( root, [ 3 ] ) ) ] );
+			expect( command.checkSchema() ).to.be.true;
+
+			// Selection on empty P. P can have the attribute.
+			modelDoc.selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 3 ] ) ) ] );
+			expect( command.checkSchema() ).to.be.true;
+		} );
+
+		it( 'should return false if there are no nodes in selection that can have the attribute', () => {
+			// Selection on DIV which can't have bold text.
+			modelDoc.selection.setRanges( [ new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) ) ] );
+			expect( command.checkSchema() ).to.be.false;
+
+			// Selection on two images which can't be bolded.
+			modelDoc.selection.setRanges( [ new Range( new Position( root, [ 0, 3 ] ), new Position( root, [ 0, 5 ] ) ) ] );
+			expect( command.checkSchema() ).to.be.false;
+		} );
+	} );
+
+	it( 'should return false if selection has no ranges', () => {
+		modelDoc.selection.removeAllRanges();
+		expect( command.checkSchema() ).to.be.false;
 	} );
 } );