浏览代码

Renamed and simplified AttributeCommand.

Oskar Wrobel 9 年之前
父节点
当前提交
98c2042aa8

+ 50 - 0
packages/ckeditor5-core/src/command/helpers/getschemavalidranges.js

@@ -0,0 +1,50 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import TreeWalker from '../../../engine/model/treewalker.js';
+import Range from '../../../engine/model/range.js';
+
+/**
+ * Walks through given array of ranges and removes parts of them that are not allowed by passed schema to have the
+ * attribute set. This is done by breaking a range in two and omitting the not allowed part.
+ *
+ * @param {String} attribute Attribute key.
+ * @param {Array.<engine.model.Range>} ranges Ranges to be validated.
+ * @param {engine.model.Schema} schema Document schema.
+ * @returns {Array.<engine.model.Range>} Ranges without invalid parts.
+ */
+export default function getSchemaValidRanges( attribute, ranges, schema ) {
+	const validRanges = [];
+
+	for ( let range of ranges ) {
+		const walker = new TreeWalker( { boundaries: range, mergeCharacters: true } );
+		let step = walker.next();
+
+		let last = range.start;
+		let from = range.start;
+		let to = range.end;
+
+		while ( !step.done ) {
+			const name = step.value.item.name || '$text';
+
+			if ( !schema.check( { name: name, inside: last, attributes: attribute } ) ) {
+				if ( !from.isEqual( last ) ) {
+					validRanges.push( new Range( from, last ) );
+				}
+
+				from = walker.position;
+			}
+
+			last = walker.position;
+			step = walker.next();
+		}
+
+		if ( from && !from.isEqual( to ) ) {
+			validRanges.push( new Range( from, to ) );
+		}
+	}
+
+	return validRanges;
+}

+ 49 - 0
packages/ckeditor5-core/src/command/helpers/isattributeallowedinselection.js

@@ -0,0 +1,49 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import TreeWalker from '../../../engine/model/treewalker.js';
+
+/**
+ * Checks {@link engine.model.Document#schema} if attribute is allowed in selection:
+ * * if selection is on range, the command is enabled if any of nodes in that range can have bold,
+ * * if selection is collapsed, the command is enabled if text with bold is allowed in that node.
+ *
+ * @param {String} attribute Attribute key.
+ * @param {engine.model.Selection} selection Selection which ranges will be validate.
+ * @param {engine.model.Schema} schema Document schema.
+ * @returns {Boolean}
+ */
+export default function isAttributeAllowedInSelection( attribute, selection, schema ) {
+	if ( selection.isCollapsed ) {
+		// Check whether schema allows for a test with `attributeKey` in caret position.
+		return schema.check( { name: '$text', inside: selection.getFirstPosition(), attributes: attribute } );
+	} else {
+		const ranges = selection.getRanges();
+
+		// For all ranges, check nodes in them until you find a node that is allowed to have `attributeKey` attribute.
+		for ( let range of ranges ) {
+			const walker = new TreeWalker( { boundaries: range, mergeCharacters: true } );
+			let last = walker.position;
+			let step = walker.next();
+
+			// Walk the range.
+			while ( !step.done ) {
+				// If returned item does not have name property, it is a model.TextFragment.
+				const name = step.value.item.name || '$text';
+
+				if ( schema.check( { name: name, inside: last, attributes: attribute } ) ) {
+					// If we found a node that is allowed to have the attribute, return true.
+					return true;
+				}
+
+				last = walker.position;
+				step = walker.next();
+			}
+		}
+	}
+
+	// If we haven't found such node, return false.
+	return false;
+}

+ 12 - 86
packages/ckeditor5-core/src/command/attributecommand.js

@@ -4,13 +4,13 @@
  */
 
 import Command from './command.js';
-import TreeWalker from '../../engine/model/treewalker.js';
-import Range from '../../engine/model/range.js';
+import getSchemaValidRanges from './helpers/getschemavalidranges.js';
+import isAttributeAllowedInSelection from './helpers/isattributeallowedinselection.js';
 
 /**
- * An extension of basic {@link core.command.Command} class, which provides utilities for a command that sets a single
- * attribute on a text or element with value `true`. AttributeCommand uses {@link engine.model.Document#selection} to
- * decide which nodes (if any) should be changed, and applies or removes attributes from them.
+ * An extension of basic {@link core.command.Command} class, which provides utilities for a command that toggle a single
+ * attribute on a text or element with value `true`. ToggleAttributeCommand uses {@link engine.model.Document#selection}
+ * to decide which nodes (if any) should be changed, and applies or removes attributes from them.
  * See {@link engine.view.Converter#execute} for more.
  *
  * The command checks {@link engine.model.Document#schema} to decide if it should be enabled.
@@ -18,7 +18,7 @@ import Range from '../../engine/model/range.js';
  *
  * @memberOf core.command
  */
-export default class AttributeCommand extends Command {
+export default class ToggleAttributeCommand extends Command {
 	/**
 	 * @see core.command.Command
 	 * @param {core.editor.Editor} editor
@@ -30,7 +30,7 @@ export default class AttributeCommand extends Command {
 		/**
 		 * Attribute that will be set by the command.
 		 *
-		 * @member {String} core.command.AttributeCommand#attributeKey
+		 * @member {String} core.command.ToggleAttributeCommand#attributeKey
 		 */
 		this.attributeKey = attributeKey;
 
@@ -39,7 +39,7 @@ export default class AttributeCommand extends Command {
 		 * the command's attribute set. For range selection it means that all nodes inside have the attribute applied.
 		 *
 		 * @observable
-		 * @member {Boolean} core.command.AttributeCommand#value
+		 * @member {Boolean} core.command.ToggleAttributeCommand#value
 		 */
 		this.set( 'value', false );
 
@@ -49,47 +49,15 @@ export default class AttributeCommand extends Command {
 	}
 
 	/**
-	 * Checks {@link engine.model.Document#schema} to decide if the command should be enabled:
-	 * * if selection is on range, the command is enabled if any of nodes in that range can have bold,
-	 * * if selection is collapsed, the command is enabled if text with bold is allowed in that node.
+	 * Checks if {@link engine.model.Document#schema} allows to create attribute in {@link engine.model.Document#selection}
 	 *
 	 * @private
 	 * @returns {Boolean}
 	 */
 	_checkEnabled() {
-		const selection = this.editor.document.selection;
-		const schema = this.editor.document.schema;
-
-		if ( selection.isCollapsed ) {
-			// Check whether schema allows for a test with `attributeKey` in caret position.
-			return schema.check( { name: '$text', inside: selection.getFirstPosition(), attributes: this.attributeKey } );
-		} else {
-			const ranges = selection.getRanges();
-
-			// For all ranges, check nodes in them until you find a node that is allowed to have `attributeKey` attribute.
-			for ( let range of ranges ) {
-				const walker = new TreeWalker( { boundaries: range, mergeCharacters: true } );
-				let last = walker.position;
-				let step = walker.next();
-
-				// Walk the range.
-				while ( !step.done ) {
-					// If returned item does not have name property, it is a model.TextFragment.
-					const name = step.value.item.name || '$text';
-
-					if ( schema.check( { name: name, inside: last, attributes: this.attributeKey } ) ) {
-						// If we found a node that is allowed to have the attribute, return true.
-						return true;
-					}
-
-					last = walker.position;
-					step = walker.next();
-				}
-			}
-		}
+		const document = this.editor.document;
 
-		// If we haven't found such node, return false.
-		return false;
+		return isAttributeAllowedInSelection( this.attributeKey, document.selection, document.schema );
 	}
 
 	/**
@@ -125,7 +93,7 @@ export default class AttributeCommand extends Command {
 		} else {
 			// If selection has non-collapsed ranges, we change attribute on nodes inside those ranges.
 			document.enqueueChanges( () => {
-				const ranges = this._getSchemaValidRanges( selection.getRanges() );
+				const ranges = getSchemaValidRanges( this.attributeKey, selection.getRanges(), document.schema );
 
 				// Keep it as one undo step.
 				const batch = document.batch();
@@ -140,46 +108,4 @@ export default class AttributeCommand extends Command {
 			} );
 		}
 	}
-
-	/**
-	 * Walks through given array of ranges and removes parts of them that are not allowed by schema to have the
-	 * attribute set. This is done by breaking a range in two and omitting the not allowed part.
-	 *
-	 * @private
-	 * @param {Array.<engine.model.Range>} ranges Ranges to be validated.
-	 * @returns {Array.<engine.model.Range>} Ranges without invalid parts.
-	 */
-	_getSchemaValidRanges( ranges ) {
-		const validRanges = [];
-
-		for ( let range of ranges ) {
-			const walker = new TreeWalker( { boundaries: range, mergeCharacters: true } );
-			let step = walker.next();
-
-			let last = range.start;
-			let from = range.start;
-			let to = range.end;
-
-			while ( !step.done ) {
-				const name = step.value.item.name || '$text';
-
-				if ( !this.editor.document.schema.check( { name: name, inside: last, attributes: this.attributeKey } ) ) {
-					if ( !from.isEqual( last ) ) {
-						validRanges.push( new Range( from, last ) );
-					}
-
-					from = walker.position;
-				}
-
-				last = walker.position;
-				step = walker.next();
-			}
-
-			if ( from && !from.isEqual( to ) ) {
-				validRanges.push( new Range( from, to ) );
-			}
-		}
-
-		return validRanges;
-	}
 }

+ 59 - 0
packages/ckeditor5-core/tests/command/helpers/getschemavalidranges.js

@@ -0,0 +1,59 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Document from '/ckeditor5/engine/model/document.js';
+import Range from '/ckeditor5/engine/model/range.js';
+import Text from '/ckeditor5/engine/model/text.js';
+import Element from '/ckeditor5/engine/model/element.js';
+import getSchemaValidRanges from '/ckeditor5/core/command/helpers/getschemavalidranges.js';
+
+const attribute = 'bold';
+let document, root, schema;
+
+beforeEach( () => {
+	document = new Document();
+	schema = document.schema;
+	root = document.createRoot();
+
+	schema.registerItem( 'p', '$block' );
+	schema.registerItem( 'h1', '$block' );
+	schema.registerItem( 'img', '$inline' );
+
+	// Allow bold on p
+	schema.allow( { name: '$text', attributes: 'bold', inside: 'p' } );
+	schema.allow( { name: 'p', attributes: 'bold', inside: '$root' } );
+
+	root.insertChildren( 0, [
+		new Element( 'p', [], [
+			new Text( 'foo' ),
+			new Element( 'img' ),
+			new Element( 'img' ),
+			new Text( 'bar' )
+		] )
+	] );
+} );
+
+it( 'should return unmodified ranges when attribute is allowed on each element', () => {
+	// Allow bold on img
+	schema.allow( { name: 'img', attributes: 'bold', inside: 'p' } );
+
+	const ranges = [ Range.createOn( root.getChild( 0 ) ) ];
+
+	expect( getSchemaValidRanges( attribute, ranges, schema ) ).to.deep.equal( ranges );
+} );
+
+it( 'should split range into two ranges and omit disallowed elements', () => {
+	// Disallow bold on img.
+	document.schema.disallow( { name: 'img', attributes: 'bold', inside: 'p' } );
+
+	const ranges = [ Range.createOn( root.getChild( 0 ) ) ];
+	const result = getSchemaValidRanges( attribute, ranges, schema );
+
+	expect( result ).to.length( 2 );
+	expect( result[ 0 ].start.path ).to.members( [ 0 ] );
+	expect( result[ 0 ].end.path ).to.members( [ 0, 3 ] );
+	expect( result[ 1 ].start.path ) .to.members( [ 0, 5 ] );
+	expect( result[ 1 ].end.path ).to.members( [ 1 ] );
+} );

+ 86 - 0
packages/ckeditor5-core/tests/command/helpers/isattributeallowedinselection.js

@@ -0,0 +1,86 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Document from '/ckeditor5/engine/model/document.js';
+import Range from '/ckeditor5/engine/model/range.js';
+import Position from '/ckeditor5/engine/model/position.js';
+import Text from '/ckeditor5/engine/model/text.js';
+import Element from '/ckeditor5/engine/model/element.js';
+import isAttributeAllowedInSelection from '/ckeditor5/core/command/helpers/isattributeallowedinselection.js';
+
+const attribute = 'bold';
+let document, root;
+
+beforeEach( () => {
+	document = new Document();
+	root = document.createRoot();
+
+	document.schema.registerItem( 'p', '$block' );
+	document.schema.registerItem( 'h1', '$block' );
+	document.schema.registerItem( 'img', '$inline' );
+
+	// Bold text is allowed only in P.
+	document.schema.allow( { name: '$text', attributes: 'bold', inside: 'p' } );
+	document.schema.allow( { name: 'p', attributes: 'bold', inside: '$root' } );
+
+	// Disallow bold on image.
+	document.schema.disallow( { name: 'img', attributes: 'bold', inside: '$root' } );
+
+	root.insertChildren( 0, [
+		new Element( 'p', [], [
+			new Text( 'foo' ),
+			new Element( 'img' ),
+			new Element( 'img' ),
+			new Text( 'bar' )
+		] ),
+		new Element( 'h1' ),
+		new Element( 'p' )
+	] );
+} );
+
+describe( 'when selection is collapsed', () => {
+	it( 'should return true if characters with the attribute can be placed at caret position', () => {
+		document.selection.setRanges( [ new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 0, 1 ] ) ) ] );
+		expect( isAttributeAllowedInSelection( attribute, document.selection, document.schema ) ).to.be.true;
+	} );
+
+	it( 'should return false if characters with the attribute cannot be placed at caret position', () => {
+		document.selection.setRanges( [ new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 1, 0 ] ) ) ] );
+		expect( isAttributeAllowedInSelection( attribute, document.selection, document.schema ) ).to.be.false;
+
+		document.selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) ] );
+		expect( isAttributeAllowedInSelection( attribute, document.selection, document.schema ) ).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.
+		document.selection.setRanges( [ new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 3 ] ) ) ] );
+		expect( isAttributeAllowedInSelection( attribute, document.selection, document.schema ) ).to.be.true;
+
+		// Selection spans over characters but also include nodes that can't have attribute.
+		document.selection.setRanges( [ new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 0, 6 ] ) ) ] );
+		expect( isAttributeAllowedInSelection( attribute, document.selection, document.schema ) ).to.be.true;
+
+		// Selection on whole root content. Characters in P can have an attribute so it's valid.
+		document.selection.setRanges( [ new Range( new Position( root, [ 0 ] ), new Position( root, [ 3 ] ) ) ] );
+		expect( isAttributeAllowedInSelection( attribute, document.selection, document.schema ) ).to.be.true;
+
+		// Selection on empty P. P can have the attribute.
+		document.selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 3 ] ) ) ] );
+		expect( isAttributeAllowedInSelection( attribute, document.selection, document.schema ) ).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.
+		document.selection.setRanges( [ new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) ) ] );
+		expect( isAttributeAllowedInSelection( attribute, document.selection, document.schema ) ).to.be.false;
+
+		// Selection on two images which can't be bold.
+		document.selection.setRanges( [ new Range( new Position( root, [ 0, 3 ] ), new Position( root, [ 0, 5 ] ) ) ] );
+		expect( isAttributeAllowedInSelection( attribute, document.selection, document.schema ) ).to.be.false;
+	} );
+} );

+ 2 - 2
packages/ckeditor5-core/tests/command/attributecommand.js

@@ -5,7 +5,7 @@
 
 import Editor from '/ckeditor5/core/editor/editor.js';
 import Document from '/ckeditor5/engine/model/document.js';
-import AttributeCommand from '/ckeditor5/core/command/attributecommand.js';
+import ToggleAttributeCommand from '/ckeditor5/core/command/toggleattributecommand.js';
 import Text from '/ckeditor5/engine/model/text.js';
 import Range from '/ckeditor5/engine/model/range.js';
 import Position from '/ckeditor5/engine/model/position.js';
@@ -24,7 +24,7 @@ beforeEach( () => {
 	modelDoc = editor.document;
 	root = modelDoc.createRoot();
 
-	command = new AttributeCommand( editor, attrKey );
+	command = new ToggleAttributeCommand( editor, attrKey );
 
 	modelDoc.schema.registerItem( 'p', '$block' );
 	modelDoc.schema.registerItem( 'h1', '$block' );