@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,605 @@
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
+ /* eslint-disable @typescript-eslint/unified-signatures */
6
+ /**
7
+ * @module utils/observablemixin
8
+ */
9
+ import EmitterMixin from './emittermixin';
10
+ import CKEditorError from './ckeditorerror';
11
+ import { isObject } from 'lodash-es';
12
+ const observablePropertiesSymbol = Symbol('observableProperties');
13
+ const boundObservablesSymbol = Symbol('boundObservables');
14
+ const boundPropertiesSymbol = Symbol('boundProperties');
15
+ const decoratedMethods = Symbol('decoratedMethods');
16
+ const decoratedOriginal = Symbol('decoratedOriginal');
17
+ /**
18
+ * A mixin that injects the "observable properties" and data binding functionality described in the
19
+ * {@link ~Observable} interface.
20
+ *
21
+ * Read more about the concept of observables in the:
22
+ * * {@glink framework/guides/architecture/core-editor-architecture#event-system-and-observables Event system and observables}
23
+ * section of the {@glink framework/guides/architecture/core-editor-architecture Core editor architecture} guide,
24
+ * * {@glink framework/guides/deep-dive/observables Observables deep dive} guide.
25
+ *
26
+ * @mixin ObservableMixin
27
+ * @mixes module:utils/emittermixin~EmitterMixin
28
+ * @implements module:utils/observablemixin~Observable
29
+ */
30
+ const ObservableMixin = {
31
+ /**
32
+ * @inheritDoc
33
+ */
34
+ set(name, value) {
35
+ // If the first parameter is an Object, iterate over its properties.
36
+ if (isObject(name)) {
37
+ Object.keys(name).forEach(property => {
38
+ this.set(property, name[property]);
39
+ }, this);
40
+ return;
41
+ }
42
+ initObservable(this);
43
+ const properties = this[observablePropertiesSymbol];
44
+ if ((name in this) && !properties.has(name)) {
45
+ /**
46
+ * Cannot override an existing property.
47
+ *
48
+ * This error is thrown when trying to {@link ~Observable#set set} a property with
49
+ * a name of an already existing property. For example:
50
+ *
51
+ * let observable = new Model();
52
+ * observable.property = 1;
53
+ * observable.set( 'property', 2 ); // throws
54
+ *
55
+ * observable.set( 'property', 1 );
56
+ * observable.set( 'property', 2 ); // ok, because this is an existing property.
57
+ *
58
+ * @error observable-set-cannot-override
59
+ */
60
+ throw new CKEditorError('observable-set-cannot-override', this);
61
+ }
62
+ Object.defineProperty(this, name, {
63
+ enumerable: true,
64
+ configurable: true,
65
+ get() {
66
+ return properties.get(name);
67
+ },
68
+ set(value) {
69
+ const oldValue = properties.get(name);
70
+ // Fire `set` event before the new value will be set to make it possible
71
+ // to override observable property without affecting `change` event.
72
+ // See https://github.com/ckeditor/ckeditor5-utils/issues/171.
73
+ let newValue = this.fire('set:' + name, name, value, oldValue);
74
+ if (newValue === undefined) {
75
+ newValue = value;
76
+ }
77
+ // Allow undefined as an initial value like A.define( 'x', undefined ) (#132).
78
+ // Note: When properties map has no such own property, then its value is undefined.
79
+ if (oldValue !== newValue || !properties.has(name)) {
80
+ properties.set(name, newValue);
81
+ this.fire('change:' + name, name, newValue, oldValue);
82
+ }
83
+ }
84
+ });
85
+ this[name] = value;
86
+ },
87
+ /**
88
+ * @inheritDoc
89
+ */
90
+ bind(...bindProperties) {
91
+ if (!bindProperties.length || !isStringArray(bindProperties)) {
92
+ /**
93
+ * All properties must be strings.
94
+ *
95
+ * @error observable-bind-wrong-properties
96
+ */
97
+ throw new CKEditorError('observable-bind-wrong-properties', this);
98
+ }
99
+ if ((new Set(bindProperties)).size !== bindProperties.length) {
100
+ /**
101
+ * Properties must be unique.
102
+ *
103
+ * @error observable-bind-duplicate-properties
104
+ */
105
+ throw new CKEditorError('observable-bind-duplicate-properties', this);
106
+ }
107
+ initObservable(this);
108
+ const boundProperties = this[boundPropertiesSymbol];
109
+ bindProperties.forEach(propertyName => {
110
+ if (boundProperties.has(propertyName)) {
111
+ /**
112
+ * Cannot bind the same property more than once.
113
+ *
114
+ * @error observable-bind-rebind
115
+ */
116
+ throw new CKEditorError('observable-bind-rebind', this);
117
+ }
118
+ });
119
+ const bindings = new Map();
120
+ // @typedef {Object} Binding
121
+ // @property {Array} property Property which is bound.
122
+ // @property {Array} to Array of observable–property components of the binding (`{ observable: ..., property: .. }`).
123
+ // @property {Array} callback A function which processes `to` components.
124
+ bindProperties.forEach(a => {
125
+ const binding = { property: a, to: [] };
126
+ boundProperties.set(a, binding);
127
+ bindings.set(a, binding);
128
+ });
129
+ // @typedef {Object} BindChain
130
+ // @property {Function} to See {@link ~ObservableMixin#_bindTo}.
131
+ // @property {Function} toMany See {@link ~ObservableMixin#_bindToMany}.
132
+ // @property {module:utils/observablemixin~Observable} _observable The observable which initializes the binding.
133
+ // @property {Array} _bindProperties Array of `_observable` properties to be bound.
134
+ // @property {Array} _to Array of `to()` observable–properties (`{ observable: toObservable, properties: ...toProperties }`).
135
+ // @property {Map} _bindings Stores bindings to be kept in
136
+ // {@link ~ObservableMixin#_boundProperties}/{@link ~ObservableMixin#_boundObservables}
137
+ // initiated in this binding chain.
138
+ return {
139
+ to: bindTo,
140
+ toMany: bindToMany,
141
+ _observable: this,
142
+ _bindProperties: bindProperties,
143
+ _to: [],
144
+ _bindings: bindings
145
+ };
146
+ },
147
+ /**
148
+ * @inheritDoc
149
+ */
150
+ unbind(...unbindProperties) {
151
+ // Nothing to do here if not inited yet.
152
+ if (!(this[observablePropertiesSymbol])) {
153
+ return;
154
+ }
155
+ const boundProperties = this[boundPropertiesSymbol];
156
+ const boundObservables = this[boundObservablesSymbol];
157
+ if (unbindProperties.length) {
158
+ if (!isStringArray(unbindProperties)) {
159
+ /**
160
+ * Properties must be strings.
161
+ *
162
+ * @error observable-unbind-wrong-properties
163
+ */
164
+ throw new CKEditorError('observable-unbind-wrong-properties', this);
165
+ }
166
+ unbindProperties.forEach(propertyName => {
167
+ const binding = boundProperties.get(propertyName);
168
+ // Nothing to do if the binding is not defined
169
+ if (!binding) {
170
+ return;
171
+ }
172
+ binding.to.forEach(([toObservable, toProperty]) => {
173
+ const toProperties = boundObservables.get(toObservable);
174
+ const toPropertyBindings = toProperties[toProperty];
175
+ toPropertyBindings.delete(binding);
176
+ if (!toPropertyBindings.size) {
177
+ delete toProperties[toProperty];
178
+ }
179
+ if (!Object.keys(toProperties).length) {
180
+ boundObservables.delete(toObservable);
181
+ this.stopListening(toObservable, 'change');
182
+ }
183
+ });
184
+ boundProperties.delete(propertyName);
185
+ });
186
+ }
187
+ else {
188
+ boundObservables.forEach((bindings, boundObservable) => {
189
+ this.stopListening(boundObservable, 'change');
190
+ });
191
+ boundObservables.clear();
192
+ boundProperties.clear();
193
+ }
194
+ },
195
+ /**
196
+ * @inheritDoc
197
+ */
198
+ decorate(methodName) {
199
+ initObservable(this);
200
+ const originalMethod = this[methodName];
201
+ if (!originalMethod) {
202
+ /**
203
+ * Cannot decorate an undefined method.
204
+ *
205
+ * @error observablemixin-cannot-decorate-undefined
206
+ * @param {Object} object The object which method should be decorated.
207
+ * @param {String} methodName Name of the method which does not exist.
208
+ */
209
+ throw new CKEditorError('observablemixin-cannot-decorate-undefined', this, { object: this, methodName });
210
+ }
211
+ this.on(methodName, (evt, args) => {
212
+ evt.return = originalMethod.apply(this, args);
213
+ });
214
+ this[methodName] = function (...args) {
215
+ return this.fire(methodName, args);
216
+ };
217
+ this[methodName][decoratedOriginal] = originalMethod;
218
+ if (!this[decoratedMethods]) {
219
+ this[decoratedMethods] = [];
220
+ }
221
+ this[decoratedMethods].push(methodName);
222
+ },
223
+ ...EmitterMixin
224
+ };
225
+ // Override the EmitterMixin stopListening method to be able to clean (and restore) decorated methods.
226
+ // This is needed in case of:
227
+ // 1. Have x.foo() decorated.
228
+ // 2. Call x.stopListening()
229
+ // 3. Call x.foo(). Problem: nothing happens (the original foo() method is not executed)
230
+ ObservableMixin.stopListening = function (emitter, event, callback) {
231
+ // Removing all listeners so let's clean the decorated methods to the original state.
232
+ if (!emitter && this[decoratedMethods]) {
233
+ for (const methodName of this[decoratedMethods]) {
234
+ this[methodName] = this[methodName][decoratedOriginal];
235
+ }
236
+ delete this[decoratedMethods];
237
+ }
238
+ EmitterMixin.stopListening.call(this, emitter, event, callback);
239
+ };
240
+ export default ObservableMixin;
241
+ // Init symbol properties needed for the observable mechanism to work.
242
+ //
243
+ // @private
244
+ // @param {module:utils/observablemixin~ObservableMixin} observable
245
+ function initObservable(observable) {
246
+ // Do nothing if already inited.
247
+ if (observable[observablePropertiesSymbol]) {
248
+ return;
249
+ }
250
+ // The internal hash containing the observable's state.
251
+ //
252
+ // @private
253
+ // @type {Map}
254
+ Object.defineProperty(observable, observablePropertiesSymbol, {
255
+ value: new Map()
256
+ });
257
+ // Map containing bindings to external observables. It shares the binding objects
258
+ // (`{ observable: A, property: 'a', to: ... }`) with {@link module:utils/observablemixin~ObservableMixin#_boundProperties} and
259
+ // it is used to observe external observables to update own properties accordingly.
260
+ // See {@link module:utils/observablemixin~ObservableMixin#bind}.
261
+ //
262
+ // A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
263
+ // console.log( A._boundObservables );
264
+ //
265
+ // Map( {
266
+ // B: {
267
+ // x: Set( [
268
+ // { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
269
+ // { observable: A, property: 'c', to: [ [ B, 'x' ] ] }
270
+ // ] ),
271
+ // y: Set( [
272
+ // { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
273
+ // ] )
274
+ // }
275
+ // } )
276
+ //
277
+ // A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
278
+ // console.log( A._boundObservables );
279
+ //
280
+ // Map( {
281
+ // B: {
282
+ // x: Set( [
283
+ // { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
284
+ // { observable: A, property: 'c', to: [ [ B, 'x' ] ] }
285
+ // ] ),
286
+ // y: Set( [
287
+ // { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
288
+ // ] ),
289
+ // z: Set( [
290
+ // { observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
291
+ // ] )
292
+ // },
293
+ // C: {
294
+ // w: Set( [
295
+ // { observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
296
+ // ] )
297
+ // }
298
+ // } )
299
+ //
300
+ // @private
301
+ // @type {Map}
302
+ Object.defineProperty(observable, boundObservablesSymbol, {
303
+ value: new Map()
304
+ });
305
+ // Object that stores which properties of this observable are bound and how. It shares
306
+ // the binding objects (`{ observable: A, property: 'a', to: ... }`) with
307
+ // {@link module:utils/observablemixin~ObservableMixin#_boundObservables}. This data structure is
308
+ // a reverse of {@link module:utils/observablemixin~ObservableMixin#_boundObservables} and it is helpful for
309
+ // {@link module:utils/observablemixin~ObservableMixin#unbind}.
310
+ //
311
+ // See {@link module:utils/observablemixin~ObservableMixin#bind}.
312
+ //
313
+ // A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
314
+ // console.log( A._boundProperties );
315
+ //
316
+ // Map( {
317
+ // a: { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
318
+ // b: { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
319
+ // c: { observable: A, property: 'c', to: [ [ B, 'x' ] ] }
320
+ // } )
321
+ //
322
+ // A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
323
+ // console.log( A._boundProperties );
324
+ //
325
+ // Map( {
326
+ // a: { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
327
+ // b: { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
328
+ // c: { observable: A, property: 'c', to: [ [ B, 'x' ] ] },
329
+ // d: { observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
330
+ // } )
331
+ //
332
+ // @private
333
+ // @type {Map}
334
+ Object.defineProperty(observable, boundPropertiesSymbol, {
335
+ value: new Map()
336
+ });
337
+ }
338
+ // A chaining for {@link module:utils/observablemixin~ObservableMixin#bind} providing `.to()` interface.
339
+ //
340
+ // @private
341
+ // @param {...[Observable|String|Function]} args Arguments of the `.to( args )` binding.
342
+ function bindTo(...args) {
343
+ const parsedArgs = parseBindToArgs(...args);
344
+ const bindingsKeys = Array.from(this._bindings.keys());
345
+ const numberOfBindings = bindingsKeys.length;
346
+ // Eliminate A.bind( 'x' ).to( B, C )
347
+ if (!parsedArgs.callback && parsedArgs.to.length > 1) {
348
+ /**
349
+ * Binding multiple observables only possible with callback.
350
+ *
351
+ * @error observable-bind-to-no-callback
352
+ */
353
+ throw new CKEditorError('observable-bind-to-no-callback', this);
354
+ }
355
+ // Eliminate A.bind( 'x', 'y' ).to( B, callback )
356
+ if (numberOfBindings > 1 && parsedArgs.callback) {
357
+ /**
358
+ * Cannot bind multiple properties and use a callback in one binding.
359
+ *
360
+ * @error observable-bind-to-extra-callback
361
+ */
362
+ throw new CKEditorError('observable-bind-to-extra-callback', this);
363
+ }
364
+ parsedArgs.to.forEach(to => {
365
+ // Eliminate A.bind( 'x', 'y' ).to( B, 'a' )
366
+ if (to.properties.length && to.properties.length !== numberOfBindings) {
367
+ /**
368
+ * The number of properties must match.
369
+ *
370
+ * @error observable-bind-to-properties-length
371
+ */
372
+ throw new CKEditorError('observable-bind-to-properties-length', this);
373
+ }
374
+ // When no to.properties specified, observing source properties instead i.e.
375
+ // A.bind( 'x', 'y' ).to( B ) -> Observe B.x and B.y
376
+ if (!to.properties.length) {
377
+ to.properties = this._bindProperties;
378
+ }
379
+ });
380
+ this._to = parsedArgs.to;
381
+ // Fill {@link BindChain#_bindings} with callback. When the callback is set there's only one binding.
382
+ if (parsedArgs.callback) {
383
+ this._bindings.get(bindingsKeys[0]).callback = parsedArgs.callback;
384
+ }
385
+ attachBindToListeners(this._observable, this._to);
386
+ // Update observable._boundProperties and observable._boundObservables.
387
+ updateBindToBound(this);
388
+ // Set initial values of bound properties.
389
+ this._bindProperties.forEach(propertyName => {
390
+ updateBoundObservableProperty(this._observable, propertyName);
391
+ });
392
+ }
393
+ // Binds to an attribute in a set of iterable observables.
394
+ //
395
+ // @private
396
+ // @param {Array.<Observable>} observables
397
+ // @param {String} attribute
398
+ // @param {Function} callback
399
+ function bindToMany(observables, attribute, callback) {
400
+ if (this._bindings.size > 1) {
401
+ /**
402
+ * Binding one attribute to many observables only possible with one attribute.
403
+ *
404
+ * @error observable-bind-to-many-not-one-binding
405
+ */
406
+ throw new CKEditorError('observable-bind-to-many-not-one-binding', this);
407
+ }
408
+ this.to(
409
+ // Bind to #attribute of each observable...
410
+ ...getBindingTargets(observables, attribute),
411
+ // ...using given callback to parse attribute values.
412
+ callback);
413
+ }
414
+ // Returns an array of binding components for
415
+ // {@link Observable#bind} from a set of iterable observables.
416
+ //
417
+ // @param {Array.<Observable>} observables
418
+ // @param {String} attribute
419
+ // @returns {Array.<String|Observable>}
420
+ function getBindingTargets(observables, attribute) {
421
+ const observableAndAttributePairs = observables.map(observable => [observable, attribute]);
422
+ // Merge pairs to one-dimension array of observables and attributes.
423
+ return Array.prototype.concat.apply([], observableAndAttributePairs);
424
+ }
425
+ // Check if all entries of the array are of `String` type.
426
+ //
427
+ // @private
428
+ // @param {Array} arr An array to be checked.
429
+ // @returns {Boolean}
430
+ function isStringArray(arr) {
431
+ return arr.every(a => typeof a == 'string');
432
+ }
433
+ // Parses and validates {@link Observable#bind}`.to( args )` arguments and returns
434
+ // an object with a parsed structure. For example
435
+ //
436
+ // A.bind( 'x' ).to( B, 'a', C, 'b', call );
437
+ //
438
+ // becomes
439
+ //
440
+ // {
441
+ // to: [
442
+ // { observable: B, properties: [ 'a' ] },
443
+ // { observable: C, properties: [ 'b' ] },
444
+ // ],
445
+ // callback: call
446
+ // }
447
+ //
448
+ // @private
449
+ // @param {...*} args Arguments of {@link Observable#bind}`.to( args )`.
450
+ // @returns {Object}
451
+ function parseBindToArgs(...args) {
452
+ // Eliminate A.bind( 'x' ).to()
453
+ if (!args.length) {
454
+ /**
455
+ * Invalid argument syntax in `to()`.
456
+ *
457
+ * @error observable-bind-to-parse-error
458
+ */
459
+ throw new CKEditorError('observable-bind-to-parse-error', null);
460
+ }
461
+ const parsed = { to: [] };
462
+ let lastObservable;
463
+ if (typeof args[args.length - 1] == 'function') {
464
+ parsed.callback = args.pop();
465
+ }
466
+ args.forEach(a => {
467
+ if (typeof a == 'string') {
468
+ lastObservable.properties.push(a);
469
+ }
470
+ else if (typeof a == 'object') {
471
+ lastObservable = { observable: a, properties: [] };
472
+ parsed.to.push(lastObservable);
473
+ }
474
+ else {
475
+ throw new CKEditorError('observable-bind-to-parse-error', null);
476
+ }
477
+ });
478
+ return parsed;
479
+ }
480
+ // Synchronizes {@link module:utils/observablemixin#_boundObservables} with {@link Binding}.
481
+ //
482
+ // @private
483
+ // @param {Binding} binding A binding to store in {@link Observable#_boundObservables}.
484
+ // @param {Observable} toObservable A observable, which is a new component of `binding`.
485
+ // @param {String} toPropertyName A name of `toObservable`'s property, a new component of the `binding`.
486
+ function updateBoundObservables(observable, binding, toObservable, toPropertyName) {
487
+ const boundObservables = observable[boundObservablesSymbol];
488
+ const bindingsToObservable = boundObservables.get(toObservable);
489
+ const bindings = bindingsToObservable || {};
490
+ if (!bindings[toPropertyName]) {
491
+ bindings[toPropertyName] = new Set();
492
+ }
493
+ // Pass the binding to a corresponding Set in `observable._boundObservables`.
494
+ bindings[toPropertyName].add(binding);
495
+ if (!bindingsToObservable) {
496
+ boundObservables.set(toObservable, bindings);
497
+ }
498
+ }
499
+ // Synchronizes {@link Observable#_boundProperties} and {@link Observable#_boundObservables}
500
+ // with {@link BindChain}.
501
+ //
502
+ // Assuming the following binding being created
503
+ //
504
+ // A.bind( 'a', 'b' ).to( B, 'x', 'y' );
505
+ //
506
+ // the following bindings were initialized by {@link Observable#bind} in {@link BindChain#_bindings}:
507
+ //
508
+ // {
509
+ // a: { observable: A, property: 'a', to: [] },
510
+ // b: { observable: A, property: 'b', to: [] },
511
+ // }
512
+ //
513
+ // Iterate over all bindings in this chain and fill their `to` properties with
514
+ // corresponding to( ... ) arguments (components of the binding), so
515
+ //
516
+ // {
517
+ // a: { observable: A, property: 'a', to: [ B, 'x' ] },
518
+ // b: { observable: A, property: 'b', to: [ B, 'y' ] },
519
+ // }
520
+ //
521
+ // Then update the structure of {@link Observable#_boundObservables} with updated
522
+ // binding, so it becomes:
523
+ //
524
+ // Map( {
525
+ // B: {
526
+ // x: Set( [
527
+ // { observable: A, property: 'a', to: [ [ B, 'x' ] ] }
528
+ // ] ),
529
+ // y: Set( [
530
+ // { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
531
+ // ] )
532
+ // }
533
+ // } )
534
+ //
535
+ // @private
536
+ // @param {BindChain} chain The binding initialized by {@link Observable#bind}.
537
+ function updateBindToBound(chain) {
538
+ let toProperty;
539
+ chain._bindings.forEach((binding, propertyName) => {
540
+ // Note: For a binding without a callback, this will run only once
541
+ // like in A.bind( 'x', 'y' ).to( B, 'a', 'b' )
542
+ // TODO: ES6 destructuring.
543
+ chain._to.forEach(to => {
544
+ toProperty = to.properties[binding.callback ? 0 : chain._bindProperties.indexOf(propertyName)];
545
+ binding.to.push([to.observable, toProperty]);
546
+ updateBoundObservables(chain._observable, binding, to.observable, toProperty);
547
+ });
548
+ });
549
+ }
550
+ // Updates an property of a {@link Observable} with a value
551
+ // determined by an entry in {@link Observable#_boundProperties}.
552
+ //
553
+ // @private
554
+ // @param {Observable} observable A observable which property is to be updated.
555
+ // @param {String} propertyName An property to be updated.
556
+ function updateBoundObservableProperty(observable, propertyName) {
557
+ const boundProperties = observable[boundPropertiesSymbol];
558
+ const binding = boundProperties.get(propertyName);
559
+ let propertyValue;
560
+ // When a binding with callback is created like
561
+ //
562
+ // A.bind( 'a' ).to( B, 'b', C, 'c', callback );
563
+ //
564
+ // collect B.b and C.c, then pass them to callback to set A.a.
565
+ if (binding.callback) {
566
+ propertyValue = binding.callback.apply(observable, binding.to.map(to => to[0][to[1]]));
567
+ }
568
+ else {
569
+ propertyValue = binding.to[0];
570
+ propertyValue = propertyValue[0][propertyValue[1]];
571
+ }
572
+ if (Object.prototype.hasOwnProperty.call(observable, propertyName)) {
573
+ observable[propertyName] = propertyValue;
574
+ }
575
+ else {
576
+ observable.set(propertyName, propertyValue);
577
+ }
578
+ }
579
+ // Starts listening to changes in {@link BindChain._to} observables to update
580
+ // {@link BindChain._observable} {@link BindChain._bindProperties}. Also sets the
581
+ // initial state of {@link BindChain._observable}.
582
+ //
583
+ // @private
584
+ // @param {Observable} observable
585
+ // @param {BindChain} chain The chain initialized by {@link Observable#bind}.
586
+ function attachBindToListeners(observable, toBindings) {
587
+ toBindings.forEach(to => {
588
+ const boundObservables = observable[boundObservablesSymbol];
589
+ let bindings;
590
+ // If there's already a chain between the observables (`observable` listens to
591
+ // `to.observable`), there's no need to create another `change` event listener.
592
+ if (!boundObservables.get(to.observable)) {
593
+ observable.listenTo(to.observable, 'change', (evt, propertyName) => {
594
+ bindings = boundObservables.get(to.observable)[propertyName];
595
+ // Note: to.observable will fire for any property change, react
596
+ // to changes of properties which are bound only.
597
+ if (bindings) {
598
+ bindings.forEach(binding => {
599
+ updateBoundObservableProperty(observable, binding.property);
600
+ });
601
+ }
602
+ });
603
+ }
604
+ });
605
+ }
@@ -0,0 +1,32 @@
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
+ * Provides group of constants to use instead of hardcoding numeric priority values.
7
+ *
8
+ * @namespace
9
+ */
10
+ const priorities = {
11
+ /**
12
+ * Converts a string with priority name to it's numeric value. If `Number` is given, it just returns it.
13
+ *
14
+ * @static
15
+ * @param {module:utils/priorities~PriorityString|Number} [priority] Priority to convert.
16
+ * @returns {Number} Converted priority.
17
+ */
18
+ get(priority = 'normal') {
19
+ if (typeof priority != 'number') {
20
+ return this[priority] || this.normal;
21
+ }
22
+ else {
23
+ return priority;
24
+ }
25
+ },
26
+ highest: 100000,
27
+ high: 1000,
28
+ normal: 0,
29
+ low: -1000,
30
+ lowest: -100000
31
+ };
32
+ export default priorities;
package/src/spy.js ADDED
@@ -0,0 +1,22 @@
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/spy
7
+ */
8
+ /**
9
+ * Creates a spy function (ala Sinon.js) that can be used to inspect call to it.
10
+ *
11
+ * The following are the present features:
12
+ *
13
+ * * spy.called: property set to `true` if the function has been called at least once.
14
+ *
15
+ * @returns {Function} The spy function.
16
+ */
17
+ function spy() {
18
+ return function spy() {
19
+ spy.called = true;
20
+ };
21
+ }
22
+ export default spy;
package/src/toarray.js ADDED
@@ -0,0 +1,7 @@
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
+ export default function toArray(data) {
6
+ return Array.isArray(data) ? data : [data];
7
+ }