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

#6444: Balloon toolbar should reposition and ungroup items correctly when the window resizes #7795

Merged
merged 3 commits into from
Aug 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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