mopidy-musicbox-webclient/mopidy_musicbox_webclient/static/js/jquery.mobile.iscrollview.js
woutervanwijk b39530e882 merged #37
2014-09-19 00:59:45 +02:00

1861 lines
79 KiB
JavaScript

/******************************************************************************
jslint directives. In case you hate yourself, and need that reinforced...
You will still get a few warnings that can't be turned off, or that I'm just
too stubborn to "fix"
sloppy, white: let me indent any way I damn please! I like to line things
up nice and purty.
nomen: tolerate leading _ for variable names. Leading _ is a requirement for
JQuery Widget Factory private members
*******************************************************************************/
/*jslint browser: true, sloppy: true, white: true, nomen: true, regexp: true, todo: true,
maxerr: 50, indent: 2 */
/*global jQuery:false, iScroll:false, console:false, Event:false*/
/*******************************************************************************
But instead, be kind to yourself, and use jshint.
Note jshint nomen and white options are opposite of jslint
You can't specify an indent of you use white: false, otherwise it will
still complain
*******************************************************************************/
/*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true,
curly:true, browser:true, jquery:true, indent:2, maxerr:50, sloppy:true, white:false, nomen:false,
regexp:false, todo:true */
/*
jquery.mobile.iscrollview.js
Version: 1.2.6
jQuery Mobile iScroll4 view widget
Copyright (c), 2012, 2013 Watusiware Corporation
Distributed under the MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following
conditions: NO ADDITIONAl CONDITIONS.
The above copyright notice and this permission notice shall be included in all copies
or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
Derived in part from jquery.mobile.iscroll.js:
Portions Copyright (c) Kazuhiro Osawa
Dual licensed under the MIT or GPL Version 2 licenses.
Derived in part from (jQuery mobile) jQuery UI Widget-factory
plugin boilerplate (for 1.8/9+)
Author: @scottjehl
Further changes: @addyosmani
Licensed under the MIT license
dependency: iScroll 4.1.9 https://github.com/cubiq/iscroll or later (4.2 provided in demo)
jQuery 1.6.4 (JQM 1.0.1) or 1.7.1 (JQM 1.1) or 1.7.2 (JQM 1.2)
JQuery Mobile = 1.0.1 or 1.1 or 1.2-alpha1
*/
; // Ignore jslint/jshint warning - for safety - terminate previous file if unterminated
(function ($, window, document, undefined) { /* Ignore islint warning on "undefined" */
"use strict";
//----------------------------------
// "class constants"
//----------------------------------
var HasTouch = document.ontouchend !== undefined,
IsWebkit = (/webkit/i).test(navigator.appVersion),
IsAndroid = (/android/gi).test(navigator.appVersion),
IsFirefox = (/firefox/i).test(navigator.userAgent),
IsTouchPad = (/hp-tablet/gi).test(navigator.appVersion),
IsIDevice = (/(iPhone|iPad|iPod).*AppleWebKit/).test(navigator.appVersion),
IsIPad = (/iPad.*AppleWebKit/).test(navigator.appVersion),
// IDevice running Mobile Safari - not embedded UIWebKit or Standalone (= saved to desktop)
IsMobileSafari = (/(iPhone|iPad|iPod).*AppleWebKit.*Safari/).test(navigator.appVersion),
// IDevice native app using embedded UIWebView
IsUIWebView = (/(iPhone|iPad|iPod).*AppleWebKit.(?!.*Safari)/).test(navigator.appVersion),
// Standalone is when running a website saved to the desktop (SpringBoard)
IsIDeviceStandalone = IsIDevice && (window.navigator.Standalone !== undefined),
// Kludgey way to seeing if we have JQM v1.0.x, since there apparently is no
// way to access the version number!
JQMIsV1_0 = $.mobile.ignoreContentEnabled === undefined,
nextPageID = 1; // Used to generate event namespaces
/* Placed here instead of anonymous functions to facilitate debugging.
No logging, because these events are too frequent */
function _pageTouchmoveFunc(e) {
e.preventDefault();
}
//===============================================================================
// This essentially subclasses iScroll. Originally, this was just so that we could
// inject an iscrollview variable at the time of construction (so that it is
// available from the refresh callback which is first called during construction).
// But now we override several iScroll methods, as well.
//===============================================================================
// See: www.golimojo.com/etc/js-subclass.html
function _subclass(constructor, superConstructor) {
function SurrogateConstructor() {}
SurrogateConstructor.prototype = superConstructor.prototype;
var prototypeObject = new SurrogateConstructor();
prototypeObject.constructor = constructor;
constructor.prototype = prototypeObject;
}
function IScroll(iscrollview, scroller, options) {
// We need to add an iscrollview member to iScroll, so that we can efficiently
// pass the iscrollview when triggering jQuery events. Otherwise, we'd have to
// make a call to $(wrapper).jqmData() on each event trigger, which could have an impact
// on performance for high-frequency events.
this.iscrollview = iscrollview;
// The following functions are called from the proxy event functions. These are things
// we want to do in certain iScroll4 events.
// Emulate bottomOffset functionality in case iScroll doesn't have patch for bottomOffset
this._emulateBottomOffset = function(e) {
if (this.iscrollview.options.emulateBottomOffset) {
this.maxScrollY = this.wrapperH - this.scrollerH +
this.minScrollY + this.iscrollview.options.bottomOffset;
}
};
// Allow mouse clicks through to input elements
// Note that this is not an issue for touch devices, just mouse
this._fixInput = function(e) {
if (this.iscrollview.options.fixInput ) {
var tagName,
target = e.target;
while (target.nodeType !== 1) { target = target.parentNode; }
tagName = target.tagName.toLowerCase();
if (tagName === "select" || tagName === "input" || tagName === "textarea") {
return;
}
}
// If preventTouchHover, stop hover from occuring inside scroller for jQuery Mobile 1.0
// (Not used for 1.1)
if (this.iscrollview.options.preventTouchHover) { e.stopImmediatePropagation(); }
else { e.preventDefault(); }
};
// Perform an iScroll callback.
this._doCallback = function(callbackName, e, f) {
var v = this.iscrollview,
then = v._logCallback(callbackName, e);
if (f) { f.call(this, e); } // Perform passed function if present
v._trigger(callbackName.toLowerCase(), e, {"iscrollview": v}); // Then trigger widget event
v._logCallback(callbackName, e, then);
};
// Override _bind and _unbind functions in iScroll, so that we can monitor performance,
// gain control over events reaching/not reaching iScroll, and potentially use jQuery events
// instead of addEventListener().
//
// As of v1.2, using jQuery events is an experimental feature, and does not work in all
// scenarios. For example, jQuery 1.7.1 breaks mousewheel support. This feature is left in
// only to permit further experimentation.
//
// If using jQuery events, we ignore bubble (really, useCapture) parameter. Fortunately,
// iScroll never uses it.
//
// If using jQuery events, we substitute jQuery's mouseleave for mouseout, to prevent iScroll
// from getting a cascade of events when the mouse enters some inner element within the
// scroller. iScroll is only interested in the mouse leaving the scroller to the OUTSIDE.
// While iScroll doesn't spend much time in the callback if moving to an inner element,
// the cascade of events is annoying when monitoring performance with the debug option.
this._bind = function (type, el, bubble) {
var jqEvents = this.iscrollview.options.bindIscrollUsingJqueryEvents,
_type = jqEvents && type === "mouseout" ? "mouseleave" : type;
// Ignore attempt to bind to orientationchange or resize, since the widget handles that
if (type === "orientationchange" || type === "resize") {
this.iscrollview._logIscrollEvent("iScroll bind (ignored)", type);
return;
}
this.iscrollview._logIscrollEvent("iScroll bind", type);
if (jqEvents) { (el ? $(el) : this.iscrollview.$scroller).bind(_type, $.proxy(this.handleEvent, this)); }
else { (el || this.scroller).addEventListener(_type, this, !!bubble); }
};
this._unbind = function(type, el, bubble) {
var jqEvents = this.iscrollview.options.bindIscrollUsingJqueryEvents,
_type = jqEvents && type === "mouseout" ? "mouseleave" : type;
if (type === "orientationchange" || type === "resize") {
this.iscrollview._logIscrollEvent("iScroll unbind (ignored)");
return;
}
this.iscrollview._logIscrollEvent("iScroll unbind", type);
if (jqEvents) { $(el || this.iscrollview.$scroller).unbind(_type, this.handleEvent); }
else { (el || this.scroller).removeEventListener(_type, this, !!bubble); }
};
// Save a reference to the original handleEvent in iScroll. We'll need to call it from our
// override.
this._origHandleEvent = iScroll.prototype.handleEvent;
// Shim around iScroll.handleEvent, allows us to trace
this.handleEvent = function(e) {
var jqEvents = this.iscrollview.options.bindIscrollUsingJqueryEvents,
then;
then = this.iscrollview._logIscrollEvent("iScroll.handleEvent", e);
// If jQuery mouseleave, make iScroll think we are handling a mouseout event
if (jqEvents && e.type === "mouseleave") {
e.type = "mouseout";
this._origHandleEvent(e);
e.type = "mouseleave";
}
else { this._origHandleEvent(e); }
this.iscrollview._logIscrollEvent("iScroll.handleEvent", e, then);
};
// Override _resize function in iScroll, which calls refresh() and is only called on resize
// and orientationchange events. We call refresh() when necessary, so these are redundant.
// As well, some refreshes are deferred, and the user will need to refresh any jQuery Mobile
// widgets using a callbackBefore. So, it makes no sense to have iScroll do event-based
// refresh.
this._resize = function() { };
iScroll.call(this, scroller, options);
}
_subclass(IScroll, iScroll);
$.widget("mobile.iscrollview", $.mobile.widget, {
widgetEventPrefix: "iscroll_",
//=========================================================
// All instance variables are declared here. This is not
// strictly necessary, but is helpful to document the use
// of instance variables.
//=========================================================
iscroll: null, // The underlying iScroll object
$window: $(window),
$wrapper: [], // The wrapper element
$scroller: [], // The scroller element (first child of wrapper)
$scrollerContent: [], // Content of scroller, sandwitched between any pull-down/pull-up
$pullDown: [], // The pull-down element (if any)
$pullUp: [], // The pull-up element (if any)
$pullUpSpacer: [],
$page: [], // The page element that contains the wrapper
_wrapperHeightAdjustForBoxModel: 0, // This is set in _create
_firstScrollerExpand: true, // True on first scroller expand, so we can capture original CSS
createdAt: null, // Time when created - used as unique ID
pageID: null, // Each page that has 1 or more iscrollviews gets a unique page ID #
instanceID: null, // Each isntance of iscrollview created on a page gets a unique instance ID #
// True if this scroller content is "dirty" - i.e. needs refresh because refresh
// was deferred when the page was not the active page. This does NOT imply that the wrapper
// needs to be refreshed - see _sizeDirty, below.
_dirty: false,
_dirtyCallbackBefore: null,
_dirtyCallbackAfter: null,
_sizeDirty: false, // True if wrapper resize is needed because page size or fixed content
// size changed
//----------------------------------------------------
// Options to be used as defaults
//----------------------------------------------------
options: {
// iScroll4 options
// We only define those options here which have values that differ from
// iscroll4 defaults.
hScroll: false, // iScroll4 default is true
hScrollbar: false, // iScroll4 default is true
// Additional iScroll4 options will be back-filled from iscroll4
// iscrollview widget options
debug: false, // Enable some messages to console
// Debug true needed for any trace options
traceResizeWrapper: false, // Enable to trace resize wrapper
traceRefresh: false, // Enable to trace refresh
traceCreateDestroy: false, // Enable to trace create/destroy
traceIscrollEvents: false, // Enable to trace events handled by iScroll
tracedIscrollEvents: [], // List of specific iScroll events to trace, empty list for all
// Items are strings, like "touchstart"
traceWidgetEvents: false, // Enable to trace events registered by widget
// Note: in some cases we might bind to multiple events. You will have to include the multiple
// events in one string to filter on such a bind. For example, "resize orientationchange"
tracedWidgetEvents: [], // List of specific widget events to trace
traceIscrollCallbacks: false, // Enable to trace iScroll callbacks to the widget
tracedIscrollCallbacks: [], // List of specific iScroll callbacks to trace, empty list for all
// Items are strings, like "onRefresh"
traceWidgetCallbacks: false,
tracedWidgetCallbacks: [],
// bottomOffset is currently only in Watusi-patched iScroll. We emulate it in case it isn't
// there.
bottomOffset: 0,
emulateBottomOffset: true,
pageClass: "iscroll-page", // Class to be applied to pages containing this widget
wrapperClass: "iscroll-wrapper", // Class to be applied to wrapper containing this widget
scrollerClass: "iscroll-scroller", // Class to be applied to scroller within wrapper
pullDownClass: "iscroll-pulldown", // Class for pulldown element (if any)
pullUpClass: "iscroll-pullup", // Class for pullup element (if any)
pullLabelClass: "iscroll-pull-label", // Class for pull element label span
pullUpSpacerClass: "iscroll-pullup-spacer", // Class added to generated pullup spacer
scrollerContentClass: "iscroll-content", // Real content of scroller, not including pull-up, pull-down
fixedHeightClass: "iscroll-fixed", // Class applied to elements that match fixedHeightSelector
// The widget adds the fixedHeightClass to all elements that match fixedHeightSelector.
// Don't add the fixedHeightClass to elements manually. Use data-iscroll-fixed instead.
fixedHeightSelector: ":jqmData(role='header'), :jqmData(role='footer'), :jqmData(iscroll-fixed)",
// true to resize the wrapper to take all viewport space after fixed-height elements
// (typically header/footer)
// false to not change the size of the wrapper
// For example, if using multiple iscrollview widgets on the same page, a maximum
// of one of them could resize to remaining space. You would need to explicitly
// set the height of additional iscrollviews and give them the fixed height class.
resizeWrapper: true,
// Space-separated list of events on which to resize/refresh iscroll4
// On some mobile devices you may wish to add/substitute orientationchange event
// iOS 4.x will trigger resize twice then orientationchange
// iOS 5.x will trigger resize once then orientationchange
// Android devices can trigger multiple events, but generally orientationchange before resize
// Devices are inconsistent as to when they first report the new width/height
// Android tends to first trigger orientationchange with the width/height unchanged, the
// orientationchange with the new width/height.
// Experimentation with other devices would be useful
resizeEvents: "resize" + ($.support.orientation ? " orientationchange" : ""),
// Refresh iscrollview on page show event. This should be true if content inside a
// scrollview might change while the page is cached but not shown, and application hasn't
// called refresh(), or deferRefresh is false.
refreshOnPageBeforeShow: false,
// true to fix iscroll4 input element focus problem in the widget.
// false if you are using a patched iscroll4 with different fix or to
// disable for some other reason
fixInput: true,
wrapperAdd: 0, // Shouldn't be necessary, but in case user needs to fudge
// Can be + or -
// Timeout to allow page to render prior to refresh()
refreshDelay: IsAndroid ? 200 : 0, // Wild-ass guesses
// true to set the minimum height of scroller content (not including
// any pull-down or pull-up) to the height of the wrapper. Note that
// if there is a pull-down or pull-up, then this is done regardless of
// this option, because you have to be able to scroll the empty content
// to access the pull-down or pull-up. Set this option false if you do
// not want to show a scrollbar on short content. However, this will have
// the side-effect of making the "empty" part of the scroller non-draggable.
// Leaving this true provides a more consistent UI behaviour.
scrollShortContent: true,
// Normally, we need the wrapper to have no padding. Otherwise, the result will look awkward,
// you won't be able to grab the padded area to scroll, etc.
removeWrapperPadding: true,
// But we want to add that padding back inside the scroller. We add a div around the content
// inside any pull-down/pull-up to replace the padding removed from the wrapper.
addScrollerPadding: true,
// On some platforms (iOS, for example) we need to scroll to top after orientation change,
// because the address bar pushed the window down. jQuery Mobile handles this for page links,
// but doesn't for orientationchange.
// If you have multiple scrollers, only enable this for one of them
scrollTopOnResize: true,
scrollTopOnOrientatationChange: true,
// iScroll scrolls the first child of the wrapper. I don't see a use case for having more
// than one child. What kind of mess is going to be shown in that case? So, by default, we
// just wrap ALL of the children of the wrapper with a new <div> that will be the scroller.
// This way you don't need to worry about wrapping all the elements to be scrolled if there
// is more than one. If there is only one child, we create this <div> unnecessarily, but -
// big deal. If, for some reason, you want to create the markup for the scroller yourself,
// set this to false.
createScroller: true,
// True to defer refresh() on non-active pages until pagebeforeshow. This avoids
// unnecessary refresh in case of resize/orientation change when pages are cached,
// as well as unnecessary refresh when pages are updated when they are not the active
// page.
deferNonActiveRefresh: true,
// Same deal, for re-sizing the wrapper
deferNonActiveResize: true,
// True to prevent hover in scroller touch devices. If this is false, you will get
// "piano keyboard" effect in JQM <1.1 when scrolling due to hover, which is both
// time-consuming and distracting. A negative is that with the current implementation, you will
// never get a "hover" visual effect within a scroller on touch devices, even when not scrolling.
// But you still will on desktop browser with mouse, and you will still get "down" effect
// when a link is selected. This really is a jQuery Mobile problem with listview, and is
// fixed in JQM 1.1.
preventTouchHover: JQMIsV1_0 && HasTouch, // Enable if touch device and JQM version is < 1.1
// This is an experimental feature under development and DOES NOT WORK completely!
// For one, it breaks mousewheel with jQuery Mobile 1.1 (because jQuery Mobile 1.1 breaks
// mousewheel...)
bindIscrollUsingJqueryEvents: false,
// If fastDestroy is true, don't tear down the widget on destroy. The assumption is destroy
// will only be called when the page is removed, so there is no need. Is anyone really
// going to un-enhance a scroller? If so, set this to false, but then you will have to
// fix the unbind issue...
fastDestroy: false,
// Prevent scrolling the page by grabbing areas outside of the scroller.
// Normally, this should be true. Set this false if you are NOT using a fixed-height page,
// but instead are using iScroll to scroll an area within a scollable page. If you have
// multiple scrollers on a scrollable page, then set this false for all of them.
// Note that we ALWAYS prevent scrolling the page by dragging inside the scroller.
preventPageScroll: true,
pullDownResetText : "Pull down to refresh...",
pullDownPulledText : "Release to refresh...",
pullDownLoadingText : "Loading...",
pullUpResetText : "Pull up to refresh...",
pullUpPulledText : "Release to refresh...",
pullUpLoadingText : "Loading...",
pullPulledClass : "iscroll-pull-pulled",
pullLoadingClass : "iscroll-pull-loading",
//-------------------------------------------------------------
// For better or worse, widgets have two mechanisms for dealing
// with events. The needs to be a set of options that correspond
// to each event. If present, the option is a function. As
// well, the widget prepends the widget event prefix ("iscroll_")
// to each event name and triggers a jQuery event by that name.
// BOTH mechanisms can be used simultaneously, though not sure
// why you'd want to. If you need to handle an event during
// iScroll4 instantiation, (only one I know about that might be
// called is refresh) then you have to use a function option.
//-------------------------------------------------------------
onrefresh: null,
onbeforescrollstart: null,
onscrollstart: null,
onbeforescrollmove: null,
onscrollmove: null,
onbeforescrollend: null,
onscrollend: null,
ontouchend: null,
ondestroy: null,
onzoomstart: null,
onzoom: null,
onzoomend: null,
onpulldownreset: null,
onpulldownpulled: null,
onpulldown: null,
onpullupreset: null,
onpulluppulled: null,
onpullup: null,
onbeforerefresh: null,
onafterrefresh: null
},
//---------------------------------------------------------------------------------------
// Array of keys of options that are widget-only options (not options in iscroll4 object)
//---------------------------------------------------------------------------------------
_widgetOnlyOptions: [
"debug",
"traceIscrollEvents",
"tracedIscrollEvents",
"traceIscrollCallbacks",
"tracedIscrollCallbacks",
"traceWidgetEvents",
"tracedWidgetEvents",
"traceWidgetCallbacks",
"tracedWidgetCallbacks",
"traceResizeWrapper",
"traceRefresh",
"traceCreateDestroy",
"bottomOffset",
"emulateBottomOffset",
"pageClass",
"wrapperClass",
"scrollerClass",
"pullDownClass",
"pullUpClass",
"scrollerContentClass",
"pullLabelClass",
"pullUpSpacerClass",
"fixedHeightSelector",
"resizeWrapper",
"resizeEvents",
"refreshOnPageBeforeShow",
"fixInput",
"wrapperAdd",
"refreshDelay",
"scrollShortContent",
"removeWrapperPadding",
"addScrollerPadding",
"createScroller",
"deferNonActiveRefresh",
"preventTouchHover",
"deferNonActiveResize",
"bindIscrollUsingJqueryEvents",
"scrollTopOnResize",
"scrollTopOnOrientationChange",
"pullDownResetText",
"pullDownPulledText",
"pullDownLoadingText",
"pullUpResetText",
"pullUpPulledText",
"pullUpLoadingText",
"pullPulledClass",
"pullLoadingClass",
"onpulldownreset",
"onpulldownpulled",
"onpulldown",
"onpullupreset",
"onpulluppulled",
"onpullup",
"onbeforerefresh",
"onafterrefresh",
"fastDestroy"
],
//-----------------------------------------------------------------------
// Map of widget event names to corresponding iscroll4 object event names
//-----------------------------------------------------------------------
_event_map: {
onrefresh: "onRefresh",
onbeforescrollstart: "onBeforeScrollStart",
onscrollstart: "onScrollStart",
onbeforescrollmove: "onBeforeScrollMove",
onscrollmove: 'onScrollMove',
onbeforescrollend: 'onBeforeScrollEnd',
onscrollend: "onScrollEnd",
ontouchend: "onTouchEnd",
ondestroy: "onDetroy",
onzoomstart: "onZoomStart",
onzoom: "onZoom",
onzoomend: "onZoomEnd"
},
//------------------------------------------------------------------------------
// Functions that adapt iscroll callbacks to Widget Factory conventions.
// These are copied to the iscroll object's options object on instantiation.
// They call the private _trigger method provided by the widget factory
// base object. Normally, iscroll4 callbacks can be null or omitted. But since
// we can't know in advance whether the corresponding widget events might be bound
// or delegated in the future, we have set a callback for each that calls _trigger.
// This will call the corresponding widget callback as well as trigger the
// corresponding widget event if bound.
//
// Event callbacks are passed two values:
//
// e The underlying DOM event (if any) associated with this event
// d Data map
// iscrollview : The iscrollview object
//------------------------------------------------------------------------------
_proxy_event_funcs: {
onRefresh: function(e) {
this._doCallback("onRefresh", e, function(e) {
this._emulateBottomOffset();
this.iscrollview._pullOnRefresh.call(this.iscrollview,e);
});
},
onBeforeScrollStart: function(e) {
this._doCallback("onBeforeScrollStart", e, function(e) {
this._fixInput(e);
});
},
onScrollStart: function(e) { this._doCallback("onScrollStart", e); },
onBeforeScrollMove: function(e) {
this._doCallback("onBeforeScrollMove", e);
e.preventDefault(); // Don't scroll the page for touchmove inside scroller
},
onScrollMove: function(e) {
this._doCallback("onScrollMove", e, function(e) {
this.iscrollview._pullOnScrollMove.call(this.iscrollview, e);
});
},
onBeforeScrollEnd: function(e) { this._doCallback("onBeforeScrollEnd", e); },
onScrollEnd: function(e) {
this._doCallback("onScrollEnd", e, function(e){
this.iscrollview._pullOnScrollEnd.call(this.iscrollview, e);
});
},
onTouchEnd: function(e) { this._doCallback("onTouchEnd", e); },
onDestroy: function(e) { this._doCallback("onDestroy", e); },
onZoomStart: function(e) { this._doCallback("onZoomStart", e); },
onZoom: function(e) { this._doCallback("onZoom", e); },
onZoomEnd: function(e) { this._doCallback("onZoomEnd", e); }
},
// Merge options from the iscroll object into the widget options
// So, this will backfill iscroll4 defaults that we don't set in
// the widget, giving a full set of iscroll options, and leaving
// widget-only options untouched.
_merge_from_iscroll_options: function() {
var options = $.extend(true, {}, this.iscroll.options);
// Delete event options from the temp options
$.each(this._proxy_event_funcs, function(k,v) {delete options[k];});
if (this.options.emulateBottomOffset) { delete options.bottomOffset; }
$.extend(this.options, options); // Merge result into widget options
},
// Create a set of iscroll4 object options from the widget options.
// We have to omit any widget-specific options that are
// not also iscroll4 options. Also, copy the proxy event functions to the
// iscroll4 options.
_create_iscroll_options: function() {
var options = $.extend(true, {}, this.options); // temporary copy of widget options
// Remove options that are widget-only options
$.each(this._widgetOnlyOptions, function(i,v) {delete options[v];});
// Remove widget event options
$.each(this._event_map, function(k,v) {delete options[k];});
if (this.options.emulateBottomOffset) { delete options.bottomOffset; }
// Add proxy event functions
return $.extend(options, this._proxy_event_funcs);
},
// Formats number with fixed digits
_pad: function(num, digits, padChar) {
var str = num.toString(),
_padChar = padChar || "0";
while (str.length < digits) { str = _padChar + str; }
return str;
},
// Format time for logging
_toTime: function(date) {
return this._pad(date.getHours(), 2) + ":" +
this._pad(date.getMinutes(), 2) + ":" +
this._pad(date.getSeconds(), 2) + "." +
this._pad(date.getMilliseconds(), 3);
},
// Log a message to console
// text - message to log
// now - optional timestamp, if missing generates new timestamp
// Returns timestamp
_log: function(text, now) {
var _now, id, idStr;
if (!this.options.debug) { return null; }
_now = now || new Date();
id = this.$wrapper.attr("id");
idStr = id ? "#" + id : "";
console.log(this._toTime(_now) + " " +
$.mobile.path.parseUrl(this.$page.jqmData("url")).filename + idStr + " " +
text );
return _now;
},
// Log elapsed time from then to now
_logInterval: function(text, then) {
var now;
if (!this.options.debug) { return null; }
now = new Date();
return this._log(text + " " + (now - then) + "mS from " + this._toTime(then), now );
},
// Log an event
// Like _logInterval, but additional optional parameter e
// If e is present, additionally show interval from original event to now
_logEvent: function(text, e, then) {
var now,
eventTime,
haveEvent = e && e instanceof Object,
type = haveEvent ? e.type : e,
_text = type + " " + text;
if (!this.options.debug) { return null; }
now = new Date();
if (then) {
_text += " end " + (+(now-then)) + "mS from " + this._toTime(then);
}
else if (haveEvent) {
_text += " begin";
}
if (haveEvent) {
eventTime = new Date(e.timeStamp);
_text += " (" + (now - eventTime) + "mS from " +e.type + " @ " + this._toTime(eventTime) + ")";
}
return this._log(_text, now);
},
// Log a callback issued by iScroll
_logCallback: function(callbackName, e, then) {
if (!this.options.debug ||
!this.options.traceIscrollCallbacks ||
(this.options.tracedIscrollCallbacks.length !== 0 &&
$.inArray(callbackName, this.options.tracedIscrollCallbacks) === -1) ) {
return null;
}
if (e) { return this._logEvent(callbackName, e, then); }
if (then) { return this._logInterval(callbackName + " end", then); }
return this._log(callbackName + " begin");
},
// Log an event handled by Iscroll
// e can be Event or event name
_logIscrollEvent: function(text, e, then) {
var haveEvent = e instanceof Event,
type = haveEvent ? e.type : e;
if (!this.options.debug ||
!this.options.traceIscrollEvents ||
(this.options.tracedIscrollEvents.length !== 0 &&
$.inArray(type, this.options.tracedIscrollEvents) === -1)) {
return null;
}
return this._logEvent(text, e, then);
},
// Log an event handled by the widget
_logWidgetEvent: function(text, e, then) {
var haveEvent = e instanceof Object,
type = haveEvent ? e.type : e;
if (!this.options.debug ||
!this.options.traceWidgetEvents ||
(this.options.tracedWidgetEvents.length !== 0 &&
$.inArray(type, this.options.tracedWidgetEvents) === -1)) {
return null;
}
return this._logEvent(text, e, then);
},
// Log a callback issued by the widtet
_logWidgetCallback: function(callbackName, e, then) {
if (!this.options.debug ||
!this.options.traceWidgetCallbacks ||
(this.options.tracedWidgetCallbacks.length !== 0 &&
$.inArray(callbackName, this.options.tracedWidgetCallbacks) === -1) ) {
return null;
}
if (e) { return this._logEvent(callbackName, e, then); }
if (then) { return this._logInterval(callbackName + " end", then); }
return this._log(callbackName + " begin");
},
// Log elapsed time from then to now and later to now
_logInterval2: function(text, then, later) {
var now;
if (!this.options.debug) { return; }
now = new Date();
this._log(text + " " +
(now - later) + "mS from " + this._toTime(later) +
" (" + (now - then) + "mS from " + this._toTime(then) + ")" );
},
_startTiming: function() {
if (!this.options.debug) { return null; }
return new Date();
},
// Returns the event namespace for the page containing this widget
_pageEventNamespace: function() {
return ".iscroll_" + this.pageID;
},
// Returns the event namespace for this widget
_instanceEventNamespace: function() {
return this._pageEventNamespace() + "_" + this.instanceID;
},
// Takes a space-separated list of event types, and appends the given namespace to each
_addEventsNamespace: function(types_in, namespace) {
var types = types_in.split(" ");
$.each(types, function(k,v) {types[k] += namespace;});
return types.join(" ");
},
// All bind/unbind done by the widget goes through here, to permit logging
// Note: this used to be called _bind, but starting with JQM 1.2, this is a naming conflict with
// the Widget Factory code in JQM
_isvBind: function(obj, types_in, func, objName) {
var types = this._addEventsNamespace(types_in, this._instanceEventNamespace());
this._logWidgetEvent("bind " + objName, types);
obj.bind(types, $.proxy(func, this));
},
_bindPage: function(types_in, func) {
var types = this._addEventsNamespace(types_in, this._pageEventNamespace());
this._logWidgetEvent("bind $page", types);
this.$page.bind(types, $.proxy(func, this));
},
_isvUnbind: function(obj, types_in, objName) {
var types = this._addEventsNamespace(types_in, this._instanceEventNamespace());
this._logWidgetEvent("unbind " + objName, types);
obj.unbind(types);
},
_unbindPage: function(types_in) {
var types = this._addEventsNamespace(types_in, this._instanceEventNamespace());
this._logWidgetEvent("unbind $page", types);
this.$page.unbind(types);
},
// Currently unused - just in case we need it
_delegate: function(obj, selector, type, func, objName) {
this._logWidgetEvent("delegate " + objName + " " + selector, type);
obj.delegate(selector, type, $.proxy(func, this));
},
_triggerWidget: function(type, e) {
var then = this._logWidgetCallback(type);
this._trigger(type, e, {"iscrollview":this});
this._logWidgetCallback(type, e, then);
},
//-------------------------------------------------------------------
// Returns status of dirty flag, indicating that refresh() was called
// while the page was not active, and refresh will be deferred until
// pagebeforeshow.
//-------------------------------------------------------------------
isDirty: function() {
return this._dirty;
},
//-----------------------------------------------------------------------------
// Restore an element's styles to original
// If the style was never modified by the widget, the value passed in
// originalStyle will be undefined.
//
//If there originally was no style attribute, but styles were added by the
// widget, the value passed in originalStyle will be null.
//
// If there originally was a style attribute, but the widget modified it
// (actually, set some CSS, which changes the style, the value is a string in
// originalStyle.
//-----------------------------------------------------------------------------
_restoreStyle: function($ele, originalStyle) {
if (originalStyle === undefined) { return; }
if (originalStyle === null) { $ele.removeAttr("style"); }
else { $ele.attr("style", originalStyle); }
},
//------------------------------------------------------------------------------
// Functions that we bind to. They are declared as named members rather than as
// inline closures so we can properly unbind them.
//------------------------------------------------------------------------------
_pageBeforeShowFunc: function(e) {
var then = this._logWidgetEvent("_pageBeforeShowFunc", e);
if (this._dirty) {
this.resizeWrapper(true);
this.refresh(null, this._dirtyCallbackBefore, this._dirtyCallbackAfter, true);
this._dirty = false;
this._dirtyCallbackBefore = null;
this._dirtyCallbackAfter = null;
}
else if (this.options.refreshOnPageBeforeShow || this._sizeDirty) {
this.refresh(null,$.proxy(this._resizeWrapper, this),null,true);
}
this._sizeDirty = false;
this._logWidgetEvent("_pageBeforeShowFunc", e, then);
},
// Called on resize events
// TODO: Detect if size is unchanged, and if so just ignore?
_windowResizeFunc: function(e) {
var then = this._logWidgetEvent("_windowResizeFunc", e);
// Defer if not active page
if (this.options.deferNonActiveResize && !(this.$page.is($.mobile.activePage))) {
this._sizeDirty = true;
if (this.options.traceResizeWrapper) { this._log("resizeWrapper() (deferred)"); }
}
else {
this.resizeWrapper(true);
this.refresh(null,null,null,true);
}
this._logWidgetEvent("_windowResizeFunc", e, then);
},
// On some platforms (iOS, for example) you need to scroll back to top after orientation change,
// because the address bar pushed the window down. jQuery Mobile handles this for page links,
// but doesn't for orientationchange
_orientationChangeFunc: function(e) {
var then = this._logWidgetEvent("_orientationChangeFunc", e);
if (this.options.scrollTopOnOrientationChange) {
$.mobile.silentScroll(0);
}
this._logWidgetEvent("_orientationChangeFunc", e, then);
},
// Called when jQuery Mobile updates content such that a reflow is needed. This happens
// on collapsible content, etc.
_updateLayoutFunc: function(e) {
this.refresh();
},
// Get or set the count of instances on the page containing the widget
// This increases or decreases depending on the number of iscrollview widgets currently
// instantiated on the page.
_instanceCount: function(count_in) {
var key = "iscroll-private",
count = 0,
data = this.$page.jqmData(key) || {};
if (count_in !== undefined) {
count = count_in;
data.instanceCount = count;
this.$page.jqmData(key, data);
}
else {
if (data.instanceCount !== undefined) {
count = data.instanceCount;
}
}
return count;
},
_nextInstanceID: function(id_in) {
var key = "iscroll-private",
id = 1,
data = this.$page.jqmData(key) || {};
if (id_in !== undefined) {
id = id_in;
data.nextInstanceID = id;
this.$page.jqmData(key, data);
}
else {
if (data.nextInstanceID !== undefined) {
id = data.nextInstanceID;
}
}
return id;
},
_pageID: function(id_in) {
var key = "iscroll-private",
id = 1,
data = this.$page.jqmData(key) || {};
if (id_in !== undefined) {
id = id_in;
data.pageID = id;
this.$page.jqmData(key, data);
}
else {
if (data.pageID !== undefined) {
id = data.pageID;
}
}
return id;
},
//--------------------------------------------------------------------------
// Adapt the page for this widget
//--------------------------------------------------------------------------
_adaptPage: function() {
var _this = this;
// Only adapt the page if this is the only iscrollview widget instantiated on the page
// If the count >1, then the page has already been adapted. When the count goes back
// to 0, the changes will be un-done
if (this._instanceCount() === 1) {
this.$page.addClass(this.options.pageClass);
this.$page.find(this.options.fixedHeightSelector).each(function() { // Iterate over headers/footers/etc.
$(this).addClass(_this.options.fixedHeightClass);
});
if (HasTouch && this.options.preventPageScroll) {
this._bindPage("touchmove", _pageTouchmoveFunc);
}
}
},
_undoAdaptPage: function() {
var _this = this;
if (this._instanceCount() === 1) {
this.$page.find(this.options.fixedHeightSelector).each(function() { // Iterate over headers/footers/etc.
$(this).removeClass(_this.options.fixedHeightClass);
});
this.$page.removeClass(this.options.pageClass);
}
},
//--------------------------------------------------------
// Calculate total bar heights.
//--------------------------------------------------------
_calculateBarsHeight: function() {
var barsHeight = 0,
fixedHeightSelector = "." + this.options.fixedHeightClass,
// Persistent footers are sometimes inside the page, sometimes outside of all pages! (as
// direct descendant of <body>). And sometimes both. During transitions, the page that
// is transitioning in will have had it's persistent footer moved outside of the page,
// while all other pages will have their persistent footer internal to the page.
//
// To deal with this, we find iscroll-fixed elements in the page, as well as outside
// of the page (as direct descendants of <body>). We avoid double-counting persistent
// footers that have the same data-id. (Experimentally, then, we also permit the user
// to place fixed-height elements outside of the page, but unsure if this is of any
// practical use.)
$barsInPage = this.$page.find(fixedHeightSelector),
$barsOutsidePage = $("body").children(fixedHeightSelector);
$barsInPage.each(function() { // Iterate over headers/footers/etc.
barsHeight += $(this).outerHeight(true);
});
$barsOutsidePage.each(function() {
var id = $(this).jqmData("id"); // Find data-id if present
// Count bars outside of the page if they don't have data-id (so not a persistent
// footer, but something the developer put there and tagged with data-iscroll-fixed class),
// or if a matching data-id is NOT found among the bars that are inside the page.
if (id === "" || !$barsInPage.is( ":jqmData(id='" + id + "')" )) {
barsHeight += $(this).outerHeight(true);
}
});
return barsHeight;
},
//-----------------------------------------------------------------------
// Determine the box-sizing model of an element
// While jQuery normalizes box-sizing models when retriving geometry,
// it doesn't consider it when SETTING geometry. So, this is useful when
// setting geometry. (e.g. the height of the wrapper)
//-----------------------------------------------------------------------
_getBoxSizing: function($elem) {
var boxSizing,
prefix = "";
if (IsFirefox) { prefix = "-moz-"; }
else if (IsWebkit) { prefix = "-webkit-"; } // note: can drop prefix for Chrome >=10, Safari >= 5.1 (534.12)
boxSizing = $elem.css(prefix + "box-sizing");
if (!boxSizing && prefix) { boxSizing = $elem.css("box-sizing"); } // Not found, try again with standard CSS
if (!boxSizing) { // Still not found - no CSS property available to guide us.
// See what JQuery thinks the global box model is
if ($.boxModel) { boxSizing = "content-box"; }
else { boxSizing = "border-box"; }
}
return boxSizing;
},
//-----------------------------------------------------------------
// Get the height adjustment for setting the height of an element,
// based on the content-box model
//-----------------------------------------------------------------
_getHeightAdjustForBoxModel: function($elem) {
// Take into account the box model. This defaults to either W3C or traditional
// model for a given browser, but can be overridden with CSS
var adjust;
switch (this._getBoxSizing($elem)) {
case "border-box": // AKA traditional, or IE5 (old browsers and IE quirks mode)
// only subtract margin
adjust = $elem.outerHeight(true) - $elem.outerHeight();
break;
case "padding-box": // Firefox-only
// subtract margin and border
adjust = $elem.outerHeight() - $elem.height();
break;
case "content-box": // AKA W3C Ignore jshint warning
default: // Ignore jslint warning
// We will subtract padding, border, margin
adjust = $elem.outerHeight(true) - $elem.height();
break;
}
return adjust;
},
//--------------------------------------------------------
// If there's a pull-down element, we need to set the
// topOffset to the height of that element. If user
// specified a topOffset option, use that instead, though.
//--------------------------------------------------------
_setTopOffsetForPullDown: function() {
if (this.$pullDown.length && !this.options.topOffset) {
this.options.topOffset = this.$pullDown.outerHeight(true);
}
},
//--------------------------------------------------------
// If there's a pull-up element, we need to set the
// bottomOffset to the height of that element. If user
// specified a bottomOffset option, use that instead, though.
//--------------------------------------------------------
_setBottomOffsetForPullUp: function() {
if (this.$pullUp.length && !this.options.bottomOffset) {
this.options.bottomOffset = this.$pullUp.outerHeight(true);
}
},
_removeWrapperPadding: function() {
var $wrapper = this.$wrapper;
if (this.options.removeWrapperPadding) {
// Save padding so we can re-apply it to the iscroll-content div that we create
this._origWrapperPaddingLeft = $wrapper.css("padding-left");
this._origWrapperPaddingRight = $wrapper.css("padding-right");
this._origWrapperPaddingTop = $wrapper.css("padding-top");
this._origWrapperPaddingBottom = $wrapper.css("padding-bottom");
this.$wrapper.css("padding", 0);
}
},
//---------------------------------------------------------
// Modify some wrapper CSS
//---------------------------------------------------------
_modifyWrapperCSS: function() {
this._origWrapperStyle = this.$wrapper.attr("style") || null;
this._removeWrapperPadding();
},
_undoModifyWrapperCSS: function() {
this._restoreStyle(this.$wrapper, this._origWrapperStyle);
},
//---------------------------------------------------------
// Adds padding around scrolled content (not including
// any pull-down or pull-up) using a div with padding
// removed from wrapper.
//---------------------------------------------------------
_addScrollerPadding: function () {
if (this.options.removeWrapperPadding && this.options.addScrollerPadding) {
// We do not store $scrollerContent in the object, because elements might be added/deleted
// after instantiation. When we undo, we need the CURRENT children in order to unwrap
var $scrollerContentWrapper,
$scrollerChildren = this.$scroller.children(),
$scrollerContent = $scrollerChildren.not(this.$pullDown).not(this.$pullUp).not(this.$pullUpSpacer);
$scrollerContent.wrapAll("<div/>");
$scrollerContentWrapper = $scrollerContent.parent().addClass(this.options.scrollerContentClass);
$scrollerContentWrapper.css({
"padding-left" : this._origWrapperPaddingLeft,
"padding-right" : this._origWrapperPaddingRight,
"padding-top" : this._origWrapperPaddingTop,
"padding-bottom" : this._origWrapperPaddingBottom
});
}
},
_undoAddScrollerPadding: function () {
if (this.options.removeWrapperPadding && this.options.addScrollerPadding) {
$("." + this.options.scrollerContentClass, this.$scroller).children().unwrap();
}
},
//---------------------------------------------------------
// Add some convenient classes in case user wants to style
// wrappers/scrollers that use iscroll.
//---------------------------------------------------------
_addWrapperClasses: function() {
this.$wrapper.addClass(this.options.wrapperClass);
this.$scroller.addClass(this.options.scrollerClass);
},
_undoAddWrapperClasses: function() {
this.$scroller.removeClass(this.options.scrollerClass);
this.$wrapper.removeClass(this.options.wrapperClass);
},
//--------------------------------------------------------
// Expands the scroller to fill the wrapper. This permits
// dragging an empty scroller, or one that is shorter than
// the wrapper. Otherwise, you could never do pull to
// refresh if some content wasn't initially present. As
// well, this pushes any pull-up element down so that it
// will not be visible until the user pulls up.
//--------------------------------------------------------
_expandScrollerToFillWrapper: function() {
if (this.options.scrollShortContent || this.$pullDown.length || this.pullUp.length) {
if (this._firstScrollerExpand) {
this._origScrollerStyle = this.$scroller.attr("style") || null;
this._firstScrollerExpand = false;
}
this.$scroller.css("min-height",
this.$wrapper.height() +
(this.$pullDown.length ? this.$pullDown.outerHeight(true) : 0) +
(this.$pullUp.length ? this.$pullUp.outerHeight(true) : 0)
);
}
},
_undoExpandScrollerToFillWrapper: function() {
this._restoreStyle(this.$scroller, this._origScrollerStyle);
},
//--------------------------------------------------------
//Resize the wrapper for the scrolled region to fill the
// viewport remaining after all fixed-height elements
// force makes it ignore resizeWrapper option
//--------------------------------------------------------
_resizeWrapper: function(force) {
var then = null,
viewportHeight,
barsHeight,
newWrapperHeight;
if ( !force && !this.options.resizeWrapper ) {
return;
}
if (this.options.traceResizeWrapper) {
then = this._log("resizeWrapper() start");
}
this.$wrapper.trigger("updatelayout"); // Let jQuery mobile update fixed header/footer, collapsables, etc.
// Get technically-correct viewport height. jQuery documentation is in error on this.
// The viewport height is NOT in all cases the same as the window height, because the height
// of window might have been manually set. And, guess what? jQuery Mobile sets it to 99.99%.
// The viewport is considered the parent of window, and can be retrieved as shown below.
// At 99.99% and common browser sizes, this is probably not an issue. But let's do it right.
//viewportHeight = this.$window.height(); // Wrong
viewportHeight = document.documentElement.clientHeight;
barsHeight = this._calculateBarsHeight();
newWrapperHeight =
viewportHeight -
barsHeight - // Height of fixed bars or "other stuff" outside of the wrapper
this._wrapperHeightAdjustForBoxModel + // Make adjustment based on content-box model
// Note: the following will fail for Safari desktop with Develop/User Agent/iPhone
// Fake fullscreen or webview by using custom user agent and removing "Safari" from string
(IsMobileSafari && !IsIPad ? 60 : 0) + // Add 60px for space recovered from Mobile Safari address bar
this.options.wrapperAdd; // User-supplied fudge-factor if needed
this.$wrapper.css("height", newWrapperHeight);
this._expandScrollerToFillWrapper();
if (this.options.traceResizeWrapper) {
this._logInterval("resizeWrapper() end" + (this._sizeDirty ? " (dirty)" : ""), then);
}
},
// Internal flag is meant for internal use from jquery.mobile.iscrollview only
// If this flag is omitted, then the force flag is set when calling _resizeWrapper
// This causes it to ignore the resizeWrapper option setting. i.e. if user code
// calls resizeWrapper, always resize the wrapper, regardless of setting.
resizeWrapper: function (internal) {
var hidden = this._setPageVisible();
this._resizeWrapper(internal !== undefined);
this._restorePageVisibility(hidden);
},
_undoResizeWrapper: function() {
},
//---------------------------------------------------------
// Make various modifications to the wrapper
//---------------------------------------------------------
_modifyWrapper: function() {
this._addWrapperClasses();
this._modifyWrapperCSS();
this._wrapperHeightAdjustForBoxModel = this._getHeightAdjustForBoxModel(this.$wrapper);
},
_undoModifyWrapper: function() {
this._undoResizeWrapper();
this._undoModifyWrapperCSS();
this._undoAddWrapperClasses();
},
//--------------------------------------------------------
// Modify the pull-down (if any) with reset text
// Also, read data-iscroll-release and data-iscroll-loading
// values (if present ) into the corresponding options.
//--------------------------------------------------------
_modifyPullDown: function () {
var $pullDownLabel, pulledText, loadingText;
if (this.$pullDown.length === 0) { return; }
$pullDownLabel = $("." + this.options.pullLabelClass, this.$pullDown);
if ($pullDownLabel.length) {
this._origPullDownLabelText = $pullDownLabel.text();
if (this._origPullDownLabelText) { this.options.pullDownResetText = this._origPullDownLabelText; }
else { $pullDownLabel.text(this.options.pullDownResetText); }
pulledText = $pullDownLabel.jqmData("iscroll-pulled-text");
if (pulledText) { this.options.pullDownPulledText = pulledText; }
loadingText = $pullDownLabel.jqmData("iscroll-loading-text");
if (loadingText) { this.options.pullDownLoadingText = loadingText; }
}
},
_undoModifyPullDown: function () {
if (this.$pullDown.length === 0) { return; }
var $pullDownLabel = $("." + this.options.pullLabelClass, this.$pullDown);
if ($pullDownLabel.length === 0) { return; }
$pullDownLabel.text(this._origPullDownLabelText);
},
//--------------------------------------------------------
// Modify the pullup element (if any) to prevent visual
// glitching. Position at the bottom of the scroller.
//
// Modify the pull-up (if any) with reset text
// Also, read data-iscroll-release and data-iscroll-loading
// values (if present ) into the corresponding options.
//--------------------------------------------------------
_modifyPullUp: function () {
var $pullUpLabel, pulledText, loadingText;
if (this.$pullUp.length === 0) { return; }
// Since we are positioning the pullUp element absolutely, it is pulled out of the
// document flow. We need to add a dummy <div> with the same height as the pullUp.
$("<div></div>").insertBefore(this.$pullUp).css(
"height", this.$pullUp.outerHeight(true) );
this.$pullUpSpacer = this.$pullUp.prev();
this.$pullUpSpacer.addClass(this.options.pullUpSpacerClass);
$pullUpLabel = $("." + this.options.pullLabelClass, this.$pullUp);
if ($pullUpLabel.length) {
this._origPullUpLabelText = $pullUpLabel.text();
if (this._origPullUpLabelText) { this.options.pullUpResetText = this._origPullUpLabelText; }
else { $pullUpLabel.text(this.options.pullUpResetText); }
pulledText = $pullUpLabel.jqmData("iscroll-pulled-text");
if (pulledText) { this.options.pullUpPulledText = pulledText; }
loadingText = $pullUpLabel.jqmData("iscroll-loading-text");
if (loadingText) { this.options.pullUpLoadingText = loadingText; }
}
},
_undoModifyPullUp: function () {
if (this.$pullUp.length === 0) { return; }
this.$pullUp.prev().remove(); // Remove the dummy div
if (this._origPullUpLabelText) {
$("." + this.options.pullLabelClass, this.$pullUp).text(this._origPullUpLabelText);
}
},
_correctPushedDownPage: function() {
// Scroll to top in case address bar pushed the page down
if (this.options.resizeWrapper && this.options.scrollTopOnResize) {
$.mobile.silentScroll(0);
}
},
//----------------------------------------------------------------------
// Refresh the iscroll object. Insure that refresh is called with proper
// timing. Call optional before and after refresh callbacks and trigger
// before and after refresh events.
//-----------------------------------------------------------------------
refresh: function(delay, callbackBefore, callbackAfter, noDefer) {
var _this, _delay, _callbackBefore, _callbackAfter, _noDefer, then;
// If non-active-page refresh is deferred, make a note of it.
// Note that each call to refresh() overwrites the callback and context variables.
// Our expectation is that callback and context will be identical for all such refresh
// calls. In any case, only the last callback and context will be used. This allows
// refresh of jQuery Mobile widgets within the scroller to be deferred, as well.
if (!noDefer && this.options.deferNonActiveRefresh && !(this.$page.is($.mobile.activePage))) {
this._dirty = true;
this._dirtyCallbackBefore = callbackBefore;
this._dirtyCallbackAfter = callbackAfter;
if (this.options.traceRefresh) {
this._log("refresh() (deferred)");
}
return;
}
// Let the browser complete rendering, then refresh the scroller
//
// Optional delay parameter for timeout before actually calling iscroll.refresh().
// If missing (undefined) or null, use options.refreshDelay.
//
// Optional callback parameters are called if present before and after iScroll internal
// refresh() is called. While the caller might bind to the before or after refresh events,
// this can be more convenient and avoids any ambiguity over WHICH call to refresh is involved.
_this = this;
_delay = delay;
_callbackBefore = callbackBefore;
_callbackAfter = callbackAfter;
_noDefer = noDefer;
then = this._startTiming();
if ((_delay === undefined) || (_delay === null) ) { _delay = this.options.refreshDelay; }
setTimeout(function() {
var later = null,
hidden;
if (_this.options.traceRefresh) {
later = _this._logInterval("refresh() start", then);
}
hidden = _this._setPageVisible();
if (_callbackBefore) { _callbackBefore(); }
_this._triggerWidget("onbeforerefresh");
_this.iscroll.refresh();
_this._triggerWidget("onafterrefresh");
if (_callbackAfter) { _callbackAfter(); }
_this._restorePageVisibility(hidden);
// Scroll to top in case address bar pushed the page down
if (!hidden) { _this._correctPushedDownPage(); }
if (_this.options.traceRefresh) {
_this._logInterval2("refresh() end" + (_noDefer ? " (dirty)" : ""), then, later);
}
}, _delay);
if (this.options.traceRefresh) {
this._log("refresh() will occur after >= " + _delay + "mS");
}
},
//---------------------------
// Create the iScroll object
//---------------------------
_create_iscroll_object: function() {
/*jslint newcap:true */
this.iscroll = new IScroll(this, this.$wrapper.get(0), this._create_iscroll_options());
/* jslint newcap:false */
},
//-----------------------------------------
// Create scroller
//-----------------------------------------
_createScroller: function() {
if (this.options.createScroller) {
if (this.$wrapper.children().length) {
// Wrap the content with a div
this.$wrapper.children().wrapAll("<div/>");
}
else {
// Create an empty div for content and wrap with a div
this.$wrapper.append("<div><div></div></div>");
}
}
},
_undoCreateScroller: function() {
if (this.options.createScroller) {
this.$scroller.children().unwrap();
}
},
// Temporarily change page CSS to make it "visible" so that dimensions can be read.
// This can be used in any event callback, and so can be used in _create(), since it's called
// from pageinit event. Because event processing is synchronous, the browser won't render the
// change, as long as the page style is set back before the callback returns. So, a hidden
// page will remain hidden as long as _restorePageVisibility() is called before return.
// This way, we can just use normal dimension functions and avoid using jquery.actual, which
// slows things down significantly.
//
// jquery.actual goes up the tree from the element being measured and sets every element to
// visible, which is unnecessary. (We only have to be concerned about the page element, which
// jQuery Mobile sets to display:none for all but the currently-visible page.) It also does this
// for every dimension read. This essentially does the same thing, but then allows us to "batch"
// reading dimensions
_setPageVisible: function() {
var hidden = this.$page.is(":hidden");
if (hidden) { this.$page.css("display", "block"); }
return hidden;
},
_restorePageVisibility: function(hidden) {
if (hidden) { this.$page.css("display", ""); }
},
//-----------------------------------------
// Automatically called on page creation
//-----------------------------------------
_create: function() {
var then = new Date(),
hidden;
this.$wrapper = this.element; // JQuery object containing the element we are creating this widget for
this.$page = this.$wrapper.parents(":jqmData(role='page')"); // The page containing the wrapper
if (this.options.debug && this.options.traceCreateDestroy) {
this._log("_create() start", then);
}
this.createdAt = then;
this._instanceCount(this._instanceCount() + 1); // The count of extant instances of this widget on the page
this.instanceID = this._nextInstanceID(); // The serial ID of this instance of this widget on the page
this._nextInstanceID(this._instanceID + 1);
if (this.instanceID === 1) {
this._pageID(nextPageID);
nextPageID += 1;
}
this.pageID = this._pageID();
hidden = this._setPageVisible(); // Fake page visibility, so dimension functions work
this._adaptPage();
this._createScroller();
this.$scroller = this.$wrapper.children(":first"); // Get the first child of the wrapper, which is the
// element that we will scroll
if (this.$scroller.length === 0) { return; }
// Find pull elements, if present
this.$pullDown = $("." + this.options.pullDownClass, this.$scroller);
this._modifyPullDown();
this.$pullUp = $("." + this.options.pullUpClass, this.$scroller);
this._modifyPullUp();
// Merge options from data-iscroll, if present
$.extend(true, this.options, this.$wrapper.jqmData("iscroll"));
this._modifyWrapper(); // Various changes to the wrapper
// Need this for deferred refresh processing
this._bindPage("pagebeforeshow", this._pageBeforeShowFunc);
this._setTopOffsetForPullDown(); // If there's a pull-down, set the top offset
this._setBottomOffsetForPullUp(); // If there's a pull-up, set the bottom offset
this._resizeWrapper(); // Resize the wrapper to fill available space
this._addScrollerPadding(); // Put back padding removed from wrapper
this._create_iscroll_object();
this._merge_from_iscroll_options(); // Merge iscroll options into widget options
this._restorePageVisibility(hidden);
// Setup bindings for window resize and orientationchange
if (this.options.resizeWrapper) {
this._isvBind(this.$window, this.options.resizeEvents, this._windowResizeFunc, "$window");
if (this.options.scrollTopOnOrientationChange) {
this._isvBind(this.$window, "orientationchange", this._orientationChangeFunc, "$window");
}
}
// Refresh on trigger of updatelayout of content
this.$scrollerContent = this.$scroller.find("." + this.options.scrollerContentClass);
this._isvBind(this.$scrollerContent, "updatelayout", this._updateLayoutFunc, "$scrollerContent");
if (this.options.debug && this.options.traceCreateDestroy) {
this._logInterval("_create() end", then);
}
},
//----------------------------------------------------------
// Destroy an instantiated plugin and clean up modifications
// the widget has made to the DOM
//----------------------------------------------------------
destroy: function () {
var then = null;
if (this.options.debug && this.options.traceCreateDestroy) {
then = this._log("destroy() start");
}
// Unbind events
this._isvUnbind(this.$scrollerContent, "updatelayout", "$scrollerContent");
this._isvUnbind(this.$window, this.options.resizeEvents, "$window");
this._isvUnbind(this.$window, "orientationchange", "$window");
if (this._instanceCount() === 1) {
this._unbindPage("pagebeforeshow");
if (HasTouch) {
this._unbindPage("touchmove");
}
}
// fastDestroy option skips tearing down the modifications to the page, because we assume
// that the page itself is being removed, and nobody is going to be silly enough to
// un-ehance a scroller and keep the page.
if (!this.options.fastDestroy) {
this.iscroll.destroy();
this.iscroll = null;
this._undoExpandScrollerToFillWrapper();
this._undoModifyPullDown();
this._undoModifyPullUp();
this._undoAddScrollerPadding();
this._undoModifyWrapper();
this.$wrapper.removeClass(this.options.wrapperClass);
this.$scroller.removeClass(this.options.scrollerClass);
this._undoCreateScroller();
}
this._instanceCount(this._instanceCount() - 1); // The count of extant instances of this widget on the page
if (this._instanceCount() === 0) {
this._undoAdaptPage();
}
// For UI 1.8, destroy must be invoked from the
// base widget
$.Widget.prototype.destroy.call(this);
if (this.options.debug && this.options.traceCreateDestroy) {
this._logInterval("destroy() end", then);
}
// For UI 1.9, define _destroy instead and don't
// worry about calling the base widget
},
// Enable the widget
enable: function() {
this.iscroll.enable();
$.Widget.prototype.enable.call(this);
},
// Disable the widget
disable: function() {
this.iscroll.disable();
$.Widget.prototype.disable.call(this);
},
//----------------------------------------------------------
//Respond to any changes the user makes to the option method
//----------------------------------------------------------
_setOption: function( key, value ) {
var hidden;
// iScroll4 doesn't officially support changing options after an iscroll object has been
// instantiated. However, some changes will work if you do a refresh() after changing the
// option. This is undocumented other than from user comments on the iscroll4 Google
// Groups support group. If an option change doesn't work with refresh(), then it
// is necessary to destroy and re-create the iscroll object. This is a functionality
// that the author of iscroll4 intends to support in the future.
//
// TODO: Research which options can be successfully changed without destroying and
// re-creating the iscroll object. For now, I'm taking a safe approach and
// always destroying and re-creating the iscroll object.
//switch (key) {
//case "hScroll":
//case "vScroll":
//case "hScrollbar":
//case "vScrollbar":
//this.options[ key ] = value; // Change our options object
//this.iscroll.options[ key ] = value; // ... and iscroll's options object
//this.iscroll.refresh(); // Don't think we need the timing hack here
//break;
//default:
this.options[ key ] = value;
this.iscroll.destroy();
hidden = this._setPageVisible();
this._create_iscroll_object();
this._restorePageVisibility(hidden);
//break;
//}
// For UI 1.8, _setOption must be manually invoked from
// the base widget
$.Widget.prototype._setOption.apply(this, arguments);
// For UI 1.9 the _super method can be used instead
// this._super( "_setOption", key, value );
},
//----------------------------------------------------
// Convenience wrappers around iscroll4 public methods
// So, you can use:
//
// $(".some-class").iscrollview("scrollTo", x, y, time, relative);
//
// instead of:
//
// $(".some-class").jqmData("iscrollview").iscroll.scrollTo(x, y, time, relative);
//
//----------------------------------------------------
scrollTo: function(x,y,time,relative) { this.iscroll.scrollTo(x,y,time,relative); },
scrollToElement: function(el,time) { this.iscroll.scrollToElement(el,time); },
scrollToPage: function(pageX,pageY,time) { this.iscroll.scrollToPage(pageX,pageY,time); },
stop: function() { this.iscroll.stop(); },
zoom: function(x,y,scale,time) { this.iscroll.zoom(x,y,scale,time); },
isReady: function() { return this.iscroll.isReady(); },
// See disable() enable() elsewhere above - they are standard widget methods
//----------------------------------------------------------------------------------
// Accessors for iscroll4 internal variables. These are sometimes useful externally.
// For example, let's say you are adding elements to the end of a scrolled list.
// You'd like to scroll up (using scrollToElement) if the new element would be
// below the visible area. But if the list is intially empty, you'd want to avoid
// this until the scrolling area is initially full. So you need to compare the
// scroller height (scrollerH) to the wrapper height (wrapperH).
//
// These are also useful for creating "pull to refresh" functionality.
//
//-----------------------------------------------------------------------------------
x: function() { return this.iscroll.x; },
y: function() { return this.iscroll.y; },
wrapperW: function() { return this.iscroll.wrapperW; },
wrapperH: function() { return this.iscroll.wrapperH; },
scrollerW: function() { return this.iscroll.scrollerW; },
scrollerH: function() { return this.iscroll.scrollerH; },
// These have setters. Useful for "pull to refresh".
minScrollX: function(val) { if (val !== undefined) { this.iscroll.minScrollX = val; } return this.iscroll.minScrollX; },
minScrollY: function(val) { if (val !== undefined) { this.iscroll.minScrollY = val; } return this.iscroll.minScrollY; },
maxScrollX: function(val) { if (val !== undefined) { this.iscroll.maxScrollX = val; } return this.iscroll.maxScrollX; },
maxScrollY: function(val) { if (val !== undefined) { this.iscroll.maxScrollY = val; } return this.iscroll.maxScrollY; },
//-----------------------------------------------------------------------------------
// Pull-down/Pull-up support
//-----------------------------------------------------------------------------------
// Is pull-down in "pulled" state?
_pullDownIsPulled: function () {
return this.$pullDown.length && this.$pullDown.hasClass(this.options.pullPulledClass);
},
// Is pull-up in "pulled" state?
_pullUpIsPulled: function () {
return this.$pullUp.length && this.$pullUp.hasClass(this.options.pullPulledClass);
},
// Replace the text in a pull block
_replacePullText: function ($pull, text) {
var $label;
if (text) {
$label = $("." + this.options.pullLabelClass, $pull);
if ($label) { $label.text(text); }
}
},
// Reset a pull block to the initial state
_pullSetStateReset: function ($pull, text) {
if ($pull.is("." + this.options.pullLoadingClass + ", ." + this.options.pullPulledClass)) {
$pull.removeClass(this.options.pullPulledClass + " " + this.options.pullLoadingClass);
this._replacePullText($pull, text);
}
},
_pullDownSetStateReset: function(e) {
this._pullSetStateReset(this.$pullDown, this.options.pullDownResetText);
this._triggerWidget("onpulldownreset", e);
},
_pullUpSetStateReset: function(e) {
this._pullSetStateReset(this.$pullUp, this.options.pullUpResetText);
this._triggerWidget("onpullupreset", e);
},
// Set a pull block to pulled state
_pullSetStatePulled: function($pull, text) {
$pull.removeClass(this.options.pullLoadingClass).addClass(this.options.pullPulledClass);
this._replacePullText($pull, text);
},
_pullDownSetStatePulled: function(e) {
this._pullSetStatePulled(this.$pullDown, this.options.pullDownPulledText);
this._triggerWidget("onpulldownpulled", e);
},
_pullUpSetStatePulled: function (e) {
this._pullSetStatePulled(this.$pullUp, this.options.pullUpPulledText);
this._triggerWidget("onpulluppulled", e);
},
// Set a pull block to the loading state
_pullSetStateLoading: function($pull, text) {
$pull.removeClass(this.options.pullPulledClass).addClass(this.options.pullLoadingClass);
this._replacePullText($pull, text);
},
_pullDownSetStateLoading: function (e) {
this._pullSetStateLoading(this.$pullDown, this.options.pullDownLoadingText);
this._triggerWidget("onpulldownloading", e);
},
_pullUpSetStateLoading: function(e) {
this._pullSetStateLoading(this.$pullUp, this.options.pullUpLoadingText);
this._triggerWidget("onpulluploading", e);
},
_pullOnRefresh: function (e) {
// It's debatable if this is the right place to do this. On one hand, it might be best
// to do this in the pullup/down action function. We expect that we will always do a refresh
// after the action, though (unless the action doesn't actually update anything, in which
// case it can still call refresh().) On the other hand, it might be desirable to
// "reset" the pull if a refresh comes along for some other reason. If the content were
// updated because of something other than the user's pull action, then we consider the
// pull moot.
// Reset pull blocks to their initial state
if (this.$pullDown.length) { this._pullDownSetStateReset(e); }
if (this.$pullUp.length) { this._pullUpSetStateReset(e); }
},
_pullOnScrollMove: function (e) {
var pullDownIsPulled, pullUpIsPulled, pullDownHeight, pullDownPast, pullUpHeight, pullUpPast,
y = this.y();
if (this.$pullDown.length) {
pullDownIsPulled = this._pullDownIsPulled();
pullDownHeight = this.options.topOffset;
// User needs to pull down past the top edge of the pulldown element. To prevent false
// triggers from aggressive scrolling, they should have to pull down some additional
// amount. Half the height of the pulldown seems reasonable, but adjust per preference.
pullDownPast = pullDownHeight / 2;
// Set "pulled" state if not pulled and user has pulled past the pulldown element
// by pullDownPast pixels
if (!pullDownIsPulled && y > pullDownPast ) {
this._pullDownSetStatePulled(e);
this.minScrollY(0); // Circumvent top offset so pull-down element doesn't rubber-band
}
// Allow user to "oopsie", and scroll back to cancel and avoid pull-down action
// Cancel if pulled and user has scrolled back to top of pulldown element
else if (pullDownIsPulled && y <= 0) {
this._pullDownSetStateReset(e);
this.minScrollY(-pullDownHeight); // Re-instate top offset
}
}
if (this.$pullUp.length) {
pullUpIsPulled = this._pullUpIsPulled();
pullUpHeight = this.options.bottomOffset;
pullUpPast = pullUpHeight / 2;
if (!pullUpIsPulled && y < this.maxScrollY() - pullUpHeight - pullUpPast ) {
this._pullUpSetStatePulled(e);
this.maxScrollY(this.wrapperH() - this.scrollerH() + this.minScrollY());
}
else if (pullUpIsPulled && y >= this.maxScrollY() ) {
this._pullUpSetStateReset(e);
this.maxScrollY(this.wrapperH() - this.scrollerH() + this.minScrollY() + pullUpHeight);
}
}
},
_pullOnScrollEnd: function (e) {
if (this._pullDownIsPulled(e)) {
this._pullDownSetStateLoading(e);
this._triggerWidget("onpulldown", e);
}
else if (this._pullUpIsPulled(e)) {
this._pullUpSetStateLoading(e);
this._triggerWidget("onpullup", e);
}
}
});
}( jQuery, window, document ));
// Self-init
jQuery(document).bind("pagecreate", function (e) {
"use strict";
// In here, e.target refers to the page that was created (it's the target of the pagecreate event)
// So, we can simply find elements on this page that match a selector of our choosing, and call
// our plugin on them.
// The find() below returns an array of elements within a newly-created page that have
// the data-iscroll attribute. The Widget Factory will enumerate these and call the widget
// _create() function for each member of the array.
// If the array is of zero length, then no _create() fucntion is called.
var elements = jQuery(e.target).find(":jqmData(iscroll)");
elements.iscrollview();
});