/* Copyright © 2015-2016 David Valdman */
define(function(require, exports, module){
var Transform = require('../core/Transform');
var View = require('../core/View');
var Stream = require('../streams/Stream');
var ReduceStream = require('../streams/ReduceStream');
var Accumulator = require('../streams/Accumulator');
var Differential = require('../streams/Differential');
var CONSTANTS = {
DIRECTION : {
X : 0,
Y : 1
}
};
// Default map to convert displacement to transform
function DEFAULT_LENGTH_MAP(length){
return (this.options.direction === CONSTANTS.DIRECTION.X)
? Transform.translateX(length)
: Transform.translateY(length);
}
/**
* A layout which arranges items vertically or horizontally within a containing size.
* Items with a definite size in the specified direction keep their size, where
* items with an `undefined` size in the specified direction have a flexible size.
* Flexible sized items split up the left over size relative to their flex value.
*
* @class FlexLayout
* @constructor
* @namespace Layouts
* @extends Core.View
* @param [options] {Object} Options
* @param [options.direction]{Number} Direction to lay out items
* @param [options.spacing]{Number} Spacing between items
*/
var FlexLayout = View.extend({
defaults : {
direction : CONSTANTS.DIRECTION.X,
spacing : 0
},
initialize : function initialize(options){
// Store nodes and flex values
this.nodes = [];
this.flexs = [];
// Displacement for each item
this.lengthStream = new ReduceStream(function(prev, size){
if (!size) return false;
return prev + size[options.direction] + options.spacing;
});
// Amount of length used by fixed sized items
this.usedLength = new ReduceStream(function(prev, size){
if (!size) return false;
return prev + size[options.direction];
});
// Amount of length left over for flex items
this.availableLength = Stream.lift(function(totalSize, usedLength){
if (!totalSize) return false;
return totalSize[options.direction] - usedLength;
}, [this.size, this.usedLength.headOutput]);
// Total amount of flex
this.totalFlex = new Accumulator(0);
// Map to convert displacement to transform
this.setLengthMap(DEFAULT_LENGTH_MAP);
},
/*
* Set a custom map from length displacements to transforms.
* `this` will automatically be bound to the instance.
*
* @method setLengthMap
* @param map {Function} Map `(length) -> transform`
*/
setLengthMap : function(map){
this.transformMap = map.bind(this);
},
/*
* Add a renderable to the end of the layout
*
* @method push
* @param item {Surface|View} Renderable
* @param flex {Number|Transitionable} Flex amount
*/
push : function(item, flex){
this.nodes.push(item);
this.flexs.push(flex);
if (flex === undefined) this.usedLength.push(item.size);
var length = this.lengthStream.push(item.size);
var node = createNodeFromLength.call(this, length, flex);
this.add(node).add(item);
},
/*
* Unlink the last renderable in the layout
*
* @method pop
* @return item
*/
pop : function(){
return this.unlink(this.nodes.length - 1);
},
/*
* Add a renderable to the beginning of the layout
*
* @method unshift
* @param item {Surface|View} Renderable
* @param flex {Number|Transitionable} Flex amount
*/
unshift : function(item, flex){
this.nodes.unshift(item);
this.flexs.unshift(flex);
if (flex === undefined) this.usedLength.push(item.size);
var length = this.lengthStream.unshift(item.size);
var node = createNodeFromLength.call(this, length, flex);
this.add(node).add(item);
},
/*
* Unlink the first renderable in the layout
*
* @method shift
* @return item
*/
shift : function(){
return this.unlink(0);
},
/*
* Add a renderable after a specified renderable
*
* @method insertAfter
* @param prevItem {Number|Surface|View} Index or renderable to insert after
* @param item {Surface|View} Renderable to insert
* @param flex {Number|Transitionable} Flex amount
*/
insertAfter : function(prevItem, item, flex){
var index;
if (typeof prevItem === 'number'){
index = prevItem + 1;
prevItem = this.nodes[prevItem];
}
else index = this.nodes.indexOf(prevItem) + 1;
this.nodes.splice(index, 0, item);
this.flexs.splice(index, 0, flex);
if (flex === undefined) this.usedLength.push(item.size);
var length = this.lengthStream.insertAfter(prevItem.size, item.size);
var node = createNodeFromLength.call(this, length, flex);
this.add(node).add(item);
},
/*
* Add a renderable before a specified renderable
*
* @method insertAfter
* @param prevItem {Number|Surface|View} Index or renderable to insert before
* @param item {Surface|View} Renderable to insert
* @param flex {Number|Transitionable} Flex amount
*/
insertBefore : function(postItem, item, flex){
var index;
if (typeof postItem === 'number'){
index = postItem - 1;
postItem = this.nodes[postItem];
}
else index = this.nodes.indexOf(postItem) - 1;
this.nodes.splice(index + 1, 0, item);
this.flexs.splice(index + 1, 0, flex);
if (flex === undefined) this.usedLength.push(item.size);
var length = this.lengthStream.insertBefore(postItem.size, item.size);
var node = createNodeFromLength.call(this, length, flex);
this.add(node).add(item);
},
/*
* Unlink the renderable.
* To remove the renderable, call the `.remove` method on it after unlinking.
*
* @method unlink
* @param item {Number|Surface|View} Index or item to remove
* @return item
*/
unlink : function(item){
var index = (typeof item === 'number')
? item
: this.nodes.indexOf(item);
item = this.nodes.splice(index, 1)[0];
var flex = this.flexs.splice(index, 1)[0];
if (flex === undefined) this.usedLength.remove(item.size);
else {
if (typeof flex === 'number')
this.totalFlex.set(this.totalFlex.get() - flex);
else
this.totalFlex.set(this.totalFlex.get() - flex.get());
}
this.lengthStream.remove(item.size);
return item;
},
length : function(){
return this.nodes.length;
},
/*
* Returns flex for an item or index
*
* @method getFlexFor
* @param item {Index|Surface|View} Index or item to get flex for
* @return flex {Number|Transitionable}
*/
getFlexFor : function(item){
if (item === undefined) return this.getFlexes();
return (typeof item === 'number')
? this.flexs[item]
: this.flexs[this.nodes.indexOf(item)];
},
/*
* Returns flexes of all current renderables
*
* @method getFlexes
* @return flexes {Array}
*/
getFlexes : function(){
return this.flexs;
}
}, CONSTANTS);
function createNodeFromLength(length, flex){
var transform = length.map(this.transformMap);
if (flex !== undefined){
var size;
if (typeof flex === 'number'){
this.totalFlex.set(this.totalFlex.get() + flex);
// Flexible sized item: layout defines the size and transform
size = Stream.lift(function(availableLength, totalFlex){
if (!availableLength) return false;
var itemLength = (availableLength - (this.nodes.length - 1) * this.options.spacing) * (flex / totalFlex);
return (this.options.direction === CONSTANTS.DIRECTION.X)
? [itemLength, undefined]
: [undefined, itemLength];
}.bind(this), [this.availableLength, this.totalFlex]);
}
else {
this.totalFlex.set(this.totalFlex.get() + flex.get());
var flexDelta = new Differential();
flexDelta.subscribe(flex);
this.totalFlex.subscribe(flexDelta);
size = Stream.lift(function(availableLength, flex, totalFlex){
if (!availableLength) return false;
var itemLength = (availableLength - (this.nodes.length - 1) * this.options.spacing) * (flex / totalFlex);
return (this.options.direction === CONSTANTS.DIRECTION.X)
? [itemLength, undefined]
: [undefined, itemLength];
}.bind(this), [this.availableLength, flex, this.totalFlex]);
}
return {transform : transform, size : size};
}
else {
// Fixed size item: layout only defines the transform
return {transform : transform};
}
}
module.exports = FlexLayout;
});