Browse Source

Integrated ImageTextAlternative with ImageBalloon.

Aleksander Nowodzinski 8 năm trước cách đây
mục cha
commit
863ddee972

+ 76 - 78
packages/ckeditor5-image/src/imagetextalternative.js

@@ -10,18 +10,18 @@
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import ImageTextAlternativeEngine from './imagetextalternative/imagetextalternativeengine';
-import escPressHandler from '@ckeditor/ckeditor5-ui/src/bindings/escpresshandler';
 import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsidehandler';
-import ImageToolbar from './imagetoolbar';
 import TextAlternativeFormView from './imagetextalternative/ui/textalternativeformview';
-import ImageBalloonPanel from './image/ui/imageballoonpanelview';
-
+import ImageBalloon from './image/ui/imageballoon';
 import textAlternativeIcon from '@ckeditor/ckeditor5-core/theme/icons/low-vision.svg';
+
 import '../theme/imagetextalternative/theme.scss';
 
 /**
  * The image text alternative plugin.
  *
+ * The plugin uses {@link module:image/image/ui/imageballoon~ImageBalloon}.
+ *
  * @extends module:core/plugin~Plugin
  */
 export default class ImageTextAlternative extends Plugin {
@@ -29,7 +29,7 @@ export default class ImageTextAlternative extends Plugin {
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ ImageTextAlternativeEngine ];
+		return [ ImageTextAlternativeEngine, ImageBalloon ];
 	}
 
 	/**
@@ -44,22 +44,7 @@ export default class ImageTextAlternative extends Plugin {
 	 */
 	init() {
 		this._createButton();
-
-		const panel = this._createBalloonPanel();
-
-		/**
-		 * Balloon panel containing text alternative change form.
-		 *
-		 * @member {module:image/image/ui/imageballoonpanel~ImageBalloonPanelView} #baloonPanel
-		 */
-		this.balloonPanel = panel;
-
-		/**
-		 * Form containing textarea and buttons, used to change `alt` text value.
-		 *
-		 * @member {module:image/imagetextalternative/ui/textalternativeformview~TextAlternativeFormView} #form
-		 */
-		this.form = panel.content.get( 0 );
+		this._createForm();
 	}
 
 	/**
@@ -84,96 +69,109 @@ export default class ImageTextAlternative extends Plugin {
 
 			view.bind( 'isEnabled' ).to( command, 'isEnabled' );
 
-			this.listenTo( view, 'execute', () => this._showBalloonPanel() );
+			this.listenTo( view, 'execute', () => this._showForm() );
 
 			return view;
 		} );
 	}
 
 	/**
-	 * Creates balloon panel.
+	 * Creates the {@link module:image/imagetextalternative/ui/textalternativeformview~TextAlternativeFormView}
+	 * form.
 	 *
-	 * @private
-	 * @return {module:image/image/ui/imageballoonpanel~ImageBalloonPanelView}
+	 * @protected
 	 */
-	_createBalloonPanel() {
+	_createForm() {
 		const editor = this.editor;
 
-		const panel = new ImageBalloonPanel( editor );
-		const form = new TextAlternativeFormView( editor.locale );
-
-		this.listenTo( form, 'submit', () => {
-			editor.execute( 'imageTextAlternative', { newValue: form.labeledInput.inputView.element.value } );
-			this._hideBalloonPanel();
-		} );
+		/**
+		 * Form containing textarea and buttons, used to change `alt` text value.
+		 *
+		 * @member {module:image/imagetextalternative/ui/textalternativeformview~TextAlternativeFormView} #form
+		 */
+		this._form = new TextAlternativeFormView( editor.locale );
 
-		// If image toolbar is present - hide it when text alternative balloon is visible.
-		const imageToolbar = editor.plugins.get( ImageToolbar );
-
-		if ( imageToolbar ) {
-			this.listenTo( panel, 'change:isVisible', () => {
-				if ( panel.isVisible ) {
-					imageToolbar.hide();
-					imageToolbar.isEnabled = false;
-				} else {
-					imageToolbar.show();
-					imageToolbar.isEnabled = true;
-				}
+		this.listenTo( this._form, 'submit', () => {
+			editor.execute( 'imageTextAlternative', {
+				newValue: this._form.labeledInput.inputView.element.value
 			} );
-		}
 
-		this.listenTo( form, 'cancel', () => this._hideBalloonPanel() );
+			this._hideForm( true );
+		} );
+
+		this.listenTo( this._form, 'cancel', () => {
+			this._hideForm( true );
+		} );
 
-		// Close on `ESC` press.
-		escPressHandler( {
-			emitter: panel,
-			activator: () => panel.isVisible,
-			callback: () => this._hideBalloonPanel()
+		// Close the form on Esc key press.
+		this._form.keystrokes.set( 'Esc', ( data, cancel ) => {
+			this._hideForm( true );
+			cancel();
 		} );
 
 		// Close on click outside of balloon panel element.
 		clickOutsideHandler( {
-			emitter: panel,
-			activator: () => panel.isVisible,
-			contextElement: panel.element,
-			callback: () => this._hideBalloonPanel()
+			emitter: this._form,
+			activator: () => this._isVisible,
+			contextElement: this._form.element,
+			callback: () => this._hideForm()
 		} );
-
-		panel.content.add( form );
-		editor.ui.view.body.add( panel );
-
-		return panel;
 	}
 
 	/**
-	 * Shows the balloon panel.
+	 * Shows the form in a balloon.
 	 *
-	 * @private
+	 * @protected
 	 */
-	_showBalloonPanel() {
+	_showForm() {
 		const editor = this.editor;
+		const balloon = editor.plugins.get( 'ImageBalloon' );
 		const command = editor.commands.get( 'imageTextAlternative' );
-		const labeledInput = this.form.labeledInput;
-		this.balloonPanel.attach();
+		const labeledInput = this._form.labeledInput;
+
+		if ( !balloon.hasView( this._form ) ) {
+			balloon.add( {
+				view: this._form
+			} );
 
-		// 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 balloon panel.
+	 * Hides the form in a balloon.
 	 *
-	 * @private
+	 * @param {Boolean} focusEditable Control whether the editing view is focused afterwards.
+	 * @protected
 	 */
-	_hideBalloonPanel() {
+	_hideForm( focusEditable ) {
 		const editor = this.editor;
-		this.balloonPanel.detach();
-		editor.editing.view.focus();
+		const balloon = editor.plugins.get( 'ImageBalloon' );
+
+		if ( balloon.hasView( this._form ) ) {
+			balloon.remove( this._form );
+
+			if ( focusEditable ) {
+				editor.editing.view.focus();
+			}
+		}
+	}
+
+	/**
+	 * Returns `true` when the {@link _form} is the visible view
+	 * in {@link module:image/ui/imageballoon~ImageBalloon}.
+	 *
+	 * @protected
+	 * @type {Boolean}
+	 */
+	get _isVisible() {
+		return this.editor.plugins.get( 'ImageBalloon' ).visibleView == this._form;
 	}
 }

+ 68 - 115
packages/ckeditor5-image/tests/imagetextalternative.js

@@ -5,7 +5,6 @@
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import Image from '../src/image';
-import ImageToolbar from '../src/imagetoolbar';
 import ImageTextAlternative from '../src/imagetextalternative';
 import ImageTextAlternativeEngine from '../src/imagetextalternative/imagetextalternativeengine';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
@@ -16,10 +15,10 @@ import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 /* global Event */
 
 describe( 'ImageTextAlternative', () => {
-	let editor, plugin, command, balloonPanel, form;
+	let editor, doc, plugin, command, form, imageBalloon, editorElement;
 
 	beforeEach( () => {
-		const editorElement = global.document.createElement( 'div' );
+		editorElement = global.document.createElement( 'div' );
 		global.document.body.appendChild( editorElement );
 
 		return ClassicTestEditor.create( editorElement, {
@@ -27,15 +26,18 @@ describe( 'ImageTextAlternative', () => {
 		} )
 		.then( newEditor => {
 			editor = newEditor;
+			doc = editor.document;
 			newEditor.editing.view.attachDomRoot( editorElement );
 			plugin = editor.plugins.get( ImageTextAlternative );
 			command = editor.commands.get( 'imageTextAlternative' );
-			balloonPanel = plugin.balloonPanel;
-			form = plugin.form;
+			form = plugin._form;
+			imageBalloon = editor.plugins.get( 'ImageBalloon' );
 		} );
 	} );
 
 	afterEach( () => {
+		editorElement.remove();
+
 		return editor.destroy();
 	} );
 
@@ -67,29 +69,37 @@ describe( 'ImageTextAlternative', () => {
 		} );
 
 		it( 'should show balloon panel on execute', () => {
-			const spy = sinon.spy( balloonPanel, 'attach' );
-			setData( editor.document, '[<image src="" alt="foo bar"></image>]' );
+			expect( imageBalloon.visibleView ).to.be.null;
+
+			setData( doc, '[<image src="" alt="foo bar"></image>]' );
+
 			button.fire( 'execute' );
+			expect( imageBalloon.visibleView ).to.equal( form );
 
-			sinon.assert.calledOnce( spy );
+			// Make sure successive execute does not throw, e.g. attempting
+			// to display the form twice.
+			button.fire( 'execute' );
+			expect( imageBalloon.visibleView ).to.equal( form );
 		} );
 
 		it( 'should set alt attribute value to textarea and select it', () => {
 			const spy = sinon.spy( form.labeledInput, 'select' );
-			setData( editor.document, '[<image src="" alt="foo bar"></image>]' );
-			button.fire( 'execute' );
 
+			setData( doc, '[<image src="" alt="foo bar"></image>]' );
+
+			button.fire( 'execute' );
 			sinon.assert.calledOnce( spy );
-			expect( plugin.form.labeledInput.value ).equals( 'foo bar' );
+			expect( form.labeledInput.value ).equals( 'foo bar' );
 		} );
 
 		it( 'should set empty text to textarea and select it when there is no alt attribute', () => {
 			const spy = sinon.spy( form.labeledInput, 'select' );
-			setData( editor.document, '[<image src=""></image>]' );
-			button.fire( 'execute' );
 
+			setData( doc, '[<image src=""></image>]' );
+
+			button.fire( 'execute' );
 			sinon.assert.calledOnce( spy );
-			expect( plugin.form.labeledInput.value ).equals( '' );
+			expect( form.labeledInput.value ).equals( '' );
 		} );
 	} );
 
@@ -106,7 +116,7 @@ describe( 'ImageTextAlternative', () => {
 			form.labeledInput.inputView.element.value = 'This value was canceled.';
 
 			// Mock the user editing the same image once again.
-			setData( editor.document, '[<image src="" alt="foo"></image>]' );
+			setData( doc, '[<image src="" alt="foo"></image>]' );
 
 			button.fire( 'execute' );
 			expect( form.labeledInput.inputView.element.value ).to.equal( 'foo' );
@@ -117,126 +127,69 @@ describe( 'ImageTextAlternative', () => {
 			form.fire( 'submit' );
 
 			sinon.assert.calledOnce( spy );
-			sinon.assert.calledWithExactly( spy, 'imageTextAlternative', { newValue: form.labeledInput.inputView.element.value } );
+			sinon.assert.calledWithExactly( spy, 'imageTextAlternative', {
+				newValue: form.labeledInput.inputView.element.value
+			} );
 		} );
 
-		it( 'should detach panel on cancel', () => {
-			const spy = sinon.spy( balloonPanel, 'detach' );
-			form.fire( 'cancel' );
+		it( 'should hide the panel on cancel and focus the editing view', () => {
+			const spy = sinon.spy( editor.editing.view, 'focus' );
 
-			sinon.assert.called( spy );
-		} );
+			setData( doc, '[<image src="" alt="foo bar"></image>]' );
 
-		it( 'should show ImageToolbar on cancel if present', () => {
-			const editorElement = global.document.createElement( 'div' );
-			global.document.body.appendChild( editorElement );
-
-			return ClassicTestEditor.create( editorElement, {
-				plugins: [ ImageTextAlternative, Image, ImageToolbar ],
-				image: {
-					toolbar: [ 'imageTextAlternative' ]
-				}
-			} )
-			.then( newEditor => {
-				newEditor.editing.view.attachDomRoot( editorElement );
-				const plugin = newEditor.plugins.get( ImageTextAlternative );
-				const toolbarPlugin = newEditor.plugins.get( ImageToolbar );
-				const form = plugin.form;
-
-				const spy = sinon.spy( toolbarPlugin, 'show' );
-				form.fire( 'cancel' );
-
-				sinon.assert.called( spy );
-			} );
+			editor.ui.componentFactory.create( 'imageTextAlternative' ).fire( 'execute' );
+			expect( imageBalloon.visibleView ).to.equal( form );
+
+			form.fire( 'cancel' );
+			expect( imageBalloon.visibleView ).to.be.null;
+			sinon.assert.calledOnce( spy );
 		} );
 
 		describe( 'close listeners', () => {
-			let hidePanelSpy;
-
-			beforeEach( () => {
-				hidePanelSpy = sinon.spy( balloonPanel, 'detach' );
-			} );
-
 			describe( 'keyboard', () => {
-				it( 'should close after `ESC` press', () => {
-					balloonPanel.isVisible = true;
-					const keyCode = keyCodes.esc;
-					const event = global.document.createEvent( 'Events' );
-					event.initEvent( 'keydown', true, true );
-					event.which = keyCode;
-					event.keyCode = keyCode;
-					global.document.dispatchEvent( event );
-
-					sinon.assert.called( hidePanelSpy );
+				it( 'should close upon Esc key press and focus the editing view', () => {
+					const hideSpy = sinon.spy( plugin, '_hideForm' );
+					const focusSpy = sinon.spy( editor.editing.view, 'focus' );
+
+					setData( doc, '[<image src=""></image>]' );
+					imageBalloon.add( { view: form } );
+
+					const keyEvtData = {
+						keyCode: keyCodes.esc,
+						preventDefault: sinon.spy(),
+						stopPropagation: sinon.spy()
+					};
+
+					form.keystrokes.press( keyEvtData );
+					sinon.assert.calledOnce( hideSpy );
+					sinon.assert.calledOnce( keyEvtData.preventDefault );
+					sinon.assert.calledOnce( focusSpy );
 				} );
 			} );
 
 			describe( 'mouse', () => {
 				it( 'should close and not focus editable on click outside the panel', () => {
-					balloonPanel.isVisible = true;
-					global.document.body.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
+					const hideSpy = sinon.spy( plugin, '_hideForm' );
+					const focusSpy = sinon.spy( editor.editing.view, 'focus' );
+
+					setData( doc, '[<image src=""></image>]' );
+					imageBalloon.add( { view: form } );
 
-					sinon.assert.called( hidePanelSpy );
+					global.document.body.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
+					sinon.assert.called( hideSpy );
+					sinon.assert.notCalled( focusSpy );
 				} );
 
 				it( 'should not close on click inside the panel', () => {
-					balloonPanel.isVisible = true;
-					balloonPanel.element.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
-
-					sinon.assert.notCalled( hidePanelSpy );
-				} );
-			} );
-		} );
-	} );
+					const spy = sinon.spy( plugin, '_hideForm' );
 
-	describe( 'working with ImageToolbar', () => {
-		let editor, button, imageToolbarPlugin, plugin;
+					setData( doc, '[<image src=""></image>]' );
+					imageBalloon.add( { view: form } );
 
-		beforeEach( () => {
-			const editorElement = global.document.createElement( 'div' );
-			global.document.body.appendChild( editorElement );
-
-			return ClassicTestEditor.create( editorElement, {
-				plugins: [ ImageTextAlternative, Image, ImageToolbar ],
-				image: {
-					toolbar: [ 'imageTextAlternative' ]
-				}
-			} )
-			.then( newEditor => {
-				editor = newEditor;
-				editor.editing.view.attachDomRoot( editorElement );
-				button = newEditor.ui.componentFactory.create( 'imageTextAlternative' );
-				imageToolbarPlugin = newEditor.plugins.get( ImageToolbar );
-				plugin = editor.plugins.get( ImageTextAlternative );
+					form.element.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
+					sinon.assert.notCalled( spy );
+				} );
 			} );
 		} );
-
-		afterEach( () => editor.destroy() );
-
-		it( 'should hide ImageToolbar when visible', () => {
-			setData( editor.document, '[<image src="" alt="foo bar"></image>]' );
-			const spy = sinon.spy( imageToolbarPlugin, 'hide' );
-			button.fire( 'execute' );
-
-			sinon.assert.calledOnce( spy );
-		} );
-
-		it( 'ImageToolbar should not show when text alternative panel is visible', () => {
-			setData( editor.document, '[<image src="" alt="foo bar"></image>]' );
-			button.fire( 'execute' );
-			const spy = sinon.spy( imageToolbarPlugin, 'show' );
-			editor.editing.view.render();
-
-			sinon.assert.notCalled( spy );
-		} );
-
-		it( 'ImageToolbar should show when text alternative panel is not visible', () => {
-			setData( editor.document, '[<image src="" alt="foo bar"></image>]' );
-			button.fire( 'execute' );
-			const spy = sinon.spy( imageToolbarPlugin, 'show' );
-			plugin.balloonPanel.isVisible = false;
-
-			sinon.assert.called( spy );
-		} );
 	} );
 } );