amtgard/ORK3

View on GitHub
orkui/template/default/script/development-bundle/ui/jquery.ui.accordion.js

Summary

Maintainability
F
1 mo
Test Coverage
/*
 * jQuery UI Accordion 1.8.18
 *
 * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * http://jquery.org/license
 *
 * http://docs.jquery.com/UI/Accordion
 *
 * Depends:
 *    jquery.ui.core.js
 *    jquery.ui.widget.js
 */
(function( $, undefined ) {

$.widget( "ui.accordion", {
    options: {
        active: 0,
        animated: "slide",
        autoHeight: true,
        clearStyle: false,
        collapsible: false,
        event: "click",
        fillSpace: false,
        header: "> li > :first-child,> :not(li):even",
        icons: {
            header: "ui-icon-triangle-1-e",
            headerSelected: "ui-icon-triangle-1-s"
        },
        navigation: false,
        navigationFilter: function() {
            return this.href.toLowerCase() === location.href.toLowerCase();
        }
    },

    _create: function() {
        var self = this,
            options = self.options;

        self.running = 0;

        self.element
            .addClass( "ui-accordion ui-widget ui-helper-reset" )
            // in lack of child-selectors in CSS
            // we need to mark top-LIs in a UL-accordion for some IE-fix
            .children( "li" )
                .addClass( "ui-accordion-li-fix" );

        self.headers = self.element.find( options.header )
            .addClass( "ui-accordion-header ui-helper-reset ui-state-default ui-corner-all" )
            .bind( "mouseenter.accordion", function() {
                if ( options.disabled ) {
                    return;
                }
                $( this ).addClass( "ui-state-hover" );
            })
            .bind( "mouseleave.accordion", function() {
                if ( options.disabled ) {
                    return;
                }
                $( this ).removeClass( "ui-state-hover" );
            })
            .bind( "focus.accordion", function() {
                if ( options.disabled ) {
                    return;
                }
                $( this ).addClass( "ui-state-focus" );
            })
            .bind( "blur.accordion", function() {
                if ( options.disabled ) {
                    return;
                }
                $( this ).removeClass( "ui-state-focus" );
            });

        self.headers.next()
            .addClass( "ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom" );

        if ( options.navigation ) {
            var current = self.element.find( "a" ).filter( options.navigationFilter ).eq( 0 );
            if ( current.length ) {
                var header = current.closest( ".ui-accordion-header" );
                if ( header.length ) {
                    // anchor within header
                    self.active = header;
                } else {
                    // anchor within content
                    self.active = current.closest( ".ui-accordion-content" ).prev();
                }
            }
        }

        self.active = self._findActive( self.active || options.active )
            .addClass( "ui-state-default ui-state-active" )
            .toggleClass( "ui-corner-all" )
            .toggleClass( "ui-corner-top" );
        self.active.next().addClass( "ui-accordion-content-active" );

        self._createIcons();
        self.resize();
        
        // ARIA
        self.element.attr( "role", "tablist" );

        self.headers
            .attr( "role", "tab" )
            .bind( "keydown.accordion", function( event ) {
                return self._keydown( event );
            })
            .next()
                .attr( "role", "tabpanel" );

        self.headers
            .not( self.active || "" )
            .attr({
                "aria-expanded": "false",
                "aria-selected": "false",
                tabIndex: -1
            })
            .next()
                .hide();

        // make sure at least one header is in the tab order
        if ( !self.active.length ) {
            self.headers.eq( 0 ).attr( "tabIndex", 0 );
        } else {
            self.active
                .attr({
                    "aria-expanded": "true",
                    "aria-selected": "true",
                    tabIndex: 0
                });
        }

        // only need links in tab order for Safari
        if ( !$.browser.safari ) {
            self.headers.find( "a" ).attr( "tabIndex", -1 );
        }

        if ( options.event ) {
            self.headers.bind( options.event.split(" ").join(".accordion ") + ".accordion", function(event) {
                self._clickHandler.call( self, event, this );
                event.preventDefault();
            });
        }
    },

    _createIcons: function() {
        var options = this.options;
        if ( options.icons ) {
            $( "<span></span>" )
                .addClass( "ui-icon " + options.icons.header )
                .prependTo( this.headers );
            this.active.children( ".ui-icon" )
                .toggleClass(options.icons.header)
                .toggleClass(options.icons.headerSelected);
            this.element.addClass( "ui-accordion-icons" );
        }
    },

    _destroyIcons: function() {
        this.headers.children( ".ui-icon" ).remove();
        this.element.removeClass( "ui-accordion-icons" );
    },

    destroy: function() {
        var options = this.options;

        this.element
            .removeClass( "ui-accordion ui-widget ui-helper-reset" )
            .removeAttr( "role" );

        this.headers
            .unbind( ".accordion" )
            .removeClass( "ui-accordion-header ui-accordion-disabled ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top" )
            .removeAttr( "role" )
            .removeAttr( "aria-expanded" )
            .removeAttr( "aria-selected" )
            .removeAttr( "tabIndex" );

        this.headers.find( "a" ).removeAttr( "tabIndex" );
        this._destroyIcons();
        var contents = this.headers.next()
            .css( "display", "" )
            .removeAttr( "role" )
            .removeClass( "ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-accordion-disabled ui-state-disabled" );
        if ( options.autoHeight || options.fillHeight ) {
            contents.css( "height", "" );
        }

        return $.Widget.prototype.destroy.call( this );
    },

    _setOption: function( key, value ) {
        $.Widget.prototype._setOption.apply( this, arguments );
            
        if ( key == "active" ) {
            this.activate( value );
        }
        if ( key == "icons" ) {
            this._destroyIcons();
            if ( value ) {
                this._createIcons();
            }
        }
        // #5332 - opacity doesn't cascade to positioned elements in IE
        // so we need to add the disabled class to the headers and panels
        if ( key == "disabled" ) {
            this.headers.add(this.headers.next())
                [ value ? "addClass" : "removeClass" ](
                    "ui-accordion-disabled ui-state-disabled" );
        }
    },

    _keydown: function( event ) {
        if ( this.options.disabled || event.altKey || event.ctrlKey ) {
            return;
        }

        var keyCode = $.ui.keyCode,
            length = this.headers.length,
            currentIndex = this.headers.index( event.target ),
            toFocus = false;

        switch ( event.keyCode ) {
            case keyCode.RIGHT:
            case keyCode.DOWN:
                toFocus = this.headers[ ( currentIndex + 1 ) % length ];
                break;
            case keyCode.LEFT:
            case keyCode.UP:
                toFocus = this.headers[ ( currentIndex - 1 + length ) % length ];
                break;
            case keyCode.SPACE:
            case keyCode.ENTER:
                this._clickHandler( { target: event.target }, event.target );
                event.preventDefault();
        }

        if ( toFocus ) {
            $( event.target ).attr( "tabIndex", -1 );
            $( toFocus ).attr( "tabIndex", 0 );
            toFocus.focus();
            return false;
        }

        return true;
    },

    resize: function() {
        var options = this.options,
            maxHeight;

        if ( options.fillSpace ) {
            if ( $.browser.msie ) {
                var defOverflow = this.element.parent().css( "overflow" );
                this.element.parent().css( "overflow", "hidden");
            }
            maxHeight = this.element.parent().height();
            if ($.browser.msie) {
                this.element.parent().css( "overflow", defOverflow );
            }

            this.headers.each(function() {
                maxHeight -= $( this ).outerHeight( true );
            });

            this.headers.next()
                .each(function() {
                    $( this ).height( Math.max( 0, maxHeight -
                        $( this ).innerHeight() + $( this ).height() ) );
                })
                .css( "overflow", "auto" );
        } else if ( options.autoHeight ) {
            maxHeight = 0;
            this.headers.next()
                .each(function() {
                    maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() );
                })
                .height( maxHeight );
        }

        return this;
    },

    activate: function( index ) {
        // TODO this gets called on init, changing the option without an explicit call for that
        this.options.active = index;
        // call clickHandler with custom event
        var active = this._findActive( index )[ 0 ];
        this._clickHandler( { target: active }, active );

        return this;
    },

    _findActive: function( selector ) {
        return selector
            ? typeof selector === "number"
                ? this.headers.filter( ":eq(" + selector + ")" )
                : this.headers.not( this.headers.not( selector ) )
            : selector === false
                ? $( [] )
                : this.headers.filter( ":eq(0)" );
    },

    // TODO isn't event.target enough? why the separate target argument?
    _clickHandler: function( event, target ) {
        var options = this.options;
        if ( options.disabled ) {
            return;
        }

        // called only when using activate(false) to close all parts programmatically
        if ( !event.target ) {
            if ( !options.collapsible ) {
                return;
            }
            this.active
                .removeClass( "ui-state-active ui-corner-top" )
                .addClass( "ui-state-default ui-corner-all" )
                .children( ".ui-icon" )
                    .removeClass( options.icons.headerSelected )
                    .addClass( options.icons.header );
            this.active.next().addClass( "ui-accordion-content-active" );
            var toHide = this.active.next(),
                data = {
                    options: options,
                    newHeader: $( [] ),
                    oldHeader: options.active,
                    newContent: $( [] ),
                    oldContent: toHide
                },
                toShow = ( this.active = $( [] ) );
            this._toggle( toShow, toHide, data );
            return;
        }

        // get the click target
        var clicked = $( event.currentTarget || target ),
            clickedIsActive = clicked[0] === this.active[0];

        // TODO the option is changed, is that correct?
        // TODO if it is correct, shouldn't that happen after determining that the click is valid?
        options.active = options.collapsible && clickedIsActive ?
            false :
            this.headers.index( clicked );

        // if animations are still active, or the active header is the target, ignore click
        if ( this.running || ( !options.collapsible && clickedIsActive ) ) {
            return;
        }

        // find elements to show and hide
        var active = this.active,
            toShow = clicked.next(),
            toHide = this.active.next(),
            data = {
                options: options,
                newHeader: clickedIsActive && options.collapsible ? $([]) : clicked,
                oldHeader: this.active,
                newContent: clickedIsActive && options.collapsible ? $([]) : toShow,
                oldContent: toHide
            },
            down = this.headers.index( this.active[0] ) > this.headers.index( clicked[0] );

        // when the call to ._toggle() comes after the class changes
        // it causes a very odd bug in IE 8 (see #6720)
        this.active = clickedIsActive ? $([]) : clicked;
        this._toggle( toShow, toHide, data, clickedIsActive, down );

        // switch classes
        active
            .removeClass( "ui-state-active ui-corner-top" )
            .addClass( "ui-state-default ui-corner-all" )
            .children( ".ui-icon" )
                .removeClass( options.icons.headerSelected )
                .addClass( options.icons.header );
        if ( !clickedIsActive ) {
            clicked
                .removeClass( "ui-state-default ui-corner-all" )
                .addClass( "ui-state-active ui-corner-top" )
                .children( ".ui-icon" )
                    .removeClass( options.icons.header )
                    .addClass( options.icons.headerSelected );
            clicked
                .next()
                .addClass( "ui-accordion-content-active" );
        }

        return;
    },

    _toggle: function( toShow, toHide, data, clickedIsActive, down ) {
        var self = this,
            options = self.options;

        self.toShow = toShow;
        self.toHide = toHide;
        self.data = data;

        var complete = function() {
            if ( !self ) {
                return;
            }
            return self._completed.apply( self, arguments );
        };

        // trigger changestart event
        self._trigger( "changestart", null, self.data );

        // count elements to animate
        self.running = toHide.size() === 0 ? toShow.size() : toHide.size();

        if ( options.animated ) {
            var animOptions = {};

            if ( options.collapsible && clickedIsActive ) {
                animOptions = {
                    toShow: $( [] ),
                    toHide: toHide,
                    complete: complete,
                    down: down,
                    autoHeight: options.autoHeight || options.fillSpace
                };
            } else {
                animOptions = {
                    toShow: toShow,
                    toHide: toHide,
                    complete: complete,
                    down: down,
                    autoHeight: options.autoHeight || options.fillSpace
                };
            }

            if ( !options.proxied ) {
                options.proxied = options.animated;
            }

            if ( !options.proxiedDuration ) {
                options.proxiedDuration = options.duration;
            }

            options.animated = $.isFunction( options.proxied ) ?
                options.proxied( animOptions ) :
                options.proxied;

            options.duration = $.isFunction( options.proxiedDuration ) ?
                options.proxiedDuration( animOptions ) :
                options.proxiedDuration;

            var animations = $.ui.accordion.animations,
                duration = options.duration,
                easing = options.animated;

            if ( easing && !animations[ easing ] && !$.easing[ easing ] ) {
                easing = "slide";
            }
            if ( !animations[ easing ] ) {
                animations[ easing ] = function( options ) {
                    this.slide( options, {
                        easing: easing,
                        duration: duration || 700
                    });
                };
            }

            animations[ easing ]( animOptions );
        } else {
            if ( options.collapsible && clickedIsActive ) {
                toShow.toggle();
            } else {
                toHide.hide();
                toShow.show();
            }

            complete( true );
        }

        // TODO assert that the blur and focus triggers are really necessary, remove otherwise
        toHide.prev()
            .attr({
                "aria-expanded": "false",
                "aria-selected": "false",
                tabIndex: -1
            })
            .blur();
        toShow.prev()
            .attr({
                "aria-expanded": "true",
                "aria-selected": "true",
                tabIndex: 0
            })
            .focus();
    },

    _completed: function( cancel ) {
        this.running = cancel ? 0 : --this.running;
        if ( this.running ) {
            return;
        }

        if ( this.options.clearStyle ) {
            this.toShow.add( this.toHide ).css({
                height: "",
                overflow: ""
            });
        }

        // other classes are removed before the animation; this one needs to stay until completed
        this.toHide.removeClass( "ui-accordion-content-active" );
        // Work around for rendering bug in IE (#5421)
        if ( this.toHide.length ) {
            this.toHide.parent()[0].className = this.toHide.parent()[0].className;
        }

        this._trigger( "change", null, this.data );
    }
});

$.extend( $.ui.accordion, {
    version: "1.8.18",
    animations: {
        slide: function( options, additions ) {
            options = $.extend({
                easing: "swing",
                duration: 300
            }, options, additions );
            if ( !options.toHide.size() ) {
                options.toShow.animate({
                    height: "show",
                    paddingTop: "show",
                    paddingBottom: "show"
                }, options );
                return;
            }
            if ( !options.toShow.size() ) {
                options.toHide.animate({
                    height: "hide",
                    paddingTop: "hide",
                    paddingBottom: "hide"
                }, options );
                return;
            }
            var overflow = options.toShow.css( "overflow" ),
                percentDone = 0,
                showProps = {},
                hideProps = {},
                fxAttrs = [ "height", "paddingTop", "paddingBottom" ],
                originalWidth;
            // fix width before calculating height of hidden element
            var s = options.toShow;
            originalWidth = s[0].style.width;
            s.width( s.parent().width()
                - parseFloat( s.css( "paddingLeft" ) )
                - parseFloat( s.css( "paddingRight" ) )
                - ( parseFloat( s.css( "borderLeftWidth" ) ) || 0 )
                - ( parseFloat( s.css( "borderRightWidth" ) ) || 0 ) );

            $.each( fxAttrs, function( i, prop ) {
                hideProps[ prop ] = "hide";

                var parts = ( "" + $.css( options.toShow[0], prop ) ).match( /^([\d+-.]+)(.*)$/ );
                showProps[ prop ] = {
                    value: parts[ 1 ],
                    unit: parts[ 2 ] || "px"
                };
            });
            options.toShow.css({ height: 0, overflow: "hidden" }).show();
            options.toHide
                .filter( ":hidden" )
                    .each( options.complete )
                .end()
                .filter( ":visible" )
                .animate( hideProps, {
                step: function( now, settings ) {
                    // only calculate the percent when animating height
                    // IE gets very inconsistent results when animating elements
                    // with small values, which is common for padding
                    if ( settings.prop == "height" ) {
                        percentDone = ( settings.end - settings.start === 0 ) ? 0 :
                            ( settings.now - settings.start ) / ( settings.end - settings.start );
                    }

                    options.toShow[ 0 ].style[ settings.prop ] =
                        ( percentDone * showProps[ settings.prop ].value )
                        + showProps[ settings.prop ].unit;
                },
                duration: options.duration,
                easing: options.easing,
                complete: function() {
                    if ( !options.autoHeight ) {
                        options.toShow.css( "height", "" );
                    }
                    options.toShow.css({
                        width: originalWidth,
                        overflow: overflow
                    });
                    options.complete();
                }
            });
        },
        bounceslide: function( options ) {
            this.slide( options, {
                easing: options.down ? "easeOutBounce" : "swing",
                duration: options.down ? 1000 : 200
            });
        }
    }
});

})( jQuery );