@ckeditor/ckeditor5-utils
Advanced tools
Comparing version 18.0.0 to 19.0.0
Changelog | ||
========= | ||
## [19.0.0](https://github.com/ckeditor/ckeditor5-utils/compare/v18.0.0...v19.0.0) (2020-04-29) | ||
### MINOR BREAKING CHANGES | ||
* The `translate` function from the `translation-service` was marked as protected. See [#334](https://github.com/ckeditor/ckeditor5-utils/issues/334). | ||
* The format of stored editor translations has been changed. If you use `window.CKEDITOR_TRANSLATIONS` please see [#334](https://github.com/ckeditor/ckeditor5-utils/issues/334). | ||
* The `getPositionedAncestor()` helper will no longer return the passed element when it is positioned. | ||
### MAJOR BREAKING CHANGES | ||
* `env.isEdge` is no longer available. See [ckeditor/ckeditor5#6202](https://github.com/ckeditor/ckeditor5/issues/6202). | ||
### Features | ||
* Added the support for initializing `Collection` items via the `constructor()`. Closes [ckeditor/ckeditor5#6319](https://github.com/ckeditor/ckeditor5/issues/6319). ([8846e66](https://github.com/ckeditor/ckeditor5-utils/commit/8846e66)) | ||
* Provided support for plural forms internalization. Part of [ckeditor/ckeditor5#6526](https://github.com/ckeditor/ckeditor5/issues/6526). ([5f6ea75](https://github.com/ckeditor/ckeditor5-utils/commit/5f6ea75)) | ||
### Bug fixes | ||
* Do not execute `ResizeObserver` callbacks when the resized element is invisible (but still in DOM) (see [ckeditor/ckeditor5#6570](https://github.com/ckeditor/ckeditor5/issues/6570)). ([fb13d9d](https://github.com/ckeditor/ckeditor5-utils/commit/fb13d9d)) | ||
* Editor will now load correctly in environment with `Symbol` polyfill. Closes [ckeditor/ckeditor5#6489](https://github.com/ckeditor/ckeditor5/issues/6489). ([7cd1f48](https://github.com/ckeditor/ckeditor5-utils/commit/7cd1f48)) | ||
* Fixed various cases with typing multi-byte unicode sequences (e.g. emojis). Closes [ckeditor/ckeditor5#3147](https://github.com/ckeditor/ckeditor5/issues/3147). Closes [ckeditor/ckeditor5#6495](https://github.com/ckeditor/ckeditor5/issues/6495). ([6dc1ba6](https://github.com/ckeditor/ckeditor5-utils/commit/6dc1ba6)) | ||
* The `getOptimalPosition()` helper should prefer positions that fit inside the viewport even though there are some others that fit better into the limiter. Closes [ckeditor/ckeditor5#6181](https://github.com/ckeditor/ckeditor5/issues/6181). ([7cd1238](https://github.com/ckeditor/ckeditor5-utils/commit/7cd1238)) | ||
### Other changes | ||
* Removed `env.isEdge` as Edge is now detected and treated as Chrome. Closes [ckeditor/ckeditor5#6202](https://github.com/ckeditor/ckeditor5/issues/6202). ([2902b30](https://github.com/ckeditor/ckeditor5-utils/commit/2902b30)) | ||
* The `getPositionedAncestor()` helper should use `offsetParent` instead of `getComputedStyle()` for performance reasons. Closes [ckeditor/ckeditor5#6573](https://github.com/ckeditor/ckeditor5/issues/6573). ([7939756](https://github.com/ckeditor/ckeditor5-utils/commit/7939756)) | ||
## [18.0.0](https://github.com/ckeditor/ckeditor5-utils/compare/v17.0.0...v18.0.0) (2020-03-19) | ||
@@ -5,0 +35,0 @@ |
{ | ||
"name": "@ckeditor/ckeditor5-utils", | ||
"version": "18.0.0", | ||
"version": "19.0.0", | ||
"description": "Miscellaneous utils used by CKEditor 5.", | ||
@@ -15,6 +15,6 @@ "keywords": [ | ||
"devDependencies": { | ||
"@ckeditor/ckeditor5-build-classic": "^18.0.0", | ||
"@ckeditor/ckeditor5-editor-classic": "^18.0.0", | ||
"@ckeditor/ckeditor5-core": "^18.0.0", | ||
"@ckeditor/ckeditor5-engine": "^18.0.0", | ||
"@ckeditor/ckeditor5-build-classic": "^19.0.0", | ||
"@ckeditor/ckeditor5-editor-classic": "^19.0.0", | ||
"@ckeditor/ckeditor5-core": "^19.0.0", | ||
"@ckeditor/ckeditor5-engine": "^19.0.0", | ||
"assertion-error": "^1.1.0", | ||
@@ -21,0 +21,0 @@ "eslint": "^5.5.0", |
@@ -13,2 +13,3 @@ /** | ||
import uid from './uid'; | ||
import isIterable from './isiterable'; | ||
import mix from './mix'; | ||
@@ -32,6 +33,42 @@ | ||
* | ||
* @param {Object} [options={}] The options object. | ||
* @param {String} [options.idProperty='id'] The name of the property which is considered to identify an item. | ||
* You can provide an iterable of initial items the collection will be created with: | ||
* | ||
* const collection = new Collection( [ { id: 'John' }, { id: 'Mike' } ] ); | ||
* | ||
* console.log( collection.get( 0 ) ); // -> { id: 'John' } | ||
* console.log( collection.get( 1 ) ); // -> { id: 'Mike' } | ||
* console.log( collection.get( 'Mike' ) ); // -> { id: 'Mike' } | ||
* | ||
* Or you can first create a collection and then add new items using the {@link #add} method: | ||
* | ||
* const collection = new Collection(); | ||
* | ||
* collection.add( { id: 'John' } ); | ||
* console.log( collection.get( 0 ) ); // -> { id: 'John' } | ||
* | ||
* Whatever option you choose, you can always pass a configuration object as the last argument | ||
* of the constructor: | ||
* | ||
* const emptyCollection = new Collection( { idProperty: 'name' } ); | ||
* emptyCollection.add( { name: 'John' } ); | ||
* console.log( collection.get( 'John' ) ); // -> { name: 'John' } | ||
* | ||
* const nonEmptyCollection = new Collection( [ { name: 'John' } ], { idProperty: 'name' } ); | ||
* nonEmptyCollection.add( { name: 'George' } ); | ||
* console.log( collection.get( 'George' ) ); // -> { name: 'George' } | ||
* console.log( collection.get( 'John' ) ); // -> { name: 'John' } | ||
* | ||
* @param {Iterable.<Object>|Object} initialItemsOrOptions The initial items of the collection or | ||
* the options object. | ||
* @param {Object} [options={}] The options object, when the first argument is an array of initial items. | ||
* @param {String} [options.idProperty='id'] The name of the property which is used to identify an item. | ||
* Items that do not have such a property will be assigned one when added to the collection. | ||
*/ | ||
constructor( options = {} ) { | ||
constructor( initialItemsOrOptions = {}, options = {} ) { | ||
const hasInitialItems = isIterable( initialItemsOrOptions ); | ||
if ( !hasInitialItems ) { | ||
options = initialItemsOrOptions; | ||
} | ||
/** | ||
@@ -93,2 +130,10 @@ * The internal list of items in the collection. | ||
// Set the initial content of the collection (if provided in the constructor). | ||
if ( hasInitialItems ) { | ||
for ( const item of initialItemsOrOptions ) { | ||
this._items.push( item ); | ||
this._itemMap.set( this._getItemIdBeforeAdding( item ), item ); | ||
} | ||
} | ||
/** | ||
@@ -142,29 +187,4 @@ * A collection instance this collection is bound to as a result | ||
add( item, index ) { | ||
let itemId; | ||
const idProperty = this._idProperty; | ||
const itemId = this._getItemIdBeforeAdding( item ); | ||
if ( ( idProperty in item ) ) { | ||
itemId = item[ idProperty ]; | ||
if ( typeof itemId != 'string' ) { | ||
/** | ||
* This item's id should be a string. | ||
* | ||
* @error collection-add-invalid-id | ||
*/ | ||
throw new CKEditorError( 'collection-add-invalid-id', this ); | ||
} | ||
if ( this.get( itemId ) ) { | ||
/** | ||
* This item already exists in the collection. | ||
* | ||
* @error collection-add-item-already-exists | ||
*/ | ||
throw new CKEditorError( 'collection-add-item-already-exists', this ); | ||
} | ||
} else { | ||
item[ idProperty ] = itemId = uid(); | ||
} | ||
// TODO: Use ES6 default function argument. | ||
@@ -612,2 +632,42 @@ if ( index === undefined ) { | ||
/** | ||
* Returns an unique id property for a given `item`. | ||
* | ||
* The method will generate new id and assign it to the `item` if it doesn't have any. | ||
* | ||
* @private | ||
* @param {Object} item Item to be added. | ||
* @returns {String} | ||
*/ | ||
_getItemIdBeforeAdding( item ) { | ||
const idProperty = this._idProperty; | ||
let itemId; | ||
if ( ( idProperty in item ) ) { | ||
itemId = item[ idProperty ]; | ||
if ( typeof itemId != 'string' ) { | ||
/** | ||
* This item's id should be a string. | ||
* | ||
* @error collection-add-invalid-id | ||
*/ | ||
throw new CKEditorError( 'collection-add-invalid-id', this ); | ||
} | ||
if ( this.get( itemId ) ) { | ||
/** | ||
* This item already exists in the collection. | ||
* | ||
* @error collection-add-item-already-exists | ||
*/ | ||
throw new CKEditorError( 'collection-add-item-already-exists', this ); | ||
} | ||
} else { | ||
item[ idProperty ] = itemId = uid(); | ||
} | ||
return itemId; | ||
} | ||
/** | ||
* Iterable interface. | ||
@@ -614,0 +674,0 @@ * |
@@ -19,11 +19,11 @@ /** | ||
export default function getPositionedAncestor( element ) { | ||
while ( element && element.tagName.toLowerCase() != 'html' ) { | ||
if ( global.window.getComputedStyle( element ).position != 'static' ) { | ||
return element; | ||
} | ||
if ( !element || !element.parentNode ) { | ||
return null; | ||
} | ||
element = element.parentElement; | ||
if ( element.offsetParent === global.document.body ) { | ||
return null; | ||
} | ||
return null; | ||
return element.offsetParent; | ||
} |
@@ -93,54 +93,33 @@ /** | ||
const positionedElementAncestor = getPositionedAncestor( element.parentElement ); | ||
const positionedElementAncestor = getPositionedAncestor( element ); | ||
const elementRect = new Rect( element ); | ||
const targetRect = new Rect( target ); | ||
let bestPosition; | ||
let name; | ||
let bestPositionRect; | ||
let bestPositionName; | ||
// 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 ); | ||
[ bestPositionName, bestPositionRect ] = getPositionNameAndRect( positions[ 0 ], targetRect, elementRect ); | ||
} else { | ||
const limiterRect = limiter && new Rect( limiter ).getVisible(); | ||
const viewportRect = fitInViewport && new Rect( global.window ); | ||
const bestPosition = getBestPositionNameAndRect( positions, { targetRect, elementRect, limiterRect, viewportRect } ); | ||
[ 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 ); | ||
// 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 ); | ||
} | ||
let { left, top } = getAbsoluteRectCoordinates( bestPosition ); | ||
let absoluteRectCoordinates = getAbsoluteRectCoordinates( bestPositionRect ); | ||
if ( positionedElementAncestor ) { | ||
const ancestorPosition = getAbsoluteRectCoordinates( new Rect( positionedElementAncestor ) ); | ||
const ancestorBorderWidths = getBorderWidths( positionedElementAncestor ); | ||
// (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; | ||
// (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; | ||
// (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; | ||
absoluteRectCoordinates = shiftRectCoordinatesDueToPositionedAncestor( absoluteRectCoordinates, positionedElementAncestor ); | ||
} | ||
return { left, top, name }; | ||
return { | ||
left: absoluteRectCoordinates.left, | ||
top: absoluteRectCoordinates.top, | ||
name: bestPositionName | ||
}; | ||
} | ||
@@ -155,3 +134,3 @@ | ||
// @returns {Array} An array containing position name and its Rect. | ||
function getPosition( position, targetRect, elementRect ) { | ||
function getPositionNameAndRect( position, targetRect, elementRect ) { | ||
const { left, top, name } = position( targetRect, elementRect ); | ||
@@ -166,14 +145,15 @@ | ||
// @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. | ||
// | ||
// @param {Object} options | ||
// @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} 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; | ||
function getBestPositionNameAndRect( positions, options ) { | ||
const { elementRect, viewportRect } = options; | ||
@@ -183,7 +163,54 @@ // This is when element is fully visible. | ||
positions.some( position => { | ||
const [ positionName, positionRect ] = getPosition( position, targetRect, elementRect ); | ||
let limiterIntersectArea; | ||
let viewportIntersectArea; | ||
// Let's calculate intersection areas for positions. It will end early if best match is found. | ||
const processedPositions = processPositionsToAreas( positions, options ); | ||
// First let's check all positions that fully fit in the viewport. | ||
if ( viewportRect ) { | ||
const processedPositionsInViewport = processedPositions.filter( ( { viewportIntersectArea } ) => { | ||
return viewportIntersectArea === elementRectArea; | ||
} ); | ||
// Try to find best position from those which fit completely in viewport. | ||
const bestPositionData = getBestOfProcessedPositions( processedPositionsInViewport, elementRectArea ); | ||
if ( bestPositionData ) { | ||
return bestPositionData; | ||
} | ||
} | ||
// 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.<Object>} 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 [ positionName, positionRect ] = getPositionNameAndRect( position, targetRect, elementRect ); | ||
let limiterIntersectArea = 0; | ||
let viewportIntersectArea = 0; | ||
if ( limiterRect ) { | ||
@@ -198,4 +225,2 @@ if ( viewportRect ) { | ||
limiterIntersectArea = limiterViewportIntersectRect.getIntersectionArea( positionRect ); | ||
} else { | ||
limiterIntersectArea = 0; | ||
} | ||
@@ -211,38 +236,109 @@ } else { | ||
// The only criterion: intersection with the viewport. | ||
if ( viewportRect && !limiterRect ) { | ||
if ( viewportIntersectArea > maxViewportIntersectArea ) { | ||
setBestPosition(); | ||
} | ||
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 ]; | ||
} | ||
// The only criterion: intersection with the limiter. | ||
else if ( !viewportRect && limiterRect ) { | ||
if ( limiterIntersectArea > maxLimiterIntersectArea ) { | ||
setBestPosition(); | ||
} | ||
processedPositions.push( processedPosition ); | ||
} | ||
return processedPositions; | ||
} | ||
// 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.<Object>} 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. | ||
// | ||
// @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 ) { | ||
let maxFitFactor = 0; | ||
let bestPositionRect; | ||
let bestPositionName; | ||
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 ]; | ||
} | ||
// 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; | ||
// 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; | ||
if ( fitFactor > maxFitFactor ) { | ||
maxFitFactor = fitFactor; | ||
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; | ||
} | ||
// For a given absolute Rect coordinates object and a positioned element ancestor, it returns an object with | ||
// new Rect coordinates that make up for the position and the scroll of the ancestor. | ||
// | ||
// This is necessary because while Rects (and DOMRects) are relative to the browser's viewport, their coordinates | ||
// are used in real–life to position elements with `position: absolute`, which are scoped by any positioned | ||
// (and scrollable) ancestors. | ||
// | ||
// @private | ||
// | ||
// @param {Object} absoluteRectCoordinates An object with absolute rect coordinates. | ||
// @param {Object} absoluteRectCoordinates.top | ||
// @param {Object} absoluteRectCoordinates.left | ||
// @param {HTMLElement} positionedElementAncestor An ancestor element that should be considered. | ||
// | ||
// @returns {Object} An object corresponding to `absoluteRectCoordinates` input but with values shifted | ||
// to make up for the positioned element ancestor. | ||
function shiftRectCoordinatesDueToPositionedAncestor( { left, top }, positionedElementAncestor ) { | ||
const ancestorPosition = getAbsoluteRectCoordinates( new Rect( positionedElementAncestor ) ); | ||
const ancestorBorderWidths = getBorderWidths( positionedElementAncestor ); | ||
// (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; | ||
// (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; | ||
// (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; | ||
return { left, top }; | ||
} | ||
// DOMRect (also Rect) works in a scroll–independent geometry but `position: absolute` doesn't. | ||
@@ -249,0 +345,0 @@ // This function converts Rect to `position: absolute` coordinates. |
@@ -171,2 +171,8 @@ /** | ||
for ( const entry of entries ) { | ||
// Do not execute callbacks for elements that are invisible. | ||
// https://github.com/ckeditor/ckeditor5/issues/6570 | ||
if ( !entry.target.offsetParent ) { | ||
continue; | ||
} | ||
const callbacks = ResizeObserver._getElementCallbacks( entry.target ); | ||
@@ -173,0 +179,0 @@ |
@@ -29,10 +29,2 @@ /** | ||
/** | ||
* Indicates that the application is running in Microsoft Edge. | ||
* | ||
* @static | ||
* @type {Boolean} | ||
*/ | ||
isEdge: isEdge( userAgent ), | ||
/** | ||
* Indicates that the application is running in Firefox (Gecko). | ||
@@ -92,12 +84,2 @@ * | ||
/** | ||
* Checks if User Agent represented by the string is Microsoft Edge. | ||
* | ||
* @param {String} userAgent **Lowercase** `navigator.userAgent` string. | ||
* @returns {Boolean} Whether User Agent is Edge or not. | ||
*/ | ||
export function isEdge( userAgent ) { | ||
return !!userAgent.match( /edge\/(\d+.?\d*)/ ); | ||
} | ||
/** | ||
* Checks if User Agent represented by the string is Firefox (Gecko). | ||
@@ -142,3 +124,3 @@ * | ||
// Feature detection for Unicode properties. Added in ES2018. Currently Firefox and Edge do not support it. | ||
// Feature detection for Unicode properties. Added in ES2018. Currently Firefox does not support it. | ||
// See https://github.com/ckeditor/ckeditor5-mention/issues/44#issuecomment-487002174. | ||
@@ -145,0 +127,0 @@ |
@@ -103,9 +103,14 @@ /** | ||
// Transform text or any iterable into arrays for easier, consistent processing. | ||
// Convert the string (or any array-like object - eg. NodeList) to an array by using the slice() method because, | ||
// unlike Array.from(), it returns array of UTF-16 code units instead of the code points of a string. | ||
// One code point might be a surrogate pair of two code units. All text offsets are expected to be in code units. | ||
// See ckeditor/ckeditor5#3147. | ||
// | ||
// We need to make sure here that fastDiff() works identical to diff(). | ||
if ( !Array.isArray( a ) ) { | ||
a = Array.from( a ); | ||
a = Array.prototype.slice.call( a ); | ||
} | ||
if ( !Array.isArray( b ) ) { | ||
b = Array.from( b ); | ||
b = Array.prototype.slice.call( b ); | ||
} | ||
@@ -112,0 +117,0 @@ |
@@ -27,2 +27,4 @@ /** | ||
* | ||
* Check out the {@glink framework/guides/deep-dive/ui/focus-tracking "Deep dive into focus tracking" guide} to learn more. | ||
* | ||
* @mixes module:utils/dom/emittermixin~EmitterMixin | ||
@@ -51,3 +53,3 @@ * @mixes module:utils/observablemixin~ObservableMixin | ||
* @observable | ||
* @member {HTMLElement|null} | ||
* @member {HTMLElement|null} #focusedElement | ||
*/ | ||
@@ -54,0 +56,0 @@ this.set( 'focusedElement', null ); |
@@ -12,3 +12,3 @@ /** | ||
import { translate } from './translation-service'; | ||
import { _translate } from './translation-service'; | ||
@@ -22,4 +22,4 @@ const RTL_LANGUAGE_CODES = [ 'ar', 'fa', 'he', 'ku', 'ug' ]; | ||
/** | ||
* Creates a new instance of the Locale class. Learn more about | ||
* {@glink features/ui-language configuring language of the editor}. | ||
* Creates a new instance of the locale class. Learn more about | ||
* {@glink features/ui-language configuring the language of the editor}. | ||
* | ||
@@ -48,3 +48,3 @@ * @param {Object} [options] Locale configuration. | ||
* | ||
* Usually the same as {@link #uiLanguage editor language}, it can be customized by passing an optional | ||
* Usually the same as the {@link #uiLanguage editor language}, it can be customized by passing an optional | ||
* argument to the `Locale` constructor. | ||
@@ -82,21 +82,44 @@ * | ||
/** | ||
* Translates the given string to the {@link #uiLanguage}. This method is also available in | ||
* {@link module:core/editor/editor~Editor#t} and {@link module:ui/view~View#t}. | ||
* Translates the given message to the {@link #uiLanguage}. This method is also available in | ||
* {@link module:core/editor/editor~Editor#t `Editor`} and {@link module:ui/view~View#t `View`}. | ||
* | ||
* The strings may contain placeholders (`%<index>`) for values which are passed as the second argument. | ||
* `<index>` is the index in the `values` array. | ||
* This method's context is statically bound to the `Locale` instance and **should always be called as a function**: | ||
* | ||
* editor.t( 'Created file "%0" in %1ms.', [ fileName, timeTaken ] ); | ||
* const t = locale.t; | ||
* t( 'Label' ); | ||
* | ||
* This method's context is statically bound to Locale instance, | ||
* so it can be called as a function: | ||
* The message can be either a string or an object implementing the {@link module:utils/translation-service~Message} interface. | ||
* | ||
* const t = this.t; | ||
* t( 'Label' ); | ||
* The message may contain placeholders (`%<index>`) for value(s) that are passed as a `values` parameter. | ||
* For an array of values, the `%<index>` will be changed to an element of that array at the given index. | ||
* For a single value passed as the second argument, only the `%0` placeholders will be changed to the provided value. | ||
* | ||
* t( 'Created file "%0" in %1ms.', [ fileName, timeTaken ] ); | ||
* t( 'Created file "%0", fileName ); | ||
* | ||
* The message supports plural forms. To specify the plural form, use the `plural` property. Singular or plural form | ||
* will be chosen depending on the first value from the passed `values`. The value of the `plural` property is used | ||
* as a default plural translation when the translation for the target language is missing. | ||
* | ||
* t( { string: 'Add a space', plural: 'Add %0 spaces' }, 1 ); // 'Add a space' for the English language. | ||
* t( { string: 'Add a space', plural: 'Add %0 spaces' }, 5 ); // 'Add 5 spaces' for the English language. | ||
* t( { string: '%1 a space', plural: '%1 %0 spaces' }, [ 2, 'Add' ] ); // 'Add 2 spaces' for the English language. | ||
* | ||
* t( { string: 'Add a space', plural: 'Add %0 spaces' }, 1 ); // 'Dodaj spację' for the Polish language. | ||
* t( { string: 'Add a space', plural: 'Add %0 spaces' }, 5 ); // 'Dodaj 5 spacji' for the Polish language. | ||
* t( { string: '%1 a space', plural: '%1 %0 spaces' }, [ 2, 'Add' ] ); // 'Dodaj 2 spacje' for the Polish language. | ||
* | ||
* * The message should provide an ID using the `id` property when the message strings are not unique and their | ||
* translations should be different. | ||
* | ||
* translate( 'en', { string: 'image', id: 'ADD_IMAGE' } ); | ||
* translate( 'en', { string: 'image', id: 'AN_IMAGE' } ); | ||
* | ||
* @method #t | ||
* @param {String} str The string to translate. | ||
* @param {String[]} [values] Values that should be used to interpolate the string. | ||
* @param {String|module:utils/translation-service~Message} message A message that will be localized (translated). | ||
* @param {String|Number|Array.<String|Number>} [values] A value or an array of values that will fill message placeholders. | ||
* For messages supporting plural forms the first value will determine the plural form. | ||
* @returns {String} | ||
*/ | ||
this.t = ( ...args ) => this._t( ...args ); | ||
this.t = ( message, values ) => this._t( message, values ); | ||
} | ||
@@ -107,3 +130,3 @@ | ||
* | ||
* **Note**: This property has been deprecated. Please use {@link #uiLanguage} and {@link #contentLanguage} | ||
* **Note**: This property was deprecated. Please use {@link #uiLanguage} and {@link #contentLanguage} | ||
* properties instead. | ||
@@ -116,4 +139,4 @@ * | ||
/** | ||
* The {@link module:utils/locale~Locale#language `Locale#language`} property has been deprecated and will | ||
* be removed in the near future. Please use {@link #uiLanguage} and {@link #contentLanguage} properties instead. | ||
* The {@link module:utils/locale~Locale#language `Locale#language`} property was deprecated and will | ||
* be removed in the near future. Please use the {@link #uiLanguage} and {@link #contentLanguage} properties instead. | ||
* | ||
@@ -131,19 +154,34 @@ * @error locale-deprecated-language-property | ||
/** | ||
* Base for the {@link #t} method. | ||
* An unbound version of the {@link #t} method. | ||
* | ||
* @private | ||
* @param {String|module:utils/translation-service~Message} message | ||
* @param {Number|String|Array.<Number|String>} [values] | ||
* @returns {String} | ||
*/ | ||
_t( str, values ) { | ||
let translatedString = translate( this.uiLanguage, str ); | ||
_t( message, values = [] ) { | ||
if ( !Array.isArray( values ) ) { | ||
values = [ values ]; | ||
} | ||
if ( values ) { | ||
translatedString = translatedString.replace( /%(\d+)/g, ( match, index ) => { | ||
return ( index < values.length ) ? values[ index ] : match; | ||
} ); | ||
if ( typeof message === 'string' ) { | ||
message = { string: message }; | ||
} | ||
return translatedString; | ||
const hasPluralForm = !!message.plural; | ||
const quantity = hasPluralForm ? values[ 0 ] : 1; | ||
const translatedString = _translate( this.uiLanguage, message, quantity ); | ||
return interpolateString( translatedString, values ); | ||
} | ||
} | ||
// Fills the `%0, %1, ...` string placeholders with values. | ||
function interpolateString( string, values ) { | ||
return string.replace( /%(\d+)/g, ( match, index ) => { | ||
return ( index < values.length ) ? values[ index ] : match; | ||
} ); | ||
} | ||
// Helps determine whether a language is LTR or RTL. | ||
@@ -150,0 +188,0 @@ // |
@@ -175,3 +175,3 @@ /** | ||
// Nothing to do here if not inited yet. | ||
if ( !( observablePropertiesSymbol in this ) ) { | ||
if ( !( this[ observablePropertiesSymbol ] ) ) { | ||
return; | ||
@@ -275,3 +275,3 @@ } | ||
// Do nothing if already inited. | ||
if ( observablePropertiesSymbol in observable ) { | ||
if ( observable[ observablePropertiesSymbol ] ) { | ||
return; | ||
@@ -278,0 +278,0 @@ } |
@@ -12,2 +12,4 @@ /** | ||
import CKEditorError from './ckeditorerror'; | ||
/* istanbul ignore else */ | ||
@@ -19,52 +21,132 @@ if ( !window.CKEDITOR_TRANSLATIONS ) { | ||
/** | ||
* Adds translations to existing ones. | ||
* These translations will later be available for the {@link module:utils/translation-service~translate `translate()`} function. | ||
* Adds translations to existing ones or overrides the existing translations. These translations will later | ||
* be available for the {@link module:utils/locale~Locale#t `t()`} function. | ||
* | ||
* The `translations` is an object which consists of `messageId: translation` pairs. Note that the message ID can be | ||
* either constructed from the message string or from the message ID if it was passed | ||
* (this happens rarely and mostly for short messages or messages with placeholders). | ||
* Since the editor displays only the message string, the message ID can be found either in the source code or in the | ||
* built translations for another language. | ||
* | ||
* add( 'pl', { | ||
* 'OK': 'OK', | ||
* 'Cancel [context: reject]': 'Anuluj' | ||
* 'Cancel': 'Anuluj', | ||
* 'IMAGE': 'obraz', // Note that the `IMAGE` comes from the message ID, while the string can be `image`. | ||
* } ); | ||
* | ||
* If you cannot import this function from this module (e.g. because you use a CKEditor 5 build), then you can | ||
* If the message is supposed to support various plural forms, make sure to provide an array with the singular form and all plural forms: | ||
* | ||
* add( 'pl', { | ||
* 'Add space': [ 'Dodaj spację', 'Dodaj %0 spacje', 'Dodaj %0 spacji' ] | ||
* } ); | ||
* | ||
* You should also specify the third argument (the `getPluralForm()` function) that will be used to determine the plural form if no | ||
* language file was loaded for that language. All language files coming from CKEditor 5 sources will have this option set, so | ||
* these plural form rules will be reused by other translations added to the registered languages. The `getPluralForm()` function | ||
* can return either a Boolean or a number. | ||
* | ||
* add( 'en', { | ||
* // ... Translations. | ||
* }, n => n !== 1 ); | ||
* add( 'pl', { | ||
* // ... Translations. | ||
* }, n => n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && ( n % 100 < 10 || n % 100 >= 20 ) ? 1 : 2 ); | ||
* | ||
* All translations extend the global `window.CKEDITOR_TRANSLATIONS` object. An example of this object can be found below: | ||
* | ||
* { | ||
* pl: { | ||
* dictionary: { | ||
* 'Cancel': 'Anuluj', | ||
* 'Add space': [ 'Dodaj spację', 'Dodaj %0 spacje', 'Dodaj %0 spacji' ] | ||
* }, | ||
* // A function that returns the plural form index. | ||
* getPluralForm: n => n !==1 | ||
* } | ||
* // Other languages. | ||
* } | ||
* | ||
* If you cannot import this function from this module (e.g. because you use a CKEditor 5 build), you can | ||
* still add translations by extending the global `window.CKEDITOR_TRANSLATIONS` object by using a function like | ||
* the one below: | ||
* | ||
* function addTranslations( language, translations ) { | ||
* function addTranslations( language, translations, getPluralForm ) { | ||
* if ( !window.CKEDITOR_TRANSLATIONS ) { | ||
* window.CKEDITOR_TRANSLATIONS = {}; | ||
* } | ||
* if ( !window.CKEDITOR_TRANSLATIONS[ language ] ) { | ||
* window.CKEDITOR_TRANSLATIONS[ language ] = {}; | ||
* } | ||
* | ||
* const dictionary = window.CKEDITOR_TRANSLATIONS[ language ] || ( window.CKEDITOR_TRANSLATIONS[ language ] = {} ); | ||
* const languageTranslations = window.CKEDITOR_TRANSLATIONS[ language ]; | ||
* | ||
* languageTranslations.dictionary = languageTranslations.dictionary || {}; | ||
* languageTranslations.getPluralForm = getPluralForm || languageTranslations.getPluralForm; | ||
* | ||
* // Extend the dictionary for the given language. | ||
* Object.assign( dictionary, translations ); | ||
* Object.assign( languageTranslations.dictionary, translations ); | ||
* } | ||
* | ||
* @param {String} language Target language. | ||
* @param {Object.<String, String>} translations Translations which will be added to the dictionary. | ||
* @param {Object.<String,*>} translations An object with translations which will be added to the dictionary. | ||
* For each message ID the value should be either a translation or an array of translations if the message | ||
* should support plural forms. | ||
* @param {Function} getPluralForm A function that returns the plural form index (a number). | ||
*/ | ||
export function add( language, translations ) { | ||
const dictionary = window.CKEDITOR_TRANSLATIONS[ language ] || ( window.CKEDITOR_TRANSLATIONS[ language ] = {} ); | ||
export function add( language, translations, getPluralForm ) { | ||
if ( !window.CKEDITOR_TRANSLATIONS[ language ] ) { | ||
window.CKEDITOR_TRANSLATIONS[ language ] = {}; | ||
} | ||
Object.assign( dictionary, translations ); | ||
const languageTranslations = window.CKEDITOR_TRANSLATIONS[ language ]; | ||
languageTranslations.dictionary = languageTranslations.dictionary || {}; | ||
languageTranslations.getPluralForm = getPluralForm || languageTranslations.getPluralForm; | ||
Object.assign( languageTranslations.dictionary, translations ); | ||
} | ||
/** | ||
* Translates string if the translation of the string was previously added to the dictionary. | ||
* See {@link module:utils/translation-service Translation Service}. | ||
* This happens in a multi-language mode were translation modules are created by the bundler. | ||
* **Note:** This method is internal, use {@link module:utils/locale~Locale#t the `t()` function} instead to translate | ||
* the editor UI parts. | ||
* | ||
* When no translation is defined in the dictionary or the dictionary doesn't exist this function returns | ||
* the original string without the `'[context: ]'` (happens in development and single-language modes). | ||
* This function is responsible for translating messages to the specified language. It uses translations added perviously | ||
* by {@link module:utils/translation-service~add} (a translations dictionary and the `getPluralForm()` function | ||
* to provide accurate translations of plural forms). | ||
* | ||
* In a single-language mode (when values passed to `t()` were replaced with target language strings) the dictionary | ||
* is left empty, so this function will return the original strings always. | ||
* When no translation is defined in the dictionary or the dictionary does not exist, this function returns | ||
* the original message string or the message plural depending on the number of elements. | ||
* | ||
* translate( 'pl', 'Cancel [context: reject]' ); | ||
* translate( 'pl', { string: 'Cancel' } ); // 'Cancel' | ||
* | ||
* The third optional argument is the number of elements, based on which the single form or one of the plural forms | ||
* should be picked when the message is supposed to support various plural forms. | ||
* | ||
* translate( 'en', { string: 'Add a space', plural: 'Add %0 spaces' }, 1 ); // 'Add a space' | ||
* translate( 'en', { string: 'Add a space', plural: 'Add %0 spaces' }, 3 ); // 'Add %0 spaces' | ||
* | ||
* The message should provide an ID using the `id` property when the message strings are not unique and their | ||
* translations should be different. | ||
* | ||
* translate( 'en', { string: 'image', id: 'ADD_IMAGE' } ); | ||
* translate( 'en', { string: 'image', id: 'AN_IMAGE' } ); | ||
* | ||
* @protected | ||
* @param {String} language Target language. | ||
* @param {String} translationKey String that will be translated. | ||
* @param {module:utils/translation-service~Message|String} message A message that will be translated. | ||
* @param {Number} [quantity] The number of elements for which a plural form should be picked from the target language dictionary. | ||
* @returns {String} Translated sentence. | ||
*/ | ||
export function translate( language, translationKey ) { | ||
export function _translate( language, message, quantity = 1 ) { | ||
if ( typeof quantity !== 'number' ) { | ||
/** | ||
* An incorrect value was passed to the translation function. This was probably caused | ||
* by an incorrect message interpolation of a plural form. Note that for messages supporting plural forms | ||
* the second argument of the `t()` function should always be a number or an array with a number as the first element. | ||
* | ||
* @error translation-service-quantity-not-a-number | ||
*/ | ||
throw new CKEditorError( 'translation-service-quantity-not-a-number: Expecting `quantity` to be a number.', null, { quantity } ); | ||
} | ||
const numberOfLanguages = getNumberOfLanguages(); | ||
@@ -78,10 +160,24 @@ | ||
if ( numberOfLanguages === 0 || !hasTranslation( language, translationKey ) ) { | ||
return translationKey.replace( / \[context: [^\]]+\]$/, '' ); | ||
const messageId = message.id || message.string; | ||
if ( numberOfLanguages === 0 || !hasTranslation( language, messageId ) ) { | ||
if ( quantity !== 1 ) { | ||
// Return the default plural form that was passed in the `message.plural` parameter. | ||
return message.plural; | ||
} | ||
return message.string; | ||
} | ||
const dictionary = window.CKEDITOR_TRANSLATIONS[ language ]; | ||
const dictionary = window.CKEDITOR_TRANSLATIONS[ language ].dictionary; | ||
const getPluralForm = window.CKEDITOR_TRANSLATIONS[ language ].getPluralForm || ( n => n === 1 ? 0 : 1 ); | ||
// In case of missing translations we still need to cut off the `[context: ]` parts. | ||
return dictionary[ translationKey ].replace( / \[context: [^\]]+\]$/, '' ); | ||
if ( typeof dictionary[ messageId ] === 'string' ) { | ||
return dictionary[ messageId ]; | ||
} | ||
const pluralFormIndex = Number( getPluralForm( quantity ) ); | ||
// Note: The `translate` function is not responsible for replacing `%0, %1, ...` with values. | ||
return dictionary[ messageId ][ pluralFormIndex ]; | ||
} | ||
@@ -99,6 +195,6 @@ | ||
// Checks whether the dictionary exists and translation in that dictionary exists. | ||
function hasTranslation( language, translationKey ) { | ||
function hasTranslation( language, messageId ) { | ||
return ( | ||
( language in window.CKEDITOR_TRANSLATIONS ) && | ||
( translationKey in window.CKEDITOR_TRANSLATIONS[ language ] ) | ||
!!window.CKEDITOR_TRANSLATIONS[ language ] && | ||
!!window.CKEDITOR_TRANSLATIONS[ language ].dictionary[ messageId ] | ||
); | ||
@@ -110,1 +206,16 @@ } | ||
} | ||
/** | ||
* The internationalization message interface. A message that implements this interface can be passed to the `t()` function | ||
* to be translated to the target UI language. | ||
* | ||
* @typedef {Object} module:utils/translation-service~Message | ||
* | ||
* @property {String} string The message string to translate. Acts as a default translation if the translation for a given language | ||
* is not defined. When the message is supposed to support plural forms, the string should be the English singular form of the message. | ||
* @property {String} [id] The message ID. If passed, the message ID is taken from this property instead of the `message.string`. | ||
* This property is useful when various messages share the same message string, for example, the `editor` string in `in the editor` | ||
* and `my editor` sentences. | ||
* @property {String} [plural] The plural form of the message. This property should be skipped when a message is not supposed | ||
* to support plural forms. Otherwise it should always be set to a string with the English plural form of the message. | ||
*/ |
@@ -14,3 +14,3 @@ /** | ||
const version = '18.0.0'; | ||
const version = '19.0.0'; | ||
@@ -123,2 +123,7 @@ /* istanbul ignore next */ | ||
* | ||
* **Note:** All official CKEditor 5 packages (excluding integrations and `ckeditor5-dev-*` packages) are released in the | ||
* same major version. This is — in the `x.y.z`, the `x` is the same for all packages. This is the simplest way to check | ||
* whether you use packages coming from the same CKEditor 5 version. You can read more about versioning in the | ||
* {@glink framework/guides/support/versioning-policy Versioning policy} guide. | ||
* | ||
* # Packages were duplicated in `node_modules` | ||
@@ -125,0 +130,0 @@ * |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
261949
6360