src/utils/view/RenderableHelper.js
/**
* Created by lundfall on 02/09/16.
*/
import OrderedHashMap from 'ordered-hashmap';
import Transitionable from 'famous/transitions/Transitionable.js';
import Easing from 'famous/transitions/Easing.js';
import Draggable from 'famous/modifiers/Draggable.js';
import ContainerSurface from 'famous/Surfaces/ContainerSurface.js';
import Transform from 'famous/core/Transform.js';
import Timer from 'famous/utilities/Timer.js';
import GenericSync from 'famous/inputs/GenericSync.js';
import MouseSync from 'famous/inputs/MouseSync.js';
import TouchSync from 'famous/inputs/TouchSync.js';
import RenderNode from 'famous/core/RenderNode';
import Modifier from 'famous/core/Modifier.js';
import AnimationController from 'famous-flex/AnimationController.js';
import {Throttler} from 'arva-js/utils/Throttler.js';
import {limit} from '../Limiter.js';
import {Utils} from './Utils.js';
import {
callbackToPromise,
waitMilliseconds
} from '../CallbackHelpers.js';
export class RenderableHelper {
/**
* Creates a utility for maintaining proper state of decorated renderables
* @param {Function} bindMethod
* @param {Function} pipeMethod
* @param {Object|Renderable} outputRenderables
* @param sizeResolver
*/
constructor(bindMethod, pipeMethod, outputRenderables, sizeResolver) {
this._bindMethod = bindMethod;
this._renderableCounterparts = outputRenderables;
this._sizeResolver = sizeResolver;
this._pipeToView = pipeMethod;
this.waitingAnimations = [];
this._renderables = {};
this._groupedRenderables = {};
this._pipedRenderables = {};
this._groupedRenderables = {};
this._runningFlowStates = {};
}
assignRenderable(renderable, renderableName) {
this._renderables[renderableName] = renderable;
let renderableEquivalent = this._addDecoratedRenderable(renderable, renderableName);
this._renderableCounterparts[renderableName] = renderableEquivalent;
this._setupAllRenderableListeners(renderableName);
}
/**
* Setups all renderable listeners (decoration events, decoration pipes, pipe to the view)
* @param {String} renderableName the name of the renderable
* @param {Boolean} enabled set to false to unset all the events
* @private
*/
_setupAllRenderableListeners(renderableName, enabled = true) {
/* If the this._renderableCounterparts equivalent doesn't have the pipe function as is the case with the draggable, then use the regular renderable */
let renderableOrEquivalent = this._getPipeableRenderableFromName(renderableName);
if (enabled) {
this._pipeRenderable(renderableOrEquivalent, renderableName);
} else {
this._unpipeRenderable(renderableOrEquivalent, renderableName);
}
let {decorations} = this._renderables[renderableName];
if (decorations) {
this._setDecorationPipes(renderableOrEquivalent, decorations.pipes, enabled);
this._setDecorationEvents(renderableOrEquivalent, decorations.eventSubscriptions, enabled);
}
}
/**
* Sets the decoration events that are specified with (among potential others) @layout.on and @layout.once
* @param {String} renderableName
* @param enable. If false, removes the events.
* @private
*/
_setDecorationEvents(renderable, subscriptions, enable = true) {
for (let subscription of subscriptions || []) {
let subscriptionType = subscription.type || 'on';
if (!enable) {
/* In famous, you remove a listener by calling removeListener, but some classes might have another event
* listener that is called off
*/
subscriptionType = renderable.removeListener ? 'removeListener' : 'off';
}
let eventName = subscription.eventName;
let callback = subscription.callback;
if (subscriptionType in renderable) {
renderable[subscriptionType](eventName, this._bindMethod(callback));
}
}
}
/**
* Pipes the renderable to a list of other renderables
* @param {Renderable} renderable
* @param {Array|String} Names of renderables that have to be piped.
* @param {Boolean} enabled. Set to false to unpipe
* @private
*/
_setDecorationPipes(renderable, pipes, enabled = true) {
for (let pipeToName of pipes || []) {
let target = pipeToName ? this._renderables[pipeToName] : this;
let pipeFn = (enabled ? '' : 'un') + 'pipe';
/* In order to keep things consistent and easier to use, we pipe from the renderable equivalent */
if (renderable[pipeFn]) {
renderable[pipeFn](target);
}
if (renderable[pipeFn] && target._eventOutput) {
renderable[pipeFn](target._eventOutput);
}
}
}
/**
* Unpipes a renderables that has been piped to this view
* @param {String} renderableName The name of the renderable
* @private
*/
_unpipeRenderable(renderableName) {
if(this._pipeToView(this._pipedRenderables[renderableName]), false){
delete this._pipedRenderables[renderableName];
}
}
/**
* Pipes a renderable to this view
* @param {Renderable} renderable. The renderable that is going to be piped
* @param {String} renderableName. The name of the renderable that is going to be piped.
* @private
*/
_pipeRenderable(renderable, renderableName) {
/* Auto pipe events from the renderable to the view */
if(this._pipeToView(renderable, true)){
this._pipedRenderables[renderableName] = renderable;
}
}
/**
* Determines whether the renderable counterpart (i.e. animationcontroller or containersurface) should be used
* when piping, or the renderable itself
* @param {String} renderableName The name of the renderable
* @returns {Renderable} the renderable or its counterpart
* @private
*/
_getPipeableRenderableFromName(renderableName) {
return this._renderableCounterparts[renderableName].pipe ? this._renderableCounterparts[renderableName] : this._renderables[renderableName];
}
/**
* Adds a decorated renderable to the bookkeeping of the view
* @param renderable
* @param renderableName
* @returns {Renderable} newRenderable The renderable that is normally stored this._renderableCounterpart[renderableName]
* @private
*/
_addDecoratedRenderable(renderable, renderableName) {
let {flow, size, dock} = renderable.decorations;
if (size) {
this._bindSizeFunctions(size);
}
if (dock && dock.size) {
this._bindSizeFunctions(dock.size);
}
let renderableCounterpart = this._processsDecoratedRenderableCounterpart(renderable, renderableName);
this._addRenderableToDecoratorGroup(renderable, renderableCounterpart, renderableName);
return renderableCounterpart;
}
/**
* Bind the size functions so that they don't have to be bound afterwards
* @param {Array|Number} size
* @private
*/
_bindSizeFunctions(size) {
for (let index = 0; index < 2; index++) {
if (typeof size[index] === 'function') {
size[index] = this._bindMethod(size[index]);
}
}
}
/**
* Returns true if there are any flowy renderables.
* @returns {Boolean} hasFlowyRenderables
*/
hasFlowyRenderables() {
for (let groupName in this._groupedRenderables) {
let renderableGroup = this._groupedRenderables[groupName];
if (!renderableGroup.keys().every((renderableName) => !renderableGroup.get(renderableName)[0].decorations.flow)) {
return true;
}
}
return false;
}
/**
* Processes the renderable counter-part of the renderable. The counterpart is different from the renderable
* in @layout.draggable, @layout.swipable, @layout.animate, and others.
* @param {Renderable} renderable the renderable which has renderable.decorations set to determine the counter part
* @param {String} renderableName the name of the renderable
* @returns {AnimationController|ContainerSurface|RenderNode|*} The renderable counterpart
* @private
*/
_processsDecoratedRenderableCounterpart(renderable, renderableName) {
let {draggableOptions, swipableOptions, clip, animation, flow} = renderable.decorations;
/* If we clip, then we need to create a containerSurface */
if (clip) {
let clipSize = clip.size;
/* Resolve clipSize specified as undefined */
let containerSurface = new ContainerSurface({
size: clipSize,
properties: {overflow: 'hidden', ...clip.properties}
});
containerSurface.add(renderable);
if (containerSurface.pipe) {
containerSurface.pipe(renderable._eventOutput);
}
renderable.containerSurface = containerSurface;
}
if (animation) {
this._processAnimatedRenderable(renderable, renderableName, animation);
}
if (swipableOptions) {
renderable = this._initSwipable(swipableOptions, renderable);
} else if (draggableOptions && !renderable.node) {
renderable.node = new RenderNode();
let draggable = new Draggable(draggableOptions);
renderable.draggable = draggable;
renderable.node.add(draggable).add(renderable);
renderable.pipe(draggable);
//TODO: We don't do an unpiping of the draggable, which might be dangerous
this._pipeToView(draggable);
}
if (renderable.node) {
/* Assign output handler */
renderable.node._eventOutput = renderable._eventOutput;
}
let renderableCounterpart = renderable.animationController || renderable.containerSurface || renderable.node || renderable;
/* If a renderable has an AnimationController used to animate it, add that to this._renderableCounterparts.
* If a renderable has an ContainerSurface used to clip it, add that to this._renderableCounterparts.
* this._renderableCounterparts is used in the LayoutController in this.layout to render this view. */
if (flow) {
renderableCounterpart.isFlowy = true;
}
return renderableCounterpart;
}
/**
* Pipes the output events of all items in the renderable counterparts that might have been forgotten due to legacy way of declaring
* renderables
* @returns {void}
* @private
*/
pipeAllRenderables() {
for (let renderableName in this.renderables) {
if (!this._pipedRenderables[renderableName]) {
this._pipeRenderable(this._getPipeableRenderableFromName(renderableName), renderableName);
}
}
}
/**
* Initialize all animation set by @layout.animate
*/
initializeAnimations() {
for (let animation of (this.waitingAnimations || [])) {
let renderableToWaitFor = this._renderables[animation.waitFor];
if (renderableToWaitFor && renderableToWaitFor.on) {
renderableToWaitFor.on('shown', function subscription() {
animation.showMethod();
if ('off' in renderableToWaitFor) {
renderableToWaitFor.off('shown', subscription);
}
if ('removeListener' in renderableToWaitFor) {
renderableToWaitFor.removeListener('shown', subscription);
}
});
} else {
Utils.warn(`Attempted to delay showing renderable ${animation.waitFor}, which does not exist or contain an on() method.`);
}
}
}
//Done
/**
* Processes an animated renderable
* @param renderable
* @param renderableName
* @param options
* @private
*/
_processAnimatedRenderable(renderable, renderableName, options) {
let pipeRenderable = () => {
if (renderable.pipe) renderable.pipe(renderable.animationController._eventOutput)
};
/* If there's already an animationcontroller present, just change the options */
let renderableCounterpart = this._renderableCounterparts[renderableName];
if (renderableCounterpart instanceof AnimationController) {
renderable.animationController = renderableCounterpart;
renderable.animationController.setOptions(options);
pipeRenderable();
} else {
let animationController = renderable.animationController = new AnimationController(options);
pipeRenderable();
let showMethod = this.showWithAnimationController.bind(this, animationController, renderable);
if (options.delay && options.delay > 0 && options.showInitially) {
Timer.setTimeout(showMethod, options.delay);
} else if (options.waitFor) {
this.waitingAnimations.push({showMethod: showMethod, waitFor: options.waitFor});
} else if (options.showInitially) {
showMethod();
}
}
}
//Done
/**
* Shows a renderable using the animationController specified. When operation is complete, the renderable emits
* the one events 'show' or 'hide', depending on what operation that was done.
* @param animationController
* @param renderable
* @param show
* @private
*/
showWithAnimationController(animationController, renderable, show = true, callback) {
animationController._showingRenderable = show;
let callbackIfExists = () => {
if(callback) {
callback();
}
};
let emitOnFinished = () => {
if (renderable.emit) {
renderable.emit(show ? 'shown' : 'hidden');
}
callbackIfExists();
};
if (show) {
animationController.show(renderable.containerSurface || renderable, null, emitOnFinished);
} else {
animationController.hide(null, emitOnFinished);
}
}
//Done
_addRenderableToDecoratorGroup(renderable, renderableCounterpart, renderableName) {
/* Group the renderable */
let groupName = this._getGroupName(renderable);
if (groupName) {
if (!(groupName in this._groupedRenderables)) {
this._groupedRenderables[groupName] = new OrderedHashMap();
}
/* We save the both the renderable and the renderable counterpart in pairs */
this._groupedRenderables[groupName].set(renderableName, [renderable, renderableCounterpart]);
}
}
//Done
_getGroupName(renderable) {
let {decorations} = renderable;
if (!!decorations.dock) {
/* 'filled' is a special subset of 'docked' renderables, that need to be rendered after the normal 'docked' renderables are rendered. */
return decorations.dock.dockMethod === 'fill' ? 'filled' : 'docked';
} else if (!!decorations.fullSize) {
return 'fullSize';
} else if (decorations.size || decorations.origin || decorations.align || decorations.translate) {
return 'traditional';
} else {
/* This occurs e.g. when a renderable is only marked @renderable, and its parent view has a @layout.custom decorator to define its context. */
return 'ignored';
}
}
/**
* Gets the renderables of a certain group
* @param {String} The name of the group
* @returns {OrderedHashMap} A map containing Array-pairs of [renderable, renderableCounterpart] containing the renderables of the specified type.
*/
getRenderableGroup(groupName) {
return this._groupedRenderables[groupName];
}
//Done
/**
* Removes the renderable from the view
* @param {String} renderableName The name of the renderable
*/
removeRenderable(renderableName) {
let renderable = this._renderables[renderableName];
this._setDecorationPipes(renderableName, false);
this._setDecorationEvents(renderableName, false);
this._unpipeRenderable(renderableName, renderableName);
this._removeRenderableFromDecoratorGroup(renderable, renderableName);
delete this._renderableCounterparts[renderableName];
delete this._renderables[renderableName];
}
//Done
_removeRenderableFromDecoratorGroup(renderable, renderableName) {
let groupName = this._getGroupName(renderable);
this._removeRenderableFromGroupWithName(renderableName, groupName);
}
//Done
_removeRenderableFromGroupWithName(renderableName, groupName) {
let group = this._groupedRenderables[groupName];
group.remove(renderableName);
if (!group.count()) {
delete this._groupedRenderables[groupName];
}
}
//done
/**
* @example
* decorateRenderable('myRenderable',layout.size(100, 100));
*
* Decorates a renderable with other decorators. Using the same decorators as used previously will override the old ones.
* @param {String} renderableName The name of the renderable
* @param ...decorators The decorators that should be applied
*/
decorateRenderable(renderableName, ...decorators) {
let renderable = this._renderables[renderableName];
/* Add translate and rotate to be sure that there decorators translateFrom and rotateFrom work */
let fakeRenderable = {
decorations: {
translate: renderable.decorations.translate || [0, 0, 0],
rotate: renderable.decorations.rotate || [0, 0, 0]
}
};
if (!decorators.length) {
Utils.warn('No decorators specified to decorateRenderable(renderableName, ...decorators)');
}
/* There can be existing decorators already, which are preserved. We are extending the decorators object,
* by first creating a fake renderable that gets decorators */
this.applyDecoratorFunctionsToRenderable(fakeRenderable, decorators)
let {decorations} = fakeRenderable;
let renderableOrEquivalent = this._getPipeableRenderableFromName(renderableName);
/* We might need to do extra piping */
this._setDecorationPipes(renderableOrEquivalent, decorations.pipes);
this._setDecorationEvents(renderableOrEquivalent, decorations.eventSubscriptions);
/* If the renderable is surface, we need to do some special things if there is a true size being used */
if (Utils.renderableIsSurface(renderable)) {
let sizesToCheck = [];
let {size, dock} = decorations;
if (size) {
sizesToCheck.push(size);
}
if (dock) {
sizesToCheck.push(dock.size);
}
let renderableSize = [undefined, undefined];
let trueSizedInfo = this._sizeResolver.getSurfaceTrueSizedInfo(renderable);
for (let sizeToCheck of sizesToCheck) {
for (let dimension of [0, 1]) {
if (this._sizeResolver.isValueTrueSized(sizeToCheck[dimension])) {
if (!trueSizedInfo) {
trueSizedInfo = this._sizeResolver.configureTrueSizedSurface(renderable);
}
trueSizedInfo.trueSizedDimensions[dimension] = true;
renderableSize[dimension] = true;
} else {
if (trueSizedInfo) {
trueSizedInfo.trueSizedDimensions[dimension] = false;
}
}
}
}
if (sizesToCheck.length) {
renderable.setSize(renderableSize);
}
}
let oldRenderableGroupName = this._getGroupName(renderable);
let shouldDisableDock = (fakeRenderable.decorations.disableDock && renderable.decorations.dock);
let shouldDisableFullSize = (fakeRenderable.decorations.size && renderable.decorations.fullSize);
if (shouldDisableDock) {
delete renderable.decorations.dock;
}
if (shouldDisableFullSize) {
delete renderable.decorations.fullSize;
}
/* Extend the object */
Object.assign(renderable.decorations, fakeRenderable.decorations);
/* See if we have to redo the grouping */
let needToChangeDecoratorGroup = (oldRenderableGroupName !== this._getGroupName(renderable)) || shouldDisableDock || shouldDisableFullSize;
/* Process new renderable equivalent, if that applies */
let renderableCounterpart = this._renderableCounterparts[renderableName] = this._processsDecoratedRenderableCounterpart(renderable, renderableName);
if (needToChangeDecoratorGroup) {
this._removeRenderableFromGroupWithName(renderableName, oldRenderableGroupName);
this._addRenderableToDecoratorGroup(renderable, renderableCounterpart, renderableName);
}
}
//done
applyDecoratorFunctionsToRenderable(renderable, decorators){
for (let decorator of decorators) {
/* There can be existing decorators already, which are preserved. We are extending the decorators object,
* by first creating a fake renderable that gets decorators */
decorator(renderable);
}
}
//Done
replaceRenderable(renderableName, newRenderable) {
let renderable = this._renderables[renderableName];
let renderableHasAnimationController = (this._renderableCounterparts[renderableName] instanceof AnimationController);
/* If there isn't a renderable equivalent animationController that does the piping, then we need to redo the event piping */
if (!renderableHasAnimationController) {
this._setupAllRenderableListeners(renderableName, false);
}
newRenderable.decorations = {...newRenderable.decorations, ...renderable.decorations};
let newRenderableCounterpart = this._processsDecoratedRenderableCounterpart(newRenderable, renderableName);
this._groupedRenderables[this._getGroupName(renderable)].set(renderableName, [newRenderable, newRenderableCounterpart]);
if (!renderableHasAnimationController) {
this._renderableCounterparts[renderableName] = newRenderableCounterpart;
this._setupAllRenderableListeners(renderableName, true);
}
this._renderables[renderableName] = newRenderable;
}
//Done
async setRenderableFlowState(renderableName = '', stateName = '') {
let renderable = this._renderables[renderableName];
if (!renderable || !renderable.decorations || !renderable.decorations.flow) {
return Utils.warn(`setRenderableFlowState called on non-existing or renderable '${renderableName}' without flowstate`);
}
let flowOptions = renderable.decorations.flow;
/* Keep track of which flow state changes are running. We only allow one at a time per renderable.
* The latest one is always the valid one.
*/
let flowWasInterrupted = false;
flowOptions.currentState = stateName;
for (let {transformations, options} of flowOptions.states[stateName].steps) {
flowOptions.currentTransition = options.transition;
this.decorateRenderable(renderableName, ...transformations);
/* Make sure FlowLayoutNode.set() is called next render tick */
this._sizeResolver.requestReflow();
/* Set the callback of the renderable so it's passed to the flowLayoutNode */
let resolveData = await new Promise((resolve) => renderable.decorations.flow.callback = resolve);
/* Optionally, we insert a delay in between ending the previous state change, and starting on the new one. */
if (options.delay) {
await waitMilliseconds(options.delay);
}
/* If the flow has been interrupted */
if (resolveData.reason === 'flowInterrupted') {
flowWasInterrupted = true;
break;
}
let emit = (renderable._eventOutput && renderable._eventOutput.emit || renderable.emit).bind(renderable._eventOutput || renderable);
emit('flowStep', {state: stateName});
}
return !flowWasInterrupted;
}
//Done
async setViewFlowState(stateName = '', flowOptions) {
let steps = flowOptions.viewStates[stateName];
/* This is intended to be overwritten by other asynchronous calls to this method, see the stateName check below. */
flowOptions.currentState = stateName;
for (let step of steps) {
await Promise.all(this.generateWaitQueueFromViewStateStep(step));
/* If another state has been set since the invocation of this method, skip any remaining transformations. */
if (flowOptions.currentState !== stateName) {
break;
}
}
return true;
}
generateWaitQueueFromViewStateStep(step) {
let waitQueue = [];
for (let renderableName in step) {
let state = step[renderableName];
waitQueue.push(this.setRenderableFlowState(renderableName, state));
}
return waitQueue;
}
getRenderableFlowState(renderableName = '') {
let renderable = this._renderables[renderableName];
if (!renderable || !renderable.decorations || !renderable.decorations.flow) {
return Utils.warn(`getRenderableFlowState called on non-existing or renderable '${renderableName}' without flowstate`);
}
let flowOptions = renderable.decorations.flow;
return flowOptions.currentState;
}
getViewFlowState(flowOptions = {}) {
return flowOptions.currentState;
}
/**
* Create the swipable and register all the event logic for a swipable renderable
* @private
*/
_initSwipable(swipableOptions = {}, renderable = {}) {
GenericSync.register({
'mouse': MouseSync,
'touch': TouchSync
});
let sync = new GenericSync({
'mouse': {},
'touch': {}
});
renderable.pipe(sync);
/* Translation modifier */
var positionModifier = new Modifier({
transform: function () {
let [x, y] = position.get();
return Transform.translate(x, y, 0);
}
});
var position = new Transitionable([0, 0]);
sync.on('update', (data)=> {
let [x,y] = position.get();
x += !swipableOptions.snapX ? data.delta[0] : 0;
y += !swipableOptions.snapY ? data.delta[1] : 0;
let {yRange = [0, 0], xRange = [0, 0]} = swipableOptions;
y = limit(yRange[0], y, yRange[1]);
x = limit(xRange[0], x, xRange[1]);
position.set([x, y]);
});
sync.on('end', (data)=> {
let [x,y] = position.get();
data.velocity[0] = Math.abs(data.velocity[0]) < 0.5 ? data.velocity[0] * 2 : data.velocity[0];
let endX = swipableOptions.snapX ? 0 : x + data.delta[0] + (data.velocity[0] * 175);
let endY = swipableOptions.snapY ? 0 : y + data.delta[1] + (data.velocity[1] * 175);
let {yRange = [0, 0], xRange = [0, 0]} = swipableOptions;
endY = limit(yRange[0], endY, yRange[1]);
endX = limit(xRange[0], endX, xRange[1]);
position.set([endX, endY], {
curve: Easing.outCirc,
duration: (750 - Math.abs((data.velocity[0] * 150)))
});
this._determineSwipeEvents(renderable, swipableOptions, endX, endY);
});
renderable.node = new RenderNode();
renderable.node.add(positionModifier).add(renderable);
return renderable;
}
_determineSwipeEvents(renderable, swipableOptions = {}, endX = 0, endY = 0) {
if (!renderable || !renderable._eventOutput) return;
let xThreshold = swipableOptions.xThreshold || [undefined, undefined];
let yThreshold = swipableOptions.yThreshold || [undefined, undefined];
if (xThreshold[1] && endX > xThreshold[1]) {
renderable._eventOutput.emit('swiped', {
direction: 0,
displacement: 'right'
});
}
if (xThreshold[0] && endX < xThreshold[0]) {
renderable._eventOutput.emit('swiped', {
direction: 0,
displacement: 'left'
});
}
if (yThreshold[1] && endY > yThreshold[1]) {
renderable._eventOutput.emit('swiped', {
direction: 1,
displacement: 'bottom'
});
}
if (yThreshold[0] && endY < yThreshold[0]) {
renderable._eventOutput.emit('swiped', {
direction: 1,
displacement: 'top'
});
}
}
/**
* 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) {
let dockedRenderables = this._groupedRenderables.docked;
if (!dockedRenderables) {
Utils.warn(`Could not prioritise '${renderableName}' before '${nextRenderableName}': no docked renderables present.`);
return false;
}
let result = this._prioritiseDockAtIndex(renderableName, dockedRenderables.indexOf(nextRenderableName));
if (!result) {
Utils.warn(`Could not prioritise '${renderableName}' before '${nextRenderableName}': could not find one of the renderables by name.
The following docked renderables are present: ${dockedRenderables.keys()}`);
}
return result;
}
/**
* @param {String} renderableName
* @param {String} prevRenderableName
*/
prioritiseDockAfter(renderableName, prevRenderableName) {
let dockedRenderables = this._groupedRenderables.docked;
if (!dockedRenderables) {
Utils.warn(`Could not prioritise '${renderableName}' after '${prevRenderableName}': no docked renderables present.`);
return false;
}
let result = this._prioritiseDockAtIndex(renderableName, dockedRenderables.indexOf(prevRenderableName) + 1);
if (!result) {
Utils.warn(`Could not prioritise '${renderableName}' after '${prevRenderableName}': could not find one of the renderables by name.
The following docked renderables are present: ${dockedRenderables.keys()}`);
}
return result;
}
/**
* Helper function used by prioritiseDockBefore and prioritiseDockAfter to change order of docked renderables
* @param renderableName
* @param index
* @returns {boolean}
* @private
*/
_prioritiseDockAtIndex(renderableName, index) {
let dockedRenderables = this._groupedRenderables.docked;
let renderableToRearrange = dockedRenderables.get(renderableName);
if (index < 0 || !renderableToRearrange) {
return false;
}
dockedRenderables.remove(renderableName);
dockedRenderables.insert(index, renderableName, renderableToRearrange);
return true;
}
}