// Copyright 2009 Microsoft Corporation
// General use methods to handle cross-browser event setup, element search and effects operations, etc.

// Provides geenric utility functions (not tied to specific HTML elements) for the library.
var $$util = {

    // Appends an array to an array.
    // src = the array to append to
    // add = the array that will be appended to src
    appendArray: function(src, add) {
        if (src !== undefined && add !== undefined && src.length !== undefined && add.length !== undefined) {
            for (var i = 0; i < add.length; i++) {
                src[src.length] = add[i];
            }
        }
        return src;
    },

    // Returns the first value in an array, or the passed value if not an array.
    // arr = the array or value to inspect
    first: function(arr) {
        if (arr) {
            if (arr.length !== undefined) {
                return arr[0];
            }
            return arr;
        }
        return null;
    },

    // Returns the last value in an array, or the passed value if not an array.
    // arr = the array or value to inspect
    last: function(arr) {
        if (arr) {
            if (arr.length !== undefined) {
                if (arr.length > 0) {
                    return arr[arr.length - 1];
                }
                return null;
            }
            return arr;
        }
        return null;
    },

    // Returns the all child elements in an HTML element, accounting for browser inconsistencies.
    // inObj = the element to inspect for children
    getChildElements: function(inObj) {
        if (inObj !== undefined && inObj !== null) {
            if (inObj == window) {
                inObj = document;
            }
        } else {
            inObj = document;
        }
        var arr = [];
        var els = inObj.childNodes;
        if (els !== undefined && els !== null) {
            for (var i = 0; i < els.length; i++) {
                if (els[i].nodeType != 1) {
                    continue; // empty nodes in Firefox
                }
                arr[arr.length] = els[i];
                $$util.appendArray(arr, this.getChildElements(els[i]));

            }
        }
        return arr;
    },

    // Finds a list of child elements in an HTML element by ID.
    // ** May want to extend to allow other means of lookup based on input syntax in the future.
    // ref = reference value to use for lookup
    // inObj = the HTML element or DOM object to evaluate for children
    findElements: function(ref, inObj) {
        if (!inObj || inObj === null || inObj == window) {
            inObj = document;
        }
        var els = [];
        if (inObj == document) {
            var el = document.getElementById(ref);
            if (el) {
                els[0] = el;
            }
            return els;
        }
        els = this.getChildElements(inObj);
        var ret = [];
        for (var i = 0; i < els.length; i++) {
            if (els[i] && els[i].id == ref) {
                ret[ret.length] = els[i];
            }
        }
        return ret;
    },

    // Returns the HTML element referenced by an event, accounting for browser inconsistencies.
    // e = event
    getEventTarget: function(e) {
        if (window.event) {
            e = window.event;
        }
        return e ? (e.srcElement ? e.srcElement : e.target) : null;
    },

    // Returns a comma-separated list of properties on an object as a string, typically for debugging.
    // obj = the object to evaluate for properties
    properties: function(obj) {
        var str = "";
        for (var prop in obj) {
            str += prop + " = " + obj[prop] + ", ";
        }
        return str;
    },
    
    // Purges all references to functions on a DOM element
    // obj = DOM element to purge
    purgeDomReferences: function(obj) {
        var attrs = obj.attributes, i, len, name, nodes;
        if (attrs) {
            len = attrs.length;
            for (i = 0; i < len; i += 1) {
                name = attrs[i].name;
                if (typeof obj[name] === 'function') {
                    obj[name] = null;
                }
            }
        }
        nodes = obj.childNodes;
        if (nodes) {
            len = nodes.length;
            for (i = 0; i < len; i += 1) {
                $$util.purgeDomReferences(obj.childNodes[i]);
            }
        }
    }
};

// Creates a new core object. This is basically a shortcut/friendly name for "new $$obj(ref);"
// ref = "findable" reference string for HTML elements, a reference to an HTML element, a reference
// to an array of HTML elements, or some other DOM object reference
function core(ref) {
    if (ref == window || ref == document) {
        return new $$obj(ref);
    }
    var els = $$util.findElements(ref, document);
    if (els.length < 1) {
        els = ref;
    }
    return new $$obj($$util.first(els));
}

// The core script library object. This is the main interface to the library.
// All methods on the core object return "this" or another core object, so calls can be linked
// together using dot syntax. For example: core("elementID").height(100).width(100);
// The core object contains a reference to one or more HTML elements or DOM objects, and the
// member methods are intended to function within the context of those objects.
// ref = "findable" reference string for HTML elements, a reference to an HTML element, a reference
// to an array of HTML elements, or some other DOM object reference
function $$obj(ref) {
    this.$el = document;
    this.$isValid = false;
    if (ref) {
        if (ref == window || ref == document || ref.nodeType) {
            this.$el = ref;
            if (this.$el.nodeType) {
                this.$isValid = true;
            }
        } else {
            this.$el = $$util.findElements(ref, document);
            if (this.$el.length == 1) {
                this.$el = this.$el[0];
                if (this.$el.tagName) {
                    this.$isValid = true;
                }
            } else if (this.$el.length < 1) {
                this.$el = ref;
                if (this.$el.tagName) {
                    this.$isValid = true;
                }
            } else {
                if (this.$el[0] && this.$el[0].nodeType) {
                    this.$isValid = true;
                }
            }
        }
    }
    return this;
}

// A shortcut for in-line utility methods on a core object.
// This allows for friendlier "core().util.xxx" syntax in addition to "$$util.xxx".
$$obj.prototype.util = $$util;

// Indicates whether a core object references a valid HTML element or elements.
$$obj.prototype.isValid = function() {
    return (this.$isValid === true);
};

// Marks this core object as invalid. Other contained values are unaffected.
$$obj.prototype.invalidate = function() {
    this.$isValid = false;
    this.$el = document;
    return this;
};

// Returns a reference to the first or only associated HTML element
$$obj.prototype.element = function() {
    return this.elements()[0];
};

// Returns an array of associated HTML elements (even if there is one or less)
$$obj.prototype.elements = function() {
    if (this.$el.length) {
        return this.$el;
    }
    var els = [];
    els[els.length] = this.$el;
    return els;
};

// Finds child HTML elements based on the reference string and related to the list of
// elements associated with this core object. Returns a new core object.
// ref = "findable" reference to child HTML elements
$$obj.prototype.find = function(ref) {
    var els = [];
    if (ref !== undefined) {
        if (this.$el.length !== undefined) {
            for (var i = 0; i < this.$el.length; i++) {
                $$util.appendArray(els, $$util.findElements(ref, this.$el[i]));
            }
        } else {
            els = $$util.findElements(ref, this.$el);
        }
    } else {
        if (this.$el.length !== undefined) {
            for (var i = 0; i < this.$el.length; i++) {
                $$util.appendArray(els, $$util.getChildElements(this.$el[i]));
            }
        } else {
            els = $$util.getChildElements(this.$el);
        }
    }
    return new $$obj(els);
};

// Finds and returns a new core object containing a single reference to the first
// of any HTML elements found using the findable reference.
// ref = "findable" reference to child HTML elements
$$obj.prototype.findSingle = function(ref) {
    return this.find(ref).first();
};

// Finds child elements by CSS class. Returns a new core object.
// ref = "findable" reference to child HTML elements
$$obj.prototype.findByClass = function(str) {
    var els = this.elements();
    var all = [];
    for (var i = 0; i < els.length; i++) {
        var children = $$util.getChildElements(els[i]);
        for (var k = 0; k < children.length; k++) {
            if (children[k].className && children[k].className == str) {
                all[all.length] = children[k];
            }
        }
    }
    return new $$obj(all);
};

// Finds child elements by property/attribute and value.
// For example, "this.findByValue("type", "text");" returns a core object that references
// all elements that have a "type" attribute or property with a value of "text".
// prop = name of the type or property to evaluate on child elements
// val = value to compare on the property
$$obj.prototype.findByValue = function(prop, val) {
    var els = this.elements();
    var all = [];
    for (var i = 0; i < els.length; i++) {
        var children = $$util.getChildElements(els[i]);
        for (var k = 0; k < children.length; k++) {
            if (val !== undefined) {
                if (children[k][prop] !== undefined) {
                    all[all.length] = children[k];
                }
            } else {
                if (children[k][prop] !== undefined && children[k][prop] == val) {
                    all[all.length] = children[k];
                }
            }
        }
    }
    return new $$obj(all);
};

// Finds child elements by the value of the "type" property or attribute
// str = type value to compare
$$obj.prototype.findByType = function(str) {
    var els = this.elements();
    var all = [];
    for (var i = 0; i < els.length; i++) {
        var children = $$util.getChildElements(els[i]);
        for (var k = 0; k < children.length; k++) {
            if (children[k].nodeName !== undefined && children[k].nodeName == str.toUpperCase()) {
                all[all.length] = children[k];
            }
        }
    }
    return new $$obj(all);
};

// Returns a new core object containing a reference to the immediate parent of the first
// associated element in "this". If the returned object has "obj.isValid() === false", then
// no parent element is available for "this".
$$obj.prototype.parent = function() {
    var el = this.element();
    if (el && el.parentNode) {
        return new $$obj(el.parentNode);
    }
    return new $$obj();
};

// Returns a new core object containing references to all parents in the stack of parents of
// the first associated element in "this".
$$obj.prototype.parents = function() {
    var els = [];
    var el = this;
    var parent = el.parent();
    while (parent.isValid() === true) {
        els[els.length] = el;
        el = parent;
        parent = el.parent();
    }
    return new $$obj(els);
};

// Finds a parent element by CSS class name.
// str = CSS class name to compare
$$obj.prototype.parentByClass = function(str) {
    var els = this.parents().elements();
    for (var i = 0; i < els.length; i++) {
        var obj = new $$obj(els[i]);
        if (obj.hasClass(str)) {
            return obj;
        }
    }
    return new $$obj();
};

// Finds all next and previous sibings to the first associated HTML element in "this"
// and returns a new core object containing those references.
$$obj.prototype.siblings = function() {
    var ret = [];
    var el = this;
    while (el.previous().isValid() === true) {
        ret[ret.length] = el.previous();
        el = el.previous();
    }
    while (el.next().isValid() === true) {
        ret[ret.length] = el.next();
        el = el.next();
    }
    return new $$obj(ret);
};

// Returns a new core object containg a single reference to the first associated HTML
// element in "this".
$$obj.prototype.first = function() {
    return new $$obj($$util.first(this.$el));
};

// Returns a new core object containing a single reference to the last associated HTML
// element in "this".
$$obj.prototype.last = function() {
    return new $$obj($$util.last(this.$el));
};

// Returns a new core object contianing a single reference to the associated HTML element
// based on its index.
$$obj.prototype.getAt = function(index) {
    var els = this.elements();
    return new $$obj(els[index]);
};

// Returns the number of associated HTML elements for "this".
$$obj.prototype.count = function() {
    var els = this.elements();
    return els.length;
};

// Returns a new core object containing a single reference to the next sibling to the first
// associated HTML element in "this".
$$obj.prototype.next = function() {
    var el = $$util.first(this.$el);
    if (!el) return new $$obj();
    var found = el;
    if (found && found.nextSibling) {
        do {
            found = found.nextSibling;
        } while (found && found.nodeType != 1);
    }
    return new $$obj(found);
};

// Returns a new core object containing a single reference to the next sibling to the first
// associated HTML element in "this".
$$obj.prototype.nextByClass = function(str) {
    var nextEl = this.next();
    while (nextEl && nextEl.hasClass(str) === false) {
        nextEl = nextEl.next();
    }
    if (nextEl && nextEl.hasClass(str) === true) {
        return nextEl;
    }
    return new $$obj();
};

// Returns a new core object containing a single reference to the previous sibling to the first
// associated HTML element in "this".
$$obj.prototype.previous = function() {
    var el = $$util.first(this.$el);
    if (!el) return new $$obj();
    var found = el;
    if (found && found.previousSibling) {
        do {
            found = found.previousSibling;
        } while (found && found.nodeType != 1);
    }
    return new $$obj(found);
};

// Returns a new core object containing a single reference to the next sibling to the first
// associated HTML element in "this" that has a reference to the given CSS class.
// str = CSS class name to compare
$$obj.prototype.previousByClass = function(str) {
    var prevEl = this.previous();
    while (prevEl && prevEl.hasClass(str) === false) {
        prevEl = prevEl.previous();
    }
    if (prevEl && prevEl.hasClass(str) === true) {
        return new $$obj(prevEl);
    }
    return new $$obj();
};

// Indicates whether the first associated HTML element in "this" is visible on the page.
$$obj.prototype.isVisible = function() {
    var el = this.element();
    if (el && el.style) {
        return el.style.display != "none";
    }
    return false;
};

// Toggles the visibility of all of the associated HTML elements in "this".
// func = callback function to run when the toggle process is completed
$$obj.prototype.toggle = function(func) {
    if (this.isVisible()) {
        this.hide(func);
    } else {
        this.show(func);
    }
    return this;
};

// Ensures that all of the associated HTML elements in "this" are visible on the page.
// func = callback function to run when the show process is completed.
$$obj.prototype.show = function(func) {
    if (this.$el && this.$el.length) {
        for (var i = 0; i < this.$el.length; i++) {
            if (this.$el[i] && this.$el[i].style) {
                if (this.$el[i].style.display == "none") {
                    this.$el[i].style.display = "";
                }
            }
        }
    } else {
        var el = $$util.first(this.$el);
        if (el && el.style) {
            if (el.style.display == "none") {
                el.style.display = "";
            }
        }
    }
    if (func) {
        func();
    }
    return this;
};

// Ensures that all of the associated HTML elements in "this" are hidden on the page.
// func = callback function to run when the hide process is completed.
$$obj.prototype.hide = function(func) {
    if (this.$el && this.$el.length) {
        for (var i = 0; i < this.$el.length; i++) {
            if (this.$el[i]) {
                if (this.$el[i].style.display != "none") {
                    this.$el[i].style.display = "none";
                }
            }
        }
    } else {
        var el = $$util.first(this.$el);
        if (el) {
            if (el.style.display != "none") {
                el.style.display = "none";
            }
        }
    }
    if (func) {
        func();
    }
    return this;
};

// Gets or sets the width of the first associated HTML element in "this".
// ** Might want to extend this to work on all associated elements in the future.
// val = (optional) new width to set
// If val is provided, then the width is set and "this" is returned. Otherwise,
// the width value of the first associated element is returned.
$$obj.prototype.width = function(val) {
    var el = $$util.first(this.$el);
    if (val !== undefined) {
        if (el && el.style) {
            if (val === 0 || isNaN(val)) {
                this.property("hideBySize", true);
                this.hide();
            } else {
                var flg = this.property("hideBySize");
                if (flg) {
                    this.show();
                    this.property("hideBySize", null);
                }
            }
            if (!isNaN(val)) {
                el.style.width = val + "px";
            }
        }
        return this;
    } else {
        if (el) {
            return el.offsetWidth;
        }
        return 0;
    }
};

// Gets or sets the height of the first associated HTML element in "this".
// ** Might want to extend this to work on all associated elements in the future.
// val = (optional) new height to set
// If val is provided, then the height is set and "this" is returned. Otherwise,
// the height value of the first associated element is returned.
$$obj.prototype.height = function(val) {
    var el = $$util.first(this.$el);
    if (val !== undefined) {
        if (el && el.style) {
            if (val === 0 || isNaN(val)) {
                this.property("hideBySize", true);
                this.hide();
            } else {
                var flg = this.property("hideBySize");
                if (flg) {
                    this.show();
                    this.property("hideBySize", null);
                }
            }
            if (!isNaN(val)) {
                el.style.height = val + "px";
            }
        }
        return this;
    } else {
        if (el && el !== null) {
            return el.offsetHeight;
        }
        return 0;
    }
};

// Gets or sets the CSS class of the first associated HTML element in "this".
// ** Might want to extend this to work on all associated elements in the future.
// val = (optional) new CSS class name to set
// If val is provided, then the CSS class is set and "this" is returned. Otherwise,
// the CSS class name value of the first associated element is returned.
$$obj.prototype.cssClass = function(val) {
    var el = $$util.first(this.$el);
    if (val !== undefined) {
        el.className = val;
    } else {
        return el.className;
    }
    return this;
};

// Indicates whether the given class name is referenced by the first associated HTML
// element in "this".
$$obj.prototype.hasClass = function(val) {
    var el = this.element();
    if (el && el.className) {
        var arr = el.className.split(' ');
        for (var i = 0; i < arr.length; i++) {
            if (arr[i] == val) {
                return true;
            }
        }
    }
    return false;
};

// Gets or sets the the value of a property or attribute of the first associated HTML
// element in "this".
// ** Might want to extend this to work on all associated elements in the future.
// prop = property or attribute name to get or set
// val = (optional) new property value to set
// If val is provided, then the property is set and "this" is returned. Otherwise,
// the value of the given property of the first associated element is returned.
$$obj.prototype.attr = function(prop, val) {
    var els = this.elements();
    var ret = "";
    for (var i = 0; i < els.length; i++) {
        if (val !== undefined) {
            els[i][prop] = val;
        } else {
            ret += els[i][prop];
        }
    }
    if (val !== undefined) {
        return this;
    }
    return ret;
};

// Gets or sets the the inner HTML string of the first associated HTML element in "this".
// ** Might want to extend this to work on all associated elements in the future.
// val = (optional) new HTML string to set
// If val is provided, then the inner HTML string is set and "this" is returned. Otherwise,
// the inner HTML string of the first associated element is returned.
$$obj.prototype.html = function(val) {
    return this.attr("innerHTML", val);
};

// Gets or sets the the text value of the first associated HTML element in "this".
// ** Might want to extend this to work on all associated elements in the future.
// val = (optional) new text value to set
// If val is provided, then the text value is set and "this" is returned. Otherwise,
// the text value of the first associated element is returned.
$$obj.prototype.text = function(val) {
    return this.attr("text", val);
};

// Gets or sets the the value string of the first associated HTML element in "this".
// ** Might want to extend this to work on all associated elements in the future.
// val = (optional) new value to set
// If val is provided, then the value is set and "this" is returned. Otherwise,
// the value string of the first associated element is returned.
$$obj.prototype.value = function(val) {
    return this.attr("value", val);
};

// Gets or sets custom properties on a library-specific object containr applied to the
// first associated element in "this". Properties set in this way can be recalled later without
// affecting HTML display or other the functionality of other libraries.
// name = name of the property to get or set
// val = (optional) value of the property to set
// If val is provided, then the value is set on the property and "this" is returned. Otherwise,
// the value of the named property is returned.
$$obj.prototype.property = function(name, val) {
    var el = this.element();
    if (!el.$properties) {
        el.$properties = {};
    }
    if (name) {
        if (val) {
            el.$properties[name] = val;
        } else {
            return el.$properties[name];
        }
    }
    return this;
};

// Exposes a simple mechanism to loop through all of the associated elements in "this".
// func = callback function to run for each associated element.
// When fun is run for each element, it is passed a new core object as an argument. The core
// object contains a single reference to the indexed element.
$$obj.prototype.each = function(func) {
    var els = this.elements();
    for (var i = 0; i < els.length; i++) {
        if (func) {
            func(new $$obj(els[i]), i);
        }
    }
};

/* *** events *** */

// Internal methods for working with events
$$obj.prototype._events = {

    // Handles adding an event on an object, accounting for browser inconsistencies
    // obj = the object to attach an event to
    // evt = the name of the event to attach
    // func = callback function to run when the event fires
    add: function(obj, evt, func) {
        if (obj) {
            if (obj.addEventListener) {
                obj.addEventListener(evt, func, false);
                
                // also need to store a list of listeners so they can be safely removed later
                var ref = new $$obj(obj);
                var listName = evt + "_handlers";
                var list = ref.property(listName);
                if (list === undefined || list === null) {
                    list = [];
                }
                list[list.length] = func;
                ref.property(listName, list);
                return true;
            } else if (obj.attachEvent) {
                return obj.attachEvent("on" + evt, func);
            }
        }
    },

    // Handles remonving an event from an object, accounting for browser inconsistencies
    // obj = the object to remove an event handler from
    // evt = the name of the event handler to remove
    remove: function(obj, evt) {
        if (obj) {
            if (obj.removeEventListener) {
                // remove all handlers in the list for this event
                var ref = new $$obj(obj);
                var listName = evt + "_handlers";
                var list = ref.property(listName);
                if (list !== undefined && list !== null) {
                    for (var i = 0; i < list.length; i++) {
                        obj.removeEventListener(evt, list[i], false);
                    }
                }
                ref.property(listName, null);
            } else if (obj.detachEvent) {
                obj.detachEvent("on" + evt);
            }
            if (obj[evt]) {
                obj[evt] = null;
            }
        }
    },

    // Ensures that the object describing the context for DOM events and properties is loaded and returns it
    getDomHandlerContext: function() {
        if (!window.$domHandlerContext) {
            window.$domHandlerContext = new $$obj(window);
        }
        if (!window.$domHandlerContext.$domLoadedHandlers) {
            window.$domHandlerContext.$domLoadedHandlers = [];
        }
        return window.$domHandlerContext;
    },

    // Adds a handler function to run on the DOM loaded event (.ready)
    // func = a function to run when DOM is loaded
    addDomLoadedHandler: function(func) {
        var context = this.getDomHandlerContext();
        context.$domLoadedHandlers[context.$domLoadedHandlers.length] = func;
    },

    // Runs each of the DOM loaded event handler functions in order
    runDomLoadedHandlers: function() {
        var context = this.getDomHandlerContext();
        for (var i = 0; i < context.$domLoadedHandlers.length; i++) {
            context.$domLoadedHandlers[i]();
        }
        context.$domLoadedHandlers = [];
    }
};

// Adds an event to all of the elements associated with "this".
// evt = name of the event
// func = callback function to run when the event fires
$$obj.prototype.addEvent = function(evt, func) {
    this.each(function(obj) {
        if (obj.isValid() === true) {
            var el = obj.element();
            obj._events.add(el, evt, func);
            obj._events.add(el, "unload", function(e){
                $$util.purgeDomReferences($$util.getEventTarget(e));
            });
        }
    });
    return this;
};

// Removes an event from all of the elements associated with "this".
// evt = name of the event
$$obj.prototype.removeEvent = function(evt) {
    this.each(function(obj) {
        if (obj.isValid() === true) {
            obj._events.remove(obj.element(), evt);
        }
    });
    return this;
};

// Adds event handlers for ASP.Net update panel events.
// onRequest = callback function to run when an update panel begins to post its request to the server
// onResponse = callback function to run when the server responds and the update panel is refreshed
// When either onRequest or onResponse is run, arguments for the sender of the event and the event
// arguments are included. (The prototype should resemble this: "function(sender, args) {}".)
$$obj.prototype.addUpdatePanelEvents = function(onRequest, onResponse) {
    try {
        Sys.Application.add_load(function() {
            if (onRequest) {
                Sys.WebForms.PageRequestManager.getInstance().add_beginRequest(onRequest);
            }
            if (onResponse) {
                Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(onResponse);
            }
        });
    } catch (err) {
        // no .net update panels
    }
    return this;
};

// Provides access to the browser event that fires when the DOM is loaded and can safely be scripted
// against, accounting for browser inconsistencies. (The DOM loaded event fires before window.onload.)
// func = callback function to run when the DOM is loaded
$$obj.prototype.ready = function(func) {
    if (this.$el == window) {
        this.addEvent("load", func);
    } else {
        this.$readyScriptUpdated = false;

        // Internet Explorer solution
        if (navigator.appName == "Microsoft Internet Explorer" &&
            this._events !== undefined && this._events.addDomLoadedHandler !== undefined) {
            this.$readyScriptUpdated = true;
            this._events.addDomLoadedHandler(func);
            document.write("<script id='__ie_onload' defer><\/script>");
            var script = document.getElementById("__ie_onload");
            var context = this;
            script.onreadystatechange = function() {
                if (this.readyState == "complete") {
                    (new $$obj())._events.runDomLoadedHandlers();
                }
            };
        }

        // Safari solution
        if (!this.$readyScriptUpdated && /WebKit/i.test(navigator.userAgent)) {
            this.$readyScriptUpdated = true;
            var _timer = setInterval(function() {
                if (/loaded|complete/.test(document.readyState)) {
                    clearInterval(_timer);
                    func();
                }
            }, 10);
        }

        // Mozilla solution
        if (!this.$readyScriptUpdated && document.addEventListener) {
            this.$readyScriptUpdated = true;
            document.addEventListener("DOMContentLoaded", func, false);
        }

        // Solution for anything else
        if (!this.$readyScriptUpdated) {
            this.$readyScriptUpdated = true;
            var win = new $$obj(window);
            win.addEvent("load", func);
        }
    }
    return this;
};

/* *** roll effects *** */

// Internal functions for working with roll effects.
$$obj.prototype._rollEffect = {

    // Handles the actual work of updating the roll effect over time.
    doEffect: function(el) {
        var obj = new $$obj(el);
        var vals = obj.property("rollEffectValues");
        if (!vals || vals === null || vals.rollVector === 0) {
            return;
        }
        if (vals.heights && vals.index >= 0 && vals.index < vals.heights.length) {
            obj.height(vals.heights[vals.index]);
            vals.index += vals.rollVector;
            setTimeout(function() {
                obj._rollEffect.doEffect(obj.element());
                obj = null; // for the garbage collector bug in IE
            }, vals.ms);
        } else {
            if (vals.rollVector < 0) {
                vals.rollVector = 0;
                obj.property("rollEffectValues", vals);
            } else {
                obj.height(vals.original.height);
                obj.element().style.overflow = vals.original.overflow;
                obj.property("rollEffectValues", null);
            }
            if (vals.func !== null) {
                vals.func();
            }
        }
    }
};

// Initializes a roll effect for all of the elements associated with "this". If an
// element is visible, it is rolled closed. If it is not, it is rolled open.
// fps = frames per second
// ms = total miliseconds for the transition
// func = callback function to run when the effect is completed for each element
$$obj.prototype.rollToggle = function(fps, ms, func) {
    var els = this.elements();
    for (var i = 0; i < els.length; i++) {
        var obj = new $$obj(els[i]);
        var el = els[i];
        var vals = obj.property("rollEffectValues");
        if (!vals || vals === null || vals.rollVector > 0) {
            obj.rollClosed(fps, ms, func);
        } else {
            obj.rollOpen(fps, ms, func);
        }
    }
};

// Initializes a roll closed effect for all of the elements associated with "this".
// Important: This method sets up values needed to unroll the effect, and it therefore must
// be called prior to "this.rollOpen(...)".
// fps = frames per second
// ms = total miliseconds for the transistion
// func = callback function to run when the effect is completed for each element
$$obj.prototype.rollClosed = function(fps, ms, func) {
    var els = this.elements();
    for (var i = 0; i < els.length; i++) {
        var obj = new $$obj(els[i]);
        var el = els[i];
        var vals = obj.property("rollEffectValues");
        var height = obj.height();
        if (height > 0) {
            if (!vals || vals === null || vals.rollVector === 0) {
                var heights = [];
                var maxFrames = Math.round(fps / (1000 / ms));
                for (var i = 0; i <= maxFrames; i++) {
                    heights[heights.length] = Math.round(i * (height / maxFrames));
                }
                vals = {};
                vals.original = {};
                vals.original.overflow = el.style.overflow;
                vals.original.height = height;
                vals.heights = heights;
                vals.index = heights.length - 1;
            } else if (vals.index >= vals.heights.length) {
                vals.index = vals.heights.length - 1;
            }
            vals.rollVector = -1;
            vals.fps = fps;
            vals.ms = ms / vals.heights.length;
            vals.func = func;
            obj.property("rollEffectValues", vals);
            el.style.overflow = "hidden";
            this._rollEffect.doEffect(el);
        }
    }
    return this;
};

// Initializes a roll open effect for all of the elements associated with "this".
// Important: This method expects setup values to have been previously created
// using "this.rollOpen(...)".
// fps = frames per second
// ms = total miliseconds for the transistion
// func = callback function to run when the effect is completed for each element
$$obj.prototype.rollOpen = function(fps, ms, func) {
    var els = this.elements();
    for (var i = 0; i < els.length; i++) {
        var obj = new $$obj(els[i]);
        var vals = obj.property("rollEffectValues");
        if (vals) {
            // a .rollClosed operation is required before a .rollOpen can occur
            // because .rollClosed sets up values such as heights needed by the animation
            // (a 'closed' element has no height, so heights values cannot be generated)
            if (vals.index < 0) {
                vals.index = 0;
            }
            vals.rollVector = 1;
            vals.fps = fps;
            vals.ms = ms / vals.heights.length;
            vals.func = func;
            obj.property("rollEffectValues", vals);
            els[i].style.overflow = "hidden";
            this._rollEffect.doEffect(els[i]);
        }
    }
};
