8
0

observablemixin.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875
  1. /**
  2. * @license Copyright (c) 2003-2020, 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. * A 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} a 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', this );
  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', this );
  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', this );
  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 than once.
  110. *
  111. * @error observable-bind-rebind
  112. */
  113. throw new CKEditorError( 'observable-bind-rebind', this );
  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 ( !( this[ observablePropertiesSymbol ] ) ) {
  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', this );
  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',
  210. this,
  211. { object: this, methodName }
  212. );
  213. }
  214. this.on( methodName, ( evt, args ) => {
  215. evt.return = originalMethod.apply( this, args );
  216. } );
  217. this[ methodName ] = function( ...args ) {
  218. return this.fire( methodName, args );
  219. };
  220. }
  221. };
  222. extend( ObservableMixin, EmitterMixin );
  223. export default ObservableMixin;
  224. // Init symbol properties needed for the observable mechanism to work.
  225. //
  226. // @private
  227. // @param {module:utils/observablemixin~ObservableMixin} observable
  228. function initObservable( observable ) {
  229. // Do nothing if already inited.
  230. if ( observable[ observablePropertiesSymbol ] ) {
  231. return;
  232. }
  233. // The internal hash containing the observable's state.
  234. //
  235. // @private
  236. // @type {Map}
  237. Object.defineProperty( observable, observablePropertiesSymbol, {
  238. value: new Map()
  239. } );
  240. // Map containing bindings to external observables. It shares the binding objects
  241. // (`{ observable: A, property: 'a', to: ... }`) with {@link module:utils/observablemixin~ObservableMixin#_boundProperties} and
  242. // it is used to observe external observables to update own properties accordingly.
  243. // See {@link module:utils/observablemixin~ObservableMixin#bind}.
  244. //
  245. // A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
  246. // console.log( A._boundObservables );
  247. //
  248. // Map( {
  249. // B: {
  250. // x: Set( [
  251. // { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
  252. // { observable: A, property: 'c', to: [ [ B, 'x' ] ] }
  253. // ] ),
  254. // y: Set( [
  255. // { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
  256. // ] )
  257. // }
  258. // } )
  259. //
  260. // A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
  261. // console.log( A._boundObservables );
  262. //
  263. // Map( {
  264. // B: {
  265. // x: Set( [
  266. // { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
  267. // { observable: A, property: 'c', to: [ [ B, 'x' ] ] }
  268. // ] ),
  269. // y: Set( [
  270. // { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
  271. // ] ),
  272. // z: Set( [
  273. // { observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
  274. // ] )
  275. // },
  276. // C: {
  277. // w: Set( [
  278. // { observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
  279. // ] )
  280. // }
  281. // } )
  282. //
  283. // @private
  284. // @type {Map}
  285. Object.defineProperty( observable, boundObservablesSymbol, {
  286. value: new Map()
  287. } );
  288. // Object that stores which properties of this observable are bound and how. It shares
  289. // the binding objects (`{ observable: A, property: 'a', to: ... }`) with
  290. // {@link module:utils/observablemixin~ObservableMixin#_boundObservables}. This data structure is
  291. // a reverse of {@link module:utils/observablemixin~ObservableMixin#_boundObservables} and it is helpful for
  292. // {@link module:utils/observablemixin~ObservableMixin#unbind}.
  293. //
  294. // See {@link module:utils/observablemixin~ObservableMixin#bind}.
  295. //
  296. // A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
  297. // console.log( A._boundProperties );
  298. //
  299. // Map( {
  300. // a: { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
  301. // b: { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
  302. // c: { observable: A, property: 'c', to: [ [ B, 'x' ] ] }
  303. // } )
  304. //
  305. // A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
  306. // console.log( A._boundProperties );
  307. //
  308. // Map( {
  309. // a: { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
  310. // b: { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
  311. // c: { observable: A, property: 'c', to: [ [ B, 'x' ] ] },
  312. // d: { observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
  313. // } )
  314. //
  315. // @private
  316. // @type {Map}
  317. Object.defineProperty( observable, boundPropertiesSymbol, {
  318. value: new Map()
  319. } );
  320. }
  321. // A chaining for {@link module:utils/observablemixin~ObservableMixin#bind} providing `.to()` interface.
  322. //
  323. // @private
  324. // @param {...[Observable|String|Function]} args Arguments of the `.to( args )` binding.
  325. function bindTo( ...args ) {
  326. const parsedArgs = parseBindToArgs( ...args );
  327. const bindingsKeys = Array.from( this._bindings.keys() );
  328. const numberOfBindings = bindingsKeys.length;
  329. // Eliminate A.bind( 'x' ).to( B, C )
  330. if ( !parsedArgs.callback && parsedArgs.to.length > 1 ) {
  331. /**
  332. * Binding multiple observables only possible with callback.
  333. *
  334. * @error observable-bind-to-no-callback
  335. */
  336. throw new CKEditorError( 'observable-bind-to-no-callback', this );
  337. }
  338. // Eliminate A.bind( 'x', 'y' ).to( B, callback )
  339. if ( numberOfBindings > 1 && parsedArgs.callback ) {
  340. /**
  341. * Cannot bind multiple properties and use a callback in one binding.
  342. *
  343. * @error observable-bind-to-extra-callback
  344. */
  345. throw new CKEditorError(
  346. 'observable-bind-to-extra-callback',
  347. this
  348. );
  349. }
  350. parsedArgs.to.forEach( to => {
  351. // Eliminate A.bind( 'x', 'y' ).to( B, 'a' )
  352. if ( to.properties.length && to.properties.length !== numberOfBindings ) {
  353. /**
  354. * The number of properties must match.
  355. *
  356. * @error observable-bind-to-properties-length
  357. */
  358. throw new CKEditorError( 'observable-bind-to-properties-length', this );
  359. }
  360. // When no to.properties specified, observing source properties instead i.e.
  361. // A.bind( 'x', 'y' ).to( B ) -> Observe B.x and B.y
  362. if ( !to.properties.length ) {
  363. to.properties = this._bindProperties;
  364. }
  365. } );
  366. this._to = parsedArgs.to;
  367. // Fill {@link BindChain#_bindings} with callback. When the callback is set there's only one binding.
  368. if ( parsedArgs.callback ) {
  369. this._bindings.get( bindingsKeys[ 0 ] ).callback = parsedArgs.callback;
  370. }
  371. attachBindToListeners( this._observable, this._to );
  372. // Update observable._boundProperties and observable._boundObservables.
  373. updateBindToBound( this );
  374. // Set initial values of bound properties.
  375. this._bindProperties.forEach( propertyName => {
  376. updateBoundObservableProperty( this._observable, propertyName );
  377. } );
  378. }
  379. // Binds to an attribute in a set of iterable observables.
  380. //
  381. // @private
  382. // @param {Array.<Observable>} observables
  383. // @param {String} attribute
  384. // @param {Function} callback
  385. function bindToMany( observables, attribute, callback ) {
  386. if ( this._bindings.size > 1 ) {
  387. /**
  388. * Binding one attribute to many observables only possible with one attribute.
  389. *
  390. * @error observable-bind-to-many-not-one-binding
  391. */
  392. throw new CKEditorError( 'observable-bind-to-many-not-one-binding', this );
  393. }
  394. this.to(
  395. // Bind to #attribute of each observable...
  396. ...getBindingTargets( observables, attribute ),
  397. // ...using given callback to parse attribute values.
  398. callback
  399. );
  400. }
  401. // Returns an array of binding components for
  402. // {@link Observable#bind} from a set of iterable observables.
  403. //
  404. // @param {Array.<Observable>} observables
  405. // @param {String} attribute
  406. // @returns {Array.<String|Observable>}
  407. function getBindingTargets( observables, attribute ) {
  408. const observableAndAttributePairs = observables.map( observable => [ observable, attribute ] );
  409. // Merge pairs to one-dimension array of observables and attributes.
  410. return Array.prototype.concat.apply( [], observableAndAttributePairs );
  411. }
  412. // Check if all entries of the array are of `String` type.
  413. //
  414. // @private
  415. // @param {Array} arr An array to be checked.
  416. // @returns {Boolean}
  417. function isStringArray( arr ) {
  418. return arr.every( a => typeof a == 'string' );
  419. }
  420. // Parses and validates {@link Observable#bind}`.to( args )` arguments and returns
  421. // an object with a parsed structure. For example
  422. //
  423. // A.bind( 'x' ).to( B, 'a', C, 'b', call );
  424. //
  425. // becomes
  426. //
  427. // {
  428. // to: [
  429. // { observable: B, properties: [ 'a' ] },
  430. // { observable: C, properties: [ 'b' ] },
  431. // ],
  432. // callback: call
  433. // }
  434. //
  435. // @private
  436. // @param {...*} args Arguments of {@link Observable#bind}`.to( args )`.
  437. // @returns {Object}
  438. function parseBindToArgs( ...args ) {
  439. // Eliminate A.bind( 'x' ).to()
  440. if ( !args.length ) {
  441. /**
  442. * Invalid argument syntax in `to()`.
  443. *
  444. * @error observable-bind-to-parse-error
  445. */
  446. throw new CKEditorError( 'observable-bind-to-parse-error', null );
  447. }
  448. const parsed = { to: [] };
  449. let lastObservable;
  450. if ( typeof args[ args.length - 1 ] == 'function' ) {
  451. parsed.callback = args.pop();
  452. }
  453. args.forEach( a => {
  454. if ( typeof a == 'string' ) {
  455. lastObservable.properties.push( a );
  456. } else if ( typeof a == 'object' ) {
  457. lastObservable = { observable: a, properties: [] };
  458. parsed.to.push( lastObservable );
  459. } else {
  460. throw new CKEditorError( 'observable-bind-to-parse-error', null );
  461. }
  462. } );
  463. return parsed;
  464. }
  465. // Synchronizes {@link module:utils/observablemixin#_boundObservables} with {@link Binding}.
  466. //
  467. // @private
  468. // @param {Binding} binding A binding to store in {@link Observable#_boundObservables}.
  469. // @param {Observable} toObservable A observable, which is a new component of `binding`.
  470. // @param {String} toPropertyName A name of `toObservable`'s property, a new component of the `binding`.
  471. function updateBoundObservables( observable, binding, toObservable, toPropertyName ) {
  472. const boundObservables = observable[ boundObservablesSymbol ];
  473. const bindingsToObservable = boundObservables.get( toObservable );
  474. const bindings = bindingsToObservable || {};
  475. if ( !bindings[ toPropertyName ] ) {
  476. bindings[ toPropertyName ] = new Set();
  477. }
  478. // Pass the binding to a corresponding Set in `observable._boundObservables`.
  479. bindings[ toPropertyName ].add( binding );
  480. if ( !bindingsToObservable ) {
  481. boundObservables.set( toObservable, bindings );
  482. }
  483. }
  484. // Synchronizes {@link Observable#_boundProperties} and {@link Observable#_boundObservables}
  485. // with {@link BindChain}.
  486. //
  487. // Assuming the following binding being created
  488. //
  489. // A.bind( 'a', 'b' ).to( B, 'x', 'y' );
  490. //
  491. // the following bindings were initialized by {@link Observable#bind} in {@link BindChain#_bindings}:
  492. //
  493. // {
  494. // a: { observable: A, property: 'a', to: [] },
  495. // b: { observable: A, property: 'b', to: [] },
  496. // }
  497. //
  498. // Iterate over all bindings in this chain and fill their `to` properties with
  499. // corresponding to( ... ) arguments (components of the binding), so
  500. //
  501. // {
  502. // a: { observable: A, property: 'a', to: [ B, 'x' ] },
  503. // b: { observable: A, property: 'b', to: [ B, 'y' ] },
  504. // }
  505. //
  506. // Then update the structure of {@link Observable#_boundObservables} with updated
  507. // binding, so it becomes:
  508. //
  509. // Map( {
  510. // B: {
  511. // x: Set( [
  512. // { observable: A, property: 'a', to: [ [ B, 'x' ] ] }
  513. // ] ),
  514. // y: Set( [
  515. // { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
  516. // ] )
  517. // }
  518. // } )
  519. //
  520. // @private
  521. // @param {BindChain} chain The binding initialized by {@link Observable#bind}.
  522. function updateBindToBound( chain ) {
  523. let toProperty;
  524. chain._bindings.forEach( ( binding, propertyName ) => {
  525. // Note: For a binding without a callback, this will run only once
  526. // like in A.bind( 'x', 'y' ).to( B, 'a', 'b' )
  527. // TODO: ES6 destructuring.
  528. chain._to.forEach( to => {
  529. toProperty = to.properties[ binding.callback ? 0 : chain._bindProperties.indexOf( propertyName ) ];
  530. binding.to.push( [ to.observable, toProperty ] );
  531. updateBoundObservables( chain._observable, binding, to.observable, toProperty );
  532. } );
  533. } );
  534. }
  535. // Updates an property of a {@link Observable} with a value
  536. // determined by an entry in {@link Observable#_boundProperties}.
  537. //
  538. // @private
  539. // @param {Observable} observable A observable which property is to be updated.
  540. // @param {String} propertyName An property to be updated.
  541. function updateBoundObservableProperty( observable, propertyName ) {
  542. const boundProperties = observable[ boundPropertiesSymbol ];
  543. const binding = boundProperties.get( propertyName );
  544. let propertyValue;
  545. // When a binding with callback is created like
  546. //
  547. // A.bind( 'a' ).to( B, 'b', C, 'c', callback );
  548. //
  549. // collect B.b and C.c, then pass them to callback to set A.a.
  550. if ( binding.callback ) {
  551. propertyValue = binding.callback.apply( observable, binding.to.map( to => to[ 0 ][ to[ 1 ] ] ) );
  552. } else {
  553. propertyValue = binding.to[ 0 ];
  554. propertyValue = propertyValue[ 0 ][ propertyValue[ 1 ] ];
  555. }
  556. if ( Object.prototype.hasOwnProperty.call( observable, propertyName ) ) {
  557. observable[ propertyName ] = propertyValue;
  558. } else {
  559. observable.set( propertyName, propertyValue );
  560. }
  561. }
  562. // Starts listening to changes in {@link BindChain._to} observables to update
  563. // {@link BindChain._observable} {@link BindChain._bindProperties}. Also sets the
  564. // initial state of {@link BindChain._observable}.
  565. //
  566. // @private
  567. // @param {BindChain} chain The chain initialized by {@link Observable#bind}.
  568. function attachBindToListeners( observable, toBindings ) {
  569. toBindings.forEach( to => {
  570. const boundObservables = observable[ boundObservablesSymbol ];
  571. let bindings;
  572. // If there's already a chain between the observables (`observable` listens to
  573. // `to.observable`), there's no need to create another `change` event listener.
  574. if ( !boundObservables.get( to.observable ) ) {
  575. observable.listenTo( to.observable, 'change', ( evt, propertyName ) => {
  576. bindings = boundObservables.get( to.observable )[ propertyName ];
  577. // Note: to.observable will fire for any property change, react
  578. // to changes of properties which are bound only.
  579. if ( bindings ) {
  580. bindings.forEach( binding => {
  581. updateBoundObservableProperty( observable, binding.property );
  582. } );
  583. }
  584. } );
  585. }
  586. } );
  587. }
  588. /**
  589. * An interface which adds "observable properties" and data binding functionality.
  590. *
  591. * Can be easily implemented by a class by mixing the {@link module:utils/observablemixin~ObservableMixin} mixin.
  592. *
  593. * Read more about the usage of this interface in the:
  594. * * {@glink framework/guides/architecture/core-editor-architecture#event-system-and-observables "Event system and observables"}
  595. * section of the {@glink framework/guides/architecture/core-editor-architecture "Core editor architecture"} guide,
  596. * * {@glink framework/guides/deep-dive/observables "Observables" deep dive} guide.
  597. *
  598. * @interface Observable
  599. * @extends module:utils/emittermixin~Emitter
  600. */
  601. /**
  602. * Fired when a property changed value.
  603. *
  604. * observable.set( 'prop', 1 );
  605. *
  606. * observable.on( 'change:prop', ( evt, propertyName, newValue, oldValue ) => {
  607. * console.log( `${ propertyName } has changed from ${ oldValue } to ${ newValue }` );
  608. * } );
  609. *
  610. * observable.prop = 2; // -> 'prop has changed from 1 to 2'
  611. *
  612. * @event change:{property}
  613. * @param {String} name The property name.
  614. * @param {*} value The new property value.
  615. * @param {*} oldValue The previous property value.
  616. */
  617. /**
  618. * Fired when a property value is going to be set but is not set yet (before the `change` event is fired).
  619. *
  620. * You can control the final value of the property by using
  621. * the {@link module:utils/eventinfo~EventInfo#return event's `return` property}.
  622. *
  623. * observable.set( 'prop', 1 );
  624. *
  625. * observable.on( 'set:prop', ( evt, propertyName, newValue, oldValue ) => {
  626. * console.log( `Value is going to be changed from ${ oldValue } to ${ newValue }` );
  627. * console.log( `Current property value is ${ observable[ propertyName ] }` );
  628. *
  629. * // Let's override the value.
  630. * evt.return = 3;
  631. * } );
  632. *
  633. * observable.on( 'change:prop', ( evt, propertyName, newValue, oldValue ) => {
  634. * console.log( `Value has changed from ${ oldValue } to ${ newValue }` );
  635. * } );
  636. *
  637. * observable.prop = 2; // -> 'Value is going to be changed from 1 to 2'
  638. * // -> 'Current property value is 1'
  639. * // -> 'Value has changed from 1 to 3'
  640. *
  641. * **Note:** The event is fired even when the new value is the same as the old value.
  642. *
  643. * @event set:{property}
  644. * @param {String} name The property name.
  645. * @param {*} value The new property value.
  646. * @param {*} oldValue The previous property value.
  647. */
  648. /**
  649. * Creates and sets the value of an observable property of this object. Such a property becomes a part
  650. * of the state and is observable.
  651. *
  652. * It accepts also a single object literal containing key/value pairs with properties to be set.
  653. *
  654. * This method throws the `observable-set-cannot-override` error if the observable instance already
  655. * has a property with the given property name. This prevents from mistakenly overriding existing
  656. * properties and methods, but means that `foo.set( 'bar', 1 )` may be slightly slower than `foo.bar = 1`.
  657. *
  658. * @method #set
  659. * @param {String|Object} name The property's name or object with `name=>value` pairs.
  660. * @param {*} [value] The property's value (if `name` was passed in the first parameter).
  661. */
  662. /**
  663. * Binds {@link #set observable properties} to other objects implementing the
  664. * {@link module:utils/observablemixin~Observable} interface.
  665. *
  666. * Read more in the {@glink framework/guides/deep-dive/observables#property-bindings dedicated guide}
  667. * covering the topic of property bindings with some additional examples.
  668. *
  669. * Consider two objects: a `button` and an associated `command` (both `Observable`).
  670. *
  671. * A simple property binding could be as follows:
  672. *
  673. * button.bind( 'isEnabled' ).to( command, 'isEnabled' );
  674. *
  675. * or even shorter:
  676. *
  677. * button.bind( 'isEnabled' ).to( command );
  678. *
  679. * which works in the following way:
  680. *
  681. * * `button.isEnabled` **instantly equals** `command.isEnabled`,
  682. * * whenever `command.isEnabled` changes, `button.isEnabled` will immediately reflect its value.
  683. *
  684. * **Note**: To release the binding, use {@link module:utils/observablemixin~Observable#unbind}.
  685. *
  686. * You can also "rename" the property in the binding by specifying the new name in the `to()` chain:
  687. *
  688. * button.bind( 'isEnabled' ).to( command, 'isWorking' );
  689. *
  690. * It is possible to bind more than one property at a time to shorten the code:
  691. *
  692. * button.bind( 'isEnabled', 'value' ).to( command );
  693. *
  694. * which corresponds to:
  695. *
  696. * button.bind( 'isEnabled' ).to( command );
  697. * button.bind( 'value' ).to( command );
  698. *
  699. * The binding can include more than one observable, combining multiple data sources in a custom callback:
  700. *
  701. * button.bind( 'isEnabled' ).to( command, 'isEnabled', ui, 'isVisible',
  702. * ( isCommandEnabled, isUIVisible ) => isCommandEnabled && isUIVisible );
  703. *
  704. * It is also possible to bind to the same property in an array of observables.
  705. * To bind a `button` to multiple commands (also `Observables`) so that each and every one of them
  706. * must be enabled for the button to become enabled, use the following code:
  707. *
  708. * button.bind( 'isEnabled' ).toMany( [ commandA, commandB, commandC ], 'isEnabled',
  709. * ( isAEnabled, isBEnabled, isCEnabled ) => isAEnabled && isBEnabled && isCEnabled );
  710. *
  711. * @method #bind
  712. * @param {...String} bindProperties Observable properties that will be bound to other observable(s).
  713. * @returns {Object} The bind chain with the `to()` and `toMany()` methods.
  714. */
  715. /**
  716. * Removes the binding created with {@link #bind}.
  717. *
  718. * // Removes the binding for the 'a' property.
  719. * A.unbind( 'a' );
  720. *
  721. * // Removes bindings for all properties.
  722. * A.unbind();
  723. *
  724. * @method #unbind
  725. * @param {...String} [unbindProperties] Observable properties to be unbound. All the bindings will
  726. * be released if no properties are provided.
  727. */
  728. /**
  729. * Turns the given methods of this object into event-based ones. This means that the new method will fire an event
  730. * (named after the method) and the original action will be plugged as a listener to that event.
  731. *
  732. * Read more in the {@glink framework/guides/deep-dive/observables#decorating-object-methods dedicated guide}
  733. * covering the topic of decorating methods with some additional examples.
  734. *
  735. * Decorating the method does not change its behavior (it only adds an event),
  736. * but it allows to modify it later on by listening to the method's event.
  737. *
  738. * For example, to cancel the method execution the event can be {@link module:utils/eventinfo~EventInfo#stop stopped}:
  739. *
  740. * class Foo {
  741. * constructor() {
  742. * this.decorate( 'method' );
  743. * }
  744. *
  745. * method() {
  746. * console.log( 'called!' );
  747. * }
  748. * }
  749. *
  750. * const foo = new Foo();
  751. * foo.on( 'method', ( evt ) => {
  752. * evt.stop();
  753. * }, { priority: 'high' } );
  754. *
  755. * foo.method(); // Nothing is logged.
  756. *
  757. *
  758. * **Note**: The high {@link module:utils/priorities~PriorityString priority} listener
  759. * has been used to execute this particular callback before the one which calls the original method
  760. * (which uses the "normal" priority).
  761. *
  762. * It is also possible to change the returned value:
  763. *
  764. * foo.on( 'method', ( evt ) => {
  765. * evt.return = 'Foo!';
  766. * } );
  767. *
  768. * foo.method(); // -> 'Foo'
  769. *
  770. * Finally, it is possible to access and modify the arguments the method is called with:
  771. *
  772. * method( a, b ) {
  773. * console.log( `${ a }, ${ b }` );
  774. * }
  775. *
  776. * // ...
  777. *
  778. * foo.on( 'method', ( evt, args ) => {
  779. * args[ 0 ] = 3;
  780. *
  781. * console.log( args[ 1 ] ); // -> 2
  782. * }, { priority: 'high' } );
  783. *
  784. * foo.method( 1, 2 ); // -> '3, 2'
  785. *
  786. * @method #decorate
  787. * @param {String} methodName Name of the method to decorate.
  788. */