observablemixin.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module utils/observablemixin
  7. */
  8. import EmitterMixin from './emittermixin';
  9. import CKEditorError from './ckeditorerror';
  10. import extend from './lib/lodash/extend';
  11. import isObject from './lib/lodash/isObject';
  12. const attributesSymbol = Symbol( 'attributes' );
  13. const boundObservablesSymbol = Symbol( 'boundObservables' );
  14. const boundAttributesSymbol = Symbol( 'boundAttributes' );
  15. /**
  16. * Mixin that injects the "observable attributes" and data binding functionality.
  17. * Used mainly in the {@link module:ui/model~Model} class.
  18. *
  19. * @mixin ObservableMixin
  20. * @mixes module:utils/emittermixin~EmitterMixin
  21. * @implements module:utils/observablemixin~Observable
  22. */
  23. const ObservableMixin = {
  24. /**
  25. * Creates and sets the value of an observable attribute of this object. Such an attribute becomes a part
  26. * of the state and is be observable.
  27. *
  28. * It accepts also a single object literal containing key/value pairs with attributes to be set.
  29. *
  30. * This method throws the observable-set-cannot-override error if the observable instance already
  31. * have a property with a given attribute name. This prevents from mistakenly overriding existing
  32. * properties and methods, but means that `foo.set( 'bar', 1 )` may be slightly slower than `foo.bar = 1`.
  33. *
  34. * @method #set
  35. * @param {String} name The attributes name.
  36. * @param {*} value The attributes value.
  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( attr => {
  42. this.set( attr, name[ attr ] );
  43. }, this );
  44. return;
  45. }
  46. initObservable( this );
  47. const attributes = this[ attributesSymbol ];
  48. if ( ( name in this ) && !attributes.has( name ) ) {
  49. /**
  50. * Cannot override an existing property.
  51. *
  52. * This error is thrown when trying to {@link ~Observable#set set} an attribute with
  53. * a name of an already existing property. For example:
  54. *
  55. * let observable = new Model();
  56. * observable.property = 1;
  57. * observable.set( 'property', 2 ); // throws
  58. *
  59. * observable.set( 'attr', 1 );
  60. * observable.set( 'attr', 2 ); // ok, because this is an existing attribute.
  61. *
  62. * @error observable-set-cannot-override
  63. */
  64. throw new CKEditorError( 'observable-set-cannot-override: Cannot override an existing property.' );
  65. }
  66. Object.defineProperty( this, name, {
  67. enumerable: true,
  68. configurable: true,
  69. get() {
  70. return attributes.get( name );
  71. },
  72. set( value ) {
  73. const oldValue = attributes.get( name );
  74. // Allow undefined as an initial value like A.define( 'x', undefined ) (#132).
  75. // Note: When attributes map has no such own property, then its value is undefined.
  76. if ( oldValue !== value || !attributes.has( name ) ) {
  77. attributes.set( name, value );
  78. this.fire( 'change:' + name, name, value, oldValue );
  79. }
  80. }
  81. } );
  82. this[ name ] = value;
  83. },
  84. /**
  85. * Binds observable attributes to another objects implementing {@link ~ObservableMixin}
  86. * interface (like {@link module:ui/model~Model}).
  87. *
  88. * Once bound, the observable will immediately share the current state of attributes
  89. * of the observable it is bound to and react to the changes to these attributes
  90. * in the future.
  91. *
  92. * **Note**: To release the binding use {@link module:utils/observablemixin~ObservableMixin#unbind}.
  93. *
  94. * A.bind( 'a' ).to( B );
  95. * A.bind( 'a' ).to( B, 'b' );
  96. * A.bind( 'a', 'b' ).to( B, 'c', 'd' );
  97. * A.bind( 'a' ).to( B, 'b', C, 'd', ( b, d ) => b + d );
  98. *
  99. * @method #bind
  100. * @param {...String} bindAttrs Observable attributes that will be bound to another observable(s).
  101. * @returns {module:utils/observablemixin~BindChain}
  102. */
  103. bind( ...bindAttrs ) {
  104. if ( !bindAttrs.length || !isStringArray( bindAttrs ) ) {
  105. /**
  106. * All attributes must be strings.
  107. *
  108. * @error observable-bind-wrong-attrs
  109. */
  110. throw new CKEditorError( 'observable-bind-wrong-attrs: All attributes must be strings.' );
  111. }
  112. if ( ( new Set( bindAttrs ) ).size !== bindAttrs.length ) {
  113. /**
  114. * Attributes must be unique.
  115. *
  116. * @error observable-bind-duplicate-attrs
  117. */
  118. throw new CKEditorError( 'observable-bind-duplicate-attrs: Attributes must be unique.' );
  119. }
  120. initObservable( this );
  121. const boundAttributes = this[ boundAttributesSymbol ];
  122. bindAttrs.forEach( attrName => {
  123. if ( boundAttributes.has( attrName ) ) {
  124. /**
  125. * Cannot bind the same attribute more that once.
  126. *
  127. * @error observable-bind-rebind
  128. */
  129. throw new CKEditorError( 'observable-bind-rebind: Cannot bind the same attribute more that once.' );
  130. }
  131. } );
  132. const bindings = new Map();
  133. /**
  134. * @typedef Binding
  135. * @type Object
  136. * @property {Array} attr Attribute which is bound.
  137. * @property {Array} to Array of observable–attribute components of the binding (`{ observable: ..., attr: .. }`).
  138. * @property {Array} callback A function which processes `to` components.
  139. */
  140. bindAttrs.forEach( a => {
  141. const binding = { attr: a, to: [] };
  142. boundAttributes.set( a, binding );
  143. bindings.set( a, binding );
  144. } );
  145. /**
  146. * @typedef BindChain
  147. * @type Object
  148. * @property {Function} to See {@link ~ObservableMixin#_bindTo}.
  149. * @property {module:utils/observablemixin~Observable} _observable The observable which initializes the binding.
  150. * @property {Array} _bindAttrs Array of `_observable` attributes to be bound.
  151. * @property {Array} _to Array of `to()` observable–attributes (`{ observable: toObservable, attrs: ...toAttrs }`).
  152. * @property {Map} _bindings Stores bindings to be kept in
  153. * {@link ~ObservableMixin#_boundAttributes}/{@link ~ObservableMixin#_boundObservables}
  154. * initiated in this binding chain.
  155. */
  156. return {
  157. to: bindTo,
  158. _observable: this,
  159. _bindAttrs: bindAttrs,
  160. _to: [],
  161. _bindings: bindings
  162. };
  163. },
  164. /**
  165. * Removes the binding created with {@link ~ObservableMixin#bind}.
  166. *
  167. * A.unbind( 'a' );
  168. * A.unbind();
  169. *
  170. * @method #unbind
  171. * @param {...String} [unbindAttrs] Observable attributes to be unbound. All the bindings will
  172. * be released if no attributes provided.
  173. */
  174. unbind( ...unbindAttrs ) {
  175. // Nothing to do here if not inited yet.
  176. if ( !( attributesSymbol in this ) ) {
  177. return;
  178. }
  179. const boundAttributes = this[ boundAttributesSymbol ];
  180. const boundObservables = this[ boundObservablesSymbol ];
  181. if ( unbindAttrs.length ) {
  182. if ( !isStringArray( unbindAttrs ) ) {
  183. /**
  184. * Attributes must be strings.
  185. *
  186. * @error observable-unbind-wrong-attrs
  187. */
  188. throw new CKEditorError( 'observable-unbind-wrong-attrs: Attributes must be strings.' );
  189. }
  190. unbindAttrs.forEach( attrName => {
  191. const binding = boundAttributes.get( attrName );
  192. let toObservable, toAttr, toAttrs, toAttrBindings;
  193. binding.to.forEach( to => {
  194. // TODO: ES6 destructuring.
  195. toObservable = to[ 0 ];
  196. toAttr = to[ 1 ];
  197. toAttrs = boundObservables.get( toObservable );
  198. toAttrBindings = toAttrs[ toAttr ];
  199. toAttrBindings.delete( binding );
  200. if ( !toAttrBindings.size ) {
  201. delete toAttrs[ toAttr ];
  202. }
  203. if ( !Object.keys( toAttrs ).length ) {
  204. boundObservables.delete( toObservable );
  205. this.stopListening( toObservable, 'change' );
  206. }
  207. } );
  208. boundAttributes.delete( attrName );
  209. } );
  210. } else {
  211. boundObservables.forEach( ( bindings, boundObservable ) => {
  212. this.stopListening( boundObservable, 'change' );
  213. } );
  214. boundObservables.clear();
  215. boundAttributes.clear();
  216. }
  217. },
  218. /**
  219. * Turns the given methods of this object into event-based ones. This means that the new method will fire an event
  220. * (named after the method) and the original action will be plugged as a listener to that event.
  221. *
  222. * This is a very simplified method decoration. Itself it doesn't change the behavior of a method (expect adding the event),
  223. * but it allows to modify it later on by listening to the method's event.
  224. *
  225. * For example, in order to cancel the method execution one can stop the event:
  226. *
  227. * class Foo {
  228. * constructor() {
  229. * this.decorate( 'method' );
  230. * }
  231. *
  232. * method() {
  233. * console.log( 'called!' );
  234. * }
  235. * }
  236. *
  237. * const foo = new Foo();
  238. * foo.on( 'method', ( evt ) => {
  239. * evt.stop();
  240. * }, { priority: 'high' } );
  241. *
  242. * foo.method(); // Nothing is logged.
  243. *
  244. *
  245. * Note: we used a high priority listener here to execute this callback before the one which
  246. * calls the orignal method (which used the default priority).
  247. *
  248. * It's also possible to change the return value:
  249. *
  250. * foo.on( 'method', ( evt ) => {
  251. * evt.return = 'Foo!';
  252. * } );
  253. *
  254. * foo.method(); // -> 'Foo'
  255. *
  256. * Finally, it's possible to access and modify the parameters:
  257. *
  258. * method( a, b ) {
  259. * console.log( `${ a }, ${ b }` );
  260. * }
  261. *
  262. * // ...
  263. *
  264. * foo.on( 'method', ( evt, args ) => {
  265. * args[ 0 ] = 3;
  266. *
  267. * console.log( args[ 1 ] ); // -> 2
  268. * }, { priority: 'high' } );
  269. *
  270. * foo.method( 1, 2 ); // -> '3, 2'
  271. *
  272. * @param {String} ...methodNames Names of the methods to decorate.
  273. */
  274. decorate( ...methodNames ) {
  275. methodNames.forEach( methodName => {
  276. const originalMethod = this[ methodName ];
  277. if ( !originalMethod ) {
  278. /**
  279. * Cannot decorate an undefined method.
  280. *
  281. * @error observablemixin-cannot-decorate-undefined
  282. * @param {Object} object The object on which you try to decorate a method.
  283. * @param {String} methodName Name of the method which does not exist.
  284. */
  285. throw new CKEditorError(
  286. 'observablemixin-cannot-decorate-undefined: Cannot decorate an undefined method.',
  287. { object: this, methodName }
  288. );
  289. }
  290. this.on( methodName, ( evt, args ) => {
  291. evt.return = originalMethod.apply( this, args );
  292. } );
  293. this[ methodName ] = function( ...args ) {
  294. return this.fire( methodName, args );
  295. };
  296. } );
  297. }
  298. /**
  299. * @private
  300. * @member ~ObservableMixin#_boundAttributes
  301. */
  302. /**
  303. * @private
  304. * @member ~ObservableMixin#_boundObservables
  305. */
  306. /**
  307. * @private
  308. * @member ~ObservableMixin#_bindTo
  309. */
  310. };
  311. export default ObservableMixin;
  312. // Init symbol properties needed to for the observable mechanism to work.
  313. //
  314. // @private
  315. // @param {module:utils/observablemixin~ObservableMixin} observable
  316. function initObservable( observable ) {
  317. // Do nothing if already inited.
  318. if ( attributesSymbol in observable ) {
  319. return;
  320. }
  321. // The internal hash containing the observable's state.
  322. //
  323. // @private
  324. // @type {Map}
  325. Object.defineProperty( observable, attributesSymbol, {
  326. value: new Map()
  327. } );
  328. // Map containing bindings to external observables. It shares the binding objects
  329. // (`{ observable: A, attr: 'a', to: ... }`) with {@link module:utils/observablemixin~ObservableMixin#_boundAttributes} and
  330. // it is used to observe external observables to update own attributes accordingly.
  331. // See {@link module:utils/observablemixin~ObservableMixin#bind}.
  332. //
  333. // A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
  334. // console.log( A._boundObservables );
  335. //
  336. // Map( {
  337. // B: {
  338. // x: Set( [
  339. // { observable: A, attr: 'a', to: [ [ B, 'x' ] ] },
  340. // { observable: A, attr: 'c', to: [ [ B, 'x' ] ] }
  341. // ] ),
  342. // y: Set( [
  343. // { observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
  344. // ] )
  345. // }
  346. // } )
  347. //
  348. // A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
  349. // console.log( A._boundObservables );
  350. //
  351. // Map( {
  352. // B: {
  353. // x: Set( [
  354. // { observable: A, attr: 'a', to: [ [ B, 'x' ] ] },
  355. // { observable: A, attr: 'c', to: [ [ B, 'x' ] ] }
  356. // ] ),
  357. // y: Set( [
  358. // { observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
  359. // ] ),
  360. // z: Set( [
  361. // { observable: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
  362. // ] )
  363. // },
  364. // C: {
  365. // w: Set( [
  366. // { observable: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
  367. // ] )
  368. // }
  369. // } )
  370. //
  371. // @private
  372. // @type {Map}
  373. Object.defineProperty( observable, boundObservablesSymbol, {
  374. value: new Map()
  375. } );
  376. // Object that stores which attributes of this observable are bound and how. It shares
  377. // the binding objects (`{ observable: A, attr: 'a', to: ... }`) with {@link utils.ObservableMixin#_boundObservables}.
  378. // This data structure is a reverse of {@link utils.ObservableMixin#_boundObservables} and it is helpful for
  379. // {@link utils.ObservableMixin#unbind}.
  380. //
  381. // See {@link utils.ObservableMixin#bind}.
  382. //
  383. // A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
  384. // console.log( A._boundAttributes );
  385. //
  386. // Map( {
  387. // a: { observable: A, attr: 'a', to: [ [ B, 'x' ] ] },
  388. // b: { observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
  389. // c: { observable: A, attr: 'c', to: [ [ B, 'x' ] ] }
  390. // } )
  391. //
  392. // A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
  393. // console.log( A._boundAttributes );
  394. //
  395. // Map( {
  396. // a: { observable: A, attr: 'a', to: [ [ B, 'x' ] ] },
  397. // b: { observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
  398. // c: { observable: A, attr: 'c', to: [ [ B, 'x' ] ] },
  399. // d: { observable: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
  400. // } )
  401. //
  402. // @private
  403. // @type {Map}
  404. Object.defineProperty( observable, boundAttributesSymbol, {
  405. value: new Map()
  406. } );
  407. }
  408. // A chaining for {@link module:utils/observablemixin~ObservableMixin#bind} providing `.to()` interface.
  409. //
  410. // @private
  411. // @param {...[Observable|String|Function]} args Arguments of the `.to( args )` binding.
  412. function bindTo( ...args ) {
  413. const parsedArgs = parseBindToArgs( ...args );
  414. const bindingsKeys = Array.from( this._bindings.keys() );
  415. const numberOfBindings = bindingsKeys.length;
  416. // Eliminate A.bind( 'x' ).to( B, C )
  417. if ( !parsedArgs.callback && parsedArgs.to.length > 1 ) {
  418. /**
  419. * Binding multiple observables only possible with callback.
  420. *
  421. * @error observable-bind-no-callback
  422. */
  423. throw new CKEditorError( 'observable-bind-to-no-callback: Binding multiple observables only possible with callback.' );
  424. }
  425. // Eliminate A.bind( 'x', 'y' ).to( B, callback )
  426. if ( numberOfBindings > 1 && parsedArgs.callback ) {
  427. /**
  428. * Cannot bind multiple attributes and use a callback in one binding.
  429. *
  430. * @error observable-bind-to-extra-callback
  431. */
  432. throw new CKEditorError( 'observable-bind-to-extra-callback: Cannot bind multiple attributes and use a callback in one binding.' );
  433. }
  434. parsedArgs.to.forEach( to => {
  435. // Eliminate A.bind( 'x', 'y' ).to( B, 'a' )
  436. if ( to.attrs.length && to.attrs.length !== numberOfBindings ) {
  437. /**
  438. * The number of attributes must match.
  439. *
  440. * @error observable-bind-to-attrs-length
  441. */
  442. throw new CKEditorError( 'observable-bind-to-attrs-length: The number of attributes must match.' );
  443. }
  444. // When no to.attrs specified, observing source attributes instead i.e.
  445. // A.bind( 'x', 'y' ).to( B ) -> Observe B.x and B.y
  446. if ( !to.attrs.length ) {
  447. to.attrs = this._bindAttrs;
  448. }
  449. } );
  450. this._to = parsedArgs.to;
  451. // Fill {@link BindChain#_bindings} with callback. When the callback is set there's only one binding.
  452. if ( parsedArgs.callback ) {
  453. this._bindings.get( bindingsKeys[ 0 ] ).callback = parsedArgs.callback;
  454. }
  455. attachBindToListeners( this._observable, this._to );
  456. // Update observable._boundAttributes and observable._boundObservables.
  457. updateBindToBound( this );
  458. // Set initial values of bound attributes.
  459. this._bindAttrs.forEach( attrName => {
  460. updateBoundObservableAttr( this._observable, attrName );
  461. } );
  462. }
  463. // Check if all entries of the array are of `String` type.
  464. //
  465. // @private
  466. // @param {Array} arr An array to be checked.
  467. // @returns {Boolean}
  468. function isStringArray( arr ) {
  469. return arr.every( a => typeof a == 'string' );
  470. }
  471. // Parses and validates {@link Observable#bind}`.to( args )` arguments and returns
  472. // an object with a parsed structure. For example
  473. //
  474. // A.bind( 'x' ).to( B, 'a', C, 'b', call );
  475. //
  476. // becomes
  477. //
  478. // {
  479. // to: [
  480. // { observable: B, attrs: [ 'a' ] },
  481. // { observable: C, attrs: [ 'b' ] },
  482. // ],
  483. // callback: call
  484. // }
  485. //
  486. // @private
  487. // @param {...*} args Arguments of {@link Observable#bind}`.to( args )`.
  488. // @returns {Object}
  489. function parseBindToArgs( ...args ) {
  490. // Eliminate A.bind( 'x' ).to()
  491. if ( !args.length ) {
  492. /**
  493. * Invalid argument syntax in `to()`.
  494. *
  495. * @error observable-bind-to-parse-error
  496. */
  497. throw new CKEditorError( 'observable-bind-to-parse-error: Invalid argument syntax in `to()`.' );
  498. }
  499. const parsed = { to: [] };
  500. let lastObservable;
  501. if ( typeof args[ args.length - 1 ] == 'function' ) {
  502. parsed.callback = args.pop();
  503. }
  504. args.forEach( a => {
  505. if ( typeof a == 'string' ) {
  506. lastObservable.attrs.push( a );
  507. } else if ( typeof a == 'object' ) {
  508. lastObservable = { observable: a, attrs: [] };
  509. parsed.to.push( lastObservable );
  510. } else {
  511. throw new CKEditorError( 'observable-bind-to-parse-error: Invalid argument syntax in `to()`.' );
  512. }
  513. } );
  514. return parsed;
  515. }
  516. // Synchronizes {@link module:utils/observablemixin#_boundObservables} with {@link Binding}.
  517. //
  518. // @private
  519. // @param {Binding} binding A binding to store in {@link Observable#_boundObservables}.
  520. // @param {Observable} toObservable A observable, which is a new component of `binding`.
  521. // @param {String} toAttrName A name of `toObservable`'s attribute, a new component of the `binding`.
  522. function updateBoundObservables( observable, binding, toObservable, toAttrName ) {
  523. const boundObservables = observable[ boundObservablesSymbol ];
  524. const bindingsToObservable = boundObservables.get( toObservable );
  525. const bindings = bindingsToObservable || {};
  526. if ( !bindings[ toAttrName ] ) {
  527. bindings[ toAttrName ] = new Set();
  528. }
  529. // Pass the binding to a corresponding Set in `observable._boundObservables`.
  530. bindings[ toAttrName ].add( binding );
  531. if ( !bindingsToObservable ) {
  532. boundObservables.set( toObservable, bindings );
  533. }
  534. }
  535. // Synchronizes {@link Observable#_boundAttributes} and {@link Observable#_boundObservables}
  536. // with {@link BindChain}.
  537. //
  538. // Assuming the following binding being created
  539. //
  540. // A.bind( 'a', 'b' ).to( B, 'x', 'y' );
  541. //
  542. // the following bindings were initialized by {@link Observable#bind} in {@link BindChain#_bindings}:
  543. //
  544. // {
  545. // a: { observable: A, attr: 'a', to: [] },
  546. // b: { observable: A, attr: 'b', to: [] },
  547. // }
  548. //
  549. // Iterate over all bindings in this chain and fill their `to` properties with
  550. // corresponding to( ... ) arguments (components of the binding), so
  551. //
  552. // {
  553. // a: { observable: A, attr: 'a', to: [ B, 'x' ] },
  554. // b: { observable: A, attr: 'b', to: [ B, 'y' ] },
  555. // }
  556. //
  557. // Then update the structure of {@link Observable#_boundObservables} with updated
  558. // binding, so it becomes:
  559. //
  560. // Map( {
  561. // B: {
  562. // x: Set( [
  563. // { observable: A, attr: 'a', to: [ [ B, 'x' ] ] }
  564. // ] ),
  565. // y: Set( [
  566. // { observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
  567. // ] )
  568. // }
  569. // } )
  570. //
  571. // @private
  572. // @param {BindChain} chain The binding initialized by {@link Observable#bind}.
  573. function updateBindToBound( chain ) {
  574. let toAttr;
  575. chain._bindings.forEach( ( binding, attrName ) => {
  576. // Note: For a binding without a callback, this will run only once
  577. // like in A.bind( 'x', 'y' ).to( B, 'a', 'b' )
  578. // TODO: ES6 destructuring.
  579. chain._to.forEach( to => {
  580. toAttr = to.attrs[ binding.callback ? 0 : chain._bindAttrs.indexOf( attrName ) ];
  581. binding.to.push( [ to.observable, toAttr ] );
  582. updateBoundObservables( chain._observable, binding, to.observable, toAttr );
  583. } );
  584. } );
  585. }
  586. // Updates an attribute of a {@link Observable} with a value
  587. // determined by an entry in {@link Observable#_boundAttributes}.
  588. //
  589. // @private
  590. // @param {Observable} observable A observable which attribute is to be updated.
  591. // @param {String} attrName An attribute to be updated.
  592. function updateBoundObservableAttr( observable, attrName ) {
  593. const boundAttributes = observable[ boundAttributesSymbol ];
  594. const binding = boundAttributes.get( attrName );
  595. let attrValue;
  596. // When a binding with callback is created like
  597. //
  598. // A.bind( 'a' ).to( B, 'b', C, 'c', callback );
  599. //
  600. // collect B.b and C.c, then pass them to callback to set A.a.
  601. if ( binding.callback ) {
  602. attrValue = binding.callback.apply( observable, binding.to.map( to => to[ 0 ][ to[ 1 ] ] ) );
  603. } else {
  604. attrValue = binding.to[ 0 ];
  605. attrValue = attrValue[ 0 ][ attrValue[ 1 ] ];
  606. }
  607. if ( observable.hasOwnProperty( attrName ) ) {
  608. observable[ attrName ] = attrValue;
  609. } else {
  610. observable.set( attrName, attrValue );
  611. }
  612. }
  613. // Starts listening to changes in {@link BindChain._to} observables to update
  614. // {@link BindChain._observable} {@link BindChain._bindAttrs}. Also sets the
  615. // initial state of {@link BindChain._observable}.
  616. //
  617. // @private
  618. // @param {BindChain} chain The chain initialized by {@link Observable#bind}.
  619. function attachBindToListeners( observable, toBindings ) {
  620. toBindings.forEach( to => {
  621. const boundObservables = observable[ boundObservablesSymbol ];
  622. let bindings;
  623. // If there's already a chain between the observables (`observable` listens to
  624. // `to.observable`), there's no need to create another `change` event listener.
  625. if ( !boundObservables.get( to.observable ) ) {
  626. observable.listenTo( to.observable, 'change', ( evt, attrName ) => {
  627. bindings = boundObservables.get( to.observable )[ attrName ];
  628. // Note: to.observable will fire for any attribute change, react
  629. // to changes of attributes which are bound only.
  630. if ( bindings ) {
  631. bindings.forEach( binding => {
  632. updateBoundObservableAttr( observable, binding.attr );
  633. } );
  634. }
  635. } );
  636. }
  637. } );
  638. }
  639. extend( ObservableMixin, EmitterMixin );
  640. /**
  641. * Fired when an attribute changed value.
  642. *
  643. * @event module:utils/observablemixin~ObservableMixin#change:{attribute}
  644. * @param {String} name The attribute name.
  645. * @param {*} value The new attribute value.
  646. * @param {*} oldValue The previous attribute value.
  647. */
  648. /**
  649. * Interface representing classes which mix in {@link module:utils/observablemixin~ObservableMixin}.
  650. *
  651. * @interface Observable
  652. */