Subversion Repositories Transart

Rev

Blame | Last modification | View Log | RSS feed

/* SpryCollapsiblePanel.js - Revision: Spry Preview Release 1.4 */

// Copyright (c) 2006. Adobe Systems Incorporated.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//   * Redistributions of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//   * 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.
//   * Neither the name of Adobe Systems Incorporated 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 OWNER 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.

var Spry;
if (!Spry) Spry = {};
if (!Spry.Widget) Spry.Widget = {};

Spry.Widget.CollapsiblePanel = function(element, opts)
{
        this.init(element);

        Spry.Widget.CollapsiblePanel.setOptions(this, opts);

        this.attachBehaviors();
};

Spry.Widget.CollapsiblePanel.prototype.init = function(element)
{
        this.element = this.getElement(element);
        this.focusElement = null;
        this.hoverClass = "CollapsiblePanelTabHover";
        this.openClass = "CollapsiblePanelOpen";
        this.closedClass = "CollapsiblePanelClosed";
        this.focusedClass = "CollapsiblePanelFocused";
        this.enableAnimation = true;
        this.enableKeyboardNavigation = true;
        this.animator = null;
        this.hasFocus = false;
        this.contentIsOpen = true;
};

Spry.Widget.CollapsiblePanel.prototype.getElement = function(ele)
{
        if (ele && typeof ele == "string")
                return document.getElementById(ele);
        return ele;
};

Spry.Widget.CollapsiblePanel.prototype.addClassName = function(ele, className)
{
        if (!ele || !className || (ele.className && ele.className.search(new RegExp("\\b" + className + "\\b")) != -1))
                return;
        ele.className += (ele.className ? " " : "") + className;
};

Spry.Widget.CollapsiblePanel.prototype.removeClassName = function(ele, className)
{
        if (!ele || !className || (ele.className && ele.className.search(new RegExp("\\b" + className + "\\b")) == -1))
                return;
        ele.className = ele.className.replace(new RegExp("\\s*\\b" + className + "\\b", "g"), "");
};

Spry.Widget.CollapsiblePanel.prototype.hasClassName = function(ele, className)
{
        if (!ele || !className || !ele.className || ele.className.search(new RegExp("\\b" + className + "\\b")) == -1)
                return false;
        return true;
};

Spry.Widget.CollapsiblePanel.prototype.setDisplay = function(ele, display)
{
        if( ele )
                ele.style.display = display;
};

Spry.Widget.CollapsiblePanel.setOptions = function(obj, optionsObj, ignoreUndefinedProps)
{
        if (!optionsObj)
                return;
        for (var optionName in optionsObj)
        {
                if (ignoreUndefinedProps && optionsObj[optionName] == undefined)
                        continue;
                obj[optionName] = optionsObj[optionName];
        }
};

Spry.Widget.CollapsiblePanel.prototype.onTabMouseOver = function()
{
        this.addClassName(this.getTab(), this.hoverClass);
};

Spry.Widget.CollapsiblePanel.prototype.onTabMouseOut = function()
{
        this.removeClassName(this.getTab(), this.hoverClass);
};

Spry.Widget.CollapsiblePanel.prototype.open = function()
{
        this.contentIsOpen = true;
        if (this.enableAnimation)
        {
                if (this.animator)
                        this.animator.stop();
                this.animator = new Spry.Widget.CollapsiblePanel.PanelAnimator(this, true);
                this.animator.start();
        }
        else
                this.setDisplay(this.getContent(), "block");

        this.removeClassName(this.element, this.closedClass);
        this.addClassName(this.element, this.openClass);
};

Spry.Widget.CollapsiblePanel.prototype.close = function()
{
        this.contentIsOpen = false;
        if (this.enableAnimation)
        {
                if (this.animator)
                        this.animator.stop();
                this.animator = new Spry.Widget.CollapsiblePanel.PanelAnimator(this, false);
                this.animator.start();
        }
        else
                this.setDisplay(this.getContent(), "none");

        this.removeClassName(this.element, this.openClass);
        this.addClassName(this.element, this.closedClass);
};

Spry.Widget.CollapsiblePanel.prototype.onTabClick = function()
{
        if (this.isOpen())
                this.close();
        else
                this.open();
        this.focus();
};

Spry.Widget.CollapsiblePanel.prototype.onFocus = function(e)
{
        this.hasFocus = true;
        this.addClassName(this.element, this.focusedClass);
};

Spry.Widget.CollapsiblePanel.prototype.onBlur = function(e)
{
        this.hasFocus = false;
        this.removeClassName(this.element, this.focusedClass);
};

Spry.Widget.CollapsiblePanel.ENTER_KEY = 13;
Spry.Widget.CollapsiblePanel.SPACE_KEY = 32;

Spry.Widget.CollapsiblePanel.prototype.onKeyDown = function(e)
{
        var key = e.keyCode;
        if (!this.hasFocus || (key != Spry.Widget.CollapsiblePanel.ENTER_KEY && key != Spry.Widget.CollapsiblePanel.SPACE_KEY))
                return true;
        
        if (this.isOpen())
                this.close();
        else
                this.open();

        if (e.stopPropagation)
                e.stopPropagation();
        if (e.preventDefault)
                e.preventDefault();

        return false;
};

Spry.Widget.CollapsiblePanel.prototype.attachPanelHandlers = function()
{
        var tab = this.getTab();
        if (!tab)
                return;

        var self = this;
        Spry.Widget.CollapsiblePanel.addEventListener(tab, "dblclick", function(e) { return self.onTabClick(); }, false);
        Spry.Widget.CollapsiblePanel.addEventListener(tab, "mouseover", function(e) { return self.onTabMouseOver(); }, false);
        Spry.Widget.CollapsiblePanel.addEventListener(tab, "mouseout", function(e) { return self.onTabMouseOut(); }, false);

        if (this.enableKeyboardNavigation)
        {
                // XXX: IE doesn't allow the setting of tabindex dynamically. This means we can't
                // rely on adding the tabindex attribute if it is missing to enable keyboard navigation
                // by default.

                // Find the first element within the tab container that has a tabindex or the first
                // anchor tag.
                
                var tabIndexEle = null;
                var tabAnchorEle = null;

                this.preorderTraversal(tab, function(node) {
                        if (node.nodeType == 1 /* NODE.ELEMENT_NODE */)
                        {
                                var tabIndexAttr = tab.attributes.getNamedItem("tabindex");
                                if (tabIndexAttr)
                                {
                                        tabIndexEle = node;
                                        return true;
                                }
                                if (!tabAnchorEle && node.nodeName.toLowerCase() == "a")
                                        tabAnchorEle = node;
                        }
                        return false;
                });

                if (tabIndexEle)
                        this.focusElement = tabIndexEle;
                else if (tabAnchorEle)
                        this.focusElement = tabAnchorEle;

                if (this.focusElement)
                {
                        Spry.Widget.CollapsiblePanel.addEventListener(this.focusElement, "focus", function(e) { return self.onFocus(e); }, false);
                        Spry.Widget.CollapsiblePanel.addEventListener(this.focusElement, "blur", function(e) { return self.onBlur(e); }, false);
                        Spry.Widget.CollapsiblePanel.addEventListener(this.focusElement, "keydown", function(e) { return self.onKeyDown(e); }, false);
                }
        }
};

Spry.Widget.CollapsiblePanel.addEventListener = function(element, eventType, handler, capture)
{
        try
        {
                if (element.addEventListener)
                        element.addEventListener(eventType, handler, capture);
                else if (element.attachEvent)
                        element.attachEvent("on" + eventType, handler);
        }
        catch (e) {}
};

Spry.Widget.CollapsiblePanel.prototype.preorderTraversal = function(root, func)
{
        var stopTraversal = false;
        if (root)
        {
                stopTraversal = func(root);
                if (root.hasChildNodes())
                {
                        var child = root.firstChild;
                        while (!stopTraversal && child)
                        {
                                stopTraversal = this.preorderTraversal(child, func);
                                try { child = child.nextSibling; } catch (e) { child = null; }
                        }
                }
        }
        return stopTraversal;
};

Spry.Widget.CollapsiblePanel.prototype.attachBehaviors = function()
{
        var panel = this.element;
        var tab = this.getTab();
        var content = this.getContent();

        if (this.contentIsOpen || this.hasClassName(panel, this.openClass))
        {
                this.removeClassName(panel, this.closedClass);
                this.setDisplay(content, "block");
                this.contentIsOpen = true;
        }
        else
        {
                this.removeClassName(panel, this.openClass);
                this.addClassName(panel, this.closedClass);
                this.setDisplay(content, "none");
                this.contentIsOpen = false;
        }

        this.attachPanelHandlers();
};

Spry.Widget.CollapsiblePanel.prototype.getTab = function()
{
        return this.getElementChildren(this.element)[0];
};

Spry.Widget.CollapsiblePanel.prototype.getContent = function()
{
        return this.getElementChildren(this.element)[1];
};

Spry.Widget.CollapsiblePanel.prototype.isOpen = function()
{
        return this.contentIsOpen;
};

Spry.Widget.CollapsiblePanel.prototype.getElementChildren = function(element)
{
        var children = [];
        var child = element.firstChild;
        while (child)
        {
                if (child.nodeType == 1 /* Node.ELEMENT_NODE */)
                        children.push(child);
                child = child.nextSibling;
        }
        return children;
};

Spry.Widget.CollapsiblePanel.prototype.focus = function()
{
        if (this.focusElement && this.focusElement.focus)
                this.focusElement.focus();
};

/////////////////////////////////////////////////////

Spry.Widget.CollapsiblePanel.PanelAnimator = function(panel, doOpen, opts)
{
        this.timer = null;
        this.interval = 0;
        this.stepCount = 0;

        this.fps = 0;
        this.steps = 10;
        this.duration = 500;
        this.onComplete = null;

        this.panel = panel;
        this.content = panel.getContent();
        this.panelData = [];
        this.doOpen = doOpen;

        Spry.Widget.CollapsiblePanel.setOptions(this, opts);


        // If caller specified speed in terms of frames per second,
        // convert them into steps.

        if (this.fps > 0)
        {
                this.interval = Math.floor(1000 / this.fps);
                this.steps = parseInt((this.duration + (this.interval - 1)) / this.interval);
        }
        else if (this.steps > 0)
                this.interval = this.duration / this.steps;

        var c = this.content;

        var curHeight = c.offsetHeight ? c.offsetHeight : 0;
        
        if (doOpen && c.style.display == "none")
                this.fromHeight = 0;
        else
                this.fromHeight = curHeight;

        if (!doOpen)
                this.toHeight = 0;
        else
        {
                if (c.style.display == "none")
                {
                        // The content area is not displayed so in order to calculate the extent
                        // of the content inside it, we have to set its display to block.

                        c.style.visibility = "hidden";
                        c.style.display = "block";
                }

                // Unfortunately in Mozilla/Firefox, fetching the offsetHeight seems to cause
                // the browser to synchronously re-layout and re-display content on the page,
                // so we see a brief flash of content that is *after* the panel being positioned
                // where it should when the panel is fully expanded. To get around this, we
                // temporarily position the content area of the panel absolutely off-screen.
                // This has the effect of taking the content out-of-flow, so nothing shifts around.

                // var oldPos = c.style.position;
                // var oldLeft = c.style.left;
                // c.style.position = "absolute";
                // c.style.left = "-2000em";

                // Clear the height property so we can calculate
                // the full height of the content we are going to show.
                c.style.height = "";
                this.toHeight = c.offsetHeight;

                // Now restore the position and offset to what it was!
                // c.style.position = oldPos;
                // c.style.left = oldLeft;
        }

        this.increment = (this.toHeight - this.fromHeight) / this.steps;
        this.overflow = c.style.overflow;

        c.style.height = this.fromHeight + "px";
        c.style.visibility = "visible";
        c.style.overflow = "hidden";
        c.style.display = "block";
};

Spry.Widget.CollapsiblePanel.PanelAnimator.prototype.start = function()
{
        var self = this;
        this.timer = setTimeout(function() { self.stepAnimation(); }, this.interval);
};

Spry.Widget.CollapsiblePanel.PanelAnimator.prototype.stop = function()
{
        if (this.timer)
        {
                clearTimeout(this.timer);

                // If we're killing the timer, restore the overflow
                // properties on the panels we were animating!

                if (this.stepCount < this.steps)
                        this.content.style.overflow = this.overflow;
        }

        this.timer = null;
};

Spry.Widget.CollapsiblePanel.PanelAnimator.prototype.stepAnimation = function()
{
        ++this.stepCount;

        this.animate();

        if (this.stepCount < this.steps)
                this.start();
        else if (this.onComplete)
                this.onComplete();
};

Spry.Widget.CollapsiblePanel.PanelAnimator.prototype.animate = function()
{
        if (this.stepCount >= this.steps)
        {
                if (!this.doOpen)
                        this.content.style.display = "none";
                this.content.style.overflow = this.overflow;
                this.content.style.height = this.toHeight + "px";
        }
        else
        {
                this.fromHeight += this.increment;
                this.content.style.height = this.fromHeight + "px";
        }
};