Browse Source

Merge branch 'master' into t/ckeditor5-theme-lark/107

Aleksander Nowodzinski 8 years ago
parent
commit
fe107798be

+ 16 - 0
packages/ckeditor5-widget/CHANGELOG.md

@@ -1,6 +1,22 @@
 Changelog
 =========
 
+## [0.2.0](https://github.com/ckeditor/ckeditor5-widget/compare/v0.1.1...v0.2.0) (2017-09-03)
+
+### Bug fixes
+
+* Added initial contenteditable state for editable widget. Closes [#9](https://github.com/ckeditor/ckeditor5-widget/issues/9). ([c6321ff](https://github.com/ckeditor/ckeditor5-widget/commit/c6321ff))
+
+### Features
+
+* <kbd>Ctrl</kbd>+<kbd>A</kbd> in a nested editable should select nested editable's content. Closes [#13](https://github.com/ckeditor/ckeditor5-widget/issues/13). ([35a8aff](https://github.com/ckeditor/ckeditor5-widget/commit/35a8aff))
+
+### Other changes
+
+* Adjusted widget to the editor read-only mode. Closes [#7](https://github.com/ckeditor/ckeditor5-widget/issues/7). ([2726873](https://github.com/ckeditor/ckeditor5-widget/commit/2726873))
+* Introduced highlights support for widgets. Closes [#11](https://github.com/ckeditor/ckeditor5-widget/issues/11). ([0bd3d66](https://github.com/ckeditor/ckeditor5-widget/commit/0bd3d66))
+
+
 ## [0.1.1](https://github.com/ckeditor/ckeditor5-widget/compare/v0.1.0...v0.1.1) (2017-05-07)
 
 Internal changes only (updated dependencies, documentation, etc.).

+ 5 - 5
packages/ckeditor5-widget/package.json

@@ -1,13 +1,13 @@
 {
   "name": "@ckeditor/ckeditor5-widget",
-  "version": "0.1.1",
+  "version": "0.2.0",
   "description": "Widget API for CKEditor 5.",
   "keywords": [],
   "dependencies": {
-    "@ckeditor/ckeditor5-core": "^0.8.1",
-    "@ckeditor/ckeditor5-engine": "^0.10.0",
-    "@ckeditor/ckeditor5-utils": "^0.9.1",
-    "@ckeditor/ckeditor5-theme-lark": "^0.8.0"
+    "@ckeditor/ckeditor5-core": "^0.9.0",
+    "@ckeditor/ckeditor5-engine": "^0.11.0",
+    "@ckeditor/ckeditor5-utils": "^0.10.0",
+    "@ckeditor/ckeditor5-theme-lark": "^0.9.0"
   },
   "devDependencies": {
     "@ckeditor/ckeditor5-dev-lint": "^3.1.0",

+ 72 - 69
packages/ckeditor5-widget/src/highlightstack.js

@@ -16,7 +16,7 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
  * elements. When different highlights are applied to same element correct order should be preserved:
  * * highlight with highest priority should be applied,
  * * if two highlights have same priority - sort by CSS class provided in
- * {@link module:engine/conversion/buildmodelconverter~HighlightDescriptor}.
+ * {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor}.
  * This way, highlight will be applied with the same rules it is applied on texts.
  */
 export default class HighlightStack {
@@ -31,108 +31,111 @@ export default class HighlightStack {
 	 * Adds highlight descriptor to the stack.
 	 *
 	 * @fires change:top
-	 * @param {module:engine/conversion/buildmodelconverter~HighlightDescriptor} descriptor
+	 * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} descriptor
 	 */
 	add( descriptor ) {
 		const stack = this._stack;
-		let i = 0;
 
-		// Find correct place to insert descriptor in the stack.
-		while ( stack[ i ] && shouldABeBeforeB( stack[ i ], descriptor ) ) {
-			i++;
+		// Save top descriptor and insert new one. If top is changed - fire event.
+		const oldTop = stack[ 0 ];
+		this._insertDescriptor( descriptor );
+		const newTop = stack[ 0 ];
+
+		// When new object is at the top and stores different information.
+		if ( oldTop !== newTop && !compareDescriptors( oldTop, newTop ) ) {
+			this.fire( 'change:top', {
+				oldDescriptor: oldTop,
+				newDescriptor: newTop
+			} );
 		}
+	}
 
-		stack.splice( i, 0, descriptor );
-
-		// New element at the stack top.
-		if ( i === 0 ) {
-			const data = {
-				newDescriptor: descriptor
-			};
-
-			// If old descriptor is present it was pushed down the stack.
-			if ( stack[ 1 ] ) {
-				const oldDescriptor = stack[ 1 ];
-
-				// New descriptor on the top is same as previous one - do not fire any event.
-				if ( compareDescriptors( descriptor, oldDescriptor ) ) {
-					return;
-				}
+	/**
+	 * Removes highlight descriptor from the stack.
+	 *
+	 * @fires change:top
+	 * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} descriptor
+	 */
+	remove( descriptor ) {
+		const stack = this._stack;
 
-				data.oldDescriptor = oldDescriptor;
-			}
+		const oldTop = stack[ 0 ];
+		this._removeDescriptor( descriptor );
+		const newTop = stack[ 0 ];
 
-			this.fire( 'change:top', data );
+		// When new object is at the top and stores different information.
+		if ( oldTop !== newTop && !compareDescriptors( oldTop, newTop ) ) {
+			this.fire( 'change:top', {
+				oldDescriptor: oldTop,
+				newDescriptor: newTop
+			} );
 		}
 	}
 
 	/**
-	 * Removes highlight descriptor from the stack.
+	 * Inserts given descriptor in correct place in the stack. It also takes care about updating information when
+	 * descriptor with same id is already present.
 	 *
-	 * @fires change:top
-	 * @param {module:engine/conversion/buildmodelconverter~HighlightDescriptor} descriptor
+	 * @private
+	 * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} descriptor
 	 */
-	remove( descriptor ) {
+	_insertDescriptor( descriptor ) {
 		const stack = this._stack;
-		const length = stack.length;
+		const index = stack.findIndex( item => item.id === descriptor.id );
 
-		if ( length === 0 ) {
+		// Inserting exact same descriptor - do nothing.
+		if ( compareDescriptors( descriptor, stack[ index ] ) ) {
 			return;
 		}
 
+		// If descriptor with same id but with different information is on the stack - remove it.
+		if ( index > -1 ) {
+			stack.splice( index, 1 );
+		}
+
+		// Find correct place to insert descriptor in the stack.
+		// It have different information (for example priority) so it must be re-inserted in correct place.
 		let i = 0;
 
-		while ( stack[ i ] && !compareDescriptors( descriptor, stack[ i ] ) ) {
+		while ( stack[ i ] && shouldABeBeforeB( stack[ i ], descriptor ) ) {
 			i++;
-
-			// Descriptor not found.
-			if ( i >= stack.length ) {
-				return;
-			}
 		}
 
-		stack.splice( i, 1 );
-
-		// Element from stack top was removed - fire `change:top` event with new first element. It might be `undefined`
-		// which informs that no descriptor is currently at the top.
-		if ( i === 0 ) {
-			const data = {
-				oldDescriptor: descriptor
-			};
-
-			if ( stack[ 0 ] ) {
-				const newDescriptor = stack[ 0 ];
-
-				// New descriptor on the top is same as removed one - do not fire any event.
-				if ( compareDescriptors( descriptor, newDescriptor ) ) {
-					return;
-				}
+		stack.splice( i, 0, descriptor );
+	}
 
-				data.newDescriptor = newDescriptor;
-			}
+	/**
+	 * Removes descriptor with given id from the stack.
+	 *
+	 * @private
+	 * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} descriptor
+	 */
+	_removeDescriptor( descriptor ) {
+		const stack = this._stack;
+		const index = stack.findIndex( item => item.id === descriptor.id );
 
-			this.fire( 'change:top', data );
+		// If descriptor with same id is on the list - remove it.
+		if ( index > -1 ) {
+			stack.splice( index, 1 );
 		}
 	}
 }
 
 mix( HighlightStack, EmitterMixin );
 
-// Compares two highlight descriptors by priority and CSS class names. Returns `true` when both descriptors are
-// considered equal.
+// Compares two descriptors by checking their priority and class list.
 //
-// @param {module:engine/conversion/buildmodelconverter~HighlightDescriptor} descriptorA
-// @param {module:engine/conversion/buildmodelconverter~HighlightDescriptor} descriptorB
-// @returns {Boolean}
-function compareDescriptors( descriptorA, descriptorB ) {
-	return descriptorA.priority == descriptorB.priority &&
-		classesToString( descriptorA.class ) == classesToString( descriptorB.class );
+// @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} a
+// @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} b
+// @returns {Boolean} Returns true if both descriptors are defined and have same priority and classes.
+function compareDescriptors( a, b ) {
+	return a && b && a.priority == b.priority && classesToString( a.class ) == classesToString( b.class );
 }
 
 // Checks whenever first descriptor should be placed in the stack before second one.
 //
-// @param {module:engine/conversion/buildmodelconverter~HighlightDescriptor} a
-// @param {module:engine/conversion/buildmodelconverter~HighlightDescriptor} b
+// @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} a
+// @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} b
 // @returns {Boolean}
 function shouldABeBeforeB( a, b ) {
 	if ( a.priority > b.priority ) {
@@ -145,7 +148,7 @@ function shouldABeBeforeB( a, b ) {
 	return classesToString( a.class ) > classesToString( b.class );
 }
 
-// Converts CSS classes passed with {@link module:engine/conversion/buildmodelconverter~HighlightDescriptor} to
+// Converts CSS classes passed with {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor} to
 // sorted string.
 //
 // @param {String|Array<String>} descriptor
@@ -159,8 +162,8 @@ function classesToString( classes ) {
  *
  * @event change:top
  * @param {Object} data Additional information about the change.
- * @param {module:engine/conversion/buildmodelconverter~HighlightDescriptor} [data.newDescriptor] New highlight
+ * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} [data.newDescriptor] New highlight
  * descriptor. It will be `undefined` when last descriptor is removed from the stack.
- * @param {module:engine/conversion/buildmodelconverter~HighlightDescriptor} [data.oldDescriptor] Old highlight
+ * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} [data.oldDescriptor] Old highlight
  * descriptor. It will be `undefined` when first descriptor is added to the stack.
  */

+ 48 - 5
packages/ckeditor5-widget/src/widget.js

@@ -15,10 +15,12 @@ import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
 import ViewEditableElement from '@ckeditor/ckeditor5-engine/src/view/editableelement';
 import RootEditableElement from '@ckeditor/ckeditor5-engine/src/view/rooteditableelement';
 import { isWidget, WIDGET_SELECTED_CLASS_NAME, getLabel } from './utils';
-import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import { keyCodes, getCode, parseKeystroke } from '@ckeditor/ckeditor5-utils/src/keyboard';
 
 import '../theme/theme.scss';
 
+const selectAllKeystrokeCode = parseKeystroke( 'Ctrl+A' );
+
 /**
  * The widget plugin.
  * Registers model to view selection converter for editing pipeline. It is hooked after default selection conversion.
@@ -125,11 +127,19 @@ export default class Widget extends Plugin {
 	_onKeydown( eventInfo, domEventData ) {
 		const keyCode = domEventData.keyCode;
 		const isForward = keyCode == keyCodes.delete || keyCode == keyCodes.arrowdown || keyCode == keyCodes.arrowright;
+		let wasHandled = false;
+
+		// Checks if the keys were handled and then prevents the default event behaviour and stops
+		// the propagation.
+		if ( isDeleteKeyCode( keyCode ) ) {
+			wasHandled = this._handleDelete( isForward );
+		} else if ( isArrowKeyCode( keyCode ) ) {
+			wasHandled = this._handleArrowKeys( isForward );
+		} else if ( isSelectAllKeyCode( domEventData ) ) {
+			wasHandled = this._selectAllNestedEditableContent();
+		}
 
-		// Checks if delete/backspace or arrow keys were handled and then prevents default event behaviour and stops
-		// event propagation.
-		if ( ( isDeleteKeyCode( keyCode ) && this._handleDelete( isForward ) ) ||
-			( isArrowKeyCode( keyCode ) && this._handleArrowKeys( isForward ) ) ) {
+		if ( wasHandled ) {
 			domEventData.preventDefault();
 			eventInfo.stop();
 		}
@@ -214,6 +224,31 @@ export default class Widget extends Plugin {
 	}
 
 	/**
+	 * Extends the {@link module:engine/model/selection~Selection document's selection} to span the entire
+	 * content of the nested editable if already anchored in one.
+	 *
+	 * See: {@link module:engine/model/schema~Schema#getLimitElement}.
+	 *
+	 * @private
+	 */
+	_selectAllNestedEditableContent() {
+		const modelDocument = this.editor.document;
+		const modelSelection = modelDocument.selection;
+		const schema = modelDocument.schema;
+		const limitElement = schema.getLimitElement( modelSelection );
+
+		if ( modelSelection.getFirstRange().root == limitElement ) {
+			return false;
+		}
+
+		modelDocument.enqueueChanges( () => {
+			modelSelection.setIn( limitElement );
+		} );
+
+		return true;
+	}
+
+	/**
 	 * Sets {@link module:engine/model/selection~Selection document's selection} over given element.
 	 *
 	 * @private
@@ -271,6 +306,14 @@ function isDeleteKeyCode( keyCode ) {
 	return keyCode == keyCodes.delete || keyCode == keyCodes.backspace;
 }
 
+// Returns 'true' if provided (DOM) key event data corresponds with the Ctrl+A keystroke.
+//
+// @param {module:engine/view/observer/keyobserver~KeyEventData} domEventData
+// @returns {Boolean}
+function isSelectAllKeyCode( domEventData ) {
+	return getCode( domEventData ) == selectAllKeystrokeCode;
+}
+
 // Returns `true` when element is a nested editable or is placed inside one.
 //
 // @param {module:engine/view/element~Element}

+ 39 - 22
packages/ckeditor5-widget/tests/highlightstack.js

@@ -14,7 +14,7 @@ describe( 'HighlightStack', () => {
 
 	it( 'should fire event when new descriptor is provided to an empty stack', () => {
 		const spy = sinon.spy();
-		const descriptor = { priority: 10, class: 'css-class' };
+		const descriptor = { priority: 10, class: 'css-class', id: 'descriptor-id' };
 
 		stack.on( 'change:top', spy );
 		stack.add( descriptor );
@@ -26,8 +26,8 @@ describe( 'HighlightStack', () => {
 
 	it( 'should fire event when new top element has changed', () => {
 		const spy = sinon.spy();
-		const descriptor = { priority: 10, class: 'css-class' };
-		const secondDescriptor = { priority: 11, class: 'css-class' };
+		const descriptor = { priority: 10, class: 'css-class', id: 'descriptor-1' };
+		const secondDescriptor = { priority: 11, class: 'css-class', id: 'descriptor-2' };
 
 		stack.on( 'change:top', spy );
 		stack.add( descriptor );
@@ -38,10 +38,27 @@ describe( 'HighlightStack', () => {
 		expect( spy.secondCall.args[ 1 ].oldDescriptor ).to.equal( descriptor );
 	} );
 
+	it( 'should fire event when top element has updated', () => {
+		const spy = sinon.spy();
+		const descriptor = { priority: 10, class: 'css-class', id: 'descriptor-1' };
+		const secondDescriptor = { priority: 11, class: 'css-class', id: 'descriptor-1' };
+
+		stack.on( 'change:top', spy );
+		stack.add( descriptor );
+		stack.add( secondDescriptor );
+
+		sinon.assert.calledTwice( spy );
+		expect( spy.firstCall.args[ 1 ].newDescriptor ).to.equal( descriptor );
+		expect( spy.firstCall.args[ 1 ].oldDescriptor ).to.be.undefined;
+
+		expect( spy.secondCall.args[ 1 ].newDescriptor ).to.equal( secondDescriptor );
+		expect( spy.secondCall.args[ 1 ].oldDescriptor ).to.equal( descriptor );
+	} );
+
 	it( 'should not fire event when element with lower priority was added', () => {
 		const spy = sinon.spy();
-		const descriptor = { priority: 10, class: 'css-class' };
-		const secondDescriptor = { priority: 9, class: 'css-class' };
+		const descriptor = { priority: 10, class: 'css-class', id: 'descriptor-1' };
+		const secondDescriptor = { priority: 9, class: 'css-class', id: 'descriptor-2' };
 
 		stack.on( 'change:top', spy );
 		stack.add( descriptor );
@@ -54,8 +71,8 @@ describe( 'HighlightStack', () => {
 
 	it( 'should fire event when top element was removed', () => {
 		const spy = sinon.spy();
-		const descriptor = { priority: 10, class: 'css-class' };
-		const secondDescriptor = { priority: 11, class: 'css-class' };
+		const descriptor = { priority: 10, class: 'css-class', id: 'descriptor-1' };
+		const secondDescriptor = { priority: 11, class: 'css-class', id: 'descriptor-2' };
 
 		stack.add( descriptor );
 		stack.add( secondDescriptor );
@@ -71,8 +88,8 @@ describe( 'HighlightStack', () => {
 
 	it( 'should not fire event when other than top element is removed', () => {
 		const spy = sinon.spy();
-		const descriptor = { priority: 10, class: 'css-class' };
-		const secondDescriptor = { priority: 11, class: 'css-class' };
+		const descriptor = { priority: 10, class: 'css-class', id: 'descriptor-1' };
+		const secondDescriptor = { priority: 11, class: 'css-class', id: 'descriptor-2' };
 
 		stack.add( descriptor );
 		stack.add( secondDescriptor );
@@ -86,8 +103,8 @@ describe( 'HighlightStack', () => {
 
 	it( 'should not fire event when same descriptor is added', () => {
 		const spy = sinon.spy();
-		const descriptor = { priority: 10, class: 'css-class' };
-		const secondDescriptor = { priority: 10, class: 'css-class' };
+		const descriptor = { priority: 10, class: 'css-class', id: 'descriptor-1' };
+		const secondDescriptor = { priority: 10, class: 'css-class', id: 'descriptor-1' };
 
 		stack.on( 'change:top', spy );
 		stack.add( descriptor );
@@ -110,8 +127,8 @@ describe( 'HighlightStack', () => {
 
 	it( 'should not fire when trying to remove descriptor which is not present', () => {
 		const spy = sinon.spy();
-		const descriptor = { priority: 10, class: 'css-class' };
-		const secondDescriptor = { priority: 12, class: 'css-class' };
+		const descriptor = { priority: 10, class: 'css-class', id: 'descriptor-1' };
+		const secondDescriptor = { priority: 12, class: 'css-class', id: 'descriptor-2' };
 
 		stack.add( descriptor );
 		stack.on( 'change:top', spy );
@@ -122,7 +139,7 @@ describe( 'HighlightStack', () => {
 
 	it( 'should fire event when last element from stack was removed', () => {
 		const spy = sinon.spy();
-		const descriptor = { priority: 10, class: 'css-class' };
+		const descriptor = { priority: 10, class: 'css-class', id: 'descriptor' };
 
 		stack.add( descriptor );
 		stack.on( 'change:top', spy );
@@ -135,8 +152,8 @@ describe( 'HighlightStack', () => {
 
 	it( 'should not fire event when new top descriptor is same as previous', () => {
 		const spy = sinon.spy();
-		const descriptor = { priority: 10, class: 'css-class' };
-		const secondDescriptor = { priority: 10, class: 'css-class' };
+		const descriptor = { priority: 10, class: 'css-class', id: 'descriptor-1' };
+		const secondDescriptor = { priority: 10, class: 'css-class', id: 'descriptor-2' };
 
 		stack.add( descriptor );
 		stack.add( secondDescriptor );
@@ -148,9 +165,9 @@ describe( 'HighlightStack', () => {
 
 	it( 'should sort by class when priorities are the same', () => {
 		const spy = sinon.spy();
-		const descriptorA = { priority: 10, class: 'css-a' };
-		const descriptorB = { priority: 10, class: 'css-b' };
-		const descriptorC = { priority: 10, class: 'css-c' };
+		const descriptorA = { priority: 10, class: 'css-a', id: 'descriptor-1' };
+		const descriptorB = { priority: 10, class: 'css-b', id: 'descriptor-2' };
+		const descriptorC = { priority: 10, class: 'css-c', id: 'descriptor-3' };
 
 		stack.on( 'change:top', spy );
 		stack.add( descriptorB );
@@ -167,9 +184,9 @@ describe( 'HighlightStack', () => {
 
 	it( 'should sort by class when priorities are the same - array of CSS classes', () => {
 		const spy = sinon.spy();
-		const descriptorA = { priority: 10, class: [ 'css-a', 'css-z' ] };
-		const descriptorB = { priority: 10, class: [ 'css-a', 'css-y' ] };
-		const descriptorC = { priority: 10, class: 'css-c' };
+		const descriptorA = { priority: 10, class: [ 'css-a', 'css-z' ], id: 'descriptor-1' };
+		const descriptorB = { priority: 10, class: [ 'css-a', 'css-y' ], id: 'descriptor-2' };
+		const descriptorC = { priority: 10, class: 'css-c', id: 'descriptor-3' };
 
 		stack.on( 'change:top', spy );
 		stack.add( descriptorB );

+ 15 - 15
packages/ckeditor5-widget/tests/utils.js

@@ -59,10 +59,10 @@ describe( 'widget utils', () => {
 			expect( typeof set ).to.equal( 'function' );
 			expect( typeof remove ).to.equal( 'function' );
 
-			set( element, { priority: 1, class: 'highlight' } );
+			set( element, { priority: 1, class: 'highlight', id: 'highlight' } );
 			expect( element.hasClass( 'highlight' ) ).to.be.true;
 
-			remove( element, { priority: 1, class: 'highlight' } );
+			remove( element, { priority: 1, class: 'highlight', id: 'highlight' } );
 			expect( element.hasClass( 'highlight' ) ).to.be.false;
 		} );
 
@@ -75,11 +75,11 @@ describe( 'widget utils', () => {
 			expect( typeof set ).to.equal( 'function' );
 			expect( typeof remove ).to.equal( 'function' );
 
-			set( element, { priority: 1, class: [ 'highlight', 'foo' ] } );
+			set( element, { priority: 1, class: [ 'highlight', 'foo' ], id: 'highlight' } );
 			expect( element.hasClass( 'highlight' ) ).to.be.true;
 			expect( element.hasClass( 'foo' ) ).to.be.true;
 
-			remove( element, { priority: 1, class: [ 'foo', 'highlight' ] } );
+			remove( element, { priority: 1, class: [ 'foo', 'highlight' ], id: 'highlight' } );
 			expect( element.hasClass( 'highlight' ) ).to.be.false;
 			expect( element.hasClass( 'foo' ) ).to.be.false;
 		} );
@@ -174,7 +174,7 @@ describe( 'widget utils', () => {
 		} );
 
 		it( 'should call highlight methods when descriptor is added and removed', () => {
-			const descriptor = { priority: 10, class: 'highlight' };
+			const descriptor = { priority: 10, class: 'highlight', id: 'highlight' };
 
 			set( element, descriptor );
 			remove( element, descriptor );
@@ -187,8 +187,8 @@ describe( 'widget utils', () => {
 		} );
 
 		it( 'should call highlight methods when next descriptor is added', () => {
-			const descriptor = { priority: 10, class: 'highlight' };
-			const secondDescriptor = { priority: 11, class: 'highlight' };
+			const descriptor = { priority: 10, class: 'highlight', id: 'highlight-1' };
+			const secondDescriptor = { priority: 11, class: 'highlight', id: 'highlight-2' };
 
 			set( element, descriptor );
 			set( element, secondDescriptor );
@@ -199,8 +199,8 @@ describe( 'widget utils', () => {
 		} );
 
 		it( 'should not call highlight methods when descriptor with lower priority is added', () => {
-			const descriptor = { priority: 10, class: 'highlight' };
-			const secondDescriptor = { priority: 9, class: 'highlight' };
+			const descriptor = { priority: 10, class: 'highlight', id: 'highlight-1' };
+			const secondDescriptor = { priority: 9, class: 'highlight', id: 'highlight-2' };
 
 			set( element, descriptor );
 			set( element, secondDescriptor );
@@ -210,8 +210,8 @@ describe( 'widget utils', () => {
 		} );
 
 		it( 'should call highlight methods when descriptor is removed changing active descriptor', () => {
-			const descriptor = { priority: 10, class: 'highlight' };
-			const secondDescriptor = { priority: 11, class: 'highlight' };
+			const descriptor = { priority: 10, class: 'highlight', id: 'highlight-1' };
+			const secondDescriptor = { priority: 11, class: 'highlight', id: 'highlight-2' };
 
 			set( element, descriptor );
 			set( element, secondDescriptor );
@@ -228,8 +228,8 @@ describe( 'widget utils', () => {
 		} );
 
 		it( 'should call highlight methods when descriptor is removed not changing active descriptor', () => {
-			const descriptor = { priority: 10, class: 'highlight' };
-			const secondDescriptor = { priority: 9, class: 'highlight' };
+			const descriptor = { priority: 10, class: 'highlight', id: 'highlight-1' };
+			const secondDescriptor = { priority: 9, class: 'highlight', id: 'highlight-2' };
 
 			set( element, descriptor );
 			set( element, secondDescriptor );
@@ -242,8 +242,8 @@ describe( 'widget utils', () => {
 		} );
 
 		it( 'should call highlight methods - CSS class array', () => {
-			const descriptor = { priority: 10, class: [ 'highlight', 'a' ] };
-			const secondDescriptor = { priority: 10, class: [ 'highlight', 'b' ] };
+			const descriptor = { priority: 10, class: [ 'highlight', 'a' ], id: 'highlight-1' };
+			const secondDescriptor = { priority: 10, class: [ 'highlight', 'b' ], id: 'highlight-2' };
 
 			set( element, descriptor );
 			set( element, secondDescriptor );

+ 17 - 0
packages/ckeditor5-widget/tests/widget.js

@@ -34,6 +34,7 @@ describe( 'Widget', () => {
 				doc.schema.registerItem( 'inline', '$inline' );
 				doc.schema.objects.add( 'inline' );
 				doc.schema.registerItem( 'nested' );
+				doc.schema.limits.add( 'nested' );
 				doc.schema.allow( { name: '$inline', inside: 'nested' } );
 				doc.schema.allow( { name: 'nested', inside: 'widget' } );
 				doc.schema.registerItem( 'editable' );
@@ -744,6 +745,22 @@ describe( 'Widget', () => {
 			);
 		} );
 
+		describe( 'Ctrl+A', () => {
+			test(
+				'should select the entire content of the nested editable',
+				'<widget><nested>foo[]</nested></widget><paragraph>bar</paragraph>',
+				{ keyCode: keyCodes.a, ctrlKey: true },
+				'<widget><nested>[foo]</nested></widget><paragraph>bar</paragraph>'
+			);
+
+			test(
+				'should not change the selection if outside of the nested editable',
+				'<widget><nested>foo</nested></widget><paragraph>[]bar</paragraph>',
+				{ keyCode: keyCodes.a, ctrlKey: true },
+				'<widget><nested>foo</nested></widget><paragraph>[]bar</paragraph>'
+			);
+		} );
+
 		function test( name, data, keyCodeOrMock, expected ) {
 			it( name, () => {
 				const domEventDataMock = ( typeof keyCodeOrMock == 'object' ) ? keyCodeOrMock : {