diff --git a/package.json b/package.json index 1407bcab3c0..9647ddfdae6 100644 --- a/package.json +++ b/package.json @@ -168,7 +168,8 @@ "switch-to-dev-dev": "sh ./scripts/switch-to-dev-dev.sh", "clean-up-svg-icons": "sh ./scripts/clean-up-svg-icons.sh", "dll:build": "node ./scripts/dll/build-dlls.js", - "check-dependencies": "ckeditor5-dev-tests-check-dependencies" + "check-dependencies": "ckeditor5-dev-tests-check-dependencies", + "serve": "http-server ./" }, "lint-staged": { "**/*.js": [ diff --git a/packages/ckeditor5-image/src/image/ui/utils.js b/packages/ckeditor5-image/src/image/ui/utils.js index 30d80dc7fa1..8f5e8f9cc2c 100644 --- a/packages/ckeditor5-image/src/image/ui/utils.js +++ b/packages/ckeditor5-image/src/image/ui/utils.js @@ -47,7 +47,8 @@ export function getBalloonPositionData( editor ) { defaultPositions.northArrowSouthEast, defaultPositions.southArrowNorth, defaultPositions.southArrowNorthWest, - defaultPositions.southArrowNorthEast + defaultPositions.southArrowNorthEast, + defaultPositions.viewportStickyNorth ] }; } diff --git a/packages/ckeditor5-table/src/utils/ui/contextualballoon.js b/packages/ckeditor5-table/src/utils/ui/contextualballoon.js index 7b81d367624..ffcfba75f6f 100644 --- a/packages/ckeditor5-table/src/utils/ui/contextualballoon.js +++ b/packages/ckeditor5-table/src/utils/ui/contextualballoon.js @@ -7,7 +7,6 @@ * @module table/utils/ui/contextualballoon */ -import { centeredBalloonPositionForLongWidgets } from 'ckeditor5/src/widget'; import { Rect } from 'ckeditor5/src/utils'; import { BalloonPanelView } from 'ckeditor5/src/ui'; @@ -21,12 +20,8 @@ const BALLOON_POSITIONS = [ DEFAULT_BALLOON_POSITIONS.northArrowSouthEast, DEFAULT_BALLOON_POSITIONS.southArrowNorth, DEFAULT_BALLOON_POSITIONS.southArrowNorthWest, - DEFAULT_BALLOON_POSITIONS.southArrowNorthEast -]; - -const TABLE_PROPERTIES_BALLOON_POSITIONS = [ - ...BALLOON_POSITIONS, - centeredBalloonPositionForLongWidgets + DEFAULT_BALLOON_POSITIONS.southArrowNorthEast, + DEFAULT_BALLOON_POSITIONS.viewportStickyNorth ]; /** @@ -67,9 +62,11 @@ export function getBalloonTablePositionData( editor ) { const modelTable = firstPosition.findAncestor( 'table' ); const viewTable = editor.editing.mapper.toViewElement( modelTable ); + console.log(BALLOON_POSITIONS); + return { target: editor.editing.view.domConverter.viewToDom( viewTable ), - positions: TABLE_PROPERTIES_BALLOON_POSITIONS + positions: BALLOON_POSITIONS }; } @@ -96,6 +93,8 @@ export function getBalloonCellPositionData( editor ) { const modelTableCell = getTableCellAtPosition( selection.getFirstPosition() ); const viewTableCell = mapper.toViewElement( modelTableCell ); + console.log(BALLOON_POSITIONS); + return { target: domConverter.viewToDom( viewTableCell ), positions: BALLOON_POSITIONS diff --git a/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js b/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js index b9cf9d3cd1a..8fa8f6d76e2 100644 --- a/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js +++ b/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js @@ -232,7 +232,8 @@ export default class BalloonPanelView extends View { defaultPositions.northArrowSouthMiddleWest, defaultPositions.northArrowSouthMiddleEast, defaultPositions.northArrowSouthWest, - defaultPositions.northArrowSouthEast + defaultPositions.northArrowSouthEast, + defaultPositions.viewportStickyNorth ], limiter: defaultLimiterElement, fitInViewport: true @@ -240,6 +241,8 @@ export default class BalloonPanelView extends View { const optimalPosition = BalloonPanelView._getOptimalPosition( positionOptions ); + console.log( optimalPosition ); + // 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 ); @@ -425,6 +428,9 @@ BalloonPanelView.arrowHorizontalOffset = 25; */ BalloonPanelView.arrowVerticalOffset = 10; +// TODO +BalloonPanelView.stickyVerticalOffset = 20; + /** * Function used to calculate the optimal position for the balloon. * @@ -702,6 +708,22 @@ BalloonPanelView._getOptimalPosition = getOptimalPosition; * | Balloon | * +-----------------+ * + * * `viewportStickyNorth` + * + * +---------------------------+ + * | [ Target ] | + * | | + * +-----------------------------------+ + * | | +-----------------+ | | + * | | | Balloon | | | + * | | +-----------------+ | | + * | | | | + * | | | | + * | | | | + * | | | | + * | +---------------------------+ | + * | Viewport | + * +-----------------------------------+ * * See {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#attachTo}. * @@ -791,6 +813,7 @@ BalloonPanelView.defaultPositions = { left: targetRect.right - ( balloonRect.width * .25 ) - BalloonPanelView.arrowHorizontalOffset, name: 'arrow_smw' } ), + northEastArrowSouth: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), left: targetRect.right - balloonRect.width / 2, @@ -808,6 +831,7 @@ BalloonPanelView.defaultPositions = { left: targetRect.right - balloonRect.width + BalloonPanelView.arrowHorizontalOffset, name: 'arrow_se' } ), + // ------- South west southWestArrowNorthWest: ( targetRect, balloonRect ) => ( { @@ -901,8 +925,22 @@ BalloonPanelView.defaultPositions = { 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; + } + + return { + top: viewportRect.top + BalloonPanelView.stickyVerticalOffset, + left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2, + name: 'arrowless', + withArrow: false + }; + } }; // Returns the top coordinate for positions starting with `north*`. diff --git a/packages/ckeditor5-ui/src/panel/balloon/contextualballoon.js b/packages/ckeditor5-ui/src/panel/balloon/contextualballoon.js index 38a6b575d9c..b4532a0d228 100644 --- a/packages/ckeditor5-ui/src/panel/balloon/contextualballoon.js +++ b/packages/ckeditor5-ui/src/panel/balloon/contextualballoon.js @@ -498,11 +498,18 @@ export default class ContextualBalloon extends Plugin { _getBalloonPosition() { let position = Array.from( this._visibleStack.values() ).pop().position; - // Use the default limiter if none has been specified. - if ( position && !position.limiter ) { + if ( position ) { + // Use the default limiter if none has been specified. + if ( !position.limiter ) { + // Don't modify the original options object. + position = Object.assign( {}, position, { + limiter: this.positionLimiter + } ); + } + // Don't modify the original options object. position = Object.assign( {}, position, { - limiter: this.positionLimiter + viewportOffsetConfig: this.editor.config.get( 'ui.viewportOffset' ) } ); } diff --git a/packages/ckeditor5-ui/tests/manual/tickets/9892/1.html b/packages/ckeditor5-ui/tests/manual/tickets/9892/1.html new file mode 100644 index 00000000000..ab503970c38 --- /dev/null +++ b/packages/ckeditor5-ui/tests/manual/tickets/9892/1.html @@ -0,0 +1,123 @@ + + + +

Other webpage content

+

Lorem ipsum dolor sit amet consectetur, adipisicing elit. Officia itaque eum necessitatibus possimus adipisci mollitia dolor quia molestias voluptate dignissimos? Optio architecto dicta dolorum laborum nam nulla minus aspernatur iste.

+
+

Short table

+ + + + + + + + + + + +
+


























+

Very long table

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+


























+

Short image

+
+ Autumn fields +
+


























+

Very tall image table

+
+ Very tall image +
+


























+


























+


























+
+ + diff --git a/packages/ckeditor5-ui/tests/manual/tickets/9892/1.js b/packages/ckeditor5-ui/tests/manual/tickets/9892/1.js new file mode 100644 index 00000000000..fb20b6be929 --- /dev/null +++ b/packages/ckeditor5-ui/tests/manual/tickets/9892/1.js @@ -0,0 +1,37 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals window, document, console:false */ + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; +import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties'; +import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + image: { toolbar: [ 'toggleImageCaption', 'imageTextAlternative' ] }, + plugins: [ ArticlePluginSet, TableProperties, TableCellProperties ], + toolbar: { + items: [ 'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ], + viewportTopOffset: 100 + }, + table: { + contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells', '|', 'tableProperties', 'tableCellProperties' ], + tableToolbar: [ 'bold', 'italic' ] + }, + ui: { + viewportOffset: { + top: 100, + bottom: 100 + } + } + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-ui/tests/manual/tickets/9892/1.md b/packages/ckeditor5-ui/tests/manual/tickets/9892/1.md new file mode 100644 index 00000000000..3b2e4344d9c --- /dev/null +++ b/packages/ckeditor5-ui/tests/manual/tickets/9892/1.md @@ -0,0 +1,5 @@ +## Table toolbar does not respect viewportTopOffset configuration [#9892](https://github.com/ckeditor/ckeditor5/issues/9892) + +- select the long table +- scroll down when balloon toolbar visible +- toolbar shouldn't go above the main toolbar and overlap with sticky header diff --git a/packages/ckeditor5-ui/tests/manual/tickets/9892/sample-very-tall.jpg b/packages/ckeditor5-ui/tests/manual/tickets/9892/sample-very-tall.jpg new file mode 100644 index 00000000000..e29fd2adfb3 Binary files /dev/null and b/packages/ckeditor5-ui/tests/manual/tickets/9892/sample-very-tall.jpg differ diff --git a/packages/ckeditor5-ui/tests/manual/tickets/9892/sample.jpg b/packages/ckeditor5-ui/tests/manual/tickets/9892/sample.jpg new file mode 100644 index 00000000000..b77d07e7bff Binary files /dev/null and b/packages/ckeditor5-ui/tests/manual/tickets/9892/sample.jpg differ diff --git a/packages/ckeditor5-ui/tests/panel/balloon/balloonpanelview.js b/packages/ckeditor5-ui/tests/panel/balloon/balloonpanelview.js index 8ad02bdf38f..375fcd238cd 100644 --- a/packages/ckeditor5-ui/tests/panel/balloon/balloonpanelview.js +++ b/packages/ckeditor5-ui/tests/panel/balloon/balloonpanelview.js @@ -9,6 +9,7 @@ import ViewCollection from '../../../src/viewcollection'; import BalloonPanelView 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'; describe( 'BalloonPanelView', () => { let view; @@ -189,7 +190,8 @@ describe( 'BalloonPanelView', () => { BalloonPanelView.defaultPositions.northArrowSouthMiddleWest, BalloonPanelView.defaultPositions.northArrowSouthMiddleEast, BalloonPanelView.defaultPositions.northArrowSouthWest, - BalloonPanelView.defaultPositions.northArrowSouthEast + BalloonPanelView.defaultPositions.northArrowSouthEast, + BalloonPanelView.defaultPositions.viewportStickyNorth ], limiter: document.body, fitInViewport: true @@ -688,34 +690,43 @@ describe( 'BalloonPanelView', () => { } ); describe( 'defaultPositions', () => { - let positions, balloonRect, targetRect, arrowHOffset, arrowVOffset; + let positions, balloonRect, targetRect, viewportRect, arrowHOffset, arrowVOffset; beforeEach( () => { positions = BalloonPanelView.defaultPositions; arrowHOffset = BalloonPanelView.arrowHorizontalOffset; arrowVOffset = BalloonPanelView.arrowVerticalOffset; - targetRect = { + viewportRect = new Rect( { + top: 0, + bottom: 0, + left: 0, + right: 0, + width: 200, + height: 200 + } ); + + targetRect = new Rect( { top: 100, bottom: 200, left: 100, right: 200, width: 100, height: 100 - }; + } ); - balloonRect = { + balloonRect = new Rect( { top: 0, bottom: 0, left: 0, right: 0, width: 50, height: 50 - }; + } ); } ); it( 'should have a proper length', () => { - expect( Object.keys( positions ) ).to.have.length( 30 ); + expect( Object.keys( positions ) ).to.have.length( 31 ); } ); // ------- North @@ -969,6 +980,57 @@ describe( 'BalloonPanelView', () => { name: 'arrow_nmw' } ); } ); + + // ------- South east + + it( 'should define the "viewportStickyNorth" position', () => { + expect( positions.viewportStickyNorth( targetRect, balloonRect, viewportRect ) ).to.equal( null ); + } ); + } ); + + describe( 'stickyPositions', () => { + let positions, balloonRect, targetRect, viewportRect, stickyOffset; + + beforeEach( () => { + positions = BalloonPanelView.defaultPositions; + stickyOffset = BalloonPanelView.stickyVerticalOffset; + + viewportRect = new Rect( { + top: 300, + bottom: 800, + left: 0, + right: 200, + width: 0, + height: 0 + } ); + + balloonRect = new Rect( { + top: 0, + bottom: 0, + left: 0, + right: 0, + width: 50, + height: 50 + } ); + } ); + + it( 'should stick position to the top when target element is above viewport', () => { + targetRect = new Rect( { + top: 200, + bottom: 400, + left: 50, + right: 100, + width: 0, + height: 0 + } ); + + expect( positions.viewportStickyNorth( targetRect, balloonRect, viewportRect ) ).to.deep.equal( { + top: 300 + stickyOffset, + left: 25, + name: 'arrowless', + withArrow: false + } ); + } ); } ); } ); diff --git a/packages/ckeditor5-utils/src/dom/position.js b/packages/ckeditor5-utils/src/dom/position.js index 969000cc40a..cbe22c13c22 100644 --- a/packages/ckeditor5-utils/src/dom/position.js +++ b/packages/ckeditor5-utils/src/dom/position.js @@ -13,6 +13,8 @@ import getPositionedAncestor from './getpositionedancestor'; import getBorderWidths from './getborderwidths'; import { isFunction } from 'lodash-es'; +// @if CK_DEBUG_POSITION // import { RectDrawer } from '@ckeditor/ckeditor5-minimap/src/utils'; + /** * Calculates the `position: absolute` coordinates of a given element so it can be positioned with respect to the * target in the visually most efficient way, taking various restrictions like viewport or limiter geometry @@ -77,7 +79,7 @@ import { isFunction } from 'lodash-es'; * @param {module:utils/dom/position~Options} options Positioning options object. * @returns {module:utils/dom/position~Position} */ -export function getOptimalPosition( { element, target, positions, limiter, fitInViewport } ) { +export function getOptimalPosition( { element, target, positions, limiter, fitInViewport, viewportOffsetConfig } ) { // If the {@link module:utils/dom/position~Options#target} is a function, use what it returns. // https://github.com/ckeditor/ckeditor5-utils/issues/157 if ( isFunction( target ) ) { @@ -94,33 +96,143 @@ export function getOptimalPosition( { element, target, positions, limiter, fitIn const elementRect = new Rect( element ); const targetRect = new Rect( target ); - let bestPositionRect; - let bestPositionName; + let bestPosition; + + // @if CK_DEBUG_POSITION // RectDrawer.clear(); + // @if CK_DEBUG_POSITION // RectDrawer.draw( targetRect, { outlineWidth: '5px' }, 'Target' ); + + const positionOptions = { targetRect, elementRect, positionedElementAncestor }; // If there are no limits, just grab the very first position and be done with that drama. if ( !limiter && !fitInViewport ) { - [ bestPositionName, bestPositionRect ] = getPositionNameAndRect( positions[ 0 ], targetRect, elementRect ); + bestPosition = new Position( positions[ 0 ], positionOptions ); } else { const limiterRect = limiter && new Rect( limiter ).getVisible(); - const viewportRect = fitInViewport && new Rect( global.window ); - const bestPosition = getBestPositionNameAndRect( positions, { targetRect, elementRect, limiterRect, viewportRect } ); + const viewportRect = fitInViewport && getConstrainedViewportRect( viewportOffsetConfig ); + + // @if CK_DEBUG_POSITION // if ( viewportRect ) { + // @if CK_DEBUG_POSITION // RectDrawer.draw( viewportRect, { outlineWidth: '5px' }, 'Viewport' ); + // @if CK_DEBUG_POSITION // } + + // @if CK_DEBUG_POSITION // if ( limiter ) { + // @if CK_DEBUG_POSITION // RectDrawer.draw( limiterRect, { outlineWidth: '5px', outlineColor: 'green' }, 'Visible limiter' ); + // @if CK_DEBUG_POSITION // } + + Object.assign( positionOptions, { limiterRect, viewportRect } ); // If there's no best position found, i.e. when all intersections have no area because // rects have no width or height, then just use the first available position. - [ bestPositionName, bestPositionRect ] = bestPosition || getPositionNameAndRect( positions[ 0 ], targetRect, elementRect ); + bestPosition = getBestPosition( positions, positionOptions ) || new Position( positions[ 0 ], positionOptions ); + } + + return bestPosition; +} + +class Position { + constructor( positioningFunction, options ) { + const positioningFunctionOutput = positioningFunction( options.targetRect, options.elementRect, options.viewportRect ); + + // Nameless position for a function that didn't participate. + if ( !positioningFunctionOutput ) { + return; + } + + const { left, top } = positioningFunctionOutput; + + delete positioningFunctionOutput.left; + delete positioningFunctionOutput.top; + + Object.assign( this, positioningFunctionOutput ); + + this._positioningFunctionCorrdinates = { left, top }; + this._options = options; } - let absoluteRectCoordinates = getAbsoluteRectCoordinates( bestPositionRect ); + get left() { + return this._absoluteRect.left; + } - if ( positionedElementAncestor ) { - absoluteRectCoordinates = shiftRectCoordinatesDueToPositionedAncestor( absoluteRectCoordinates, positionedElementAncestor ); + get top() { + return this._absoluteRect.top; } - return { - left: absoluteRectCoordinates.left, - top: absoluteRectCoordinates.top, - name: bestPositionName - }; + // TODO + get limiterIntersectionArea() { + const limiterRect = this._options.limiterRect; + + if ( limiterRect ) { + const viewportRect = this._options.viewportRect; + + if ( viewportRect ) { + // Consider only the part of the limiter which is visible in the viewport. So the limiter is getting limited. + const limiterViewportIntersectRect = limiterRect.getIntersection( viewportRect ); + + if ( limiterViewportIntersectRect ) { + // If the limiter is within the viewport, then check the intersection between that part of the + // limiter and actual position. + return limiterViewportIntersectRect.getIntersectionArea( this._rect ); + } + } else { + return limiterRect.getIntersectionArea( this._rect ); + } + } + + return 0; + } + + // TODO + get viewportIntersectionArea() { + const viewportRect = this._options.viewportRect; + + if ( viewportRect ) { + return viewportRect.getIntersectionArea( this._rect ); + } + + return 0; + } + + get _rect() { + if ( this._cachedRect ) { + return this._cachedRect; + } + + this._cachedRect = this._options.elementRect.clone().moveTo( + this._positioningFunctionCorrdinates.left, + this._positioningFunctionCorrdinates.top + ); + + return this._cachedRect; + } + + get _absoluteRect() { + // Speed optimization. + if ( this._cachedAbsoluteRect ) { + return this._cachedAbsoluteRect; + } + + this._cachedAbsoluteRect = getRectForAbsolutePositioning( this._rect ); + + if ( this._options.positionedAncestor ) { + shiftRectToCompensatePositionedAncestor( this._cachedAbsoluteRect, this._options.positionedAncestor ); + } + + return this._cachedAbsoluteRect; + } +} + +// TODO +// @private +function getConstrainedViewportRect( viewportOffsetConfig ) { + viewportOffsetConfig = Object.assign( { top: 0, bottom: 0, left: 0, right: 0 }, viewportOffsetConfig ); + + const viewportRect = new Rect( global.window ); + + viewportRect.top += viewportOffsetConfig.top; + viewportRect.height -= viewportOffsetConfig.top; + viewportRect.bottom -= viewportOffsetConfig.bottom; + viewportRect.height -= viewportOffsetConfig.bottom; + + return viewportRect; } // For given position function, returns a corresponding `Rect` instance. @@ -130,17 +242,17 @@ export function getOptimalPosition( { element, target, positions, limiter, fitIn // @param {utils/dom/rect~Rect} targetRect A rect of the target. // @param {utils/dom/rect~Rect} elementRect A rect of positioned element. // @returns {Array|null} An array containing position name and its Rect (or null if position should be ignored). -function getPositionNameAndRect( position, targetRect, elementRect ) { - const positionData = position( targetRect, elementRect ); +// function getPositionNameAndRect( position, targetRect, elementRect ) { +// const positionData = position( targetRect, elementRect ); - if ( !positionData ) { - return null; - } +// if ( !positionData ) { +// return null; +// } - const { left, top, name } = positionData; +// const { left, top, name } = positionData; - return [ name, elementRect.clone().moveTo( left, top ) ]; -} +// return [ name, elementRect.clone().moveTo( left, top ) ]; +// } // For a given array of positioning functions, returns such that provides the best // fit of the `elementRect` into the `limiterRect` and `viewportRect`. @@ -157,148 +269,71 @@ function getPositionNameAndRect( position, targetRect, elementRect ) { // @param {utils/dom/rect~Rect} options.viewportRect A rect of the viewport. // // @returns {Array} An array containing the name of the position and it's rect. -function getBestPositionNameAndRect( positions, options ) { +function getBestPosition( positions, options ) { const { elementRect, viewportRect } = options; // This is when element is fully visible. const elementRectArea = elementRect.getArea(); // Let's calculate intersection areas for positions. It will end early if best match is found. - const processedPositions = processPositionsToAreas( positions, options ); + // const processedPositions = processPositionsToAreas( positions, options ); + + const _positions = positions + .map( positioningFunction => new Position( positioningFunction, options ) ) + // Some positioning functions may return `null` if they don't want to participate. + .filter( position => !!position.name ); // First let's check all positions that fully fit in the viewport. if ( viewportRect ) { - const processedPositionsInViewport = processedPositions.filter( ( { viewportIntersectArea } ) => { - return viewportIntersectArea === elementRectArea; + const positionsInViewport = _positions.filter( position => { + return position.viewportIntersectionArea === elementRectArea; } ); // Try to find best position from those which fit completely in viewport. - const bestPositionData = getBestOfProcessedPositions( processedPositionsInViewport, elementRectArea ); + const bestPosition = getBestConstrainedPosition( positionsInViewport, elementRectArea ); - if ( bestPositionData ) { - return bestPositionData; + if ( bestPosition ) { + return bestPosition; } } // Either there is no viewportRect or there is no position that fits completely in the viewport. - return getBestOfProcessedPositions( processedPositions, elementRectArea ); -} - -// For a given array of positioning functions, calculates intersection areas for them. -// -// Note: If some position fully fits into the `limiterRect`, it will be returned early, without further consideration -// of other positions. -// -// @private -// -// @param {module:utils/dom/position~Options#positions} positions Functions returning {@link module:utils/dom/position~Position} -// to be checked, in the order of preference. -// @param {Object} options -// @param {utils/dom/rect~Rect} options.targetRect A rect of the {@link module:utils/dom/position~Options#target}. -// @param {utils/dom/rect~Rect} options.elementRect A rect of positioned {@link module:utils/dom/position~Options#element}. -// @param {utils/dom/rect~Rect} options.limiterRect A rect of the {@link module:utils/dom/position~Options#limiter}. -// @param {utils/dom/rect~Rect} options.viewportRect A rect of the viewport. -// -// @returns {Array.} Array of positions with calculated intersection areas. Each item is an object containing: -// * {String} positionName Name of position. -// * {utils/dom/rect~Rect} positionRect Rect of position. -// * {Number} limiterIntersectArea Area of intersection of the position with limiter part that is in the viewport. -// * {Number} viewportIntersectArea Area of intersection of the position with viewport. -function processPositionsToAreas( positions, { targetRect, elementRect, limiterRect, viewportRect } ) { - const processedPositions = []; - - // This is when element is fully visible. - const elementRectArea = elementRect.getArea(); - - for ( const position of positions ) { - const positionData = getPositionNameAndRect( position, targetRect, elementRect ); - - if ( !positionData ) { - continue; - } - - const [ positionName, positionRect ] = positionData; - let limiterIntersectArea = 0; - let viewportIntersectArea = 0; - - if ( limiterRect ) { - if ( viewportRect ) { - // Consider only the part of the limiter which is visible in the viewport. So the limiter is getting limited. - const limiterViewportIntersectRect = limiterRect.getIntersection( viewportRect ); - - if ( limiterViewportIntersectRect ) { - // If the limiter is within the viewport, then check the intersection between that part of the - // limiter and actual position. - limiterIntersectArea = limiterViewportIntersectRect.getIntersectionArea( positionRect ); - } - } else { - limiterIntersectArea = limiterRect.getIntersectionArea( positionRect ); - } - } - - if ( viewportRect ) { - viewportIntersectArea = viewportRect.getIntersectionArea( positionRect ); - } - - const processedPosition = { - positionName, - positionRect, - limiterIntersectArea, - viewportIntersectArea - }; - - // If a such position is found that element is fully contained by the limiter then, obviously, - // there will be no better one, so finishing. - if ( limiterIntersectArea === elementRectArea ) { - return [ processedPosition ]; - } - - processedPositions.push( processedPosition ); - } - - return processedPositions; + return getBestConstrainedPosition( _positions, elementRectArea ); } // For a given array of processed position data (with calculated Rects for positions and intersection areas) -// returns such that provides the best fit of the `elementRect` into the `limiterRect` and `viewportRect` at the same time. -// -// **Note**: It will return early if some position fully fits into the `limiterRect`. -// -// @private -// @param {Array.} Array of positions with calculated intersection areas (in order of preference). -// Each item is an object containing: // // * {String} positionName Name of position. // * {utils/dom/rect~Rect} positionRect Rect of position. -// * {Number} limiterIntersectArea Area of intersection of the position with limiter part that is in the viewport. -// * {Number} viewportIntersectArea Area of intersection of the position with viewport. +// * {Number} limiterIntersectionArea Area of intersection of the position with limiter part that is in the viewport. +// * {Number} viewportIntersectionArea Area of intersection of the position with viewport. // // @param {Number} elementRectArea Area of positioned {@link module:utils/dom/position~Options#element}. // @returns {Array|null} An array containing the name of the position and it's rect, or null if not found. -function getBestOfProcessedPositions( processedPositions, elementRectArea ) { +function getBestConstrainedPosition( processedPositions, elementRectArea ) { let maxFitFactor = 0; - let bestPositionRect; - let bestPositionName; + let bestPosition = null; + + for ( const position of processedPositions ) { + const { limiterIntersectionArea, viewportIntersectionArea } = position; - for ( const { positionName, positionRect, limiterIntersectArea, viewportIntersectArea } of processedPositions ) { // If a such position is found that element is fully container by the limiter then, obviously, // there will be no better one, so finishing. - if ( limiterIntersectArea === elementRectArea ) { - return [ positionName, positionRect ]; + if ( limiterIntersectionArea === elementRectArea ) { + return position; } - // To maximize both viewport and limiter intersection areas we use distance on viewportIntersectArea - // and limiterIntersectArea plane (without sqrt because we are looking for max value). - const fitFactor = viewportIntersectArea ** 2 + limiterIntersectArea ** 2; + // To maximize both viewport and limiter intersection areas we use distance on viewportIntersectionArea + // and limiterIntersectionArea plane (without sqrt because we are looking for max value). + const fitFactor = viewportIntersectionArea ** 2 + limiterIntersectionArea ** 2; if ( fitFactor > maxFitFactor ) { maxFitFactor = fitFactor; - bestPositionRect = positionRect; - bestPositionName = positionName; + bestPosition = position; } } - return bestPositionRect ? [ bestPositionName, bestPositionRect ] : null; + return bestPosition; } // For a given absolute Rect coordinates object and a positioned element ancestor, it returns an object with @@ -310,41 +345,44 @@ function getBestOfProcessedPositions( processedPositions, elementRectArea ) { // // @private // -// @param {Object} absoluteRectCoordinates An object with absolute rect coordinates. -// @param {Object} absoluteRectCoordinates.top -// @param {Object} absoluteRectCoordinates.left +// @param {Object} absoluteRect An object with absolute rect coordinates. +// @param {Object} absoluteRect.top +// @param {Object} absoluteRect.left // @param {HTMLElement} positionedElementAncestor An ancestor element that should be considered. // -// @returns {Object} An object corresponding to `absoluteRectCoordinates` input but with values shifted +// @returns {Object} An object corresponding to `absoluteRect` input but with values shifted // to make up for the positioned element ancestor. -function shiftRectCoordinatesDueToPositionedAncestor( { left, top }, positionedElementAncestor ) { - const ancestorPosition = getAbsoluteRectCoordinates( new Rect( positionedElementAncestor ) ); +function shiftRectToCompensatePositionedAncestor( rect, positionedElementAncestor ) { + const ancestorPosition = getRectForAbsolutePositioning( new Rect( positionedElementAncestor ) ); const ancestorBorderWidths = getBorderWidths( positionedElementAncestor ); + let moveX = 0; + let moveY = 0; + // (https://github.com/ckeditor/ckeditor5-ui-default/issues/126) // If there's some positioned ancestor of the panel, then its `Rect` must be taken into // consideration. `Rect` is always relative to the viewport while `position: absolute` works // with respect to that positioned ancestor. - left -= ancestorPosition.left; - top -= ancestorPosition.top; + moveX -= ancestorPosition.left; + moveY -= ancestorPosition.top; // (https://github.com/ckeditor/ckeditor5-utils/issues/139) // If there's some positioned ancestor of the panel, not only its position must be taken into // consideration (see above) but also its internal scrolls. Scroll have an impact here because `Rect` // is relative to the viewport (it doesn't care about scrolling), while `position: absolute` // must compensate that scrolling. - left += positionedElementAncestor.scrollLeft; - top += positionedElementAncestor.scrollTop; + moveX += positionedElementAncestor.scrollLeft; + moveY += positionedElementAncestor.scrollTop; // (https://github.com/ckeditor/ckeditor5-utils/issues/139) // If there's some positioned ancestor of the panel, then its `Rect` includes its CSS `borderWidth` // while `position: absolute` positioning does not consider it. // E.g. `{ position: absolute, top: 0, left: 0 }` means upper left corner of the element, // not upper-left corner of its border. - left -= ancestorBorderWidths.left; - top -= ancestorBorderWidths.top; + moveX -= ancestorBorderWidths.left; + moveY -= ancestorBorderWidths.top; - return { left, top }; + rect.moveBy( moveX, moveY ); } // DOMRect (also Rect) works in a scroll–independent geometry but `position: absolute` doesn't. @@ -353,13 +391,10 @@ function shiftRectCoordinatesDueToPositionedAncestor( { left, top }, positionedE // @private // @param {utils/dom/rect~Rect} rect A rect to be converted. // @returns {Object} Object containing `left` and `top` properties, in absolute coordinates. -function getAbsoluteRectCoordinates( { left, top } ) { +function getRectForAbsolutePositioning( rect ) { const { scrollX, scrollY } = global.window; - return { - left: left + scrollX, - top: top + scrollY - }; + return rect.clone().moveBy( scrollX, scrollY ); } /** diff --git a/packages/ckeditor5-widget/src/utils.js b/packages/ckeditor5-widget/src/utils.js index 129c12389ce..8afbecf10a8 100644 --- a/packages/ckeditor5-widget/src/utils.js +++ b/packages/ckeditor5-widget/src/utils.js @@ -396,64 +396,64 @@ export function viewToModelPositionOutsideModelElement( model, viewElementMatche }; } -/** - * A positioning function passed to the {@link module:utils/dom/position~getOptimalPosition} helper as a last resort - * when attaching {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView balloon UI} to widgets. - * It comes in handy when a widget is longer than the visual viewport of the web browser and/or upper/lower boundaries - * of a widget are off screen because of the web page scroll. - * - * ┌─┄┄┄┄┄┄┄┄┄Widget┄┄┄┄┄┄┄┄┄┐ - * ┊ ┊ - * ┌────────────Viewport───────────┐ ┌──╁─────────Viewport────────╁──┐ - * │ ┏━━━━━━━━━━Widget━━━━━━━━━┓ │ │ ┃ ^ ┃ │ - * │ ┃ ^ ┃ │ │ ┃ ╭───────/ \───────╮ ┃ │ - * │ ┃ ╭───────/ \───────╮ ┃ │ │ ┃ │ Balloon │ ┃ │ - * │ ┃ │ Balloon │ ┃ │ │ ┃ ╰─────────────────╯ ┃ │ - * │ ┃ ╰─────────────────╯ ┃ │ │ ┃ ┃ │ - * │ ┃ ┃ │ │ ┃ ┃ │ - * │ ┃ ┃ │ │ ┃ ┃ │ - * │ ┃ ┃ │ │ ┃ ┃ │ - * │ ┃ ┃ │ │ ┃ ┃ │ - * │ ┃ ┃ │ │ ┃ ┃ │ - * │ ┃ ┃ │ │ ┃ ┃ │ - * └──╀─────────────────────────╀──┘ └──╀─────────────────────────╀──┘ - * ┊ ┊ ┊ ┊ - * ┊ ┊ └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘ - * ┊ ┊ - * └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘ - * - * **Note**: Works best if used together with - * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions default `BalloonPanelView` positions} - * like `northArrowSouth` and `southArrowNorth`; the transition between these two and this position is smooth. - * - * @param {module:utils/dom/rect~Rect} widgetRect A rect of the widget. - * @param {module:utils/dom/rect~Rect} balloonRect A rect of the balloon. - * @returns {module:utils/dom/position~Position|null} - */ -export function centeredBalloonPositionForLongWidgets( widgetRect, balloonRect ) { - const viewportRect = new Rect( global.window ); - const viewportWidgetInsersectionRect = viewportRect.getIntersection( widgetRect ); - - const balloonTotalHeight = balloonRect.height + BalloonPanelView.arrowVerticalOffset; - - // If there is enough space above or below the widget then this position should not be used. - if ( widgetRect.top - balloonTotalHeight > viewportRect.top || widgetRect.bottom + balloonTotalHeight < viewportRect.bottom ) { - return null; - } - - // Because this is a last resort positioning, to keep things simple we're not playing with positions of the arrow - // like, for instance, "south west" or whatever. Just try to keep the balloon in the middle of the visible area of - // the widget for as long as it is possible. If the widgets becomes invisible (because cropped by the viewport), - // just... place the balloon in the middle of it (because why not?). - const targetRect = viewportWidgetInsersectionRect || widgetRect; - const left = targetRect.left + targetRect.width / 2 - balloonRect.width / 2; - - return { - top: Math.max( widgetRect.top, 0 ) + BalloonPanelView.arrowVerticalOffset, - left, - name: 'arrow_n' - }; -} +// /** +// * A positioning function passed to the {@link module:utils/dom/position~getOptimalPosition} helper as a last resort +// * when attaching {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView balloon UI} to widgets. +// * It comes in handy when a widget is longer than the visual viewport of the web browser and/or upper/lower boundaries +// * of a widget are off screen because of the web page scroll. +// * +// * ┌─┄┄┄┄┄┄┄┄┄Widget┄┄┄┄┄┄┄┄┄┐ +// * ┊ ┊ +// * ┌────────────Viewport───────────┐ ┌──╁─────────Viewport────────╁──┐ +// * │ ┏━━━━━━━━━━Widget━━━━━━━━━┓ │ │ ┃ ^ ┃ │ +// * │ ┃ ^ ┃ │ │ ┃ ╭───────/ \───────╮ ┃ │ +// * │ ┃ ╭───────/ \───────╮ ┃ │ │ ┃ │ Balloon │ ┃ │ +// * │ ┃ │ Balloon │ ┃ │ │ ┃ ╰─────────────────╯ ┃ │ +// * │ ┃ ╰─────────────────╯ ┃ │ │ ┃ ┃ │ +// * │ ┃ ┃ │ │ ┃ ┃ │ +// * │ ┃ ┃ │ │ ┃ ┃ │ +// * │ ┃ ┃ │ │ ┃ ┃ │ +// * │ ┃ ┃ │ │ ┃ ┃ │ +// * │ ┃ ┃ │ │ ┃ ┃ │ +// * │ ┃ ┃ │ │ ┃ ┃ │ +// * └──╀─────────────────────────╀──┘ └──╀─────────────────────────╀──┘ +// * ┊ ┊ ┊ ┊ +// * ┊ ┊ └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘ +// * ┊ ┊ +// * └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘ +// * +// * **Note**: Works best if used together with +// * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions default `BalloonPanelView` positions} +// * like `northArrowSouth` and `southArrowNorth`; the transition between these two and this position is smooth. +// * +// * @param {module:utils/dom/rect~Rect} widgetRect A rect of the widget. +// * @param {module:utils/dom/rect~Rect} balloonRect A rect of the balloon. +// * @returns {module:utils/dom/position~Position|null} +// */ +// export function centeredBalloonPositionForLongWidgets( widgetRect, balloonRect ) { +// const viewportRect = new Rect( global.window ); +// const viewportWidgetInsersectionRect = viewportRect.getIntersection( widgetRect ); + +// const balloonTotalHeight = balloonRect.height + BalloonPanelView.arrowVerticalOffset; + +// // If there is enough space above or below the widget then this position should not be used. +// if ( widgetRect.top - balloonTotalHeight > viewportRect.top || widgetRect.bottom + balloonTotalHeight < viewportRect.bottom ) { +// return null; +// } + +// // Because this is a last resort positioning, to keep things simple we're not playing with positions of the arrow +// // like, for instance, "south west" or whatever. Just try to keep the balloon in the middle of the visible area of +// // the widget for as long as it is possible. If the widgets becomes invisible (because cropped by the viewport), +// // just... place the balloon in the middle of it (because why not?). +// const targetRect = viewportWidgetInsersectionRect || widgetRect; +// const left = targetRect.left + targetRect.width / 2 - balloonRect.width / 2; + +// return { +// top: Math.max( widgetRect.top, 0 ) + BalloonPanelView.arrowVerticalOffset, +// left, +// name: 'arrow_n' +// }; +// } // Default filler offset function applied to all widget elements. // diff --git a/packages/ckeditor5-widget/src/widgettoolbarrepository.js b/packages/ckeditor5-widget/src/widgettoolbarrepository.js index bfe74c09c3c..117dcfabc9b 100644 --- a/packages/ckeditor5-widget/src/widgettoolbarrepository.js +++ b/packages/ckeditor5-widget/src/widgettoolbarrepository.js @@ -11,10 +11,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon'; import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview'; import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview'; -import { - isWidget, - centeredBalloonPositionForLongWidgets -} from './utils'; +import { isWidget } from './utils'; import CKEditorError, { logWarning } from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; /** @@ -289,7 +286,7 @@ function getBalloonPositionData( editor, relatedElement ) { defaultPositions.southArrowNorth, defaultPositions.southArrowNorthWest, defaultPositions.southArrowNorthEast, - centeredBalloonPositionForLongWidgets + defaultPositions.viewportStickyNorth ] }; }