Socket
Socket
Sign inDemoInstall

@ckeditor/ckeditor5-utils

Package Overview
Dependencies
Maintainers
1
Versions
647
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ckeditor/ckeditor5-utils - npm Package Compare versions

Comparing version 18.0.0 to 19.0.0

30

CHANGELOG.md
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 @@

10

package.json
{
"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 &mdash; 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 @@ *

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc