| //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude); |
| //>>description: Popup windows |
| //>>label: Popups |
| //>>group: Widgets |
| //>>css.theme: ../css/themes/default/jquery.mobile.theme.css |
| //>>css.structure: ../css/structure/jquery.mobile.popup.css,../css/structure/jquery.mobile.transition.css,../css/structure/jquery.mobile.transition.fade.css |
| |
| define( [ "jquery", |
| "../jquery.mobile.widget", |
| "../jquery.mobile.support", |
| "../jquery.mobile.navigation", |
| "depend!../jquery.hashchange[jquery]" ], function( $ ) { |
| //>>excludeEnd("jqmBuildExclude"); |
| (function( $, undefined ) { |
| |
| function fitSegmentInsideSegment( winSize, segSize, offset, desired ) { |
| var ret = desired; |
| |
| if ( winSize < segSize ) { |
| // Center segment if it's bigger than the window |
| ret = offset + ( winSize - segSize ) / 2; |
| } else { |
| // Otherwise center it at the desired coordinate while keeping it completely inside the window |
| ret = Math.min( Math.max( offset, desired - segSize / 2 ), offset + winSize - segSize ); |
| } |
| |
| return ret; |
| } |
| |
| function windowCoords() { |
| var $win = $.mobile.$window; |
| |
| return { |
| x: $win.scrollLeft(), |
| y: $win.scrollTop(), |
| cx: ( window.innerWidth || $win.width() ), |
| cy: ( window.innerHeight || $win.height() ) |
| }; |
| } |
| |
| $.widget( "mobile.popup", $.mobile.widget, { |
| options: { |
| theme: null, |
| overlayTheme: null, |
| shadow: true, |
| corners: true, |
| transition: "pop", |
| positionTo: "origin", |
| tolerance: null, |
| initSelector: ":jqmData(role='popup')", |
| closeLinkSelector: "a:jqmData(rel='back')", |
| closeLinkEvents: "click.popup", |
| navigateEvents: "navigate.popup", |
| closeEvents: "navigate.popup pagebeforechange.popup", |
| |
| // NOTE Windows Phone 7 has a scroll position caching issue that |
| // requires us to disable popup history management by default |
| // https://github.com/jquery/jquery-mobile/issues/4784 |
| // |
| // NOTE this option is modified in _create! |
| history: false |
| }, |
| |
| _eatEventAndClose: function( e ) { |
| e.preventDefault(); |
| e.stopImmediatePropagation(); |
| this.close(); |
| return false; |
| }, |
| |
| // Make sure the screen size is increased beyond the page height if the popup's causes the document to increase in height |
| _resizeScreen: function() { |
| var popupHeight = this._ui.container.outerHeight( true ); |
| |
| this._ui.screen.removeAttr( "style" ); |
| if ( popupHeight > this._ui.screen.height() ) { |
| this._ui.screen.height( popupHeight ); |
| } |
| }, |
| |
| _handleWindowKeyUp: function( e ) { |
| if ( this._isOpen && e.keyCode === $.mobile.keyCode.ESCAPE ) { |
| return this._eatEventAndClose( e ); |
| } |
| }, |
| |
| _maybeRefreshTimeout: function() { |
| var winCoords = windowCoords(); |
| |
| if ( this._resizeData ) { |
| if ( winCoords.x === this._resizeData.winCoords.x && |
| winCoords.y === this._resizeData.winCoords.y && |
| winCoords.cx === this._resizeData.winCoords.cx && |
| winCoords.cy === this._resizeData.winCoords.cy ) { |
| // timeout not refreshed |
| return false; |
| } else { |
| // clear existing timeout - it will be refreshed below |
| clearTimeout( this._resizeData.timeoutId ); |
| } |
| } |
| |
| this._resizeData = { |
| timeoutId: setTimeout( $.proxy( this, "_resizeTimeout" ), 200 ), |
| winCoords: winCoords |
| }; |
| |
| return true; |
| }, |
| |
| _resizeTimeout: function() { |
| if ( !this._maybeRefreshTimeout() && this.positionTo === "window" ) { |
| // effectively rapid-open the popup while leaving the screen intact |
| this._trigger( "beforeposition" ); |
| this._ui.container |
| .removeClass( "ui-selectmenu-hidden" ) |
| .offset( this._placementCoords( this._desiredCoords( undefined, undefined, "window" ) ) ); |
| |
| this._resizeScreen(); |
| this._resizeData = null; |
| this._orientationchangeInProgress = false; |
| } |
| }, |
| |
| _handleWindowResize: function( e ) { |
| if ( this._isOpen ) { |
| // Context popup close when Window resize event |
| if( this.positionTo !== "window" ) { |
| this.close(); |
| return false; |
| } |
| this._maybeRefreshTimeout(); |
| } |
| }, |
| |
| _handleWindowOrientationchange: function( e ) { |
| |
| if ( !this._orientationchangeInProgress ) { |
| // effectively rapid-close the popup while leaving the screen intact |
| this._ui.container |
| .addClass( "ui-selectmenu-hidden" ) |
| .removeAttr( "style" ); |
| |
| this._orientationchangeInProgress = true; |
| } |
| }, |
| |
| _create: function() { |
| var ui = { |
| screen: $( "<div class='ui-screen-hidden ui-popup-screen'></div>" ), |
| placeholder: $( "<div style='display: none;'><!-- placeholder --></div>" ), |
| container: $( "<div class='ui-popup-container ui-selectmenu-hidden'></div>" ), |
| arrow : $("<div class='ui-arrow'></div>") |
| }, |
| thisPage = this.element.closest( ".ui-page" ), |
| myId = this.element.attr( "id" ), |
| self = this; |
| |
| // We need to adjust the history option to be false if there's no AJAX nav. |
| // We can't do it in the option declarations because those are run before |
| // it is determined whether there shall be AJAX nav. |
| this.options.history = this.options.history && $.mobile.ajaxEnabled && $.mobile.hashListeningEnabled; |
| |
| if ( thisPage.length === 0 ) { |
| thisPage = $( "body" ); |
| } |
| |
| // define the container for navigation event bindings |
| // TODO this would be nice at the the mobile widget level |
| this.options.container = this.options.container || $.mobile.pageContainer; |
| |
| // Apply the proto |
| thisPage.append( ui.screen ); |
| ui.container.insertAfter( ui.screen ); |
| // Leave a placeholder where the element used to be |
| ui.placeholder.insertAfter( this.element ); |
| if ( myId ) { |
| ui.screen.attr( "id", myId + "-screen" ); |
| ui.container.attr( "id", myId + "-popup" ); |
| ui.placeholder.html( "<!-- placeholder for " + myId + " -->" ); |
| } |
| ui.container.append( this.element ); |
| ui.container.append( ui.arrow ); |
| // Add class to popup element |
| this.element.addClass( "ui-popup" ); |
| |
| // Define instance variables |
| $.extend( this, { |
| _page: thisPage, |
| _ui: ui, |
| _fallbackTransition: "", |
| _currentTransition: false, |
| _prereqs: null, |
| _isOpen: false, |
| _tolerance: null, |
| _resizeData: null, |
| _orientationchangeInProgress: false, |
| _globalHandlers: [ |
| { |
| src: $.mobile.$window, |
| handler: { |
| orientationchange: $.proxy( this, "_handleWindowOrientationchange" ), |
| resize: $.proxy( this, "_handleWindowResize" ), |
| keyup: $.proxy( this, "_handleWindowKeyUp" ) |
| } |
| } |
| ] |
| }); |
| |
| $.each( this.options, function( key, value ) { |
| // Cause initial options to be applied by their handler by temporarily setting the option to undefined |
| // - the handler then sets it to the initial value |
| self.options[ key ] = undefined; |
| self._setOption( key, value, true ); |
| }); |
| |
| ui.screen.bind( "vclick", $.proxy( this, "_eatEventAndClose" ) ); |
| |
| $.each( this._globalHandlers, function( idx, value ) { |
| value.src.bind( value.handler ); |
| }); |
| }, |
| |
| _applyTheme: function( dst, theme, prefix ) { |
| var classes = ( dst.attr( "class" ) || "").split( " " ), |
| alreadyAdded = true, |
| currentTheme = null, |
| matches, |
| themeStr = String( theme ); |
| |
| while ( classes.length > 0 ) { |
| currentTheme = classes.pop(); |
| matches = ( new RegExp( "^ui-" + prefix + "-([a-z])$" ) ).exec( currentTheme ); |
| if ( matches && matches.length > 1 ) { |
| currentTheme = matches[ 1 ]; |
| break; |
| } else { |
| currentTheme = null; |
| } |
| } |
| |
| if ( theme !== currentTheme ) { |
| dst.removeClass( "ui-" + prefix + "-" + currentTheme ); |
| if ( ! ( theme === null || theme === "none" ) ) { |
| dst.addClass( "ui-" + prefix + "-" + themeStr ); |
| } |
| } |
| }, |
| |
| _setTheme: function( value ) { |
| this._applyTheme( this.element, value, "body" ); |
| }, |
| |
| _setOverlayTheme: function( value ) { |
| this._applyTheme( this._ui.screen, value, "overlay" ); |
| |
| if ( this._isOpen ) { |
| this._ui.screen.addClass( "in" ); |
| } |
| }, |
| |
| _setShadow: function( value ) { |
| this.element.toggleClass( "ui-overlay-shadow", value ); |
| }, |
| |
| _setCorners: function( value ) { |
| this.element.toggleClass( "ui-corner-all", value ); |
| }, |
| |
| _applyTransition: function( value ) { |
| this._ui.container.removeClass( this._fallbackTransition ); |
| if ( value && value !== "none" ) { |
| this._fallbackTransition = $.mobile._maybeDegradeTransition( value ); |
| this._ui.container.addClass( this._fallbackTransition ); |
| } |
| }, |
| |
| _setTransition: function( value ) { |
| if ( !this._currentTransition ) { |
| this._applyTransition( value ); |
| } |
| }, |
| |
| _setTolerance: function( value ) { |
| var tol = { t: 5, r: 5, b: 5, l: 5 }; |
| |
| if ( value ) { |
| var ar = String( value ).split( "," ); |
| |
| $.each( ar, function( idx, val ) { ar[ idx ] = parseInt( val, 10 ); } ); |
| |
| switch( ar.length ) { |
| // All values are to be the same |
| case 1: |
| if ( !isNaN( ar[ 0 ] ) ) { |
| tol.t = tol.r = tol.b = tol.l = ar[ 0 ]; |
| } |
| break; |
| |
| // The first value denotes top/bottom tolerance, and the second value denotes left/right tolerance |
| case 2: |
| if ( !isNaN( ar[ 0 ] ) ) { |
| tol.t = tol.b = ar[ 0 ]; |
| } |
| if ( !isNaN( ar[ 1 ] ) ) { |
| tol.l = tol.r = ar[ 1 ]; |
| } |
| break; |
| |
| // The array contains values in the order top, right, bottom, left |
| case 4: |
| if ( !isNaN( ar[ 0 ] ) ) { |
| tol.t = ar[ 0 ]; |
| } |
| if ( !isNaN( ar[ 1 ] ) ) { |
| tol.r = ar[ 1 ]; |
| } |
| if ( !isNaN( ar[ 2 ] ) ) { |
| tol.b = ar[ 2 ]; |
| } |
| if ( !isNaN( ar[ 3 ] ) ) { |
| tol.l = ar[ 3 ]; |
| } |
| break; |
| |
| default: |
| break; |
| } |
| } |
| |
| this._tolerance = tol; |
| }, |
| |
| _setOption: function( key, value ) { |
| var exclusions, setter = "_set" + key.charAt( 0 ).toUpperCase() + key.slice( 1 ); |
| |
| if ( this[ setter ] !== undefined ) { |
| this[ setter ]( value ); |
| } |
| |
| // TODO REMOVE FOR 1.2.1 by moving them out to a default options object |
| exclusions = [ |
| "initSelector", |
| "closeLinkSelector", |
| "closeLinkEvents", |
| "navigateEvents", |
| "closeEvents", |
| "history", |
| "container" |
| ]; |
| |
| $.mobile.widget.prototype._setOption.apply( this, arguments ); |
| if ( $.inArray( key, exclusions ) === -1 ) { |
| // Record the option change in the options and in the DOM data-* attributes |
| this.element.attr( "data-" + ( $.mobile.ns || "" ) + ( key.replace( /([A-Z])/, "-$1" ).toLowerCase() ), value ); |
| } |
| }, |
| |
| // Try and center the overlay over the given coordinates |
| _placementCoords: function( desired ) { |
| // rectangle within which the popup must fit |
| var |
| winCoords = windowCoords(), |
| rc = { |
| x: this._tolerance.l, |
| y: winCoords.y + this._tolerance.t, |
| cx: winCoords.cx - this._tolerance.l - this._tolerance.r, |
| cy: winCoords.cy - this._tolerance.t - this._tolerance.b |
| }, |
| menuSize, ret, |
| linkOffset = $(this.link).offset(), |
| positionOffsets = [], |
| correctionValue = [0,0], |
| arrayIdx; |
| |
| // Clamp the width of the menu before grabbing its size |
| this._ui.container.css( "max-width", rc.cx ); |
| menuSize = { |
| cx: this._ui.container.outerWidth( true ), |
| cy: this._ui.container.outerHeight( true ) |
| }; |
| |
| // Center the menu over the desired coordinates, while not going outside |
| // the window tolerances. This will center wrt. the window if the popup is too large. |
| ret = { |
| x: fitSegmentInsideSegment( rc.cx, menuSize.cx, rc.x, desired.x ), |
| y: fitSegmentInsideSegment( rc.cy, menuSize.cy, rc.y, desired.y ) |
| }; |
| |
| // Make sure the top of the menu is visible |
| ret.y = Math.max( 0, ret.y ); |
| |
| // If the height of the menu is smaller than the height of the document |
| // align the bottom with the bottom of the document |
| |
| // fix for $( document ).height() bug in core 1.7.2. |
| var docEl = document.documentElement, docBody = document.body, |
| docHeight = Math.max( docEl.clientHeight, docBody.scrollHeight, docBody.offsetHeight, docEl.scrollHeight, docEl.offsetHeight ); |
| |
| ret.y -= Math.min( ret.y, Math.max( 0, ret.y + menuSize.cy - docHeight ) ); |
| |
| if ( this.positionTo !== "origin" ) |
| { |
| return { left: ret.x, top: ret.y , arrowleft: 0 , arrowtop: 0}; |
| } |
| |
| positionOffsets = [ linkOffset.left, |
| linkOffset.top, |
| docEl.clientHeight - ( linkOffset.top + $(this.link).height() ), |
| docEl.clientWidth - ( linkOffset.left + $(this.link).width() )]; |
| arrayIdx = positionOffsets.indexOf(Math.max.apply(window,positionOffsets)); |
| |
| switch( arrayIdx ) |
| { |
| case 0: |
| correctionValue = [ -$(this.link).width() , 0]; |
| arrowtop = ( linkOffset.top - ret.y ) + ( $(this.link).height() / 2 ) - parseInt( $(this._ui.arrow).css("border-width") ) ; |
| arrowleft = menuSize.cx; |
| $(this._ui.arrow).attr( "class", "" ) |
| .addClass( "ui-arrow left" ) |
| break; |
| case 1: |
| correctionValue = [ 0 , -(ret.y + menuSize.cy - linkOffset.top)]; |
| arrowtop = menuSize.cy - 2; |
| arrowleft = (linkOffset.left - ret.x + correctionValue[0]) + ( $(this.link).width() / 2 ) - parseInt( $(this._ui.arrow).css("border-width") ) / 2; |
| $(this._ui.arrow).attr( "class", "" ) |
| .addClass( "ui-arrow bottom" ); |
| break; |
| case 2: |
| correctionValue = [ 0 , ( linkOffset.top + $(this.link).height() - ret.y ) ]; |
| arrowtop = - parseInt( $(this._ui.arrow).css("border-width") ) * 2 + 1; |
| arrowleft = (linkOffset.left - ret.x + correctionValue[0]) + ( $(this.link).width() / 2 ) - parseInt( $(this._ui.arrow).css("border-width") ) / 2; |
| $(this._ui.arrow).attr( "class", "" ) |
| .addClass("ui-arrow top"); |
| break; |
| case 3: |
| correctionValue = [ ( menuSize.cx < $(this.link).width() ) ? ( $(this.link).width() / 2 ) + ( menuSize.cx / 2) : $(this.link).width() , 0]; |
| arrowtop = ( linkOffset.top - ret.y ) + ( $(this.link).height() / 2 ) - parseInt( $(this._ui.arrow).css("border-width") ) ; |
| arrowleft = - parseInt( $(this._ui.arrow).css("border-width") ) * 2; |
| $(this._ui.arrow).attr( "class", "" ) |
| .addClass("ui-arrow right"); |
| break; |
| } |
| |
| return { left: ret.x + correctionValue[0], top: ret.y + correctionValue[1] , arrowleft: arrowleft , arrowtop: arrowtop }; |
| }, |
| |
| _createPrereqs: function( screenPrereq, containerPrereq, whenDone ) { |
| var self = this, prereqs; |
| |
| // It is important to maintain both the local variable prereqs and self._prereqs. The local variable remains in |
| // the closure of the functions which call the callbacks passed in. The comparison between the local variable and |
| // self._prereqs is necessary, because once a function has been passed to .animationComplete() it will be called |
| // next time an animation completes, even if that's not the animation whose end the function was supposed to catch |
| // (for example, if an abort happens during the opening animation, the .animationComplete handler is not called for |
| // that animation anymore, but the handler remains attached, so it is called the next time the popup is opened |
| // - making it stale. Comparing the local variable prereqs to the widget-level variable self._prereqs ensures that |
| // callbacks triggered by a stale .animationComplete will be ignored. |
| |
| prereqs = { |
| screen: $.Deferred(), |
| container: $.Deferred() |
| }; |
| |
| prereqs.screen.then( function() { |
| if ( prereqs === self._prereqs ) { |
| screenPrereq(); |
| } |
| }); |
| |
| prereqs.container.then( function() { |
| if ( prereqs === self._prereqs ) { |
| containerPrereq(); |
| } |
| }); |
| |
| $.when( prereqs.screen, prereqs.container ).done( function() { |
| if ( prereqs === self._prereqs ) { |
| self._prereqs = null; |
| whenDone(); |
| } |
| }); |
| |
| self._prereqs = prereqs; |
| }, |
| |
| _animate: function( args ) { |
| // NOTE before removing the default animation of the screen |
| // this had an animate callback that would relove the deferred |
| // now the deferred is resolved immediately |
| // TODO remove the dependency on the screen deferred |
| this._ui.screen |
| .removeClass( args.classToRemove ) |
| .addClass( args.screenClassToAdd ); |
| |
| args.prereqs.screen.resolve(); |
| |
| if ( args.transition && args.transition !== "none" ) { |
| if ( args.applyTransition ) { |
| this._applyTransition( args.transition ); |
| } |
| this._ui.container |
| .animationComplete( $.proxy( args.prereqs.container, "resolve" ) ) |
| .addClass( args.containerClassToAdd ) |
| .removeClass( args.classToRemove ); |
| } else { |
| this._ui.container.removeClass( args.classToRemove ); |
| args.prereqs.container.resolve(); |
| } |
| }, |
| |
| // The desired coordinates passed in will be returned untouched if no reference element can be identified via |
| // desiredPosition.positionTo. Nevertheless, this function ensures that its return value always contains valid |
| // x and y coordinates by specifying the center middle of the window if the coordinates are absent. |
| _desiredCoords: function( x, y, positionTo ) { |
| var dst = null, offset, winCoords = windowCoords(); |
| |
| self.positionTo = positionTo; |
| |
| // Establish which element will serve as the reference |
| if ( positionTo && positionTo !== "origin" ) { |
| if ( positionTo === "window" ) { |
| x = winCoords.cx / 2 + winCoords.x; |
| y = winCoords.cy / 2 + winCoords.y; |
| } else { |
| try { |
| dst = $( positionTo ); |
| } catch( e ) { |
| dst = null; |
| } |
| if ( dst ) { |
| dst.filter( ":visible" ); |
| if ( dst.length === 0 ) { |
| dst = null; |
| } |
| } |
| } |
| } |
| |
| // If an element was found, center over it |
| if ( dst ) { |
| offset = dst.offset(); |
| x = offset.left + dst.outerWidth() / 2; |
| y = offset.top + dst.outerHeight() / 2; |
| } |
| |
| // Make sure x and y are valid numbers - center over the window |
| if ( $.type( x ) !== "number" || isNaN( x ) ) { |
| x = winCoords.cx / 2 + winCoords.x; |
| } |
| if ( $.type( y ) !== "number" || isNaN( y ) ) { |
| y = winCoords.cy / 2 + winCoords.y; |
| } |
| |
| return { x: x, y: y }; |
| }, |
| |
| _reposition: function() { |
| var self = this, |
| coords; |
| |
| if( self._isOpen |
| && self.link |
| && self.positionTo !== "window") { |
| coords = self._placementCoords( self._desiredCoords( $(self.link).offset().left + $(self.link).outerWidth() /2 , $(self.link).offset().top + $(self.link).outerHeight() /2 , self.positionTo || self.options.positionTo || "origin" ) ); |
| self._ui.container |
| .offset( { top : coords.top } ); |
| } |
| }, |
| |
| _openPrereqsComplete: function() { |
| var self = this; |
| |
| self._ui.container.addClass( "ui-popup-active" ); |
| self._isOpen = true; |
| self._resizeScreen(); |
| |
| // Android appears to trigger the animation complete before the popup |
| // is visible. Allowing the stack to unwind before applying focus prevents |
| // the "blue flash" of element focus in android 4.0 |
| setTimeout(function(){ |
| self._ui.container.attr( "tabindex", "0" ).focus(); |
| self._trigger( "afteropen" ); |
| self._reposition(); |
| }); |
| }, |
| |
| _open: function( options ) { |
| var coords, transition, |
| androidBlacklist = ( function() { |
| var w = window, |
| ua = navigator.userAgent, |
| // Rendering engine is Webkit, and capture major version |
| wkmatch = ua.match( /AppleWebKit\/([0-9\.]+)/ ), |
| wkversion = !!wkmatch && wkmatch[ 1 ], |
| androidmatch = ua.match( /Android (\d+(?:\.\d+))/ ), |
| andversion = !!androidmatch && androidmatch[ 1 ], |
| chromematch = ua.indexOf( "Chrome" ) > -1; |
| |
| // Platform is Android, WebKit version is greater than 534.13 ( Android 3.2.1 ) and not Chrome. |
| if( androidmatch !== null && andversion === "4.0" && wkversion && wkversion > 534.13 && !chromematch ) { |
| return true; |
| } |
| return false; |
| }()); |
| |
| // Make sure options is defined |
| options = ( options || {} ); |
| |
| // Copy out the transition, because we may be overwriting it later and we don't want to pass that change back to the caller |
| transition = options.transition || this.options.transition; |
| |
| // Give applications a chance to modify the contents of the container before it appears |
| this._trigger( "beforeposition" ); |
| |
| coords = this._placementCoords( this._desiredCoords( options.x, options.y, options.positionTo || this.options.positionTo || "origin" ) ); |
| |
| // Count down to triggering "popupafteropen" - we have two prerequisites: |
| // 1. The popup window animation completes (container()) |
| // 2. The screen opacity animation completes (screen()) |
| this._createPrereqs( |
| $.noop, |
| $.noop, |
| $.proxy( this, "_openPrereqsComplete" ) ); |
| |
| if ( transition ) { |
| this._currentTransition = transition; |
| this._applyTransition( transition ); |
| } else { |
| transition = this.options.transition; |
| } |
| |
| if ( !this.options.theme ) { |
| this._setTheme( this._page.jqmData( "theme" ) || $.mobile.getInheritedTheme( this._page, "c" ) ); |
| } |
| |
| this._ui.screen.removeClass( "ui-screen-hidden" ); |
| |
| this._ui.container |
| .removeClass( "ui-selectmenu-hidden" ) |
| .offset( coords ); |
| this._ui.arrow.css( { top : coords.arrowtop, left : coords.arrowleft } ); |
| if ( this.options.overlayTheme && androidBlacklist ) { |
| /* TODO: |
| The native browser on Android 4.0.X ("Ice Cream Sandwich") suffers from an issue where the popup overlay appears to be z-indexed |
| above the popup itself when certain other styles exist on the same page -- namely, any element set to `position: fixed` and certain |
| types of input. These issues are reminiscent of previously uncovered bugs in older versions of Android's native browser: |
| https://github.com/scottjehl/Device-Bugs/issues/3 |
| |
| This fix closes the following bugs ( I use "closes" with reluctance, and stress that this issue should be revisited as soon as possible ): |
| |
| https://github.com/jquery/jquery-mobile/issues/4816 |
| https://github.com/jquery/jquery-mobile/issues/4844 |
| https://github.com/jquery/jquery-mobile/issues/4874 |
| */ |
| |
| // TODO sort out why this._page isn't working |
| this.element.closest( ".ui-page" ).addClass( "ui-popup-open" ); |
| } |
| this._animate({ |
| additionalCondition: true, |
| transition: transition, |
| classToRemove: "", |
| screenClassToAdd: "in", |
| containerClassToAdd: "in", |
| applyTransition: false, |
| prereqs: this._prereqs |
| }); |
| }, |
| |
| _closePrereqScreen: function() { |
| this._ui.screen |
| .removeClass( "out" ) |
| .addClass( "ui-screen-hidden" ); |
| }, |
| |
| _closePrereqContainer: function() { |
| this._ui.container |
| .removeClass( "reverse out" ) |
| .addClass( "ui-selectmenu-hidden" ) |
| .removeAttr( "style" ); |
| }, |
| |
| _closePrereqsDone: function() { |
| var self = this, opts = self.options; |
| |
| self._ui.container.removeAttr( "tabindex" ); |
| |
| // remove nav bindings if they are still present |
| opts.container.unbind( opts.closeEvents ); |
| |
| // unbind click handlers added when history is disabled |
| self.element.undelegate( opts.closeLinkSelector, opts.closeLinkEvents ); |
| |
| // remove the global mutex for popups |
| $.mobile.popup.active = undefined; |
| |
| // alert users that the popup is closed |
| self._trigger( "afterclose" ); |
| }, |
| |
| _close: function( immediate ) { |
| this._ui.container.removeClass( "ui-popup-active" ); |
| this._page.removeClass( "ui-popup-open" ); |
| |
| this._isOpen = false; |
| |
| // IME hide when popup is closed |
| this.element.find("input").blur(); |
| |
| // Count down to triggering "popupafterclose" - we have two prerequisites: |
| // 1. The popup window reverse animation completes (container()) |
| // 2. The screen opacity animation completes (screen()) |
| this._createPrereqs( |
| $.proxy( this, "_closePrereqScreen" ), |
| $.proxy( this, "_closePrereqContainer" ), |
| $.proxy( this, "_closePrereqsDone" ) ); |
| |
| this._animate( { |
| additionalCondition: this._ui.screen.hasClass( "in" ), |
| transition: ( immediate ? "none" : ( this._currentTransition || this.options.transition ) ), |
| classToRemove: "in", |
| screenClassToAdd: "out", |
| containerClassToAdd: "reverse out", |
| applyTransition: true, |
| prereqs: this._prereqs |
| }); |
| }, |
| |
| _destroy: function() { |
| var self = this; |
| |
| // hide and remove bindings |
| self._close(); |
| |
| // Put the element back to where the placeholder was and remove the "ui-popup" class |
| self._setTheme( "none" ); |
| self.element |
| .insertAfter( self._ui.placeholder ) |
| .removeClass( "ui-popup ui-overlay-shadow ui-corner-all" ); |
| self._ui.screen.remove(); |
| self._ui.container.remove(); |
| self._ui.placeholder.remove(); |
| |
| // Unbind handlers that were bound to elements outside self.element (the window, in self case) |
| $.each( self._globalHandlers, function( idx, oneSrc ) { |
| $.each( oneSrc.handler, function( eventType, handler ) { |
| oneSrc.src.unbind( eventType, handler ); |
| }); |
| }); |
| }, |
| |
| _closePopup: function( e, data ) { |
| var parsedDst, toUrl; |
| |
| if ( e.type === "pagebeforechange" && data ) { |
| if ( typeof data.toPage === "string" ) { |
| parsedDst = data.toPage; |
| } else { |
| parsedDst = data.toPage.jqmData( "url" ); |
| } |
| parsedDst = $.mobile.path.parseUrl( parsedDst ); |
| toUrl = parsedDst.pathname + parsedDst.search + parsedDst.hash; |
| |
| if ( this._myUrl !== toUrl ) { |
| this.options.container.unbind( this.options.closeEvents ); |
| this._close( true ); |
| } else { |
| this._close(); |
| } |
| return; // skip normal close |
| } |
| |
| this._close(); |
| }, |
| |
| // any navigation event after a popup is opened should close the popup |
| // NOTE the pagebeforechange is bound to catch navigation events that don't |
| // alter the url (eg, dialogs from popups) |
| _bindContainerClose: function() { |
| var self = this; |
| |
| self.options.container |
| .one( self.options.closeEvents, $.proxy( self, "_closePopup" ) ); |
| }, |
| |
| // TODO no clear deliniation of what should be here and |
| // what should be in _open. Seems to be "visual" vs "history" for now |
| open: function( options ) { |
| var self = this, opts = this.options, url, hashkey, activePage, currentIsDialog, hasHash, urlHistory; |
| // self.link = ( $(event.target).attr('data-role') === 'button') ? event.target : $(event.target).closest('[data-role="button"]')[0]; |
| // make sure open is idempotent |
| if( $.mobile.popup.active ) { |
| return; |
| } |
| // set the global popup mutex |
| $.mobile.popup.active = this; |
| if( !options ) { |
| options = []; |
| } |
| |
| if ( !options.link ) { |
| if ( !event ) { |
| self.positionTo = "window"; |
| } else { |
| self.link = ( $(event.target).closest('a')[0] || $(event.target).closest('div')[0] ); |
| } |
| } else { |
| self.link = options.link; |
| } |
| if ( event ) { |
| self.positionTo = ( options != null && options.positionTo != null ) ? options.positionTo : "origin"; |
| } |
| |
| if ( $(self.link).jqmData("position-to") !== "window" |
| && self.positionTo !== "window" ) { |
| |
| $(self.element).addClass("ui-ctxpopup"); |
| $(self._ui.container).removeClass("ui-popup-container") |
| .addClass("ui-ctxpopup-container"); |
| |
| if( self.positionTo !== "origin" ) { |
| $(self._ui.arrow).hide(); |
| } else { |
| $(self._ui.arrow).show(); |
| } |
| } else { |
| $(self._ui.arrow).hide(); |
| // apply opacity back screen |
| this._setOverlayTheme( "dim" ); |
| } |
| if( !options.x |
| && self.positionTo === "origin" ) { |
| options.x = $(self.link).offset().left + $(self.link).outerWidth() / 2; |
| } |
| if( !options.y |
| && self.positionTo === "origin" ) { |
| options.y = $(self.link).offset().top + $(self.link).outerHeight() / 2; |
| } |
| // if history alteration is disabled close on navigate events |
| // and leave the url as is |
| if( !( opts.history ) ) { |
| self._open( options ); |
| self._bindContainerClose(); |
| |
| // When histoy is disabled we have to grab the data-rel |
| // back link clicks so we can close the popup instead of |
| // relying on history to do it for us |
| self.element |
| .delegate( opts.closeLinkSelector, opts.closeLinkEvents, function( e ) { |
| self._close(); |
| |
| // NOTE prevent the browser and navigation handlers from |
| // working with the link's rel=back. This may cause |
| // issues for developers expecting the event to bubble |
| return false; |
| }); |
| |
| return; |
| } |
| |
| // cache some values for min/readability |
| hashkey = $.mobile.dialogHashKey; |
| activePage = $.mobile.activePage; |
| currentIsDialog = activePage.is( ".ui-dialog" ); |
| // Set active page url |
| this._myUrl = url = urlHistory.getActive().url; |
| // |
| url = $.mobile.urlHistory.getActive().url; |
| hasHash = ( url.indexOf( hashkey ) > -1 ) && !currentIsDialog; |
| urlHistory = $.mobile.urlHistory; |
| |
| if ( hasHash ) { |
| self._open( options ); |
| self._bindContainerClose(); |
| return; |
| } |
| |
| // if the current url has no dialog hash key proceed as normal |
| // otherwise, if the page is a dialog simply tack on the hash key |
| if ( url.indexOf( hashkey ) === -1 && !currentIsDialog ){ |
| url = url + hashkey; |
| } else { |
| url = $.mobile.path.parseLocation().hash + hashkey; |
| } |
| |
| // Tack on an extra hashkey if this is the first page and we've just reconstructed the initial hash |
| if ( urlHistory.activeIndex === 0 && url === urlHistory.initialDst ) { |
| url += hashkey; |
| } |
| |
| // swallow the the initial navigation event, and bind for the next |
| opts.container.one( opts.navigateEvents, function( e ) { |
| e.preventDefault(); |
| self._open( options ); |
| self._bindContainerClose(); |
| }); |
| |
| urlHistory.ignoreNextHashChange = currentIsDialog; |
| |
| // Gotta love methods with 1mm args :( |
| urlHistory.addNew( url, undefined, undefined, undefined, "dialog" ); |
| |
| // set the new url with (or without) the new dialog hash key |
| $.mobile.path.set( url ); |
| }, |
| |
| close: function() { |
| // make sure close is idempotent |
| if( !$.mobile.popup.active ){ |
| return; |
| } |
| |
| if( this.options.history ) { |
| $.mobile.back(); |
| } else { |
| this._close(); |
| } |
| } |
| }); |
| |
| |
| // TODO this can be moved inside the widget |
| $.mobile.popup.handleLink = function( $link ) { |
| var closestPage = $link.closest( ":jqmData(role='page')" ), |
| scope = ( ( closestPage.length === 0 ) ? $( "body" ) : closestPage ), |
| // NOTE make sure to get only the hash, ie7 (wp7) return the absolute href |
| // in this case ruining the element selection |
| popup = $( $.mobile.path.parseUrl($link.attr( "href" )).hash, scope[0] ), |
| offset; |
| |
| if ( popup.data( "popup" ) ) { |
| offset = $link.offset(); |
| popup.popup( "open", { |
| x: offset.left + $link.outerWidth() / 2, |
| y: offset.top + $link.outerHeight() / 2, |
| transition: $.mobile.getAttrFixed( $link[0], "data-" + $.mobile.ns + "transition" ), |
| positionTo: $.mobile.getAttrFixed( $link[0], "data-" + $.mobile.ns + "position-to" ), |
| link: $link |
| }); |
| } |
| |
| //remove after delay |
| setTimeout( function() { |
| $link.removeClass( $.mobile.activeBtnClass ); |
| }, 300 ); |
| }; |
| |
| // TODO move inside _create |
| $.mobile.$document.bind( "pagebeforechange", function( e, data ) { |
| if ( data.options.role === "popup" ) { |
| $.mobile.popup.handleLink( data.options.link ); |
| e.preventDefault(); |
| } |
| }); |
| |
| //delegate self-init widgets |
| $.delegateSelfInitWithSingleSelector( $.mobile.popup, true ); |
| |
| })( jQuery ); |
| //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude); |
| }); |
| //>>excludeEnd("jqmBuildExclude"); |