diff --git a/packages/ckeditor5-ckbox/src/ckboxui.ts b/packages/ckeditor5-ckbox/src/ckboxui.ts index 41325325b47..30d9d49be49 100644 --- a/packages/ckeditor5-ckbox/src/ckboxui.ts +++ b/packages/ckeditor5-ckbox/src/ckboxui.ts @@ -13,7 +13,14 @@ import { ButtonView, MenuBarMenuListItemButtonView } from 'ckeditor5/src/ui.js'; import type { ImageInsertUI } from '@ckeditor/ckeditor5-image'; /** - * The CKBoxUI plugin. It introduces the `'ckbox'` toolbar button. + * Introduces UI components for the `CKBox` plugin. + * + * The plugin introduces two UI components to the {@link module:ui/componentfactory~ComponentFactory UI component factory}: + * + * * the `'ckbox'` toolbar button, + * * the `'menuBar:ckbox'` menu bar component, which is by default added to the `'Insert'` menu. + * + * It also integrates with the `insertImage` toolbar component and `menuBar:insertImage` menu component. */ export default class CKBoxUI extends Plugin { /** @@ -35,60 +42,22 @@ export default class CKBoxUI extends Plugin { return; } - const t = editor.t; - const componentFactory = editor.ui.componentFactory; - - componentFactory.add( 'ckbox', () => { - const button = this._createButton( ButtonView ); - - button.tooltip = true; - - return button; - } ); - - componentFactory.add( 'menuBar:ckbox', () => this._createButton( MenuBarMenuListItemButtonView ) ); + editor.ui.componentFactory.add( 'ckbox', () => this._createFileToolbarButton() ); + editor.ui.componentFactory.add( 'menuBar:ckbox', () => this._createFileMenuBarButton() ); if ( editor.plugins.has( 'ImageInsertUI' ) ) { - const imageInsertUI: ImageInsertUI = editor.plugins.get( 'ImageInsertUI' ); - - imageInsertUI.registerIntegration( { + editor.plugins.get( 'ImageInsertUI' ).registerIntegration( { name: 'assetManager', observable: () => editor.commands.get( 'ckbox' )!, - - buttonViewCreator: () => { - const button = this.editor.ui.componentFactory.create( 'ckbox' ) as ButtonView; - - button.icon = icons.imageAssetManager; - button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace image with file manager' ) : - t( 'Insert image with file manager' ) - ); - - return button; - }, - - formViewCreator: () => { - const button = this.editor.ui.componentFactory.create( 'ckbox' ) as ButtonView; - - button.icon = icons.imageAssetManager; - button.withText = true; - button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace with file manager' ) : - t( 'Insert with file manager' ) - ); - - button.on( 'execute', () => { - imageInsertUI.dropdownView!.isOpen = false; - } ); - - return button; - } + buttonViewCreator: () => this._createImageToolbarButton(), + formViewCreator: () => this._createImageDropdownButton(), + menuBarButtonViewCreator: isOnly => this._createImageMenuBarButton( isOnly ? 'insertOnly' : 'insertNested' ) } ); } } /** - * Creates a button for CKBox command to use either in toolbar or in menu bar. + * Creates the base for various kinds of the button component provided by this feature. */ private _createButton( ButtonClass: T ): InstanceType { const editor = this.editor; @@ -97,11 +66,6 @@ export default class CKBoxUI extends Plugin { const command = editor.commands.get( 'ckbox' )!; const t = locale.t; - view.set( { - label: t( 'Open file manager' ), - icon: icons.browseFiles - } ); - view.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' ); view.on( 'execute', () => { @@ -110,4 +74,98 @@ export default class CKBoxUI extends Plugin { return view; } + + /** + * Creates a simple toolbar button for files management, with an icon and a tooltip. + */ + private _createFileToolbarButton(): ButtonView { + const t = this.editor.locale.t; + const button = this._createButton( ButtonView ); + + button.icon = icons.browseFiles; + button.label = t( 'Open file manager' ); + button.tooltip = true; + + return button; + } + + /** + * Creates a simple toolbar button for images management, with an icon and a tooltip. + */ + private _createImageToolbarButton(): ButtonView { + const t = this.editor.locale.t; + const imageInsertUI: ImageInsertUI = this.editor.plugins.get( 'ImageInsertUI' ); + + const button = this._createButton( ButtonView ); + + button.icon = icons.imageAssetManager; + button.bind( 'label' ).to( + imageInsertUI, + 'isImageSelected', + isImageSelected => isImageSelected ? t( 'Replace image with file manager' ) : t( 'Insert image with file manager' ) + ); + button.tooltip = true; + + return button; + } + + /** + * Creates a button for images management for the dropdown view, with an icon, text and no tooltip. + */ + private _createImageDropdownButton(): ButtonView { + const t = this.editor.locale.t; + const imageInsertUI: ImageInsertUI = this.editor.plugins.get( 'ImageInsertUI' ); + + const button = this._createButton( ButtonView ); + + button.icon = icons.imageAssetManager; + button.withText = true; + button.bind( 'label' ).to( + imageInsertUI, + 'isImageSelected', + isImageSelected => isImageSelected ? t( 'Replace with file manager' ) : t( 'Insert with file manager' ) + ); + + button.on( 'execute', () => { + imageInsertUI.dropdownView!.isOpen = false; + } ); + + return button; + } + + /** + * Creates a button for files management for the menu bar. + */ + private _createFileMenuBarButton(): MenuBarMenuListItemButtonView { + const t = this.editor.locale.t; + const button = this._createButton( MenuBarMenuListItemButtonView ); + + button.icon = icons.browseFiles; + button.withText = true; + button.label = t( 'File' ); + + return button; + } + + /** + * Creates a button for images management for the menu bar. + */ + private _createImageMenuBarButton( type: 'insertOnly' | 'insertNested' ): MenuBarMenuListItemButtonView { + const t = this.editor.locale.t; + const button = this._createButton( MenuBarMenuListItemButtonView ); + + button.icon = icons.imageAssetManager; + button.withText = true; + + switch ( type ) { + case 'insertOnly': + button.label = t( 'Image' ); + break; + case 'insertNested': + button.label = t( 'With file manager' ); + break; + } + + return button; + } } diff --git a/packages/ckeditor5-ckbox/tests/ckboxui.js b/packages/ckeditor5-ckbox/tests/ckboxui.js index 164c03a9c7b..7897883f9ed 100644 --- a/packages/ckeditor5-ckbox/tests/ckboxui.js +++ b/packages/ckeditor5-ckbox/tests/ckboxui.js @@ -110,7 +110,7 @@ describe( 'CKBoxUI', () => { button = editor.ui.componentFactory.create( 'ckbox' ); } ); - testButton( ButtonView ); + testButton( ButtonView, 'Open file manager' ); it( 'should enable tooltips for the #buttonView', () => { expect( button.tooltip ).to.be.true; @@ -122,32 +122,26 @@ describe( 'CKBoxUI', () => { button = editor.ui.componentFactory.create( 'menuBar:ckbox' ); } ); - testButton( MenuBarMenuListItemButtonView ); + testButton( MenuBarMenuListItemButtonView, 'File' ); } ); describe( 'InsertImageUI integration', () => { it( 'should create CKBox button in split button dropdown button', () => { - mockAssetManagerIntegration(); + mockAnotherIntegration(); - const spy = sinon.spy( editor.ui.componentFactory, 'create' ); const dropdown = editor.ui.componentFactory.create( 'insertImage' ); const dropdownButton = dropdown.buttonView.actionView; expect( dropdownButton ).to.be.instanceOf( ButtonView ); expect( dropdownButton.withText ).to.be.false; expect( dropdownButton.icon ).to.equal( icons.imageAssetManager ); - - expect( spy.calledTwice ).to.be.true; - expect( spy.firstCall.args[ 0 ] ).to.equal( 'insertImage' ); - expect( spy.secondCall.args[ 0 ] ).to.equal( 'ckbox' ); - expect( spy.firstCall.returnValue ).to.equal( dropdown.buttonView.actionView ); + expect( dropdownButton.label ).to.equal( 'Insert image with file manager' ); } ); it( 'should create CKBox button in dropdown panel', () => { - mockAssetManagerIntegration(); + mockAnotherIntegration(); const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - const spy = sinon.spy( editor.ui.componentFactory, 'create' ); dropdown.isOpen = true; @@ -157,16 +151,34 @@ describe( 'CKBoxUI', () => { expect( buttonView ).to.be.instanceOf( ButtonView ); expect( buttonView.withText ).to.be.true; expect( buttonView.icon ).to.equal( icons.imageAssetManager ); + expect( buttonView.label ).to.equal( 'Insert with file manager' ); + } ); + + it( 'should create CKBox button in menu bar', () => { + mockAnotherIntegration(); - expect( spy.calledOnce ).to.be.true; - expect( spy.firstCall.args[ 0 ] ).to.equal( 'ckbox' ); - expect( spy.firstCall.returnValue ).to.equal( buttonView ); + const submenu = editor.ui.componentFactory.create( 'menuBar:insertImage' ); + const buttonView = submenu.panelView.children.first.items.first.children.first; + + expect( buttonView ).to.be.instanceOf( MenuBarMenuListItemButtonView ); + expect( buttonView.withText ).to.be.true; + expect( buttonView.icon ).to.equal( icons.imageAssetManager ); + expect( buttonView.label ).to.equal( 'With file manager' ); + } ); + + it( 'should create CKBox button in menu bar - only integration', () => { + const buttonView = editor.ui.componentFactory.create( 'menuBar:insertImage' ); + + expect( buttonView ).to.be.instanceOf( MenuBarMenuListItemButtonView ); + expect( buttonView.withText ).to.be.true; + expect( buttonView.icon ).to.equal( icons.imageAssetManager ); + expect( buttonView.label ).to.equal( 'Image' ); } ); it( 'should bind to #isImageSelected', () => { const insertImageUI = editor.plugins.get( 'ImageInsertUI' ); - mockAssetManagerIntegration(); + mockAnotherIntegration(); const dropdown = editor.ui.componentFactory.create( 'insertImage' ); @@ -186,7 +198,7 @@ describe( 'CKBoxUI', () => { } ); it( 'should close dropdown on execute', () => { - mockAssetManagerIntegration(); + mockAnotherIntegration(); const dropdown = editor.ui.componentFactory.create( 'insertImage' ); @@ -203,7 +215,7 @@ describe( 'CKBoxUI', () => { } ); } ); - function testButton( Component ) { + function testButton( Component, label ) { it( 'should add the "ckbox" component to the factory if the "ckbox" command exists', () => { expect( button ).to.be.instanceOf( Component ); } ); @@ -225,7 +237,7 @@ describe( 'CKBoxUI', () => { } ); it( 'should set a #label of the #buttonView', () => { - expect( button.label ).to.equal( 'Open file manager' ); + expect( button.label ).to.equal( label ); } ); it( 'should set an #icon of the #buttonView', () => { @@ -244,7 +256,7 @@ describe( 'CKBoxUI', () => { } ); } - function mockAssetManagerIntegration() { + function mockAnotherIntegration() { const insertImageUI = editor.plugins.get( 'ImageInsertUI' ); const observable = new Model( { isEnabled: true } ); @@ -263,6 +275,13 @@ describe( 'CKBoxUI', () => { button.label = 'bar'; + return button; + }, + menuBarButtonViewCreator() { + const button = new ButtonView( editor.locale ); + + button.label = 'bar'; + return button; } } ); diff --git a/packages/ckeditor5-ckfinder/lang/contexts.json b/packages/ckeditor5-ckfinder/lang/contexts.json index f9a785ccdb7..ee2ef60989f 100644 --- a/packages/ckeditor5-ckfinder/lang/contexts.json +++ b/packages/ckeditor5-ckfinder/lang/contexts.json @@ -1,6 +1,5 @@ { "Insert image or file": "Toolbar button tooltip for inserting an image or file via a CKFinder file browser.", - "Image or file": "The label for the button that opens the dialog for inserting an image or file via a CKFinder from the application menu bar.", "Could not obtain resized image URL.": "Error message displayed when inserting a resized version of an image failed.", "Selecting resized image failed": "Title of a notification displayed when inserting a resized version of an image failed.", "Could not insert image at the current position.": "Error message displayed when an image cannot be inserted at the current position.", diff --git a/packages/ckeditor5-ckfinder/src/ckfinderui.ts b/packages/ckeditor5-ckfinder/src/ckfinderui.ts index 4b67a9be7d4..279bf448bd2 100644 --- a/packages/ckeditor5-ckfinder/src/ckfinderui.ts +++ b/packages/ckeditor5-ckfinder/src/ckfinderui.ts @@ -8,13 +8,25 @@ */ import { icons, Plugin } from 'ckeditor5/src/core.js'; -import { ButtonView, MenuBarMenuListItemButtonView } from 'ckeditor5/src/ui.js'; +import { + ButtonView, + FileDialogButtonView, + MenuBarMenuListItemButtonView, + MenuBarMenuListItemFileDialogButtonView +} from 'ckeditor5/src/ui.js'; import type { ImageInsertUI } from '@ckeditor/ckeditor5-image'; import type CKFinderCommand from './ckfindercommand.js'; /** - * The CKFinder UI plugin. It introduces the `'ckfinder'` toolbar button. + * Introduces UI components for `CKFinder` plugin. + * + * The plugin introduces two UI components to the {@link module:ui/componentfactory~ComponentFactory UI component factory}: + * + * * the `'ckfinder'` toolbar button, + * * the `'menuBar:ckfinder'` menu bar component, which is by default added to the `'Insert'` menu. + * + * It also integrates with the `insertImage` toolbar component and `menuBar:insertImage` menu component. */ export default class CKFinderUI extends Plugin { /** @@ -29,71 +41,23 @@ export default class CKFinderUI extends Plugin { */ public init(): void { const editor = this.editor; - const componentFactory = editor.ui.componentFactory; - const t = editor.t; - componentFactory.add( 'ckfinder', locale => { - const button = this._createButton( ButtonView ); - const t = locale.t; - - button.set( { - label: t( 'Insert image or file' ), - tooltip: true - } ); - - return button; - } ); - - componentFactory.add( 'menuBar:ckfinder', locale => { - const button = this._createButton( MenuBarMenuListItemButtonView ); - const t = locale.t; - - button.label = t( 'Image or file' ); - - return button; - } ); + editor.ui.componentFactory.add( 'ckfinder', () => this._createFileToolbarButton() ); + editor.ui.componentFactory.add( 'menuBar:ckfinder', () => this._createFileMenuBarButton() ); if ( editor.plugins.has( 'ImageInsertUI' ) ) { - const imageInsertUI: ImageInsertUI = editor.plugins.get( 'ImageInsertUI' ); - - imageInsertUI.registerIntegration( { + editor.plugins.get( 'ImageInsertUI' ).registerIntegration( { name: 'assetManager', observable: () => editor.commands.get( 'ckfinder' )!, - - buttonViewCreator: () => { - const button = this.editor.ui.componentFactory.create( 'ckfinder' ) as ButtonView; - - button.icon = icons.imageAssetManager; - button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace image with file manager' ) : - t( 'Insert image with file manager' ) - ); - - return button; - }, - - formViewCreator: () => { - const button = this.editor.ui.componentFactory.create( 'ckfinder' ) as ButtonView; - - button.icon = icons.imageAssetManager; - button.withText = true; - button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace with file manager' ) : - t( 'Insert with file manager' ) - ); - - button.on( 'execute', () => { - imageInsertUI.dropdownView!.isOpen = false; - } ); - - return button; - } + buttonViewCreator: () => this._createImageToolbarButton(), + formViewCreator: () => this._createImageDropdownButton(), + menuBarButtonViewCreator: isOnly => this._createImageMenuBarButton( isOnly ? 'insertOnly' : 'insertNested' ) } ); } } /** - * Creates a button for CKFinder command to use either in toolbar or in menu bar. + * Creates the base for various kinds of the button component provided by this feature. */ private _createButton( ButtonClass: T ): InstanceType { const editor = this.editor; @@ -101,8 +65,6 @@ export default class CKFinderUI extends Plugin { const view = new ButtonClass( locale ) as InstanceType; const command: CKFinderCommand = editor.commands.get( 'ckfinder' )!; - view.icon = icons.browseFiles; - view.bind( 'isEnabled' ).to( command ); view.on( 'execute', () => { @@ -112,4 +74,98 @@ export default class CKFinderUI extends Plugin { return view; } + + /** + * Creates a simple toolbar button for files management, with an icon and a tooltip. + */ + private _createFileToolbarButton(): ButtonView { + const t = this.editor.locale.t; + const button = this._createButton( ButtonView ); + + button.icon = icons.browseFiles; + button.label = t( 'Insert image or file' ); + button.tooltip = true; + + return button; + } + + /** + * Creates a simple toolbar button for images management, with an icon and a tooltip. + */ + private _createImageToolbarButton(): ButtonView { + const t = this.editor.locale.t; + const imageInsertUI: ImageInsertUI = this.editor.plugins.get( 'ImageInsertUI' ); + + const button = this._createButton( ButtonView ); + + button.icon = icons.imageAssetManager; + button.bind( 'label' ).to( + imageInsertUI, + 'isImageSelected', + isImageSelected => isImageSelected ? t( 'Replace image with file manager' ) : t( 'Insert image with file manager' ) + ); + button.tooltip = true; + + return button; + } + + /** + * Creates a button for images management for the dropdown view, with an icon, text and no tooltip. + */ + private _createImageDropdownButton(): ButtonView { + const t = this.editor.locale.t; + const imageInsertUI: ImageInsertUI = this.editor.plugins.get( 'ImageInsertUI' ); + + const button = this._createButton( ButtonView ); + + button.icon = icons.imageAssetManager; + button.withText = true; + button.bind( 'label' ).to( + imageInsertUI, + 'isImageSelected', + isImageSelected => isImageSelected ? t( 'Replace with file manager' ) : t( 'Insert with file manager' ) + ); + + button.on( 'execute', () => { + imageInsertUI.dropdownView!.isOpen = false; + } ); + + return button; + } + + /** + * Creates a button for files management for the menu bar. + */ + private _createFileMenuBarButton(): MenuBarMenuListItemButtonView { + const t = this.editor.locale.t; + const button = this._createButton( MenuBarMenuListItemButtonView ); + + button.icon = icons.browseFiles; + button.withText = true; + button.label = t( 'File' ); + + return button; + } + + /** + * Creates a button for images management for the menu bar. + */ + private _createImageMenuBarButton( type: 'insertOnly' | 'insertNested' ): MenuBarMenuListItemButtonView { + const t = this.editor.locale.t; + const button = this._createButton( MenuBarMenuListItemButtonView ); + + button.icon = icons.imageAssetManager; + button.withText = true; + + switch ( type ) { + case 'insertOnly': + button.label = t( 'Image' ); + break; + case 'insertNested': + button.label = t( 'With file manager' ); + break; + } + + return button; + } } diff --git a/packages/ckeditor5-ckfinder/tests/ckfinderui.js b/packages/ckeditor5-ckfinder/tests/ckfinderui.js index fbd7859e0ad..58dcc1cff17 100644 --- a/packages/ckeditor5-ckfinder/tests/ckfinderui.js +++ b/packages/ckeditor5-ckfinder/tests/ckfinderui.js @@ -65,7 +65,7 @@ describe( 'CKFinderUI', () => { button = editor.ui.componentFactory.create( 'menuBar:ckfinder' ); } ); - testButton( 'Image or file' ); + testButton( 'File' ); it( 'should add the "ckfinder" component to the factory', () => { expect( button ).to.be.instanceOf( MenuBarMenuListItemButtonView ); @@ -74,27 +74,21 @@ describe( 'CKFinderUI', () => { describe( 'InsertImageUI integration', () => { it( 'should create CKFinder button in split button dropdown button', () => { - mockAssetManagerIntegration(); + mockAnotherIntegration(); - const spy = sinon.spy( editor.ui.componentFactory, 'create' ); const dropdown = editor.ui.componentFactory.create( 'insertImage' ); const dropdownButton = dropdown.buttonView.actionView; expect( dropdownButton ).to.be.instanceOf( ButtonView ); expect( dropdownButton.withText ).to.be.false; expect( dropdownButton.icon ).to.equal( icons.imageAssetManager ); - - expect( spy.calledTwice ).to.be.true; - expect( spy.firstCall.args[ 0 ] ).to.equal( 'insertImage' ); - expect( spy.secondCall.args[ 0 ] ).to.equal( 'ckfinder' ); - expect( spy.firstCall.returnValue ).to.equal( dropdown.buttonView.actionView ); + expect( dropdownButton.label ).to.equal( 'Insert image with file manager' ); } ); it( 'should create CKFinder button in dropdown panel', () => { - mockAssetManagerIntegration(); + mockAnotherIntegration(); const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - const spy = sinon.spy( editor.ui.componentFactory, 'create' ); dropdown.isOpen = true; @@ -104,16 +98,34 @@ describe( 'CKFinderUI', () => { expect( buttonView ).to.be.instanceOf( ButtonView ); expect( buttonView.withText ).to.be.true; expect( buttonView.icon ).to.equal( icons.imageAssetManager ); + expect( buttonView.label ).to.equal( 'Insert with file manager' ); + } ); + + it( 'should create CKFinder button in menu bar', () => { + mockAnotherIntegration(); - expect( spy.calledOnce ).to.be.true; - expect( spy.firstCall.args[ 0 ] ).to.equal( 'ckfinder' ); - expect( spy.firstCall.returnValue ).to.equal( buttonView ); + const submenu = editor.ui.componentFactory.create( 'menuBar:insertImage' ); + const buttonView = submenu.panelView.children.first.items.first.children.first; + + expect( buttonView ).to.be.instanceOf( MenuBarMenuListItemButtonView ); + expect( buttonView.withText ).to.be.true; + expect( buttonView.icon ).to.equal( icons.imageAssetManager ); + expect( buttonView.label ).to.equal( 'With file manager' ); + } ); + + it( 'should create CKFinder button in menu bar - only integration', () => { + const buttonView = editor.ui.componentFactory.create( 'menuBar:insertImage' ); + + expect( buttonView ).to.be.instanceOf( MenuBarMenuListItemButtonView ); + expect( buttonView.withText ).to.be.true; + expect( buttonView.icon ).to.equal( icons.imageAssetManager ); + expect( buttonView.label ).to.equal( 'Image' ); } ); it( 'should bind to #isImageSelected', () => { const insertImageUI = editor.plugins.get( 'ImageInsertUI' ); - mockAssetManagerIntegration(); + mockAnotherIntegration(); const dropdown = editor.ui.componentFactory.create( 'insertImage' ); @@ -133,7 +145,7 @@ describe( 'CKFinderUI', () => { } ); it( 'should close dropdown on execute', () => { - mockAssetManagerIntegration(); + mockAnotherIntegration(); const dropdown = editor.ui.componentFactory.create( 'insertImage' ); @@ -182,7 +194,7 @@ describe( 'CKFinderUI', () => { } ); } - function mockAssetManagerIntegration() { + function mockAnotherIntegration() { const insertImageUI = editor.plugins.get( 'ImageInsertUI' ); const observable = new Model( { isEnabled: true } ); @@ -201,6 +213,13 @@ describe( 'CKFinderUI', () => { button.label = 'bar'; + return button; + }, + menuBarButtonViewCreator() { + const button = new ButtonView( editor.locale ); + + button.label = 'bar'; + return button; } } ); diff --git a/packages/ckeditor5-core/lang/contexts.json b/packages/ckeditor5-core/lang/contexts.json index 33c930ff7af..cea75c8c4bb 100644 --- a/packages/ckeditor5-core/lang/contexts.json +++ b/packages/ckeditor5-core/lang/contexts.json @@ -12,6 +12,8 @@ "Replace with file manager": "The label for the replace image with the file manager toolbar button with visible label in insert image dropdown.", "Insert image with file manager": "The label for the insert image with the file manager toolbar button.", "Replace image with file manager": "The label for the replace image with the file manager toolbar button.", + "File": "The label for a button that opens a file manager in order to insert a file.", + "With file manager": "The label for the insert image with the file manager menu bar button (inside 'Insert' menu)", "Toggle caption off": "The button label for the object (e.g. image, table) toolbar for hiding the attached caption.", "Toggle caption on": "The button label for the object (e.g. image, table) toolbar for showing the attached caption.", "Content editing keystrokes": "Accessibility help dialog category header text for keystrokes related to content creation.", diff --git a/packages/ckeditor5-image/ckeditor5-metadata.json b/packages/ckeditor5-image/ckeditor5-metadata.json index 144e2ac02d1..ed183f188c3 100644 --- a/packages/ckeditor5-image/ckeditor5-metadata.json +++ b/packages/ckeditor5-image/ckeditor5-metadata.json @@ -230,7 +230,7 @@ "uiComponents": [ { "type": "Button", - "name": "imageUpload", + "name": "uploadImage", "iconPath": "@ckeditor/ckeditor5-core/theme/icons/image.svg" } ] @@ -247,8 +247,13 @@ "uiComponents": [ { "type": "SplitButton", - "name": "imageInsert", + "name": "insertImage", "iconPath": "@ckeditor/ckeditor5-core/theme/icons/image.svg" + }, + { + "type": "Button", + "name": "insertImageViaUrl", + "iconPath": "@ckeditor/ckeditor5-core/theme/icons/image-url.svg" } ] } diff --git a/packages/ckeditor5-image/docs/features/images-inserting.md b/packages/ckeditor5-image/docs/features/images-inserting.md index ebc6aea3904..96abbdd736e 100644 --- a/packages/ckeditor5-image/docs/features/images-inserting.md +++ b/packages/ckeditor5-image/docs/features/images-inserting.md @@ -76,11 +76,13 @@ If the automatic embedding was unexpected, for instance when the link was meant The {@link module:image/image~Image} plugin registers: +* The `'insertImage'` toolbar dropdown component that aggregates all image insert methods available in the current editor setup. +* The `'insertImageViaUrl'` toolbar button that opens a modal dialog to let you insert an image by specifying the image URL. * The {@link module:image/image/insertimagecommand~InsertImageCommand `'insertImage'` command} that accepts a source (for example a URL) of an image to insert. The {@link module:image/imageupload~ImageUpload} plugin registers: -* The `'uploadImage'` button that opens the native file browser to let you upload a file directly from your disk (to use in the {@link features/images-overview#image-contextual-toolbar image toolbar}). +* The `'uploadImage'` toolbar button that opens the native file browser to let you upload a file directly from your disk (to use in the {@link features/images-overview#image-contextual-toolbar image toolbar}). * The {@link module:image/imageupload/uploadimagecommand~UploadImageCommand `'uploadImage'` command} that accepts the file to upload. diff --git a/packages/ckeditor5-image/lang/contexts.json b/packages/ckeditor5-image/lang/contexts.json index 2ae5515f3f3..25beebd491a 100644 --- a/packages/ckeditor5-image/lang/contexts.json +++ b/packages/ckeditor5-image/lang/contexts.json @@ -16,7 +16,8 @@ "Upload from computer": "The label for the upload image from computer toolbar button with visible label in insert image dropdown.", "Replace from computer": "The label for the replace image by upload from computer toolbar button with visible label in insert image dropdown.", "Upload image from computer": "The label for the upload image from computer toolbar button.", - "Image from computer": "The label for the upload image from computer menu bar button.", + "Image from computer": "The label for the upload image from computer menu bar button (standalone button).", + "From computer": "The label for the upload image from computer menu bar button (inside 'Image' menu).", "Replace image from computer": "The label for the replace image by upload from computer toolbar button.", "Upload failed": "The title of the notification displayed when upload fails.", "Image toolbar": "The label used by assistive technologies describing an image toolbar attached to an image widget.", @@ -28,9 +29,10 @@ "Custom image size": "The accessibility label of the standalone image resize custom option button in the image toolbar for screen readers.", "Custom": "The label for the resize option that allows user to enter custom size of the image.", "Image resize list": "The accessibility label of the image resize dropdown for screen readers.", - "Insert": "The label of the form submit button if the image source URL input has no value.", - "Update": "The label of the form submit button if the image source URL input has a value.", "Insert image via URL": "The input label for the Insert image via URL form.", + "Insert via URL": "The label for the insert image via url dropdown button.", + "Image via URL": "The label for the insert image via url menu bar button (standalone button).", + "Via URL": "The label for the insert image via url menu bar button (inside 'Image' menu).", "Update image URL": "The input label for the Insert image via URL form for a pre-existing image.", "Caption for the image": "Text used by screen readers do describe an image when the image has no text alternative.", "Caption for image: %0": "Text used by screen readers do describe an image when there is a text alternative available, e.g. 'Caption for image: this is a description of the image.'", @@ -38,5 +40,6 @@ "The value should be a plain number.": "Text used as error label when user submitted custom image resize form with incorrect value.", "Uploading image": "Aria status message indicating that the image is being uploaded. Example: 'Uploading image'.", "Image upload complete": "Aria status message indicating that the image has been uploaded successfully. Example: 'Image upload complete'.", - "Error during image upload": "Aria status message indicating that an error has occurred during image upload. Example: 'Error during image upload'." + "Error during image upload": "Aria status message indicating that an error has occurred during image upload. Example: 'Error during image upload'.", + "Image": "Label for the widget inserted by the image feature (as in 'insert image')" } diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index e353ccb2084..351989dfee8 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -9,7 +9,8 @@ import { Plugin, - type Editor + type Editor, + icons } from 'ckeditor5/src/core.js'; import { logWarning, @@ -18,11 +19,16 @@ import { } from 'ckeditor5/src/utils.js'; import { createDropdown, - SplitButtonView, type ButtonView, type DropdownButtonView, type DropdownView, - type FocusableView + type FocusableView, + type MenuBarMenuListItemButtonView, + MenuBarMenuListItemView, + MenuBarMenuListView, + MenuBarMenuView, + SplitButtonView, + type View } from 'ckeditor5/src/ui.js'; import ImageInsertFormView from './ui/imageinsertformview.js'; @@ -36,6 +42,9 @@ import ImageUtils from '../imageutils.js'; * * Adds the `'insertImage'` dropdown to the {@link module:ui/componentfactory~ComponentFactory UI component factory} * and also the `imageInsert` dropdown as an alias for backward compatibility. + * + * Adds the `'menuBar:insertImage'` sub-menu to the {@link module:ui/componentfactory~ComponentFactory UI component factory}, which is + * by default added to the `'Insert'` menu. */ export default class ImageInsertUI extends Plugin { /** @@ -97,10 +106,13 @@ export default class ImageInsertUI extends Plugin { } ); const componentCreator = ( locale: Locale ) => this._createToolbarComponent( locale ); + const menuBarComponentCreator = ( locale: Locale ) => this._createMenuBarComponent( locale ); // Register `insertImage` dropdown and add `imageInsert` dropdown as an alias for backward compatibility. editor.ui.componentFactory.add( 'insertImage', componentCreator ); editor.ui.componentFactory.add( 'imageInsert', componentCreator ); + + editor.ui.componentFactory.add( 'menuBar:insertImage', menuBarComponentCreator ); } /** @@ -111,12 +123,14 @@ export default class ImageInsertUI extends Plugin { observable, buttonViewCreator, formViewCreator, - requiresForm + menuBarButtonViewCreator, + requiresForm = false }: { name: string; observable: Observable & { isEnabled: boolean } | ( () => Observable & { isEnabled: boolean } ); buttonViewCreator: ( isOnlyOne: boolean ) => ButtonView; formViewCreator: ( isOnlyOne: boolean ) => FocusableView; + menuBarButtonViewCreator: ( isOnlyOne: boolean ) => MenuBarMenuListItemButtonView; requiresForm?: boolean; } ): void { if ( this._integrations.has( name ) ) { @@ -133,8 +147,9 @@ export default class ImageInsertUI extends Plugin { this._integrations.set( name, { observable, buttonViewCreator, + menuBarButtonViewCreator, formViewCreator, - requiresForm: !!requiresForm + requiresForm } ); } @@ -190,6 +205,45 @@ export default class ImageInsertUI extends Plugin { return dropdownView; } + /** + * Creates the menu bar component. + */ + private _createMenuBarComponent( locale: Locale ): View { + const t = locale.t; + + const integrations = this._prepareIntegrations(); + + if ( !integrations.length ) { + return null as any; + } + + let resultView: MenuBarMenuListItemButtonView | MenuBarMenuView | undefined; + const firstIntegration = integrations[ 0 ]; + + if ( integrations.length == 1 ) { + resultView = firstIntegration.menuBarButtonViewCreator( true ); + } else { + resultView = new MenuBarMenuView( locale ); + const listView = new MenuBarMenuListView( locale ); + resultView.panelView.children.add( listView ); + + resultView.buttonView.set( { + icon: icons.image, + label: t( 'Image' ) + } ); + + for ( const integration of integrations ) { + const listItemView = new MenuBarMenuListItemView( locale, resultView ); + const buttonView = integration.menuBarButtonViewCreator( false ); + + listItemView.children.add( buttonView ); + listView.items.add( listItemView ); + } + } + + return resultView; + } + /** * Validates the integrations list. */ @@ -252,6 +306,7 @@ export default class ImageInsertUI extends Plugin { type IntegrationData = { observable: Observable & { isEnabled: boolean } | ( () => Observable & { isEnabled: boolean } ); buttonViewCreator: ( isOnlyOne: boolean ) => ButtonView; + menuBarButtonViewCreator: ( isOnlyOne: boolean ) => MenuBarMenuListItemButtonView; formViewCreator: ( isOnlyOne: boolean ) => FocusableView; requiresForm: boolean; }; diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertviaurlui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertviaurlui.ts index 41e56f7bb03..569fd7d0c9a 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertviaurlui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertviaurlui.ts @@ -8,20 +8,23 @@ */ import { icons, Plugin } from 'ckeditor5/src/core.js'; -import { ButtonView, CollapsibleView, DropdownButtonView, type FocusableView } from 'ckeditor5/src/ui.js'; +import { ButtonView, Dialog, MenuBarMenuListItemButtonView } from 'ckeditor5/src/ui.js'; import ImageInsertUI from './imageinsertui.js'; import type InsertImageCommand from '../image/insertimagecommand.js'; import type ReplaceImageSourceCommand from '../image/replaceimagesourcecommand.js'; -import ImageInsertUrlView, { type ImageInsertUrlViewCancelEvent, type ImageInsertUrlViewSubmitEvent } from './ui/imageinserturlview.js'; +import ImageInsertUrlView from './ui/imageinserturlview.js'; /** * The image insert via URL plugin (UI part). * - * For a detailed overview, check the {@glink features/images/images-inserting - * Insert images via source URL} documentation. + * The plugin introduces two UI components to the {@link module:ui/componentfactory~ComponentFactory UI component factory}: * - * This plugin registers the {@link module:image/imageinsert/imageinsertui~ImageInsertUI} integration for `url`. + * * the `'insertImageViaUrl'` toolbar button, + * * the `'menuBar:insertImageViaUrl'` menu bar component. + * + * It also integrates with the `insertImage` toolbar component and `menuBar:insertImage` menu component, which are default components + * through which inserting image via URL is available. */ export default class ImageInsertViaUrlUI extends Plugin { private _imageInsertUI!: ImageInsertUI; @@ -37,7 +40,12 @@ export default class ImageInsertViaUrlUI extends Plugin { * @inheritDoc */ public static get requires() { - return [ ImageInsertUI ] as const; + return [ ImageInsertUI, Dialog ] as const; + } + + public init(): void { + this.editor.ui.componentFactory.add( 'insertImageViaUrl', () => this._createToolbarButton() ); + this.editor.ui.componentFactory.add( 'menuBar:insertImageViaUrl', () => this._createMenuBarButton( 'standalone' ) ); } /** @@ -49,25 +57,97 @@ export default class ImageInsertViaUrlUI extends Plugin { this._imageInsertUI.registerIntegration( { name: 'url', observable: () => this.editor.commands.get( 'insertImage' )!, - requiresForm: true, - buttonViewCreator: isOnlyOne => this._createInsertUrlButton( isOnlyOne ), - formViewCreator: isOnlyOne => this._createInsertUrlView( isOnlyOne ) + buttonViewCreator: () => this._createToolbarButton(), + formViewCreator: () => this._createDropdownButton(), + menuBarButtonViewCreator: isOnly => this._createMenuBarButton( isOnly ? 'insertOnly' : 'insertNested' ) } ); } /** - * Creates the view displayed in the dropdown. + * Creates the base for various kinds of the button component provided by this feature. */ - private _createInsertUrlView( isOnlyOne: boolean ): FocusableView { + private _createInsertUrlButton( + ButtonClass: T + ): InstanceType { + const button = new ButtonClass( this.editor.locale ) as InstanceType; + + button.icon = icons.imageUrl; + button.on( 'execute', () => { + this._showModal(); + } ); + + return button; + } + + /** + * Creates a simple toolbar button, with an icon and a tooltip. + */ + private _createToolbarButton(): ButtonView { + const t = this.editor.locale.t; + const button = this._createInsertUrlButton( ButtonView ); + + button.tooltip = true; + button.bind( 'label' ).to( + this._imageInsertUI, + 'isImageSelected', + isImageSelected => isImageSelected ? t( 'Update image URL' ) : t( 'Insert image via URL' ) + ); + + return button; + } + + /** + * Creates a button for the dropdown view, with an icon, text and no tooltip. + */ + private _createDropdownButton(): ButtonView { + const t = this.editor.locale.t; + const button = this._createInsertUrlButton( ButtonView ); + + button.withText = true; + button.bind( 'label' ).to( + this._imageInsertUI, + 'isImageSelected', + isImageSelected => isImageSelected ? t( 'Update image URL' ) : t( 'Insert via URL' ) + ); + + return button; + } + + /** + * Creates a button for the menu bar. + */ + private _createMenuBarButton( type: 'standalone' | 'insertOnly' | 'insertNested' ): MenuBarMenuListItemButtonView { + const t = this.editor.locale.t; + const button = this._createInsertUrlButton( MenuBarMenuListItemButtonView ); + + button.withText = true; + + switch ( type ) { + case 'standalone': + button.label = t( 'Image via URL' ); + break; + case 'insertOnly': + button.label = t( 'Image' ); + break; + case 'insertNested': + button.label = t( 'Via URL' ); + break; + } + + return button; + } + + /** + * Creates the form view used to submit the image URL. + */ + private _createInsertUrlView(): ImageInsertUrlView { const editor = this.editor; const locale = editor.locale; - const t = locale.t; const replaceImageSourceCommand: ReplaceImageSourceCommand = editor.commands.get( 'replaceImageSource' )!; const insertImageCommand: InsertImageCommand = editor.commands.get( 'insertImage' )!; const imageInsertUrlView = new ImageInsertUrlView( locale ); - const collapsibleView = isOnlyOne ? null : new CollapsibleView( locale, [ imageInsertUrlView ] ); imageInsertUrlView.bind( 'isImageSelected' ).to( this._imageInsertUI ); imageInsertUrlView.bind( 'isEnabled' ).toMany( [ insertImageCommand, replaceImageSourceCommand ], 'isEnabled', ( ...isEnabled ) => ( @@ -77,80 +157,57 @@ export default class ImageInsertViaUrlUI extends Plugin { // Set initial value because integrations are created on first dropdown open. imageInsertUrlView.imageURLInputValue = replaceImageSourceCommand.value || ''; - this._imageInsertUI.dropdownView!.on( 'change:isOpen', () => { - if ( this._imageInsertUI.dropdownView!.isOpen ) { - // Make sure that each time the panel shows up, the URL field remains in sync with the value of - // the command. If the user typed in the input, then canceled and re-opened it without changing - // the value of the media command (e.g. because they didn't change the selection), they would see - // the old value instead of the actual value of the command. - imageInsertUrlView.imageURLInputValue = replaceImageSourceCommand.value || ''; - - if ( collapsibleView ) { - collapsibleView.isCollapsed = true; - } - } - - // Note: Use the low priority to make sure the following listener starts working after the - // default action of the drop-down is executed (i.e. the panel showed up). Otherwise, the - // invisible form/input cannot be focused/selected. - }, { priority: 'low' } ); - - imageInsertUrlView.on( 'submit', () => { - if ( replaceImageSourceCommand.isEnabled ) { - editor.execute( 'replaceImageSource', { source: imageInsertUrlView.imageURLInputValue } ); - } else { - editor.execute( 'insertImage', { source: imageInsertUrlView.imageURLInputValue } ); - } - - this._closePanel(); - } ); - - imageInsertUrlView.on( 'cancel', () => this._closePanel() ); - - if ( collapsibleView ) { - collapsibleView.set( { - isCollapsed: true - } ); - - collapsibleView.bind( 'label' ).to( this._imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Update image URL' ) : - t( 'Insert image via URL' ) - ); - - return collapsibleView; - } - return imageInsertUrlView; } /** - * Creates the toolbar button. + * Shows the insert image via URL form view in a modal. */ - private _createInsertUrlButton( isOnlyOne: boolean ): ButtonView { - const ButtonClass = isOnlyOne ? DropdownButtonView : ButtonView; - + private _showModal() { const editor = this.editor; - const button = new ButtonClass( editor.locale ); - const t = editor.locale.t; - - button.set( { - icon: icons.imageUrl, - tooltip: true - } ); + const locale = editor.locale; + const t = locale.t; + const dialog = editor.plugins.get( 'Dialog' ); - button.bind( 'label' ).to( this._imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Update image URL' ) : - t( 'Insert image via URL' ) - ); + const form = this._createInsertUrlView(); - return button; + dialog.show( { + id: 'insertImageViaUrl', + title: this._imageInsertUI.isImageSelected ? + t( 'Update image URL' ) : + t( 'Insert image via URL' ), + isModal: true, + content: form, + actionButtons: [ + { + label: t( 'Cancel' ), + withText: true, + onExecute: () => dialog.hide() + }, + { + label: t( 'Accept' ), + class: 'ck-button-action', + withText: true, + onExecute: () => this._handleSave( form as ImageInsertUrlView ) + } + ] + } ); } /** - * Closes the dropdown. + * Executes appropriate command depending on selection and form value. */ - private _closePanel(): void { - this.editor.editing.view.focus(); - this._imageInsertUI.dropdownView!.isOpen = false; + private _handleSave( form: ImageInsertUrlView ) { + const replaceImageSourceCommand: ReplaceImageSourceCommand = this.editor.commands.get( 'replaceImageSource' )!; + + // If an image element is currently selected, we want to replace its source attribute (instead of inserting a new image). + // We detect if an image is selected by checking `replaceImageSource` command state. + if ( replaceImageSourceCommand.isEnabled ) { + this.editor.execute( 'replaceImageSource', { source: form.imageURLInputValue } ); + } else { + this.editor.execute( 'insertImage', { source: form.imageURLInputValue } ); + } + + this.editor.plugins.get( 'Dialog' ).hide(); } } diff --git a/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts b/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts index 24a36c5a8a7..325fed5424c 100644 --- a/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts +++ b/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts @@ -13,8 +13,6 @@ import { submitHandler, FocusCycler, CollapsibleView, - type FocusCyclerForwardCycleEvent, - type FocusCyclerBackwardCycleEvent, type FocusableView } from 'ckeditor5/src/ui.js'; import { FocusTracker, KeystrokeHandler, type Locale } from 'ckeditor5/src/utils.js'; @@ -88,22 +86,6 @@ export default class ImageInsertFormView extends View { } } - if ( this._focusables.length > 1 ) { - for ( const view of this._focusables ) { - if ( isViewWithFocusCycler( view ) ) { - view.focusCycler.on( 'forwardCycle', evt => { - this._focusCycler.focusNext(); - evt.stop(); - } ); - - view.focusCycler.on( 'backwardCycle', evt => { - this._focusCycler.focusPrevious(); - evt.stop(); - } ); - } - } - } - this.setTemplate( { tag: 'form', @@ -164,7 +146,3 @@ export default class ImageInsertFormView extends View { this._focusCycler.focusFirst(); } } - -function isViewWithFocusCycler( view: View ): view is View & { focusCycler: FocusCycler } { - return 'focusCycler' in view; -} diff --git a/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts b/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts index 413471e12be..9b6960a3b62 100644 --- a/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts +++ b/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts @@ -7,18 +7,13 @@ * @module image/imageinsert/ui/imageinserturlview */ -import { icons } from 'ckeditor5/src/core.js'; import { - ButtonView, View, - ViewCollection, - FocusCycler, LabeledFieldView, createLabeledInputText, - type InputTextView, - type FocusableView + type InputTextView } from 'ckeditor5/src/ui.js'; -import { FocusTracker, KeystrokeHandler, type Locale } from 'ckeditor5/src/utils.js'; +import { KeystrokeHandler, type Locale } from 'ckeditor5/src/utils.js'; /** * The insert an image via URL view. @@ -31,16 +26,6 @@ export default class ImageInsertUrlView extends View { */ public urlInputView: LabeledFieldView; - /** - * The "insert/update" button view. - */ - public insertButtonView: ButtonView; - - /** - * The "cancel" button view. - */ - public cancelButtonView: ButtonView; - /** * The value of the URL input. * @@ -62,26 +47,11 @@ export default class ImageInsertUrlView extends View { */ declare public isEnabled: boolean; - /** - * Tracks information about DOM focus in the form. - */ - public readonly focusTracker: FocusTracker; - /** * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}. */ public readonly keystrokes: KeystrokeHandler; - /** - * Helps cycling over {@link #_focusables} in the form. - */ - public readonly focusCycler: FocusCycler; - - /** - * A collection of views that can be focused in the form. - */ - private readonly _focusables: ViewCollection; - /** * Creates a view for the dropdown panel of {@link module:image/imageinsert/imageinsertui~ImageInsertUI}. * @@ -94,32 +64,9 @@ export default class ImageInsertUrlView extends View { this.set( 'isImageSelected', false ); this.set( 'isEnabled', true ); - this.focusTracker = new FocusTracker(); this.keystrokes = new KeystrokeHandler(); - this._focusables = new ViewCollection(); - - this.focusCycler = new FocusCycler( { - focusables: this._focusables, - focusTracker: this.focusTracker, - keystrokeHandler: this.keystrokes, - actions: { - // Navigate form fields backwards using the Shift + Tab keystroke. - focusPrevious: 'shift + tab', - - // Navigate form fields forwards using the Tab key. - focusNext: 'tab' - } - } ); this.urlInputView = this._createUrlInputView(); - this.insertButtonView = this._createInsertButton(); - this.cancelButtonView = this._createCancelButton(); - - this._focusables.addMany( [ - this.urlInputView, - this.insertButtonView, - this.cancelButtonView - ] ); this.setTemplate( { tag: 'div', @@ -140,12 +87,7 @@ export default class ImageInsertUrlView extends View { 'ck', 'ck-image-insert-url__action-row' ] - }, - - children: [ - this.insertButtonView, - this.cancelButtonView - ] + } } ] } ); @@ -157,10 +99,6 @@ export default class ImageInsertUrlView extends View { public override render(): void { super.render(); - for ( const view of this._focusables ) { - this.focusTracker.add( view.element! ); - } - // Start listening for the keystrokes coming from #element. this.keystrokes.listenTo( this.element! ); } @@ -171,7 +109,6 @@ export default class ImageInsertUrlView extends View { public override destroy(): void { super.destroy(); - this.focusTracker.destroy(); this.keystrokes.destroy(); } @@ -189,6 +126,7 @@ export default class ImageInsertUrlView extends View { urlInputView.bind( 'isEnabled' ).to( this ); + urlInputView.fieldView.inputMode = 'url'; urlInputView.fieldView.placeholder = 'https://example.com/image.png'; urlInputView.fieldView.bind( 'value' ).to( this, 'imageURLInputValue', ( value: string ) => value || '' ); @@ -199,81 +137,10 @@ export default class ImageInsertUrlView extends View { return urlInputView; } - /** - * Creates the {@link #insertButtonView}. - */ - private _createInsertButton(): ButtonView { - const locale = this.locale!; - const t = locale.t; - const insertButtonView = new ButtonView( locale ); - - insertButtonView.set( { - icon: icons.check, - class: 'ck-button-save', - type: 'submit', - withText: true - } ); - - insertButtonView.bind( 'label' ).to( this, 'isImageSelected', value => value ? t( 'Update' ) : t( 'Insert' ) ); - insertButtonView.bind( 'isEnabled' ).to( this, 'imageURLInputValue', this, 'isEnabled', - ( ...values ) => values.every( value => value ) - ); - - insertButtonView.delegate( 'execute' ).to( this, 'submit' ); - - return insertButtonView; - } - - /** - * Creates the {@link #cancelButtonView}. - */ - private _createCancelButton(): ButtonView { - const locale = this.locale!; - const t = locale.t; - const cancelButtonView = new ButtonView( locale ); - - cancelButtonView.set( { - label: t( 'Cancel' ), - icon: icons.cancel, - class: 'ck-button-cancel', - withText: true - } ); - - cancelButtonView.bind( 'isEnabled' ).to( this ); - - cancelButtonView.delegate( 'execute' ).to( this, 'cancel' ); - - return cancelButtonView; - } - /** * Focuses the view. */ - public focus( direction: 1 | -1 ): void { - if ( direction === -1 ) { - this.focusCycler.focusLast(); - } else { - this.focusCycler.focusFirst(); - } + public focus(): void { + this.urlInputView.focus(); } } - -/** - * Fired when the form view is submitted. - * - * @eventName ~ImageInsertUrlView#submit - */ -export type ImageInsertUrlViewSubmitEvent = { - name: 'submit'; - args: []; -}; - -/** - * Fired when the form view is canceled. - * - * @eventName ~ImageInsertUrlView#cancel - */ -export type ImageInsertUrlViewCancelEvent = { - name: 'cancel'; - args: []; -}; diff --git a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts index 648fda2d422..4f2a56c03fc 100644 --- a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts +++ b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts @@ -9,8 +9,8 @@ import { Plugin, icons } from 'ckeditor5/src/core.js'; import { - FileDialogButtonView, - MenuBarMenuListItemFileDialogButtonView + FileDialogButtonView, MenuBarMenuListItemFileDialogButtonView, + type ButtonView, type MenuBarMenuListItemButtonView } from 'ckeditor5/src/ui.js'; import { createImageTypeRegExp } from './utils.js'; import type ImageInsertUI from '../imageinsert/imageinsertui.js'; @@ -22,6 +22,11 @@ import type ImageInsertUI from '../imageinsert/imageinsertui.js'; * * Adds the `'uploadImage'` button to the {@link module:ui/componentfactory~ComponentFactory UI component factory} * and also the `imageUpload` button as an alias for backward compatibility. + * + * Adds the `'menuBar:uploadImage'` menu button to the {@link module:ui/componentfactory~ComponentFactory UI component factory}. + * + * It also integrates with the `insertImage` toolbar component and `menuBar:insertImage` menu component, which are the default components + * through which image upload is available. */ export default class ImageUploadUI extends Plugin { /** @@ -36,70 +41,26 @@ export default class ImageUploadUI extends Plugin { */ public init(): void { const editor = this.editor; - const t = editor.t; - - const toolbarComponentCreator = () => { - const button = this._createButton( FileDialogButtonView ); - - button.set( { - label: t( 'Upload image from computer' ), - tooltip: true - } ); - - return button; - }; // Setup `uploadImage` button and add `imageUpload` button as an alias for backward compatibility. - editor.ui.componentFactory.add( 'uploadImage', toolbarComponentCreator ); - editor.ui.componentFactory.add( 'imageUpload', toolbarComponentCreator ); - - editor.ui.componentFactory.add( 'menuBar:uploadImage', () => { - const button = this._createButton( MenuBarMenuListItemFileDialogButtonView ); + editor.ui.componentFactory.add( 'uploadImage', () => this._createToolbarButton() ); + editor.ui.componentFactory.add( 'imageUpload', () => this._createToolbarButton() ); - button.label = t( 'Image from computer' ); - - return button; - } ); + editor.ui.componentFactory.add( 'menuBar:uploadImage', () => this._createMenuBarButton( 'standalone' ) ); if ( editor.plugins.has( 'ImageInsertUI' ) ) { - const imageInsertUI: ImageInsertUI = editor.plugins.get( 'ImageInsertUI' ); - - imageInsertUI.registerIntegration( { + editor.plugins.get( 'ImageInsertUI' ).registerIntegration( { name: 'upload', observable: () => editor.commands.get( 'uploadImage' )!, - - buttonViewCreator: () => { - const uploadImageButton = editor.ui.componentFactory.create( 'uploadImage' ) as FileDialogButtonView; - - uploadImageButton.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace image from computer' ) : - t( 'Upload image from computer' ) - ); - - return uploadImageButton; - }, - - formViewCreator: () => { - const uploadImageButton = editor.ui.componentFactory.create( 'uploadImage' ) as FileDialogButtonView; - - uploadImageButton.withText = true; - uploadImageButton.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace from computer' ) : - t( 'Upload from computer' ) - ); - - uploadImageButton.on( 'execute', () => { - imageInsertUI.dropdownView!.isOpen = false; - } ); - - return uploadImageButton; - } + buttonViewCreator: () => this._createToolbarButton(), + formViewCreator: () => this._createDropdownButton(), + menuBarButtonViewCreator: isOnly => this._createMenuBarButton( isOnly ? 'insertOnly' : 'insertNested' ) } ); } } /** - * Creates a button for image upload command to use either in toolbar or in menu bar. + * Creates the base for various kinds of the button component provided by this feature. */ private _createButton( ButtonClass: T @@ -116,7 +77,7 @@ export default class ImageUploadUI extends Plugin { view.set( { acceptedType: imageTypes.map( type => `image/${ type }` ).join( ',' ), allowMultipleFiles: true, - label: t( 'Upload image from computer' ), + label: t( 'Upload from computer' ), icon: icons.imageUpload } ); @@ -134,4 +95,71 @@ export default class ImageUploadUI extends Plugin { return view; } + + /** + * Creates a simple toolbar button, with an icon and a tooltip. + */ + private _createToolbarButton(): ButtonView { + const t = this.editor.locale.t; + const imageInsertUI: ImageInsertUI = this.editor.plugins.get( 'ImageInsertUI' ); + + const button = this._createButton( FileDialogButtonView ); + + button.tooltip = true; + button.bind( 'label' ).to( + imageInsertUI, + 'isImageSelected', + isImageSelected => isImageSelected ? t( 'Replace image from computer' ) : t( 'Upload image from computer' ) + ); + + return button; + } + + /** + * Creates a button for the dropdown view, with an icon, text and no tooltip. + */ + private _createDropdownButton(): ButtonView { + const t = this.editor.locale.t; + const imageInsertUI: ImageInsertUI = this.editor.plugins.get( 'ImageInsertUI' ); + + const button = this._createButton( FileDialogButtonView ); + + button.withText = true; + + button.bind( 'label' ).to( + imageInsertUI, + 'isImageSelected', + isImageSelected => isImageSelected ? t( 'Replace from computer' ) : t( 'Upload from computer' ) + ); + + button.on( 'execute', () => { + imageInsertUI.dropdownView!.isOpen = false; + } ); + + return button; + } + + /** + * Creates a button for the menu bar. + */ + private _createMenuBarButton( type: 'standalone' | 'insertOnly' | 'insertNested' ): MenuBarMenuListItemButtonView { + const t = this.editor.locale.t; + const button = this._createButton( MenuBarMenuListItemFileDialogButtonView ); + + button.withText = true; + + switch ( type ) { + case 'standalone': + button.label = t( 'Image from computer' ); + break; + case 'insertOnly': + button.label = t( 'Image' ); + break; + case 'insertNested': + button.label = t( 'From computer' ); + break; + } + + return button; + } } diff --git a/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js b/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js index f61c6040842..9a8bb07abb5 100644 --- a/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js +++ b/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js @@ -19,6 +19,7 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils.js'; import Image from '../../src/image.js'; import ImageInsertUI from '../../src/imageinsert/imageinsertui.js'; import ImageInsertFormView from '../../src/imageinsert/ui/imageinsertformview.js'; +import { MenuBarMenuListItemButtonView, MenuBarMenuView } from '@ckeditor/ckeditor5-ui'; describe( 'ImageInsertUI', () => { let editor, editorElement, insertImageUI; @@ -61,6 +62,7 @@ describe( 'ImageInsertUI', () => { it( 'should register component in component factory', () => { expect( editor.ui.componentFactory.has( 'insertImage' ) ).to.be.true; expect( editor.ui.componentFactory.has( 'imageInsert' ) ).to.be.true; + expect( editor.ui.componentFactory.has( 'menuBar:insertImage' ) ).to.be.true; } ); it( 'should register "imageInsert" dropdown as an alias for the "insertImage" dropdown', () => { @@ -133,12 +135,14 @@ describe( 'ImageInsertUI', () => { const observable = new Model( { isEnabled: true } ); const buttonViewCreator = () => {}; const formViewCreator = () => {}; + const menuBarButtonViewCreator = () => {}; insertImageUI.registerIntegration( { name: 'foobar', observable, buttonViewCreator, - formViewCreator + formViewCreator, + menuBarButtonViewCreator } ); expect( insertImageUI._integrations.has( 'foobar' ) ).to.be.true; @@ -148,6 +152,7 @@ describe( 'ImageInsertUI', () => { expect( integrationData.observable ).to.equal( observable ); expect( integrationData.buttonViewCreator ).to.equal( buttonViewCreator ); expect( integrationData.formViewCreator ).to.equal( formViewCreator ); + expect( integrationData.menuBarButtonViewCreator ).to.equal( menuBarButtonViewCreator ); expect( integrationData.requiresForm ).to.be.false; } ); @@ -155,12 +160,14 @@ describe( 'ImageInsertUI', () => { const observable = new Model( { isEnabled: true } ); const buttonViewCreator = () => {}; const formViewCreator = () => {}; + const menuBarButtonViewCreator = () => {}; insertImageUI.registerIntegration( { name: 'foobar', observable, buttonViewCreator, formViewCreator, + menuBarButtonViewCreator, requiresForm: true } ); @@ -171,6 +178,7 @@ describe( 'ImageInsertUI', () => { expect( integrationData.observable ).to.equal( observable ); expect( integrationData.buttonViewCreator ).to.equal( buttonViewCreator ); expect( integrationData.formViewCreator ).to.equal( formViewCreator ); + expect( integrationData.menuBarButtonViewCreator ).to.equal( menuBarButtonViewCreator ); expect( integrationData.requiresForm ).to.be.true; } ); @@ -178,6 +186,7 @@ describe( 'ImageInsertUI', () => { const observable = new Model( { isEnabled: true } ); const buttonViewCreator = () => {}; const formViewCreator = () => {}; + const menuBarButtonViewCreator = () => {}; const warnStub = sinon.stub( console, 'warn' ); insertImageUI.registerIntegration( { @@ -185,6 +194,7 @@ describe( 'ImageInsertUI', () => { observable, buttonViewCreator, formViewCreator, + menuBarButtonViewCreator, requiresForm: true } ); @@ -195,6 +205,7 @@ describe( 'ImageInsertUI', () => { observable, buttonViewCreator, formViewCreator, + menuBarButtonViewCreator, requiresForm: true } ); @@ -219,6 +230,12 @@ describe( 'ImageInsertUI', () => { expect( dropdown ).to.be.null; expect( warnStub.calledOnce ).to.be.true; expect( warnStub.firstCall.args[ 0 ] ).to.equal( 'image-insert-integrations-not-specified' ); + + const menuComponent = editor.ui.componentFactory.create( 'menuBar:insertImage' ); + + expect( menuComponent ).to.be.null; + expect( warnStub.calledTwice ).to.be.true; + expect( warnStub.secondCall.args[ 0 ] ).to.equal( 'image-insert-integrations-not-specified' ); } ); it( 'should warn if unknown integration is requested by config', () => { @@ -294,6 +311,14 @@ describe( 'ImageInsertUI', () => { expect( formView.children.get( 0 ) ).to.be.instanceOf( ButtonView ); expect( formView.children.get( 0 ).label ).to.equal( 'dropdown url single' ); } ); + + it( 'should create a menu bar button', () => { + const button = editor.ui.componentFactory.create( 'menuBar:insertImage' ); + + expect( button ).to.be.instanceOf( MenuBarMenuListItemButtonView ); + expect( button.label ).to.equal( 'button url' ); + expect( button.isEnabled ).to.be.true; + } ); } ); describe( 'single integration with form view required and observalbe as a function', () => { @@ -379,6 +404,17 @@ describe( 'ImageInsertUI', () => { expect( formView.children.get( 1 ) ).to.be.instanceOf( ButtonView ); expect( formView.children.get( 1 ).label ).to.equal( 'dropdown url multiple' ); } ); + + it( 'should create a menu bar sub menu', () => { + const menu = editor.ui.componentFactory.create( 'menuBar:insertImage' ); + + expect( menu ).to.be.instanceOf( MenuBarMenuView ); + + const submenuList = menu.panelView.children.get( 0 ); + + expect( submenuList.items.get( 0 ).children.get( 0 ).label ).to.equal( 'button upload' ); + expect( submenuList.items.get( 1 ).children.get( 0 ).label ).to.equal( 'button url' ); + } ); } ); describe( 'multiple integrations and observalbe as a function', () => { @@ -427,6 +463,13 @@ describe( 'ImageInsertUI', () => { button.label = 'dropdown url ' + ( isOnlyOne ? 'single' : 'multiple' ); + return button; + }, + menuBarButtonViewCreator() { + const button = new MenuBarMenuListItemButtonView( editor.locale ); + + button.label = 'button url'; + return button; } } ); @@ -450,6 +493,13 @@ describe( 'ImageInsertUI', () => { button.label = 'dropdown upload ' + ( isOnlyOne ? 'single' : 'multiple' ); + return button; + }, + menuBarButtonViewCreator() { + const button = new MenuBarMenuListItemButtonView( editor.locale ); + + button.label = 'button upload'; + return button; } } ); diff --git a/packages/ckeditor5-image/tests/imageinsert/imageinsertviaurlui.js b/packages/ckeditor5-image/tests/imageinsert/imageinsertviaurlui.js index ff0f290fc12..05c8e2088c3 100644 --- a/packages/ckeditor5-image/tests/imageinsert/imageinsertviaurlui.js +++ b/packages/ckeditor5-image/tests/imageinsert/imageinsertviaurlui.js @@ -7,21 +7,19 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor.js'; import Model from '@ckeditor/ckeditor5-ui/src/model.js'; -import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview.js'; import SplitButtonView from '@ckeditor/ckeditor5-ui/src/dropdown/button/splitbuttonview.js'; -import { CollapsibleView, DropdownButtonView } from '@ckeditor/ckeditor5-ui'; import { icons } from '@ckeditor/ckeditor5-core'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils.js'; +import { ButtonView, MenuBarMenuListItemButtonView } from '@ckeditor/ckeditor5-ui'; import Image from '../../src/image.js'; import ImageInsertFormView from '../../src/imageinsert/ui/imageinsertformview.js'; import ImageInsertViaUrlUI from '../../src/imageinsert/imageinsertviaurlui.js'; import { ImageInsertViaUrl } from '../../src/index.js'; -import ImageInsertUrlView from '../../src/imageinsert/ui/imageinserturlview.js'; describe( 'ImageInsertViaUrlUI', () => { - let editor, editorElement, insertImageUI; + let editor, editorElement, insertImageUI, button; testUtils.createSinonSandbox(); @@ -48,369 +46,333 @@ describe( 'ImageInsertViaUrlUI', () => { editor.ui.componentFactory.create( 'insertImage' ); } ); - describe( 'single integration', () => { + describe( 'UI components', () => { beforeEach( async () => { await createEditor( { plugins: [ Image, ImageInsertViaUrl ] } ); } ); - it( 'should create toolbar dropdown button', () => { - const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + describe( 'toolbar button', () => { + beforeEach( () => { + button = editor.ui.componentFactory.create( 'insertImageViaUrl' ); + } ); - expect( dropdown.buttonView ).to.be.instanceOf( DropdownButtonView ); - expect( dropdown.buttonView.icon ).to.equal( icons.imageUrl ); - expect( dropdown.buttonView.tooltip ).to.be.true; - expect( dropdown.buttonView.label ).to.equal( 'Insert image via URL' ); - } ); + testButton( ButtonView, 'Insert image via URL' ); - it( 'should bind button label to ImageInsertUI#isImageSelected', () => { - const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + it( 'should bind button label to ImageInsertUI#isImageSelected', () => { + expect( button.label ).to.equal( 'Insert image via URL' ); - expect( dropdown.buttonView.label ).to.equal( 'Insert image via URL' ); + insertImageUI.isImageSelected = true; + expect( button.label ).to.equal( 'Update image URL' ); - insertImageUI.isImageSelected = true; - expect( dropdown.buttonView.label ).to.equal( 'Update image URL' ); + insertImageUI.isImageSelected = false; + expect( button.label ).to.equal( 'Insert image via URL' ); + } ); - insertImageUI.isImageSelected = false; - expect( dropdown.buttonView.label ).to.equal( 'Insert image via URL' ); + it( 'should have a tooltip', () => { + expect( button.tooltip ).to.be.true; + } ); } ); - it( 'should create form view on first open of dropdown', () => { - const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - - expect( dropdown.panelView.children.length ).to.equal( 0 ); - - dropdown.isOpen = true; - expect( dropdown.panelView.children.length ).to.equal( 1 ); + describe( 'menu bar button', () => { + beforeEach( () => { + button = editor.ui.componentFactory.create( 'menuBar:insertImageViaUrl' ); + } ); - const formView = dropdown.panelView.children.get( 0 ); - expect( formView ).to.be.instanceOf( ImageInsertFormView ); - expect( formView.children.length ).to.equal( 1 ); - expect( formView.children.get( 0 ) ).to.be.instanceOf( ImageInsertUrlView ); + testButton( MenuBarMenuListItemButtonView, 'Image via URL' ); } ); + } ); - describe( 'form bindings', () => { - let dropdown, formView, urlView; + describe( 'dialog', () => { + let dialog, urlView, acceptButton, cancelButton; - beforeEach( () => { - dropdown = editor.ui.componentFactory.create( 'insertImage' ); - dropdown.isOpen = true; - formView = dropdown.panelView.children.get( 0 ); - urlView = formView.children.get( 0 ); + function openDialog() { + button.fire( 'execute' ); + urlView = dialog.view.contentView.children.get( 0 ); + cancelButton = dialog.view.actionsView.children.get( 0 ); + acceptButton = dialog.view.actionsView.children.get( 1 ); + } + + beforeEach( async () => { + await createEditor( { + plugins: [ Image, ImageInsertViaUrl ] } ); - it( 'should bind #isImageSelected', () => { - expect( urlView.isImageSelected ).to.be.false; + button = editor.ui.componentFactory.create( 'insertImageViaUrl' ); + dialog = editor.plugins.get( 'Dialog' ); + const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + replaceImageSourceCommand.value = 'foobar'; - insertImageUI.isImageSelected = true; - expect( urlView.isImageSelected ).to.be.true; + openDialog(); + } ); - insertImageUI.isImageSelected = false; - expect( urlView.isImageSelected ).to.be.false; - } ); + it( 'has two action buttons', () => { + expect( dialog.view.actionsView.children ).to.have.length( 2 ); + expect( dialog.view.actionsView.children.get( 0 ).label ).to.equal( 'Cancel' ); + expect( dialog.view.actionsView.children.get( 1 ).label ).to.equal( 'Accept' ); + } ); - it( 'should bind #isEnabled', () => { - const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); - const insertImageCommand = editor.commands.get( 'insertImage' ); + it( 'should bind #isImageSelected', () => { + expect( urlView.isImageSelected ).to.be.false; - replaceImageSourceCommand.isEnabled = false; - insertImageCommand.isEnabled = false; - expect( urlView.isEnabled ).to.be.false; + insertImageUI.isImageSelected = true; + expect( urlView.isImageSelected ).to.be.true; - replaceImageSourceCommand.isEnabled = true; - insertImageCommand.isEnabled = false; - expect( urlView.isEnabled ).to.be.true; + insertImageUI.isImageSelected = false; + expect( urlView.isImageSelected ).to.be.false; + } ); - replaceImageSourceCommand.isEnabled = false; - insertImageCommand.isEnabled = true; - expect( urlView.isEnabled ).to.be.true; + it( 'should change title if image is selected', () => { + expect( dialog.view.headerView.label ).to.equal( 'Insert image via URL' ); - replaceImageSourceCommand.isEnabled = true; - insertImageCommand.isEnabled = true; - expect( urlView.isEnabled ).to.be.true; - } ); + dialog.hide(); + insertImageUI.isImageSelected = true; + openDialog(); - it( 'should set #imageURLInputValue at first open', () => { - const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + expect( dialog.view.headerView.label ).to.equal( 'Update image URL' ); + } ); - replaceImageSourceCommand.value = 'foobar'; + it( 'should bind #isEnabled', () => { + const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + const insertImageCommand = editor.commands.get( 'insertImage' ); - const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + replaceImageSourceCommand.isEnabled = false; + insertImageCommand.isEnabled = false; + expect( urlView.isEnabled ).to.be.false; - dropdown.isOpen = true; + replaceImageSourceCommand.isEnabled = true; + insertImageCommand.isEnabled = false; + expect( urlView.isEnabled ).to.be.true; - const formView = dropdown.panelView.children.get( 0 ); - const urlView = formView.children.get( 0 ); + replaceImageSourceCommand.isEnabled = false; + insertImageCommand.isEnabled = true; + expect( urlView.isEnabled ).to.be.true; - expect( urlView.imageURLInputValue ).to.equal( 'foobar' ); - } ); + replaceImageSourceCommand.isEnabled = true; + insertImageCommand.isEnabled = true; + expect( urlView.isEnabled ).to.be.true; + } ); - it( 'should reset #imageURLInputValue on dropdown reopen', () => { - const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + it( 'should set #imageURLInputValue at open', () => { + expect( urlView.imageURLInputValue ).to.equal( 'foobar' ); + } ); - replaceImageSourceCommand.value = 'abc'; - dropdown.isOpen = false; - dropdown.isOpen = true; - expect( urlView.imageURLInputValue ).to.equal( 'abc' ); + it( 'should reset #imageURLInputValue on dialog reopen', () => { + const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); - replaceImageSourceCommand.value = '123'; - dropdown.isOpen = false; - dropdown.isOpen = true; - expect( urlView.imageURLInputValue ).to.equal( '123' ); + replaceImageSourceCommand.value = 'abc'; + dialog.hide(); + openDialog(); + expect( urlView.imageURLInputValue ).to.equal( 'abc' ); - replaceImageSourceCommand.value = undefined; - dropdown.isOpen = false; - dropdown.isOpen = true; - expect( urlView.imageURLInputValue ).to.equal( '' ); - } ); + replaceImageSourceCommand.value = '123'; + dialog.hide(); + openDialog(); + expect( urlView.imageURLInputValue ).to.equal( '123' ); - it( 'should execute replaceImageSource command and close dropdown', () => { - const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); - const stubExecute = sinon.stub( editor, 'execute' ); - const stubFocus = sinon.stub( editor.editing.view, 'focus' ); + replaceImageSourceCommand.value = undefined; + dialog.hide(); + openDialog(); + expect( urlView.imageURLInputValue ).to.equal( '' ); + } ); - replaceImageSourceCommand.isEnabled = true; - urlView.imageURLInputValue = 'foo'; + it( 'should execute replaceImageSource command and close dialog', () => { + const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + const stubExecute = sinon.stub( editor, 'execute' ); + const stubFocus = sinon.stub( editor.editing.view, 'focus' ); - urlView.fire( 'submit' ); + replaceImageSourceCommand.isEnabled = true; + urlView.imageURLInputValue = 'foo'; - expect( stubExecute.calledOnce ).to.be.true; - expect( stubExecute.firstCall.args[ 0 ] ).to.equal( 'replaceImageSource' ); - expect( stubExecute.firstCall.args[ 1 ] ).to.deep.equal( { source: 'foo' } ); - expect( stubFocus.calledOnce ).to.be.true; - expect( dropdown.isOpen ).to.be.false; - } ); + acceptButton.fire( 'execute' ); - it( 'should execute insertImage command', () => { - const replaceImageSourceCommand = editor.commands.get( 'insertImage' ); - const stubExecute = sinon.stub( editor, 'execute' ); - const stubFocus = sinon.stub( editor.editing.view, 'focus' ); + expect( stubExecute.calledOnce ).to.be.true; + expect( stubExecute.firstCall.args[ 0 ] ).to.equal( 'replaceImageSource' ); + expect( stubExecute.firstCall.args[ 1 ] ).to.deep.equal( { source: 'foo' } ); + expect( stubFocus.calledOnce ).to.be.true; + expect( dialog.id ).to.be.null; + } ); - replaceImageSourceCommand.isEnabled = true; - urlView.imageURLInputValue = 'foo'; + it( 'should execute insertImage command', () => { + const replaceImageSourceCommand = editor.commands.get( 'insertImage' ); + const stubExecute = sinon.stub( editor, 'execute' ); + const stubFocus = sinon.stub( editor.editing.view, 'focus' ); - urlView.fire( 'submit' ); + replaceImageSourceCommand.isEnabled = true; + urlView.imageURLInputValue = 'foo'; - expect( stubExecute.calledOnce ).to.be.true; - expect( stubExecute.firstCall.args[ 0 ] ).to.equal( 'insertImage' ); - expect( stubExecute.firstCall.args[ 1 ] ).to.deep.equal( { source: 'foo' } ); - expect( stubFocus.calledOnce ).to.be.true; - expect( dropdown.isOpen ).to.be.false; - } ); + acceptButton.fire( 'execute' ); - it( 'should close dropdown', () => { - const stubExecute = sinon.stub( editor, 'execute' ); - const stubFocus = sinon.stub( editor.editing.view, 'focus' ); + expect( stubExecute.calledOnce ).to.be.true; + expect( stubExecute.firstCall.args[ 0 ] ).to.equal( 'insertImage' ); + expect( stubExecute.firstCall.args[ 1 ] ).to.deep.equal( { source: 'foo' } ); + expect( stubFocus.calledOnce ).to.be.true; + expect( dialog.id ).to.be.null; + } ); - urlView.fire( 'cancel' ); + it( 'should close dropdown', () => { + const stubExecute = sinon.stub( editor, 'execute' ); + const stubFocus = sinon.stub( editor.editing.view, 'focus' ); - expect( stubExecute.notCalled ).to.be.true; - expect( stubFocus.calledOnce ).to.be.true; - expect( dropdown.isOpen ).to.be.false; - } ); + cancelButton.fire( 'execute' ); + + expect( stubExecute.notCalled ).to.be.true; + expect( stubFocus.calledOnce ).to.be.true; + expect( dialog.id ).to.be.null; } ); } ); - describe( 'multiple integrations', () => { - beforeEach( async () => { - await createEditor( { - plugins: [ Image, ImageInsertViaUrl ] + describe( 'ImageInsertUI integration', () => { + describe( 'single integration', () => { + beforeEach( async () => { + await createEditor( { + plugins: [ Image, ImageInsertViaUrl ] + } ); } ); - const observable = new Model( { isEnabled: true } ); + describe( 'toolbar button', () => { + beforeEach( () => { + button = editor.ui.componentFactory.create( 'insertImage' ); + } ); - insertImageUI.registerIntegration( { - name: 'foo', - observable, - buttonViewCreator() { - const button = new ButtonView( editor.locale ); + testButton( ButtonView, 'Insert image via URL' ); - button.label = 'foo'; + it( 'should bind button label to ImageInsertUI#isImageSelected', () => { + expect( button.label ).to.equal( 'Insert image via URL' ); - return button; - }, - formViewCreator() { - const button = new ButtonView( editor.locale ); + insertImageUI.isImageSelected = true; + expect( button.label ).to.equal( 'Update image URL' ); - button.label = 'bar'; + insertImageUI.isImageSelected = false; + expect( button.label ).to.equal( 'Insert image via URL' ); + } ); - return button; - } + it( 'should have a tooltip', () => { + expect( button.tooltip ).to.be.true; + } ); } ); - editor.config.set( 'image.insert.integrations', [ 'url', 'foo' ] ); - } ); - - it( 'should create toolbar dropdown button', () => { - const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - - expect( dropdown.buttonView ).to.be.instanceOf( SplitButtonView ); - expect( dropdown.buttonView.tooltip ).to.be.true; - expect( dropdown.buttonView.label ).to.equal( 'Insert image' ); - expect( dropdown.buttonView.actionView.icon ).to.equal( icons.imageUrl ); - expect( dropdown.buttonView.actionView.tooltip ).to.be.true; - expect( dropdown.buttonView.actionView.label ).to.equal( 'Insert image via URL' ); - } ); + describe( 'menu bar button', () => { + beforeEach( () => { + button = editor.ui.componentFactory.create( 'menuBar:insertImage' ); + } ); - it( 'should bind button label to ImageInsertUI#isImageSelected', () => { - const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - - expect( dropdown.buttonView.label ).to.equal( 'Insert image' ); - expect( dropdown.buttonView.actionView.label ).to.equal( 'Insert image via URL' ); - - insertImageUI.isImageSelected = true; - expect( dropdown.buttonView.label ).to.equal( 'Replace image' ); - expect( dropdown.buttonView.actionView.label ).to.equal( 'Update image URL' ); - - insertImageUI.isImageSelected = false; - expect( dropdown.buttonView.label ).to.equal( 'Insert image' ); - expect( dropdown.buttonView.actionView.label ).to.equal( 'Insert image via URL' ); + testButton( MenuBarMenuListItemButtonView, 'Image' ); + } ); } ); - it( 'should create form view on first open of dropdown', () => { - const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + describe( 'multiple integrations', () => { + beforeEach( async () => { + await createEditor( { + plugins: [ Image, ImageInsertViaUrl ] + } ); - expect( dropdown.panelView.children.length ).to.equal( 0 ); + const observable = new Model( { isEnabled: true } ); - dropdown.isOpen = true; - expect( dropdown.panelView.children.length ).to.equal( 1 ); + insertImageUI.registerIntegration( { + name: 'foo', + observable, + buttonViewCreator() { + const button = new ButtonView( editor.locale ); - const formView = dropdown.panelView.children.get( 0 ); - expect( formView ).to.be.instanceOf( ImageInsertFormView ); - expect( formView.children.length ).to.equal( 2 ); + button.label = 'foo'; - const collapsibleView = formView.children.get( 0 ); - expect( collapsibleView ).to.be.instanceOf( CollapsibleView ); - expect( collapsibleView.children.get( 0 ) ).to.be.instanceOf( ImageInsertUrlView ); - } ); + return button; + }, + formViewCreator() { + const button = new ButtonView( editor.locale ); - describe( 'form bindings', () => { - let dropdown, formView, collapsibleView, urlView; + button.label = 'bar'; - beforeEach( () => { - dropdown = editor.ui.componentFactory.create( 'insertImage' ); - dropdown.isOpen = true; - formView = dropdown.panelView.children.get( 0 ); - collapsibleView = formView.children.get( 0 ); - urlView = collapsibleView.children.get( 0 ); - } ); + return button; + }, + menuBarButtonViewCreator() { + const button = new ButtonView( editor.locale ); - it( 'should bind #isImageSelected', () => { - expect( urlView.isImageSelected ).to.be.false; + button.label = 'baz'; - insertImageUI.isImageSelected = true; - expect( urlView.isImageSelected ).to.be.true; - expect( collapsibleView.label ).to.equal( 'Update image URL' ); + return button; + } + } ); - insertImageUI.isImageSelected = false; - expect( urlView.isImageSelected ).to.be.false; - expect( collapsibleView.label ).to.equal( 'Insert image via URL' ); + editor.config.set( 'image.insert.integrations', [ 'url', 'foo' ] ); } ); - it( 'should bind #isEnabled', () => { - const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); - const insertImageCommand = editor.commands.get( 'insertImage' ); + describe( 'toolbar button', () => { + it( 'should create toolbar split button view', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - replaceImageSourceCommand.isEnabled = false; - insertImageCommand.isEnabled = false; - expect( urlView.isEnabled ).to.be.false; + expect( dropdown.buttonView ).to.be.instanceOf( SplitButtonView ); + expect( dropdown.buttonView.tooltip ).to.be.true; + expect( dropdown.buttonView.label ).to.equal( 'Insert image' ); + expect( dropdown.buttonView.actionView.icon ).to.equal( icons.imageUrl ); + expect( dropdown.buttonView.actionView.tooltip ).to.be.true; + expect( dropdown.buttonView.actionView.label ).to.equal( 'Insert image via URL' ); + } ); - replaceImageSourceCommand.isEnabled = true; - insertImageCommand.isEnabled = false; - expect( urlView.isEnabled ).to.be.true; + it( 'should bind button label to ImageInsertUI#isImageSelected', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - replaceImageSourceCommand.isEnabled = false; - insertImageCommand.isEnabled = true; - expect( urlView.isEnabled ).to.be.true; + expect( dropdown.buttonView.label ).to.equal( 'Insert image' ); + expect( dropdown.buttonView.actionView.label ).to.equal( 'Insert image via URL' ); - replaceImageSourceCommand.isEnabled = true; - insertImageCommand.isEnabled = true; - expect( urlView.isEnabled ).to.be.true; - } ); + insertImageUI.isImageSelected = true; + expect( dropdown.buttonView.label ).to.equal( 'Replace image' ); + expect( dropdown.buttonView.actionView.label ).to.equal( 'Update image URL' ); - it( 'should set #imageURLInputValue and CollapsibleView#isCollapsed at first open', () => { - const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + insertImageUI.isImageSelected = false; + expect( dropdown.buttonView.label ).to.equal( 'Insert image' ); + expect( dropdown.buttonView.actionView.label ).to.equal( 'Insert image via URL' ); + } ); - replaceImageSourceCommand.value = 'foobar'; + it( 'should create form view on first open of dropdown', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + expect( dropdown.panelView.children.length ).to.equal( 0 ); - dropdown.isOpen = true; + dropdown.isOpen = true; + expect( dropdown.panelView.children.length ).to.equal( 1 ); - const formView = dropdown.panelView.children.get( 0 ); - const collapsibleView = formView.children.get( 0 ); - const urlView = collapsibleView.children.get( 0 ); - - expect( urlView.imageURLInputValue ).to.equal( 'foobar' ); - expect( collapsibleView.isCollapsed ).to.be.true; - } ); + const formView = dropdown.panelView.children.get( 0 ); + expect( formView ).to.be.instanceOf( ImageInsertFormView ); + expect( formView.children.length ).to.equal( 2 ); - it( 'should reset #imageURLInputValue and CollapsibleView#isCollapsed on dropdown reopen', () => { - const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); - - replaceImageSourceCommand.value = 'abc'; - dropdown.isOpen = false; - dropdown.isOpen = true; - expect( urlView.imageURLInputValue ).to.equal( 'abc' ); - expect( collapsibleView.isCollapsed ).to.be.true; - - replaceImageSourceCommand.value = '123'; - dropdown.isOpen = false; - dropdown.isOpen = true; - expect( urlView.imageURLInputValue ).to.equal( '123' ); - expect( collapsibleView.isCollapsed ).to.be.true; - - replaceImageSourceCommand.value = undefined; - dropdown.isOpen = false; - dropdown.isOpen = true; - expect( urlView.imageURLInputValue ).to.equal( '' ); - expect( collapsibleView.isCollapsed ).to.be.true; + const buttonView = formView.children.get( 0 ); + expect( buttonView ).to.be.instanceOf( ButtonView ); + } ); } ); - it( 'should execute replaceImageSource command and close dropdown', () => { - const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); - const stubExecute = sinon.stub( editor, 'execute' ); - const stubFocus = sinon.stub( editor.editing.view, 'focus' ); + describe( 'dropdown button', () => { + beforeEach( () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - replaceImageSourceCommand.isEnabled = true; - urlView.imageURLInputValue = 'foo'; + dropdown.isOpen = true; - urlView.fire( 'submit' ); + const formView = dropdown.panelView.children.get( 0 ); + button = formView.children.get( 0 ); + } ); - expect( stubExecute.calledOnce ).to.be.true; - expect( stubExecute.firstCall.args[ 0 ] ).to.equal( 'replaceImageSource' ); - expect( stubExecute.firstCall.args[ 1 ] ).to.deep.equal( { source: 'foo' } ); - expect( stubFocus.calledOnce ).to.be.true; - expect( dropdown.isOpen ).to.be.false; - } ); - - it( 'should execute insertImage command', () => { - const replaceImageSourceCommand = editor.commands.get( 'insertImage' ); - const stubExecute = sinon.stub( editor, 'execute' ); - const stubFocus = sinon.stub( editor.editing.view, 'focus' ); + testButton( ButtonView, 'Insert via URL' ); - replaceImageSourceCommand.isEnabled = true; - urlView.imageURLInputValue = 'foo'; + it( 'should bind button label to ImageInsertUI#isImageSelected', () => { + expect( button.label ).to.equal( 'Insert via URL' ); - urlView.fire( 'submit' ); + insertImageUI.isImageSelected = true; + expect( button.label ).to.equal( 'Update image URL' ); - expect( stubExecute.calledOnce ).to.be.true; - expect( stubExecute.firstCall.args[ 0 ] ).to.equal( 'insertImage' ); - expect( stubExecute.firstCall.args[ 1 ] ).to.deep.equal( { source: 'foo' } ); - expect( stubFocus.calledOnce ).to.be.true; - expect( dropdown.isOpen ).to.be.false; + insertImageUI.isImageSelected = false; + expect( button.label ).to.equal( 'Insert via URL' ); + } ); } ); - it( 'should close dropdown', () => { - const stubExecute = sinon.stub( editor, 'execute' ); - const stubFocus = sinon.stub( editor.editing.view, 'focus' ); - - urlView.fire( 'cancel' ); + describe( 'menu button', () => { + beforeEach( () => { + const submenu = editor.ui.componentFactory.create( 'menuBar:insertImage' ); + button = submenu.panelView.children.first.items.first.children.first; + } ); - expect( stubExecute.notCalled ).to.be.true; - expect( stubFocus.calledOnce ).to.be.true; - expect( dropdown.isOpen ).to.be.false; + testButton( MenuBarMenuListItemButtonView, 'Via URL' ); } ); } ); } ); @@ -423,4 +385,27 @@ describe( 'ImageInsertViaUrlUI', () => { insertImageUI = editor.plugins.get( 'ImageInsertUI' ); } + + function testButton( expectedType, expectedInsertLabel ) { + it( 'should add the component to the factory', () => { + expect( button ).to.be.instanceOf( expectedType ); + } ); + + it( 'should set a #label of the #buttonView', () => { + expect( button.label ).to.equal( expectedInsertLabel ); + } ); + + it( 'should set an #icon of the #buttonView', () => { + expect( button.icon ).to.equal( icons.imageUrl ); + } ); + + it( 'should open insert image via url dialog', () => { + const dialogPlugin = editor.plugins.get( 'Dialog' ); + expect( dialogPlugin.id ).to.be.null; + + button.fire( 'execute' ); + + expect( dialogPlugin.id ).to.equal( 'insertImageViaUrl' ); + } ); + } } ); diff --git a/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertformview.js b/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertformview.js index 5b6f235b51f..467a2da7168 100644 --- a/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertformview.js +++ b/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertformview.js @@ -278,10 +278,9 @@ describe( 'ImageInsertFormView', () => { } ); describe( 'focus cycling', () => { - let view, inputIntegrationView, buttonIntegrationView, otherButtonIntegrationView, collapsibleIntegrationView; + let view, buttonIntegrationView, otherButtonIntegrationView; beforeEach( () => { - inputIntegrationView = new ImageInsertUrlView( { t: val => val } ); buttonIntegrationView = new ButtonView( { t: val => val } ); otherButtonIntegrationView = new ButtonView( { t: val => val } ); } ); @@ -332,80 +331,6 @@ describe( 'ImageInsertFormView', () => { } ); } ); - describe( 'single URL input integration', () => { - beforeEach( () => { - view = new ImageInsertFormView( { t: val => val }, [ - inputIntegrationView - ] ); - - view.render(); - document.body.appendChild( view.element ); - } ); - - afterEach( () => { - view.element.remove(); - view.destroy(); - } ); - - it( 'forward cycling', () => { - const keyEvtData = { - keyCode: keyCodes.tab, - shiftKey: false, - stopPropagation: sinon.spy(), - preventDefault: sinon.spy() - }; - - view.focus(); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); - - view.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); - - inputIntegrationView.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.insertButtonView.element ); - - inputIntegrationView.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.cancelButtonView.element ); - - inputIntegrationView.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); - } ); - - it( 'backward cycling', () => { - const keyEvtData = { - keyCode: keyCodes.tab, - shiftKey: true, - stopPropagation: sinon.spy(), - preventDefault: sinon.spy() - }; - - view.focus(); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); - - view.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); - - inputIntegrationView.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.cancelButtonView.element ); - - inputIntegrationView.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.insertButtonView.element ); - - inputIntegrationView.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); - } ); - } ); - describe( 'multiple button integrations', () => { beforeEach( () => { view = new ImageInsertFormView( { t: val => val }, [ @@ -458,165 +383,5 @@ describe( 'ImageInsertFormView', () => { expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); } ); } ); - - describe( 'mixed integrations (button and URL input)', () => { - beforeEach( () => { - view = new ImageInsertFormView( { t: val => val }, [ - buttonIntegrationView, - inputIntegrationView - ] ); - - view.render(); - document.body.appendChild( view.element ); - } ); - - afterEach( () => { - view.element.remove(); - view.destroy(); - } ); - - it( 'forward cycling', () => { - const keyEvtData = { - keyCode: keyCodes.tab, - shiftKey: false, - stopPropagation: sinon.spy(), - preventDefault: sinon.spy() - }; - - view.focus(); - expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); - expect( buttonIntegrationView.focusTracker ).to.be.undefined; - - view.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); - - inputIntegrationView.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.insertButtonView.element ); - - inputIntegrationView.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.cancelButtonView.element ); - - inputIntegrationView.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); - expect( buttonIntegrationView.focusTracker ).to.be.undefined; - } ); - - it( 'backward cycling', () => { - const keyEvtData = { - keyCode: keyCodes.tab, - shiftKey: true, - stopPropagation: sinon.spy(), - preventDefault: sinon.spy() - }; - - view.focus(); - expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); - expect( buttonIntegrationView.focusTracker ).to.be.undefined; - - view.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.cancelButtonView.element ); - - inputIntegrationView.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.insertButtonView.element ); - - inputIntegrationView.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); - - inputIntegrationView.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); - expect( buttonIntegrationView.focusTracker ).to.be.undefined; - } ); - } ); - - describe( 'mixed integrations (button and URL input inside a collapsible view)', () => { - beforeEach( () => { - collapsibleIntegrationView = new CollapsibleView( { t: val => val }, [ inputIntegrationView ] ); - - view = new ImageInsertFormView( { t: val => val }, [ - buttonIntegrationView, - collapsibleIntegrationView - ] ); - - view.render(); - document.body.appendChild( view.element ); - } ); - - afterEach( () => { - view.element.remove(); - view.destroy(); - } ); - - it( 'forward cycling', () => { - const keyEvtData = { - keyCode: keyCodes.tab, - shiftKey: false, - stopPropagation: sinon.spy(), - preventDefault: sinon.spy() - }; - - view.focus(); - expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); - expect( buttonIntegrationView.focusTracker ).to.be.undefined; - - view.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( collapsibleIntegrationView.element ); - expect( collapsibleIntegrationView.focusTracker ).to.be.undefined; - - view.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); - - inputIntegrationView.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.insertButtonView.element ); - - inputIntegrationView.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.cancelButtonView.element ); - - inputIntegrationView.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); - expect( buttonIntegrationView.focusTracker ).to.be.undefined; - } ); - - it( 'backward cycling', () => { - const keyEvtData = { - keyCode: keyCodes.tab, - shiftKey: true, - stopPropagation: sinon.spy(), - preventDefault: sinon.spy() - }; - - view.focus(); - expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); - expect( buttonIntegrationView.focusTracker ).to.be.undefined; - - view.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.cancelButtonView.element ); - - inputIntegrationView.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.insertButtonView.element ); - - inputIntegrationView.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); - expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); - - inputIntegrationView.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( collapsibleIntegrationView.element ); - expect( collapsibleIntegrationView.focusTracker ).to.be.undefined; - - view.keystrokes.press( keyEvtData ); - expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); - expect( buttonIntegrationView.focusTracker ).to.be.undefined; - } ); - } ); } ); } ); diff --git a/packages/ckeditor5-image/tests/imageinsert/ui/imageinserturlview.js b/packages/ckeditor5-image/tests/imageinsert/ui/imageinserturlview.js index 42fda0f64a4..8b006bcea88 100644 --- a/packages/ckeditor5-image/tests/imageinsert/ui/imageinserturlview.js +++ b/packages/ckeditor5-image/tests/imageinsert/ui/imageinserturlview.js @@ -8,17 +8,11 @@ import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview.js'; import ImageInsertUrlView from '../../../src/imageinsert/ui/imageinserturlview.js'; -import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview.js'; -import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard.js'; import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler.js'; -import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker.js'; -import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler.js'; -import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection.js'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils.js'; import InputTextView from '@ckeditor/ckeditor5-ui/src/inputtext/inputtextview.js'; -import { icons } from '@ckeditor/ckeditor5-core'; describe( 'ImageInsertUrlView', () => { let view; @@ -49,21 +43,9 @@ describe( 'ImageInsertUrlView', () => { expect( view.isEnabled ).to.be.true; } ); - it( 'should create #focusTracker instance', () => { - expect( view.focusTracker ).to.be.instanceOf( FocusTracker ); - } ); - it( 'should create #keystrokes instance', () => { expect( view.keystrokes ).to.be.instanceOf( KeystrokeHandler ); } ); - - it( 'should create #focusCycler instance', () => { - expect( view.focusCycler ).to.be.instanceOf( FocusCycler ); - } ); - - it( 'should create #_focusables view collection', () => { - expect( view._focusables ).to.be.instanceOf( ViewCollection ); - } ); } ); describe( 'template', () => { @@ -78,11 +60,6 @@ describe( 'ImageInsertUrlView', () => { expect( childNodes[ 1 ].tagName ).to.equal( 'DIV' ); expect( childNodes[ 1 ].classList.contains( 'ck' ) ).to.be.true; expect( childNodes[ 1 ].classList.contains( 'ck-image-insert-url__action-row' ) ).to.be.true; - - const childNodes2 = childNodes[ 1 ].childNodes; - - expect( childNodes2[ 0 ] ).to.equal( view.insertButtonView.element ); - expect( childNodes2[ 1 ] ).to.equal( view.cancelButtonView.element ); } ); it( 'should use dedicated views', () => { @@ -92,79 +69,7 @@ describe( 'ImageInsertUrlView', () => { } ); } ); - describe( 'render()', () => { - it( 'should register child views in #_focusables', () => { - expect( view._focusables.map( f => f ) ).to.have.members( [ - view.urlInputView, - view.insertButtonView, - view.cancelButtonView - ] ); - } ); - - it( 'should register child views\' #element in #focusTracker', () => { - const view = new ImageInsertUrlView( { t: () => {} } ); - - const spy = sinon.spy( view.focusTracker, 'add' ); - view.render(); - - sinon.assert.calledWithExactly( spy.getCall( 0 ), view.urlInputView.element ); - sinon.assert.calledWithExactly( spy.getCall( 1 ), view.insertButtonView.element ); - sinon.assert.calledWithExactly( spy.getCall( 2 ), view.cancelButtonView.element ); - - view.destroy(); - } ); - - describe( 'activates keyboard navigation for the toolbar', () => { - it( 'so "tab" focuses the next focusable item', () => { - const keyEvtData = { - keyCode: keyCodes.tab, - preventDefault: sinon.spy(), - stopPropagation: sinon.spy() - }; - - // Mock the url input is focused. - view.focusTracker.isFocused = true; - view.focusTracker.focusedElement = view.urlInputView.element; - - const spy = sinon.spy( view.insertButtonView, 'focus' ); - - view.keystrokes.press( keyEvtData ); - sinon.assert.calledOnce( keyEvtData.preventDefault ); - sinon.assert.calledOnce( keyEvtData.stopPropagation ); - sinon.assert.calledOnce( spy ); - } ); - - it( 'so "shift + tab" focuses the previous focusable item', () => { - const keyEvtData = { - keyCode: keyCodes.tab, - shiftKey: true, - preventDefault: sinon.spy(), - stopPropagation: sinon.spy() - }; - - // Mock the cancel button is focused. - view.focusTracker.isFocused = true; - view.focusTracker.focusedElement = view.cancelButtonView.element; - - const spy = sinon.spy( view.insertButtonView, 'focus' ); - - view.keystrokes.press( keyEvtData ); - sinon.assert.calledOnce( keyEvtData.preventDefault ); - sinon.assert.calledOnce( keyEvtData.stopPropagation ); - sinon.assert.calledOnce( spy ); - } ); - } ); - } ); - describe( 'destroy()', () => { - it( 'should destroy the FocusTracker instance', () => { - const destroySpy = sinon.spy( view.focusTracker, 'destroy' ); - - view.destroy(); - - sinon.assert.calledOnce( destroySpy ); - } ); - it( 'should destroy the KeystrokeHandler instance', () => { const destroySpy = sinon.spy( view.keystrokes, 'destroy' ); @@ -182,14 +87,6 @@ describe( 'ImageInsertUrlView', () => { sinon.assert.calledOnce( spy ); } ); - - it( 'should focus the last focusable', () => { - const spy = sinon.spy( view.cancelButtonView, 'focus' ); - - view.focus( -1 ); - - sinon.assert.calledOnce( spy ); - } ); } ); describe( '#urlInputView', () => { @@ -260,118 +157,4 @@ describe( 'ImageInsertUrlView', () => { expect( view.imageURLInputValue ).to.equal( 'test' ); } ); } ); - - describe( '#insertButtonView', () => { - it( 'should be an instance of the ButtonView', () => { - expect( view.insertButtonView ).to.be.instanceOf( ButtonView ); - } ); - - it( 'should have an icon', () => { - expect( view.insertButtonView.icon ).to.equal( icons.check ); - } ); - - it( 'should have a class', () => { - expect( view.insertButtonView.class ).to.equal( 'ck-button-save' ); - } ); - - it( 'should be a submit button', () => { - expect( view.insertButtonView.type ).to.equal( 'submit' ); - } ); - - it( 'should have text', () => { - expect( view.insertButtonView.withText ).to.be.true; - } ); - - it( 'should bind label to #isImageSelected', () => { - view.isImageSelected = false; - - expect( view.insertButtonView.label ).to.equal( 'Insert' ); - - view.isImageSelected = true; - - expect( view.insertButtonView.label ).to.equal( 'Update' ); - } ); - - it( 'should bind isEnabled to #isEnabled and #imageURLInputValue', () => { - view.isEnabled = false; - view.imageURLInputValue = ''; - - expect( view.insertButtonView.isEnabled ).to.be.false; - - view.isEnabled = true; - view.imageURLInputValue = 'abc'; - - expect( view.insertButtonView.isEnabled ).to.be.true; - - view.isEnabled = false; - view.imageURLInputValue = 'abc'; - - expect( view.insertButtonView.isEnabled ).to.be.false; - - view.isEnabled = true; - view.imageURLInputValue = ''; - - expect( view.insertButtonView.isEnabled ).to.be.false; - } ); - - it( 'should fire "submit" event on insertButtonView#execute', () => { - const spy = sinon.spy(); - - view.on( 'submit', spy ); - - view.insertButtonView.fire( 'execute' ); - - expect( spy.calledOnce ).to.true; - } ); - } ); - - describe( '#cancelButtonView', () => { - it( 'should be an instance of the ButtonView', () => { - expect( view.cancelButtonView ).to.be.instanceOf( ButtonView ); - } ); - - it( 'should have an icon', () => { - expect( view.cancelButtonView.icon ).to.equal( icons.cancel ); - } ); - - it( 'should have a class', () => { - expect( view.cancelButtonView.class ).to.equal( 'ck-button-cancel' ); - } ); - - it( 'should be a plain button', () => { - expect( view.cancelButtonView.type ).to.equal( 'button' ); - } ); - - it( 'should have text', () => { - expect( view.cancelButtonView.withText ).to.be.true; - } ); - - it( 'should have label', () => { - expect( view.cancelButtonView.label ).to.equal( 'Cancel' ); - } ); - - it( 'should bind isEnabled to #isEnabled', () => { - view.isEnabled = false; - - expect( view.cancelButtonView.isEnabled ).to.be.false; - - view.isEnabled = true; - - expect( view.cancelButtonView.isEnabled ).to.be.true; - - view.isEnabled = false; - - expect( view.cancelButtonView.isEnabled ).to.be.false; - } ); - - it( 'should fire "cancel" event on cancelButtonView#execute', () => { - const spy = sinon.spy(); - - view.on( 'cancel', spy ); - - view.cancelButtonView.fire( 'execute' ); - - expect( spy.calledOnce ).to.true; - } ); - } ); } ); diff --git a/packages/ckeditor5-image/tests/imageupload/imageuploadui.js b/packages/ckeditor5-image/tests/imageupload/imageuploadui.js index 03ccedac08b..51597a0c5f7 100644 --- a/packages/ckeditor5-image/tests/imageupload/imageuploadui.js +++ b/packages/ckeditor5-image/tests/imageupload/imageuploadui.js @@ -23,7 +23,7 @@ import { icons } from 'ckeditor5/src/core.js'; import { createNativeFileMock, UploadAdapterMock } from '@ckeditor/ckeditor5-upload/tests/_utils/mocks.js'; import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model.js'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils.js'; -import { MenuBarMenuListItemFileDialogButtonView } from '@ckeditor/ckeditor5-ui'; +import { MenuBarMenuListItemButtonView, MenuBarMenuListItemFileDialogButtonView } from '@ckeditor/ckeditor5-ui'; describe( 'ImageUploadUI', () => { let editor, model, editorElement, fileRepository, button; @@ -69,21 +69,29 @@ describe( 'ImageUploadUI', () => { } ); describe( 'toolbar button', () => { - beforeEach( () => { - button = editor.ui.componentFactory.create( 'imageUpload' ); - } ); - - testButton( 'uploadImage', 'Upload image from computer', ButtonView ); + describe( 'uploadImage', () => { + beforeEach( () => { + button = editor.ui.componentFactory.create( 'uploadImage' ); + } ); - it( 'should register imageUpload button as an alias for uploadImage button', () => { - const buttonCreator = editor.ui.componentFactory._components.get( 'uploadImage'.toLowerCase() ); - const buttonAliasCreator = editor.ui.componentFactory._components.get( 'imageUpload'.toLowerCase() ); + testButton( 'uploadImage', 'Upload image from computer', ButtonView ); - expect( buttonCreator.callback ).to.equal( buttonAliasCreator.callback ); + it( 'should have tooltip', () => { + expect( button.tooltip ).to.be.true; + } ); } ); - it( 'should have tooltip', () => { - expect( button.tooltip ).to.be.true; + // Check backward compatibility. + describe( 'imageUpload', () => { + beforeEach( () => { + button = editor.ui.componentFactory.create( 'imageUpload' ); + } ); + + testButton( 'uploadImage', 'Upload image from computer', ButtonView ); + + it( 'should have tooltip', () => { + expect( button.tooltip ).to.be.true; + } ); } ); } ); @@ -95,29 +103,22 @@ describe( 'ImageUploadUI', () => { testButton( 'uploadImage', 'Image from computer', MenuBarMenuListItemFileDialogButtonView ); } ); - describe( 'InsertImageUI integration', () => { + describe( 'InsertImageUI toolbar integration', () => { it( 'should create FileDialogButtonView in split button dropdown button', () => { - mockAssetManagerIntegration(); + mockAnotherIntegration(); - const spy = sinon.spy( editor.ui.componentFactory, 'create' ); const dropdown = editor.ui.componentFactory.create( 'insertImage' ); const dropdownButton = dropdown.buttonView.actionView; expect( dropdownButton ).to.be.instanceOf( FileDialogButtonView ); expect( dropdownButton.withText ).to.be.false; expect( dropdownButton.icon ).to.equal( icons.imageUpload ); - - expect( spy.calledTwice ).to.be.true; - expect( spy.firstCall.args[ 0 ] ).to.equal( 'insertImage' ); - expect( spy.secondCall.args[ 0 ] ).to.equal( 'uploadImage' ); - expect( spy.firstCall.returnValue ).to.equal( dropdown.buttonView.actionView ); } ); it( 'should create FileDialogButtonView in dropdown panel', () => { - mockAssetManagerIntegration(); + mockAnotherIntegration(); const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - const spy = sinon.spy( editor.ui.componentFactory, 'create' ); dropdown.isOpen = true; @@ -127,16 +128,12 @@ describe( 'ImageUploadUI', () => { expect( buttonView ).to.be.instanceOf( FileDialogButtonView ); expect( buttonView.withText ).to.be.true; expect( buttonView.icon ).to.equal( icons.imageUpload ); - - expect( spy.calledOnce ).to.be.true; - expect( spy.firstCall.args[ 0 ] ).to.equal( 'uploadImage' ); - expect( spy.firstCall.returnValue ).to.equal( buttonView ); } ); it( 'should bind to #isImageSelected', () => { const insertImageUI = editor.plugins.get( 'ImageInsertUI' ); - mockAssetManagerIntegration(); + mockAnotherIntegration(); const dropdown = editor.ui.componentFactory.create( 'insertImage' ); @@ -156,7 +153,7 @@ describe( 'ImageUploadUI', () => { } ); it( 'should close dropdown on execute', () => { - mockAssetManagerIntegration(); + mockAnotherIntegration(); const dropdown = editor.ui.componentFactory.create( 'insertImage' ); @@ -173,7 +170,31 @@ describe( 'ImageUploadUI', () => { } ); } ); - function mockAssetManagerIntegration() { + describe( 'InsertImageUI menu bar integration', () => { + it( 'should create FileDialogButtonView in insert image submenu', () => { + mockAnotherIntegration(); + + const submenu = editor.ui.componentFactory.create( 'menuBar:insertImage' ); + + button = submenu.panelView.children.first.items.first.children.first; + + expect( button ).to.be.instanceOf( MenuBarMenuListItemFileDialogButtonView ); + expect( button.withText ).to.be.true; + expect( button.icon ).to.equal( icons.imageUpload ); + expect( button.label ).to.equal( 'From computer' ); + } ); + + it( 'should create FileDialogButtonView in insert image submenu - only integration', () => { + button = editor.ui.componentFactory.create( 'menuBar:insertImage' ); + + expect( button ).to.be.instanceOf( MenuBarMenuListItemFileDialogButtonView ); + expect( button.withText ).to.be.true; + expect( button.icon ).to.equal( icons.imageUpload ); + expect( button.label ).to.equal( 'Image' ); + } ); + } ); + + function mockAnotherIntegration() { const insertImageUI = editor.plugins.get( 'ImageInsertUI' ); const observable = new Model( { isEnabled: true } ); @@ -192,6 +213,13 @@ describe( 'ImageUploadUI', () => { button.label = 'bar'; + return button; + }, + menuBarButtonViewCreator() { + const button = new MenuBarMenuListItemButtonView( editor.locale ); + + button.label = 'menu foo'; + return button; } } ); diff --git a/packages/ckeditor5-image/tests/manual/imageinsert.js b/packages/ckeditor5-image/tests/manual/imageinsert.js index 49fcbeb748c..ae3e82efba3 100644 --- a/packages/ckeditor5-image/tests/manual/imageinsert.js +++ b/packages/ckeditor5-image/tests/manual/imageinsert.js @@ -31,6 +31,7 @@ async function createEditor( elementId, imageType ) { 'undo', 'redo' ], + menuBar: { isVisible: true }, image: { toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:wrapText', '|', 'toggleImageCaption', 'imageTextAlternative' ], insert: { diff --git a/packages/ckeditor5-image/theme/imageinsert.css b/packages/ckeditor5-image/theme/imageinsert.css index e68845ed4bb..936e7c6608c 100644 --- a/packages/ckeditor5-image/theme/imageinsert.css +++ b/packages/ckeditor5-image/theme/imageinsert.css @@ -4,6 +4,9 @@ */ .ck.ck-image-insert-url { + width: 400px; + padding: var(--ck-spacing-large) var(--ck-spacing-large) 0; + & .ck-image-insert-url__action-row { display: grid; grid-template-columns: repeat(2, 1fr); diff --git a/packages/ckeditor5-ui/src/menubar/utils.ts b/packages/ckeditor5-ui/src/menubar/utils.ts index f1cc51d6cc2..a243b23ec8e 100644 --- a/packages/ckeditor5-ui/src/menubar/utils.ts +++ b/packages/ckeditor5-ui/src/menubar/utils.ts @@ -752,7 +752,7 @@ export const DefaultMenuBarItems: DeepReadonly = { groupId: 'insertMainWidgets', items: [ - 'menuBar:uploadImage', + 'menuBar:insertImage', 'menuBar:ckbox', 'menuBar:ckfinder', 'menuBar:insertTable'