瀏覽代碼

Merge branch 'master' into ckeditor5/t/1214

Piotrek Koszuliński 7 年之前
父節點
當前提交
fad3913913

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

@@ -1,6 +1,17 @@
 Changelog
 =========
 
+## [10.3.1](https://github.com/ckeditor/ckeditor5-widget/compare/v10.3.0...v10.3.1) (2018-12-05)
+
+### Bug fixes
+
+* Selection converter will mark only the topmost widget in case of selecting a widget with another widget nested inside it. Closes [#57](https://github.com/ckeditor/ckeditor5-widget/issues/57). ([a78efec](https://github.com/ckeditor/ckeditor5-widget/commit/a78efec))
+
+### Other changes
+
+* Improved SVG icons size. See [ckeditor/ckeditor5-theme-lark#206](https://github.com/ckeditor/ckeditor5-theme-lark/issues/206). ([5b7a457](https://github.com/ckeditor/ckeditor5-widget/commit/5b7a457))
+
+
 ## [10.3.0](https://github.com/ckeditor/ckeditor5-widget/compare/v10.2.0...v10.3.0) (2018-10-08)
 
 ### Features

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

@@ -1,6 +1,6 @@
 {
   "name": "@ckeditor/ckeditor5-widget",
-  "version": "10.3.0",
+  "version": "10.3.1",
   "description": "Widget API for CKEditor 5.",
   "keywords": [
     "ckeditor",
@@ -9,21 +9,21 @@
     "ckeditor5-lib"
   ],
   "dependencies": {
-    "@ckeditor/ckeditor5-core": "^11.0.1",
-    "@ckeditor/ckeditor5-engine": "^11.0.0",
-    "@ckeditor/ckeditor5-theme-lark": "^11.1.0",
-    "@ckeditor/ckeditor5-ui": "^11.1.0",
-    "@ckeditor/ckeditor5-utils": "^11.0.0"
+    "@ckeditor/ckeditor5-core": "^11.1.0",
+    "@ckeditor/ckeditor5-engine": "^12.0.0",
+    "@ckeditor/ckeditor5-theme-lark": "^12.0.0",
+    "@ckeditor/ckeditor5-ui": "^11.2.0",
+    "@ckeditor/ckeditor5-utils": "^11.1.0"
   },
   "devDependencies": {
-    "@ckeditor/ckeditor5-basic-styles": "^10.0.3",
-    "@ckeditor/ckeditor5-editor-balloon": "^11.0.1",
-    "@ckeditor/ckeditor5-editor-classic": "^11.0.1",
-    "@ckeditor/ckeditor5-essentials": "^10.1.2",
-    "@ckeditor/ckeditor5-paragraph": "^10.0.3",
-    "@ckeditor/ckeditor5-typing": "^11.0.1",
+    "@ckeditor/ckeditor5-basic-styles": "^10.1.0",
+    "@ckeditor/ckeditor5-editor-balloon": "^11.0.2",
+    "@ckeditor/ckeditor5-editor-classic": "^11.0.2",
+    "@ckeditor/ckeditor5-essentials": "^10.1.3",
+    "@ckeditor/ckeditor5-paragraph": "^10.0.4",
+    "@ckeditor/ckeditor5-typing": "^11.0.2",
     "eslint": "^5.5.0",
-    "eslint-config-ckeditor5": "^1.0.7",
+    "eslint-config-ckeditor5": "^1.0.9",
     "husky": "^0.14.3",
     "lint-staged": "^7.0.0"
   },

+ 10 - 10
packages/ckeditor5-widget/src/highlightstack.js

@@ -17,7 +17,7 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
  *
  * * highlight with highest priority should be applied,
  * * if two highlights have same priority - sort by CSS class provided in
- * {@link module:engine/conversion/downcast-converters~HighlightDescriptor}.
+ * {@link module:engine/conversion/downcasthelpers~HighlightDescriptor}.
  *
  * This way, highlight will be applied with the same rules it is applied on texts.
  */
@@ -33,7 +33,7 @@ export default class HighlightStack {
 	 * Adds highlight descriptor to the stack.
 	 *
 	 * @fires change:top
-	 * @param {module:engine/conversion/downcast-converters~HighlightDescriptor} descriptor
+	 * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} descriptor
 	 * @param {module:engine/view/downcastwriter~DowncastWriter} writer
 	 */
 	add( descriptor, writer ) {
@@ -83,7 +83,7 @@ export default class HighlightStack {
 	 * descriptor with same id is already present.
 	 *
 	 * @private
-	 * @param {module:engine/conversion/downcast-converters~HighlightDescriptor} descriptor
+	 * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} descriptor
 	 */
 	_insertDescriptor( descriptor ) {
 		const stack = this._stack;
@@ -131,8 +131,8 @@ mix( HighlightStack, EmitterMixin );
 
 // Compares two descriptors by checking their priority and class list.
 //
-// @param {module:engine/conversion/downcast-converters~HighlightDescriptor} a
-// @param {module:engine/conversion/downcast-converters~HighlightDescriptor} b
+// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} a
+// @param {module:engine/conversion/downcasthelpers~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.classes ) == classesToString( b.classes );
@@ -140,8 +140,8 @@ function compareDescriptors( a, b ) {
 
 // Checks whenever first descriptor should be placed in the stack before second one.
 //
-// @param {module:engine/conversion/downcast-converters~HighlightDescriptor} a
-// @param {module:engine/conversion/downcast-converters~HighlightDescriptor} b
+// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} a
+// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} b
 // @returns {Boolean}
 function shouldABeBeforeB( a, b ) {
 	if ( a.priority > b.priority ) {
@@ -154,7 +154,7 @@ function shouldABeBeforeB( a, b ) {
 	return classesToString( a.classes ) > classesToString( b.classes );
 }
 
-// Converts CSS classes passed with {@link module:engine/conversion/downcast-converters~HighlightDescriptor} to
+// Converts CSS classes passed with {@link module:engine/conversion/downcasthelpers~HighlightDescriptor} to
 // sorted string.
 //
 // @param {String|Array<String>} descriptor
@@ -168,9 +168,9 @@ function classesToString( classes ) {
  *
  * @event change:top
  * @param {Object} data Additional information about the change.
- * @param {module:engine/conversion/downcast-converters~HighlightDescriptor} [data.newDescriptor] New highlight
+ * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} [data.newDescriptor] New highlight
  * descriptor. It will be `undefined` when last descriptor is removed from the stack.
- * @param {module:engine/conversion/downcast-converters~HighlightDescriptor} [data.oldDescriptor] Old highlight
+ * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} [data.oldDescriptor] Old highlight
  * descriptor. It will be `undefined` when first descriptor is added to the stack.
  * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that can be used to modify element.
  */

+ 23 - 16
packages/ckeditor5-widget/src/utils.js

@@ -31,15 +31,20 @@ export const WIDGET_CLASS_NAME = 'ck-widget';
 export const WIDGET_SELECTED_CLASS_NAME = 'ck-widget_selected';
 
 /**
- * Returns `true` if given {@link module:engine/view/element~Element} is a widget.
+ * Returns `true` if given {@link module:engine/view/node~Node} is an {@link module:engine/view/element~Element} and a widget.
  *
- * @param {module:engine/view/element~Element} element
+ * @param {module:engine/view/node~Node} node
  * @returns {Boolean}
  */
-export function isWidget( element ) {
-	return !!element.getCustomProperty( widgetSymbol );
+export function isWidget( node ) {
+	if ( !node.is( 'element' ) ) {
+		return false;
+	}
+
+	return !!node.getCustomProperty( widgetSymbol );
 }
 
+/* eslint-disable max-len */
 /**
  * Converts the given {@link module:engine/view/element~Element} to a widget in the following way:
  *
@@ -49,30 +54,31 @@ export function isWidget( element ) {
  * * adds a custom property allowing to recognize widget elements by using {@link ~isWidget `isWidget()`},
  * * implements the {@link ~setHighlightHandling view highlight on widgets}.
  *
- * This function needs to be used in conjuction with {@link module:engine/conversion/downcast-converters downcast converters}
- * like {@link module:engine/conversion/downcast-converters~downcastElementToElement `downcastElementToElement()`}.
+ * This function needs to be used in conjunction with
+ * {@link module:engine/conversion/downcasthelpers~DowncastHelpers downcast conversion helpers}
+ * like {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`}.
  * Moreover, typically you will want to use `toWidget()` only for `editingDowncast`, while keeping the `dataDowncast` clean.
  *
  * For example, in order to convert a `<widget>` model element to `<div class="widget">` in the view, you can define
  * such converters:
  *
  *		editor.conversion.for( 'editingDowncast' )
- *			.add( downcastElementToElement( {
+ *			.elementToElement( {
  *				model: 'widget',
  *				view: ( modelItem, writer ) => {
  *					const div = writer.createContainerElement( 'div', { class: 'widget' } );
  *
  *					return toWidget( div, writer, { label: 'some widget' } );
  *				}
- *			} ) );
+ *			} );
  *
  *		editor.conversion.for( 'dataDowncast' )
- *			.add( downcastElementToElement( {
+ *			.elementToElement( {
  *				model: 'widget',
  *				view: ( modelItem, writer ) => {
  *					return writer.createContainerElement( 'div', { class: 'widget' } );
  *				}
- *			} ) );
+ *			} );
  *
  * See the full source code of the widget (with a nested editable) schema definition and converters in
  * [this sample](https://github.com/ckeditor/ckeditor5-widget/blob/master/tests/manual/widget-with-nestededitable.js).
@@ -85,8 +91,9 @@ export function isWidget( element ) {
  * @param {Boolean} [options.hasSelectionHandler=false] If `true`, the widget will have a selection handler added.
  * @returns {module:engine/view/element~Element} Returns the same element.
  */
+/* eslint-enable max-len */
 export function toWidget( element, writer, options = {} ) {
-	// The selection on Edge behaves better when the whole editor contents is in a single contentedible element.
+	// The selection on Edge behaves better when the whole editor contents is in a single contenteditable element.
 	// https://github.com/ckeditor/ckeditor5/issues/1079
 	if ( !env.isEdge ) {
 		writer.setAttribute( 'contenteditable', 'false', element );
@@ -183,28 +190,28 @@ export function getLabel( element ) {
  * * adds the `ck-editor__nested-editable_focused` CSS class when the editable is focused and removes it when it is blurred.
  *
  * Similarly to {@link ~toWidget `toWidget()`} this function should be used in `dataDowncast` only and it is usually
- * used together with {@link module:engine/conversion/downcast-converters~downcastElementToElement `downcastElementToElement()`}.
+ * used together with {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`}.
  *
  * For example, in order to convert a `<nested>` model element to `<div class="nested">` in the view, you can define
  * such converters:
  *
  *		editor.conversion.for( 'editingDowncast' )
- *			.add( downcastElementToElement( {
+ *			.elementToElement( {
  *				model: 'nested',
  *				view: ( modelItem, writer ) => {
  *					const div = writer.createEditableElement( 'div', { class: 'nested' } );
  *
  *					return toWidgetEditable( nested, writer );
  *				}
- *			} ) );
+ *			} );
  *
  *		editor.conversion.for( 'dataDowncast' )
- *			.add( downcastElementToElement( {
+ *			.elementToElement( {
  *				model: 'nested',
  *				view: ( modelItem, writer ) => {
  *					return writer.createContainerElement( 'div', { class: 'nested' } );
  *				}
- *			} ) );
+ *			} );
  *
  * See the full source code of the widget (with nested editable) schema definition and converters in
  * [this sample](https://github.com/ckeditor/ckeditor5-widget/blob/master/tests/manual/widget-with-nestededitable.js).

+ 26 - 4
packages/ckeditor5-widget/src/widget.js

@@ -9,8 +9,8 @@
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobserver';
-import { isWidget, WIDGET_SELECTED_CLASS_NAME, getLabel } from './utils';
-import { keyCodes, getCode, parseKeystroke } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import { getLabel, isWidget, WIDGET_SELECTED_CLASS_NAME } from './utils';
+import { getCode, keyCodes, parseKeystroke } from '@ckeditor/ckeditor5-utils/src/keyboard';
 
 import '../theme/widget.css';
 
@@ -63,14 +63,18 @@ export default class Widget extends Plugin {
 			const viewWriter = conversionApi.writer;
 			const viewSelection = viewWriter.document.selection;
 			const selectedElement = viewSelection.getSelectedElement();
+			let lastMarked = null;
 
 			for ( const range of viewSelection.getRanges() ) {
 				for ( const value of range ) {
 					const node = value.item;
 
-					if ( node.is( 'element' ) && isWidget( node ) ) {
+					// Do not mark nested widgets in selected one. See: #57.
+					if ( isWidget( node ) && !isChild( node, lastMarked ) ) {
 						viewWriter.addClass( WIDGET_SELECTED_CLASS_NAME, node );
+
 						this._previouslySelected.add( node );
+						lastMarked = node;
 
 						// Check if widget is a single element selected.
 						if ( node == selectedElement ) {
@@ -411,12 +415,30 @@ function isSelectAllKeyCode( domEventData ) {
 // @returns {Boolean}
 function isInsideNestedEditable( element ) {
 	while ( element ) {
-		if ( !!element && element.is( 'editableElement' ) && !element.is( 'rootElement' ) ) {
+		if ( element.is( 'editableElement' ) && !element.is( 'rootElement' ) ) {
 			return true;
 		}
 
+		// Click on nested widget should select it.
+		if ( isWidget( element ) ) {
+			return false;
+		}
+
 		element = element.parent;
 	}
 
 	return false;
 }
+
+// Checks whether the specified `element` is a child of the `parent` element.
+//
+// @param {module:engine/view/element~Element} element An element to check.
+// @param {module:engine/view/element~Element|null} parent A parent for the element.
+// @returns {Boolean}
+function isChild( element, parent ) {
+	if ( !parent ) {
+		return false;
+	}
+
+	return Array.from( element.getAncestors() ).includes( parent );
+}

+ 3 - 2
packages/ckeditor5-widget/src/widgettoolbarrepository.js

@@ -58,10 +58,11 @@ export default class WidgetToolbarRepository extends Plugin {
 	 */
 	init() {
 		const editor = this.editor;
-		const balloonToolbar = editor.plugins.get( 'BalloonToolbar' );
 
 		// Disables the default balloon toolbar for all widgets.
-		if ( balloonToolbar ) {
+		if ( editor.plugins.has( 'BalloonToolbar' ) ) {
+			const balloonToolbar = editor.plugins.get( 'BalloonToolbar' );
+
 			this.listenTo( balloonToolbar, 'show', evt => {
 				if ( isWidgetSelected( editor.editing.view.document.selection ) ) {
 					evt.stop();

+ 23 - 0
packages/ckeditor5-widget/tests/manual/nested-widgets.html

@@ -0,0 +1,23 @@
+<head>
+	<style>
+		body {
+			max-width: 800px;
+			margin: 20px auto;
+		}
+
+		.ck-content .widget {
+			background: rgba( 0, 0, 0, 0.1 );
+			padding: 20px !important;
+			min-height: 50px;
+		}
+	</style>
+</head>
+<div id="editor">
+	<h2>Heading 1</h2>
+	<p>Paragraph</p>
+	<div class="widget">
+		<div class="widget"></div>
+		<div class="widget"></div>
+	</div>
+	<p>Paragraph</p>
+</div>

+ 76 - 0
packages/ckeditor5-widget/tests/manual/nested-widgets.js

@@ -0,0 +1,76 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals console, window, document, setInterval */
+
+import Widget from '../../src/widget';
+import { toWidget } from '../../src/utils';
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+
+function MyPlugin( editor ) {
+	editor.model.schema.register( 'div', {
+		allowIn: [ '$root', 'div' ],
+		isObject: true
+	} );
+
+	editor.model.schema.extend( '$text', {
+		allowIn: 'div'
+	} );
+
+	editor.conversion.for( 'downcast' ).elementToElement( {
+		model: 'div',
+		view: ( modelElement, writer ) => {
+			return toWidget( writer.createContainerElement( 'div', { class: 'widget' } ), writer );
+		}
+	} );
+
+	editor.conversion.for( 'upcast' ).elementToElement( {
+		model: 'div',
+		view: 'div'
+	} );
+}
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ ArticlePluginSet, Widget, MyPlugin ],
+		toolbar: [
+			'heading',
+			'|',
+			'bold',
+			'italic',
+			'link',
+			'bulletedList',
+			'numberedList',
+			'blockQuote',
+			'insertTable',
+			'mediaEmbed',
+			'undo',
+			'redo'
+		],
+		image: {
+			toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
+		},
+		table: {
+			contentToolbar: [
+				'tableColumn',
+				'tableRow',
+				'mergeTableCells'
+			]
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+
+		setInterval( () => {
+			console.log( getModelData( editor.model ) );
+			console.log( getViewData( editor.editing.view ) );
+		}, 3000 );
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 3 - 0
packages/ckeditor5-widget/tests/manual/nested-widgets.md

@@ -0,0 +1,3 @@
+# Nested widgets
+
+* When selecting the most top-outer widget, nested widgets should not be selected.

+ 12 - 15
packages/ckeditor5-widget/tests/manual/widget-with-nestededitable.js

@@ -13,9 +13,6 @@ import Widget from '../../src/widget';
 
 import { toWidget, toWidgetEditable } from '../../src/utils';
 
-import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
-import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
-
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 		plugins: [ Essentials, Paragraph, Bold, Widget ],
@@ -41,52 +38,52 @@ ClassicEditor
 		} );
 
 		editor.conversion.for( 'dataDowncast' )
-			.add( downcastElementToElement( {
+			.elementToElement( {
 				model: 'widget',
 				view: ( modelItem, writer ) => {
 					return writer.createContainerElement( 'div', { class: 'widget' } );
 				}
-			} ) )
-			.add( downcastElementToElement( {
+			} )
+			.elementToElement( {
 				model: 'nested',
 				view: ( modelItem, writer ) => {
 					return writer.createContainerElement( 'div', { class: 'nested' } );
 				}
-			} ) );
+			} );
 
 		editor.conversion.for( 'editingDowncast' )
-			.add( downcastElementToElement( {
+			.elementToElement( {
 				model: 'widget',
 				view: ( modelItem, writer ) => {
 					const div = writer.createContainerElement( 'div', { class: 'widget' } );
 
 					return toWidget( div, writer, { label: 'widget label' } );
 				}
-			} ) )
-			.add( downcastElementToElement( {
+			} )
+			.elementToElement( {
 				model: 'nested',
 				view: ( modelItem, writer ) => {
 					const nested = writer.createEditableElement( 'div', { class: 'nested' } );
 
 					return toWidgetEditable( nested, writer );
 				}
-			} ) );
+			} );
 
 		editor.conversion.for( 'upcast' )
-			.add( upcastElementToElement( {
+			.elementToElement( {
 				view: {
 					name: 'div',
 					class: 'widget'
 				},
 				model: 'widget'
-			} ) )
-			.add( upcastElementToElement( {
+			} )
+			.elementToElement( {
 				view: {
 					name: 'div',
 					class: 'nested'
 				},
 				model: 'nested'
-			} ) );
+			} );
 
 		editor.setData(
 			'<p>foobar</p>' +

+ 5 - 0
packages/ckeditor5-widget/tests/utils.js

@@ -6,6 +6,7 @@
 /* global document */
 
 import DowncastWriter from '@ckeditor/ckeditor5-engine/src/view/downcastwriter';
+import Text from '@ckeditor/ckeditor5-engine/src/view/text';
 import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element';
 import ViewEditableElement from '@ckeditor/ckeditor5-engine/src/view/editableelement';
 import ViewDocument from '@ckeditor/ckeditor5-engine/src/view/document';
@@ -143,6 +144,10 @@ describe( 'widget utils', () => {
 		it( 'should return false for non-widgetized elements', () => {
 			expect( isWidget( new ViewElement( 'p' ) ) ).to.be.false;
 		} );
+
+		it( 'should return false for text node', () => {
+			expect( isWidget( new Text( 'p' ) ) ).to.be.false;
+		} );
 	} );
 
 	describe( 'label utils', () => {

+ 182 - 18
packages/ckeditor5-widget/tests/widget.js

@@ -7,7 +7,6 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtest
 import Widget from '../src/widget';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
 import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobserver';
-import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
 import { toWidget } from '../src/utils';
 import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
 import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
@@ -76,12 +75,12 @@ describe( 'Widget', () => {
 				} );
 
 				editor.conversion.for( 'downcast' )
-					.add( downcastElementToElement( { model: 'paragraph', view: 'p' } ) )
-					.add( downcastElementToElement( { model: 'inline', view: 'figure' } ) )
-					.add( downcastElementToElement( { model: 'image', view: 'img' } ) )
-					.add( downcastElementToElement( { model: 'blockQuote', view: 'blockquote' } ) )
-					.add( downcastElementToElement( { model: 'div', view: 'div' } ) )
-					.add( downcastElementToElement( {
+					.elementToElement( { model: 'paragraph', view: 'p' } )
+					.elementToElement( { model: 'inline', view: 'figure' } )
+					.elementToElement( { model: 'image', view: 'img' } )
+					.elementToElement( { model: 'blockQuote', view: 'blockquote' } )
+					.elementToElement( { model: 'div', view: 'div' } )
+					.elementToElement( {
 						model: 'widget',
 						view: ( modelItem, viewWriter ) => {
 							const b = viewWriter.createAttributeElement( 'b' );
@@ -90,15 +89,15 @@ describe( 'Widget', () => {
 
 							return toWidget( div, viewWriter, { label: 'element label' } );
 						}
-					} ) )
-					.add( downcastElementToElement( {
+					} )
+					.elementToElement( {
 						model: 'nested',
 						view: ( modelItem, viewWriter ) => viewWriter.createEditableElement( 'figcaption', { contenteditable: true } )
-					} ) )
-					.add( downcastElementToElement( {
+					} )
+					.elementToElement( {
 						model: 'editable',
 						view: ( modelItem, viewWriter ) => viewWriter.createEditableElement( 'figcaption', { contenteditable: true } )
-					} ) );
+					} );
 			} );
 	} );
 
@@ -201,13 +200,13 @@ describe( 'Widget', () => {
 		expect( viewDocument.selection.fakeSelectionLabel ).to.equal( 'element label' );
 	} );
 
-	it( 'should add selected class when no only a widget is selected', () => {
+	it( 'should add selected class when other content is selected with widget', () => {
 		setModelData( model, '[<paragraph>foo</paragraph><widget></widget><widget></widget>]' );
 
 		expect( viewDocument.selection.isFake ).to.be.false;
 		expect( getViewData( view ) ).to.equal(
-			'[' +
-			'<p>foo</p>' +
+
+			'<p>{foo</p>' +
 			'<div class="ck-widget ck-widget_selected" contenteditable="false"><b></b></div>' +
 			'<div class="ck-widget ck-widget_selected" contenteditable="false"><b></b></div>' +
 			']'
@@ -1177,22 +1176,34 @@ describe( 'Widget', () => {
 
 					model.schema.register( 'widget', {
 						inheritAllFrom: '$block',
+						allowIn: 'widget',
 						isObject: true
 					} );
 					model.schema.register( 'paragraph', {
 						inheritAllFrom: '$block'
 					} );
+					model.schema.register( 'nested', {
+						allowIn: 'widget',
+						isLimit: true
+					} );
+					model.schema.extend( '$text', {
+						allowIn: 'nested'
+					} );
 
 					editor.conversion.for( 'downcast' )
-						.add( downcastElementToElement( { model: 'paragraph', view: 'p' } ) )
-						.add( downcastElementToElement( {
+						.elementToElement( { model: 'paragraph', view: 'p' } )
+						.elementToElement( {
 							model: 'widget',
 							view: ( modelItem, viewWriter ) => {
 								const widget = viewWriter.createContainerElement( 'div' );
 
 								return toWidget( widget, viewWriter, { hasSelectionHandler: true } );
 							}
-						} ) );
+						} )
+						.elementToElement( {
+							model: 'nested',
+							view: ( modelItem, viewWriter ) => viewWriter.createEditableElement( 'figcaption', { contenteditable: true } )
+						} );
 				} );
 		} );
 
@@ -1210,5 +1221,158 @@ describe( 'Widget', () => {
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>bar</paragraph>[<widget></widget>]<paragraph>foo</paragraph>' );
 		} );
+
+		it( 'should select the most top-outer widget if widgets are nested', () => {
+			setModelData( model, '<widget><widget></widget><widget></widget></widget>' );
+
+			// The top-outer widget.
+			const viewWidgetSelectionHandler = viewDocument.getRoot().getChild( 0 );
+
+			const domEventDataMock = {
+				target: viewWidgetSelectionHandler,
+				preventDefault: sinon.spy()
+			};
+
+			viewDocument.fire( 'mousedown', domEventDataMock );
+
+			expect( getViewData( view ) ).to.equal(
+				'[<div class="ck-widget ck-widget_selectable ck-widget_selected" contenteditable="false">' +
+					'<div class="ck-widget ck-widget_selectable" contenteditable="false">' +
+						'<div class="ck ck-widget__selection-handler"></div>' +
+					'</div>' +
+					'<div class="ck-widget ck-widget_selectable" contenteditable="false">' +
+						'<div class="ck ck-widget__selection-handler"></div>' +
+					'</div>' +
+					'<div class="ck ck-widget__selection-handler"></div>' +
+				'</div>]'
+			);
+		} );
+
+		it( 'should select a proper widget if they are nested and multiplied', () => {
+			setModelData( model,
+				'<widget></widget>' +
+				'<widget>' +
+					'<widget></widget>' +
+					'<widget></widget>' +
+				'</widget>' +
+				'<widget></widget>'
+			);
+
+			const viewWidgetSelectionHandler = viewDocument.getRoot().getChild( 1 );
+
+			const domEventDataMock = {
+				target: viewWidgetSelectionHandler,
+				preventDefault: sinon.spy()
+			};
+
+			viewDocument.fire( 'mousedown', domEventDataMock );
+
+			expect( getViewData( view ) ).to.equal(
+				'<div class="ck-widget ck-widget_selectable" contenteditable="false">' +
+					'<div class="ck ck-widget__selection-handler"></div>' +
+				'</div>' +
+				'[<div class="ck-widget ck-widget_selectable ck-widget_selected" contenteditable="false">' +
+					'<div class="ck-widget ck-widget_selectable" contenteditable="false">' +
+						'<div class="ck ck-widget__selection-handler"></div>' +
+					'</div>' +
+					'<div class="ck-widget ck-widget_selectable" contenteditable="false">' +
+						'<div class="ck ck-widget__selection-handler"></div>' +
+					'</div>' +
+					'<div class="ck ck-widget__selection-handler"></div>' +
+				'</div>]' +
+				'<div class="ck-widget ck-widget_selectable" contenteditable="false">' +
+					'<div class="ck ck-widget__selection-handler"></div>' +
+				'</div>'
+			);
+		} );
+
+		it( 'works fine with a widget that contains more children', () => {
+			setModelData( model,
+				'<widget>' +
+					'<nested>foo bar</nested>' +
+					'<widget></widget>' +
+				'</widget>'
+			);
+
+			const viewWidgetSelectionHandler = viewDocument.getRoot().getChild( 0 );
+
+			const domEventDataMock = {
+				target: viewWidgetSelectionHandler,
+				preventDefault: sinon.spy()
+			};
+
+			viewDocument.fire( 'mousedown', domEventDataMock );
+
+			expect( getViewData( view ) ).to.equal(
+				'[<div class="ck-widget ck-widget_selectable ck-widget_selected" contenteditable="false">' +
+					'<figcaption contenteditable="true">foo bar</figcaption>' +
+					'<div class="ck-widget ck-widget_selectable" contenteditable="false">' +
+						'<div class="ck ck-widget__selection-handler"></div>' +
+					'</div>' +
+					'<div class="ck ck-widget__selection-handler"></div>' +
+				'</div>]'
+			);
+		} );
+
+		it( 'should select a proper widget for more complex structures', () => {
+			setModelData( model,
+				'<widget>' +
+					'<widget></widget>' +
+					'<widget>' +
+						'<widget></widget>' +
+					'</widget>' +
+				'</widget>'
+			);
+
+			const viewWidgetSelectionHandler = viewDocument.getRoot().getChild( 0 ).getChild( 1 );
+
+			const domEventDataMock = {
+				target: viewWidgetSelectionHandler,
+				preventDefault: sinon.spy()
+			};
+
+			viewDocument.fire( 'mousedown', domEventDataMock );
+
+			expect( getViewData( view ) ).to.equal(
+				'<div class="ck-widget ck-widget_selectable" contenteditable="false">' +
+					'<div class="ck-widget ck-widget_selectable" contenteditable="false">' +
+						'<div class="ck ck-widget__selection-handler"></div>' +
+					'</div>' +
+					'[<div class="ck-widget ck-widget_selectable ck-widget_selected" contenteditable="false">' +
+						'<div class="ck-widget ck-widget_selectable" contenteditable="false">' +
+							'<div class="ck ck-widget__selection-handler"></div>' +
+						'</div>' +
+						'<div class="ck ck-widget__selection-handler"></div>' +
+					'</div>]' +
+					'<div class="ck ck-widget__selection-handler"></div>' +
+				'</div>'
+			);
+		} );
+
+		it( 'should select widget in editable', () => {
+			model.schema.extend( 'widget', { allowIn: 'nested' } );
+
+			setModelData( model, '[]<widget><nested><widget></widget></nested></widget>' );
+
+			const widgetInEditable = viewDocument.getRoot().getChild( 0 ).getChild( 0 ).getChild( 0 );
+
+			const domEventDataMock = {
+				target: widgetInEditable,
+				preventDefault: sinon.spy()
+			};
+
+			viewDocument.fire( 'mousedown', domEventDataMock );
+
+			expect( getViewData( view ) ).to.equal(
+				'<div class="ck-widget ck-widget_selectable" contenteditable="false">' +
+					'<figcaption contenteditable="true">' +
+						'[<div class="ck-widget ck-widget_selectable ck-widget_selected" contenteditable="false">' +
+							'<div class="ck ck-widget__selection-handler"></div>' +
+						'</div>]' +
+					'</figcaption>' +
+					'<div class="ck ck-widget__selection-handler"></div>' +
+				'</div>'
+			);
+		} );
 	} );
 } );

+ 17 - 8
packages/ckeditor5-widget/tests/widgettoolbarrepository.js

@@ -13,8 +13,6 @@ import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
 import Widget from '../src/widget';
 import WidgetToolbarRepository from '../src/widgettoolbarrepository';
 import { isWidget, toWidget } from '../src/utils';
-import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
-import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import View from '@ckeditor/ckeditor5-ui/src/view';
 
@@ -56,6 +54,17 @@ describe( 'WidgetToolbarRepository', () => {
 		expect( editor.plugins.get( WidgetToolbarRepository ) ).to.be.instanceOf( WidgetToolbarRepository );
 	} );
 
+	it( 'should work if balloon toolbar is not available', () => {
+		editorElement.remove();
+		editor.destroy();
+
+		editorElement = document.createElement( 'div' );
+		document.body.appendChild( editorElement );
+
+		expect( editor.plugins.has( 'BalloonToolbar' ) ).to.be.false;
+		expect( editor.plugins.has( WidgetToolbarRepository ) ).to.be.true;
+	} );
+
 	describe( 'register()', () => {
 		it( 'should create a widget toolbar and add it to the collection', () => {
 			widgetToolbarRepository.register( 'fake', {
@@ -315,14 +324,14 @@ class FakeWidget extends Plugin {
 
 		const conversion = editor.conversion;
 
-		conversion.for( 'dataDowncast' ).add( downcastElementToElement( {
+		conversion.for( 'dataDowncast' ).elementToElement( {
 			model: 'fake-widget',
 			view: ( modelElement, viewWriter ) => {
 				return viewWriter.createContainerElement( 'div' );
 			}
-		} ) );
+		} );
 
-		conversion.for( 'editingDowncast' ).add( downcastElementToElement( {
+		conversion.for( 'editingDowncast' ).elementToElement( {
 			model: 'fake-widget',
 			view: ( modelElement, viewWriter ) => {
 				const fakeWidget = viewWriter.createContainerElement( 'div' );
@@ -330,15 +339,15 @@ class FakeWidget extends Plugin {
 
 				return toWidget( fakeWidget, viewWriter, { label: 'fake-widget' } );
 			}
-		} ) );
+		} );
 
-		conversion.for( 'upcast' ).add( upcastElementToElement( {
+		conversion.for( 'upcast' ).elementToElement( {
 			view: {
 				name: 'div'
 			},
 			model: ( view, modelWriter ) => {
 				return modelWriter.createElement( 'fake-widget' );
 			}
-		} ) );
+		} );
 	}
 }

+ 1 - 1
packages/ckeditor5-widget/theme/icons/drag-handler.svg

@@ -1 +1 @@
-<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g><path d="M4 0v1H1v3H0V.5A.5.5 0 0 1 .5 0H4zm8 0h3.5a.5.5 0 0 1 .5.5V4h-1V1h-3V0zM4 16H.5a.5.5 0 0 1-.5-.5V12h1v3h3v1zm8 0v-1h3v-3h1v3.5a.5.5 0 0 1-.5.5H12z"/><path fill-opacity=".256" d="M1 1h14v14H1z"/><g class="ck-icon__selected-indicator"><path d="M7 0h2v1H7V0zM0 7h1v2H0V7zm15 0h1v2h-1V7zm-8 8h2v1H7v-1z"/><path fill-opacity=".254" d="M1 1h14v14H1z"/></g></g></svg>
+<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M4 0v1H1v3H0V.5A.5.5 0 0 1 .5 0H4zm8 0h3.5a.5.5 0 0 1 .5.5V4h-1V1h-3V0zM4 16H.5a.5.5 0 0 1-.5-.5V12h1v3h3v1zm8 0v-1h3v-3h1v3.5a.5.5 0 0 1-.5.5H12z"/><path fill-opacity=".256" d="M1 1h14v14H1z"/><g class="ck-icon__selected-indicator"><path d="M7 0h2v1H7V0zM0 7h1v2H0V7zm15 0h1v2h-1V7zm-8 8h2v1H7v-1z"/><path fill-opacity=".254" d="M1 1h14v14H1z"/></g></svg>