Home Reference Source

src/core/View.js

/**


 @author: Hans van den Akker (mysim1)
 @license NPOSL-3.0
 @copyright Bizboard, 2015

 */

import extend                       from 'lodash/extend.js';
import cloneDeep                    from 'lodash/cloneDeep.js';
import FamousView                   from 'famous/core/View.js';
import Surface                      from 'famous/core/Surface.js';
import LayoutController             from 'famous-flex/LayoutController.js';
import Engine                       from 'famous/core/Engine.js';

import {limit}                      from 'arva-js/utils/Limiter.js';

import {layout}                     from '../layout/Decorators.js';
import {ObjectHelper}               from '../utils/ObjectHelper.js';
import {SizeResolver}               from '../utils/view/SizeResolver.js';
import {Utils}                      from '../utils/view/Utils.js';
import {
    DockedLayoutHelper,
    FullSizeLayoutHelper,
    TraditionalLayoutHelper
}
                                    from '../utils/view/LayoutHelpers.js';
import {RenderableHelper}           from '../utils/view/RenderableHelper.js';
import {ReflowingScrollView}        from '../components/ReflowingScrollView.js';

/**
 * An Arva View. Can be constructed explicitly by using new View() but is more commonly used as a base class for
 * views used by the app.
 *
 */
export class View extends FamousView {

    /**
     * @example
     * HomeController extends Controller {
     *      Index() {
     *          let view = new View();
     *          view.add(new Surface({properties: {backgroundColor: 'red'}}));
     *          return view
     *      }
     * }
     * @example
     * class HomeView extends View {
     *      @layout.size(100, 100)
     *      @layout.place.center()
     *      mySurface = new Surface({properties: {backgroundColor: 'red'}})
     * }
     *
     *
     *
     * @param {Object} options. The options passed to the view will be stored in this.options, but won't change any
     * behaviour of the core functionality of the view. Instead, configuration of the View is done by decorators.
     *
     */
    constructor(options = {}) {

        super(options);

        /* Bind all local methods to the current object instance, so we can refer to 'this'
         * in the methods as expected, even when they're called from event handlers.        */
        ObjectHelper.bindAllMethods(this, this);


        this._copyPrototypeProperties();
        this._initDataStructures();
        this._initOwnDecorations();
        this._initOptions(options);
        this._initUtils();
        this._constructDecoratedRenderables();

        this._createLayoutController();
        this._initTrueSizedBookkeeping();

    }

    //noinspection JSUnusedGlobalSymbols
    /**
     * Deprecated, it is no longer required to call build() from within your View instances.
     * @deprecated
     * @returns {void}
     */
    build() {
        Utils.warn(`Arva: calling build() from within views is no longer necessary, any existing calls can safely be removed. Called from ${this._name()}`);
    }

    /**
     * Reflows the layout while also informing any subscribing parents that a reflow has to take place
     */
    reflowRecursively() {
        this.layout.reflowLayout();
        this._eventOutput.emit('recursiveReflow');
    }

    /**
     * Gets the size used when displaying a renderable on the screen the last tick
     * @param {Renderable/Name} renderableOrName The renderable or the name of the renderable of which you need the size
     */
    getResolvedSize(renderableOrName) {
        let renderable = renderableOrName;
        if (typeof renderableOrName === 'string') {
            renderable = this.renderables[renderableOrName];
        }
        let size = this._sizeResolver.getResolvedSize(renderable);

        /* Backup: If size can't be resolved, then see if there's a size specified on the decorator */
        if (!size && renderable.decorations) {
            let decoratedSize = renderable.decorations.size;
            let isValidSize = (inputSize) => typeof inputSize == 'number' && inputSize > 0;
            if (decoratedSize && decoratedSize.every(isValidSize)) {
                size = decoratedSize;
            }
        }

        return size || [undefined, undefined];
    }

    /**
     * Returns true if the view contains uncalculated surfaces
     * @returns {Boolean}
     */
    containsUncalculatedSurfaces() {
        return this._sizeResolver.containsUncalculatedSurfaces();
    }

    /**
     * Adds a renderable to the layout.
     * @param {Renderable} renderable The renderable to be added
     * @param {String} renderableName The name (key) of the renderable
     * @param {Decorator} Decorator Any decorator(s) to apply to the renderable
     * @returns {Renderable} The renderable that was assigned
     */
    addRenderable(renderable, renderableName, ...decorators) {
        /* Due to common mistake, we check if renderableName is a string */
        if (typeof renderableName !== 'string') {
            Utils.warn(`The second argument of addRenderable(...) was not a string. Please pass the renderable name in ${this._name()}`);
        }
        this._renderableHelper.applyDecoratorFunctionsToRenderable(renderable, decorators);
        this._assignRenderable(renderable, renderableName);
        this.layout.reflowLayout();
        return renderable;
    }

    /**
     * Removes the renderable from the view
     * @param {String} renderableName The name of the renderable
     */
    removeRenderable(renderableName) {
        this._renderableHelper.removeRenderable(renderableName);
        this[renderableName] = undefined;
        this.layout.reflowLayout();
    }

    /**
     * Rearranges the order in which docked renderables are parsed for rendering, ensuring that 'renderableName' is processed
     * before 'nextRenderableName'.
     * @param {String} renderableName
     * @param {String} nextRenderableName
     */
    prioritiseDockBefore(renderableName, nextRenderableName) {
        this.reflowRecursively();
        return this._renderableHelper.prioritiseDockBefore(renderableName, nextRenderableName);
    }

    /**
     * @param {String} renderableName
     * @param {String} prevRenderableName
     */
    prioritiseDockAfter(renderableName, prevRenderableName) {
        this.reflowRecursively();
        return this._renderableHelper.prioritiseDockAfter(renderableName, prevRenderableName);
    }

    /**
     *
     * @param {String} renderableName
     * @param {Boolean} show. Whether to show or not
     * @returns {Promise} when the renderable has finished its animation
     */
    showRenderable(renderableName, show = true) {
        let renderable = this[renderableName];
        if (!renderable.animationController) {
            Utils.warn(`Trying to show renderable ${renderableName} which does not have an animationcontroller. Please use @layout.animate`);
            return;
        }
        let decoratedSize = this[renderableName].decorations.size || (this[renderableName].decorations.dock ? this[renderableName].decorations.dock.size : undefined);
        if (decoratedSize) {
            /* Check if animationController has a true size specified. If so a reflow needs to be performed since there is a
             * new size to take into account. */
            for (let dimension of [0, 1]) {
                if (this._sizeResolver.isValueTrueSized(this._sizeResolver.resolveSingleSize(decoratedSize[dimension], [NaN, NaN], dimension))) {
                    this.reflowRecursively();
                    break;
                }

            }
        }
        
        return new Promise((resolve) => this._renderableHelper.showWithAnimationController(this.renderables[renderableName], renderable, show, resolve));
    }

    /**
     * Decorates a renderable with other decorators. Using the same decorators as used previously will override the old ones.
     * @example
     * this.decorateRenderable('myRenderable',layout.size(100, 100));
     *
     * @param {String} renderableName The name of the renderable
     * @param ...decorators The decorators that should be applied
     */
    decorateRenderable(renderableName, ...decorators) {
        this._renderableHelper.decorateRenderable(renderableName, ...decorators);
        this.reflowRecursively();
    }

    /**
     * Sets a renderable flow state as declared in the @flow.stateStep, or @flow.defaultState
     * @param {String} renderableName. The name of the renderable
     * @param {String} stateName. The name of the state as declared in the first argument of the decorator
     * @returns {*}
     */
    setRenderableFlowState(renderableName = '', stateName = '') {
        return this._renderableHelper.setRenderableFlowState(renderableName, stateName);
    }

    /**
     * Sets a renderable flow state as declared in the @flow.viewState
     * @param {String} renderableName. The name of the renderable
     * @param {String} stateName. The name of the state as declared in the first argument of the decorator
     * @returns {*}
     */
    setViewFlowState(stateName = '') {
        return this._renderableHelper.setViewFlowState(stateName, this.decorations.flow);
    }

    /**
     * Gets the name of a flow state of a renderable.
     *
     * @param {String} renderableName the name of the renderable of which the flow state is concerned
     * @returns {String} stateName the name of the state that the renderable is in
     */
    getRenderableFlowState(renderableName = '') {
        return this._renderableHelper.getRenderableFlowState(renderableName);
    }

    /**
     * Gets the name of the flow state of a view.
     *
     * @returns {String} stateName the name of the state that this view is in.
     */
    getViewFlowState() {
        return this._renderableHelper.getViewFlowState(this.decorations.flow);
    }

    /**
     * Replaces an existing decorated renderable with a new renderable, preserving all necessary state and decorations
     * @param {String} renderableName. The name of the renderable
     * @param newRenderable
     */
    replaceRenderable(renderableName, newRenderable) {
        this._renderableHelper.replaceRenderable(renderableName, newRenderable);
        this.reflowRecursively();
        this[renderableName] = newRenderable;
    }

    /**
     * Gets the scroll view that was set if @layout.scrollable was used on the view
     * @returns {ReflowingScrollView}
     */
    getScrollView() {
        return this._scrollView;
    }

    /**
     * getSize() is called by this view and by layoutControllers. For lazy people that don't want to specifiy their own getSize() function,
     * we provide a fallback. This function can be performance expensive when using non-docked renderables, but for docked renderables it
     * is efficient and convenient]
     * @returns {*[]}
     */
    getSize() {
        return this._getLayoutSize();
    }

    /**
     * Hides a renderable that has been declared with @layout.animate
     * @param renderableName
     * @returns {Promise} when the renderable has finished its animation
     */
    hideRenderable(renderableName) {
        return this.showRenderable(renderableName, false);
    }

    /**
     * Passes a callback that gets called every time the context size changes.
     *
     * @param {Function} callback a callback with arguments (width, height)
     */
    onNewSize(callback) {
        this._onNewSizeCallbacks.push(callback);
    }

    /**
     * Gets a (new) context size of the view. This will always happen at least once immediately after the view is constructed.
     * Hence, it can safely be used in the constructor to get the (initial) size of the view.
     *
     * @example
     * constructor(options){
     *  super(options);
     *  onceNewSize.then((width, height) => {
     *      console.log(width, height);
     *  });
     * }
     *
     * @returns {Promise} Resolves when there's a new size
     */
    onceNewSize() {
        return new Promise((resolve) => {
            this._onNewSizeCallbacks.push(function onNewSize(size)  {
                this._onNewSizeCallbacks.splice(this._onNewSizeCallbacks.indexOf(onNewSize), 1);
                resolve(size);
            }.bind(this))
        })
    }

    /**
     * Repeat a certain flowState indefinitely
     * @param renderableName
     * @param stateName
     * @param {Boolean} persistent. If true, then it will keep on repeating until explicitly cancelled by cancelRepeatFlowState.
     * If false, it will be interrupted automatically by any interrput to another state. Defaults to true
     * @returns {Promise} resolves to false if the flow state can't be repeated due to an existing running repeat
     */
    async repeatFlowState(renderableName = '', stateName = '', persistent = true){
        if(!this._runningRepeatingFlowStates[renderableName]){
            this._runningRepeatingFlowStates[renderableName] = {persistent};
            while(this._runningRepeatingFlowStates[renderableName] && (await this.setRenderableFlowState(renderableName, stateName) || persistent))
            {}
            delete this._runningRepeatingFlowStates[renderableName];
            return true;
        } else {
            return false;
        }
    }

    /**
     * Cancel a repeating renderable. This will cancel the animation for next flow-cycle, it won't interject the current animation cycle.
     * @param renderableName
     */
    cancelRepeatFlowState(renderableName){
        if(this._runningRepeatingFlowStates){
            delete this._runningRepeatingFlowStates[renderableName];
        }
    }

    /**
     * Initiate a renderable to a default flow state.
     * @param renderableName
     * @param stateName
     */
    setDefaultState(renderableName, stateName) {
        for (let step of this[renderableName].decorations.flow.states[stateName].steps) {
            this.decorateRenderable(renderableName, ...step.transformations);
        }
    }

    /**
     * Inits the utils that are used as helper classes for the view
     * @private
     */
    _initUtils() {
        this._sizeResolver = new SizeResolver();
        this._sizeResolver.on('layoutControllerReflow', this._requestLayoutControllerReflow);
        this._sizeResolver.on('reflow', () => this.layout.reflowLayout());
        this._sizeResolver.on('reflowRecursively', this.reflowRecursively);
        this._dockedRenderablesHelper = new DockedLayoutHelper(this._sizeResolver);
        this._fullSizeLayoutHelper = new FullSizeLayoutHelper(this._sizeResolver);
        this._traditionalLayoutHelper = new TraditionalLayoutHelper(this._sizeResolver);
        this._renderableHelper = new RenderableHelper(this._bindToSelf,this._setPipeToSelf, this.renderables, this._sizeResolver);
    }

    /** Requests for a parent LayoutController trying to resolve the size of this view
     * @private
     */
    _requestLayoutControllerReflow() {
        this._nodes = {_trueSizeRequested: true};
        //TODO: Do we really need to emit this?
        this._eventOutput.emit('layoutControllerReflow');
    }

    _getRenderableOptions(renderableName, decorations = this.renderables[renderableName]) {
        let decoratorOptions = decorations.constructionOptionsMethod ? decorations.constructionOptionsMethod.call(this, this.options) : {};
        if (!Utils.isPlainObject(decoratorOptions)) {
            Utils.warn(`Invalid option '${decoratorOptions}' given to item ${renderableName}`);
        }
        return decoratorOptions;
    }

    /**
     * Construct all the renderables that have been decorated in the class.
     * @private
     */
    _constructDecoratedRenderables() {

        let classConstructorList = [];

        /* Reverse the class list becaues rit makes more sense to make the renderables of the parent before the renderables
         * of this view
        */
        for (let currentClass = this; currentClass.__proto__.constructor !== View; currentClass = Object.getPrototypeOf(currentClass)) {
            classConstructorList.push(currentClass.__proto__.constructor);
        }
        classConstructorList.reverse();


        for (let currentClassConstructor of classConstructorList) {
            let renderableConstructors = this.renderableConstructors.get(currentClassConstructor);
            for (let renderableName in renderableConstructors) {
                let decorations = renderableConstructors[renderableName].decorations;

                let renderable = renderableConstructors[renderableName].call(this, this._getRenderableOptions(renderableName, decorations));

                /* Clone the decorator properties, because otherwise every view of the same type willl share them between
                 * the same corresponding renderable. TODO: profiling reveals that cloneDeep affects performance
                 */
                renderable.decorations = cloneDeep(extend({}, decorations, renderable.decorations || {}));


                /* Since after constructor() of this View class is called, all decorated renderables will
                 * be attempted to be initialized by Babel / the ES7 class properties spec, we'll need to
                 * override the descriptor get/initializer to return this specific instance once.
                 *
                 * If we don't do this, the View will have its renderables overwritten by new renderable instances
                 * that don't have constructor.options applied to them correctly. If we always return this specific instance
                 * instead of only just once, any instantiation of the same View class somewhere else in the code will refer
                 * to the renderables of this instance, which is unwanted.
                 */
                let {descriptor} = decorations;
                if (descriptor) {
                    if (descriptor.get) {
                        let originalGet = decorations.descriptor.get;
                        descriptor.get = () => {
                            descriptor.get = originalGet;
                            return renderable;
                        }
                    }
                    if (descriptor.initializer) {
                        let originalInitializer = decorations.descriptor.initializer;
                        descriptor.initializer = () => {
                            descriptor.initializer = originalInitializer;
                            return renderable;
                        }
                    }
                }
                this._assignRenderable(renderable, renderableName);
            }
        }
    }


    /**
     * Assigns a renderable to this view, without setting this[renderableName]
     * @param {Renderable} renderable the renderable that is going to be added
     * @param {String} renderableName the name of the renderable
     * @private
     */
    _assignRenderable(renderable, renderableName) {
        this._renderableHelper.assignRenderable(renderable, renderableName);
        /* Do add property to object because there can be a getter defined instead of a class property,
         * in which case we have to use the ObjectHelper
         */
        ObjectHelper.addPropertyToObject(this, renderableName, renderable, true, true, null, false);
    }

    _layoutDecoratedRenderables(context, options) {
        let dockedRenderables = this._renderableHelper;
        let nativeScrollableOptions = this.decorations.nativeScrollable;
        if(nativeScrollableOptions) {
            let thisSize  = this.getSize();
            context.size = context.size.map((size, index) =>
            (nativeScrollableOptions[`scroll${index === 0 ? 'X' : 'Y'}`] && Math.max(thisSize[index],size)) || size);
        }
        this._dockedRenderablesHelper.layout(dockedRenderables.getRenderableGroup('docked'), dockedRenderables.getRenderableGroup('filled'), context, this.decorations);
        this._fullSizeLayoutHelper.layout(dockedRenderables.getRenderableGroup('fullSize'), context, this.decorations);
        this._traditionalLayoutHelper.layout(dockedRenderables.getRenderableGroup('traditional'), context, this.decorations);
    }

    /**
     * Combines all layouts defined in subclasses of the View into a single layout for the LayoutController.
     * @returns {void}
     * @private
     */
    _createLayoutController() {
        let hasFlowyRenderables = this._renderableHelper.hasFlowyRenderables();
        this.layout = new LayoutController({
            flow: !!this.decorations.useFlow || hasFlowyRenderables,
            partialFlow: !this.decorations.useFlow,
            nativeScroll: !!this.decorations.nativeScrollable,
            flowOptions: this.decorations.flowOptions || {spring: {period: 200}},
            layout: function (context, options) {

                /* Because views that extend this View class first call super() and then define their renderables,
                 * we wait until the first engine render tick to add our renderables to the layout, when the view will have declared them all.
                 * layout.setDataSource() will automatically pipe events from the renderables to this View. */
                if (!this._initialised) {
                    this.layout.setDataSource(this.renderables);
                    this._renderableHelper.pipeAllRenderables();
                    this._renderableHelper.initializeAnimations();
                    this._initialised = true;
                    this.layout.reflowLayout();

                    /*
                     * When the data source is set, it will not be reflected in the context yet because the layout is already
                     * prepared for the previous (empty) renderable data source. Therefore, it's a waste of resources
                     * and mysterious bugs to continue. We will wait for the next rendering cycle. However, if views
                     * are only having decorated renderables, then we don't have to do this whatsoever
                     */
                    return;
                }

                /* Layout all renderables that have decorators (e.g. @someDecorator) */
                this._layoutDecoratedRenderables(context, options);
                if (this.decorations.customLayoutFunction) {
                    this.decorations.customLayoutFunction(context);
                }

                this._doTrueSizedSurfacesBookkeeping();

                /* Legacy context.set() based layout functions */
                if (this.layouts.length) {
                    this._callLegacyLayoutFunctions(context, options);
                }
            }.bind(this)
        });

        this._eventInput.on('recursiveReflow', () => {
            this.reflowRecursively();
        });

        /* Add the layoutController to this View's rendering context. */
        this._prepareLayoutController();


        if((this.decorations.scrollable || this.decorations.nativeScrollable) && !this._renderableHelper.getRenderableGroup('fullSize')){
            this.addRenderable(new Surface(), '_fullScreenTouchArea', layout.fullSize(), layout.translate(0, 0, -10));
        }
    }

    /**
     * Layout all renderables that have explicit context.set() calls in this View's legacy layout array.
     * @returns {void}
     * @private
     */
    _callLegacyLayoutFunctions(context, options) {
        for (let layout of this.layouts) {
            try {
                switch (typeof layout) {
                    case 'function':
                        layout.call(this, context, options);
                        break;
                    default:
                        Utils.warn(`Unrecognized layout specification in view '${this._name()}'.`);
                        break;
                }
            } catch (error) {
                Utils.warn(`Exception thrown in ${this._name()}:`);
                console.log(error);
            }
        }
    }

    /**
     * Either adds this.layout (a LayoutController) to the current View, or a FlexScrollView containing this.layout if this view
     * has been decorated with a @scrollable.
     * @returns {void}
     * @private
     */
    _prepareLayoutController() {
        let {scrollableOptions} = this.decorations;
        if (scrollableOptions) {
            this._scrollView = new ReflowingScrollView(scrollableOptions);
            this.layout.getSize = this.getSize;
            this._scrollView.push(this.layout);
            this.pipe(this._scrollView);
            this.add(this._scrollView);
        }
        else {
            this.add(this.layout);
        }
    }

    /**
     * Calculates the total height of the View's layout when it's embedded inside a FlexScrollView (i.e. @scrollable is set on the View),
     * by iterating over each renderable inside the View, and finding the minimum and maximum y values at which they are drawn.
     *
     *
     * @returns {*[]}
     * @private
     */
    _getLayoutSize() {
        let dockedRenderables = this._renderableHelper.getRenderableGroup('docked');
        let traditionalRenderables = this._renderableHelper.getRenderableGroup('traditional');
        let filledRenderables = this._renderableHelper.getRenderableGroup('filled');
        if (!traditionalRenderables && !dockedRenderables) {
            return [undefined, undefined];
        }
        let totalSize = [undefined, undefined];
        if (dockedRenderables || filledRenderables) {
            totalSize = this._dockedRenderablesHelper.boundingBoxSize(dockedRenderables, filledRenderables, this.decorations);
        }

        if (traditionalRenderables) {
            let traditionalRenderablesBoundingBox = this._traditionalLayoutHelper.boundingBoxSize(traditionalRenderables);
            for (let [dimension, singleSize] of totalSize.entries()) {
                let traditionalSingleSize = traditionalRenderablesBoundingBox[dimension];
                if (traditionalSingleSize !== undefined && (singleSize === undefined || singleSize < traditionalSingleSize)) {
                    totalSize[dimension] = traditionalSingleSize;
                }
            }
        }
        return totalSize;

    }

    /**
     * Retrieves the class name of the subclass View instance.
     * @returns {string}
     * @private
     */
    _name() {
        return Object.getPrototypeOf(this).constructor.name;
    }

    /**
     * Copies prototype properties set by decorators to this
     * @private
     */
    _copyPrototypeProperties() {
        let prototype = Object.getPrototypeOf(this);

        /* Move over all renderable- and decoration information that decorators.js set to the View prototype */
        for (let name of ['decorationsMap', 'renderableConstructors']) {
            this[name] = cloneDeep(prototype[name]) || new Map();
        }
    }

    /**
     * Inits the decorations that is set on a class level
     * @private
     */
    _initOwnDecorations() {
        for (let currentClass = this; currentClass.__proto__.constructor !== View; currentClass = Object.getPrototypeOf(currentClass)) {
            /* The close the decoration is to this constructor in the prototype chain, the higher the priority */
            let decorations = this.decorationsMap.get(currentClass.__proto__.constructor);
            for (let property in decorations) {
                if (!(property in this.decorations)) {
                    this.decorations[property] = decorations[property];
                }
            }
        }

        if(this.decorations.dynamicDockPadding) {
            this.onNewSize((size) => this.decorations.viewMargins = this.decorations.dynamicDockPadding(size));
        }

        if (!this.decorations.extraTranslate) {
            this.decorations.extraTranslate = [0, 0, 10];
        }

        if (this.decorations.nativeScrollable){
            Engine.enableTouchMove();
        }

    }

    _doTrueSizedSurfacesBookkeeping() {
        this._nodes._trueSizeRequested = false;
    }

    _initTrueSizedBookkeeping() {
        this.layout.on('layoutstart', ({oldSize, size}) => {
            if (size[0] !== oldSize[0] ||
                size[1] !== oldSize[1]) {
                this._sizeResolver.doTrueSizedBookkeeping();
                ///
                //TODO: Kept for legacy reasons, but remove all listeners to this function
                this._eventOutput.emit('newSize', size);
                for(let callback of this._onNewSizeCallbacks){
                    callback(size);
                }
            }
        });
        /* Hack to make the layoutcontroller reevaluate sizes on resize of the parent */
        this._nodes = {_trueSizedRequested: false};
        /* This needs to be set in order for the LayoutNodeManager to be happy */
        this.options.size = this.options.size || [true, true];
    }

    _initOptions(options) {
        if (!Utils.isPlainObject(options)) {
            Utils.warn(`View ${this._name()} initialized with invalid non-object arguments`);
        }
        /**
         * A copy of the options that were passed in the constructor
         *
         * @type {Object}
         */
        this.options = options;
    }

    _initDataStructures() {
        if (!this.renderables) {
            /**
             * The renderables "outputted" by the view that are passed to the underlying famous-flex layer
             *
             * @type {Object}
             */
            this.renderables = {};
        }
        if (!this.layouts) {
            /**
             * @deprecated
             *`
             * The old way of setting the spec of the renderables created by adding renderables through
             * `this.renderables.myRenderable = ....
             *
             * @type {Array|Function}
             */
            this.layouts = [];
        }

        if (!this.decorations) {
            this.decorations = {};
        }


        this._runningRepeatingFlowStates = {};
        this._onNewSizeCallbacks = [];
    }

    /**
     * Binds the method to this view. Used by the util DecoratedRenderables
     * @param {Function} method The method that is about to be bound
     * @returns {*}
     * @private
     */
    _bindToSelf(method) {
        return method.bind(this);
    }

    /**
     * Pipes a renderable to this view. Used by the util DecoratedRenderables
     * @param {Function} method The method that is about to be bound
     * @param {Boolean} enable set to false to unpipe
     * @returns {Boolean} true if piping was successful, otherwise false
     * @private
     */
    _setPipeToSelf(renderable, enable = true) {
        let methodName = enable ? 'pipe' : 'unpipe';
        /* Auto pipe events from the renderable to the view */
        if (renderable && renderable[methodName]) {
            /*
             * We see it as a bit of a mystery why the piping needs to be done both to this and this._eventOutput,
             * but they both seem to be necessary so I'm gonna leave it for now.
             */
            renderable[methodName](this);
            renderable[methodName](this._eventOutput);
            return true;
        }
        return false;
    }
}