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( '
' + - 'alt text' + - '
' + + 'alt text' + + '
' + '
' + '[
' + - 'alt text' + - '
' + + 'alt text' + + '
' + '
]' ); } ); } ); + + 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, '[alt text]' ); + 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' );