/* 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: Box.js 743 2013-07-18 10:12:39Z geert $ */ "use strict"; SUI.Box = SUI.defineClass( /** @lends SUI.Box.prototype */{ /** * @classdesc * <p>SUI.Box is the basic building block of the SUI library. SUI.Box are * objects that represent absolutely positioned div elements and implement * the functionality for the framework to size and display them as * required. SUI.Boxes are basically wrappers for HTML elements and the * method <i>el()</i> will return the box's element ({@link SUI.Box#el}). * </p> * <p>Because we like to work with absolute distances measured in pixels * you'll find the properties common to the style attributes of HTML * elements like top, width, etc. (re)defined as fields of the SUI.Box * class. However in the SUI.Box class no CSS lengths but only integer * values are supported to simplify layout arithmetic. * </p> * <p>To support the layout mechanism you'll find two methods: * <i>layOut</i> ({@link SUI.Box#layOut}) and <i>display</i> * ({@link SUI.Box#display}). These methods will be called when * rendering box objects on the browser's client area. The basic idea * is that the <i>layOut</i> method will calculate the size and position * of the box (or an object that inherits SUI.Box) and the <i>display</i> * method will actually set the CSS properties to the size and position * that was calculated by the <i>layOut</i> method during the lay out * phase. * </p> * <p>The reason to separate between these two operations is that it is * possible that layout operations interact: trying to set a box to * a certain width might be readjusted later in the layout process due * to a child box that has a larger minimum width set. We first want to * fully calculate the layout before starting the rendering process. It * is expected that this will result in a snappier GUI experience and * that's what we're after. * </p> * <p>Boxes can be anchored. This only has meaning if the box is a child * box of a {@link SUI.AnchorLayout}. When anchored a child box of * a container will set its size and position relative to the * container's sides for the given anchor. * </p> * <p>Suppose all sides (left, top, bottom, right) of a box are set to * 10 and the box is anchored to all sides of the container box * (<i>{left: true, right: true, top: true, bottom: true}</i>). * The result will be a box on the container's client area with a * 10 pixel margin, no matter how the parent container is (and will be) * sized, thus ignoring the box's width and height settings. Setting * the bottom anchor to false will keep the box 10 pixels from the top, * right and left sides of the container but the box height setting will * now be respected. * </p> * <p>SUI.Box objects also offer a way to a attach events. More complex * derivatives of SUI.Box will use these: f.i. an subclass of SUI.Box might * implement an 'onLoad' event because it loads its contents using an XHR * request. * </p> * * @summary * A SUI.Box represents a box structure and is the basic building block of * the SUI library. * * @description * Create a SUI.Box component. * * @constructs * * @param {Object} [arg.elemAttr] Additional (element) attributes for the * box's element given as an object containing name and value pairs. * @param {Object} [arg.anchor={left: true, top: true}] The anchors for * the box. * @param {SUI.Border} [arg.border=new SUI.Border()] the border (width) * of the box. Note: don't edit the border's members, you'll edit the * boxes prototype. Assign a new SUI.Border object instead. * @param {int} [arg.bottom=0] The bottom position of the box. * @param {int} [arg.height=0] The height of the box. * @param {int} [arg.id=auto generated] The id of the HTML element of * the box. * @param {int} [arg.left=0] The left position of the box. * @param {int} [arg.maxHeight=1000000] The maximum height of the box. * @param {int} [arg.minHeight=0] The minimal height of the box. * @param {int} [arg.maxWidth=100000] The maximal width of the box. * @param {int} [arg.minWidth=0] The minimal width of the box. * @param {SUI.Padding} [arg.padding=new SUI.Padding()] the padding of * the box. Note: don't edit the padding's members, you'll edit the * boxes prototype. Assign a new SUI.Padding object instead. * @param {SUI.Box} [arg.parent] the parent box of the box. * @param {int} [arg.right=0] The right position of the box. * @param {String} [arg.tag="DIV"] An alternative tag name for the box. * @param {int} [arg.top=0] The top position of the box. * @param {int} [arg.width=0] The width of the box. * * @exception {String} If the box was created without argument object. * Note: an empty argument object is allowed. */ initializer: function(arg) { // no argument object is an error in the SUI library if (!arg) { throw "creating a box without argument object"; } // anchors default to left and right this.anchor = {left: true, right: false, top: true, bottom: false}; // start with an empty bin this.bin = []; // create the box's element, allow for non-div elements too this._el = SUI.browser.createElement( arg.tag || null, arg.elemAttr || null); // set the element's id if (arg.id) { this._el.id = arg.id; } // set properties from arguments if (arg.anchor) { this.anchor = arg.anchor; } if (arg.bottom) { this.bottom(arg.bottom); } if (arg.left) { this.left(arg.left); } if (arg.right) { this.right(arg.right); } if (arg.top) { this.top(arg.top); } if (arg.maxHeight) { this.maxHeight(arg.maxHeight); } if (arg.minHeight) { this.minHeight(arg.minHeight); } if (arg.minWidth) { this.minWidth(arg.minWidth); } if (arg.maxWidth) { this.maxWidth(arg.maxWidth); } if (arg.border) { this.border(arg.border); } if (arg.padding) { this.padding(arg.padding); } // note: set height and weight after max/min setting if (arg.height) { this.height(arg.height); } if (arg.width) { this.width(arg.width); } // note: set parent after element node was created if (arg.parent) { this.parent(arg.parent); } }, /** * Get the absolute position (top/left) of this box on the page. * Note: You can only use this function if the HTML content of the page * is fully rendered. */ absPos: function() { var l = 0, t = 0, x = 0; var el = this._el; // Note that we can use computed style because offsetLeft and // offsetRight are computed members themselves are computed members // themselves if (el.offsetParent) { do { l += el.offsetLeft; x = parseInt( SUI.browser.currentStyle(el, "borderLeftWidth"), 10); l += (isNaN(x) ? 0 : x); t += el.offsetTop; x = parseInt( SUI.browser.currentStyle(el, "borderTopWidth"), 10); t += (isNaN(x) ? 0 : x); } while (null !== (el = el.offsetParent)); } return {l:l,t:t}; }, /** * Add a CSS class name to the class list of the HTML element associated * with the box. * @param {String} cls the CSS class name to add */ addClass: function(cls) { SUI.style.addClass(this._el, cls); }, /** * Add/register a listener function. This way it is possible to register * more than one listener function on one target. * @param {String} type The listener type (i.e. "onClick", "onOK", etc). * @param {Function} listener The listener function. */ addListener: function(type, listener) { // if the listeners object is not initialized the do it now if (!this.listeners) { this.listeners = {}; } // if there are no listeners for this type create the listeners array if (!this.listeners[type]) { this.listeners[type] = []; } // add the listener this.listeners[type].push(listener); }, /** * Get or set the border definition of the box. * @param {SUI.Border} [b] The new border definition (or none to use * this method as a getter). * @return {SUI.Border} the border definition of the box (or null if this * method was used as a setter). */ border: function(b) { if (b !== undefined && typeof b === "string") { throw "Convert to type Border please"; } return b !== undefined ? (this._border = b) && null : this._border; }, /** * Call a listener function. Execute the default and additional listener * functions. Note: the framework should not execute the default listeners * directly but always through this method to ensure the execution of * additional listener functions. * @param {String} type The listener type (i.e. "onClick", "onOK", etc). */ callListener: function(type) { // get the arguments for the listener: current arguments minus 'type' var p = [].slice.call(arguments, 1); // execute the default listener if any if (this[type]) { this[type].apply(this, p); } // check the additional listeners ... if (this.listeners) { if (this.listeners[type]) { if (this.listeners[type].length) { // ... and if there are any, execute them for (var i=0; i<this.listeners[type].length; i++) { this.listeners[type][i].apply(this, p); } } } } }, /** * Get or set the bottom of the box. * @param {int} [b] The new bottom of the box (or none to use this * method as a getter). * @return {int} The bottom of the box (or null if this method was used as * a setter). */ bottom: function(b) { return b !== undefined ? (this._bottom = b) && null : this._bottom; }, /** * Get the client height of the box. The client height is the height of * the box minus the top and bottom border and padding width. * @return {int} The client height of the box. */ clientHeight: function() { return this.height() - (this._border ? this._border.height : 0) - (this._padding ? this._padding.height : 0); }, /** * Get the client width of the box. The client width is the width of * the box minus the left and right border and padding width. * @return {int} The client width of the box. */ clientWidth: function() { return this.width() - (this._border ? this._border.width : 0) - (this._padding ? this._padding.width : 0); }, /** * Display the box. Set the CSS positions of the element's box(es) and * of the children of the box. */ display: function() { if (this.width() > 0 && this.height() > 0) { this.setDim(); } }, /** * Draw the box on the screen. It executes a two phase process: a layout * phase in which the size and positions of the box (and of it's contents, * for more complex derivatives) is calculated and a display phase in * which the CSS size and position of the box's (and possible it's * child elements) is set. */ draw: function() { this.layOut(); this.display(); }, /** * Get the HTML element node of the box. * @return {HTMLElementNode} the HTML element node of the box. */ el: function() { return this._el; }, /** * Get or set the height of the box. * @param {int} [h] The new height of the box (or none to use this * method as a getter). * @return {int} The height of the box (or null if this method was used as * a setter). */ height: function(h) { if (h !== undefined) { h = h < this._minHeight ? this._minHeight : h > this._maxHeight ? this._maxHeight : h; // TODO: checking for errors if (isNaN(h)) { console.log("height: NaN"); } if (h === undefined) { console.log("height: undefined"); } this._height = h; return null; } else { return this._height; } }, /** * Lay out the box. Calculate the position of the box and its contents. The * layOut function of a simple box does nothing, but it's important for * more complex objects extended from SUI.Box. The layOut mechanism will * set the size and position of the child boxes of the box based on the * on the available space and within the restrictions of the box's minimum * and maximum width and height. So the job of the overridden layOut method * is to recalculate the size and position of all the child elements of * the box when the layout manager sets the size of the box and calls the * box's layOut method. */ layOut: function() { }, /** * Get or set the left of the box. * @param {int} [l] The new left of the box (or none to use this * method as a getter). * @return {int} The left of the box (or null if this method was used as * a setter). */ left: function(l) { return l !== undefined ? (this._left = l) && null : this._left; }, /** * Get or set the maximum height of the box. * @param {int} [mh] The new maximum height of the box (or none to use * this method as a getter). * @return {int} The maximum height of the box (or null if this method was * used as a setter). */ maxHeight: function(mh) { if (mh !== undefined) { this._maxHeight = mh; if (this._height > this._maxHeight) { this._height = this._maxHeight; } return null; } else { return this._maxHeight; } }, /** * Get or set the maximum width of the box. * @param {int} [mw] The new maximum width of the box (or none to use * this method as a getter). * @return {int} The maximum width of the box (or null if this method was * used as a setter). */ maxWidth: function(mw) { if (mw !== undefined) { this._maxWidth = mw; if (this._width > this._maxWidth) { this._width = this._maxWidth; } return null; } else { return this._maxWidth; } }, /** * Get or set the minimum height of the box. * @param {int} [mh] The new minimum height of the box (or none to use * this method as a getter). * @return {int} The minimum height of the box (or null if this method was * used as a setter). */ minHeight: function(mh) { if (mh !== undefined) { this._minHeight = mh; if (this._height < this._minHeight) { this._height = this._minHeight; } return null; } else { return this._minHeight; } }, /** * Get or set the minimum width of the box. * @param {int} [mw] The new minimum width of the box (or none to use * this method as a getter). * @return {int} The minimum width of the box (or null if this method was * used as a setter). */ minWidth: function(mw) { if (mw !== undefined) { this._minWidth = mw; if (this._width < this._minWidth) { this._width = this._minWidth; } return null; } else { return this._minWidth; } }, /** * Get or set the padding definition of the box. * @param {SUI.Padding} [p] The new padding definition (or none to use * this method as a getter). * @return {SUI.Padding} The padding definition of the box (or null if * this method was used as a setter). */ padding: function(p) { if (p !== undefined) { if (typeof p === "string") { throw "Convert to type Padding please"; } this._padding = p; return null; } else { return this._padding; } }, /** * Get or set the parent box of the box. When setting the parent the box's * element node will be appended to the parent box's HTML element node. * @param {SUI.Box} p The parent box for this box (or none to use * this method as a getter). * @return {SUI.Box} The parent box of the box (or null if this method was * used as a setter). */ parent: function(p) { if (p !== undefined) { this._parent = p; this._parent.el().appendChild(this._el); return null; } else { return this._parent; } }, /** * Remove a box from the DOM tree. */ removeBox: function() { this._parent.el().removeChild(this._el); }, /** * Remove a CSS class name from the class list of the HTML element * associated with the box. * @param {String} cls The CSS class name to remove. */ removeClass: function(cls) { SUI.style.removeClass(this._el, cls); }, /** * Get or set the right of the box. * @param {int} r The new right of the box (or none to use this * method as a getter). * @return {int} The right of the box (or null if this method was used * as a setter). */ right: function(r) { return r !== undefined ? (this._right = r) && null : this._right; }, /** * Set the CSS dimensions of this box and it's borders and padding. This is * typically used in display functions to display boxes at the size and * position that was calculated during layout. */ setDim: function() { this.setPos(); // get the control's client width var cw = this.clientWidth(); // TODO: cw is sometimes null: in what case? // Answer: Select list on the link dialog had no initial width // Do or don't we want that? if (isNaN(cw)) { console.log(cw); cw = 0; } // if it is negative try to sacrifice some of the padding if (cw <0) { this._padding.growW(cw); cw = 0; } // set the CSS width to the client width this._el.style.width = cw+"px"; // get the control's client height var ch = this.clientHeight(); // if it is negative try to sacrifice some of the padding if (ch <0) { this._padding.growH(ch); ch = 0; } // set the CSS height to the client height this._el.style.height = ch+"px"; // set the CSS border widths if (this._border) { this._border.set(this._el); } // set the CSS padding widths if (this._padding) { this._padding.set(this._el); } }, /** * Set the CSS postion of this box. This is typically used in display * functions to display boxes at position that was calculated during * layout. Note: use setDim if you want to set the position and size * of the box. */ setPos: function() { // set the top and left position of the box var p = this.parent() instanceof SUI.Box && this.parent().padding(); this._el.style.top = ((p ? p.top : 0) + this.top())+"px"; this._el.style.left = ((p ? p.left : 0) + this.left())+"px"; //this._el.style.top = this.top()+"px"; //this._el.style.left = this.left()+"px"; }, /** * Set the top, left, width and height of a box in one go. * @param {int|SUI.Box} t The new top of the box or another * reference box to copy the values from. * @param {int} [l] The new left of the box (if the t parameter wasn't a * reference Box). * @param {int} [w] The new width of the box (if the t parameter wasn't a * reference Box). * @param {int} [h] The new length of the box (if the t parameter wasn't a * reference Box). */ setRect: function(t,l,w,h) { if (1 === arguments.length) { this.top(t.top()); this.left(t.left()); this.width(t.width()); this.height(t.height()); } else { this.top(parseInt(t, 10)); this.left(parseInt(l, 10)); this.width(parseInt(w, 10)); this.height(parseInt(h, 10)); } }, /** * Get or set the top of the box. * @param {int} [t] The new top of the box (or none to use this method * as a getter). * @return {int} The top of the box (or null if this method was used as * a setter). */ top: function(t) { return t !== undefined ? (this._top = t) && null : this._top; }, /** * Get or set the width of the box. * @param {int} [w] The new width of the box (or none to use this method * as a getter). * @return {int} The width of the box (or null if this method was used as * a setter). */ width: function(w) { if (w !== undefined) { w = w < this._minWidth ? this._minWidth : w > this._maxWidth ? this._maxWidth : w; // TODO: checking for errors if (isNaN(w)) { console.log("width: NaN"); } this._width = w; return null; } else { return this._width; } }, /** * The box's border. * @type SUI.Border * @private */ _border: new SUI.Border(), /** * The bottom position of the box. * @type int * @private */ _bottom: 0, /** * The box's element node. * @type HTMLElementNode * @private */ _el: null, /** * The height of the box. * @type int * @private */ _height: 0, /** * The left position of the box. * @type int * @private */ _left: 0, /** * The listeners array. * @type int * @private */ _listeners: null, /** * The maximum height of the box. * @type int * @private */ _maxHeight: 1000000, /** * The maximum width of the box. * @type int * @private */ _maxWidth: 100000, /** * The minimum height of the box. * @type int * @private */ _minHeight: 0, /** * The minimum width of the box. * @type int * @private */ _minWidth: 0, /** * The box's padding. * @type SUI.Padding * @private */ _padding: new SUI.Padding(), /** * The box's parent element. * @type SUI.Box * @private */ _parent: null, /** * The right position of the box. * @type int * @private */ _right: 0, /** * The top position of the box. * @type int * @private */ _top: 0, /** * The width of the box. * @type int * @private */ _width: 0 });