@ckeditor/ckeditor5-utils 34.1.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 (61) hide show
  1. package/CHANGELOG.md +324 -0
  2. package/LICENSE.md +1 -1
  3. package/package.json +19 -8
  4. package/src/areconnectedthroughproperties.js +54 -71
  5. package/src/ckeditorerror.js +92 -114
  6. package/src/collection.js +594 -760
  7. package/src/comparearrays.js +22 -28
  8. package/src/config.js +193 -223
  9. package/src/count.js +8 -12
  10. package/src/diff.js +85 -110
  11. package/src/difftochanges.js +47 -57
  12. package/src/dom/createelement.js +17 -25
  13. package/src/dom/emittermixin.js +217 -257
  14. package/src/dom/getancestors.js +9 -13
  15. package/src/dom/getborderwidths.js +10 -13
  16. package/src/dom/getcommonancestor.js +9 -15
  17. package/src/dom/getdatafromelement.js +5 -9
  18. package/src/dom/getpositionedancestor.js +9 -14
  19. package/src/dom/global.js +0 -3
  20. package/src/dom/indexof.js +7 -11
  21. package/src/dom/insertat.js +2 -4
  22. package/src/dom/iscomment.js +2 -5
  23. package/src/dom/isnode.js +10 -12
  24. package/src/dom/isrange.js +2 -4
  25. package/src/dom/istext.js +2 -4
  26. package/src/dom/isvisible.js +2 -4
  27. package/src/dom/iswindow.js +11 -16
  28. package/src/dom/position.js +220 -410
  29. package/src/dom/rect.js +335 -414
  30. package/src/dom/remove.js +5 -8
  31. package/src/dom/resizeobserver.js +109 -342
  32. package/src/dom/scroll.js +151 -183
  33. package/src/dom/setdatainelement.js +5 -9
  34. package/src/dom/tounit.js +10 -12
  35. package/src/elementreplacer.js +30 -44
  36. package/src/emittermixin.js +383 -631
  37. package/src/env.js +93 -115
  38. package/src/eventinfo.js +12 -65
  39. package/src/fastdiff.js +96 -128
  40. package/src/first.js +8 -12
  41. package/src/focustracker.js +77 -131
  42. package/src/index.js +0 -9
  43. package/src/inserttopriorityarray.js +9 -30
  44. package/src/isiterable.js +2 -4
  45. package/src/keyboard.js +117 -196
  46. package/src/keystrokehandler.js +72 -88
  47. package/src/language.js +9 -15
  48. package/src/locale.js +61 -158
  49. package/src/mapsequal.js +12 -17
  50. package/src/mix.js +13 -16
  51. package/src/nth.js +8 -11
  52. package/src/objecttomap.js +7 -11
  53. package/src/observablemixin.js +465 -768
  54. package/src/priorities.js +20 -32
  55. package/src/spy.js +3 -6
  56. package/src/toarray.js +2 -13
  57. package/src/tomap.js +8 -10
  58. package/src/translation-service.js +51 -87
  59. package/src/uid.js +34 -38
  60. package/src/unicode.js +28 -43
  61. package/src/version.js +134 -143
@@ -2,22 +2,18 @@
2
2
  * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
5
+ /* eslint-disable @typescript-eslint/unified-signatures */
6
6
  /**
7
7
  * @module utils/observablemixin
8
8
  */
9
-
10
9
  import EmitterMixin from './emittermixin';
11
10
  import CKEditorError from './ckeditorerror';
12
- import { extend, isObject } from 'lodash-es';
13
-
14
- const observablePropertiesSymbol = Symbol( 'observableProperties' );
15
- const boundObservablesSymbol = Symbol( 'boundObservables' );
16
- const boundPropertiesSymbol = Symbol( 'boundProperties' );
17
-
18
- const _decoratedMethods = Symbol( 'decoratedMethods' );
19
- const _decoratedOriginal = Symbol( 'decoratedOriginal' );
20
-
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');
21
17
  /**
22
18
  * A mixin that injects the "observable properties" and data binding functionality described in the
23
19
  * {@link ~Observable} interface.
@@ -32,483 +28,408 @@ const _decoratedOriginal = Symbol( 'decoratedOriginal' );
32
28
  * @implements module:utils/observablemixin~Observable
33
29
  */
34
30
  const ObservableMixin = {
35
- /**
36
- * @inheritDoc
37
- */
38
- set( name, value ) {
39
- // If the first parameter is an Object, iterate over its properties.
40
- if ( isObject( name ) ) {
41
- Object.keys( name ).forEach( property => {
42
- this.set( property, name[ property ] );
43
- }, this );
44
-
45
- return;
46
- }
47
-
48
- initObservable( this );
49
-
50
- const properties = this[ observablePropertiesSymbol ];
51
-
52
- if ( ( name in this ) && !properties.has( name ) ) {
53
- /**
54
- * Cannot override an existing property.
55
- *
56
- * This error is thrown when trying to {@link ~Observable#set set} a property with
57
- * a name of an already existing property. For example:
58
- *
59
- * let observable = new Model();
60
- * observable.property = 1;
61
- * observable.set( 'property', 2 ); // throws
62
- *
63
- * observable.set( 'property', 1 );
64
- * observable.set( 'property', 2 ); // ok, because this is an existing property.
65
- *
66
- * @error observable-set-cannot-override
67
- */
68
- throw new CKEditorError( 'observable-set-cannot-override', this );
69
- }
70
-
71
- Object.defineProperty( this, name, {
72
- enumerable: true,
73
- configurable: true,
74
-
75
- get() {
76
- return properties.get( name );
77
- },
78
-
79
- set( value ) {
80
- const oldValue = properties.get( name );
81
-
82
- // Fire `set` event before the new value will be set to make it possible
83
- // to override observable property without affecting `change` event.
84
- // See https://github.com/ckeditor/ckeditor5-utils/issues/171.
85
- let newValue = this.fire( 'set:' + name, name, value, oldValue );
86
-
87
- if ( newValue === undefined ) {
88
- newValue = value;
89
- }
90
-
91
- // Allow undefined as an initial value like A.define( 'x', undefined ) (#132).
92
- // Note: When properties map has no such own property, then its value is undefined.
93
- if ( oldValue !== newValue || !properties.has( name ) ) {
94
- properties.set( name, newValue );
95
- this.fire( 'change:' + name, name, newValue, oldValue );
96
- }
97
- }
98
- } );
99
-
100
- this[ name ] = value;
101
- },
102
-
103
- /**
104
- * @inheritDoc
105
- */
106
- bind( ...bindProperties ) {
107
- if ( !bindProperties.length || !isStringArray( bindProperties ) ) {
108
- /**
109
- * All properties must be strings.
110
- *
111
- * @error observable-bind-wrong-properties
112
- */
113
- throw new CKEditorError( 'observable-bind-wrong-properties', this );
114
- }
115
-
116
- if ( ( new Set( bindProperties ) ).size !== bindProperties.length ) {
117
- /**
118
- * Properties must be unique.
119
- *
120
- * @error observable-bind-duplicate-properties
121
- */
122
- throw new CKEditorError( 'observable-bind-duplicate-properties', this );
123
- }
124
-
125
- initObservable( this );
126
-
127
- const boundProperties = this[ boundPropertiesSymbol ];
128
-
129
- bindProperties.forEach( propertyName => {
130
- if ( boundProperties.has( propertyName ) ) {
131
- /**
132
- * Cannot bind the same property more than once.
133
- *
134
- * @error observable-bind-rebind
135
- */
136
- throw new CKEditorError( 'observable-bind-rebind', this );
137
- }
138
- } );
139
-
140
- const bindings = new Map();
141
-
142
- // @typedef {Object} Binding
143
- // @property {Array} property Property which is bound.
144
- // @property {Array} to Array of observable–property components of the binding (`{ observable: ..., property: .. }`).
145
- // @property {Array} callback A function which processes `to` components.
146
- bindProperties.forEach( a => {
147
- const binding = { property: a, to: [] };
148
-
149
- boundProperties.set( a, binding );
150
- bindings.set( a, binding );
151
- } );
152
-
153
- // @typedef {Object} BindChain
154
- // @property {Function} to See {@link ~ObservableMixin#_bindTo}.
155
- // @property {Function} toMany See {@link ~ObservableMixin#_bindToMany}.
156
- // @property {module:utils/observablemixin~Observable} _observable The observable which initializes the binding.
157
- // @property {Array} _bindProperties Array of `_observable` properties to be bound.
158
- // @property {Array} _to Array of `to()` observable–properties (`{ observable: toObservable, properties: ...toProperties }`).
159
- // @property {Map} _bindings Stores bindings to be kept in
160
- // {@link ~ObservableMixin#_boundProperties}/{@link ~ObservableMixin#_boundObservables}
161
- // initiated in this binding chain.
162
- return {
163
- to: bindTo,
164
- toMany: bindToMany,
165
-
166
- _observable: this,
167
- _bindProperties: bindProperties,
168
- _to: [],
169
- _bindings: bindings
170
- };
171
- },
172
-
173
- /**
174
- * @inheritDoc
175
- */
176
- unbind( ...unbindProperties ) {
177
- // Nothing to do here if not inited yet.
178
- if ( !( this[ observablePropertiesSymbol ] ) ) {
179
- return;
180
- }
181
-
182
- const boundProperties = this[ boundPropertiesSymbol ];
183
- const boundObservables = this[ boundObservablesSymbol ];
184
-
185
- if ( unbindProperties.length ) {
186
- if ( !isStringArray( unbindProperties ) ) {
187
- /**
188
- * Properties must be strings.
189
- *
190
- * @error observable-unbind-wrong-properties
191
- */
192
- throw new CKEditorError( 'observable-unbind-wrong-properties', this );
193
- }
194
-
195
- unbindProperties.forEach( propertyName => {
196
- const binding = boundProperties.get( propertyName );
197
-
198
- // Nothing to do if the binding is not defined
199
- if ( !binding ) {
200
- return;
201
- }
202
-
203
- let toObservable, toProperty, toProperties, toPropertyBindings;
204
-
205
- binding.to.forEach( to => {
206
- // TODO: ES6 destructuring.
207
- toObservable = to[ 0 ];
208
- toProperty = to[ 1 ];
209
- toProperties = boundObservables.get( toObservable );
210
- toPropertyBindings = toProperties[ toProperty ];
211
-
212
- toPropertyBindings.delete( binding );
213
-
214
- if ( !toPropertyBindings.size ) {
215
- delete toProperties[ toProperty ];
216
- }
217
-
218
- if ( !Object.keys( toProperties ).length ) {
219
- boundObservables.delete( toObservable );
220
- this.stopListening( toObservable, 'change' );
221
- }
222
- } );
223
-
224
- boundProperties.delete( propertyName );
225
- } );
226
- } else {
227
- boundObservables.forEach( ( bindings, boundObservable ) => {
228
- this.stopListening( boundObservable, 'change' );
229
- } );
230
-
231
- boundObservables.clear();
232
- boundProperties.clear();
233
- }
234
- },
235
-
236
- /**
237
- * @inheritDoc
238
- */
239
- decorate( methodName ) {
240
- const originalMethod = this[ methodName ];
241
-
242
- if ( !originalMethod ) {
243
- /**
244
- * Cannot decorate an undefined method.
245
- *
246
- * @error observablemixin-cannot-decorate-undefined
247
- * @param {Object} object The object which method should be decorated.
248
- * @param {String} methodName Name of the method which does not exist.
249
- */
250
- throw new CKEditorError(
251
- 'observablemixin-cannot-decorate-undefined',
252
- this,
253
- { object: this, methodName }
254
- );
255
- }
256
-
257
- this.on( methodName, ( evt, args ) => {
258
- evt.return = originalMethod.apply( this, args );
259
- } );
260
-
261
- this[ methodName ] = function( ...args ) {
262
- return this.fire( methodName, args );
263
- };
264
-
265
- this[ methodName ][ _decoratedOriginal ] = originalMethod;
266
-
267
- if ( !this[ _decoratedMethods ] ) {
268
- this[ _decoratedMethods ] = [];
269
- }
270
-
271
- this[ _decoratedMethods ].push( methodName );
272
- }
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
273
224
  };
274
-
275
- extend( ObservableMixin, EmitterMixin );
276
-
277
225
  // Override the EmitterMixin stopListening method to be able to clean (and restore) decorated methods.
278
226
  // This is needed in case of:
279
227
  // 1. Have x.foo() decorated.
280
228
  // 2. Call x.stopListening()
281
229
  // 3. Call x.foo(). Problem: nothing happens (the original foo() method is not executed)
282
- ObservableMixin.stopListening = function( emitter, event, callback ) {
283
- // Removing all listeners so let's clean the decorated methods to the original state.
284
- if ( !emitter && this[ _decoratedMethods ] ) {
285
- for ( const methodName of this[ _decoratedMethods ] ) {
286
- this[ methodName ] = this[ methodName ][ _decoratedOriginal ];
287
- }
288
-
289
- delete this[ _decoratedMethods ];
290
- }
291
-
292
- EmitterMixin.stopListening.call( this, emitter, event, callback );
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);
293
239
  };
294
-
295
240
  export default ObservableMixin;
296
-
297
241
  // Init symbol properties needed for the observable mechanism to work.
298
242
  //
299
243
  // @private
300
244
  // @param {module:utils/observablemixin~ObservableMixin} observable
301
- function initObservable( observable ) {
302
- // Do nothing if already inited.
303
- if ( observable[ observablePropertiesSymbol ] ) {
304
- return;
305
- }
306
-
307
- // The internal hash containing the observable's state.
308
- //
309
- // @private
310
- // @type {Map}
311
- Object.defineProperty( observable, observablePropertiesSymbol, {
312
- value: new Map()
313
- } );
314
-
315
- // Map containing bindings to external observables. It shares the binding objects
316
- // (`{ observable: A, property: 'a', to: ... }`) with {@link module:utils/observablemixin~ObservableMixin#_boundProperties} and
317
- // it is used to observe external observables to update own properties accordingly.
318
- // See {@link module:utils/observablemixin~ObservableMixin#bind}.
319
- //
320
- // A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
321
- // console.log( A._boundObservables );
322
- //
323
- // Map( {
324
- // B: {
325
- // x: Set( [
326
- // { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
327
- // { observable: A, property: 'c', to: [ [ B, 'x' ] ] }
328
- // ] ),
329
- // y: Set( [
330
- // { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
331
- // ] )
332
- // }
333
- // } )
334
- //
335
- // A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
336
- // console.log( A._boundObservables );
337
- //
338
- // Map( {
339
- // B: {
340
- // x: Set( [
341
- // { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
342
- // { observable: A, property: 'c', to: [ [ B, 'x' ] ] }
343
- // ] ),
344
- // y: Set( [
345
- // { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
346
- // ] ),
347
- // z: Set( [
348
- // { observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
349
- // ] )
350
- // },
351
- // C: {
352
- // w: Set( [
353
- // { observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
354
- // ] )
355
- // }
356
- // } )
357
- //
358
- // @private
359
- // @type {Map}
360
- Object.defineProperty( observable, boundObservablesSymbol, {
361
- value: new Map()
362
- } );
363
-
364
- // Object that stores which properties of this observable are bound and how. It shares
365
- // the binding objects (`{ observable: A, property: 'a', to: ... }`) with
366
- // {@link module:utils/observablemixin~ObservableMixin#_boundObservables}. This data structure is
367
- // a reverse of {@link module:utils/observablemixin~ObservableMixin#_boundObservables} and it is helpful for
368
- // {@link module:utils/observablemixin~ObservableMixin#unbind}.
369
- //
370
- // See {@link module:utils/observablemixin~ObservableMixin#bind}.
371
- //
372
- // A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
373
- // console.log( A._boundProperties );
374
- //
375
- // Map( {
376
- // a: { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
377
- // b: { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
378
- // c: { observable: A, property: 'c', to: [ [ B, 'x' ] ] }
379
- // } )
380
- //
381
- // A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
382
- // console.log( A._boundProperties );
383
- //
384
- // Map( {
385
- // a: { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
386
- // b: { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
387
- // c: { observable: A, property: 'c', to: [ [ B, 'x' ] ] },
388
- // d: { observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
389
- // } )
390
- //
391
- // @private
392
- // @type {Map}
393
- Object.defineProperty( observable, boundPropertiesSymbol, {
394
- value: new Map()
395
- } );
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
+ });
396
337
  }
397
-
398
338
  // A chaining for {@link module:utils/observablemixin~ObservableMixin#bind} providing `.to()` interface.
399
339
  //
400
340
  // @private
401
341
  // @param {...[Observable|String|Function]} args Arguments of the `.to( args )` binding.
402
- function bindTo( ...args ) {
403
- const parsedArgs = parseBindToArgs( ...args );
404
- const bindingsKeys = Array.from( this._bindings.keys() );
405
- const numberOfBindings = bindingsKeys.length;
406
-
407
- // Eliminate A.bind( 'x' ).to( B, C )
408
- if ( !parsedArgs.callback && parsedArgs.to.length > 1 ) {
409
- /**
410
- * Binding multiple observables only possible with callback.
411
- *
412
- * @error observable-bind-to-no-callback
413
- */
414
- throw new CKEditorError( 'observable-bind-to-no-callback', this );
415
- }
416
-
417
- // Eliminate A.bind( 'x', 'y' ).to( B, callback )
418
- if ( numberOfBindings > 1 && parsedArgs.callback ) {
419
- /**
420
- * Cannot bind multiple properties and use a callback in one binding.
421
- *
422
- * @error observable-bind-to-extra-callback
423
- */
424
- throw new CKEditorError(
425
- 'observable-bind-to-extra-callback',
426
- this
427
- );
428
- }
429
-
430
- parsedArgs.to.forEach( to => {
431
- // Eliminate A.bind( 'x', 'y' ).to( B, 'a' )
432
- if ( to.properties.length && to.properties.length !== numberOfBindings ) {
433
- /**
434
- * The number of properties must match.
435
- *
436
- * @error observable-bind-to-properties-length
437
- */
438
- throw new CKEditorError( 'observable-bind-to-properties-length', this );
439
- }
440
-
441
- // When no to.properties specified, observing source properties instead i.e.
442
- // A.bind( 'x', 'y' ).to( B ) -> Observe B.x and B.y
443
- if ( !to.properties.length ) {
444
- to.properties = this._bindProperties;
445
- }
446
- } );
447
-
448
- this._to = parsedArgs.to;
449
-
450
- // Fill {@link BindChain#_bindings} with callback. When the callback is set there's only one binding.
451
- if ( parsedArgs.callback ) {
452
- this._bindings.get( bindingsKeys[ 0 ] ).callback = parsedArgs.callback;
453
- }
454
-
455
- attachBindToListeners( this._observable, this._to );
456
-
457
- // Update observable._boundProperties and observable._boundObservables.
458
- updateBindToBound( this );
459
-
460
- // Set initial values of bound properties.
461
- this._bindProperties.forEach( propertyName => {
462
- updateBoundObservableProperty( this._observable, propertyName );
463
- } );
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
+ });
464
392
  }
465
-
466
393
  // Binds to an attribute in a set of iterable observables.
467
394
  //
468
395
  // @private
469
396
  // @param {Array.<Observable>} observables
470
397
  // @param {String} attribute
471
398
  // @param {Function} callback
472
- function bindToMany( observables, attribute, callback ) {
473
- if ( this._bindings.size > 1 ) {
474
- /**
475
- * Binding one attribute to many observables only possible with one attribute.
476
- *
477
- * @error observable-bind-to-many-not-one-binding
478
- */
479
- throw new CKEditorError( 'observable-bind-to-many-not-one-binding', this );
480
- }
481
-
482
- this.to(
483
- // Bind to #attribute of each observable...
484
- ...getBindingTargets( observables, attribute ),
485
- // ...using given callback to parse attribute values.
486
- callback
487
- );
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);
488
413
  }
489
-
490
414
  // Returns an array of binding components for
491
415
  // {@link Observable#bind} from a set of iterable observables.
492
416
  //
493
417
  // @param {Array.<Observable>} observables
494
418
  // @param {String} attribute
495
419
  // @returns {Array.<String|Observable>}
496
- function getBindingTargets( observables, attribute ) {
497
- const observableAndAttributePairs = observables.map( observable => [ observable, attribute ] );
498
-
499
- // Merge pairs to one-dimension array of observables and attributes.
500
- return Array.prototype.concat.apply( [], observableAndAttributePairs );
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);
501
424
  }
502
-
503
425
  // Check if all entries of the array are of `String` type.
504
426
  //
505
427
  // @private
506
428
  // @param {Array} arr An array to be checked.
507
429
  // @returns {Boolean}
508
- function isStringArray( arr ) {
509
- return arr.every( a => typeof a == 'string' );
430
+ function isStringArray(arr) {
431
+ return arr.every(a => typeof a == 'string');
510
432
  }
511
-
512
433
  // Parses and validates {@link Observable#bind}`.to( args )` arguments and returns
513
434
  // an object with a parsed structure. For example
514
435
  //
@@ -527,61 +448,54 @@ function isStringArray( arr ) {
527
448
  // @private
528
449
  // @param {...*} args Arguments of {@link Observable#bind}`.to( args )`.
529
450
  // @returns {Object}
530
- function parseBindToArgs( ...args ) {
531
- // Eliminate A.bind( 'x' ).to()
532
- if ( !args.length ) {
533
- /**
534
- * Invalid argument syntax in `to()`.
535
- *
536
- * @error observable-bind-to-parse-error
537
- */
538
- throw new CKEditorError( 'observable-bind-to-parse-error', null );
539
- }
540
-
541
- const parsed = { to: [] };
542
- let lastObservable;
543
-
544
- if ( typeof args[ args.length - 1 ] == 'function' ) {
545
- parsed.callback = args.pop();
546
- }
547
-
548
- args.forEach( a => {
549
- if ( typeof a == 'string' ) {
550
- lastObservable.properties.push( a );
551
- } else if ( typeof a == 'object' ) {
552
- lastObservable = { observable: a, properties: [] };
553
- parsed.to.push( lastObservable );
554
- } else {
555
- throw new CKEditorError( 'observable-bind-to-parse-error', null );
556
- }
557
- } );
558
-
559
- return parsed;
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;
560
479
  }
561
-
562
480
  // Synchronizes {@link module:utils/observablemixin#_boundObservables} with {@link Binding}.
563
481
  //
564
482
  // @private
565
483
  // @param {Binding} binding A binding to store in {@link Observable#_boundObservables}.
566
484
  // @param {Observable} toObservable A observable, which is a new component of `binding`.
567
485
  // @param {String} toPropertyName A name of `toObservable`'s property, a new component of the `binding`.
568
- function updateBoundObservables( observable, binding, toObservable, toPropertyName ) {
569
- const boundObservables = observable[ boundObservablesSymbol ];
570
- const bindingsToObservable = boundObservables.get( toObservable );
571
- const bindings = bindingsToObservable || {};
572
-
573
- if ( !bindings[ toPropertyName ] ) {
574
- bindings[ toPropertyName ] = new Set();
575
- }
576
-
577
- // Pass the binding to a corresponding Set in `observable._boundObservables`.
578
- bindings[ toPropertyName ].add( binding );
579
-
580
- if ( !bindingsToObservable ) {
581
- boundObservables.set( toObservable, bindings );
582
- }
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
+ }
583
498
  }
584
-
585
499
  // Synchronizes {@link Observable#_boundProperties} and {@link Observable#_boundObservables}
586
500
  // with {@link BindChain}.
587
501
  //
@@ -620,289 +534,72 @@ function updateBoundObservables( observable, binding, toObservable, toPropertyNa
620
534
  //
621
535
  // @private
622
536
  // @param {BindChain} chain The binding initialized by {@link Observable#bind}.
623
- function updateBindToBound( chain ) {
624
- let toProperty;
625
-
626
- chain._bindings.forEach( ( binding, propertyName ) => {
627
- // Note: For a binding without a callback, this will run only once
628
- // like in A.bind( 'x', 'y' ).to( B, 'a', 'b' )
629
- // TODO: ES6 destructuring.
630
- chain._to.forEach( to => {
631
- toProperty = to.properties[ binding.callback ? 0 : chain._bindProperties.indexOf( propertyName ) ];
632
-
633
- binding.to.push( [ to.observable, toProperty ] );
634
- updateBoundObservables( chain._observable, binding, to.observable, toProperty );
635
- } );
636
- } );
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
+ });
637
549
  }
638
-
639
550
  // Updates an property of a {@link Observable} with a value
640
551
  // determined by an entry in {@link Observable#_boundProperties}.
641
552
  //
642
553
  // @private
643
554
  // @param {Observable} observable A observable which property is to be updated.
644
555
  // @param {String} propertyName An property to be updated.
645
- function updateBoundObservableProperty( observable, propertyName ) {
646
- const boundProperties = observable[ boundPropertiesSymbol ];
647
- const binding = boundProperties.get( propertyName );
648
- let propertyValue;
649
-
650
- // When a binding with callback is created like
651
- //
652
- // A.bind( 'a' ).to( B, 'b', C, 'c', callback );
653
- //
654
- // collect B.b and C.c, then pass them to callback to set A.a.
655
- if ( binding.callback ) {
656
- propertyValue = binding.callback.apply( observable, binding.to.map( to => to[ 0 ][ to[ 1 ] ] ) );
657
- } else {
658
- propertyValue = binding.to[ 0 ];
659
- propertyValue = propertyValue[ 0 ][ propertyValue[ 1 ] ];
660
- }
661
-
662
- if ( Object.prototype.hasOwnProperty.call( observable, propertyName ) ) {
663
- observable[ propertyName ] = propertyValue;
664
- } else {
665
- observable.set( propertyName, propertyValue );
666
- }
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
+ }
667
578
  }
668
-
669
579
  // Starts listening to changes in {@link BindChain._to} observables to update
670
580
  // {@link BindChain._observable} {@link BindChain._bindProperties}. Also sets the
671
581
  // initial state of {@link BindChain._observable}.
672
582
  //
673
583
  // @private
584
+ // @param {Observable} observable
674
585
  // @param {BindChain} chain The chain initialized by {@link Observable#bind}.
675
- function attachBindToListeners( observable, toBindings ) {
676
- toBindings.forEach( to => {
677
- const boundObservables = observable[ boundObservablesSymbol ];
678
- let bindings;
679
-
680
- // If there's already a chain between the observables (`observable` listens to
681
- // `to.observable`), there's no need to create another `change` event listener.
682
- if ( !boundObservables.get( to.observable ) ) {
683
- observable.listenTo( to.observable, 'change', ( evt, propertyName ) => {
684
- bindings = boundObservables.get( to.observable )[ propertyName ];
685
-
686
- // Note: to.observable will fire for any property change, react
687
- // to changes of properties which are bound only.
688
- if ( bindings ) {
689
- bindings.forEach( binding => {
690
- updateBoundObservableProperty( observable, binding.property );
691
- } );
692
- }
693
- } );
694
- }
695
- } );
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
+ });
696
605
  }
697
-
698
- /**
699
- * An interface which adds "observable properties" and data binding functionality.
700
- *
701
- * Can be easily implemented by a class by mixing the {@link module:utils/observablemixin~ObservableMixin} mixin.
702
- *
703
- * Read more about the usage of this interface in the:
704
- * * {@glink framework/guides/architecture/core-editor-architecture#event-system-and-observables Event system and observables}
705
- * section of the {@glink framework/guides/architecture/core-editor-architecture Core editor architecture} guide,
706
- * * {@glink framework/guides/deep-dive/observables Observables deep dive} guide.
707
- *
708
- * @interface Observable
709
- * @extends module:utils/emittermixin~Emitter
710
- */
711
-
712
- /**
713
- * Fired when a property changed value.
714
- *
715
- * observable.set( 'prop', 1 );
716
- *
717
- * observable.on( 'change:prop', ( evt, propertyName, newValue, oldValue ) => {
718
- * console.log( `${ propertyName } has changed from ${ oldValue } to ${ newValue }` );
719
- * } );
720
- *
721
- * observable.prop = 2; // -> 'prop has changed from 1 to 2'
722
- *
723
- * @event change:{property}
724
- * @param {String} name The property name.
725
- * @param {*} value The new property value.
726
- * @param {*} oldValue The previous property value.
727
- */
728
-
729
- /**
730
- * Fired when a property value is going to be set but is not set yet (before the `change` event is fired).
731
- *
732
- * You can control the final value of the property by using
733
- * the {@link module:utils/eventinfo~EventInfo#return event's `return` property}.
734
- *
735
- * observable.set( 'prop', 1 );
736
- *
737
- * observable.on( 'set:prop', ( evt, propertyName, newValue, oldValue ) => {
738
- * console.log( `Value is going to be changed from ${ oldValue } to ${ newValue }` );
739
- * console.log( `Current property value is ${ observable[ propertyName ] }` );
740
- *
741
- * // Let's override the value.
742
- * evt.return = 3;
743
- * } );
744
- *
745
- * observable.on( 'change:prop', ( evt, propertyName, newValue, oldValue ) => {
746
- * console.log( `Value has changed from ${ oldValue } to ${ newValue }` );
747
- * } );
748
- *
749
- * observable.prop = 2; // -> 'Value is going to be changed from 1 to 2'
750
- * // -> 'Current property value is 1'
751
- * // -> 'Value has changed from 1 to 3'
752
- *
753
- * **Note:** The event is fired even when the new value is the same as the old value.
754
- *
755
- * @event set:{property}
756
- * @param {String} name The property name.
757
- * @param {*} value The new property value.
758
- * @param {*} oldValue The previous property value.
759
- */
760
-
761
- /**
762
- * Creates and sets the value of an observable property of this object. Such a property becomes a part
763
- * of the state and is observable.
764
- *
765
- * It accepts also a single object literal containing key/value pairs with properties to be set.
766
- *
767
- * This method throws the `observable-set-cannot-override` error if the observable instance already
768
- * has a property with the given property name. This prevents from mistakenly overriding existing
769
- * properties and methods, but means that `foo.set( 'bar', 1 )` may be slightly slower than `foo.bar = 1`.
770
- *
771
- * @method #set
772
- * @param {String|Object} name The property's name or object with `name=>value` pairs.
773
- * @param {*} [value] The property's value (if `name` was passed in the first parameter).
774
- */
775
-
776
- /**
777
- * Binds {@link #set observable properties} to other objects implementing the
778
- * {@link module:utils/observablemixin~Observable} interface.
779
- *
780
- * Read more in the {@glink framework/guides/deep-dive/observables#property-bindings dedicated guide}
781
- * covering the topic of property bindings with some additional examples.
782
- *
783
- * Consider two objects: a `button` and an associated `command` (both `Observable`).
784
- *
785
- * A simple property binding could be as follows:
786
- *
787
- * button.bind( 'isEnabled' ).to( command, 'isEnabled' );
788
- *
789
- * or even shorter:
790
- *
791
- * button.bind( 'isEnabled' ).to( command );
792
- *
793
- * which works in the following way:
794
- *
795
- * * `button.isEnabled` **instantly equals** `command.isEnabled`,
796
- * * whenever `command.isEnabled` changes, `button.isEnabled` will immediately reflect its value.
797
- *
798
- * **Note**: To release the binding, use {@link module:utils/observablemixin~Observable#unbind}.
799
- *
800
- * You can also "rename" the property in the binding by specifying the new name in the `to()` chain:
801
- *
802
- * button.bind( 'isEnabled' ).to( command, 'isWorking' );
803
- *
804
- * It is possible to bind more than one property at a time to shorten the code:
805
- *
806
- * button.bind( 'isEnabled', 'value' ).to( command );
807
- *
808
- * which corresponds to:
809
- *
810
- * button.bind( 'isEnabled' ).to( command );
811
- * button.bind( 'value' ).to( command );
812
- *
813
- * The binding can include more than one observable, combining multiple data sources in a custom callback:
814
- *
815
- * button.bind( 'isEnabled' ).to( command, 'isEnabled', ui, 'isVisible',
816
- * ( isCommandEnabled, isUIVisible ) => isCommandEnabled && isUIVisible );
817
- *
818
- * Using a custom callback allows processing the value before passing it to the target property:
819
- *
820
- * button.bind( 'isEnabled' ).to( command, 'value', value => value === 'heading1' );
821
- *
822
- * It is also possible to bind to the same property in an array of observables.
823
- * To bind a `button` to multiple commands (also `Observables`) so that each and every one of them
824
- * must be enabled for the button to become enabled, use the following code:
825
- *
826
- * button.bind( 'isEnabled' ).toMany( [ commandA, commandB, commandC ], 'isEnabled',
827
- * ( isAEnabled, isBEnabled, isCEnabled ) => isAEnabled && isBEnabled && isCEnabled );
828
- *
829
- * @method #bind
830
- * @param {...String} bindProperties Observable properties that will be bound to other observable(s).
831
- * @returns {Object} The bind chain with the `to()` and `toMany()` methods.
832
- */
833
-
834
- /**
835
- * Removes the binding created with {@link #bind}.
836
- *
837
- * // Removes the binding for the 'a' property.
838
- * A.unbind( 'a' );
839
- *
840
- * // Removes bindings for all properties.
841
- * A.unbind();
842
- *
843
- * @method #unbind
844
- * @param {...String} [unbindProperties] Observable properties to be unbound. All the bindings will
845
- * be released if no properties are provided.
846
- */
847
-
848
- /**
849
- * Turns the given methods of this object into event-based ones. This means that the new method will fire an event
850
- * (named after the method) and the original action will be plugged as a listener to that event.
851
- *
852
- * Read more in the {@glink framework/guides/deep-dive/observables#decorating-object-methods dedicated guide}
853
- * covering the topic of decorating methods with some additional examples.
854
- *
855
- * Decorating the method does not change its behavior (it only adds an event),
856
- * but it allows to modify it later on by listening to the method's event.
857
- *
858
- * For example, to cancel the method execution the event can be {@link module:utils/eventinfo~EventInfo#stop stopped}:
859
- *
860
- * class Foo {
861
- * constructor() {
862
- * this.decorate( 'method' );
863
- * }
864
- *
865
- * method() {
866
- * console.log( 'called!' );
867
- * }
868
- * }
869
- *
870
- * const foo = new Foo();
871
- * foo.on( 'method', ( evt ) => {
872
- * evt.stop();
873
- * }, { priority: 'high' } );
874
- *
875
- * foo.method(); // Nothing is logged.
876
- *
877
- *
878
- * **Note**: The high {@link module:utils/priorities~PriorityString priority} listener
879
- * has been used to execute this particular callback before the one which calls the original method
880
- * (which uses the "normal" priority).
881
- *
882
- * It is also possible to change the returned value:
883
- *
884
- * foo.on( 'method', ( evt ) => {
885
- * evt.return = 'Foo!';
886
- * } );
887
- *
888
- * foo.method(); // -> 'Foo'
889
- *
890
- * Finally, it is possible to access and modify the arguments the method is called with:
891
- *
892
- * method( a, b ) {
893
- * console.log( `${ a }, ${ b }` );
894
- * }
895
- *
896
- * // ...
897
- *
898
- * foo.on( 'method', ( evt, args ) => {
899
- * args[ 0 ] = 3;
900
- *
901
- * console.log( args[ 1 ] ); // -> 2
902
- * }, { priority: 'high' } );
903
- *
904
- * foo.method( 1, 2 ); // -> '3, 2'
905
- *
906
- * @method #decorate
907
- * @param {String} methodName Name of the method to decorate.
908
- */