Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #113 from ckeditor/i/5964
Browse files Browse the repository at this point in the history
Feature: Introduced API to temporarily disable the `WidgetToolbarRepository` plugin (prevent the toolbar from showing up). Closes ckeditor/ckeditor5#5964.
  • Loading branch information
Reinmar authored Dec 17, 2019
2 parents aa5eca3 + 86ed3af commit b9cf062
Show file tree
Hide file tree
Showing 2 changed files with 202 additions and 4 deletions.
104 changes: 100 additions & 4 deletions src/widgettoolbarrepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,23 @@ export default class WidgetToolbarRepository extends Plugin {
}, { priority: 'high' } );
}

/**
* Flag indicating whether a plugin is enabled or disabled.
* A disabled plugin won't show any toolbar.
*
* Plugin can be simply disabled like that:
*
* // Disable the plugin so that no toolbars are visible.
* editor.plugins.get( 'WidgetToolbarRepository' ).isEnabled = false;
*
* You can also use {@link #forceDisabled} method.
*
* @observable
* @readonly
* @member {Boolean} #isEnabled
*/
this.set( 'isEnabled', true );

/**
* A map of toolbar definitions.
*
Expand All @@ -83,6 +100,18 @@ export default class WidgetToolbarRepository extends Plugin {
*/
this._balloon = this.editor.plugins.get( 'ContextualBalloon' );

/**
* Holds identifiers for {@link #forceDisabled} mechanism.
*
* @type {Set.<String>}
* @private
*/
this._disableStack = new Set();

this.on( 'change:isEnabled', () => {
this._updateToolbarsVisibility();
} );

this.listenTo( editor.ui, 'update', () => {
this._updateToolbarsVisibility();
} );
Expand Down Expand Up @@ -142,6 +171,67 @@ export default class WidgetToolbarRepository extends Plugin {
} );
}

/**
* Forces the plugin to be disabled.
*
* Plugin may be disabled by multiple features or algorithms (at once). When disabling a plugin, unique id should be passed
* (e.g. feature name). The same identifier should be used when {@link #clearForceDisabled enabling back} the plugin.
* The plugin becomes enabled only after all features {@link #clearForceDisabled enabled it back}.
*
* Disabling and enabling a plugin:
*
* const plugin = editor.plugins.get( 'WidgetToolbarRepository' );
*
* plugin.isEnabled; // -> true
* plugin.forceDisabled( 'MyFeature' );
* plugin.isEnabled; // -> false
* plugin.clearForceDisabled( 'MyFeature' );
* plugin.isEnabled; // -> true
*
* Plugin disabled by multiple features:
*
* plugin.forceDisabled( 'MyFeature' );
* plugin.forceDisabled( 'OtherFeature' );
* plugin.clearForceDisabled( 'MyFeature' );
* plugin.isEnabled; // -> false
* plugin.clearForceDisabled( 'OtherFeature' );
* plugin.isEnabled; // -> true
*
* Multiple disabling with the same identifier is redundant:
*
* plugin.forceDisabled( 'MyFeature' );
* plugin.forceDisabled( 'MyFeature' );
* plugin.clearForceDisabled( 'MyFeature' );
* plugin.isEnabled; // -> true
*
* **Note:** some plugins or algorithms may have more complex logic when it comes to enabling or disabling certain plugins,
* so the plugin might be still disabled after {@link #clearForceDisabled} was used.
*
* @param {String} id Unique identifier for disabling. Use the same id when {@link #clearForceDisabled enabling back} the plugin.
*/
forceDisabled( id ) {
this._disableStack.add( id );

if ( this._disableStack.size == 1 ) {
this.on( 'set:isEnabled', forceDisable, { priority: 'highest' } );
this.isEnabled = false;
}
}

/**
* Clears forced disable previously set through {@link #forceDisabled}. See {@link #forceDisabled}.
*
* @param {String} id Unique identifier, equal to the one passed in {@link #forceDisabled} call.
*/
clearForceDisabled( id ) {
this._disableStack.delete( id );

if ( this._disableStack.size == 0 ) {
this.off( 'set:isEnabled', forceDisable );
this.isEnabled = true;
}
}

/**
* Iterates over stored toolbars and makes them visible or hidden.
*
Expand All @@ -155,12 +245,12 @@ export default class WidgetToolbarRepository extends Plugin {
for ( const definition of this._toolbarDefinitions.values() ) {
const relatedElement = definition.getRelatedElement( this.editor.editing.view.document.selection );

if ( !this.editor.ui.focusTracker.isFocused ) {
if ( this._isToolbarVisible( definition ) ) {
if ( !this.isEnabled || !relatedElement ) {
if ( this._isToolbarInBalloon( definition ) ) {
this._hideToolbar( definition );
}
} else if ( !relatedElement ) {
if ( this._isToolbarInBalloon( definition ) ) {
} else if ( !this.editor.ui.focusTracker.isFocused ) {
if ( this._isToolbarVisible( definition ) ) {
this._hideToolbar( definition );
}
} else {
Expand Down Expand Up @@ -293,3 +383,9 @@ function isWidgetSelected( selection ) {
* there is no such element). The function accepts an instance of {@link module:engine/view/selection~Selection}.
* @property {String} balloonClassName CSS class for the widget balloon when a toolbar is displayed.
*/

// Helper function that forces command to be disabled.
function forceDisable( evt ) {
evt.return = false;
evt.stop();
}
102 changes: 102 additions & 0 deletions tests/widgettoolbarrepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,32 @@ describe( 'WidgetToolbarRepository', () => {
expect( balloon.visibleView ).to.equal( fakeWidgetToolbarView );
} );

it( 'toolbar should be hidden when the plugin gets disabled', () => {
widgetToolbarRepository.register( 'fake', {
items: editor.config.get( 'fake.toolbar' ),
getRelatedElement: getSelectedFakeWidget
} );

setData( model, '<paragraph>foo</paragraph>[<fake-widget></fake-widget>]' );

widgetToolbarRepository.isEnabled = false;

expect( balloon.visibleView ).to.be.null;
} );

it( 'toolbar should be hidden when the plugin was disabled prior changing selection', () => {
widgetToolbarRepository.register( 'fake', {
items: editor.config.get( 'fake.toolbar' ),
getRelatedElement: getSelectedFakeWidget
} );

widgetToolbarRepository.isEnabled = false;

setData( model, '<paragraph>foo</paragraph>[<fake-widget></fake-widget>]' );

expect( balloon.visibleView ).to.be.null;
} );

it( 'toolbar should be hidden when the `getRelatedElement` callback returns null', () => {
widgetToolbarRepository.register( 'fake', {
items: editor.config.get( 'fake.toolbar' ),
Expand Down Expand Up @@ -510,6 +536,82 @@ describe( 'WidgetToolbarRepository - integration with the BalloonToolbar', () =>

expect( balloon.visibleView ).to.equal( balloonToolbar.toolbarView );
} );

describe( 'disableable', () => {
describe( 'isEnabled', () => {
it( 'is enabled by default', () => {
expect( widgetToolbarRepository.isEnabled ).to.be.true;
} );

it( 'fires change event', () => {
const spy = sinon.spy();

widgetToolbarRepository.on( 'change:isEnabled', spy );

widgetToolbarRepository.isEnabled = false;

expect( spy.calledOnce ).to.be.true;
} );
} );

describe( 'forceDisabled() / clearForceDisabled()', () => {
it( 'forceDisabled() should disable the plugin', () => {
widgetToolbarRepository.forceDisabled( 'foo' );
widgetToolbarRepository.isEnabled = true;

expect( widgetToolbarRepository.isEnabled ).to.be.false;
} );

it( 'clearForceDisabled() should enable the plugin', () => {
widgetToolbarRepository.forceDisabled( 'foo' );
widgetToolbarRepository.clearForceDisabled( 'foo' );

expect( widgetToolbarRepository.isEnabled ).to.be.true;
} );

it( 'clearForceDisabled() used with wrong identifier should not enable the plugin', () => {
widgetToolbarRepository.forceDisabled( 'foo' );
widgetToolbarRepository.clearForceDisabled( 'bar' );
widgetToolbarRepository.isEnabled = true;

expect( widgetToolbarRepository.isEnabled ).to.be.false;
} );

it( 'using forceDisabled() twice with the same identifier should not have any effect', () => {
widgetToolbarRepository.forceDisabled( 'foo' );
widgetToolbarRepository.forceDisabled( 'foo' );
widgetToolbarRepository.clearForceDisabled( 'foo' );

expect( widgetToolbarRepository.isEnabled ).to.be.true;
} );

it( 'plugin is enabled only after all disables were cleared', () => {
widgetToolbarRepository.forceDisabled( 'foo' );
widgetToolbarRepository.forceDisabled( 'bar' );
widgetToolbarRepository.clearForceDisabled( 'foo' );
widgetToolbarRepository.isEnabled = true;

expect( widgetToolbarRepository.isEnabled ).to.be.false;

widgetToolbarRepository.clearForceDisabled( 'bar' );

expect( widgetToolbarRepository.isEnabled ).to.be.true;
} );

it( 'plugin should remain disabled if isEnabled has a callback disabling it', () => {
widgetToolbarRepository.on( 'set:isEnabled', evt => {
evt.return = false;
evt.stop();
} );

widgetToolbarRepository.forceDisabled( 'foo' );
widgetToolbarRepository.clearForceDisabled( 'foo' );
widgetToolbarRepository.isEnabled = true;

expect( widgetToolbarRepository.isEnabled ).to.be.false;
} );
} );
} );
} );

function getSelectedFakeWidget( selection ) {
Expand Down

0 comments on commit b9cf062

Please sign in to comment.