Procházet zdrojové kódy

Introduced Schema#checkAttributeInSelection.

Piotrek Koszuliński před 8 roky
rodič
revize
5cef1bf318

+ 45 - 0
packages/ckeditor5-engine/src/model/schema.js

@@ -13,6 +13,7 @@ import clone from '@ckeditor/ckeditor5-utils/src/lib/lodash/clone';
 import isArray from '@ckeditor/ckeditor5-utils/src/lib/lodash/isArray';
 import isArray from '@ckeditor/ckeditor5-utils/src/lib/lodash/isArray';
 import isString from '@ckeditor/ckeditor5-utils/src/lib/lodash/isString';
 import isString from '@ckeditor/ckeditor5-utils/src/lib/lodash/isString';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import TreeWalker from '@ckeditor/ckeditor5-engine/src/model/treewalker';
 
 
 /**
 /**
  * Schema is a definition of the structure of the document. It allows to define which tree model items (element, text, etc.)
  * Schema is a definition of the structure of the document. It allows to define which tree model items (element, text, etc.)
@@ -305,6 +306,50 @@ export default class Schema {
 		return chain.some( itemName => itemName == parentItemName );
 		return chain.some( itemName => itemName == parentItemName );
 	}
 	}
 
 
+	/**
+	 * Checks whether the attribute is allowed in selection:
+	 *
+	 * * if the selection is not collapsed, then checks if the attribute is allowed on any of nodes in that range,
+	 * * if the selection is collapsed, then checks if on the selection position there's a text with the
+	 * specified attribute allowed.
+	 *
+	 * @param {module:engine/model/selection~Selection} selection Selection which will be checked.
+	 * @param {String} attribute The name of the attribute to check.
+	 * @returns {Boolean}
+	 */
+	checkAttributeInSelection( selection, attribute ) {
+		if ( selection.isCollapsed ) {
+			// Check whether schema allows for a text with the attribute in the selection.
+			return this.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 the attribute.
+			for ( const 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 TextFragment.
+					const name = step.value.item.name || '$text';
+
+					if ( this.check( { 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;
+	}
+
 	/**
 	/**
 	 * Returns {@link module:engine/model/schema~SchemaItem schema item} that was registered in the schema under given name.
 	 * Returns {@link module:engine/model/schema~SchemaItem schema item} that was registered in the schema under given name.
 	 * If item has not been found, throws error.
 	 * If item has not been found, throws error.

+ 77 - 8
packages/ckeditor5-engine/tests/model/schema/schema.js

@@ -9,6 +9,7 @@ import Element from '../../../src/model/element';
 import Position from '../../../src/model/position';
 import Position from '../../../src/model/position';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import { setData } from '../../../src/dev-utils/model';
 
 
 testUtils.createSinonSandbox();
 testUtils.createSinonSandbox();
 
 
@@ -61,7 +62,7 @@ describe( 'Schema', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'registerItem', () => {
+	describe( 'registerItem()', () => {
 		it( 'should register in schema item under given name', () => {
 		it( 'should register in schema item under given name', () => {
 			schema.registerItem( 'new' );
 			schema.registerItem( 'new' );
 
 
@@ -101,7 +102,7 @@ describe( 'Schema', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'hasItem', () => {
+	describe( 'hasItem()', () => {
 		it( 'should return true if given item name has been registered in schema', () => {
 		it( 'should return true if given item name has been registered in schema', () => {
 			expect( schema.hasItem( '$block' ) ).to.be.true;
 			expect( schema.hasItem( '$block' ) ).to.be.true;
 		} );
 		} );
@@ -111,7 +112,7 @@ describe( 'Schema', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( '_getItem', () => {
+	describe( '_getItem()', () => {
 		it( 'should return SchemaItem registered under given name', () => {
 		it( 'should return SchemaItem registered under given name', () => {
 			schema.registerItem( 'new' );
 			schema.registerItem( 'new' );
 
 
@@ -127,7 +128,7 @@ describe( 'Schema', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'allow', () => {
+	describe( 'allow()', () => {
 		it( 'should add passed query to allowed in schema', () => {
 		it( 'should add passed query to allowed in schema', () => {
 			schema.registerItem( 'p', '$block' );
 			schema.registerItem( 'p', '$block' );
 			schema.registerItem( 'div', '$block' );
 			schema.registerItem( 'div', '$block' );
@@ -140,7 +141,7 @@ describe( 'Schema', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'disallow', () => {
+	describe( 'disallow()', () => {
 		it( 'should add passed query to disallowed in schema', () => {
 		it( 'should add passed query to disallowed in schema', () => {
 			schema.registerItem( 'p', '$block' );
 			schema.registerItem( 'p', '$block' );
 			schema.registerItem( 'div', '$block' );
 			schema.registerItem( 'div', '$block' );
@@ -155,7 +156,7 @@ describe( 'Schema', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'check', () => {
+	describe( 'check()', () => {
 		describe( 'string or array of strings as inside', () => {
 		describe( 'string or array of strings as inside', () => {
 			it( 'should return false if given element is not registered in schema', () => {
 			it( 'should return false if given element is not registered in schema', () => {
 				expect( schema.check( { name: 'new', inside: [ 'div', 'header' ] } ) ).to.be.false;
 				expect( schema.check( { name: 'new', inside: [ 'div', 'header' ] } ) ).to.be.false;
@@ -409,7 +410,7 @@ describe( 'Schema', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'itemExtends', () => {
+	describe( 'itemExtends()', () => {
 		it( 'should return true if given item extends another given item', () => {
 		it( 'should return true if given item extends another given item', () => {
 			schema.registerItem( 'div', '$block' );
 			schema.registerItem( 'div', '$block' );
 			schema.registerItem( 'myDiv', 'div' );
 			schema.registerItem( 'myDiv', 'div' );
@@ -438,7 +439,7 @@ describe( 'Schema', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( '_normalizeQueryPath', () => {
+	describe( '_normalizeQueryPath()', () => {
 		it( 'should normalize string with spaces to an array of strings', () => {
 		it( 'should normalize string with spaces to an array of strings', () => {
 			expect( Schema._normalizeQueryPath( '$root div strong' ) ).to.deep.equal( [ '$root', 'div', 'strong' ] );
 			expect( Schema._normalizeQueryPath( '$root div strong' ) ).to.deep.equal( [ '$root', 'div', 'strong' ] );
 		} );
 		} );
@@ -471,4 +472,72 @@ describe( 'Schema', () => {
 			expect( Schema._normalizeQueryPath( input ) ).to.deep.equal( [ '$root', 'div', 'p', 'strong' ] );
 			expect( Schema._normalizeQueryPath( input ) ).to.deep.equal( [ '$root', 'div', 'p', 'strong' ] );
 		} );
 		} );
 	} );
 	} );
+
+	describe( 'checkAttributeInSelection()', () => {
+		const attribute = 'bold';
+		let doc, schema;
+
+		beforeEach( () => {
+			doc = new Document();
+			doc.createRoot();
+
+			schema = doc.schema;
+
+			schema.registerItem( 'p', '$block' );
+			schema.registerItem( 'h1', '$block' );
+			schema.registerItem( 'img', '$inline' );
+
+			// Bold text is allowed only in P.
+			schema.allow( { name: '$text', attributes: 'bold', inside: 'p' } );
+			schema.allow( { name: 'p', attributes: 'bold', inside: '$root' } );
+
+			// Disallow bold on image.
+			schema.disallow( { name: 'img', attributes: 'bold', inside: '$root' } );
+		} );
+
+		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( schema.checkAttributeInSelection( doc.selection, attribute ) ).to.be.true;
+			} );
+
+			it( 'should return false if characters with the attribute cannot be placed at caret position', () => {
+				setData( doc, '<h1>[]</h1>' );
+				expect( schema.checkAttributeInSelection( doc.selection, attribute ) ).to.be.false;
+
+				setData( doc, '[]' );
+				expect( schema.checkAttributeInSelection( doc.selection, attribute ) ).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.
+				setData( doc, '<p>[foo]</p>' );
+				expect( schema.checkAttributeInSelection( doc.selection, attribute ) ).to.be.true;
+
+				// Selection spans over characters but also include nodes that can't have attribute.
+				setData( doc, '<p>fo[o<img />b]ar</p>' );
+				expect( schema.checkAttributeInSelection( doc.selection, attribute ) ).to.be.true;
+
+				// Selection on whole root content. Characters in P can have an attribute so it's valid.
+				setData( doc, '[<p>foo<img />bar</p><h1></h1>]' );
+				expect( schema.checkAttributeInSelection( doc.selection, attribute ) ).to.be.true;
+
+				// Selection on empty P. P can have the attribute.
+				setData( doc, '[<p></p>]' );
+				expect( schema.checkAttributeInSelection( doc.selection, attribute ) ).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.
+				setData( doc, '[<h1></h1>]' );
+				expect( schema.checkAttributeInSelection( doc.selection, attribute ) ).to.be.false;
+
+				// Selection on two images which can't be bold.
+				setData( doc, '<p>foo[<img /><img />]bar</p>' );
+				expect( schema.checkAttributeInSelection( doc.selection, attribute ) ).to.be.false;
+			} );
+		} );
+	} );
 } );
 } );