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

BlockToolbar and BalloonToolbar should init after all plugins are init-ed (and afterInit-ed). #15898

Merged
merged 7 commits into from Feb 26, 2024
43 changes: 34 additions & 9 deletions packages/ckeditor5-ckbox/src/ckboxediting.ts
Expand Up @@ -62,29 +62,47 @@ export default class CKBoxEditing extends Plugin {
*/
public init(): void {
const editor = this.editor;
const hasConfiguration = !!editor.config.get( 'ckbox' );
const isLibraryLoaded = !!window.CKBox;

// Proceed with plugin initialization only when the integrator intentionally wants to use it, i.e. when the `config.ckbox` exists or
// the CKBox JavaScript library is loaded.
if ( !hasConfiguration && !isLibraryLoaded ) {
if ( !this._shouldBeInitialised() ) {
return;
}

this._checkImagePlugins();

// Registering the `ckbox` command makes sense only if the CKBox library is loaded, as the `ckbox` command opens the CKBox dialog.
if ( isLibraryLoaded() ) {
editor.commands.add( 'ckbox', new CKBoxCommand( editor ) );
}
}

/**
* @inheritdoc
*/
public afterInit(): void {
const editor = this.editor;

if ( !this._shouldBeInitialised() ) {
return;
}

// Extending the schema, registering converters and applying fixers only make sense if the configuration option to assign
// the assets ID with the model elements is enabled.
if ( !editor.config.get( 'ckbox.ignoreDataId' ) ) {
this._initSchema();
this._initConversion();
this._initFixers();
}
}

// Registering the `ckbox` command makes sense only if the CKBox library is loaded, as the `ckbox` command opens the CKBox dialog.
if ( isLibraryLoaded ) {
editor.commands.add( 'ckbox', new CKBoxCommand( editor ) );
}
/**
* Returns true only when the integrator intentionally wants to use the plugin, i.e. when the `config.ckbox` exists or
* the CKBox JavaScript library is loaded.
*/
private _shouldBeInitialised(): boolean {
const editor = this.editor;
const hasConfiguration = !!editor.config.get( 'ckbox' );

return hasConfiguration || isLibraryLoaded();
}

/**
Expand Down Expand Up @@ -421,3 +439,10 @@ function shouldUpcastAttributeForNode( node: Node ) {

return false;
}

/**
* Returns true if the CKBox library is loaded, false otherwise.
*/
function isLibraryLoaded(): boolean {
return !!window.CKBox;
}
8 changes: 4 additions & 4 deletions packages/ckeditor5-ckbox/src/ckboxui.ts
Expand Up @@ -31,17 +31,17 @@ export default class CKBoxUI extends Plugin {
public afterInit(): void {
const editor = this.editor;

const command: CKBoxCommand | undefined = editor.commands.get( 'ckbox' );

// Do not register the `ckbox` button if the command does not exist.
if ( !command ) {
// This might happen when CKBox library is not loaded on the page.
if ( !editor.commands.get( 'ckbox' ) ) {
return;
}

const t = editor.t;
const componentFactory = editor.ui.componentFactory;

componentFactory.add( 'ckbox', locale => {
const command: CKBoxCommand = editor.commands.get( 'ckbox' )!;
const button = new ButtonView( locale );

button.set( {
Expand All @@ -64,7 +64,7 @@ export default class CKBoxUI extends Plugin {

imageInsertUI.registerIntegration( {
name: 'assetManager',
observable: command,
observable: () => editor.commands.get( 'ckbox' )!,

buttonViewCreator: () => {
const button = this.editor.ui.componentFactory.create( 'ckbox' ) as ButtonView;
Expand Down
67 changes: 52 additions & 15 deletions packages/ckeditor5-ckbox/tests/ckboxediting.js
Expand Up @@ -187,6 +187,36 @@ describe( 'CKBoxEditing', () => {

await editor.destroy();
} );

describe( 'CKBox loaded before the ImageBlock and ImageInline plugins', () => {
let editor, model, originalCKBox;

beforeEach( async () => {
TokenMock.initialToken = 'ckbox-token';

originalCKBox = window.CKBox;
window.CKBox = {};

editor = await createTestEditor( {
ckbox: {
tokenUrl: 'http://cs.example.com'
}
}, true );

model = editor.model;
} );

afterEach( async () => {
window.CKBox = originalCKBox;
await editor.destroy();
} );

// https://github.com/ckeditor/ckeditor5/issues/15581
it( 'should extend the schema rules for imageBlock and imageInline', () => {
expect( model.schema.checkAttribute( [ '$root', 'imageBlock' ], 'ckboxImageId' ) ).to.be.true;
expect( model.schema.checkAttribute( [ '$root', '$block', 'imageInline' ], 'ckboxImageId' ) ).to.be.true;
} );
} );
} );

describe( 'conversion', () => {
Expand Down Expand Up @@ -1802,22 +1832,29 @@ describe( 'CKBoxEditing', () => {
} );
} );

function createTestEditor( config = {} ) {
function createTestEditor( config = {}, loadCKBoxFirst = false ) {
const plugins = [
Paragraph,
ImageBlockEditing,
ImageInlineEditing,
ImageCaptionEditing,
LinkEditing,
LinkImageEditing,
PictureEditing,
ImageUploadEditing,
ImageUploadProgress,
CloudServices,
CKBoxUploadAdapter
];

if ( loadCKBoxFirst ) {
plugins.unshift( CKBoxEditing );
} else {
plugins.push( CKBoxEditing );
}

return VirtualTestEditor.create( {
plugins: [
Paragraph,
ImageBlockEditing,
ImageInlineEditing,
ImageCaptionEditing,
LinkEditing,
LinkImageEditing,
PictureEditing,
ImageUploadEditing,
ImageUploadProgress,
CloudServices,
CKBoxUploadAdapter,
CKBoxEditing
],
plugins,
substitutePlugins: [
CloudServicesCoreMock
],
Expand Down
3 changes: 1 addition & 2 deletions packages/ckeditor5-ckfinder/src/ckfinderui.ts
Expand Up @@ -55,11 +55,10 @@ export default class CKFinderUI extends Plugin {

if ( editor.plugins.has( 'ImageInsertUI' ) ) {
const imageInsertUI: ImageInsertUI = editor.plugins.get( 'ImageInsertUI' );
const command: CKFinderCommand = editor.commands.get( 'ckfinder' )!;

imageInsertUI.registerIntegration( {
name: 'assetManager',
observable: command,
observable: () => editor.commands.get( 'ckfinder' )!,

buttonViewCreator: () => {
const button = this.editor.ui.componentFactory.create( 'ckfinder' ) as ButtonView;
Expand Down
8 changes: 4 additions & 4 deletions packages/ckeditor5-image/src/imageinsert/imageinsertui.ts
Expand Up @@ -114,11 +114,11 @@ export default class ImageInsertUI extends Plugin {
requiresForm
}: {
name: string;
observable: Observable & { isEnabled: boolean };
observable: Observable & { isEnabled: boolean } | ( () => Observable & { isEnabled: boolean } );
buttonViewCreator: ( isOnlyOne: boolean ) => ButtonView;
formViewCreator: ( isOnlyOne: boolean ) => FocusableView;
requiresForm?: boolean;
} ): void {
} ): void {
if ( this._integrations.has( name ) ) {
/**
* There are two insert-image integrations registered with the same name.
Expand Down Expand Up @@ -174,7 +174,7 @@ export default class ImageInsertUI extends Plugin {
}

const dropdownView = this.dropdownView = createDropdown( locale, dropdownButton );
const observables = integrations.map( ( { observable } ) => observable );
const observables = integrations.map( ( { observable } ) => typeof observable == 'function' ? observable() : observable );

dropdownView.bind( 'isEnabled' ).toMany( observables, 'isEnabled', ( ...isEnabled ) => (
isEnabled.some( isEnabled => isEnabled )
Expand Down Expand Up @@ -250,7 +250,7 @@ export default class ImageInsertUI extends Plugin {
}

type IntegrationData = {
observable: Observable & { isEnabled: boolean };
observable: Observable & { isEnabled: boolean } | ( () => Observable & { isEnabled: boolean } );
buttonViewCreator: ( isOnlyOne: boolean ) => ButtonView;
formViewCreator: ( isOnlyOne: boolean ) => FocusableView;
requiresForm: boolean;
Expand Down
Expand Up @@ -45,11 +45,10 @@ export default class ImageInsertViaUrlUI extends Plugin {
*/
public afterInit(): void {
this._imageInsertUI = this.editor.plugins.get( 'ImageInsertUI' );
const insertImageCommand: InsertImageCommand = this.editor.commands.get( 'insertImage' )!;

this._imageInsertUI.registerIntegration( {
name: 'url',
observable: insertImageCommand,
observable: () => this.editor.commands.get( 'insertImage' )!,
requiresForm: true,
buttonViewCreator: isOnlyOne => this._createInsertUrlButton( isOnlyOne ),
formViewCreator: isOnlyOne => this._createInsertUrlView( isOnlyOne )
Expand Down
3 changes: 1 addition & 2 deletions packages/ckeditor5-image/src/imageupload/imageuploadui.ts
Expand Up @@ -72,11 +72,10 @@ export default class ImageUploadUI extends Plugin {

if ( editor.plugins.has( 'ImageInsertUI' ) ) {
const imageInsertUI: ImageInsertUI = editor.plugins.get( 'ImageInsertUI' );
const command: UploadImageCommand = editor.commands.get( 'uploadImage' )!;

imageInsertUI.registerIntegration( {
name: 'upload',
observable: command,
observable: () => editor.commands.get( 'uploadImage' )!,

buttonViewCreator: () => {
const uploadImageButton = editor.ui.componentFactory.create( 'uploadImage' ) as FileDialogButtonView;
Expand Down
51 changes: 47 additions & 4 deletions packages/ckeditor5-image/tests/imageinsert/imageinsertui.js
Expand Up @@ -296,6 +296,22 @@ describe( 'ImageInsertUI', () => {
} );
} );

describe( 'single integration with form view required and observalbe as a function', () => {
beforeEach( async () => {
registerUrlIntegration( true );
} );

it( 'should bind isEnabled state to observable', () => {
const dropdown = editor.ui.componentFactory.create( 'insertImage' );

observableUrl.isEnabled = false;
expect( dropdown.isEnabled ).to.be.false;

observableUrl.isEnabled = true;
expect( dropdown.isEnabled ).to.be.true;
} );
} );

describe( 'multiple integrations', () => {
beforeEach( async () => {
registerUploadIntegration();
Expand Down Expand Up @@ -365,12 +381,39 @@ describe( 'ImageInsertUI', () => {
} );
} );

function registerUrlIntegration() {
describe( 'multiple integrations and observalbe as a function', () => {
beforeEach( async () => {
registerUploadIntegration( true );
registerUrlIntegration( true );
} );

it( 'should bind isEnabled state to observables', () => {
const dropdown = editor.ui.componentFactory.create( 'insertImage' );

observableUrl.isEnabled = false;
observableUpload.isEnabled = false;
expect( dropdown.isEnabled ).to.be.false;

observableUrl.isEnabled = true;
observableUpload.isEnabled = false;
expect( dropdown.isEnabled ).to.be.true;

observableUrl.isEnabled = false;
observableUpload.isEnabled = true;
expect( dropdown.isEnabled ).to.be.true;

observableUrl.isEnabled = true;
observableUpload.isEnabled = true;
expect( dropdown.isEnabled ).to.be.true;
} );
} );

function registerUrlIntegration( observableAsFunc ) {
observableUrl = new Model( { isEnabled: true } );

insertImageUI.registerIntegration( {
name: 'url',
observable: observableUrl,
observable: observableAsFunc ? () => observableUrl : observableUrl,
requiresForm: true,
buttonViewCreator( isOnlyOne ) {
const button = new ButtonView( editor.locale );
Expand All @@ -389,12 +432,12 @@ describe( 'ImageInsertUI', () => {
} );
}

function registerUploadIntegration() {
function registerUploadIntegration( observableAsFunc ) {
observableUpload = new Model( { isEnabled: true } );

insertImageUI.registerIntegration( {
name: 'upload',
observable: observableUpload,
observable: observableAsFunc ? () => observableUpload : observableUpload,
buttonViewCreator( isOnlyOne ) {
const button = new ButtonView( editor.locale );

Expand Down
14 changes: 5 additions & 9 deletions packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.ts
Expand Up @@ -192,16 +192,12 @@ export default class BalloonToolbar extends Plugin {
this.listenTo<ToolbarViewGroupedItemsUpdateEvent>( this.toolbarView, 'groupedItemsUpdate', () => {
this._updatePosition();
} );
}

/**
* Creates toolbar components based on given configuration.
* This needs to be done when all plugins are ready.
*/
public afterInit(): void {
const factory = this.editor.ui.componentFactory;

this.toolbarView.fillFromConfig( this._balloonConfig, factory );
// Creates toolbar components based on given configuration.
// This needs to be done when all plugins are ready.
editor.ui.once<EditorUIReadyEvent>( 'ready', () => {
this.toolbarView.fillFromConfig( this._balloonConfig, this.editor.ui.componentFactory );
} );
}

/**
Expand Down
23 changes: 10 additions & 13 deletions packages/ckeditor5-ui/src/toolbar/block/blocktoolbar.ts
Expand Up @@ -30,7 +30,7 @@ import clickOutsideHandler from '../../bindings/clickoutsidehandler.js';
import normalizeToolbarConfig from '../normalizetoolbarconfig.js';

import type { ButtonExecuteEvent } from '../../button/button.js';
import type { EditorUIUpdateEvent } from '../../editorui/editorui.js';
import type { EditorUIReadyEvent, EditorUIUpdateEvent } from '../../editorui/editorui.js';

const toPx = toUnit( 'px' );

Expand Down Expand Up @@ -191,20 +191,17 @@ export default class BlockToolbar extends Plugin {
beforeFocus: () => this._showPanel(),
afterBlur: () => this._hidePanel()
} );
}

/**
* Fills the toolbar with its items based on the configuration.
*
* **Note:** This needs to be done after all plugins are ready.
*/
public afterInit(): void {
this.toolbarView.fillFromConfig( this._blockToolbarConfig, this.editor.ui.componentFactory );
// Fills the toolbar with its items based on the configuration.
// This needs to be done after all plugins are ready.
editor.ui.once<EditorUIReadyEvent>( 'ready', () => {
this.toolbarView.fillFromConfig( this._blockToolbarConfig, this.editor.ui.componentFactory );

// Hide panel before executing each button in the panel.
for ( const item of this.toolbarView.items ) {
item.on<ButtonExecuteEvent>( 'execute', () => this._hidePanel( true ), { priority: 'high' } );
}
// Hide panel before executing each button in the panel.
for ( const item of this.toolbarView.items ) {
item.on<ButtonExecuteEvent>( 'execute', () => this._hidePanel( true ), { priority: 'high' } );
}
} );
}

/**
Expand Down