ckeditor/ckeditor5-utils

View on GitHub
src/observablemixin.js

Summary

Maintainability
C
1 day
Test Coverage
/**
 * @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.
 */