Skip to content

Commit

Permalink
Merge pull request #9023 from ckeditor/i/8640
Browse files Browse the repository at this point in the history
Feature (engine): Introduced bubbling of `view.Document` events, similar to how bubbling works in the DOM. Bubbling allows listening on a view event on a specific kind of an element, hence simplifying code that needs to handle a specific event for only that element (e.g. `enter` in `blockquote`s only). Read more in the documentation: **\[TODO\]**. Closes #8640.

Feature (engine): Introduced `ArrowKeysObserver`. See #8640.

Fix (utils): The `EmitterMixin#listenTo()` method is split into listener and emitter parts. The `ObservableMixin` decorated methods reverted to the original method while destroying an observable.

Other (typing): The `TwoStepCaretMovement` feature is now using bubbling events. Closes #7437.

BREAKING CHANGE: We introduced bubbling of `view.Document` events, similar to how bubbling works in the DOM. That allowed us to reprioritize many listeners that previously had to rely on `priority`. However, it means that existing listeners that use priorities may now be executed in a wrong moment. The listeners to such events should be reviewed in terms of when they should be executed (in what context/element/phase). You can find more information regarding bubbling in the documentation: **\[TODO\]**. See #8640.

Internal (widget): The enter, delete and arrow key events handling moved to the usage of the bubbling observer.

Internal (block-quote): The enter and delete events handling moved to the usage of the bubbling observer.

Internal (code-block): The enter event handling moved to the usage of the bubbling observer.

Internal (list): The enter and delete events handling moved to the usage of the bubbling observer.

Internal (table): The arrow keys handling moved to the usage of the bubbling observer.
  • Loading branch information
Reinmar committed Mar 8, 2021
2 parents bdc9425 + 9e65219 commit 5527283
Show file tree
Hide file tree
Showing 40 changed files with 2,193 additions and 189 deletions.
1 change: 1 addition & 0 deletions docs/framework/guides/architecture/editing-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ By default, the view adds the following observers:
* {@link module:engine/view/observer/keyobserver~KeyObserver}
* {@link module:engine/view/observer/fakeselectionobserver~FakeSelectionObserver}
* {@link module:engine/view/observer/compositionobserver~CompositionObserver}
* {@link module:engine/view/observer/arrowkeysobserver~ArrowKeysObserver}

Additionally, some features add their own observers. For instance, the {@link module:clipboard/clipboard~Clipboard clipboard feature} adds {@link module:clipboard/clipboardobserver~ClipboardObserver}.

Expand Down
18 changes: 11 additions & 7 deletions packages/ckeditor5-block-quote/src/blockquoteediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
*/

import { Plugin } from 'ckeditor5/src/core';
import { priorities } from 'ckeditor5/src/utils';
import { Enter } from 'ckeditor5/src/enter';
import { Delete } from 'ckeditor5/src/typing';

import BlockQuoteCommand from './blockquotecommand';

Expand All @@ -27,6 +28,13 @@ export default class BlockQuoteEditing extends Plugin {
return 'BlockQuoteEditing';
}

/**
* @inheritDoc
*/
static get requires() {
return [ Enter, Delete ];
}

/**
* @inheritDoc
*/
Expand Down Expand Up @@ -110,8 +118,6 @@ export default class BlockQuoteEditing extends Plugin {

// Overwrite default Enter key behavior.
// If Enter key is pressed with selection collapsed in empty block inside a quote, break the quote.
//
// Priority normal - 10 to override default handler but not list's feature listener.
this.listenTo( viewDocument, 'enter', ( evt, data ) => {
if ( !selection.isCollapsed || !blockQuoteCommand.value ) {
return;
Expand All @@ -126,12 +132,10 @@ export default class BlockQuoteEditing extends Plugin {
data.preventDefault();
evt.stop();
}
}, { priority: priorities.normal - 10 } );
}, { context: 'blockquote' } );

// Overwrite default Backspace key behavior.
// If Backspace key is pressed with selection collapsed in first empty block inside a quote, break the quote.
//
// Priority high + 5 to override widget's feature listener but not list's feature listener.
this.listenTo( viewDocument, 'delete', ( evt, data ) => {
if ( data.direction != 'backward' || !selection.isCollapsed || !blockQuoteCommand.value ) {
return;
Expand All @@ -146,6 +150,6 @@ export default class BlockQuoteEditing extends Plugin {
data.preventDefault();
evt.stop();
}
}, { priority: priorities.high + 5 } );
}, { context: 'blockquote' } );
}
}
56 changes: 56 additions & 0 deletions packages/ckeditor5-block-quote/tests/integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,62 @@ describe( 'BlockQuote integration', () => {
'<paragraph>y</paragraph>'
);
} );

it( 'does nothing if selection is in an empty block but not in a block quote', () => {
const data = fakeEventData();
const execSpy = sinon.spy( editor, 'execute' );

setModelData( model, '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>x</paragraph>' );

viewDocument.fire( 'delete', data );

// Only enter command should be executed.
expect( data.preventDefault.called ).to.be.true;
expect( execSpy.calledOnce ).to.be.true;
expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'delete' );
} );

it( 'does nothing if selection is in a non-empty block (at the end) in a block quote', () => {
const data = fakeEventData();
const execSpy = sinon.spy( editor, 'execute' );

setModelData( model, '<blockQuote><paragraph>xx[]</paragraph></blockQuote>' );

viewDocument.fire( 'delete', data );

// Only enter command should be executed.
expect( data.preventDefault.called ).to.be.true;
expect( execSpy.calledOnce ).to.be.true;
expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'delete' );
} );

it( 'does nothing if selection is in a non-empty block (at the beginning) in a block quote', () => {
const data = fakeEventData();
const execSpy = sinon.spy( editor, 'execute' );

setModelData( model, '<blockQuote><paragraph>[]xx</paragraph></blockQuote>' );

viewDocument.fire( 'delete', data );

// Only enter command should be executed.
expect( data.preventDefault.called ).to.be.true;
expect( execSpy.calledOnce ).to.be.true;
expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'delete' );
} );

it( 'does nothing if selection is not collapsed', () => {
const data = fakeEventData();
const execSpy = sinon.spy( editor, 'execute' );

setModelData( model, '<blockQuote><paragraph>[</paragraph><paragraph>]</paragraph></blockQuote>' );

viewDocument.fire( 'delete', data );

// Only enter command should be executed.
expect( data.preventDefault.called ).to.be.true;
expect( execSpy.calledOnce ).to.be.true;
expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'delete' );
} );
} );

// Historically, due to problems with schema, images were not quotable.
Expand Down
2 changes: 1 addition & 1 deletion packages/ckeditor5-code-block/src/codeblockediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ export default class CodeBlockEditing extends Plugin {

data.preventDefault();
evt.stop();
} );
}, { context: 'pre' } );
}
}

Expand Down
22 changes: 22 additions & 0 deletions packages/ckeditor5-code-block/tests/codeblockediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,28 @@ describe( 'CodeBlockEditing', () => {
sinon.assert.notCalled( shiftEnterCommand.execute );
} );

it( 'should execute enter command when pressing enter in an element nested inside a codeBlock', () => {
model.schema.register( 'codeBlockSub', { allowIn: 'codeBlock', isInline: true } );
model.schema.extend( '$text', { allowIn: 'codeBlockSub' } );
editor.conversion.elementToElement( { model: 'codeBlockSub', view: 'codeBlockSub' } );

const enterCommand = editor.commands.get( 'enter' );
const shiftEnterCommand = editor.commands.get( 'shiftEnter' );

sinon.spy( enterCommand, 'execute' );
sinon.spy( shiftEnterCommand, 'execute' );

setModelData( model, '<codeBlock>foo<codeBlockSub>b[]a</codeBlockSub>r</codeBlock>' );

viewDoc.fire( 'enter', getEvent() );

expect( getModelData( model ) ).to.equal(
'<codeBlock>foo<codeBlockSub>b</codeBlockSub><codeBlockSub>[]a</codeBlockSub>r</codeBlock>'
);
sinon.assert.calledOnce( enterCommand.execute );
sinon.assert.notCalled( shiftEnterCommand.execute );
} );

describe( 'indentation retention', () => {
it( 'should work when indentation is with spaces', () => {
setModelData( model, '<codeBlock language="css">foo[]</codeBlock>' );
Expand Down
3 changes: 3 additions & 0 deletions packages/ckeditor5-engine/src/view/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import DocumentSelection from './documentselection';
import Collection from '@ckeditor/ckeditor5-utils/src/collection';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
import BubblingEmitterMixin from './observer/bubblingemittermixin';
import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';

// @if CK_DEBUG_ENGINE // const { logDocument } = require( '../dev-utils/utils' );
Expand All @@ -18,6 +19,7 @@ import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
* Document class creates an abstract layer over the content editable area, contains a tree of view elements and
* {@link module:engine/view/documentselection~DocumentSelection view selection} associated with this document.
*
* @mixes module:engine/view/observer/bubblingemittermixin~BubblingEmitterMixin
* @mixes module:utils/observablemixin~ObservableMixin
*/
export default class Document {
Expand Down Expand Up @@ -203,6 +205,7 @@ export default class Document {
// @if CK_DEBUG_ENGINE // }
}

mix( Document, BubblingEmitterMixin );
mix( Document, ObservableMixin );

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/ckeditor5-engine/src/view/filler.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export function getDataWithoutFiller( domText ) {
* @param {module:engine/view/view~View} view View controller instance we should inject quirks handling on.
*/
export function injectQuirksHandling( view ) {
view.document.on( 'keydown', jumpOverInlineFiller );
view.document.on( 'arrowKey', jumpOverInlineFiller, { priority: 'low' } );
}

// Move cursor from the end of the inline filler to the beginning of it when, so the filler does not break navigation.
Expand Down
58 changes: 58 additions & 0 deletions packages/ckeditor5-engine/src/view/observer/arrowkeysobserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module engine/view/observer/arrowkeysobserver
*/

import Observer from './observer';
import BubblingEventInfo from './bubblingeventinfo';

import { isArrowKeyCode } from '@ckeditor/ckeditor5-utils';

/**
* Arrow keys observer introduces the {@link module:engine/view/document~Document#event:arrowKey `Document#arrowKey`} event.
*
* Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
*
* @extends module:engine/view/observer/observer~Observer
*/
export default class ArrowKeysObserver extends Observer {
/**
* @inheritDoc
*/
constructor( view ) {
super( view );

this.document.on( 'keydown', ( event, data ) => {
if ( this.isEnabled && isArrowKeyCode( data.keyCode ) ) {
const eventInfo = new BubblingEventInfo( this.document, 'arrowKey', this.document.selection.getFirstRange() );

this.document.fire( eventInfo, data );

if ( eventInfo.stop.called ) {
event.stop();
}
}
} );
}

/**
* @inheritDoc
*/
observe() {}
}

/**
* Event fired when the user presses an arrow keys.
*
* Introduced by {@link module:engine/view/observer/arrowkeysobserver~ArrowKeysObserver}.
*
* Note that because {@link module:engine/view/observer/arrowkeysobserver~ArrowKeysObserver} is attached by the
* {@link module:engine/view/view~View} this event is available by default.
*
* @event module:engine/view/document~Document#event:arrowKey
* @param {module:engine/view/observer/domeventdata~DomEventData} data
*/
Loading

0 comments on commit 5527283

Please sign in to comment.