Explorar o código

Moved ImageBalloon logic to ImageTextAlternative.

Aleksander Nowodzinski %!s(int64=8) %!d(string=hai) anos
pai
achega
21b9c1e4a7

+ 58 - 31
packages/ckeditor5-image/src/imagetextalternative.js

@@ -12,15 +12,16 @@ import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import ImageTextAlternativeEngine from './imagetextalternative/imagetextalternativeengine';
 import ImageTextAlternativeEngine from './imagetextalternative/imagetextalternativeengine';
 import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsidehandler';
 import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsidehandler';
 import TextAlternativeFormView from './imagetextalternative/ui/textalternativeformview';
 import TextAlternativeFormView from './imagetextalternative/ui/textalternativeformview';
-import ImageBalloon from './image/ui/imageballoon';
+import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
 import textAlternativeIcon from '@ckeditor/ckeditor5-core/theme/icons/low-vision.svg';
 import textAlternativeIcon from '@ckeditor/ckeditor5-core/theme/icons/low-vision.svg';
+import { repositionContextualBalloon, getBalloonPositionData } from './image/ui/utils';
 
 
 import '../theme/imagetextalternative/theme.scss';
 import '../theme/imagetextalternative/theme.scss';
 
 
 /**
 /**
  * The image text alternative plugin.
  * The image text alternative plugin.
  *
  *
- * The plugin uses {@link module:image/image/ui/imageballoon~ImageBalloon}.
+ * The plugin uses the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon}.
  *
  *
  * @extends module:core/plugin~Plugin
  * @extends module:core/plugin~Plugin
  */
  */
@@ -29,7 +30,7 @@ export default class ImageTextAlternative extends Plugin {
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
 	static get requires() {
 	static get requires() {
-		return [ ImageTextAlternativeEngine, ImageBalloon ];
+		return [ ImageTextAlternativeEngine, ContextualBalloon ];
 	}
 	}
 
 
 	/**
 	/**
@@ -79,10 +80,19 @@ export default class ImageTextAlternative extends Plugin {
 	 * Creates the {@link module:image/imagetextalternative/ui/textalternativeformview~TextAlternativeFormView}
 	 * Creates the {@link module:image/imagetextalternative/ui/textalternativeformview~TextAlternativeFormView}
 	 * form.
 	 * form.
 	 *
 	 *
-	 * @protected
+	 * @private
 	 */
 	 */
 	_createForm() {
 	_createForm() {
 		const editor = this.editor;
 		const editor = this.editor;
+		const editingView = editor.editing.view;
+
+		/**
+		 * The contextual balloon plugin instance.
+		 *
+		 * @private
+		 * @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
+		 */
+		this._balloon = this.editor.plugins.get( 'ContextualBalloon' );
 
 
 		/**
 		/**
 		 * Form containing textarea and buttons, used to change `alt` text value.
 		 * Form containing textarea and buttons, used to change `alt` text value.
@@ -109,6 +119,20 @@ export default class ImageTextAlternative extends Plugin {
 			cancel();
 			cancel();
 		} );
 		} );
 
 
+		// Reposition the balloon upon #render.
+		this.listenTo( editingView, 'render', () => {
+			if ( this._isVisible ) {
+				repositionContextualBalloon( editor );
+			}
+		}, { priority: 'low' } );
+
+		// Hide the form when the editor is blurred.
+		this.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, is ) => {
+			if ( !is ) {
+				this._hideForm();
+			}
+		}, { priority: 'low' } );
+
 		// Close on click outside of balloon panel element.
 		// Close on click outside of balloon panel element.
 		clickOutsideHandler( {
 		clickOutsideHandler( {
 			emitter: this._form,
 			emitter: this._form,
@@ -119,59 +143,62 @@ export default class ImageTextAlternative extends Plugin {
 	}
 	}
 
 
 	/**
 	/**
-	 * Shows the form in a balloon.
+	 * Shows the {@link #_form} in the {@link #_balloon}.
 	 *
 	 *
-	 * @protected
+	 * @private
 	 */
 	 */
 	_showForm() {
 	_showForm() {
+		if ( this._isVisible ) {
+			return;
+		}
+
 		const editor = this.editor;
 		const editor = this.editor;
-		const balloon = editor.plugins.get( 'ImageBalloon' );
 		const command = editor.commands.get( 'imageTextAlternative' );
 		const command = editor.commands.get( 'imageTextAlternative' );
 		const labeledInput = this._form.labeledInput;
 		const labeledInput = this._form.labeledInput;
 
 
-		if ( !balloon.hasView( this._form ) ) {
-			balloon.add( {
-				view: this._form
+		if ( !this._balloon.hasView( this._form ) ) {
+			this._balloon.add( {
+				view: this._form,
+				position: getBalloonPositionData( editor )
 			} );
 			} );
+		}
 
 
-			// Make sure that each time the panel shows up, the field remains in sync with the value of
-			// the command. If the user typed in the input, then canceled the balloon (`labeledInput#value`
-			// stays unaltered) and re-opened it without changing the value of the command, they would see the
-			// old value instead of the actual value of the command.
-			// https://github.com/ckeditor/ckeditor5-image/issues/114
-			labeledInput.value = labeledInput.inputView.element.value = command.value || '';
+		// Make sure that each time the panel shows up, the field remains in sync with the value of
+		// the command. If the user typed in the input, then canceled the balloon (`labeledInput#value`
+		// stays unaltered) and re-opened it without changing the value of the command, they would see the
+		// old value instead of the actual value of the command.
+		// https://github.com/ckeditor/ckeditor5-image/issues/114
+		labeledInput.value = labeledInput.inputView.element.value = command.value || '';
 
 
-			this._form.labeledInput.select();
-		}
+		this._form.labeledInput.select();
 	}
 	}
 
 
 	/**
 	/**
-	 * Hides the form in a balloon.
+	 * Removes the {@link #_form} from the {@link #_balloon}.
 	 *
 	 *
 	 * @param {Boolean} focusEditable Control whether the editing view is focused afterwards.
 	 * @param {Boolean} focusEditable Control whether the editing view is focused afterwards.
-	 * @protected
+	 * @private
 	 */
 	 */
 	_hideForm( focusEditable ) {
 	_hideForm( focusEditable ) {
-		const editor = this.editor;
-		const balloon = editor.plugins.get( 'ImageBalloon' );
+		if ( !this._isVisible ) {
+			return;
+		}
 
 
-		if ( balloon.hasView( this._form ) ) {
-			balloon.remove( this._form );
+		this._balloon.remove( this._form );
 
 
-			if ( focusEditable ) {
-				editor.editing.view.focus();
-			}
+		if ( focusEditable ) {
+			this.editor.editing.view.focus();
 		}
 		}
 	}
 	}
 
 
 	/**
 	/**
-	 * Returns `true` when the {@link _form} is the visible view
-	 * in {@link module:image/ui/imageballoon~ImageBalloon}.
+	 * Returns `true` when the {@link #_form} is the visible view
+	 * in the {@link #_balloon}.
 	 *
 	 *
-	 * @protected
+	 * @private
 	 * @type {Boolean}
 	 * @type {Boolean}
 	 */
 	 */
 	get _isVisible() {
 	get _isVisible() {
-		return this.editor.plugins.get( 'ImageBalloon' ).visibleView == this._form;
+		return this._balloon.visibleView == this._form;
 	}
 	}
 }
 }

+ 56 - 18
packages/ckeditor5-image/tests/imagetextalternative.js

@@ -3,19 +3,20 @@
  * For licensing, see LICENSE.md.
  * For licensing, see LICENSE.md.
  */
  */
 
 
+/* global Event, document */
+
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import Image from '../src/image';
 import Image from '../src/image';
 import ImageTextAlternative from '../src/imagetextalternative';
 import ImageTextAlternative from '../src/imagetextalternative';
 import ImageTextAlternativeEngine from '../src/imagetextalternative/imagetextalternativeengine';
 import ImageTextAlternativeEngine from '../src/imagetextalternative/imagetextalternativeengine';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import View from '@ckeditor/ckeditor5-ui/src/view';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 
 
-/* global Event */
-
 describe( 'ImageTextAlternative', () => {
 describe( 'ImageTextAlternative', () => {
-	let editor, doc, plugin, command, form, imageBalloon, editorElement;
+	let editor, editingView, doc, plugin, command, form, balloon, editorElement, button;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
 		editorElement = global.document.createElement( 'div' );
 		editorElement = global.document.createElement( 'div' );
@@ -26,12 +27,14 @@ describe( 'ImageTextAlternative', () => {
 		} )
 		} )
 		.then( newEditor => {
 		.then( newEditor => {
 			editor = newEditor;
 			editor = newEditor;
+			editingView = editor.editing.view;
 			doc = editor.document;
 			doc = editor.document;
 			newEditor.editing.view.attachDomRoot( editorElement );
 			newEditor.editing.view.attachDomRoot( editorElement );
 			plugin = editor.plugins.get( ImageTextAlternative );
 			plugin = editor.plugins.get( ImageTextAlternative );
 			command = editor.commands.get( 'imageTextAlternative' );
 			command = editor.commands.get( 'imageTextAlternative' );
 			form = plugin._form;
 			form = plugin._form;
-			imageBalloon = editor.plugins.get( 'ImageBalloon' );
+			balloon = editor.plugins.get( 'ContextualBalloon' );
+			button = editor.ui.componentFactory.create( 'imageTextAlternative' );
 		} );
 		} );
 	} );
 	} );
 
 
@@ -50,12 +53,6 @@ describe( 'ImageTextAlternative', () => {
 	} );
 	} );
 
 
 	describe( 'toolbar button', () => {
 	describe( 'toolbar button', () => {
-		let button;
-
-		beforeEach( () => {
-			button = editor.ui.componentFactory.create( 'imageTextAlternative' );
-		} );
-
 		it( 'should be registered in component factory', () => {
 		it( 'should be registered in component factory', () => {
 			expect( button ).to.be.instanceOf( ButtonView );
 			expect( button ).to.be.instanceOf( ButtonView );
 		} );
 		} );
@@ -69,17 +66,17 @@ describe( 'ImageTextAlternative', () => {
 		} );
 		} );
 
 
 		it( 'should show balloon panel on execute', () => {
 		it( 'should show balloon panel on execute', () => {
-			expect( imageBalloon.visibleView ).to.be.null;
+			expect( balloon.visibleView ).to.be.null;
 
 
 			setData( doc, '[<image src="" alt="foo bar"></image>]' );
 			setData( doc, '[<image src="" alt="foo bar"></image>]' );
 
 
 			button.fire( 'execute' );
 			button.fire( 'execute' );
-			expect( imageBalloon.visibleView ).to.equal( form );
+			expect( balloon.visibleView ).to.equal( form );
 
 
 			// Make sure successive execute does not throw, e.g. attempting
 			// Make sure successive execute does not throw, e.g. attempting
 			// to display the form twice.
 			// to display the form twice.
 			button.fire( 'execute' );
 			button.fire( 'execute' );
-			expect( imageBalloon.visibleView ).to.equal( form );
+			expect( balloon.visibleView ).to.equal( form );
 		} );
 		} );
 
 
 		it( 'should set alt attribute value to textarea and select it', () => {
 		it( 'should set alt attribute value to textarea and select it', () => {
@@ -138,13 +135,54 @@ describe( 'ImageTextAlternative', () => {
 			setData( doc, '[<image src="" alt="foo bar"></image>]' );
 			setData( doc, '[<image src="" alt="foo bar"></image>]' );
 
 
 			editor.ui.componentFactory.create( 'imageTextAlternative' ).fire( 'execute' );
 			editor.ui.componentFactory.create( 'imageTextAlternative' ).fire( 'execute' );
-			expect( imageBalloon.visibleView ).to.equal( form );
+			expect( balloon.visibleView ).to.equal( form );
 
 
 			form.fire( 'cancel' );
 			form.fire( 'cancel' );
-			expect( imageBalloon.visibleView ).to.be.null;
+			expect( balloon.visibleView ).to.be.null;
 			sinon.assert.calledOnce( spy );
 			sinon.assert.calledOnce( spy );
 		} );
 		} );
 
 
+		it( 'should not engage when the form is in the balloon yet invisible', () => {
+			setData( doc, '[<image src=""></image>]' );
+			button.fire( 'execute' );
+			expect( balloon.visibleView ).to.equal( form );
+
+			const lastView = new View();
+			lastView.element = document.createElement( 'div' );
+
+			balloon.add( { view: lastView } );
+			expect( balloon.visibleView ).to.equal( lastView );
+
+			button.fire( 'execute' );
+			expect( balloon.visibleView ).to.equal( lastView );
+		} );
+
+		describe( 'integration with the editor selection (#render event)', () => {
+			it( 'should re-position the form', () => {
+				setData( doc, '[<image src=""></image>]' );
+				button.fire( 'execute' );
+
+				const spy = sinon.spy( balloon, 'updatePosition' );
+
+				editingView.fire( 'render' );
+				sinon.assert.calledOnce( spy );
+			} );
+		} );
+
+		describe( 'integration with the editor focus', () => {
+			it( 'should hide the toolbar when the editor loses focus and the image is selected', () => {
+				editor.ui.focusTracker.isFocused = true;
+
+				setData( doc, '[<image src=""></image>]' );
+				button.fire( 'execute' );
+
+				const spy = sinon.spy( balloon, 'remove' );
+
+				editor.ui.focusTracker.isFocused = false;
+				sinon.assert.calledWithExactly( spy, form );
+			} );
+		} );
+
 		describe( 'close listeners', () => {
 		describe( 'close listeners', () => {
 			describe( 'keyboard', () => {
 			describe( 'keyboard', () => {
 				it( 'should close upon Esc key press and focus the editing view', () => {
 				it( 'should close upon Esc key press and focus the editing view', () => {
@@ -152,7 +190,7 @@ describe( 'ImageTextAlternative', () => {
 					const focusSpy = sinon.spy( editor.editing.view, 'focus' );
 					const focusSpy = sinon.spy( editor.editing.view, 'focus' );
 
 
 					setData( doc, '[<image src=""></image>]' );
 					setData( doc, '[<image src=""></image>]' );
-					imageBalloon.add( { view: form } );
+					button.fire( 'execute' );
 
 
 					const keyEvtData = {
 					const keyEvtData = {
 						keyCode: keyCodes.esc,
 						keyCode: keyCodes.esc,
@@ -173,7 +211,7 @@ describe( 'ImageTextAlternative', () => {
 					const focusSpy = sinon.spy( editor.editing.view, 'focus' );
 					const focusSpy = sinon.spy( editor.editing.view, 'focus' );
 
 
 					setData( doc, '[<image src=""></image>]' );
 					setData( doc, '[<image src=""></image>]' );
-					imageBalloon.add( { view: form } );
+					button.fire( 'execute' );
 
 
 					global.document.body.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
 					global.document.body.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
 					sinon.assert.called( hideSpy );
 					sinon.assert.called( hideSpy );
@@ -184,7 +222,7 @@ describe( 'ImageTextAlternative', () => {
 					const spy = sinon.spy( plugin, '_hideForm' );
 					const spy = sinon.spy( plugin, '_hideForm' );
 
 
 					setData( doc, '[<image src=""></image>]' );
 					setData( doc, '[<image src=""></image>]' );
-					imageBalloon.add( { view: form } );
+					button.fire( 'execute' );
 
 
 					form.element.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
 					form.element.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
 					sinon.assert.notCalled( spy );
 					sinon.assert.notCalled( spy );