src/observablemixin.js
/**
* @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' );
/**
* 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} an 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: Cannot override an existing property.', 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: All properties must be strings.', this );
}
if ( ( new Set( bindProperties ) ).size !== bindProperties.length ) {
/**
* Properties must be unique.
*
* @error observable-bind-duplicate-properties
*/
throw new CKEditorError( 'observable-bind-duplicate-properties: Properties must be unique.', 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: Cannot bind the same property more than once.', 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: Properties must be strings.', 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: Cannot decorate an undefined method.',
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 to 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-no-callback
*/
throw new CKEditorError(
'observable-bind-to-no-callback: Binding multiple observables only possible with 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: Cannot bind multiple properties and use a callback in one binding.',
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: The number of properties must match.', 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: Cannot bind multiple properties with toMany().', 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: Invalid argument syntax in `to()`.', 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: Invalid argument syntax in `to()`.', 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 ( observable.hasOwnProperty( 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 );
} );
}
} );
}
} );
}
/**
* 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:** 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 an property becomes a part
* of the state and is be 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
* have 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.
*/