swc/crates/swc_ecma_minifier/tests/projects/files/jquery.mobile-1.4.2.js

16745 lines
534 KiB
JavaScript
Raw Normal View History

/*!
* jQuery Mobile 1.4.2
* Git HEAD hash: 9d9a42a27d0c693e8b5569c3a10d771916af5045 <> Date: Fri Feb 28 2014 17:32:01 UTC
* http://jquerymobile.com
*
* Copyright 2010, 2014 jQuery Foundation, Inc. and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
*/
(function (root, doc, factory) {
if (typeof define === "function" && define.amd) {
// AMD. Register as an anonymous module.
define(["jquery"], function ($) {
factory($, root, doc);
return $.mobile;
});
} else {
// Browser globals
factory(root.jQuery, root, doc);
}
})(this, document, function (jQuery, window, document, undefined) {
(function ($) {
$.mobile = {};
})(jQuery);
(function ($, window, undefined) {
$.extend($.mobile, {
// Version of the jQuery Mobile Framework
version: "1.4.2",
// Deprecated and no longer used in 1.4 remove in 1.5
// Define the url parameter used for referencing widget-generated sub-pages.
// Translates to example.html&ui-page=subpageIdentifier
// hash segment before &ui-page= is used to make Ajax request
subPageUrlKey: "ui-page",
hideUrlBar: true,
// Keepnative Selector
keepNative: ":jqmData(role='none'), :jqmData(role='nojs')",
// Deprecated in 1.4 remove in 1.5
// Class assigned to page currently in view, and during transitions
activePageClass: "ui-page-active",
// Deprecated in 1.4 remove in 1.5
// Class used for "active" button state, from CSS framework
activeBtnClass: "ui-btn-active",
// Deprecated in 1.4 remove in 1.5
// Class used for "focus" form element state, from CSS framework
focusClass: "ui-focus",
// Automatically handle clicks and form submissions through Ajax, when same-domain
ajaxEnabled: true,
// Automatically load and show pages based on location.hash
hashListeningEnabled: true,
// disable to prevent jquery from bothering with links
linkBindingEnabled: true,
// Set default page transition - 'none' for no transitions
defaultPageTransition: "fade",
// Set maximum window width for transitions to apply - 'false' for no limit
maxTransitionWidth: false,
// Minimum scroll distance that will be remembered when returning to a page
// Deprecated remove in 1.5
minScrollBack: 0,
// Set default dialog transition - 'none' for no transitions
defaultDialogTransition: "pop",
// Error response message - appears when an Ajax page request fails
pageLoadErrorMessage: "Error Loading Page",
// For error messages, which theme does the box uses?
pageLoadErrorMessageTheme: "a",
// replace calls to window.history.back with phonegaps navigation helper
// where it is provided on the window object
phonegapNavigationEnabled: false,
//automatically initialize the DOM when it's ready
autoInitializePage: true,
pushStateEnabled: true,
// allows users to opt in to ignoring content by marking a parent element as
// data-ignored
ignoreContentEnabled: false,
buttonMarkup: {
hoverDelay: 200,
},
// disable the alteration of the dynamic base tag or links in the case
// that a dynamic base tag isn't supported
dynamicBaseEnabled: true,
// default the property to remove dependency on assignment in init module
pageContainer: $(),
//enable cross-domain page support
allowCrossDomainPages: false,
dialogHashKey: "&ui-state=dialog",
});
})(jQuery, this);
(function ($, window, undefined) {
var nsNormalizeDict = {},
oldFind = $.find,
rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/,
jqmDataRE = /:jqmData\(([^)]*)\)/g;
$.extend($.mobile, {
// Namespace used framework-wide for data-attrs. Default is no namespace
ns: "",
// Retrieve an attribute from an element and perform some massaging of the value
getAttribute: function (element, key) {
var data;
element = element.jquery ? element[0] : element;
if (element && element.getAttribute) {
data = element.getAttribute("data-" + $.mobile.ns + key);
}
// Copied from core's src/data.js:dataAttr()
// Convert from a string to a proper data type
try {
data =
data === "true"
? true
: data === "false"
? false
: data === "null"
? null
: // Only convert to a number if it doesn't change the string
+data + "" === data
? +data
: rbrace.test(data)
? JSON.parse(data)
: data;
} catch (err) {}
return data;
},
// Expose our cache for testing purposes.
nsNormalizeDict: nsNormalizeDict,
// Take a data attribute property, prepend the namespace
// and then camel case the attribute string. Add the result
// to our nsNormalizeDict so we don't have to do this again.
nsNormalize: function (prop) {
return (
nsNormalizeDict[prop] ||
(nsNormalizeDict[prop] = $.camelCase($.mobile.ns + prop))
);
},
// Find the closest javascript page element to gather settings data jsperf test
// http://jsperf.com/single-complex-selector-vs-many-complex-selectors/edit
// possibly naive, but it shows that the parsing overhead for *just* the page selector vs
// the page and dialog selector is negligable. This could probably be speed up by
// doing a similar parent node traversal to the one found in the inherited theme code above
closestPageData: function ($target) {
return $target
.closest(":jqmData(role='page'), :jqmData(role='dialog')")
.data("mobile-page");
},
});
// Mobile version of data and removeData and hasData methods
// ensures all data is set and retrieved using jQuery Mobile's data namespace
$.fn.jqmData = function (prop, value) {
var result;
if (typeof prop !== "undefined") {
if (prop) {
prop = $.mobile.nsNormalize(prop);
}
// undefined is permitted as an explicit input for the second param
// in this case it returns the value and does not set it to undefined
if (arguments.length < 2 || value === undefined) {
result = this.data(prop);
} else {
result = this.data(prop, value);
}
}
return result;
};
$.jqmData = function (elem, prop, value) {
var result;
if (typeof prop !== "undefined") {
result = $.data(elem, prop ? $.mobile.nsNormalize(prop) : prop, value);
}
return result;
};
$.fn.jqmRemoveData = function (prop) {
return this.removeData($.mobile.nsNormalize(prop));
};
$.jqmRemoveData = function (elem, prop) {
return $.removeData(elem, $.mobile.nsNormalize(prop));
};
$.find = function (selector, context, ret, extra) {
if (selector.indexOf(":jqmData") > -1) {
selector = selector.replace(
jqmDataRE,
"[data-" + ($.mobile.ns || "") + "$1]",
);
}
return oldFind.call(this, selector, context, ret, extra);
};
$.extend($.find, oldFind);
})(jQuery, this);
/*!
* jQuery UI Core c0ab71056b936627e8a7821f03c044aec6280a40
* http://jqueryui.com
*
* Copyright 2013 jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
* http://api.jqueryui.com/category/ui-core/
*/
(function ($, undefined) {
var uuid = 0,
runiqueId = /^ui-id-\d+$/;
// $.ui might exist from components with no dependencies, e.g., $.ui.position
$.ui = $.ui || {};
$.extend($.ui, {
version: "c0ab71056b936627e8a7821f03c044aec6280a40",
keyCode: {
BACKSPACE: 8,
COMMA: 188,
DELETE: 46,
DOWN: 40,
END: 35,
ENTER: 13,
ESCAPE: 27,
HOME: 36,
LEFT: 37,
PAGE_DOWN: 34,
PAGE_UP: 33,
PERIOD: 190,
RIGHT: 39,
SPACE: 32,
TAB: 9,
UP: 38,
},
});
// plugins
$.fn.extend({
focus: (function (orig) {
return function (delay, fn) {
return typeof delay === "number"
? this.each(function () {
var elem = this;
setTimeout(function () {
$(elem).focus();
if (fn) {
fn.call(elem);
}
}, delay);
})
: orig.apply(this, arguments);
};
})($.fn.focus),
scrollParent: function () {
var scrollParent;
if (
($.ui.ie && /(static|relative)/.test(this.css("position"))) ||
/absolute/.test(this.css("position"))
) {
scrollParent = this.parents()
.filter(function () {
return (
/(relative|absolute|fixed)/.test($.css(this, "position")) &&
/(auto|scroll)/.test(
$.css(this, "overflow") +
$.css(this, "overflow-y") +
$.css(this, "overflow-x"),
)
);
})
.eq(0);
} else {
scrollParent = this.parents()
.filter(function () {
return /(auto|scroll)/.test(
$.css(this, "overflow") +
$.css(this, "overflow-y") +
$.css(this, "overflow-x"),
);
})
.eq(0);
}
return /fixed/.test(this.css("position")) || !scrollParent.length
? $(this[0].ownerDocument || document)
: scrollParent;
},
uniqueId: function () {
return this.each(function () {
if (!this.id) {
this.id = "ui-id-" + ++uuid;
}
});
},
removeUniqueId: function () {
return this.each(function () {
if (runiqueId.test(this.id)) {
$(this).removeAttr("id");
}
});
},
});
// selectors
function focusable(element, isTabIndexNotNaN) {
var map,
mapName,
img,
nodeName = element.nodeName.toLowerCase();
if ("area" === nodeName) {
map = element.parentNode;
mapName = map.name;
if (!element.href || !mapName || map.nodeName.toLowerCase() !== "map") {
return false;
}
img = $("img[usemap=#" + mapName + "]")[0];
return !!img && visible(img);
}
return (
(/input|select|textarea|button|object/.test(nodeName)
? !element.disabled
: "a" === nodeName
? element.href || isTabIndexNotNaN
: isTabIndexNotNaN) &&
// the element and all of its ancestors must be visible
visible(element)
);
}
function visible(element) {
return (
$.expr.filters.visible(element) &&
!$(element)
.parents()
.addBack()
.filter(function () {
return $.css(this, "visibility") === "hidden";
}).length
);
}
$.extend($.expr[":"], {
data: $.expr.createPseudo
? $.expr.createPseudo(function (dataName) {
return function (elem) {
return !!$.data(elem, dataName);
};
})
: // support: jQuery <1.8
function (elem, i, match) {
return !!$.data(elem, match[3]);
},
focusable: function (element) {
return focusable(element, !isNaN($.attr(element, "tabindex")));
},
tabbable: function (element) {
var tabIndex = $.attr(element, "tabindex"),
isTabIndexNaN = isNaN(tabIndex);
return (
(isTabIndexNaN || tabIndex >= 0) && focusable(element, !isTabIndexNaN)
);
},
});
// support: jQuery <1.8
if (!$("<a>").outerWidth(1).jquery) {
$.each(["Width", "Height"], function (i, name) {
var side = name === "Width" ? ["Left", "Right"] : ["Top", "Bottom"],
type = name.toLowerCase(),
orig = {
innerWidth: $.fn.innerWidth,
innerHeight: $.fn.innerHeight,
outerWidth: $.fn.outerWidth,
outerHeight: $.fn.outerHeight,
};
function reduce(elem, size, border, margin) {
$.each(side, function () {
size -= parseFloat($.css(elem, "padding" + this)) || 0;
if (border) {
size -= parseFloat($.css(elem, "border" + this + "Width")) || 0;
}
if (margin) {
size -= parseFloat($.css(elem, "margin" + this)) || 0;
}
});
return size;
}
$.fn["inner" + name] = function (size) {
if (size === undefined) {
return orig["inner" + name].call(this);
}
return this.each(function () {
$(this).css(type, reduce(this, size) + "px");
});
};
$.fn["outer" + name] = function (size, margin) {
if (typeof size !== "number") {
return orig["outer" + name].call(this, size);
}
return this.each(function () {
$(this).css(type, reduce(this, size, true, margin) + "px");
});
};
});
}
// support: jQuery <1.8
if (!$.fn.addBack) {
$.fn.addBack = function (selector) {
return this.add(
selector == null ? this.prevObject : this.prevObject.filter(selector),
);
};
}
// support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413)
if ($("<a>").data("a-b", "a").removeData("a-b").data("a-b")) {
$.fn.removeData = (function (removeData) {
return function (key) {
if (arguments.length) {
return removeData.call(this, $.camelCase(key));
} else {
return removeData.call(this);
}
};
})($.fn.removeData);
}
// deprecated
$.ui.ie = !!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase());
$.support.selectstart = "onselectstart" in document.createElement("div");
$.fn.extend({
disableSelection: function () {
return this.bind(
($.support.selectstart ? "selectstart" : "mousedown") +
".ui-disableSelection",
function (event) {
event.preventDefault();
},
);
},
enableSelection: function () {
return this.unbind(".ui-disableSelection");
},
zIndex: function (zIndex) {
if (zIndex !== undefined) {
return this.css("zIndex", zIndex);
}
if (this.length) {
var elem = $(this[0]),
position,
value;
while (elem.length && elem[0] !== document) {
// Ignore z-index if position is set to a value where z-index is ignored by the browser
// This makes behavior of this function consistent across browsers
// WebKit always returns auto if the element is positioned
position = elem.css("position");
if (
position === "absolute" ||
position === "relative" ||
position === "fixed"
) {
// IE returns 0 when zIndex is not specified
// other browsers return a string
// we ignore the case of nested elements with an explicit value of 0
// <div style="z-index: -10;"><div style="z-index: 0;"></div></div>
value = parseInt(elem.css("zIndex"), 10);
if (!isNaN(value) && value !== 0) {
return value;
}
}
elem = elem.parent();
}
}
return 0;
},
});
// $.ui.plugin is deprecated. Use $.widget() extensions instead.
$.ui.plugin = {
add: function (module, option, set) {
var i,
proto = $.ui[module].prototype;
for (i in set) {
proto.plugins[i] = proto.plugins[i] || [];
proto.plugins[i].push([option, set[i]]);
}
},
call: function (instance, name, args, allowDisconnected) {
var i,
set = instance.plugins[name];
if (!set) {
return;
}
if (
!allowDisconnected &&
(!instance.element[0].parentNode ||
instance.element[0].parentNode.nodeType === 11)
) {
return;
}
for (i = 0; i < set.length; i++) {
if (instance.options[set[i][0]]) {
set[i][1].apply(instance.element, args);
}
}
},
};
})(jQuery);
(function ($, window, undefined) {
// Subtract the height of external toolbars from the page height, if the page does not have
// internal toolbars of the same type
var compensateToolbars = function (page, desiredHeight) {
var pageParent = page.parent(),
toolbarsAffectingHeight = [],
externalHeaders = pageParent.children(":jqmData(role='header')"),
internalHeaders = page.children(":jqmData(role='header')"),
externalFooters = pageParent.children(":jqmData(role='footer')"),
internalFooters = page.children(":jqmData(role='footer')");
// If we have no internal headers, but we do have external headers, then their height
// reduces the page height
if (internalHeaders.length === 0 && externalHeaders.length > 0) {
toolbarsAffectingHeight = toolbarsAffectingHeight.concat(
externalHeaders.toArray(),
);
}
// If we have no internal footers, but we do have external footers, then their height
// reduces the page height
if (internalFooters.length === 0 && externalFooters.length > 0) {
toolbarsAffectingHeight = toolbarsAffectingHeight.concat(
externalFooters.toArray(),
);
}
$.each(toolbarsAffectingHeight, function (index, value) {
desiredHeight -= $(value).outerHeight();
});
// Height must be at least zero
return Math.max(0, desiredHeight);
};
$.extend($.mobile, {
// define the window and the document objects
window: $(window),
document: $(document),
// TODO: Remove and use $.ui.keyCode directly
keyCode: $.ui.keyCode,
// Place to store various widget extensions
behaviors: {},
// Scroll page vertically: scroll to 0 to hide iOS address bar, or pass a Y value
silentScroll: function (ypos) {
if ($.type(ypos) !== "number") {
ypos = $.mobile.defaultHomeScroll;
}
// prevent scrollstart and scrollstop events
$.event.special.scrollstart.enabled = false;
setTimeout(function () {
window.scrollTo(0, ypos);
$.mobile.document.trigger("silentscroll", { x: 0, y: ypos });
}, 20);
setTimeout(function () {
$.event.special.scrollstart.enabled = true;
}, 150);
},
getClosestBaseUrl: function (ele) {
// Find the closest page and extract out its url.
var url = $(ele).closest(".ui-page").jqmData("url"),
base = $.mobile.path.documentBase.hrefNoHash;
if (
!$.mobile.dynamicBaseEnabled ||
!url ||
!$.mobile.path.isPath(url)
) {
url = base;
}
return $.mobile.path.makeUrlAbsolute(url, base);
},
removeActiveLinkClass: function (forceRemoval) {
if (
!!$.mobile.activeClickedLink &&
(!$.mobile.activeClickedLink.closest("." + $.mobile.activePageClass)
.length ||
forceRemoval)
) {
$.mobile.activeClickedLink.removeClass($.mobile.activeBtnClass);
}
$.mobile.activeClickedLink = null;
},
// DEPRECATED in 1.4
// Find the closest parent with a theme class on it. Note that
// we are not using $.fn.closest() on purpose here because this
// method gets called quite a bit and we need it to be as fast
// as possible.
getInheritedTheme: function (el, defaultTheme) {
var e = el[0],
ltr = "",
re = /ui-(bar|body|overlay)-([a-z])\b/,
c,
m;
while (e) {
c = e.className || "";
if (c && (m = re.exec(c)) && (ltr = m[2])) {
// We found a parent with a theme class
// on it so bail from this loop.
break;
}
e = e.parentNode;
}
// Return the theme letter we found, if none, return the
// specified default.
return ltr || defaultTheme || "a";
},
enhanceable: function (elements) {
return this.haveParents(elements, "enhance");
},
hijackable: function (elements) {
return this.haveParents(elements, "ajax");
},
haveParents: function (elements, attr) {
if (!$.mobile.ignoreContentEnabled) {
return elements;
}
var count = elements.length,
$newSet = $(),
e,
$element,
excluded,
i,
c;
for (i = 0; i < count; i++) {
$element = elements.eq(i);
excluded = false;
e = elements[i];
while (e) {
c = e.getAttribute
? e.getAttribute("data-" + $.mobile.ns + attr)
: "";
if (c === "false") {
excluded = true;
break;
}
e = e.parentNode;
}
if (!excluded) {
$newSet = $newSet.add($element);
}
}
return $newSet;
},
getScreenHeight: function () {
// Native innerHeight returns more accurate value for this across platforms,
// jQuery version is here as a normalized fallback for platforms like Symbian
return window.innerHeight || $.mobile.window.height();
},
//simply set the active page's minimum height to screen height, depending on orientation
resetActivePageHeight: function (height) {
var page = $("." + $.mobile.activePageClass),
pageHeight = page.height(),
pageOuterHeight = page.outerHeight(true);
height = compensateToolbars(
page,
typeof height === "number" ? height : $.mobile.getScreenHeight(),
);
page.css("min-height", height - (pageOuterHeight - pageHeight));
},
loading: function () {
// If this is the first call to this function, instantiate a loader widget
var loader =
this.loading._widget ||
$($.mobile.loader.prototype.defaultHtml).loader(),
// Call the appropriate method on the loader
returnValue = loader.loader.apply(loader, arguments);
// Make sure the loader is retained for future calls to this function.
this.loading._widget = loader;
return returnValue;
},
});
$.addDependents = function (elem, newDependents) {
var $elem = $(elem),
dependents = $elem.jqmData("dependents") || $();
$elem.jqmData("dependents", $(dependents).add(newDependents));
};
// plugins
$.fn.extend({
removeWithDependents: function () {
$.removeWithDependents(this);
},
// Enhance child elements
enhanceWithin: function () {
var index,
widgetElements = {},
keepNative = $.mobile.page.prototype.keepNativeSelector(),
that = this;
// Add no js class to elements
if ($.mobile.nojs) {
$.mobile.nojs(this);
}
// Bind links for ajax nav
if ($.mobile.links) {
$.mobile.links(this);
}
// Degrade inputs for styleing
if ($.mobile.degradeInputsWithin) {
$.mobile.degradeInputsWithin(this);
}
// Run buttonmarkup
if ($.fn.buttonMarkup) {
this.find($.fn.buttonMarkup.initSelector)
.not(keepNative)
.jqmEnhanceable()
.buttonMarkup();
}
// Add classes for fieldContain
if ($.fn.fieldcontain) {
this.find(":jqmData(role='fieldcontain')")
.not(keepNative)
.jqmEnhanceable()
.fieldcontain();
}
// Enhance widgets
$.each($.mobile.widgets, function (name, constructor) {
// If initSelector not false find elements
if (constructor.initSelector) {
// Filter elements that should not be enhanced based on parents
var elements = $.mobile.enhanceable(
that.find(constructor.initSelector),
);
// If any matching elements remain filter ones with keepNativeSelector
if (elements.length > 0) {
// $.mobile.page.prototype.keepNativeSelector is deprecated this is just for backcompat
// Switch to $.mobile.keepNative in 1.5 which is just a value not a function
elements = elements.not(keepNative);
}
// Enhance whatever is left
if (elements.length > 0) {
widgetElements[constructor.prototype.widgetName] = elements;
}
}
});
for (index in widgetElements) {
widgetElements[index][index]();
}
return this;
},
addDependents: function (newDependents) {
$.addDependents(this, newDependents);
},
// note that this helper doesn't attempt to handle the callback
// or setting of an html element's text, its only purpose is
// to return the html encoded version of the text in all cases. (thus the name)
getEncodedText: function () {
return $("<a>").text(this.text()).html();
},
// fluent helper function for the mobile namespaced equivalent
jqmEnhanceable: function () {
return $.mobile.enhanceable(this);
},
jqmHijackable: function () {
return $.mobile.hijackable(this);
},
});
$.removeWithDependents = function (nativeElement) {
var element = $(nativeElement);
(element.jqmData("dependents") || $()).remove();
element.remove();
};
$.addDependents = function (nativeElement, newDependents) {
var element = $(nativeElement),
dependents = element.jqmData("dependents") || $();
element.jqmData("dependents", $(dependents).add(newDependents));
};
$.find.matches = function (expr, set) {
return $.find(expr, null, null, set);
};
$.find.matchesSelector = function (node, expr) {
return $.find(expr, null, null, [node]).length > 0;
};
})(jQuery, this);
/*!
* jQuery UI Widget c0ab71056b936627e8a7821f03c044aec6280a40
* http://jqueryui.com
*
* Copyright 2013 jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
* http://api.jqueryui.com/jQuery.widget/
*/
(function ($, undefined) {
var uuid = 0,
slice = Array.prototype.slice,
_cleanData = $.cleanData;
$.cleanData = function (elems) {
for (var i = 0, elem; (elem = elems[i]) != null; i++) {
try {
$(elem).triggerHandler("remove");
// http://bugs.jquery.com/ticket/8235
} catch (e) {}
}
_cleanData(elems);
};
$.widget = function (name, base, prototype) {
var fullName,
existingConstructor,
constructor,
basePrototype,
// proxiedPrototype allows the provided prototype to remain unmodified
// so that it can be used as a mixin for multiple widgets (#8876)
proxiedPrototype = {},
namespace = name.split(".")[0];
name = name.split(".")[1];
fullName = namespace + "-" + name;
if (!prototype) {
prototype = base;
base = $.Widget;
}
// create selector for plugin
$.expr[":"][fullName.toLowerCase()] = function (elem) {
return !!$.data(elem, fullName);
};
$[namespace] = $[namespace] || {};
existingConstructor = $[namespace][name];
constructor = $[namespace][name] = function (options, element) {
// allow instantiation without "new" keyword
if (!this._createWidget) {
return new constructor(options, element);
}
// allow instantiation without initializing for simple inheritance
// must use "new" keyword (the code above always passes args)
if (arguments.length) {
this._createWidget(options, element);
}
};
// extend with the existing constructor to carry over any static properties
$.extend(constructor, existingConstructor, {
version: prototype.version,
// copy the object used to create the prototype in case we need to
// redefine the widget later
_proto: $.extend({}, prototype),
// track widgets that inherit from this widget in case this widget is
// redefined after a widget inherits from it
_childConstructors: [],
});
basePrototype = new base();
// we need to make the options hash a property directly on the new instance
// otherwise we'll modify the options hash on the prototype that we're
// inheriting from
basePrototype.options = $.widget.extend({}, basePrototype.options);
$.each(prototype, function (prop, value) {
if (!$.isFunction(value)) {
proxiedPrototype[prop] = value;
return;
}
proxiedPrototype[prop] = (function () {
var _super = function () {
return base.prototype[prop].apply(this, arguments);
},
_superApply = function (args) {
return base.prototype[prop].apply(this, args);
};
return function () {
var __super = this._super,
__superApply = this._superApply,
returnValue;
this._super = _super;
this._superApply = _superApply;
returnValue = value.apply(this, arguments);
this._super = __super;
this._superApply = __superApply;
return returnValue;
};
})();
});
constructor.prototype = $.widget.extend(
basePrototype,
{
// TODO: remove support for widgetEventPrefix
// always use the name + a colon as the prefix, e.g., draggable:start
// don't prefix for widgets that aren't DOM-based
widgetEventPrefix: existingConstructor
? basePrototype.widgetEventPrefix || name
: name,
},
proxiedPrototype,
{
constructor: constructor,
namespace: namespace,
widgetName: name,
widgetFullName: fullName,
},
);
// If this widget is being redefined then we need to find all widgets that
// are inheriting from it and redefine all of them so that they inherit from
// the new version of this widget. We're essentially trying to replace one
// level in the prototype chain.
if (existingConstructor) {
$.each(existingConstructor._childConstructors, function (i, child) {
var childPrototype = child.prototype;
// redefine the child widget using the same prototype that was
// originally used, but inherit from the new version of the base
$.widget(
childPrototype.namespace + "." + childPrototype.widgetName,
constructor,
child._proto,
);
});
// remove the list of existing child constructors from the old constructor
// so the old child constructors can be garbage collected
delete existingConstructor._childConstructors;
} else {
base._childConstructors.push(constructor);
}
$.widget.bridge(name, constructor);
return constructor;
};
$.widget.extend = function (target) {
var input = slice.call(arguments, 1),
inputIndex = 0,
inputLength = input.length,
key,
value;
for (; inputIndex < inputLength; inputIndex++) {
for (key in input[inputIndex]) {
value = input[inputIndex][key];
if (input[inputIndex].hasOwnProperty(key) && value !== undefined) {
// Clone objects
if ($.isPlainObject(value)) {
target[key] = $.isPlainObject(target[key])
? $.widget.extend({}, target[key], value)
: // Don't extend strings, arrays, etc. with objects
$.widget.extend({}, value);
// Copy everything else by reference
} else {
target[key] = value;
}
}
}
}
return target;
};
$.widget.bridge = function (name, object) {
var fullName = object.prototype.widgetFullName || name;
$.fn[name] = function (options) {
var isMethodCall = typeof options === "string",
args = slice.call(arguments, 1),
returnValue = this;
// allow multiple hashes to be passed on init
options =
!isMethodCall && args.length
? $.widget.extend.apply(null, [options].concat(args))
: options;
if (isMethodCall) {
this.each(function () {
var methodValue,
instance = $.data(this, fullName);
if (options === "instance") {
returnValue = instance;
return false;
}
if (!instance) {
return $.error(
"cannot call methods on " +
name +
" prior to initialization; " +
"attempted to call method '" +
options +
"'",
);
}
if (!$.isFunction(instance[options]) || options.charAt(0) === "_") {
return $.error(
"no such method '" +
options +
"' for " +
name +
" widget instance",
);
}
methodValue = instance[options].apply(instance, args);
if (methodValue !== instance && methodValue !== undefined) {
returnValue =
methodValue && methodValue.jquery
? returnValue.pushStack(methodValue.get())
: methodValue;
return false;
}
});
} else {
this.each(function () {
var instance = $.data(this, fullName);
if (instance) {
instance.option(options || {})._init();
} else {
$.data(this, fullName, new object(options, this));
}
});
}
return returnValue;
};
};
$.Widget = function (/* options, element */) {};
$.Widget._childConstructors = [];
$.Widget.prototype = {
widgetName: "widget",
widgetEventPrefix: "",
defaultElement: "<div>",
options: {
disabled: false,
// callbacks
create: null,
},
_createWidget: function (options, element) {
element = $(element || this.defaultElement || this)[0];
this.element = $(element);
this.uuid = uuid++;
this.eventNamespace = "." + this.widgetName + this.uuid;
this.options = $.widget.extend(
{},
this.options,
this._getCreateOptions(),
options,
);
this.bindings = $();
this.hoverable = $();
this.focusable = $();
if (element !== this) {
$.data(element, this.widgetFullName, this);
this._on(true, this.element, {
remove: function (event) {
if (event.target === element) {
this.destroy();
}
},
});
this.document = $(
element.style
? // element within the document
element.ownerDocument
: // element is window or document
element.document || element,
);
this.window = $(
this.document[0].defaultView || this.document[0].parentWindow,
);
}
this._create();
this._trigger("create", null, this._getCreateEventData());
this._init();
},
_getCreateOptions: $.noop,
_getCreateEventData: $.noop,
_create: $.noop,
_init: $.noop,
destroy: function () {
this._destroy();
// we can probably remove the unbind calls in 2.0
// all event bindings should go through this._on()
this.element
.unbind(this.eventNamespace)
.removeData(this.widgetFullName)
// support: jquery <1.6.3
// http://bugs.jquery.com/ticket/9413
.removeData($.camelCase(this.widgetFullName));
this.widget()
.unbind(this.eventNamespace)
.removeAttr("aria-disabled")
.removeClass(
this.widgetFullName + "-disabled " + "ui-state-disabled",
);
// clean up events and states
this.bindings.unbind(this.eventNamespace);
this.hoverable.removeClass("ui-state-hover");
this.focusable.removeClass("ui-state-focus");
},
_destroy: $.noop,
widget: function () {
return this.element;
},
option: function (key, value) {
var options = key,
parts,
curOption,
i;
if (arguments.length === 0) {
// don't return a reference to the internal hash
return $.widget.extend({}, this.options);
}
if (typeof key === "string") {
// handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } }
options = {};
parts = key.split(".");
key = parts.shift();
if (parts.length) {
curOption = options[key] = $.widget.extend({}, this.options[key]);
for (i = 0; i < parts.length - 1; i++) {
curOption[parts[i]] = curOption[parts[i]] || {};
curOption = curOption[parts[i]];
}
key = parts.pop();
if (value === undefined) {
return curOption[key] === undefined ? null : curOption[key];
}
curOption[key] = value;
} else {
if (value === undefined) {
return this.options[key] === undefined ? null : this.options[key];
}
options[key] = value;
}
}
this._setOptions(options);
return this;
},
_setOptions: function (options) {
var key;
for (key in options) {
this._setOption(key, options[key]);
}
return this;
},
_setOption: function (key, value) {
this.options[key] = value;
if (key === "disabled") {
this.widget().toggleClass(this.widgetFullName + "-disabled", !!value);
this.hoverable.removeClass("ui-state-hover");
this.focusable.removeClass("ui-state-focus");
}
return this;
},
enable: function () {
return this._setOptions({ disabled: false });
},
disable: function () {
return this._setOptions({ disabled: true });
},
_on: function (suppressDisabledCheck, element, handlers) {
var delegateElement,
instance = this;
// no suppressDisabledCheck flag, shuffle arguments
if (typeof suppressDisabledCheck !== "boolean") {
handlers = element;
element = suppressDisabledCheck;
suppressDisabledCheck = false;
}
// no element argument, shuffle and use this.element
if (!handlers) {
handlers = element;
element = this.element;
delegateElement = this.widget();
} else {
// accept selectors, DOM elements
element = delegateElement = $(element);
this.bindings = this.bindings.add(element);
}
$.each(handlers, function (event, handler) {
function handlerProxy() {
// allow widgets to customize the disabled handling
// - disabled as an array instead of boolean
// - disabled class as method for disabling individual parts
if (
!suppressDisabledCheck &&
(instance.options.disabled === true ||
$(this).hasClass("ui-state-disabled"))
) {
return;
}
return (
typeof handler === "string" ? instance[handler] : handler
).apply(instance, arguments);
}
// copy the guid so direct unbinding works
if (typeof handler !== "string") {
handlerProxy.guid = handler.guid =
handler.guid || handlerProxy.guid || $.guid++;
}
var match = event.match(/^(\w+)\s*(.*)$/),
eventName = match[1] + instance.eventNamespace,
selector = match[2];
if (selector) {
delegateElement.delegate(selector, eventName, handlerProxy);
} else {
element.bind(eventName, handlerProxy);
}
});
},
_off: function (element, eventName) {
eventName =
(eventName || "").split(" ").join(this.eventNamespace + " ") +
this.eventNamespace;
element.unbind(eventName).undelegate(eventName);
},
_delay: function (handler, delay) {
function handlerProxy() {
return (
typeof handler === "string" ? instance[handler] : handler
).apply(instance, arguments);
}
var instance = this;
return setTimeout(handlerProxy, delay || 0);
},
_hoverable: function (element) {
this.hoverable = this.hoverable.add(element);
this._on(element, {
mouseenter: function (event) {
$(event.currentTarget).addClass("ui-state-hover");
},
mouseleave: function (event) {
$(event.currentTarget).removeClass("ui-state-hover");
},
});
},
_focusable: function (element) {
this.focusable = this.focusable.add(element);
this._on(element, {
focusin: function (event) {
$(event.currentTarget).addClass("ui-state-focus");
},
focusout: function (event) {
$(event.currentTarget).removeClass("ui-state-focus");
},
});
},
_trigger: function (type, event, data) {
var prop,
orig,
callback = this.options[type];
data = data || {};
event = $.Event(event);
event.type = (
type === this.widgetEventPrefix ? type : this.widgetEventPrefix + type
).toLowerCase();
// the original event may come from any element
// so we need to reset the target on the new event
event.target = this.element[0];
// copy original event properties over to the new event
orig = event.originalEvent;
if (orig) {
for (prop in orig) {
if (!(prop in event)) {
event[prop] = orig[prop];
}
}
}
this.element.trigger(event, data);
return !(
($.isFunction(callback) &&
callback.apply(this.element[0], [event].concat(data)) === false) ||
event.isDefaultPrevented()
);
},
};
$.each(
{ show: "fadeIn", hide: "fadeOut" },
function (method, defaultEffect) {
$.Widget.prototype["_" + method] = function (
element,
options,
callback,
) {
if (typeof options === "string") {
options = { effect: options };
}
var hasOptions,
effectName = !options
? method
: options === true || typeof options === "number"
? defaultEffect
: options.effect || defaultEffect;
options = options || {};
if (typeof options === "number") {
options = { duration: options };
}
hasOptions = !$.isEmptyObject(options);
options.complete = callback;
if (options.delay) {
element.delay(options.delay);
}
if (hasOptions && $.effects && $.effects.effect[effectName]) {
element[method](options);
} else if (effectName !== method && element[effectName]) {
element[effectName](options.duration, options.easing, callback);
} else {
element.queue(function (next) {
$(this)[method]();
if (callback) {
callback.call(element[0]);
}
next();
});
}
};
},
);
})(jQuery);
(function ($, undefined) {
var rcapitals = /[A-Z]/g,
replaceFunction = function (c) {
return "-" + c.toLowerCase();
};
$.extend($.Widget.prototype, {
_getCreateOptions: function () {
var option,
value,
elem = this.element[0],
options = {};
//
if (!$.mobile.getAttribute(elem, "defaults")) {
for (option in this.options) {
value = $.mobile.getAttribute(
elem,
option.replace(rcapitals, replaceFunction),
);
if (value != null) {
options[option] = value;
}
}
}
return options;
},
});
//TODO: Remove in 1.5 for backcompat only
$.mobile.widget = $.Widget;
})(jQuery);
(function ($) {
// TODO move loader class down into the widget settings
var loaderClass = "ui-loader",
$html = $("html");
$.widget("mobile.loader", {
// NOTE if the global config settings are defined they will override these
// options
options: {
// the theme for the loading message
theme: "a",
// whether the text in the loading message is shown
textVisible: false,
// custom html for the inner content of the loading message
html: "",
// the text to be displayed when the popup is shown
text: "loading",
},
defaultHtml:
"<div class='" +
loaderClass +
"'>" +
"<span class='ui-icon-loading'></span>" +
"<h1></h1>" +
"</div>",
// For non-fixed supportin browsers. Position at y center (if scrollTop supported), above the activeBtn (if defined), or just 100px from top
fakeFixLoader: function () {
var activeBtn = $("." + $.mobile.activeBtnClass).first();
this.element.css({
top:
($.support.scrollTop &&
this.window.scrollTop() + this.window.height() / 2) ||
(activeBtn.length && activeBtn.offset().top) ||
100,
});
},
// check position of loader to see if it appears to be "fixed" to center
// if not, use abs positioning
checkLoaderPosition: function () {
var offset = this.element.offset(),
scrollTop = this.window.scrollTop(),
screenHeight = $.mobile.getScreenHeight();
if (offset.top < scrollTop || offset.top - scrollTop > screenHeight) {
this.element.addClass("ui-loader-fakefix");
this.fakeFixLoader();
this.window
.unbind("scroll", this.checkLoaderPosition)
.bind("scroll", $.proxy(this.fakeFixLoader, this));
}
},
resetHtml: function () {
this.element.html($(this.defaultHtml).html());
},
// Turn on/off page loading message. Theme doubles as an object argument
// with the following shape: { theme: '', text: '', html: '', textVisible: '' }
// NOTE that the $.mobile.loading* settings and params past the first are deprecated
// TODO sweet jesus we need to break some of this out
show: function (theme, msgText, textonly) {
var textVisible, message, loadSettings;
this.resetHtml();
// use the prototype options so that people can set them globally at
// mobile init. Consistency, it's what's for dinner
if ($.type(theme) === "object") {
loadSettings = $.extend({}, this.options, theme);
theme = loadSettings.theme;
} else {
loadSettings = this.options;
// here we prefer the theme value passed as a string argument, then
// we prefer the global option because we can't use undefined default
// prototype options, then the prototype option
theme = theme || loadSettings.theme;
}
// set the message text, prefer the param, then the settings object
// then loading message
message =
msgText || (loadSettings.text === false ? "" : loadSettings.text);
// prepare the dom
$html.addClass("ui-loading");
textVisible = loadSettings.textVisible;
// add the proper css given the options (theme, text, etc)
// Force text visibility if the second argument was supplied, or
// if the text was explicitly set in the object args
this.element.attr(
"class",
loaderClass +
" ui-corner-all ui-body-" +
theme +
" ui-loader-" +
(textVisible || msgText || theme.text ? "verbose" : "default") +
(loadSettings.textonly || textonly ? " ui-loader-textonly" : ""),
);
// TODO verify that jquery.fn.html is ok to use in both cases here
// this might be overly defensive in preventing unknowing xss
// if the html attribute is defined on the loading settings, use that
// otherwise use the fallbacks from above
if (loadSettings.html) {
this.element.html(loadSettings.html);
} else {
this.element.find("h1").text(message);
}
// attach the loader to the DOM
this.element.appendTo($.mobile.pageContainer);
// check that the loader is visible
this.checkLoaderPosition();
// on scroll check the loader position
this.window.bind("scroll", $.proxy(this.checkLoaderPosition, this));
},
hide: function () {
$html.removeClass("ui-loading");
if (this.options.text) {
this.element.removeClass("ui-loader-fakefix");
}
$.mobile.window.unbind("scroll", this.fakeFixLoader);
$.mobile.window.unbind("scroll", this.checkLoaderPosition);
},
});
})(jQuery, this);
/*!
* jQuery hashchange event - v1.3 - 7/21/2010
* http://benalman.com/projects/jquery-hashchange-plugin/
*
* Copyright (c) 2010 "Cowboy" Ben Alman
* Dual licensed under the MIT and GPL licenses.
* http://benalman.com/about/license/
*/
// Script: jQuery hashchange event
//
// *Version: 1.3, Last updated: 7/21/2010*
//
// Project Home - http://benalman.com/projects/jquery-hashchange-plugin/
// GitHub - http://github.com/cowboy/jquery-hashchange/
// Source - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.js
// (Minified) - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.min.js (0.8kb gzipped)
//
// About: License
//
// Copyright (c) 2010 "Cowboy" Ben Alman,
// Dual licensed under the MIT and GPL licenses.
// http://benalman.com/about/license/
//
// About: Examples
//
// These working examples, complete with fully commented code, illustrate a few
// ways in which this plugin can be used.
//
// hashchange event - http://benalman.com/code/projects/jquery-hashchange/examples/hashchange/
// document.domain - http://benalman.com/code/projects/jquery-hashchange/examples/document_domain/
//
// About: Support and Testing
//
// Information about what version or versions of jQuery this plugin has been
// tested with, what browsers it has been tested in, and where the unit tests
// reside (so you can test it yourself).
//
// jQuery Versions - 1.2.6, 1.3.2, 1.4.1, 1.4.2
// Browsers Tested - Internet Explorer 6-8, Firefox 2-4, Chrome 5-6, Safari 3.2-5,
// Opera 9.6-10.60, iPhone 3.1, Android 1.6-2.2, BlackBerry 4.6-5.
// Unit Tests - http://benalman.com/code/projects/jquery-hashchange/unit/
//
// About: Known issues
//
// While this jQuery hashchange event implementation is quite stable and
// robust, there are a few unfortunate browser bugs surrounding expected
// hashchange event-based behaviors, independent of any JavaScript
// window.onhashchange abstraction. See the following examples for more
// information:
//
// Chrome: Back Button - http://benalman.com/code/projects/jquery-hashchange/examples/bug-chrome-back-button/
// Firefox: Remote XMLHttpRequest - http://benalman.com/code/projects/jquery-hashchange/examples/bug-firefox-remote-xhr/
// WebKit: Back Button in an Iframe - http://benalman.com/code/projects/jquery-hashchange/examples/bug-webkit-hash-iframe/
// Safari: Back Button from a different domain - http://benalman.com/code/projects/jquery-hashchange/examples/bug-safari-back-from-diff-domain/
//
// Also note that should a browser natively support the window.onhashchange
// event, but not report that it does, the fallback polling loop will be used.
//
// About: Release History
//
// 1.3 - (7/21/2010) Reorganized IE6/7 Iframe code to make it more
// "removable" for mobile-only development. Added IE6/7 document.title
// support. Attempted to make Iframe as hidden as possible by using
// techniques from http://www.paciellogroup.com/blog/?p=604. Added
// support for the "shortcut" format $(window).hashchange( fn ) and
// $(window).hashchange() like jQuery provides for built-in events.
// Renamed jQuery.hashchangeDelay to <jQuery.fn.hashchange.delay> and
// lowered its default value to 50. Added <jQuery.fn.hashchange.domain>
// and <jQuery.fn.hashchange.src> properties plus document-domain.html
// file to address access denied issues when setting document.domain in
// IE6/7.
// 1.2 - (2/11/2010) Fixed a bug where coming back to a page using this plugin
// from a page on another domain would cause an error in Safari 4. Also,
// IE6/7 Iframe is now inserted after the body (this actually works),
// which prevents the page from scrolling when the event is first bound.
// Event can also now be bound before DOM ready, but it won't be usable
// before then in IE6/7.
// 1.1 - (1/21/2010) Incorporated document.documentMode test to fix IE8 bug
// where browser version is incorrectly reported as 8.0, despite
// inclusion of the X-UA-Compatible IE=EmulateIE7 meta tag.
// 1.0 - (1/9/2010) Initial Release. Broke out the jQuery BBQ event.special
// window.onhashchange functionality into a separate plugin for users
// who want just the basic event & back button support, without all the
// extra awesomeness that BBQ provides. This plugin will be included as
// part of jQuery BBQ, but also be available separately.
(function ($, window, undefined) {
"$:nomunge"; // Used by YUI compressor.
// Reused string.
var str_hashchange = "hashchange",
// Method / object references.
doc = document,
fake_onhashchange,
special = $.event.special,
// Does the browser support window.onhashchange? Note that IE8 running in
// IE7 compatibility mode reports true for 'onhashchange' in window, even
// though the event isn't supported, so also test document.documentMode.
doc_mode = doc.documentMode,
supports_onhashchange =
"on" + str_hashchange in window &&
(doc_mode === undefined || doc_mode > 7);
// Get location.hash (or what you'd expect location.hash to be) sans any
// leading #. Thanks for making this necessary, Firefox!
function get_fragment(url) {
url = url || location.href;
return "#" + url.replace(/^[^#]*#?(.*)$/, "$1");
}
// Method: jQuery.fn.hashchange
//
// Bind a handler to the window.onhashchange event or trigger all bound
// window.onhashchange event handlers. This behavior is consistent with
// jQuery's built-in event handlers.
//
// Usage:
//
// > jQuery(window).hashchange( [ handler ] );
//
// Arguments:
//
// handler - (Function) Optional handler to be bound to the hashchange
// event. This is a "shortcut" for the more verbose form:
// jQuery(window).bind( 'hashchange', handler ). If handler is omitted,
// all bound window.onhashchange event handlers will be triggered. This
// is a shortcut for the more verbose
// jQuery(window).trigger( 'hashchange' ). These forms are described in
// the <hashchange event> section.
//
// Returns:
//
// (jQuery) The initial jQuery collection of elements.
// Allow the "shortcut" format $(elem).hashchange( fn ) for binding and
// $(elem).hashchange() for triggering, like jQuery does for built-in events.
$.fn[str_hashchange] = function (fn) {
return fn ? this.bind(str_hashchange, fn) : this.trigger(str_hashchange);
};
// Property: jQuery.fn.hashchange.delay
//
// The numeric interval (in milliseconds) at which the <hashchange event>
// polling loop executes. Defaults to 50.
// Property: jQuery.fn.hashchange.domain
//
// If you're setting document.domain in your JavaScript, and you want hash
// history to work in IE6/7, not only must this property be set, but you must
// also set document.domain BEFORE jQuery is loaded into the page. This
// property is only applicable if you are supporting IE6/7 (or IE8 operating
// in "IE7 compatibility" mode).
//
// In addition, the <jQuery.fn.hashchange.src> property must be set to the
// path of the included "document-domain.html" file, which can be renamed or
// modified if necessary (note that the document.domain specified must be the
// same in both your main JavaScript as well as in this file).
//
// Usage:
//
// jQuery.fn.hashchange.domain = document.domain;
// Property: jQuery.fn.hashchange.src
//
// If, for some reason, you need to specify an Iframe src file (for example,
// when setting document.domain as in <jQuery.fn.hashchange.domain>), you can
// do so using this property. Note that when using this property, history
// won't be recorded in IE6/7 until the Iframe src file loads. This property
// is only applicable if you are supporting IE6/7 (or IE8 operating in "IE7
// compatibility" mode).
//
// Usage:
//
// jQuery.fn.hashchange.src = 'path/to/file.html';
$.fn[str_hashchange].delay = 50;
/*
$.fn[ str_hashchange ].domain = null;
$.fn[ str_hashchange ].src = null;
*/
// Event: hashchange event
//
// Fired when location.hash changes. In browsers that support it, the native
// HTML5 window.onhashchange event is used, otherwise a polling loop is
// initialized, running every <jQuery.fn.hashchange.delay> milliseconds to
// see if the hash has changed. In IE6/7 (and IE8 operating in "IE7
// compatibility" mode), a hidden Iframe is created to allow the back button
// and hash-based history to work.
//
// Usage as described in <jQuery.fn.hashchange>:
//
// > // Bind an event handler.
// > jQuery(window).hashchange( function(e) {
// > var hash = location.hash;
// > ...
// > });
// >
// > // Manually trigger the event handler.
// > jQuery(window).hashchange();
//
// A more verbose usage that allows for event namespacing:
//
// > // Bind an event handler.
// > jQuery(window).bind( 'hashchange', function(e) {
// > var hash = location.hash;
// > ...
// > });
// >
// > // Manually trigger the event handler.
// > jQuery(window).trigger( 'hashchange' );
//
// Additional Notes:
//
// * The polling loop and Iframe are not created until at least one handler
// is actually bound to the 'hashchange' event.
// * If you need the bound handler(s) to execute immediately, in cases where
// a location.hash exists on page load, via bookmark or page refresh for
// example, use jQuery(window).hashchange() or the more verbose
// jQuery(window).trigger( 'hashchange' ).
// * The event can be bound before DOM ready, but since it won't be usable
// before then in IE6/7 (due to the necessary Iframe), recommended usage is
// to bind it inside a DOM ready handler.
// Override existing $.event.special.hashchange methods (allowing this plugin
// to be defined after jQuery BBQ in BBQ's source code).
special[str_hashchange] = $.extend(special[str_hashchange], {
// Called only when the first 'hashchange' event is bound to window.
setup: function () {
// If window.onhashchange is supported natively, there's nothing to do..
if (supports_onhashchange) {
return false;
}
// Otherwise, we need to create our own. And we don't want to call this
// until the user binds to the event, just in case they never do, since it
// will create a polling loop and possibly even a hidden Iframe.
$(fake_onhashchange.start);
},
// Called only when the last 'hashchange' event is unbound from window.
teardown: function () {
// If window.onhashchange is supported natively, there's nothing to do..
if (supports_onhashchange) {
return false;
}
// Otherwise, we need to stop ours (if possible).
$(fake_onhashchange.stop);
},
});
// fake_onhashchange does all the work of triggering the window.onhashchange
// event for browsers that don't natively support it, including creating a
// polling loop to watch for hash changes and in IE 6/7 creating a hidden
// Iframe to enable back and forward.
fake_onhashchange = (function () {
var self = {},
timeout_id,
// Remember the initial hash so it doesn't get triggered immediately.
last_hash = get_fragment(),
fn_retval = function (val) {
return val;
},
history_set = fn_retval,
history_get = fn_retval;
// Start the polling loop.
self.start = function () {
timeout_id || poll();
};
// Stop the polling loop.
self.stop = function () {
timeout_id && clearTimeout(timeout_id);
timeout_id = undefined;
};
// This polling loop checks every $.fn.hashchange.delay milliseconds to see
// if location.hash has changed, and triggers the 'hashchange' event on
// window when necessary.
function poll() {
var hash = get_fragment(),
history_hash = history_get(last_hash);
if (hash !== last_hash) {
history_set((last_hash = hash), history_hash);
$(window).trigger(str_hashchange);
} else if (history_hash !== last_hash) {
location.href = location.href.replace(/#.*/, "") + history_hash;
}
timeout_id = setTimeout(poll, $.fn[str_hashchange].delay);
}
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
// vvvvvvvvvvvvvvvvvvv REMOVE IF NOT SUPPORTING IE6/7/8 vvvvvvvvvvvvvvvvvvv
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
window.attachEvent &&
!window.addEventListener &&
!supports_onhashchange &&
(function () {
// Not only do IE6/7 need the "magical" Iframe treatment, but so does IE8
// when running in "IE7 compatibility" mode.
var iframe, iframe_src;
// When the event is bound and polling starts in IE 6/7, create a hidden
// Iframe for history handling.
self.start = function () {
if (!iframe) {
iframe_src = $.fn[str_hashchange].src;
iframe_src = iframe_src && iframe_src + get_fragment();
// Create hidden Iframe. Attempt to make Iframe as hidden as possible
// by using techniques from http://www.paciellogroup.com/blog/?p=604.
iframe = $('<iframe tabindex="-1" title="empty"/>')
.hide()
// When Iframe has completely loaded, initialize the history and
// start polling.
.one("load", function () {
iframe_src || history_set(get_fragment());
poll();
})
// Load Iframe src if specified, otherwise nothing.
.attr("src", iframe_src || "javascript:0")
// Append Iframe after the end of the body to prevent unnecessary
// initial page scrolling (yes, this works).
.insertAfter("body")[0].contentWindow;
// Whenever `document.title` changes, update the Iframe's title to
// prettify the back/next history menu entries. Since IE sometimes
// errors with "Unspecified error" the very first time this is set
// (yes, very useful) wrap this with a try/catch block.
doc.onpropertychange = function () {
try {
if (event.propertyName === "title") {
iframe.document.title = doc.title;
}
} catch (e) {}
};
}
};
// Override the "stop" method since an IE6/7 Iframe was created. Even
// if there are no longer any bound event handlers, the polling loop
// is still necessary for back/next to work at all!
self.stop = fn_retval;
// Get history by looking at the hidden Iframe's location.hash.
history_get = function () {
return get_fragment(iframe.location.href);
};
// Set a new history item by opening and then closing the Iframe
// document, *then* setting its location.hash. If document.domain has
// been set, update that as well.
history_set = function (hash, history_hash) {
var iframe_doc = iframe.document,
domain = $.fn[str_hashchange].domain;
if (hash !== history_hash) {
// Update Iframe with any initial `document.title` that might be set.
iframe_doc.title = doc.title;
// Opening the Iframe's document after it has been closed is what
// actually adds a history entry.
iframe_doc.open();
// Set document.domain for the Iframe document as well, if necessary.
domain &&
iframe_doc.write(
'<script>document.domain="' + domain + '"</script>',
);
iframe_doc.close();
// Update the Iframe's hash, for great justice.
iframe.location.hash = hash;
}
};
})();
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// ^^^^^^^^^^^^^^^^^^^ REMOVE IF NOT SUPPORTING IE6/7/8 ^^^^^^^^^^^^^^^^^^^
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
return self;
})();
})(jQuery, this);
(function ($, undefined) {
/*! matchMedia() polyfill - Test a CSS media type/query in JS. Authors & copyright (c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas. Dual MIT/BSD license */
window.matchMedia =
window.matchMedia ||
(function (doc, undefined) {
var bool,
docElem = doc.documentElement,
refNode = docElem.firstElementChild || docElem.firstChild,
// fakeBody required for <FF4 when executed in <head>
fakeBody = doc.createElement("body"),
div = doc.createElement("div");
div.id = "mq-test-1";
div.style.cssText = "position:absolute;top:-100em";
fakeBody.style.background = "none";
fakeBody.appendChild(div);
return function (q) {
div.innerHTML =
'&shy;<style media="' +
q +
'"> #mq-test-1 { width: 42px; }</style>';
docElem.insertBefore(fakeBody, refNode);
bool = div.offsetWidth === 42;
docElem.removeChild(fakeBody);
return {
matches: bool,
media: q,
};
};
})(document);
// $.mobile.media uses matchMedia to return a boolean.
$.mobile.media = function (q) {
return window.matchMedia(q).matches;
};
})(jQuery);
(function ($, undefined) {
var support = {
touch: "ontouchend" in document,
};
$.mobile.support = $.mobile.support || {};
$.extend($.support, support);
$.extend($.mobile.support, support);
})(jQuery);
(function ($, undefined) {
$.extend($.support, {
orientation: "orientation" in window && "onorientationchange" in window,
});
})(jQuery);
(function ($, undefined) {
// thx Modernizr
function propExists(prop) {
var uc_prop = prop.charAt(0).toUpperCase() + prop.substr(1),
props = (prop + " " + vendors.join(uc_prop + " ") + uc_prop).split(" "),
v;
for (v in props) {
if (fbCSS[props[v]] !== undefined) {
return true;
}
}
}
var fakeBody = $("<body>").prependTo("html"),
fbCSS = fakeBody[0].style,
vendors = ["Webkit", "Moz", "O"],
webos = "palmGetResource" in window, //only used to rule out scrollTop
operamini =
window.operamini &&
{}.toString.call(window.operamini) === "[object OperaMini]",
bb = window.blackberry && !propExists("-webkit-transform"), //only used to rule out box shadow, as it's filled opaque on BB 5 and lower
nokiaLTE7_3;
// inline SVG support test
function inlineSVG() {
// Thanks Modernizr & Erik Dahlstrom
var w = window,
svg =
!!w.document.createElementNS &&
!!w.document.createElementNS("http://www.w3.org/2000/svg", "svg")
.createSVGRect &&
!(w.opera && navigator.userAgent.indexOf("Chrome") === -1),
support = function (data) {
if (!(data && svg)) {
$("html").addClass("ui-nosvg");
}
},
img = new w.Image();
img.onerror = function () {
support(false);
};
img.onload = function () {
support(img.width === 1 && img.height === 1);
};
img.src =
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
}
function transform3dTest() {
var mqProp = "transform-3d",
// Because the `translate3d` test below throws false positives in Android:
ret = $.mobile.media(
"(-" +
vendors.join("-" + mqProp + "),(-") +
"-" +
mqProp +
"),(" +
mqProp +
")",
),
el,
transforms,
t;
if (ret) {
return !!ret;
}
el = document.createElement("div");
transforms = {
// Were omitting Opera for the time being; MS uses unprefixed.
MozTransform: "-moz-transform",
transform: "transform",
};
fakeBody.append(el);
for (t in transforms) {
if (el.style[t] !== undefined) {
el.style[t] = "translate3d( 100px, 1px, 1px )";
ret = window.getComputedStyle(el).getPropertyValue(transforms[t]);
}
}
return !!ret && ret !== "none";
}
// Test for dynamic-updating base tag support ( allows us to avoid href,src attr rewriting )
function baseTagTest() {
var fauxBase =
location.protocol +
"//" +
location.host +
location.pathname +
"ui-dir/",
base = $("head base"),
fauxEle = null,
href = "",
link,
rebase;
if (!base.length) {
base = fauxEle = $("<base>", { href: fauxBase }).appendTo("head");
} else {
href = base.attr("href");
}
link = $("<a href='testurl' />").prependTo(fakeBody);
rebase = link[0].href;
base[0].href = href || location.pathname;
if (fauxEle) {
fauxEle.remove();
}
return rebase.indexOf(fauxBase) === 0;
}
// Thanks Modernizr
function cssPointerEventsTest() {
var element = document.createElement("x"),
documentElement = document.documentElement,
getComputedStyle = window.getComputedStyle,
supports;
if (!("pointerEvents" in element.style)) {
return false;
}
element.style.pointerEvents = "auto";
element.style.pointerEvents = "x";
documentElement.appendChild(element);
supports =
getComputedStyle &&
getComputedStyle(element, "").pointerEvents === "auto";
documentElement.removeChild(element);
return !!supports;
}
function boundingRect() {
var div = document.createElement("div");
return typeof div.getBoundingClientRect !== "undefined";
}
// 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;
})();
function fixedPosition() {
var w = window,
ua = navigator.userAgent,
platform = navigator.platform,
// Rendering engine is Webkit, and capture major version
wkmatch = ua.match(/AppleWebKit\/([0-9]+)/),
wkversion = !!wkmatch && wkmatch[1],
ffmatch = ua.match(/Fennec\/([0-9]+)/),
ffversion = !!ffmatch && ffmatch[1],
operammobilematch = ua.match(/Opera Mobi\/([0-9]+)/),
omversion = !!operammobilematch && operammobilematch[1];
if (
// iOS 4.3 and older : Platform is iPhone/Pad/Touch and Webkit version is less than 534 (ios5)
((platform.indexOf("iPhone") > -1 ||
platform.indexOf("iPad") > -1 ||
platform.indexOf("iPod") > -1) &&
wkversion &&
wkversion < 534) ||
// Opera Mini
(w.operamini &&
{}.toString.call(w.operamini) === "[object OperaMini]") ||
(operammobilematch && omversion < 7458) ||
//Android lte 2.1: Platform is Android and Webkit version is less than 533 (Android 2.2)
(ua.indexOf("Android") > -1 && wkversion && wkversion < 533) ||
// Firefox Mobile before 6.0 -
(ffversion && ffversion < 6) ||
// WebOS less than 3
("palmGetResource" in window && wkversion && wkversion < 534) ||
// MeeGo
(ua.indexOf("MeeGo") > -1 && ua.indexOf("NokiaBrowser/8.5.0") > -1)
) {
return false;
}
return true;
}
$.extend($.support, {
// Note, Chrome for iOS has an extremely quirky implementation of popstate.
// We've chosen to take the shortest path to a bug fix here for issue #5426
// See the following link for information about the regex chosen
// https://developers.google.com/chrome/mobile/docs/user-agent#chrome_for_ios_user-agent
pushState:
"pushState" in history &&
"replaceState" in history &&
// When running inside a FF iframe, calling replaceState causes an error
!(
window.navigator.userAgent.indexOf("Firefox") >= 0 &&
window.top !== window
) &&
window.navigator.userAgent.search(/CriOS/) === -1,
mediaquery: $.mobile.media("only all"),
cssPseudoElement: !!propExists("content"),
touchOverflow: !!propExists("overflowScrolling"),
cssTransform3d: transform3dTest(),
boxShadow: !!propExists("boxShadow") && !bb,
fixedPosition: fixedPosition(),
scrollTop:
("pageXOffset" in window ||
"scrollTop" in document.documentElement ||
"scrollTop" in fakeBody[0]) &&
!webos &&
!operamini,
dynamicBaseTag: baseTagTest(),
cssPointerEvents: cssPointerEventsTest(),
boundingRect: boundingRect(),
inlineSVG: inlineSVG,
});
fakeBody.remove();
// $.mobile.ajaxBlacklist is used to override ajaxEnabled on platforms that have known conflicts with hash history updates (BB5, Symbian)
// or that generally work better browsing in regular http for full page refreshes (Opera Mini)
// Note: This detection below is used as a last resort.
// We recommend only using these detection methods when all other more reliable/forward-looking approaches are not possible
nokiaLTE7_3 = (function () {
var ua = window.navigator.userAgent;
//The following is an attempt to match Nokia browsers that are running Symbian/s60, with webkit, version 7.3 or older
return (
ua.indexOf("Nokia") > -1 &&
(ua.indexOf("Symbian/3") > -1 || ua.indexOf("Series60/5") > -1) &&
ua.indexOf("AppleWebKit") > -1 &&
ua.match(/(BrowserNG|NokiaBrowser)\/7\.[0-3]/)
);
})();
// Support conditions that must be met in order to proceed
// default enhanced qualifications are media query support OR IE 7+
$.mobile.gradeA = function () {
return (
(($.support.mediaquery && $.support.cssPseudoElement) ||
($.mobile.browser.oldIE && $.mobile.browser.oldIE >= 8)) &&
($.support.boundingRect ||
$.fn.jquery.match(/1\.[0-7+]\.[0-9+]?/) !== null)
);
};
$.mobile.ajaxBlacklist =
// BlackBerry browsers, pre-webkit
(window.blackberry && !window.WebKitPoint) ||
// Opera Mini
operamini ||
// Symbian webkits pre 7.3
nokiaLTE7_3;
// Lastly, this workaround is the only way we've found so far to get pre 7.3 Symbian webkit devices
// to render the stylesheets when they're referenced before this script, as we'd recommend doing.
// This simply reappends the CSS in place, which for some reason makes it apply
if (nokiaLTE7_3) {
$(function () {
$("head link[rel='stylesheet']")
.attr("rel", "alternate stylesheet")
.attr("rel", "stylesheet");
});
}
// For ruling out shadows via css
if (!$.support.boxShadow) {
$("html").addClass("ui-noboxshadow");
}
})(jQuery);
(function ($, undefined) {
var $win = $.mobile.window,
self,
dummyFnToInitNavigate = function () {};
$.event.special.beforenavigate = {
setup: function () {
$win.on("navigate", dummyFnToInitNavigate);
},
teardown: function () {
$win.off("navigate", dummyFnToInitNavigate);
},
};
$.event.special.navigate = self = {
bound: false,
pushStateEnabled: true,
originalEventName: undefined,
// If pushstate support is present and push state support is defined to
// be true on the mobile namespace.
isPushStateEnabled: function () {
return (
$.support.pushState &&
$.mobile.pushStateEnabled === true &&
this.isHashChangeEnabled()
);
},
// !! assumes mobile namespace is present
isHashChangeEnabled: function () {
return $.mobile.hashListeningEnabled === true;
},
// TODO a lot of duplication between popstate and hashchange
popstate: function (event) {
var newEvent = new $.Event("navigate"),
beforeNavigate = new $.Event("beforenavigate"),
state = event.originalEvent.state || {};
beforeNavigate.originalEvent = event;
$win.trigger(beforeNavigate);
if (beforeNavigate.isDefaultPrevented()) {
return;
}
if (event.historyState) {
$.extend(state, event.historyState);
}
// Make sure the original event is tracked for the end
// user to inspect incase they want to do something special
newEvent.originalEvent = event;
// NOTE we let the current stack unwind because any assignment to
// location.hash will stop the world and run this event handler. By
// doing this we create a similar behavior to hashchange on hash
// assignment
setTimeout(function () {
$win.trigger(newEvent, {
state: state,
});
}, 0);
},
hashchange: function (event /*, data */) {
var newEvent = new $.Event("navigate"),
beforeNavigate = new $.Event("beforenavigate");
beforeNavigate.originalEvent = event;
$win.trigger(beforeNavigate);
if (beforeNavigate.isDefaultPrevented()) {
return;
}
// Make sure the original event is tracked for the end
// user to inspect incase they want to do something special
newEvent.originalEvent = event;
// Trigger the hashchange with state provided by the user
// that altered the hash
$win.trigger(newEvent, {
// Users that want to fully normalize the two events
// will need to do history management down the stack and
// add the state to the event before this binding is fired
// TODO consider allowing for the explicit addition of callbacks
// to be fired before this value is set to avoid event timing issues
state: event.hashchangeState || {},
});
},
// TODO We really only want to set this up once
// but I'm not clear if there's a beter way to achieve
// this with the jQuery special event structure
setup: function (/* data, namespaces */) {
if (self.bound) {
return;
}
self.bound = true;
if (self.isPushStateEnabled()) {
self.originalEventName = "popstate";
$win.bind("popstate.navigate", self.popstate);
} else if (self.isHashChangeEnabled()) {
self.originalEventName = "hashchange";
$win.bind("hashchange.navigate", self.hashchange);
}
},
};
})(jQuery);
(function ($, undefined) {
var path,
$base,
dialogHashKey = "&ui-state=dialog";
$.mobile.path = path = {
uiStateKey: "&ui-state",
// This scary looking regular expression parses an absolute URL or its relative
// variants (protocol, site, document, query, and hash), into the various
// components (protocol, host, path, query, fragment, etc that make up the
// URL as well as some other commonly used sub-parts. When used with RegExp.exec()
// or String.match, it parses the URL into a results array that looks like this:
//
// [0]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread#msg-content
// [1]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread
// [2]: http://jblas:password@mycompany.com:8080/mail/inbox
// [3]: http://jblas:password@mycompany.com:8080
// [4]: http:
// [5]: //
// [6]: jblas:password@mycompany.com:8080
// [7]: jblas:password
// [8]: jblas
// [9]: password
// [10]: mycompany.com:8080
// [11]: mycompany.com
// [12]: 8080
// [13]: /mail/inbox
// [14]: /mail/
// [15]: inbox
// [16]: ?msg=1234&type=unread
// [17]: #msg-content
//
urlParseRE:
/^\s*(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/,
// Abstraction to address xss (Issue #4787) by removing the authority in
// browsers that auto decode it. All references to location.href should be
// replaced with a call to this method so that it can be dealt with properly here
getLocation: function (url) {
var uri = url ? this.parseUrl(url) : location,
hash = this.parseUrl(url || location.href).hash;
// mimic the browser with an empty string when the hash is empty
hash = hash === "#" ? "" : hash;
// Make sure to parse the url or the location object for the hash because using location.hash
// is autodecoded in firefox, the rest of the url should be from the object (location unless
// we're testing) to avoid the inclusion of the authority
return (
uri.protocol + "//" + uri.host + uri.pathname + uri.search + hash
);
},
//return the original document url
getDocumentUrl: function (asParsedObject) {
return asParsedObject
? $.extend({}, path.documentUrl)
: path.documentUrl.href;
},
parseLocation: function () {
return this.parseUrl(this.getLocation());
},
//Parse a URL into a structure that allows easy access to
//all of the URL components by name.
parseUrl: function (url) {
// If we're passed an object, we'll assume that it is
// a parsed url object and just return it back to the caller.
if ($.type(url) === "object") {
return url;
}
var matches = path.urlParseRE.exec(url || "") || [];
// Create an object that allows the caller to access the sub-matches
// by name. Note that IE returns an empty string instead of undefined,
// like all other browsers do, so we normalize everything so its consistent
// no matter what browser we're running on.
return {
href: matches[0] || "",
hrefNoHash: matches[1] || "",
hrefNoSearch: matches[2] || "",
domain: matches[3] || "",
protocol: matches[4] || "",
doubleSlash: matches[5] || "",
authority: matches[6] || "",
username: matches[8] || "",
password: matches[9] || "",
host: matches[10] || "",
hostname: matches[11] || "",
port: matches[12] || "",
pathname: matches[13] || "",
directory: matches[14] || "",
filename: matches[15] || "",
search: matches[16] || "",
hash: matches[17] || "",
};
},
//Turn relPath into an asbolute path. absPath is
//an optional absolute path which describes what
//relPath is relative to.
makePathAbsolute: function (relPath, absPath) {
var absStack, relStack, i, d;
if (relPath && relPath.charAt(0) === "/") {
return relPath;
}
relPath = relPath || "";
absPath = absPath ? absPath.replace(/^\/|(\/[^\/]*|[^\/]+)$/g, "") : "";
absStack = absPath ? absPath.split("/") : [];
relStack = relPath.split("/");
for (i = 0; i < relStack.length; i++) {
d = relStack[i];
switch (d) {
case ".":
break;
case "..":
if (absStack.length) {
absStack.pop();
}
break;
default:
absStack.push(d);
break;
}
}
return "/" + absStack.join("/");
},
//Returns true if both urls have the same domain.
isSameDomain: function (absUrl1, absUrl2) {
return path.parseUrl(absUrl1).domain === path.parseUrl(absUrl2).domain;
},
//Returns true for any relative variant.
isRelativeUrl: function (url) {
// All relative Url variants have one thing in common, no protocol.
return path.parseUrl(url).protocol === "";
},
//Returns true for an absolute url.
isAbsoluteUrl: function (url) {
return path.parseUrl(url).protocol !== "";
},
//Turn the specified realtive URL into an absolute one. This function
//can handle all relative variants (protocol, site, document, query, fragment).
makeUrlAbsolute: function (relUrl, absUrl) {
if (!path.isRelativeUrl(relUrl)) {
return relUrl;
}
if (absUrl === undefined) {
absUrl = this.documentBase;
}
var relObj = path.parseUrl(relUrl),
absObj = path.parseUrl(absUrl),
protocol = relObj.protocol || absObj.protocol,
doubleSlash = relObj.protocol
? relObj.doubleSlash
: relObj.doubleSlash || absObj.doubleSlash,
authority = relObj.authority || absObj.authority,
hasPath = relObj.pathname !== "",
pathname = path.makePathAbsolute(
relObj.pathname || absObj.filename,
absObj.pathname,
),
search = relObj.search || (!hasPath && absObj.search) || "",
hash = relObj.hash;
return protocol + doubleSlash + authority + pathname + search + hash;
},
//Add search (aka query) params to the specified url.
addSearchParams: function (url, params) {
var u = path.parseUrl(url),
p = typeof params === "object" ? $.param(params) : params,
s = u.search || "?";
return (
u.hrefNoSearch +
s +
(s.charAt(s.length - 1) !== "?" ? "&" : "") +
p +
(u.hash || "")
);
},
convertUrlToDataUrl: function (absUrl) {
var u = path.parseUrl(absUrl);
if (path.isEmbeddedPage(u)) {
// For embedded pages, remove the dialog hash key as in getFilePath(),
// and remove otherwise the Data Url won't match the id of the embedded Page.
return u.hash
.split(dialogHashKey)[0]
.replace(/^#/, "")
.replace(/\?.*$/, "");
} else if (path.isSameDomain(u, this.documentBase)) {
return u.hrefNoHash
.replace(this.documentBase.domain, "")
.split(dialogHashKey)[0];
}
return window.decodeURIComponent(absUrl);
},
//get path from current hash, or from a file path
get: function (newPath) {
if (newPath === undefined) {
newPath = path.parseLocation().hash;
}
return path.stripHash(newPath).replace(/[^\/]*\.[^\/*]+$/, "");
},
//set location hash to path
set: function (path) {
location.hash = path;
},
//test if a given url (string) is a path
//NOTE might be exceptionally naive
isPath: function (url) {
return /\//.test(url);
},
//return a url path with the window's location protocol/hostname/pathname removed
clean: function (url) {
return url.replace(this.documentBase.domain, "");
},
//just return the url without an initial #
stripHash: function (url) {
return url.replace(/^#/, "");
},
stripQueryParams: function (url) {
return url.replace(/\?.*$/, "");
},
//remove the preceding hash, any query params, and dialog notations
cleanHash: function (hash) {
return path.stripHash(
hash.replace(/\?.*$/, "").replace(dialogHashKey, ""),
);
},
isHashValid: function (hash) {
return /^#[^#]+$/.test(hash);
},
//check whether a url is referencing the same domain, or an external domain or different protocol
//could be mailto, etc
isExternal: function (url) {
var u = path.parseUrl(url);
return u.protocol && u.domain !== this.documentUrl.domain
? true
: false;
},
hasProtocol: function (url) {
return /^(:?\w+:)/.test(url);
},
isEmbeddedPage: function (url) {
var u = path.parseUrl(url);
//if the path is absolute, then we need to compare the url against
//both the this.documentUrl and the documentBase. The main reason for this
//is that links embedded within external documents will refer to the
//application document, whereas links embedded within the application
//document will be resolved against the document base.
if (u.protocol !== "") {
return (
!this.isPath(u.hash) &&
u.hash &&
(u.hrefNoHash === this.documentUrl.hrefNoHash ||
(this.documentBaseDiffers &&
u.hrefNoHash === this.documentBase.hrefNoHash))
);
}
return /^#/.test(u.href);
},
squash: function (url, resolutionUrl) {
var href,
cleanedUrl,
search,
stateIndex,
isPath = this.isPath(url),
uri = this.parseUrl(url),
preservedHash = uri.hash,
uiState = "";
// produce a url against which we can resole the provided path
resolutionUrl =
resolutionUrl ||
(path.isPath(url) ? path.getLocation() : path.getDocumentUrl());
// If the url is anything but a simple string, remove any preceding hash
// eg #foo/bar -> foo/bar
// #foo -> #foo
cleanedUrl = isPath ? path.stripHash(url) : url;
// If the url is a full url with a hash check if the parsed hash is a path
// if it is, strip the #, and use it otherwise continue without change
cleanedUrl = path.isPath(uri.hash)
? path.stripHash(uri.hash)
: cleanedUrl;
// Split the UI State keys off the href
stateIndex = cleanedUrl.indexOf(this.uiStateKey);
// store the ui state keys for use
if (stateIndex > -1) {
uiState = cleanedUrl.slice(stateIndex);
cleanedUrl = cleanedUrl.slice(0, stateIndex);
}
// make the cleanedUrl absolute relative to the resolution url
href = path.makeUrlAbsolute(cleanedUrl, resolutionUrl);
// grab the search from the resolved url since parsing from
// the passed url may not yield the correct result
search = this.parseUrl(href).search;
// TODO all this crap is terrible, clean it up
if (isPath) {
// reject the hash if it's a path or it's just a dialog key
if (
path.isPath(preservedHash) ||
preservedHash.replace("#", "").indexOf(this.uiStateKey) === 0
) {
preservedHash = "";
}
// Append the UI State keys where it exists and it's been removed
// from the url
if (uiState && preservedHash.indexOf(this.uiStateKey) === -1) {
preservedHash += uiState;
}
// make sure that pound is on the front of the hash
if (preservedHash.indexOf("#") === -1 && preservedHash !== "") {
preservedHash = "#" + preservedHash;
}
// reconstruct each of the pieces with the new search string and hash
href = path.parseUrl(href);
href =
href.protocol +
"//" +
href.host +
href.pathname +
search +
preservedHash;
} else {
href += href.indexOf("#") > -1 ? uiState : "#" + uiState;
}
return href;
},
isPreservableHash: function (hash) {
return hash.replace("#", "").indexOf(this.uiStateKey) === 0;
},
// Escape weird characters in the hash if it is to be used as a selector
hashToSelector: function (hash) {
var hasHash = hash.substring(0, 1) === "#";
if (hasHash) {
hash = hash.substring(1);
}
return (
(hasHash ? "#" : "") +
hash.replace(/([!"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g, "\\$1")
);
},
// return the substring of a filepath before the sub-page key, for making
// a server request
getFilePath: function (path) {
var splitkey = "&" + $.mobile.subPageUrlKey;
return path && path.split(splitkey)[0].split(dialogHashKey)[0];
},
// check if the specified url refers to the first page in the main
// application document.
isFirstPageUrl: function (url) {
// We only deal with absolute paths.
var u = path.parseUrl(path.makeUrlAbsolute(url, this.documentBase)),
// Does the url have the same path as the document?
samePath =
u.hrefNoHash === this.documentUrl.hrefNoHash ||
(this.documentBaseDiffers &&
u.hrefNoHash === this.documentBase.hrefNoHash),
// Get the first page element.
fp = $.mobile.firstPage,
// Get the id of the first page element if it has one.
fpId = fp && fp[0] ? fp[0].id : undefined;
// The url refers to the first page if the path matches the document and
// it either has no hash value, or the hash is exactly equal to the id
// of the first page element.
return (
samePath &&
(!u.hash ||
u.hash === "#" ||
(fpId && u.hash.replace(/^#/, "") === fpId))
);
},
// Some embedded browsers, like the web view in Phone Gap, allow
// cross-domain XHR requests if the document doing the request was loaded
// via the file:// protocol. This is usually to allow the application to
// "phone home" and fetch app specific data. We normally let the browser
// handle external/cross-domain urls, but if the allowCrossDomainPages
// option is true, we will allow cross-domain http/https requests to go
// through our page loading logic.
isPermittedCrossDomainRequest: function (docUrl, reqUrl) {
return (
$.mobile.allowCrossDomainPages &&
(docUrl.protocol === "file:" || docUrl.protocol === "content:") &&
reqUrl.search(/^https?:/) !== -1
);
},
};
path.documentUrl = path.parseLocation();
$base = $("head").find("base");
path.documentBase = $base.length
? path.parseUrl(
path.makeUrlAbsolute($base.attr("href"), path.documentUrl.href),
)
: path.documentUrl;
path.documentBaseDiffers =
path.documentUrl.hrefNoHash !== path.documentBase.hrefNoHash;
//return the original document base url
path.getDocumentBase = function (asParsedObject) {
return asParsedObject
? $.extend({}, path.documentBase)
: path.documentBase.href;
};
// DEPRECATED as of 1.4.0 - remove in 1.5.0
$.extend($.mobile, {
//return the original document url
getDocumentUrl: path.getDocumentUrl,
//return the original document base url
getDocumentBase: path.getDocumentBase,
});
})(jQuery);
(function ($, undefined) {
$.mobile.History = function (stack, index) {
this.stack = stack || [];
this.activeIndex = index || 0;
};
$.extend($.mobile.History.prototype, {
getActive: function () {
return this.stack[this.activeIndex];
},
getLast: function () {
return this.stack[this.previousIndex];
},
getNext: function () {
return this.stack[this.activeIndex + 1];
},
getPrev: function () {
return this.stack[this.activeIndex - 1];
},
// addNew is used whenever a new page is added
add: function (url, data) {
data = data || {};
//if there's forward history, wipe it
if (this.getNext()) {
this.clearForward();
}
// if the hash is included in the data make sure the shape
// is consistent for comparison
if (data.hash && data.hash.indexOf("#") === -1) {
data.hash = "#" + data.hash;
}
data.url = url;
this.stack.push(data);
this.activeIndex = this.stack.length - 1;
},
//wipe urls ahead of active index
clearForward: function () {
this.stack = this.stack.slice(0, this.activeIndex + 1);
},
find: function (url, stack, earlyReturn) {
stack = stack || this.stack;
var entry,
i,
length = stack.length,
index;
for (i = 0; i < length; i++) {
entry = stack[i];
if (
decodeURIComponent(url) === decodeURIComponent(entry.url) ||
decodeURIComponent(url) === decodeURIComponent(entry.hash)
) {
index = i;
if (earlyReturn) {
return index;
}
}
}
return index;
},
closest: function (url) {
var closest,
a = this.activeIndex;
// First, take the slice of the history stack before the current index and search
// for a url match. If one is found, we'll avoid avoid looking through forward history
// NOTE the preference for backward history movement is driven by the fact that
// most mobile browsers only have a dedicated back button, and users rarely use
// the forward button in desktop browser anyhow
closest = this.find(url, this.stack.slice(0, a));
// If nothing was found in backward history check forward. The `true`
// value passed as the third parameter causes the find method to break
// on the first match in the forward history slice. The starting index
// of the slice must then be added to the result to get the element index
// in the original history stack :( :(
//
// TODO this is hyper confusing and should be cleaned up (ugh so bad)
if (closest === undefined) {
closest = this.find(url, this.stack.slice(a), true);
closest = closest === undefined ? closest : closest + a;
}
return closest;
},
direct: function (opts) {
var newActiveIndex = this.closest(opts.url),
a = this.activeIndex;
// save new page index, null check to prevent falsey 0 result
// record the previous index for reference
if (newActiveIndex !== undefined) {
this.activeIndex = newActiveIndex;
this.previousIndex = a;
}
// invoke callbacks where appropriate
//
// TODO this is also convoluted and confusing
if (newActiveIndex < a) {
(opts.present || opts.back || $.noop)(this.getActive(), "back");
} else if (newActiveIndex > a) {
(opts.present || opts.forward || $.noop)(this.getActive(), "forward");
} else if (newActiveIndex === undefined && opts.missing) {
opts.missing(this.getActive());
}
},
});
})(jQuery);
(function ($, undefined) {
var path = $.mobile.path,
initialHref = location.href;
$.mobile.Navigator = function (history) {
this.history = history;
this.ignoreInitialHashChange = true;
$.mobile.window.bind({
"popstate.history": $.proxy(this.popstate, this),
"hashchange.history": $.proxy(this.hashchange, this),
});
};
$.extend($.mobile.Navigator.prototype, {
squash: function (url, data) {
var state,
href,
hash = path.isPath(url) ? path.stripHash(url) : url;
href = path.squash(url);
// make sure to provide this information when it isn't explicitly set in the
// data object that was passed to the squash method
state = $.extend(
{
hash: hash,
url: href,
},
data,
);
// replace the current url with the new href and store the state
// Note that in some cases we might be replacing an url with the
// same url. We do this anyways because we need to make sure that
// all of our history entries have a state object associated with
// them. This allows us to work around the case where $.mobile.back()
// is called to transition from an external page to an embedded page.
// In that particular case, a hashchange event is *NOT* generated by the browser.
// Ensuring each history entry has a state object means that onPopState()
// will always trigger our hashchange callback even when a hashchange event
// is not fired.
window.history.replaceState(state, state.title || document.title, href);
return state;
},
hash: function (url, href) {
var parsed, loc, hash, resolved;
// Grab the hash for recording. If the passed url is a path
// we used the parsed version of the squashed url to reconstruct,
// otherwise we assume it's a hash and store it directly
parsed = path.parseUrl(url);
loc = path.parseLocation();
if (loc.pathname + loc.search === parsed.pathname + parsed.search) {
// If the pathname and search of the passed url is identical to the current loc
// then we must use the hash. Otherwise there will be no event
// eg, url = "/foo/bar?baz#bang", location.href = "http://example.com/foo/bar?baz"
hash = parsed.hash ? parsed.hash : parsed.pathname + parsed.search;
} else if (path.isPath(url)) {
resolved = path.parseUrl(href);
// If the passed url is a path, make it domain relative and remove any trailing hash
hash =
resolved.pathname +
resolved.search +
(path.isPreservableHash(resolved.hash)
? resolved.hash.replace("#", "")
: "");
} else {
hash = url;
}
return hash;
},
// TODO reconsider name
go: function (url, data, noEvents) {
var state,
href,
hash,
popstateEvent,
isPopStateEvent = $.event.special.navigate.isPushStateEnabled();
// Get the url as it would look squashed on to the current resolution url
href = path.squash(url);
// sort out what the hash sould be from the url
hash = this.hash(url, href);
// Here we prevent the next hash change or popstate event from doing any
// history management. In the case of hashchange we don't swallow it
// if there will be no hashchange fired (since that won't reset the value)
// and will swallow the following hashchange
if (noEvents && hash !== path.stripHash(path.parseLocation().hash)) {
this.preventNextHashChange = noEvents;
}
// IMPORTANT in the case where popstate is supported the event will be triggered
// directly, stopping further execution - ie, interupting the flow of this
// method call to fire bindings at this expression. Below the navigate method
// there is a binding to catch this event and stop its propagation.
//
// We then trigger a new popstate event on the window with a null state
// so that the navigate events can conclude their work properly
//
// if the url is a path we want to preserve the query params that are available on
// the current url.
this.preventHashAssignPopState = true;
window.location.hash = hash;
// If popstate is enabled and the browser triggers `popstate` events when the hash
// is set (this often happens immediately in browsers like Chrome), then the
// this flag will be set to false already. If it's a browser that does not trigger
// a `popstate` on hash assignement or `replaceState` then we need avoid the branch
// that swallows the event created by the popstate generated by the hash assignment
// At the time of this writing this happens with Opera 12 and some version of IE
this.preventHashAssignPopState = false;
state = $.extend(
{
url: href,
hash: hash,
title: document.title,
},
data,
);
if (isPopStateEvent) {
popstateEvent = new $.Event("popstate");
popstateEvent.originalEvent = {
type: "popstate",
state: null,
};
this.squash(url, state);
// Trigger a new faux popstate event to replace the one that we
// caught that was triggered by the hash setting above.
if (!noEvents) {
this.ignorePopState = true;
$.mobile.window.trigger(popstateEvent);
}
}
// record the history entry so that the information can be included
// in hashchange event driven navigate events in a similar fashion to
// the state that's provided by popstate
this.history.add(state.url, state);
},
// This binding is intended to catch the popstate events that are fired
// when execution of the `$.navigate` method stops at window.location.hash = url;
// and completely prevent them from propagating. The popstate event will then be
// retriggered after execution resumes
//
// TODO grab the original event here and use it for the synthetic event in the
// second half of the navigate execution that will follow this binding
popstate: function (event) {
var hash, state;
// Partly to support our test suite which manually alters the support
// value to test hashchange. Partly to prevent all around weirdness
if (!$.event.special.navigate.isPushStateEnabled()) {
return;
}
// If this is the popstate triggered by the actual alteration of the hash
// prevent it completely. History is tracked manually
if (this.preventHashAssignPopState) {
this.preventHashAssignPopState = false;
event.stopImmediatePropagation();
return;
}
// if this is the popstate triggered after the `replaceState` call in the go
// method, then simply ignore it. The history entry has already been captured
if (this.ignorePopState) {
this.ignorePopState = false;
return;
}
// If there is no state, and the history stack length is one were
// probably getting the page load popstate fired by browsers like chrome
// avoid it and set the one time flag to false.
// TODO: Do we really need all these conditions? Comparing location hrefs
// should be sufficient.
if (
!event.originalEvent.state &&
this.history.stack.length === 1 &&
this.ignoreInitialHashChange
) {
this.ignoreInitialHashChange = false;
if (location.href === initialHref) {
event.preventDefault();
return;
}
}
// account for direct manipulation of the hash. That is, we will receive a popstate
// when the hash is changed by assignment, and it won't have a state associated. We
// then need to squash the hash. See below for handling of hash assignment that
// matches an existing history entry
// TODO it might be better to only add to the history stack
// when the hash is adjacent to the active history entry
hash = path.parseLocation().hash;
if (!event.originalEvent.state && hash) {
// squash the hash that's been assigned on the URL with replaceState
// also grab the resulting state object for storage
state = this.squash(hash);
// record the new hash as an additional history entry
// to match the browser's treatment of hash assignment
this.history.add(state.url, state);
// pass the newly created state information
// along with the event
event.historyState = state;
// do not alter history, we've added a new history entry
// so we know where we are
return;
}
// If all else fails this is a popstate that comes from the back or forward buttons
// make sure to set the state of our history stack properly, and record the directionality
this.history.direct({
url: (event.originalEvent.state || {}).url || hash,
// When the url is either forward or backward in history include the entry
// as data on the event object for merging as data in the navigate event
present: function (historyEntry, direction) {
// make sure to create a new object to pass down as the navigate event data
event.historyState = $.extend({}, historyEntry);
event.historyState.direction = direction;
},
});
},
// NOTE must bind before `navigate` special event hashchange binding otherwise the
// navigation data won't be attached to the hashchange event in time for those
// bindings to attach it to the `navigate` special event
// TODO add a check here that `hashchange.navigate` is bound already otherwise it's
// broken (exception?)
hashchange: function (event) {
var history, hash;
// If hashchange listening is explicitly disabled or pushstate is supported
// avoid making use of the hashchange handler.
if (
!$.event.special.navigate.isHashChangeEnabled() ||
$.event.special.navigate.isPushStateEnabled()
) {
return;
}
// On occasion explicitly want to prevent the next hash from propogating because we only
// with to alter the url to represent the new state do so here
if (this.preventNextHashChange) {
this.preventNextHashChange = false;
event.stopImmediatePropagation();
return;
}
history = this.history;
hash = path.parseLocation().hash;
// If this is a hashchange caused by the back or forward button
// make sure to set the state of our history stack properly
this.history.direct({
url: hash,
// When the url is either forward or backward in history include the entry
// as data on the event object for merging as data in the navigate event
present: function (historyEntry, direction) {
// make sure to create a new object to pass down as the navigate event data
event.hashchangeState = $.extend({}, historyEntry);
event.hashchangeState.direction = direction;
},
// When we don't find a hash in our history clearly we're aiming to go there
// record the entry as new for future traversal
//
// NOTE it's not entirely clear that this is the right thing to do given that we
// can't know the users intention. It might be better to explicitly _not_
// support location.hash assignment in preference to $.navigate calls
// TODO first arg to add should be the href, but it causes issues in identifying
// embeded pages
missing: function () {
history.add(hash, {
hash: hash,
title: document.title,
});
},
});
},
});
})(jQuery);
(function ($, undefined) {
// TODO consider queueing navigation activity until previous activities have completed
// so that end users don't have to think about it. Punting for now
// TODO !! move the event bindings into callbacks on the navigate event
$.mobile.navigate = function (url, data, noEvents) {
$.mobile.navigate.navigator.go(url, data, noEvents);
};
// expose the history on the navigate method in anticipation of full integration with
// existing navigation functionalty that is tightly coupled to the history information
$.mobile.navigate.history = new $.mobile.History();
// instantiate an instance of the navigator for use within the $.navigate method
$.mobile.navigate.navigator = new $.mobile.Navigator(
$.mobile.navigate.history,
);
var loc = $.mobile.path.parseLocation();
$.mobile.navigate.history.add(loc.href, { hash: loc.hash });
})(jQuery);
(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();
}
});
// If a valid prefix was found then the it is supported by the browser
$.support.cssTransitions = props["transition"]["prefix"] !== undefined;
$.support.cssAnimations = props["animation"]["prefix"] !== undefined;
// Remove the testElement
$(testElement).remove();
// Animation complete callback
$.fn.animationComplete = function (callback, type, fallbackTime) {
var timer,
duration,
that = this,
animationType =
!type || type === "animation" ? "animation" : "transition";
// Make sure selected type is supported by browser
if (
($.support.cssTransitions && animationType === "transition") ||
($.support.cssAnimations && animationType === "animation")
) {
// 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);
callback.apply(that);
}, duration);
// Bind the event
return $(this).one(props[animationType].event, function () {
// Clear the timer so we dont call callback twice
clearTimeout(timer);
callback.call(this, arguments);
});
} else {
// CSS animation / transitions not supported
// Defer execution for consistency between webkit/non webkit
setTimeout($.proxy(callback, this), 0);
return $(this);
}
};
// Allow default callback to be configured on mobileInit
$.fn.animationComplete.defaultDuration = 1000;
})(jQuery);
// This plugin is an experiment for abstracting away the touch and mouse
// events so that developers don't have to worry about which method of input
// the device their document is loaded on supports.
//
// The idea here is to allow the developer to register listeners for the
// basic mouse events, such as mousedown, mousemove, mouseup, and click,
// and the plugin will take care of registering the correct listeners
// behind the scenes to invoke the listener at the fastest possible time
// for that device, while still retaining the order of event firing in
// the traditional mouse environment, should multiple handlers be registered
// on the same element for different events.
//
// The current version exposes the following virtual events to jQuery bind methods:
// "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel"
(function ($, window, document, undefined) {
var dataPropertyName = "virtualMouseBindings",
touchTargetPropertyName = "virtualTouchID",
virtualEventNames =
"vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel".split(
" ",
),
touchEventProps = "clientX clientY pageX pageY screenX screenY".split(
" ",
),
mouseHookProps = $.event.mouseHooks ? $.event.mouseHooks.props : [],
mouseEventProps = $.event.props.concat(mouseHookProps),
activeDocHandlers = {},
resetTimerID = 0,
startX = 0,
startY = 0,
didScroll = false,
clickBlockList = [],
blockMouseTriggers = false,
blockTouchTriggers = false,
eventCaptureSupported = "addEventListener" in document,
$document = $(document),
nextTouchID = 1,
lastTouchID = 0,
threshold,
i;
$.vmouse = {
moveDistanceThreshold: 10,
clickDistanceThreshold: 10,
resetTimerDuration: 1500,
};
function getNativeEvent(event) {
while (event && typeof event.originalEvent !== "undefined") {
event = event.originalEvent;
}
return event;
}
function createVirtualEvent(event, eventType) {
var t = event.type,
oe,
props,
ne,
prop,
ct,
touch,
i,
j,
len;
event = $.Event(event);
event.type = eventType;
oe = event.originalEvent;
props = $.event.props;
// addresses separation of $.event.props in to $.event.mouseHook.props and Issue 3280
// https://github.com/jquery/jquery-mobile/issues/3280
if (t.search(/^(mouse|click)/) > -1) {
props = mouseEventProps;
}
// copy original event properties over to the new event
// this would happen if we could call $.event.fix instead of $.Event
// but we don't have a way to force an event to be fixed multiple times
if (oe) {
for (i = props.length, prop; i; ) {
prop = props[--i];
event[prop] = oe[prop];
}
}
// make sure that if the mouse and click virtual events are generated
// without a .which one is defined
if (t.search(/mouse(down|up)|click/) > -1 && !event.which) {
event.which = 1;
}
if (t.search(/^touch/) !== -1) {
ne = getNativeEvent(oe);
t = ne.touches;
ct = ne.changedTouches;
touch = t && t.length ? t[0] : ct && ct.length ? ct[0] : undefined;
if (touch) {
for (j = 0, len = touchEventProps.length; j < len; j++) {
prop = touchEventProps[j];
event[prop] = touch[prop];
}
}
}
return event;
}
function getVirtualBindingFlags(element) {
var flags = {},
b,
k;
while (element) {
b = $.data(element, dataPropertyName);
for (k in b) {
if (b[k]) {
flags[k] = flags.hasVirtualBinding = true;
}
}
element = element.parentNode;
}
return flags;
}
function getClosestElementWithVirtualBinding(element, eventType) {
var b;
while (element) {
b = $.data(element, dataPropertyName);
if (b && (!eventType || b[eventType])) {
return element;
}
element = element.parentNode;
}
return null;
}
function enableTouchBindings() {
blockTouchTriggers = false;
}
function disableTouchBindings() {
blockTouchTriggers = true;
}
function enableMouseBindings() {
lastTouchID = 0;
clickBlockList.length = 0;
blockMouseTriggers = false;
// When mouse bindings are enabled, our
// touch bindings are disabled.
disableTouchBindings();
}
function disableMouseBindings() {
// When mouse bindings are disabled, our
// touch bindings are enabled.
enableTouchBindings();
}
function startResetTimer() {
clearResetTimer();
resetTimerID = setTimeout(function () {
resetTimerID = 0;
enableMouseBindings();
}, $.vmouse.resetTimerDuration);
}
function clearResetTimer() {
if (resetTimerID) {
clearTimeout(resetTimerID);
resetTimerID = 0;
}
}
function triggerVirtualEvent(eventType, event, flags) {
var ve;
if (
(flags && flags[eventType]) ||
(!flags && getClosestElementWithVirtualBinding(event.target, eventType))
) {
ve = createVirtualEvent(event, eventType);
$(event.target).trigger(ve);
}
return ve;
}
function mouseEventCallback(event) {
var touchID = $.data(event.target, touchTargetPropertyName),
ve;
if (!blockMouseTriggers && (!lastTouchID || lastTouchID !== touchID)) {
ve = triggerVirtualEvent("v" + event.type, event);
if (ve) {
if (ve.isDefaultPrevented()) {
event.preventDefault();
}
if (ve.isPropagationStopped()) {
event.stopPropagation();
}
if (ve.isImmediatePropagationStopped()) {
event.stopImmediatePropagation();
}
}
}
}
function handleTouchStart(event) {
var touches = getNativeEvent(event).touches,
target,
flags,
t;
if (touches && touches.length === 1) {
target = event.target;
flags = getVirtualBindingFlags(target);
if (flags.hasVirtualBinding) {
lastTouchID = nextTouchID++;
$.data(target, touchTargetPropertyName, lastTouchID);
clearResetTimer();
disableMouseBindings();
didScroll = false;
t = getNativeEvent(event).touches[0];
startX = t.pageX;
startY = t.pageY;
triggerVirtualEvent("vmouseover", event, flags);
triggerVirtualEvent("vmousedown", event, flags);
}
}
}
function handleScroll(event) {
if (blockTouchTriggers) {
return;
}
if (!didScroll) {
triggerVirtualEvent(
"vmousecancel",
event,
getVirtualBindingFlags(event.target),
);
}
didScroll = true;
startResetTimer();
}
function handleTouchMove(event) {
if (blockTouchTriggers) {
return;
}
var t = getNativeEvent(event).touches[0],
didCancel = didScroll,
moveThreshold = $.vmouse.moveDistanceThreshold,
flags = getVirtualBindingFlags(event.target);
didScroll =
didScroll ||
Math.abs(t.pageX - startX) > moveThreshold ||
Math.abs(t.pageY - startY) > moveThreshold;
if (didScroll && !didCancel) {
triggerVirtualEvent("vmousecancel", event, flags);
}
triggerVirtualEvent("vmousemove", event, flags);
startResetTimer();
}
function handleTouchEnd(event) {
if (blockTouchTriggers) {
return;
}
disableTouchBindings();
var flags = getVirtualBindingFlags(event.target),
ve,
t;
triggerVirtualEvent("vmouseup", event, flags);
if (!didScroll) {
ve = triggerVirtualEvent("vclick", event, flags);
if (ve && ve.isDefaultPrevented()) {
// The target of the mouse events that follow the touchend
// event don't necessarily match the target used during the
// touch. This means we need to rely on coordinates for blocking
// any click that is generated.
t = getNativeEvent(event).changedTouches[0];
clickBlockList.push({
touchID: lastTouchID,
x: t.clientX,
y: t.clientY,
});
// Prevent any mouse events that follow from triggering
// virtual event notifications.
blockMouseTriggers = true;
}
}
triggerVirtualEvent("vmouseout", event, flags);
didScroll = false;
startResetTimer();
}
function hasVirtualBindings(ele) {
var bindings = $.data(ele, dataPropertyName),
k;
if (bindings) {
for (k in bindings) {
if (bindings[k]) {
return true;
}
}
}
return false;
}
function dummyMouseHandler() {}
function getSpecialEventObject(eventType) {
var realType = eventType.substr(1);
return {
setup: function (/* data, namespace */) {
// If this is the first virtual mouse binding for this element,
// add a bindings object to its data.
if (!hasVirtualBindings(this)) {
$.data(this, dataPropertyName, {});
}
// If setup is called, we know it is the first binding for this
// eventType, so initialize the count for the eventType to zero.
var bindings = $.data(this, dataPropertyName);
bindings[eventType] = true;
// If this is the first virtual mouse event for this type,
// register a global handler on the document.
activeDocHandlers[eventType] =
(activeDocHandlers[eventType] || 0) + 1;
if (activeDocHandlers[eventType] === 1) {
$document.bind(realType, mouseEventCallback);
}
// Some browsers, like Opera Mini, won't dispatch mouse/click events
// for elements unless they actually have handlers registered on them.
// To get around this, we register dummy handlers on the elements.
$(this).bind(realType, dummyMouseHandler);
// For now, if event capture is not supported, we rely on mouse handlers.
if (eventCaptureSupported) {
// If this is the first virtual mouse binding for the document,
// register our touchstart handler on the document.
activeDocHandlers["touchstart"] =
(activeDocHandlers["touchstart"] || 0) + 1;
if (activeDocHandlers["touchstart"] === 1) {
$document
.bind("touchstart", handleTouchStart)
.bind("touchend", handleTouchEnd)
// On touch platforms, touching the screen and then dragging your finger
// causes the window content to scroll after some distance threshold is
// exceeded. On these platforms, a scroll prevents a click event from being
// dispatched, and on some platforms, even the touchend is suppressed. To
// mimic the suppression of the click event, we need to watch for a scroll
// event. Unfortunately, some platforms like iOS don't dispatch scroll
// events until *AFTER* the user lifts their finger (touchend). This means
// we need to watch both scroll and touchmove events to figure out whether
// or not a scroll happenens before the touchend event is fired.
.bind("touchmove", handleTouchMove)
.bind("scroll", handleScroll);
}
}
},
teardown: function (/* data, namespace */) {
// If this is the last virtual binding for this eventType,
// remove its global handler from the document.
--activeDocHandlers[eventType];
if (!activeDocHandlers[eventType]) {
$document.unbind(realType, mouseEventCallback);
}
if (eventCaptureSupported) {
// If this is the last virtual mouse binding in existence,
// remove our document touchstart listener.
--activeDocHandlers["touchstart"];
if (!activeDocHandlers["touchstart"]) {
$document
.unbind("touchstart", handleTouchStart)
.unbind("touchmove", handleTouchMove)
.unbind("touchend", handleTouchEnd)
.unbind("scroll", handleScroll);
}
}
var $this = $(this),
bindings = $.data(this, dataPropertyName);
// teardown may be called when an element was
// removed from the DOM. If this is the case,
// jQuery core may have already stripped the element
// of any data bindings so we need to check it before
// using it.
if (bindings) {
bindings[eventType] = false;
}
// Unregister the dummy event handler.
$this.unbind(realType, dummyMouseHandler);
// If this is the last virtual mouse binding on the
// element, remove the binding data from the element.
if (!hasVirtualBindings(this)) {
$this.removeData(dataPropertyName);
}
},
};
}
// Expose our custom events to the jQuery bind/unbind mechanism.
for (i = 0; i < virtualEventNames.length; i++) {
$.event.special[virtualEventNames[i]] = getSpecialEventObject(
virtualEventNames[i],
);
}
// Add a capture click handler to block clicks.
// Note that we require event capture support for this so if the device
// doesn't support it, we punt for now and rely solely on mouse events.
if (eventCaptureSupported) {
document.addEventListener(
"click",
function (e) {
var cnt = clickBlockList.length,
target = e.target,
x,
y,
ele,
i,
o,
touchID;
if (cnt) {
x = e.clientX;
y = e.clientY;
threshold = $.vmouse.clickDistanceThreshold;
// The idea here is to run through the clickBlockList to see if
// the current click event is in the proximity of one of our
// vclick events that had preventDefault() called on it. If we find
// one, then we block the click.
//
// Why do we have to rely on proximity?
//
// Because the target of the touch event that triggered the vclick
// can be different from the target of the click event synthesized
// by the browser. The target of a mouse/click event that is synthesized
// from a touch event seems to be implementation specific. For example,
// some browsers will fire mouse/click events for a link that is near
// a touch event, even though the target of the touchstart/touchend event
// says the user touched outside the link. Also, it seems that with most
// browsers, the target of the mouse/click event is not calculated until the
// time it is dispatched, so if you replace an element that you touched
// with another element, the target of the mouse/click will be the new
// element underneath that point.
//
// Aside from proximity, we also check to see if the target and any
// of its ancestors were the ones that blocked a click. This is necessary
// because of the strange mouse/click target calculation done in the
// Android 2.1 browser, where if you click on an element, and there is a
// mouse/click handler on one of its ancestors, the target will be the
// innermost child of the touched element, even if that child is no where
// near the point of touch.
ele = target;
while (ele) {
for (i = 0; i < cnt; i++) {
o = clickBlockList[i];
touchID = 0;
if (
(ele === target &&
Math.abs(o.x - x) < threshold &&
Math.abs(o.y - y) < threshold) ||
$.data(ele, touchTargetPropertyName) === o.touchID
) {
// XXX: We may want to consider removing matches from the block list
// instead of waiting for the reset timer to fire.
e.preventDefault();
e.stopPropagation();
return;
}
}
ele = ele.parentNode;
}
}
},
true,
);
}
})(jQuery, window, document);
(function ($, window, undefined) {
var $document = $(document),
supportTouch = $.mobile.support.touch,
scrollEvent = "touchmove scroll",
touchStartEvent = supportTouch ? "touchstart" : "mousedown",
touchStopEvent = supportTouch ? "touchend" : "mouseup",
touchMoveEvent = supportTouch ? "touchmove" : "mousemove";
// setup new event shortcuts
$.each(
(
"touchstart touchmove touchend " +
"tap taphold " +
"swipe swipeleft swiperight " +
"scrollstart scrollstop"
).split(" "),
function (i, name) {
$.fn[name] = function (fn) {
return fn ? this.bind(name, fn) : this.trigger(name);
};
// jQuery < 1.8
if ($.attrFn) {
$.attrFn[name] = true;
}
},
);
function triggerCustomEvent(obj, eventType, event, bubble) {
var originalType = event.type;
event.type = eventType;
if (bubble) {
$.event.trigger(event, undefined, obj);
} else {
$.event.dispatch.call(obj, event);
}
event.type = originalType;
}
// also handles scrollstop
$.event.special.scrollstart = {
enabled: true,
setup: function () {
var thisObject = this,
$this = $(thisObject),
scrolling,
timer;
function trigger(event, state) {
scrolling = state;
triggerCustomEvent(
thisObject,
scrolling ? "scrollstart" : "scrollstop",
event,
);
}
// iPhone triggers scroll after a small delay; use touchmove instead
$this.bind(scrollEvent, function (event) {
if (!$.event.special.scrollstart.enabled) {
return;
}
if (!scrolling) {
trigger(event, true);
}
clearTimeout(timer);
timer = setTimeout(function () {
trigger(event, false);
}, 50);
});
},
teardown: function () {
$(this).unbind(scrollEvent);
},
};
// also handles taphold
$.event.special.tap = {
tapholdThreshold: 750,
emitTapOnTaphold: true,
setup: function () {
var thisObject = this,
$this = $(thisObject),
isTaphold = false;
$this.bind("vmousedown", function (event) {
isTaphold = false;
if (event.which && event.which !== 1) {
return false;
}
var origTarget = event.target,
timer;
function clearTapTimer() {
clearTimeout(timer);
}
function clearTapHandlers() {
clearTapTimer();
$this
.unbind("vclick", clickHandler)
.unbind("vmouseup", clearTapTimer);
$document.unbind("vmousecancel", clearTapHandlers);
}
function clickHandler(event) {
clearTapHandlers();
// ONLY trigger a 'tap' event if the start target is
// the same as the stop target.
if (!isTaphold && origTarget === event.target) {
triggerCustomEvent(thisObject, "tap", event);
} else if (isTaphold) {
event.stopPropagation();
}
}
$this.bind("vmouseup", clearTapTimer).bind("vclick", clickHandler);
$document.bind("vmousecancel", clearTapHandlers);
timer = setTimeout(function () {
if (!$.event.special.tap.emitTapOnTaphold) {
isTaphold = true;
}
triggerCustomEvent(
thisObject,
"taphold",
$.Event("taphold", { target: origTarget }),
);
}, $.event.special.tap.tapholdThreshold);
});
},
teardown: function () {
$(this).unbind("vmousedown").unbind("vclick").unbind("vmouseup");
$document.unbind("vmousecancel");
},
};
// Also handles swipeleft, swiperight
$.event.special.swipe = {
// More than this horizontal displacement, and we will suppress scrolling.
scrollSupressionThreshold: 30,
// More time than this, and it isn't a swipe.
durationThreshold: 1000,
// Swipe horizontal displacement must be more than this.
horizontalDistanceThreshold: 30,
// Swipe vertical displacement must be less than this.
verticalDistanceThreshold: 30,
getLocation: function (event) {
var winPageX = window.pageXOffset,
winPageY = window.pageYOffset,
x = event.clientX,
y = event.clientY;
if (
(event.pageY === 0 && Math.floor(y) > Math.floor(event.pageY)) ||
(event.pageX === 0 && Math.floor(x) > Math.floor(event.pageX))
) {
// iOS4 clientX/clientY have the value that should have been
// in pageX/pageY. While pageX/page/ have the value 0
x = x - winPageX;
y = y - winPageY;
} else if (y < event.pageY - winPageY || x < event.pageX - winPageX) {
// Some Android browsers have totally bogus values for clientX/Y
// when scrolling/zooming a page. Detectable since clientX/clientY
// should never be smaller than pageX/pageY minus page scroll
x = event.pageX - winPageX;
y = event.pageY - winPageY;
}
return {
x: x,
y: y,
};
},
start: function (event) {
var data = event.originalEvent.touches
? event.originalEvent.touches[0]
: event,
location = $.event.special.swipe.getLocation(data);
return {
time: new Date().getTime(),
coords: [location.x, location.y],
origin: $(event.target),
};
},
stop: function (event) {
var data = event.originalEvent.touches
? event.originalEvent.touches[0]
: event,
location = $.event.special.swipe.getLocation(data);
return {
time: new Date().getTime(),
coords: [location.x, location.y],
};
},
handleSwipe: function (start, stop, thisObject, origTarget) {
if (
stop.time - start.time < $.event.special.swipe.durationThreshold &&
Math.abs(start.coords[0] - stop.coords[0]) >
$.event.special.swipe.horizontalDistanceThreshold &&
Math.abs(start.coords[1] - stop.coords[1]) <
$.event.special.swipe.verticalDistanceThreshold
) {
var direction =
start.coords[0] > stop.coords[0] ? "swipeleft" : "swiperight";
triggerCustomEvent(
thisObject,
"swipe",
$.Event("swipe", {
target: origTarget,
swipestart: start,
swipestop: stop,
}),
true,
);
triggerCustomEvent(
thisObject,
direction,
$.Event(direction, {
target: origTarget,
swipestart: start,
swipestop: stop,
}),
true,
);
return true;
}
return false;
},
// This serves as a flag to ensure that at most one swipe event event is
// in work at any given time
eventInProgress: false,
setup: function () {
var events,
thisObject = this,
$this = $(thisObject),
context = {};
// Retrieve the events data for this element and add the swipe context
events = $.data(this, "mobile-events");
if (!events) {
events = { length: 0 };
$.data(this, "mobile-events", events);
}
events.length++;
events.swipe = context;
context.start = function (event) {
// Bail if we're already working on a swipe event
if ($.event.special.swipe.eventInProgress) {
return;
}
$.event.special.swipe.eventInProgress = true;
var stop,
start = $.event.special.swipe.start(event),
origTarget = event.target,
emitted = false;
context.move = function (event) {
if (!start) {
return;
}
stop = $.event.special.swipe.stop(event);
if (!emitted) {
emitted = $.event.special.swipe.handleSwipe(
start,
stop,
thisObject,
origTarget,
);
if (emitted) {
// Reset the context to make way for the next swipe event
$.event.special.swipe.eventInProgress = false;
}
}
// prevent scrolling
if (
Math.abs(start.coords[0] - stop.coords[0]) >
$.event.special.swipe.scrollSupressionThreshold
) {
event.preventDefault();
}
};
context.stop = function () {
emitted = true;
// Reset the context to make way for the next swipe event
$.event.special.swipe.eventInProgress = false;
$document.off(touchMoveEvent, context.move);
context.move = null;
};
$document
.on(touchMoveEvent, context.move)
.one(touchStopEvent, context.stop);
};
$this.on(touchStartEvent, context.start);
},
teardown: function () {
var events, context;
events = $.data(this, "mobile-events");
if (events) {
context = events.swipe;
delete events.swipe;
events.length--;
if (events.length === 0) {
$.removeData(this, "mobile-events");
}
}
if (context) {
if (context.start) {
$(this).off(touchStartEvent, context.start);
}
if (context.move) {
$document.off(touchMoveEvent, context.move);
}
if (context.stop) {
$document.off(touchStopEvent, context.stop);
}
}
},
};
$.each(
{
scrollstop: "scrollstart",
taphold: "tap",
swipeleft: "swipe",
swiperight: "swipe",
},
function (event, sourceEvent) {
$.event.special[event] = {
setup: function () {
$(this).bind(sourceEvent, $.noop);
},
teardown: function () {
$(this).unbind(sourceEvent);
},
};
},
);
})(jQuery, this);
// throttled resize event
(function ($) {
$.event.special.throttledresize = {
setup: function () {
$(this).bind("resize", handler);
},
teardown: function () {
$(this).unbind("resize", handler);
},
};
var throttle = 250,
handler = function () {
curr = new Date().getTime();
diff = curr - lastCall;
if (diff >= throttle) {
lastCall = curr;
$(this).trigger("throttledresize");
} else {
if (heldCall) {
clearTimeout(heldCall);
}
// Promise a held call will still execute
heldCall = setTimeout(handler, throttle - diff);
}
},
lastCall = 0,
heldCall,
curr,
diff;
})(jQuery);
(function ($, window) {
var win = $(window),
event_name = "orientationchange",
get_orientation,
last_orientation,
initial_orientation_is_landscape,
initial_orientation_is_default,
portrait_map = { 0: true, 180: true },
ww,
wh,
landscape_threshold;
// It seems that some device/browser vendors use window.orientation values 0 and 180 to
// denote the "default" orientation. For iOS devices, and most other smart-phones tested,
// the default orientation is always "portrait", but in some Android and RIM based tablets,
// the default orientation is "landscape". The following code attempts to use the window
// dimensions to figure out what the current orientation is, and then makes adjustments
// to the to the portrait_map if necessary, so that we can properly decode the
// window.orientation value whenever get_orientation() is called.
//
// Note that we used to use a media query to figure out what the orientation the browser
// thinks it is in:
//
// initial_orientation_is_landscape = $.mobile.media("all and (orientation: landscape)");
//
// but there was an iPhone/iPod Touch bug beginning with iOS 4.2, up through iOS 5.1,
// where the browser *ALWAYS* applied the landscape media query. This bug does not
// happen on iPad.
if ($.support.orientation) {
// Check the window width and height to figure out what the current orientation
// of the device is at this moment. Note that we've initialized the portrait map
// values to 0 and 180, *AND* we purposely check for landscape so that if we guess
// wrong, , we default to the assumption that portrait is the default orientation.
// We use a threshold check below because on some platforms like iOS, the iPhone
// form-factor can report a larger width than height if the user turns on the
// developer console. The actual threshold value is somewhat arbitrary, we just
// need to make sure it is large enough to exclude the developer console case.
ww = window.innerWidth || win.width();
wh = window.innerHeight || win.height();
landscape_threshold = 50;
initial_orientation_is_landscape =
ww > wh && ww - wh > landscape_threshold;
// Now check to see if the current window.orientation is 0 or 180.
initial_orientation_is_default = portrait_map[window.orientation];
// If the initial orientation is landscape, but window.orientation reports 0 or 180, *OR*
// if the initial orientation is portrait, but window.orientation reports 90 or -90, we
// need to flip our portrait_map values because landscape is the default orientation for
// this device/browser.
if (
(initial_orientation_is_landscape && initial_orientation_is_default) ||
(!initial_orientation_is_landscape && !initial_orientation_is_default)
) {
portrait_map = { "-90": true, 90: true };
}
}
$.event.special.orientationchange = $.extend(
{},
$.event.special.orientationchange,
{
setup: function () {
// If the event is supported natively, return false so that jQuery
// will bind to the event using DOM methods.
if (
$.support.orientation &&
!$.event.special.orientationchange.disabled
) {
return false;
}
// Get the current orientation to avoid initial double-triggering.
last_orientation = get_orientation();
// Because the orientationchange event doesn't exist, simulate the
// event by testing window dimensions on resize.
win.bind("throttledresize", handler);
},
teardown: function () {
// If the event is not supported natively, return false so that
// jQuery will unbind the event using DOM methods.
if (
$.support.orientation &&
!$.event.special.orientationchange.disabled
) {
return false;
}
// Because the orientationchange event doesn't exist, unbind the
// resize event handler.
win.unbind("throttledresize", handler);
},
add: function (handleObj) {
// Save a reference to the bound event handler.
var old_handler = handleObj.handler;
handleObj.handler = function (event) {
// Modify event object, adding the .orientation property.
event.orientation = get_orientation();
// Call the originally-bound event handler and return its result.
return old_handler.apply(this, arguments);
};
},
},
);
// If the event is not supported natively, this handler will be bound to
// the window resize event to simulate the orientationchange event.
function handler() {
// Get the current orientation.
var orientation = get_orientation();
if (orientation !== last_orientation) {
// The orientation has changed, so trigger the orientationchange event.
last_orientation = orientation;
win.trigger(event_name);
}
}
// Get the current page orientation. This method is exposed publicly, should it
// be needed, as jQuery.event.special.orientationchange.orientation()
$.event.special.orientationchange.orientation = get_orientation =
function () {
var isPortrait = true,
elem = document.documentElement;
// prefer window orientation to the calculation based on screensize as
// the actual screen resize takes place before or after the orientation change event
// has been fired depending on implementation (eg android 2.3 is before, iphone after).
// More testing is required to determine if a more reliable method of determining the new screensize
// is possible when orientationchange is fired. (eg, use media queries + element + opacity)
if ($.support.orientation) {
// if the window orientation registers as 0 or 180 degrees report
// portrait, otherwise landscape
isPortrait = portrait_map[window.orientation];
} else {
isPortrait = elem && elem.clientWidth / elem.clientHeight < 1.1;
}
return isPortrait ? "portrait" : "landscape";
};
$.fn[event_name] = function (fn) {
return fn ? this.bind(event_name, fn) : this.trigger(event_name);
};
// jQuery < 1.8
if ($.attrFn) {
$.attrFn[event_name] = true;
}
})(jQuery, this);
(function ($, undefined) {
// existing base tag?
var baseElement = $("head").children("base"),
// base element management, defined depending on dynamic base tag support
// TODO move to external widget
base = {
// define base element, for use in routing asset urls that are referenced
// in Ajax-requested markup
element: baseElement.length
? baseElement
: $("<base>", {
href: $.mobile.path.documentBase.hrefNoHash,
}).prependTo($("head")),
linkSelector:
"[src], link[href], a[rel='external'], :jqmData(ajax='false'), a[target]",
// set the generated BASE element's href to a new page's base path
set: function (href) {
// we should do nothing if the user wants to manage their url base
// manually
if (!$.mobile.dynamicBaseEnabled) {
return;
}
// we should use the base tag if we can manipulate it dynamically
if ($.support.dynamicBaseTag) {
base.element.attr(
"href",
$.mobile.path.makeUrlAbsolute(href, $.mobile.path.documentBase),
);
}
},
rewrite: function (href, page) {
var newPath = $.mobile.path.get(href);
page.find(base.linkSelector).each(function (i, link) {
var thisAttr = $(link).is("[href]")
? "href"
: $(link).is("[src]")
? "src"
: "action",
thisUrl = $(link).attr(thisAttr);
// XXX_jblas: We need to fix this so that it removes the document
// base URL, and then prepends with the new page URL.
// if full path exists and is same, chop it - helps IE out
thisUrl = thisUrl.replace(
location.protocol + "//" + location.host + location.pathname,
"",
);
if (!/^(\w+:|#|\/)/.test(thisUrl)) {
$(link).attr(thisAttr, newPath + thisUrl);
}
});
},
// set the generated BASE element's href to a new page's base path
reset: function (/* href */) {
base.element.attr("href", $.mobile.path.documentBase.hrefNoSearch);
},
};
$.mobile.base = base;
})(jQuery);
(function ($, undefined) {
$.mobile.widgets = {};
var originalWidget = $.widget,
// Record the original, non-mobileinit-modified version of $.mobile.keepNative
// so we can later determine whether someone has modified $.mobile.keepNative
keepNativeFactoryDefault = $.mobile.keepNative;
$.widget = (function (orig) {
return function () {
var constructor = orig.apply(this, arguments),
name = constructor.prototype.widgetName;
constructor.initSelector =
constructor.prototype.initSelector !== undefined
? constructor.prototype.initSelector
: ":jqmData(role='" + name + "')";
$.mobile.widgets[name] = constructor;
return constructor;
};
})($.widget);
// Make sure $.widget still has bridge and extend methods
$.extend($.widget, originalWidget);
// For backcompat remove in 1.5
$.mobile.document.on("create", function (event) {
$(event.target).enhanceWithin();
});
$.widget("mobile.page", {
options: {
theme: "a",
domCache: false,
// Deprecated in 1.4 remove in 1.5
keepNativeDefault: $.mobile.keepNative,
// Deprecated in 1.4 remove in 1.5
contentTheme: null,
enhanced: false,
},
// DEPRECATED for > 1.4
// TODO remove at 1.5
_createWidget: function () {
$.Widget.prototype._createWidget.apply(this, arguments);
this._trigger("init");
},
_create: function () {
// If false is returned by the callbacks do not create the page
if (this._trigger("beforecreate") === false) {
return false;
}
if (!this.options.enhanced) {
this._enhance();
}
this._on(this.element, {
pagebeforehide: "removeContainerBackground",
pagebeforeshow: "_handlePageBeforeShow",
});
this.element.enhanceWithin();
// Dialog widget is deprecated in 1.4 remove this in 1.5
if (
$.mobile.getAttribute(this.element[0], "role") === "dialog" &&
$.mobile.dialog
) {
this.element.dialog();
}
},
_enhance: function () {
var attrPrefix = "data-" + $.mobile.ns,
self = this;
if (this.options.role) {
this.element.attr("data-" + $.mobile.ns + "role", this.options.role);
}
this.element
.attr("tabindex", "0")
.addClass("ui-page ui-page-theme-" + this.options.theme);
// Manipulation of content os Deprecated as of 1.4 remove in 1.5
this.element
.find("[" + attrPrefix + "role='content']")
.each(function () {
var $this = $(this),
theme = this.getAttribute(attrPrefix + "theme") || undefined;
self.options.contentTheme =
theme ||
self.options.contentTheme ||
(self.options.dialog && self.options.theme) ||
(self.element.jqmData("role") === "dialog" && self.options.theme);
$this.addClass("ui-content");
if (self.options.contentTheme) {
$this.addClass("ui-body-" + self.options.contentTheme);
}
// Add ARIA role
$this.attr("role", "main").addClass("ui-content");
});
},
bindRemove: function (callback) {
var page = this.element;
// when dom caching is not enabled or the page is embedded bind to remove the page on hide
if (
!page.data("mobile-page").options.domCache &&
page.is(":jqmData(external-page='true')")
) {
// TODO use _on - that is, sort out why it doesn't work in this case
page.bind(
"pagehide.remove",
callback ||
function (e, data) {
//check if this is a same page transition and if so don't remove the page
if (!data.samePage) {
var $this = $(this),
prEvent = new $.Event("pageremove");
$this.trigger(prEvent);
if (!prEvent.isDefaultPrevented()) {
$this.removeWithDependents();
}
}
},
);
}
},
_setOptions: function (o) {
if (o.theme !== undefined) {
this.element
.removeClass("ui-page-theme-" + this.options.theme)
.addClass("ui-page-theme-" + o.theme);
}
if (o.contentTheme !== undefined) {
this.element
.find("[data-" + $.mobile.ns + "='content']")
.removeClass("ui-body-" + this.options.contentTheme)
.addClass("ui-body-" + o.contentTheme);
}
},
_handlePageBeforeShow: function (/* e */) {
this.setContainerBackground();
},
// Deprecated in 1.4 remove in 1.5
removeContainerBackground: function () {
this.element
.closest(":mobile-pagecontainer")
.pagecontainer({ theme: "none" });
},
// Deprecated in 1.4 remove in 1.5
// set the page container background to the page theme
setContainerBackground: function (theme) {
this.element
.parent()
.pagecontainer({ theme: theme || this.options.theme });
},
// Deprecated in 1.4 remove in 1.5
keepNativeSelector: function () {
var options = this.options,
keepNative = $.trim(options.keepNative || ""),
globalValue = $.trim($.mobile.keepNative),
optionValue = $.trim(options.keepNativeDefault),
// Check if $.mobile.keepNative has changed from the factory default
newDefault =
keepNativeFactoryDefault === globalValue ? "" : globalValue,
// If $.mobile.keepNative has not changed, use options.keepNativeDefault
oldDefault = newDefault === "" ? optionValue : "";
// Concatenate keepNative selectors from all sources where the value has
// changed or, if nothing has changed, return the default
return (keepNative ? [keepNative] : [])
.concat(newDefault ? [newDefault] : [])
.concat(oldDefault ? [oldDefault] : [])
.join(", ");
},
});
})(jQuery);
(function ($, undefined) {
$.widget("mobile.pagecontainer", {
options: {
theme: "a",
},
initSelector: false,
_create: function () {
this.setLastScrollEnabled = true;
this._on(this.window, {
// disable an scroll setting when a hashchange has been fired,
// this only works because the recording of the scroll position
// is delayed for 100ms after the browser might have changed the
// position because of the hashchange
navigate: "_disableRecordScroll",
// bind to scrollstop for the first page, "pagechange" won't be
// fired in that case
scrollstop: "_delayedRecordScroll",
});
// TODO consider moving the navigation handler OUT of widget into
// some other object as glue between the navigate event and the
// content widget load and change methods
this._on(this.window, { navigate: "_filterNavigateEvents" });
// TODO move from page* events to content* events
this._on({ pagechange: "_afterContentChange" });
// handle initial hashchange from chrome :(
this.window.one(
"navigate",
$.proxy(function () {
this.setLastScrollEnabled = true;
}, this),
);
},
_setOptions: function (options) {
if (options.theme !== undefined && options.theme !== "none") {
this.element
.removeClass("ui-overlay-" + this.options.theme)
.addClass("ui-overlay-" + options.theme);
} else if (options.theme !== undefined) {
this.element.removeClass("ui-overlay-" + this.options.theme);
}
this._super(options);
},
_disableRecordScroll: function () {
this.setLastScrollEnabled = false;
},
_enableRecordScroll: function () {
this.setLastScrollEnabled = true;
},
// TODO consider the name here, since it's purpose specific
_afterContentChange: function () {
// once the page has changed, re-enable the scroll recording
this.setLastScrollEnabled = true;
// remove any binding that previously existed on the get scroll
// which may or may not be different than the scroll element
// determined for this page previously
this._off(this.window, "scrollstop");
// determine and bind to the current scoll element which may be the
// window or in the case of touch overflow the element touch overflow
this._on(this.window, { scrollstop: "_delayedRecordScroll" });
},
_recordScroll: function () {
// this barrier prevents setting the scroll value based on
// the browser scrolling the window based on a hashchange
if (!this.setLastScrollEnabled) {
return;
}
var active = this._getActiveHistory(),
currentScroll,
minScroll,
defaultScroll;
if (active) {
currentScroll = this._getScroll();
minScroll = this._getMinScroll();
defaultScroll = this._getDefaultScroll();
// Set active page's lastScroll prop. If the location we're
// scrolling to is less than minScrollBack, let it go.
active.lastScroll =
currentScroll < minScroll ? defaultScroll : currentScroll;
}
},
_delayedRecordScroll: function () {
setTimeout($.proxy(this, "_recordScroll"), 100);
},
_getScroll: function () {
return this.window.scrollTop();
},
_getMinScroll: function () {
return $.mobile.minScrollBack;
},
_getDefaultScroll: function () {
return $.mobile.defaultHomeScroll;
},
_filterNavigateEvents: function (e, data) {
var url;
if (e.originalEvent && e.originalEvent.isDefaultPrevented()) {
return;
}
url =
e.originalEvent.type.indexOf("hashchange") > -1
? data.state.hash
: data.state.url;
if (!url) {
url = this._getHash();
}
if (
!url ||
url === "#" ||
url.indexOf("#" + $.mobile.path.uiStateKey) === 0
) {
url = location.href;
}
this._handleNavigate(url, data.state);
},
_getHash: function () {
return $.mobile.path.parseLocation().hash;
},
// TODO active page should be managed by the container (ie, it should be a property)
getActivePage: function () {
return this.activePage;
},
// TODO the first page should be a property set during _create using the logic
// that currently resides in init
_getInitialContent: function () {
return $.mobile.firstPage;
},
// TODO each content container should have a history object
_getHistory: function () {
return $.mobile.navigate.history;
},
// TODO use _getHistory
_getActiveHistory: function () {
return $.mobile.navigate.history.getActive();
},
// TODO the document base should be determined at creation
_getDocumentBase: function () {
return $.mobile.path.documentBase;
},
back: function () {
this.go(-1);
},
forward: function () {
this.go(1);
},
go: function (steps) {
//if hashlistening is enabled use native history method
if ($.mobile.hashListeningEnabled) {
window.history.go(steps);
} else {
//we are not listening to the hash so handle history internally
var activeIndex = $.mobile.navigate.history.activeIndex,
index = activeIndex + parseInt(steps, 10),
url = $.mobile.navigate.history.stack[index].url,
direction = steps >= 1 ? "forward" : "back";
//update the history object
$.mobile.navigate.history.activeIndex = index;
$.mobile.navigate.history.previousIndex = activeIndex;
//change to the new page
this.change(url, {
direction: direction,
changeHash: false,
fromHashChange: true,
});
}
},
// TODO rename _handleDestination
_handleDestination: function (to) {
var history;
// clean the hash for comparison if it's a url
if ($.type(to) === "string") {
to = $.mobile.path.stripHash(to);
}
if (to) {
history = this._getHistory();
// At this point, 'to' can be one of 3 things, a cached page
// element from a history stack entry, an id, or site-relative /
// absolute URL. If 'to' is an id, we need to resolve it against
// the documentBase, not the location.href, since the hashchange
// could've been the result of a forward/backward navigation
// that crosses from an external page/dialog to an internal
// page/dialog.
//
// TODO move check to history object or path object?
to = !$.mobile.path.isPath(to)
? $.mobile.path.makeUrlAbsolute("#" + to, this._getDocumentBase())
: to;
// If we're about to go to an initial URL that contains a
// reference to a non-existent internal page, go to the first
// page instead. We know that the initial hash refers to a
// non-existent page, because the initial hash did not end
// up in the initial history entry
// TODO move check to history object?
if (
to ===
$.mobile.path.makeUrlAbsolute(
"#" + history.initialDst,
this._getDocumentBase(),
) &&
history.stack.length &&
history.stack[0].url !==
history.initialDst.replace($.mobile.dialogHashKey, "")
) {
to = this._getInitialContent();
}
}
return to || this._getInitialContent();
},
_handleDialog: function (changePageOptions, data) {
var to,
active,
activeContent = this.getActivePage();
// If current active page is not a dialog skip the dialog and continue
// in the same direction
if (activeContent && !activeContent.hasClass("ui-dialog")) {
// determine if we're heading forward or backward and continue
// accordingly past the current dialog
if (data.direction === "back") {
this.back();
} else {
this.forward();
}
// prevent changePage call
return false;
} else {
// if the current active page is a dialog and we're navigating
// to a dialog use the dialog objected saved in the stack
to = data.pageUrl;
active = this._getActiveHistory();
// make sure to set the role, transition and reversal
// as most of this is lost by the domCache cleaning
$.extend(changePageOptions, {
role: active.role,
transition: active.transition,
reverse: data.direction === "back",
});
}
return to;
},
_handleNavigate: function (url, data) {
//find first page via hash
// TODO stripping the hash twice with handleUrl
var to = $.mobile.path.stripHash(url),
history = this._getHistory(),
// transition is false if it's the first page, undefined
// otherwise (and may be overridden by default)
transition = history.stack.length === 0 ? "none" : undefined,
// default options for the changPage calls made after examining
// the current state of the page and the hash, NOTE that the
// transition is derived from the previous history entry
changePageOptions = {
changeHash: false,
fromHashChange: true,
reverse: data.direction === "back",
};
$.extend(changePageOptions, data, {
transition: (history.getLast() || {}).transition || transition,
});
// TODO move to _handleDestination ?
// If this isn't the first page, if the current url is a dialog hash
// key, and the initial destination isn't equal to the current target
// page, use the special dialog handling
if (
history.activeIndex > 0 &&
to.indexOf($.mobile.dialogHashKey) > -1 &&
history.initialDst !== to
) {
to = this._handleDialog(changePageOptions, data);
if (to === false) {
return;
}
}
this._changeContent(this._handleDestination(to), changePageOptions);
},
_changeContent: function (to, opts) {
$.mobile.changePage(to, opts);
},
_getBase: function () {
return $.mobile.base;
},
_getNs: function () {
return $.mobile.ns;
},
_enhance: function (content, role) {
// TODO consider supporting a custom callback, and passing in
// the settings which includes the role
return content.page({ role: role });
},
_include: function (page, settings) {
// append to page and enhance
page.appendTo(this.element);
// use the page widget to enhance
this._enhance(page, settings.role);
// remove page on hide
page.page("bindRemove");
},
_find: function (absUrl) {
// TODO consider supporting a custom callback
var fileUrl = this._createFileUrl(absUrl),
dataUrl = this._createDataUrl(absUrl),
page,
initialContent = this._getInitialContent();
// Check to see if the page already exists in the DOM.
// NOTE do _not_ use the :jqmData pseudo selector because parenthesis
// are a valid url char and it breaks on the first occurence
page = this.element.children(
"[data-" + this._getNs() + "url='" + dataUrl + "']",
);
// If we failed to find the page, check to see if the url is a
// reference to an embedded page. If so, it may have been dynamically
// injected by a developer, in which case it would be lacking a
// data-url attribute and in need of enhancement.
if (page.length === 0 && dataUrl && !$.mobile.path.isPath(dataUrl)) {
page = this.element
.children($.mobile.path.hashToSelector("#" + dataUrl))
.attr("data-" + this._getNs() + "url", dataUrl)
.jqmData("url", dataUrl);
}
// If we failed to find a page in the DOM, check the URL to see if it
// refers to the first page in the application. Also check to make sure
// our cached-first-page is actually in the DOM. Some user deployed
// apps are pruning the first page from the DOM for various reasons.
// We check for this case here because we don't want a first-page with
// an id falling through to the non-existent embedded page error case.
if (
page.length === 0 &&
$.mobile.path.isFirstPageUrl(fileUrl) &&
initialContent &&
initialContent.parent().length
) {
page = $(initialContent);
}
return page;
},
_getLoader: function () {
return $.mobile.loading();
},
_showLoading: function (delay, theme, msg, textonly) {
// This configurable timeout allows cached pages a brief
// delay to load without showing a message
if (this._loadMsg) {
return;
}
this._loadMsg = setTimeout(
$.proxy(function () {
this._getLoader().loader("show", theme, msg, textonly);
this._loadMsg = 0;
}, this),
delay,
);
},
_hideLoading: function () {
// Stop message show timer
clearTimeout(this._loadMsg);
this._loadMsg = 0;
// Hide loading message
this._getLoader().loader("hide");
},
_showError: function () {
// make sure to remove the current loading message
this._hideLoading();
// show the error message
this._showLoading(
0,
$.mobile.pageLoadErrorMessageTheme,
$.mobile.pageLoadErrorMessage,
true,
);
// hide the error message after a delay
// TODO configuration
setTimeout($.proxy(this, "_hideLoading"), 1500);
},
_parse: function (html, fileUrl) {
// TODO consider allowing customization of this method. It's very JQM specific
var page,
all = $("<div></div>");
//workaround to allow scripts to execute when included in page divs
all.get(0).innerHTML = html;
page = all
.find(":jqmData(role='page'), :jqmData(role='dialog')")
.first();
//if page elem couldn't be found, create one and insert the body element's contents
if (!page.length) {
page = $(
"<div data-" +
this._getNs() +
"role='page'>" +
(html.split(/<\/?body[^>]*>/gim)[1] || "") +
"</div>",
);
}
// TODO tagging a page with external to make sure that embedded pages aren't
// removed by the various page handling code is bad. Having page handling code
// in many places is bad. Solutions post 1.0
page
.attr(
"data-" + this._getNs() + "url",
$.mobile.path.convertUrlToDataUrl(fileUrl),
)
.attr("data-" + this._getNs() + "external-page", true);
return page;
},
_setLoadedTitle: function (page, html) {
//page title regexp
var newPageTitle = html.match(/<title[^>]*>([^<]*)/) && RegExp.$1;
if (newPageTitle && !page.jqmData("title")) {
newPageTitle = $("<div>" + newPageTitle + "</div>").text();
page.jqmData("title", newPageTitle);
}
},
_isRewritableBaseTag: function () {
return $.mobile.dynamicBaseEnabled && !$.support.dynamicBaseTag;
},
_createDataUrl: function (absoluteUrl) {
return $.mobile.path.convertUrlToDataUrl(absoluteUrl);
},
_createFileUrl: function (absoluteUrl) {
return $.mobile.path.getFilePath(absoluteUrl);
},
_triggerWithDeprecated: function (name, data, page) {
var deprecatedEvent = $.Event("page" + name),
newEvent = $.Event(this.widgetName + name);
// DEPRECATED
// trigger the old deprecated event on the page if it's provided
(page || this.element).trigger(deprecatedEvent, data);
// use the widget trigger method for the new content* event
this.element.trigger(newEvent, data);
return {
deprecatedEvent: deprecatedEvent,
event: newEvent,
};
},
// TODO it would be nice to split this up more but everything appears to be "one off"
// or require ordering such that other bits are sprinkled in between parts that
// could be abstracted out as a group
_loadSuccess: function (absUrl, triggerData, settings, deferred) {
var fileUrl = this._createFileUrl(absUrl),
dataUrl = this._createDataUrl(absUrl);
return $.proxy(function (html, textStatus, xhr) {
//pre-parse html to check for a data-url,
//use it as the new fileUrl, base path, etc
var content,
// TODO handle dialogs again
pageElemRegex = new RegExp(
"(<[^>]+\\bdata-" +
this._getNs() +
"role=[\"']?page[\"']?[^>]*>)",
),
dataUrlRegex = new RegExp(
"\\bdata-" + this._getNs() + "url=[\"']?([^\"'>]*)[\"']?",
);
// data-url must be provided for the base tag so resource requests
// can be directed to the correct url. loading into a temprorary
// element makes these requests immediately
if (
pageElemRegex.test(html) &&
RegExp.$1 &&
dataUrlRegex.test(RegExp.$1) &&
RegExp.$1
) {
fileUrl = $.mobile.path.getFilePath(
$("<div>" + RegExp.$1 + "</div>").text(),
);
}
//dont update the base tag if we are prefetching
if (settings.prefetch === undefined) {
this._getBase().set(fileUrl);
}
content = this._parse(html, fileUrl);
this._setLoadedTitle(content, html);
// Add the content reference and xhr to our triggerData.
triggerData.xhr = xhr;
triggerData.textStatus = textStatus;
// DEPRECATED
triggerData.page = content;
triggerData.content = content;
// If the default behavior is prevented, stop here!
// Note that it is the responsibility of the listener/handler
// that called preventDefault(), to resolve/reject the
// deferred object within the triggerData.
if (!this._trigger("load", undefined, triggerData)) {
return;
}
// rewrite src and href attrs to use a base url if the base tag won't work
if (this._isRewritableBaseTag() && content) {
this._getBase().rewrite(fileUrl, content);
}
this._include(content, settings);
// Enhancing the content may result in new dialogs/sub content being inserted
// into the DOM. If the original absUrl refers to a sub-content, that is the
// real content we are interested in.
if (absUrl.indexOf("&" + $.mobile.subPageUrlKey) > -1) {
content = this.element.children(
"[data-" + this._getNs() + "url='" + dataUrl + "']",
);
}
// Remove loading message.
if (settings.showLoadMsg) {
this._hideLoading();
}
// BEGIN DEPRECATED ---------------------------------------------------
// Let listeners know the content loaded successfully.
this.element.trigger("pageload");
// END DEPRECATED -----------------------------------------------------
deferred.resolve(absUrl, settings, content);
}, this);
},
_loadDefaults: {
type: "get",
data: undefined,
// DEPRECATED
reloadPage: false,
reload: false,
// By default we rely on the role defined by the @data-role attribute.
role: undefined,
showLoadMsg: false,
// This delay allows loads that pull from browser cache to
// occur without showing the loading message.
loadMsgDelay: 50,
},
load: function (url, options) {
// This function uses deferred notifications to let callers
// know when the content is done loading, or if an error has occurred.
var deferred = (options && options.deferred) || $.Deferred(),
// The default load options with overrides specified by the caller.
settings = $.extend({}, this._loadDefaults, options),
// The DOM element for the content after it has been loaded.
content = null,
// The absolute version of the URL passed into the function. This
// version of the URL may contain dialog/subcontent params in it.
absUrl = $.mobile.path.makeUrlAbsolute(
url,
this._findBaseWithDefault(),
),
fileUrl,
dataUrl,
pblEvent,
triggerData;
// DEPRECATED reloadPage
settings.reload = settings.reloadPage;
// If the caller provided data, and we're using "get" request,
// append the data to the URL.
if (settings.data && settings.type === "get") {
absUrl = $.mobile.path.addSearchParams(absUrl, settings.data);
settings.data = undefined;
}
// If the caller is using a "post" request, reload must be true
if (settings.data && settings.type === "post") {
settings.reload = true;
}
// The absolute version of the URL minus any dialog/subcontent params.
// In otherwords the real URL of the content to be loaded.
fileUrl = this._createFileUrl(absUrl);
// The version of the Url actually stored in the data-url attribute of
// the content. For embedded content, it is just the id of the page. For
// content within the same domain as the document base, it is the site
// relative path. For cross-domain content (Phone Gap only) the entire
// absolute Url is used to load the content.
dataUrl = this._createDataUrl(absUrl);
content = this._find(absUrl);
// If it isn't a reference to the first content and refers to missing
// embedded content reject the deferred and return
if (
content.length === 0 &&
$.mobile.path.isEmbeddedPage(fileUrl) &&
!$.mobile.path.isFirstPageUrl(fileUrl)
) {
deferred.reject(absUrl, settings);
return;
}
// Reset base to the default document base
// TODO figure out why we doe this
this._getBase().reset();
// If the content we are interested in is already in the DOM,
// and the caller did not indicate that we should force a
// reload of the file, we are done. Resolve the deferrred so that
// users can bind to .done on the promise
if (content.length && !settings.reload) {
this._enhance(content, settings.role);
deferred.resolve(absUrl, settings, content);
//if we are reloading the content make sure we update
// the base if its not a prefetch
if (!settings.prefetch) {
this._getBase().set(url);
}
return;
}
triggerData = {
url: url,
absUrl: absUrl,
dataUrl: dataUrl,
deferred: deferred,
options: settings,
};
// Let listeners know we're about to load content.
pblEvent = this._triggerWithDeprecated("beforeload", triggerData);
// If the default behavior is prevented, stop here!
if (
pblEvent.deprecatedEvent.isDefaultPrevented() ||
pblEvent.event.isDefaultPrevented()
) {
return;
}
if (settings.showLoadMsg) {
this._showLoading(settings.loadMsgDelay);
}
// Reset base to the default document base.
// only reset if we are not prefetching
if (settings.prefetch === undefined) {
this._getBase().reset();
}
if (
!(
$.mobile.allowCrossDomainPages ||
$.mobile.path.isSameDomain($.mobile.path.documentUrl, absUrl)
)
) {
deferred.reject(absUrl, settings);
return;
}
// Load the new content.
$.ajax({
url: fileUrl,
type: settings.type,
data: settings.data,
contentType: settings.contentType,
dataType: "html",
success: this._loadSuccess(absUrl, triggerData, settings, deferred),
error: this._loadError(absUrl, triggerData, settings, deferred),
});
},
_loadError: function (absUrl, triggerData, settings, deferred) {
return $.proxy(function (xhr, textStatus, errorThrown) {
//set base back to current path
this._getBase().set($.mobile.path.get());
// Add error info to our triggerData.
triggerData.xhr = xhr;
triggerData.textStatus = textStatus;
triggerData.errorThrown = errorThrown;
// Let listeners know the page load failed.
var plfEvent = this._triggerWithDeprecated("loadfailed", triggerData);
// If the default behavior is prevented, stop here!
// Note that it is the responsibility of the listener/handler
// that called preventDefault(), to resolve/reject the
// deferred object within the triggerData.
if (
plfEvent.deprecatedEvent.isDefaultPrevented() ||
plfEvent.event.isDefaultPrevented()
) {
return;
}
// Remove loading message.
if (settings.showLoadMsg) {
this._showError();
}
deferred.reject(absUrl, settings);
}, this);
},
_getTransitionHandler: function (transition) {
transition = $.mobile._maybeDegradeTransition(transition);
//find the transition handler for the specified transition. If there
//isn't one in our transitionHandlers dictionary, use the default one.
//call the handler immediately to kick-off the transition.
return (
$.mobile.transitionHandlers[transition] ||
$.mobile.defaultTransitionHandler
);
},
// TODO move into transition handlers?
_triggerCssTransitionEvents: function (to, from, prefix) {
var samePage = false;
prefix = prefix || "";
// TODO decide if these events should in fact be triggered on the container
if (from) {
//Check if this is a same page transition and tell the handler in page
if (to[0] === from[0]) {
samePage = true;
}
//trigger before show/hide events
// TODO deprecate nextPage in favor of next
this._triggerWithDeprecated(
prefix + "hide",
{ nextPage: to, samePage: samePage },
from,
);
}
// TODO deprecate prevPage in favor of previous
this._triggerWithDeprecated(
prefix + "show",
{ prevPage: from || $("") },
to,
);
},
// TODO make private once change has been defined in the widget
_cssTransition: function (to, from, options) {
var transition = options.transition,
reverse = options.reverse,
deferred = options.deferred,
TransitionHandler,
promise;
this._triggerCssTransitionEvents(to, from, "before");
// TODO put this in a binding to events *outside* the widget
this._hideLoading();
TransitionHandler = this._getTransitionHandler(transition);
promise = new TransitionHandler(
transition,
reverse,
to,
from,
).transition();
// TODO temporary accomodation of argument deferred
promise.done(function () {
deferred.resolve.apply(deferred, arguments);
});
promise.done(
$.proxy(function () {
this._triggerCssTransitionEvents(to, from);
}, this),
);
},
_releaseTransitionLock: function () {
//release transition lock so navigation is free again
isPageTransitioning = false;
if (pageTransitionQueue.length > 0) {
$.mobile.changePage.apply(null, pageTransitionQueue.pop());
}
},
_removeActiveLinkClass: function (force) {
//clear out the active button state
$.mobile.removeActiveLinkClass(force);
},
_loadUrl: function (to, triggerData, settings) {
// preserve the original target as the dataUrl value will be
// simplified eg, removing ui-state, and removing query params
// from the hash this is so that users who want to use query
// params have access to them in the event bindings for the page
// life cycle See issue #5085
settings.target = to;
settings.deferred = $.Deferred();
this.load(to, settings);
settings.deferred.done(
$.proxy(function (url, options, content) {
isPageTransitioning = false;
// store the original absolute url so that it can be provided
// to events in the triggerData of the subsequent changePage call
options.absUrl = triggerData.absUrl;
this.transition(content, triggerData, options);
}, this),
);
settings.deferred.fail(
$.proxy(
function (/* url, options */) {
this._removeActiveLinkClass(true);
this._releaseTransitionLock();
this._triggerWithDeprecated("changefailed", triggerData);
},
this,
),
);
},
_triggerPageBeforeChange: function (to, triggerData, settings) {
var pbcEvent = new $.Event("pagebeforechange");
$.extend(triggerData, { toPage: to, options: settings });
// NOTE: preserve the original target as the dataUrl value will be
// simplified eg, removing ui-state, and removing query params from
// the hash this is so that users who want to use query params have
// access to them in the event bindings for the page life cycle
// See issue #5085
if ($.type(to) === "string") {
// if the toPage is a string simply convert it
triggerData.absUrl = $.mobile.path.makeUrlAbsolute(
to,
this._findBaseWithDefault(),
);
} else {
// if the toPage is a jQuery object grab the absolute url stored
// in the loadPage callback where it exists
triggerData.absUrl = settings.absUrl;
}
// Let listeners know we're about to change the current page.
this.element.trigger(pbcEvent, triggerData);
// If the default behavior is prevented, stop here!
if (pbcEvent.isDefaultPrevented()) {
return false;
}
return true;
},
change: function (to, options) {
// If we are in the midst of a transition, queue the current request.
// We'll call changePage() once we're done with the current transition
// to service the request.
if (isPageTransitioning) {
pageTransitionQueue.unshift(arguments);
return;
}
var settings = $.extend({}, $.mobile.changePage.defaults, options),
triggerData = {};
// Make sure we have a fromPage.
settings.fromPage = settings.fromPage || this.activePage;
// if the page beforechange default is prevented return early
if (!this._triggerPageBeforeChange(to, triggerData, settings)) {
return;
}
// We allow "pagebeforechange" observers to modify the to in
// the trigger data to allow for redirects. Make sure our to is
// updated. We also need to re-evaluate whether it is a string,
// because an object can also be replaced by a string
to = triggerData.toPage;
// If the caller passed us a url, call loadPage()
// to make sure it is loaded into the DOM. We'll listen
// to the promise object it returns so we know when
// it is done loading or if an error ocurred.
if ($.type(to) === "string") {
// Set the isPageTransitioning flag to prevent any requests from
// entering this method while we are in the midst of loading a page
// or transitioning.
isPageTransitioning = true;
this._loadUrl(to, triggerData, settings);
} else {
this.transition(to, triggerData, settings);
}
},
transition: function (toPage, triggerData, settings) {
var fromPage,
url,
pageUrl,
fileUrl,
active,
activeIsInitialPage,
historyDir,
pageTitle,
isDialog,
alreadyThere,
newPageTitle,
params,
cssTransitionDeferred,
beforeTransition;
// If we are in the midst of a transition, queue the current request.
// We'll call changePage() once we're done with the current transition
// to service the request.
if (isPageTransitioning) {
// make sure to only queue the to and settings values so the arguments
// work with a call to the change method
pageTransitionQueue.unshift([toPage, settings]);
return;
}
// DEPRECATED - this call only, in favor of the before transition
// if the page beforechange default is prevented return early
if (!this._triggerPageBeforeChange(toPage, triggerData, settings)) {
return;
}
// if the (content|page)beforetransition default is prevented return early
// Note, we have to check for both the deprecated and new events
beforeTransition = this._triggerWithDeprecated(
"beforetransition",
triggerData,
);
if (
beforeTransition.deprecatedEvent.isDefaultPrevented() ||
beforeTransition.event.isDefaultPrevented()
) {
return;
}
// Set the isPageTransitioning flag to prevent any requests from
// entering this method while we are in the midst of loading a page
// or transitioning.
isPageTransitioning = true;
// If we are going to the first-page of the application, we need to make
// sure settings.dataUrl is set to the application document url. This allows
// us to avoid generating a document url with an id hash in the case where the
// first-page of the document has an id attribute specified.
if (toPage[0] === $.mobile.firstPage[0] && !settings.dataUrl) {
settings.dataUrl = $.mobile.path.documentUrl.hrefNoHash;
}
// The caller passed us a real page DOM element. Update our
// internal state and then trigger a transition to the page.
fromPage = settings.fromPage;
url =
(settings.dataUrl &&
$.mobile.path.convertUrlToDataUrl(settings.dataUrl)) ||
toPage.jqmData("url");
// The pageUrl var is usually the same as url, except when url is obscured
// as a dialog url. pageUrl always contains the file path
pageUrl = url;
fileUrl = $.mobile.path.getFilePath(url);
active = $.mobile.navigate.history.getActive();
activeIsInitialPage = $.mobile.navigate.history.activeIndex === 0;
historyDir = 0;
pageTitle = document.title;
isDialog =
(settings.role === "dialog" || toPage.jqmData("role") === "dialog") &&
toPage.jqmData("dialog") !== true;
// By default, we prevent changePage requests when the fromPage and toPage
// are the same element, but folks that generate content
// manually/dynamically and reuse pages want to be able to transition to
// the same page. To allow this, they will need to change the default
// value of allowSamePageTransition to true, *OR*, pass it in as an
// option when they manually call changePage(). It should be noted that
// our default transition animations assume that the formPage and toPage
// are different elements, so they may behave unexpectedly. It is up to
// the developer that turns on the allowSamePageTransitiona option to
// either turn off transition animations, or make sure that an appropriate
// animation transition is used.
if (
fromPage &&
fromPage[0] === toPage[0] &&
!settings.allowSamePageTransition
) {
isPageTransitioning = false;
this._triggerWithDeprecated("transition", triggerData);
this.element.trigger("pagechange", triggerData);
// Even if there is no page change to be done, we should keep the
// urlHistory in sync with the hash changes
if (settings.fromHashChange) {
$.mobile.navigate.history.direct({ url: url });
}
return;
}
// We need to make sure the page we are given has already been enhanced.
toPage.page({ role: settings.role });
// If the changePage request was sent from a hashChange event, check to
// see if the page is already within the urlHistory stack. If so, we'll
// assume the user hit the forward/back button and will try to match the
// transition accordingly.
if (settings.fromHashChange) {
historyDir = settings.direction === "back" ? -1 : 1;
}
// Kill the keyboard.
// XXX_jblas: We need to stop crawling the entire document to kill focus.
// Instead, we should be tracking focus with a delegate()
// handler so we already have the element in hand at this
// point.
// Wrap this in a try/catch block since IE9 throw "Unspecified error" if
// document.activeElement is undefined when we are in an IFrame.
try {
if (
document.activeElement &&
document.activeElement.nodeName.toLowerCase() !== "body"
) {
$(document.activeElement).blur();
} else {
$("input:focus, textarea:focus, select:focus").blur();
}
} catch (e) {}
// Record whether we are at a place in history where a dialog used to be -
// if so, do not add a new history entry and do not change the hash either
alreadyThere = false;
// If we're displaying the page as a dialog, we don't want the url
// for the dialog content to be used in the hash. Instead, we want
// to append the dialogHashKey to the url of the current page.
if (isDialog && active) {
// on the initial page load active.url is undefined and in that case
// should be an empty string. Moving the undefined -> empty string back
// into urlHistory.addNew seemed imprudent given undefined better
// represents the url state
// If we are at a place in history that once belonged to a dialog, reuse
// this state without adding to urlHistory and without modifying the
// hash. However, if a dialog is already displayed at this point, and
// we're about to display another dialog, then we must add another hash
// and history entry on top so that one may navigate back to the
// original dialog
if (
active.url &&
active.url.indexOf($.mobile.dialogHashKey) > -1 &&
this.activePage &&
!this.activePage.hasClass("ui-dialog") &&
$.mobile.navigate.history.activeIndex > 0
) {
settings.changeHash = false;
alreadyThere = true;
}
// Normally, we tack on a dialog hash key, but if this is the location
// of a stale dialog, we reuse the URL from the entry
url = active.url || "";
// account for absolute urls instead of just relative urls use as hashes
if (!alreadyThere && url.indexOf("#") > -1) {
url += $.mobile.dialogHashKey;
} else {
url += "#" + $.mobile.dialogHashKey;
}
// tack on another dialogHashKey if this is the same as the initial hash
// this makes sure that a history entry is created for this dialog
if (
$.mobile.navigate.history.activeIndex === 0 &&
url === $.mobile.navigate.history.initialDst
) {
url += $.mobile.dialogHashKey;
}
}
// if title element wasn't found, try the page div data attr too
// If this is a deep-link or a reload ( active === undefined ) then just
// use pageTitle
newPageTitle = !active
? pageTitle
: toPage.jqmData("title") ||
toPage.children(":jqmData(role='header')").find(".ui-title").text();
if (!!newPageTitle && pageTitle === document.title) {
pageTitle = newPageTitle;
}
if (!toPage.jqmData("title")) {
toPage.jqmData("title", pageTitle);
}
// Make sure we have a transition defined.
settings.transition =
settings.transition ||
(historyDir && !activeIsInitialPage
? active.transition
: undefined) ||
(isDialog
? $.mobile.defaultDialogTransition
: $.mobile.defaultPageTransition);
//add page to history stack if it's not back or forward
if (!historyDir && alreadyThere) {
$.mobile.navigate.history.getActive().pageUrl = pageUrl;
}
// Set the location hash.
if (url && !settings.fromHashChange) {
// rebuilding the hash here since we loose it earlier on
// TODO preserve the originally passed in path
if (!$.mobile.path.isPath(url) && url.indexOf("#") < 0) {
url = "#" + url;
}
// TODO the property names here are just silly
params = {
transition: settings.transition,
title: pageTitle,
pageUrl: pageUrl,
role: settings.role,
};
if (settings.changeHash !== false && $.mobile.hashListeningEnabled) {
$.mobile.navigate(url, params, true);
} else if (toPage[0] !== $.mobile.firstPage[0]) {
$.mobile.navigate.history.add(url, params);
}
}
//set page title
document.title = pageTitle;
//set "toPage" as activePage deprecated in 1.4 remove in 1.5
$.mobile.activePage = toPage;
//new way to handle activePage
this.activePage = toPage;
// If we're navigating back in the URL history, set reverse accordingly.
settings.reverse = settings.reverse || historyDir < 0;
cssTransitionDeferred = $.Deferred();
this._cssTransition(toPage, fromPage, {
transition: settings.transition,
reverse: settings.reverse,
deferred: cssTransitionDeferred,
});
cssTransitionDeferred.done(
$.proxy(function (name, reverse, $to, $from, alreadyFocused) {
$.mobile.removeActiveLinkClass();
//if there's a duplicateCachedPage, remove it from the DOM now that it's hidden
if (settings.duplicateCachedPage) {
settings.duplicateCachedPage.remove();
}
// despite visibility: hidden addresses issue #2965
// https://github.com/jquery/jquery-mobile/issues/2965
if (!alreadyFocused) {
$.mobile.focusPage(toPage);
}
this._releaseTransitionLock();
this.element.trigger("pagechange", triggerData);
this._triggerWithDeprecated("transition", triggerData);
}, this),
);
},
// determine the current base url
_findBaseWithDefault: function () {
var closestBase =
this.activePage && $.mobile.getClosestBaseUrl(this.activePage);
return closestBase || $.mobile.path.documentBase.hrefNoHash;
},
});
// The following handlers should be bound after mobileinit has been triggered
// the following deferred is resolved in the init file
$.mobile.navreadyDeferred = $.Deferred();
//these variables make all page containers use the same queue and only navigate one at a time
// queue to hold simultanious page transitions
var pageTransitionQueue = [],
// indicates whether or not page is in process of transitioning
isPageTransitioning = false;
})(jQuery);
(function ($, undefined) {
// resolved on domready
var domreadyDeferred = $.Deferred(),
// resolved and nulled on window.load()
loadDeferred = $.Deferred(),
documentUrl = $.mobile.path.documentUrl,
// used to track last vclicked element to make sure its value is added to form data
$lastVClicked = null;
/* Event Bindings - hashchange, submit, and click */
function findClosestLink(ele) {
while (ele) {
// Look for the closest element with a nodeName of "a".
// Note that we are checking if we have a valid nodeName
// before attempting to access it. This is because the
// node we get called with could have originated from within
// an embedded SVG document where some symbol instance elements
// don't have nodeName defined on them, or strings are of type
// SVGAnimatedString.
if (
typeof ele.nodeName === "string" &&
ele.nodeName.toLowerCase() === "a"
) {
break;
}
ele = ele.parentNode;
}
return ele;
}
$.mobile.loadPage = function (url, opts) {
var container;
opts = opts || {};
container = opts.pageContainer || $.mobile.pageContainer;
// create the deferred that will be supplied to loadPage callers
// and resolved by the content widget's load method
opts.deferred = $.Deferred();
// Preferring to allow exceptions for uninitialized opts.pageContainer
// widgets so we know if we need to force init here for users
container.pagecontainer("load", url, opts);
// provide the deferred
return opts.deferred.promise();
};
//define vars for interal use
/* internal utility functions */
// NOTE Issue #4950 Android phonegap doesn't navigate back properly
// when a full page refresh has taken place. It appears that hashchange
// and replacestate history alterations work fine but we need to support
// both forms of history traversal in our code that uses backward history
// movement
$.mobile.back = function () {
var nav = window.navigator;
// if the setting is on and the navigator object is
// available use the phonegap navigation capability
if (
this.phonegapNavigationEnabled &&
nav &&
nav.app &&
nav.app.backHistory
) {
nav.app.backHistory();
} else {
$.mobile.pageContainer.pagecontainer("back");
}
};
// Direct focus to the page title, or otherwise first focusable element
$.mobile.focusPage = function (page) {
var autofocus = page.find("[autofocus]"),
pageTitle = page.find(".ui-title:eq(0)");
if (autofocus.length) {
autofocus.focus();
return;
}
if (pageTitle.length) {
pageTitle.focus();
} else {
page.focus();
}
};
// No-op implementation of transition degradation
$.mobile._maybeDegradeTransition =
$.mobile._maybeDegradeTransition ||
function (transition) {
return transition;
};
// Exposed $.mobile methods
$.mobile.changePage = function (to, options) {
$.mobile.pageContainer.pagecontainer("change", to, options);
};
$.mobile.changePage.defaults = {
transition: undefined,
reverse: false,
changeHash: true,
fromHashChange: false,
role: undefined, // By default we rely on the role defined by the @data-role attribute.
duplicateCachedPage: undefined,
pageContainer: undefined,
showLoadMsg: true, //loading message shows by default when pages are being fetched during changePage
dataUrl: undefined,
fromPage: undefined,
allowSamePageTransition: false,
};
$.mobile._registerInternalEvents = function () {
var getAjaxFormData = function ($form, calculateOnly) {
var url,
ret = true,
formData,
vclickedName,
method;
if (
!$.mobile.ajaxEnabled ||
// test that the form is, itself, ajax false
$form.is(":jqmData(ajax='false')") ||
// test that $.mobile.ignoreContentEnabled is set and
// the form or one of it's parents is ajax=false
!$form.jqmHijackable().length ||
$form.attr("target")
) {
return false;
}
url =
($lastVClicked && $lastVClicked.attr("formaction")) ||
$form.attr("action");
method = ($form.attr("method") || "get").toLowerCase();
// If no action is specified, browsers default to using the
// URL of the document containing the form. Since we dynamically
// pull in pages from external documents, the form should submit
// to the URL for the source document of the page containing
// the form.
if (!url) {
// Get the @data-url for the page containing the form.
url = $.mobile.getClosestBaseUrl($form);
// NOTE: If the method is "get", we need to strip off the query string
// because it will get replaced with the new form data. See issue #5710.
if (method === "get") {
url = $.mobile.path.parseUrl(url).hrefNoSearch;
}
if (url === $.mobile.path.documentBase.hrefNoHash) {
// The url we got back matches the document base,
// which means the page must be an internal/embedded page,
// so default to using the actual document url as a browser
// would.
url = documentUrl.hrefNoSearch;
}
}
url = $.mobile.path.makeUrlAbsolute(
url,
$.mobile.getClosestBaseUrl($form),
);
if (
$.mobile.path.isExternal(url) &&
!$.mobile.path.isPermittedCrossDomainRequest(documentUrl, url)
) {
return false;
}
if (!calculateOnly) {
formData = $form.serializeArray();
if ($lastVClicked && $lastVClicked[0].form === $form[0]) {
vclickedName = $lastVClicked.attr("name");
if (vclickedName) {
// Make sure the last clicked element is included in the form
$.each(formData, function (key, value) {
if (value.name === vclickedName) {
// Unset vclickedName - we've found it in the serialized data already
vclickedName = "";
return false;
}
});
if (vclickedName) {
formData.push({
name: vclickedName,
value: $lastVClicked.attr("value"),
});
}
}
}
ret = {
url: url,
options: {
type: method,
data: $.param(formData),
transition: $form.jqmData("transition"),
reverse: $form.jqmData("direction") === "reverse",
reloadPage: true,
},
};
}
return ret;
};
//bind to form submit events, handle with Ajax
$.mobile.document.delegate("form", "submit", function (event) {
var formData;
if (!event.isDefaultPrevented()) {
formData = getAjaxFormData($(this));
if (formData) {
$.mobile.changePage(formData.url, formData.options);
event.preventDefault();
}
}
});
//add active state on vclick
$.mobile.document.bind("vclick", function (event) {
var $btn,
btnEls,
target = event.target,
needClosest = false;
// if this isn't a left click we don't care. Its important to note
// that when the virtual event is generated it will create the which attr
if (event.which > 1 || !$.mobile.linkBindingEnabled) {
return;
}
// Record that this element was clicked, in case we need it for correct
// form submission during the "submit" handler above
$lastVClicked = $(target);
// Try to find a target element to which the active class will be applied
if ($.data(target, "mobile-button")) {
// If the form will not be submitted via AJAX, do not add active class
if (!getAjaxFormData($(target).closest("form"), true)) {
return;
}
// We will apply the active state to this button widget - the parent
// of the input that was clicked will have the associated data
if (target.parentNode) {
target = target.parentNode;
}
} else {
target = findClosestLink(target);
if (
!(
target &&
$.mobile.path.parseUrl(target.getAttribute("href") || "#")
.hash !== "#"
)
) {
return;
}
// TODO teach $.mobile.hijackable to operate on raw dom elements so the
// link wrapping can be avoided
if (!$(target).jqmHijackable().length) {
return;
}
}
// Avoid calling .closest by using the data set during .buttonMarkup()
// List items have the button data in the parent of the element clicked
if (!!~target.className.indexOf("ui-link-inherit")) {
if (target.parentNode) {
btnEls = $.data(target.parentNode, "buttonElements");
}
// Otherwise, look for the data on the target itself
} else {
btnEls = $.data(target, "buttonElements");
}
// If found, grab the button's outer element
if (btnEls) {
target = btnEls.outer;
} else {
needClosest = true;
}
$btn = $(target);
// If the outer element wasn't found by the our heuristics, use .closest()
if (needClosest) {
$btn = $btn.closest(".ui-btn");
}
if (
$btn.length > 0 &&
!$btn.hasClass(
"ui-state-disabled" ||
// DEPRECATED as of 1.4.0 - remove after 1.4.0 release
// only ui-state-disabled should be present thereafter
$btn.hasClass("ui-disabled"),
)
) {
$.mobile.removeActiveLinkClass(true);
$.mobile.activeClickedLink = $btn;
$.mobile.activeClickedLink.addClass($.mobile.activeBtnClass);
}
});
// click routing - direct to HTTP or Ajax, accordingly
$.mobile.document.bind("click", function (event) {
if (!$.mobile.linkBindingEnabled || event.isDefaultPrevented()) {
return;
}
var link = findClosestLink(event.target),
$link = $(link),
//remove active link class if external (then it won't be there if you come back)
httpCleanup = function () {
window.setTimeout(function () {
$.mobile.removeActiveLinkClass(true);
}, 200);
},
baseUrl,
href,
useDefaultUrlHandling,
isExternal,
transition,
reverse,
role;
// If a button was clicked, clean up the active class added by vclick above
if (
$.mobile.activeClickedLink &&
$.mobile.activeClickedLink[0] === event.target.parentNode
) {
httpCleanup();
}
// If there is no link associated with the click or its not a left
// click we want to ignore the click
// TODO teach $.mobile.hijackable to operate on raw dom elements so the link wrapping
// can be avoided
if (!link || event.which > 1 || !$link.jqmHijackable().length) {
return;
}
//if there's a data-rel=back attr, go back in history
if ($link.is(":jqmData(rel='back')")) {
$.mobile.back();
return false;
}
baseUrl = $.mobile.getClosestBaseUrl($link);
//get href, if defined, otherwise default to empty hash
href = $.mobile.path.makeUrlAbsolute(
$link.attr("href") || "#",
baseUrl,
);
//if ajax is disabled, exit early
if (!$.mobile.ajaxEnabled && !$.mobile.path.isEmbeddedPage(href)) {
httpCleanup();
//use default click handling
return;
}
// XXX_jblas: Ideally links to application pages should be specified as
// an url to the application document with a hash that is either
// the site relative path or id to the page. But some of the
// internal code that dynamically generates sub-pages for nested
// lists and select dialogs, just write a hash in the link they
// create. This means the actual URL path is based on whatever
// the current value of the base tag is at the time this code
// is called. For now we are just assuming that any url with a
// hash in it is an application page reference.
if (href.search("#") !== -1) {
href = href.replace(/[^#]*#/, "");
if (!href) {
//link was an empty hash meant purely
//for interaction, so we ignore it.
event.preventDefault();
return;
} else if ($.mobile.path.isPath(href)) {
//we have apath so make it the href we want to load.
href = $.mobile.path.makeUrlAbsolute(href, baseUrl);
} else {
//we have a simple id so use the documentUrl as its base.
href = $.mobile.path.makeUrlAbsolute(
"#" + href,
documentUrl.hrefNoHash,
);
}
}
// Should we handle this link, or let the browser deal with it?
useDefaultUrlHandling =
$link.is("[rel='external']") ||
$link.is(":jqmData(ajax='false')") ||
$link.is("[target]");
// Some embedded browsers, like the web view in Phone Gap, allow cross-domain XHR
// requests if the document doing the request was loaded via the file:// protocol.
// This is usually to allow the application to "phone home" and fetch app specific
// data. We normally let the browser handle external/cross-domain urls, but if the
// allowCrossDomainPages option is true, we will allow cross-domain http/https
// requests to go through our page loading logic.
//check for protocol or rel and its not an embedded page
//TODO overlap in logic from isExternal, rel=external check should be
// moved into more comprehensive isExternalLink
isExternal =
useDefaultUrlHandling ||
($.mobile.path.isExternal(href) &&
!$.mobile.path.isPermittedCrossDomainRequest(documentUrl, href));
if (isExternal) {
httpCleanup();
//use default click handling
return;
}
//use ajax
transition = $link.jqmData("transition");
reverse =
$link.jqmData("direction") === "reverse" ||
// deprecated - remove by 1.0
$link.jqmData("back");
//this may need to be more specific as we use data-rel more
role = $link.attr("data-" + $.mobile.ns + "rel") || undefined;
$.mobile.changePage(href, {
transition: transition,
reverse: reverse,
role: role,
link: $link,
});
event.preventDefault();
});
//prefetch pages when anchors with data-prefetch are encountered
$.mobile.document.delegate(".ui-page", "pageshow.prefetch", function () {
var urls = [];
$(this)
.find("a:jqmData(prefetch)")
.each(function () {
var $link = $(this),
url = $link.attr("href");
if (url && $.inArray(url, urls) === -1) {
urls.push(url);
$.mobile.loadPage(url, {
role: $link.attr("data-" + $.mobile.ns + "rel"),
prefetch: true,
});
}
});
});
// TODO ensure that the navigate binding in the content widget happens at the right time
$.mobile.pageContainer.pagecontainer();
//set page min-heights to be device specific
$.mobile.document.bind("pageshow", function () {
// We need to wait for window.load to make sure that styles have already been rendered,
// otherwise heights of external toolbars will have the wrong value
if (loadDeferred) {
loadDeferred.done($.mobile.resetActivePageHeight);
} else {
$.mobile.resetActivePageHeight();
}
});
$.mobile.window.bind("throttledresize", $.mobile.resetActivePageHeight);
}; //navreadyDeferred done callback
$(function () {
domreadyDeferred.resolve();
});
$.mobile.window.load(function () {
// Resolve and null the deferred
loadDeferred.resolve();
loadDeferred = null;
});
$.when(domreadyDeferred, $.mobile.navreadyDeferred).done(function () {
$.mobile._registerInternalEvents();
});
})(jQuery);
(function ($, window, undefined) {
// TODO remove direct references to $.mobile and properties, we should
// favor injection with params to the constructor
$.mobile.Transition = function () {
this.init.apply(this, arguments);
};
$.extend($.mobile.Transition.prototype, {
toPreClass: " ui-page-pre-in",
init: function (name, reverse, $to, $from) {
$.extend(this, {
name: name,
reverse: reverse,
$to: $to,
$from: $from,
deferred: new $.Deferred(),
});
},
cleanFrom: function () {
this.$from
.removeClass(
$.mobile.activePageClass + " out in reverse " + this.name,
)
.height("");
},
// NOTE overridden by child object prototypes, noop'd here as defaults
beforeDoneIn: function () {},
beforeDoneOut: function () {},
beforeStartOut: function () {},
doneIn: function () {
this.beforeDoneIn();
this.$to.removeClass("out in reverse " + this.name).height("");
this.toggleViewportClass();
// In some browsers (iOS5), 3D transitions block the ability to scroll to the desired location during transition
// This ensures we jump to that spot after the fact, if we aren't there already.
if ($.mobile.window.scrollTop() !== this.toScroll) {
this.scrollPage();
}
if (!this.sequential) {
this.$to.addClass($.mobile.activePageClass);
}
this.deferred.resolve(
this.name,
this.reverse,
this.$to,
this.$from,
true,
);
},
doneOut: function (screenHeight, reverseClass, none, preventFocus) {
this.beforeDoneOut();
this.startIn(screenHeight, reverseClass, none, preventFocus);
},
hideIn: function (callback) {
// Prevent flickering in phonegap container: see comments at #4024 regarding iOS
this.$to.css("z-index", -10);
callback.call(this);
this.$to.css("z-index", "");
},
scrollPage: function () {
// By using scrollTo instead of silentScroll, we can keep things better in order
// Just to be precautios, disable scrollstart listening like silentScroll would
$.event.special.scrollstart.enabled = false;
//if we are hiding the url bar or the page was previously scrolled scroll to hide or return to position
if (
$.mobile.hideUrlBar ||
this.toScroll !== $.mobile.defaultHomeScroll
) {
window.scrollTo(0, this.toScroll);
}
// reenable scrollstart listening like silentScroll would
setTimeout(function () {
$.event.special.scrollstart.enabled = true;
}, 150);
},
startIn: function (screenHeight, reverseClass, none, preventFocus) {
this.hideIn(function () {
this.$to.addClass($.mobile.activePageClass + this.toPreClass);
// Send focus to page as it is now display: block
if (!preventFocus) {
$.mobile.focusPage(this.$to);
}
// Set to page height
this.$to.height(screenHeight + this.toScroll);
if (!none) {
this.scrollPage();
}
});
this.$to
.removeClass(this.toPreClass)
.addClass(this.name + " in " + reverseClass);
if (!none) {
this.$to.animationComplete(
$.proxy(function () {
this.doneIn();
}, this),
);
} else {
this.doneIn();
}
},
startOut: function (screenHeight, reverseClass, none) {
this.beforeStartOut(screenHeight, reverseClass, none);
// Set the from page's height and start it transitioning out
// Note: setting an explicit height helps eliminate tiling in the transitions
this.$from
.height(screenHeight + $.mobile.window.scrollTop())
.addClass(this.name + " out" + reverseClass);
},
toggleViewportClass: function () {
$.mobile.pageContainer.toggleClass(
"ui-mobile-viewport-transitioning viewport-" + this.name,
);
},
transition: function () {
// NOTE many of these could be calculated/recorded in the constructor, it's my
// opinion that binding them as late as possible has value with regards to
// better transitions with fewer bugs. Ie, it's not guaranteed that the
// object will be created and transition will be run immediately after as
// it is today. So we wait until transition is invoked to gather the following
var none,
reverseClass = this.reverse ? " reverse" : "",
screenHeight = $.mobile.getScreenHeight(),
maxTransitionOverride =
$.mobile.maxTransitionWidth !== false &&
$.mobile.window.width() > $.mobile.maxTransitionWidth;
this.toScroll =
$.mobile.navigate.history.getActive().lastScroll ||
$.mobile.defaultHomeScroll;
none =
!$.support.cssTransitions ||
!$.support.cssAnimations ||
maxTransitionOverride ||
!this.name ||
this.name === "none" ||
Math.max($.mobile.window.scrollTop(), this.toScroll) >
$.mobile.getMaxScrollForTransition();
this.toggleViewportClass();
if (this.$from && !none) {
this.startOut(screenHeight, reverseClass, none);
} else {
this.doneOut(screenHeight, reverseClass, none, true);
}
return this.deferred.promise();
},
});
})(jQuery, this);
(function ($) {
$.mobile.SerialTransition = function () {
this.init.apply(this, arguments);
};
$.extend(
$.mobile.SerialTransition.prototype,
$.mobile.Transition.prototype,
{
sequential: true,
beforeDoneOut: function () {
if (this.$from) {
this.cleanFrom();
}
},
beforeStartOut: function (screenHeight, reverseClass, none) {
this.$from.animationComplete(
$.proxy(function () {
this.doneOut(screenHeight, reverseClass, none);
}, this),
);
},
},
);
})(jQuery);
(function ($) {
$.mobile.ConcurrentTransition = function () {
this.init.apply(this, arguments);
};
$.extend(
$.mobile.ConcurrentTransition.prototype,
$.mobile.Transition.prototype,
{
sequential: false,
beforeDoneIn: function () {
if (this.$from) {
this.cleanFrom();
}
},
beforeStartOut: function (screenHeight, reverseClass, none) {
this.doneOut(screenHeight, reverseClass, none);
},
},
);
})(jQuery);
(function ($) {
// generate the handlers from the above
var defaultGetMaxScrollForTransition = function () {
return $.mobile.getScreenHeight() * 3;
};
//transition handler dictionary for 3rd party transitions
$.mobile.transitionHandlers = {
sequential: $.mobile.SerialTransition,
simultaneous: $.mobile.ConcurrentTransition,
};
// Make our transition handler the public default.
$.mobile.defaultTransitionHandler = $.mobile.transitionHandlers.sequential;
$.mobile.transitionFallbacks = {};
// If transition is defined, check if css 3D transforms are supported, and if not, if a fallback is specified
$.mobile._maybeDegradeTransition = function (transition) {
if (
transition &&
!$.support.cssTransform3d &&
$.mobile.transitionFallbacks[transition]
) {
transition = $.mobile.transitionFallbacks[transition];
}
return transition;
};
// Set the getMaxScrollForTransition to default if no implementation was set by user
$.mobile.getMaxScrollForTransition =
$.mobile.getMaxScrollForTransition || defaultGetMaxScrollForTransition;
})(jQuery);
/*
* fallback transition for flip in non-3D supporting browsers (which tend to handle complex transitions poorly in general
*/
(function ($, window, undefined) {
$.mobile.transitionFallbacks.flip = "fade";
})(jQuery, this);
/*
* fallback transition for flow in non-3D supporting browsers (which tend to handle complex transitions poorly in general
*/
(function ($, window, undefined) {
$.mobile.transitionFallbacks.flow = "fade";
})(jQuery, this);
/*
* fallback transition for pop in non-3D supporting browsers (which tend to handle complex transitions poorly in general
*/
(function ($, window, undefined) {
$.mobile.transitionFallbacks.pop = "fade";
})(jQuery, this);
/*
* fallback transition for slide in non-3D supporting browsers (which tend to handle complex transitions poorly in general
*/
(function ($, window, undefined) {
// Use the simultaneous transitions handler for slide transitions
$.mobile.transitionHandlers.slide =
$.mobile.transitionHandlers.simultaneous;
// Set the slide transitions's fallback to "fade"
$.mobile.transitionFallbacks.slide = "fade";
})(jQuery, this);
/*
* fallback transition for slidedown in non-3D supporting browsers (which tend to handle complex transitions poorly in general
*/
(function ($, window, undefined) {
$.mobile.transitionFallbacks.slidedown = "fade";
})(jQuery, this);
/*
* fallback transition for slidefade in non-3D supporting browsers (which tend to handle complex transitions poorly in general
*/
(function ($, window, undefined) {
// Set the slide transitions's fallback to "fade"
$.mobile.transitionFallbacks.slidefade = "fade";
})(jQuery, this);
/*
* fallback transition for slideup in non-3D supporting browsers (which tend to handle complex transitions poorly in general
*/
(function ($, window, undefined) {
$.mobile.transitionFallbacks.slideup = "fade";
})(jQuery, this);
/*
* fallback transition for turn in non-3D supporting browsers (which tend to handle complex transitions poorly in general
*/
(function ($, window, undefined) {
$.mobile.transitionFallbacks.turn = "fade";
})(jQuery, this);
(function ($, undefined) {
$.mobile.degradeInputs = {
color: false,
date: false,
datetime: false,
"datetime-local": false,
email: false,
month: false,
number: false,
range: "number",
search: "text",
tel: false,
time: false,
url: false,
week: false,
};
// Backcompat remove in 1.5
$.mobile.page.prototype.options.degradeInputs = $.mobile.degradeInputs;
// Auto self-init widgets
$.mobile.degradeInputsWithin = function (target) {
target = $(target);
// Degrade inputs to avoid poorly implemented native functionality
target
.find("input")
.not($.mobile.page.prototype.keepNativeSelector())
.each(function () {
var element = $(this),
type = this.getAttribute("type"),
optType = $.mobile.degradeInputs[type] || "text",
html,
hasType,
findstr,
repstr;
if ($.mobile.degradeInputs[type]) {
html = $("<div>").html(element.clone()).html();
// In IE browsers, the type sometimes doesn't exist in the cloned markup, so we replace the closing tag instead
hasType = html.indexOf(" type=") > -1;
findstr = hasType ? /\s+type=["']?\w+['"]?/ : /\/?>/;
repstr =
' type="' +
optType +
'" data-' +
$.mobile.ns +
'type="' +
type +
'"' +
(hasType ? "" : ">");
element.replaceWith(html.replace(findstr, repstr));
}
});
};
})(jQuery);
(function ($, window, undefined) {
$.widget("mobile.page", $.mobile.page, {
options: {
// Accepts left, right and none
closeBtn: "left",
closeBtnText: "Close",
overlayTheme: "a",
corners: true,
dialog: false,
},
_create: function () {
this._super();
if (this.options.dialog) {
$.extend(this, {
_inner: this.element.children(),
_headerCloseButton: null,
});
if (!this.options.enhanced) {
this._setCloseBtn(this.options.closeBtn);
}
}
},
_enhance: function () {
this._super();
// Class the markup for dialog styling and wrap interior
if (this.options.dialog) {
this.element.addClass("ui-dialog").wrapInner(
$("<div/>", {
// ARIA role
role: "dialog",
class:
"ui-dialog-contain ui-overlay-shadow" +
(this.options.corners ? " ui-corner-all" : ""),
}),
);
}
},
_setOptions: function (options) {
var closeButtonLocation,
closeButtonText,
currentOpts = this.options;
if (options.corners !== undefined) {
this._inner.toggleClass("ui-corner-all", !!options.corners);
}
if (options.overlayTheme !== undefined) {
if ($.mobile.activePage[0] === this.element[0]) {
currentOpts.overlayTheme = options.overlayTheme;
this._handlePageBeforeShow();
}
}
if (options.closeBtnText !== undefined) {
closeButtonLocation = currentOpts.closeBtn;
closeButtonText = options.closeBtnText;
}
if (options.closeBtn !== undefined) {
closeButtonLocation = options.closeBtn;
}
if (closeButtonLocation) {
this._setCloseBtn(closeButtonLocation, closeButtonText);
}
this._super(options);
},
_handlePageBeforeShow: function () {
if (this.options.overlayTheme && this.options.dialog) {
this.removeContainerBackground();
this.setContainerBackground(this.options.overlayTheme);
} else {
this._super();
}
},
_setCloseBtn: function (location, text) {
var dst,
btn = this._headerCloseButton;
// Sanitize value
location =
"left" === location
? "left"
: "right" === location
? "right"
: "none";
if ("none" === location) {
if (btn) {
btn.remove();
btn = null;
}
} else if (btn) {
btn
.removeClass("ui-btn-left ui-btn-right")
.addClass("ui-btn-" + location);
if (text) {
btn.text(text);
}
} else {
dst = this._inner.find(":jqmData(role='header')").first();
btn = $("<a></a>", {
href: "#",
class:
"ui-btn ui-corner-all ui-icon-delete ui-btn-icon-notext ui-btn-" +
location,
})
.attr("data-" + $.mobile.ns + "rel", "back")
.text(text || this.options.closeBtnText || "")
.prependTo(dst);
}
this._headerCloseButton = btn;
},
});
})(jQuery, this);
(function ($, window, undefined) {
$.widget("mobile.dialog", {
options: {
// Accepts left, right and none
closeBtn: "left",
closeBtnText: "Close",
overlayTheme: "a",
corners: true,
},
// Override the theme set by the page plugin on pageshow
_handlePageBeforeShow: function () {
this._isCloseable = true;
if (this.options.overlayTheme) {
this.element
.page("removeContainerBackground")
.page("setContainerBackground", this.options.overlayTheme);
}
},
_handlePageBeforeHide: function () {
this._isCloseable = false;
},
// click and submit events:
// - clicks and submits should use the closing transition that the dialog
// opened with unless a data-transition is specified on the link/form
// - if the click was on the close button, or the link has a data-rel="back"
// it'll go back in history naturally
_handleVClickSubmit: function (event) {
var attrs,
$target = $(event.target).closest(
event.type === "vclick" ? "a" : "form",
);
if ($target.length && !$target.jqmData("transition")) {
attrs = {};
attrs["data-" + $.mobile.ns + "transition"] =
($.mobile.navigate.history.getActive() || {})["transition"] ||
$.mobile.defaultDialogTransition;
attrs["data-" + $.mobile.ns + "direction"] = "reverse";
$target.attr(attrs);
}
},
_create: function () {
var elem = this.element,
opts = this.options;
// Class the markup for dialog styling and wrap interior
elem.addClass("ui-dialog").wrapInner(
$("<div/>", {
// ARIA role
role: "dialog",
class:
"ui-dialog-contain ui-overlay-shadow" +
(!!opts.corners ? " ui-corner-all" : ""),
}),
);
$.extend(this, {
_isCloseable: false,
_inner: elem.children(),
_headerCloseButton: null,
});
this._on(elem, {
vclick: "_handleVClickSubmit",
submit: "_handleVClickSubmit",
pagebeforeshow: "_handlePageBeforeShow",
pagebeforehide: "_handlePageBeforeHide",
});
this._setCloseBtn(opts.closeBtn);
},
_setOptions: function (options) {
var closeButtonLocation,
closeButtonText,
currentOpts = this.options;
if (options.corners !== undefined) {
this._inner.toggleClass("ui-corner-all", !!options.corners);
}
if (options.overlayTheme !== undefined) {
if ($.mobile.activePage[0] === this.element[0]) {
currentOpts.overlayTheme = options.overlayTheme;
this._handlePageBeforeShow();
}
}
if (options.closeBtnText !== undefined) {
closeButtonLocation = currentOpts.closeBtn;
closeButtonText = options.closeBtnText;
}
if (options.closeBtn !== undefined) {
closeButtonLocation = options.closeBtn;
}
if (closeButtonLocation) {
this._setCloseBtn(closeButtonLocation, closeButtonText);
}
this._super(options);
},
_setCloseBtn: function (location, text) {
var dst,
btn = this._headerCloseButton;
// Sanitize value
location =
"left" === location
? "left"
: "right" === location
? "right"
: "none";
if ("none" === location) {
if (btn) {
btn.remove();
btn = null;
}
} else if (btn) {
btn
.removeClass("ui-btn-left ui-btn-right")
.addClass("ui-btn-" + location);
if (text) {
btn.text(text);
}
} else {
dst = this._inner.find(":jqmData(role='header')").first();
btn = $("<a></a>", {
role: "button",
href: "#",
class:
"ui-btn ui-corner-all ui-icon-delete ui-btn-icon-notext ui-btn-" +
location,
})
.text(text || this.options.closeBtnText || "")
.prependTo(dst);
this._on(btn, { click: "close" });
}
this._headerCloseButton = btn;
},
// Close method goes back in history
close: function () {
var hist = $.mobile.navigate.history;
if (this._isCloseable) {
this._isCloseable = false;
// If the hash listening is enabled and there is at least one preceding history
// entry it's ok to go back. Initial pages with the dialog hash state are an example
// where the stack check is necessary
if ($.mobile.hashListeningEnabled && hist.activeIndex > 0) {
$.mobile.back();
} else {
$.mobile.pageContainer.pagecontainer("back");
}
}
},
});
})(jQuery, this);
(function ($, undefined) {
var rInitialLetter = /([A-Z])/g,
// Construct iconpos class from iconpos value
iconposClass = function (iconpos) {
return "ui-btn-icon-" + (iconpos === null ? "left" : iconpos);
};
$.widget("mobile.collapsible", {
options: {
enhanced: false,
expandCueText: null,
collapseCueText: null,
collapsed: true,
heading: "h1,h2,h3,h4,h5,h6,legend",
collapsedIcon: null,
expandedIcon: null,
iconpos: null,
theme: null,
contentTheme: null,
inset: null,
corners: null,
mini: null,
},
_create: function () {
var elem = this.element,
ui = {
accordion: elem
.closest(
":jqmData(role='collapsible-set')," +
":jqmData(role='collapsibleset')" +
($.mobile.collapsibleset ? ", :mobile-collapsibleset" : ""),
)
.addClass("ui-collapsible-set"),
};
this._ui = ui;
this._renderedOptions = this._getOptions(this.options);
if (this.options.enhanced) {
ui.heading = $(".ui-collapsible-heading", this.element[0]);
ui.content = ui.heading.next();
ui.anchor = $("a", ui.heading[0]).first();
ui.status = ui.anchor.children(".ui-collapsible-heading-status");
} else {
this._enhance(elem, ui);
}
this._on(ui.heading, {
tap: function () {
ui.heading.find("a").first().addClass($.mobile.activeBtnClass);
},
click: function (event) {
this._handleExpandCollapse(
!ui.heading.hasClass("ui-collapsible-heading-collapsed"),
);
event.preventDefault();
event.stopPropagation();
},
});
},
// Adjust the keys inside options for inherited values
_getOptions: function (options) {
var key,
accordion = this._ui.accordion,
accordionWidget = this._ui.accordionWidget;
// Copy options
options = $.extend({}, options);
if (accordion.length && !accordionWidget) {
this._ui.accordionWidget = accordionWidget = accordion.data(
"mobile-collapsibleset",
);
}
for (key in options) {
// Retrieve the option value first from the options object passed in and, if
// null, from the parent accordion or, if that's null too, or if there's no
// parent accordion, then from the defaults.
options[key] =
options[key] != null
? options[key]
: accordionWidget
? accordionWidget.options[key]
: accordion.length
? $.mobile.getAttribute(
accordion[0],
key.replace(rInitialLetter, "-$1").toLowerCase(),
)
: null;
if (null == options[key]) {
options[key] = $.mobile.collapsible.defaults[key];
}
}
return options;
},
_themeClassFromOption: function (prefix, value) {
return value ? (value === "none" ? "" : prefix + value) : "";
},
_enhance: function (elem, ui) {
var iconclass,
opts = this._renderedOptions,
contentThemeClass = this._themeClassFromOption(
"ui-body-",
opts.contentTheme,
);
elem.addClass(
"ui-collapsible " +
(opts.inset ? "ui-collapsible-inset " : "") +
(opts.inset && opts.corners ? "ui-corner-all " : "") +
(contentThemeClass ? "ui-collapsible-themed-content " : ""),
);
(ui.originalHeading = elem.children(this.options.heading).first()),
(ui.content = elem
.wrapInner(
"<div " +
"class='ui-collapsible-content " +
contentThemeClass +
"'></div>",
)
.children(".ui-collapsible-content")),
(ui.heading = ui.originalHeading);
// Replace collapsibleHeading if it's a legend
if (ui.heading.is("legend")) {
ui.heading = $("<div role='heading'>" + ui.heading.html() + "</div>");
ui.placeholder = $(
"<div><!-- placeholder for legend --></div>",
).insertBefore(ui.originalHeading);
ui.originalHeading.remove();
}
iconclass = opts.collapsed
? opts.collapsedIcon
? "ui-icon-" + opts.collapsedIcon
: ""
: opts.expandedIcon
? "ui-icon-" + opts.expandedIcon
: "";
ui.status = $("<span class='ui-collapsible-heading-status'></span>");
ui.anchor = ui.heading
.detach()
//modify markup & attributes
.addClass("ui-collapsible-heading")
.append(ui.status)
.wrapInner("<a href='#' class='ui-collapsible-heading-toggle'></a>")
.find("a")
.first()
.addClass(
"ui-btn " +
(iconclass ? iconclass + " " : "") +
(iconclass ? iconposClass(opts.iconpos) + " " : "") +
this._themeClassFromOption("ui-btn-", opts.theme) +
" " +
(opts.mini ? "ui-mini " : ""),
);
//drop heading in before content
ui.heading.insertBefore(ui.content);
this._handleExpandCollapse(this.options.collapsed);
return ui;
},
refresh: function () {
this._applyOptions(this.options);
this._renderedOptions = this._getOptions(this.options);
},
_applyOptions: function (options) {
var isCollapsed,
newTheme,
oldTheme,
hasCorners,
hasIcon,
elem = this.element,
currentOpts = this._renderedOptions,
ui = this._ui,
anchor = ui.anchor,
status = ui.status,
opts = this._getOptions(options);
// First and foremost we need to make sure the collapsible is in the proper
// state, in case somebody decided to change the collapsed option at the
// same time as another option
if (options.collapsed !== undefined) {
this._handleExpandCollapse(options.collapsed);
}
isCollapsed = elem.hasClass("ui-collapsible-collapsed");
// We only need to apply the cue text for the current state right away.
// The cue text for the alternate state will be stored in the options
// and applied the next time the collapsible's state is toggled
if (isCollapsed) {
if (opts.expandCueText !== undefined) {
status.text(opts.expandCueText);
}
} else {
if (opts.collapseCueText !== undefined) {
status.text(opts.collapseCueText);
}
}
// Update icon
// Is it supposed to have an icon?
hasIcon =
// If the collapsedIcon is being set, consult that
opts.collapsedIcon !== undefined
? opts.collapsedIcon !== false
: // Otherwise consult the existing option value
currentOpts.collapsedIcon !== false;
// If any icon-related options have changed, make sure the new icon
// state is reflected by first removing all icon-related classes
// reflecting the current state and then adding all icon-related
// classes for the new state
if (
!(
opts.iconpos === undefined &&
opts.collapsedIcon === undefined &&
opts.expandedIcon === undefined
)
) {
// Remove all current icon-related classes
anchor.removeClass(
[iconposClass(currentOpts.iconpos)]
.concat(
currentOpts.expandedIcon
? ["ui-icon-" + currentOpts.expandedIcon]
: [],
)
.concat(
currentOpts.collapsedIcon
? ["ui-icon-" + currentOpts.collapsedIcon]
: [],
)
.join(" "),
);
// Add new classes if an icon is supposed to be present
if (hasIcon) {
anchor.addClass(
[
iconposClass(
opts.iconpos !== undefined
? opts.iconpos
: currentOpts.iconpos,
),
]
.concat(
isCollapsed
? [
"ui-icon-" +
(opts.collapsedIcon !== undefined
? opts.collapsedIcon
: currentOpts.collapsedIcon),
]
: [
"ui-icon-" +
(opts.expandedIcon !== undefined
? opts.expandedIcon
: currentOpts.expandedIcon),
],
)
.join(" "),
);
}
}
if (opts.theme !== undefined) {
oldTheme = this._themeClassFromOption("ui-btn-", currentOpts.theme);
newTheme = this._themeClassFromOption("ui-btn-", opts.theme);
anchor.removeClass(oldTheme).addClass(newTheme);
}
if (opts.contentTheme !== undefined) {
oldTheme = this._themeClassFromOption(
"ui-body-",
currentOpts.contentTheme,
);
newTheme = this._themeClassFromOption("ui-body-", opts.contentTheme);
ui.content.removeClass(oldTheme).addClass(newTheme);
}
if (opts.inset !== undefined) {
elem.toggleClass("ui-collapsible-inset", opts.inset);
hasCorners = !!(opts.inset && (opts.corners || currentOpts.corners));
}
if (opts.corners !== undefined) {
hasCorners = !!(opts.corners && (opts.inset || currentOpts.inset));
}
if (hasCorners !== undefined) {
elem.toggleClass("ui-corner-all", hasCorners);
}
if (opts.mini !== undefined) {
anchor.toggleClass("ui-mini", opts.mini);
}
},
_setOptions: function (options) {
this._applyOptions(options);
this._super(options);
this._renderedOptions = this._getOptions(this.options);
},
_handleExpandCollapse: function (isCollapse) {
var opts = this._renderedOptions,
ui = this._ui;
ui.status.text(isCollapse ? opts.expandCueText : opts.collapseCueText);
ui.heading
.toggleClass("ui-collapsible-heading-collapsed", isCollapse)
.find("a")
.first()
.toggleClass("ui-icon-" + opts.expandedIcon, !isCollapse)
// logic or cause same icon for expanded/collapsed state would remove the ui-icon-class
.toggleClass(
"ui-icon-" + opts.collapsedIcon,
isCollapse || opts.expandedIcon === opts.collapsedIcon,
)
.removeClass($.mobile.activeBtnClass);
this.element.toggleClass("ui-collapsible-collapsed", isCollapse);
ui.content
.toggleClass("ui-collapsible-content-collapsed", isCollapse)
.attr("aria-hidden", isCollapse)
.trigger("updatelayout");
this.options.collapsed = isCollapse;
this._trigger(isCollapse ? "collapse" : "expand");
},
expand: function () {
this._handleExpandCollapse(false);
},
collapse: function () {
this._handleExpandCollapse(true);
},
_destroy: function () {
var ui = this._ui,
opts = this.options;
if (opts.enhanced) {
return;
}
if (ui.placeholder) {
ui.originalHeading.insertBefore(ui.placeholder);
ui.placeholder.remove();
ui.heading.remove();
} else {
ui.status.remove();
ui.heading
.removeClass(
"ui-collapsible-heading ui-collapsible-heading-collapsed",
)
.children()
.contents()
.unwrap();
}
ui.anchor.contents().unwrap();
ui.content.contents().unwrap();
this.element.removeClass(
"ui-collapsible ui-collapsible-collapsed " +
"ui-collapsible-themed-content ui-collapsible-inset ui-corner-all",
);
},
});
// Defaults to be used by all instances of collapsible if per-instance values
// are unset or if nothing is specified by way of inheritance from an accordion.
// Note that this hash does not contain options "collapsed" or "heading",
// because those are not inheritable.
$.mobile.collapsible.defaults = {
expandCueText: " click to expand contents",
collapseCueText: " click to collapse contents",
collapsedIcon: "plus",
contentTheme: "inherit",
expandedIcon: "minus",
iconpos: "left",
inset: true,
corners: true,
theme: "inherit",
mini: false,
};
})(jQuery);
(function ($, undefined) {
$.mobile.behaviors.addFirstLastClasses = {
_getVisibles: function ($els, create) {
var visibles;
if (create) {
visibles = $els.not(".ui-screen-hidden");
} else {
visibles = $els.filter(":visible");
if (visibles.length === 0) {
visibles = $els.not(".ui-screen-hidden");
}
}
return visibles;
},
_addFirstLastClasses: function ($els, $visibles, create) {
$els.removeClass("ui-first-child ui-last-child");
$visibles
.eq(0)
.addClass("ui-first-child")
.end()
.last()
.addClass("ui-last-child");
if (!create) {
this.element.trigger("updatelayout");
}
},
_removeFirstLastClasses: function ($els) {
$els.removeClass("ui-first-child ui-last-child");
},
};
})(jQuery);
(function ($, undefined) {
var childCollapsiblesSelector =
":mobile-collapsible, " + $.mobile.collapsible.initSelector;
$.widget(
"mobile.collapsibleset",
$.extend(
{
// The initSelector is deprecated as of 1.4.0. In 1.5.0 we will use
// :jqmData(role='collapsibleset') which will allow us to get rid of the line
// below altogether, because the autoinit will generate such an initSelector
initSelector:
":jqmData(role='collapsible-set'),:jqmData(role='collapsibleset')",
options: $.extend(
{
enhanced: false,
},
$.mobile.collapsible.defaults,
),
_handleCollapsibleExpand: function (event) {
var closestCollapsible = $(event.target).closest(".ui-collapsible");
if (
closestCollapsible
.parent()
.is(":mobile-collapsibleset, :jqmData(role='collapsible-set')")
) {
closestCollapsible
.siblings(".ui-collapsible:not(.ui-collapsible-collapsed)")
.collapsible("collapse");
}
},
_create: function () {
var elem = this.element,
opts = this.options;
$.extend(this, {
_classes: "",
});
if (!opts.enhanced) {
elem.addClass(
"ui-collapsible-set " +
this._themeClassFromOption("ui-group-theme-", opts.theme) +
" " +
(opts.corners && opts.inset ? "ui-corner-all " : ""),
);
this.element
.find($.mobile.collapsible.initSelector)
.collapsible();
}
this._on(elem, { collapsibleexpand: "_handleCollapsibleExpand" });
},
_themeClassFromOption: function (prefix, value) {
return value ? (value === "none" ? "" : prefix + value) : "";
},
_init: function () {
this._refresh(true);
// Because the corners are handled by the collapsible itself and the default state is collapsed
// That was causing https://github.com/jquery/jquery-mobile/issues/4116
this.element
.children(childCollapsiblesSelector)
.filter(":jqmData(collapsed='false')")
.collapsible("expand");
},
_setOptions: function (options) {
var ret,
hasCorners,
elem = this.element,
themeClass = this._themeClassFromOption(
"ui-group-theme-",
options.theme,
);
if (themeClass) {
elem
.removeClass(
this._themeClassFromOption(
"ui-group-theme-",
this.options.theme,
),
)
.addClass(themeClass);
}
if (options.inset !== undefined) {
hasCorners = !!(
options.inset &&
(options.corners || this.options.corners)
);
}
if (options.corners !== undefined) {
hasCorners = !!(
options.corners &&
(options.inset || this.options.inset)
);
}
if (hasCorners !== undefined) {
elem.toggleClass("ui-corner-all", hasCorners);
}
ret = this._super(options);
this.element.children(":mobile-collapsible").collapsible("refresh");
return ret;
},
_destroy: function () {
var el = this.element;
this._removeFirstLastClasses(
el.children(childCollapsiblesSelector),
);
el.removeClass(
"ui-collapsible-set ui-corner-all " +
this._themeClassFromOption(
"ui-group-theme-",
this.options.theme,
),
)
.children(":mobile-collapsible")
.collapsible("destroy");
},
_refresh: function (create) {
var collapsiblesInSet = this.element.children(
childCollapsiblesSelector,
);
this.element
.find($.mobile.collapsible.initSelector)
.not(".ui-collapsible")
.collapsible();
this._addFirstLastClasses(
collapsiblesInSet,
this._getVisibles(collapsiblesInSet, create),
create,
);
},
refresh: function () {
this._refresh(false);
},
},
$.mobile.behaviors.addFirstLastClasses,
),
);
})(jQuery);
(function ($, undefined) {
// Deprecated in 1.4
$.fn.fieldcontain = function (/* options */) {
return this.addClass("ui-field-contain");
};
})(jQuery);
(function ($, undefined) {
$.fn.grid = function (options) {
return this.each(function () {
var $this = $(this),
o = $.extend(
{
grid: null,
},
options,
),
$kids = $this.children(),
gridCols = { solo: 1, a: 2, b: 3, c: 4, d: 5 },
grid = o.grid,
iterator,
letter;
if (!grid) {
if ($kids.length <= 5) {
for (letter in gridCols) {
if (gridCols[letter] === $kids.length) {
grid = letter;
}
}
} else {
grid = "a";
$this.addClass("ui-grid-duo");
}
}
iterator = gridCols[grid];
$this.addClass("ui-grid-" + grid);
$kids.filter(":nth-child(" + iterator + "n+1)").addClass("ui-block-a");
if (iterator > 1) {
$kids
.filter(":nth-child(" + iterator + "n+2)")
.addClass("ui-block-b");
}
if (iterator > 2) {
$kids
.filter(":nth-child(" + iterator + "n+3)")
.addClass("ui-block-c");
}
if (iterator > 3) {
$kids
.filter(":nth-child(" + iterator + "n+4)")
.addClass("ui-block-d");
}
if (iterator > 4) {
$kids
.filter(":nth-child(" + iterator + "n+5)")
.addClass("ui-block-e");
}
});
};
})(jQuery);
(function ($, undefined) {
$.widget("mobile.navbar", {
options: {
iconpos: "top",
grid: null,
},
_create: function () {
var $navbar = this.element,
$navbtns = $navbar.find("a"),
iconpos = $navbtns.filter(":jqmData(icon)").length
? this.options.iconpos
: undefined;
$navbar
.addClass("ui-navbar")
.attr("role", "navigation")
.find("ul")
.jqmEnhanceable()
.grid({ grid: this.options.grid });
$navbtns.each(function () {
var icon = $.mobile.getAttribute(this, "icon"),
theme = $.mobile.getAttribute(this, "theme"),
classes = "ui-btn";
if (theme) {
classes += " ui-btn-" + theme;
}
if (icon) {
classes += " ui-icon-" + icon + " ui-btn-icon-" + iconpos;
}
$(this).addClass(classes);
});
$navbar.delegate("a", "vclick", function (/* event */) {
var activeBtn = $(this);
if (
!(
activeBtn.hasClass("ui-state-disabled") ||
// DEPRECATED as of 1.4.0 - remove after 1.4.0 release
// only ui-state-disabled should be present thereafter
activeBtn.hasClass("ui-disabled") ||
activeBtn.hasClass($.mobile.activeBtnClass)
)
) {
$navbtns.removeClass($.mobile.activeBtnClass);
activeBtn.addClass($.mobile.activeBtnClass);
// The code below is a workaround to fix #1181
$(document).one("pagehide", function () {
activeBtn.removeClass($.mobile.activeBtnClass);
});
}
});
// Buttons in the navbar with ui-state-persist class should regain their active state before page show
$navbar.closest(".ui-page").bind("pagebeforeshow", function () {
$navbtns
.filter(".ui-state-persist")
.addClass($.mobile.activeBtnClass);
});
},
});
})(jQuery);
(function ($, undefined) {
var getAttr = $.mobile.getAttribute;
$.widget(
"mobile.listview",
$.extend(
{
options: {
theme: null,
countTheme: null /* Deprecated in 1.4 */,
dividerTheme: null,
icon: "carat-r",
splitIcon: "carat-r",
splitTheme: null,
corners: true,
shadow: true,
inset: false,
},
_create: function () {
var t = this,
listviewClasses = "";
listviewClasses += t.options.inset ? " ui-listview-inset" : "";
if (!!t.options.inset) {
listviewClasses += t.options.corners ? " ui-corner-all" : "";
listviewClasses += t.options.shadow ? " ui-shadow" : "";
}
// create listview markup
t.element.addClass(" ui-listview" + listviewClasses);
t.refresh(true);
},
// TODO: Remove in 1.5
_findFirstElementByTagName: function (ele, nextProp, lcName, ucName) {
var dict = {};
dict[lcName] = dict[ucName] = true;
while (ele) {
if (dict[ele.nodeName]) {
return ele;
}
ele = ele[nextProp];
}
return null;
},
// TODO: Remove in 1.5
_addThumbClasses: function (containers) {
var i,
img,
len = containers.length;
for (i = 0; i < len; i++) {
img = $(
this._findFirstElementByTagName(
containers[i].firstChild,
"nextSibling",
"img",
"IMG",
),
);
if (img.length) {
$(
this._findFirstElementByTagName(
img[0].parentNode,
"parentNode",
"li",
"LI",
),
).addClass(
img.hasClass("ui-li-icon")
? "ui-li-has-icon"
: "ui-li-has-thumb",
);
}
}
},
_getChildrenByTagName: function (ele, lcName, ucName) {
var results = [],
dict = {};
dict[lcName] = dict[ucName] = true;
ele = ele.firstChild;
while (ele) {
if (dict[ele.nodeName]) {
results.push(ele);
}
ele = ele.nextSibling;
}
return $(results);
},
_beforeListviewRefresh: $.noop,
_afterListviewRefresh: $.noop,
refresh: function (create) {
var buttonClass,
pos,
numli,
item,
itemClass,
itemTheme,
itemIcon,
icon,
a,
isDivider,
startCount,
newStartCount,
value,
last,
splittheme,
splitThemeClass,
spliticon,
altButtonClass,
dividerTheme,
li,
o = this.options,
$list = this.element,
ol = !!$.nodeName($list[0], "ol"),
start = $list.attr("start"),
itemClassDict = {},
countBubbles = $list.find(".ui-li-count"),
countTheme =
getAttr($list[0], "counttheme") || this.options.countTheme,
countThemeClass = countTheme
? "ui-body-" + countTheme
: "ui-body-inherit";
if (o.theme) {
$list.addClass("ui-group-theme-" + o.theme);
}
// Check if a start attribute has been set while taking a value of 0 into account
if (ol && (start || start === 0)) {
startCount = parseInt(start, 10) - 1;
$list.css("counter-reset", "listnumbering " + startCount);
}
this._beforeListviewRefresh();
li = this._getChildrenByTagName($list[0], "li", "LI");
for (pos = 0, numli = li.length; pos < numli; pos++) {
item = li.eq(pos);
itemClass = "";
if (
create ||
item[0].className.search(/\bui-li-static\b|\bui-li-divider\b/) <
0
) {
a = this._getChildrenByTagName(item[0], "a", "A");
isDivider = getAttr(item[0], "role") === "list-divider";
value = item.attr("value");
itemTheme = getAttr(item[0], "theme");
if (
a.length &&
a[0].className.search(/\bui-btn\b/) < 0 &&
!isDivider
) {
itemIcon = getAttr(item[0], "icon");
icon = itemIcon === false ? false : itemIcon || o.icon;
// TODO: Remove in 1.5 together with links.js (links.js / .ui-link deprecated in 1.4)
a.removeClass("ui-link");
buttonClass = "ui-btn";
if (itemTheme) {
buttonClass += " ui-btn-" + itemTheme;
}
if (a.length > 1) {
itemClass = "ui-li-has-alt";
last = a.last();
splittheme =
getAttr(last[0], "theme") ||
o.splitTheme ||
getAttr(item[0], "theme", true);
splitThemeClass = splittheme ? " ui-btn-" + splittheme : "";
spliticon =
getAttr(last[0], "icon") ||
getAttr(item[0], "icon") ||
o.splitIcon;
altButtonClass =
"ui-btn ui-btn-icon-notext ui-icon-" +
spliticon +
splitThemeClass;
last
.attr("title", $.trim(last.getEncodedText()))
.addClass(altButtonClass)
.empty();
} else if (icon) {
buttonClass += " ui-btn-icon-right ui-icon-" + icon;
}
a.first().addClass(buttonClass);
} else if (isDivider) {
dividerTheme =
getAttr(item[0], "theme") || o.dividerTheme || o.theme;
itemClass =
"ui-li-divider ui-bar-" +
(dividerTheme ? dividerTheme : "inherit");
item.attr("role", "heading");
} else if (a.length <= 0) {
itemClass =
"ui-li-static ui-body-" +
(itemTheme ? itemTheme : "inherit");
}
if (ol && value) {
newStartCount = parseInt(value, 10) - 1;
item.css("counter-reset", "listnumbering " + newStartCount);
}
}
// Instead of setting item class directly on the list item
// at this point in time, push the item into a dictionary
// that tells us what class to set on it so we can do this after this
// processing loop is finished.
if (!itemClassDict[itemClass]) {
itemClassDict[itemClass] = [];
}
itemClassDict[itemClass].push(item[0]);
}
// Set the appropriate listview item classes on each list item.
// The main reason we didn't do this
// in the for-loop above is because we can eliminate per-item function overhead
// by calling addClass() and children() once or twice afterwards. This
// can give us a significant boost on platforms like WP7.5.
for (itemClass in itemClassDict) {
$(itemClassDict[itemClass]).addClass(itemClass);
}
countBubbles.each(function () {
$(this).closest("li").addClass("ui-li-has-count");
});
if (countThemeClass) {
countBubbles.addClass(countThemeClass);
}
// Deprecated in 1.4. From 1.5 you have to add class ui-li-has-thumb or ui-li-has-icon to the LI.
this._addThumbClasses(li);
this._addThumbClasses(li.find(".ui-btn"));
this._afterListviewRefresh();
this._addFirstLastClasses(
li,
this._getVisibles(li, create),
create,
);
},
},
$.mobile.behaviors.addFirstLastClasses,
),
);
})(jQuery);
(function ($, undefined) {
function defaultAutodividersSelector(elt) {
// look for the text in the given element
var text = $.trim(elt.text()) || null;
if (!text) {
return null;
}
// create the text for the divider (first uppercased letter)
text = text.slice(0, 1).toUpperCase();
return text;
}
$.widget("mobile.listview", $.mobile.listview, {
options: {
autodividers: false,
autodividersSelector: defaultAutodividersSelector,
},
_beforeListviewRefresh: function () {
if (this.options.autodividers) {
this._replaceDividers();
this._superApply(arguments);
}
},
_replaceDividers: function () {
var i,
lis,
li,
dividerText,
lastDividerText = null,
list = this.element,
divider;
list.children("li:jqmData(role='list-divider')").remove();
lis = list.children("li");
for (i = 0; i < lis.length; i++) {
li = lis[i];
dividerText = this.options.autodividersSelector($(li));
if (dividerText && lastDividerText !== dividerText) {
divider = document.createElement("li");
divider.appendChild(document.createTextNode(dividerText));
divider.setAttribute(
"data-" + $.mobile.ns + "role",
"list-divider",
);
li.parentNode.insertBefore(divider, li);
}
lastDividerText = dividerText;
}
},
});
})(jQuery);
(function ($, undefined) {
var rdivider = /(^|\s)ui-li-divider($|\s)/,
rhidden = /(^|\s)ui-screen-hidden($|\s)/;
$.widget("mobile.listview", $.mobile.listview, {
options: {
hideDividers: false,
},
_afterListviewRefresh: function () {
var items,
idx,
item,
hideDivider = true;
this._superApply(arguments);
if (this.options.hideDividers) {
items = this._getChildrenByTagName(this.element[0], "li", "LI");
for (idx = items.length - 1; idx > -1; idx--) {
item = items[idx];
if (item.className.match(rdivider)) {
if (hideDivider) {
item.className = item.className + " ui-screen-hidden";
}
hideDivider = true;
} else {
if (!item.className.match(rhidden)) {
hideDivider = false;
}
}
}
}
},
});
})(jQuery);
(function ($, undefined) {
$.mobile.nojs = function (target) {
$(":jqmData(role='nojs')", target).addClass("ui-nojs");
};
})(jQuery);
(function ($, undefined) {
$.mobile.behaviors.formReset = {
_handleFormReset: function () {
this._on(this.element.closest("form"), {
reset: function () {
this._delay("_reset");
},
});
},
};
})(jQuery);
/*
* "checkboxradio" plugin
*/
(function ($, undefined) {
var escapeId = $.mobile.path.hashToSelector;
$.widget(
"mobile.checkboxradio",
$.extend(
{
initSelector:
"input:not( :jqmData(role='flipswitch' ) )[type='checkbox'],input[type='radio']:not( :jqmData(role='flipswitch' ))",
options: {
theme: "inherit",
mini: false,
wrapperClass: null,
enhanced: false,
iconpos: "left",
},
_create: function () {
var input = this.element,
o = this.options,
inheritAttr = function (input, dataAttr) {
return (
input.jqmData(dataAttr) ||
input.closest("form, fieldset").jqmData(dataAttr)
);
},
// NOTE: Windows Phone could not find the label through a selector
// filter works though.
parentLabel = input.closest("label"),
label = parentLabel.length
? parentLabel
: input
.closest(
"form, fieldset, :jqmData(role='page'), :jqmData(role='dialog')",
)
.find("label")
.filter("[for='" + escapeId(input[0].id) + "']")
.first(),
inputtype = input[0].type,
checkedClass = "ui-" + inputtype + "-on",
uncheckedClass = "ui-" + inputtype + "-off";
if (inputtype !== "checkbox" && inputtype !== "radio") {
return;
}
if (this.element[0].disabled) {
this.options.disabled = true;
}
(o.iconpos =
inheritAttr(input, "iconpos") ||
label.attr("data-" + $.mobile.ns + "iconpos") ||
o.iconpos),
// Establish options
(o.mini = inheritAttr(input, "mini") || o.mini);
// Expose for other methods
$.extend(this, {
input: input,
label: label,
parentLabel: parentLabel,
inputtype: inputtype,
checkedClass: checkedClass,
uncheckedClass: uncheckedClass,
});
if (!this.options.enhanced) {
this._enhance();
}
this._on(label, {
vmouseover: "_handleLabelVMouseOver",
vclick: "_handleLabelVClick",
});
this._on(input, {
vmousedown: "_cacheVals",
vclick: "_handleInputVClick",
focus: "_handleInputFocus",
blur: "_handleInputBlur",
});
this._handleFormReset();
this.refresh();
},
_enhance: function () {
this.label.addClass("ui-btn ui-corner-all");
if (this.parentLabel.length > 0) {
this.input.add(this.label).wrapAll(this._wrapper());
} else {
//this.element.replaceWith( this.input.add( this.label ).wrapAll( this._wrapper() ) );
this.element.wrap(this._wrapper());
this.element.parent().prepend(this.label);
}
// Wrap the input + label in a div
this._setOptions({
theme: this.options.theme,
iconpos: this.options.iconpos,
mini: this.options.mini,
});
},
_wrapper: function () {
return $(
"<div class='" +
(this.options.wrapperClass ? this.options.wrapperClass : "") +
" ui-" +
this.inputtype +
(this.options.disabled ? " ui-state-disabled" : "") +
"' ></div>",
);
},
_handleInputFocus: function () {
this.label.addClass($.mobile.focusClass);
},
_handleInputBlur: function () {
this.label.removeClass($.mobile.focusClass);
},
_handleInputVClick: function () {
// Adds checked attribute to checked input when keyboard is used
this.element.prop("checked", this.element.is(":checked"));
this._getInputSet().not(this.element).prop("checked", false);
this._updateAll();
},
_handleLabelVMouseOver: function (event) {
if (this.label.parent().hasClass("ui-state-disabled")) {
event.stopPropagation();
}
},
_handleLabelVClick: function (event) {
var input = this.element;
if (input.is(":disabled")) {
event.preventDefault();
return;
}
this._cacheVals();
input.prop(
"checked",
(this.inputtype === "radio" && true) || !input.prop("checked"),
);
// trigger click handler's bound directly to the input as a substitute for
// how label clicks behave normally in the browsers
// TODO: it would be nice to let the browser's handle the clicks and pass them
// through to the associate input. we can swallow that click at the parent
// wrapper element level
input.triggerHandler("click");
// Input set for common radio buttons will contain all the radio
// buttons, but will not for checkboxes. clearing the checked status
// of other radios ensures the active button state is applied properly
this._getInputSet().not(input).prop("checked", false);
this._updateAll();
return false;
},
_cacheVals: function () {
this._getInputSet().each(function () {
$(this).attr("data-" + $.mobile.ns + "cacheVal", this.checked);
});
},
// Returns those radio buttons that are supposed to be in the same group as
// this radio button. In the case of a checkbox or a radio lacking a name
// attribute, it returns this.element.
_getInputSet: function () {
var selector,
formId,
radio = this.element[0],
name = radio.name,
form = radio.form,
doc = this.element.parents().last().get(0),
// A radio is always a member of its own group
radios = this.element;
// Only start running selectors if this is an attached radio button with a name
if (name && this.inputtype === "radio" && doc) {
selector = "input[type='radio'][name='" + escapeId(name) + "']";
// If we're inside a form
if (form) {
formId = form.id;
// If the form has an ID, collect radios scattered throught the document which
// nevertheless are part of the form by way of the value of their form attribute
if (formId) {
radios = $(
selector + "[form='" + escapeId(formId) + "']",
doc,
);
}
// Also add to those the radios in the form itself
radios = $(form)
.find(selector)
.filter(function () {
// Some radios inside the form may belong to some other form by virtue of
// having a form attribute defined on them, so we must filter them out here
return this.form === form;
})
.add(radios);
// If we're outside a form
} else {
// Collect all those radios which are also outside of a form and match our name
radios = $(selector, doc).filter(function () {
return !this.form;
});
}
}
return radios;
},
_updateAll: function () {
var self = this;
this._getInputSet()
.each(function () {
var $this = $(this);
if (this.checked || self.inputtype === "checkbox") {
$this.trigger("change");
}
})
.checkboxradio("refresh");
},
_reset: function () {
this.refresh();
},
// Is the widget supposed to display an icon?
_hasIcon: function () {
var controlgroup,
controlgroupWidget,
controlgroupConstructor = $.mobile.controlgroup;
// If the controlgroup widget is defined ...
if (controlgroupConstructor) {
controlgroup = this.element.closest(
":mobile-controlgroup," +
controlgroupConstructor.prototype.initSelector,
);
// ... and the checkbox is in a controlgroup ...
if (controlgroup.length > 0) {
// ... look for a controlgroup widget instance, and ...
controlgroupWidget = $.data(
controlgroup[0],
"mobile-controlgroup",
);
// ... if found, decide based on the option value, ...
return (
(controlgroupWidget
? controlgroupWidget.options.type
: // ... otherwise decide based on the "type" data attribute.
controlgroup.attr("data-" + $.mobile.ns + "type")) !==
"horizontal"
);
}
}
// Normally, the widget displays an icon.
return true;
},
refresh: function () {
var hasIcon = this._hasIcon(),
isChecked = this.element[0].checked,
active = $.mobile.activeBtnClass,
iconposClass = "ui-btn-icon-" + this.options.iconpos,
addClasses = [],
removeClasses = [];
if (hasIcon) {
removeClasses.push(active);
addClasses.push(iconposClass);
} else {
removeClasses.push(iconposClass);
(isChecked ? addClasses : removeClasses).push(active);
}
if (isChecked) {
addClasses.push(this.checkedClass);
removeClasses.push(this.uncheckedClass);
} else {
addClasses.push(this.uncheckedClass);
removeClasses.push(this.checkedClass);
}
this.label
.addClass(addClasses.join(" "))
.removeClass(removeClasses.join(" "));
},
widget: function () {
return this.label.parent();
},
_setOptions: function (options) {
var label = this.label,
currentOptions = this.options,
outer = this.widget(),
hasIcon = this._hasIcon();
if (options.disabled !== undefined) {
this.input.prop("disabled", !!options.disabled);
outer.toggleClass("ui-state-disabled", !!options.disabled);
}
if (options.mini !== undefined) {
outer.toggleClass("ui-mini", !!options.mini);
}
if (options.theme !== undefined) {
label
.removeClass("ui-btn-" + currentOptions.theme)
.addClass("ui-btn-" + options.theme);
}
if (options.wrapperClass !== undefined) {
outer
.removeClass(currentOptions.wrapperClass)
.addClass(options.wrapperClass);
}
if (options.iconpos !== undefined && hasIcon) {
label
.removeClass("ui-btn-icon-" + currentOptions.iconpos)
.addClass("ui-btn-icon-" + options.iconpos);
} else if (!hasIcon) {
label.removeClass("ui-btn-icon-" + currentOptions.iconpos);
}
this._super(options);
},
},
$.mobile.behaviors.formReset,
),
);
})(jQuery);
(function ($, undefined) {
$.widget("mobile.button", {
initSelector:
"input[type='button'], input[type='submit'], input[type='reset']",
options: {
theme: null,
icon: null,
iconpos: "left",
iconshadow: false /* TODO: Deprecated in 1.4, remove in 1.5. */,
corners: true,
shadow: true,
inline: null,
mini: null,
wrapperClass: null,
enhanced: false,
},
_create: function () {
if (this.element.is(":disabled")) {
this.options.disabled = true;
}
if (!this.options.enhanced) {
this._enhance();
}
$.extend(this, {
wrapper: this.element.parent(),
});
this._on({
focus: function () {
this.widget().addClass($.mobile.focusClass);
},
blur: function () {
this.widget().removeClass($.mobile.focusClass);
},
});
this.refresh(true);
},
_enhance: function () {
this.element.wrap(this._button());
},
_button: function () {
var options = this.options,
iconClasses = this._getIconClasses(this.options);
return $(
"<div class='ui-btn ui-input-btn" +
(options.wrapperClass ? " " + options.wrapperClass : "") +
(options.theme ? " ui-btn-" + options.theme : "") +
(options.corners ? " ui-corner-all" : "") +
(options.shadow ? " ui-shadow" : "") +
(options.inline ? " ui-btn-inline" : "") +
(options.mini ? " ui-mini" : "") +
(options.disabled ? " ui-state-disabled" : "") +
(iconClasses ? " " + iconClasses : "") +
"' >" +
this.element.val() +
"</div>",
);
},
widget: function () {
return this.wrapper;
},
_destroy: function () {
this.element.insertBefore(this.button);
this.button.remove();
},
_getIconClasses: function (options) {
return options.icon
? "ui-icon-" +
options.icon +
(options.iconshadow
? " ui-shadow-icon"
: "") /* TODO: Deprecated in 1.4, remove in 1.5. */ +
" ui-btn-icon-" +
options.iconpos
: "";
},
_setOptions: function (options) {
var outer = this.widget();
if (options.theme !== undefined) {
outer
.removeClass(this.options.theme)
.addClass("ui-btn-" + options.theme);
}
if (options.corners !== undefined) {
outer.toggleClass("ui-corner-all", options.corners);
}
if (options.shadow !== undefined) {
outer.toggleClass("ui-shadow", options.shadow);
}
if (options.inline !== undefined) {
outer.toggleClass("ui-btn-inline", options.inline);
}
if (options.mini !== undefined) {
outer.toggleClass("ui-mini", options.mini);
}
if (options.disabled !== undefined) {
this.element.prop("disabled", options.disabled);
outer.toggleClass("ui-state-disabled", options.disabled);
}
if (
options.icon !== undefined ||
options.iconshadow !==
undefined /* TODO: Deprecated in 1.4, remove in 1.5. */ ||
options.iconpos !== undefined
) {
outer
.removeClass(this._getIconClasses(this.options))
.addClass(
this._getIconClasses($.extend({}, this.options, options)),
);
}
this._super(options);
},
refresh: function (create) {
var originalElement,
isDisabled = this.element.prop("disabled");
if (
this.options.icon &&
this.options.iconpos === "notext" &&
this.element.attr("title")
) {
this.element.attr("title", this.element.val());
}
if (!create) {
originalElement = this.element.detach();
$(this.wrapper).text(this.element.val()).append(originalElement);
}
if (this.options.disabled !== isDisabled) {
this._setOptions({ disabled: isDisabled });
}
},
});
})(jQuery);
(function ($) {
var meta = $("meta[name=viewport]"),
initialContent = meta.attr("content"),
disabledZoom = initialContent + ",maximum-scale=1, user-scalable=no",
enabledZoom = initialContent + ",maximum-scale=10, user-scalable=yes",
disabledInitially =
/(user-scalable[\s]*=[\s]*no)|(maximum-scale[\s]*=[\s]*1)[$,\s]/.test(
initialContent,
);
$.mobile.zoom = $.extend(
{},
{
enabled: !disabledInitially,
locked: false,
disable: function (lock) {
if (!disabledInitially && !$.mobile.zoom.locked) {
meta.attr("content", disabledZoom);
$.mobile.zoom.enabled = false;
$.mobile.zoom.locked = lock || false;
}
},
enable: function (unlock) {
if (
!disabledInitially &&
(!$.mobile.zoom.locked || unlock === true)
) {
meta.attr("content", enabledZoom);
$.mobile.zoom.enabled = true;
$.mobile.zoom.locked = false;
}
},
restore: function () {
if (!disabledInitially) {
meta.attr("content", initialContent);
$.mobile.zoom.enabled = true;
}
},
},
);
})(jQuery);
(function ($, undefined) {
$.widget("mobile.textinput", {
initSelector:
"input[type='text']," +
"input[type='search']," +
":jqmData(type='search')," +
"input[type='number']," +
":jqmData(type='number')," +
"input[type='password']," +
"input[type='email']," +
"input[type='url']," +
"input[type='tel']," +
"textarea," +
"input[type='time']," +
"input[type='date']," +
"input[type='month']," +
"input[type='week']," +
"input[type='datetime']," +
"input[type='datetime-local']," +
"input[type='color']," +
"input:not([type])," +
"input[type='file']",
options: {
theme: null,
corners: true,
mini: false,
// This option defaults to true on iOS devices.
preventFocusZoom:
/iPhone|iPad|iPod/.test(navigator.platform) &&
navigator.userAgent.indexOf("AppleWebKit") > -1,
wrapperClass: "",
enhanced: false,
},
_create: function () {
var options = this.options,
isSearch = this.element.is(
"[type='search'], :jqmData(type='search')",
),
isTextarea = this.element[0].tagName === "TEXTAREA",
isRange = this.element.is(
"[data-" + ($.mobile.ns || "") + "type='range']",
),
inputNeedsWrap =
(this.element.is("input") ||
this.element.is(
"[data-" + ($.mobile.ns || "") + "type='search']",
)) &&
!isRange;
if (this.element.prop("disabled")) {
options.disabled = true;
}
$.extend(this, {
classes: this._classesFromOptions(),
isSearch: isSearch,
isTextarea: isTextarea,
isRange: isRange,
inputNeedsWrap: inputNeedsWrap,
});
this._autoCorrect();
if (!options.enhanced) {
this._enhance();
}
this._on({
focus: "_handleFocus",
blur: "_handleBlur",
});
},
refresh: function () {
this.setOptions({
disabled: this.element.is(":disabled"),
});
},
_enhance: function () {
var elementClasses = [];
if (this.isTextarea) {
elementClasses.push("ui-input-text");
}
if (this.isTextarea || this.isRange) {
elementClasses.push("ui-shadow-inset");
}
//"search" and "text" input widgets
if (this.inputNeedsWrap) {
this.element.wrap(this._wrap());
} else {
elementClasses = elementClasses.concat(this.classes);
}
this.element.addClass(elementClasses.join(" "));
},
widget: function () {
return this.inputNeedsWrap ? this.element.parent() : this.element;
},
_classesFromOptions: function () {
var options = this.options,
classes = [];
classes.push(
"ui-body-" + (options.theme === null ? "inherit" : options.theme),
);
if (options.corners) {
classes.push("ui-corner-all");
}
if (options.mini) {
classes.push("ui-mini");
}
if (options.disabled) {
classes.push("ui-state-disabled");
}
if (options.wrapperClass) {
classes.push(options.wrapperClass);
}
return classes;
},
_wrap: function () {
return $(
"<div class='" +
(this.isSearch ? "ui-input-search " : "ui-input-text ") +
this.classes.join(" ") +
" " +
"ui-shadow-inset'></div>",
);
},
_autoCorrect: function () {
// XXX: Temporary workaround for issue 785 (Apple bug 8910589).
// Turn off autocorrect and autocomplete on non-iOS 5 devices
// since the popup they use can't be dismissed by the user. Note
// that we test for the presence of the feature by looking for
// the autocorrect property on the input element. We currently
// have no test for iOS 5 or newer so we're temporarily using
// the touchOverflow support flag for jQM 1.0. Yes, I feel dirty.
// - jblas
if (
typeof this.element[0].autocorrect !== "undefined" &&
!$.support.touchOverflow
) {
// Set the attribute instead of the property just in case there
// is code that attempts to make modifications via HTML.
this.element[0].setAttribute("autocorrect", "off");
this.element[0].setAttribute("autocomplete", "off");
}
},
_handleBlur: function () {
this.widget().removeClass($.mobile.focusClass);
if (this.options.preventFocusZoom) {
$.mobile.zoom.enable(true);
}
},
_handleFocus: function () {
// In many situations, iOS will zoom into the input upon tap, this
// prevents that from happening
if (this.options.preventFocusZoom) {
$.mobile.zoom.disable(true);
}
this.widget().addClass($.mobile.focusClass);
},
_setOptions: function (options) {
var outer = this.widget();
this._super(options);
if (
!(
options.disabled === undefined &&
options.mini === undefined &&
options.corners === undefined &&
options.theme === undefined &&
options.wrapperClass === undefined
)
) {
outer.removeClass(this.classes.join(" "));
this.classes = this._classesFromOptions();
outer.addClass(this.classes.join(" "));
}
if (options.disabled !== undefined) {
this.element.prop("disabled", !!options.disabled);
}
},
_destroy: function () {
if (this.options.enhanced) {
return;
}
if (this.inputNeedsWrap) {
this.element.unwrap();
}
this.element.removeClass("ui-input-text " + this.classes.join(" "));
},
});
})(jQuery);
(function ($, undefined) {
$.widget(
"mobile.slider",
$.extend(
{
initSelector:
"input[type='range'], :jqmData(type='range'), :jqmData(role='slider')",
widgetEventPrefix: "slide",
options: {
theme: null,
trackTheme: null,
corners: true,
mini: false,
highlight: false,
},
_create: function () {
// TODO: Each of these should have comments explain what they're for
var self = this,
control = this.element,
trackTheme =
this.options.trackTheme ||
$.mobile.getAttribute(control[0], "theme"),
trackThemeClass = trackTheme
? " ui-bar-" + trackTheme
: " ui-bar-inherit",
cornerClass =
this.options.corners || control.jqmData("corners")
? " ui-corner-all"
: "",
miniClass =
this.options.mini || control.jqmData("mini") ? " ui-mini" : "",
cType = control[0].nodeName.toLowerCase(),
isToggleSwitch = cType === "select",
isRangeslider = control
.parent()
.is(":jqmData(role='rangeslider')"),
selectClass = isToggleSwitch ? "ui-slider-switch" : "",
controlID = control.attr("id"),
$label = $("[for='" + controlID + "']"),
labelID = $label.attr("id") || controlID + "-label",
min = !isToggleSwitch ? parseFloat(control.attr("min")) : 0,
max = !isToggleSwitch
? parseFloat(control.attr("max"))
: control.find("option").length - 1,
step = window.parseFloat(control.attr("step") || 1),
domHandle = document.createElement("a"),
handle = $(domHandle),
domSlider = document.createElement("div"),
slider = $(domSlider),
valuebg =
this.options.highlight && !isToggleSwitch
? (function () {
var bg = document.createElement("div");
bg.className = "ui-slider-bg " + $.mobile.activeBtnClass;
return $(bg).prependTo(slider);
})()
: false,
options,
wrapper,
j,
length,
i,
optionsCount,
origTabIndex,
side,
activeClass,
sliderImg;
$label.attr("id", labelID);
this.isToggleSwitch = isToggleSwitch;
domHandle.setAttribute("href", "#");
domSlider.setAttribute("role", "application");
domSlider.className = [
this.isToggleSwitch
? "ui-slider ui-slider-track ui-shadow-inset "
: "ui-slider-track ui-shadow-inset ",
selectClass,
trackThemeClass,
cornerClass,
miniClass,
].join("");
domHandle.className = "ui-slider-handle";
domSlider.appendChild(domHandle);
handle.attr({
role: "slider",
"aria-valuemin": min,
"aria-valuemax": max,
"aria-valuenow": this._value(),
"aria-valuetext": this._value(),
title: this._value(),
"aria-labelledby": labelID,
});
$.extend(this, {
slider: slider,
handle: handle,
control: control,
type: cType,
step: step,
max: max,
min: min,
valuebg: valuebg,
isRangeslider: isRangeslider,
dragging: false,
beforeStart: null,
userModified: false,
mouseMoved: false,
});
if (isToggleSwitch) {
// TODO: restore original tabindex (if any) in a destroy method
origTabIndex = control.attr("tabindex");
if (origTabIndex) {
handle.attr("tabindex", origTabIndex);
}
control.attr("tabindex", "-1").focus(function () {
$(this).blur();
handle.focus();
});
wrapper = document.createElement("div");
wrapper.className = "ui-slider-inneroffset";
for (
j = 0, length = domSlider.childNodes.length;
j < length;
j++
) {
wrapper.appendChild(domSlider.childNodes[j]);
}
domSlider.appendChild(wrapper);
// slider.wrapInner( "<div class='ui-slider-inneroffset'></div>" );
// make the handle move with a smooth transition
handle.addClass("ui-slider-handle-snapping");
options = control.find("option");
for (
i = 0, optionsCount = options.length;
i < optionsCount;
i++
) {
side = !i ? "b" : "a";
activeClass = !i ? "" : " " + $.mobile.activeBtnClass;
sliderImg = document.createElement("span");
sliderImg.className = [
"ui-slider-label ui-slider-label-",
side,
activeClass,
].join("");
sliderImg.setAttribute("role", "img");
sliderImg.appendChild(
document.createTextNode(options[i].innerHTML),
);
$(sliderImg).prependTo(slider);
}
self._labels = $(".ui-slider-label", slider);
}
// monitor the input for updated values
control.addClass(
isToggleSwitch ? "ui-slider-switch" : "ui-slider-input",
);
this._on(control, {
change: "_controlChange",
keyup: "_controlKeyup",
blur: "_controlBlur",
vmouseup: "_controlVMouseUp",
});
slider
.bind("vmousedown", $.proxy(this._sliderVMouseDown, this))
.bind("vclick", false);
// We have to instantiate a new function object for the unbind to work properly
// since the method itself is defined in the prototype (causing it to unbind everything)
this._on(document, { vmousemove: "_preventDocumentDrag" });
this._on(slider.add(document), { vmouseup: "_sliderVMouseUp" });
slider.insertAfter(control);
// wrap in a div for styling purposes
if (!isToggleSwitch && !isRangeslider) {
wrapper = this.options.mini
? "<div class='ui-slider ui-mini'>"
: "<div class='ui-slider'>";
control.add(slider).wrapAll(wrapper);
}
// bind the handle event callbacks and set the context to the widget instance
this._on(this.handle, {
vmousedown: "_handleVMouseDown",
keydown: "_handleKeydown",
keyup: "_handleKeyup",
});
this.handle.bind("vclick", false);
this._handleFormReset();
this.refresh(undefined, undefined, true);
},
_setOptions: function (options) {
if (options.theme !== undefined) {
this._setTheme(options.theme);
}
if (options.trackTheme !== undefined) {
this._setTrackTheme(options.trackTheme);
}
if (options.corners !== undefined) {
this._setCorners(options.corners);
}
if (options.mini !== undefined) {
this._setMini(options.mini);
}
if (options.highlight !== undefined) {
this._setHighlight(options.highlight);
}
if (options.disabled !== undefined) {
this._setDisabled(options.disabled);
}
this._super(options);
},
_controlChange: function (event) {
// if the user dragged the handle, the "change" event was triggered from inside refresh(); don't call refresh() again
if (this._trigger("controlchange", event) === false) {
return false;
}
if (!this.mouseMoved) {
this.refresh(this._value(), true);
}
},
_controlKeyup: function (/* event */) {
// necessary?
this.refresh(this._value(), true, true);
},
_controlBlur: function (/* event */) {
this.refresh(this._value(), true);
},
// it appears the clicking the up and down buttons in chrome on
// range/number inputs doesn't trigger a change until the field is
// blurred. Here we check thif the value has changed and refresh
_controlVMouseUp: function (/* event */) {
this._checkedRefresh();
},
// NOTE force focus on handle
_handleVMouseDown: function (/* event */) {
this.handle.focus();
},
_handleKeydown: function (event) {
var index = this._value();
if (this.options.disabled) {
return;
}
// In all cases prevent the default and mark the handle as active
switch (event.keyCode) {
case $.mobile.keyCode.HOME:
case $.mobile.keyCode.END:
case $.mobile.keyCode.PAGE_UP:
case $.mobile.keyCode.PAGE_DOWN:
case $.mobile.keyCode.UP:
case $.mobile.keyCode.RIGHT:
case $.mobile.keyCode.DOWN:
case $.mobile.keyCode.LEFT:
event.preventDefault();
if (!this._keySliding) {
this._keySliding = true;
this.handle.addClass(
"ui-state-active",
); /* TODO: We don't use this class for styling. Do we need to add it? */
}
break;
}
// move the slider according to the keypress
switch (event.keyCode) {
case $.mobile.keyCode.HOME:
this.refresh(this.min);
break;
case $.mobile.keyCode.END:
this.refresh(this.max);
break;
case $.mobile.keyCode.PAGE_UP:
case $.mobile.keyCode.UP:
case $.mobile.keyCode.RIGHT:
this.refresh(index + this.step);
break;
case $.mobile.keyCode.PAGE_DOWN:
case $.mobile.keyCode.DOWN:
case $.mobile.keyCode.LEFT:
this.refresh(index - this.step);
break;
}
}, // remove active mark
_handleKeyup: function (/* event */) {
if (this._keySliding) {
this._keySliding = false;
this.handle.removeClass(
"ui-state-active",
); /* See comment above. */
}
},
_sliderVMouseDown: function (event) {
// NOTE: we don't do this in refresh because we still want to
// support programmatic alteration of disabled inputs
if (
this.options.disabled ||
!(
event.which === 1 ||
event.which === 0 ||
event.which === undefined
)
) {
return false;
}
if (this._trigger("beforestart", event) === false) {
return false;
}
this.dragging = true;
this.userModified = false;
this.mouseMoved = false;
if (this.isToggleSwitch) {
this.beforeStart = this.element[0].selectedIndex;
}
this.refresh(event);
this._trigger("start");
return false;
},
_sliderVMouseUp: function () {
if (this.dragging) {
this.dragging = false;
if (this.isToggleSwitch) {
// make the handle move with a smooth transition
this.handle.addClass("ui-slider-handle-snapping");
if (this.mouseMoved) {
// this is a drag, change the value only if user dragged enough
if (this.userModified) {
this.refresh(this.beforeStart === 0 ? 1 : 0);
} else {
this.refresh(this.beforeStart);
}
} else {
// this is just a click, change the value
this.refresh(this.beforeStart === 0 ? 1 : 0);
}
}
this.mouseMoved = false;
this._trigger("stop");
return false;
}
},
_preventDocumentDrag: function (event) {
// NOTE: we don't do this in refresh because we still want to
// support programmatic alteration of disabled inputs
if (this._trigger("drag", event) === false) {
return false;
}
if (this.dragging && !this.options.disabled) {
// this.mouseMoved must be updated before refresh() because it will be used in the control "change" event
this.mouseMoved = true;
if (this.isToggleSwitch) {
// make the handle move in sync with the mouse
this.handle.removeClass("ui-slider-handle-snapping");
}
this.refresh(event);
// only after refresh() you can calculate this.userModified
this.userModified =
this.beforeStart !== this.element[0].selectedIndex;
return false;
}
},
_checkedRefresh: function () {
if (this.value !== this._value()) {
this.refresh(this._value());
}
},
_value: function () {
return this.isToggleSwitch
? this.element[0].selectedIndex
: parseFloat(this.element.val());
},
_reset: function () {
this.refresh(undefined, false, true);
},
refresh: function (val, isfromControl, preventInputUpdate) {
// NOTE: we don't return here because we want to support programmatic
// alteration of the input value, which should still update the slider
var self = this,
parentTheme = $.mobile.getAttribute(this.element[0], "theme"),
theme = this.options.theme || parentTheme,
themeClass = theme ? " ui-btn-" + theme : "",
trackTheme = this.options.trackTheme || parentTheme,
trackThemeClass = trackTheme
? " ui-bar-" + trackTheme
: " ui-bar-inherit",
cornerClass = this.options.corners ? " ui-corner-all" : "",
miniClass = this.options.mini ? " ui-mini" : "",
left,
width,
data,
tol,
pxStep,
percent,
control,
isInput,
optionElements,
min,
max,
step,
newval,
valModStep,
alignValue,
percentPerStep,
handlePercent,
aPercent,
bPercent,
valueChanged;
self.slider[0].className = [
this.isToggleSwitch
? "ui-slider ui-slider-switch ui-slider-track ui-shadow-inset"
: "ui-slider-track ui-shadow-inset",
trackThemeClass,
cornerClass,
miniClass,
].join("");
if (this.options.disabled || this.element.prop("disabled")) {
this.disable();
}
// set the stored value for comparison later
this.value = this._value();
if (
this.options.highlight &&
!this.isToggleSwitch &&
this.slider.find(".ui-slider-bg").length === 0
) {
this.valuebg = (function () {
var bg = document.createElement("div");
bg.className = "ui-slider-bg " + $.mobile.activeBtnClass;
return $(bg).prependTo(self.slider);
})();
}
this.handle.addClass("ui-btn" + themeClass + " ui-shadow");
control = this.element;
isInput = !this.isToggleSwitch;
optionElements = isInput ? [] : control.find("option");
min = isInput ? parseFloat(control.attr("min")) : 0;
max = isInput
? parseFloat(control.attr("max"))
: optionElements.length - 1;
step =
isInput && parseFloat(control.attr("step")) > 0
? parseFloat(control.attr("step"))
: 1;
if (typeof val === "object") {
data = val;
// a slight tolerance helped get to the ends of the slider
tol = 8;
left = this.slider.offset().left;
width = this.slider.width();
pxStep = width / ((max - min) / step);
if (
!this.dragging ||
data.pageX < left - tol ||
data.pageX > left + width + tol
) {
return;
}
if (pxStep > 1) {
percent = ((data.pageX - left) / width) * 100;
} else {
percent = Math.round(((data.pageX - left) / width) * 100);
}
} else {
if (val == null) {
val = isInput
? parseFloat(control.val() || 0)
: control[0].selectedIndex;
}
percent = ((parseFloat(val) - min) / (max - min)) * 100;
}
if (isNaN(percent)) {
return;
}
newval = (percent / 100) * (max - min) + min;
//from jQuery UI slider, the following source will round to the nearest step
valModStep = (newval - min) % step;
alignValue = newval - valModStep;
if (Math.abs(valModStep) * 2 >= step) {
alignValue += valModStep > 0 ? step : -step;
}
percentPerStep = 100 / ((max - min) / step);
// Since JavaScript has problems with large floats, round
// the final value to 5 digits after the decimal point (see jQueryUI: #4124)
newval = parseFloat(alignValue.toFixed(5));
if (typeof pxStep === "undefined") {
pxStep = width / ((max - min) / step);
}
if (pxStep > 1 && isInput) {
percent = (newval - min) * percentPerStep * (1 / step);
}
if (percent < 0) {
percent = 0;
}
if (percent > 100) {
percent = 100;
}
if (newval < min) {
newval = min;
}
if (newval > max) {
newval = max;
}
this.handle.css("left", percent + "%");
this.handle[0].setAttribute(
"aria-valuenow",
isInput ? newval : optionElements.eq(newval).attr("value"),
);
this.handle[0].setAttribute(
"aria-valuetext",
isInput ? newval : optionElements.eq(newval).getEncodedText(),
);
this.handle[0].setAttribute(
"title",
isInput ? newval : optionElements.eq(newval).getEncodedText(),
);
if (this.valuebg) {
this.valuebg.css("width", percent + "%");
}
// drag the label widths
if (this._labels) {
handlePercent = (this.handle.width() / this.slider.width()) * 100;
aPercent =
percent &&
handlePercent + ((100 - handlePercent) * percent) / 100;
bPercent =
percent === 100
? 0
: Math.min(handlePercent + 100 - aPercent, 100);
this._labels.each(function () {
var ab = $(this).hasClass("ui-slider-label-a");
$(this).width((ab ? aPercent : bPercent) + "%");
});
}
if (!preventInputUpdate) {
valueChanged = false;
// update control"s value
if (isInput) {
valueChanged = control.val() !== newval;
control.val(newval);
} else {
valueChanged = control[0].selectedIndex !== newval;
control[0].selectedIndex = newval;
}
if (this._trigger("beforechange", val) === false) {
return false;
}
if (!isfromControl && valueChanged) {
control.trigger("change");
}
}
},
_setHighlight: function (value) {
value = !!value;
if (value) {
this.options.highlight = !!value;
this.refresh();
} else if (this.valuebg) {
this.valuebg.remove();
this.valuebg = false;
}
},
_setTheme: function (value) {
this.handle
.removeClass("ui-btn-" + this.options.theme)
.addClass("ui-btn-" + value);
var currentTheme = this.options.theme
? this.options.theme
: "inherit",
newTheme = value ? value : "inherit";
this.control
.removeClass("ui-body-" + currentTheme)
.addClass("ui-body-" + newTheme);
},
_setTrackTheme: function (value) {
var currentTrackTheme = this.options.trackTheme
? this.options.trackTheme
: "inherit",
newTrackTheme = value ? value : "inherit";
this.slider
.removeClass("ui-body-" + currentTrackTheme)
.addClass("ui-body-" + newTrackTheme);
},
_setMini: function (value) {
value = !!value;
if (!this.isToggleSwitch && !this.isRangeslider) {
this.slider.parent().toggleClass("ui-mini", value);
this.element.toggleClass("ui-mini", value);
}
this.slider.toggleClass("ui-mini", value);
},
_setCorners: function (value) {
this.slider.toggleClass("ui-corner-all", value);
if (!this.isToggleSwitch) {
this.control.toggleClass("ui-corner-all", value);
}
},
_setDisabled: function (value) {
value = !!value;
this.element.prop("disabled", value);
this.slider
.toggleClass("ui-state-disabled", value)
.attr("aria-disabled", value);
},
},
$.mobile.behaviors.formReset,
),
);
})(jQuery);
(function ($, undefined) {
var popup;
function getPopup() {
if (!popup) {
popup = $("<div></div>", {
class: "ui-slider-popup ui-shadow ui-corner-all",
});
}
return popup.clone();
}
$.widget("mobile.slider", $.mobile.slider, {
options: {
popupEnabled: false,
showValue: false,
},
_create: function () {
this._super();
$.extend(this, {
_currentValue: null,
_popup: null,
_popupVisible: false,
});
this._setOption("popupEnabled", this.options.popupEnabled);
this._on(this.handle, { vmousedown: "_showPopup" });
this._on(this.slider.add(this.document), { vmouseup: "_hidePopup" });
this._refresh();
},
// position the popup centered 5px above the handle
_positionPopup: function () {
var dstOffset = this.handle.offset();
this._popup.offset({
left:
dstOffset.left + (this.handle.width() - this._popup.width()) / 2,
top: dstOffset.top - this._popup.outerHeight() - 5,
});
},
_setOption: function (key, value) {
this._super(key, value);
if (key === "showValue") {
this.handle.html(value && !this.options.mini ? this._value() : "");
} else if (key === "popupEnabled") {
if (value && !this._popup) {
this._popup = getPopup()
.addClass("ui-body-" + (this.options.theme || "a"))
.hide()
.insertBefore(this.element);
}
}
},
// show value on the handle and in popup
refresh: function () {
this._super.apply(this, arguments);
this._refresh();
},
_refresh: function () {
var o = this.options,
newValue;
if (o.popupEnabled) {
// remove the title attribute from the handle (which is
// responsible for the annoying tooltip); NB we have
// to do it here as the jqm slider sets it every time
// the slider's value changes :(
this.handle.removeAttr("title");
}
newValue = this._value();
if (newValue === this._currentValue) {
return;
}
this._currentValue = newValue;
if (o.popupEnabled && this._popup) {
this._positionPopup();
this._popup.html(newValue);
} else if (o.showValue && !this.options.mini) {
this.handle.html(newValue);
}
},
_showPopup: function () {
if (this.options.popupEnabled && !this._popupVisible) {
this.handle.html("");
this._popup.show();
this._positionPopup();
this._popupVisible = true;
}
},
_hidePopup: function () {
var o = this.options;
if (o.popupEnabled && this._popupVisible) {
if (o.showValue && !o.mini) {
this.handle.html(this._value());
}
this._popup.hide();
this._popupVisible = false;
}
},
});
})(jQuery);
(function ($, undefined) {
$.widget(
"mobile.flipswitch",
$.extend(
{
options: {
onText: "On",
offText: "Off",
theme: null,
enhanced: false,
wrapperClass: null,
corners: true,
mini: false,
},
_create: function () {
if (!this.options.enhanced) {
this._enhance();
} else {
$.extend(this, {
flipswitch: this.element.parent(),
on: this.element.find(".ui-flipswitch-on").eq(0),
off: this.element.find(".ui-flipswitch-off").eq(0),
type: this.element.get(0).tagName,
});
}
this._handleFormReset();
// Transfer tabindex to "on" element and make input unfocusable
this._originalTabIndex = this.element.attr("tabindex");
if (this._originalTabIndex != null) {
this.on.attr("tabindex", this._originalTabIndex);
}
this.element.attr("tabindex", "-1");
this._on({
focus: "_handleInputFocus",
});
if (this.element.is(":disabled")) {
this._setOptions({
disabled: true,
});
}
this._on(this.flipswitch, {
click: "_toggle",
swipeleft: "_left",
swiperight: "_right",
});
this._on(this.on, {
keydown: "_keydown",
});
this._on({
change: "refresh",
});
},
_handleInputFocus: function () {
this.on.focus();
},
widget: function () {
return this.flipswitch;
},
_left: function () {
this.flipswitch.removeClass("ui-flipswitch-active");
if (this.type === "SELECT") {
this.element.get(0).selectedIndex = 0;
} else {
this.element.prop("checked", false);
}
this.element.trigger("change");
},
_right: function () {
this.flipswitch.addClass("ui-flipswitch-active");
if (this.type === "SELECT") {
this.element.get(0).selectedIndex = 1;
} else {
this.element.prop("checked", true);
}
this.element.trigger("change");
},
_enhance: function () {
var flipswitch = $("<div>"),
options = this.options,
element = this.element,
theme = options.theme ? options.theme : "inherit",
// The "on" button is an anchor so it's focusable
on = $("<a></a>", {
href: "#",
}),
off = $("<span></span>"),
type = element.get(0).tagName,
onText =
type === "INPUT"
? options.onText
: element.find("option").eq(1).text(),
offText =
type === "INPUT"
? options.offText
: element.find("option").eq(0).text();
on.addClass(
"ui-flipswitch-on ui-btn ui-shadow ui-btn-inherit",
).text(onText);
off.addClass("ui-flipswitch-off").text(offText);
flipswitch
.addClass(
"ui-flipswitch ui-shadow-inset " +
"ui-bar-" +
theme +
" " +
(options.wrapperClass ? options.wrapperClass : "") +
" " +
(element.is(":checked") ||
element.find("option").eq(1).is(":selected")
? "ui-flipswitch-active"
: "") +
(element.is(":disabled") ? " ui-state-disabled" : "") +
(options.corners ? " ui-corner-all" : "") +
(options.mini ? " ui-mini" : ""),
)
.append(on, off);
element
.addClass("ui-flipswitch-input")
.after(flipswitch)
.appendTo(flipswitch);
$.extend(this, {
flipswitch: flipswitch,
on: on,
off: off,
type: type,
});
},
_reset: function () {
this.refresh();
},
refresh: function () {
var direction,
existingDirection = this.flipswitch.hasClass(
"ui-flipswitch-active",
)
? "_right"
: "_left";
if (this.type === "SELECT") {
direction =
this.element.get(0).selectedIndex > 0 ? "_right" : "_left";
} else {
direction = this.element.prop("checked") ? "_right" : "_left";
}
if (direction !== existingDirection) {
this[direction]();
}
},
_toggle: function () {
var direction = this.flipswitch.hasClass("ui-flipswitch-active")
? "_left"
: "_right";
this[direction]();
},
_keydown: function (e) {
if (e.which === $.mobile.keyCode.LEFT) {
this._left();
} else if (e.which === $.mobile.keyCode.RIGHT) {
this._right();
} else if (e.which === $.mobile.keyCode.SPACE) {
this._toggle();
e.preventDefault();
}
},
_setOptions: function (options) {
if (options.theme !== undefined) {
var currentTheme = options.theme ? options.theme : "inherit",
newTheme = options.theme ? options.theme : "inherit";
this.widget()
.removeClass("ui-bar-" + currentTheme)
.addClass("ui-bar-" + newTheme);
}
if (options.onText !== undefined) {
this.on.text(options.onText);
}
if (options.offText !== undefined) {
this.off.text(options.offText);
}
if (options.disabled !== undefined) {
this.widget().toggleClass("ui-state-disabled", options.disabled);
}
if (options.mini !== undefined) {
this.widget().toggleClass("ui-mini", options.mini);
}
if (options.corners !== undefined) {
this.widget().toggleClass("ui-corner-all", options.corners);
}
this._super(options);
},
_destroy: function () {
if (this.options.enhanced) {
return;
}
if (this._originalTabIndex != null) {
this.element.attr("tabindex", this._originalTabIndex);
} else {
this.element.removeAttr("tabindex");
}
this.on.remove();
this.off.remove();
this.element.unwrap();
this.flipswitch.remove();
this.removeClass("ui-flipswitch-input");
},
},
$.mobile.behaviors.formReset,
),
);
})(jQuery);
(function ($, undefined) {
$.widget(
"mobile.rangeslider",
$.extend(
{
options: {
theme: null,
trackTheme: null,
corners: true,
mini: false,
highlight: true,
},
_create: function () {
var $el = this.element,
elClass = this.options.mini
? "ui-rangeslider ui-mini"
: "ui-rangeslider",
_inputFirst = $el.find("input").first(),
_inputLast = $el.find("input").last(),
_label = $el.find("label").first(),
_sliderWidgetFirst =
$.data(_inputFirst.get(0), "mobile-slider") ||
$.data(_inputFirst.slider().get(0), "mobile-slider"),
_sliderWidgetLast =
$.data(_inputLast.get(0), "mobile-slider") ||
$.data(_inputLast.slider().get(0), "mobile-slider"),
_sliderFirst = _sliderWidgetFirst.slider,
_sliderLast = _sliderWidgetLast.slider,
firstHandle = _sliderWidgetFirst.handle,
_sliders = $("<div class='ui-rangeslider-sliders' />").appendTo(
$el,
);
_inputFirst.addClass("ui-rangeslider-first");
_inputLast.addClass("ui-rangeslider-last");
$el.addClass(elClass);
_sliderFirst.appendTo(_sliders);
_sliderLast.appendTo(_sliders);
_label.insertBefore($el);
firstHandle.prependTo(_sliderLast);
$.extend(this, {
_inputFirst: _inputFirst,
_inputLast: _inputLast,
_sliderFirst: _sliderFirst,
_sliderLast: _sliderLast,
_label: _label,
_targetVal: null,
_sliderTarget: false,
_sliders: _sliders,
_proxy: false,
});
this.refresh();
this._on(this.element.find("input.ui-slider-input"), {
slidebeforestart: "_slidebeforestart",
slidestop: "_slidestop",
slidedrag: "_slidedrag",
slidebeforechange: "_change",
blur: "_change",
keyup: "_change",
});
this._on({
mousedown: "_change",
});
this._on(this.element.closest("form"), {
reset: "_handleReset",
});
this._on(firstHandle, {
vmousedown: "_dragFirstHandle",
});
},
_handleReset: function () {
var self = this;
//we must wait for the stack to unwind before updateing other wise sliders will not have updated yet
setTimeout(function () {
self._updateHighlight();
}, 0);
},
_dragFirstHandle: function (event) {
//if the first handle is dragged send the event to the first slider
$.data(this._inputFirst.get(0), "mobile-slider").dragging = true;
$.data(this._inputFirst.get(0), "mobile-slider").refresh(event);
return false;
},
_slidedrag: function (event) {
var first = $(event.target).is(this._inputFirst),
otherSlider = first ? this._inputLast : this._inputFirst;
this._sliderTarget = false;
//if the drag was initiated on an extreme and the other handle is focused send the events to
//the closest handle
if (
(this._proxy === "first" && first) ||
(this._proxy === "last" && !first)
) {
$.data(otherSlider.get(0), "mobile-slider").dragging = true;
$.data(otherSlider.get(0), "mobile-slider").refresh(event);
return false;
}
},
_slidestop: function (event) {
var first = $(event.target).is(this._inputFirst);
this._proxy = false;
//this stops dragging of the handle and brings the active track to the front
//this makes clicks on the track go the the last handle used
this.element.find("input").trigger("vmouseup");
this._sliderFirst.css("z-index", first ? 1 : "");
},
_slidebeforestart: function (event) {
this._sliderTarget = false;
//if the track is the target remember this and the original value
if ($(event.originalEvent.target).hasClass("ui-slider-track")) {
this._sliderTarget = true;
this._targetVal = $(event.target).val();
}
},
_setOptions: function (options) {
if (options.theme !== undefined) {
this._setTheme(options.theme);
}
if (options.trackTheme !== undefined) {
this._setTrackTheme(options.trackTheme);
}
if (options.mini !== undefined) {
this._setMini(options.mini);
}
if (options.highlight !== undefined) {
this._setHighlight(options.highlight);
}
this._super(options);
this.refresh();
},
refresh: function () {
var $el = this.element,
o = this.options;
if (
this._inputFirst.is(":disabled") ||
this._inputLast.is(":disabled")
) {
this.options.disabled = true;
}
$el
.find("input")
.slider({
theme: o.theme,
trackTheme: o.trackTheme,
disabled: o.disabled,
corners: o.corners,
mini: o.mini,
highlight: o.highlight,
})
.slider("refresh");
this._updateHighlight();
},
_change: function (event) {
if (event.type === "keyup") {
this._updateHighlight();
return false;
}
var self = this,
min = parseFloat(this._inputFirst.val(), 10),
max = parseFloat(this._inputLast.val(), 10),
first = $(event.target).hasClass("ui-rangeslider-first"),
thisSlider = first ? this._inputFirst : this._inputLast,
otherSlider = first ? this._inputLast : this._inputFirst;
if (
this._inputFirst.val() > this._inputLast.val() &&
event.type === "mousedown" &&
!$(event.target).hasClass("ui-slider-handle")
) {
thisSlider.blur();
} else if (event.type === "mousedown") {
return;
}
if (min > max && !this._sliderTarget) {
//this prevents min from being greater then max
thisSlider.val(first ? max : min).slider("refresh");
this._trigger("normalize");
} else if (min > max) {
//this makes it so clicks on the target on either extreme go to the closest handle
thisSlider.val(this._targetVal).slider("refresh");
//You must wait for the stack to unwind so first slider is updated before updating second
setTimeout(function () {
otherSlider.val(first ? min : max).slider("refresh");
$.data(otherSlider.get(0), "mobile-slider").handle.focus();
self._sliderFirst.css("z-index", first ? "" : 1);
self._trigger("normalize");
}, 0);
this._proxy = first ? "first" : "last";
}
//fixes issue where when both _sliders are at min they cannot be adjusted
if (min === max) {
$.data(thisSlider.get(0), "mobile-slider").handle.css(
"z-index",
1,
);
$.data(otherSlider.get(0), "mobile-slider").handle.css(
"z-index",
0,
);
} else {
$.data(otherSlider.get(0), "mobile-slider").handle.css(
"z-index",
"",
);
$.data(thisSlider.get(0), "mobile-slider").handle.css(
"z-index",
"",
);
}
this._updateHighlight();
if (min >= max) {
return false;
}
},
_updateHighlight: function () {
var min = parseInt(
$.data(this._inputFirst.get(0), "mobile-slider").handle.get(0)
.style.left,
10,
),
max = parseInt(
$.data(this._inputLast.get(0), "mobile-slider").handle.get(0)
.style.left,
10,
),
width = max - min;
this.element.find(".ui-slider-bg").css({
"margin-left": min + "%",
width: width + "%",
});
},
_setTheme: function (value) {
this._inputFirst.slider("option", "theme", value);
this._inputLast.slider("option", "theme", value);
},
_setTrackTheme: function (value) {
this._inputFirst.slider("option", "trackTheme", value);
this._inputLast.slider("option", "trackTheme", value);
},
_setMini: function (value) {
this._inputFirst.slider("option", "mini", value);
this._inputLast.slider("option", "mini", value);
this.element.toggleClass("ui-mini", !!value);
},
_setHighlight: function (value) {
this._inputFirst.slider("option", "highlight", value);
this._inputLast.slider("option", "highlight", value);
},
_destroy: function () {
this._label.prependTo(this.element);
this.element.removeClass("ui-rangeslider ui-mini");
this._inputFirst.after(this._sliderFirst);
this._inputLast.after(this._sliderLast);
this._sliders.remove();
this.element
.find("input")
.removeClass("ui-rangeslider-first ui-rangeslider-last")
.slider("destroy");
},
},
$.mobile.behaviors.formReset,
),
);
})(jQuery);
(function ($, undefined) {
$.widget("mobile.textinput", $.mobile.textinput, {
options: {
clearBtn: false,
clearBtnText: "Clear text",
},
_create: function () {
this._super();
if (!!this.options.clearBtn || this.isSearch) {
this._addClearBtn();
}
},
clearButton: function () {
return $(
"<a href='#' class='ui-input-clear ui-btn ui-icon-delete ui-btn-icon-notext ui-corner-all" +
"' title='" +
this.options.clearBtnText +
"'>" +
this.options.clearBtnText +
"</a>",
);
},
_clearBtnClick: function (event) {
this.element.val("").focus().trigger("change");
this._clearBtn.addClass("ui-input-clear-hidden");
event.preventDefault();
},
_addClearBtn: function () {
if (!this.options.enhanced) {
this._enhanceClear();
}
$.extend(this, {
_clearBtn: this.widget().find("a.ui-input-clear"),
});
this._bindClearEvents();
this._toggleClear();
},
_enhanceClear: function () {
this.clearButton().appendTo(this.widget());
this.widget().addClass("ui-input-has-clear");
},
_bindClearEvents: function () {
this._on(this._clearBtn, {
click: "_clearBtnClick",
});
this._on({
keyup: "_toggleClear",
change: "_toggleClear",
input: "_toggleClear",
focus: "_toggleClear",
blur: "_toggleClear",
cut: "_toggleClear",
paste: "_toggleClear",
});
},
_unbindClear: function () {
this._off(this._clearBtn, "click");
this._off(this.element, "keyup change input focus blur cut paste");
},
_setOptions: function (options) {
this._super(options);
if (
options.clearBtn !== undefined &&
!this.element.is("textarea, :jqmData(type='range')")
) {
if (options.clearBtn) {
this._addClearBtn();
} else {
this._destroyClear();
}
}
if (
options.clearBtnText !== undefined &&
this._clearBtn !== undefined
) {
this._clearBtn
.text(options.clearBtnText)
.attr("title", options.clearBtnText);
}
},
_toggleClear: function () {
this._delay("_toggleClearClass", 0);
},
_toggleClearClass: function () {
this._clearBtn.toggleClass(
"ui-input-clear-hidden",
!this.element.val(),
);
},
_destroyClear: function () {
this.widget().removeClass("ui-input-has-clear");
this._unbindClear();
this._clearBtn.remove();
},
_destroy: function () {
this._super();
this._destroyClear();
},
});
})(jQuery);
(function ($, undefined) {
$.widget("mobile.textinput", $.mobile.textinput, {
options: {
autogrow: true,
keyupTimeoutBuffer: 100,
},
_create: function () {
this._super();
if (this.options.autogrow && this.isTextarea) {
this._autogrow();
}
},
_autogrow: function () {
this.element.addClass("ui-textinput-autogrow");
this._on({
keyup: "_timeout",
change: "_timeout",
input: "_timeout",
paste: "_timeout",
});
// Attach to the various you-have-become-visible notifications that the
// various framework elements emit.
// TODO: Remove all but the updatelayout handler once #6426 is fixed.
this._on(true, this.document, {
// TODO: Move to non-deprecated event
pageshow: "_handleShow",
popupbeforeposition: "_handleShow",
updatelayout: "_handleShow",
panelopen: "_handleShow",
});
},
// Synchronously fix the widget height if this widget's parents are such
// that they show/hide content at runtime. We still need to check whether
// the widget is actually visible in case it is contained inside multiple
// such containers. For example: panel contains collapsible contains
// autogrow textinput. The panel may emit "panelopen" indicating that its
// content has become visible, but the collapsible is still collapsed, so
// the autogrow textarea is still not visible.
_handleShow: function (event) {
if (
$.contains(event.target, this.element[0]) &&
this.element.is(":visible")
) {
if (event.type !== "popupbeforeposition") {
this.element
.addClass("ui-textinput-autogrow-resize")
.animationComplete(
$.proxy(function () {
this.element.removeClass("ui-textinput-autogrow-resize");
}, this),
"transition",
);
}
this._timeout();
}
},
_unbindAutogrow: function () {
this.element.removeClass("ui-textinput-autogrow");
this._off(this.element, "keyup change input paste");
this._off(
this.document,
"pageshow popupbeforeposition updatelayout panelopen",
);
},
keyupTimeout: null,
_prepareHeightUpdate: function (delay) {
if (this.keyupTimeout) {
clearTimeout(this.keyupTimeout);
}
if (delay === undefined) {
this._updateHeight();
} else {
this.keyupTimeout = this._delay("_updateHeight", delay);
}
},
_timeout: function () {
this._prepareHeightUpdate(this.options.keyupTimeoutBuffer);
},
_updateHeight: function () {
var paddingTop,
paddingBottom,
paddingHeight,
scrollHeight,
clientHeight,
borderTop,
borderBottom,
borderHeight,
height,
scrollTop = this.window.scrollTop();
this.keyupTimeout = 0;
// IE8 textareas have the onpage property - others do not
if (!("onpage" in this.element[0])) {
this.element.css({
height: 0,
"min-height": 0,
"max-height": 0,
});
}
scrollHeight = this.element[0].scrollHeight;
clientHeight = this.element[0].clientHeight;
borderTop = parseFloat(this.element.css("border-top-width"));
borderBottom = parseFloat(this.element.css("border-bottom-width"));
borderHeight = borderTop + borderBottom;
height = scrollHeight + borderHeight + 15;
// Issue 6179: Padding is not included in scrollHeight and
// clientHeight by Firefox if no scrollbar is visible. Because
// textareas use the border-box box-sizing model, padding should be
// included in the new (assigned) height. Because the height is set
// to 0, clientHeight == 0 in Firefox. Therefore, we can use this to
// check if padding must be added.
if (clientHeight === 0) {
paddingTop = parseFloat(this.element.css("padding-top"));
paddingBottom = parseFloat(this.element.css("padding-bottom"));
paddingHeight = paddingTop + paddingBottom;
height += paddingHeight;
}
this.element.css({
height: height,
"min-height": "",
"max-height": "",
});
this.window.scrollTop(scrollTop);
},
refresh: function () {
if (this.options.autogrow && this.isTextarea) {
this._updateHeight();
}
},
_setOptions: function (options) {
this._super(options);
if (options.autogrow !== undefined && this.isTextarea) {
if (options.autogrow) {
this._autogrow();
} else {
this._unbindAutogrow();
}
}
},
});
})(jQuery);
(function ($, undefined) {
$.widget(
"mobile.selectmenu",
$.extend(
{
initSelector:
"select:not( :jqmData(role='slider')):not( :jqmData(role='flipswitch') )",
options: {
theme: null,
icon: "carat-d",
iconpos: "right",
inline: false,
corners: true,
shadow: true,
iconshadow: false /* TODO: Deprecated in 1.4, remove in 1.5. */,
overlayTheme: null,
dividerTheme: null,
hidePlaceholderMenuItems: true,
closeText: "Close",
nativeMenu: true,
// This option defaults to true on iOS devices.
preventFocusZoom:
/iPhone|iPad|iPod/.test(navigator.platform) &&
navigator.userAgent.indexOf("AppleWebKit") > -1,
mini: false,
},
_button: function () {
return $("<div/>");
},
_setDisabled: function (value) {
this.element.attr("disabled", value);
this.button.attr("aria-disabled", value);
return this._setOption("disabled", value);
},
_focusButton: function () {
var self = this;
setTimeout(function () {
self.button.focus();
}, 40);
},
_selectOptions: function () {
return this.select.find("option");
},
// setup items that are generally necessary for select menu extension
_preExtension: function () {
var inline = this.options.inline || this.element.jqmData("inline"),
mini = this.options.mini || this.element.jqmData("mini"),
classes = "";
// TODO: Post 1.1--once we have time to test thoroughly--any classes manually applied to the original element should be carried over to the enhanced element, with an `-enhanced` suffix. See https://github.com/jquery/jquery-mobile/issues/3577
/* if ( $el[0].className.length ) {
classes = $el[0].className;
} */
if (!!~this.element[0].className.indexOf("ui-btn-left")) {
classes = " ui-btn-left";
}
if (!!~this.element[0].className.indexOf("ui-btn-right")) {
classes = " ui-btn-right";
}
if (inline) {
classes += " ui-btn-inline";
}
if (mini) {
classes += " ui-mini";
}
this.select = this.element
.removeClass("ui-btn-left ui-btn-right")
.wrap("<div class='ui-select" + classes + "'>");
this.selectId = this.select.attr("id") || "select-" + this.uuid;
this.buttonId = this.selectId + "-button";
this.label = $("label[for='" + this.selectId + "']");
this.isMultiple = this.select[0].multiple;
},
_destroy: function () {
var wrapper = this.element.parents(".ui-select");
if (wrapper.length > 0) {
if (wrapper.is(".ui-btn-left, .ui-btn-right")) {
this.element.addClass(
wrapper.hasClass("ui-btn-left")
? "ui-btn-left"
: "ui-btn-right",
);
}
this.element.insertAfter(wrapper);
wrapper.remove();
}
},
_create: function () {
this._preExtension();
this.button = this._button();
var self = this,
options = this.options,
iconpos = options.icon
? options.iconpos || this.select.jqmData("iconpos")
: false,
button = this.button
.insertBefore(this.select)
.attr("id", this.buttonId)
.addClass(
"ui-btn" +
(options.icon
? " ui-icon-" +
options.icon +
" ui-btn-icon-" +
iconpos +
(options.iconshadow ? " ui-shadow-icon" : "")
: "") /* TODO: Remove in 1.5. */ +
(options.theme ? " ui-btn-" + options.theme : "") +
(options.corners ? " ui-corner-all" : "") +
(options.shadow ? " ui-shadow" : ""),
);
this.setButtonText();
// Opera does not properly support opacity on select elements
// In Mini, it hides the element, but not its text
// On the desktop,it seems to do the opposite
// for these reasons, using the nativeMenu option results in a full native select in Opera
if (options.nativeMenu && window.opera && window.opera.version) {
button.addClass("ui-select-nativeonly");
}
// Add counter for multi selects
if (this.isMultiple) {
this.buttonCount = $("<span>")
.addClass("ui-li-count ui-body-inherit")
.hide()
.appendTo(button.addClass("ui-li-has-count"));
}
// Disable if specified
if (options.disabled || this.element.attr("disabled")) {
this.disable();
}
// Events on native select
this.select.change(function () {
self.refresh();
if (!!options.nativeMenu) {
this.blur();
}
});
this._handleFormReset();
this._on(this.button, {
keydown: "_handleKeydown",
});
this.build();
},
build: function () {
var self = this;
this.select
.appendTo(self.button)
.bind("vmousedown", function () {
// Add active class to button
self.button.addClass($.mobile.activeBtnClass);
})
.bind("focus", function () {
self.button.addClass($.mobile.focusClass);
})
.bind("blur", function () {
self.button.removeClass($.mobile.focusClass);
})
.bind("focus vmouseover", function () {
self.button.trigger("vmouseover");
})
.bind("vmousemove", function () {
// Remove active class on scroll/touchmove
self.button.removeClass($.mobile.activeBtnClass);
})
.bind("change blur vmouseout", function () {
self.button
.trigger("vmouseout")
.removeClass($.mobile.activeBtnClass);
});
// In many situations, iOS will zoom into the select upon tap, this prevents that from happening
self.button.bind("vmousedown", function () {
if (self.options.preventFocusZoom) {
$.mobile.zoom.disable(true);
}
});
self.label.bind("click focus", function () {
if (self.options.preventFocusZoom) {
$.mobile.zoom.disable(true);
}
});
self.select.bind("focus", function () {
if (self.options.preventFocusZoom) {
$.mobile.zoom.disable(true);
}
});
self.button.bind("mouseup", function () {
if (self.options.preventFocusZoom) {
setTimeout(function () {
$.mobile.zoom.enable(true);
}, 0);
}
});
self.select.bind("blur", function () {
if (self.options.preventFocusZoom) {
$.mobile.zoom.enable(true);
}
});
},
selected: function () {
return this._selectOptions().filter(":selected");
},
selectedIndices: function () {
var self = this;
return this.selected()
.map(function () {
return self._selectOptions().index(this);
})
.get();
},
setButtonText: function () {
var self = this,
selected = this.selected(),
text = this.placeholder,
span = $(document.createElement("span"));
this.button
.children("span")
.not(".ui-li-count")
.remove()
.end()
.end()
.prepend(
(function () {
if (selected.length) {
text = selected
.map(function () {
return $(this).text();
})
.get()
.join(", ");
} else {
text = self.placeholder;
}
if (text) {
span.text(text);
} else {
// Set the contents to &nbsp; which we write as &#160; to be XHTML compliant - see gh-6699
span.html("&#160;");
}
// TODO possibly aggregate multiple select option classes
return span
.addClass(self.select.attr("class"))
.addClass(selected.attr("class"))
.removeClass("ui-screen-hidden");
})(),
);
},
setButtonCount: function () {
var selected = this.selected();
// multiple count inside button
if (this.isMultiple) {
this.buttonCount[selected.length > 1 ? "show" : "hide"]().text(
selected.length,
);
}
},
_handleKeydown: function (/* event */) {
this._delay("_refreshButton");
},
_reset: function () {
this.refresh();
},
_refreshButton: function () {
this.setButtonText();
this.setButtonCount();
},
refresh: function () {
this._refreshButton();
},
// open and close preserved in native selects
// to simplify users code when looping over selects
open: $.noop,
close: $.noop,
disable: function () {
this._setDisabled(true);
this.button.addClass("ui-state-disabled");
},
enable: function () {
this._setDisabled(false);
this.button.removeClass("ui-state-disabled");
},
},
$.mobile.behaviors.formReset,
),
);
})(jQuery);
(function ($, undefined) {
$.mobile.links = function (target) {
//links within content areas, tests included with page
$(target)
.find("a")
.jqmEnhanceable()
.filter(":jqmData(rel='popup')[href][href!='']")
.each(function () {
// Accessibility info for popups
var element = this,
idref = element.getAttribute("href").substring(1);
if (idref) {
element.setAttribute("aria-haspopup", true);
element.setAttribute("aria-owns", idref);
element.setAttribute("aria-expanded", false);
}
})
.end()
.not(".ui-btn, :jqmData(role='none'), :jqmData(role='nojs')")
.addClass("ui-link");
};
})(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(),
};
}
$.widget("mobile.popup", {
options: {
wrapperClass: null,
theme: null,
overlayTheme: null,
shadow: true,
corners: true,
transition: "none",
positionTo: "origin",
tolerance: null,
closeLinkSelector: "a:jqmData(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,
},
_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;
// 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, { vclick: "_eatEventAndClose" });
this._on(this.window, {
orientationchange: $.proxy(this, "_handleWindowOrientationchange"),
resize: $.proxy(this, "_handleWindowResize"),
keyup: $.proxy(this, "_handleWindowKeyUp"),
});
this._on(this.document, { focusin: "_handleDocumentFocusIn" });
},
_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);
}
},
_handleWindowKeyUp: function (theEvent) {
if (this._isOpen && theEvent.keyCode === $.mobile.keyCode.ESCAPE) {
return this._eatEventAndClose(theEvent);
}
},
_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 (0 === target.parents().filter(ui.container[0]).length) {
$(this.document[0].activeElement).one(
"focus",
function (/* theEvent */) {
target.blur();
},
);
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();
}
});
$.when(prerequisites.screen, prerequisites.container).done(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);
}
},
_openPrerequisitesComplete: function () {
var id = this.element.attr("id");
this._ui.container.addClass("ui-popup-active");
this._isOpen = true;
this._resizeScreen();
this._ui.container.attr("tabindex", "0").focus();
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");
container.removeAttr("tabindex");
// 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.jqmData("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;
}
// 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
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.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.jqmData("transition"),
positionTo: $link.jqmData("position-to"),
});
}
//remove after delay
setTimeout(function () {
$link.removeClass($.mobile.activeBtnClass);
}, 300);
};
// TODO move inside _create
$.mobile.document.on("pagebeforechange", function (theEvent, data) {
if (data.options.role === "popup") {
$.mobile.popup.handleLink(data.options.link);
theEvent.preventDefault();
}
});
})(jQuery);
/*
* custom "selectmenu" plugin
*/
(function ($, undefined) {
var unfocusableItemSelector =
".ui-disabled,.ui-state-disabled,.ui-li-divider,.ui-screen-hidden,:jqmData(role='placeholder')",
goToAdjacentItem = function (item, target, direction) {
var adjacent = item[direction + "All"]()
.not(unfocusableItemSelector)
.first();
// if there's a previous option, focus it
if (adjacent.length) {
target.blur().attr("tabindex", "-1");
adjacent.find("a").first().focus();
}
};
$.widget("mobile.selectmenu", $.mobile.selectmenu, {
_create: function () {
var o = this.options;
// Custom selects cannot exist inside popups, so revert the "nativeMenu"
// option to true if a parent is a popup
o.nativeMenu =
o.nativeMenu ||
this.element.parents(":jqmData(role='popup'),:mobile-popup").length >
0;
return this._super();
},
_handleSelectFocus: function () {
this.element.blur();
this.button.focus();
},
_handleKeydown: function (event) {
this._super(event);
this._handleButtonVclickKeydown(event);
},
_handleButtonVclickKeydown: function (event) {
if (this.options.disabled || this.isOpen) {
return;
}
if (
event.type === "vclick" ||
(event.keyCode &&
(event.keyCode === $.mobile.keyCode.ENTER ||
event.keyCode === $.mobile.keyCode.SPACE))
) {
this._decideFormat();
if (this.menuType === "overlay") {
this.button
.attr("href", "#" + this.popupId)
.attr("data-" + ($.mobile.ns || "") + "rel", "popup");
} else {
this.button
.attr("href", "#" + this.dialogId)
.attr("data-" + ($.mobile.ns || "") + "rel", "dialog");
}
this.isOpen = true;
// Do not prevent default, so the navigation may have a chance to actually open the chosen format
}
},
_handleListFocus: function (e) {
var params =
e.type === "focusin"
? { tabindex: "0", event: "vmouseover" }
: { tabindex: "-1", event: "vmouseout" };
$(e.target).attr("tabindex", params.tabindex).trigger(params.event);
},
_handleListKeydown: function (event) {
var target = $(event.target),
li = target.closest("li");
// switch logic based on which key was pressed
switch (event.keyCode) {
// up or left arrow keys
case 38:
goToAdjacentItem(li, target, "prev");
return false;
// down or right arrow keys
case 40:
goToAdjacentItem(li, target, "next");
return false;
// If enter or space is pressed, trigger click
case 13:
case 32:
target.trigger("click");
return false;
}
},
_handleMenuPageHide: function () {
// TODO centralize page removal binding / handling in the page plugin.
// Suggestion from @jblas to do refcounting
//
// TODO extremely confusing dependency on the open method where the pagehide.remove
// bindings are stripped to prevent the parent page from disappearing. The way
// we're keeping pages in the DOM right now sucks
//
// rebind the page remove that was unbound in the open function
// to allow for the parent page removal from actions other than the use
// of a dialog sized custom select
//
// doing this here provides for the back button on the custom select dialog
this.thisPage.page("bindRemove");
},
_handleHeaderCloseClick: function () {
if (this.menuType === "overlay") {
this.close();
return false;
}
},
build: function () {
var selectId,
popupId,
dialogId,
label,
thisPage,
isMultiple,
menuId,
themeAttr,
overlayTheme,
overlayThemeAttr,
dividerThemeAttr,
menuPage,
listbox,
list,
header,
headerTitle,
menuPageContent,
menuPageClose,
headerClose,
self,
o = this.options;
if (o.nativeMenu) {
return this._super();
}
self = this;
selectId = this.selectId;
popupId = selectId + "-listbox";
dialogId = selectId + "-dialog";
label = this.label;
thisPage = this.element.closest(".ui-page");
isMultiple = this.element[0].multiple;
menuId = selectId + "-menu";
themeAttr = o.theme
? " data-" + $.mobile.ns + "theme='" + o.theme + "'"
: "";
overlayTheme = o.overlayTheme || o.theme || null;
overlayThemeAttr = overlayTheme
? " data-" + $.mobile.ns + "overlay-theme='" + overlayTheme + "'"
: "";
dividerThemeAttr =
o.dividerTheme && isMultiple
? " data-" + $.mobile.ns + "divider-theme='" + o.dividerTheme + "'"
: "";
menuPage = $(
"<div data-" +
$.mobile.ns +
"role='dialog' class='ui-selectmenu' id='" +
dialogId +
"'" +
themeAttr +
overlayThemeAttr +
">" +
"<div data-" +
$.mobile.ns +
"role='header'>" +
"<div class='ui-title'>" +
label.getEncodedText() +
"</div>" +
"</div>" +
"<div data-" +
$.mobile.ns +
"role='content'></div>" +
"</div>",
);
listbox = $("<div id='" + popupId + "' class='ui-selectmenu'></div>")
.insertAfter(this.select)
.popup({ theme: o.overlayTheme });
list = $(
"<ul class='ui-selectmenu-list' id='" +
menuId +
"' role='listbox' aria-labelledby='" +
this.buttonId +
"'" +
themeAttr +
dividerThemeAttr +
"></ul>",
).appendTo(listbox);
header = $(
"<div class='ui-header ui-bar-" +
(o.theme ? o.theme : "inherit") +
"'></div>",
).prependTo(listbox);
headerTitle = $("<h1 class='ui-title'></h1>").appendTo(header);
if (this.isMultiple) {
headerClose = $("<a>", {
role: "button",
text: o.closeText,
href: "#",
class:
"ui-btn ui-corner-all ui-btn-left ui-btn-icon-notext ui-icon-delete",
}).appendTo(header);
}
$.extend(this, {
selectId: selectId,
menuId: menuId,
popupId: popupId,
dialogId: dialogId,
thisPage: thisPage,
menuPage: menuPage,
label: label,
isMultiple: isMultiple,
theme: o.theme,
listbox: listbox,
list: list,
header: header,
headerTitle: headerTitle,
headerClose: headerClose,
menuPageContent: menuPageContent,
menuPageClose: menuPageClose,
placeholder: "",
});
// Create list from select, update state
this.refresh();
if (this._origTabIndex === undefined) {
// Map undefined to false, because this._origTabIndex === undefined
// indicates that we have not yet checked whether the select has
// originally had a tabindex attribute, whereas false indicates that
// we have checked the select for such an attribute, and have found
// none present.
this._origTabIndex =
this.select[0].getAttribute("tabindex") === null
? false
: this.select.attr("tabindex");
}
this.select.attr("tabindex", "-1");
this._on(this.select, { focus: "_handleSelectFocus" });
// Button events
this._on(this.button, {
vclick: "_handleButtonVclickKeydown",
});
// Events for list items
this.list.attr("role", "listbox");
this._on(this.list, {
focusin: "_handleListFocus",
focusout: "_handleListFocus",
keydown: "_handleListKeydown",
});
this.list.delegate(
"li:not(.ui-disabled,.ui-state-disabled,.ui-li-divider)",
"click",
function (event) {
// index of option tag to be selected
var oldIndex = self.select[0].selectedIndex,
newIndex = $.mobile.getAttribute(this, "option-index"),
option = self._selectOptions().eq(newIndex)[0];
// toggle selected status on the tag for multi selects
option.selected = self.isMultiple ? !option.selected : true;
// toggle checkbox class for multiple selects
if (self.isMultiple) {
$(this)
.find("a")
.toggleClass("ui-checkbox-on", option.selected)
.toggleClass("ui-checkbox-off", !option.selected);
}
// trigger change if value changed
if (self.isMultiple || oldIndex !== newIndex) {
self.select.trigger("change");
}
// hide custom select for single selects only - otherwise focus clicked item
// We need to grab the clicked item the hard way, because the list may have been rebuilt
if (self.isMultiple) {
self.list
.find("li:not(.ui-li-divider)")
.eq(newIndex)
.find("a")
.first()
.focus();
} else {
self.close();
}
event.preventDefault();
},
);
// button refocus ensures proper height calculation
// by removing the inline style and ensuring page inclusion
this._on(this.menuPage, { pagehide: "_handleMenuPageHide" });
// Events on the popup
this._on(this.listbox, { popupafterclose: "close" });
// Close button on small overlays
if (this.isMultiple) {
this._on(this.headerClose, { click: "_handleHeaderCloseClick" });
}
return this;
},
_isRebuildRequired: function () {
var list = this.list.find("li"),
options = this._selectOptions().not(".ui-screen-hidden");
// TODO exceedingly naive method to determine difference
// ignores value changes etc in favor of a forcedRebuild
// from the user in the refresh method
return options.text() !== list.text();
},
selected: function () {
return this._selectOptions().filter(
":selected:not( :jqmData(placeholder='true') )",
);
},
refresh: function (force) {
var self, indices;
if (this.options.nativeMenu) {
return this._super(force);
}
self = this;
if (force || this._isRebuildRequired()) {
self._buildList();
}
indices = this.selectedIndices();
self.setButtonText();
self.setButtonCount();
self.list
.find("li:not(.ui-li-divider)")
.find("a")
.removeClass($.mobile.activeBtnClass)
.end()
.attr("aria-selected", false)
.each(function (i) {
if ($.inArray(i, indices) > -1) {
var item = $(this);
// Aria selected attr
item.attr("aria-selected", true);
// Multiple selects: add the "on" checkbox state to the icon
if (self.isMultiple) {
item
.find("a")
.removeClass("ui-checkbox-off")
.addClass("ui-checkbox-on");
} else {
if (item.hasClass("ui-screen-hidden")) {
item.next().find("a").addClass($.mobile.activeBtnClass);
} else {
item.find("a").addClass($.mobile.activeBtnClass);
}
}
}
});
},
close: function () {
if (this.options.disabled || !this.isOpen) {
return;
}
var self = this;
if (self.menuType === "page") {
self.menuPage.dialog("close");
self.list.appendTo(self.listbox);
} else {
self.listbox.popup("close");
}
self._focusButton();
// allow the dialog to be closed again
self.isOpen = false;
},
open: function () {
this.button.click();
},
_focusMenuItem: function () {
var selector = this.list.find("a." + $.mobile.activeBtnClass);
if (selector.length === 0) {
selector = this.list.find(
"li:not(" + unfocusableItemSelector + ") a.ui-btn",
);
}
selector.first().focus();
},
_decideFormat: function () {
var self = this,
$window = this.window,
selfListParent = self.list.parent(),
menuHeight = selfListParent.outerHeight(),
scrollTop = $window.scrollTop(),
btnOffset = self.button.offset().top,
screenHeight = $window.height();
if (menuHeight > screenHeight - 80 || !$.support.scrollTop) {
self.menuPage.appendTo($.mobile.pageContainer).page();
self.menuPageContent = self.menuPage.find(".ui-content");
self.menuPageClose = self.menuPage.find(".ui-header a");
// prevent the parent page from being removed from the DOM,
// otherwise the results of selecting a list item in the dialog
// fall into a black hole
self.thisPage.unbind("pagehide.remove");
//for WebOS/Opera Mini (set lastscroll using button offset)
if (scrollTop === 0 && btnOffset > screenHeight) {
self.thisPage.one("pagehide", function () {
$(this).jqmData("lastScroll", btnOffset);
});
}
self.menuPage.one({
pageshow: $.proxy(this, "_focusMenuItem"),
pagehide: $.proxy(this, "close"),
});
self.menuType = "page";
self.menuPageContent.append(self.list);
self.menuPage.find("div .ui-title").text(self.label.text());
} else {
self.menuType = "overlay";
self.listbox.one({ popupafteropen: $.proxy(this, "_focusMenuItem") });
}
},
_buildList: function () {
var self = this,
o = this.options,
placeholder = this.placeholder,
needPlaceholder = true,
dataIcon = "false",
$options,
numOptions,
select,
dataPrefix = "data-" + $.mobile.ns,
dataIndexAttr = dataPrefix + "option-index",
dataIconAttr = dataPrefix + "icon",
dataRoleAttr = dataPrefix + "role",
dataPlaceholderAttr = dataPrefix + "placeholder",
fragment = document.createDocumentFragment(),
isPlaceholderItem = false,
optGroup,
i,
option,
$option,
parent,
text,
anchor,
classes,
optLabel,
divider,
item;
self.list.empty().filter(".ui-listview").listview("destroy");
$options = this._selectOptions();
numOptions = $options.length;
select = this.select[0];
for (i = 0; i < numOptions; i++, isPlaceholderItem = false) {
option = $options[i];
$option = $(option);
// Do not create options based on ui-screen-hidden select options
if ($option.hasClass("ui-screen-hidden")) {
continue;
}
parent = option.parentNode;
text = $option.text();
anchor = document.createElement("a");
classes = [];
anchor.setAttribute("href", "#");
anchor.appendChild(document.createTextNode(text));
// Are we inside an optgroup?
if (
parent !== select &&
parent.nodeName.toLowerCase() === "optgroup"
) {
optLabel = parent.getAttribute("label");
if (optLabel !== optGroup) {
divider = document.createElement("li");
divider.setAttribute(dataRoleAttr, "list-divider");
divider.setAttribute("role", "option");
divider.setAttribute("tabindex", "-1");
divider.appendChild(document.createTextNode(optLabel));
fragment.appendChild(divider);
optGroup = optLabel;
}
}
if (
needPlaceholder &&
(!option.getAttribute("value") ||
text.length === 0 ||
$option.jqmData("placeholder"))
) {
needPlaceholder = false;
isPlaceholderItem = true;
// If we have identified a placeholder, record the fact that it was
// us who have added the placeholder to the option and mark it
// retroactively in the select as well
if (null === option.getAttribute(dataPlaceholderAttr)) {
this._removePlaceholderAttr = true;
}
option.setAttribute(dataPlaceholderAttr, true);
if (o.hidePlaceholderMenuItems) {
classes.push("ui-screen-hidden");
}
if (placeholder !== text) {
placeholder = self.placeholder = text;
}
}
item = document.createElement("li");
if (option.disabled) {
classes.push("ui-state-disabled");
item.setAttribute("aria-disabled", true);
}
item.setAttribute(dataIndexAttr, i);
item.setAttribute(dataIconAttr, dataIcon);
if (isPlaceholderItem) {
item.setAttribute(dataPlaceholderAttr, true);
}
item.className = classes.join(" ");
item.setAttribute("role", "option");
anchor.setAttribute("tabindex", "-1");
if (this.isMultiple) {
$(anchor).addClass("ui-btn ui-checkbox-off ui-btn-icon-right");
}
item.appendChild(anchor);
fragment.appendChild(item);
}
self.list[0].appendChild(fragment);
// Hide header if it's not a multiselect and there's no placeholder
if (!this.isMultiple && !placeholder.length) {
this.header.addClass("ui-screen-hidden");
} else {
this.headerTitle.text(this.placeholder);
}
// Now populated, create listview
self.list.listview();
},
_button: function () {
return this.options.nativeMenu
? this._super()
: $("<a>", {
href: "#",
role: "button",
// TODO value is undefined at creation
id: this.buttonId,
"aria-haspopup": "true",
// TODO value is undefined at creation
"aria-owns": this.menuId,
});
},
_destroy: function () {
if (!this.options.nativeMenu) {
this.close();
// Restore the tabindex attribute to its original value
if (this._origTabIndex !== undefined) {
if (this._origTabIndex !== false) {
this.select.attr("tabindex", this._origTabIndex);
} else {
this.select.removeAttr("tabindex");
}
}
// Remove the placeholder attribute if we were the ones to add it
if (this._removePlaceholderAttr) {
this._selectOptions().removeAttr(
"data-" + $.mobile.ns + "placeholder",
);
}
// Remove the popup
this.listbox.remove();
// Remove the dialog
this.menuPage.remove();
}
// Chain up
this._super();
},
});
})(jQuery);
// buttonMarkup is deprecated as of 1.4.0 and will be removed in 1.5.0.
(function ($, undefined) {
// General policy: Do not access data-* attributes except during enhancement.
// In all other cases we determine the state of the button exclusively from its
// className. That's why optionsToClasses expects a full complement of options,
// and the jQuery plugin completes the set of options from the default values.
// Map classes to buttonMarkup boolean options - used in classNameToOptions()
var reverseBoolOptionMap = {
"ui-shadow": "shadow",
"ui-corner-all": "corners",
"ui-btn-inline": "inline",
"ui-shadow-icon": "iconshadow" /* TODO: Remove in 1.5 */,
"ui-mini": "mini",
},
getAttrFixed = function () {
var ret = $.mobile.getAttribute.apply(this, arguments);
return ret == null ? undefined : ret;
},
capitalLettersRE = /[A-Z]/g;
// optionsToClasses:
// @options: A complete set of options to convert to class names.
// @existingClasses: extra classes to add to the result
//
// Converts @options to buttonMarkup classes and returns the result as an array
// that can be converted to an element's className with .join( " " ). All
// possible options must be set inside @options. Use $.fn.buttonMarkup.defaults
// to get a complete set and use $.extend to override your choice of options
// from that set.
function optionsToClasses(options, existingClasses) {
var classes = existingClasses ? existingClasses : [];
// Add classes to the array - first ui-btn
classes.push("ui-btn");
// If there is a theme
if (options.theme) {
classes.push("ui-btn-" + options.theme);
}
// If there's an icon, add the icon-related classes
if (options.icon) {
classes = classes.concat([
"ui-icon-" + options.icon,
"ui-btn-icon-" + options.iconpos,
]);
if (options.iconshadow) {
classes.push("ui-shadow-icon"); /* TODO: Remove in 1.5 */
}
}
// Add the appropriate class for each boolean option
if (options.inline) {
classes.push("ui-btn-inline");
}
if (options.shadow) {
classes.push("ui-shadow");
}
if (options.corners) {
classes.push("ui-corner-all");
}
if (options.mini) {
classes.push("ui-mini");
}
// Create a string from the array and return it
return classes;
}
// classNameToOptions:
// @classes: A string containing a .className-style space-separated class list
//
// Loops over @classes and calculates an options object based on the
// buttonMarkup-related classes it finds. It records unrecognized classes in an
// array.
//
// Returns: An object containing the following items:
//
// "options": buttonMarkup options found to be present because of the
// presence/absence of corresponding classes
//
// "unknownClasses": a string containing all the non-buttonMarkup-related
// classes found in @classes
//
// "alreadyEnhanced": A boolean indicating whether the ui-btn class was among
// those found to be present
function classNameToOptions(classes) {
var idx,
map,
unknownClass,
alreadyEnhanced = false,
noIcon = true,
o = {
icon: "",
inline: false,
shadow: false,
corners: false,
iconshadow: false,
mini: false,
},
unknownClasses = [];
classes = classes.split(" ");
// Loop over the classes
for (idx = 0; idx < classes.length; idx++) {
// Assume it's an unrecognized class
unknownClass = true;
// Recognize boolean options from the presence of classes
map = reverseBoolOptionMap[classes[idx]];
if (map !== undefined) {
unknownClass = false;
o[map] = true;
// Recognize the presence of an icon and establish the icon position
} else if (classes[idx].indexOf("ui-btn-icon-") === 0) {
unknownClass = false;
noIcon = false;
o.iconpos = classes[idx].substring(12);
// Establish which icon is present
} else if (classes[idx].indexOf("ui-icon-") === 0) {
unknownClass = false;
o.icon = classes[idx].substring(8);
// Establish the theme - this recognizes one-letter theme swatch names
} else if (
classes[idx].indexOf("ui-btn-") === 0 &&
classes[idx].length === 8
) {
unknownClass = false;
o.theme = classes[idx].substring(7);
// Recognize that this element has already been buttonMarkup-enhanced
} else if (classes[idx] === "ui-btn") {
unknownClass = false;
alreadyEnhanced = true;
}
// If this class has not been recognized, add it to the list
if (unknownClass) {
unknownClasses.push(classes[idx]);
}
}
// If a "ui-btn-icon-*" icon position class is absent there cannot be an icon
if (noIcon) {
o.icon = "";
}
return {
options: o,
unknownClasses: unknownClasses,
alreadyEnhanced: alreadyEnhanced,
};
}
function camelCase2Hyphenated(c) {
return "-" + c.toLowerCase();
}
// $.fn.buttonMarkup:
// DOM: gets/sets .className
//
// @options: options to apply to the elements in the jQuery object
// @overwriteClasses: boolean indicating whether to honour existing classes
//
// Calculates the classes to apply to the elements in the jQuery object based on
// the options passed in. If @overwriteClasses is true, it sets the className
// property of each element in the jQuery object to the buttonMarkup classes
// it calculates based on the options passed in.
//
// If you wish to preserve any classes that are already present on the elements
// inside the jQuery object, including buttonMarkup-related classes that were
// added by a previous call to $.fn.buttonMarkup() or during page enhancement
// then you should omit @overwriteClasses or set it to false.
$.fn.buttonMarkup = function (options, overwriteClasses) {
var idx,
data,
el,
retrievedOptions,
optionKey,
defaults = $.fn.buttonMarkup.defaults;
for (idx = 0; idx < this.length; idx++) {
el = this[idx];
data = overwriteClasses
? // Assume this element is not enhanced and ignore its classes
{ alreadyEnhanced: false, unknownClasses: [] }
: // Otherwise analyze existing classes to establish existing options and
// classes
classNameToOptions(el.className);
retrievedOptions = $.extend(
{},
// If the element already has the class ui-btn, then we assume that
// it has passed through buttonMarkup before - otherwise, the options
// returned by classNameToOptions do not correctly reflect the state of
// the element
data.alreadyEnhanced ? data.options : {},
// Finally, apply the options passed in
options,
);
// If this is the first call on this element, retrieve remaining options
// from the data-attributes
if (!data.alreadyEnhanced) {
for (optionKey in defaults) {
if (retrievedOptions[optionKey] === undefined) {
retrievedOptions[optionKey] = getAttrFixed(
el,
optionKey.replace(capitalLettersRE, camelCase2Hyphenated),
);
}
}
}
el.className = optionsToClasses(
// Merge all the options and apply them as classes
$.extend(
{},
// The defaults form the basis
defaults,
// Add the computed options
retrievedOptions,
),
// ... and re-apply any unrecognized classes that were found
data.unknownClasses,
).join(" ");
if (el.tagName.toLowerCase() !== "button") {
el.setAttribute("role", "button");
}
}
return this;
};
// buttonMarkup defaults. This must be a complete set, i.e., a value must be
// given here for all recognized options
$.fn.buttonMarkup.defaults = {
icon: "",
iconpos: "left",
theme: null,
inline: false,
shadow: true,
corners: true,
iconshadow: false /* TODO: Remove in 1.5. Option deprecated in 1.4. */,
mini: false,
};
$.extend($.fn.buttonMarkup, {
initSelector:
"a:jqmData(role='button'), .ui-bar > a, .ui-bar > :jqmData(role='controlgroup') > a, button",
});
})(jQuery);
(function ($, undefined) {
$.widget(
"mobile.controlgroup",
$.extend(
{
options: {
enhanced: false,
theme: null,
shadow: false,
corners: true,
excludeInvisible: true,
type: "vertical",
mini: false,
},
_create: function () {
var elem = this.element,
opts = this.options;
// Run buttonmarkup
if ($.fn.buttonMarkup) {
this.element.find($.fn.buttonMarkup.initSelector).buttonMarkup();
}
// Enhance child widgets
$.each(
this._childWidgets,
$.proxy(function (number, widgetName) {
if ($.mobile[widgetName]) {
this.element
.find($.mobile[widgetName].initSelector)
.not($.mobile.page.prototype.keepNativeSelector())
[widgetName]();
}
}, this),
);
$.extend(this, {
_ui: null,
_initialRefresh: true,
});
if (opts.enhanced) {
this._ui = {
groupLegend: elem.children(".ui-controlgroup-label").children(),
childWrapper: elem.children(".ui-controlgroup-controls"),
};
} else {
this._ui = this._enhance();
}
},
_childWidgets: ["checkboxradio", "selectmenu", "button"],
_themeClassFromOption: function (value) {
return value
? value === "none"
? ""
: "ui-group-theme-" + value
: "";
},
_enhance: function () {
var elem = this.element,
opts = this.options,
ui = {
groupLegend: elem.children("legend"),
childWrapper: elem
.addClass(
"ui-controlgroup " +
"ui-controlgroup-" +
(opts.type === "horizontal" ? "horizontal" : "vertical") +
" " +
this._themeClassFromOption(opts.theme) +
" " +
(opts.corners ? "ui-corner-all " : "") +
(opts.mini ? "ui-mini " : ""),
)
.wrapInner(
"<div " +
"class='ui-controlgroup-controls " +
(opts.shadow === true ? "ui-shadow" : "") +
"'></div>",
)
.children(),
};
if (ui.groupLegend.length > 0) {
$("<div role='heading' class='ui-controlgroup-label'></div>")
.append(ui.groupLegend)
.prependTo(elem);
}
return ui;
},
_init: function () {
this.refresh();
},
_setOptions: function (options) {
var callRefresh,
returnValue,
elem = this.element;
// Must have one of horizontal or vertical
if (options.type !== undefined) {
elem
.removeClass(
"ui-controlgroup-horizontal ui-controlgroup-vertical",
)
.addClass(
"ui-controlgroup-" +
(options.type === "horizontal" ? "horizontal" : "vertical"),
);
callRefresh = true;
}
if (options.theme !== undefined) {
elem
.removeClass(this._themeClassFromOption(this.options.theme))
.addClass(this._themeClassFromOption(options.theme));
}
if (options.corners !== undefined) {
elem.toggleClass("ui-corner-all", options.corners);
}
if (options.mini !== undefined) {
elem.toggleClass("ui-mini", options.mini);
}
if (options.shadow !== undefined) {
this._ui.childWrapper.toggleClass("ui-shadow", options.shadow);
}
if (options.excludeInvisible !== undefined) {
this.options.excludeInvisible = options.excludeInvisible;
callRefresh = true;
}
returnValue = this._super(options);
if (callRefresh) {
this.refresh();
}
return returnValue;
},
container: function () {
return this._ui.childWrapper;
},
refresh: function () {
var $el = this.container(),
els = $el.find(".ui-btn").not(".ui-slider-handle"),
create = this._initialRefresh;
if ($.mobile.checkboxradio) {
$el.find(":mobile-checkboxradio").checkboxradio("refresh");
}
this._addFirstLastClasses(
els,
this.options.excludeInvisible
? this._getVisibles(els, create)
: els,
create,
);
this._initialRefresh = false;
},
// Caveat: If the legend is not the first child of the controlgroup at enhance
// time, it will be after _destroy().
_destroy: function () {
var ui,
buttons,
opts = this.options;
if (opts.enhanced) {
return this;
}
ui = this._ui;
buttons = this.element
.removeClass(
"ui-controlgroup " +
"ui-controlgroup-horizontal ui-controlgroup-vertical ui-corner-all ui-mini " +
this._themeClassFromOption(opts.theme),
)
.find(".ui-btn")
.not(".ui-slider-handle");
this._removeFirstLastClasses(buttons);
ui.groupLegend.unwrap();
ui.childWrapper.children().unwrap();
},
},
$.mobile.behaviors.addFirstLastClasses,
),
);
})(jQuery);
(function ($, undefined) {
$.widget("mobile.toolbar", {
initSelector: ":jqmData(role='footer'), :jqmData(role='header')",
options: {
theme: null,
addBackBtn: false,
backBtnTheme: null,
backBtnText: "Back",
},
_create: function () {
var leftbtn,
rightbtn,
role = this.element.is(":jqmData(role='header')")
? "header"
: "footer",
page = this.element.closest(".ui-page");
if (page.length === 0) {
page = false;
this._on(this.document, {
pageshow: "refresh",
});
}
$.extend(this, {
role: role,
page: page,
leftbtn: leftbtn,
rightbtn: rightbtn,
});
this.element
.attr("role", role === "header" ? "banner" : "contentinfo")
.addClass("ui-" + role);
this.refresh();
this._setOptions(this.options);
},
_setOptions: function (o) {
if (o.addBackBtn !== undefined) {
if (
this.options.addBackBtn &&
this.role === "header" &&
$(".ui-page").length > 1 &&
this.page[0].getAttribute("data-" + $.mobile.ns + "url") !==
$.mobile.path.stripHash(location.hash) &&
!this.leftbtn
) {
this._addBackButton();
} else {
this.element.find(".ui-toolbar-back-btn").remove();
}
}
if (o.backBtnTheme != null) {
this.element
.find(".ui-toolbar-back-btn")
.addClass("ui-btn ui-btn-" + o.backBtnTheme);
}
if (o.backBtnText !== undefined) {
this.element
.find(".ui-toolbar-back-btn .ui-btn-text")
.text(o.backBtnText);
}
if (o.theme !== undefined) {
var currentTheme = this.options.theme
? this.options.theme
: "inherit",
newTheme = o.theme ? o.theme : "inherit";
this.element
.removeClass("ui-bar-" + currentTheme)
.addClass("ui-bar-" + newTheme);
}
this._super(o);
},
refresh: function () {
if (this.role === "header") {
this._addHeaderButtonClasses();
}
if (!this.page) {
this._setRelative();
if (this.role === "footer") {
this.element.appendTo("body");
}
}
this._addHeadingClasses();
this._btnMarkup();
},
//we only want this to run on non fixed toolbars so make it easy to override
_setRelative: function () {
$("[data-" + $.mobile.ns + "role='page']").css({
position: "relative",
});
},
// Deprecated in 1.4. As from 1.5 button classes have to be present in the markup.
_btnMarkup: function () {
this.element
.children("a")
.filter(":not([data-" + $.mobile.ns + "role='none'])")
.attr("data-" + $.mobile.ns + "role", "button");
this.element.trigger("create");
},
// Deprecated in 1.4. As from 1.5 ui-btn-left/right classes have to be present in the markup.
_addHeaderButtonClasses: function () {
var $headeranchors = this.element.children("a, button");
this.leftbtn = $headeranchors.hasClass("ui-btn-left");
this.rightbtn = $headeranchors.hasClass("ui-btn-right");
this.leftbtn =
this.leftbtn ||
$headeranchors.eq(0).not(".ui-btn-right").addClass("ui-btn-left")
.length;
this.rightbtn =
this.rightbtn || $headeranchors.eq(1).addClass("ui-btn-right").length;
},
_addBackButton: function () {
var options = this.options,
theme = options.backBtnTheme || options.theme;
$(
"<a role='button' href='javascript:void(0);' " +
"class='ui-btn ui-corner-all ui-shadow ui-btn-left " +
(theme ? "ui-btn-" + theme + " " : "") +
"ui-toolbar-back-btn ui-icon-carat-l ui-btn-icon-left' " +
"data-" +
$.mobile.ns +
"rel='back'>" +
options.backBtnText +
"</a>",
).prependTo(this.element);
},
_addHeadingClasses: function () {
this.element
.children("h1, h2, h3, h4, h5, h6")
.addClass("ui-title")
// Regardless of h element number in src, it becomes h1 for the enhanced page
.attr({
role: "heading",
"aria-level": "1",
});
},
});
})(jQuery);
(function ($, undefined) {
$.widget("mobile.toolbar", $.mobile.toolbar, {
options: {
position: null,
visibleOnPageShow: true,
disablePageZoom: true,
transition: "slide", //can be none, fade, slide (slide maps to slideup or slidedown)
fullscreen: false,
tapToggle: true,
tapToggleBlacklist:
"a, button, input, select, textarea, .ui-header-fixed, .ui-footer-fixed, .ui-flipswitch, .ui-popup, .ui-panel, .ui-panel-dismiss-open",
hideDuringFocus: "input, textarea, select",
updatePagePadding: true,
trackPersistentToolbars: true,
// Browser detection! Weeee, here we go...
// Unfortunately, position:fixed is costly, not to mention probably impossible, to feature-detect accurately.
// Some tests exist, but they currently return false results in critical devices and browsers, which could lead to a broken experience.
// Testing fixed positioning is also pretty obtrusive to page load, requiring injected elements and scrolling the window
// The following function serves to rule out some popular browsers with known fixed-positioning issues
// This is a plugin option like any other, so feel free to improve or overwrite it
supportBlacklist: function () {
return !$.support.fixedPosition;
},
},
_create: function () {
this._super();
if (
this.options.position === "fixed" &&
!this.options.supportBlacklist()
) {
this._makeFixed();
}
},
_makeFixed: function () {
this.element.addClass("ui-" + this.role + "-fixed");
this.updatePagePadding();
this._addTransitionClass();
this._bindPageEvents();
this._bindToggleHandlers();
},
_setOptions: function (o) {
if (o.position === "fixed" && this.options.position !== "fixed") {
this._makeFixed();
}
if (
this.options.position === "fixed" &&
!this.options.supportBlacklist()
) {
var $page = !!this.page
? this.page
: $(".ui-page-active").length > 0
? $(".ui-page-active")
: $(".ui-page").eq(0);
if (o.fullscreen !== undefined) {
if (o.fullscreen) {
this.element.addClass("ui-" + this.role + "-fullscreen");
$page.addClass("ui-page-" + this.role + "-fullscreen");
}
// If not fullscreen, add class to page to set top or bottom padding
else {
this.element.removeClass("ui-" + this.role + "-fullscreen");
$page
.removeClass("ui-page-" + this.role + "-fullscreen")
.addClass("ui-page-" + this.role + "-fixed");
}
}
}
this._super(o);
},
_addTransitionClass: function () {
var tclass = this.options.transition;
if (tclass && tclass !== "none") {
// use appropriate slide for header or footer
if (tclass === "slide") {
tclass = this.element.hasClass("ui-header")
? "slidedown"
: "slideup";
}
this.element.addClass(tclass);
}
},
_bindPageEvents: function () {
var page = !!this.page
? this.element.closest(".ui-page")
: this.document;
//page event bindings
// Fixed toolbars require page zoom to be disabled, otherwise usability issues crop up
// This method is meant to disable zoom while a fixed-positioned toolbar page is visible
this._on(page, {
pagebeforeshow: "_handlePageBeforeShow",
webkitAnimationStart: "_handleAnimationStart",
animationstart: "_handleAnimationStart",
updatelayout: "_handleAnimationStart",
pageshow: "_handlePageShow",
pagebeforehide: "_handlePageBeforeHide",
});
},
_handlePageBeforeShow: function () {
var o = this.options;
if (o.disablePageZoom) {
$.mobile.zoom.disable(true);
}
if (!o.visibleOnPageShow) {
this.hide(true);
}
},
_handleAnimationStart: function () {
if (this.options.updatePagePadding) {
this.updatePagePadding(!!this.page ? this.page : ".ui-page-active");
}
},
_handlePageShow: function () {
this.updatePagePadding(!!this.page ? this.page : ".ui-page-active");
if (this.options.updatePagePadding) {
this._on(this.window, { throttledresize: "updatePagePadding" });
}
},
_handlePageBeforeHide: function (e, ui) {
var o = this.options,
thisFooter,
thisHeader,
nextFooter,
nextHeader;
if (o.disablePageZoom) {
$.mobile.zoom.enable(true);
}
if (o.updatePagePadding) {
this._off(this.window, "throttledresize");
}
if (o.trackPersistentToolbars) {
thisFooter = $(".ui-footer-fixed:jqmData(id)", this.page);
thisHeader = $(".ui-header-fixed:jqmData(id)", this.page);
nextFooter =
(thisFooter.length &&
ui.nextPage &&
$(
".ui-footer-fixed:jqmData(id='" +
thisFooter.jqmData("id") +
"')",
ui.nextPage,
)) ||
$();
nextHeader =
(thisHeader.length &&
ui.nextPage &&
$(
".ui-header-fixed:jqmData(id='" +
thisHeader.jqmData("id") +
"')",
ui.nextPage,
)) ||
$();
if (nextFooter.length || nextHeader.length) {
nextFooter.add(nextHeader).appendTo($.mobile.pageContainer);
ui.nextPage.one("pageshow", function () {
nextHeader.prependTo(this);
nextFooter.appendTo(this);
});
}
}
},
_visible: true,
// This will set the content element's top or bottom padding equal to the toolbar's height
updatePagePadding: function (tbPage) {
var $el = this.element,
header = this.role === "header",
pos = parseFloat($el.css(header ? "top" : "bottom"));
// This behavior only applies to "fixed", not "fullscreen"
if (this.options.fullscreen) {
return;
}
// tbPage argument can be a Page object or an event, if coming from throttled resize.
tbPage =
(tbPage && tbPage.type === undefined && tbPage) ||
this.page ||
$el.closest(".ui-page");
tbPage = !!this.page ? this.page : ".ui-page-active";
$(tbPage).css(
"padding-" + (header ? "top" : "bottom"),
$el.outerHeight() + pos,
);
},
_useTransition: function (notransition) {
var $win = this.window,
$el = this.element,
scroll = $win.scrollTop(),
elHeight = $el.height(),
pHeight = !!this.page
? $el.closest(".ui-page").height()
: $(".ui-page-active").height(),
viewportHeight = $.mobile.getScreenHeight();
return (
!notransition &&
((this.options.transition &&
this.options.transition !== "none" &&
((this.role === "header" &&
!this.options.fullscreen &&
scroll > elHeight) ||
(this.role === "footer" &&
!this.options.fullscreen &&
scroll + viewportHeight < pHeight - elHeight))) ||
this.options.fullscreen)
);
},
show: function (notransition) {
var hideClass = "ui-fixed-hidden",
$el = this.element;
if (this._useTransition(notransition)) {
$el
.removeClass("out " + hideClass)
.addClass("in")
.animationComplete(function () {
$el.removeClass("in");
});
} else {
$el.removeClass(hideClass);
}
this._visible = true;
},
hide: function (notransition) {
var hideClass = "ui-fixed-hidden",
$el = this.element,
// if it's a slide transition, our new transitions need the reverse class as well to slide outward
outclass =
"out" + (this.options.transition === "slide" ? " reverse" : "");
if (this._useTransition(notransition)) {
$el
.addClass(outclass)
.removeClass("in")
.animationComplete(function () {
$el.addClass(hideClass).removeClass(outclass);
});
} else {
$el.addClass(hideClass).removeClass(outclass);
}
this._visible = false;
},
toggle: function () {
this[this._visible ? "hide" : "show"]();
},
_bindToggleHandlers: function () {
var self = this,
o = self.options,
delayShow,
delayHide,
isVisible = true,
page = !!this.page ? this.page : $(".ui-page");
// tap toggle
page
.bind("vclick", function (e) {
if (
o.tapToggle &&
!$(e.target).closest(o.tapToggleBlacklist).length
) {
self.toggle();
}
})
.bind("focusin focusout", function (e) {
//this hides the toolbars on a keyboard pop to give more screen room and prevent ios bug which
//positions fixed toolbars in the middle of the screen on pop if the input is near the top or
//bottom of the screen addresses issues #4410 Footer navbar moves up when clicking on a textbox in an Android environment
//and issue #4113 Header and footer change their position after keyboard popup - iOS
//and issue #4410 Footer navbar moves up when clicking on a textbox in an Android environment
if (
screen.width < 1025 &&
$(e.target).is(o.hideDuringFocus) &&
!$(e.target).closest(".ui-header-fixed, .ui-footer-fixed").length
) {
//Fix for issue #4724 Moving through form in Mobile Safari with "Next" and "Previous" system
//controls causes fixed position, tap-toggle false Header to reveal itself
// isVisible instead of self._visible because the focusin and focusout events fire twice at the same time
// Also use a delay for hiding the toolbars because on Android native browser focusin is direclty followed
// by a focusout when a native selects opens and the other way around when it closes.
if (e.type === "focusout" && !isVisible) {
isVisible = true;
//wait for the stack to unwind and see if we have jumped to another input
clearTimeout(delayHide);
delayShow = setTimeout(function () {
self.show();
}, 0);
} else if (e.type === "focusin" && !!isVisible) {
//if we have jumped to another input clear the time out to cancel the show.
clearTimeout(delayShow);
isVisible = false;
delayHide = setTimeout(function () {
self.hide();
}, 0);
}
}
});
},
_setRelative: function () {
if (this.options.position !== "fixed") {
$("[data-" + $.mobile.ns + "role='page']").css({
position: "relative",
});
}
},
_destroy: function () {
var $el = this.element,
header = $el.hasClass("ui-header");
$el
.closest(".ui-page")
.css("padding-" + (header ? "top" : "bottom"), "");
$el.removeClass(
"ui-header-fixed ui-footer-fixed ui-header-fullscreen ui-footer-fullscreen in out fade slidedown slideup ui-fixed-hidden",
);
$el
.closest(".ui-page")
.removeClass(
"ui-page-header-fixed ui-page-footer-fixed ui-page-header-fullscreen ui-page-footer-fullscreen",
);
},
});
})(jQuery);
(function ($, undefined) {
$.widget("mobile.toolbar", $.mobile.toolbar, {
_makeFixed: function () {
this._super();
this._workarounds();
},
//check the browser and version and run needed workarounds
_workarounds: function () {
var ua = navigator.userAgent,
platform = navigator.platform,
// Rendering engine is Webkit, and capture major version
wkmatch = ua.match(/AppleWebKit\/([0-9]+)/),
wkversion = !!wkmatch && wkmatch[1],
os = null,
self = this;
//set the os we are working in if it dosent match one with workarounds return
if (
platform.indexOf("iPhone") > -1 ||
platform.indexOf("iPad") > -1 ||
platform.indexOf("iPod") > -1
) {
os = "ios";
} else if (ua.indexOf("Android") > -1) {
os = "android";
} else {
return;
}
//check os version if it dosent match one with workarounds return
if (os === "ios") {
//iOS workarounds
self._bindScrollWorkaround();
} else if (os === "android" && wkversion && wkversion < 534) {
//Android 2.3 run all Android 2.3 workaround
self._bindScrollWorkaround();
self._bindListThumbWorkaround();
} else {
return;
}
},
//Utility class for checking header and footer positions relative to viewport
_viewportOffset: function () {
var $el = this.element,
header = $el.hasClass("ui-header"),
offset = Math.abs($el.offset().top - this.window.scrollTop());
if (!header) {
offset =
Math.round(offset - this.window.height() + $el.outerHeight()) - 60;
}
return offset;
},
//bind events for _triggerRedraw() function
_bindScrollWorkaround: function () {
var self = this;
//bind to scrollstop and check if the toolbars are correctly positioned
this._on(this.window, {
scrollstop: function () {
var viewportOffset = self._viewportOffset();
//check if the header is visible and if its in the right place
if (viewportOffset > 2 && self._visible) {
self._triggerRedraw();
}
},
});
},
//this addresses issue #4250 Persistent footer instability in v1.1 with long select lists in Android 2.3.3
//and issue #3748 Android 2.x: Page transitions broken when fixed toolbars used
//the absolutely positioned thumbnail in a list view causes problems with fixed position buttons above in a nav bar
//setting the li's to -webkit-transform:translate3d(0,0,0); solves this problem to avoide potential issues in other
//platforms we scope this with the class ui-android-2x-fix
_bindListThumbWorkaround: function () {
this.element.closest(".ui-page").addClass("ui-android-2x-fixed");
},
//this addresses issues #4337 Fixed header problem after scrolling content on iOS and Android
//and device bugs project issue #1 Form elements can lose click hit area in position: fixed containers.
//this also addresses not on fixed toolbars page in docs
//adding 1px of padding to the bottom then removing it causes a "redraw"
//which positions the toolbars correctly (they will always be visually correct)
_triggerRedraw: function () {
var paddingBottom = parseFloat(
$(".ui-page-active").css("padding-bottom"),
);
//trigger page redraw to fix incorrectly positioned fixed elements
$(".ui-page-active").css("padding-bottom", paddingBottom + 1 + "px");
//if the padding is reset with out a timeout the reposition will not occure.
//this is independant of JQM the browser seems to need the time to react.
setTimeout(function () {
$(".ui-page-active").css("padding-bottom", paddingBottom + "px");
}, 0);
},
destroy: function () {
this._super();
//Remove the class we added to the page previously in android 2.x
this.element
.closest(".ui-page-active")
.removeClass("ui-android-2x-fix");
},
});
})(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);
(function ($, undefined) {
$.widget("mobile.panel", {
options: {
classes: {
panel: "ui-panel",
panelOpen: "ui-panel-open",
panelClosed: "ui-panel-closed",
panelFixed: "ui-panel-fixed",
panelInner: "ui-panel-inner",
modal: "ui-panel-dismiss",
modalOpen: "ui-panel-dismiss-open",
pageContainer: "ui-panel-page-container",
pageWrapper: "ui-panel-wrapper",
pageFixedToolbar: "ui-panel-fixed-toolbar",
pageContentPrefix:
"ui-panel-page-content" /* Used for wrapper and fixed toolbars position, display and open classes. */,
animate: "ui-panel-animate",
},
animate: true,
theme: null,
position: "left",
dismissible: true,
display: "reveal", //accepts reveal, push, overlay
swipeClose: true,
positionFixed: false,
},
_closeLink: null,
_parentPage: null,
_page: null,
_modal: null,
_panelInner: null,
_wrapper: null,
_fixedToolbars: null,
_create: function () {
var el = this.element,
parentPage = el.closest(".ui-page, :jqmData(role='page')");
// expose some private props to other methods
$.extend(this, {
_closeLink: el.find(":jqmData(rel='close')"),
_parentPage: parentPage.length > 0 ? parentPage : false,
_openedPage: null,
_page: this._getPage,
_panelInner: this._getPanelInner(),
_fixedToolbars: this._getFixedToolbars,
});
if (this.options.display !== "overlay") {
this._getWrapper();
}
this._addPanelClasses();
// if animating, add the class to do so
if ($.support.cssTransform3d && !!this.options.animate) {
this.element.addClass(this.options.classes.animate);
}
this._bindUpdateLayout();
this._bindCloseEvents();
this._bindLinkListeners();
this._bindPageEvents();
if (!!this.options.dismissible) {
this._createModal();
}
this._bindSwipeEvents();
},
_getPanelInner: function () {
var panelInner = this.element.find(
"." + this.options.classes.panelInner,
);
if (panelInner.length === 0) {
panelInner = this.element
.children()
.wrapAll("<div class='" + this.options.classes.panelInner + "' />")
.parent();
}
return panelInner;
},
_createModal: function () {
var self = this,
target = self._parentPage
? self._parentPage.parent()
: self.element.parent();
self._modal = $(
"<div class='" + self.options.classes.modal + "'></div>",
)
.on("mousedown", function () {
self.close();
})
.appendTo(target);
},
_getPage: function () {
var page =
this._openedPage ||
this._parentPage ||
$("." + $.mobile.activePageClass);
return page;
},
_getWrapper: function () {
var wrapper = this._page().find("." + this.options.classes.pageWrapper);
if (wrapper.length === 0) {
wrapper = this._page()
.children(
".ui-header:not(.ui-header-fixed), .ui-content:not(.ui-popup), .ui-footer:not(.ui-footer-fixed)",
)
.wrapAll(
"<div class='" + this.options.classes.pageWrapper + "'></div>",
)
.parent();
}
this._wrapper = wrapper;
},
_getFixedToolbars: function () {
var extFixedToolbars = $("body").children(
".ui-header-fixed, .ui-footer-fixed",
),
intFixedToolbars = this._page().find(
".ui-header-fixed, .ui-footer-fixed",
),
fixedToolbars = extFixedToolbars
.add(intFixedToolbars)
.addClass(this.options.classes.pageFixedToolbar);
return fixedToolbars;
},
_getPosDisplayClasses: function (prefix) {
return (
prefix +
"-position-" +
this.options.position +
" " +
prefix +
"-display-" +
this.options.display
);
},
_getPanelClasses: function () {
var panelClasses =
this.options.classes.panel +
" " +
this._getPosDisplayClasses(this.options.classes.panel) +
" " +
this.options.classes.panelClosed +
" " +
"ui-body-" +
(this.options.theme ? this.options.theme : "inherit");
if (!!this.options.positionFixed) {
panelClasses += " " + this.options.classes.panelFixed;
}
return panelClasses;
},
_addPanelClasses: function () {
this.element.addClass(this._getPanelClasses());
},
_handleCloseClickAndEatEvent: function (event) {
if (!event.isDefaultPrevented()) {
event.preventDefault();
this.close();
return false;
}
},
_handleCloseClick: function (event) {
if (!event.isDefaultPrevented()) {
this.close();
}
},
_bindCloseEvents: function () {
this._on(this._closeLink, {
click: "_handleCloseClick",
});
this._on({
"click a:jqmData(ajax='false')": "_handleCloseClick",
});
},
_positionPanel: function (scrollToTop) {
var self = this,
panelInnerHeight = self._panelInner.outerHeight(),
expand = panelInnerHeight > $.mobile.getScreenHeight();
if (expand || !self.options.positionFixed) {
if (expand) {
self._unfixPanel();
$.mobile.resetActivePageHeight(panelInnerHeight);
}
if (scrollToTop) {
this.window[0].scrollTo(0, $.mobile.defaultHomeScroll);
}
} else {
self._fixPanel();
}
},
_bindFixListener: function () {
this._on($(window), { throttledresize: "_positionPanel" });
},
_unbindFixListener: function () {
this._off($(window), "throttledresize");
},
_unfixPanel: function () {
if (!!this.options.positionFixed && $.support.fixedPosition) {
this.element.removeClass(this.options.classes.panelFixed);
}
},
_fixPanel: function () {
if (!!this.options.positionFixed && $.support.fixedPosition) {
this.element.addClass(this.options.classes.panelFixed);
}
},
_bindUpdateLayout: function () {
var self = this;
self.element.on("updatelayout", function (/* e */) {
if (self._open) {
self._positionPanel();
}
});
},
_bindLinkListeners: function () {
this._on("body", {
"click a": "_handleClick",
});
},
_handleClick: function (e) {
var link,
panelId = this.element.attr("id");
if (
e.currentTarget.href.split("#")[1] === panelId &&
panelId !== undefined
) {
e.preventDefault();
link = $(e.target);
if (link.hasClass("ui-btn")) {
link.addClass($.mobile.activeBtnClass);
this.element.one("panelopen panelclose", function () {
link.removeClass($.mobile.activeBtnClass);
});
}
this.toggle();
return false;
}
},
_bindSwipeEvents: function () {
var self = this,
area = self._modal ? self.element.add(self._modal) : self.element;
// on swipe, close the panel
if (!!self.options.swipeClose) {
if (self.options.position === "left") {
area.on("swipeleft.panel", function (/* e */) {
self.close();
});
} else {
area.on("swiperight.panel", function (/* e */) {
self.close();
});
}
}
},
_bindPageEvents: function () {
var self = this;
this.document
// Close the panel if another panel on the page opens
.on("panelbeforeopen", function (e) {
if (self._open && e.target !== self.element[0]) {
self.close();
}
})
// On escape, close? might need to have a target check too...
.on("keyup.panel", function (e) {
if (e.keyCode === 27 && self._open) {
self.close();
}
});
if (!this._parentPage && this.options.display !== "overlay") {
this._on(this.document, {
pageshow: "_getWrapper",
});
}
// Clean up open panels after page hide
if (self._parentPage) {
this.document.on("pagehide", ":jqmData(role='page')", function () {
if (self._open) {
self.close(true);
}
});
} else {
this.document.on("pagebeforehide", function () {
if (self._open) {
self.close(true);
}
});
}
},
// state storage of open or closed
_open: false,
_pageContentOpenClasses: null,
_modalOpenClasses: null,
open: function (immediate) {
if (!this._open) {
var self = this,
o = self.options,
_openPanel = function () {
self.document.off("panelclose");
self._page().jqmData("panel", "open");
if (
$.support.cssTransform3d &&
!!o.animate &&
o.display !== "overlay"
) {
self._wrapper.addClass(o.classes.animate);
self._fixedToolbars().addClass(o.classes.animate);
}
if (!immediate && $.support.cssTransform3d && !!o.animate) {
self.element.animationComplete(complete, "transition");
} else {
setTimeout(complete, 0);
}
if (o.theme && o.display !== "overlay") {
self
._page()
.parent()
.addClass(
o.classes.pageContainer +
"-themed " +
o.classes.pageContainer +
"-" +
o.theme,
);
}
self.element
.removeClass(o.classes.panelClosed)
.addClass(o.classes.panelOpen);
self._positionPanel(true);
self._pageContentOpenClasses = self._getPosDisplayClasses(
o.classes.pageContentPrefix,
);
if (o.display !== "overlay") {
self._page().parent().addClass(o.classes.pageContainer);
self._wrapper.addClass(self._pageContentOpenClasses);
self._fixedToolbars().addClass(self._pageContentOpenClasses);
}
self._modalOpenClasses =
self._getPosDisplayClasses(o.classes.modal) +
" " +
o.classes.modalOpen;
if (self._modal) {
self._modal
.addClass(self._modalOpenClasses)
.height(
Math.max(self._modal.height(), self.document.height()),
);
}
},
complete = function () {
if (o.display !== "overlay") {
self._wrapper.addClass(o.classes.pageContentPrefix + "-open");
self
._fixedToolbars()
.addClass(o.classes.pageContentPrefix + "-open");
}
self._bindFixListener();
self._trigger("open");
self._openedPage = self._page();
};
self._trigger("beforeopen");
if (self._page().jqmData("panel") === "open") {
self.document.on("panelclose", function () {
_openPanel();
});
} else {
_openPanel();
}
self._open = true;
}
},
close: function (immediate) {
if (this._open) {
var self = this,
o = this.options,
_closePanel = function () {
self.element.removeClass(o.classes.panelOpen);
if (o.display !== "overlay") {
self._wrapper.removeClass(self._pageContentOpenClasses);
self._fixedToolbars().removeClass(self._pageContentOpenClasses);
}
if (!immediate && $.support.cssTransform3d && !!o.animate) {
self.element.animationComplete(complete, "transition");
} else {
setTimeout(complete, 0);
}
if (self._modal) {
self._modal.removeClass(self._modalOpenClasses);
}
},
complete = function () {
if (o.theme && o.display !== "overlay") {
self
._page()
.parent()
.removeClass(
o.classes.pageContainer +
"-themed " +
o.classes.pageContainer +
"-" +
o.theme,
);
}
self.element.addClass(o.classes.panelClosed);
if (o.display !== "overlay") {
self._page().parent().removeClass(o.classes.pageContainer);
self._wrapper.removeClass(
o.classes.pageContentPrefix + "-open",
);
self
._fixedToolbars()
.removeClass(o.classes.pageContentPrefix + "-open");
}
if (
$.support.cssTransform3d &&
!!o.animate &&
o.display !== "overlay"
) {
self._wrapper.removeClass(o.classes.animate);
self._fixedToolbars().removeClass(o.classes.animate);
}
self._fixPanel();
self._unbindFixListener();
$.mobile.resetActivePageHeight();
self._page().jqmRemoveData("panel");
self._trigger("close");
self._openedPage = null;
};
self._trigger("beforeclose");
_closePanel();
self._open = false;
}
},
toggle: function () {
this[this._open ? "close" : "open"]();
},
_destroy: function () {
var otherPanels,
o = this.options,
multiplePanels =
$("body > :mobile-panel").length +
$.mobile.activePage.find(":mobile-panel").length >
1;
if (o.display !== "overlay") {
// remove the wrapper if not in use by another panel
otherPanels = $("body > :mobile-panel").add(
$.mobile.activePage.find(":mobile-panel"),
);
if (
otherPanels.not(".ui-panel-display-overlay").not(this.element)
.length === 0
) {
this._wrapper.children().unwrap();
}
if (this._open) {
this._fixedToolbars().removeClass(
o.classes.pageContentPrefix + "-open",
);
if ($.support.cssTransform3d && !!o.animate) {
this._fixedToolbars().removeClass(o.classes.animate);
}
this._page().parent().removeClass(o.classes.pageContainer);
if (o.theme) {
this._page()
.parent()
.removeClass(
o.classes.pageContainer +
"-themed " +
o.classes.pageContainer +
"-" +
o.theme,
);
}
}
}
if (!multiplePanels) {
this.document.off("panelopen panelclose");
}
if (this._open) {
this._page().jqmRemoveData("panel");
}
this._panelInner.children().unwrap();
this.element
.removeClass(
[
this._getPanelClasses(),
o.classes.panelOpen,
o.classes.animate,
].join(" "),
)
.off("swipeleft.panel swiperight.panel")
.off("panelbeforeopen")
.off("panelhide")
.off("keyup.panel")
.off("updatelayout");
if (this._modal) {
this._modal.remove();
}
},
});
})(jQuery);
(function ($, undefined) {
$.widget("mobile.table", {
options: {
classes: {
table: "ui-table",
},
enhanced: false,
},
_create: function () {
if (!this.options.enhanced) {
this.element.addClass(this.options.classes.table);
}
// extend here, assign on refresh > _setHeaders
$.extend(this, {
// Expose headers and allHeaders properties on the widget
// headers references the THs within the first TR in the table
headers: undefined,
// allHeaders references headers, plus all THs in the thead, which may
// include several rows, or not
allHeaders: undefined,
});
this._refresh(true);
},
_setHeaders: function () {
var trs = this.element.find("thead tr");
this.headers = this.element.find("tr:eq(0)").children();
this.allHeaders = this.headers.add(trs.children());
},
refresh: function () {
this._refresh();
},
rebuild: $.noop,
_refresh: function (/* create */) {
var table = this.element,
trs = table.find("thead tr");
// updating headers on refresh (fixes #5880)
this._setHeaders();
// Iterate over the trs
trs.each(function () {
var columnCount = 0;
// Iterate over the children of the tr
$(this)
.children()
.each(function () {
var span = parseInt(this.getAttribute("colspan"), 10),
selector = ":nth-child(" + (columnCount + 1) + ")",
j;
this.setAttribute(
"data-" + $.mobile.ns + "colstart",
columnCount + 1,
);
if (span) {
for (j = 0; j < span - 1; j++) {
columnCount++;
selector += ", :nth-child(" + (columnCount + 1) + ")";
}
}
// Store "cells" data on header as a reference to all cells in the
// same column as this TH
$(this).jqmData(
"cells",
table.find("tr").not(trs.eq(0)).not(this).children(selector),
);
columnCount++;
});
});
},
});
})(jQuery);
(function ($, undefined) {
$.widget("mobile.table", $.mobile.table, {
options: {
mode: "columntoggle",
columnBtnTheme: null,
columnPopupTheme: null,
columnBtnText: "Columns...",
classes: $.extend($.mobile.table.prototype.options.classes, {
popup: "ui-table-columntoggle-popup",
columnBtn: "ui-table-columntoggle-btn",
priorityPrefix: "ui-table-priority-",
columnToggleTable: "ui-table-columntoggle",
}),
},
_create: function () {
this._super();
if (this.options.mode !== "columntoggle") {
return;
}
$.extend(this, {
_menu: null,
});
if (this.options.enhanced) {
this._menu = $(this.document[0].getElementById(this._id() + "-popup"))
.children()
.first();
this._addToggles(this._menu, true);
} else {
this._menu = this._enhanceColToggle();
this.element.addClass(this.options.classes.columnToggleTable);
}
this._setupEvents();
this._setToggleState();
},
_id: function () {
return this.element.attr("id") || this.widgetName + this.uuid;
},
_setupEvents: function () {
//NOTE: inputs are bound in bindToggles,
// so it can be called on refresh, too
// update column toggles on resize
this._on(this.window, {
throttledresize: "_setToggleState",
});
this._on(this._menu, {
"change input": "_menuInputChange",
});
},
_addToggles: function (menu, keep) {
var inputs,
checkboxIndex = 0,
opts = this.options,
container = menu.controlgroup("container");
// allow update of menu on refresh (fixes #5880)
if (keep) {
inputs = menu.find("input");
} else {
container.empty();
}
// create the hide/show toggles
this.headers.not("td").each(function () {
var header = $(this),
priority = $.mobile.getAttribute(this, "priority"),
cells = header.add(header.jqmData("cells"));
if (priority) {
cells.addClass(opts.classes.priorityPrefix + priority);
(keep
? inputs.eq(checkboxIndex++)
: $(
"<label><input type='checkbox' checked />" +
(header.children("abbr").first().attr("title") ||
header.text()) +
"</label>",
)
.appendTo(container)
.children(0)
.checkboxradio({
theme: opts.columnPopupTheme,
})
).jqmData("cells", cells);
}
});
// set bindings here
if (!keep) {
menu.controlgroup("refresh");
}
},
_menuInputChange: function (evt) {
var input = $(evt.target),
checked = input[0].checked;
input
.jqmData("cells")
.toggleClass("ui-table-cell-hidden", !checked)
.toggleClass("ui-table-cell-visible", checked);
if (input[0].getAttribute("locked")) {
input.removeAttr("locked");
this._unlockCells(input.jqmData("cells"));
} else {
input.attr("locked", true);
}
},
_unlockCells: function (cells) {
// allow hide/show via CSS only = remove all toggle-locks
cells.removeClass("ui-table-cell-hidden ui-table-cell-visible");
},
_enhanceColToggle: function () {
var id,
menuButton,
popup,
menu,
table = this.element,
opts = this.options,
ns = $.mobile.ns,
fragment = this.document[0].createDocumentFragment();
id = this._id() + "-popup";
menuButton = $(
"<a href='#" +
id +
"' " +
"class='" +
opts.classes.columnBtn +
" ui-btn " +
"ui-btn-" +
(opts.columnBtnTheme || "a") +
" ui-corner-all ui-shadow ui-mini' " +
"data-" +
ns +
"rel='popup'>" +
opts.columnBtnText +
"</a>",
);
popup = $(
"<div class='" + opts.classes.popup + "' id='" + id + "'></div>",
);
menu = $("<fieldset></fieldset>").controlgroup();
// set extension here, send "false" to trigger build/rebuild
this._addToggles(menu, false);
menu.appendTo(popup);
fragment.appendChild(popup[0]);
fragment.appendChild(menuButton[0]);
table.before(fragment);
popup.popup();
return menu;
},
rebuild: function () {
this._super();
if (this.options.mode === "columntoggle") {
// NOTE: rebuild passes "false", while refresh passes "undefined"
// both refresh the table, but inside addToggles, !false will be true,
// so a rebuild call can be indentified
this._refresh(false);
}
},
_refresh: function (create) {
this._super(create);
if (!create && this.options.mode === "columntoggle") {
// columns not being replaced must be cleared from input toggle-locks
this._unlockCells(
this.element.find(
".ui-table-cell-hidden, " + ".ui-table-cell-visible",
),
);
// update columntoggles and cells
this._addToggles(this._menu, create);
// check/uncheck
this._setToggleState();
}
},
_setToggleState: function () {
this._menu.find("input").each(function () {
var checkbox = $(this);
this.checked =
checkbox.jqmData("cells").eq(0).css("display") === "table-cell";
checkbox.checkboxradio("refresh");
});
},
_destroy: function () {
this._super();
},
});
})(jQuery);
(function ($, undefined) {
$.widget("mobile.table", $.mobile.table, {
options: {
mode: "reflow",
classes: $.extend($.mobile.table.prototype.options.classes, {
reflowTable: "ui-table-reflow",
cellLabels: "ui-table-cell-label",
}),
},
_create: function () {
this._super();
// If it's not reflow mode, return here.
if (this.options.mode !== "reflow") {
return;
}
if (!this.options.enhanced) {
this.element.addClass(this.options.classes.reflowTable);
this._updateReflow();
}
},
rebuild: function () {
this._super();
if (this.options.mode === "reflow") {
this._refresh(false);
}
},
_refresh: function (create) {
this._super(create);
if (!create && this.options.mode === "reflow") {
this._updateReflow();
}
},
_updateReflow: function () {
var table = this,
opts = this.options;
// get headers in reverse order so that top-level headers are appended last
$(table.allHeaders.get().reverse()).each(function () {
var cells = $(this).jqmData("cells"),
colstart = $.mobile.getAttribute(this, "colstart"),
hierarchyClass =
cells.not(this).filter("thead th").length &&
" ui-table-cell-label-top",
text = $(this).text(),
iteration,
filter;
if (text !== "") {
if (hierarchyClass) {
iteration = parseInt(this.getAttribute("colspan"), 10);
filter = "";
if (iteration) {
filter = "td:nth-child(" + iteration + "n + " + colstart + ")";
}
table._addLabels(
cells.filter(filter),
opts.classes.cellLabels + hierarchyClass,
text,
);
} else {
table._addLabels(cells, opts.classes.cellLabels, text);
}
}
});
},
_addLabels: function (cells, label, text) {
// .not fixes #6006
cells
.not(":has(b." + label + ")")
.prepend("<b class='" + label + "'>" + text + "</b>");
},
});
})(jQuery);
(function ($, undefined) {
// TODO rename filterCallback/deprecate and default to the item itself as the first argument
var defaultFilterCallback = function (index, searchValue) {
return (
("" + ($.mobile.getAttribute(this, "filtertext") || $(this).text()))
.toLowerCase()
.indexOf(searchValue) === -1
);
};
$.widget("mobile.filterable", {
initSelector: ":jqmData(filter='true')",
options: {
filterReveal: false,
filterCallback: defaultFilterCallback,
enhanced: false,
input: null,
children:
"> li, > option, > optgroup option, > tbody tr, > .ui-controlgroup-controls > .ui-btn, > .ui-controlgroup-controls > .ui-checkbox, > .ui-controlgroup-controls > .ui-radio",
},
_create: function () {
var opts = this.options;
$.extend(this, {
_search: null,
_timer: 0,
});
this._setInput(opts.input);
if (!opts.enhanced) {
this._filterItems(
((this._search && this._search.val()) || "").toLowerCase(),
);
}
},
_onKeyUp: function () {
var val,
lastval,
search = this._search;
if (search) {
(val = search.val().toLowerCase()),
(lastval = $.mobile.getAttribute(search[0], "lastval") + "");
if (lastval && lastval === val) {
// Execute the handler only once per value change
return;
}
if (this._timer) {
window.clearTimeout(this._timer);
this._timer = 0;
}
this._timer = this._delay(function () {
this._trigger("beforefilter", null, { input: search });
// Change val as lastval for next execution
search[0].setAttribute("data-" + $.mobile.ns + "lastval", val);
this._filterItems(val);
this._timer = 0;
}, 250);
}
},
_getFilterableItems: function () {
var elem = this.element,
children = this.options.children,
items = !children
? { length: 0 }
: $.isFunction(children)
? children()
: children.nodeName
? $(children)
: children.jquery
? children
: this.element.find(children);
if (items.length === 0) {
items = elem.children();
}
return items;
},
_filterItems: function (val) {
var idx,
callback,
length,
dst,
show = [],
hide = [],
opts = this.options,
filterItems = this._getFilterableItems();
if (val != null) {
callback = opts.filterCallback || defaultFilterCallback;
length = filterItems.length;
// Partition the items into those to be hidden and those to be shown
for (idx = 0; idx < length; idx++) {
dst = callback.call(filterItems[idx], idx, val) ? hide : show;
dst.push(filterItems[idx]);
}
}
// If nothing is hidden, then the decision whether to hide or show the items
// is based on the "filterReveal" option.
if (hide.length === 0) {
filterItems[opts.filterReveal ? "addClass" : "removeClass"](
"ui-screen-hidden",
);
} else {
$(hide).addClass("ui-screen-hidden");
$(show).removeClass("ui-screen-hidden");
}
this._refreshChildWidget();
this._trigger("filter", null, {
items: filterItems,
});
},
// The Default implementation of _refreshChildWidget attempts to call
// refresh on collapsibleset, controlgroup, selectmenu, or listview
_refreshChildWidget: function () {
var widget,
idx,
recognizedWidgets = [
"collapsibleset",
"selectmenu",
"controlgroup",
"listview",
];
for (idx = recognizedWidgets.length - 1; idx > -1; idx--) {
widget = recognizedWidgets[idx];
if ($.mobile[widget]) {
widget = this.element.data("mobile-" + widget);
if (widget && $.isFunction(widget.refresh)) {
widget.refresh();
}
}
}
},
// TODO: When the input is not internal, do not even store it in this._search
_setInput: function (selector) {
var search = this._search;
// Stop a pending filter operation
if (this._timer) {
window.clearTimeout(this._timer);
this._timer = 0;
}
if (search) {
this._off(search, "keyup change input");
search = null;
}
if (selector) {
search = selector.jquery
? selector
: selector.nodeName
? $(selector)
: this.document.find(selector);
this._on(search, {
keyup: "_onKeyUp",
change: "_onKeyUp",
input: "_onKeyUp",
});
}
this._search = search;
},
_setOptions: function (options) {
var refilter = !(
options.filterReveal === undefined &&
options.filterCallback === undefined &&
options.children === undefined
);
this._super(options);
if (options.input !== undefined) {
this._setInput(options.input);
refilter = true;
}
if (refilter) {
this.refresh();
}
},
_destroy: function () {
var opts = this.options,
items = this._getFilterableItems();
if (opts.enhanced) {
items.toggleClass("ui-screen-hidden", opts.filterReveal);
} else {
items.removeClass("ui-screen-hidden");
}
},
refresh: function () {
if (this._timer) {
window.clearTimeout(this._timer);
this._timer = 0;
}
this._filterItems(
((this._search && this._search.val()) || "").toLowerCase(),
);
},
});
})(jQuery);
(function ($, undefined) {
// Create a function that will replace the _setOptions function of a widget,
// and will pass the options on to the input of the filterable.
var replaceSetOptions = function (self, orig) {
return function (options) {
orig.call(this, options);
self._syncTextInputOptions(options);
};
},
rDividerListItem = /(^|\s)ui-li-divider(\s|$)/,
origDefaultFilterCallback =
$.mobile.filterable.prototype.options.filterCallback;
// Override the default filter callback with one that does not hide list dividers
$.mobile.filterable.prototype.options.filterCallback = function (
index,
searchValue,
) {
return (
!this.className.match(rDividerListItem) &&
origDefaultFilterCallback.call(this, index, searchValue)
);
};
$.widget("mobile.filterable", $.mobile.filterable, {
options: {
filterPlaceholder: "Filter items...",
filterTheme: null,
},
_create: function () {
var idx,
widgetName,
elem = this.element,
recognizedWidgets = [
"collapsibleset",
"selectmenu",
"controlgroup",
"listview",
],
createHandlers = {};
this._super();
$.extend(this, {
_widget: null,
});
for (idx = recognizedWidgets.length - 1; idx > -1; idx--) {
widgetName = recognizedWidgets[idx];
if ($.mobile[widgetName]) {
if (this._setWidget(elem.data("mobile-" + widgetName))) {
break;
} else {
createHandlers[widgetName + "create"] = "_handleCreate";
}
}
}
if (!this._widget) {
this._on(elem, createHandlers);
}
},
_handleCreate: function (evt) {
this._setWidget(
this.element.data(
"mobile-" + evt.type.substring(0, evt.type.length - 6),
),
);
},
_trigger: function (type, event, data) {
if (
this._widget &&
this._widget.widgetFullName === "mobile-listview" &&
type === "beforefilter"
) {
// Also trigger listviewbeforefilter if this widget is also a listview
this._widget._trigger("beforefilter", event, data);
}
this._super(type, event, data);
},
_setWidget: function (widget) {
if (!this._widget && widget) {
this._widget = widget;
this._widget._setOptions = replaceSetOptions(
this,
this._widget._setOptions,
);
}
if (!!this._widget) {
this._syncTextInputOptions(this._widget.options);
if (this._widget.widgetName === "listview") {
this._widget.options.hideDividers = true;
this._widget.element.listview("refresh");
}
}
return !!this._widget;
},
_isSearchInternal: function () {
return (
this._search &&
this._search.jqmData("ui-filterable-" + this.uuid + "-internal")
);
},
_setInput: function (selector) {
var opts = this.options,
updatePlaceholder = true,
textinputOpts = {};
if (!selector) {
if (this._isSearchInternal()) {
// Ignore the call to set a new input if the selector goes to falsy and
// the current textinput is already of the internally generated variety.
return;
} else {
// Generating a new textinput widget. No need to set the placeholder
// further down the function.
updatePlaceholder = false;
selector = $(
"<input " +
"data-" +
$.mobile.ns +
"type='search' " +
"placeholder='" +
opts.filterPlaceholder +
"'></input>",
).jqmData("ui-filterable-" + this.uuid + "-internal", true);
$("<form class='ui-filterable'></form>")
.append(selector)
.submit(function (evt) {
evt.preventDefault();
selector.blur();
})
.insertBefore(this.element);
if ($.mobile.textinput) {
if (this.options.filterTheme != null) {
textinputOpts["theme"] = opts.filterTheme;
}
selector.textinput(textinputOpts);
}
}
}
this._super(selector);
if (this._isSearchInternal() && updatePlaceholder) {
this._search.attr("placeholder", this.options.filterPlaceholder);
}
},
_setOptions: function (options) {
var ret = this._super(options);
// Need to set the filterPlaceholder after having established the search input
if (options.filterPlaceholder !== undefined) {
if (this._isSearchInternal()) {
this._search.attr("placeholder", options.filterPlaceholder);
}
}
if (
options.filterTheme !== undefined &&
this._search &&
$.mobile.textinput
) {
this._search.textinput("option", "theme", options.filterTheme);
}
return ret;
},
_destroy: function () {
if (this._isSearchInternal()) {
this._search.remove();
}
this._super();
},
_syncTextInputOptions: function (options) {
var idx,
textinputOptions = {};
// We only sync options if the filterable's textinput is of the internally
// generated variety, rather than one specified by the user.
if (this._isSearchInternal() && $.mobile.textinput) {
// Apply only the options understood by textinput
for (idx in $.mobile.textinput.prototype.options) {
if (options[idx] !== undefined) {
if (idx === "theme" && this.options.filterTheme != null) {
textinputOptions[idx] = this.options.filterTheme;
} else {
textinputOptions[idx] = options[idx];
}
}
}
this._search.textinput("option", textinputOptions);
}
},
});
})(jQuery);
/*!
* jQuery UI Tabs fadf2b312a05040436451c64bbfaf4814bc62c56
* http://jqueryui.com
*
* Copyright 2013 jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
* http://api.jqueryui.com/tabs/
*
* Depends:
* jquery.ui.core.js
* jquery.ui.widget.js
*/
(function ($, undefined) {
var tabId = 0,
rhash = /#.*$/;
function getNextTabId() {
return ++tabId;
}
function isLocal(anchor) {
return (
anchor.hash.length > 1 &&
decodeURIComponent(anchor.href.replace(rhash, "")) ===
decodeURIComponent(location.href.replace(rhash, ""))
);
}
$.widget("ui.tabs", {
version: "fadf2b312a05040436451c64bbfaf4814bc62c56",
delay: 300,
options: {
active: null,
collapsible: false,
event: "click",
heightStyle: "content",
hide: null,
show: null,
// callbacks
activate: null,
beforeActivate: null,
beforeLoad: null,
load: null,
},
_create: function () {
var that = this,
options = this.options;
this.running = false;
this.element
.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all")
.toggleClass("ui-tabs-collapsible", options.collapsible)
// Prevent users from focusing disabled tabs via click
.delegate(
".ui-tabs-nav > li",
"mousedown" + this.eventNamespace,
function (event) {
if ($(this).is(".ui-state-disabled")) {
event.preventDefault();
}
},
)
// support: IE <9
// Preventing the default action in mousedown doesn't prevent IE
// from focusing the element, so if the anchor gets focused, blur.
// We don't have to worry about focusing the previously focused
// element since clicking on a non-focusable element should focus
// the body anyway.
.delegate(
".ui-tabs-anchor",
"focus" + this.eventNamespace,
function () {
if ($(this).closest("li").is(".ui-state-disabled")) {
this.blur();
}
},
);
this._processTabs();
options.active = this._initialActive();
// Take disabling tabs via class attribute from HTML
// into account and update option properly.
if ($.isArray(options.disabled)) {
options.disabled = $.unique(
options.disabled.concat(
$.map(this.tabs.filter(".ui-state-disabled"), function (li) {
return that.tabs.index(li);
}),
),
).sort();
}
// check for length avoids error when initializing empty list
if (this.options.active !== false && this.anchors.length) {
this.active = this._findActive(options.active);
} else {
this.active = $();
}
this._refresh();
if (this.active.length) {
this.load(options.active);
}
},
_initialActive: function () {
var active = this.options.active,
collapsible = this.options.collapsible,
locationHash = location.hash.substring(1);
if (active === null) {
// check the fragment identifier in the URL
if (locationHash) {
this.tabs.each(function (i, tab) {
if ($(tab).attr("aria-controls") === locationHash) {
active = i;
return false;
}
});
}
// check for a tab marked active via a class
if (active === null) {
active = this.tabs.index(this.tabs.filter(".ui-tabs-active"));
}
// no active tab, set to false
if (active === null || active === -1) {
active = this.tabs.length ? 0 : false;
}
}
// handle numbers: negative, out of range
if (active !== false) {
active = this.tabs.index(this.tabs.eq(active));
if (active === -1) {
active = collapsible ? false : 0;
}
}
// don't allow collapsible: false and active: false
if (!collapsible && active === false && this.anchors.length) {
active = 0;
}
return active;
},
_getCreateEventData: function () {
return {
tab: this.active,
panel: !this.active.length ? $() : this._getPanelForTab(this.active),
};
},
_tabKeydown: function (event) {
var focusedTab = $(this.document[0].activeElement).closest("li"),
selectedIndex = this.tabs.index(focusedTab),
goingForward = true;
if (this._handlePageNav(event)) {
return;
}
switch (event.keyCode) {
case $.ui.keyCode.RIGHT:
case $.ui.keyCode.DOWN:
selectedIndex++;
break;
case $.ui.keyCode.UP:
case $.ui.keyCode.LEFT:
goingForward = false;
selectedIndex--;
break;
case $.ui.keyCode.END:
selectedIndex = this.anchors.length - 1;
break;
case $.ui.keyCode.HOME:
selectedIndex = 0;
break;
case $.ui.keyCode.SPACE:
// Activate only, no collapsing
event.preventDefault();
clearTimeout(this.activating);
this._activate(selectedIndex);
return;
case $.ui.keyCode.ENTER:
// Toggle (cancel delayed activation, allow collapsing)
event.preventDefault();
clearTimeout(this.activating);
// Determine if we should collapse or activate
this._activate(
selectedIndex === this.options.active ? false : selectedIndex,
);
return;
default:
return;
}
// Focus the appropriate tab, based on which key was pressed
event.preventDefault();
clearTimeout(this.activating);
selectedIndex = this._focusNextTab(selectedIndex, goingForward);
// Navigating with control key will prevent automatic activation
if (!event.ctrlKey) {
// Update aria-selected immediately so that AT think the tab is already selected.
// Otherwise AT may confuse the user by stating that they need to activate the tab,
// but the tab will already be activated by the time the announcement finishes.
focusedTab.attr("aria-selected", "false");
this.tabs.eq(selectedIndex).attr("aria-selected", "true");
this.activating = this._delay(function () {
this.option("active", selectedIndex);
}, this.delay);
}
},
_panelKeydown: function (event) {
if (this._handlePageNav(event)) {
return;
}
// Ctrl+up moves focus to the current tab
if (event.ctrlKey && event.keyCode === $.ui.keyCode.UP) {
event.preventDefault();
this.active.focus();
}
},
// Alt+page up/down moves focus to the previous/next tab (and activates)
_handlePageNav: function (event) {
if (event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP) {
this._activate(this._focusNextTab(this.options.active - 1, false));
return true;
}
if (event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN) {
this._activate(this._focusNextTab(this.options.active + 1, true));
return true;
}
},
_findNextTab: function (index, goingForward) {
var lastTabIndex = this.tabs.length - 1;
function constrain() {
if (index > lastTabIndex) {
index = 0;
}
if (index < 0) {
index = lastTabIndex;
}
return index;
}
while ($.inArray(constrain(), this.options.disabled) !== -1) {
index = goingForward ? index + 1 : index - 1;
}
return index;
},
_focusNextTab: function (index, goingForward) {
index = this._findNextTab(index, goingForward);
this.tabs.eq(index).focus();
return index;
},
_setOption: function (key, value) {
if (key === "active") {
// _activate() will handle invalid values and update this.options
this._activate(value);
return;
}
if (key === "disabled") {
// don't use the widget factory's disabled handling
this._setupDisabled(value);
return;
}
this._super(key, value);
if (key === "collapsible") {
this.element.toggleClass("ui-tabs-collapsible", value);
// Setting collapsible: false while collapsed; open first panel
if (!value && this.options.active === false) {
this._activate(0);
}
}
if (key === "event") {
this._setupEvents(value);
}
if (key === "heightStyle") {
this._setupHeightStyle(value);
}
},
_tabId: function (tab) {
return tab.attr("aria-controls") || "ui-tabs-" + getNextTabId();
},
_sanitizeSelector: function (hash) {
return hash
? hash.replace(/[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g, "\\$&")
: "";
},
refresh: function () {
var options = this.options,
lis = this.tablist.children(":has(a[href])");
// get disabled tabs from class attribute from HTML
// this will get converted to a boolean if needed in _refresh()
options.disabled = $.map(
lis.filter(".ui-state-disabled"),
function (tab) {
return lis.index(tab);
},
);
this._processTabs();
// was collapsed or no tabs
if (options.active === false || !this.anchors.length) {
options.active = false;
this.active = $();
// was active, but active tab is gone
} else if (
this.active.length &&
!$.contains(this.tablist[0], this.active[0])
) {
// all remaining tabs are disabled
if (this.tabs.length === options.disabled.length) {
options.active = false;
this.active = $();
// activate previous tab
} else {
this._activate(
this._findNextTab(Math.max(0, options.active - 1), false),
);
}
// was active, active tab still exists
} else {
// make sure active index is correct
options.active = this.tabs.index(this.active);
}
this._refresh();
},
_refresh: function () {
this._setupDisabled(this.options.disabled);
this._setupEvents(this.options.event);
this._setupHeightStyle(this.options.heightStyle);
this.tabs.not(this.active).attr({
"aria-selected": "false",
tabIndex: -1,
});
this.panels.not(this._getPanelForTab(this.active)).hide().attr({
"aria-expanded": "false",
"aria-hidden": "true",
});
// Make sure one tab is in the tab order
if (!this.active.length) {
this.tabs.eq(0).attr("tabIndex", 0);
} else {
this.active.addClass("ui-tabs-active ui-state-active").attr({
"aria-selected": "true",
tabIndex: 0,
});
this._getPanelForTab(this.active).show().attr({
"aria-expanded": "true",
"aria-hidden": "false",
});
}
},
_processTabs: function () {
var that = this;
this.tablist = this._getList()
.addClass(
"ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all",
)
.attr("role", "tablist");
this.tabs = this.tablist
.find("> li:has(a[href])")
.addClass("ui-state-default ui-corner-top")
.attr({
role: "tab",
tabIndex: -1,
});
this.anchors = this.tabs
.map(function () {
return $("a", this)[0];
})
.addClass("ui-tabs-anchor")
.attr({
role: "presentation",
tabIndex: -1,
});
this.panels = $();
this.anchors.each(function (i, anchor) {
var selector,
panel,
panelId,
anchorId = $(anchor).uniqueId().attr("id"),
tab = $(anchor).closest("li"),
originalAriaControls = tab.attr("aria-controls");
// inline tab
if (isLocal(anchor)) {
selector = anchor.hash;
panel = that.element.find(that._sanitizeSelector(selector));
// remote tab
} else {
panelId = that._tabId(tab);
selector = "#" + panelId;
panel = that.element.find(selector);
if (!panel.length) {
panel = that._createPanel(panelId);
panel.insertAfter(that.panels[i - 1] || that.tablist);
}
panel.attr("aria-live", "polite");
}
if (panel.length) {
that.panels = that.panels.add(panel);
}
if (originalAriaControls) {
tab.data("ui-tabs-aria-controls", originalAriaControls);
}
tab.attr({
"aria-controls": selector.substring(1),
"aria-labelledby": anchorId,
});
panel.attr("aria-labelledby", anchorId);
});
this.panels
.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom")
.attr("role", "tabpanel");
},
// allow overriding how to find the list for rare usage scenarios (#7715)
_getList: function () {
return this.element.find("ol,ul").eq(0);
},
_createPanel: function (id) {
return $("<div>")
.attr("id", id)
.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom")
.data("ui-tabs-destroy", true);
},
_setupDisabled: function (disabled) {
if ($.isArray(disabled)) {
if (!disabled.length) {
disabled = false;
} else if (disabled.length === this.anchors.length) {
disabled = true;
}
}
// disable tabs
for (var i = 0, li; (li = this.tabs[i]); i++) {
if (disabled === true || $.inArray(i, disabled) !== -1) {
$(li).addClass("ui-state-disabled").attr("aria-disabled", "true");
} else {
$(li).removeClass("ui-state-disabled").removeAttr("aria-disabled");
}
}
this.options.disabled = disabled;
},
_setupEvents: function (event) {
var events = {
click: function (event) {
event.preventDefault();
},
};
if (event) {
$.each(event.split(" "), function (index, eventName) {
events[eventName] = "_eventHandler";
});
}
this._off(this.anchors.add(this.tabs).add(this.panels));
this._on(this.anchors, events);
this._on(this.tabs, { keydown: "_tabKeydown" });
this._on(this.panels, { keydown: "_panelKeydown" });
this._focusable(this.tabs);
this._hoverable(this.tabs);
},
_setupHeightStyle: function (heightStyle) {
var maxHeight,
parent = this.element.parent();
if (heightStyle === "fill") {
maxHeight = parent.height();
maxHeight -= this.element.outerHeight() - this.element.height();
this.element.siblings(":visible").each(function () {
var elem = $(this),
position = elem.css("position");
if (position === "absolute" || position === "fixed") {
return;
}
maxHeight -= elem.outerHeight(true);
});
this.element
.children()
.not(this.panels)
.each(function () {
maxHeight -= $(this).outerHeight(true);
});
this.panels
.each(function () {
$(this).height(
Math.max(
0,
maxHeight - $(this).innerHeight() + $(this).height(),
),
);
})
.css("overflow", "auto");
} else if (heightStyle === "auto") {
maxHeight = 0;
this.panels
.each(function () {
maxHeight = Math.max(maxHeight, $(this).height("").height());
})
.height(maxHeight);
}
},
_eventHandler: function (event) {
var options = this.options,
active = this.active,
anchor = $(event.currentTarget),
tab = anchor.closest("li"),
clickedIsActive = tab[0] === active[0],
collapsing = clickedIsActive && options.collapsible,
toShow = collapsing ? $() : this._getPanelForTab(tab),
toHide = !active.length ? $() : this._getPanelForTab(active),
eventData = {
oldTab: active,
oldPanel: toHide,
newTab: collapsing ? $() : tab,
newPanel: toShow,
};
event.preventDefault();
if (
tab.hasClass("ui-state-disabled") ||
// tab is already loading
tab.hasClass("ui-tabs-loading") ||
// can't switch durning an animation
this.running ||
// click on active header, but not collapsible
(clickedIsActive && !options.collapsible) ||
// allow canceling activation
this._trigger("beforeActivate", event, eventData) === false
) {
return;
}
options.active = collapsing ? false : this.tabs.index(tab);
this.active = clickedIsActive ? $() : tab;
if (this.xhr) {
this.xhr.abort();
}
if (!toHide.length && !toShow.length) {
$.error("jQuery UI Tabs: Mismatching fragment identifier.");
}
if (toShow.length) {
this.load(this.tabs.index(tab), event);
}
this._toggle(event, eventData);
},
// handles show/hide for selecting tabs
_toggle: function (event, eventData) {
var that = this,
toShow = eventData.newPanel,
toHide = eventData.oldPanel;
this.running = true;
function complete() {
that.running = false;
that._trigger("activate", event, eventData);
}
function show() {
eventData.newTab
.closest("li")
.addClass("ui-tabs-active ui-state-active");
if (toShow.length && that.options.show) {
that._show(toShow, that.options.show, complete);
} else {
toShow.show();
complete();
}
}
// start out by hiding, then showing, then completing
if (toHide.length && this.options.hide) {
this._hide(toHide, this.options.hide, function () {
eventData.oldTab
.closest("li")
.removeClass("ui-tabs-active ui-state-active");
show();
});
} else {
eventData.oldTab
.closest("li")
.removeClass("ui-tabs-active ui-state-active");
toHide.hide();
show();
}
toHide.attr({
"aria-expanded": "false",
"aria-hidden": "true",
});
eventData.oldTab.attr("aria-selected", "false");
// If we're switching tabs, remove the old tab from the tab order.
// If we're opening from collapsed state, remove the previous tab from the tab order.
// If we're collapsing, then keep the collapsing tab in the tab order.
if (toShow.length && toHide.length) {
eventData.oldTab.attr("tabIndex", -1);
} else if (toShow.length) {
this.tabs
.filter(function () {
return $(this).attr("tabIndex") === 0;
})
.attr("tabIndex", -1);
}
toShow.attr({
"aria-expanded": "true",
"aria-hidden": "false",
});
eventData.newTab.attr({
"aria-selected": "true",
tabIndex: 0,
});
},
_activate: function (index) {
var anchor,
active = this._findActive(index);
// trying to activate the already active panel
if (active[0] === this.active[0]) {
return;
}
// trying to collapse, simulate a click on the current active header
if (!active.length) {
active = this.active;
}
anchor = active.find(".ui-tabs-anchor")[0];
this._eventHandler({
target: anchor,
currentTarget: anchor,
preventDefault: $.noop,
});
},
_findActive: function (index) {
return index === false ? $() : this.tabs.eq(index);
},
_getIndex: function (index) {
// meta-function to give users option to provide a href string instead of a numerical index.
if (typeof index === "string") {
index = this.anchors.index(
this.anchors.filter("[href$='" + index + "']"),
);
}
return index;
},
_destroy: function () {
if (this.xhr) {
this.xhr.abort();
}
this.element.removeClass(
"ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible",
);
this.tablist
.removeClass(
"ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all",
)
.removeAttr("role");
this.anchors
.removeClass("ui-tabs-anchor")
.removeAttr("role")
.removeAttr("tabIndex")
.removeUniqueId();
this.tabs.add(this.panels).each(function () {
if ($.data(this, "ui-tabs-destroy")) {
$(this).remove();
} else {
$(this)
.removeClass(
"ui-state-default ui-state-active ui-state-disabled " +
"ui-corner-top ui-corner-bottom ui-widget-content ui-tabs-active ui-tabs-panel",
)
.removeAttr("tabIndex")
.removeAttr("aria-live")
.removeAttr("aria-busy")
.removeAttr("aria-selected")
.removeAttr("aria-labelledby")
.removeAttr("aria-hidden")
.removeAttr("aria-expanded")
.removeAttr("role");
}
});
this.tabs.each(function () {
var li = $(this),
prev = li.data("ui-tabs-aria-controls");
if (prev) {
li.attr("aria-controls", prev).removeData("ui-tabs-aria-controls");
} else {
li.removeAttr("aria-controls");
}
});
this.panels.show();
if (this.options.heightStyle !== "content") {
this.panels.css("height", "");
}
},
enable: function (index) {
var disabled = this.options.disabled;
if (disabled === false) {
return;
}
if (index === undefined) {
disabled = false;
} else {
index = this._getIndex(index);
if ($.isArray(disabled)) {
disabled = $.map(disabled, function (num) {
return num !== index ? num : null;
});
} else {
disabled = $.map(this.tabs, function (li, num) {
return num !== index ? num : null;
});
}
}
this._setupDisabled(disabled);
},
disable: function (index) {
var disabled = this.options.disabled;
if (disabled === true) {
return;
}
if (index === undefined) {
disabled = true;
} else {
index = this._getIndex(index);
if ($.inArray(index, disabled) !== -1) {
return;
}
if ($.isArray(disabled)) {
disabled = $.merge([index], disabled).sort();
} else {
disabled = [index];
}
}
this._setupDisabled(disabled);
},
load: function (index, event) {
index = this._getIndex(index);
var that = this,
tab = this.tabs.eq(index),
anchor = tab.find(".ui-tabs-anchor"),
panel = this._getPanelForTab(tab),
eventData = {
tab: tab,
panel: panel,
};
// not remote
if (isLocal(anchor[0])) {
return;
}
this.xhr = $.ajax(this._ajaxSettings(anchor, event, eventData));
// support: jQuery <1.8
// jQuery <1.8 returns false if the request is canceled in beforeSend,
// but as of 1.8, $.ajax() always returns a jqXHR object.
if (this.xhr && this.xhr.statusText !== "canceled") {
tab.addClass("ui-tabs-loading");
panel.attr("aria-busy", "true");
this.xhr
.success(function (response) {
// support: jQuery <1.8
// http://bugs.jquery.com/ticket/11778
setTimeout(function () {
panel.html(response);
that._trigger("load", event, eventData);
}, 1);
})
.complete(function (jqXHR, status) {
// support: jQuery <1.8
// http://bugs.jquery.com/ticket/11778
setTimeout(function () {
if (status === "abort") {
that.panels.stop(false, true);
}
tab.removeClass("ui-tabs-loading");
panel.removeAttr("aria-busy");
if (jqXHR === that.xhr) {
delete that.xhr;
}
}, 1);
});
}
},
_ajaxSettings: function (anchor, event, eventData) {
var that = this;
return {
url: anchor.attr("href"),
beforeSend: function (jqXHR, settings) {
return that._trigger(
"beforeLoad",
event,
$.extend({ jqXHR: jqXHR, ajaxSettings: settings }, eventData),
);
},
};
},
_getPanelForTab: function (tab) {
var id = $(tab).attr("aria-controls");
return this.element.find(this._sanitizeSelector("#" + id));
},
});
})(jQuery);
(function ($, undefined) {})(jQuery);
(function ($, window) {
$.mobile.iosorientationfixEnabled = true;
// This fix addresses an iOS bug, so return early if the UA claims it's something else.
var ua = navigator.userAgent,
zoom,
evt,
x,
y,
z,
aig;
if (
!(
/iPhone|iPad|iPod/.test(navigator.platform) &&
/OS [1-5]_[0-9_]* like Mac OS X/i.test(ua) &&
ua.indexOf("AppleWebKit") > -1
)
) {
$.mobile.iosorientationfixEnabled = false;
return;
}
zoom = $.mobile.zoom;
function checkTilt(e) {
evt = e.originalEvent;
aig = evt.accelerationIncludingGravity;
x = Math.abs(aig.x);
y = Math.abs(aig.y);
z = Math.abs(aig.z);
// If portrait orientation and in one of the danger zones
if (
!window.orientation &&
(x > 7 || (((z > 6 && y < 8) || (z < 8 && y > 6)) && x > 5))
) {
if (zoom.enabled) {
zoom.disable();
}
} else if (!zoom.enabled) {
zoom.enable();
}
}
$.mobile.document.on("mobileinit", function () {
if ($.mobile.iosorientationfixEnabled) {
$.mobile.window
.bind("orientationchange.iosorientationfix", zoom.enable)
.bind("devicemotion.iosorientationfix", checkTilt);
}
});
})(jQuery, this);
(function ($, window, undefined) {
var $html = $("html"),
$window = $.mobile.window;
//remove initial build class (only present on first pageshow)
function hideRenderingClass() {
$html.removeClass("ui-mobile-rendering");
}
// trigger mobileinit event - useful hook for configuring $.mobile settings before they're used
$(window.document).trigger("mobileinit");
// support conditions
// if device support condition(s) aren't met, leave things as they are -> a basic, usable experience,
// otherwise, proceed with the enhancements
if (!$.mobile.gradeA()) {
return;
}
// override ajaxEnabled on platforms that have known conflicts with hash history updates
// or generally work better browsing in regular http for full page refreshes (BB5, Opera Mini)
if ($.mobile.ajaxBlacklist) {
$.mobile.ajaxEnabled = false;
}
// Add mobile, initial load "rendering" classes to docEl
$html.addClass("ui-mobile ui-mobile-rendering");
// This is a fallback. If anything goes wrong (JS errors, etc), or events don't fire,
// this ensures the rendering class is removed after 5 seconds, so content is visible and accessible
setTimeout(hideRenderingClass, 5000);
$.extend($.mobile, {
// find and enhance the pages in the dom and transition to the first page.
initializePage: function () {
// find present pages
var path = $.mobile.path,
$pages = $(":jqmData(role='page'), :jqmData(role='dialog')"),
hash = path.stripHash(
path.stripQueryParams(path.parseLocation().hash),
),
hashPage = document.getElementById(hash);
// if no pages are found, create one with body's inner html
if (!$pages.length) {
$pages = $("body")
.wrapInner("<div data-" + $.mobile.ns + "role='page'></div>")
.children(0);
}
// add dialogs, set data-url attrs
$pages.each(function () {
var $this = $(this);
// unless the data url is already set set it to the pathname
if (!$this[0].getAttribute("data-" + $.mobile.ns + "url")) {
$this.attr(
"data-" + $.mobile.ns + "url",
$this.attr("id") || location.pathname + location.search,
);
}
});
// define first page in dom case one backs out to the directory root (not always the first page visited, but defined as fallback)
$.mobile.firstPage = $pages.first();
// define page container
$.mobile.pageContainer = $.mobile.firstPage
.parent()
.addClass("ui-mobile-viewport")
.pagecontainer();
// initialize navigation events now, after mobileinit has occurred and the page container
// has been created but before the rest of the library is alerted to that fact
$.mobile.navreadyDeferred.resolve();
// alert listeners that the pagecontainer has been determined for binding
// to events triggered on it
$window.trigger("pagecontainercreate");
// cue page loading message
$.mobile.loading("show");
//remove initial build class (only present on first pageshow)
hideRenderingClass();
// if hashchange listening is disabled, there's no hash deeplink,
// the hash is not valid (contains more than one # or does not start with #)
// or there is no page with that hash, change to the first page in the DOM
// Remember, however, that the hash can also be a path!
if (
!(
$.mobile.hashListeningEnabled &&
$.mobile.path.isHashValid(location.hash) &&
($(hashPage).is(":jqmData(role='page')") ||
$.mobile.path.isPath(hash) ||
hash === $.mobile.dialogHashKey)
)
) {
// Store the initial destination
if ($.mobile.path.isHashValid(location.hash)) {
$.mobile.navigate.history.initialDst = hash.replace("#", "");
}
// make sure to set initial popstate state if it exists
// so that navigation back to the initial page works properly
if ($.event.special.navigate.isPushStateEnabled()) {
$.mobile.navigate.navigator.squash(path.parseLocation().href);
}
$.mobile.changePage($.mobile.firstPage, {
transition: "none",
reverse: true,
changeHash: false,
fromHashChange: true,
});
} else {
// trigger hashchange or navigate to squash and record the correct
// history entry for an initial hash path
if (!$.event.special.navigate.isPushStateEnabled()) {
$window.trigger("hashchange", [true]);
} else {
// TODO figure out how to simplify this interaction with the initial history entry
// at the bottom js/navigate/navigate.js
$.mobile.navigate.history.stack = [];
$.mobile.navigate(
$.mobile.path.isPath(location.hash)
? location.hash
: location.href,
);
}
}
},
});
$(function () {
//Run inlineSVG support test
$.support.inlineSVG();
// check which scrollTop value should be used by scrolling to 1 immediately at domready
// then check what the scroll top is. Android will report 0... others 1
// note that this initial scroll won't hide the address bar. It's just for the check.
// hide iOS browser chrome on load if hideUrlBar is true this is to try and do it as soon as possible
if ($.mobile.hideUrlBar) {
window.scrollTo(0, 1);
}
// if defaultHomeScroll hasn't been set yet, see if scrollTop is 1
// it should be 1 in most browsers, but android treats 1 as 0 (for hiding addr bar)
// so if it's 1, use 0 from now on
$.mobile.defaultHomeScroll =
!$.support.scrollTop || $.mobile.window.scrollTop() === 1 ? 0 : 1;
//dom-ready inits
if ($.mobile.autoInitializePage) {
$.mobile.initializePage();
}
// window load event
// hide iOS browser chrome on load if hideUrlBar is true this is as fall back incase we were too early before
if ($.mobile.hideUrlBar) {
$window.load($.mobile.silentScroll);
}
if (!$.support.cssPointerEvents) {
// IE and Opera don't support CSS pointer-events: none that we use to disable link-based buttons
// by adding the 'ui-disabled' class to them. Using a JavaScript workaround for those browser.
// https://github.com/jquery/jquery-mobile/issues/3558
// DEPRECATED as of 1.4.0 - remove ui-disabled after 1.4.0 release
// only ui-state-disabled should be present thereafter
$.mobile.document.delegate(
".ui-state-disabled,.ui-disabled",
"vclick",
function (e) {
e.preventDefault();
e.stopImmediatePropagation();
},
);
}
});
})(jQuery, this);
});