@ckeditor/ckeditor5-ui
Version:
The UI framework and standard UI library of CKEditor 5.
1,079 lines (1,078 loc) • 40 kB
JavaScript
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module ui/panel/balloon/balloonpanelview
*/
import View from '../../view.js';
import { getOptimalPosition, global, isRange, toUnit, isVisible, isText, ResizeObserver, Rect } from '@ckeditor/ckeditor5-utils';
import { isElement } from 'es-toolkit/compat';
import '../../../theme/components/panel/balloonpanel.css';
const toPx = /* #__PURE__ */ toUnit('px');
// A static balloon panel positioning function that moves the balloon far off the viewport.
// It is used as a fallback when there is no way to position the balloon using provided
// positioning functions (see: `getOptimalPosition()`), for instance, when the target the
// balloon should be attached to gets obscured by scrollable containers or the viewport.
//
// It prevents the balloon from being attached to the void and possible degradation of the UX.
// At the same time, it keeps the balloon physically visible in the DOM so the focus remains
// uninterrupted.
const POSITION_OFF_SCREEN = {
top: -99999,
left: -99999,
name: 'arrowless',
config: {
withArrow: false
}
};
/**
* The balloon panel view class.
*
* A floating container which can
* {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#pin pin} to any
* {@link module:utils/dom/position~Options#target target} in the DOM and remain in that position
* e.g. when the web page is scrolled.
*
* The balloon panel can be used to display contextual, non-blocking UI like forms, toolbars and
* the like in its {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#content} view
* collection.
*
* There is a number of {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}
* that the balloon can use, automatically switching from one to another when the viewport space becomes
* scarce to keep the balloon visible to the user as long as it is possible. The balloon will also
* accept any custom position set provided by the user compatible with the
* {@link module:utils/dom/position~Options options}.
*
* ```ts
* const panel = new BalloonPanelView( locale );
* const childView = new ChildView();
* const positions = BalloonPanelView.defaultPositions;
*
* panel.render();
*
* // Add a child view to the panel's content collection.
* panel.content.add( childView );
*
* // Start pinning the panel to an element with the "target" id DOM.
* // The balloon will remain pinned until unpin() is called.
* panel.pin( {
* target: document.querySelector( '#target' ),
* positions: [
* positions.northArrowSouth,
* positions.southArrowNorth
* ]
* } );
* ```
*/
class BalloonPanelView extends View {
/**
* A collection of the child views that creates the balloon panel contents.
*/
content;
/**
* A callback that starts pinning the panel when {@link #isVisible} gets
* `true`. Used by {@link #pin}.
*
* @private
*/
_pinWhenIsVisibleCallback;
/**
* An instance of resize observer used to detect if target element is still visible.
*/
_resizeObserver;
/**
* @inheritDoc
*/
constructor(locale) {
super(locale);
const bind = this.bindTemplate;
this.set('top', 0);
this.set('left', 0);
this.set('position', 'arrow_nw');
this.set('isVisible', false);
this.set('withArrow', true);
this.set('class', undefined);
this._pinWhenIsVisibleCallback = null;
this._resizeObserver = null;
this.content = this.createCollection();
this.setTemplate({
tag: 'div',
attributes: {
class: [
'ck',
'ck-balloon-panel',
bind.to('position', value => `ck-balloon-panel_${value}`),
bind.if('isVisible', 'ck-balloon-panel_visible'),
bind.if('withArrow', 'ck-balloon-panel_with-arrow'),
bind.to('class')
],
style: {
top: bind.to('top', toPx),
left: bind.to('left', toPx)
}
},
children: this.content
});
}
/**
* @inheritDoc
*/
destroy() {
this.hide();
super.destroy();
}
/**
* Shows the panel.
*
* See {@link #isVisible}.
*/
show() {
this.isVisible = true;
}
/**
* Hides the panel.
*
* See {@link #isVisible}.
*/
hide() {
this.isVisible = false;
}
/**
* Attaches the panel to a specified {@link module:utils/dom/position~Options#target} with a
* smart positioning heuristics that chooses from available positions to make sure the panel
* is visible to the user i.e. within the limits of the viewport.
*
* This method accepts configuration {@link module:utils/dom/position~Options options}
* to set the `target`, optional `limiter` and `positions` the balloon should choose from.
*
* ```ts
* const panel = new BalloonPanelView( locale );
* const positions = BalloonPanelView.defaultPositions;
*
* panel.render();
*
* // Attach the panel to an element with the "target" id DOM.
* panel.attachTo( {
* target: document.querySelector( '#target' ),
* positions: [
* positions.northArrowSouth,
* positions.southArrowNorth
* ]
* } );
* ```
*
* **Note**: Attaching the panel will also automatically {@link #show} it.
*
* **Note**: An attached panel will not follow its target when the window is scrolled or resized.
* See the {@link #pin} method for a more permanent positioning strategy.
*
* @param options Positioning options compatible with {@link module:utils/dom/position~getOptimalPosition}.
* Default `positions` array is {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}.
* @returns Whether the balloon was shown and successfully attached or not. Attaching can fail if the target
* provided in the options is invisible (e.g. element detached from DOM).
*/
attachTo(options) {
const target = getDomElement(options.target);
if (target && !isVisible(target)) {
return false;
}
this.show();
const defaultPositions = BalloonPanelView.defaultPositions;
const positionOptions = Object.assign({}, {
element: this.element,
positions: [
defaultPositions.southArrowNorth,
defaultPositions.southArrowNorthMiddleWest,
defaultPositions.southArrowNorthMiddleEast,
defaultPositions.southArrowNorthWest,
defaultPositions.southArrowNorthEast,
defaultPositions.northArrowSouth,
defaultPositions.northArrowSouthMiddleWest,
defaultPositions.northArrowSouthMiddleEast,
defaultPositions.northArrowSouthWest,
defaultPositions.northArrowSouthEast,
defaultPositions.viewportStickyNorth
],
limiter: global.document.body,
fitInViewport: true
}, options);
const optimalPosition = BalloonPanelView._getOptimalPosition(positionOptions) || POSITION_OFF_SCREEN;
// Usually browsers make some problems with super accurate values like 104.345px
// so it is better to use int values.
const left = parseInt(optimalPosition.left);
const top = parseInt(optimalPosition.top);
const position = optimalPosition.name;
const config = optimalPosition.config || {};
const { withArrow = true } = config;
this.top = top;
this.left = left;
this.position = position;
this.withArrow = withArrow;
return true;
}
/**
* Works the same way as the {@link #attachTo} method except that the position of the panel is
* continuously updated when:
*
* * any ancestor of the {@link module:utils/dom/position~Options#target}
* or {@link module:utils/dom/position~Options#limiter} is scrolled,
* * the browser window gets resized or scrolled.
*
* Thanks to that, the panel always sticks to the {@link module:utils/dom/position~Options#target}
* and is immune to the changing environment.
*
* ```ts
* const panel = new BalloonPanelView( locale );
* const positions = BalloonPanelView.defaultPositions;
*
* panel.render();
*
* // Pin the panel to an element with the "target" id DOM.
* panel.pin( {
* target: document.querySelector( '#target' ),
* positions: [
* positions.northArrowSouth,
* positions.southArrowNorth
* ]
* } );
* ```
*
* To leave the pinned state, use the {@link #unpin} method.
*
* **Note**: Pinning the panel will also automatically {@link #show} it.
*
* @param options Positioning options compatible with {@link module:utils/dom/position~getOptimalPosition}.
* Default `positions` array is {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}.
*/
pin(options) {
this.unpin();
if (!this._startPinning(options)) {
return;
}
this._pinWhenIsVisibleCallback = () => {
if (this.isVisible) {
this._startPinning(options);
}
else {
this._stopPinning();
}
};
// Control the state of the listeners depending on whether the panel is visible
// or not.
// TODO: Use on() (https://github.com/ckeditor/ckeditor5-utils/issues/144).
this.listenTo(this, 'change:isVisible', this._pinWhenIsVisibleCallback);
}
/**
* Stops pinning the panel, as set up by {@link #pin}.
*/
unpin() {
if (this._pinWhenIsVisibleCallback) {
// Deactivate listeners attached by pin().
this._stopPinning();
// Deactivate the panel pin() control logic.
// TODO: Use off() (https://github.com/ckeditor/ckeditor5-utils/issues/144).
this.stopListening(this, 'change:isVisible', this._pinWhenIsVisibleCallback);
this._pinWhenIsVisibleCallback = null;
this.hide();
}
}
/**
* Starts managing the pinned state of the panel. See {@link #pin}.
*
* @param options Positioning options compatible with {@link module:utils/dom/position~getOptimalPosition}.
* @returns Whether the balloon was shown and successfully attached or not. Attaching can fail if the target
* provided in the options is invisible (e.g. element detached from DOM).
*/
_startPinning(options) {
if (!this.attachTo(options)) {
return false;
}
let targetElement = getDomElement(options.target);
const limiterElement = options.limiter ? getDomElement(options.limiter) : global.document.body;
// Then we need to listen on scroll event of eny element in the document.
this.listenTo(global.document, 'scroll', (evt, domEvt) => {
const scrollTarget = domEvt.target;
// The position needs to be updated if the positioning target is within the scrolled element.
const isWithinScrollTarget = targetElement && scrollTarget.contains(targetElement);
// The position needs to be updated if the positioning limiter is within the scrolled element.
const isLimiterWithinScrollTarget = limiterElement && scrollTarget.contains(limiterElement);
// The positioning target and/or limiter can be a Rect, object etc..
// There's no way to optimize the listener then.
if (isWithinScrollTarget || isLimiterWithinScrollTarget || !targetElement || !limiterElement) {
this.attachTo(options);
}
}, { useCapture: true });
// We need to listen on window resize event and update position.
this.listenTo(global.window, 'resize', () => {
this.attachTo(options);
});
// Hide the panel if the target element is no longer visible.
if (!this._resizeObserver) {
// If the target element is a text node, we need to check the parent element.
// It's because `ResizeObserver` accept only elements, not text nodes.
if (targetElement && isText(targetElement)) {
targetElement = targetElement.parentElement;
}
if (targetElement) {
const checkVisibility = () => {
// If the target element is no longer visible, hide the panel.
if (!isVisible(targetElement)) {
this.unpin();
}
};
// Element is being resized to 0x0 after it's parent became hidden,
// so we need to check size in order to determine if it's visible or not.
this._resizeObserver = new ResizeObserver(targetElement, checkVisibility);
}
}
return true;
}
/**
* Stops managing the pinned state of the panel. See {@link #pin}.
*/
_stopPinning() {
this.stopListening(global.document, 'scroll');
this.stopListening(global.window, 'resize');
if (this._resizeObserver) {
this._resizeObserver.destroy();
this._resizeObserver = null;
}
}
/**
* Returns available {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
* {@link module:utils/dom/position~PositioningFunction positioning functions} adjusted by the specific offsets.
*
* @internal
* @param options Options to generate positions. If not specified, this helper will simply return
* {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}.
* @param options.sideOffset A custom side offset (in pixels) of each position. If
* not specified, {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.arrowSideOffset the default value}
* will be used.
* @param options.heightOffset A custom height offset (in pixels) of each position. If
* not specified, {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.arrowHeightOffset the default value}
* will be used.
* @param options.stickyVerticalOffset A custom offset (in pixels) of the `viewportStickyNorth` positioning function.
* If not specified, {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.stickyVerticalOffset the default value}
* will be used.
* @param options.config Additional configuration of the balloon balloon panel view.
* Currently only {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#withArrow} is supported. Learn more
* about {@link module:utils/dom/position~PositioningFunction positioning functions}.
*/
static generatePositions(options = {}) {
const { sideOffset = BalloonPanelView.arrowSideOffset, heightOffset = BalloonPanelView.arrowHeightOffset, stickyVerticalOffset = BalloonPanelView.stickyVerticalOffset, config } = options;
return {
// ------- North west
northWestArrowSouthWest: (targetRect, balloonRect) => ({
top: getNorthTop(targetRect, balloonRect),
left: targetRect.left - sideOffset,
name: 'arrow_sw',
...(config && { config })
}),
northWestArrowSouthMiddleWest: (targetRect, balloonRect) => ({
top: getNorthTop(targetRect, balloonRect),
left: targetRect.left - (balloonRect.width * .25) - sideOffset,
name: 'arrow_smw',
...(config && { config })
}),
northWestArrowSouth: (targetRect, balloonRect) => ({
top: getNorthTop(targetRect, balloonRect),
left: targetRect.left - balloonRect.width / 2,
name: 'arrow_s',
...(config && { config })
}),
northWestArrowSouthMiddleEast: (targetRect, balloonRect) => ({
top: getNorthTop(targetRect, balloonRect),
left: targetRect.left - (balloonRect.width * .75) + sideOffset,
name: 'arrow_sme',
...(config && { config })
}),
northWestArrowSouthEast: (targetRect, balloonRect) => ({
top: getNorthTop(targetRect, balloonRect),
left: targetRect.left - balloonRect.width + sideOffset,
name: 'arrow_se',
...(config && { config })
}),
// ------- North
northArrowSouthWest: (targetRect, balloonRect) => ({
top: getNorthTop(targetRect, balloonRect),
left: targetRect.left + targetRect.width / 2 - sideOffset,
name: 'arrow_sw',
...(config && { config })
}),
northArrowSouthMiddleWest: (targetRect, balloonRect) => ({
top: getNorthTop(targetRect, balloonRect),
left: targetRect.left + targetRect.width / 2 - (balloonRect.width * .25) - sideOffset,
name: 'arrow_smw',
...(config && { config })
}),
northArrowSouth: (targetRect, balloonRect) => ({
top: getNorthTop(targetRect, balloonRect),
left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
name: 'arrow_s',
...(config && { config })
}),
northArrowSouthMiddleEast: (targetRect, balloonRect) => ({
top: getNorthTop(targetRect, balloonRect),
left: targetRect.left + targetRect.width / 2 - (balloonRect.width * .75) + sideOffset,
name: 'arrow_sme',
...(config && { config })
}),
northArrowSouthEast: (targetRect, balloonRect) => ({
top: getNorthTop(targetRect, balloonRect),
left: targetRect.left + targetRect.width / 2 - balloonRect.width + sideOffset,
name: 'arrow_se',
...(config && { config })
}),
// ------- North east
northEastArrowSouthWest: (targetRect, balloonRect) => ({
top: getNorthTop(targetRect, balloonRect),
left: targetRect.right - sideOffset,
name: 'arrow_sw',
...(config && { config })
}),
northEastArrowSouthMiddleWest: (targetRect, balloonRect) => ({
top: getNorthTop(targetRect, balloonRect),
left: targetRect.right - (balloonRect.width * .25) - sideOffset,
name: 'arrow_smw',
...(config && { config })
}),
northEastArrowSouth: (targetRect, balloonRect) => ({
top: getNorthTop(targetRect, balloonRect),
left: targetRect.right - balloonRect.width / 2,
name: 'arrow_s',
...(config && { config })
}),
northEastArrowSouthMiddleEast: (targetRect, balloonRect) => ({
top: getNorthTop(targetRect, balloonRect),
left: targetRect.right - (balloonRect.width * .75) + sideOffset,
name: 'arrow_sme',
...(config && { config })
}),
northEastArrowSouthEast: (targetRect, balloonRect) => ({
top: getNorthTop(targetRect, balloonRect),
left: targetRect.right - balloonRect.width + sideOffset,
name: 'arrow_se',
...(config && { config })
}),
// ------- South west
southWestArrowNorthWest: targetRect => ({
top: getSouthTop(targetRect),
left: targetRect.left - sideOffset,
name: 'arrow_nw',
...(config && { config })
}),
southWestArrowNorthMiddleWest: (targetRect, balloonRect) => ({
top: getSouthTop(targetRect),
left: targetRect.left - (balloonRect.width * .25) - sideOffset,
name: 'arrow_nmw',
...(config && { config })
}),
southWestArrowNorth: (targetRect, balloonRect) => ({
top: getSouthTop(targetRect),
left: targetRect.left - balloonRect.width / 2,
name: 'arrow_n',
...(config && { config })
}),
southWestArrowNorthMiddleEast: (targetRect, balloonRect) => ({
top: getSouthTop(targetRect),
left: targetRect.left - (balloonRect.width * .75) + sideOffset,
name: 'arrow_nme',
...(config && { config })
}),
southWestArrowNorthEast: (targetRect, balloonRect) => ({
top: getSouthTop(targetRect),
left: targetRect.left - balloonRect.width + sideOffset,
name: 'arrow_ne',
...(config && { config })
}),
// ------- South
southArrowNorthWest: targetRect => ({
top: getSouthTop(targetRect),
left: targetRect.left + targetRect.width / 2 - sideOffset,
name: 'arrow_nw',
...(config && { config })
}),
southArrowNorthMiddleWest: (targetRect, balloonRect) => ({
top: getSouthTop(targetRect),
left: targetRect.left + targetRect.width / 2 - (balloonRect.width * 0.25) - sideOffset,
name: 'arrow_nmw',
...(config && { config })
}),
southArrowNorth: (targetRect, balloonRect) => ({
top: getSouthTop(targetRect),
left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
name: 'arrow_n',
...(config && { config })
}),
southArrowNorthMiddleEast: (targetRect, balloonRect) => ({
top: getSouthTop(targetRect),
left: targetRect.left + targetRect.width / 2 - (balloonRect.width * 0.75) + sideOffset,
name: 'arrow_nme',
...(config && { config })
}),
southArrowNorthEast: (targetRect, balloonRect) => ({
top: getSouthTop(targetRect),
left: targetRect.left + targetRect.width / 2 - balloonRect.width + sideOffset,
name: 'arrow_ne',
...(config && { config })
}),
// ------- South east
southEastArrowNorthWest: targetRect => ({
top: getSouthTop(targetRect),
left: targetRect.right - sideOffset,
name: 'arrow_nw',
...(config && { config })
}),
southEastArrowNorthMiddleWest: (targetRect, balloonRect) => ({
top: getSouthTop(targetRect),
left: targetRect.right - (balloonRect.width * .25) - sideOffset,
name: 'arrow_nmw',
...(config && { config })
}),
southEastArrowNorth: (targetRect, balloonRect) => ({
top: getSouthTop(targetRect),
left: targetRect.right - balloonRect.width / 2,
name: 'arrow_n',
...(config && { config })
}),
southEastArrowNorthMiddleEast: (targetRect, balloonRect) => ({
top: getSouthTop(targetRect),
left: targetRect.right - (balloonRect.width * .75) + sideOffset,
name: 'arrow_nme',
...(config && { config })
}),
southEastArrowNorthEast: (targetRect, balloonRect) => ({
top: getSouthTop(targetRect),
left: targetRect.right - balloonRect.width + sideOffset,
name: 'arrow_ne',
...(config && { config })
}),
// ------- West
westArrowEast: (targetRect, balloonRect) => ({
top: targetRect.top + targetRect.height / 2 - balloonRect.height / 2,
left: targetRect.left - balloonRect.width - heightOffset,
name: 'arrow_e',
...(config && { config })
}),
// ------- East
eastArrowWest: (targetRect, balloonRect) => ({
top: targetRect.top + targetRect.height / 2 - balloonRect.height / 2,
left: targetRect.right + heightOffset,
name: 'arrow_w',
...(config && { config })
}),
// ------- Sticky
viewportStickyNorth: (targetRect, balloonRect, viewportRect) => {
// Get the intersection of the viewport and the document body.
const boundaryRect = new Rect(global.document.body).getIntersection(viewportRect.getVisible());
if (!boundaryRect) {
return null;
}
// Get the visible intersection of the boundary and the document body.
const visibleBoundaryRect = boundaryRect.getVisible();
// Check if the target is in the boundary.
if (!targetRect.getIntersection(visibleBoundaryRect)) {
return null;
}
// Checks if there is enough space to put the balloon on the top or bottom of the target.
// If not, makes the balloon sticky.
if (!(visibleBoundaryRect.top - targetRect.top - stickyVerticalOffset < balloonRect.height &&
visibleBoundaryRect.bottom - targetRect.bottom < balloonRect.height)) {
return null;
}
return {
top: visibleBoundaryRect.top + stickyVerticalOffset,
left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
name: 'arrowless',
config: {
withArrow: false,
...config
}
};
}
};
/**
* Returns the top coordinate for positions starting with `north*`.
*
* @param targetRect A rect of the target.
* @param balloonRect A rect of the balloon.
*/
function getNorthTop(targetRect, balloonRect) {
return targetRect.top - balloonRect.height - heightOffset;
}
/**
* Returns the top coordinate for positions starting with `south*`.
*
* @param targetRect A rect of the target.
*/
function getSouthTop(targetRect) {
return targetRect.bottom + heightOffset;
}
}
/**
* A side offset of the arrow tip from the edge of the balloon. Controlled by CSS.
*
* ```
* ┌───────────────────────┐
* │ │
* │ Balloon │
* │ Content │
* │ │
* └──+ +───────────────┘
* | \ /
* | \/
* >┼─────┼< ─────────────────────── side offset
*
* ```
*
* @default 25
*/
static arrowSideOffset = 25;
/**
* A height offset of the arrow from the edge of the balloon. Controlled by CSS.
*
* ```
* ┌───────────────────────┐
* │ │
* │ Balloon │
* │ Content │ ╱-- arrow height offset
* │ │ V
* └──+ +───────────────┘ --- ─┼───────
* \ / │
* \/ │
* ────────────────────────────────┼───────
* ^
*
*
* >┼────┼< arrow height offset
* │ │
* │ ┌────────────────────────┐
* │ │ │
* │ ╱ │
* │ ╱ Balloon │
* │ ╲ Content │
* │ ╲ │
* │ │ │
* │ └────────────────────────┘
* ```
*
* @default 10
*/
static arrowHeightOffset = 10;
/**
* A vertical offset of the balloon panel from the edge of the viewport if sticky.
* It helps in accessing toolbar buttons underneath the balloon panel.
*
* ```
* ┌───────────────────────────────────────────────────┐
* │ Target │
* │ │
* │ /── vertical offset │
* ┌─────────────────────────────V─────────────────────────┐
* │ Toolbar ┌─────────────┐ │
* ├────────────────────│ Balloon │────────────────────┤
* │ │ └─────────────┘ │ │
* │ │ │ │
* │ │ │ │
* │ │ │ │
* │ └───────────────────────────────────────────────────┘ │
* │ Viewport │
* └───────────────────────────────────────────────────────┘
* ```
*
* @default 20
*/
static stickyVerticalOffset = 20;
/**
* Function used to calculate the optimal position for the balloon.
*/
static _getOptimalPosition = getOptimalPosition;
/**
* A default set of positioning functions used by the balloon panel view
* when attaching using the {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#attachTo} method.
*
* The available positioning functions are as follows:
*
* **North west**
*
* * `northWestArrowSouthWest`
*
* ```
* +-----------------+
* | Balloon |
* +-----------------+
* V
* [ Target ]
* ```
*
* * `northWestArrowSouthMiddleWest`
*
* ```
* +-----------------+
* | Balloon |
* +-----------------+
* V
* [ Target ]
* ```
*
* * `northWestArrowSouth`
*
* ```
* +-----------------+
* | Balloon |
* +-----------------+
* V
* [ Target ]
* ```
*
* * `northWestArrowSouthMiddleEast`
*
* ```
* +-----------------+
* | Balloon |
* +-----------------+
* V
* [ Target ]
* ```
*
* * `northWestArrowSouthEast`
*
* ```
* +-----------------+
* | Balloon |
* +-----------------+
* V
* [ Target ]
* ```
*
* **North**
*
* * `northArrowSouthWest`
*
* ```
* +-----------------+
* | Balloon |
* +-----------------+
* V
* [ Target ]
* ```
*
* * `northArrowSouthMiddleWest`
*
* ```
* +-----------------+
* | Balloon |
* +-----------------+
* V
* [ Target ]
* ```
* * `northArrowSouth`
*
* ```
* +-----------------+
* | Balloon |
* +-----------------+
* V
* [ Target ]
* ```
*
* * `northArrowSouthMiddleEast`
*
* ```
* +-----------------+
* | Balloon |
* +-----------------+
* V
* [ Target ]
* ```
*
* * `northArrowSouthEast`
*
* ```
* +-----------------+
* | Balloon |
* +-----------------+
* V
* [ Target ]
* ```
*
* **North east**
*
* * `northEastArrowSouthWest`
*
* ```
* +-----------------+
* | Balloon |
* +-----------------+
* V
* [ Target ]
* ```
*
* * `northEastArrowSouthMiddleWest`
*
* ```
* +-----------------+
* | Balloon |
* +-----------------+
* V
* [ Target ]
* ```
*
* * `northEastArrowSouth`
*
* ```
* +-----------------+
* | Balloon |
* +-----------------+
* V
* [ Target ]
* ```
*
* * `northEastArrowSouthMiddleEast`
*
* ```
* +-----------------+
* | Balloon |
* +-----------------+
* V
* [ Target ]
* ```
*
* * `northEastArrowSouthEast`
*
* ```
* +-----------------+
* | Balloon |
* +-----------------+
* V
* [ Target ]
* ```
*
* **South**
*
* * `southArrowNorthWest`
*
* ```
* [ Target ]
* ^
* +-----------------+
* | Balloon |
* +-----------------+
* ```
*
* * `southArrowNorthMiddleWest`
*
* ```
* [ Target ]
* ^
* +-----------------+
* | Balloon |
* +-----------------+
* ```
*
* * `southArrowNorth`
*
* ```
* [ Target ]
* ^
* +-----------------+
* | Balloon |
* +-----------------+
* ```
*
* * `southArrowNorthMiddleEast`
*
* ```
* [ Target ]
* ^
* +-----------------+
* | Balloon |
* +-----------------+
* ```
*
* * `southArrowNorthEast`
*
* ```
* [ Target ]
* ^
* +-----------------+
* | Balloon |
* +-----------------+
* ```
*
* **South west**
*
* * `southWestArrowNorthWest`
*
*
* ```
* [ Target ]
* ^
* +-----------------+
* | Balloon |
* +-----------------+
* ```
*
* * `southWestArrowNorthMiddleWest`
*
* ```
* [ Target ]
* ^
* +-----------------+
* | Balloon |
* +-----------------+
* ```
*
* * `southWestArrowNorth`
*
* ```
* [ Target ]
* ^
* +-----------------+
* | Balloon |
* +-----------------+
* ```
*
* * `southWestArrowNorthMiddleEast`
*
* ```
* [ Target ]
* ^
* +-----------------+
* | Balloon |
* +-----------------+
* ```
*
* * `southWestArrowNorthEast`
*
* ```
* [ Target ]
* ^
* +-----------------+
* | Balloon |
* +-----------------+
* ```
*
* **South east**
*
* * `southEastArrowNorthWest`
*
* ```
* [ Target ]
* ^
* +-----------------+
* | Balloon |
* +-----------------+
* ```
*
* * `southEastArrowNorthMiddleWest`
*
* ```
* [ Target ]
* ^
* +-----------------+
* | Balloon |
* +-----------------+
* ```
*
* * `southEastArrowNorth`
*
* ```
* [ Target ]
* ^
* +-----------------+
* | Balloon |
* +-----------------+
* ```
*
* * `southEastArrowNorthMiddleEast`
*
* ```
* [ Target ]
* ^
* +-----------------+
* | Balloon |
* +-----------------+
* ```
*
* * `southEastArrowNorthEast`
*
* ```
* [ Target ]
* ^
* +-----------------+
* | Balloon |
* +-----------------+
* ```
*
* **West**
*
* * `westArrowEast`
*
* ```
* +-----------------+
* | Balloon |>[ Target ]
* +-----------------+
* ```
*
* **East**
*
* * `eastArrowWest`
*
* ```
* +-----------------+
* [ Target ]<| Balloon |
* +-----------------+
* ```
*
* **Sticky**
*
* * `viewportStickyNorth`
*
* ```
* +---------------------------+
* | [ Target ] |
* | |
* +-----------------------------------+
* | | +-----------------+ | |
* | | | Balloon | | |
* | | +-----------------+ | |
* | | | |
* | | | |
* | | | |
* | | | |
* | +---------------------------+ |
* | Viewport |
* +-----------------------------------+
* ```
*
* See {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#attachTo}.
*
* Positioning functions must be compatible with {@link module:utils/dom/position~DomPoint}.
*
* Default positioning functions with customized offsets can be generated using
* {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.generatePositions}.
*
* The name that the position function returns will be reflected in the balloon panel's class that
* controls the placement of the "arrow". See {@link #position} to learn more.
*/
static defaultPositions = /* #__PURE__ */ BalloonPanelView.generatePositions();
}
export default BalloonPanelView;
/**
* Returns the DOM element for given object or null, if there is none,
* e.g. when the passed object is a Rect instance or so.
*/
function getDomElement(object) {
if (isElement(object)) {
return object;
}
if (isRange(object)) {
return object.commonAncestorContainer;
}
if (typeof object == 'function') {
return getDomElement(object());
}
return null;
}