Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add prototype submenu for image insert. #16334

Merged
merged 13 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/ckeditor5-ckbox/src/ckboxui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ export default class CKBoxUI extends Plugin {
return button;
},

menuBarButtonViewCreator: () => {
const button = this.editor.ui.componentFactory.create( 'menuBar:ckbox' ) as MenuBarMenuListItemButtonView;

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;

Expand Down
12 changes: 12 additions & 0 deletions packages/ckeditor5-ckfinder/src/ckfinderui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ export default class CKFinderUI extends Plugin {
return button;
},

menuBarButtonViewCreator: () => {
const button = this.editor.ui.componentFactory.create( 'menuBar:ckfinder' ) as MenuBarMenuListItemButtonView;

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;

Expand Down
55 changes: 53 additions & 2 deletions packages/ckeditor5-image/src/imageinsert/imageinsertui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ import {
type ButtonView,
type DropdownButtonView,
type DropdownView,
type FocusableView
type FocusableView,
type MenuBarMenuListItemButtonView,
MenuBarMenuView,
MenuBarMenuListView,
MenuBarMenuListItemView
} from 'ckeditor5/src/ui.js';

import ImageInsertFormView from './ui/imageinsertformview.js';
Expand Down Expand Up @@ -97,10 +101,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 );
}

/**
Expand All @@ -111,13 +118,15 @@ export default class ImageInsertUI extends Plugin {
observable,
buttonViewCreator,
formViewCreator,
requiresForm
requiresForm,
menuBarButtonViewCreator
}: {
name: string;
observable: Observable & { isEnabled: boolean } | ( () => Observable & { isEnabled: boolean } );
buttonViewCreator: ( isOnlyOne: boolean ) => ButtonView;
formViewCreator: ( isOnlyOne: boolean ) => FocusableView;
requiresForm?: boolean;
menuBarButtonViewCreator: ( isOnlyOne: boolean ) => MenuBarMenuListItemButtonView;
scofalik marked this conversation as resolved.
Show resolved Hide resolved
} ): void {
if ( this._integrations.has( name ) ) {
/**
Expand All @@ -133,6 +142,7 @@ export default class ImageInsertUI extends Plugin {
this._integrations.set( name, {
observable,
buttonViewCreator,
menuBarButtonViewCreator,
formViewCreator,
requiresForm: !!requiresForm
} );
Expand Down Expand Up @@ -190,6 +200,46 @@ export default class ImageInsertUI extends Plugin {
return dropdownView;
}

/**
* Creates the menu bar component.
*/
private _createMenuBarComponent( locale: Locale ): any {
scofalik marked this conversation as resolved.
Show resolved Hide resolved
const editor = this.editor;
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.bind( 'label' ).to( this, 'isImageSelected', isImageSelected => isImageSelected ?
t( 'Replace image' ) :
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.
*/
Expand Down Expand Up @@ -252,6 +302,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;
};
128 changes: 71 additions & 57 deletions packages/ckeditor5-image/src/imageinsert/imageinsertviaurlui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
*/

import { icons, Plugin } from 'ckeditor5/src/core.js';
import { ButtonView, CollapsibleView, DropdownButtonView, type FocusableView } from 'ckeditor5/src/ui.js';
import { ButtonView, 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).
Expand All @@ -40,6 +40,15 @@ export default class ImageInsertViaUrlUI extends Plugin {
return [ ImageInsertUI ] as const;
}

public init(): void {
this.editor.ui.componentFactory.add( 'menuBar:uploadUrl', locale => {
scofalik marked this conversation as resolved.
Show resolved Hide resolved
const t = locale.t;
const button = this._createInsertUrlButton( MenuBarMenuListItemButtonView );

return button;
} );
}

/**
* @inheritDoc
*/
Expand All @@ -50,24 +59,23 @@ export default class ImageInsertViaUrlUI extends Plugin {
name: 'url',
observable: () => this.editor.commands.get( 'insertImage' )!,
requiresForm: true,
buttonViewCreator: isOnlyOne => this._createInsertUrlButton( isOnlyOne ),
formViewCreator: isOnlyOne => this._createInsertUrlView( isOnlyOne )
buttonViewCreator: () => this._createInsertUrlButton( ButtonView ),
formViewCreator: () => this._createInsertUrlView(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should look and behave similar to this._createInsertUrlButton( MenuBarMenuListItemButtonView ). It should be a dropdown item (button) like other integration methods.

menuBarButtonViewCreator: () => this._createInsertUrlButton( MenuBarMenuListItemButtonView )
} );
}

/**
* Creates the view displayed in the dropdown.
*/
private _createInsertUrlView( isOnlyOne: boolean ): FocusableView {
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 ) => (
Expand All @@ -77,62 +85,30 @@ 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<ImageInsertUrlViewSubmitEvent>( 'submit', () => {
if ( replaceImageSourceCommand.isEnabled ) {
editor.execute( 'replaceImageSource', { source: imageInsertUrlView.imageURLInputValue } );
} else {
editor.execute( 'insertImage', { source: imageInsertUrlView.imageURLInputValue } );
}

this._closePanel();
} );

imageInsertUrlView.on<ImageInsertUrlViewCancelEvent>( '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.
*/
private _createInsertUrlButton( isOnlyOne: boolean ): ButtonView {
const ButtonClass = isOnlyOne ? DropdownButtonView : ButtonView;
private _createInsertUrlButton<T extends typeof ButtonView | typeof MenuBarMenuListItemButtonView>(
ButtonClass: T
): InstanceType<T> {
// const ButtonClass = isOnlyOne ? DropdownButtonView : ButtonView;
scofalik marked this conversation as resolved.
Show resolved Hide resolved

const editor = this.editor;
const button = new ButtonClass( editor.locale );
const button = new ButtonClass( editor.locale ) as InstanceType<T>;
const t = editor.locale.t;

button.set( {
label: t( 'Set image url' ),
icon: icons.imageUpload
} );
scofalik marked this conversation as resolved.
Show resolved Hide resolved

button.on( 'execute', () => {
this._showModal();
} );

button.set( {
icon: icons.imageUrl,
tooltip: true
Expand All @@ -147,10 +123,48 @@ export default class ImageInsertViaUrlUI extends Plugin {
}

/**
* Closes the dropdown.
*
*/
private _closePanel(): void {
this.editor.editing.view.focus();
this._imageInsertUI.dropdownView!.isOpen = false;
private _showModal() {
const editor = this.editor;
const locale = editor.locale;
const t = locale.t;
const dialog = editor.plugins.get( 'Dialog' );

const form = this._createInsertUrlView();

function handleSave( form: ImageInsertUrlView ) {
const replaceImageSourceCommand: ReplaceImageSourceCommand = editor.commands.get( 'replaceImageSource' )!;

if ( replaceImageSourceCommand.isEnabled ) {
scofalik marked this conversation as resolved.
Show resolved Hide resolved
editor.execute( 'replaceImageSource', { source: form.imageURLInputValue } );
} else {
editor.execute( 'insertImage', { source: form.imageURLInputValue } );
}

dialog.hide();
}
scofalik marked this conversation as resolved.
Show resolved Hide resolved

dialog.show( {
id: 'insertUrl',
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: () => handleSave( form as ImageInsertUrlView )
}
]
} );
}
}