This repository has been archived by the owner on Jun 26, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 12
t/105: Moved rect utilities from BalloonPanelView #106
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
4dc2e55
Added Rect class with tests.
oleq facc6d0
Added getPositionedAncestor() helper function with tests.
oleq 5be9ab2
Extended utils.dom.Rect class documentation.
oleq 920770b
Added getOptimalPosition helper with tests.
oleq 6807d15
Improvements to getOptimalPosition util.
oleq d2aa09f
Fixed: getOptimalPosition not returning a position when there's no in…
oleq ddb5adf
Fixed: Rect.getViewportRect() returns position:absolute coordinates i…
oleq d2418c9
Fixed: Wrong position returned when window is scrolled. Code refactor…
oleq 45bddb4
Doc fixes.
oleq b01afb5
Merge branch 'master' into t/105
oleq 35563c2
Fixed: stubWindowScroll in getOptimalPosition may leave the test env …
oleq 0124b48
API docs fixes.
Reinmar 23ca971
Fixed: Rect.getIntersectionArea() returns positive area for intersect…
oleq 32d893b
Allowed DOM ClientRect as a constructor argument of the Rect.
oleq 9c6cd5f
Since Rect supports ClientRect as a source, so does getOptimalPosition.
oleq b1eb2b0
Added isRange DOM helper with tests.
oleq a543c47
Simplified Rect class constructor.
oleq c95cac7
Doc fixes and improvements.
oleq 23072cf
Extended getOptimalPosition docs.
oleq File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
/** | ||
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/ | ||
|
||
/* globals window */ | ||
|
||
/** | ||
* @module utils/dom/getpositionedancestor | ||
*/ | ||
|
||
/** | ||
* For a given element, returns the nearest ancestor element which CSS position is not "static". | ||
* | ||
* @param {HTMLElement} element Native DOM element to be checked. | ||
* @returns {HTMLElement|null} | ||
*/ | ||
export default function getPositionedAncestor( element ) { | ||
while ( element && element.tagName.toLowerCase() != 'html' ) { | ||
if ( window.getComputedStyle( element ).position != 'static' ) { | ||
return element; | ||
} | ||
|
||
element = element.parentElement; | ||
} | ||
|
||
return null; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/** | ||
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/ | ||
|
||
/** | ||
* @module utils/dom/isrange | ||
*/ | ||
|
||
/** | ||
* Checks if the object is a native DOM Range. | ||
* | ||
* @param {*} obj | ||
* @returns {Boolean} | ||
*/ | ||
export default function isRange( obj ) { | ||
return Object.prototype.toString.apply( obj ) == '[object Range]'; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,272 @@ | ||
/** | ||
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/ | ||
|
||
/* globals window */ | ||
|
||
/** | ||
* @module utils/dom/position | ||
*/ | ||
|
||
import Rect from './rect.js'; | ||
import getPositionedAncestor from './getpositionedancestor.js'; | ||
|
||
/** | ||
* 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 | ||
* into consideration. | ||
* | ||
* // The element which is to be positioned. | ||
* const element = document.body.querySelector( '#toolbar' ); | ||
* | ||
* // A target to which the element is positioned relatively. | ||
* const target = document.body.querySelector( '#container' ); | ||
* | ||
* // Finding the optimal coordinates for the positioning. | ||
* const { left, top, name } = getOptimalPosition( { | ||
* element: element, | ||
* target: target, | ||
* | ||
* // The algorithm will chose among these positions to meet the requirements such | ||
* // as "limiter" element or "fitInViewport", set below. The positions are considered | ||
* // in the order of the array. | ||
* positions: [ | ||
* // | ||
* // [ Target ] | ||
* // +-----------------+ | ||
* // | Element | | ||
* // +-----------------+ | ||
* // | ||
* targetRect => ( { | ||
* top: targetRect.bottom, | ||
* left: targetRect.left, | ||
* name: 'mySouthEastPosition' | ||
* } ), | ||
* | ||
* // | ||
* // +-----------------+ | ||
* // | Element | | ||
* // +-----------------+ | ||
* // [ Target ] | ||
* // | ||
* ( targetRect, elementRect ) => ( { | ||
* top: targetRect.top - elementRect.height, | ||
* left: targetRect.left, | ||
* name: 'myNorthEastPosition' | ||
* } ) | ||
* ], | ||
* | ||
* // Find a position such guarantees the element remains within visible boundaries of <body>. | ||
* limiter: document.body, | ||
* | ||
* // Find a position such guarantees the element remains within visible boundaries of the browser viewport. | ||
* fitInViewport: true | ||
* } ); | ||
* | ||
* // The best position which fits into document.body and the viewport. May be useful | ||
* // to set proper class on the `element`. | ||
* console.log( name ); -> "myNorthEastPosition" | ||
* | ||
* // Using the absolute coordinates which has been found to position the element | ||
* // as in the diagram depicting the "myNorthEastPosition" position. | ||
* element.style.top = top; | ||
* element.style.left = left; | ||
* | ||
* @param {module:utils/dom/position~Options} options Positioning options object. | ||
* @returns {module:utils/dom/position~Position} | ||
*/ | ||
export function getOptimalPosition( { element, target, positions, limiter, fitInViewport } ) { | ||
const positionedElementAncestor = getPositionedAncestor( element.parentElement ); | ||
const elementRect = new Rect( element ); | ||
const targetRect = new Rect( target ); | ||
|
||
let bestPosition; | ||
let name; | ||
|
||
// If there are no limits, just grab the very first position and be done with that drama. | ||
if ( !limiter && !fitInViewport ) { | ||
[ name, bestPosition ] = getPosition( positions[ 0 ], targetRect, elementRect ); | ||
} else { | ||
const limiterRect = limiter && new Rect( limiter ); | ||
const viewportRect = fitInViewport && Rect.getViewportRect(); | ||
|
||
[ name, bestPosition ] = | ||
getBestPosition( positions, targetRect, elementRect, 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. | ||
getPosition( positions[ 0 ], targetRect, elementRect ); | ||
} | ||
|
||
let { left, top } = getAbsoluteRectCoordinates( bestPosition ); | ||
|
||
// (#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. | ||
if ( positionedElementAncestor ) { | ||
const ancestorPosition = getAbsoluteRectCoordinates( new Rect( positionedElementAncestor ) ); | ||
|
||
left -= ancestorPosition.left; | ||
top -= ancestorPosition.top; | ||
} | ||
|
||
return { left, top, name }; | ||
} | ||
|
||
// For given position function, returns a corresponding `Rect` instance. | ||
// | ||
// @private | ||
// @param {Function} position A function returning {@link module:utils/dom/position~Position}. | ||
// @param {utils/dom/rect~Rect} targetRect A rect of the target. | ||
// @param {utils/dom/rect~Rect} elementRect A rect of positioned element. | ||
// @returns {Array} An array containing position name and its Rect. | ||
function getPosition( position, targetRect, elementRect ) { | ||
const { left, top, name } = position( targetRect, elementRect ); | ||
|
||
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`. | ||
// | ||
// @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 {utils/dom/rect~Rect} targetRect A rect of the {@link module:utils/dom/position~Options#target}. | ||
// @param {utils/dom/rect~Rect} elementRect A rect of positioned {@link module:utils/dom/position~Options#element}. | ||
// @param {utils/dom/rect~Rect} limiterRect A rect of the {@link module:utils/dom/position~Options#limiter}. | ||
// @param {utils/dom/rect~Rect} viewportRect A rect of the viewport. | ||
// @returns {Array} An array containing the name of the position and it's rect. | ||
function getBestPosition( positions, targetRect, elementRect, limiterRect, viewportRect ) { | ||
let maxLimiterIntersectArea = 0; | ||
let maxViewportIntersectArea = 0; | ||
let bestPositionRect; | ||
let bestPositionName; | ||
|
||
// This is when element is fully visible. | ||
const elementRectArea = elementRect.getArea(); | ||
|
||
positions.some( position => { | ||
const [ positionName, positionRect ] = getPosition( position, targetRect, elementRect ); | ||
let limiterIntersectArea; | ||
let viewportIntersectArea; | ||
|
||
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 = 0; | ||
} | ||
} else { | ||
limiterIntersectArea = limiterRect.getIntersectionArea( positionRect ); | ||
} | ||
} | ||
|
||
if ( viewportRect ) { | ||
viewportIntersectArea = viewportRect.getIntersectionArea( positionRect ); | ||
} | ||
|
||
// The only criterion: intersection with the viewport. | ||
if ( viewportRect && !limiterRect ) { | ||
if ( viewportIntersectArea > maxViewportIntersectArea ) { | ||
setBestPosition(); | ||
} | ||
} | ||
// The only criterion: intersection with the limiter. | ||
else if ( !viewportRect && limiterRect ) { | ||
if ( limiterIntersectArea > maxLimiterIntersectArea ) { | ||
setBestPosition(); | ||
} | ||
} | ||
// Two criteria: intersection with the viewport and the limiter visible in the viewport. | ||
else { | ||
if ( viewportIntersectArea > maxViewportIntersectArea && limiterIntersectArea >= maxLimiterIntersectArea ) { | ||
setBestPosition(); | ||
} else if ( viewportIntersectArea >= maxViewportIntersectArea && limiterIntersectArea > maxLimiterIntersectArea ) { | ||
setBestPosition(); | ||
} | ||
} | ||
|
||
function setBestPosition() { | ||
maxViewportIntersectArea = viewportIntersectArea; | ||
maxLimiterIntersectArea = limiterIntersectArea; | ||
bestPositionRect = positionRect; | ||
bestPositionName = positionName; | ||
} | ||
|
||
// If a such position is found that element is fully container by the limiter then, obviously, | ||
// there will be no better one, so finishing. | ||
return limiterIntersectArea === elementRectArea; | ||
} ); | ||
|
||
return bestPositionRect ? [ bestPositionName, bestPositionRect ] : null; | ||
} | ||
|
||
// DOMRect (also Rect) works in a scroll–independent geometry but `position: absolute` doesn't. | ||
// This function converts Rect to `position: absolute` coordinates. | ||
// | ||
// @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 } ) { | ||
return { | ||
left: left + window.scrollX, | ||
top: top + window.scrollY, | ||
}; | ||
} | ||
|
||
/** | ||
* The `getOptimalPosition` helper options. | ||
* | ||
* @interface module:utils/dom/position~Options | ||
*/ | ||
|
||
/** | ||
* Element that is to be positioned. | ||
* | ||
* @member {HTMLElement} #element | ||
*/ | ||
|
||
/** | ||
* Target with respect to which the `element` is to be positioned. | ||
* | ||
* @member {HTMLElement|Range|ClientRect} #target | ||
*/ | ||
|
||
/** | ||
* An array of functions which return {@link module:utils/dom/position~Position} relative | ||
* to the `target`, in the order of preference. | ||
* | ||
* @member {Array.<Function>} #positions | ||
*/ | ||
|
||
/** | ||
* When set, the algorithm will chose position which fits the most in the | ||
* limiter's bounding rect. | ||
* | ||
* @member {HTMLElement|Range|ClientRect} #limiter | ||
*/ | ||
|
||
/** | ||
* When set, the algorithm will chose such a position which fits `element` | ||
* the most inside visible viewport. | ||
* | ||
* @member {Boolean} #fitInViewport | ||
*/ | ||
|
||
/** | ||
* An object describing a position in `position: absolute` coordinate | ||
* system, along with position name. | ||
* | ||
* @typedef {Object} module:utils/dom/position~Position | ||
* | ||
* @property {Number} top Top position offset. | ||
* @property {Number} left Left position offset. | ||
* @property {String} name Name of the position. | ||
*/ |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And that's what?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Positions are named. This is useful because as the
getOptimalPosition
util selects the best position, some changes to the DOM may be needed, i.e. the position needs a proper class to be displayed (like an arrow inBalloonPanelView
which changes with each position).So, in fact, the knowledge about position geometry (
top
,left
) is not enough. We need to know, which of the positioning functions passed to https://github.com/ckeditor/ckeditor5-ui-default/blob/t/131/src/balloonpanel/balloonpanelview.js#L145-L150 has been chosen.That's why positioning functions are named, like https://github.com/ckeditor/ckeditor5-ui-default/blob/t/131/src/balloonpanel/balloonpanelview.js#L207-L211.
TBH, I don't like it either but I couldn't find any other way to simplify this API. I mean, to pass a number of functions, get the output data out of one of them (the best one) and to know precisely which function returned this output data. Ideas?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know that, I didn't mean that it's wrong. I think we can live with it (TBH, I don't have energy to try to find a better solution because this one isn't that bad :D). I just wanted this to be better documented. A single example in this module would do enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, the only place where position names look odd is https://github.com/ckeditor/ckeditor5-ui-default/blob/t/131/src/balloonpanel/balloonpanelview.js#L207. But it's reasonable there too. You can both, access the returned value satisfy the interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually 2, you could mention there that these functions returns objects implementing this specific interface, just to make it clear.