Skip to content

Commit

Permalink
Merge pull request #7582 from ckeditor/i/6774-disable-type-around
Browse files Browse the repository at this point in the history
Feature (widget): Made it possible to disable the `WidgetTypeAround` plugin on the fly. Closes #6774.
  • Loading branch information
niegowski committed Jul 9, 2020
2 parents 33bcf73 + de82f2c commit 8cecf39
Show file tree
Hide file tree
Showing 6 changed files with 353 additions and 116 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
category: framework-deep-dive-ui
menu-title: Widget internals
---

# Deep dive into widget internals

## Disabling the widget type around feature

The {@link module:widget/widgettypearound~WidgetTypeAround `WidgetTypeAround`} plugin allows users to type around widgets where normally it is impossible to place the caret due to limitations of web browsers. Although this feature boosts the productivity, some integrations may not want or need it, for instance:

* when the UI of the feature collides with the integration,
* when typing outside widgets should be impossible (content made exclusively of widgets),
* when certain widgets have a fixed location in the document that should never change.

In the sections that follows, you will learn how to configure the editor to address these specific cases.

### Hiding the buttons that insert paragraphs

{@img assets/img/framework-deep-dive-widget-type-around-buttons.gif A screenshot showing the location of the buttons that insert paragraphs around widgets.}

The easiest way to get rid of the type around buttons is to hide them using CSS. Put the following code snippet anywhere in your application and the buttons will no longer bother the users:

```css
.ck.ck-editor__editable .ck-widget .ck-widget__type-around__button {
display: none;
}
```

If you only want to customize the type around buttons you can use the same CSS selector to modify their look or the position.

<info-box hint>
Hiding the type around buttons does not disable the feature. Users will still be able to activate the caret before or after individual widgets using the keyboard and start typing. [Learn how to disable the entire feature](#disabling-the-entire-feature).
</info-box>

### Disabling the entire feature

Although the {@link module:widget/widgettypearound~WidgetTypeAround `WidgetTypeAround`} plugin is an integral part of the {@link module:widget/widget~Widget widget} sub–system and loaded by default whenever an editor feature uses widgets (for instance, {@link features/image images} or {@link features/table tables}), you can still disable on the fly. Disabling the feature will both hide the buttons in the widgets and disable other behaviors, for instance:

* the caret will not be rendered before or after a widget when a user navigates the document using arrow keys,
* the <kbd>Enter</kbd> and <kbd>Shift</kbd>+<kbd>Enter</kbd> keystrokes will no longer insert paragraphs if pressed when a widget is selected.

Use the {@link module:core/plugin~Plugin#forceDisabled `forceDisabled()`} method of the plugin to disable it on the fly like in the snippet below:

```js
ClassicEditor
.create( document.querySelector( '#editor' ), {
// ...
} )
.then( editor => {
const widgetTypeAroundPlugin = editor.plugins.get( 'WidgetTypeAround' );

// Disable the WidgetTypeAround plugin.
widgetTypeAroundPlugin.forceDisabled( 'MyApplication' );
} )
.catch( err => {
console.error( err.stack );
} );
```

<info-box hint>
You can always re–enable the plugin using the following snippet
```js
widgetTypeAroundPlugin.clearForceDisabled( 'MyApplication' );
```
Please refer to the {@link module:core/plugin~Plugin#clearForceDisabled API documentation} to learn more.
</info-box>
68 changes: 57 additions & 11 deletions packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const POSSIBLE_INSERTION_POSITIONS = [ 'before', 'after' ];
// Do the SVG parsing once and then clone the result <svg> DOM element for each new button.
const RETURN_ARROW_ICON_ELEMENT = new DOMParser().parseFromString( returnIcon, 'image/svg+xml' ).firstChild;

const PLUGIN_DISABLED_EDITING_ROOT_CLASS = 'ck-widget__type-around_disabled';

/**
* A plugin that allows users to type around widgets where normally it is impossible to place the caret due
* to limitations of web browsers. These "tight spots" occur, for instance, before (or after) a widget being
Expand All @@ -50,7 +52,6 @@ const RETURN_ARROW_ICON_ELEMENT = new DOMParser().parseFromString( returnIcon, '
* in it so that users can type (or insert content, paste, etc.) straight away.
*
* @extends module:core/plugin~Plugin
* @private
*/
export default class WidgetTypeAround extends Plugin {
/**
Expand Down Expand Up @@ -81,6 +82,29 @@ export default class WidgetTypeAround extends Plugin {
* @inheritDoc
*/
init() {
const editor = this.editor;
const editingView = editor.editing.view;

// Set a CSS class on the view editing root when the plugin is disabled so all the buttons
// and lines visually disappear. All the interactions are disabled in individual plugin methods.
this.on( 'change:isEnabled', ( evt, data, isEnabled ) => {
editingView.change( writer => {
for ( const root of editingView.document.roots ) {
if ( isEnabled ) {
writer.removeClass( PLUGIN_DISABLED_EDITING_ROOT_CLASS, root );
} else {
writer.addClass( PLUGIN_DISABLED_EDITING_ROOT_CLASS, root );
}
}
} );

if ( !isEnabled ) {
editor.model.change( writer => {
writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
} );
}
} );

this._enableTypeAroundUIInjection();
this._enableInsertingParagraphsOnButtonClick();
this._enableInsertingParagraphsOnEnterKeypress();
Expand Down Expand Up @@ -119,6 +143,28 @@ export default class WidgetTypeAround extends Plugin {
editingView.scrollToTheSelection();
}

/**
* A wrapper for the {@link module:utils/emittermixin~EmitterMixin#listenTo} method that executes the callbacks only
* when the plugin {@link #isEnabled is enabled}.
*
* @private
* @param {module:utils/emittermixin~Emitter} emitter The object that fires the event.
* @param {String} event The name of the event.
* @param {Function} callback The function to be called on event.
* @param {Object} [options={}] Additional options.
* @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
* the priority value the sooner the callback will be fired. Events having the same priority are called in the
* order they were added.
*/
_listenToIfEnabled( emitter, event, callback, options ) {
this.listenTo( emitter, event, ( ...args ) => {
// Do not respond if the plugin is disabled.
if ( this.isEnabled ) {
callback( ...args );
}
}, options );
}

/**
* Similar to {@link #_insertParagraph}, this method inserts a paragraph except that it
* does not expect a position but it performs the insertion next to a selected widget
Expand Down Expand Up @@ -213,7 +259,7 @@ export default class WidgetTypeAround extends Plugin {
// This is the main listener responsible for the "fake caret".
// Note: The priority must precede the default Widget class keydown handler ("high") and the
// TableKeyboard keydown handler ("high-10").
editingView.document.on( 'keydown', ( evt, domEventData ) => {
this._listenToIfEnabled( editingView.document, 'keydown', ( evt, domEventData ) => {
if ( isArrowKeyCode( domEventData.keyCode ) ) {
this._handleArrowKeyPress( evt, domEventData );
}
Expand All @@ -223,7 +269,7 @@ export default class WidgetTypeAround extends Plugin {
// selection as soon as the model range changes. This attribute only makes sense when a widget is selected
// (and the "fake horizontal caret" is visible) so whenever the range changes (e.g. selection moved somewhere else),
// let's get rid of the attribute so that the selection downcast dispatcher isn't even bothered.
modelSelection.on( 'change:range', ( evt, data ) => {
this._listenToIfEnabled( modelSelection, 'change:range', ( evt, data ) => {
// Do not reset the selection attribute when the change was indirect.
if ( !data.directChange ) {
return;
Expand All @@ -238,7 +284,7 @@ export default class WidgetTypeAround extends Plugin {

// Get rid of the widget type around attribute of the selection on every document change
// that makes widget not selected any more (i.e. widget was removed).
model.document.on( 'change:data', () => {
this._listenToIfEnabled( model.document, 'change:data', () => {
const selectedModelElement = modelSelection.getSelectedElement();

if ( selectedModelElement ) {
Expand All @@ -257,7 +303,7 @@ export default class WidgetTypeAround extends Plugin {
// React to changes of the model selection attribute made by the arrow keys listener.
// If the block widget is selected and the attribute changes, downcast the attribute to special
// CSS classes associated with the active ("fake horizontal caret") mode of the widget.
editor.editing.downcastDispatcher.on( 'selection', ( evt, data, conversionApi ) => {
this._listenToIfEnabled( editor.editing.downcastDispatcher, 'selection', ( evt, data, conversionApi ) => {
const writer = conversionApi.writer;

if ( this._currentFakeCaretModelElement ) {
Expand Down Expand Up @@ -296,7 +342,7 @@ export default class WidgetTypeAround extends Plugin {
this._currentFakeCaretModelElement = selectedModelElement;
} );

this.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, isFocused ) => {
this._listenToIfEnabled( editor.ui.focusTracker, 'change:isFocused', ( evt, name, isFocused ) => {
if ( !isFocused ) {
editor.model.change( writer => {
writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
Expand Down Expand Up @@ -450,7 +496,7 @@ export default class WidgetTypeAround extends Plugin {
const editor = this.editor;
const editingView = editor.editing.view;

editingView.document.on( 'mousedown', ( evt, domEventData ) => {
this._listenToIfEnabled( editingView.document, 'mousedown', ( evt, domEventData ) => {
const button = getClosestTypeAroundDomButton( domEventData.domTarget );

if ( !button ) {
Expand Down Expand Up @@ -487,7 +533,7 @@ export default class WidgetTypeAround extends Plugin {
const editor = this.editor;
const editingView = editor.editing.view;

this.listenTo( editingView.document, 'enter', ( evt, domEventData ) => {
this._listenToIfEnabled( editingView.document, 'enter', ( evt, domEventData ) => {
const selectedViewElement = editingView.document.selection.getSelectedElement();
const selectedModelElement = editor.editing.mapper.toModelElement( selectedViewElement );
const schema = editor.model.schema;
Expand Down Expand Up @@ -543,7 +589,7 @@ export default class WidgetTypeAround extends Plugin {

// Note: The priority must precede the default Widget class keydown handler ("high") and the
// TableKeyboard keydown handler ("high + 1").
editingView.document.on( 'keydown', ( evt, domEventData ) => {
this._listenToIfEnabled( editingView.document, 'keydown', ( evt, domEventData ) => {
// Don't handle enter/backspace/delete here. They are handled in dedicated listeners.
if ( !keyCodesHandledSomewhereElse.includes( domEventData.keyCode ) && !isNonTypingKeystroke( domEventData ) ) {
this._insertParagraphAccordingToFakeCaretPosition();
Expand All @@ -568,7 +614,7 @@ export default class WidgetTypeAround extends Plugin {
const schema = model.schema;

// Note: The priority must precede the default Widget class delete handler.
this.listenTo( editingView.document, 'delete', ( evt, domEventData ) => {
this._listenToIfEnabled( editingView.document, 'delete', ( evt, domEventData ) => {
const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition( model.document.selection );

// This listener handles only these cases when the "fake caret" is active.
Expand Down Expand Up @@ -647,7 +693,7 @@ export default class WidgetTypeAround extends Plugin {
const model = this.editor.model;
const documentSelection = model.document.selection;

this.listenTo( editor.model, 'insertContent', ( evt, [ content, selectable ] ) => {
this._listenToIfEnabled( editor.model, 'insertContent', ( evt, [ content, selectable ] ) => {
if ( selectable && !selectable.is( 'documentSelection' ) ) {
return;
}
Expand Down
6 changes: 6 additions & 0 deletions packages/ckeditor5-widget/tests/widgettypearound/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ describe( 'widget type around utils', () => {
selection = new Selection();
} );

describe( 'TYPE_AROUND_SELECTION_ATTRIBUTE', () => {
it( 'should be defined', () => {
expect( TYPE_AROUND_SELECTION_ATTRIBUTE ).to.equal( 'widget-type-around' );
} );
} );

describe( 'getTypeAroundFakeCaretPosition()', () => {
it( 'should return "before" if the model selection attribute is "before"', () => {
selection.setAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' );
Expand Down
Loading

0 comments on commit 8cecf39

Please sign in to comment.