Suggested Extensions to MochiKit.Style

Below follows a few suggested extensions to the MochiKit.Style namespace. Please discuss bugs or issues with these on the mailing list and feel free to add any additional code snipplets here.

Functions for Padding & Border Size

See #84 for discussion. These are rough drafts of the suggested functions. Please report issues either on the mailing list or in the linked ticket.

/**
 * Returns the border widths for an HTML DOM node. The widths for
 * all four sides will be returned.
 *
 * @param {Object} node the HTML DOM node
 *
 * @return {Object} an object with "t", "b", "l" and "r" properties,
 *         each containing either an integer value or null
 */
MochiKit.Style.getBorderBox = function(node) {
    var getStyle = MochiKit.Style.getStyle;
    var px = MochiKit.Style._toPixels;
    return { t: px(getStyle(node, "border-width-top")),
             b: px(getStyle(node, "border-width-bottom")),
             l: px(getStyle(node, "border-width-left")),
             r: px(getStyle(node, "border-width-right")) };
}

/**
 * Returns the padding sizes for an HTML DOM node. The sizes for all
 * four sides will be returned.
 *
 * @param {Object} node the HTML DOM node
 *
 * @return {Object} an object with "t", "b", "l" and "r" properties,
 *         each containing either an integer value or null
 */
MochiKit.Style.getPaddingBox = function(node) {
    var getStyle = MochiKit.Style.getStyle;
    var px = MochiKit.Style._toPixels;
    return { t: px(getStyle(node, "padding-top")),
             b: px(getStyle(node, "padding-bottom")),
             l: px(getStyle(node, "padding-left")),
             r: px(getStyle(node, "padding-right")) };
}

/**
 * Converts a style pixel value to the corresponding integer. If the
 * string ends with "px", those characters will be silently removed.
 *
 * @param {String} value the style string value to convert
 *
 * @return {Number} the numeric value, or
 *         null if the conversion failed
 */
MochiKit.Style._toPixels = function(value) {
    if (value != null) {
        try {
            value = MochiKit.Format.rstrip(value, "px");
            value = Math.round(parseFloat(value));
        } catch (ignore) {
            value = null;
        }
    }
    return (value == null || isNaN(value)) ? null : value;
}

Functions for Scrolling

Below are a few draft functions for manipulating the scrolling of HTML elements. Please report any issues found on the mailing list.

/**
 * Returns the scroll offset for an HTML DOM node.
 *
 * @param {Object} node the HTML DOM node
 *
 * @return {Object} a MochiKit.Style.Coordinates object with "x" and
 *         "y" properties containing the element scroll offset
 */
MochiKit.Style.getScrollOffset = function(node) {
    node = MochiKit.DOM.getElement(node);
    var x = node.scrollLeft || 0;
    var y = node.scrollTop || 0;
    return new MochiKit.Style.Coordinates(x, y);
}

/**
 * Sets the scroll offset for an HTML DOM node.
 *
 * @param {Object} node the HTML DOM node
 * @param {Object} offset the MochiKit.Style.Coordinates containing
 *            the new scroll offset "x" and "y" values
 */
MochiKit.Style.setScrollOffset = function(node, offset) {
    node = MochiKit.DOM.getElement(node);
    node.scrollLeft = offset.x;
    node.scrollTop = offset.y;
}

/**
 * Resets the scroll offsets to zero for for an HTML DOM node.
 * Optionally all child node offsets can also be reset.
 *
 * @param {Object} node the HTML DOM node
 * @param {Boolean} [recursive] the recursive flag, defaults to
 *            false
 */
MochiKit.Style.resetScrollOffset = function(node, recursive) {
    node = MochiKit.DOM.getElement(node);
    node.scrollLeft = 0;
    node.scrollTop = 0;
    if (recursive) {
        node = node.firstChild;
        while (node != null) {
            if (node.nodeType === 1) { // Node.ELEMENT_NODE
                MochiKit.Style.resetScrollOffset(node, true);
            }
            node = node.nextSibling;
        }
    }
}

/**
 * Adjusts the scroll offsets for an HTML DOM node to ensure optimal
 * visibility for the specified coordinates box. This function will
 * scroll the node both vertially and horizontally to ensure that
 * the top left corner of the box is always visible and that as much
 * of the box extent as possible is visible.
 *
 * @param {Object} node the HTML DOM node
 * @param {Object} box the coordinates box with optional properties
 *            {l, t, r, b} or {x, y, w, h}  
 */
MochiKit.Style.adjustScrollOffset = function(node, box) {
    node = MochiKit.DOM.getElement(node);
    var dim = MochiKit.Style.getElementDimensions(node);
    var xMin = box.l || box.x || NaN;
    var xMax = box.r || xMin + box.w || NaN;
    var yMin = box.t || box.y || NaN;
    var yMax = box.b || yMin + box.h || NaN;
    if (!isNaN(xMax) && node.scrollLeft + dim.w < xMax) {
        node.scrollLeft = xMax - dim.h;
    }
    if (!isNaN(xMin) && node.scrollLeft > xMin) {
        node.scrollLeft = xMin;
    }
    if (!isNaN(yMax) && node.scrollTop + dim.h < yMax) {
        node.scrollTop = yMax - dim.h;
    }
    if (!isNaN(yMin) && node.scrollTop > yMin) {
        node.scrollTop = yMin;
    }
}

Functions for Dynamic Resize

Standard CSS styles are normally sufficient for web page or simple application layouts. But when using complex layout in web applications, a few CSS layout issues become visible. One such issue is related to dynamic resizing of elements, where width and height values often render incorrectly when specified as relative values (with %).

Below follows a few functions that attempts to provide a JavaScript solution for dynamic resize of HTML elements. They allow simple arithmetic to be used to declare the element width and/or height, using the standard JavaScript eval() mechanism for evaluating their numeric values. References to the parent element width or height is also possible by using a % character.

Please report any issues found on the mailing list.

Example HTML:

<div id="parent" style="width: 100%; height: 200px; background: blue;">
  <div id="child" style="background: yellow; border: 1px solid red;">
    Text text text text text text text text text text text text text text text text text
    text text text text text text text text text text text text text text text text text
  </div>
</div>

Example JavaScript:

registerSizeConstraints('child', "50% + 22", "100%");
resizeElements('parent');

Source Code:

/**
 * Registers algebraic constraints (formulas) for element width and
 * height. The constraint strings specified will be converted to
 * JavaScript functions, replacing any "%" character with a scaled
 * reference to the maximum value (i.e. the parent element size).
 * The result from the expressions will be bounded by  the parent
 * element size.
 *
 * @example registerSizeConstraints(node, "50% - 20", "100%");
 *
 * @param {Object} node the HTML DOM node
 * @param {String} [width] the element width constraint formula
 * @param {String} [height] the element height constraint formula
 */
MochiKit.Style.registerSizeConstraints = function(node, width, height) {
    node = MochiKit.DOM.getElement(node);
    if (width != null) {
        var code = "return Math.round(Math.max(0, Math.min(max, " +
                   width.replace(/%/g, "*0.01*max") + ")));";
        width = new Function("max", code);
    }
    if (height != null) {
        var code = "return Math.round(Math.max(0, Math.min(max, " +
                   height.replace(/%/g, "*0.01*max") + ")));";
        height = new Function("max", code);
    }
    node.sizeConstraints = { w: width, h: height };
}

/**
 * Resizes a list of DOM nodes using their parent element sizes and
 * any registered size constraints. The resize operation is recursive
 * and will also be applied to all child nodes. If an element lacks a
 * size constraint for either width or height, that size aspect will
 * not be modified.
 *
 * @param {Object} [...] the HTML DOM nodes to resize
 *
 * @see registerSizeConstraints
 */
MochiKit.Style.resizeElements = function(/* ... */) {
    var args = MochiKit.Base.flattenArray(arguments);
    for (var i = 0; i < args.length; i++) {
        var node = MochiKit.DOM.getElement(args[i]);
        if (node != null && node.nodeType === 1 && // Node.ELEMENT_NODE
            node.parentNode != null && node.sizeConstraints != null) {

            var ref = MochiKit.Style.getElementDimensions(node.parentNode);
            var dim = MochiKit.Style._evalConstraints(node.sizeConstraints, ref);
            MochiKit.Style.setElementDimensions(node, dim);
            node.w = dim.w;
            node.h = dim.h;
        }
        if (node != null && typeof(node.resizeContent) == "function") {
            node.resizeContent();
        } else {
            node = node.firstChild;
            while (node != null) {
                if (node.nodeType === 1) { // Node.ELEMENT_NODE
                    MochiKit.Style.resizeElements(node);
                }
                node = node.nextSibling;
            }
        }
    }
}

/**
 * Evaluates the size constraint functions with a refeence dimension
 * object. This is an internal function used to encapsulate the
 * function calls and provide logging on errors.
 *
 * @param {Object} size the size constraints object
 * @param {Object} ref the MochiKit.Style.Dimensions maximum
 *            reference values
 *
 * @return {Object} the MochiKit.Style.Dimensions with evaluated size
 *         constraint values (some may be null)
 *
 * @private
 */
MochiKit.Style._evalConstraints = function(size, ref) {
    var log = MochiKit.Logging.logError;
    if (typeof(size.w) == "function") {
        try {
            var w = size.w(ref.w);
        } catch (e) {
            log("Error evaluating width size constraint", "max: " + ref.w, e);
        }
    }
    if (typeof(size.h) == "function") {
        try {
            var h = size.h(ref.h);
        } catch (e) {
            log("Error evaluating height size constraint", "max: " + ref.h, e);
        }
    }
    return new MochiKit.Style.Dimensions(w, h);
}