8
0

observablemixin.js 26 KB

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