From 1d27c077fdce18b14282f66d3c4e5a62386b7f03 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 19 Oct 2021 13:31:34 +0200 Subject: [PATCH 01/10] PoC. --- .../tests/manual/ballooneditor.js | 2 + .../src/panel/balloon/balloonpanelview.js | 491 ++++++++++-------- .../src/toolbar/balloon/balloontoolbar.js | 103 ++-- 3 files changed, 338 insertions(+), 258 deletions(-) diff --git a/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor.js b/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor.js index fad69b090f1..caef3ff4dfd 100644 --- a/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor.js +++ b/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor.js @@ -66,3 +66,5 @@ function destroyEditors() { document.getElementById( 'initEditors' ).addEventListener( 'click', initEditors ); document.getElementById( 'destroyEditors' ).addEventListener( 'click', destroyEditors ); + +initEditors(); diff --git a/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js b/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js index 405585902c5..d5fdb2164d6 100644 --- a/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js +++ b/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js @@ -756,233 +756,276 @@ BalloonPanelView._getOptimalPosition = getOptimalPosition; * @member {Object.} * module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions */ -BalloonPanelView.defaultPositions = { - - // ------- North west - - northWestArrowSouthWest: ( targetRect, balloonRect ) => ( { - top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left - BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_sw' - } ), - - northWestArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( { - top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left - ( balloonRect.width * .25 ) - BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_smw' - } ), - - northWestArrowSouth: ( targetRect, balloonRect ) => ( { - top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left - balloonRect.width / 2, - name: 'arrow_s' - } ), - - northWestArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( { - top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left - ( balloonRect.width * .75 ) + BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_sme' - } ), - - northWestArrowSouthEast: ( targetRect, balloonRect ) => ( { - top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left - balloonRect.width + BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_se' - } ), - - // ------- North - - northArrowSouthWest: ( targetRect, balloonRect ) => ( { - top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_sw' - } ), - - northArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( { - top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * .25 ) - BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_smw' - } ), - - northArrowSouth: ( targetRect, balloonRect ) => ( { - top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2, - name: 'arrow_s' - } ), - - northArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( { - top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * .75 ) + BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_sme' - } ), - - northArrowSouthEast: ( targetRect, balloonRect ) => ( { - top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_se' - } ), - - // ------- North east - - northEastArrowSouthWest: ( targetRect, balloonRect ) => ( { - top: getNorthTop( targetRect, balloonRect ), - left: targetRect.right - BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_sw' - } ), - - northEastArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( { - top: getNorthTop( targetRect, balloonRect ), - left: targetRect.right - ( balloonRect.width * .25 ) - BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_smw' - } ), - - northEastArrowSouth: ( targetRect, balloonRect ) => ( { - top: getNorthTop( targetRect, balloonRect ), - left: targetRect.right - balloonRect.width / 2, - name: 'arrow_s' - } ), - - northEastArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( { - top: getNorthTop( targetRect, balloonRect ), - left: targetRect.right - ( balloonRect.width * .75 ) + BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_sme' - } ), - - northEastArrowSouthEast: ( targetRect, balloonRect ) => ( { - top: getNorthTop( targetRect, balloonRect ), - left: targetRect.right - balloonRect.width + BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_se' - } ), - - // ------- South west - - southWestArrowNorthWest: ( targetRect, balloonRect ) => ( { - top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left - BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_nw' - } ), - - southWestArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( { - top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left - ( balloonRect.width * .25 ) - BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_nmw' - } ), - - southWestArrowNorth: ( targetRect, balloonRect ) => ( { - top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left - balloonRect.width / 2, - name: 'arrow_n' - } ), - - southWestArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( { - top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left - ( balloonRect.width * .75 ) + BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_nme' - } ), - - southWestArrowNorthEast: ( targetRect, balloonRect ) => ( { - top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left - balloonRect.width + BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_ne' - } ), - - // ------- South - - southArrowNorthWest: ( targetRect, balloonRect ) => ( { - top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_nw' - } ), - southArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( { - top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * 0.25 ) - BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_nmw' - } ), - - southArrowNorth: ( targetRect, balloonRect ) => ( { - top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2, - name: 'arrow_n' - } ), - - southArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( { - top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * 0.75 ) + BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_nme' - } ), - - southArrowNorthEast: ( targetRect, balloonRect ) => ( { - top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_ne' - } ), - - // ------- South east - - southEastArrowNorthWest: ( targetRect, balloonRect ) => ( { - top: getSouthTop( targetRect, balloonRect ), - left: targetRect.right - BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_nw' - } ), - - southEastArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( { - top: getSouthTop( targetRect, balloonRect ), - left: targetRect.right - ( balloonRect.width * .25 ) - BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_nmw' - } ), - - southEastArrowNorth: ( targetRect, balloonRect ) => ( { - top: getSouthTop( targetRect, balloonRect ), - left: targetRect.right - balloonRect.width / 2, - name: 'arrow_n' - } ), - - southEastArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( { - top: getSouthTop( targetRect, balloonRect ), - left: targetRect.right - ( balloonRect.width * .75 ) + BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_nme' - } ), - - southEastArrowNorthEast: ( targetRect, balloonRect ) => ( { - top: getSouthTop( targetRect, balloonRect ), - left: targetRect.right - balloonRect.width + BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_ne' - } ), - - // ------- Sticky - - viewportStickyNorth: ( targetRect, balloonRect, viewportRect ) => { - if ( !targetRect.getIntersection( viewportRect ) ) { - return null; - } +BalloonPanelView.defaultPositions = generatePositions( { + arrowHorizontalOffset: BalloonPanelView.arrowHorizontalOffset, + arrowVerticalOffset: BalloonPanelView.arrowVerticalOffset, + stickyVerticalOffset: BalloonPanelView.stickyVerticalOffset +} ); - return { - top: viewportRect.top + BalloonPanelView.stickyVerticalOffset, +/** + * TODO + * + * @param {*} + * @returns + */ +export function generatePositions( { arrowHorizontalOffset, arrowVerticalOffset, stickyVerticalOffset, config = {} } ) { + return { + // ------- North west + + northWestArrowSouthWest: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.left - arrowHorizontalOffset, + name: 'arrow_sw', + config + } ), + + northWestArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.left - ( balloonRect.width * .25 ) - arrowHorizontalOffset, + name: 'arrow_smw', + config + } ), + + northWestArrowSouth: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.left - balloonRect.width / 2, + name: 'arrow_s', + config + } ), + + northWestArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.left - ( balloonRect.width * .75 ) + arrowHorizontalOffset, + name: 'arrow_sme', + config + } ), + + northWestArrowSouthEast: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.left - balloonRect.width + arrowHorizontalOffset, + name: 'arrow_se', + config + } ), + + // ------- North + + northArrowSouthWest: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.left + targetRect.width / 2 - arrowHorizontalOffset, + name: 'arrow_sw', + config + } ), + + northArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * .25 ) - arrowHorizontalOffset, + name: 'arrow_smw', + config + } ), + + northArrowSouth: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2, - name: 'arrowless', - config: { - withArrow: false + name: 'arrow_s', + config + } ), + + northArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * .75 ) + arrowHorizontalOffset, + name: 'arrow_sme', + config + } ), + + northArrowSouthEast: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.left + targetRect.width / 2 - balloonRect.width + arrowHorizontalOffset, + name: 'arrow_se', + config + } ), + + // ------- North east + + northEastArrowSouthWest: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.right - arrowHorizontalOffset, + name: 'arrow_sw', + config + } ), + + northEastArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.right - ( balloonRect.width * .25 ) - arrowHorizontalOffset, + name: 'arrow_smw', + config + } ), + + northEastArrowSouth: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.right - balloonRect.width / 2, + name: 'arrow_s', + config + } ), + + northEastArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.right - ( balloonRect.width * .75 ) + arrowHorizontalOffset, + name: 'arrow_sme', + config + } ), + + northEastArrowSouthEast: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.right - balloonRect.width + arrowHorizontalOffset, + name: 'arrow_se', + config + } ), + + // ------- South west + + southWestArrowNorthWest: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.left - arrowHorizontalOffset, + name: 'arrow_nw', + config + } ), + + southWestArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.left - ( balloonRect.width * .25 ) - arrowHorizontalOffset, + name: 'arrow_nmw', + config + } ), + + southWestArrowNorth: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.left - balloonRect.width / 2, + name: 'arrow_n', + config + } ), + + southWestArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.left - ( balloonRect.width * .75 ) + arrowHorizontalOffset, + name: 'arrow_nme', + config + } ), + + southWestArrowNorthEast: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.left - balloonRect.width + arrowHorizontalOffset, + name: 'arrow_ne', + config + } ), + + // ------- South + + southArrowNorthWest: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.left + targetRect.width / 2 - arrowHorizontalOffset, + name: 'arrow_nw', + config + } ), + southArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * 0.25 ) - arrowHorizontalOffset, + name: 'arrow_nmw', + config + } ), + + southArrowNorth: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2, + name: 'arrow_n', + config + } ), + + southArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * 0.75 ) + arrowHorizontalOffset, + name: 'arrow_nme', + config + } ), + + southArrowNorthEast: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.left + targetRect.width / 2 - balloonRect.width + arrowHorizontalOffset, + name: 'arrow_ne', + config + } ), + + // ------- South east + + southEastArrowNorthWest: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.right - arrowHorizontalOffset, + name: 'arrow_nw', + config + } ), + + southEastArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.right - ( balloonRect.width * .25 ) - arrowHorizontalOffset, + name: 'arrow_nmw', + config + } ), + + southEastArrowNorth: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.right - balloonRect.width / 2, + name: 'arrow_n', + config + } ), + + southEastArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.right - ( balloonRect.width * .75 ) + arrowHorizontalOffset, + name: 'arrow_nme', + config + } ), + + southEastArrowNorthEast: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.right - balloonRect.width + arrowHorizontalOffset, + name: 'arrow_ne', + config + } ), + + // ------- Sticky + + viewportStickyNorth: ( targetRect, balloonRect, viewportRect ) => { + if ( !targetRect.getIntersection( viewportRect ) ) { + return null; } - }; - } -}; -// Returns the top coordinate for positions starting with `north*`. -// -// @private -// @param {utils/dom/rect~Rect} targetRect A rect of the target. -// @param {utils/dom/rect~Rect} elementRect A rect of the balloon. -// @returns {Number} -function getNorthTop( targetRect, balloonRect ) { - return targetRect.top - balloonRect.height - BalloonPanelView.arrowVerticalOffset; -} + return { + top: viewportRect.top + stickyVerticalOffset, + left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2, + name: 'arrowless', + config: { + withArrow: false + } + }; + } + }; + + // Returns the top coordinate for positions starting with `north*`. + // + // @private + // @param {utils/dom/rect~Rect} targetRect A rect of the target. + // @param {utils/dom/rect~Rect} elementRect A rect of the balloon. + // @returns {Number} + function getNorthTop( targetRect, balloonRect ) { + return targetRect.top - balloonRect.height - arrowVerticalOffset; + } -// Returns the top coordinate for positions starting with `south*`. -// -// @private -// @param {utils/dom/rect~Rect} targetRect A rect of the target. -// @param {utils/dom/rect~Rect} elementRect A rect of the balloon. -// @returns {Number} -function getSouthTop( targetRect ) { - return targetRect.bottom + BalloonPanelView.arrowVerticalOffset; + // Returns the top coordinate for positions starting with `south*`. + // + // @private + // @param {utils/dom/rect~Rect} targetRect A rect of the target. + // @param {utils/dom/rect~Rect} elementRect A rect of the balloon. + // @returns {Number} + function getSouthTop( targetRect ) { + return targetRect.bottom + arrowVerticalOffset; + } } diff --git a/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js b/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js index 430f74e43e3..9ab74dd70bc 100644 --- a/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js +++ b/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js @@ -10,13 +10,16 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import ContextualBalloon from '../../panel/balloon/contextualballoon'; import ToolbarView from '../toolbarview'; -import BalloonPanelView from '../../panel/balloon/balloonpanelview.js'; +import BalloonPanelView, { generatePositions } from '../../panel/balloon/balloonpanelview.js'; import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker'; import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect'; import normalizeToolbarConfig from '../normalizetoolbarconfig'; import { debounce } from 'lodash-es'; import ResizeObserver from '@ckeditor/ckeditor5-utils/src/dom/resizeobserver'; import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit'; +import { env } from '@ckeditor/ckeditor5-utils'; +import { RectDrawer } from '../../../../ckeditor5-minimap/src/utils'; +import areConnectedThroughProperties from '../../../../ckeditor5-utils/src/areconnectedthroughproperties'; const toPx = toUnit( 'px' ); @@ -249,6 +252,22 @@ export default class BalloonToolbar extends Plugin { position: this._getBalloonPositionData(), balloonClassName: 'ck-toolbar-container' } ); + + const rect = new Rect( this.toolbarView.items.get( 0 ).element ); + const lastItemRect = new Rect( this.toolbarView.items.last.element ); + + rect.moveBy( 0, rect.height ); + rect.height = BalloonToolbar.arrowVerticalOffset; + rect.bottom = rect.top + BalloonToolbar.arrowVerticalOffset; + rect.right = lastItemRect.right; + rect.width = lastItemRect.right - rect.left; + + RectDrawer.clear(); + RectDrawer.draw( rect, { + zIndex: 9999999, + backgroundColor: 'rgba(255,0,0,.2)', + transform: `translate3d(${ window.visualViewport.offsetLeft }px,${ window.visualViewport.offsetTop }px,0)` + } ); } /** @@ -259,6 +278,8 @@ export default class BalloonToolbar extends Plugin { this.stopListening( this.editor.ui, 'update' ); this._balloon.remove( this.toolbarView ); } + + RectDrawer.clear(); } /** @@ -300,7 +321,7 @@ export default class BalloonToolbar extends Plugin { return rangeRects[ rangeRects.length - 1 ]; } }, - positions: getBalloonPositions( isBackward ) + positions: this._getBalloonPositions( isBackward ) }; } @@ -345,39 +366,53 @@ export default class BalloonToolbar extends Plugin { * @protected * @event _selectionChangeDebounced */ -} -// Returns toolbar positions for the given direction of the selection. -// -// @private -// @param {Boolean} isBackward -// @returns {Array.} -function getBalloonPositions( isBackward ) { - const defaultPositions = BalloonPanelView.defaultPositions; - - return isBackward ? [ - defaultPositions.northWestArrowSouth, - defaultPositions.northWestArrowSouthWest, - defaultPositions.northWestArrowSouthEast, - defaultPositions.northWestArrowSouthMiddleEast, - defaultPositions.northWestArrowSouthMiddleWest, - defaultPositions.southWestArrowNorth, - defaultPositions.southWestArrowNorthWest, - defaultPositions.southWestArrowNorthEast, - defaultPositions.southWestArrowNorthMiddleWest, - defaultPositions.southWestArrowNorthMiddleEast - ] : [ - defaultPositions.southEastArrowNorth, - defaultPositions.southEastArrowNorthEast, - defaultPositions.southEastArrowNorthWest, - defaultPositions.southEastArrowNorthMiddleEast, - defaultPositions.southEastArrowNorthMiddleWest, - defaultPositions.northEastArrowSouth, - defaultPositions.northEastArrowSouthEast, - defaultPositions.northEastArrowSouthWest, - defaultPositions.northEastArrowSouthMiddleEast, - defaultPositions.northEastArrowSouthMiddleWest - ]; + /** + * TODO + */ + static get arrowVerticalOffset() { + return env.isSafari ? ( 35 / window.visualViewport.scale ) : BalloonPanelView.arrowVerticalOffset; + } + + // Returns toolbar positions for the given direction of the selection. + // + // @private + // @param {Boolean} isBackward + // @returns {Array.} + _getBalloonPositions( isBackward ) { + const generatedPositions = generatePositions( { + arrowHorizontalOffset: BalloonPanelView.arrowHorizontalOffset, + arrowVerticalOffset: BalloonToolbar.arrowVerticalOffset, + stickyVerticalOffset: BalloonPanelView.stickyVerticalOffset, + config: { + withArrow: !env.isSafari + } + } ); + + return isBackward ? [ + generatedPositions.northWestArrowSouth, + generatedPositions.northWestArrowSouthWest, + generatedPositions.northWestArrowSouthEast, + generatedPositions.northWestArrowSouthMiddleEast, + generatedPositions.northWestArrowSouthMiddleWest, + generatedPositions.southWestArrowNorth, + generatedPositions.southWestArrowNorthWest, + generatedPositions.southWestArrowNorthEast, + generatedPositions.southWestArrowNorthMiddleWest, + generatedPositions.southWestArrowNorthMiddleEast + ] : [ + generatedPositions.southEastArrowNorth, + generatedPositions.southEastArrowNorthEast, + generatedPositions.southEastArrowNorthWest, + generatedPositions.southEastArrowNorthMiddleEast, + generatedPositions.southEastArrowNorthMiddleWest, + generatedPositions.northEastArrowSouth, + generatedPositions.northEastArrowSouthEast, + generatedPositions.northEastArrowSouthWest, + generatedPositions.northEastArrowSouthMiddleEast, + generatedPositions.northEastArrowSouthMiddleWest + ]; + } } // Returns "true" when the selection has multiple ranges and each range contains a selectable element From 341503ca1eb44a28cc21c92311bc796e181d1cf2 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 21 Oct 2021 14:58:21 +0200 Subject: [PATCH 02/10] Better iOS/Safari detection. Limited arrow-less balloon positions for better UX. --- .../src/toolbar/balloon/balloontoolbar.js | 103 ++++++++---------- packages/ckeditor5-utils/src/env.js | 17 ++- 2 files changed, 63 insertions(+), 57 deletions(-) diff --git a/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js b/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js index 9ab74dd70bc..2c1c70322dc 100644 --- a/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js +++ b/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js @@ -3,6 +3,8 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ +/* global window */ + /** * @module ui/toolbar/balloon/balloontoolbar */ @@ -18,8 +20,6 @@ import { debounce } from 'lodash-es'; import ResizeObserver from '@ckeditor/ckeditor5-utils/src/dom/resizeobserver'; import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit'; import { env } from '@ckeditor/ckeditor5-utils'; -import { RectDrawer } from '../../../../ckeditor5-minimap/src/utils'; -import areConnectedThroughProperties from '../../../../ckeditor5-utils/src/areconnectedthroughproperties'; const toPx = toUnit( 'px' ); @@ -252,22 +252,6 @@ export default class BalloonToolbar extends Plugin { position: this._getBalloonPositionData(), balloonClassName: 'ck-toolbar-container' } ); - - const rect = new Rect( this.toolbarView.items.get( 0 ).element ); - const lastItemRect = new Rect( this.toolbarView.items.last.element ); - - rect.moveBy( 0, rect.height ); - rect.height = BalloonToolbar.arrowVerticalOffset; - rect.bottom = rect.top + BalloonToolbar.arrowVerticalOffset; - rect.right = lastItemRect.right; - rect.width = lastItemRect.right - rect.left; - - RectDrawer.clear(); - RectDrawer.draw( rect, { - zIndex: 9999999, - backgroundColor: 'rgba(255,0,0,.2)', - transform: `translate3d(${ window.visualViewport.offsetLeft }px,${ window.visualViewport.offsetTop }px,0)` - } ); } /** @@ -278,8 +262,6 @@ export default class BalloonToolbar extends Plugin { this.stopListening( this.editor.ui, 'update' ); this._balloon.remove( this.toolbarView ); } - - RectDrawer.clear(); } /** @@ -368,50 +350,59 @@ export default class BalloonToolbar extends Plugin { */ /** - * TODO + * Returns toolbar positions for the given direction of the selection. + * + * @private + * @param {Boolean} isBackward + * @returns {Array.} */ - static get arrowVerticalOffset() { - return env.isSafari ? ( 35 / window.visualViewport.scale ) : BalloonPanelView.arrowVerticalOffset; - } - - // Returns toolbar positions for the given direction of the selection. - // - // @private - // @param {Boolean} isBackward - // @returns {Array.} _getBalloonPositions( isBackward ) { const generatedPositions = generatePositions( { - arrowHorizontalOffset: BalloonPanelView.arrowHorizontalOffset, - arrowVerticalOffset: BalloonToolbar.arrowVerticalOffset, + arrowHorizontalOffset: 0, + arrowVerticalOffset: env.isIOSSafari ? ( 35 / window.visualViewport.scale ) : BalloonPanelView.arrowVerticalOffset, stickyVerticalOffset: BalloonPanelView.stickyVerticalOffset, config: { - withArrow: !env.isSafari + withArrow: !env.isIOSSafari } } ); - return isBackward ? [ - generatedPositions.northWestArrowSouth, - generatedPositions.northWestArrowSouthWest, - generatedPositions.northWestArrowSouthEast, - generatedPositions.northWestArrowSouthMiddleEast, - generatedPositions.northWestArrowSouthMiddleWest, - generatedPositions.southWestArrowNorth, - generatedPositions.southWestArrowNorthWest, - generatedPositions.southWestArrowNorthEast, - generatedPositions.southWestArrowNorthMiddleWest, - generatedPositions.southWestArrowNorthMiddleEast - ] : [ - generatedPositions.southEastArrowNorth, - generatedPositions.southEastArrowNorthEast, - generatedPositions.southEastArrowNorthWest, - generatedPositions.southEastArrowNorthMiddleEast, - generatedPositions.southEastArrowNorthMiddleWest, - generatedPositions.northEastArrowSouth, - generatedPositions.northEastArrowSouthEast, - generatedPositions.northEastArrowSouthWest, - generatedPositions.northEastArrowSouthMiddleEast, - generatedPositions.northEastArrowSouthMiddleWest - ]; + if ( env.isIOSSafari ) { + return isBackward ? [ + generatedPositions.northArrowSouth, + generatedPositions.northArrowSouth, + generatedPositions.northEastArrowSouthEast, + generatedPositions.northWestArrowSouthWest + ] : [ + generatedPositions.southArrowNorth, + generatedPositions.southArrowNorth, + generatedPositions.southWestArrowNorthWest, + generatedPositions.southEastArrowNorthEast + ]; + } else { + return isBackward ? [ + generatedPositions.northWestArrowSouth, + generatedPositions.northWestArrowSouthWest, + generatedPositions.northWestArrowSouthEast, + generatedPositions.northWestArrowSouthMiddleEast, + generatedPositions.northWestArrowSouthMiddleWest, + generatedPositions.southWestArrowNorth, + generatedPositions.southWestArrowNorthWest, + generatedPositions.southWestArrowNorthEast, + generatedPositions.southWestArrowNorthMiddleWest, + generatedPositions.southWestArrowNorthMiddleEast + ] : [ + generatedPositions.southEastArrowNorth, + generatedPositions.southEastArrowNorthEast, + generatedPositions.southEastArrowNorthWest, + generatedPositions.southEastArrowNorthMiddleEast, + generatedPositions.southEastArrowNorthMiddleWest, + generatedPositions.northEastArrowSouth, + generatedPositions.northEastArrowSouthEast, + generatedPositions.northEastArrowSouthWest, + generatedPositions.northEastArrowSouthMiddleEast, + generatedPositions.northEastArrowSouthMiddleWest + ]; + } } } diff --git a/packages/ckeditor5-utils/src/env.js b/packages/ckeditor5-utils/src/env.js index e7db0939fff..b7a2f262778 100644 --- a/packages/ckeditor5-utils/src/env.js +++ b/packages/ckeditor5-utils/src/env.js @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* globals navigator:false */ +/* globals navigator:false, document */ /** * @module utils/env @@ -41,6 +41,11 @@ const env = { */ isSafari: isSafari( userAgent ), + /** + * TODO + */ + isIOSSafari: isIOSSafari( userAgent ), + /** * Indicates that the application is running on Android mobile device. * @@ -107,6 +112,16 @@ export function isSafari( userAgent ) { return userAgent.indexOf( ' applewebkit/' ) > -1 && userAgent.indexOf( 'chrome' ) === -1; } +/** + * Checks if User Agent represented by the string is running in Safari on iOS. + * + * @param {String} userAgent **Lowercase** `navigator.userAgent` string. + * @returns {Boolean} Whether User Agent is Safari running on iOS. + */ +export function isIOSSafari( userAgent ) { + return isSafari( userAgent ) && 'ontouchend' in document; +} + /** * Checks if User Agent represented by the string is Android mobile device. * From 21cc9c73fb0ea9cfd08141d2be9d26c280467d64 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 21 Oct 2021 15:13:25 +0200 Subject: [PATCH 03/10] Code refactoring. --- .../src/panel/balloon/balloonpanelview.js | 58 +++++++++---------- .../src/toolbar/balloon/balloontoolbar.js | 4 +- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js b/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js index d5fdb2164d6..fa6c6c1f36f 100644 --- a/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js +++ b/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js @@ -757,8 +757,8 @@ BalloonPanelView._getOptimalPosition = getOptimalPosition; * module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions */ BalloonPanelView.defaultPositions = generatePositions( { - arrowHorizontalOffset: BalloonPanelView.arrowHorizontalOffset, - arrowVerticalOffset: BalloonPanelView.arrowVerticalOffset, + horizontalOffset: BalloonPanelView.arrowHorizontalOffset, + verticalOffset: BalloonPanelView.arrowVerticalOffset, stickyVerticalOffset: BalloonPanelView.stickyVerticalOffset } ); @@ -768,20 +768,20 @@ BalloonPanelView.defaultPositions = generatePositions( { * @param {*} * @returns */ -export function generatePositions( { arrowHorizontalOffset, arrowVerticalOffset, stickyVerticalOffset, config = {} } ) { +export function generatePositions( { horizontalOffset, verticalOffset, stickyVerticalOffset, config = {} } ) { return { // ------- North west northWestArrowSouthWest: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left - arrowHorizontalOffset, + left: targetRect.left - horizontalOffset, name: 'arrow_sw', config } ), northWestArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left - ( balloonRect.width * .25 ) - arrowHorizontalOffset, + left: targetRect.left - ( balloonRect.width * .25 ) - horizontalOffset, name: 'arrow_smw', config } ), @@ -795,14 +795,14 @@ export function generatePositions( { arrowHorizontalOffset, arrowVerticalOffset, northWestArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left - ( balloonRect.width * .75 ) + arrowHorizontalOffset, + left: targetRect.left - ( balloonRect.width * .75 ) + horizontalOffset, name: 'arrow_sme', config } ), northWestArrowSouthEast: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left - balloonRect.width + arrowHorizontalOffset, + left: targetRect.left - balloonRect.width + horizontalOffset, name: 'arrow_se', config } ), @@ -811,14 +811,14 @@ export function generatePositions( { arrowHorizontalOffset, arrowVerticalOffset, northArrowSouthWest: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - arrowHorizontalOffset, + left: targetRect.left + targetRect.width / 2 - horizontalOffset, name: 'arrow_sw', config } ), northArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * .25 ) - arrowHorizontalOffset, + left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * .25 ) - horizontalOffset, name: 'arrow_smw', config } ), @@ -832,14 +832,14 @@ export function generatePositions( { arrowHorizontalOffset, arrowVerticalOffset, northArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * .75 ) + arrowHorizontalOffset, + left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * .75 ) + horizontalOffset, name: 'arrow_sme', config } ), northArrowSouthEast: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - balloonRect.width + arrowHorizontalOffset, + left: targetRect.left + targetRect.width / 2 - balloonRect.width + horizontalOffset, name: 'arrow_se', config } ), @@ -848,14 +848,14 @@ export function generatePositions( { arrowHorizontalOffset, arrowVerticalOffset, northEastArrowSouthWest: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), - left: targetRect.right - arrowHorizontalOffset, + left: targetRect.right - horizontalOffset, name: 'arrow_sw', config } ), northEastArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), - left: targetRect.right - ( balloonRect.width * .25 ) - arrowHorizontalOffset, + left: targetRect.right - ( balloonRect.width * .25 ) - horizontalOffset, name: 'arrow_smw', config } ), @@ -869,14 +869,14 @@ export function generatePositions( { arrowHorizontalOffset, arrowVerticalOffset, northEastArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), - left: targetRect.right - ( balloonRect.width * .75 ) + arrowHorizontalOffset, + left: targetRect.right - ( balloonRect.width * .75 ) + horizontalOffset, name: 'arrow_sme', config } ), northEastArrowSouthEast: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), - left: targetRect.right - balloonRect.width + arrowHorizontalOffset, + left: targetRect.right - balloonRect.width + horizontalOffset, name: 'arrow_se', config } ), @@ -885,14 +885,14 @@ export function generatePositions( { arrowHorizontalOffset, arrowVerticalOffset, southWestArrowNorthWest: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left - arrowHorizontalOffset, + left: targetRect.left - horizontalOffset, name: 'arrow_nw', config } ), southWestArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left - ( balloonRect.width * .25 ) - arrowHorizontalOffset, + left: targetRect.left - ( balloonRect.width * .25 ) - horizontalOffset, name: 'arrow_nmw', config } ), @@ -906,14 +906,14 @@ export function generatePositions( { arrowHorizontalOffset, arrowVerticalOffset, southWestArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left - ( balloonRect.width * .75 ) + arrowHorizontalOffset, + left: targetRect.left - ( balloonRect.width * .75 ) + horizontalOffset, name: 'arrow_nme', config } ), southWestArrowNorthEast: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left - balloonRect.width + arrowHorizontalOffset, + left: targetRect.left - balloonRect.width + horizontalOffset, name: 'arrow_ne', config } ), @@ -922,13 +922,13 @@ export function generatePositions( { arrowHorizontalOffset, arrowVerticalOffset, southArrowNorthWest: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - arrowHorizontalOffset, + left: targetRect.left + targetRect.width / 2 - horizontalOffset, name: 'arrow_nw', config } ), southArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * 0.25 ) - arrowHorizontalOffset, + left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * 0.25 ) - horizontalOffset, name: 'arrow_nmw', config } ), @@ -942,14 +942,14 @@ export function generatePositions( { arrowHorizontalOffset, arrowVerticalOffset, southArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * 0.75 ) + arrowHorizontalOffset, + left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * 0.75 ) + horizontalOffset, name: 'arrow_nme', config } ), southArrowNorthEast: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - balloonRect.width + arrowHorizontalOffset, + left: targetRect.left + targetRect.width / 2 - balloonRect.width + horizontalOffset, name: 'arrow_ne', config } ), @@ -958,14 +958,14 @@ export function generatePositions( { arrowHorizontalOffset, arrowVerticalOffset, southEastArrowNorthWest: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), - left: targetRect.right - arrowHorizontalOffset, + left: targetRect.right - horizontalOffset, name: 'arrow_nw', config } ), southEastArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), - left: targetRect.right - ( balloonRect.width * .25 ) - arrowHorizontalOffset, + left: targetRect.right - ( balloonRect.width * .25 ) - horizontalOffset, name: 'arrow_nmw', config } ), @@ -979,14 +979,14 @@ export function generatePositions( { arrowHorizontalOffset, arrowVerticalOffset, southEastArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), - left: targetRect.right - ( balloonRect.width * .75 ) + arrowHorizontalOffset, + left: targetRect.right - ( balloonRect.width * .75 ) + horizontalOffset, name: 'arrow_nme', config } ), southEastArrowNorthEast: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), - left: targetRect.right - balloonRect.width + arrowHorizontalOffset, + left: targetRect.right - balloonRect.width + horizontalOffset, name: 'arrow_ne', config } ), @@ -1016,7 +1016,7 @@ export function generatePositions( { arrowHorizontalOffset, arrowVerticalOffset, // @param {utils/dom/rect~Rect} elementRect A rect of the balloon. // @returns {Number} function getNorthTop( targetRect, balloonRect ) { - return targetRect.top - balloonRect.height - arrowVerticalOffset; + return targetRect.top - balloonRect.height - verticalOffset; } // Returns the top coordinate for positions starting with `south*`. @@ -1026,6 +1026,6 @@ export function generatePositions( { arrowHorizontalOffset, arrowVerticalOffset, // @param {utils/dom/rect~Rect} elementRect A rect of the balloon. // @returns {Number} function getSouthTop( targetRect ) { - return targetRect.bottom + arrowVerticalOffset; + return targetRect.bottom + verticalOffset; } } diff --git a/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js b/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js index 2c1c70322dc..3b22a2ecfe2 100644 --- a/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js +++ b/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js @@ -358,8 +358,8 @@ export default class BalloonToolbar extends Plugin { */ _getBalloonPositions( isBackward ) { const generatedPositions = generatePositions( { - arrowHorizontalOffset: 0, - arrowVerticalOffset: env.isIOSSafari ? ( 35 / window.visualViewport.scale ) : BalloonPanelView.arrowVerticalOffset, + horizontalOffset: env.isIOSSafari ? 0 : BalloonPanelView.arrowHorizontalOffset, + verticalOffset: env.isIOSSafari ? ( 35 / window.visualViewport.scale ) : BalloonPanelView.arrowVerticalOffset, stickyVerticalOffset: BalloonPanelView.stickyVerticalOffset, config: { withArrow: !env.isIOSSafari From 634a27670e2a95d103b26332322cef686d961a35 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 26 Oct 2021 13:29:38 +0200 Subject: [PATCH 04/10] Moved the RectDrawer dev util to ckeditor5-utils. --- packages/ckeditor5-minimap/src/minimap.js | 4 +- packages/ckeditor5-minimap/src/utils.js | 121 ---------------- .../tests/_utils-tests/rectdrawer.js | 134 ++++++++++++++++++ .../tests/_utils/rectdrawer.js | 126 ++++++++++++++++ 4 files changed, 262 insertions(+), 123 deletions(-) create mode 100644 packages/ckeditor5-utils/tests/_utils-tests/rectdrawer.js create mode 100644 packages/ckeditor5-utils/tests/_utils/rectdrawer.js diff --git a/packages/ckeditor5-minimap/src/minimap.js b/packages/ckeditor5-minimap/src/minimap.js index b2305012438..946c62c1e26 100644 --- a/packages/ckeditor5-minimap/src/minimap.js +++ b/packages/ckeditor5-minimap/src/minimap.js @@ -11,8 +11,6 @@ import { Plugin } from 'ckeditor5/src/core'; import { global } from 'ckeditor5/src/utils'; import MinimapView from './minimapview'; import { - // @if CK_DEBUG_MINIMAP // RectDrawer, - cloneEditingViewDomRoot, getClientHeight, getDomElementRect, @@ -21,6 +19,8 @@ import { findClosestScrollableAncestor } from './utils'; +// @if CK_DEBUG_MINIMAP // import RectDrawer from '@ckeditor/ckeditor5-utils/tests/_utils/rectdrawer'; + import '../theme/minimap.css'; /** diff --git a/packages/ckeditor5-minimap/src/utils.js b/packages/ckeditor5-minimap/src/utils.js index ee1ac83509d..52a67fe2cec 100644 --- a/packages/ckeditor5-minimap/src/utils.js +++ b/packages/ckeditor5-minimap/src/utils.js @@ -140,124 +140,3 @@ export function findClosestScrollableAncestor( domElement ) { return domElement; } - -/** - * A helper class that makes it possible to visualize {@link module:utils/dom/rect~Rect rect objects}. - * - * TODO: Move this class to shared utils. - * - * @protected - */ -export class RectDrawer { - /** - * Draws a rect object on the screen. - * - * const rect = new Rect( domElement ); - * - * // Simple usage. - * RectDrawer.draw( rect ); - * - * // With custom styles. - * RectDrawer.draw( rect, { outlineWidth: '3px', opacity: '.8' } ); - * - * // With custom styles and a name. - * RectDrawer.draw( rect, { outlineWidth: '3px', opacity: '.8' }, 'Main element' ); - * - * **Note**: In most cases, drawing a rect should be preceded by {@link module:minimap/utils~RectDrawer.clear}. - * - * @static - * @param {module:utils/dom/rect~Rect} rect The rect to be drawn. - * @param {Object} [userStyles] An optional object with custom styles for the rect. - * @param {String} [name] The optional name of the rect. - */ - static draw( rect, userStyles = {}, name ) { - if ( !RectDrawer._stylesElement ) { - RectDrawer._injectStyles(); - } - - const element = global.document.createElement( 'div' ); - - const rectGeometryStyles = { - top: `${ rect.top }px`, - left: `${ rect.left }px`, - width: `${ rect.width }px`, - height: `${ rect.height }px` - }; - - Object.assign( element.style, RectDrawer._defaultStyles, rectGeometryStyles, userStyles ); - - element.classList.add( 'ck-rect-preview' ); - - if ( name ) { - element.dataset.name = name; - } - - global.document.body.appendChild( element ); - - this._domElements.push( element ); - } - - /** - * Clears all previously {@link module:minimap/utils~RectDrawer.draw drawn} rects. - * - * @static - */ - static clear() { - for ( const element of this._domElements ) { - element.remove(); - } - - this._domElements.length = 0; - } - - /** - * @private - * @static - */ - static _injectStyles() { - RectDrawer._stylesElement = global.document.createElement( 'style' ); - RectDrawer._stylesElement.innerHTML = ` - div.ck-rect-preview::after { - content: attr(data-name); - position: absolute; - left: 3px; - top: 3px; - font-family: monospace; - background: #000; - color: #fff; - font-size: 10px; - padding: 1px 3px; - pointer-events: none; - } - `; - - global.document.head.appendChild( RectDrawer._stylesElement ); - } -} - -/** - * @private - * @member {Object} module:minimap/utils~RectDrawer._defaultStyles - */ -RectDrawer._defaultStyles = { - position: 'fixed', - outlineWidth: '1px', - outlineStyle: 'solid', - outlineColor: 'blue', - outlineOffset: '-1px', - zIndex: 999, - opacity: .5, - pointerEvents: 'none' -}; - -/** - * @private - * @member {Array.} module:minimap/utils~RectDrawer._domElements - */ -RectDrawer._domElements = []; - -/** - * @private - * @member {HTMLElement|null} module:minimap/utils~RectDrawer._stylesElement - */ -RectDrawer._stylesElement = null; diff --git a/packages/ckeditor5-utils/tests/_utils-tests/rectdrawer.js b/packages/ckeditor5-utils/tests/_utils-tests/rectdrawer.js new file mode 100644 index 00000000000..d995fb86062 --- /dev/null +++ b/packages/ckeditor5-utils/tests/_utils-tests/rectdrawer.js @@ -0,0 +1,134 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* global document */ + +import Rect from '../../src/dom/rect'; +import createElement from '../../src/dom/createelement'; +import RectDrawer from '../../tests/_utils/rectdrawer'; + +const DEFAULT_STYLES = 'position: fixed; ' + + 'outline: blue solid 1px; ' + + 'outline-offset: -1px; ' + + 'z-index: 999; ' + + 'opacity: 0.5; ' + + 'pointer-events: none; '; + +describe( 'utils', () => { + describe( 'RectDrawer', () => { + describe( 'draw()', () => { + afterEach( () => { + RectDrawer.clear(); + } ); + + it( 'should draw a Rect', () => { + const domElement = createElement( document, 'div' ); + + Object.assign( domElement.style, { + top: '123px', + left: '456px', + position: 'absolute', + width: '100px', + height: '100px' + } ); + + document.body.appendChild( domElement ); + + const rect = new Rect( domElement ); + + RectDrawer.draw( rect ); + + domElement.remove(); + + const rectPreview = document.querySelector( '.ck-rect-drawer-preview' ); + + expect( rectPreview.outerHTML ).to.equal( + '
' + + '
' + ); + } ); + + it( 'should draw a Rect with custom styles', () => { + const domElement = createElement( document, 'div' ); + + Object.assign( domElement.style, { + top: '123px', + left: '456px', + position: 'absolute', + width: '100px', + height: '100px' + } ); + + document.body.appendChild( domElement ); + + const rect = new Rect( domElement ); + + RectDrawer.draw( rect, { border: '1px solid red' } ); + + domElement.remove(); + + const rectPreview = document.querySelector( '.ck-rect-drawer-preview' ); + + expect( rectPreview.outerHTML ).to.equal( + '
' + + '
' + ); + } ); + + it( 'should draw a Rect with a name', () => { + const domElement = createElement( document, 'div' ); + + Object.assign( domElement.style, { + top: '123px', + left: '456px', + position: 'absolute', + width: '100px', + height: '100px' + } ); + + document.body.appendChild( domElement ); + + const rect = new Rect( domElement ); + + RectDrawer.draw( rect, null, 'foo' ); + + domElement.remove(); + + const rectPreview = document.querySelector( '.ck-rect-drawer-preview' ); + + expect( rectPreview.outerHTML ).to.equal( + '
' + + '
' + ); + } ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-utils/tests/_utils/rectdrawer.js b/packages/ckeditor5-utils/tests/_utils/rectdrawer.js new file mode 100644 index 00000000000..e6878933d0a --- /dev/null +++ b/packages/ckeditor5-utils/tests/_utils/rectdrawer.js @@ -0,0 +1,126 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import global from '../../src/dom/global'; + +/** + * A helper class that makes it possible to visualize {@link module:utils/dom/rect~Rect rect objects}. + */ +export default class RectDrawer { + /** + * Draws a rect object on the screen. + * + * const rect = new Rect( domElement ); + * + * // Simple usage. + * RectDrawer.draw( rect ); + * + * // With custom styles. + * RectDrawer.draw( rect, { outlineWidth: '3px', opacity: '.8' } ); + * + * // With custom styles and a name. + * RectDrawer.draw( rect, { outlineWidth: '3px', opacity: '.8' }, 'Main element' ); + * + * **Note**: In most cases, drawing a rect should be preceded by {@link module:minimap/utils~RectDrawer.clear}. + * + * @static + * @param {module:utils/dom/rect~Rect} rect The rect to be drawn. + * @param {Object} [userStyles] An optional object with custom styles for the rect. + * @param {String} [name] The optional name of the rect. + */ + static draw( rect, userStyles = {}, name ) { + if ( !RectDrawer._stylesElement ) { + RectDrawer._injectStyles(); + } + + const element = global.document.createElement( 'div' ); + + // Make it work when the browser viewport is zoomed in (mainly on mobiles). + const { offsetLeft, offsetTop } = global.window.visualViewport; + + const rectGeometryStyles = { + top: `${ rect.top + offsetTop }px`, + left: `${ rect.left + offsetLeft }px`, + width: `${ rect.width }px`, + height: `${ rect.height }px` + }; + + Object.assign( element.style, RectDrawer._defaultStyles, rectGeometryStyles, userStyles ); + + element.classList.add( 'ck-rect-drawer-preview' ); + + if ( name ) { + element.dataset.name = name; + } + + global.document.body.appendChild( element ); + + this._domElements.push( element ); + } + + /** + * Clears all previously {@link module:minimap/utils~RectDrawer.draw drawn} rects. + * + * @static + */ + static clear() { + for ( const element of this._domElements ) { + element.remove(); + } + + this._domElements.length = 0; + } + + /** + * @private + * @static + */ + static _injectStyles() { + RectDrawer._stylesElement = global.document.createElement( 'style' ); + RectDrawer._stylesElement.innerHTML = ` + div.ck-rect-drawer-preview[data-name]::after { + content: attr(data-name); + position: absolute; + left: 3px; + top: 3px; + font-family: monospace; + background: #000; + color: #fff; + font-size: 9px; + padding: 1px 3px; + pointer-events: none; + } + `; + + global.document.head.appendChild( RectDrawer._stylesElement ); + } +} + +/** + * @private + * @member {Object} module:minimap/utils~RectDrawer._defaultStyles + */ +RectDrawer._defaultStyles = { + position: 'fixed', + outlineWidth: '1px', + outlineStyle: 'solid', + outlineColor: 'blue', + outlineOffset: '-1px', + zIndex: 999, + opacity: .5, + pointerEvents: 'none' +}; + +/** + * @private + * @member {Array.} module:minimap/utils~RectDrawer._domElements + */ +RectDrawer._domElements = []; + +/** + * @private + * @member {HTMLElement|null} module:minimap/utils~RectDrawer._stylesElement + */ +RectDrawer._stylesElement = null; From b3fb2e8ab97c55ed461e7d2c8d58c47575cca33c Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 26 Oct 2021 14:43:12 +0200 Subject: [PATCH 05/10] Better iOS/Safari detection. Limited arrow-less balloon positions for better UX. --- .../src/panel/balloon/balloonpanelview.js | 17 ++-- .../src/toolbar/balloon/balloontoolbar.js | 83 ++++++++----------- packages/ckeditor5-utils/src/env.js | 17 ++-- 3 files changed, 55 insertions(+), 62 deletions(-) diff --git a/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js b/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js index fa6c6c1f36f..df596f53a45 100644 --- a/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js +++ b/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js @@ -756,11 +756,7 @@ BalloonPanelView._getOptimalPosition = getOptimalPosition; * @member {Object.} * module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions */ -BalloonPanelView.defaultPositions = generatePositions( { - horizontalOffset: BalloonPanelView.arrowHorizontalOffset, - verticalOffset: BalloonPanelView.arrowVerticalOffset, - stickyVerticalOffset: BalloonPanelView.stickyVerticalOffset -} ); +BalloonPanelView.defaultPositions = generatePositions(); /** * TODO @@ -768,7 +764,12 @@ BalloonPanelView.defaultPositions = generatePositions( { * @param {*} * @returns */ -export function generatePositions( { horizontalOffset, verticalOffset, stickyVerticalOffset, config = {} } ) { +export function generatePositions( { + horizontalOffset = BalloonPanelView.arrowHorizontalOffset, + verticalOffset = BalloonPanelView.arrowVerticalOffset, + stickyVerticalOffset = BalloonPanelView.stickyVerticalOffset, + config = {} +} = {} ) { return { // ------- North west @@ -1002,9 +1003,9 @@ export function generatePositions( { horizontalOffset, verticalOffset, stickyVer top: viewportRect.top + stickyVerticalOffset, left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2, name: 'arrowless', - config: { + config: Object.assign( { withArrow: false - } + } ) }; } }; diff --git a/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js b/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js index 3b22a2ecfe2..c18fdd48c7d 100644 --- a/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js +++ b/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js @@ -3,8 +3,6 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* global window */ - /** * @module ui/toolbar/balloon/balloontoolbar */ @@ -19,7 +17,7 @@ import normalizeToolbarConfig from '../normalizetoolbarconfig'; import { debounce } from 'lodash-es'; import ResizeObserver from '@ckeditor/ckeditor5-utils/src/dom/resizeobserver'; import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit'; -import { env } from '@ckeditor/ckeditor5-utils'; +import { env, global } from '@ckeditor/ckeditor5-utils'; const toPx = toUnit( 'px' ); @@ -357,52 +355,43 @@ export default class BalloonToolbar extends Plugin { * @returns {Array.} */ _getBalloonPositions( isBackward ) { - const generatedPositions = generatePositions( { - horizontalOffset: env.isIOSSafari ? 0 : BalloonPanelView.arrowHorizontalOffset, - verticalOffset: env.isIOSSafari ? ( 35 / window.visualViewport.scale ) : BalloonPanelView.arrowVerticalOffset, - stickyVerticalOffset: BalloonPanelView.stickyVerticalOffset, - config: { - withArrow: !env.isIOSSafari - } - } ); - - if ( env.isIOSSafari ) { - return isBackward ? [ - generatedPositions.northArrowSouth, - generatedPositions.northArrowSouth, - generatedPositions.northEastArrowSouthEast, - generatedPositions.northWestArrowSouthWest - ] : [ - generatedPositions.southArrowNorth, - generatedPositions.southArrowNorth, - generatedPositions.southWestArrowNorthWest, - generatedPositions.southEastArrowNorthEast - ]; + const isSafariIniOS = env.isSafari && env.isiOS; + let generatedPositions; + + if ( isSafariIniOS ) { + generatedPositions = generatePositions( { + // 20px when zoomed out. + // Less when zoomed in. + // No less than the default v-offsset, though. + verticalOffset: Math.max( BalloonPanelView.arrowVerticalOffset, 20 / global.window.visualViewport.scale ) + } ); } else { - return isBackward ? [ - generatedPositions.northWestArrowSouth, - generatedPositions.northWestArrowSouthWest, - generatedPositions.northWestArrowSouthEast, - generatedPositions.northWestArrowSouthMiddleEast, - generatedPositions.northWestArrowSouthMiddleWest, - generatedPositions.southWestArrowNorth, - generatedPositions.southWestArrowNorthWest, - generatedPositions.southWestArrowNorthEast, - generatedPositions.southWestArrowNorthMiddleWest, - generatedPositions.southWestArrowNorthMiddleEast - ] : [ - generatedPositions.southEastArrowNorth, - generatedPositions.southEastArrowNorthEast, - generatedPositions.southEastArrowNorthWest, - generatedPositions.southEastArrowNorthMiddleEast, - generatedPositions.southEastArrowNorthMiddleWest, - generatedPositions.northEastArrowSouth, - generatedPositions.northEastArrowSouthEast, - generatedPositions.northEastArrowSouthWest, - generatedPositions.northEastArrowSouthMiddleEast, - generatedPositions.northEastArrowSouthMiddleWest - ]; + generatedPositions = generatePositions(); } + + return isBackward ? [ + generatedPositions.northWestArrowSouth, + generatedPositions.northWestArrowSouthWest, + generatedPositions.northWestArrowSouthEast, + generatedPositions.northWestArrowSouthMiddleEast, + generatedPositions.northWestArrowSouthMiddleWest, + generatedPositions.southWestArrowNorth, + generatedPositions.southWestArrowNorthWest, + generatedPositions.southWestArrowNorthEast, + generatedPositions.southWestArrowNorthMiddleWest, + generatedPositions.southWestArrowNorthMiddleEast + ] : [ + generatedPositions.southEastArrowNorth, + generatedPositions.southEastArrowNorthEast, + generatedPositions.southEastArrowNorthWest, + generatedPositions.southEastArrowNorthMiddleEast, + generatedPositions.southEastArrowNorthMiddleWest, + generatedPositions.northEastArrowSouth, + generatedPositions.northEastArrowSouthEast, + generatedPositions.northEastArrowSouthWest, + generatedPositions.northEastArrowSouthMiddleEast, + generatedPositions.northEastArrowSouthMiddleWest + ]; } } diff --git a/packages/ckeditor5-utils/src/env.js b/packages/ckeditor5-utils/src/env.js index b7a2f262778..903075c929d 100644 --- a/packages/ckeditor5-utils/src/env.js +++ b/packages/ckeditor5-utils/src/env.js @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* globals navigator:false, document */ +/* globals navigator:false */ /** * @module utils/env @@ -42,9 +42,12 @@ const env = { isSafari: isSafari( userAgent ), /** - * TODO + * Indicates the the application is running in iOS. + * + * @static + * @type {Boolean} */ - isIOSSafari: isIOSSafari( userAgent ), + isiOS: isiOS( userAgent ), /** * Indicates that the application is running on Android mobile device. @@ -113,13 +116,13 @@ export function isSafari( userAgent ) { } /** - * Checks if User Agent represented by the string is running in Safari on iOS. + * Checks if User Agent represented by the string is running in iOS. * * @param {String} userAgent **Lowercase** `navigator.userAgent` string. - * @returns {Boolean} Whether User Agent is Safari running on iOS. + * @returns {Boolean} Whether User Agent is running in iOS or not. */ -export function isIOSSafari( userAgent ) { - return isSafari( userAgent ) && 'ontouchend' in document; +export function isiOS( userAgent ) { + return !!userAgent.match( /iphone|ipad/i ); } /** From 76d653e24d522be0d5b531075db6bfc594014f10 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 26 Oct 2021 16:44:47 +0200 Subject: [PATCH 06/10] Improved iOS detection. --- packages/ckeditor5-utils/src/env.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-utils/src/env.js b/packages/ckeditor5-utils/src/env.js index 903075c929d..304144ca938 100644 --- a/packages/ckeditor5-utils/src/env.js +++ b/packages/ckeditor5-utils/src/env.js @@ -122,7 +122,7 @@ export function isSafari( userAgent ) { * @returns {Boolean} Whether User Agent is running in iOS or not. */ export function isiOS( userAgent ) { - return !!userAgent.match( /iphone|ipad/i ); + return !!userAgent.match( /iphone|ipad/i ) || ( isMac( userAgent ) && navigator.maxTouchPoints > 0 ); } /** From afd3de59aac207ef73996297db9124aa9ed8d921 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 26 Oct 2021 16:51:34 +0200 Subject: [PATCH 07/10] Tests: Reverted changes in tests. --- packages/ckeditor5-editor-balloon/tests/manual/ballooneditor.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor.js b/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor.js index caef3ff4dfd..fad69b090f1 100644 --- a/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor.js +++ b/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor.js @@ -66,5 +66,3 @@ function destroyEditors() { document.getElementById( 'initEditors' ).addEventListener( 'click', initEditors ); document.getElementById( 'destroyEditors' ).addEventListener( 'click', destroyEditors ); - -initEditors(); From 3fe0f09894ba054a011ac06efe821fe56fc1ef12 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 27 Oct 2021 10:30:35 +0200 Subject: [PATCH 08/10] Docs. --- packages/ckeditor5-utils/src/env.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ckeditor5-utils/src/env.js b/packages/ckeditor5-utils/src/env.js index 304144ca938..731c4181406 100644 --- a/packages/ckeditor5-utils/src/env.js +++ b/packages/ckeditor5-utils/src/env.js @@ -122,6 +122,7 @@ export function isSafari( userAgent ) { * @returns {Boolean} Whether User Agent is running in iOS or not. */ export function isiOS( userAgent ) { + // "Request mobile site" || "Request desktop site". return !!userAgent.match( /iphone|ipad/i ) || ( isMac( userAgent ) && navigator.maxTouchPoints > 0 ); } From 59611b5c6c51f9c411b98cb15b7a3ada0a392ea4 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 2 Nov 2021 14:48:10 +0100 Subject: [PATCH 09/10] Tests, docs, and code refactoring. --- .../src/panel/balloon/balloonpanelview.js | 93 +++++++------ .../src/toolbar/balloon/balloontoolbar.js | 62 +++++---- .../tests/panel/balloon/balloonpanelview.js | 124 +++++++++++++++++- .../tests/toolbar/balloon/balloontoolbar.js | 87 ++++++++++++ packages/ckeditor5-utils/tests/env.js | 61 ++++++++- 5 files changed, 356 insertions(+), 71 deletions(-) diff --git a/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js b/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js index df596f53a45..99c423bf080 100644 --- a/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js +++ b/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js @@ -750,6 +750,8 @@ BalloonPanelView._getOptimalPosition = getOptimalPosition; * * Positioning functions must be compatible with {@link module:utils/dom/position~Position}. * + * Default positioning functions with customized offsets can be generated using {@link module:utils/dom/position~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. * @@ -759,16 +761,31 @@ BalloonPanelView._getOptimalPosition = getOptimalPosition; BalloonPanelView.defaultPositions = generatePositions(); /** - * TODO + * Returns available {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView} + * {@link module:utils/dom/position~positioningFunction positioning functions} adjusted by the specific offsets. * - * @param {*} - * @returns + * @protected + * @param {Object} [options] Options to generate positions. If not specified, this helper will simply return + * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}. + * @param {Number} [options.horizontalOffset] A custom horizontal offset (in pixels) of each position. If + * not specified, {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.arrowHorizontalOffset the default value} + * will be used. + * @param {Number} [options.verticalOffset] A custom vertical offset (in pixels) of each position. If + * not specified, {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.arrowVerticalOffset the default value} + * will be used. + * @param {Number} [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 {Object} [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}. + * @returns {Object.} */ export function generatePositions( { horizontalOffset = BalloonPanelView.arrowHorizontalOffset, verticalOffset = BalloonPanelView.arrowVerticalOffset, stickyVerticalOffset = BalloonPanelView.stickyVerticalOffset, - config = {} + config } = {} ) { return { // ------- North west @@ -777,35 +794,35 @@ export function generatePositions( { top: getNorthTop( targetRect, balloonRect ), left: targetRect.left - horizontalOffset, name: 'arrow_sw', - config + ...( config && { config } ) } ), northWestArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), left: targetRect.left - ( balloonRect.width * .25 ) - horizontalOffset, name: 'arrow_smw', - config + ...( config && { config } ) } ), northWestArrowSouth: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), left: targetRect.left - balloonRect.width / 2, name: 'arrow_s', - config + ...( config && { config } ) } ), northWestArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), left: targetRect.left - ( balloonRect.width * .75 ) + horizontalOffset, name: 'arrow_sme', - config + ...( config && { config } ) } ), northWestArrowSouthEast: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), left: targetRect.left - balloonRect.width + horizontalOffset, name: 'arrow_se', - config + ...( config && { config } ) } ), // ------- North @@ -814,35 +831,35 @@ export function generatePositions( { top: getNorthTop( targetRect, balloonRect ), left: targetRect.left + targetRect.width / 2 - horizontalOffset, name: 'arrow_sw', - config + ...( config && { config } ) } ), northArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * .25 ) - horizontalOffset, name: 'arrow_smw', - config + ...( config && { config } ) } ), northArrowSouth: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2, name: 'arrow_s', - config + ...( config && { config } ) } ), northArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * .75 ) + horizontalOffset, name: 'arrow_sme', - config + ...( config && { config } ) } ), northArrowSouthEast: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), left: targetRect.left + targetRect.width / 2 - balloonRect.width + horizontalOffset, name: 'arrow_se', - config + ...( config && { config } ) } ), // ------- North east @@ -851,35 +868,35 @@ export function generatePositions( { top: getNorthTop( targetRect, balloonRect ), left: targetRect.right - horizontalOffset, name: 'arrow_sw', - config + ...( config && { config } ) } ), northEastArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), left: targetRect.right - ( balloonRect.width * .25 ) - horizontalOffset, name: 'arrow_smw', - config + ...( config && { config } ) } ), northEastArrowSouth: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), left: targetRect.right - balloonRect.width / 2, name: 'arrow_s', - config + ...( config && { config } ) } ), northEastArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), left: targetRect.right - ( balloonRect.width * .75 ) + horizontalOffset, name: 'arrow_sme', - config + ...( config && { config } ) } ), northEastArrowSouthEast: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), left: targetRect.right - balloonRect.width + horizontalOffset, name: 'arrow_se', - config + ...( config && { config } ) } ), // ------- South west @@ -888,35 +905,35 @@ export function generatePositions( { top: getSouthTop( targetRect, balloonRect ), left: targetRect.left - horizontalOffset, name: 'arrow_nw', - config + ...( config && { config } ) } ), southWestArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), left: targetRect.left - ( balloonRect.width * .25 ) - horizontalOffset, name: 'arrow_nmw', - config + ...( config && { config } ) } ), southWestArrowNorth: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), left: targetRect.left - balloonRect.width / 2, name: 'arrow_n', - config + ...( config && { config } ) } ), southWestArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), left: targetRect.left - ( balloonRect.width * .75 ) + horizontalOffset, name: 'arrow_nme', - config + ...( config && { config } ) } ), southWestArrowNorthEast: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), left: targetRect.left - balloonRect.width + horizontalOffset, name: 'arrow_ne', - config + ...( config && { config } ) } ), // ------- South @@ -925,34 +942,35 @@ export function generatePositions( { top: getSouthTop( targetRect, balloonRect ), left: targetRect.left + targetRect.width / 2 - horizontalOffset, name: 'arrow_nw', - config + ...( config && { config } ) } ), + southArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * 0.25 ) - horizontalOffset, name: 'arrow_nmw', - config + ...( config && { config } ) } ), southArrowNorth: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2, name: 'arrow_n', - config + ...( config && { config } ) } ), southArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * 0.75 ) + horizontalOffset, name: 'arrow_nme', - config + ...( config && { config } ) } ), southArrowNorthEast: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), left: targetRect.left + targetRect.width / 2 - balloonRect.width + horizontalOffset, name: 'arrow_ne', - config + ...( config && { config } ) } ), // ------- South east @@ -961,35 +979,35 @@ export function generatePositions( { top: getSouthTop( targetRect, balloonRect ), left: targetRect.right - horizontalOffset, name: 'arrow_nw', - config + ...( config && { config } ) } ), southEastArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), left: targetRect.right - ( balloonRect.width * .25 ) - horizontalOffset, name: 'arrow_nmw', - config + ...( config && { config } ) } ), southEastArrowNorth: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), left: targetRect.right - balloonRect.width / 2, name: 'arrow_n', - config + ...( config && { config } ) } ), southEastArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), left: targetRect.right - ( balloonRect.width * .75 ) + horizontalOffset, name: 'arrow_nme', - config + ...( config && { config } ) } ), southEastArrowNorthEast: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), left: targetRect.right - balloonRect.width + horizontalOffset, name: 'arrow_ne', - config + ...( config && { config } ) } ), // ------- Sticky @@ -1003,9 +1021,10 @@ export function generatePositions( { top: viewportRect.top + stickyVerticalOffset, left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2, name: 'arrowless', - config: Object.assign( { - withArrow: false - } ) + config: { + withArrow: false, + ...config + } }; } }; diff --git a/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js b/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js index c18fdd48c7d..c9e08a1b09b 100644 --- a/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js +++ b/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js @@ -356,41 +356,39 @@ export default class BalloonToolbar extends Plugin { */ _getBalloonPositions( isBackward ) { const isSafariIniOS = env.isSafari && env.isiOS; - let generatedPositions; - - if ( isSafariIniOS ) { - generatedPositions = generatePositions( { - // 20px when zoomed out. - // Less when zoomed in. - // No less than the default v-offsset, though. - verticalOffset: Math.max( BalloonPanelView.arrowVerticalOffset, 20 / global.window.visualViewport.scale ) - } ); - } else { - generatedPositions = generatePositions(); - } + + // https://github.com/ckeditor/ckeditor5/issues/7707 + const positions = isSafariIniOS ? generatePositions( { + // 20px when zoomed out. Less then 20px when zoomed in; the "radius" of the native selection handle gets + // smaller as the user zooms in. No less than the default v-offset, though. + verticalOffset: Math.max( + BalloonPanelView.arrowVerticalOffset, + Math.round( 20 / global.window.visualViewport.scale ) + ) + } ) : BalloonPanelView.defaultPositions; return isBackward ? [ - generatedPositions.northWestArrowSouth, - generatedPositions.northWestArrowSouthWest, - generatedPositions.northWestArrowSouthEast, - generatedPositions.northWestArrowSouthMiddleEast, - generatedPositions.northWestArrowSouthMiddleWest, - generatedPositions.southWestArrowNorth, - generatedPositions.southWestArrowNorthWest, - generatedPositions.southWestArrowNorthEast, - generatedPositions.southWestArrowNorthMiddleWest, - generatedPositions.southWestArrowNorthMiddleEast + positions.northWestArrowSouth, + positions.northWestArrowSouthWest, + positions.northWestArrowSouthEast, + positions.northWestArrowSouthMiddleEast, + positions.northWestArrowSouthMiddleWest, + positions.southWestArrowNorth, + positions.southWestArrowNorthWest, + positions.southWestArrowNorthEast, + positions.southWestArrowNorthMiddleWest, + positions.southWestArrowNorthMiddleEast ] : [ - generatedPositions.southEastArrowNorth, - generatedPositions.southEastArrowNorthEast, - generatedPositions.southEastArrowNorthWest, - generatedPositions.southEastArrowNorthMiddleEast, - generatedPositions.southEastArrowNorthMiddleWest, - generatedPositions.northEastArrowSouth, - generatedPositions.northEastArrowSouthEast, - generatedPositions.northEastArrowSouthWest, - generatedPositions.northEastArrowSouthMiddleEast, - generatedPositions.northEastArrowSouthMiddleWest + positions.southEastArrowNorth, + positions.southEastArrowNorthEast, + positions.southEastArrowNorthWest, + positions.southEastArrowNorthMiddleEast, + positions.southEastArrowNorthMiddleWest, + positions.northEastArrowSouth, + positions.northEastArrowSouthEast, + positions.northEastArrowSouthWest, + positions.northEastArrowSouthMiddleEast, + positions.northEastArrowSouthMiddleWest ]; } } diff --git a/packages/ckeditor5-ui/tests/panel/balloon/balloonpanelview.js b/packages/ckeditor5-ui/tests/panel/balloon/balloonpanelview.js index b48e5083b3d..6ea42bb2c67 100644 --- a/packages/ckeditor5-ui/tests/panel/balloon/balloonpanelview.js +++ b/packages/ckeditor5-ui/tests/panel/balloon/balloonpanelview.js @@ -6,7 +6,7 @@ /* global window, document, Event */ import ViewCollection from '../../../src/viewcollection'; -import BalloonPanelView from '../../../src/panel/balloon/balloonpanelview'; +import BalloonPanelView, { generatePositions } from '../../../src/panel/balloon/balloonpanelview'; import ButtonView from '../../../src/button/buttonview'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { Rect } from '@ckeditor/ckeditor5-utils'; @@ -1089,6 +1089,128 @@ describe( 'BalloonPanelView', () => { expect( positions.viewportStickyNorth( targetRect, balloonRect, viewportRect ) ).to.equal( null ); } ); } ); + + describe( 'generatePositions()', () => { + let defaultPositions, balloonRect, targetRect, viewportRect; + + beforeEach( () => { + defaultPositions = BalloonPanelView.defaultPositions; + + viewportRect = new Rect( { + top: 300, + bottom: 800, + left: 0, + right: 200, + width: 0, + height: 0 + } ); + + targetRect = new Rect( { + top: 200, + bottom: 400, + left: 50, + right: 100, + width: 0, + height: 0 + } ); + + balloonRect = new Rect( { + top: 0, + bottom: 0, + left: 0, + right: 0, + width: 50, + height: 50 + } ); + } ); + + it( 'should generate the same set of positions as BalloonPanelView#defaultPositions when no options specified', () => { + const generatedPositions = generatePositions(); + + for ( const name in generatedPositions ) { + const generatedResult = generatedPositions[ name ]( targetRect, balloonRect, viewportRect ); + const defaultResult = defaultPositions[ name ]( targetRect, balloonRect, viewportRect ); + + expect( generatedResult ).to.deep.equal( defaultResult, name ); + } + } ); + + it( 'should respect the "horizontalOffset" option', () => { + const generatedPositions = generatePositions( { + horizontalOffset: BalloonPanelView.arrowHorizontalOffset + 100 + } ); + + for ( const name in generatedPositions ) { + const generatedResult = generatedPositions[ name ]( targetRect, balloonRect, viewportRect ); + + if ( name.match( /Arrow(South|North)(.+)?East/ ) ) { + generatedResult.left -= 100; + } else if ( name.match( /Arrow(South|North)(.+)?West/ ) ) { + generatedResult.left += 100; + } + + const defaultResult = defaultPositions[ name ]( targetRect, balloonRect, viewportRect ); + + expect( generatedResult ).to.deep.equal( defaultResult, name ); + } + } ); + + it( 'should respect the "verticalOffset" option', () => { + const generatedPositions = generatePositions( { + verticalOffset: BalloonPanelView.arrowVerticalOffset + 100 + } ); + + for ( const name in generatedPositions ) { + const generatedResult = generatedPositions[ name ]( targetRect, balloonRect, viewportRect ); + + if ( name.match( /^south/ ) ) { + generatedResult.top -= 100; + } else if ( name.match( /^north/ ) ) { + generatedResult.top += 100; + } + + const defaultResult = defaultPositions[ name ]( targetRect, balloonRect, viewportRect ); + + expect( generatedResult ).to.deep.equal( defaultResult, name ); + } + } ); + + it( 'should respect the "stickyVerticalOffset" option', () => { + const generatedPositions = generatePositions( { + stickyVerticalOffset: BalloonPanelView.stickyVerticalOffset + 100 + } ); + + for ( const name in generatedPositions ) { + const generatedResult = generatedPositions[ name ]( targetRect, balloonRect, viewportRect ); + + if ( name.match( /sticky/i ) ) { + generatedResult.top -= 100; + } + + const defaultResult = defaultPositions[ name ]( targetRect, balloonRect, viewportRect ); + + expect( generatedResult ).to.deep.equal( defaultResult, name ); + } + } ); + + it( 'should respect the "config" option', () => { + const generatedPositions = generatePositions( { + config: { + foo: 'bar', + withArrow: true + } + } ); + + for ( const name in generatedPositions ) { + const generatedResult = generatedPositions[ name ]( targetRect, balloonRect, viewportRect ); + + expect( generatedResult.config ).to.deep.equal( { + foo: 'bar', + withArrow: true + }, name ); + } + } ); + } ); } ); function mockBoundingBox( element, data ) { diff --git a/packages/ckeditor5-ui/tests/toolbar/balloon/balloontoolbar.js b/packages/ckeditor5-ui/tests/toolbar/balloon/balloontoolbar.js index 873ad1f6bb4..446e2182c06 100644 --- a/packages/ckeditor5-ui/tests/toolbar/balloon/balloontoolbar.js +++ b/packages/ckeditor5-ui/tests/toolbar/balloon/balloontoolbar.js @@ -18,6 +18,7 @@ import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalli import TableEditing from '@ckeditor/ckeditor5-table/src/tableediting'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; import ResizeObserver from '@ckeditor/ckeditor5-utils/src/dom/resizeobserver'; +import env from '@ckeditor/ckeditor5-utils/src/env'; import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { stringify as viewStringify } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; @@ -455,6 +456,92 @@ describe( 'BalloonToolbar', () => { expect( balloonToolbar.toolbarView.maxWidth ).to.equal( expectedWidth ); } ); + + // https://github.com/ckeditor/ckeditor5/issues/7707 + describe( 'on iOS (avoiding the clash with native selection handles)', () => { + let targetRect, balloonRect; + + beforeEach( () => { + targetRect = new Rect( { + top: 200, + bottom: 400, + left: 50, + right: 100, + width: 0, + height: 0 + } ); + + balloonRect = new Rect( { + top: 0, + bottom: 0, + left: 0, + right: 0, + width: 50, + height: 50 + } ); + } ); + + it( 'should attach the balloon farther away', () => { + setData( model, 'b[a]r' ); + + balloonToolbar.show(); + + const defaultPositioningFunctions = balloonAddSpy.firstCall.args[ 0 ].position.positions; + + balloonToolbar.hide(); + + testUtils.sinon.stub( env, 'isSafari' ).get( () => true ); + testUtils.sinon.stub( env, 'isiOS' ).get( () => true ); + balloonToolbar.show(); + + const iOSPositioningFuctions = balloonAddSpy.secondCall.args[ 0 ].position.positions; + + defaultPositioningFunctions.forEach( ( defaultPositioningFunction, index ) => { + const defaultResult = defaultPositioningFunction( targetRect, balloonRect ); + const iOSResult = iOSPositioningFuctions[ index ]( targetRect, balloonRect ); + + // Default non-iOS offset is 10px. On iOS it is 20px/1, which is 20px. The difference is 10px. + if ( defaultResult.name.match( /^arrow_n/ ) ) { + defaultResult.top += 10; + } else if ( defaultResult.name.match( /^arrow_s/ ) ) { + defaultResult.top -= 10; + } + + expect( iOSResult ).to.deep.equal( defaultResult, index ); + } ); + } ); + + it( 'should change the distance depending on the scale of the visual viewport', () => { + setData( model, 'b[a]r' ); + + balloonToolbar.show(); + + const defaultPositioningFunctions = balloonAddSpy.firstCall.args[ 0 ].position.positions; + + balloonToolbar.hide(); + + testUtils.sinon.stub( global.window.visualViewport, 'scale' ).get( () => 0.5 ); + testUtils.sinon.stub( env, 'isSafari' ).get( () => true ); + testUtils.sinon.stub( env, 'isiOS' ).get( () => true ); + balloonToolbar.show(); + + const iOSPositioningFuctions = balloonAddSpy.secondCall.args[ 0 ].position.positions; + + defaultPositioningFunctions.forEach( ( defaultPositioningFunction, index ) => { + const defaultResult = defaultPositioningFunction( targetRect, balloonRect ); + const iOSResult = iOSPositioningFuctions[ index ]( targetRect, balloonRect ); + + // Default non-iOS offset is 10px. On iOS it is 20px/0.5, which is 40px. The difference is 30px. + if ( defaultResult.name.match( /^arrow_n/ ) ) { + defaultResult.top += 30; + } else if ( defaultResult.name.match( /^arrow_s/ ) ) { + defaultResult.top -= 30; + } + + expect( iOSResult ).to.deep.equal( defaultResult, index ); + } ); + } ); + } ); } ); describe( 'hide()', () => { diff --git a/packages/ckeditor5-utils/tests/env.js b/packages/ckeditor5-utils/tests/env.js index b21135a29ae..2db9aa8ca22 100644 --- a/packages/ckeditor5-utils/tests/env.js +++ b/packages/ckeditor5-utils/tests/env.js @@ -3,13 +3,20 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -import env, { isMac, isGecko, isSafari, isAndroid, isRegExpUnicodePropertySupported, isBlink } from '../src/env'; +import env, { + isMac, isGecko, isSafari, isiOS, isAndroid, isRegExpUnicodePropertySupported, isBlink +} from '../src/env'; + +import global from '../src/dom/global'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; function toLowerCase( str ) { return str.toLowerCase(); } describe( 'Env', () => { + testUtils.createSinonSandbox(); + it( 'is an object', () => { expect( env ).to.be.an( 'object' ); } ); @@ -32,6 +39,12 @@ describe( 'Env', () => { } ); } ); + describe( 'isiOS', () => { + it( 'is a boolean', () => { + expect( env.isiOS ).to.be.a( 'boolean' ); + } ); + } ); + describe( 'isAndroid', () => { it( 'is a boolean', () => { expect( env.isAndroid ).to.be.a( 'boolean' ); @@ -124,6 +137,52 @@ describe( 'Env', () => { /* eslint-enable max-len */ } ); + describe( 'isiOS()', () => { + /* eslint-disable max-len */ + it( 'returns true for Safari@iPhone UA string ("Request Mobile Website")', () => { + expect( isiOS( toLowerCase( + 'Mozilla/5.0 (iPhone; CPU OS 15_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Mobile/15E148 Safari/604.1' + ) ) ).to.be.true; + } ); + + it( 'returns true for Safari@iPad UA string ("Request Mobile Website")', () => { + expect( isiOS( toLowerCase( + 'Mozilla/5.0 (iPad; CPU OS 15_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Mobile/15E148 Safari/604.1' + ) ) ).to.be.true; + } ); + + it( 'returns true for Safari UA string ("Request Desktop Website")', () => { + // This is how you tell Safari@Mac from Safari@iOS. + testUtils.sinon.stub( global.window.navigator, 'maxTouchPoints' ).get( () => 3 ); + + expect( isiOS( toLowerCase( + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15' + ) ) ).to.be.true; + } ); + + it( 'returns true for Chrome UA string', () => { + expect( isiOS( toLowerCase( + 'Mozilla/5.0 (iPad; CPU OS 15_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/95.0.4638.50 Mobile/15E148 Safari/604.1' + ) ) ).to.be.true; + } ); + + it( 'returns false for non-iOS UA strings', () => { + // Safari on Mac + expect( isiOS( toLowerCase( + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15' + ) ) ).to.be.false; + + expect( isiOS( toLowerCase( + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0' + ) ) ).to.be.false; + + expect( isiOS( toLowerCase( + 'Mozilla/5.0 (Linux; Android 7.1; Mi A1 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36' + ) ) ).to.be.false; + } ); + /* eslint-enable max-len */ + } ); + describe( 'isAndroid()', () => { /* eslint-disable max-len */ it( 'returns true for Android UA strings', () => { From abd340e9cfece4f86ae6a602f324a8d5c0364516 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 2 Nov 2021 15:18:52 +0100 Subject: [PATCH 10/10] Docs: Fixed a wrong link. --- packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js b/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js index 99c423bf080..59823ac4afe 100644 --- a/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js +++ b/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js @@ -750,7 +750,8 @@ BalloonPanelView._getOptimalPosition = getOptimalPosition; * * Positioning functions must be compatible with {@link module:utils/dom/position~Position}. * - * Default positioning functions with customized offsets can be generated using {@link module:utils/dom/position~generatePositions}. + * Default positioning functions with customized offsets can be generated using + * {@link module:ui/panel/balloon/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.