@ckeditor/ckeditor5-utils 35.0.0 → 35.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/package.json +5 -5
  2. package/src/areconnectedthroughproperties.js +75 -0
  3. package/src/ckeditorerror.js +195 -0
  4. package/src/collection.js +619 -0
  5. package/src/comparearrays.js +45 -0
  6. package/src/config.js +216 -0
  7. package/src/count.js +22 -0
  8. package/src/diff.js +113 -0
  9. package/src/difftochanges.js +76 -0
  10. package/src/dom/createelement.js +41 -0
  11. package/src/dom/emittermixin.js +301 -0
  12. package/src/dom/getancestors.js +27 -0
  13. package/src/dom/getborderwidths.js +24 -0
  14. package/src/dom/getcommonancestor.js +25 -0
  15. package/src/dom/getdatafromelement.js +20 -0
  16. package/src/dom/getpositionedancestor.js +23 -0
  17. package/src/dom/global.js +23 -0
  18. package/src/dom/indexof.js +21 -0
  19. package/src/dom/insertat.js +17 -0
  20. package/src/dom/iscomment.js +17 -0
  21. package/src/dom/isnode.js +24 -0
  22. package/src/dom/isrange.js +16 -0
  23. package/src/dom/istext.js +16 -0
  24. package/src/dom/isvisible.js +23 -0
  25. package/src/dom/iswindow.js +25 -0
  26. package/src/dom/position.js +328 -0
  27. package/src/dom/rect.js +364 -0
  28. package/src/dom/remove.js +18 -0
  29. package/src/dom/resizeobserver.js +145 -0
  30. package/src/dom/scroll.js +270 -0
  31. package/src/dom/setdatainelement.js +20 -0
  32. package/src/dom/tounit.js +25 -0
  33. package/src/elementreplacer.js +43 -0
  34. package/src/emittermixin.js +471 -0
  35. package/src/env.js +168 -0
  36. package/src/eventinfo.js +26 -0
  37. package/src/fastdiff.js +229 -0
  38. package/src/first.js +20 -0
  39. package/src/focustracker.js +103 -0
  40. package/src/index.js +36 -0
  41. package/src/inserttopriorityarray.js +21 -0
  42. package/src/isiterable.js +16 -0
  43. package/src/keyboard.js +222 -0
  44. package/src/keystrokehandler.js +114 -0
  45. package/src/language.js +20 -0
  46. package/src/locale.js +79 -0
  47. package/src/mapsequal.js +27 -0
  48. package/src/mix.js +44 -0
  49. package/src/nth.js +28 -0
  50. package/src/objecttomap.js +25 -0
  51. package/src/observablemixin.js +605 -0
  52. package/src/priorities.js +32 -0
  53. package/src/spy.js +22 -0
  54. package/src/toarray.js +7 -0
  55. package/src/tomap.js +27 -0
  56. package/src/translation-service.js +180 -0
  57. package/src/uid.js +55 -0
  58. package/src/unicode.js +91 -0
  59. package/src/version.js +148 -0
@@ -0,0 +1,229 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * Finds positions of the first and last change in the given string/array and generates a set of changes:
7
+ *
8
+ * fastDiff( '12a', '12xyza' );
9
+ * // [ { index: 2, type: 'insert', values: [ 'x', 'y', 'z' ] } ]
10
+ *
11
+ * fastDiff( '12a', '12aa' );
12
+ * // [ { index: 3, type: 'insert', values: [ 'a' ] } ]
13
+ *
14
+ * fastDiff( '12xyza', '12a' );
15
+ * // [ { index: 2, type: 'delete', howMany: 3 } ]
16
+ *
17
+ * fastDiff( [ '1', '2', 'a', 'a' ], [ '1', '2', 'a' ] );
18
+ * // [ { index: 3, type: 'delete', howMany: 1 } ]
19
+ *
20
+ * fastDiff( [ '1', '2', 'a', 'b', 'c', '3' ], [ '2', 'a', 'b' ] );
21
+ * // [ { index: 0, type: 'insert', values: [ '2', 'a', 'b' ] }, { index: 3, type: 'delete', howMany: 6 } ]
22
+ *
23
+ * Passed arrays can contain any type of data, however to compare them correctly custom comparator function
24
+ * should be passed as a third parameter:
25
+ *
26
+ * fastDiff( [ { value: 1 }, { value: 2 } ], [ { value: 1 }, { value: 3 } ], ( a, b ) => {
27
+ * return a.value === b.value;
28
+ * } );
29
+ * // [ { index: 1, type: 'insert', values: [ { value: 3 } ] }, { index: 2, type: 'delete', howMany: 1 } ]
30
+ *
31
+ * The resulted set of changes can be applied to the input in order to transform it into the output, for example:
32
+ *
33
+ * let input = '12abc3';
34
+ * const output = '2ab';
35
+ * const changes = fastDiff( input, output );
36
+ *
37
+ * changes.forEach( change => {
38
+ * if ( change.type == 'insert' ) {
39
+ * input = input.substring( 0, change.index ) + change.values.join( '' ) + input.substring( change.index );
40
+ * } else if ( change.type == 'delete' ) {
41
+ * input = input.substring( 0, change.index ) + input.substring( change.index + change.howMany );
42
+ * }
43
+ * } );
44
+ *
45
+ * // input equals output now
46
+ *
47
+ * or in case of arrays:
48
+ *
49
+ * let input = [ '1', '2', 'a', 'b', 'c', '3' ];
50
+ * const output = [ '2', 'a', 'b' ];
51
+ * const changes = fastDiff( input, output );
52
+ *
53
+ * changes.forEach( change => {
54
+ * if ( change.type == 'insert' ) {
55
+ * input = input.slice( 0, change.index ).concat( change.values, input.slice( change.index ) );
56
+ * } else if ( change.type == 'delete' ) {
57
+ * input = input.slice( 0, change.index ).concat( input.slice( change.index + change.howMany ) );
58
+ * }
59
+ * } );
60
+ *
61
+ * // input equals output now
62
+ *
63
+ * By passing `true` as the fourth parameter (`atomicChanges`) the output of this function will become compatible with
64
+ * the {@link module:utils/diff~diff `diff()`} function:
65
+ *
66
+ * fastDiff( '12a', '12xyza' );
67
+ * // [ 'equal', 'equal', 'insert', 'insert', 'insert', 'equal' ]
68
+ *
69
+ * The default output format of this function is compatible with the output format of
70
+ * {@link module:utils/difftochanges~diffToChanges `diffToChanges()`}. The `diffToChanges()` input format is, in turn,
71
+ * compatible with the output of {@link module:utils/diff~diff `diff()`}:
72
+ *
73
+ * const a = '1234';
74
+ * const b = '12xyz34';
75
+ *
76
+ * // Both calls will return the same results (grouped changes format).
77
+ * fastDiff( a, b );
78
+ * diffToChanges( diff( a, b ) );
79
+ *
80
+ * // Again, both calls will return the same results (atomic changes format).
81
+ * fastDiff( a, b, null, true );
82
+ * diff( a, b );
83
+ *
84
+ *
85
+ * @param {Array|String} a Input array or string.
86
+ * @param {Array|String} b Input array or string.
87
+ * @param {Function} [cmp] Optional function used to compare array values, by default `===` (strict equal operator) is used.
88
+ * @param {Boolean} [atomicChanges=false] Whether an array of `inset|delete|equal` operations should
89
+ * be returned instead of changes set. This makes this function compatible with {@link module:utils/diff~diff `diff()`}.
90
+ * @returns {Array} Array of changes.
91
+ */
92
+ export default function fastDiff(a, b, cmp, atomicChanges = false) {
93
+ // Set the comparator function.
94
+ cmp = cmp || function (a, b) {
95
+ return a === b;
96
+ };
97
+ // Convert the string (or any array-like object - eg. NodeList) to an array by using the slice() method because,
98
+ // unlike Array.from(), it returns array of UTF-16 code units instead of the code points of a string.
99
+ // One code point might be a surrogate pair of two code units. All text offsets are expected to be in code units.
100
+ // See ckeditor/ckeditor5#3147.
101
+ //
102
+ // We need to make sure here that fastDiff() works identical to diff().
103
+ const arrayA = Array.isArray(a) ? a : Array.prototype.slice.call(a);
104
+ const arrayB = Array.isArray(b) ? b : Array.prototype.slice.call(b);
105
+ // Find first and last change.
106
+ const changeIndexes = findChangeBoundaryIndexes(arrayA, arrayB, cmp);
107
+ // Transform into changes array.
108
+ return atomicChanges ? changeIndexesToAtomicChanges(changeIndexes, arrayB.length) : changeIndexesToChanges(arrayB, changeIndexes);
109
+ }
110
+ // Finds position of the first and last change in the given arrays. For example:
111
+ //
112
+ // const indexes = findChangeBoundaryIndexes( [ '1', '2', '3', '4' ], [ '1', '3', '4', '2', '4' ] );
113
+ // console.log( indexes ); // { firstIndex: 1, lastIndexOld: 3, lastIndexNew: 4 }
114
+ //
115
+ // The above indexes means that in the first array the modified part is `1[23]4` and in the second array it is `1[342]4`.
116
+ // Based on such indexes, array with `insert`/`delete` operations which allows transforming first value into the second one
117
+ // can be generated.
118
+ //
119
+ // @param {Array} arr1
120
+ // @param {Array} arr2
121
+ // @param {Function} cmp Comparator function.
122
+ // @returns {Object}
123
+ // @returns {Number} return.firstIndex Index of the first change in both values (always the same for both).
124
+ // @returns {Number} result.lastIndexOld Index of the last common value in `arr1`.
125
+ // @returns {Number} result.lastIndexNew Index of the last common value in `arr2`.
126
+ function findChangeBoundaryIndexes(arr1, arr2, cmp) {
127
+ // Find the first difference between passed values.
128
+ const firstIndex = findFirstDifferenceIndex(arr1, arr2, cmp);
129
+ // If arrays are equal return -1 indexes object.
130
+ if (firstIndex === -1) {
131
+ return { firstIndex: -1, lastIndexOld: -1, lastIndexNew: -1 };
132
+ }
133
+ // Remove the common part of each value and reverse them to make it simpler to find the last difference between them.
134
+ const oldArrayReversed = cutAndReverse(arr1, firstIndex);
135
+ const newArrayReversed = cutAndReverse(arr2, firstIndex);
136
+ // Find the first difference between reversed values.
137
+ // It should be treated as "how many elements from the end the last difference occurred".
138
+ //
139
+ // For example:
140
+ //
141
+ // initial -> after cut -> reversed:
142
+ // oldValue: '321ba' -> '21ba' -> 'ab12'
143
+ // newValue: '31xba' -> '1xba' -> 'abx1'
144
+ // lastIndex: -> 2
145
+ //
146
+ // So the last change occurred two characters from the end of the arrays.
147
+ const lastIndex = findFirstDifferenceIndex(oldArrayReversed, newArrayReversed, cmp);
148
+ // Use `lastIndex` to calculate proper offset, starting from the beginning (`lastIndex` kind of starts from the end).
149
+ const lastIndexOld = arr1.length - lastIndex;
150
+ const lastIndexNew = arr2.length - lastIndex;
151
+ return { firstIndex, lastIndexOld, lastIndexNew };
152
+ }
153
+ // Returns a first index on which given arrays differ. If both arrays are the same, -1 is returned.
154
+ //
155
+ // @param {Array} arr1
156
+ // @param {Array} arr2
157
+ // @param {Function} cmp Comparator function.
158
+ // @returns {Number}
159
+ function findFirstDifferenceIndex(arr1, arr2, cmp) {
160
+ for (let i = 0; i < Math.max(arr1.length, arr2.length); i++) {
161
+ if (arr1[i] === undefined || arr2[i] === undefined || !cmp(arr1[i], arr2[i])) {
162
+ return i;
163
+ }
164
+ }
165
+ return -1; // Return -1 if arrays are equal.
166
+ }
167
+ // Returns a copy of the given array with `howMany` elements removed starting from the beginning and in reversed order.
168
+ //
169
+ // @param {Array} arr Array to be processed.
170
+ // @param {Number} howMany How many elements from array beginning to remove.
171
+ // @returns {Array} Shortened and reversed array.
172
+ function cutAndReverse(arr, howMany) {
173
+ return arr.slice(howMany).reverse();
174
+ }
175
+ // Generates changes array based on change indexes from `findChangeBoundaryIndexes` function. This function will
176
+ // generate array with 0 (no changes), 1 (deletion or insertion) or 2 records (insertion and deletion).
177
+ //
178
+ // @param {Array} newArray New array for which change indexes were calculated.
179
+ // @param {Object} changeIndexes Change indexes object from `findChangeBoundaryIndexes` function.
180
+ // @returns {Array.<module:utils/difftochanges~Change>} Array of changes compatible with
181
+ // {@link module:utils/difftochanges~diffToChanges} format.
182
+ function changeIndexesToChanges(newArray, changeIndexes) {
183
+ const result = [];
184
+ const { firstIndex, lastIndexOld, lastIndexNew } = changeIndexes;
185
+ // Order operations as 'insert', 'delete' array to keep compatibility with {@link module:utils/difftochanges~diffToChanges}
186
+ // in most cases. However, 'diffToChanges' does not stick to any order so in some cases
187
+ // (for example replacing '12345' with 'abcd') it will generate 'delete', 'insert' order.
188
+ if (lastIndexNew - firstIndex > 0) {
189
+ result.push({
190
+ index: firstIndex,
191
+ type: 'insert',
192
+ values: newArray.slice(firstIndex, lastIndexNew)
193
+ });
194
+ }
195
+ if (lastIndexOld - firstIndex > 0) {
196
+ result.push({
197
+ index: firstIndex + (lastIndexNew - firstIndex),
198
+ type: 'delete',
199
+ howMany: lastIndexOld - firstIndex
200
+ });
201
+ }
202
+ return result;
203
+ }
204
+ // Generates array with set `equal|insert|delete` operations based on change indexes from `findChangeBoundaryIndexes` function.
205
+ //
206
+ // @param {Object} changeIndexes Change indexes object from `findChangeBoundaryIndexes` function.
207
+ // @param {Number} newLength Length of the new array on which `findChangeBoundaryIndexes` calculated change indexes.
208
+ // @returns {Array.<module:utils/diff~DiffResult>} Array of changes compatible with {@link module:utils/diff~diff} format.
209
+ function changeIndexesToAtomicChanges(changeIndexes, newLength) {
210
+ const { firstIndex, lastIndexOld, lastIndexNew } = changeIndexes;
211
+ // No changes.
212
+ if (firstIndex === -1) {
213
+ return Array(newLength).fill('equal');
214
+ }
215
+ let result = [];
216
+ if (firstIndex > 0) {
217
+ result = result.concat(Array(firstIndex).fill('equal'));
218
+ }
219
+ if (lastIndexNew - firstIndex > 0) {
220
+ result = result.concat(Array(lastIndexNew - firstIndex).fill('insert'));
221
+ }
222
+ if (lastIndexOld - firstIndex > 0) {
223
+ result = result.concat(Array(lastIndexOld - firstIndex).fill('delete'));
224
+ }
225
+ if (lastIndexNew < newLength) {
226
+ result = result.concat(Array(newLength - lastIndexNew).fill('equal'));
227
+ }
228
+ return result;
229
+ }
package/src/first.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module utils/first
7
+ */
8
+ /**
9
+ * Returns first item of the given `iterator`.
10
+ *
11
+ * @param {Iterator.<*>} iterator
12
+ * @returns {*}
13
+ */
14
+ export default function first(iterator) {
15
+ const iteratorItem = iterator.next();
16
+ if (iteratorItem.done) {
17
+ return null;
18
+ }
19
+ return iteratorItem.value;
20
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /* global setTimeout, clearTimeout */
6
+ /**
7
+ * @module utils/focustracker
8
+ */
9
+ import DomEmitterMixin from './dom/emittermixin';
10
+ import ObservableMixin from './observablemixin';
11
+ import CKEditorError from './ckeditorerror';
12
+ import mix from './mix';
13
+ /**
14
+ * Allows observing a group of `Element`s whether at least one of them is focused.
15
+ *
16
+ * Used by the {@link module:core/editor/editor~Editor} in order to track whether the focus is still within the application,
17
+ * or were used outside of its UI.
18
+ *
19
+ * **Note** `focus` and `blur` listeners use event capturing, so it is only needed to register wrapper `Element`
20
+ * which contain other `focusable` elements. But note that this wrapper element has to be focusable too
21
+ * (have e.g. `tabindex="-1"`).
22
+ *
23
+ * Check out the {@glink framework/guides/deep-dive/ui/focus-tracking "Deep dive into focus tracking" guide} to learn more.
24
+ *
25
+ * @mixes module:utils/dom/emittermixin~EmitterMixin
26
+ * @mixes module:utils/observablemixin~ObservableMixin
27
+ */
28
+ class FocusTracker {
29
+ constructor() {
30
+ this.set('isFocused', false);
31
+ this.set('focusedElement', null);
32
+ this._elements = new Set();
33
+ this._nextEventLoopTimeout = null;
34
+ }
35
+ /**
36
+ * Starts tracking the specified element.
37
+ *
38
+ * @param {Element} element
39
+ */
40
+ add(element) {
41
+ if (this._elements.has(element)) {
42
+ /**
43
+ * This element is already tracked by {@link module:utils/focustracker~FocusTracker}.
44
+ *
45
+ * @error focustracker-add-element-already-exist
46
+ */
47
+ throw new CKEditorError('focustracker-add-element-already-exist', this);
48
+ }
49
+ this.listenTo(element, 'focus', () => this._focus(element), { useCapture: true });
50
+ this.listenTo(element, 'blur', () => this._blur(), { useCapture: true });
51
+ this._elements.add(element);
52
+ }
53
+ /**
54
+ * Stops tracking the specified element and stops listening on this element.
55
+ *
56
+ * @param {Element} element
57
+ */
58
+ remove(element) {
59
+ if (element === this.focusedElement) {
60
+ this._blur();
61
+ }
62
+ if (this._elements.has(element)) {
63
+ this.stopListening(element);
64
+ this._elements.delete(element);
65
+ }
66
+ }
67
+ /**
68
+ * Destroys the focus tracker by:
69
+ * - Disabling all event listeners attached to tracked elements.
70
+ * - Removing all tracked elements that were previously added.
71
+ */
72
+ destroy() {
73
+ this.stopListening();
74
+ }
75
+ /**
76
+ * Stores currently focused element and set {#isFocused} as `true`.
77
+ *
78
+ * @private
79
+ * @param {Element} element Element which has been focused.
80
+ */
81
+ _focus(element) {
82
+ clearTimeout(this._nextEventLoopTimeout);
83
+ this.focusedElement = element;
84
+ this.isFocused = true;
85
+ }
86
+ /**
87
+ * Clears currently focused element and set {@link #isFocused} as `false`.
88
+ * This method uses `setTimeout` to change order of fires `blur` and `focus` events.
89
+ *
90
+ * @private
91
+ * @fires blur
92
+ */
93
+ _blur() {
94
+ clearTimeout(this._nextEventLoopTimeout);
95
+ this._nextEventLoopTimeout = setTimeout(() => {
96
+ this.focusedElement = null;
97
+ this.isFocused = false;
98
+ }, 0);
99
+ }
100
+ }
101
+ mix(FocusTracker, DomEmitterMixin);
102
+ mix(FocusTracker, ObservableMixin);
103
+ export default FocusTracker;
package/src/index.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module utils
7
+ */
8
+ export { default as env } from './env';
9
+ export { default as diff } from './diff';
10
+ export { default as mix } from './mix';
11
+ export { default as EmitterMixin } from './emittermixin';
12
+ export { default as ObservableMixin } from './observablemixin';
13
+ export { default as CKEditorError, logError, logWarning } from './ckeditorerror';
14
+ export { default as ElementReplacer } from './elementreplacer';
15
+ export { default as createElement } from './dom/createelement';
16
+ export { default as DomEmitterMixin } from './dom/emittermixin';
17
+ export { default as global } from './dom/global';
18
+ export { default as getDataFromElement } from './dom/getdatafromelement';
19
+ export { default as Rect } from './dom/rect';
20
+ export { default as ResizeObserver } from './dom/resizeobserver';
21
+ export { default as setDataInElement } from './dom/setdatainelement';
22
+ export { default as toUnit } from './dom/tounit';
23
+ export { default as isVisible } from './dom/isvisible';
24
+ export * from './dom/scroll';
25
+ export * from './keyboard';
26
+ export * from './language';
27
+ export { default as Locale } from './locale';
28
+ export { default as Collection } from './collection';
29
+ export { default as first } from './first';
30
+ export { default as FocusTracker } from './focustracker';
31
+ export { default as KeystrokeHandler } from './keystrokehandler';
32
+ export { default as toArray } from './toarray';
33
+ export { default as toMap } from './tomap';
34
+ export { default as priorities } from './priorities';
35
+ export { default as uid } from './uid';
36
+ export { default as version } from './version';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ import priorities from './priorities';
6
+ /**
7
+ * Inserts any object with priority at correct index by priority so registered objects are always sorted from highest to lowest priority.
8
+ *
9
+ * @param {Array.<module:utils/inserttopriorityarray~ObjectWithPriority>} objects Array of objects with priority to insert object to.
10
+ * @param {module:utils/inserttopriorityarray~ObjectWithPriority} objectToInsert Object with `priority` property.
11
+ */
12
+ export default function insertToPriorityArray(objects, objectToInsert) {
13
+ const priority = priorities.get(objectToInsert.priority);
14
+ for (let i = 0; i < objects.length; i++) {
15
+ if (priorities.get(objects[i].priority) < priority) {
16
+ objects.splice(i, 0, objectToInsert);
17
+ return;
18
+ }
19
+ }
20
+ objects.push(objectToInsert);
21
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module utils/isiterable
7
+ */
8
+ /**
9
+ * Checks if value implements iterator interface.
10
+ *
11
+ * @param {*} value The value to check.
12
+ * @returns {Boolean} True if value implements iterator interface.
13
+ */
14
+ export default function isIterable(value) {
15
+ return !!(value && value[Symbol.iterator]);
16
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ import CKEditorError from './ckeditorerror';
6
+ import env from './env';
7
+ const modifiersToGlyphsMac = {
8
+ ctrl: '⌃',
9
+ cmd: '⌘',
10
+ alt: '⌥',
11
+ shift: '⇧'
12
+ };
13
+ const modifiersToGlyphsNonMac = {
14
+ ctrl: 'Ctrl+',
15
+ alt: 'Alt+',
16
+ shift: 'Shift+'
17
+ };
18
+ /**
19
+ * An object with `keyName => keyCode` pairs for a set of known keys.
20
+ *
21
+ * Contains:
22
+ *
23
+ * * `a-z`,
24
+ * * `0-9`,
25
+ * * `f1-f12`,
26
+ * * `` ` ``, `-`, `=`, `[`, `]`, `;`, `'`, `,`, `.`, `/`, `\`,
27
+ * * `arrow(left|up|right|bottom)`,
28
+ * * `backspace`, `delete`, `enter`, `esc`, `tab`,
29
+ * * `ctrl`, `cmd`, `shift`, `alt`.
30
+ */
31
+ export const keyCodes = generateKnownKeyCodes();
32
+ const keyCodeNames = Object.fromEntries(Object.entries(keyCodes).map(([name, code]) => [code, name.charAt(0).toUpperCase() + name.slice(1)]));
33
+ /**
34
+ * Converts a key name or {@link module:utils/keyboard~KeystrokeInfo keystroke info} into a key code.
35
+ *
36
+ * Note: Key names are matched with {@link module:utils/keyboard~keyCodes} in a case-insensitive way.
37
+ *
38
+ * @param {String|module:utils/keyboard~KeystrokeInfo} A key name (see {@link module:utils/keyboard~keyCodes})
39
+ * or a keystroke data object.
40
+ * @returns {Number} Key or keystroke code.
41
+ */
42
+ export function getCode(key) {
43
+ let keyCode;
44
+ if (typeof key == 'string') {
45
+ keyCode = keyCodes[key.toLowerCase()];
46
+ if (!keyCode) {
47
+ /**
48
+ * Unknown key name. Only key names included in the {@link module:utils/keyboard~keyCodes} can be used.
49
+ *
50
+ * @error keyboard-unknown-key
51
+ * @param {String} key
52
+ */
53
+ throw new CKEditorError('keyboard-unknown-key', null, { key });
54
+ }
55
+ }
56
+ else {
57
+ keyCode = key.keyCode +
58
+ (key.altKey ? keyCodes.alt : 0) +
59
+ (key.ctrlKey ? keyCodes.ctrl : 0) +
60
+ (key.shiftKey ? keyCodes.shift : 0) +
61
+ (key.metaKey ? keyCodes.cmd : 0);
62
+ }
63
+ return keyCode;
64
+ }
65
+ /**
66
+ * Parses the keystroke and returns a keystroke code that will match the code returned by
67
+ * {@link module:utils/keyboard~getCode} for the corresponding {@link module:utils/keyboard~KeystrokeInfo keystroke info}.
68
+ *
69
+ * The keystroke can be passed in two formats:
70
+ *
71
+ * * as a single string – e.g. `ctrl + A`,
72
+ * * as an array of {@link module:utils/keyboard~keyCodes known key names} and key codes – e.g.:
73
+ * * `[ 'ctrl', 32 ]` (ctrl + space),
74
+ * * `[ 'ctrl', 'a' ]` (ctrl + A).
75
+ *
76
+ * Note: Key names are matched with {@link module:utils/keyboard~keyCodes} in a case-insensitive way.
77
+ *
78
+ * Note: Only keystrokes with a single non-modifier key are supported (e.g. `ctrl+A` is OK, but `ctrl+A+B` is not).
79
+ *
80
+ * Note: On macOS, keystroke handling is translating the `Ctrl` key to the `Cmd` key and handling only that keystroke.
81
+ * For example, a registered keystroke `Ctrl+A` will be translated to `Cmd+A` on macOS. To disable the translation of some keystroke,
82
+ * use the forced modifier: `Ctrl!+A` (note the exclamation mark).
83
+ *
84
+ * @param {String|Array.<Number|String>} keystroke The keystroke definition.
85
+ * @returns {Number} Keystroke code.
86
+ */
87
+ export function parseKeystroke(keystroke) {
88
+ if (typeof keystroke == 'string') {
89
+ keystroke = splitKeystrokeText(keystroke);
90
+ }
91
+ return keystroke
92
+ .map(key => (typeof key == 'string') ? getEnvKeyCode(key) : key)
93
+ .reduce((key, sum) => sum + key, 0);
94
+ }
95
+ /**
96
+ * Translates any keystroke string text like `"Ctrl+A"` to an
97
+ * environment–specific keystroke, i.e. `"⌘A"` on macOS.
98
+ *
99
+ * @param {String} keystroke The keystroke text.
100
+ * @returns {String} The keystroke text specific for the environment.
101
+ */
102
+ export function getEnvKeystrokeText(keystroke) {
103
+ let keystrokeCode = parseKeystroke(keystroke);
104
+ const modifiersToGlyphs = Object.entries(env.isMac ? modifiersToGlyphsMac : modifiersToGlyphsNonMac);
105
+ const modifiers = modifiersToGlyphs.reduce((modifiers, [name, glyph]) => {
106
+ // Modifier keys are stored as a bit mask so extract those from the keystroke code.
107
+ if ((keystrokeCode & keyCodes[name]) != 0) {
108
+ keystrokeCode &= ~keyCodes[name];
109
+ modifiers += glyph;
110
+ }
111
+ return modifiers;
112
+ }, '');
113
+ return modifiers + (keystrokeCode ? keyCodeNames[keystrokeCode] : '');
114
+ }
115
+ /**
116
+ * Returns `true` if the provided key code represents one of the arrow keys.
117
+ *
118
+ * @param {Number} keyCode A key code as in {@link module:utils/keyboard~KeystrokeInfo#keyCode}.
119
+ * @returns {Boolean}
120
+ */
121
+ export function isArrowKeyCode(keyCode) {
122
+ return keyCode == keyCodes.arrowright ||
123
+ keyCode == keyCodes.arrowleft ||
124
+ keyCode == keyCodes.arrowup ||
125
+ keyCode == keyCodes.arrowdown;
126
+ }
127
+ /**
128
+ * Returns the direction in which the {@link module:engine/model/documentselection~DocumentSelection selection}
129
+ * will move when the provided arrow key code is pressed considering the language direction of the editor content.
130
+ *
131
+ * For instance, in right–to–left (RTL) content languages, pressing the left arrow means moving the selection right (forward)
132
+ * in the model structure. Similarly, pressing the right arrow moves the selection left (backward).
133
+ *
134
+ * @param {Number} keyCode A key code as in {@link module:utils/keyboard~KeystrokeInfo#keyCode}.
135
+ * @param {module:utils/language~LanguageDirection} contentLanguageDirection The content language direction, corresponding to
136
+ * {@link module:utils/locale~Locale#contentLanguageDirection}.
137
+ * @returns {module:utils/keyboard~ArrowKeyCodeDirection|undefined} Localized arrow direction or `undefined` for non-arrow key codes.
138
+ */
139
+ export function getLocalizedArrowKeyCodeDirection(keyCode, contentLanguageDirection) {
140
+ const isLtrContent = contentLanguageDirection === 'ltr';
141
+ switch (keyCode) {
142
+ case keyCodes.arrowleft:
143
+ return isLtrContent ? 'left' : 'right';
144
+ case keyCodes.arrowright:
145
+ return isLtrContent ? 'right' : 'left';
146
+ case keyCodes.arrowup:
147
+ return 'up';
148
+ case keyCodes.arrowdown:
149
+ return 'down';
150
+ }
151
+ }
152
+ // Converts a key name to the key code with mapping based on the env.
153
+ //
154
+ // See: {@link module:utils/keyboard~getCode}.
155
+ //
156
+ // @param {String} key The key name (see {@link module:utils/keyboard~keyCodes}).
157
+ // @returns {Number} Key code.
158
+ function getEnvKeyCode(key) {
159
+ // Don't remap modifier key for forced modifiers.
160
+ if (key.endsWith('!')) {
161
+ return getCode(key.slice(0, -1));
162
+ }
163
+ const code = getCode(key);
164
+ return env.isMac && code == keyCodes.ctrl ? keyCodes.cmd : code;
165
+ }
166
+ /**
167
+ * Determines if the provided key code moves the {@link module:engine/model/documentselection~DocumentSelection selection}
168
+ * forward or backward considering the language direction of the editor content.
169
+ *
170
+ * For instance, in right–to–left (RTL) languages, pressing the left arrow means moving forward
171
+ * in the model structure. Similarly, pressing the right arrow moves the selection backward.
172
+ *
173
+ * @param {Number} keyCode A key code as in {@link module:utils/keyboard~KeystrokeInfo#keyCode}.
174
+ * @param {module:utils/language~LanguageDirection} contentLanguageDirection The content language direction, corresponding to
175
+ * {@link module:utils/locale~Locale#contentLanguageDirection}.
176
+ * @returns {Boolean}
177
+ */
178
+ export function isForwardArrowKeyCode(keyCode, contentLanguageDirection) {
179
+ const localizedKeyCodeDirection = getLocalizedArrowKeyCodeDirection(keyCode, contentLanguageDirection);
180
+ return localizedKeyCodeDirection === 'down' || localizedKeyCodeDirection === 'right';
181
+ }
182
+ function generateKnownKeyCodes() {
183
+ const keyCodes = {
184
+ arrowleft: 37,
185
+ arrowup: 38,
186
+ arrowright: 39,
187
+ arrowdown: 40,
188
+ backspace: 8,
189
+ delete: 46,
190
+ enter: 13,
191
+ space: 32,
192
+ esc: 27,
193
+ tab: 9,
194
+ // The idea about these numbers is that they do not collide with any real key codes, so we can use them
195
+ // like bit masks.
196
+ ctrl: 0x110000,
197
+ shift: 0x220000,
198
+ alt: 0x440000,
199
+ cmd: 0x880000
200
+ };
201
+ // a-z
202
+ for (let code = 65; code <= 90; code++) {
203
+ const letter = String.fromCharCode(code);
204
+ keyCodes[letter.toLowerCase()] = code;
205
+ }
206
+ // 0-9
207
+ for (let code = 48; code <= 57; code++) {
208
+ keyCodes[code - 48] = code;
209
+ }
210
+ // F1-F12
211
+ for (let code = 112; code <= 123; code++) {
212
+ keyCodes['f' + (code - 111)] = code;
213
+ }
214
+ // other characters
215
+ for (const char of '`-=[];\',./\\') {
216
+ keyCodes[char] = char.charCodeAt(0);
217
+ }
218
+ return keyCodes;
219
+ }
220
+ function splitKeystrokeText(keystroke) {
221
+ return keystroke.split('+').map(key => key.trim());
222
+ }