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

Commit 322563e

Browse files
authored
Merge pull request #288 from ckeditor/t/260
Feature: Allowed `BalloonPanelView` position limiter defined as a function. Made `ContextualBalloon` position limiter configurable via `#positionLimiter` property. Closes #260.
2 parents b2d9002 + 13ae5be commit 322563e

File tree

6 files changed

+289
-41
lines changed

6 files changed

+289
-41
lines changed

src/panel/balloon/balloonpanelview.js

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -253,15 +253,8 @@ export default class BalloonPanelView extends View {
253253
_startPinning( options ) {
254254
this.attachTo( options );
255255

256-
const limiter = options.limiter || defaultLimiterElement;
257-
let targetElement = null;
258-
259-
// We need to take HTMLElement related to the target if it is possible.
260-
if ( isElement( options.target ) ) {
261-
targetElement = options.target;
262-
} else if ( isRange( options.target ) ) {
263-
targetElement = options.target.commonAncestorContainer;
264-
}
256+
const targetElement = getDomElement( options.target );
257+
const limiterElement = options.limiter ? getDomElement( options.limiter ) : defaultLimiterElement;
265258

266259
// Then we need to listen on scroll event of eny element in the document.
267260
this.listenTo( global.document, 'scroll', ( evt, domEvt ) => {
@@ -271,10 +264,11 @@ export default class BalloonPanelView extends View {
271264
const isWithinScrollTarget = targetElement && scrollTarget.contains( targetElement );
272265

273266
// The position needs to be updated if the positioning limiter is within the scrolled element.
274-
const isLimiterWithinScrollTarget = scrollTarget.contains( limiter );
267+
const isLimiterWithinScrollTarget = limiterElement && scrollTarget.contains( limiterElement );
275268

276-
// The positioning target can be a Rect, object etc.. There's no way to optimize the listener then.
277-
if ( isWithinScrollTarget || isLimiterWithinScrollTarget || !targetElement ) {
269+
// The positioning target and/or limiter can be a Rect, object etc..
270+
// There's no way to optimize the listener then.
271+
if ( isWithinScrollTarget || isLimiterWithinScrollTarget || !targetElement || !limiterElement ) {
278272
this.attachTo( options );
279273
}
280274
}, { useCapture: true } );
@@ -296,6 +290,28 @@ export default class BalloonPanelView extends View {
296290
}
297291
}
298292

293+
// Returns the DOM element for given object or null, if there's none,
294+
// e.g. when passed object is a Rect instance or so.
295+
//
296+
// @private
297+
// @param {*} object
298+
// @returns {HTMLElement|null}
299+
function getDomElement( object ) {
300+
if ( isElement( object ) ) {
301+
return object;
302+
}
303+
304+
if ( isRange( object ) ) {
305+
return object.commonAncestorContainer;
306+
}
307+
308+
if ( typeof object == 'function' ) {
309+
return getDomElement( object() );
310+
}
311+
312+
return null;
313+
}
314+
299315
/**
300316
* A horizontal offset of the arrow tip from the edge of the balloon. Controlled by CSS.
301317
*

src/panel/balloon/contextualballoon.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
1111
import BalloonPanelView from './balloonpanelview';
1212
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
13+
import first from '@ckeditor/ckeditor5-utils/src/first';
1314

1415
/**
1516
* Provides the common contextual balloon panel for the editor.
@@ -47,6 +48,28 @@ export default class ContextualBalloon extends Plugin {
4748
*/
4849
this.view = new BalloonPanelView();
4950

51+
/**
52+
* The {@link module:utils/dom/position~Options#limiter position limiter}
53+
* for the {@link #view}, used when no `limiter` has been passed into {@link #add}
54+
* or {@link #updatePosition}.
55+
*
56+
* By default, a function, which obtains the farthest DOM
57+
* {@link module:engine/view/rooteditableelement~RootEditableElement}
58+
* of the {@link module:engine/view/document~Document#selection}.
59+
*
60+
* @member {module:utils/dom/position~Options#limiter} #positionLimiter
61+
*/
62+
this.positionLimiter = () => {
63+
const view = this.editor.editing.view;
64+
const editableElement = view.selection.editableElement;
65+
66+
if ( editableElement ) {
67+
return view.domConverter.mapViewToDom( editableElement.root );
68+
}
69+
70+
return null;
71+
};
72+
5073
/**
5174
* Stack of the views injected into the balloon. Last one in the stack is displayed
5275
* as a content of {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon#view}.
@@ -196,6 +219,16 @@ export default class ContextualBalloon extends Plugin {
196219
* @returns {module:utils/dom/position~Options}
197220
*/
198221
_getBalloonPosition() {
199-
return this._stack.values().next().value.position;
222+
let position = first( this._stack.values() ).position;
223+
224+
// Use the default limiter if none has been specified.
225+
if ( position && !position.limiter ) {
226+
// Don't modify the original options object.
227+
position = Object.assign( {}, position, {
228+
limiter: this.positionLimiter
229+
} );
230+
}
231+
232+
return position;
200233
}
201234
}

src/toolbar/contextual/contextualtoolbar.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@ export default class ContextualToolbar extends Plugin {
4141
* @inheritDoc
4242
*/
4343
init() {
44+
const editor = this.editor;
45+
4446
/**
4547
* The toolbar view displayed in the balloon.
4648
*
4749
* @member {module:ui/toolbar/toolbarview~ToolbarView}
4850
*/
49-
this.toolbarView = new ToolbarView( this.editor.locale );
51+
this.toolbarView = new ToolbarView( editor.locale );
5052

5153
Template.extend( this.toolbarView.template, {
5254
attributes: {
@@ -63,7 +65,7 @@ export default class ContextualToolbar extends Plugin {
6365
* @private
6466
* @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
6567
*/
66-
this._balloon = this.editor.plugins.get( ContextualBalloon );
68+
this._balloon = editor.plugins.get( ContextualBalloon );
6769

6870
/**
6971
* Fires {@link #event:_selectionChangeDebounced} event using `lodash#debounce`.
@@ -196,7 +198,8 @@ export default class ContextualToolbar extends Plugin {
196198
* @returns {module:utils/dom/position~Options}
197199
*/
198200
_getBalloonPositionData() {
199-
const editingView = this.editor.editing.view;
201+
const editor = this.editor;
202+
const editingView = editor.editing.view;
200203

201204
// Get direction of the selection.
202205
const isBackward = editingView.selection.isBackward;
@@ -213,7 +216,6 @@ export default class ContextualToolbar extends Plugin {
213216
// Select the proper range rect depending on the direction of the selection.
214217
return rangeRects[ isBackward ? 0 : rangeRects.length - 1 ];
215218
},
216-
limiter: this.editor.ui.view.editable.element,
217219
positions: getBalloonPositions( isBackward )
218220
};
219221
}

tests/panel/balloon/balloonpanelview.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,22 @@ describe( 'BalloonPanelView', () => {
614614
sinon.assert.calledTwice( attachToSpy );
615615
} );
616616

617+
it( 'should work for a function as a target/limiter', () => {
618+
// Just check if this normally works without errors.
619+
const rect = {};
620+
621+
view.pin( {
622+
target() { return rect; },
623+
limiter() { return limiter; }
624+
} );
625+
626+
sinon.assert.calledOnce( attachToSpy );
627+
628+
limiter.dispatchEvent( new Event( 'scroll' ) );
629+
630+
sinon.assert.calledTwice( attachToSpy );
631+
} );
632+
617633
// https://github.com/ckeditor/ckeditor5-ui/issues/227
618634
it( 'should react to #scroll from anywhere when the target is not an HTMLElement or Range', () => {
619635
const rect = {};
@@ -624,6 +640,17 @@ describe( 'BalloonPanelView', () => {
624640
notRelatedElement.dispatchEvent( new Event( 'scroll' ) );
625641
sinon.assert.calledTwice( attachToSpy );
626642
} );
643+
644+
// https://github.com/ckeditor/ckeditor5-ui/issues/260
645+
it( 'should react to #scroll from anywhere when the limiter is not an HTMLElement` or Range', () => {
646+
const rect = {};
647+
648+
view.pin( { target, limiter: rect } );
649+
sinon.assert.calledOnce( attachToSpy );
650+
651+
notRelatedElement.dispatchEvent( new Event( 'scroll' ) );
652+
sinon.assert.calledTwice( attachToSpy );
653+
} );
627654
} );
628655

629656
describe( 'unpin()', () => {

0 commit comments

Comments
 (0)