jellyfin-web/dashboard-ui/thirdparty/jquerymobile-1.4.5/jqm.popup.js
2015-12-14 10:43:03 -05:00

1284 lines
54 KiB
JavaScript

define(['jqmwidget'], function () {
(function ($, undefined) {
var props = {
"animation": {},
"transition": {}
},
testElement = document.createElement("a"),
vendorPrefixes = ["", "webkit-", "moz-", "o-"];
$.each(["animation", "transition"], function (i, test) {
// Get correct name for test
var testName = (i === 0) ? test + "-" + "name" : test;
$.each(vendorPrefixes, function (j, prefix) {
if (testElement.style[$.camelCase(prefix + testName)] !== undefined) {
props[test]["prefix"] = prefix;
return false;
}
});
// Set event and duration names for later use
props[test]["duration"] =
$.camelCase(props[test]["prefix"] + test + "-" + "duration");
props[test]["event"] =
$.camelCase(props[test]["prefix"] + test + "-" + "end");
// All lower case if not a vendor prop
if (props[test]["prefix"] === "") {
props[test]["event"] = props[test]["event"].toLowerCase();
}
});
// Remove the testElement
$(testElement).remove();
// Animation complete callback
$.fn.animationComplete = function (callback, type, fallbackTime) {
var timer, duration,
that = this,
eventBinding = function () {
// Clear the timer so we don't call callback twice
clearTimeout(timer);
callback.apply(this, arguments);
},
animationType = (!type || type === "animation") ? "animation" : "transition";
// If a fallback time was not passed set one
if (fallbackTime === undefined) {
// Make sure the was not bound to document before checking .css
if ($(this).context !== document) {
// Parse the durration since its in second multiple by 1000 for milliseconds
// Multiply by 3 to make sure we give the animation plenty of time.
duration = parseFloat(
$(this).css(props[animationType].duration)
) * 3000;
}
// If we could not read a duration use the default
if (duration === 0 || duration === undefined || isNaN(duration)) {
duration = $.fn.animationComplete.defaultDuration;
}
}
// Sets up the fallback if event never comes
timer = setTimeout(function () {
$(that).off(props[animationType].event, eventBinding);
callback.apply(that);
}, duration);
// Bind the event
return $(this).one(props[animationType].event, eventBinding);
};
// Allow default callback to be configured on mobileInit
$.fn.animationComplete.defaultDuration = 1000;
})(jQuery);
(function ($, undefined) {
function fitSegmentInsideSegment(windowSize, segmentSize, offset, desired) {
var returnValue = desired;
if (windowSize < segmentSize) {
// Center segment if it's bigger than the window
returnValue = offset + (windowSize - segmentSize) / 2;
} else {
// Otherwise center it at the desired coordinate while keeping it completely inside the window
returnValue = Math.min(Math.max(offset, desired - segmentSize / 2), offset + windowSize - segmentSize);
}
return returnValue;
}
function getWindowCoordinates(theWindow) {
return {
x: theWindow.scrollLeft(),
y: theWindow.scrollTop(),
cx: (theWindow[0].innerWidth || theWindow.width()),
cy: (theWindow[0].innerHeight || theWindow.height())
};
}
// non-UA-based IE version check by James Padolsey, modified by jdalton - from http://gist.github.com/527683
// allows for inclusion of IE 6+, including Windows Mobile 7
$.extend($.mobile, { browser: {} });
$.mobile.browser.oldIE = (function () {
var v = 3,
div = document.createElement("div"),
a = div.all || [];
do {
div.innerHTML = "<!--[if gt IE " + (++v) + "]><br><![endif]-->";
} while (a[0]);
return v > 4 ? v : !v;
})();
$.widget("mobile.popup", {
options: {
wrapperClass: null,
theme: null,
overlayTheme: null,
shadow: true,
corners: true,
transition: "none",
positionTo: "origin",
tolerance: null,
closeLinkSelector: "a[data-rel='back']",
closeLinkEvents: "click.popup",
navigateEvents: "navigate.popup",
closeEvents: "navigate.popup pagebeforechange.popup",
dismissible: true,
enhanced: false,
// 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: !$.mobile.browser.oldIE
},
// When the user depresses the mouse/finger on an element inside the popup while the popup is
// open, we ignore resize events for a short while. This prevents #6961.
_handleDocumentVmousedown: function (theEvent) {
if (this._isOpen && $.contains(this._ui.container[0], theEvent.target)) {
this._ignoreResizeEvents();
}
},
_create: function () {
var theElement = this.element,
myId = theElement.attr("id"),
currentOptions = this.options;
// 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.
currentOptions.history = currentOptions.history && $.mobile.ajaxEnabled && $.mobile.hashListeningEnabled;
this._on(this.document, {
"mousedown": "_handleDocumentVmousedown"
});
// Define instance variables
$.extend(this, {
_scrollTop: 0,
_page: theElement.closest(".ui-page"),
_ui: null,
_fallbackTransition: "",
_currentTransition: false,
_prerequisites: null,
_isOpen: false,
_tolerance: null,
_resizeData: null,
_ignoreResizeTo: 0,
_orientationchangeInProgress: false
});
if (this._page.length === 0) {
this._page = $("body");
}
if (currentOptions.enhanced) {
this._ui = {
container: theElement.parent(),
screen: theElement.parent().prev(),
placeholder: $(this.document[0].getElementById(myId + "-placeholder"))
};
} else {
this._ui = this._enhance(theElement, myId);
this._applyTransition(currentOptions.transition);
}
this
._setTolerance(currentOptions.tolerance)
._ui.focusElement = this._ui.container;
// Event handlers
this._on(this._ui.screen, { "click": "_eatEventAndClose" });
this._on(this.window, {
orientationchange: $.proxy(this, "_handleWindowOrientationchange"),
resize: $.proxy(this, "_handleWindowResize")
});
this._on(this.document, { "focusin": "_handleDocumentFocusIn" });
},
_delay: function (handler, delay) {
function handlerProxy() {
return (typeof handler === "string" ? instance[handler] : handler)
.apply(instance, arguments);
}
var instance = this;
return setTimeout(handlerProxy, delay || 0);
},
_enhance: function (theElement, myId) {
var currentOptions = this.options,
wrapperClass = currentOptions.wrapperClass,
ui = {
screen: $("<div class='ui-screen-hidden ui-popup-screen " +
this._themeClassFromOption("ui-overlay-", currentOptions.overlayTheme) + "'></div>"),
placeholder: $("<div style='display: none;'><!-- placeholder --></div>"),
container: $("<div class='ui-popup-container ui-popup-hidden ui-popup-truncate" +
(wrapperClass ? (" " + wrapperClass) : "") + "'></div>")
},
fragment = this.document[0].createDocumentFragment();
fragment.appendChild(ui.screen[0]);
fragment.appendChild(ui.container[0]);
if (myId) {
ui.screen.attr("id", myId + "-screen");
ui.container.attr("id", myId + "-popup");
ui.placeholder
.attr("id", myId + "-placeholder")
.html("<!-- placeholder for " + myId + " -->");
}
// Apply the proto
this._page[0].appendChild(fragment);
// Leave a placeholder where the element used to be
ui.placeholder.insertAfter(theElement);
theElement
.detach()
.addClass("ui-popup " +
this._themeClassFromOption("ui-body-", currentOptions.theme) + " " +
(currentOptions.shadow ? "ui-overlay-shadow " : "") +
(currentOptions.corners ? "ui-corner-all " : ""))
.appendTo(ui.container);
return ui;
},
_eatEventAndClose: function (theEvent) {
theEvent.preventDefault();
theEvent.stopImmediatePropagation();
if (this.options.dismissible) {
this.close();
}
return false;
},
// Make sure the screen covers the entire document - CSS is sometimes not
// enough to accomplish this.
_resizeScreen: function () {
var screen = this._ui.screen,
popupHeight = this._ui.container.outerHeight(true),
screenHeight = screen.removeAttr("style").height(),
// Subtracting 1 here is necessary for an obscure Andrdoid 4.0 bug where
// the browser hangs if the screen covers the entire document :/
documentHeight = this.document.height() - 1;
if (screenHeight < documentHeight) {
screen.height(documentHeight);
} else if (popupHeight > screenHeight) {
screen.height(popupHeight);
}
},
_expectResizeEvent: function () {
var windowCoordinates = getWindowCoordinates(this.window);
if (this._resizeData) {
if (windowCoordinates.x === this._resizeData.windowCoordinates.x &&
windowCoordinates.y === this._resizeData.windowCoordinates.y &&
windowCoordinates.cx === this._resizeData.windowCoordinates.cx &&
windowCoordinates.cy === this._resizeData.windowCoordinates.cy) {
// timeout not refreshed
return false;
} else {
// clear existing timeout - it will be refreshed below
clearTimeout(this._resizeData.timeoutId);
}
}
this._resizeData = {
timeoutId: this._delay("_resizeTimeout", 200),
windowCoordinates: windowCoordinates
};
return true;
},
_resizeTimeout: function () {
if (this._isOpen) {
if (!this._expectResizeEvent()) {
if (this._ui.container.hasClass("ui-popup-hidden")) {
// effectively rapid-open the popup while leaving the screen intact
this._ui.container.removeClass("ui-popup-hidden ui-popup-truncate");
this.reposition({ positionTo: "window" });
this._ignoreResizeEvents();
}
this._resizeScreen();
this._resizeData = null;
this._orientationchangeInProgress = false;
}
} else {
this._resizeData = null;
this._orientationchangeInProgress = false;
}
},
_stopIgnoringResizeEvents: function () {
this._ignoreResizeTo = 0;
},
_ignoreResizeEvents: function () {
if (this._ignoreResizeTo) {
clearTimeout(this._ignoreResizeTo);
}
this._ignoreResizeTo = this._delay("_stopIgnoringResizeEvents", 1000);
},
_handleWindowResize: function (/* theEvent */) {
if (this._isOpen && this._ignoreResizeTo === 0) {
if ((this._expectResizeEvent() || this._orientationchangeInProgress) &&
!this._ui.container.hasClass("ui-popup-hidden")) {
// effectively rapid-close the popup while leaving the screen intact
this._ui.container
.addClass("ui-popup-hidden ui-popup-truncate")
.removeAttr("style");
}
}
},
_handleWindowOrientationchange: function (/* theEvent */) {
if (!this._orientationchangeInProgress && this._isOpen && this._ignoreResizeTo === 0) {
this._expectResizeEvent();
this._orientationchangeInProgress = true;
}
},
// When the popup is open, attempting to focus on an element that is not a
// child of the popup will redirect focus to the popup
_handleDocumentFocusIn: function (theEvent) {
var target,
targetElement = theEvent.target,
ui = this._ui;
if (!this._isOpen) {
return;
}
if (targetElement !== ui.container[0]) {
target = $(targetElement);
if (!$.contains(ui.container[0], targetElement)) {
$(this.document[0].activeElement).one("focus", $.proxy(function () {
this._safelyBlur(targetElement);
}, this));
ui.focusElement.focus();
theEvent.preventDefault();
theEvent.stopImmediatePropagation();
return false;
} else if (ui.focusElement[0] === ui.container[0]) {
ui.focusElement = target;
}
}
this._ignoreResizeEvents();
},
_themeClassFromOption: function (prefix, value) {
return (value ? (value === "none" ? "" : (prefix + value)) : (prefix + "inherit"));
},
_applyTransition: function (value) {
if (value) {
this._ui.container.removeClass(this._fallbackTransition);
if (value !== "none") {
this._fallbackTransition = $.mobile._maybeDegradeTransition(value);
if (this._fallbackTransition === "none") {
this._fallbackTransition = "";
}
this._ui.container.addClass(this._fallbackTransition);
}
}
return this;
},
_setOptions: function (newOptions) {
var currentOptions = this.options,
theElement = this.element,
screen = this._ui.screen;
if (newOptions.wrapperClass !== undefined) {
this._ui.container
.removeClass(currentOptions.wrapperClass)
.addClass(newOptions.wrapperClass);
}
if (newOptions.theme !== undefined) {
theElement
.removeClass(this._themeClassFromOption("ui-body-", currentOptions.theme))
.addClass(this._themeClassFromOption("ui-body-", newOptions.theme));
}
if (newOptions.overlayTheme !== undefined) {
screen
.removeClass(this._themeClassFromOption("ui-overlay-", currentOptions.overlayTheme))
.addClass(this._themeClassFromOption("ui-overlay-", newOptions.overlayTheme));
if (this._isOpen) {
screen.addClass("in");
}
}
if (newOptions.shadow !== undefined) {
theElement.toggleClass("ui-overlay-shadow", newOptions.shadow);
}
if (newOptions.corners !== undefined) {
theElement.toggleClass("ui-corner-all", newOptions.corners);
}
if (newOptions.transition !== undefined) {
if (!this._currentTransition) {
this._applyTransition(newOptions.transition);
}
}
if (newOptions.tolerance !== undefined) {
this._setTolerance(newOptions.tolerance);
}
if (newOptions.disabled !== undefined) {
if (newOptions.disabled) {
this.close();
}
}
return this._super(newOptions);
},
_setTolerance: function (value) {
var tol = { t: 30, r: 15, b: 30, l: 15 },
ar;
if (value !== undefined) {
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;
return this;
},
_clampPopupWidth: function (infoOnly) {
var menuSize,
windowCoordinates = getWindowCoordinates(this.window),
// rectangle within which the popup must fit
rectangle = {
x: this._tolerance.l,
y: windowCoordinates.y + this._tolerance.t,
cx: windowCoordinates.cx - this._tolerance.l - this._tolerance.r,
cy: windowCoordinates.cy - this._tolerance.t - this._tolerance.b
};
if (!infoOnly) {
// Clamp the width of the menu before grabbing its size
this._ui.container.css("max-width", rectangle.cx);
}
menuSize = {
cx: this._ui.container.outerWidth(true),
cy: this._ui.container.outerHeight(true)
};
return { rc: rectangle, menuSize: menuSize };
},
_calculateFinalLocation: function (desired, clampInfo) {
var returnValue,
rectangle = clampInfo.rc,
menuSize = clampInfo.menuSize;
// 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.
returnValue = {
left: fitSegmentInsideSegment(rectangle.cx, menuSize.cx, rectangle.x, desired.x),
top: fitSegmentInsideSegment(rectangle.cy, menuSize.cy, rectangle.y, desired.y)
};
// Make sure the top of the menu is visible
returnValue.top = Math.max(0, returnValue.top);
// If the height of the menu is smaller than the height of the document
// align the bottom with the bottom of the document
returnValue.top -= Math.min(returnValue.top,
Math.max(0, returnValue.top + menuSize.cy - this.document.height()));
return returnValue;
},
// Try and center the overlay over the given coordinates
_placementCoords: function (desired) {
return this._calculateFinalLocation(desired, this._clampPopupWidth());
},
_createPrerequisites: function (screenPrerequisite, containerPrerequisite, whenDone) {
var prerequisites,
self = this;
// It is important to maintain both the local variable prerequisites and
// self._prerequisites. The local variable remains in the closure of the
// functions which call the callbacks passed in. The comparison between the
// local variable and self._prerequisites 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 prerequisites to the widget-level variable
// self._prerequisites ensures that callbacks triggered by a stale
// .animationComplete will be ignored.
prerequisites = {
screen: $.Deferred(),
container: $.Deferred()
};
prerequisites.screen.then(function () {
if (prerequisites === self._prerequisites) {
screenPrerequisite();
}
});
prerequisites.container.then(function () {
if (prerequisites === self._prerequisites) {
containerPrerequisite();
}
});
Promise.all([prerequisites.screen, prerequisites.container]).then(function () {
if (prerequisites === self._prerequisites) {
self._prerequisites = null;
whenDone();
}
});
self._prerequisites = prerequisites;
},
_animate: function (args) {
// NOTE before removing the default animation of the screen
// this had an animate callback that would resolve 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.prerequisites.screen.resolve();
if (args.transition && args.transition !== "none") {
if (args.applyTransition) {
this._applyTransition(args.transition);
}
if (this._fallbackTransition) {
this._ui.container
.addClass(args.containerClassToAdd)
.removeClass(args.classToRemove)
.animationComplete($.proxy(args.prerequisites.container, "resolve"));
return;
}
}
this._ui.container.removeClass(args.classToRemove);
args.prerequisites.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.
// options: { x: coordinate, y: coordinate, positionTo: string: "origin", "window", or jQuery selector
_desiredCoords: function (openOptions) {
var offset,
dst = null,
windowCoordinates = getWindowCoordinates(this.window),
x = openOptions.x,
y = openOptions.y,
pTo = openOptions.positionTo;
// Establish which element will serve as the reference
if (pTo && pTo !== "origin") {
if (pTo === "window") {
x = windowCoordinates.cx / 2 + windowCoordinates.x;
y = windowCoordinates.cy / 2 + windowCoordinates.y;
} else {
try {
dst = $(pTo);
} catch (err) {
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 = windowCoordinates.cx / 2 + windowCoordinates.x;
}
if ($.type(y) !== "number" || isNaN(y)) {
y = windowCoordinates.cy / 2 + windowCoordinates.y;
}
return { x: x, y: y };
},
_reposition: function (openOptions) {
// We only care about position-related parameters for repositioning
openOptions = {
x: openOptions.x,
y: openOptions.y,
positionTo: openOptions.positionTo
};
this._trigger("beforeposition", undefined, openOptions);
this._ui.container.offset(this._placementCoords(this._desiredCoords(openOptions)));
},
reposition: function (openOptions) {
if (this._isOpen) {
this._reposition(openOptions);
}
},
_safelyBlur: function (currentElement) {
if (currentElement !== this.window[0] &&
currentElement.nodeName.toLowerCase() !== "body") {
$(currentElement).blur();
}
},
_openPrerequisitesComplete: function () {
var id = this.element.attr("id");
this._ui.container.addClass("ui-popup-active");
this._isOpen = true;
this._resizeScreen();
// Check to see if currElement is not a child of the container. If it's not, blur
if (!$.contains(this._ui.container[0], this.document[0].activeElement)) {
this._safelyBlur(this.document[0].activeElement);
}
this._ignoreResizeEvents();
if (id) {
this.document.find("[aria-haspopup='true'][aria-owns='" + id + "']").attr("aria-expanded", true);
}
this._trigger("afteropen");
},
_open: function (options) {
var openOptions = $.extend({}, this.options, options),
// TODO move blacklist to private method
androidBlacklist = (function () {
var 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;
}());
// Count down to triggering "popupafteropen" - we have two prerequisites:
// 1. The popup window animation completes (container())
// 2. The screen opacity animation completes (screen())
this._createPrerequisites(
$.noop,
$.noop,
$.proxy(this, "_openPrerequisitesComplete"));
this._currentTransition = openOptions.transition;
this._applyTransition(openOptions.transition);
this._ui.screen.removeClass("ui-screen-hidden");
this._ui.container.removeClass("ui-popup-truncate");
// Give applications a chance to modify the contents of the container before it appears
this._reposition(openOptions);
this._ui.container.removeClass("ui-popup-hidden");
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: openOptions.transition,
classToRemove: "",
screenClassToAdd: "in",
containerClassToAdd: "in",
applyTransition: false,
prerequisites: this._prerequisites
});
},
_closePrerequisiteScreen: function () {
this._ui.screen
.removeClass("out")
.addClass("ui-screen-hidden");
},
_closePrerequisiteContainer: function () {
this._ui.container
.removeClass("reverse out")
.addClass("ui-popup-hidden ui-popup-truncate")
.removeAttr("style");
},
_closePrerequisitesDone: function () {
var container = this._ui.container,
id = this.element.attr("id");
// remove the global mutex for popups
$.mobile.popup.active = undefined;
// Blur elements inside the container, including the container
$(":focus", container[0]).add(container[0]).blur();
if (id) {
this.document.find("[aria-haspopup='true'][aria-owns='" + id + "']").attr("aria-expanded", false);
}
// alert users that the popup is closed
this._trigger("afterclose");
},
_close: function (immediate) {
this._ui.container.removeClass("ui-popup-active");
this._page.removeClass("ui-popup-open");
this._isOpen = false;
// 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._createPrerequisites(
$.proxy(this, "_closePrerequisiteScreen"),
$.proxy(this, "_closePrerequisiteContainer"),
$.proxy(this, "_closePrerequisitesDone"));
this._animate({
additionalCondition: this._ui.screen.hasClass("in"),
transition: (immediate ? "none" : (this._currentTransition)),
classToRemove: "in",
screenClassToAdd: "out",
containerClassToAdd: "reverse out",
applyTransition: true,
prerequisites: this._prerequisites
});
},
_unenhance: function () {
if (this.options.enhanced) {
return;
}
// Put the element back to where the placeholder was and remove the "ui-popup" class
this._setOptions({ theme: $.mobile.popup.prototype.options.theme });
this.element
// Cannot directly insertAfter() - we need to detach() first, because
// insertAfter() will do nothing if the payload div was not attached
// to the DOM at the time the widget was created, and so the payload
// will remain inside the container even after we call insertAfter().
// If that happens and we remove the container a few lines below, we
// will cause an infinite recursion - #5244
.detach()
.insertAfter(this._ui.placeholder)
.removeClass("ui-popup ui-overlay-shadow ui-corner-all ui-body-inherit");
this._ui.screen.remove();
this._ui.container.remove();
this._ui.placeholder.remove();
},
_destroy: function () {
if ($.mobile.popup.active === this) {
this.element.one("popupafterclose", $.proxy(this, "_unenhance"));
this.close();
} else {
this._unenhance();
}
return this;
},
_closePopup: function (theEvent, data) {
var parsedDst, toUrl,
currentOptions = this.options,
immediate = false;
if ((theEvent && theEvent.isDefaultPrevented()) || $.mobile.popup.active !== this) {
return;
}
// restore location on screen
window.scrollTo(0, this._scrollTop);
if (theEvent && theEvent.type === "pagebeforechange" && data) {
// Determine whether we need to rapid-close the popup, or whether we can
// take the time to run the closing transition
if (typeof data.toPage === "string") {
parsedDst = data.toPage;
} else {
parsedDst = data.toPage.data("url");
}
parsedDst = $.mobile.path.parseUrl(parsedDst);
toUrl = parsedDst.pathname + parsedDst.search + parsedDst.hash;
if (this._myUrl !== $.mobile.path.makeUrlAbsolute(toUrl)) {
// Going to a different page - close immediately
immediate = true;
} else {
theEvent.preventDefault();
}
}
// remove nav bindings
this.window.off(currentOptions.closeEvents);
// unbind click handlers added when history is disabled
this.element.undelegate(currentOptions.closeLinkSelector, currentOptions.closeLinkEvents);
this._close(immediate);
},
// 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 () {
this.window
.on(this.options.closeEvents, $.proxy(this, "_closePopup"));
},
widget: function () {
return this._ui.container;
},
// 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 url, hashkey, activePage, currentIsDialog, hasHash, urlHistory,
self = this,
currentOptions = this.options;
// make sure open is idempotent
if ($.mobile.popup.active || currentOptions.disabled) {
return this;
}
// set the global popup mutex
$.mobile.popup.active = this;
this._scrollTop = this.window.scrollTop();
// if history alteration is disabled close on navigate events
// and leave the url as is
if (!(currentOptions.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(currentOptions.closeLinkSelector, currentOptions.closeLinkEvents, function (theEvent) {
self.close();
theEvent.preventDefault();
});
return this;
}
// cache some values for min/readability
urlHistory = $.mobile.navigate.history;
hashkey = $.mobile.dialogHashKey;
activePage = $.mobile.activePage;
currentIsDialog = (activePage ? activePage.hasClass("ui-dialog") : false);
this._myUrl = url = urlHistory.getActive().url;
hasHash = (url.indexOf(hashkey) > -1) && !currentIsDialog && (urlHistory.activeIndex > 0);
if (hasHash) {
self._open(options);
self._bindContainerClose();
return this;
}
// 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 + (url.indexOf("#") > -1 ? hashkey : "#" + hashkey);
} else {
url = $.mobile.path.parseLocation().hash + hashkey;
}
// swallow the the initial navigation event, and bind for the next
this.window.one("beforenavigate", function (theEvent) {
theEvent.preventDefault();
self._open(options);
self._bindContainerClose();
});
this.urlAltered = true;
$.mobile.navigate(url, { role: "dialog" });
return this;
},
close: function () {
// make sure close is idempotent
if ($.mobile.popup.active !== this) {
return this;
}
this._scrollTop = this.window.scrollTop();
if (this.options.history && this.urlAltered) {
$.mobile.pageContainer.pagecontainer("back");
this.urlAltered = false;
} else {
// simulate the nav bindings having fired
this._closePopup();
}
return this;
}
});
// TODO this can be moved inside the widget
$.mobile.popup.handleLink = function ($link) {
var offset,
path = $.mobile.path,
// NOTE make sure to get only the hash from the href because ie7 (wp7)
// returns the absolute href in this case ruining the element selection
popup = $(path.hashToSelector(path.parseUrl($link.attr("href")).hash)).first();
if (popup.length > 0 && popup.data("mobile-popup")) {
offset = $link.offset();
popup.popup("open", {
x: offset.left + $link.outerWidth() / 2,
y: offset.top + $link.outerHeight() / 2,
transition: $link.data("transition"),
positionTo: $link.data("position-to")
});
}
//remove after delay
setTimeout(function () {
$link.removeClass($.mobile.activeBtnClass);
}, 300);
};
// TODO move inside _create
$(document).on("pagebeforechange", function (theEvent, data) {
if (data.options.role === "popup") {
$.mobile.popup.handleLink(data.options.link);
theEvent.preventDefault();
}
});
})(jQuery);
(function ($, undefined) {
var ieHack = ($.mobile.browser.oldIE && $.mobile.browser.oldIE <= 8),
uiTemplate = $(
"<div class='ui-popup-arrow-guide'></div>" +
"<div class='ui-popup-arrow-container" + (ieHack ? " ie" : "") + "'>" +
"<div class='ui-popup-arrow'></div>" +
"</div>"
);
function getArrow() {
var clone = uiTemplate.clone(),
gd = clone.eq(0),
ct = clone.eq(1),
ar = ct.children();
return { arEls: ct.add(gd), gd: gd, ct: ct, ar: ar };
}
$.widget("mobile.popup", $.mobile.popup, {
options: {
arrow: ""
},
_create: function () {
var ar,
ret = this._super();
if (this.options.arrow) {
this._ui.arrow = ar = this._addArrow();
}
return ret;
},
_addArrow: function () {
var theme,
opts = this.options,
ar = getArrow();
theme = this._themeClassFromOption("ui-body-", opts.theme);
ar.ar.addClass(theme + (opts.shadow ? " ui-overlay-shadow" : ""));
ar.arEls.hide().appendTo(this.element);
return ar;
},
_unenhance: function () {
var ar = this._ui.arrow;
if (ar) {
ar.arEls.remove();
}
return this._super();
},
// Pretend to show an arrow described by @p and @dir and calculate the
// distance from the desired point. If a best-distance is passed in, return
// the minimum of the one passed in and the one calculated.
_tryAnArrow: function (p, dir, desired, s, best) {
var result, r, diff, desiredForArrow = {}, tip = {};
// If the arrow has no wiggle room along the edge of the popup, it cannot
// be displayed along the requested edge without it sticking out.
if (s.arFull[p.dimKey] > s.guideDims[p.dimKey]) {
return best;
}
desiredForArrow[p.fst] = desired[p.fst] +
(s.arHalf[p.oDimKey] + s.menuHalf[p.oDimKey]) * p.offsetFactor -
s.contentBox[p.fst] + (s.clampInfo.menuSize[p.oDimKey] - s.contentBox[p.oDimKey]) * p.arrowOffsetFactor;
desiredForArrow[p.snd] = desired[p.snd];
result = s.result || this._calculateFinalLocation(desiredForArrow, s.clampInfo);
r = { x: result.left, y: result.top };
tip[p.fst] = r[p.fst] + s.contentBox[p.fst] + p.tipOffset;
tip[p.snd] = Math.max(result[p.prop] + s.guideOffset[p.prop] + s.arHalf[p.dimKey],
Math.min(result[p.prop] + s.guideOffset[p.prop] + s.guideDims[p.dimKey] - s.arHalf[p.dimKey],
desired[p.snd]));
diff = Math.abs(desired.x - tip.x) + Math.abs(desired.y - tip.y);
if (!best || diff < best.diff) {
// Convert tip offset to coordinates inside the popup
tip[p.snd] -= s.arHalf[p.dimKey] + result[p.prop] + s.contentBox[p.snd];
best = { dir: dir, diff: diff, result: result, posProp: p.prop, posVal: tip[p.snd] };
}
return best;
},
_getPlacementState: function (clamp) {
var offset, gdOffset,
ar = this._ui.arrow,
state = {
clampInfo: this._clampPopupWidth(!clamp),
arFull: { cx: ar.ct.width(), cy: ar.ct.height() },
guideDims: { cx: ar.gd.width(), cy: ar.gd.height() },
guideOffset: ar.gd.offset()
};
offset = this.element.offset();
ar.gd.css({ left: 0, top: 0, right: 0, bottom: 0 });
gdOffset = ar.gd.offset();
state.contentBox = {
x: gdOffset.left - offset.left,
y: gdOffset.top - offset.top,
cx: ar.gd.width(),
cy: ar.gd.height()
};
ar.gd.removeAttr("style");
// The arrow box moves between guideOffset and guideOffset + guideDims - arFull
state.guideOffset = { left: state.guideOffset.left - offset.left, top: state.guideOffset.top - offset.top };
state.arHalf = { cx: state.arFull.cx / 2, cy: state.arFull.cy / 2 };
state.menuHalf = { cx: state.clampInfo.menuSize.cx / 2, cy: state.clampInfo.menuSize.cy / 2 };
return state;
},
_placementCoords: function (desired) {
var state, best, params, elOffset, bgRef,
optionValue = this.options.arrow,
ar = this._ui.arrow;
if (!ar) {
return this._super(desired);
}
ar.arEls.show();
bgRef = {};
state = this._getPlacementState(true);
params = {
"l": { fst: "x", snd: "y", prop: "top", dimKey: "cy", oDimKey: "cx", offsetFactor: 1, tipOffset: -state.arHalf.cx, arrowOffsetFactor: 0 },
"r": { fst: "x", snd: "y", prop: "top", dimKey: "cy", oDimKey: "cx", offsetFactor: -1, tipOffset: state.arHalf.cx + state.contentBox.cx, arrowOffsetFactor: 1 },
"b": { fst: "y", snd: "x", prop: "left", dimKey: "cx", oDimKey: "cy", offsetFactor: -1, tipOffset: state.arHalf.cy + state.contentBox.cy, arrowOffsetFactor: 1 },
"t": { fst: "y", snd: "x", prop: "left", dimKey: "cx", oDimKey: "cy", offsetFactor: 1, tipOffset: -state.arHalf.cy, arrowOffsetFactor: 0 }
};
// Try each side specified in the options to see on which one the arrow
// should be placed such that the distance between the tip of the arrow and
// the desired coordinates is the shortest.
$.each((optionValue === true ? "l,t,r,b" : optionValue).split(","),
$.proxy(function (key, value) {
best = this._tryAnArrow(params[value], value, desired, state, best);
}, this));
// Could not place the arrow along any of the edges - behave as if showing
// the arrow was turned off.
if (!best) {
ar.arEls.hide();
return this._super(desired);
}
// Move the arrow into place
ar.ct
.removeClass("ui-popup-arrow-l ui-popup-arrow-t ui-popup-arrow-r ui-popup-arrow-b")
.addClass("ui-popup-arrow-" + best.dir)
.removeAttr("style").css(best.posProp, best.posVal)
.show();
// Do not move/size the background div on IE, because we use the arrow div for background as well.
if (!ieHack) {
elOffset = this.element.offset();
bgRef[params[best.dir].fst] = ar.ct.offset();
bgRef[params[best.dir].snd] = {
left: elOffset.left + state.contentBox.x,
top: elOffset.top + state.contentBox.y
};
}
return best.result;
},
_setOptions: function (opts) {
var newTheme,
oldTheme = this.options.theme,
ar = this._ui.arrow,
ret = this._super(opts);
if (opts.arrow !== undefined) {
if (!ar && opts.arrow) {
this._ui.arrow = this._addArrow();
// Important to return here so we don't set the same options all over
// again below.
return;
} else if (ar && !opts.arrow) {
ar.arEls.remove();
this._ui.arrow = null;
}
}
// Reassign with potentially new arrow
ar = this._ui.arrow;
if (ar) {
if (opts.theme !== undefined) {
oldTheme = this._themeClassFromOption("ui-body-", oldTheme);
newTheme = this._themeClassFromOption("ui-body-", opts.theme);
ar.ar.removeClass(oldTheme).addClass(newTheme);
}
if (opts.shadow !== undefined) {
ar.ar.toggleClass("ui-overlay-shadow", opts.shadow);
}
}
return ret;
},
_destroy: function () {
var ar = this._ui.arrow;
if (ar) {
ar.arEls.remove();
}
return this._super();
}
});
})(jQuery);
});