/******************************************************************************
    DHtml.js
    Copyright (c) 2006-2008 Motorola, Inc.
    All rights reserved
******************************************************************************/
$package("netopia/dhtml.html");

$requires("netopia/textutils.html");
$requires("netopia/browser.html");

netopia.create("dhtml").add({

/**
Applies properties from a data object to the given element. These properties are:

  - html : The HTML to set via innerHTML.
  - text : The text to add as a createTextNode.
  - styles : A set of styles (dhtml.setStyles) for the element.

All other properties are set verbatim to the element.
*/
applyToElement : function (el, data)
{
    el = $(el);

    for (var n in data)
    {
        var v = data[n];

        if (n == "html")
            el.innerHTML = v;
        else if (n == "text")
            netopia.dhtml.setText(el, v);
        else if (n == "styles")
            netopia.dhtml.setStyles(el, v);
        else
            el[n] = v;
    }
},

applyToElementTree : function (root, prefix, data)
{
    var el = $(root), id = el.id;
    if (id && el.nodeType == 1) // if (element w/id)
    {
        if (prefix)
            el.id = prefix + id;
        var d = data ? (data[id] || null) : null; // for FF break on all errors
        if (d)
            netopia.dhtml.applyToElement(el, d);
    }

    for (var c = el.firstChild; c; c = c.nextSibling)
        arguments.callee(c, prefix, data);  // recursive
},

elementInClass : function (element, classAttr)
{
    if (element.className)
    {
        var attributes = element.className.split(" ");
        for (var i = 0; i < attributes.length; i++)
            if (attributes[i] == classAttr)
                return true;
    }

    return false;
},

// fixIEAlphaPngs was removed due to the layout damage it causes... It exists
// in the source control history of this file.

getElementPos : function ()
{
    if (!netopia.dhtml.browser.khtml)
        return function (el)
        {
            var p, r = { x:0, y:0, r:0, b:0 };

            for (var o = $(el); o; o = p)
            {
                r.x += o.offsetLeft || 0;
                r.y += o.offsetTop || 0;

                p = o.offsetParent;
                if (p)
                {
                    r.r += (p.offsetWidth  - o.offsetLeft - o.offsetWidth) || 0;
                    r.b += (p.offsetHeight - o.offsetTop  - o.offsetHeight) || 0;
                }
            }

            return r;
        };

    // Safari returns margins on body which is incorrect if the child
    // is absolutely positioned (from Prototype).
    return function (el)
    {
        var p, r = { x:0, y:0, r:0, b:0 };

        for (var o = $(el); o; o = p)
        {
            r.x += o.offsetLeft || 0;
            r.y += o.offsetTop || 0;

            p = o.offsetParent;
            if (p) 
            {
                r.r += (p.offsetWidth  - o.offsetLeft - o.offsetWidth) || 0;
                r.b += (p.offsetHeight - o.offsetTop  - o.offsetHeight) || 0;

                if (p == document.body)
                {
                    var position = netopia.dhtml.getStyle(o, "position");
                    if (position == "absolute")
                        break;
                }
            }
        }

        return r;
    };
}(), // <-- notice the call

getElementRect : function (el)
{
    el = $(el);
    var pos = netopia.dhtml.getElementPos(el);
    var ret = new netopia.core.Rect(pos.x, pos.y, el.offsetWidth, el.offsetHeight);
    ret.r = pos.r;
    ret.b = pos.b;

    return ret;
},

getElementSize : function (el)
{
    el = $(el);
    var display = netopia.dhtml.getStyle(el, "display");
    // According to Prototype, Safari can return null (equivalent to none)
    if (display != "none" && display != null)
        return new netopia.core.Dim(el.offsetWidth, el.offsetHeight);

    // All *Width and *Height properties give 0 on elements with display none,
    // so enable the element temporarily (borrowed from Prototype):
    var prev = netopia.dhtml.setStyles(el,
                  { visibility : "hidden", position : "absolute", display : "block" });
    var ret = new netopia.core.Dim(el.clientWidth, el.clientHeight);

    netopia.dhtml.setStyles(el, prev);

    return ret;
},

/**
Returns an array of all matching elements anchored by root with a given tagName.
The matcher function will be passed the className of all candidate elements and
must return true to include the element.

An easy way to provide a matcher function is to use matchAnyWords or matchAllWords
like so:

    // Find all divs with "foo" or "bar" class names:
    //
    var elems = netopia.dhtml.getElementsMatching(
                    netopia.text.matchAnyWords.xbind([["foo", "bar"]]),
                    "div");

    // Find all elements with "foo" and "bar" class names:
    //
    var elems = netopia.dhtml.getElementsMatching(
                    netopia.text.matchAllWords.xbind([["foo", "bar"]]));

    // Find all elements contained by "jazz" with "foo" or "bar" class names:
    //
    var elems = netopia.dhtml.getElementsMatching(
                    netopia.text.matchAnyWords.xbind([["foo", "bar"]]),
                    "div", "jazz");
*/
getElementsMatching : function (matcher, tags, root)
{
    var tag = tags || "*";
    var base = root ? $(root) : document;
    var elements = (tag=="*" && base.all) ? base.all : base.getElementsByTagName(tag);
    var length = elements.length;
    var ret = [ ];

    for (var i = 0; i < length; ++i)
    {
        var el = elements[i];
        if (matcher(el.className))
            ret.push(el);
    }

    return ret;
},

/**
Returns the first discovered descendent of 'container' (in-order traversal)
which has a class attribute that includes the member 'selector'.
   
Returns null when no match is found. 
*/
getFirstDescendantByClass : function (container, selector)
{
    var children = container.childNodes;
    for (var index = 0; index < children.length; index++)
    {	
        var child = children[index];
        if (netopia.dhtml.elementInClass(child, selector))
            return child;
        if (child.childNodes)
        {
            var subResult = netopia.dhtml.getFirstDescendantByClass(child, selector);
            if (subResult)
                return subResult;
        }
    }

    return null;
},

getScrollXY : function ()
{
    if (typeof(window.pageYOffset) == "number") // Netscape compliant
        return { x: window.pageXOffset, y: window.pageYOffset };

    var docs = [ document.documentElement, document.body ];

    for (x in docs)
    {
        var doc = docs[x];
        if (doc && (doc.scrollLeft || doc.scrollTop))
            return { x: doc.scrollLeft, y: doc.scrollTop };
    }

    return { x:0, y:0 };
},

getStyle : function (el, prop)
{
    el = $(el);
    if (prop == "float" || prop == "cssFloat")
        prop = (typeof(el.style.styleFloat) == "undefined") ? "cssFloat" : "styleFloat";
    prop = prop.camelize();
    var value = el.style[prop];
    if (!value)
    {
        if (document.defaultView && document.defaultView.getComputedStyle) // W3C
        {
            var style = document.defaultView.getComputedStyle(el, null);
            value = style ? style[prop] : null;
        }
        else if (el.currentStyle) // IE
            value = el.currentStyle[prop];
    }
    if (prop == "opacity")
    {
        if (value)
            return parseFloat(value);
        value = netopia.dhtml.getStyle(el, "filter");
        if (value)
        {
            if (!netopia.dhtml.kAlphaFilterRegEx)
                netopia.dhtml.kAlphaFilterRegEx = /alpha\(opacity=(.*)\)/;
            value = value.match(netopia.dhtml.kAlphaFilterRegEx);
            if (value && value[1])
                return parseFloat(value[1]) / 100;
        }
        return 1.0;
    }

    return (value == "auto") ? null : value;
},

getStyles : function (el, props)
{
    el = $(el);
    var ret = {}, k = 0;
    if (!(props instanceof Array))
        { props = arguments; k = 1; }
    for (var i = props.length; i-- > k; )
        ret[props[i]] = ret[i - k] = netopia.dhtml.getStyle(el, props[i]);
    return ret;
},

getZIndex : function (el)
{
    var z = netopia.dhtml.getStyle(el, "zIndex");
    z = parseInt(z);
    return (z+"" == "NaN") ? 0 : z;
},

getTopZIndex : function (parent)
{
    var z = 1;
    for (var c = parent.childNode; c; c = c.nextSibling)
    {
        var st = netopia.dhtml.getStyles(c, "visibility", "display");
        if (st[0] == "hidden" || st[1] == "none" || st[1] == null)
            continue;
        var cz = netopia.dhtml.getZIndex(c);
        z = Math.max(z, cz);
    }
    return z;
},

isParentOf : function (a, b)
{
    try
    {
        for (var p = b; p; p = p.parentNode)
            if (p == a)
                return true;
    }
    catch (e) { } // in FF we can get AccessDenied walking up the tree...

    return false;
},

CoordPair : $extends (netopia.core.Object,
{
    ctor : function (rel)
    {
        $super(arguments).call(this, rel);
        rel = rel || "LT";

        if (rel instanceof netopia.dhtml.CoordPair)
        {
            this.horz = rel.horz;
            this.vert = rel.vert;
        }
        else
        {
            var c1 = rel.charAt(0).toUpperCase();
            var c2 = rel.charAt(1).toUpperCase();

            this.horz = (c1 == "R" || c2 == "R") ? "right" : "left";
            this.vert = (c1 == "B" || c2 == "B") ? "bottom" : "top";
        }
    },

    getCoords : function (x, y)
    {
        var c = {};
        c[this.horz] = x;
        c[this.vert] = y;
        return c;
    },

    isLeft   : function () { return this.horz == "left"; },
    isRight  : function () { return this.horz == "right"; },
    isBottom : function () { return this.vert == "bottom"; },
    isTop    : function () { return this.vert == "top"; }
}),

makeSize : function (v)
{
    var r = v + "px";
    return (r.indexOf("%") == -1) ? r : v;
},

minMaxSize : function (el, prop, min, max, fixers)
{
    var t = el || window.document.documentElement;
    var t2 = el || window.document.body;
    var x = t[prop];
    for (var i = 0; i < fixers.length; ++i)
        x -= parseInt(netopia.dhtml.getStyle(t2, fixers[i]));
    if (min > 0 && x < min+2)
        return min + "px";
    if (max > 0 && x > max+2)
        return max + "px";
    return "auto";
},

minMaxHeight : function (el, min, max)
{
    return netopia.dhtml.minMaxSize(el, "clientHeight", min, max,
                                    ["margin-top", "margin-bottom"]);
},

minMaxWidth : function (el, min, max)
{
    return netopia.dhtml.minMaxSize(el, "clientWidth", min, max,
                                    ["margin-left", "margin-right"]);
},

maxHeight : function (el, max) { return netopia.dhtml.minMaxHeight(el,0,max); },
maxWidth  : function (el, max) { return netopia.dhtml.minMaxWidth(el,0,max); },
minHeight : function (el, min) { return netopia.dhtml.minMaxHeight(el,min,0); },
minWidth  : function (el, min) { return netopia.dhtml.minMaxWidth(el,min,0); },

moveAbove : function (el, ref)
{
    ref = $(ref);
    if (!ref.nextSibling)
        netopia.dhtml.moveToTop(el);
    else
        netopia.dhtml.moveBelow(el, ref.nextSibling);
},

moveBelow : function (el, ref)
{
    el = $(el); ref = $(ref);
    var par = el.parentNode;
    par.removeChild(el);
    par.insertBefore(el, ref);
},

moveToTop : function (el)
{
    el = $(el);
    var par = el.parentNode;
    par.removeChild(el);
    par.appendChild(el);
},

newElement : function (tag, id, props)
{
    var el = document.createElement(tag);
    if (id)
        el.id = id;
    if (props)
    {
        var styles = props.style; // these are special...
        if (styles)
            delete props.style;
        netopia.core.copyProps(el, props);
        if (styles)
        {
            props.style = styles;
            netopia.dhtml.setStyles(el, styles);
        }
    }

    return el;
},

newChild : function (parent, tag, id, props)
{
    var el = netopia.dhtml.newElement(tag, id, props);
    $(parent).appendChild(el);
    return el;
},

newParent : function (child, tag, id, props)
{
    var el = netopia.dhtml.newElement(tag, id, props);
    if (child)
    {
        if (typeof(child) == "string")
            el.innerHTML = child;
        else
            el.appendChild(child);
    }
    return el;
},

newDiv : function (id, props)
{
    return netopia.dhtml.newElement("div", id, props);
},

newChildDiv : function (parent, id, props)
{
    return netopia.dhtml.newChild(parent, "div", id, props);
},

newParentDiv : function (child, id, props)
{
    return netopia.dhtml.newParent(child, "div", id, props);
},

// Function objects stored in this array will be called by removeElement et.al..
_removeElementSinks : [ ],

removeElement : function (elem)
{
    var el = $(elem);
    if (!el || !el.parentNode)
        return;

    netopia.dhtml.removeElementChildren(el);

    if (el.nodeType == 1)  // only element nodes
        for (var i = netopia.dhtml._removeElementSinks.length; i-- > 0; )
            netopia.dhtml._removeElementSinks[i](el);

    el.parentNode.removeChild(el);
},

removeElementChildren : function (elem)
{
    var el = $(elem);
    if (!el)
        return;

    for (var n, c = el.firstChild; c; c = n)
    {
        n = c.nextSibling;
        if (c.nodeType == 1)  // only element nodes
            netopia.dhtml.removeElement(c);
    }
},

removeElements : function (elems)
{
    for (prop in elems)
        if (netopia.core.hasOwnProp(elems, prop))
            netopia.dhtml.removeElement(elems[prop]);
},

/**
Removes one or more words and adds other word or words to an element's className.
This is essentially equivalent to the following:

    e.className = replaceWords(e.className, remove, add, " ");

Examples:

    // Ensure that "foo" is not present:
    netopia.dhtml.replaceClassNames(el, "foo", null);

    // Ensure that "foo" is present (once):
    netopia.dhtml.replaceClassNames(el, "foo", "foo");

    // Ensure that "foo" is present (once), but "bar" is not:
    netopia.dhtml.replaceClassNames(el, ["foo", "bar"], "foo");

    // Ensure that "foo" and "bar" are present (once each):
    netopia.dhtml.replaceClassNames(el, ["foo", "bar"], ["foo", "bar"]);

@param elem The element (or its ID) to modify.
@param remove The class name(s) to remove.
@param add The class name(s) to add.
@return The previous className.
*/
replaceClassNames : function (elem, remove, add)
{
    elem = $(elem);
    var before = elem.className;

    elem.className = netopia.text.replaceWords(before, remove, add, " ");

    return before;
},

setElementPosComponent : function (el, c, v)
{
    if (v != null && v !== undefined)
        el.style[c] = netopia.dhtml.makeSize(v);
},

setElementPos : function (el, x, y, w, h)
{
    netopia.dhtml.setElementPosComponent(el, "left",   x);
    netopia.dhtml.setElementPosComponent(el, "top",    y);
    netopia.dhtml.setElementPosComponent(el, "width",  w);
    netopia.dhtml.setElementPosComponent(el, "height", h);
},

/**
Sets a named style to its new value and returns the previous value tuple. If a
name mapping was applied, the returned tuple will contain the mapped name. The
returned tuple contains the values in its name and value properties.
*/
setStyle : function (el, name, value)
{
    el = $(el);
    var t;

    // hacks ala Prototype...
    name = name.camelize();
    switch (name)
    {
     case "float": case "cssFloat":
        name = (typeof(el.style.styleFloat) != "undefined") ? "styleFloat" : "cssFloat";
        break;
     case "opacity":
        if (netopia.dhtml.browser.ie)
        {
            name = "filter";
            t = netopia.dhtml.getStyle(el, name);
            t = t.replace(/alpha\([^\)]*\)/gi,"");
        }

        if (value >= 0.99999)
        {
            if (!netopia.dhtml.browser.ie)
                value = netopia.dhtml.browser.gecko ? 0.999999 : 1.0;
        }
        else
        {
            value = (value < 0.00001) ? 0 : value;
            if (netopia.dhtml.browser.ie)
                t += "alpha(opacity="+(value * 100)+")";
        }

        if (netopia.dhtml.browser.ie)
            value = t;

        break;
    }

    var ret = { name: name, value: el.style[name] };
    el.style[name] = value;
    return ret;
},

/**
Sets multiple styles and returns the previous values in an array. The styles can
be an object containing name/value pairs or an array of objects with each object
having name and value properties (as returned get this method). In other words,
this method returns an array of properties that can be passed back to restore
the element to its previous state.
*/
setStyles : function (el, styles)
{
    var ret = [ ];
    el = $(el);

    if (styles instanceof Array)
        for (var i = 0; i < styles.length; ++i)
            ret.push(netopia.dhtml.setStyle(el, styles[i].name, styles[i].value));
    else
        for (var n in styles)
            if (netopia.core.hasOwnProp(styles, n))
                ret.push(netopia.dhtml.setStyle(el, n, styles[n]));

    return ret;
},

/** Sets the text of the given element. */
setText : function (el, text)
{
    el = $(el);
    var tn = el.ownerDocument.createTextNode(text);
    for (var n, c = el.firstChild; c; c = n)
    {
        n = c.nextSibling;
        if (c.nodeType == 3) // if (text node)
            el.removeChild(c);
    }
    el.appendChild(tn);
},

toggleDisplay : function (id)
{
    var el = $(id);
    el.style.display = (el.style.display == "none") ? "" : "none";
},

/* content can be either text for an innerHTML or 
   a reference to a presently detatched dom element.
   return is a detached dom element.
*/
wrapElement: function (content, wrapperId, wrapperClass)
{
// todo - return netopia.dhtml.newParentDiv(content, wrapperId, { className: wrapperClass });

    var div = document.createElement("div");
    div.className = wrapperClass;
    if (wrapperId )
        div.id = wrapperId;
    if ( ! content) 
        return div;
        
    if ( typeof(content) == "string" )
        div.innerHTML=content;
    else 
        div.appendChild(content);
    return div;	
},

/**
This class is a good base class for any class that wraps a DOM element. The most
important property defined by this class is mId (the DOM ID of the element).
Various helpful methods are provided to manipulate the elements properties and
handle events.
*/
Element : $extends(netopia.core.Object, function ()
{
    var uberTicketBase =
    {
        destroy : function ()
        {
            for (var i = 0; i < this.tickets.length; ++i)
                this.tickets[i].destroy();
            this.tickets = [];
        }
    };

    function makeUberTicket (id)
    {
        var ut = $chain(uberTicketBase);
        ut.id = id;
        ut.tickets = [];
        return ut;
    }

    function subscribe (evmgr, id, eventName, handler, uberTicket)
    {
        var ticket = evmgr.subscribe(id, eventName, handler);
        this.mTickets.tickets.push(ticket);
        uberTicket.tickets.push(ticket);
    }

    return $literal(
    {
        ctor : function (id)
        {
            $super(arguments).call(this, id);

            this.mId = id;
            this.mTickets = makeUberTicket(id); // handy, so just reuse it!

            this.setCoords();
        },

        destroy : function ()
        {
            this.mTickets.destroy(); // see? handy!

            $super(arguments).call(this);
        },

        // visibility

        hide : function ()
        {
            this.setVisibility(false);
        },

        getVisibility : function ()
        {
            var disp = $(this.mId).style.display;

            return disp != "none";
        },

        setVisibility : function (vis)
        {
            $super(arguments).call(this, vis);

            var el = $(this.mId);
            var st = vis ? "block" : "none";

            if (el.style.display != st) // flickers w/o this on some browsers
                el.style.display = st;
        },

        // position / stacking

        getPos : function ()  { return netopia.dhtml.getElementPos(this.mId); },
        getRect : function () { return netopia.dhtml.getElementRect(this.mId); },
        getSize : function () { return netopia.dhtml.getElementSize(this.mId); },

        moveTo : function (x, y, rel)
        {
            $super(arguments).call(this, x, y, rel);

            var el = $(this.mId);
            var c = this.mCoords;

            if (rel)
            {
                rel = new netopia.dhtml.CoordPair(rel);

                if (c.horz != rel.horz || c.vert != rel.vert)
                {
                    var size = this.getSize();
                    if (x !== undefined && c.horz != rel.horz)
                        x -= size.w;
                    if (y !== undefined && c.vert != rel.vert)
                        y -= size.h;
                }
            }

            if (x !== undefined)
                el.style[c.horz] = x + "px";
            if (y !== undefined)
                el.style[c.vert] = y + "px";
        },

        moveToTop : function ()
        {
            $super(arguments).call(this);

            netopia.dhtml.moveToTop(this.mId);
        },

        // Controls how this element wants to be positioned (left/right or
        // top/bottom). By default, we use left/top styles.
        setCoords : function (rel)
        {
            this.mCoords = new netopia.dhtml.CoordPair(rel);
        },

        setFocus : function ()
        {
            var el = $(this.mId);
            if (el && el.focus)
                try { el.focus(); } catch (e) { } // ignore
        },

        // styles

        getStyle : function (style)
            { return netopia.dhtml.getStyle(this.mId, style); },

        getStyles : function (styles)
            { return netopia.dhtml.getStyles(this.mId, styles); },

        setStyle : function (name, value)
            { return netopia.dhtml.setStyle(this.mId, name, value); },

        setStyles : function (styles)
            { return netopia.dhtml.setStyles(this.mId, styles); },

        // className manipulations

        addClassNames : function (add)
        {
            this.replaceClassNames(add, add);
        },

        enableClassNames : function (names, add)
        {
            this.replaceClassNames(names, add ? names : null);
        },

        removeClassNames : function (remove)
        {
            this.replaceClassNames(remove, null);
        },

        replaceClassNames : function (remove, add)
        {
            $super(arguments).call(this, remove, add);

            netopia.dhtml.replaceClassNames(this.mId, remove, add);
        },

        // event handling

        /**
        Subscribes a single handler to one or more events. This is often useful
        for monitoring for all kinds of activity that produces a common result.
        All subscribed event handlers can be removed using the object returned
        from this method via its destroy method (just like EventMgr's returned
        ticket objects). They will also be unplugged when this object's destroy
        method is called.
        */
        subscribeEvent : function () // [id,] handler, event1, event2, ...
        {
            var first = 1, id = this.mId, handler = arguments[0];
            if (!(handler instanceof Function))
            {
                id = handler;
                handler = arguments[first++];
            }

            var evmgr = netopia.dhtml.EventManager.getInstance();
            var uberTicket = makeUberTicket(id);

            handler = handler.xbind(this);

            for (var n = first; n < arguments.length; ++n)
            {
                var arg = arguments[n];

                if (!(arg instanceof Array))
                    subscribe.call(this, evmgr, id, arg, handler, uberTicket);
                else
                    for (var k = 0; k < arg.length; ++k)
                        subscribe.call(this, evmgr, id, arg[k], handler, uberTicket);
            }

            this.mTickets.tickets.push(uberTicket);
            return uberTicket;
        }
    });
}())

}); //netopia.dhtml

