瀏覽代碼

Refactor ImageUploadPanelView and utils.

panr 5 年之前
父節點
當前提交
632bfe9941

+ 15 - 47
packages/ckeditor5-image/src/imageupload/ui/imageuploadpanelview.js

@@ -11,10 +11,7 @@ import View from '@ckeditor/ckeditor5-ui/src/view';
 
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import SplitButtonView from '@ckeditor/ckeditor5-ui/src/dropdown/button/splitbuttonview';
-import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview';
 import ImageUploadFormRowView from './imageuploadformrowview';
-
-import { createLabeledInputText } from '@ckeditor/ckeditor5-ui/src/labeledfield/utils';
 import { createDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
 
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
@@ -42,19 +39,12 @@ export default class ImageUploadPanelView extends View {
 	 * Creates a view for the dropdown panel of {@link module:image/imageupload/imageuploadui~ImageUploadUI}.
 	 *
 	 * @param {module:utils/locale~Locale} [locale] The localization services instance..
-	 * @param {Object} [integrations={insertImageViaUrl:'insertImageViaUrl'}] Integrations object that contain
-	 * components (or tokens for components) to be shown in the panel view. By default it has `insertImageViaUrl` view.
+	 * @param {Object} [integrations] Integrations object that contain
+	 * components (or tokens for components) to be shown in the panel view.
 	 */
-	constructor( locale, integrations = { insertImageViaUrl: 'insertImageViaUrl' } ) {
+	constructor( locale, integrations ) {
 		super( locale );
 
-		/**
-		 * The labeled URL input view.
-		 *
-		 * @member {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
-		 */
-		this.labeledInputView = this._createLabeledInputView( locale );
-
 		const { insertButtonView, cancelButtonView } = this._createActionButtons( locale );
 
 		/**
@@ -139,14 +129,18 @@ export default class ImageUploadPanelView extends View {
 		 */
 		this.set( '_integrations', new Collection() );
 
-		for ( const integration of Object.values( integrations ) ) {
-			if ( integration === 'insertImageViaUrl' ) {
-				this._integrations.add( this.labeledInputView );
+		if ( integrations ) {
+			for ( const [ integration, integrationView ] of Object.entries( integrations ) ) {
+				if ( integration === 'insertImageViaUrl' ) {
+					integrationView.fieldView.bind( 'value' ).to( this, 'imageURLInputValue', value => value || '' );
 
-				continue;
-			}
+					integrationView.fieldView.on( 'input', () => {
+						this.imageURLInputValue = integrationView.fieldView.element.value;
+					} );
+				}
 
-			this._integrations.add( integration );
+				this._integrations.add( integrationView );
+			}
 		}
 
 		this.setTemplate( {
@@ -185,7 +179,7 @@ export default class ImageUploadPanelView extends View {
 		} );
 
 		const childViews = [
-			this.labeledInputView,
+			...this._integrations,
 			this.insertButtonView,
 			this.cancelButtonView
 		];
@@ -214,38 +208,12 @@ export default class ImageUploadPanelView extends View {
 		// Intercept the "selectstart" event, which is blocked by default because of the default behavior
 		// of the DropdownView#panelView.
 		// TODO: blocking "selectstart" in the #panelView should be configurable per–drop–down instance.
-		this.listenTo( this.labeledInputView.element, 'selectstart', ( evt, domEvt ) => {
+		this.listenTo( childViews[ 0 ].element, 'selectstart', ( evt, domEvt ) => {
 			domEvt.stopPropagation();
 		}, { priority: 'high' } );
 	}
 
 	/**
-	 * Creates labeled field view.
-	 *
-	 * @param {module:utils/locale~Locale} locale The localization services instance.
-	 *
-	 * @private
-	 * @returns {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
-	 */
-	_createLabeledInputView( locale ) {
-		const t = locale.t;
-		const labeledInputView = new LabeledFieldView( locale, createLabeledInputText );
-
-		labeledInputView.set( {
-			label: t( 'Insert image via URL' )
-		} );
-		labeledInputView.fieldView.placeholder = 'https://example.com/src/image.png';
-		labeledInputView.infoText = t( 'Paste the image source URL' );
-		labeledInputView.fieldView.bind( 'value' ).to( this, 'imageURLInputValue', value => value || '' );
-
-		labeledInputView.fieldView.on( 'input', () => {
-			this.imageURLInputValue = labeledInputView.fieldView.element.value;
-		} );
-
-		return labeledInputView;
-	}
-
-	/**
 	 * Creates dropdown view.
 	 *
 	 * @param {module:utils/locale~Locale} locale The localization services instance.

+ 39 - 7
packages/ckeditor5-image/src/imageupload/utils.js

@@ -7,7 +7,10 @@
  * @module image/imageupload/utils
  */
 
-/* global fetch, File */
+/* global fetch, File, console */
+
+import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview';
+import { createLabeledInputText } from '@ckeditor/ckeditor5-ui/src/labeledfield/utils';
 
 /**
  * Creates a regular expression used to test for image files.
@@ -96,16 +99,16 @@ export function prepareIntegrations( editor ) {
 	const panelItems = editor.config.get( 'image.upload.panel.items' );
 	const imageUploadUIPlugin = editor.plugins.get( 'ImageUploadUI' );
 
-	if ( !panelItems ) {
-		return null;
-	}
-
 	const PREDEFINED_INTEGRATIONS = {
-		'insertImageViaUrl': 'insertImageViaUrl'
+		'insertImageViaUrl': createLabeledInputView( editor.locale )
 	};
 
+	if ( !panelItems ) {
+		return PREDEFINED_INTEGRATIONS;
+	}
+
 	// Prepares ckfinder component for the `openCKFinder` integration token.
-	if ( editor.ui.componentFactory.has( 'ckfinder' ) ) {
+	if ( panelItems.find( item => item === 'openCKFinder' ) && editor.ui.componentFactory.has( 'ckfinder' ) ) {
 		const ckFinderButton = editor.ui.componentFactory.create( 'ckfinder' );
 		ckFinderButton.set( {
 			withText: true,
@@ -124,6 +127,15 @@ export function prepareIntegrations( editor ) {
 			object[ key ] = PREDEFINED_INTEGRATIONS[ key ];
 		} else if ( editor.ui.componentFactory.has( key ) ) {
 			object[ key ] = editor.ui.componentFactory.create( key );
+		} else {
+			// Console.warn should be enough because missing integration doesn't break the editor.
+			console.warn(
+				'It looks like integration "' + key + '" doesn\'t exist. ' +
+				'What may have happened: ' +
+				'\n- you passed the invalid integration name in the `image.upload.panel.items`,' +
+				'\n- integration component wasn\'t properly registered by the plugin,' +
+				'\n- the plugin that registers the component wasn\'t installed in the editor.'
+			);
 		}
 
 		return object;
@@ -131,3 +143,23 @@ export function prepareIntegrations( editor ) {
 
 	return integrations;
 }
+
+/**
+ * Creates labeled field view.
+ *
+ * @param {module:utils/locale~Locale} locale The localization services instance.
+ *
+ * @returns {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
+ */
+export function createLabeledInputView( locale ) {
+	const t = locale.t;
+	const labeledInputView = new LabeledFieldView( locale, createLabeledInputText );
+
+	labeledInputView.set( {
+		label: t( 'Insert image via URL' )
+	} );
+	labeledInputView.fieldView.placeholder = 'https://example.com/src/image.png';
+	labeledInputView.infoText = t( 'Paste the image source URL' );
+
+	return labeledInputView;
+}

+ 46 - 35
packages/ckeditor5-image/tests/imageupload/ui/imageuploadpanelview.js

@@ -21,21 +21,25 @@ import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler';
 import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection';
 import View from '@ckeditor/ckeditor5-ui/src/view';
 
+import { createLabeledInputView } from '../../../src/imageupload/utils';
+
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
 describe( 'ImageUploadPanelView', () => {
 	let view;
 
 	beforeEach( () => {
-		view = new ImageUploadPanelView( { t: val => val } );
+		view = new ImageUploadPanelView( { t: val => val }, {
+			'insertImageViaUrl': createLabeledInputView( { t: val => val } )
+		} );
 		view.render();
 	} );
 
-	describe( 'constructor()', () => {
-		it( 'should contain instance of LabeledInputView as #labeledInputView', () => {
-			expect( view.labeledInputView ).to.be.instanceOf( LabeledFieldView );
-		} );
+	afterEach( () => {
+		sinon.restore();
+	} );
 
+	describe( 'constructor()', () => {
 		it( 'should contain instance of ButtonView as #insertButtonView', () => {
 			expect( view.insertButtonView ).to.be.instanceOf( ButtonView );
 			expect( view.insertButtonView.label ).to.equal( 'Insert' );
@@ -64,7 +68,7 @@ describe( 'ImageUploadPanelView', () => {
 		} );
 
 		describe( 'integrations', () => {
-			it( 'should contain 2 integrations when they were passed to the ImageUloadPanelView as options.integrations', () => {
+			it( 'should contain 2 integrations when they were passed to the ImageUploadPanelView as integrations object', () => {
 				const view = new ImageUploadPanelView( { t: val => val }, {
 					'integration1': new View(),
 					'integration2': new ButtonView()
@@ -74,9 +78,9 @@ describe( 'ImageUploadPanelView', () => {
 				expect( view._integrations.length ).to.equal( 2 );
 			} );
 
-			it( 'should contain insertImageViaUrl view when "insertImageViaUrl" token is passed via options.integrations', () => {
+			it( 'should contain insertImageViaUrl view when it is passed via integrations object', () => {
 				const view = new ImageUploadPanelView( { t: val => val }, {
-					'insertImageViaUrl': 'insertImageViaUrl',
+					'insertImageViaUrl': createLabeledInputView( { t: val => val } ),
 					'integration1': new View(),
 					'integration2': new ButtonView()
 				} );
@@ -85,6 +89,13 @@ describe( 'ImageUploadPanelView', () => {
 				expect( view._integrations.length ).to.equal( 3 );
 				expect( view._integrations.first ).to.be.instanceOf( LabeledFieldView );
 			} );
+
+			it( 'should contain no integrations when they were not provided', () => {
+				const view = new ImageUploadPanelView( { t: val => val } );
+
+				expect( view._integrations ).to.be.instanceOf( Collection );
+				expect( view._integrations.length ).to.equal( 0 );
+			} );
 		} );
 
 		it( 'should create #focusTracker instance', () => {
@@ -126,16 +137,6 @@ describe( 'ImageUploadPanelView', () => {
 		} );
 	} );
 
-	describe( 'image URL input view', () => {
-		it( 'should have placeholder', () => {
-			expect( view.labeledInputView.fieldView.placeholder ).to.equal( 'https://example.com/src/image.png' );
-		} );
-
-		it( 'should have info text', () => {
-			expect( view.labeledInputView.infoText ).to.match( /^Paste the image source URL/ );
-		} );
-	} );
-
 	describe( 'template', () => {
 		it( 'should create element from the template', () => {
 			expect( view.element.classList.contains( 'ck' ) ).to.true;
@@ -143,10 +144,6 @@ describe( 'ImageUploadPanelView', () => {
 			expect( view.element.getAttribute( 'tabindex' ) ).to.equal( '-1' );
 		} );
 
-		it( 'should have URL input view', () => {
-			expect( view.template.children[ 0 ] ).to.equal( view.labeledInputView );
-		} );
-
 		it( 'should have form row view with buttons', () => {
 			expect( view.template.children[ 1 ] ).to.be.instanceOf( ImageUploadFormRowView );
 			expect( view.template.children[ 1 ].children.first ).to.equal( view.insertButtonView );
@@ -157,19 +154,31 @@ describe( 'ImageUploadPanelView', () => {
 	describe( 'render()', () => {
 		it( 'should register child views in #_focusables', () => {
 			expect( view._focusables.map( f => f ) ).to.have.members( [
-				view.labeledInputView,
+				...view._integrations,
 				view.insertButtonView,
 				view.cancelButtonView
 			] );
 		} );
 
-		it( 'should register child views\' #element in #focusTracker', () => {
+		it( 'should register child views\' #element in #focusTracker with no integrations', () => {
 			const spy = testUtils.sinon.spy( FocusTracker.prototype, 'add' );
 
 			view = new ImageUploadPanelView( { t: () => {} } );
 			view.render();
 
-			sinon.assert.calledWithExactly( spy.getCall( 0 ), view.labeledInputView.element );
+			sinon.assert.calledWithExactly( spy.getCall( 0 ), view.insertButtonView.element );
+			sinon.assert.calledWithExactly( spy.getCall( 1 ), view.cancelButtonView.element );
+		} );
+
+		it( 'should register child views\' #element in #focusTracker with "insertImageViaUrl" integration', () => {
+			const spy = testUtils.sinon.spy( FocusTracker.prototype, 'add' );
+
+			view = new ImageUploadPanelView( { t: () => {} }, {
+				'insertImageViaUrl': createLabeledInputView( { t: val => val } )
+			} );
+			view.render();
+
+			sinon.assert.calledWithExactly( spy.getCall( 0 ), view._integrations.get( 0 ).element );
 			sinon.assert.calledWithExactly( spy.getCall( 1 ), view.insertButtonView.element );
 			sinon.assert.calledWithExactly( spy.getCall( 2 ), view.cancelButtonView.element );
 		} );
@@ -206,7 +215,7 @@ describe( 'ImageUploadPanelView', () => {
 			sinon.assert.callCount( keyEvtData.stopPropagation, 4 );
 		} );
 
-		it( 'intercepts the "selectstart" event of the #labeledInputView with the high priority', () => {
+		it( 'intercepts the "selectstart" event of the first integration element with the high priority', () => {
 			const spy = sinon.spy();
 			const event = new Event( 'selectstart', {
 				bubbles: true,
@@ -215,7 +224,7 @@ describe( 'ImageUploadPanelView', () => {
 
 			event.stopPropagation = spy;
 
-			view.labeledInputView.element.dispatchEvent( event );
+			view._integrations.get( 0 ).element.dispatchEvent( event );
 			sinon.assert.calledOnce( spy );
 		} );
 
@@ -229,7 +238,7 @@ describe( 'ImageUploadPanelView', () => {
 
 				// Mock the url input is focused.
 				view.focusTracker.isFocused = true;
-				view.focusTracker.focusedElement = view.labeledInputView.element;
+				view.focusTracker.focusedElement = view._integrations.get( 0 ).element;
 
 				const spy = sinon.spy( view.insertButtonView, 'focus' );
 
@@ -262,8 +271,8 @@ describe( 'ImageUploadPanelView', () => {
 	} );
 
 	describe( 'focus()', () => {
-		it( 'should focus on the #labeledInputView', () => {
-			const spy = sinon.spy( view.labeledInputView, 'focus' );
+		it( 'should focus on the first integration', () => {
+			const spy = sinon.spy( view._integrations.get( 0 ), 'focus' );
 
 			view.focus();
 
@@ -271,15 +280,17 @@ describe( 'ImageUploadPanelView', () => {
 		} );
 	} );
 
-	describe( 'URL input', () => {
+	describe( 'Insert image via URL integration input', () => {
 		it( 'should be bound with #imageURLInputValue', () => {
-			view.labeledInputView.fieldView.element.value = 'abc';
-			view.labeledInputView.fieldView.fire( 'input' );
+			const form = view._integrations.get( 0 );
+
+			form.fieldView.element.value = 'abc';
+			form.fieldView.fire( 'input' );
 
 			expect( view.imageURLInputValue ).to.equal( 'abc' );
 
-			view.labeledInputView.fieldView.element.value = 'xyz';
-			view.labeledInputView.fieldView.fire( 'input' );
+			form.fieldView.element.value = 'xyz';
+			form.fieldView.fire( 'input' );
 
 			expect( view.imageURLInputValue ).to.equal( 'xyz' );
 		} );

+ 25 - 6
packages/ckeditor5-image/tests/imageupload/utils.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-/* globals document */
+/* globals document, console */
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 
@@ -14,9 +14,9 @@ import ImageUploadEditing from '../../src/imageupload/imageuploadediting';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Link from '@ckeditor/ckeditor5-link/src/link';
 import CKFinder from '@ckeditor/ckeditor5-ckfinder/src/ckfinder';
-import { createImageTypeRegExp, prepareIntegrations } from '../../src/imageupload/utils';
+import { createImageTypeRegExp, prepareIntegrations, createLabeledInputView } from '../../src/imageupload/utils';
 
-describe( 'upload utils', () => {
+describe( 'Upload utils', () => {
 	describe( 'createImageTypeRegExp()', () => {
 		it( 'should return RegExp for testing regular mime type', () => {
 			expect( createImageTypeRegExp( [ 'png' ] ).test( 'image/png' ) ).to.be.true;
@@ -79,10 +79,12 @@ describe( 'upload utils', () => {
 			editorElement.remove();
 		} );
 
-		it( 'should return only "inserImageViaUrl" integration', async () => {
+		it( 'should return only "insertImageViaUrl" integration and throw warning about missing integration', async () => {
 			const editorElement = document.createElement( 'div' );
 			document.body.appendChild( editorElement );
 
+			const consoleWarn = sinon.stub( console, 'warn' );
+
 			const editor = await ClassicEditor
 				.create( editorElement, {
 					plugins: [
@@ -105,6 +107,9 @@ describe( 'upload utils', () => {
 
 			expect( Object.values( prepareIntegrations( editor ) ).length ).to.equal( 1 );
 
+			sinon.assert.calledOnce( consoleWarn );
+			expect( /Integration "openCKFinder" doesn't exist/gmi.test( consoleWarn.args[ 0 ][ 0 ] ) ).to.be.true;
+
 			editor.destroy();
 			editorElement.remove();
 		} );
@@ -141,7 +146,7 @@ describe( 'upload utils', () => {
 			editorElement.remove();
 		} );
 
-		it( 'should not return any integrations, should return "undefined" instead', async () => {
+		it( 'should return "insertImageViaUrl" integration, when no integrations were configured', async () => {
 			const editorElement = document.createElement( 'div' );
 			document.body.appendChild( editorElement );
 
@@ -155,10 +160,24 @@ describe( 'upload utils', () => {
 					]
 				} );
 
-			expect( prepareIntegrations( editor ) ).to.equal( undefined );
+			expect( Object.keys( prepareIntegrations( editor ) ).length ).to.equal( 1 );
 
 			editor.destroy();
 			editorElement.remove();
 		} );
 	} );
+
+	describe( 'createLabeledInputView()', () => {
+		describe( 'image URL input view', () => {
+			it( 'should have placeholder', () => {
+				const view = createLabeledInputView( { t: val => val } );
+				expect( view.fieldView.placeholder ).to.equal( 'https://example.com/src/image.png' );
+			} );
+
+			it( 'should have info text', () => {
+				const view = createLabeledInputView( { t: val => val } );
+				expect( view.infoText ).to.match( /^Paste the image source URL/ );
+			} );
+		} );
+	} );
 } );