Skip to content

Commit

Permalink
Merge pull request #17366 from ckeditor/cc/6626
Browse files Browse the repository at this point in the history
Docs: Added information about Uploadcare integration in the Image Upload and File Adapters sections.

Other (ui): Extended the dialog API to support custom keystroke handler options, allowing to override priorities of the keystroke callback and filter keystrokes based on arbitrary criteria.

Other (ui): The `ImageInsertUI#registerIntegration` method now supports handling an array of views for a specific integration type. This allows, for example, registering an `assetManager` integration with multiple sources like `Facebook` and `Instagram`, where each source has its own dedicated button.
  • Loading branch information
DawidKossowski authored Jan 27, 2025
2 parents a9734ad + 6419558 commit 546751c
Show file tree
Hide file tree
Showing 12 changed files with 158 additions and 51 deletions.
12 changes: 12 additions & 0 deletions docs/features/image-upload.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ With CKBox, users can upload files and categorize them into different groups. Th

{@link features/ckbox **Learn how to use CKBox in your project**}.

### Uploadcare

Uploadcare is the ultimate solution for image upload and editing in CKEditor 5.

It is a modern file uploader with a clean interface, automatic support for responsive images, on-the-fly image optimization, fast delivery through global CDN network, multiple third party integrations and vast image editing capabilities such as cropping, filtering and adjusting image parameters.

Thanks to the native CKEditor 5 integration, Uploadcare supports drag-and-drop file upload as well as pasting images from the clipboard, Microsoft Word, or Google Docs.

With Uploadcare, users can upload files from external services like Dropbox, Facebook, Google Drive, Google Photos, Instagram, OneDrive or from local computer. Images can be easily adjusted with built-in image editor.

{@link features/uploadcare **Learn how to use Uploadcare in your project**}.

### CKFinder

The {@link features/ckfinder CKFinder feature} provides a bridge between the rich-text editor and [CKFinder](https://ckeditor.com/ckfinder/), a browser-based file uploader with server-side connectors (PHP, Java, and ASP.NET).
Expand Down
50 changes: 35 additions & 15 deletions docs/features/using-file-managers.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,45 @@ The most convenient way to upload, manage, and insert images into content in CKE
CKBox is a modern file management platform with a clean UI and a top-notch UX.

With CKBox you can:
- Organize images and other files into customizable categories.
- Create, rename, and delete folders.
- Delete, rename, and tag files.
- Search files and filter results by numerous properties.
- Easily access recently used files.
- View images in high-resolution full-page preview.
- Define and reuse alternative text for images.

{@link features/ckbox **Read a separate guide on CKBox**} to learn about its installation and configuration. The guide also lets you try CKBox in action.
* Organize images and other files into customizable categories.
* Create, rename, and delete folders.
* Delete, rename, and tag files.
* Search files and filter results by numerous properties.
* Easily access recently used files.
* View images in high-resolution full-page preview.
* Define and reuse alternative text for images.

**Read the {@link features/ckbox dedicated CKBox feature guide**} to learn about its installation and configuration. The guide also lets you try CKBox in action.

## Uploadcare file manager

Uploadcare is a modern file management platform with multiple integrations with services like Dropbox, Facebook, Google Drive, Google Photos, Instagram and OneDrive.

With Uploadcare you can:
* Upload images directly from:
* Dropbox,
* Facebook,
* Google Drive,
* Google Photos,
* Instagram,
* OneDrive,
* Local computer,
* External URL.
* Manage all uploaded images through a dedicated platform.
* Edit images by changing dimensions, applying filters and adjusting various image parameters.
* Get your images served quickly through global CDN.

**Read the {@link features/uploadcare dedicated Uploadcare feature guide**} to learn about its installation and configuration. The guide also lets you try Uploadcare in action.

## CKFinder file manager

CKFinder is a powerful file manager with various image editing and image upload options.

With CKFinder you can:
- Group images and other files into folders and subfolders.
- Move or copy files between folders.
- Easily filter files.
- Drag and drop images and paste them from the clipboard into the editor.
- Crop, rotate, edit, and resize images.
* Group images and other files into folders and subfolders.
* Move or copy files between folders.
* Easily filter files.
* Drag and drop images and paste them from the clipboard into the editor.
* Crop, rotate, edit, and resize images.

{@link features/ckfinder **Read a separate guide on CKFinder**} to learn about its installation and configuration. The guide also lets you try CKFinder in action.
**Read the {@link features/ckfinder dedicated CKFinder feature guide**} to learn about its installation and configuration. The guide also lets you try CKFinder in action.
3 changes: 2 additions & 1 deletion packages/ckeditor5-ckbox/tests/ckboxui.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ describe( 'CKBoxUI', () => {
} );

it( 'should create CKBox button in menu bar - only integration', () => {
const buttonView = editor.ui.componentFactory.create( 'menuBar:insertImage' );
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;
Expand Down
3 changes: 2 additions & 1 deletion packages/ckeditor5-ckfinder/tests/ckfinderui.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ describe( 'CKFinderUI', () => {
} );

it( 'should create CKFinder button in menu bar - only integration', () => {
const buttonView = editor.ui.componentFactory.create( 'menuBar:insertImage' );
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;
Expand Down
50 changes: 25 additions & 25 deletions packages/ckeditor5-image/src/imageinsert/imageinsertui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,16 +131,18 @@ export default class ImageInsertUI extends Plugin {
buttonViewCreator,
formViewCreator,
menuBarButtonViewCreator,
requiresForm = false
requiresForm = false,
override = false
}: {
name: string;
observable: Observable & { isEnabled: boolean } | ( () => Observable & { isEnabled: boolean } );
buttonViewCreator: ( isOnlyOne: boolean ) => ButtonView;
formViewCreator: ( isOnlyOne: boolean ) => FocusableView;
menuBarButtonViewCreator: ( isOnlyOne: boolean ) => MenuBarMenuListItemButtonView;
formViewCreator: ( isOnlyOne: boolean ) => FocusableView | Array<FocusableView>;
menuBarButtonViewCreator: ( isOnlyOne: boolean ) => MenuBarMenuListItemButtonView | Array<MenuBarMenuListItemButtonView>;
requiresForm?: boolean;
override?: boolean;
} ): void {
if ( this._integrations.has( name ) ) {
if ( this._integrations.has( name ) && !override ) {
/**
* There are two insert-image integrations registered with the same name.
*
Expand Down Expand Up @@ -203,7 +205,7 @@ export default class ImageInsertUI extends Plugin {
) );

dropdownView.once( 'change:isOpen', () => {
const integrationViews = integrations.map( ( { formViewCreator } ) => formViewCreator( integrations.length == 1 ) );
const integrationViews = integrations.flatMap( ( { formViewCreator } ) => formViewCreator( integrations.length == 1 ) );
const imageInsertFormView = new ImageInsertFormView( editor.locale, integrationViews );

dropdownView.panelView.children.add( imageInsertFormView );
Expand All @@ -224,28 +226,26 @@ export default class ImageInsertUI extends Plugin {
return null as any;
}

let resultView: MenuBarMenuListItemButtonView | MenuBarMenuView | undefined;
const firstIntegration = integrations[ 0 ];
const integrationViews = integrations.flatMap( ( {
menuBarButtonViewCreator
} ) => menuBarButtonViewCreator( integrations.length == 1 ) );

if ( integrations.length == 1 ) {
resultView = firstIntegration.menuBarButtonViewCreator( true );
} else {
resultView = new MenuBarMenuView( locale );
const listView = new MenuBarMenuListView( locale );
resultView.panelView.children.add( listView );
const resultView = new MenuBarMenuView( locale );
const listView = new MenuBarMenuListView( locale );
resultView.panelView.children.add( listView );

resultView.buttonView.set( {
icon: icons.image,
label: t( 'Image' )
} );
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 );
for ( const integrationView of integrationViews ) {
const listItemView = new MenuBarMenuListItemView( locale, resultView );

listItemView.children.add( buttonView );
listView.items.add( listItemView );
}
listItemView.children.add( integrationView );
listView.items.add( listItemView );

integrationView.delegate( 'execute' ).to( resultView );
}

return resultView;
Expand Down Expand Up @@ -313,7 +313,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;
menuBarButtonViewCreator: ( isOnlyOne: boolean ) => MenuBarMenuListItemButtonView | Array<MenuBarMenuListItemButtonView>;
formViewCreator: ( isOnlyOne: boolean ) => FocusableView | Array<FocusableView>;
requiresForm: boolean;
};
7 changes: 6 additions & 1 deletion packages/ckeditor5-image/tests/imageinsert/imageinsertui.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,12 @@ describe( 'ImageInsertUI', () => {
} );

it( 'should create a menu bar button', () => {
const button = editor.ui.componentFactory.create( 'menuBar:insertImage' );
const menu = editor.ui.componentFactory.create( 'menuBar:insertImage' );

expect( menu ).to.be.instanceOf( MenuBarMenuView );

const submenuList = menu.panelView.children.get( 0 );
const button = submenuList.items.get( 0 ).children.get( 0 );

expect( button ).to.be.instanceOf( MenuBarMenuListItemButtonView );
expect( button.label ).to.equal( 'button url' );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,10 @@ describe( 'ImageInsertViaUrlUI', () => {

describe( 'menu bar button', () => {
beforeEach( () => {
button = editor.ui.componentFactory.create( 'menuBar:insertImage' );
const menu = editor.ui.componentFactory.create( 'menuBar:insertImage' );
const submenuList = menu.panelView.children.get( 0 );

button = submenuList.items.get( 0 ).children.get( 0 );
} );

testButton( MenuBarMenuListItemButtonView, 'Image' );
Expand Down
4 changes: 3 additions & 1 deletion packages/ckeditor5-image/tests/imageupload/imageuploadui.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,9 @@ describe( 'ImageUploadUI', () => {
} );

it( 'should create FileDialogButtonView in insert image submenu - only integration', () => {
button = editor.ui.componentFactory.create( 'menuBar:insertImage' );
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;
Expand Down
14 changes: 12 additions & 2 deletions packages/ckeditor5-ui/src/dialog/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { type Editor, Plugin } from '@ckeditor/ckeditor5-core';
import DialogView, { type DialogViewCloseEvent, DialogViewPosition } from './dialogview.js';
import type { DialogActionButtonDefinition } from './dialogactionsview.js';
import type { DocumentChangeEvent } from '@ckeditor/ckeditor5-engine';
import type { KeystrokeHandlerOptions } from '@ckeditor/ckeditor5-utils';

/**
* The dialog controller class. It is used to show and hide the {@link module:ui/dialog/dialogview~DialogView}.
Expand Down Expand Up @@ -285,7 +286,8 @@ export default class Dialog extends Plugin {
className,
isModal,
position,
onHide
onHide,
keystrokeHandlerOptions
}: DialogDefinition ) {
const editor = this.editor;

Expand All @@ -295,7 +297,8 @@ export default class Dialog extends Plugin {
},
getViewportOffset: () => {
return editor.ui.viewportOffset;
}
},
keystrokeHandlerOptions
} );

const view = this.view;
Expand Down Expand Up @@ -478,6 +481,13 @@ export interface DialogDefinition {
* It allows for cleaning up (for example, resetting) the dialog's {@link #content}.
*/
onHide?: ( dialog: Dialog ) => void;

/**
* Options that will be passed to the {@link module:utils/keystrokehandler~KeystrokeHandler keystroke handler} of the dialog view.
*
* See {@link module:utils/keystrokehandler~KeystrokeHandlerOptions KeystrokeHandlerOptions} to learn more about the available options.
*/
keystrokeHandlerOptions?: KeystrokeHandlerOptions;
}

/**
Expand Down
10 changes: 7 additions & 3 deletions packages/ckeditor5-ui/src/dialog/dialogview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
toUnit,
type EventInfo,
type Locale,
type DecoratedMethodEvent
type DecoratedMethodEvent,
type KeystrokeHandlerOptions
} from '@ckeditor/ckeditor5-utils';
import { icons } from '@ckeditor/ckeditor5-core';
import ViewCollection from '../viewcollection.js';
Expand Down Expand Up @@ -204,10 +205,12 @@ export default class DialogView extends /* #__PURE__ */ DraggableViewMixin( View
constructor( locale: Locale,
{
getCurrentDomRoot,
getViewportOffset
getViewportOffset,
keystrokeHandlerOptions
}: {
getCurrentDomRoot: () => HTMLElement;
getViewportOffset: () => EditorUI[ 'viewportOffset' ];
keystrokeHandlerOptions?: KeystrokeHandlerOptions;
}
) {
super( locale );
Expand Down Expand Up @@ -243,7 +246,8 @@ export default class DialogView extends /* #__PURE__ */ DraggableViewMixin( View

// Navigate form fields forwards using the Tab key.
focusNext: 'tab'
}
},
keystrokeHandlerOptions
} );

this.setTemplate( {
Expand Down
25 changes: 24 additions & 1 deletion packages/ckeditor5-ui/tests/dialog/dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictest

import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
import { Dialog, DialogView, DialogViewPosition, IconView } from '../../src/index.js';
import { env, keyCodes } from '@ckeditor/ckeditor5-utils';
import { env, keyCodes, KeystrokeHandler } from '@ckeditor/ckeditor5-utils';
import loupeIcon from '@ckeditor/ckeditor5-find-and-replace/theme/icons/find-replace.svg';

/* global document */
Expand Down Expand Up @@ -487,6 +487,29 @@ describe( 'Dialog', () => {

expect( document.documentElement.classList.contains( 'ck-dialog-scroll-locked' ) ).to.be.false;
} );

it( 'should pass keystrokeHandlerOptions to its view', () => {
// The 'keystrokeHandlerOptions' are not stored anywhere so we need to somehow
// detect if those are passed correctly. It is passed like shown below:
//
// Dialog._show -> new DialogView( { ..., keystrokeHandlerOptions } )
// DialogView.constructor -> new FocusCycler( { ..., keystrokeHandler, keystrokeHandlerOptions } )
// FocusCycler.constructor -> keystrokeHandler.set( { ..., keystrokeHandlerOptions } )
//
// And so we spy on the `set` method of the KeystrokeHandler to check if options is passed there.
const spy = sinon.spy( KeystrokeHandler.prototype, 'set' );

const keystrokeHandlerOptions = {
filter: () => {}
};

dialogPlugin._show( {
keystrokeHandlerOptions
} );

expect( spy.args[ 0 ][ 2 ] ).to.equal( keystrokeHandlerOptions );
expect( spy.args[ 1 ][ 2 ] ).to.equal( keystrokeHandlerOptions );
} );
} );

describe( 'hide()', () => {
Expand Down
26 changes: 26 additions & 0 deletions packages/ckeditor5-ui/tests/dialog/dialogview.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,32 @@ describe( 'DialogView', () => {
} );
} );
} );

describe( 'keystrokeHandlerOptions', () => {
it( 'should use passed keystroke handler options filter', async () => {
const filterSpy = sinon.spy();

const newView = new DialogView( locale, {
getCurrentDomRoot: getCurrentDomRootStub,
getViewportOffset: getViewportOffsetStub,
keystrokeHandlerOptions: {
filter: filterSpy
}
} );

newView.render();

newView.keystrokes.press( {
keyCode: keyCodes.tab,
preventDefault: sinon.spy(),
stopPropagation: sinon.spy()
} );

await wait( 5 );

expect( filterSpy ).to.be.calledOnce;
} );
} );
} );

describe( 'render()', () => {
Expand Down

0 comments on commit 546751c

Please sign in to comment.