/* Copyright (c) 2011, Geert Bergman (geert@scrivo.nl)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of "Scrivo" nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* $Id: SplitLayout.js 616 2013-04-22 23:48:38Z geert $
*/
"use strict";
SUI.SplitLayout = SUI.defineClass(
/** @lends SUI.SplitLayout.prototype */{
/** @ignore */ baseClass: SUI.AnchorLayout,
/**
* @class
* The split layOut is a container type that lets you split an area into a
* center box and (optional) north, south, west and east boxes. When
* resizing the center box will shrink or expand until the minimum size of
* the content box of the center is reached. Shrinking further will cause
* the north, south, west and east boxes to shrink until the minimum width
* or height of their content boxes.
*
* @augments SUI.AnchorLayout
*
* @description
* Construct a split layOut object. You can tell the split layOut which
* areas (north/south/west/east) to set and the dimensions to use.
* It's also possible to set the child boxes directly.
*
* @constructs
* @param see base class
* @param {Object} arg.center An object with the following members:
* @param {SUI.Box} arg.center.box A client box (optional)
* @param {Object} arg.north (optional) An object with the following
* members:
* @param {int} arg.north.height height of the north area
* @param {SUI.Box} arg.north.box A client box (optional)
* @param {Object} arg.south (optional) An object with the following
* members:
* @param {int} arg.south.height height of the south area
* @param {SUI.Box} arg.south.box A client box (optional)
* @param {Object} arg.west (optional) An object with the following
* members:
* @param {int} arg.west.width width of the west area
* @param {SUI.Box} arg.west.box A client box (optional)
* @param {Object} arg.south (optional) An object with the following
* members:
* @param {int} arg.south.width width of the east area
* @param {SUI.Box} arg.south.box A client box (optional)
*/
initializer: function(arg) {
SUI.SplitLayout.initializeBase(this, arg);
// add the center
this.center = new SUI.Box({parent: this});
if (arg.center && arg.center.box) {
this.add(arg.center.box, "center");
}
// add north, south, west and east if given in arguments
if (arg.north) {
this.north = new SUI.Box({parent: this});
this.north.height(arg.north.height);
if (arg.north.box) {
this.add(arg.north.box, "north");
}
}
if (arg.south) {
this.south = new SUI.Box({parent: this});
this.south.height(arg.south.height);
if (arg.south.box) {
this.add(arg.south.box, "south");
}
}
if (arg.west) {
this.west = new SUI.Box({parent: this});
this.west.width(arg.west.width);
if (arg.west.box) {
this.add(arg.west.box, "west");
}
}
if (arg.east) {
this.east = new SUI.Box({parent: this});
this.east.width(arg.east.width);
if (arg.east.box) {
this.add(arg.east.box, "east");
}
}
},
/**
* {SUI.Box} Center box
*/
center: null,
/**
* {SUI.Box} East box
*/
east: null,
/**
* {SUI.Box} North box
*/
north: null,
/**
* {SUI.Box} South box
*/
south: null,
/**
* {SUI.Box} West box
*/
west: null,
/**
* Add a child to the current location. You might need an onremove
* handler to store the data on the current location if it is replaced
* by a new box. To allow for asynchronious usage the remainder of the
* function is not executed when the onRemove callback is provided.
* One has to finish the action yourself by calling "finishAdd"
* which will remove the current element from the frame an replaces
* it with the given child.
* @param {SUI.Box} child Box to add to the split layout
* @param {String} location Location to add the box to ("west", "south",
* "north" or "west")
* @param {Function} onRemove Function to execue when the child box is
* removed from the split layout
*/
add: function(child, location, onRemove) {
// get the requested location
var t = this[location];
if (t) {
// get the current child ...
var loc = this.get(t);
// is the location is set and there is an onRemove method ...
if (t.onRemove) {
// ... set the parameters for a 'delayed add'
this._faData = { frame: t, loc: loc, child: child,
onRemove: onRemove};
// ... do that method and leave the "finishAdd" to
// the implementation of onRemove. ...
t.callListener("onRemove", loc);
} else {
if (loc){
// remove the box at the location
this.remove(loc, t);
}
// ... else just add the child to the location
SUI.SplitLayout.parentMethod(this, "add", child, t);
this._minsizeCalc();
// store the onRemove handler, need it for the next add
// TODO note: don't use addListener
t.onRemove = onRemove || null;
}
} else {
throw "Splitlayout: Adding to an inexisting location";
}
},
/**
* Set the CSS width and height of she SplitLayout, its locations and
* all the child boxes.
*/
display: function() {
// set the CSS dimensions of the container box ...
this.setDim();
// ... and the of the location boxes ...
if (this.north) {
this.north.setDim();
}
if (this.south) {
this.south.setDim();
}
if (this.west) {
this.west.setDim();
}
if (this.east) {
this.east.setDim();
}
this.center.setDim();
// ... and of all the children
SUI.SplitLayout.parentMethod(this, "display");
},
/**
* Get the child box attached to the location.
* @param {String} location The location (west, east, north or south) for
* which to retrieve the child box
* @returns {SUI.Box} The requested child box, null if there is none
*/
get: function(location) {
for (var i=0; i<this.children.length; i++) {
if (this.children[i].parent() == location) {
return this.children[i];
}
}
return null;
},
/**
* The framework's layOut method. Set the positions of all the
* SplitLayout's location boxes and call the layOut method of all child
* boxes
*/
layOut: function() {
// get the minimum widths and heights
var tmp = this._prepareLayout();
var w = tmp.w; // corrected widths
var h = tmp.h; // corrected heights
var ct = 0; // center top
var cl = 0; // center left
var cw = w.w; // center width
var ch = h.h; // center height
// set the dimensions of the container box
this.setRect(this.top(), this.left(), w.w, h.h);
// if there is a north panel ...
if (this.north) {
// ... set the dimensions ...
this.north.setRect(0, 0, w.w, h.nh);
// ... and adjust the top and height of the center
ct += h.nh;
ch -= ct;
}
// if there is a south panel ...
if (this.south) {
// ... set the dimensions ...
this.south.setRect(h.h-h.sh, 0, w.w, h.sh);
// ... and adjust the height of the center
ch -= h.sh;
}
// if there is a west panel ...
if (this.west) {
// ... set the dimensions ...
this.west.setRect(ct, 0, w.ew, ch);
// ... and adjust the left and width of the center
cl += w.ew;
cw -= cl;
}
// if there is an east panel ...
if (this.east) {
// ... set the dimensions ...
this.east.setRect(ct, w.w-w.ww, w.ww, ch);
// ... and adjust the width of the center
cw -= w.ww;
}
// now we know the position of the center
this.center.setRect(ct, cl, cw, ch);
// layOut the child boxes
SUI.SplitLayout.parentMethod(this, "layOut"); return;
},
/**
* When you use an onRemove handler on add you'll have to finish this
* add procedure yourself by calling finishAdd(). This because your
* onRemove handler can do asynchronious stuff that is not within the
* current thread of control.
*/
finishAdd: function() {
if (this._faData) {
// remove the box at the location
this.remove(this._faData.loc, this._faData.frame);
/// and add the the new one
SUI.SplitLayout.parentMethod(this, "add",
this._faData.child, this._faData.frame);
this._minsizeCalc();
// store the onRemove handler, need it for the next add
// TODO note: don't use addListener
this._faData.frame.onRemove = this._faData.onRemove || null;
this._faData = null;
}
},
// Width of the border between panels
_borderWidth: 0,
// Data stored for a delayed add method (when onremove needs data on
// the current location)
_faData: null,
/* recalculate total, north and south height if component is too large
*/
_correctHeight: function(height) {
// north and south height
var n = this.north ? this.north.height() : 0;
var s = this.south ? this.south.height() : 0;
// total height including borders
var c = n + s + this.center.minHeight()
+ (this.north ? this._borderWidth : 0)
+ (this.south ? this._borderWidth : 0);
// recalculate
var r = this._correctSizeCalc(height, c, this.minHeight(), n, s,
this.north ? this.north.minHeight() : 0,
this.south ? this.south.minHeight() : 0,
this.north ? true : false, this.south ? true : false);
// and return total, north and south height
return {h: r.t, nh: r.nw, sh: r.se};
},
/* recalculate total, west and east width if component is too large
*/
_correctWidth: function(width) {
// west and east width
var w = this.west ? this.west.width() : 0;
var e = this.east ? this.east.width() : 0;
// total width including borders
var c = w + e + this.center.minWidth()
+ (this.west ? this._borderWidth : 0)
+ (this.east ? this._borderWidth : 0);
// recalculate
var r = this._correctSizeCalc(width, c, this.minWidth(), w, e,
this.west ? this.west.minWidth() : 0,
this.east ? this.east.minWidth() : 0,
this.west ? true : false, this.east ? true : false);
// and return total, west and east width
return {w: r.t, ew: r.nw, ww: r.se};
},
/* If total w/h (t) is smaller than min w/h (m) set total w/h as
* min w/h or if total w/h is smaller than current w/h (c) decrease
* west/north w/h (nw) and east/south w/h (se) with proper amount
* (mnw/mse: min nw/se, hnw/hse: has nw/se)
*/
_correctSizeCalc: function(t, c, m, nw, se, mnw, mse, hnw, hse) {
if (m > t) {
// if total height (or width) (t) is smaller than minimum height
// (or width) (m) then ...
// ... set north height (or west width) to set to minimum values
// if there is a north (or west) panel, ...
if (hnw) {
nw = mnw;
}
// ... set south height (or east width) to set to minimum values
// if there is a south (or east) panel, ...
if (hse) {
se = mse;
}
// ... and set the total height (or width) to the minimum value.
t = m;
} else if (c > t) {
// else if total height (or width) (t) is smaller than current
// height (or width) (c) then ...
// ... calculate the part that should be subtracted ...
var rest = (hnw && hse) ? Math.ceil((c-t)/2) : c-t;
// ... and if we have a north (or west) panel ...
if (hnw) {
// ... try to subtract it from that side ...
nw -= rest;
// ... but if it is too much and ...
if (nw < mnw) {
// ... there is an other side ...
if (hse) {
// ... try to get more from that other side ...
rest += (mnw - nw);
}
// ... and set the north height (or west width) to
// the minimum value
nw = mnw;
}
}
// ... and if we have a south (or east) panel ...
if (hse) {
// ... try to subtract it from that side ...
se -= rest;
// ... but if it is too much and ...
if (se < mse) {
// ... there is an other side ...
if (hnw) {
// ... try to get more from that other side ...
nw -= (mse - se);
// ... not not more than the maximum value ...
if (nw < mnw) {
nw = mnw;
}
}
// ... and set the south height (or east width) to
// the minimum value
se = mse;
}
}
}
// return the new center north (or west) and south (or east) size
return {t:t,nw:nw,se:se};
},
/* Get the mimimum size for the box by checking the minimal sizes of the
* client boxes
*/
_minsizeCalc: function() {
// get the minimal center width and height
this.minWidth(this.center.minWidth());
this.minHeight(this.center.minHeight());
// if there is a north box add its minimal height
if (this.north) {
this.minHeight(this.minHeight() + this.north.minHeight()
+ this._borderWidth);
}
// if there is a south box add its minimal height
if (this.south) {
this.minHeight(this.minHeight() + this.south.minHeight()
+ this._borderWidth);
}
// if there is a west box add its minimal width
if (this.west) {
this.minWidth(this.minWidth() + this.west.minWidth()
+ this._borderWidth);
}
// if there is a east box add its minimal width
if (this.east) {
this.minWidth(this.minWidth() + this.east.minWidth()
+ this._borderWidth);
}
},
/* Retrieve the minimum and maximum height and width settings of child
* boxes (can differ from the ones set in this component).
*/
_prepareLayout: function() {
// for all children ...
for (var i=0; i<this.children.length; i++) {
var c = this.children[i];
// ... set the parent's min/max width/height to the child's
// min/max width/height if appropriate
if (c.parent().minWidth() < c.minWidth()) {
c.parent().minWidth(c.minWidth());
}
if (c.parent().maxWidth() > c.maxWidth()) {
c.parent().maxWidth(c.maxWidth());
}
if (c.parent().minHeight() < c.minHeight()) {
c.parent().minHeight(c.minHeight());
}
if (c.parent().maxHeight() > c.maxHeight()) {
c.parent().maxHeight(c.maxHeight());
}
}
// return adjusted width/height if necessary
return {
w: this._correctWidth(this.width()),
h: this._correctHeight(this.height())
};
}
});