8
0

observablemixin.js 28 KB

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