GreyRook/gown.js

View on GitHub
src/layout/LayoutAlignment.js

Summary

Maintainability
D
1 day
Test Coverage
var Layout = require('./Layout');
var itemDimensions = require('./utils/itemDimensions');

/**
 * Basic layout
 *
 * @class LayoutAlignment
 * @extends GOWN.layout.Layout
 * @memberof GOWN.layout
 * @constructor
 */
function LayoutAlignment() {
    Layout.call(this);
}

LayoutAlignment.prototype = Object.create( Layout.prototype );
LayoutAlignment.prototype.constructor = LayoutAlignment;
module.exports = LayoutAlignment;

/**
 * Vertical alignment
 *
 * @static
 * @final
 * @type String
 */
LayoutAlignment.VERTICAL_ALIGNMENT = 'vertical';

/**
 * Horizontal alignment
 *
 * @static
 * @final
 * @type String
 */
LayoutAlignment.HORIZONTAL_ALIGNMENT = 'horizontal';

/**
 * Apply percentage width/height to items.
 * This will use the explicit width/height and apply it to all items
 * according to its percentages.
 *
 * Percentages have higher priorities than fixed values.
 * So if you set a width higher than 0 but also percentWidth,
 * the width will be recalculated according to percentWidth.
 *
 * @param items The items which get new width and height according to the percent {Array}
 * @param explicit The space we have for the components {Number}
 * @param [alignment=LayoutAlignment.VERTICAL_ALIGNMENT] The alignment mode {String}
 */
LayoutAlignment.prototype.applyFixedPercent = function(items, explicit, alignment) {
    var itemCount = items.length;
    var i, item, itemPercent;
    for (i = 0; i < itemCount; i++) {
        item = items[i];
        // note: this is the opposide of what we normally want
        itemPercent = 0;
        if (alignment === LayoutAlignment.HORIZONTAL_ALIGNMENT) {
            itemPercent = item.percentWidth;
        } else {
            itemPercent = item.percentHeight;
        }
        if (itemPercent > 0) {
            if (alignment === LayoutAlignment.HORIZONTAL_ALIGNMENT) {
                item.width = explicit * itemPercent / 100;
            } else {
                item.height = explicit * itemPercent / 100;
            }
        }
    }
};

/**
 * Apply percentage width/height to items.
 * This will stack items on top/aside of each other
 *
 * Percentages have higher priorities than fixed values.
 * So if you set a width higher than 0 but also percentWidth,
 * the width will be recalculated according to percentWidth.
 *
 * (this function will handle padding and gap, so the explicitWidth is
 * for the whole available width)
 *
 * @param items The items which get new width and height according to the percent {Array}
 * @param explicit space we have for the components {Number}
 */
LayoutAlignment.prototype.applyPercent = function(items, explicit) {
    var _hor = (this.alignment === LayoutAlignment.HORIZONTAL_ALIGNMENT);

    var itemCount = items.length;
    var remaining = explicit;
    var totalExplicit = 0;
    var totalPercent = 0;

    var i, itemPercent, item;
    // sum up width/height required for all items
    for (i = 0; i < itemCount; i++) {
        item = items[i];
        var itemSpace;

        var dimensions = itemDimensions(item);

        itemPercent = _hor ? item.percentWidth : item.percentHeight;
        itemSpace = _hor ? dimensions[0] : dimensions[1];

        if (!isNaN(itemPercent) && itemPercent !== undefined &&
            itemPercent !== null) {
            totalPercent += itemPercent;
        } else if (!isNaN(itemSpace)) {
            // no percentWidth/percentHeight set for this item
            totalExplicit += itemSpace;
        }
    }

    // add space for all gaps
    totalExplicit += this._firstGap > 0 ? this._firstGap : this._gap;
    totalExplicit += (this._gap * (itemCount - 3));
    totalExplicit += this._lastGap > 0 ? this._lastGap : this._gap;

    var padding = _hor ?
        this._paddingLeft + this._paddingRight :
        this._paddingTop + this._paddingBottom;
    totalExplicit += padding;

    // use whole available space - if we do not sum up to 100 we will
    // stretch the items
    if(totalPercent < 100) {
        totalPercent = 100;
    }

    var percentToPixels = (remaining - totalExplicit) / totalPercent;
    // claculate width/height for each item based on remaining width/height
    this.applyFixedPercent(items, percentToPixels * 100, this.alignment);
};

/**
 * Get the current gap (includes first and last gap)
 *
 * @param i The current item position {Number}
 * @param items The list of items (to determine if we are at the last gap) {Array}
 * @private
 */
LayoutAlignment.prototype._currentGap = function(i, items) {
    if(!isNaN(this._firstGap) && i === 0)
    {
        return this._firstGap;
    }
    else if(!isNaN(this._lastGap) && i > 0 && i === items.length - 2)
    {
        return this._lastGap;
    }
    return this._gap;
};

/**
 * Calculate the layout for a container (and its children)
 *
 * @param container The container to calculate the layout for
 * @return Number[] The width and height
 */
LayoutAlignment.prototype.layoutContainer = function(container) {
    var dimensions = itemDimensions(container);
    return this.layout(container.children, dimensions[0], dimensions[1]);
};

/**
 * Position (and possibly resize) the supplied items.
 *
 * @param items The items that will be layouted {Array}
 * @param maxWidth The maximum width for the items {Number}
 * @param maxHeight The maximum height for the items {Number}
 * @return Number[] The width and height
 */
LayoutAlignment.prototype.layout = function(items, maxWidth, maxHeight) {
    var _hor = (this.alignment === LayoutAlignment.HORIZONTAL_ALIGNMENT);

    // width/height the current layout takes
    var width = 0;
    var height = 0;
    var paddingStart = _hor ? this._paddingLeft : this._paddingTop;

    // recalculate width/height for items with percentages
    this.applyPercent(items, _hor ? maxWidth : maxHeight);
    this.applyFixedPercent(items, _hor ? maxHeight : maxWidth,
        _hor ?
            LayoutAlignment.VERTICAL_ALIGNMENT :
            LayoutAlignment.HORIZONTAL_ALIGNMENT);

    var position = paddingStart;
    var itemSpace, itemWidth, itemHeight;
    var dimensions;
    // calculate item position (x/y coordinates)
    for(var i = 0; i < items.length; i++)
    {
        var item = items[i];

        dimensions = itemDimensions(item);
        itemWidth = dimensions[0];
        itemHeight = dimensions[1];

        // move item to position calculated in previous loop
        if (_hor) {
            item.x = Math.floor(position);
            // set height of highest item
            height = Math.max(itemHeight, height);
        } else {
            item.y = Math.floor(position);
            // set width of widest item
            width = Math.max(itemWidth, width);
        }
        itemSpace = _hor ? itemWidth : itemHeight;
        // calculate position for next item
        position += itemSpace + this._currentGap(i, items);

        // if the item has a layout and children, layout the children
        if (this.layoutChildren && item.children &&
            item.layout && item.layout.layout) {
            item.layout.layout(item.children, itemWidth, itemHeight);
        }
    }
    if (_hor) {
        width = position;
    } else {
        height = position;
    }
    this._needUpdate = false;

    return [width, height];
};

/**
 * The space between the first and second element
 *
 * @name GOWN.layout.LayoutAlignment#firstGap
 * @type Number
 */
Object.defineProperty(LayoutAlignment.prototype, 'firstGap', {
    set: function(value) {
        if (value === this._firstGap) {
            return;
        }
        this._firstGap = value;
        this._needUpdate = true;
    },
    get: function() {
        return this._firstGap;
    }
});

/**
 * The space between the last and second-to-last element
 *
 * @name GOWN.layout.LayoutAlignment#lastGap
 * @type Number
 */
Object.defineProperty(LayoutAlignment.prototype, 'lastGap', {
    set: function(value) {
        if (value === this._lastGap) {
            return;
        }
        this._lastGap = value;
        this._needUpdate = true;
    },
    get: function() {
        return this._lastGap;
    }
});