UNPKG

ckeditor5-image-upload-base64

Version:

The development environment of CKEditor 5 – the best browser-based rich text editor.

879 lines (791 loc) 28.9 kB
/** * @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 ( 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 ); } ); } } ); } } ); } /** * 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. */