| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875 |
- /**
- * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
- /**
- * @module utils/observablemixin
- */
- import EmitterMixin from './emittermixin';
- import CKEditorError from './ckeditorerror';
- import { extend, isObject } from 'lodash-es';
- const observablePropertiesSymbol = Symbol( 'observableProperties' );
- const boundObservablesSymbol = Symbol( 'boundObservables' );
- const boundPropertiesSymbol = Symbol( 'boundProperties' );
- /**
- * A mixin that injects the "observable properties" and data binding functionality described in the
- * {@link ~Observable} interface.
- *
- * Read more about the concept of observables in the:
- * * {@glink framework/guides/architecture/core-editor-architecture#event-system-and-observables "Event system and observables"}
- * section of the {@glink framework/guides/architecture/core-editor-architecture "Core editor architecture"} guide,
- * * {@glink framework/guides/deep-dive/observables "Observables" deep dive} guide.
- *
- * @mixin ObservableMixin
- * @mixes module:utils/emittermixin~EmitterMixin
- * @implements module:utils/observablemixin~Observable
- */
- const ObservableMixin = {
- /**
- * @inheritDoc
- */
- set( name, value ) {
- // If the first parameter is an Object, iterate over its properties.
- if ( isObject( name ) ) {
- Object.keys( name ).forEach( property => {
- this.set( property, name[ property ] );
- }, this );
- return;
- }
- initObservable( this );
- const properties = this[ observablePropertiesSymbol ];
- if ( ( name in this ) && !properties.has( name ) ) {
- /**
- * Cannot override an existing property.
- *
- * This error is thrown when trying to {@link ~Observable#set set} a property with
- * a name of an already existing property. For example:
- *
- * let observable = new Model();
- * observable.property = 1;
- * observable.set( 'property', 2 ); // throws
- *
- * observable.set( 'property', 1 );
- * observable.set( 'property', 2 ); // ok, because this is an existing property.
- *
- * @error observable-set-cannot-override
- */
- throw new CKEditorError( 'observable-set-cannot-override', this );
- }
- Object.defineProperty( this, name, {
- enumerable: true,
- configurable: true,
- get() {
- return properties.get( name );
- },
- set( value ) {
- const oldValue = properties.get( name );
- // Fire `set` event before the new value will be set to make it possible
- // to override observable property without affecting `change` event.
- // See https://github.com/ckeditor/ckeditor5-utils/issues/171.
- let newValue = this.fire( 'set:' + name, name, value, oldValue );
- if ( newValue === undefined ) {
- newValue = value;
- }
- // Allow undefined as an initial value like A.define( 'x', undefined ) (#132).
- // Note: When properties map has no such own property, then its value is undefined.
- if ( oldValue !== newValue || !properties.has( name ) ) {
- properties.set( name, newValue );
- this.fire( 'change:' + name, name, newValue, oldValue );
- }
- }
- } );
- this[ name ] = value;
- },
- /**
- * @inheritDoc
- */
- bind( ...bindProperties ) {
- if ( !bindProperties.length || !isStringArray( bindProperties ) ) {
- /**
- * All properties must be strings.
- *
- * @error observable-bind-wrong-properties
- */
- throw new CKEditorError( 'observable-bind-wrong-properties', this );
- }
- if ( ( new Set( bindProperties ) ).size !== bindProperties.length ) {
- /**
- * Properties must be unique.
- *
- * @error observable-bind-duplicate-properties
- */
- throw new CKEditorError( 'observable-bind-duplicate-properties', this );
- }
- initObservable( this );
- const boundProperties = this[ boundPropertiesSymbol ];
- bindProperties.forEach( propertyName => {
- if ( boundProperties.has( propertyName ) ) {
- /**
- * Cannot bind the same property more than once.
- *
- * @error observable-bind-rebind
- */
- throw new CKEditorError( 'observable-bind-rebind', this );
- }
- } );
- const bindings = new Map();
- // @typedef {Object} Binding
- // @property {Array} property Property which is bound.
- // @property {Array} to Array of observable–property components of the binding (`{ observable: ..., property: .. }`).
- // @property {Array} callback A function which processes `to` components.
- bindProperties.forEach( a => {
- const binding = { property: a, to: [] };
- boundProperties.set( a, binding );
- bindings.set( a, binding );
- } );
- // @typedef {Object} BindChain
- // @property {Function} to See {@link ~ObservableMixin#_bindTo}.
- // @property {Function} toMany See {@link ~ObservableMixin#_bindToMany}.
- // @property {module:utils/observablemixin~Observable} _observable The observable which initializes the binding.
- // @property {Array} _bindProperties Array of `_observable` properties to be bound.
- // @property {Array} _to Array of `to()` observable–properties (`{ observable: toObservable, properties: ...toProperties }`).
- // @property {Map} _bindings Stores bindings to be kept in
- // {@link ~ObservableMixin#_boundProperties}/{@link ~ObservableMixin#_boundObservables}
- // initiated in this binding chain.
- return {
- to: bindTo,
- toMany: bindToMany,
- _observable: this,
- _bindProperties: bindProperties,
- _to: [],
- _bindings: bindings
- };
- },
- /**
- * @inheritDoc
- */
- unbind( ...unbindProperties ) {
- // Nothing to do here if not inited yet.
- if ( !( this[ observablePropertiesSymbol ] ) ) {
- return;
- }
- const boundProperties = this[ boundPropertiesSymbol ];
- const boundObservables = this[ boundObservablesSymbol ];
- if ( unbindProperties.length ) {
- if ( !isStringArray( unbindProperties ) ) {
- /**
- * Properties must be strings.
- *
- * @error observable-unbind-wrong-properties
- */
- throw new CKEditorError( 'observable-unbind-wrong-properties', this );
- }
- unbindProperties.forEach( propertyName => {
- const binding = boundProperties.get( propertyName );
- // Nothing to do if the binding is not defined
- if ( !binding ) {
- return;
- }
- let toObservable, toProperty, toProperties, toPropertyBindings;
- binding.to.forEach( to => {
- // TODO: ES6 destructuring.
- toObservable = to[ 0 ];
- toProperty = to[ 1 ];
- toProperties = boundObservables.get( toObservable );
- toPropertyBindings = toProperties[ toProperty ];
- toPropertyBindings.delete( binding );
- if ( !toPropertyBindings.size ) {
- delete toProperties[ toProperty ];
- }
- if ( !Object.keys( toProperties ).length ) {
- boundObservables.delete( toObservable );
- this.stopListening( toObservable, 'change' );
- }
- } );
- boundProperties.delete( propertyName );
- } );
- } else {
- boundObservables.forEach( ( bindings, boundObservable ) => {
- this.stopListening( boundObservable, 'change' );
- } );
- boundObservables.clear();
- boundProperties.clear();
- }
- },
- /**
- * @inheritDoc
- */
- decorate( methodName ) {
- const originalMethod = this[ methodName ];
- if ( !originalMethod ) {
- /**
- * Cannot decorate an undefined method.
- *
- * @error observablemixin-cannot-decorate-undefined
- * @param {Object} object The object which method should be decorated.
- * @param {String} methodName Name of the method which does not exist.
- */
- throw new CKEditorError(
- 'observablemixin-cannot-decorate-undefined',
- this,
- { object: this, methodName }
- );
- }
- this.on( methodName, ( evt, args ) => {
- evt.return = originalMethod.apply( this, args );
- } );
- this[ methodName ] = function( ...args ) {
- return this.fire( methodName, args );
- };
- }
- };
- extend( ObservableMixin, EmitterMixin );
- export default ObservableMixin;
- // Init symbol properties needed for the observable mechanism to work.
- //
- // @private
- // @param {module:utils/observablemixin~ObservableMixin} observable
- function initObservable( observable ) {
- // Do nothing if already inited.
- if ( observable[ observablePropertiesSymbol ] ) {
- return;
- }
- // The internal hash containing the observable's state.
- //
- // @private
- // @type {Map}
- Object.defineProperty( observable, observablePropertiesSymbol, {
- value: new Map()
- } );
- // Map containing bindings to external observables. It shares the binding objects
- // (`{ observable: A, property: 'a', to: ... }`) with {@link module:utils/observablemixin~ObservableMixin#_boundProperties} and
- // it is used to observe external observables to update own properties accordingly.
- // See {@link module:utils/observablemixin~ObservableMixin#bind}.
- //
- // A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
- // console.log( A._boundObservables );
- //
- // Map( {
- // B: {
- // x: Set( [
- // { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
- // { observable: A, property: 'c', to: [ [ B, 'x' ] ] }
- // ] ),
- // y: Set( [
- // { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
- // ] )
- // }
- // } )
- //
- // A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
- // console.log( A._boundObservables );
- //
- // Map( {
- // B: {
- // x: Set( [
- // { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
- // { observable: A, property: 'c', to: [ [ B, 'x' ] ] }
- // ] ),
- // y: Set( [
- // { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
- // ] ),
- // z: Set( [
- // { observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
- // ] )
- // },
- // C: {
- // w: Set( [
- // { observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
- // ] )
- // }
- // } )
- //
- // @private
- // @type {Map}
- Object.defineProperty( observable, boundObservablesSymbol, {
- value: new Map()
- } );
- // Object that stores which properties of this observable are bound and how. It shares
- // the binding objects (`{ observable: A, property: 'a', to: ... }`) with
- // {@link module:utils/observablemixin~ObservableMixin#_boundObservables}. This data structure is
- // a reverse of {@link module:utils/observablemixin~ObservableMixin#_boundObservables} and it is helpful for
- // {@link module:utils/observablemixin~ObservableMixin#unbind}.
- //
- // See {@link module:utils/observablemixin~ObservableMixin#bind}.
- //
- // A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
- // console.log( A._boundProperties );
- //
- // Map( {
- // a: { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
- // b: { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
- // c: { observable: A, property: 'c', to: [ [ B, 'x' ] ] }
- // } )
- //
- // A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
- // console.log( A._boundProperties );
- //
- // Map( {
- // a: { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
- // b: { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
- // c: { observable: A, property: 'c', to: [ [ B, 'x' ] ] },
- // d: { observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
- // } )
- //
- // @private
- // @type {Map}
- Object.defineProperty( observable, boundPropertiesSymbol, {
- value: new Map()
- } );
- }
- // A chaining for {@link module:utils/observablemixin~ObservableMixin#bind} providing `.to()` interface.
- //
- // @private
- // @param {...[Observable|String|Function]} args Arguments of the `.to( args )` binding.
- function bindTo( ...args ) {
- const parsedArgs = parseBindToArgs( ...args );
- const bindingsKeys = Array.from( this._bindings.keys() );
- const numberOfBindings = bindingsKeys.length;
- // Eliminate A.bind( 'x' ).to( B, C )
- if ( !parsedArgs.callback && parsedArgs.to.length > 1 ) {
- /**
- * Binding multiple observables only possible with callback.
- *
- * @error observable-bind-to-no-callback
- */
- throw new CKEditorError( 'observable-bind-to-no-callback', this );
- }
- // Eliminate A.bind( 'x', 'y' ).to( B, callback )
- if ( numberOfBindings > 1 && parsedArgs.callback ) {
- /**
- * Cannot bind multiple properties and use a callback in one binding.
- *
- * @error observable-bind-to-extra-callback
- */
- throw new CKEditorError(
- 'observable-bind-to-extra-callback',
- this
- );
- }
- parsedArgs.to.forEach( to => {
- // Eliminate A.bind( 'x', 'y' ).to( B, 'a' )
- if ( to.properties.length && to.properties.length !== numberOfBindings ) {
- /**
- * The number of properties must match.
- *
- * @error observable-bind-to-properties-length
- */
- throw new CKEditorError( 'observable-bind-to-properties-length', this );
- }
- // When no to.properties specified, observing source properties instead i.e.
- // A.bind( 'x', 'y' ).to( B ) -> Observe B.x and B.y
- if ( !to.properties.length ) {
- to.properties = this._bindProperties;
- }
- } );
- this._to = parsedArgs.to;
- // Fill {@link BindChain#_bindings} with callback. When the callback is set there's only one binding.
- if ( parsedArgs.callback ) {
- this._bindings.get( bindingsKeys[ 0 ] ).callback = parsedArgs.callback;
- }
- attachBindToListeners( this._observable, this._to );
- // Update observable._boundProperties and observable._boundObservables.
- updateBindToBound( this );
- // Set initial values of bound properties.
- this._bindProperties.forEach( propertyName => {
- updateBoundObservableProperty( this._observable, propertyName );
- } );
- }
- // Binds to an attribute in a set of iterable observables.
- //
- // @private
- // @param {Array.<Observable>} observables
- // @param {String} attribute
- // @param {Function} callback
- function bindToMany( observables, attribute, callback ) {
- if ( this._bindings.size > 1 ) {
- /**
- * Binding one attribute to many observables only possible with one attribute.
- *
- * @error observable-bind-to-many-not-one-binding
- */
- throw new CKEditorError( 'observable-bind-to-many-not-one-binding', this );
- }
- this.to(
- // Bind to #attribute of each observable...
- ...getBindingTargets( observables, attribute ),
- // ...using given callback to parse attribute values.
- callback
- );
- }
- // Returns an array of binding components for
- // {@link Observable#bind} from a set of iterable observables.
- //
- // @param {Array.<Observable>} observables
- // @param {String} attribute
- // @returns {Array.<String|Observable>}
- function getBindingTargets( observables, attribute ) {
- const observableAndAttributePairs = observables.map( observable => [ observable, attribute ] );
- // Merge pairs to one-dimension array of observables and attributes.
- return Array.prototype.concat.apply( [], observableAndAttributePairs );
- }
- // Check if all entries of the array are of `String` type.
- //
- // @private
- // @param {Array} arr An array to be checked.
- // @returns {Boolean}
- function isStringArray( arr ) {
- return arr.every( a => typeof a == 'string' );
- }
- // Parses and validates {@link Observable#bind}`.to( args )` arguments and returns
- // an object with a parsed structure. For example
- //
- // A.bind( 'x' ).to( B, 'a', C, 'b', call );
- //
- // becomes
- //
- // {
- // to: [
- // { observable: B, properties: [ 'a' ] },
- // { observable: C, properties: [ 'b' ] },
- // ],
- // callback: call
- // }
- //
- // @private
- // @param {...*} args Arguments of {@link Observable#bind}`.to( args )`.
- // @returns {Object}
- function parseBindToArgs( ...args ) {
- // Eliminate A.bind( 'x' ).to()
- if ( !args.length ) {
- /**
- * Invalid argument syntax in `to()`.
- *
- * @error observable-bind-to-parse-error
- */
- throw new CKEditorError( 'observable-bind-to-parse-error', null );
- }
- const parsed = { to: [] };
- let lastObservable;
- if ( typeof args[ args.length - 1 ] == 'function' ) {
- parsed.callback = args.pop();
- }
- args.forEach( a => {
- if ( typeof a == 'string' ) {
- lastObservable.properties.push( a );
- } else if ( typeof a == 'object' ) {
- lastObservable = { observable: a, properties: [] };
- parsed.to.push( lastObservable );
- } else {
- throw new CKEditorError( 'observable-bind-to-parse-error', null );
- }
- } );
- return parsed;
- }
- // Synchronizes {@link module:utils/observablemixin#_boundObservables} with {@link Binding}.
- //
- // @private
- // @param {Binding} binding A binding to store in {@link Observable#_boundObservables}.
- // @param {Observable} toObservable A observable, which is a new component of `binding`.
- // @param {String} toPropertyName A name of `toObservable`'s property, a new component of the `binding`.
- function updateBoundObservables( observable, binding, toObservable, toPropertyName ) {
- const boundObservables = observable[ boundObservablesSymbol ];
- const bindingsToObservable = boundObservables.get( toObservable );
- const bindings = bindingsToObservable || {};
- if ( !bindings[ toPropertyName ] ) {
- bindings[ toPropertyName ] = new Set();
- }
- // Pass the binding to a corresponding Set in `observable._boundObservables`.
- bindings[ toPropertyName ].add( binding );
- if ( !bindingsToObservable ) {
- boundObservables.set( toObservable, bindings );
- }
- }
- // Synchronizes {@link Observable#_boundProperties} and {@link Observable#_boundObservables}
- // with {@link BindChain}.
- //
- // Assuming the following binding being created
- //
- // A.bind( 'a', 'b' ).to( B, 'x', 'y' );
- //
- // the following bindings were initialized by {@link Observable#bind} in {@link BindChain#_bindings}:
- //
- // {
- // a: { observable: A, property: 'a', to: [] },
- // b: { observable: A, property: 'b', to: [] },
- // }
- //
- // Iterate over all bindings in this chain and fill their `to` properties with
- // corresponding to( ... ) arguments (components of the binding), so
- //
- // {
- // a: { observable: A, property: 'a', to: [ B, 'x' ] },
- // b: { observable: A, property: 'b', to: [ B, 'y' ] },
- // }
- //
- // Then update the structure of {@link Observable#_boundObservables} with updated
- // binding, so it becomes:
- //
- // Map( {
- // B: {
- // x: Set( [
- // { observable: A, property: 'a', to: [ [ B, 'x' ] ] }
- // ] ),
- // y: Set( [
- // { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
- // ] )
- // }
- // } )
- //
- // @private
- // @param {BindChain} chain The binding initialized by {@link Observable#bind}.
- function updateBindToBound( chain ) {
- let toProperty;
- chain._bindings.forEach( ( binding, propertyName ) => {
- // Note: For a binding without a callback, this will run only once
- // like in A.bind( 'x', 'y' ).to( B, 'a', 'b' )
- // TODO: ES6 destructuring.
- chain._to.forEach( to => {
- toProperty = to.properties[ binding.callback ? 0 : chain._bindProperties.indexOf( propertyName ) ];
- binding.to.push( [ to.observable, toProperty ] );
- updateBoundObservables( chain._observable, binding, to.observable, toProperty );
- } );
- } );
- }
- // Updates an property of a {@link Observable} with a value
- // determined by an entry in {@link Observable#_boundProperties}.
- //
- // @private
- // @param {Observable} observable A observable which property is to be updated.
- // @param {String} propertyName An property to be updated.
- function updateBoundObservableProperty( observable, propertyName ) {
- const boundProperties = observable[ boundPropertiesSymbol ];
- const binding = boundProperties.get( propertyName );
- let propertyValue;
- // When a binding with callback is created like
- //
- // A.bind( 'a' ).to( B, 'b', C, 'c', callback );
- //
- // collect B.b and C.c, then pass them to callback to set A.a.
- if ( binding.callback ) {
- propertyValue = binding.callback.apply( observable, binding.to.map( to => to[ 0 ][ to[ 1 ] ] ) );
- } else {
- propertyValue = binding.to[ 0 ];
- propertyValue = propertyValue[ 0 ][ propertyValue[ 1 ] ];
- }
- if ( Object.prototype.hasOwnProperty.call( observable, propertyName ) ) {
- observable[ propertyName ] = propertyValue;
- } else {
- observable.set( propertyName, propertyValue );
- }
- }
- // Starts listening to changes in {@link BindChain._to} observables to update
- // {@link BindChain._observable} {@link BindChain._bindProperties}. Also sets the
- // initial state of {@link BindChain._observable}.
- //
- // @private
- // @param {BindChain} chain The chain initialized by {@link Observable#bind}.
- function attachBindToListeners( observable, toBindings ) {
- toBindings.forEach( to => {
- const boundObservables = observable[ boundObservablesSymbol ];
- let bindings;
- // If there's already a chain between the observables (`observable` listens to
- // `to.observable`), there's no need to create another `change` event listener.
- if ( !boundObservables.get( to.observable ) ) {
- observable.listenTo( to.observable, 'change', ( evt, propertyName ) => {
- bindings = boundObservables.get( to.observable )[ propertyName ];
- // Note: to.observable will fire for any property change, react
- // to changes of properties which are bound only.
- if ( bindings ) {
- bindings.forEach( binding => {
- updateBoundObservableProperty( observable, binding.property );
- } );
- }
- } );
- }
- } );
- }
- /**
- * An interface which adds "observable properties" and data binding functionality.
- *
- * Can be easily implemented by a class by mixing the {@link module:utils/observablemixin~ObservableMixin} mixin.
- *
- * Read more about the usage of this interface in the:
- * * {@glink framework/guides/architecture/core-editor-architecture#event-system-and-observables "Event system and observables"}
- * section of the {@glink framework/guides/architecture/core-editor-architecture "Core editor architecture"} guide,
- * * {@glink framework/guides/deep-dive/observables "Observables" deep dive} guide.
- *
- * @interface Observable
- * @extends module:utils/emittermixin~Emitter
- */
- /**
- * Fired when a property changed value.
- *
- * observable.set( 'prop', 1 );
- *
- * observable.on( 'change:prop', ( evt, propertyName, newValue, oldValue ) => {
- * console.log( `${ propertyName } has changed from ${ oldValue } to ${ newValue }` );
- * } );
- *
- * observable.prop = 2; // -> 'prop has changed from 1 to 2'
- *
- * @event change:{property}
- * @param {String} name The property name.
- * @param {*} value The new property value.
- * @param {*} oldValue The previous property value.
- */
- /**
- * Fired when a property value is going to be set but is not set yet (before the `change` event is fired).
- *
- * You can control the final value of the property by using
- * the {@link module:utils/eventinfo~EventInfo#return event's `return` property}.
- *
- * observable.set( 'prop', 1 );
- *
- * observable.on( 'set:prop', ( evt, propertyName, newValue, oldValue ) => {
- * console.log( `Value is going to be changed from ${ oldValue } to ${ newValue }` );
- * console.log( `Current property value is ${ observable[ propertyName ] }` );
- *
- * // Let's override the value.
- * evt.return = 3;
- * } );
- *
- * observable.on( 'change:prop', ( evt, propertyName, newValue, oldValue ) => {
- * console.log( `Value has changed from ${ oldValue } to ${ newValue }` );
- * } );
- *
- * observable.prop = 2; // -> 'Value is going to be changed from 1 to 2'
- * // -> 'Current property value is 1'
- * // -> 'Value has changed from 1 to 3'
- *
- * **Note:** The event is fired even when the new value is the same as the old value.
- *
- * @event set:{property}
- * @param {String} name The property name.
- * @param {*} value The new property value.
- * @param {*} oldValue The previous property value.
- */
- /**
- * Creates and sets the value of an observable property of this object. Such a property becomes a part
- * of the state and is observable.
- *
- * It accepts also a single object literal containing key/value pairs with properties to be set.
- *
- * This method throws the `observable-set-cannot-override` error if the observable instance already
- * has a property with the given property name. This prevents from mistakenly overriding existing
- * properties and methods, but means that `foo.set( 'bar', 1 )` may be slightly slower than `foo.bar = 1`.
- *
- * @method #set
- * @param {String|Object} name The property's name or object with `name=>value` pairs.
- * @param {*} [value] The property's value (if `name` was passed in the first parameter).
- */
- /**
- * Binds {@link #set observable properties} to other objects implementing the
- * {@link module:utils/observablemixin~Observable} interface.
- *
- * Read more in the {@glink framework/guides/deep-dive/observables#property-bindings dedicated guide}
- * covering the topic of property bindings with some additional examples.
- *
- * Consider two objects: a `button` and an associated `command` (both `Observable`).
- *
- * A simple property binding could be as follows:
- *
- * button.bind( 'isEnabled' ).to( command, 'isEnabled' );
- *
- * or even shorter:
- *
- * button.bind( 'isEnabled' ).to( command );
- *
- * which works in the following way:
- *
- * * `button.isEnabled` **instantly equals** `command.isEnabled`,
- * * whenever `command.isEnabled` changes, `button.isEnabled` will immediately reflect its value.
- *
- * **Note**: To release the binding, use {@link module:utils/observablemixin~Observable#unbind}.
- *
- * You can also "rename" the property in the binding by specifying the new name in the `to()` chain:
- *
- * button.bind( 'isEnabled' ).to( command, 'isWorking' );
- *
- * It is possible to bind more than one property at a time to shorten the code:
- *
- * button.bind( 'isEnabled', 'value' ).to( command );
- *
- * which corresponds to:
- *
- * button.bind( 'isEnabled' ).to( command );
- * button.bind( 'value' ).to( command );
- *
- * The binding can include more than one observable, combining multiple data sources in a custom callback:
- *
- * button.bind( 'isEnabled' ).to( command, 'isEnabled', ui, 'isVisible',
- * ( isCommandEnabled, isUIVisible ) => isCommandEnabled && isUIVisible );
- *
- * It is also possible to bind to the same property in an array of observables.
- * To bind a `button` to multiple commands (also `Observables`) so that each and every one of them
- * must be enabled for the button to become enabled, use the following code:
- *
- * button.bind( 'isEnabled' ).toMany( [ commandA, commandB, commandC ], 'isEnabled',
- * ( isAEnabled, isBEnabled, isCEnabled ) => isAEnabled && isBEnabled && isCEnabled );
- *
- * @method #bind
- * @param {...String} bindProperties Observable properties that will be bound to other observable(s).
- * @returns {Object} The bind chain with the `to()` and `toMany()` methods.
- */
- /**
- * Removes the binding created with {@link #bind}.
- *
- * // Removes the binding for the 'a' property.
- * A.unbind( 'a' );
- *
- * // Removes bindings for all properties.
- * A.unbind();
- *
- * @method #unbind
- * @param {...String} [unbindProperties] Observable properties to be unbound. All the bindings will
- * be released if no properties are provided.
- */
- /**
- * Turns the given methods of this object into event-based ones. This means that the new method will fire an event
- * (named after the method) and the original action will be plugged as a listener to that event.
- *
- * Read more in the {@glink framework/guides/deep-dive/observables#decorating-object-methods dedicated guide}
- * covering the topic of decorating methods with some additional examples.
- *
- * Decorating the method does not change its behavior (it only adds an event),
- * but it allows to modify it later on by listening to the method's event.
- *
- * For example, to cancel the method execution the event can be {@link module:utils/eventinfo~EventInfo#stop stopped}:
- *
- * class Foo {
- * constructor() {
- * this.decorate( 'method' );
- * }
- *
- * method() {
- * console.log( 'called!' );
- * }
- * }
- *
- * const foo = new Foo();
- * foo.on( 'method', ( evt ) => {
- * evt.stop();
- * }, { priority: 'high' } );
- *
- * foo.method(); // Nothing is logged.
- *
- *
- * **Note**: The high {@link module:utils/priorities~PriorityString priority} listener
- * has been used to execute this particular callback before the one which calls the original method
- * (which uses the "normal" priority).
- *
- * It is also possible to change the returned value:
- *
- * foo.on( 'method', ( evt ) => {
- * evt.return = 'Foo!';
- * } );
- *
- * foo.method(); // -> 'Foo'
- *
- * Finally, it is possible to access and modify the arguments the method is called with:
- *
- * method( a, b ) {
- * console.log( `${ a }, ${ b }` );
- * }
- *
- * // ...
- *
- * foo.on( 'method', ( evt, args ) => {
- * args[ 0 ] = 3;
- *
- * console.log( args[ 1 ] ); // -> 2
- * }, { priority: 'high' } );
- *
- * foo.method( 1, 2 ); // -> '3, 2'
- *
- * @method #decorate
- * @param {String} methodName Name of the method to decorate.
- */
|