@ckeditor/ckeditor5-utils 40.0.0 → 40.2.0

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