wikimedia/mediawiki-core

View on GitHub
resources/lib/jquery.ui/jquery.ui.dialog.js

Summary

Maintainability
F
5 days
Test Coverage
/*!
 * jQuery UI Dialog 1.9.2
 * http://jqueryui.com
 *
 * Copyright 2012 jQuery Foundation and other contributors
 * Released under the MIT license.
 * http://jquery.org/license
 *
 * http://api.jqueryui.com/dialog/
 *
 * Depends:
 *    jquery.ui.core.js
 *    jquery.ui.widget.js
 *  jquery.ui.button.js
 *    jquery.ui.draggable.js
 *    jquery.ui.mouse.js
 *    jquery.ui.position.js
 *    jquery.ui.resizable.js
 */
(function( $, undefined ) {

var uiDialogClasses = "ui-dialog ui-widget ui-widget-content ui-corner-all ",
    sizeRelatedOptions = {
        buttons: true,
        height: true,
        maxHeight: true,
        maxWidth: true,
        minHeight: true,
        minWidth: true,
        width: true
    },
    resizableRelatedOptions = {
        maxHeight: true,
        maxWidth: true,
        minHeight: true,
        minWidth: true
    };

$.widget("ui.dialog", {
    version: "1.9.2",
    options: {
        autoOpen: true,
        buttons: {},
        closeOnEscape: true,
        closeText: "close",
        dialogClass: "",
        draggable: true,
        hide: null,
        height: "auto",
        maxHeight: false,
        maxWidth: false,
        minHeight: 150,
        minWidth: 150,
        modal: false,
        position: {
            my: "center",
            at: "center",
            of: window,
            collision: "fit",
            // ensure that the titlebar is never outside the document
            using: function( pos ) {
                var topOffset = $( this ).css( pos ).offset().top;
                if ( topOffset < 0 ) {
                    $( this ).css( "top", pos.top - topOffset );
                }
            }
        },
        resizable: true,
        show: null,
        stack: true,
        title: "",
        width: 300,
        zIndex: 1000
    },

    _create: function() {
        this.originalTitle = this.element.attr( "title" );
        // #5742 - .attr() might return a DOMElement
        if ( typeof this.originalTitle !== "string" ) {
            this.originalTitle = "";
        }
        this.oldPosition = {
            parent: this.element.parent(),
            index: this.element.parent().children().index( this.element )
        };
        this.options.title = this.options.title || this.originalTitle;
        var that = this,
            options = this.options,

            title = options.title || "&#160;",
            uiDialog,
            uiDialogTitlebar,
            uiDialogTitlebarClose,
            uiDialogTitle,
            uiDialogButtonPane;

            uiDialog = ( this.uiDialog = $( "<div>" ) )
                .addClass( uiDialogClasses + options.dialogClass )
                .css({
                    display: "none",
                    outline: "none", // TODO: move to stylesheet
                    zIndex: options.zIndex
                })
                // setting tabIndex makes the div focusable
                .attr( "tabIndex", -1)
                .keydown(function( event ) {
                    if ( options.closeOnEscape && !event.isDefaultPrevented() && event.keyCode &&
                            event.keyCode === $.ui.keyCode.ESCAPE ) {
                        that.close( event );
                        event.preventDefault();
                    }
                })
                .mousedown(function( event ) {
                    that.moveToTop( false, event );
                })
                .appendTo( "body" );

            this.element
                .show()
                .removeAttr( "title" )
                .addClass( "ui-dialog-content ui-widget-content" )
                .appendTo( uiDialog );

            uiDialogTitlebar = ( this.uiDialogTitlebar = $( "<div>" ) )
                .addClass( "ui-dialog-titlebar  ui-widget-header  " +
                    "ui-corner-all  ui-helper-clearfix" )
                .on( "mousedown", function() {
                    // Dialog isn't getting focus when dragging (#8063)
                    uiDialog.focus();
                })
                .prependTo( uiDialog );

            uiDialogTitlebarClose = $( "<a href='#'></a>" )
                .addClass( "ui-dialog-titlebar-close  ui-corner-all" )
                .attr( "role", "button" )
                .click(function( event ) {
                    event.preventDefault();
                    that.close( event );
                })
                .appendTo( uiDialogTitlebar );

            ( this.uiDialogTitlebarCloseText = $( "<span>" ) )
                .addClass( "ui-icon ui-icon-closethick" )
                .text( options.closeText )
                .appendTo( uiDialogTitlebarClose );

            uiDialogTitle = $( "<span>" )
                .uniqueId()
                .addClass( "ui-dialog-title" )
                .html( title )
                .prependTo( uiDialogTitlebar );

            uiDialogButtonPane = ( this.uiDialogButtonPane = $( "<div>" ) )
                .addClass( "ui-dialog-buttonpane ui-widget-content ui-helper-clearfix" );

            ( this.uiButtonSet = $( "<div>" ) )
                .addClass( "ui-dialog-buttonset" )
                .appendTo( uiDialogButtonPane );

        uiDialog.attr({
            role: "dialog",
            "aria-labelledby": uiDialogTitle.attr( "id" )
        });

        uiDialogTitlebar.find( "*" ).add( uiDialogTitlebar ).disableSelection();
        this._hoverable( uiDialogTitlebarClose );
        this._focusable( uiDialogTitlebarClose );

        if ( options.draggable && $.fn.draggable ) {
            this._makeDraggable();
        }
        if ( options.resizable && $.fn.resizable ) {
            this._makeResizable();
        }

        this._createButtons( options.buttons );
        this._isOpen = false;

        if ( $.fn.bgiframe ) {
            uiDialog.bgiframe();
        }

        // prevent tabbing out of modal dialogs
        this._on( uiDialog, { keydown: function( event ) {
            if ( !options.modal || event.keyCode !== $.ui.keyCode.TAB ) {
                return;
            }

            var tabbables = $( ":tabbable", uiDialog ),
                first = tabbables.filter( ":first" ),
                last  = tabbables.filter( ":last" );

            if ( event.target === last[0] && !event.shiftKey ) {
                first.focus( 1 );
                return false;
            } else if ( event.target === first[0] && event.shiftKey ) {
                last.focus( 1 );
                return false;
            }
        }});
    },

    _init: function() {
        if ( this.options.autoOpen ) {
            this.open();
        }
    },

    _destroy: function() {
        var next,
            oldPosition = this.oldPosition;

        if ( this.overlay ) {
            this.overlay.destroy();
        }
        this.uiDialog.hide();
        this.element
            .removeClass( "ui-dialog-content ui-widget-content" )
            .hide()
            .appendTo( "body" );
        this.uiDialog.remove();

        if ( this.originalTitle ) {
            this.element.attr( "title", this.originalTitle );
        }

        next = oldPosition.parent.children().eq( oldPosition.index );
        // Don't try to place the dialog next to itself (#8613)
        if ( next.length && next[ 0 ] !== this.element[ 0 ] ) {
            next.before( this.element );
        } else {
            oldPosition.parent.append( this.element );
        }
    },

    widget: function() {
        return this.uiDialog;
    },

    close: function( event ) {
        var that = this,
            maxZ, thisZ;

        if ( !this._isOpen ) {
            return;
        }

        if ( false === this._trigger( "beforeClose", event ) ) {
            return;
        }

        this._isOpen = false;

        if ( this.overlay ) {
            this.overlay.destroy();
        }

        if ( this.options.hide ) {
            this._hide( this.uiDialog, this.options.hide, function() {
                that._trigger( "close", event );
            });
        } else {
            this.uiDialog.hide();
            this._trigger( "close", event );
        }

        $.ui.dialog.overlay.resize();

        // adjust the maxZ to allow other modal dialogs to continue to work (see #4309)
        if ( this.options.modal ) {
            maxZ = 0;
            $( ".ui-dialog" ).each(function() {
                if ( this !== that.uiDialog[0] ) {
                    thisZ = $( this ).css( "z-index" );
                    if ( !isNaN( thisZ ) ) {
                        maxZ = Math.max( maxZ, thisZ );
                    }
                }
            });
            $.ui.dialog.maxZ = maxZ;
        }

        return this;
    },

    isOpen: function() {
        return this._isOpen;
    },

    // the force parameter allows us to move modal dialogs to their correct
    // position on open
    moveToTop: function( force, event ) {
        var options = this.options,
            saveScroll;

        if ( ( options.modal && !force ) ||
                ( !options.stack && !options.modal ) ) {
            return this._trigger( "focus", event );
        }

        if ( options.zIndex > $.ui.dialog.maxZ ) {
            $.ui.dialog.maxZ = options.zIndex;
        }
        if ( this.overlay ) {
            $.ui.dialog.maxZ += 1;
            $.ui.dialog.overlay.maxZ = $.ui.dialog.maxZ;
            this.overlay.$el.css( "z-index", $.ui.dialog.overlay.maxZ );
        }

        // Save and then restore scroll
        // Opera 9.5+ resets when parent z-index is changed.
        // http://bugs.jqueryui.com/ticket/3193
        saveScroll = {
            scrollTop: this.element.scrollTop(),
            scrollLeft: this.element.scrollLeft()
        };
        $.ui.dialog.maxZ += 1;
        this.uiDialog.css( "z-index", $.ui.dialog.maxZ );
        this.element.attr( saveScroll );
        this._trigger( "focus", event );

        return this;
    },

    open: function() {
        if ( this._isOpen ) {
            return;
        }

        var hasFocus,
            options = this.options,
            uiDialog = this.uiDialog;

        this._size();
        this._position( options.position );
        uiDialog.show( options.show );
        this.overlay = options.modal ? new $.ui.dialog.overlay( this ) : null;
        this.moveToTop( true );

        // set focus to the first tabbable element in the content area or the first button
        // if there are no tabbable elements, set focus on the dialog itself
        hasFocus = this.element.find( ":tabbable" );
        if ( !hasFocus.length ) {
            hasFocus = this.uiDialogButtonPane.find( ":tabbable" );
            if ( !hasFocus.length ) {
                hasFocus = uiDialog;
            }
        }
        hasFocus.eq( 0 ).focus();

        this._isOpen = true;
        this._trigger( "open" );

        return this;
    },

    _createButtons: function( buttons ) {
        var that = this,
            hasButtons = false;

        // if we already have a button pane, remove it
        this.uiDialogButtonPane.remove();
        this.uiButtonSet.empty();

        if ( typeof buttons === "object" && buttons !== null ) {
            $.each( buttons, function() {
                return !(hasButtons = true);
            });
        }
        if ( hasButtons ) {
            $.each( buttons, function( name, props ) {
                var button, click;
                props = typeof props === 'function' ?
                    { click: props, text: name } :
                    props;
                // Default to a non-submitting button
                props = $.extend( { type: "button" }, props );
                // Change the context for the click callback to be the main element
                click = props.click;
                props.click = function() {
                    click.apply( that.element[0], arguments );
                };
                button = $( "<button></button>", props )
                    .appendTo( that.uiButtonSet );
                if ( $.fn.button ) {
                    button.button();
                }
            });
            this.uiDialog.addClass( "ui-dialog-buttons" );
            this.uiDialogButtonPane.appendTo( this.uiDialog );
        } else {
            this.uiDialog.removeClass( "ui-dialog-buttons" );
        }
    },

    _makeDraggable: function() {
        var that = this,
            options = this.options;

        function filteredUi( ui ) {
            return {
                position: ui.position,
                offset: ui.offset
            };
        }

        this.uiDialog.draggable({
            cancel: ".ui-dialog-content, .ui-dialog-titlebar-close",
            handle: ".ui-dialog-titlebar",
            containment: "document",
            start: function( event, ui ) {
                $( this )
                    .addClass( "ui-dialog-dragging" );
                that._trigger( "dragStart", event, filteredUi( ui ) );
            },
            drag: function( event, ui ) {
                that._trigger( "drag", event, filteredUi( ui ) );
            },
            stop: function( event, ui ) {
                options.position = [
                    ui.position.left - that.document.scrollLeft(),
                    ui.position.top - that.document.scrollTop()
                ];
                $( this )
                    .removeClass( "ui-dialog-dragging" );
                that._trigger( "dragStop", event, filteredUi( ui ) );
                $.ui.dialog.overlay.resize();
            }
        });
    },

    _makeResizable: function( handles ) {
        handles = (handles === undefined ? this.options.resizable : handles);
        var that = this,
            options = this.options,
            // .ui-resizable has position: relative defined in the stylesheet
            // but dialogs have to use absolute or fixed positioning
            position = this.uiDialog.css( "position" ),
            resizeHandles = typeof handles === 'string' ?
                handles    :
                "n,e,s,w,se,sw,ne,nw";

        function filteredUi( ui ) {
            return {
                originalPosition: ui.originalPosition,
                originalSize: ui.originalSize,
                position: ui.position,
                size: ui.size
            };
        }

        this.uiDialog.resizable({
            cancel: ".ui-dialog-content",
            containment: "document",
            alsoResize: this.element,
            maxWidth: options.maxWidth,
            maxHeight: options.maxHeight,
            minWidth: options.minWidth,
            minHeight: this._minHeight(),
            handles: resizeHandles,
            start: function( event, ui ) {
                $( this ).addClass( "ui-dialog-resizing" );
                that._trigger( "resizeStart", event, filteredUi( ui ) );
            },
            resize: function( event, ui ) {
                that._trigger( "resize", event, filteredUi( ui ) );
            },
            stop: function( event, ui ) {
                $( this ).removeClass( "ui-dialog-resizing" );
                options.height = $( this ).height();
                options.width = $( this ).width();
                that._trigger( "resizeStop", event, filteredUi( ui ) );
                $.ui.dialog.overlay.resize();
            }
        })
        .css( "position", position )
        .find( ".ui-resizable-se" )
            .addClass( "ui-icon ui-icon-grip-diagonal-se" );
    },

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

        if ( options.height === "auto" ) {
            return options.minHeight;
        } else {
            return Math.min( options.minHeight, options.height );
        }
    },

    _position: function( position ) {
        var myAt = [],
            offset = [ 0, 0 ],
            isVisible;

        if ( position ) {
            // deep extending converts arrays to objects in jQuery <= 1.3.2 :-(
    //        if (typeof position == 'string' || $.isArray(position)) {
    //            myAt = $.isArray(position) ? position : position.split(' ');

            if ( typeof position === "string" || (typeof position === "object" && "0" in position ) ) {
                myAt = position.split ? position.split( " " ) : [ position[ 0 ], position[ 1 ] ];
                if ( myAt.length === 1 ) {
                    myAt[ 1 ] = myAt[ 0 ];
                }

                $.each( [ "left", "top" ], function( i, offsetPosition ) {
                    if ( +myAt[ i ] === myAt[ i ] ) {
                        offset[ i ] = myAt[ i ];
                        myAt[ i ] = offsetPosition;
                    }
                });

                position = {
                    my: myAt[0] + (offset[0] < 0 ? offset[0] : "+" + offset[0]) + " " +
                        myAt[1] + (offset[1] < 0 ? offset[1] : "+" + offset[1]),
                    at: myAt.join( " " )
                };
            }

            position = $.extend( {}, $.ui.dialog.prototype.options.position, position );
        } else {
            position = $.ui.dialog.prototype.options.position;
        }

        // need to show the dialog to get the actual offset in the position plugin
        isVisible = this.uiDialog.is( ":visible" );
        if ( !isVisible ) {
            this.uiDialog.show();
        }
        this.uiDialog.position( position );
        if ( !isVisible ) {
            this.uiDialog.hide();
        }
    },

    _setOptions: function( options ) {
        var that = this,
            resizableOptions = {},
            resize = false;

        $.each( options, function( key, value ) {
            that._setOption( key, value );

            if ( key in sizeRelatedOptions ) {
                resize = true;
            }
            if ( key in resizableRelatedOptions ) {
                resizableOptions[ key ] = value;
            }
        });

        if ( resize ) {
            this._size();
        }
        if ( this.uiDialog.is( ":data(resizable)" ) ) {
            this.uiDialog.resizable( "option", resizableOptions );
        }
    },

    _setOption: function( key, value ) {
        var isDraggable, isResizable,
            uiDialog = this.uiDialog;

        switch ( key ) {
            case "buttons":
                this._createButtons( value );
                break;
            case "closeText":
                // ensure that we always pass a string
                this.uiDialogTitlebarCloseText.text( "" + value );
                break;
            case "dialogClass":
                uiDialog
                    .removeClass( this.options.dialogClass )
                    .addClass( uiDialogClasses + value );
                break;
            case "disabled":
                if ( value ) {
                    uiDialog.addClass( "ui-dialog-disabled" );
                } else {
                    uiDialog.removeClass( "ui-dialog-disabled" );
                }
                break;
            case "draggable":
                isDraggable = uiDialog.is( ":data(draggable)" );
                if ( isDraggable && !value ) {
                    uiDialog.draggable( "destroy" );
                }

                if ( !isDraggable && value ) {
                    this._makeDraggable();
                }
                break;
            case "position":
                this._position( value );
                break;
            case "resizable":
                // currently resizable, becoming non-resizable
                isResizable = uiDialog.is( ":data(resizable)" );
                if ( isResizable && !value ) {
                    uiDialog.resizable( "destroy" );
                }

                // currently resizable, changing handles
                if ( isResizable && typeof value === "string" ) {
                    uiDialog.resizable( "option", "handles", value );
                }

                // currently non-resizable, becoming resizable
                if ( !isResizable && value !== false ) {
                    this._makeResizable( value );
                }
                break;
            case "title":
                // convert whatever was passed in o a string, for html() to not throw up
                $( ".ui-dialog-title", this.uiDialogTitlebar )
                    .html( "" + ( value || "&#160;" ) );
                break;
        }

        this._super( key, value );
    },

    _size: function() {
        /* If the user has resized the dialog, the .ui-dialog and .ui-dialog-content
         * divs will both have width and height set, so we need to reset them
         */
        var nonContentHeight, minContentHeight, autoHeight,
            options = this.options,
            isVisible = this.uiDialog.is( ":visible" );

        // reset content sizing
        this.element.show().css({
            width: "auto",
            minHeight: 0,
            height: 0
        });

        if ( options.minWidth > options.width ) {
            options.width = options.minWidth;
        }

        // reset wrapper sizing
        // determine the height of all the non-content elements
        nonContentHeight = this.uiDialog.css({
                height: "auto",
                width: options.width
            })
            .outerHeight();
        minContentHeight = Math.max( 0, options.minHeight - nonContentHeight );

        if ( options.height === "auto" ) {
            // only needed for IE6 support
            if ( $.support.minHeight ) {
                this.element.css({
                    minHeight: minContentHeight,
                    height: "auto"
                });
            } else {
                this.uiDialog.show();
                autoHeight = this.element.css( "height", "auto" ).height();
                if ( !isVisible ) {
                    this.uiDialog.hide();
                }
                this.element.height( Math.max( autoHeight, minContentHeight ) );
            }
        } else {
            this.element.height( Math.max( options.height - nonContentHeight, 0 ) );
        }

        if (this.uiDialog.is( ":data(resizable)" ) ) {
            this.uiDialog.resizable( "option", "minHeight", this._minHeight() );
        }
    }
});

$.extend($.ui.dialog, {
    uuid: 0,
    maxZ: 0,

    getTitleId: function($el) {
        var id = $el.attr( "id" );
        if ( !id ) {
            this.uuid += 1;
            id = this.uuid;
        }
        return "ui-dialog-title-" + id;
    },

    overlay: function( dialog ) {
        this.$el = $.ui.dialog.overlay.create( dialog );
    }
});

$.extend( $.ui.dialog.overlay, {
    instances: [],
    // reuse old instances due to IE memory leak with alpha transparency (see #5185)
    oldInstances: [],
    maxZ: 0,
    events: $.map(
        "focus,mousedown,mouseup,keydown,keypress,click".split( "," ),
        function( event ) {
            return event + ".dialog-overlay";
        }
    ).join( " " ),
    create: function( dialog ) {
        if ( this.instances.length === 0 ) {
            // prevent use of anchors and inputs
            // we use a setTimeout in case the overlay is created from an
            // event that we're going to be cancelling (see #2804)
            setTimeout(function() {
                // handle $(el).dialog().dialog('close') (see #4065)
                if ( $.ui.dialog.overlay.instances.length ) {
                    $( document ).on( $.ui.dialog.overlay.events, function( event ) {
                        // stop events if the z-index of the target is < the z-index of the overlay
                        // we cannot return true when we don't want to cancel the event (#3523)
                        if ( $( event.target ).zIndex() < $.ui.dialog.overlay.maxZ ) {
                            return false;
                        }
                    });
                }
            }, 1 );

            // handle window resize
            $( window ).on( "resize.dialog-overlay", $.ui.dialog.overlay.resize );
        }

        var $el = ( this.oldInstances.pop() || $( "<div>" ).addClass( "ui-widget-overlay" ) );

        // allow closing by pressing the escape key
        $( document ).on( "keydown.dialog-overlay", function( event ) {
            var instances = $.ui.dialog.overlay.instances;
            // only react to the event if we're the top overlay
            if ( instances.length !== 0 && instances[ instances.length - 1 ] === $el &&
                dialog.options.closeOnEscape && !event.isDefaultPrevented() && event.keyCode &&
                event.keyCode === $.ui.keyCode.ESCAPE ) {

                dialog.close( event );
                event.preventDefault();
            }
        });

        $el.appendTo( document.body ).css({
            width: this.width(),
            height: this.height()
        });

        if ( $.fn.bgiframe ) {
            $el.bgiframe();
        }

        this.instances.push( $el );
        return $el;
    },

    destroy: function( $el ) {
        var indexOf = $.inArray( $el, this.instances ),
            maxZ = 0;

        if ( indexOf !== -1 ) {
            this.oldInstances.push( this.instances.splice( indexOf, 1 )[ 0 ] );
        }

        if ( this.instances.length === 0 ) {
            $( [ document, window ] ).off( ".dialog-overlay" );
        }

        $el.height( 0 ).width( 0 ).remove();

        // adjust the maxZ to allow other modal dialogs to continue to work (see #4309)
        $.each( this.instances, function() {
            maxZ = Math.max( maxZ, this.css( "z-index" ) );
        });
        this.maxZ = maxZ;
    },

    height: function() {
        var scrollHeight,
            offsetHeight;
        // handle IE
        if ( $.ui.ie ) {
            scrollHeight = Math.max(
                document.documentElement.scrollHeight,
                document.body.scrollHeight
            );
            offsetHeight = Math.max(
                document.documentElement.offsetHeight,
                document.body.offsetHeight
            );

            if ( scrollHeight < offsetHeight ) {
                return $( window ).height() + "px";
            } else {
                return scrollHeight + "px";
            }
        // handle "good" browsers
        } else {
            return $( document ).height() + "px";
        }
    },

    width: function() {
        var scrollWidth,
            offsetWidth;
        // handle IE
        if ( $.ui.ie ) {
            scrollWidth = Math.max(
                document.documentElement.scrollWidth,
                document.body.scrollWidth
            );
            offsetWidth = Math.max(
                document.documentElement.offsetWidth,
                document.body.offsetWidth
            );

            if ( scrollWidth < offsetWidth ) {
                return $( window ).width() + "px";
            } else {
                return scrollWidth + "px";
            }
        // handle "good" browsers
        } else {
            return $( document ).width() + "px";
        }
    },

    resize: function() {
        /* If the dialog is draggable and the user drags it past the
         * right edge of the window, the document becomes wider so we
         * need to stretch the overlay. If the user then drags the
         * dialog back to the left, the document will become narrower,
         * so we need to shrink the overlay to the appropriate size.
         * This is handled by shrinking the overlay before setting it
         * to the full document size.
         */
        var $overlays = $( [] );
        $.each( $.ui.dialog.overlay.instances, function() {
            $overlays = $overlays.add( this );
        });

        $overlays.css({
            width: 0,
            height: 0
        }).css({
            width: $.ui.dialog.overlay.width(),
            height: $.ui.dialog.overlay.height()
        });
    }
});

$.extend( $.ui.dialog.overlay.prototype, {
    destroy: function() {
        $.ui.dialog.overlay.destroy( this.$el );
    }
});

}( jQuery ) );