1 /*
  2  * Dual-licensed under the MIT License & the Academic Free License v. 2.1.
  3  * See the file LICENSE for more information.
  4  *
  5  * (c) 2007-2008 by Per Cederberg & Dynabyte AB. All rights reserved.
  6  */
  7 
  8 // Check for loaded MochiKit
  9 if (typeof(MochiKit) == "undefined") {
 10     throw new ReferenceError("MochiKit must be loaded before loading this script");
 11 }
 12 
 13 /**
 14  * @name MochiKit.Style
 15  * @namespace Provides a painless CSS manipulation API.
 16  */
 17 // Check for loaded MochiKit.Style
 18 if (typeof(MochiKit.Style) == "undefined") {
 19     throw new ReferenceError("MochiKit.Style must be loaded before loading this script");
 20 }
 21 
 22 /**
 23  * Returns the border widths for an HTML DOM node. The widths for
 24  * all four sides will be returned.
 25  *
 26  * @param {Object} node the HTML DOM node
 27  *
 28  * @return {Object} an object with "t", "b", "l" and "r" properties,
 29  *         each containing either an integer value or null
 30  */
 31 MochiKit.Style.getBorderBox = function(node) {
 32     var getStyle = MochiKit.Style.getStyle;
 33     var px = MochiKit.Style._toPixels;
 34     return { t: px(getStyle(node, "border-width-top")),
 35              b: px(getStyle(node, "border-width-bottom")),
 36              l: px(getStyle(node, "border-width-left")),
 37              r: px(getStyle(node, "border-width-right")) };
 38 }
 39 
 40 /**
 41  * Returns the padding sizes for an HTML DOM node. The sizes for all
 42  * four sides will be returned.
 43  *
 44  * @param {Object} node the HTML DOM node
 45  *
 46  * @return {Object} an object with "t", "b", "l" and "r" properties,
 47  *         each containing either an integer value or null
 48  */
 49 MochiKit.Style.getPaddingBox = function(node) {
 50     var getStyle = MochiKit.Style.getStyle;
 51     var px = MochiKit.Style._toPixels;
 52     return { t: px(getStyle(node, "padding-top")),
 53              b: px(getStyle(node, "padding-bottom")),
 54              l: px(getStyle(node, "padding-left")),
 55              r: px(getStyle(node, "padding-right")) };
 56 }
 57 
 58 /**
 59  * Converts a style pixel value to the corresponding integer. If the
 60  * string ends with "px", those characters will be silently removed.
 61  *
 62  * @param {String} value the style string value to convert
 63  *
 64  * @return {Number} the numeric value, or
 65  *         null if the conversion failed
 66  */
 67 MochiKit.Style._toPixels = function(value) {
 68     if (value != null) {
 69         try {
 70             value = MochiKit.Format.rstrip(value, "px");
 71             value = Math.round(parseFloat(value));
 72         } catch (ignore) {
 73             value = null;
 74         }
 75     }
 76     return (value == null || isNaN(value)) ? null : value;
 77 }
 78 
 79 /**
 80  * Returns the scroll offset for an HTML DOM node.
 81  *
 82  * @param {Object} node the HTML DOM node
 83  *
 84  * @return {Object} a MochiKit.Style.Coordinates object with "x" and
 85  *         "y" properties containing the element scroll offset
 86  */
 87 MochiKit.Style.getScrollOffset = function(node) {
 88     node = MochiKit.DOM.getElement(node);
 89     var x = node.scrollLeft || 0;
 90     var y = node.scrollTop || 0;
 91     return new MochiKit.Style.Coordinates(x, y);
 92 }
 93 
 94 /**
 95  * Sets the scroll offset for an HTML DOM node.
 96  *
 97  * @param {Object} node the HTML DOM node
 98  * @param {Object} offset the MochiKit.Style.Coordinates containing
 99  *            the new scroll offset "x" and "y" values
100  */
101 MochiKit.Style.setScrollOffset = function(node, offset) {
102     node = MochiKit.DOM.getElement(node);
103     node.scrollLeft = offset.x;
104     node.scrollTop = offset.y;
105 }
106 
107 /**
108  * Resets the scroll offsets to zero for for an HTML DOM node.
109  * Optionally all child node offsets can also be reset.
110  *
111  * @param {Object} node the HTML DOM node
112  * @param {Boolean} [recursive] the recursive flag, defaults to
113  *            false
114  */
115 MochiKit.Style.resetScrollOffset = function(node, recursive) {
116     node = MochiKit.DOM.getElement(node);
117     node.scrollLeft = 0;
118     node.scrollTop = 0;
119     if (recursive) {
120         node = node.firstChild;
121         while (node != null) {
122             if (node.nodeType === 1) { // Node.ELEMENT_NODE
123                 MochiKit.Style.resetScrollOffset(node, true);
124             }
125             node = node.nextSibling;
126         }
127     }
128 }
129 
130 /**
131  * Adjusts the scroll offsets for an HTML DOM node to ensure optimal
132  * visibility for the specified coordinates box. This function will
133  * scroll the node both vertially and horizontally to ensure that
134  * the top left corner of the box is always visible and that as much
135  * of the box extent as possible is visible.
136  *
137  * @param {Object} node the HTML DOM node
138  * @param {Object} box the coordinates box with optional properties
139  *            {l, t, r, b} or {x, y, w, h}  
140  */
141 MochiKit.Style.adjustScrollOffset = function(node, box) {
142     node = MochiKit.DOM.getElement(node);
143     var dim = MochiKit.Style.getElementDimensions(node);
144     var xMin = MochiKit.Base.defaultValue(box.l, box.x, NaN);
145     var xMax = MochiKit.Base.defaultValue(box.r, xMin + box.w, NaN);
146     var yMin = MochiKit.Base.defaultValue(box.t, box.y, NaN);
147     var yMax = MochiKit.Base.defaultValue(box.b, yMin + box.h, NaN);
148     if (!isNaN(xMax) && node.scrollLeft + dim.w < xMax) {
149         node.scrollLeft = xMax - dim.h;
150     }
151     if (!isNaN(xMin) && node.scrollLeft > xMin) {
152         node.scrollLeft = xMin;
153     }
154     if (!isNaN(yMax) && node.scrollTop + dim.h < yMax) {
155         node.scrollTop = yMax - dim.h;
156     }
157     if (!isNaN(yMin) && node.scrollTop > yMin) {
158         node.scrollTop = yMin;
159     }
160 }
161 
162 /**
163  * Registers algebraic constraints for an element width, height and
164  * aspect ratio. The constraints may either be fixed numeric values,
165  * functions or string formulas. The string formulas will be
166  * converted to JavaScript functions, replacing any "%" character
167  * with the parent reference value (i.e. the parent element width,
168  * height or aspect ratio). It is also possible to directly reference
169  * the parent values with the "w" and "h" variables. Any functions
170  * specified must take both the parent element width and height as
171  * arguments (possibly ignoring one or the other) and return a
172  * number. Any value returned when evaluating the functions or
173  * formulas will be bounded to the parent element size.
174  *
175  * @example // To set width to 50% - 20 px & height to 100%
176  * registerSizeConstraints(node, "50% - 20", "100%");
177  * // To set a square aspect ratio
178  * registerSizeConstraints(node, null, null, 1.0);
179  *
180  * @param {Object} node the HTML DOM node
181  * @param {Number/Function/String} [width] the width constraint
182  * @param {Number/Function/String} [height] the height constraint
183  * @param {Number/Function/String} [aspect] the aspect ratio constraint
184  */
185 MochiKit.Style.registerSizeConstraints = function(node, width, height, aspect) {
186     node = MochiKit.DOM.getElement(node);
187     var sc = node.sizeConstraints = { w: null, h: null, a: null };
188     if (typeof(width) == "number") {
189         sc.w = function(w, h) { return width; }
190     } else if (typeof(width) == "function") {
191         sc.w = width;
192     } else if (typeof(width) == "string") {
193         var code = "return " + width.replace(/%/g, "*0.01*w") + ";";
194         sc.w = new Function("w", "h", code);
195     }
196     if (typeof(height) == "number") {
197         sc.h = function(w, h) { return height; }
198     } else if (typeof(height) == "function") {
199         sc.h = height;
200     } else if (typeof(height) == "string") {
201         var code = "return " + height.replace(/%/g, "*0.01*h") + ";";
202         sc.h = new Function("w", "h", code);
203     }
204     if (typeof(aspect) == "number") {
205         sc.a = function(w, h) { return aspect; }
206     } else if (typeof(aspect) == "function") {
207         sc.a = aspect;
208     } else if (typeof(aspect) == "string") {
209         var code = "return " + aspect.replace(/%/g, "*0.01*w/h") + ";";
210         sc.a = new Function("w", "h", code);
211     }
212 }
213 
214 /**
215  * Resizes a list of DOM nodes using their parent element sizes and
216  * any registered size constraints. The resize operation is recursive
217  * and will also be applied to all child nodes. If an element lacks a
218  * size constraint for either width or height, that size aspect will
219  * not be modified.
220  *
221  * @param {Object} [...] the HTML DOM nodes to resize
222  *
223  * @see registerSizeConstraints
224  */
225 MochiKit.Style.resizeElements = function(/* ... */) {
226     var args = MochiKit.Base.flattenArray(arguments);
227     for (var i = 0; i < args.length; i++) {
228         var node = MochiKit.DOM.getElement(args[i]);
229         if (node != null && node.nodeType === 1 && // Node.ELEMENT_NODE
230             node.parentNode != null && node.sizeConstraints != null) {
231 
232             var ref = { w: node.parentNode.w, h: node.parentNode.h };
233             if (ref.w == null && ref.h == null) {
234                 ref = MochiKit.Style.getElementDimensions(node.parentNode, true);
235             }
236             var dim = MochiKit.Style._evalConstraints(node.sizeConstraints, ref);
237             MochiKit.Style.setElementDimensions(node, dim);
238             node.w = dim.w;
239             node.h = dim.h;
240         }
241         if (node != null && typeof(node.resizeContent) == "function") {
242             node.resizeContent();
243         } else {
244             node = node.firstChild;
245             while (node != null) {
246                 if (node.nodeType === 1) { // Node.ELEMENT_NODE
247                     MochiKit.Style.resizeElements(node);
248                 }
249                 node = node.nextSibling;
250             }
251         }
252     }
253 }
254 
255 /**
256  * Evaluates the size constraint functions with a refeence dimension
257  * object. This is an internal function used to encapsulate the
258  * function calls and provide logging on errors.
259  *
260  * @param {Object} sc the size constraints object
261  * @param {Object} ref the MochiKit.Style.Dimensions maximum
262  *            reference values
263  *
264  * @return {Object} the MochiKit.Style.Dimensions with evaluated size
265  *         constraint values (some may be null)
266  */
267 MochiKit.Style._evalConstraints = function(sc, ref) {
268     var log = MochiKit.Logging.logError;
269     if (typeof(sc.w) == "function") {
270         try {
271             var w = Math.max(0, Math.min(ref.w, sc.w(ref.w, ref.h)));
272         } catch (e) {
273             log("Error evaluating width size constraint; " +
274                 "w: " + ref.w + ", h: " + ref.h, e);
275         }
276     }
277     if (typeof(sc.h) == "function") {
278         try {
279             var h = Math.max(0, Math.min(ref.h, sc.h(ref.w, ref.h)));
280         } catch (e) {
281             log("Error evaluating height size constraint; " +
282                 "w: " + ref.w + ", h: " + ref.h, e);
283         }
284     }
285     if (typeof(sc.a) == "function") {
286         try {
287             var a = sc.a(ref.w, ref.h);
288             w = w || ref.w;
289             h = h || ref.h;
290             if (h * a > ref.w) {
291                 h = ref.w / a;
292             }
293             if (w / a > ref.h) {
294                 w = ref.h * a;
295             }
296             if (w > h * a) {
297                 w = h * a;
298             } else {
299                 h = w / a;
300             }
301         } catch (e) {
302             log("Error evaluating aspect size constraint; " +
303                 "w: " + ref.w + ", h: " + ref.h, e);
304         }
305     }
306     if (w != null) {
307         w = Math.floor(w);
308     }
309     if (h != null) {
310         h = Math.floor(h);
311     }
312     return new MochiKit.Style.Dimensions(w, h);
313 }
314