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

Introduced Uploadcare integration #17366

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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-fly image optimization, fast delivery through global CDN network, multiple 3-rd party integrations and vast image editing capabilities like 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
20 changes: 20 additions & 0 deletions docs/features/using-file-managers.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,26 @@ With CKBox you can:

{@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.

## 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.

{@link features/uploadcare **Read a separate guide on Uploadcare**} 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.
Expand Down
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 @@ -274,7 +274,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 @@ -283,7 +284,8 @@ export default class Dialog extends Plugin {
className,
isModal,
position,
onHide
onHide,
keystrokeHandlerOptions
}: DialogDefinition ) {
const editor = this.editor;

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

const view = this.view;
Expand Down Expand Up @@ -476,6 +479,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 @@ -469,6 +469,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