diff --git a/packages/ckeditor5-ckfinder/src/ckfinder.js b/packages/ckeditor5-ckfinder/src/ckfinder.js
index 35b05ab37cd..b2f6a6d56c9 100644
--- a/packages/ckeditor5-ckfinder/src/ckfinder.js
+++ b/packages/ckeditor5-ckfinder/src/ckfinder.js
@@ -9,9 +9,6 @@
import { Plugin } from 'ckeditor5/src/core';
-// TODO: softRequires()
-// import CKFinderUploadAdapter from '@ckeditor/ckeditor5-adapter-ckfinder/src/uploadadapter';
-
import CKFinderUI from './ckfinderui';
import CKFinderEditing from './ckfinderediting';
@@ -45,7 +42,7 @@ export default class CKFinder extends Plugin {
* @inheritDoc
*/
static get requires() {
- return [ CKFinderEditing, CKFinderUI ];
+ return [ 'Image', 'Link', 'CKFinderUploadAdapter', CKFinderEditing, CKFinderUI ];
}
}
diff --git a/packages/ckeditor5-ckfinder/src/ckfinderediting.js b/packages/ckeditor5-ckfinder/src/ckfinderediting.js
index 1ff07378c65..7c88643f741 100644
--- a/packages/ckeditor5-ckfinder/src/ckfinderediting.js
+++ b/packages/ckeditor5-ckfinder/src/ckfinderediting.js
@@ -10,10 +10,6 @@
import { Plugin } from 'ckeditor5/src/core';
import { Notification } from 'ckeditor5/src/ui';
-// TODO: softRequires()
-// import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting';
-// import LinkEditing from '@ckeditor/ckeditor5-link/src/linkediting';
-
import CKFinderCommand from './ckfindercommand';
/**
@@ -33,7 +29,7 @@ export default class CKFinderEditing extends Plugin {
* @inheritDoc
*/
static get requires() {
- return [ Notification ];
+ return [ Notification, 'ImageEditing', 'LinkEditing' ];
}
/**
diff --git a/packages/ckeditor5-ckfinder/tests/ckfinder.js b/packages/ckeditor5-ckfinder/tests/ckfinder.js
index bb2c7e3398d..7be97d08849 100644
--- a/packages/ckeditor5-ckfinder/tests/ckfinder.js
+++ b/packages/ckeditor5-ckfinder/tests/ckfinder.js
@@ -22,7 +22,7 @@ describe( 'CKFinder', () => {
return ClassicTestEditor
.create( editorElement, {
- plugins: [ Image, Link, CKFinder ]
+ plugins: [ CKFinderUploadAdapter, Image, Link, CKFinder ]
} )
.then( newEditor => {
editor = newEditor;
@@ -47,8 +47,8 @@ describe( 'CKFinder', () => {
expect( editor.plugins.get( CKFinderEditing ) ).to.instanceOf( CKFinderEditing );
} );
- it.skip( 'should load AdapterCKFinder plugin', () => {
- expect( editor.plugins.get( CKFinderUploadAdapter ) ).to.instanceOf( CKFinderUploadAdapter );
+ it( 'should require CKFinderUploadAdapter by name', () => {
+ expect( CKFinder.requires ).to.contain( 'CKFinderUploadAdapter' );
} );
it( 'has proper name', () => {
diff --git a/packages/ckeditor5-ckfinder/tests/ckfinderediting.js b/packages/ckeditor5-ckfinder/tests/ckfinderediting.js
index a5b1db493d2..e735bb008e4 100644
--- a/packages/ckeditor5-ckfinder/tests/ckfinderediting.js
+++ b/packages/ckeditor5-ckfinder/tests/ckfinderediting.js
@@ -15,6 +15,7 @@ import CKFinder from '../src/ckfinder';
import CKFinderEditing from '../src/ckfinderediting';
import Image from '@ckeditor/ckeditor5-image/src/image';
import Link from '@ckeditor/ckeditor5-link/src/link';
+import CKFinderUploadAdapter from '@ckeditor/ckeditor5-adapter-ckfinder/src/uploadadapter';
describe( 'CKFinderEditing', () => {
let editorElement, editor;
@@ -27,7 +28,7 @@ describe( 'CKFinderEditing', () => {
return ClassicTestEditor
.create( editorElement, {
- plugins: [ Image, Link, CKFinder ]
+ plugins: [ CKFinderUploadAdapter, Image, Link, CKFinder ]
} )
.then( newEditor => {
diff --git a/packages/ckeditor5-ckfinder/tests/ckfinderui.js b/packages/ckeditor5-ckfinder/tests/ckfinderui.js
index 570fd50658b..b7dea176d5b 100644
--- a/packages/ckeditor5-ckfinder/tests/ckfinderui.js
+++ b/packages/ckeditor5-ckfinder/tests/ckfinderui.js
@@ -10,6 +10,7 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
import global from '@ckeditor/ckeditor5-utils/src/dom/global';
import Image from '@ckeditor/ckeditor5-image/src/image';
import Link from '@ckeditor/ckeditor5-link/src/link';
+import CKFinderUploadAdapter from '@ckeditor/ckeditor5-adapter-ckfinder/src/uploadadapter';
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
@@ -27,7 +28,7 @@ describe( 'CKFinderUI', () => {
return ClassicTestEditor
.create( editorElement, {
- plugins: [ Image, Link, CKFinder ]
+ plugins: [ CKFinderUploadAdapter, Image, Link, CKFinder ]
} )
.then( newEditor => {
diff --git a/packages/ckeditor5-ckfinder/tests/manual/ckfinder.js b/packages/ckeditor5-ckfinder/tests/manual/ckfinder.js
index 57477c6f729..4f3317f69b8 100644
--- a/packages/ckeditor5-ckfinder/tests/manual/ckfinder.js
+++ b/packages/ckeditor5-ckfinder/tests/manual/ckfinder.js
@@ -6,14 +6,15 @@
/* globals console, window, document */
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
-
-import CKFinder from '../../src/ckfinder';
+import CKFinderUploadAdapter from '@ckeditor/ckeditor5-adapter-ckfinder/src/uploadadapter';
import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload';
+import CKFinder from '../../src/ckfinder';
+
ClassicEditor
.create( document.querySelector( '#editor' ), {
- plugins: [ ArticlePluginSet, ImageUpload, CKFinder ],
+ plugins: [ ArticlePluginSet, ImageUpload, CKFinderUploadAdapter, CKFinder ],
toolbar: [ 'heading', '|', 'undo', 'redo', 'ckfinder' ],
image: {
toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
diff --git a/packages/ckeditor5-core/src/plugincollection.js b/packages/ckeditor5-core/src/plugincollection.js
index 609fe1befa8..02b4fe4de14 100644
--- a/packages/ckeditor5-core/src/plugincollection.js
+++ b/packages/ckeditor5-core/src/plugincollection.js
@@ -175,6 +175,15 @@ export default class PluginCollection {
const context = this._context;
const loading = new Set();
const loaded = [];
+ const loadedPluginsNames = new Set();
+
+ const pluginsMap = new Map();
+
+ for ( const pluginOrName of plugins ) {
+ if ( typeof pluginOrName == 'function' && pluginOrName.pluginName ) {
+ pluginsMap.set( pluginOrName.pluginName, pluginOrName );
+ }
+ }
const pluginConstructors = mapToAvailableConstructors( plugins );
const removePluginConstructors = mapToAvailableConstructors( removePlugins );
@@ -273,8 +282,36 @@ export default class PluginCollection {
if ( PluginConstructor.requires ) {
PluginConstructor.requires.forEach( RequiredPluginConstructorOrName => {
+ if ( loadedPluginsNames.has( RequiredPluginConstructorOrName ) ) {
+ return;
+ }
+
const RequiredPluginConstructor = getPluginConstructor( RequiredPluginConstructorOrName );
+ if ( !RequiredPluginConstructor ) {
+ /**
+ * A required "soft" dependency was not found on plugin list.
+ *
+ * Plugin classes (constructors) need to be provided to the editor before they can be loaded by name.
+ * This is usually done in CKEditor 5 builds by setting the
+ * {@link module:core/editor/editor~Editor.builtinPlugins} property. Alternatively they can be provided using
+ * {@link module:core/editor/editorconfig~EditorConfig#plugins} or
+ * {@link module:core/editor/editorconfig~EditorConfig#extraPlugins} configuration.
+ *
+ * **If you see this warning when using one of the {@glink builds/index CKEditor 5 Builds}**, it means
+ * that you didn't add the required plugin to the plugins list when loading the editor.
+ *
+ * @error plugincollection-soft-required
+ * @param {String} plugin The name of the required plugin.
+ * @param {String} requiredBy The name of the plugin that was requiring other plugin.
+ */
+ throw new CKEditorError(
+ 'plugincollection-soft-required',
+ null,
+ { plugin: RequiredPluginConstructorOrName, requiredBy: PluginConstructor.name }
+ );
+ }
+
if ( PluginConstructor.isContextPlugin && !RequiredPluginConstructor.isContextPlugin ) {
/**
* If a plugin is a context plugin, all plugins it requires should also be context plugins
@@ -318,6 +355,10 @@ export default class PluginCollection {
that._add( PluginConstructor, plugin );
loaded.push( plugin );
+ if ( PluginConstructor.pluginName ) {
+ loadedPluginsNames.add( PluginConstructor.pluginName );
+ }
+
resolve();
} );
}
@@ -327,7 +368,7 @@ export default class PluginCollection {
return PluginConstructorOrName;
}
- return that._availablePlugins.get( PluginConstructorOrName );
+ return that._availablePlugins.get( PluginConstructorOrName ) || pluginsMap.get( PluginConstructorOrName );
}
function getMissingPluginNames( plugins ) {
diff --git a/packages/ckeditor5-core/tests/plugincollection.js b/packages/ckeditor5-core/tests/plugincollection.js
index 2a9768166c4..d7a3875ab84 100644
--- a/packages/ckeditor5-core/tests/plugincollection.js
+++ b/packages/ckeditor5-core/tests/plugincollection.js
@@ -485,6 +485,54 @@ describe( 'PluginCollection', () => {
expect( plugins.get( PluginB ) ).to.equal( externalPlugins.get( PluginB ) ).to.instanceof( PluginB );
expect( plugins.get( PluginC ) ).to.instanceof( PluginC );
} );
+
+ it( 'should load dependency plugins using soft requirement', () => {
+ const plugins = new PluginCollection( editor, availablePlugins );
+ const spy = sinon.spy( plugins, '_add' );
+
+ return plugins.init( [ PluginJ ] )
+ .then( loadedPlugins => {
+ expect( getPlugins( plugins ).length ).to.equal( 3 );
+
+ expect( getPluginNames( getPluginsFromSpy( spy ) ) )
+ .to.deep.equal( [ 'A', 'K', 'J' ], 'order by plugins._add()' );
+ expect( getPluginNames( loadedPlugins ) )
+ .to.deep.equal( [ 'A', 'K', 'J' ], 'order by returned value' );
+ } );
+ } );
+
+ it( 'should reject dependency plugins using soft requirement when plugin is unavailable', () => {
+ PluginFoo.requires = [ 'A', 'Baz' ];
+ const consoleErrorStub = sinon.stub( console, 'error' );
+ const plugins = new PluginCollection( editor, availablePlugins );
+
+ return plugins.init( [ PluginFoo ] )
+ // Throw here, so if by any chance plugins.init() was resolved correctly catch() will be still executed.
+ .then( () => {
+ throw new Error( 'Test error: this promise should not be resolved successfully' );
+ } )
+ .catch( err => {
+ assertCKEditorError( err, /^plugincollection-soft-required/, null, { plugin: 'Baz', requiredBy: 'P' } );
+
+ sinon.assert.calledOnce( consoleErrorStub );
+ } );
+ } );
+
+ it( 'should not reject dependency plugins using soft requirement when plugin was loaded as dependency of other plugin', () => {
+ PluginFoo.requires = [ 'A' ];
+ const plugins = new PluginCollection( editor, availablePlugins );
+ const spy = sinon.spy( plugins, '_add' );
+
+ return plugins.init( [ PluginD, PluginFoo ] )
+ .then( loadedPlugins => {
+ expect( getPlugins( plugins ).length ).to.equal( 5 );
+
+ expect( getPluginNames( getPluginsFromSpy( spy ) ) )
+ .to.deep.equal( [ 'A', 'B', 'C', 'D', 'Foo' ], 'order by plugins._add()' );
+ expect( getPluginNames( loadedPlugins ) )
+ .to.deep.equal( [ 'A', 'B', 'C', 'D', 'Foo' ], 'order by returned value' );
+ } );
+ } );
} );
describe( 'get()', () => {
diff --git a/packages/ckeditor5-easy-image/src/easyimage.js b/packages/ckeditor5-easy-image/src/easyimage.js
index d6d124bb9b2..e05a1a1c689 100644
--- a/packages/ckeditor5-easy-image/src/easyimage.js
+++ b/packages/ckeditor5-easy-image/src/easyimage.js
@@ -18,9 +18,12 @@ import CloudServicesUploadAdapter from './cloudservicesuploadadapter';
*
* This is a "glue" plugin which enables:
*
+ * * {@link module:easy-image/cloudservicesuploadadapter~CloudServicesUploadAdapter}.
+ *
+ * This plugin requires plugin to be present in the editor configuration:
+ *
* * {@link module:image/image~Image},
* * {@link module:image/imageupload~ImageUpload},
- * * {@link module:easy-image/cloudservicesuploadadapter~CloudServicesUploadAdapter}.
*
* See the {@glink features/image-upload/easy-image "Easy Image integration" guide} to learn how to configure
* and use this feature.
@@ -39,7 +42,7 @@ export default class EasyImage extends Plugin {
* @inheritDoc
*/
static get requires() {
- return [ CloudServicesUploadAdapter ];
+ return [ CloudServicesUploadAdapter, 'Image', 'ImageUpload' ];
}
/**
diff --git a/packages/ckeditor5-easy-image/tests/easyimage.js b/packages/ckeditor5-easy-image/tests/easyimage.js
index 9199b96db8c..2e7d19adbfe 100644
--- a/packages/ckeditor5-easy-image/tests/easyimage.js
+++ b/packages/ckeditor5-easy-image/tests/easyimage.js
@@ -32,9 +32,15 @@ describe( 'EasyImage', () => {
} );
it( 'should require other plugins', () => {
- const plugins = EasyImage.requires;
+ expect( EasyImage.requires ).to.include( CloudServicesUploadAdapter );
+ } );
+
+ it( 'should require Image by name', () => {
+ expect( EasyImage.requires ).to.include( 'Image' );
+ } );
- expect( plugins ).to.include( CloudServicesUploadAdapter );
+ it( 'should require ImageUpload by name', () => {
+ expect( EasyImage.requires ).to.include( 'ImageUpload' );
} );
it( 'should be able to initialize editor with itself', () => {
diff --git a/packages/ckeditor5-image/package.json b/packages/ckeditor5-image/package.json
index f86b64499a5..61ef1ad08a2 100644
--- a/packages/ckeditor5-image/package.json
+++ b/packages/ckeditor5-image/package.json
@@ -15,6 +15,7 @@
"ckeditor5": "^24.0.0"
},
"devDependencies": {
+ "@ckeditor/ckeditor5-adapter-ckfinder": "^24.0.0",
"@ckeditor/ckeditor5-basic-styles": "^24.0.0",
"@ckeditor/ckeditor5-block-quote": "^24.0.0",
"@ckeditor/ckeditor5-ckfinder": "^24.0.0",
diff --git a/packages/ckeditor5-image/tests/image.js b/packages/ckeditor5-image/tests/image.js
index 4fa252dee83..2db00327051 100644
--- a/packages/ckeditor5-image/tests/image.js
+++ b/packages/ckeditor5-image/tests/image.js
@@ -122,20 +122,32 @@ describe( 'Image', () => {
expect( getViewData( view ) ).to.equal(
'' +
- '
' +
- '' +
+ '
' +
+ '' +
'' +
'[' +
- '
' +
- '' +
+ '
' +
+ '' +
']'
);
} );
} );
+
+ describe( 'isImageWidget()', () => {
+ it( 'should expose isImageWidget() utility', () => {
+ expect( editor.plugins.get( 'Image' ) ).to.respondTo( 'isImageWidget' );
+ } );
+
+ it( 'should return true for elements marked with toImageWidget()', () => {
+ setModelData( model, '[]' );
+ const element = viewDocument.getRoot().getChild( 0 );
+ expect( editor.plugins.get( 'Image' ).isImageWidget( element ) ).to.be.true;
+ } );
+ } );
} );
diff --git a/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js b/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js
index 2e044121e9a..6423f350870 100644
--- a/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js
+++ b/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js
@@ -25,6 +25,8 @@ import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfie
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
import { UploadAdapterMock } from '@ckeditor/ckeditor5-upload/tests/_utils/mocks';
+import CKFinderUploadAdapter from '@ckeditor/ckeditor5-adapter-ckfinder/src/uploadadapter';
+import Link from '@ckeditor/ckeditor5-link/src/link';
describe( 'ImageInsertUI', () => {
let editor, editorElement, fileRepository, dropdown;
@@ -298,9 +300,11 @@ describe( 'ImageInsertUI', () => {
const editor = await ClassicEditor
.create( editorElement, {
plugins: [
+ Link,
+ Image,
+ CKFinderUploadAdapter,
CKFinder,
Paragraph,
- Image,
ImageInsert,
ImageInsertUI,
FileRepository,
diff --git a/packages/ckeditor5-image/tests/imageinsert/utils.js b/packages/ckeditor5-image/tests/imageinsert/utils.js
index c0d155e5902..d48d19a99b1 100644
--- a/packages/ckeditor5-image/tests/imageinsert/utils.js
+++ b/packages/ckeditor5-image/tests/imageinsert/utils.js
@@ -14,6 +14,7 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Link from '@ckeditor/ckeditor5-link/src/link';
import CKFinder from '@ckeditor/ckeditor5-ckfinder/src/ckfinder';
import { prepareIntegrations, createLabeledInputView } from '../../src/imageinsert/utils';
+import CKFinderUploadAdapter from '@ckeditor/ckeditor5-adapter-ckfinder/src/uploadadapter';
describe( 'Upload utils', () => {
describe( 'prepareIntegrations()', () => {
@@ -24,10 +25,12 @@ describe( 'Upload utils', () => {
const editor = await ClassicEditor
.create( editorElement, {
plugins: [
- CKFinder,
Paragraph,
+ Link,
Image,
- ImageUploadUI
+ ImageUploadUI,
+ CKFinderUploadAdapter,
+ CKFinder
],
image: {
insert: {
diff --git a/packages/ckeditor5-link/src/linkimageediting.js b/packages/ckeditor5-link/src/linkimageediting.js
index 1d975c51bf0..dbbec156447 100644
--- a/packages/ckeditor5-link/src/linkimageediting.js
+++ b/packages/ckeditor5-link/src/linkimageediting.js
@@ -28,7 +28,7 @@ export default class LinkImageEditing extends Plugin {
* @inheritDoc
*/
static get requires() {
- return [ LinkEditing ];
+ return [ 'ImageEditing', LinkEditing ];
}
/**
diff --git a/packages/ckeditor5-link/src/linkimageui.js b/packages/ckeditor5-link/src/linkimageui.js
index 41c8f2669c9..0f49a4642ce 100644
--- a/packages/ckeditor5-link/src/linkimageui.js
+++ b/packages/ckeditor5-link/src/linkimageui.js
@@ -30,7 +30,7 @@ export default class LinkImageUI extends Plugin {
* @inheritDoc
*/
static get requires() {
- return [ LinkEditing, LinkUI ];
+ return [ LinkEditing, LinkUI, 'Image' ];
}
/**
diff --git a/packages/ckeditor5-link/tests/linkimageediting.js b/packages/ckeditor5-link/tests/linkimageediting.js
index 74b7a3157e0..38448a2d03c 100644
--- a/packages/ckeditor5-link/tests/linkimageediting.js
+++ b/packages/ckeditor5-link/tests/linkimageediting.js
@@ -40,6 +40,10 @@ describe( 'LinkImageEditing', () => {
expect( editor.plugins.get( LinkImageEditing ) ).to.be.instanceOf( LinkImageEditing );
} );
+ it( 'should require ImageEditing by name', () => {
+ expect( LinkImageEditing.requires ).to.include( 'ImageEditing' );
+ } );
+
it( 'should set proper schema rules', () => {
expect( model.schema.checkAttribute( [ '$root', 'image' ], 'linkHref' ) ).to.be.true;
} );
diff --git a/packages/ckeditor5-link/tests/linkimageui.js b/packages/ckeditor5-link/tests/linkimageui.js
index 99e385e495b..b591c49a542 100644
--- a/packages/ckeditor5-link/tests/linkimageui.js
+++ b/packages/ckeditor5-link/tests/linkimageui.js
@@ -49,6 +49,10 @@ describe( 'LinkImageUI', () => {
expect( LinkImageUI.pluginName ).to.equal( 'LinkImageUI' );
} );
+ it( 'should require Image by name', () => {
+ expect( LinkImageUI.requires ).to.include( 'Image' );
+ } );
+
describe( 'init()', () => {
it( 'should listen to the click event on the images', () => {
const listenToSpy = sinon.stub( plugin, 'listenTo' );