Skip to content

Commit

Permalink
Merge pull request #7795 from ckeditor/i/6444-toolbar-reposition-on-r…
Browse files Browse the repository at this point in the history
…egroup

Fix (ui): Balloon toolbar should reposition and ungroup items correctly when the window resizes. Closes #6444.
  • Loading branch information
panr committed Aug 7, 2020
2 parents 2a163e3 + 555fd4a commit 3252378
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 62 deletions.
23 changes: 22 additions & 1 deletion packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@ export default class BalloonToolbar extends Plugin {
} );
} );
}

// Listen to the toolbar view and whenever it changes its geometry due to some items being
// grouped or ungrouped, update the position of the balloon because a shorter/longer toolbar
// means the balloon could be pointing at the wrong place. Once updated, the balloon will point
// at the right selection in the content again.
// https://github.com/ckeditor/ckeditor5/issues/6444
this.listenTo( this.toolbarView, 'groupedItemsUpdate', () => {
this._updatePosition();
} );
}

/**
Expand Down Expand Up @@ -236,7 +245,7 @@ export default class BalloonToolbar extends Plugin {

// Update the toolbar position when the editor ui should be refreshed.
this.listenTo( this.editor.ui, 'update', () => {
this._balloon.updatePosition( this._getBalloonPositionData() );
this._updatePosition();
} );

// Add the toolbar to the common editor contextual balloon.
Expand Down Expand Up @@ -300,6 +309,18 @@ export default class BalloonToolbar extends Plugin {
};
}

/**
* Updates the position of the {@link #_balloon} to make up for changes:
*
* * in the geometry of the selection it is attached to (e.g. the selection moved in the viewport or expanded or shrunk),
* * or the geometry of the balloon toolbar itself (e.g. the toolbar has grouped or ungrouped some items and it is shorter or longer).
*
* @private
*/
_updatePosition() {
this._balloon.updatePosition( this._getBalloonPositionData() );
}

/**
* @inheritDoc
*/
Expand Down
28 changes: 28 additions & 0 deletions packages/ckeditor5-ui/src/toolbar/toolbarview.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,19 @@ export default class ToolbarView extends View {
}
} ).filter( item => item !== undefined ) );
}

/**
* Fired when some toolbar {@link #items} were grouped or ungrouped as a result of some change
* in the toolbar geometry.
*
* **Note**: This event is always fired **once** regardless of the number of items that were be
* grouped or ungrouped at a time.
*
* **Note**: This event is fired only if the items grouping functionality was enabled in
* the first place (see {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull}).
*
* @event groupedItemsUpdate
*/
}

/**
Expand Down Expand Up @@ -418,6 +431,14 @@ class DynamicGrouping {
* is added to.
*/
constructor( view ) {
/**
* A toolbar view this behavior belongs to.
*
* @readonly
* @member {module:ui/toolbar~ToolbarView}
*/
this.view = view;

/**
* A collection of toolbar children.
*
Expand Down Expand Up @@ -644,6 +665,9 @@ class DynamicGrouping {
return;
}

// Remember how many items were initially grouped so at the it is possible to figure out if the number
// of grouped items has changed. If the number has changed, geometry of the toolbar has also changed.
const initialGroupedItemsCount = this.groupedItems.length;
let wereItemsGrouped;

// Group #items as long as some wrap to the next row. This will happen, for instance,
Expand Down Expand Up @@ -672,6 +696,10 @@ class DynamicGrouping {
this._groupLastItem();
}
}

if ( this.groupedItems.length !== initialGroupedItemsCount ) {
this.view.fire( 'groupedItemsUpdate' );
}
}

/**
Expand Down
12 changes: 12 additions & 0 deletions packages/ckeditor5-ui/tests/toolbar/balloon/balloontoolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,18 @@ describe( 'BalloonToolbar', () => {
sinon.assert.calledOnce( spy );
} );

it( 'should update the balloon position whenever #toolbarView fires the #groupedItemsUpdate (it changed its geometry)', () => {
setData( model, '<paragraph>b[a]r</paragraph>' );

const spy = sinon.spy( balloon, 'updatePosition' );

balloonToolbar.show();
sinon.assert.notCalled( spy );

balloonToolbar.toolbarView.fire( 'groupedItemsUpdate' );
sinon.assert.calledOnce( spy );
} );

it( 'should not add #toolbarView to the #_balloon more than once', () => {
setData( model, '<paragraph>b[a]r</paragraph>' );

Expand Down
41 changes: 41 additions & 0 deletions packages/ckeditor5-ui/tests/toolbar/toolbarview.js
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,47 @@ describe( 'ToolbarView', () => {
expect( ungroupedItems ).to.have.length( 5 );
expect( groupedItems ).to.have.length( 0 );
} );

it( 'should fire the "groupedItemsUpdate" event on the toolbar when some item is grouped or ungrouped', () => {
const updateSpy = sinon.spy();

view.on( 'groupedItemsUpdate', updateSpy );

view.element.style.width = '200px';

view.items.add( focusable() );
view.items.add( focusable() );
view.items.add( focusable() );
view.items.add( focusable() );
view.items.add( focusable() );

resizeCallback( [ {
target: view.element,
contentRect: new Rect( view.element )
} ] );

sinon.assert.calledOnce( updateSpy );

// This 10px is not enough to ungroup an item.
view.element.style.width = '210px';

resizeCallback( [ {
target: view.element,
contentRect: new Rect( view.element )
} ] );

sinon.assert.calledOnce( updateSpy );

// But this is not enough to ungroup some items.
view.element.style.width = '300px';

resizeCallback( [ {
target: view.element,
contentRect: new Rect( view.element )
} ] );

sinon.assert.calledTwice( updateSpy );
} );
} );

describe( 'destroy()', () => {
Expand Down
6 changes: 0 additions & 6 deletions packages/ckeditor5-utils/src/dom/resizeobserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,6 @@ export default class ResizeObserver {

ResizeObserver._observerInstance = new ObserverConstructor( entries => {
for ( const entry of entries ) {
// Do not execute callbacks for elements that are invisible.
// https://github.com/ckeditor/ckeditor5/issues/6570
if ( !entry.target.offsetParent ) {
continue;
}

const callbacks = ResizeObserver._getElementCallbacks( entry.target );

if ( callbacks ) {
Expand Down
55 changes: 0 additions & 55 deletions packages/ckeditor5-utils/tests/dom/resizeobserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,61 +114,6 @@ describe( 'ResizeObserver()', () => {
observerA.destroy();
} );

it( 'should not react to resizing of an element if element is invisible', () => {
const callbackA = sinon.spy();
let resizeCallback;

testUtils.sinon.stub( global.window, 'ResizeObserver' ).callsFake( callback => {
resizeCallback = callback;

return {
observe() {},
unobserve() {}
};
} );

const observerA = new ResizeObserver( elementA, callbackA );

elementA.style.display = 'none';

resizeCallback( [
{ target: elementA }
] );

sinon.assert.notCalled( callbackA );

observerA.destroy();
} );

it( 'should not react to resizing of an element if element\'s parent is invisible', () => {
const callbackA = sinon.spy();
let resizeCallback;

testUtils.sinon.stub( global.window, 'ResizeObserver' ).callsFake( callback => {
resizeCallback = callback;

return {
observe() {},
unobserve() {}
};
} );

const observerA = new ResizeObserver( elementA, callbackA );
const parent = document.createElement( 'div' );
document.body.appendChild( parent );
parent.appendChild( elementA );
parent.style.display = 'none';

resizeCallback( [
{ target: elementA }
] );

sinon.assert.notCalled( callbackA );

parent.remove();
observerA.destroy();
} );

it( 'should be able to observe the same element along with other observers', () => {
const callbackA = sinon.spy();
const callbackB = sinon.spy();
Expand Down

0 comments on commit 3252378

Please sign in to comment.