UNPKG

@ckeditor/ckeditor5-ui

Version:

The UI framework and standard UI library of CKEditor 5.

1,079 lines (1,078 loc) 40 kB
/** * @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; }