| 15747 |
anikendra |
1 |
/*!
|
|
|
2 |
* Copyright 2014 Drifty Co.
|
|
|
3 |
* http://drifty.com/
|
|
|
4 |
*
|
|
|
5 |
* Ionic, v1.0.0
|
|
|
6 |
* A powerful HTML5 mobile app framework.
|
|
|
7 |
* http://ionicframework.com/
|
|
|
8 |
*
|
|
|
9 |
* By @maxlynch, @benjsperry, @adamdbradley <3
|
|
|
10 |
*
|
|
|
11 |
* Licensed under the MIT license. Please see LICENSE for more information.
|
|
|
12 |
*
|
|
|
13 |
*/
|
|
|
14 |
|
|
|
15 |
(function() {
|
|
|
16 |
|
|
|
17 |
// Create global ionic obj and its namespaces
|
|
|
18 |
// build processes may have already created an ionic obj
|
|
|
19 |
window.ionic = window.ionic || {};
|
|
|
20 |
window.ionic.views = {};
|
|
|
21 |
window.ionic.version = '1.0.0';
|
|
|
22 |
|
|
|
23 |
(function (ionic) {
|
|
|
24 |
|
|
|
25 |
ionic.DelegateService = function(methodNames) {
|
|
|
26 |
|
|
|
27 |
if (methodNames.indexOf('$getByHandle') > -1) {
|
|
|
28 |
throw new Error("Method '$getByHandle' is implicitly added to each delegate service. Do not list it as a method.");
|
|
|
29 |
}
|
|
|
30 |
|
|
|
31 |
function trueFn() { return true; }
|
|
|
32 |
|
|
|
33 |
return ['$log', function($log) {
|
|
|
34 |
|
|
|
35 |
/*
|
|
|
36 |
* Creates a new object that will have all the methodNames given,
|
|
|
37 |
* and call them on the given the controller instance matching given
|
|
|
38 |
* handle.
|
|
|
39 |
* The reason we don't just let $getByHandle return the controller instance
|
|
|
40 |
* itself is that the controller instance might not exist yet.
|
|
|
41 |
*
|
|
|
42 |
* We want people to be able to do
|
|
|
43 |
* `var instance = $ionicScrollDelegate.$getByHandle('foo')` on controller
|
|
|
44 |
* instantiation, but on controller instantiation a child directive
|
|
|
45 |
* may not have been compiled yet!
|
|
|
46 |
*
|
|
|
47 |
* So this is our way of solving this problem: we create an object
|
|
|
48 |
* that will only try to fetch the controller with given handle
|
|
|
49 |
* once the methods are actually called.
|
|
|
50 |
*/
|
|
|
51 |
function DelegateInstance(instances, handle) {
|
|
|
52 |
this._instances = instances;
|
|
|
53 |
this.handle = handle;
|
|
|
54 |
}
|
|
|
55 |
methodNames.forEach(function(methodName) {
|
|
|
56 |
DelegateInstance.prototype[methodName] = instanceMethodCaller(methodName);
|
|
|
57 |
});
|
|
|
58 |
|
|
|
59 |
|
|
|
60 |
/**
|
|
|
61 |
* The delegate service (eg $ionicNavBarDelegate) is just an instance
|
|
|
62 |
* with a non-defined handle, a couple extra methods for registering
|
|
|
63 |
* and narrowing down to a specific handle.
|
|
|
64 |
*/
|
|
|
65 |
function DelegateService() {
|
|
|
66 |
this._instances = [];
|
|
|
67 |
}
|
|
|
68 |
DelegateService.prototype = DelegateInstance.prototype;
|
|
|
69 |
DelegateService.prototype._registerInstance = function(instance, handle, filterFn) {
|
|
|
70 |
var instances = this._instances;
|
|
|
71 |
instance.$$delegateHandle = handle;
|
|
|
72 |
instance.$$filterFn = filterFn || trueFn;
|
|
|
73 |
instances.push(instance);
|
|
|
74 |
|
|
|
75 |
return function deregister() {
|
|
|
76 |
var index = instances.indexOf(instance);
|
|
|
77 |
if (index !== -1) {
|
|
|
78 |
instances.splice(index, 1);
|
|
|
79 |
}
|
|
|
80 |
};
|
|
|
81 |
};
|
|
|
82 |
DelegateService.prototype.$getByHandle = function(handle) {
|
|
|
83 |
return new DelegateInstance(this._instances, handle);
|
|
|
84 |
};
|
|
|
85 |
|
|
|
86 |
return new DelegateService();
|
|
|
87 |
|
|
|
88 |
function instanceMethodCaller(methodName) {
|
|
|
89 |
return function caller() {
|
|
|
90 |
var handle = this.handle;
|
|
|
91 |
var args = arguments;
|
|
|
92 |
var foundInstancesCount = 0;
|
|
|
93 |
var returnValue;
|
|
|
94 |
|
|
|
95 |
this._instances.forEach(function(instance) {
|
|
|
96 |
if ((!handle || handle == instance.$$delegateHandle) && instance.$$filterFn(instance)) {
|
|
|
97 |
foundInstancesCount++;
|
|
|
98 |
var ret = instance[methodName].apply(instance, args);
|
|
|
99 |
//Only return the value from the first call
|
|
|
100 |
if (foundInstancesCount === 1) {
|
|
|
101 |
returnValue = ret;
|
|
|
102 |
}
|
|
|
103 |
}
|
|
|
104 |
});
|
|
|
105 |
|
|
|
106 |
if (!foundInstancesCount && handle) {
|
|
|
107 |
return $log.warn(
|
|
|
108 |
'Delegate for handle "' + handle + '" could not find a ' +
|
|
|
109 |
'corresponding element with delegate-handle="' + handle + '"! ' +
|
|
|
110 |
methodName + '() was not called!\n' +
|
|
|
111 |
'Possible cause: If you are calling ' + methodName + '() immediately, and ' +
|
|
|
112 |
'your element with delegate-handle="' + handle + '" is a child of your ' +
|
|
|
113 |
'controller, then your element may not be compiled yet. Put a $timeout ' +
|
|
|
114 |
'around your call to ' + methodName + '() and try again.'
|
|
|
115 |
);
|
|
|
116 |
}
|
|
|
117 |
return returnValue;
|
|
|
118 |
};
|
|
|
119 |
}
|
|
|
120 |
|
|
|
121 |
}];
|
|
|
122 |
};
|
|
|
123 |
|
|
|
124 |
})(window.ionic);
|
|
|
125 |
|
|
|
126 |
(function(window, document, ionic) {
|
|
|
127 |
|
|
|
128 |
var readyCallbacks = [];
|
|
|
129 |
var isDomReady = document.readyState === 'complete' || document.readyState === 'interactive';
|
|
|
130 |
|
|
|
131 |
function domReady() {
|
|
|
132 |
isDomReady = true;
|
|
|
133 |
for (var x = 0; x < readyCallbacks.length; x++) {
|
|
|
134 |
ionic.requestAnimationFrame(readyCallbacks[x]);
|
|
|
135 |
}
|
|
|
136 |
readyCallbacks = [];
|
|
|
137 |
document.removeEventListener('DOMContentLoaded', domReady);
|
|
|
138 |
}
|
|
|
139 |
if (!isDomReady) {
|
|
|
140 |
document.addEventListener('DOMContentLoaded', domReady);
|
|
|
141 |
}
|
|
|
142 |
|
|
|
143 |
|
|
|
144 |
// From the man himself, Mr. Paul Irish.
|
|
|
145 |
// The requestAnimationFrame polyfill
|
|
|
146 |
// Put it on window just to preserve its context
|
|
|
147 |
// without having to use .call
|
|
|
148 |
window._rAF = (function() {
|
|
|
149 |
return window.requestAnimationFrame ||
|
|
|
150 |
window.webkitRequestAnimationFrame ||
|
|
|
151 |
window.mozRequestAnimationFrame ||
|
|
|
152 |
function(callback) {
|
|
|
153 |
window.setTimeout(callback, 16);
|
|
|
154 |
};
|
|
|
155 |
})();
|
|
|
156 |
|
|
|
157 |
var cancelAnimationFrame = window.cancelAnimationFrame ||
|
|
|
158 |
window.webkitCancelAnimationFrame ||
|
|
|
159 |
window.mozCancelAnimationFrame ||
|
|
|
160 |
window.webkitCancelRequestAnimationFrame;
|
|
|
161 |
|
|
|
162 |
/**
|
|
|
163 |
* @ngdoc utility
|
|
|
164 |
* @name ionic.DomUtil
|
|
|
165 |
* @module ionic
|
|
|
166 |
*/
|
|
|
167 |
ionic.DomUtil = {
|
|
|
168 |
//Call with proper context
|
|
|
169 |
/**
|
|
|
170 |
* @ngdoc method
|
|
|
171 |
* @name ionic.DomUtil#requestAnimationFrame
|
|
|
172 |
* @alias ionic.requestAnimationFrame
|
|
|
173 |
* @description Calls [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame), or a polyfill if not available.
|
|
|
174 |
* @param {function} callback The function to call when the next frame
|
|
|
175 |
* happens.
|
|
|
176 |
*/
|
|
|
177 |
requestAnimationFrame: function(cb) {
|
|
|
178 |
return window._rAF(cb);
|
|
|
179 |
},
|
|
|
180 |
|
|
|
181 |
cancelAnimationFrame: function(requestId) {
|
|
|
182 |
cancelAnimationFrame(requestId);
|
|
|
183 |
},
|
|
|
184 |
|
|
|
185 |
/**
|
|
|
186 |
* @ngdoc method
|
|
|
187 |
* @name ionic.DomUtil#animationFrameThrottle
|
|
|
188 |
* @alias ionic.animationFrameThrottle
|
|
|
189 |
* @description
|
|
|
190 |
* When given a callback, if that callback is called 100 times between
|
|
|
191 |
* animation frames, adding Throttle will make it only run the last of
|
|
|
192 |
* the 100 calls.
|
|
|
193 |
*
|
|
|
194 |
* @param {function} callback a function which will be throttled to
|
|
|
195 |
* requestAnimationFrame
|
|
|
196 |
* @returns {function} A function which will then call the passed in callback.
|
|
|
197 |
* The passed in callback will receive the context the returned function is
|
|
|
198 |
* called with.
|
|
|
199 |
*/
|
|
|
200 |
animationFrameThrottle: function(cb) {
|
|
|
201 |
var args, isQueued, context;
|
|
|
202 |
return function() {
|
|
|
203 |
args = arguments;
|
|
|
204 |
context = this;
|
|
|
205 |
if (!isQueued) {
|
|
|
206 |
isQueued = true;
|
|
|
207 |
ionic.requestAnimationFrame(function() {
|
|
|
208 |
cb.apply(context, args);
|
|
|
209 |
isQueued = false;
|
|
|
210 |
});
|
|
|
211 |
}
|
|
|
212 |
};
|
|
|
213 |
},
|
|
|
214 |
|
|
|
215 |
contains: function(parentNode, otherNode) {
|
|
|
216 |
var current = otherNode;
|
|
|
217 |
while (current) {
|
|
|
218 |
if (current === parentNode) return true;
|
|
|
219 |
current = current.parentNode;
|
|
|
220 |
}
|
|
|
221 |
},
|
|
|
222 |
|
|
|
223 |
/**
|
|
|
224 |
* @ngdoc method
|
|
|
225 |
* @name ionic.DomUtil#getPositionInParent
|
|
|
226 |
* @description
|
|
|
227 |
* Find an element's scroll offset within its container.
|
|
|
228 |
* @param {DOMElement} element The element to find the offset of.
|
|
|
229 |
* @returns {object} A position object with the following properties:
|
|
|
230 |
* - `{number}` `left` The left offset of the element.
|
|
|
231 |
* - `{number}` `top` The top offset of the element.
|
|
|
232 |
*/
|
|
|
233 |
getPositionInParent: function(el) {
|
|
|
234 |
return {
|
|
|
235 |
left: el.offsetLeft,
|
|
|
236 |
top: el.offsetTop
|
|
|
237 |
};
|
|
|
238 |
},
|
|
|
239 |
|
|
|
240 |
/**
|
|
|
241 |
* @ngdoc method
|
|
|
242 |
* @name ionic.DomUtil#ready
|
|
|
243 |
* @description
|
|
|
244 |
* Call a function when the DOM is ready, or if it is already ready
|
|
|
245 |
* call the function immediately.
|
|
|
246 |
* @param {function} callback The function to be called.
|
|
|
247 |
*/
|
|
|
248 |
ready: function(cb) {
|
|
|
249 |
if (isDomReady) {
|
|
|
250 |
ionic.requestAnimationFrame(cb);
|
|
|
251 |
} else {
|
|
|
252 |
readyCallbacks.push(cb);
|
|
|
253 |
}
|
|
|
254 |
},
|
|
|
255 |
|
|
|
256 |
/**
|
|
|
257 |
* @ngdoc method
|
|
|
258 |
* @name ionic.DomUtil#getTextBounds
|
|
|
259 |
* @description
|
|
|
260 |
* Get a rect representing the bounds of the given textNode.
|
|
|
261 |
* @param {DOMElement} textNode The textNode to find the bounds of.
|
|
|
262 |
* @returns {object} An object representing the bounds of the node. Properties:
|
|
|
263 |
* - `{number}` `left` The left position of the textNode.
|
|
|
264 |
* - `{number}` `right` The right position of the textNode.
|
|
|
265 |
* - `{number}` `top` The top position of the textNode.
|
|
|
266 |
* - `{number}` `bottom` The bottom position of the textNode.
|
|
|
267 |
* - `{number}` `width` The width of the textNode.
|
|
|
268 |
* - `{number}` `height` The height of the textNode.
|
|
|
269 |
*/
|
|
|
270 |
getTextBounds: function(textNode) {
|
|
|
271 |
if (document.createRange) {
|
|
|
272 |
var range = document.createRange();
|
|
|
273 |
range.selectNodeContents(textNode);
|
|
|
274 |
if (range.getBoundingClientRect) {
|
|
|
275 |
var rect = range.getBoundingClientRect();
|
|
|
276 |
if (rect) {
|
|
|
277 |
var sx = window.scrollX;
|
|
|
278 |
var sy = window.scrollY;
|
|
|
279 |
|
|
|
280 |
return {
|
|
|
281 |
top: rect.top + sy,
|
|
|
282 |
left: rect.left + sx,
|
|
|
283 |
right: rect.left + sx + rect.width,
|
|
|
284 |
bottom: rect.top + sy + rect.height,
|
|
|
285 |
width: rect.width,
|
|
|
286 |
height: rect.height
|
|
|
287 |
};
|
|
|
288 |
}
|
|
|
289 |
}
|
|
|
290 |
}
|
|
|
291 |
return null;
|
|
|
292 |
},
|
|
|
293 |
|
|
|
294 |
/**
|
|
|
295 |
* @ngdoc method
|
|
|
296 |
* @name ionic.DomUtil#getChildIndex
|
|
|
297 |
* @description
|
|
|
298 |
* Get the first index of a child node within the given element of the
|
|
|
299 |
* specified type.
|
|
|
300 |
* @param {DOMElement} element The element to find the index of.
|
|
|
301 |
* @param {string} type The nodeName to match children of element against.
|
|
|
302 |
* @returns {number} The index, or -1, of a child with nodeName matching type.
|
|
|
303 |
*/
|
|
|
304 |
getChildIndex: function(element, type) {
|
|
|
305 |
if (type) {
|
|
|
306 |
var ch = element.parentNode.children;
|
|
|
307 |
var c;
|
|
|
308 |
for (var i = 0, k = 0, j = ch.length; i < j; i++) {
|
|
|
309 |
c = ch[i];
|
|
|
310 |
if (c.nodeName && c.nodeName.toLowerCase() == type) {
|
|
|
311 |
if (c == element) {
|
|
|
312 |
return k;
|
|
|
313 |
}
|
|
|
314 |
k++;
|
|
|
315 |
}
|
|
|
316 |
}
|
|
|
317 |
}
|
|
|
318 |
return Array.prototype.slice.call(element.parentNode.children).indexOf(element);
|
|
|
319 |
},
|
|
|
320 |
|
|
|
321 |
/**
|
|
|
322 |
* @private
|
|
|
323 |
*/
|
|
|
324 |
swapNodes: function(src, dest) {
|
|
|
325 |
dest.parentNode.insertBefore(src, dest);
|
|
|
326 |
},
|
|
|
327 |
|
|
|
328 |
elementIsDescendant: function(el, parent, stopAt) {
|
|
|
329 |
var current = el;
|
|
|
330 |
do {
|
|
|
331 |
if (current === parent) return true;
|
|
|
332 |
current = current.parentNode;
|
|
|
333 |
} while (current && current !== stopAt);
|
|
|
334 |
return false;
|
|
|
335 |
},
|
|
|
336 |
|
|
|
337 |
/**
|
|
|
338 |
* @ngdoc method
|
|
|
339 |
* @name ionic.DomUtil#getParentWithClass
|
|
|
340 |
* @param {DOMElement} element
|
|
|
341 |
* @param {string} className
|
|
|
342 |
* @returns {DOMElement} The closest parent of element matching the
|
|
|
343 |
* className, or null.
|
|
|
344 |
*/
|
|
|
345 |
getParentWithClass: function(e, className, depth) {
|
|
|
346 |
depth = depth || 10;
|
|
|
347 |
while (e.parentNode && depth--) {
|
|
|
348 |
if (e.parentNode.classList && e.parentNode.classList.contains(className)) {
|
|
|
349 |
return e.parentNode;
|
|
|
350 |
}
|
|
|
351 |
e = e.parentNode;
|
|
|
352 |
}
|
|
|
353 |
return null;
|
|
|
354 |
},
|
|
|
355 |
/**
|
|
|
356 |
* @ngdoc method
|
|
|
357 |
* @name ionic.DomUtil#getParentOrSelfWithClass
|
|
|
358 |
* @param {DOMElement} element
|
|
|
359 |
* @param {string} className
|
|
|
360 |
* @returns {DOMElement} The closest parent or self matching the
|
|
|
361 |
* className, or null.
|
|
|
362 |
*/
|
|
|
363 |
getParentOrSelfWithClass: function(e, className, depth) {
|
|
|
364 |
depth = depth || 10;
|
|
|
365 |
while (e && depth--) {
|
|
|
366 |
if (e.classList && e.classList.contains(className)) {
|
|
|
367 |
return e;
|
|
|
368 |
}
|
|
|
369 |
e = e.parentNode;
|
|
|
370 |
}
|
|
|
371 |
return null;
|
|
|
372 |
},
|
|
|
373 |
|
|
|
374 |
/**
|
|
|
375 |
* @ngdoc method
|
|
|
376 |
* @name ionic.DomUtil#rectContains
|
|
|
377 |
* @param {number} x
|
|
|
378 |
* @param {number} y
|
|
|
379 |
* @param {number} x1
|
|
|
380 |
* @param {number} y1
|
|
|
381 |
* @param {number} x2
|
|
|
382 |
* @param {number} y2
|
|
|
383 |
* @returns {boolean} Whether {x,y} fits within the rectangle defined by
|
|
|
384 |
* {x1,y1,x2,y2}.
|
|
|
385 |
*/
|
|
|
386 |
rectContains: function(x, y, x1, y1, x2, y2) {
|
|
|
387 |
if (x < x1 || x > x2) return false;
|
|
|
388 |
if (y < y1 || y > y2) return false;
|
|
|
389 |
return true;
|
|
|
390 |
},
|
|
|
391 |
|
|
|
392 |
/**
|
|
|
393 |
* @ngdoc method
|
|
|
394 |
* @name ionic.DomUtil#blurAll
|
|
|
395 |
* @description
|
|
|
396 |
* Blurs any currently focused input element
|
|
|
397 |
* @returns {DOMElement} The element blurred or null
|
|
|
398 |
*/
|
|
|
399 |
blurAll: function() {
|
|
|
400 |
if (document.activeElement && document.activeElement != document.body) {
|
|
|
401 |
document.activeElement.blur();
|
|
|
402 |
return document.activeElement;
|
|
|
403 |
}
|
|
|
404 |
return null;
|
|
|
405 |
},
|
|
|
406 |
|
|
|
407 |
cachedAttr: function(ele, key, value) {
|
|
|
408 |
ele = ele && ele.length && ele[0] || ele;
|
|
|
409 |
if (ele && ele.setAttribute) {
|
|
|
410 |
var dataKey = '$attr-' + key;
|
|
|
411 |
if (arguments.length > 2) {
|
|
|
412 |
if (ele[dataKey] !== value) {
|
|
|
413 |
ele.setAttribute(key, value);
|
|
|
414 |
ele[dataKey] = value;
|
|
|
415 |
}
|
|
|
416 |
} else if (typeof ele[dataKey] == 'undefined') {
|
|
|
417 |
ele[dataKey] = ele.getAttribute(key);
|
|
|
418 |
}
|
|
|
419 |
return ele[dataKey];
|
|
|
420 |
}
|
|
|
421 |
},
|
|
|
422 |
|
|
|
423 |
cachedStyles: function(ele, styles) {
|
|
|
424 |
ele = ele && ele.length && ele[0] || ele;
|
|
|
425 |
if (ele && ele.style) {
|
|
|
426 |
for (var prop in styles) {
|
|
|
427 |
if (ele['$style-' + prop] !== styles[prop]) {
|
|
|
428 |
ele.style[prop] = ele['$style-' + prop] = styles[prop];
|
|
|
429 |
}
|
|
|
430 |
}
|
|
|
431 |
}
|
|
|
432 |
}
|
|
|
433 |
|
|
|
434 |
};
|
|
|
435 |
|
|
|
436 |
//Shortcuts
|
|
|
437 |
ionic.requestAnimationFrame = ionic.DomUtil.requestAnimationFrame;
|
|
|
438 |
ionic.cancelAnimationFrame = ionic.DomUtil.cancelAnimationFrame;
|
|
|
439 |
ionic.animationFrameThrottle = ionic.DomUtil.animationFrameThrottle;
|
|
|
440 |
|
|
|
441 |
})(window, document, ionic);
|
|
|
442 |
|
|
|
443 |
/**
|
|
|
444 |
* ion-events.js
|
|
|
445 |
*
|
|
|
446 |
* Author: Max Lynch <max@drifty.com>
|
|
|
447 |
*
|
|
|
448 |
* Framework events handles various mobile browser events, and
|
|
|
449 |
* detects special events like tap/swipe/etc. and emits them
|
|
|
450 |
* as custom events that can be used in an app.
|
|
|
451 |
*
|
|
|
452 |
* Portions lovingly adapted from github.com/maker/ratchet and github.com/alexgibson/tap.js - thanks guys!
|
|
|
453 |
*/
|
|
|
454 |
|
|
|
455 |
(function(ionic) {
|
|
|
456 |
|
|
|
457 |
// Custom event polyfill
|
|
|
458 |
ionic.CustomEvent = (function() {
|
|
|
459 |
if( typeof window.CustomEvent === 'function' ) return CustomEvent;
|
|
|
460 |
|
|
|
461 |
var customEvent = function(event, params) {
|
|
|
462 |
var evt;
|
|
|
463 |
params = params || {
|
|
|
464 |
bubbles: false,
|
|
|
465 |
cancelable: false,
|
|
|
466 |
detail: undefined
|
|
|
467 |
};
|
|
|
468 |
try {
|
|
|
469 |
evt = document.createEvent("CustomEvent");
|
|
|
470 |
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
|
|
|
471 |
} catch (error) {
|
|
|
472 |
// fallback for browsers that don't support createEvent('CustomEvent')
|
|
|
473 |
evt = document.createEvent("Event");
|
|
|
474 |
for (var param in params) {
|
|
|
475 |
evt[param] = params[param];
|
|
|
476 |
}
|
|
|
477 |
evt.initEvent(event, params.bubbles, params.cancelable);
|
|
|
478 |
}
|
|
|
479 |
return evt;
|
|
|
480 |
};
|
|
|
481 |
customEvent.prototype = window.Event.prototype;
|
|
|
482 |
return customEvent;
|
|
|
483 |
})();
|
|
|
484 |
|
|
|
485 |
|
|
|
486 |
/**
|
|
|
487 |
* @ngdoc utility
|
|
|
488 |
* @name ionic.EventController
|
|
|
489 |
* @module ionic
|
|
|
490 |
*/
|
|
|
491 |
ionic.EventController = {
|
|
|
492 |
VIRTUALIZED_EVENTS: ['tap', 'swipe', 'swiperight', 'swipeleft', 'drag', 'hold', 'release'],
|
|
|
493 |
|
|
|
494 |
/**
|
|
|
495 |
* @ngdoc method
|
|
|
496 |
* @name ionic.EventController#trigger
|
|
|
497 |
* @alias ionic.trigger
|
|
|
498 |
* @param {string} eventType The event to trigger.
|
|
|
499 |
* @param {object} data The data for the event. Hint: pass in
|
|
|
500 |
* `{target: targetElement}`
|
|
|
501 |
* @param {boolean=} bubbles Whether the event should bubble up the DOM.
|
|
|
502 |
* @param {boolean=} cancelable Whether the event should be cancelable.
|
|
|
503 |
*/
|
|
|
504 |
// Trigger a new event
|
|
|
505 |
trigger: function(eventType, data, bubbles, cancelable) {
|
|
|
506 |
var event = new ionic.CustomEvent(eventType, {
|
|
|
507 |
detail: data,
|
|
|
508 |
bubbles: !!bubbles,
|
|
|
509 |
cancelable: !!cancelable
|
|
|
510 |
});
|
|
|
511 |
|
|
|
512 |
// Make sure to trigger the event on the given target, or dispatch it from
|
|
|
513 |
// the window if we don't have an event target
|
|
|
514 |
data && data.target && data.target.dispatchEvent && data.target.dispatchEvent(event) || window.dispatchEvent(event);
|
|
|
515 |
},
|
|
|
516 |
|
|
|
517 |
/**
|
|
|
518 |
* @ngdoc method
|
|
|
519 |
* @name ionic.EventController#on
|
|
|
520 |
* @alias ionic.on
|
|
|
521 |
* @description Listen to an event on an element.
|
|
|
522 |
* @param {string} type The event to listen for.
|
|
|
523 |
* @param {function} callback The listener to be called.
|
|
|
524 |
* @param {DOMElement} element The element to listen for the event on.
|
|
|
525 |
*/
|
|
|
526 |
on: function(type, callback, element) {
|
|
|
527 |
var e = element || window;
|
|
|
528 |
|
|
|
529 |
// Bind a gesture if it's a virtual event
|
|
|
530 |
for(var i = 0, j = this.VIRTUALIZED_EVENTS.length; i < j; i++) {
|
|
|
531 |
if(type == this.VIRTUALIZED_EVENTS[i]) {
|
|
|
532 |
var gesture = new ionic.Gesture(element);
|
|
|
533 |
gesture.on(type, callback);
|
|
|
534 |
return gesture;
|
|
|
535 |
}
|
|
|
536 |
}
|
|
|
537 |
|
|
|
538 |
// Otherwise bind a normal event
|
|
|
539 |
e.addEventListener(type, callback);
|
|
|
540 |
},
|
|
|
541 |
|
|
|
542 |
/**
|
|
|
543 |
* @ngdoc method
|
|
|
544 |
* @name ionic.EventController#off
|
|
|
545 |
* @alias ionic.off
|
|
|
546 |
* @description Remove an event listener.
|
|
|
547 |
* @param {string} type
|
|
|
548 |
* @param {function} callback
|
|
|
549 |
* @param {DOMElement} element
|
|
|
550 |
*/
|
|
|
551 |
off: function(type, callback, element) {
|
|
|
552 |
element.removeEventListener(type, callback);
|
|
|
553 |
},
|
|
|
554 |
|
|
|
555 |
/**
|
|
|
556 |
* @ngdoc method
|
|
|
557 |
* @name ionic.EventController#onGesture
|
|
|
558 |
* @alias ionic.onGesture
|
|
|
559 |
* @description Add an event listener for a gesture on an element.
|
|
|
560 |
*
|
|
|
561 |
* Available eventTypes (from [hammer.js](http://eightmedia.github.io/hammer.js/)):
|
|
|
562 |
*
|
|
|
563 |
* `hold`, `tap`, `doubletap`, `drag`, `dragstart`, `dragend`, `dragup`, `dragdown`, <br/>
|
|
|
564 |
* `dragleft`, `dragright`, `swipe`, `swipeup`, `swipedown`, `swipeleft`, `swiperight`, <br/>
|
|
|
565 |
* `transform`, `transformstart`, `transformend`, `rotate`, `pinch`, `pinchin`, `pinchout`, </br>
|
|
|
566 |
* `touch`, `release`
|
|
|
567 |
*
|
|
|
568 |
* @param {string} eventType The gesture event to listen for.
|
|
|
569 |
* @param {function(e)} callback The function to call when the gesture
|
|
|
570 |
* happens.
|
|
|
571 |
* @param {DOMElement} element The angular element to listen for the event on.
|
|
|
572 |
* @param {object} options object.
|
|
|
573 |
* @returns {ionic.Gesture} The gesture object (use this to remove the gesture later on).
|
|
|
574 |
*/
|
|
|
575 |
onGesture: function(type, callback, element, options) {
|
|
|
576 |
var gesture = new ionic.Gesture(element, options);
|
|
|
577 |
gesture.on(type, callback);
|
|
|
578 |
return gesture;
|
|
|
579 |
},
|
|
|
580 |
|
|
|
581 |
/**
|
|
|
582 |
* @ngdoc method
|
|
|
583 |
* @name ionic.EventController#offGesture
|
|
|
584 |
* @alias ionic.offGesture
|
|
|
585 |
* @description Remove an event listener for a gesture created on an element.
|
|
|
586 |
* @param {ionic.Gesture} gesture The gesture that should be removed.
|
|
|
587 |
* @param {string} eventType The gesture event to remove the listener for.
|
|
|
588 |
* @param {function(e)} callback The listener to remove.
|
|
|
589 |
|
|
|
590 |
*/
|
|
|
591 |
offGesture: function(gesture, type, callback) {
|
|
|
592 |
gesture && gesture.off(type, callback);
|
|
|
593 |
},
|
|
|
594 |
|
|
|
595 |
handlePopState: function() {}
|
|
|
596 |
};
|
|
|
597 |
|
|
|
598 |
|
|
|
599 |
// Map some convenient top-level functions for event handling
|
|
|
600 |
ionic.on = function() { ionic.EventController.on.apply(ionic.EventController, arguments); };
|
|
|
601 |
ionic.off = function() { ionic.EventController.off.apply(ionic.EventController, arguments); };
|
|
|
602 |
ionic.trigger = ionic.EventController.trigger;//function() { ionic.EventController.trigger.apply(ionic.EventController.trigger, arguments); };
|
|
|
603 |
ionic.onGesture = function() { return ionic.EventController.onGesture.apply(ionic.EventController.onGesture, arguments); };
|
|
|
604 |
ionic.offGesture = function() { return ionic.EventController.offGesture.apply(ionic.EventController.offGesture, arguments); };
|
|
|
605 |
|
|
|
606 |
})(window.ionic);
|
|
|
607 |
|
|
|
608 |
/* eslint camelcase:0 */
|
|
|
609 |
/**
|
|
|
610 |
* Simple gesture controllers with some common gestures that emit
|
|
|
611 |
* gesture events.
|
|
|
612 |
*
|
|
|
613 |
* Ported from github.com/EightMedia/hammer.js Gestures - thanks!
|
|
|
614 |
*/
|
|
|
615 |
(function(ionic) {
|
|
|
616 |
|
|
|
617 |
/**
|
|
|
618 |
* ionic.Gestures
|
|
|
619 |
* use this to create instances
|
|
|
620 |
* @param {HTMLElement} element
|
|
|
621 |
* @param {Object} options
|
|
|
622 |
* @returns {ionic.Gestures.Instance}
|
|
|
623 |
* @constructor
|
|
|
624 |
*/
|
|
|
625 |
ionic.Gesture = function(element, options) {
|
|
|
626 |
return new ionic.Gestures.Instance(element, options || {});
|
|
|
627 |
};
|
|
|
628 |
|
|
|
629 |
ionic.Gestures = {};
|
|
|
630 |
|
|
|
631 |
// default settings
|
|
|
632 |
ionic.Gestures.defaults = {
|
|
|
633 |
// add css to the element to prevent the browser from doing
|
|
|
634 |
// its native behavior. this doesnt prevent the scrolling,
|
|
|
635 |
// but cancels the contextmenu, tap highlighting etc
|
|
|
636 |
// set to false to disable this
|
|
|
637 |
stop_browser_behavior: 'disable-user-behavior'
|
|
|
638 |
};
|
|
|
639 |
|
|
|
640 |
// detect touchevents
|
|
|
641 |
ionic.Gestures.HAS_POINTEREVENTS = window.navigator.pointerEnabled || window.navigator.msPointerEnabled;
|
|
|
642 |
ionic.Gestures.HAS_TOUCHEVENTS = ('ontouchstart' in window);
|
|
|
643 |
|
|
|
644 |
// dont use mouseevents on mobile devices
|
|
|
645 |
ionic.Gestures.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android|silk/i;
|
|
|
646 |
ionic.Gestures.NO_MOUSEEVENTS = ionic.Gestures.HAS_TOUCHEVENTS && window.navigator.userAgent.match(ionic.Gestures.MOBILE_REGEX);
|
|
|
647 |
|
|
|
648 |
// eventtypes per touchevent (start, move, end)
|
|
|
649 |
// are filled by ionic.Gestures.event.determineEventTypes on setup
|
|
|
650 |
ionic.Gestures.EVENT_TYPES = {};
|
|
|
651 |
|
|
|
652 |
// direction defines
|
|
|
653 |
ionic.Gestures.DIRECTION_DOWN = 'down';
|
|
|
654 |
ionic.Gestures.DIRECTION_LEFT = 'left';
|
|
|
655 |
ionic.Gestures.DIRECTION_UP = 'up';
|
|
|
656 |
ionic.Gestures.DIRECTION_RIGHT = 'right';
|
|
|
657 |
|
|
|
658 |
// pointer type
|
|
|
659 |
ionic.Gestures.POINTER_MOUSE = 'mouse';
|
|
|
660 |
ionic.Gestures.POINTER_TOUCH = 'touch';
|
|
|
661 |
ionic.Gestures.POINTER_PEN = 'pen';
|
|
|
662 |
|
|
|
663 |
// touch event defines
|
|
|
664 |
ionic.Gestures.EVENT_START = 'start';
|
|
|
665 |
ionic.Gestures.EVENT_MOVE = 'move';
|
|
|
666 |
ionic.Gestures.EVENT_END = 'end';
|
|
|
667 |
|
|
|
668 |
// hammer document where the base events are added at
|
|
|
669 |
ionic.Gestures.DOCUMENT = window.document;
|
|
|
670 |
|
|
|
671 |
// plugins namespace
|
|
|
672 |
ionic.Gestures.plugins = {};
|
|
|
673 |
|
|
|
674 |
// if the window events are set...
|
|
|
675 |
ionic.Gestures.READY = false;
|
|
|
676 |
|
|
|
677 |
/**
|
|
|
678 |
* setup events to detect gestures on the document
|
|
|
679 |
*/
|
|
|
680 |
function setup() {
|
|
|
681 |
if(ionic.Gestures.READY) {
|
|
|
682 |
return;
|
|
|
683 |
}
|
|
|
684 |
|
|
|
685 |
// find what eventtypes we add listeners to
|
|
|
686 |
ionic.Gestures.event.determineEventTypes();
|
|
|
687 |
|
|
|
688 |
// Register all gestures inside ionic.Gestures.gestures
|
|
|
689 |
for(var name in ionic.Gestures.gestures) {
|
|
|
690 |
if(ionic.Gestures.gestures.hasOwnProperty(name)) {
|
|
|
691 |
ionic.Gestures.detection.register(ionic.Gestures.gestures[name]);
|
|
|
692 |
}
|
|
|
693 |
}
|
|
|
694 |
|
|
|
695 |
// Add touch events on the document
|
|
|
696 |
ionic.Gestures.event.onTouch(ionic.Gestures.DOCUMENT, ionic.Gestures.EVENT_MOVE, ionic.Gestures.detection.detect);
|
|
|
697 |
ionic.Gestures.event.onTouch(ionic.Gestures.DOCUMENT, ionic.Gestures.EVENT_END, ionic.Gestures.detection.detect);
|
|
|
698 |
|
|
|
699 |
// ionic.Gestures is ready...!
|
|
|
700 |
ionic.Gestures.READY = true;
|
|
|
701 |
}
|
|
|
702 |
|
|
|
703 |
/**
|
|
|
704 |
* create new hammer instance
|
|
|
705 |
* all methods should return the instance itself, so it is chainable.
|
|
|
706 |
* @param {HTMLElement} element
|
|
|
707 |
* @param {Object} [options={}]
|
|
|
708 |
* @returns {ionic.Gestures.Instance}
|
|
|
709 |
* @name Gesture.Instance
|
|
|
710 |
* @constructor
|
|
|
711 |
*/
|
|
|
712 |
ionic.Gestures.Instance = function(element, options) {
|
|
|
713 |
var self = this;
|
|
|
714 |
|
|
|
715 |
// A null element was passed into the instance, which means
|
|
|
716 |
// whatever lookup was done to find this element failed to find it
|
|
|
717 |
// so we can't listen for events on it.
|
|
|
718 |
if(element === null) {
|
|
|
719 |
void 0;
|
|
|
720 |
return this;
|
|
|
721 |
}
|
|
|
722 |
|
|
|
723 |
// setup ionic.GesturesJS window events and register all gestures
|
|
|
724 |
// this also sets up the default options
|
|
|
725 |
setup();
|
|
|
726 |
|
|
|
727 |
this.element = element;
|
|
|
728 |
|
|
|
729 |
// start/stop detection option
|
|
|
730 |
this.enabled = true;
|
|
|
731 |
|
|
|
732 |
// merge options
|
|
|
733 |
this.options = ionic.Gestures.utils.extend(
|
|
|
734 |
ionic.Gestures.utils.extend({}, ionic.Gestures.defaults),
|
|
|
735 |
options || {});
|
|
|
736 |
|
|
|
737 |
// add some css to the element to prevent the browser from doing its native behavoir
|
|
|
738 |
if(this.options.stop_browser_behavior) {
|
|
|
739 |
ionic.Gestures.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
|
|
|
740 |
}
|
|
|
741 |
|
|
|
742 |
// start detection on touchstart
|
|
|
743 |
ionic.Gestures.event.onTouch(element, ionic.Gestures.EVENT_START, function(ev) {
|
|
|
744 |
if(self.enabled) {
|
|
|
745 |
ionic.Gestures.detection.startDetect(self, ev);
|
|
|
746 |
}
|
|
|
747 |
});
|
|
|
748 |
|
|
|
749 |
// return instance
|
|
|
750 |
return this;
|
|
|
751 |
};
|
|
|
752 |
|
|
|
753 |
|
|
|
754 |
ionic.Gestures.Instance.prototype = {
|
|
|
755 |
/**
|
|
|
756 |
* bind events to the instance
|
|
|
757 |
* @param {String} gesture
|
|
|
758 |
* @param {Function} handler
|
|
|
759 |
* @returns {ionic.Gestures.Instance}
|
|
|
760 |
*/
|
|
|
761 |
on: function onEvent(gesture, handler){
|
|
|
762 |
var gestures = gesture.split(' ');
|
|
|
763 |
for(var t = 0; t < gestures.length; t++) {
|
|
|
764 |
this.element.addEventListener(gestures[t], handler, false);
|
|
|
765 |
}
|
|
|
766 |
return this;
|
|
|
767 |
},
|
|
|
768 |
|
|
|
769 |
|
|
|
770 |
/**
|
|
|
771 |
* unbind events to the instance
|
|
|
772 |
* @param {String} gesture
|
|
|
773 |
* @param {Function} handler
|
|
|
774 |
* @returns {ionic.Gestures.Instance}
|
|
|
775 |
*/
|
|
|
776 |
off: function offEvent(gesture, handler){
|
|
|
777 |
var gestures = gesture.split(' ');
|
|
|
778 |
for(var t = 0; t < gestures.length; t++) {
|
|
|
779 |
this.element.removeEventListener(gestures[t], handler, false);
|
|
|
780 |
}
|
|
|
781 |
return this;
|
|
|
782 |
},
|
|
|
783 |
|
|
|
784 |
|
|
|
785 |
/**
|
|
|
786 |
* trigger gesture event
|
|
|
787 |
* @param {String} gesture
|
|
|
788 |
* @param {Object} eventData
|
|
|
789 |
* @returns {ionic.Gestures.Instance}
|
|
|
790 |
*/
|
|
|
791 |
trigger: function triggerEvent(gesture, eventData){
|
|
|
792 |
// create DOM event
|
|
|
793 |
var event = ionic.Gestures.DOCUMENT.createEvent('Event');
|
|
|
794 |
event.initEvent(gesture, true, true);
|
|
|
795 |
event.gesture = eventData;
|
|
|
796 |
|
|
|
797 |
// trigger on the target if it is in the instance element,
|
|
|
798 |
// this is for event delegation tricks
|
|
|
799 |
var element = this.element;
|
|
|
800 |
if(ionic.Gestures.utils.hasParent(eventData.target, element)) {
|
|
|
801 |
element = eventData.target;
|
|
|
802 |
}
|
|
|
803 |
|
|
|
804 |
element.dispatchEvent(event);
|
|
|
805 |
return this;
|
|
|
806 |
},
|
|
|
807 |
|
|
|
808 |
|
|
|
809 |
/**
|
|
|
810 |
* enable of disable hammer.js detection
|
|
|
811 |
* @param {Boolean} state
|
|
|
812 |
* @returns {ionic.Gestures.Instance}
|
|
|
813 |
*/
|
|
|
814 |
enable: function enable(state) {
|
|
|
815 |
this.enabled = state;
|
|
|
816 |
return this;
|
|
|
817 |
}
|
|
|
818 |
};
|
|
|
819 |
|
|
|
820 |
/**
|
|
|
821 |
* this holds the last move event,
|
|
|
822 |
* used to fix empty touchend issue
|
|
|
823 |
* see the onTouch event for an explanation
|
|
|
824 |
* type {Object}
|
|
|
825 |
*/
|
|
|
826 |
var last_move_event = null;
|
|
|
827 |
|
|
|
828 |
|
|
|
829 |
/**
|
|
|
830 |
* when the mouse is hold down, this is true
|
|
|
831 |
* type {Boolean}
|
|
|
832 |
*/
|
|
|
833 |
var enable_detect = false;
|
|
|
834 |
|
|
|
835 |
|
|
|
836 |
/**
|
|
|
837 |
* when touch events have been fired, this is true
|
|
|
838 |
* type {Boolean}
|
|
|
839 |
*/
|
|
|
840 |
var touch_triggered = false;
|
|
|
841 |
|
|
|
842 |
|
|
|
843 |
ionic.Gestures.event = {
|
|
|
844 |
/**
|
|
|
845 |
* simple addEventListener
|
|
|
846 |
* @param {HTMLElement} element
|
|
|
847 |
* @param {String} type
|
|
|
848 |
* @param {Function} handler
|
|
|
849 |
*/
|
|
|
850 |
bindDom: function(element, type, handler) {
|
|
|
851 |
var types = type.split(' ');
|
|
|
852 |
for(var t = 0; t < types.length; t++) {
|
|
|
853 |
element.addEventListener(types[t], handler, false);
|
|
|
854 |
}
|
|
|
855 |
},
|
|
|
856 |
|
|
|
857 |
|
|
|
858 |
/**
|
|
|
859 |
* touch events with mouse fallback
|
|
|
860 |
* @param {HTMLElement} element
|
|
|
861 |
* @param {String} eventType like ionic.Gestures.EVENT_MOVE
|
|
|
862 |
* @param {Function} handler
|
|
|
863 |
*/
|
|
|
864 |
onTouch: function onTouch(element, eventType, handler) {
|
|
|
865 |
var self = this;
|
|
|
866 |
|
|
|
867 |
this.bindDom(element, ionic.Gestures.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
|
|
|
868 |
var sourceEventType = ev.type.toLowerCase();
|
|
|
869 |
|
|
|
870 |
// onmouseup, but when touchend has been fired we do nothing.
|
|
|
871 |
// this is for touchdevices which also fire a mouseup on touchend
|
|
|
872 |
if(sourceEventType.match(/mouse/) && touch_triggered) {
|
|
|
873 |
return;
|
|
|
874 |
}
|
|
|
875 |
|
|
|
876 |
// mousebutton must be down or a touch event
|
|
|
877 |
else if( sourceEventType.match(/touch/) || // touch events are always on screen
|
|
|
878 |
sourceEventType.match(/pointerdown/) || // pointerevents touch
|
|
|
879 |
(sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed
|
|
|
880 |
){
|
|
|
881 |
enable_detect = true;
|
|
|
882 |
}
|
|
|
883 |
|
|
|
884 |
// mouse isn't pressed
|
|
|
885 |
else if(sourceEventType.match(/mouse/) && ev.which !== 1) {
|
|
|
886 |
enable_detect = false;
|
|
|
887 |
}
|
|
|
888 |
|
|
|
889 |
|
|
|
890 |
// we are in a touch event, set the touch triggered bool to true,
|
|
|
891 |
// this for the conflicts that may occur on ios and android
|
|
|
892 |
if(sourceEventType.match(/touch|pointer/)) {
|
|
|
893 |
touch_triggered = true;
|
|
|
894 |
}
|
|
|
895 |
|
|
|
896 |
// count the total touches on the screen
|
|
|
897 |
var count_touches = 0;
|
|
|
898 |
|
|
|
899 |
// when touch has been triggered in this detection session
|
|
|
900 |
// and we are now handling a mouse event, we stop that to prevent conflicts
|
|
|
901 |
if(enable_detect) {
|
|
|
902 |
// update pointerevent
|
|
|
903 |
if(ionic.Gestures.HAS_POINTEREVENTS && eventType != ionic.Gestures.EVENT_END) {
|
|
|
904 |
count_touches = ionic.Gestures.PointerEvent.updatePointer(eventType, ev);
|
|
|
905 |
}
|
|
|
906 |
// touch
|
|
|
907 |
else if(sourceEventType.match(/touch/)) {
|
|
|
908 |
count_touches = ev.touches.length;
|
|
|
909 |
}
|
|
|
910 |
// mouse
|
|
|
911 |
else if(!touch_triggered) {
|
|
|
912 |
count_touches = sourceEventType.match(/up/) ? 0 : 1;
|
|
|
913 |
}
|
|
|
914 |
|
|
|
915 |
// if we are in a end event, but when we remove one touch and
|
|
|
916 |
// we still have enough, set eventType to move
|
|
|
917 |
if(count_touches > 0 && eventType == ionic.Gestures.EVENT_END) {
|
|
|
918 |
eventType = ionic.Gestures.EVENT_MOVE;
|
|
|
919 |
}
|
|
|
920 |
// no touches, force the end event
|
|
|
921 |
else if(!count_touches) {
|
|
|
922 |
eventType = ionic.Gestures.EVENT_END;
|
|
|
923 |
}
|
|
|
924 |
|
|
|
925 |
// store the last move event
|
|
|
926 |
if(count_touches || last_move_event === null) {
|
|
|
927 |
last_move_event = ev;
|
|
|
928 |
}
|
|
|
929 |
|
|
|
930 |
// trigger the handler
|
|
|
931 |
handler.call(ionic.Gestures.detection, self.collectEventData(element, eventType, self.getTouchList(last_move_event, eventType), ev));
|
|
|
932 |
|
|
|
933 |
// remove pointerevent from list
|
|
|
934 |
if(ionic.Gestures.HAS_POINTEREVENTS && eventType == ionic.Gestures.EVENT_END) {
|
|
|
935 |
count_touches = ionic.Gestures.PointerEvent.updatePointer(eventType, ev);
|
|
|
936 |
}
|
|
|
937 |
}
|
|
|
938 |
|
|
|
939 |
//debug(sourceEventType +" "+ eventType);
|
|
|
940 |
|
|
|
941 |
// on the end we reset everything
|
|
|
942 |
if(!count_touches) {
|
|
|
943 |
last_move_event = null;
|
|
|
944 |
enable_detect = false;
|
|
|
945 |
touch_triggered = false;
|
|
|
946 |
ionic.Gestures.PointerEvent.reset();
|
|
|
947 |
}
|
|
|
948 |
});
|
|
|
949 |
},
|
|
|
950 |
|
|
|
951 |
|
|
|
952 |
/**
|
|
|
953 |
* we have different events for each device/browser
|
|
|
954 |
* determine what we need and set them in the ionic.Gestures.EVENT_TYPES constant
|
|
|
955 |
*/
|
|
|
956 |
determineEventTypes: function determineEventTypes() {
|
|
|
957 |
// determine the eventtype we want to set
|
|
|
958 |
var types;
|
|
|
959 |
|
|
|
960 |
// pointerEvents magic
|
|
|
961 |
if(ionic.Gestures.HAS_POINTEREVENTS) {
|
|
|
962 |
types = ionic.Gestures.PointerEvent.getEvents();
|
|
|
963 |
}
|
|
|
964 |
// on Android, iOS, blackberry, windows mobile we dont want any mouseevents
|
|
|
965 |
else if(ionic.Gestures.NO_MOUSEEVENTS) {
|
|
|
966 |
types = [
|
|
|
967 |
'touchstart',
|
|
|
968 |
'touchmove',
|
|
|
969 |
'touchend touchcancel'];
|
|
|
970 |
}
|
|
|
971 |
// for non pointer events browsers and mixed browsers,
|
|
|
972 |
// like chrome on windows8 touch laptop
|
|
|
973 |
else {
|
|
|
974 |
types = [
|
|
|
975 |
'touchstart mousedown',
|
|
|
976 |
'touchmove mousemove',
|
|
|
977 |
'touchend touchcancel mouseup'];
|
|
|
978 |
}
|
|
|
979 |
|
|
|
980 |
ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_START] = types[0];
|
|
|
981 |
ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_MOVE] = types[1];
|
|
|
982 |
ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_END] = types[2];
|
|
|
983 |
},
|
|
|
984 |
|
|
|
985 |
|
|
|
986 |
/**
|
|
|
987 |
* create touchlist depending on the event
|
|
|
988 |
* @param {Object} ev
|
|
|
989 |
* @param {String} eventType used by the fakemultitouch plugin
|
|
|
990 |
*/
|
|
|
991 |
getTouchList: function getTouchList(ev/*, eventType*/) {
|
|
|
992 |
// get the fake pointerEvent touchlist
|
|
|
993 |
if(ionic.Gestures.HAS_POINTEREVENTS) {
|
|
|
994 |
return ionic.Gestures.PointerEvent.getTouchList();
|
|
|
995 |
}
|
|
|
996 |
// get the touchlist
|
|
|
997 |
else if(ev.touches) {
|
|
|
998 |
return ev.touches;
|
|
|
999 |
}
|
|
|
1000 |
// make fake touchlist from mouse position
|
|
|
1001 |
else {
|
|
|
1002 |
ev.identifier = 1;
|
|
|
1003 |
return [ev];
|
|
|
1004 |
}
|
|
|
1005 |
},
|
|
|
1006 |
|
|
|
1007 |
|
|
|
1008 |
/**
|
|
|
1009 |
* collect event data for ionic.Gestures js
|
|
|
1010 |
* @param {HTMLElement} element
|
|
|
1011 |
* @param {String} eventType like ionic.Gestures.EVENT_MOVE
|
|
|
1012 |
* @param {Object} eventData
|
|
|
1013 |
*/
|
|
|
1014 |
collectEventData: function collectEventData(element, eventType, touches, ev) {
|
|
|
1015 |
|
|
|
1016 |
// find out pointerType
|
|
|
1017 |
var pointerType = ionic.Gestures.POINTER_TOUCH;
|
|
|
1018 |
if(ev.type.match(/mouse/) || ionic.Gestures.PointerEvent.matchType(ionic.Gestures.POINTER_MOUSE, ev)) {
|
|
|
1019 |
pointerType = ionic.Gestures.POINTER_MOUSE;
|
|
|
1020 |
}
|
|
|
1021 |
|
|
|
1022 |
return {
|
|
|
1023 |
center: ionic.Gestures.utils.getCenter(touches),
|
|
|
1024 |
timeStamp: new Date().getTime(),
|
|
|
1025 |
target: ev.target,
|
|
|
1026 |
touches: touches,
|
|
|
1027 |
eventType: eventType,
|
|
|
1028 |
pointerType: pointerType,
|
|
|
1029 |
srcEvent: ev,
|
|
|
1030 |
|
|
|
1031 |
/**
|
|
|
1032 |
* prevent the browser default actions
|
|
|
1033 |
* mostly used to disable scrolling of the browser
|
|
|
1034 |
*/
|
|
|
1035 |
preventDefault: function() {
|
|
|
1036 |
if(this.srcEvent.preventManipulation) {
|
|
|
1037 |
this.srcEvent.preventManipulation();
|
|
|
1038 |
}
|
|
|
1039 |
|
|
|
1040 |
if(this.srcEvent.preventDefault) {
|
|
|
1041 |
// this.srcEvent.preventDefault();
|
|
|
1042 |
}
|
|
|
1043 |
},
|
|
|
1044 |
|
|
|
1045 |
/**
|
|
|
1046 |
* stop bubbling the event up to its parents
|
|
|
1047 |
*/
|
|
|
1048 |
stopPropagation: function() {
|
|
|
1049 |
this.srcEvent.stopPropagation();
|
|
|
1050 |
},
|
|
|
1051 |
|
|
|
1052 |
/**
|
|
|
1053 |
* immediately stop gesture detection
|
|
|
1054 |
* might be useful after a swipe was detected
|
|
|
1055 |
* @return {*}
|
|
|
1056 |
*/
|
|
|
1057 |
stopDetect: function() {
|
|
|
1058 |
return ionic.Gestures.detection.stopDetect();
|
|
|
1059 |
}
|
|
|
1060 |
};
|
|
|
1061 |
}
|
|
|
1062 |
};
|
|
|
1063 |
|
|
|
1064 |
ionic.Gestures.PointerEvent = {
|
|
|
1065 |
/**
|
|
|
1066 |
* holds all pointers
|
|
|
1067 |
* type {Object}
|
|
|
1068 |
*/
|
|
|
1069 |
pointers: {},
|
|
|
1070 |
|
|
|
1071 |
/**
|
|
|
1072 |
* get a list of pointers
|
|
|
1073 |
* @returns {Array} touchlist
|
|
|
1074 |
*/
|
|
|
1075 |
getTouchList: function() {
|
|
|
1076 |
var self = this;
|
|
|
1077 |
var touchlist = [];
|
|
|
1078 |
|
|
|
1079 |
// we can use forEach since pointerEvents only is in IE10
|
|
|
1080 |
Object.keys(self.pointers).sort().forEach(function(id) {
|
|
|
1081 |
touchlist.push(self.pointers[id]);
|
|
|
1082 |
});
|
|
|
1083 |
return touchlist;
|
|
|
1084 |
},
|
|
|
1085 |
|
|
|
1086 |
/**
|
|
|
1087 |
* update the position of a pointer
|
|
|
1088 |
* @param {String} type ionic.Gestures.EVENT_END
|
|
|
1089 |
* @param {Object} pointerEvent
|
|
|
1090 |
*/
|
|
|
1091 |
updatePointer: function(type, pointerEvent) {
|
|
|
1092 |
if(type == ionic.Gestures.EVENT_END) {
|
|
|
1093 |
this.pointers = {};
|
|
|
1094 |
}
|
|
|
1095 |
else {
|
|
|
1096 |
pointerEvent.identifier = pointerEvent.pointerId;
|
|
|
1097 |
this.pointers[pointerEvent.pointerId] = pointerEvent;
|
|
|
1098 |
}
|
|
|
1099 |
|
|
|
1100 |
return Object.keys(this.pointers).length;
|
|
|
1101 |
},
|
|
|
1102 |
|
|
|
1103 |
/**
|
|
|
1104 |
* check if ev matches pointertype
|
|
|
1105 |
* @param {String} pointerType ionic.Gestures.POINTER_MOUSE
|
|
|
1106 |
* @param {PointerEvent} ev
|
|
|
1107 |
*/
|
|
|
1108 |
matchType: function(pointerType, ev) {
|
|
|
1109 |
if(!ev.pointerType) {
|
|
|
1110 |
return false;
|
|
|
1111 |
}
|
|
|
1112 |
|
|
|
1113 |
var types = {};
|
|
|
1114 |
types[ionic.Gestures.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == ionic.Gestures.POINTER_MOUSE);
|
|
|
1115 |
types[ionic.Gestures.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == ionic.Gestures.POINTER_TOUCH);
|
|
|
1116 |
types[ionic.Gestures.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == ionic.Gestures.POINTER_PEN);
|
|
|
1117 |
return types[pointerType];
|
|
|
1118 |
},
|
|
|
1119 |
|
|
|
1120 |
|
|
|
1121 |
/**
|
|
|
1122 |
* get events
|
|
|
1123 |
*/
|
|
|
1124 |
getEvents: function() {
|
|
|
1125 |
return [
|
|
|
1126 |
'pointerdown MSPointerDown',
|
|
|
1127 |
'pointermove MSPointerMove',
|
|
|
1128 |
'pointerup pointercancel MSPointerUp MSPointerCancel'
|
|
|
1129 |
];
|
|
|
1130 |
},
|
|
|
1131 |
|
|
|
1132 |
/**
|
|
|
1133 |
* reset the list
|
|
|
1134 |
*/
|
|
|
1135 |
reset: function() {
|
|
|
1136 |
this.pointers = {};
|
|
|
1137 |
}
|
|
|
1138 |
};
|
|
|
1139 |
|
|
|
1140 |
|
|
|
1141 |
ionic.Gestures.utils = {
|
|
|
1142 |
/**
|
|
|
1143 |
* extend method,
|
|
|
1144 |
* also used for cloning when dest is an empty object
|
|
|
1145 |
* @param {Object} dest
|
|
|
1146 |
* @param {Object} src
|
|
|
1147 |
* @param {Boolean} merge do a merge
|
|
|
1148 |
* @returns {Object} dest
|
|
|
1149 |
*/
|
|
|
1150 |
extend: function extend(dest, src, merge) {
|
|
|
1151 |
for (var key in src) {
|
|
|
1152 |
if(dest[key] !== undefined && merge) {
|
|
|
1153 |
continue;
|
|
|
1154 |
}
|
|
|
1155 |
dest[key] = src[key];
|
|
|
1156 |
}
|
|
|
1157 |
return dest;
|
|
|
1158 |
},
|
|
|
1159 |
|
|
|
1160 |
|
|
|
1161 |
/**
|
|
|
1162 |
* find if a node is in the given parent
|
|
|
1163 |
* used for event delegation tricks
|
|
|
1164 |
* @param {HTMLElement} node
|
|
|
1165 |
* @param {HTMLElement} parent
|
|
|
1166 |
* @returns {boolean} has_parent
|
|
|
1167 |
*/
|
|
|
1168 |
hasParent: function(node, parent) {
|
|
|
1169 |
while(node){
|
|
|
1170 |
if(node == parent) {
|
|
|
1171 |
return true;
|
|
|
1172 |
}
|
|
|
1173 |
node = node.parentNode;
|
|
|
1174 |
}
|
|
|
1175 |
return false;
|
|
|
1176 |
},
|
|
|
1177 |
|
|
|
1178 |
|
|
|
1179 |
/**
|
|
|
1180 |
* get the center of all the touches
|
|
|
1181 |
* @param {Array} touches
|
|
|
1182 |
* @returns {Object} center
|
|
|
1183 |
*/
|
|
|
1184 |
getCenter: function getCenter(touches) {
|
|
|
1185 |
var valuesX = [], valuesY = [];
|
|
|
1186 |
|
|
|
1187 |
for(var t = 0, len = touches.length; t < len; t++) {
|
|
|
1188 |
valuesX.push(touches[t].pageX);
|
|
|
1189 |
valuesY.push(touches[t].pageY);
|
|
|
1190 |
}
|
|
|
1191 |
|
|
|
1192 |
return {
|
|
|
1193 |
pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
|
|
|
1194 |
pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
|
|
|
1195 |
};
|
|
|
1196 |
},
|
|
|
1197 |
|
|
|
1198 |
|
|
|
1199 |
/**
|
|
|
1200 |
* calculate the velocity between two points
|
|
|
1201 |
* @param {Number} delta_time
|
|
|
1202 |
* @param {Number} delta_x
|
|
|
1203 |
* @param {Number} delta_y
|
|
|
1204 |
* @returns {Object} velocity
|
|
|
1205 |
*/
|
|
|
1206 |
getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
|
|
|
1207 |
return {
|
|
|
1208 |
x: Math.abs(delta_x / delta_time) || 0,
|
|
|
1209 |
y: Math.abs(delta_y / delta_time) || 0
|
|
|
1210 |
};
|
|
|
1211 |
},
|
|
|
1212 |
|
|
|
1213 |
|
|
|
1214 |
/**
|
|
|
1215 |
* calculate the angle between two coordinates
|
|
|
1216 |
* @param {Touch} touch1
|
|
|
1217 |
* @param {Touch} touch2
|
|
|
1218 |
* @returns {Number} angle
|
|
|
1219 |
*/
|
|
|
1220 |
getAngle: function getAngle(touch1, touch2) {
|
|
|
1221 |
var y = touch2.pageY - touch1.pageY,
|
|
|
1222 |
x = touch2.pageX - touch1.pageX;
|
|
|
1223 |
return Math.atan2(y, x) * 180 / Math.PI;
|
|
|
1224 |
},
|
|
|
1225 |
|
|
|
1226 |
|
|
|
1227 |
/**
|
|
|
1228 |
* angle to direction define
|
|
|
1229 |
* @param {Touch} touch1
|
|
|
1230 |
* @param {Touch} touch2
|
|
|
1231 |
* @returns {String} direction constant, like ionic.Gestures.DIRECTION_LEFT
|
|
|
1232 |
*/
|
|
|
1233 |
getDirection: function getDirection(touch1, touch2) {
|
|
|
1234 |
var x = Math.abs(touch1.pageX - touch2.pageX),
|
|
|
1235 |
y = Math.abs(touch1.pageY - touch2.pageY);
|
|
|
1236 |
|
|
|
1237 |
if(x >= y) {
|
|
|
1238 |
return touch1.pageX - touch2.pageX > 0 ? ionic.Gestures.DIRECTION_LEFT : ionic.Gestures.DIRECTION_RIGHT;
|
|
|
1239 |
}
|
|
|
1240 |
else {
|
|
|
1241 |
return touch1.pageY - touch2.pageY > 0 ? ionic.Gestures.DIRECTION_UP : ionic.Gestures.DIRECTION_DOWN;
|
|
|
1242 |
}
|
|
|
1243 |
},
|
|
|
1244 |
|
|
|
1245 |
|
|
|
1246 |
/**
|
|
|
1247 |
* calculate the distance between two touches
|
|
|
1248 |
* @param {Touch} touch1
|
|
|
1249 |
* @param {Touch} touch2
|
|
|
1250 |
* @returns {Number} distance
|
|
|
1251 |
*/
|
|
|
1252 |
getDistance: function getDistance(touch1, touch2) {
|
|
|
1253 |
var x = touch2.pageX - touch1.pageX,
|
|
|
1254 |
y = touch2.pageY - touch1.pageY;
|
|
|
1255 |
return Math.sqrt((x * x) + (y * y));
|
|
|
1256 |
},
|
|
|
1257 |
|
|
|
1258 |
|
|
|
1259 |
/**
|
|
|
1260 |
* calculate the scale factor between two touchLists (fingers)
|
|
|
1261 |
* no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
|
|
|
1262 |
* @param {Array} start
|
|
|
1263 |
* @param {Array} end
|
|
|
1264 |
* @returns {Number} scale
|
|
|
1265 |
*/
|
|
|
1266 |
getScale: function getScale(start, end) {
|
|
|
1267 |
// need two fingers...
|
|
|
1268 |
if(start.length >= 2 && end.length >= 2) {
|
|
|
1269 |
return this.getDistance(end[0], end[1]) /
|
|
|
1270 |
this.getDistance(start[0], start[1]);
|
|
|
1271 |
}
|
|
|
1272 |
return 1;
|
|
|
1273 |
},
|
|
|
1274 |
|
|
|
1275 |
|
|
|
1276 |
/**
|
|
|
1277 |
* calculate the rotation degrees between two touchLists (fingers)
|
|
|
1278 |
* @param {Array} start
|
|
|
1279 |
* @param {Array} end
|
|
|
1280 |
* @returns {Number} rotation
|
|
|
1281 |
*/
|
|
|
1282 |
getRotation: function getRotation(start, end) {
|
|
|
1283 |
// need two fingers
|
|
|
1284 |
if(start.length >= 2 && end.length >= 2) {
|
|
|
1285 |
return this.getAngle(end[1], end[0]) -
|
|
|
1286 |
this.getAngle(start[1], start[0]);
|
|
|
1287 |
}
|
|
|
1288 |
return 0;
|
|
|
1289 |
},
|
|
|
1290 |
|
|
|
1291 |
|
|
|
1292 |
/**
|
|
|
1293 |
* boolean if the direction is vertical
|
|
|
1294 |
* @param {String} direction
|
|
|
1295 |
* @returns {Boolean} is_vertical
|
|
|
1296 |
*/
|
|
|
1297 |
isVertical: function isVertical(direction) {
|
|
|
1298 |
return (direction == ionic.Gestures.DIRECTION_UP || direction == ionic.Gestures.DIRECTION_DOWN);
|
|
|
1299 |
},
|
|
|
1300 |
|
|
|
1301 |
|
|
|
1302 |
/**
|
|
|
1303 |
* stop browser default behavior with css class
|
|
|
1304 |
* @param {HtmlElement} element
|
|
|
1305 |
* @param {Object} css_class
|
|
|
1306 |
*/
|
|
|
1307 |
stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_class) {
|
|
|
1308 |
// changed from making many style changes to just adding a preset classname
|
|
|
1309 |
// less DOM manipulations, less code, and easier to control in the CSS side of things
|
|
|
1310 |
// hammer.js doesn't come with CSS, but ionic does, which is why we prefer this method
|
|
|
1311 |
if(element && element.classList) {
|
|
|
1312 |
element.classList.add(css_class);
|
|
|
1313 |
element.onselectstart = function() {
|
|
|
1314 |
return false;
|
|
|
1315 |
};
|
|
|
1316 |
}
|
|
|
1317 |
}
|
|
|
1318 |
};
|
|
|
1319 |
|
|
|
1320 |
|
|
|
1321 |
ionic.Gestures.detection = {
|
|
|
1322 |
// contains all registred ionic.Gestures.gestures in the correct order
|
|
|
1323 |
gestures: [],
|
|
|
1324 |
|
|
|
1325 |
// data of the current ionic.Gestures.gesture detection session
|
|
|
1326 |
current: null,
|
|
|
1327 |
|
|
|
1328 |
// the previous ionic.Gestures.gesture session data
|
|
|
1329 |
// is a full clone of the previous gesture.current object
|
|
|
1330 |
previous: null,
|
|
|
1331 |
|
|
|
1332 |
// when this becomes true, no gestures are fired
|
|
|
1333 |
stopped: false,
|
|
|
1334 |
|
|
|
1335 |
|
|
|
1336 |
/**
|
|
|
1337 |
* start ionic.Gestures.gesture detection
|
|
|
1338 |
* @param {ionic.Gestures.Instance} inst
|
|
|
1339 |
* @param {Object} eventData
|
|
|
1340 |
*/
|
|
|
1341 |
startDetect: function startDetect(inst, eventData) {
|
|
|
1342 |
// already busy with a ionic.Gestures.gesture detection on an element
|
|
|
1343 |
if(this.current) {
|
|
|
1344 |
return;
|
|
|
1345 |
}
|
|
|
1346 |
|
|
|
1347 |
this.stopped = false;
|
|
|
1348 |
|
|
|
1349 |
this.current = {
|
|
|
1350 |
inst: inst, // reference to ionic.GesturesInstance we're working for
|
|
|
1351 |
startEvent: ionic.Gestures.utils.extend({}, eventData), // start eventData for distances, timing etc
|
|
|
1352 |
lastEvent: false, // last eventData
|
|
|
1353 |
name: '' // current gesture we're in/detected, can be 'tap', 'hold' etc
|
|
|
1354 |
};
|
|
|
1355 |
|
|
|
1356 |
this.detect(eventData);
|
|
|
1357 |
},
|
|
|
1358 |
|
|
|
1359 |
|
|
|
1360 |
/**
|
|
|
1361 |
* ionic.Gestures.gesture detection
|
|
|
1362 |
* @param {Object} eventData
|
|
|
1363 |
*/
|
|
|
1364 |
detect: function detect(eventData) {
|
|
|
1365 |
if(!this.current || this.stopped) {
|
|
|
1366 |
return null;
|
|
|
1367 |
}
|
|
|
1368 |
|
|
|
1369 |
// extend event data with calculations about scale, distance etc
|
|
|
1370 |
eventData = this.extendEventData(eventData);
|
|
|
1371 |
|
|
|
1372 |
// instance options
|
|
|
1373 |
var inst_options = this.current.inst.options;
|
|
|
1374 |
|
|
|
1375 |
// call ionic.Gestures.gesture handlers
|
|
|
1376 |
for(var g = 0, len = this.gestures.length; g < len; g++) {
|
|
|
1377 |
var gesture = this.gestures[g];
|
|
|
1378 |
|
|
|
1379 |
// only when the instance options have enabled this gesture
|
|
|
1380 |
if(!this.stopped && inst_options[gesture.name] !== false) {
|
|
|
1381 |
// if a handler returns false, we stop with the detection
|
|
|
1382 |
if(gesture.handler.call(gesture, eventData, this.current.inst) === false) {
|
|
|
1383 |
this.stopDetect();
|
|
|
1384 |
break;
|
|
|
1385 |
}
|
|
|
1386 |
}
|
|
|
1387 |
}
|
|
|
1388 |
|
|
|
1389 |
// store as previous event event
|
|
|
1390 |
if(this.current) {
|
|
|
1391 |
this.current.lastEvent = eventData;
|
|
|
1392 |
}
|
|
|
1393 |
|
|
|
1394 |
// endevent, but not the last touch, so dont stop
|
|
|
1395 |
if(eventData.eventType == ionic.Gestures.EVENT_END && !eventData.touches.length - 1) {
|
|
|
1396 |
this.stopDetect();
|
|
|
1397 |
}
|
|
|
1398 |
|
|
|
1399 |
return eventData;
|
|
|
1400 |
},
|
|
|
1401 |
|
|
|
1402 |
|
|
|
1403 |
/**
|
|
|
1404 |
* clear the ionic.Gestures.gesture vars
|
|
|
1405 |
* this is called on endDetect, but can also be used when a final ionic.Gestures.gesture has been detected
|
|
|
1406 |
* to stop other ionic.Gestures.gestures from being fired
|
|
|
1407 |
*/
|
|
|
1408 |
stopDetect: function stopDetect() {
|
|
|
1409 |
// clone current data to the store as the previous gesture
|
|
|
1410 |
// used for the double tap gesture, since this is an other gesture detect session
|
|
|
1411 |
this.previous = ionic.Gestures.utils.extend({}, this.current);
|
|
|
1412 |
|
|
|
1413 |
// reset the current
|
|
|
1414 |
this.current = null;
|
|
|
1415 |
|
|
|
1416 |
// stopped!
|
|
|
1417 |
this.stopped = true;
|
|
|
1418 |
},
|
|
|
1419 |
|
|
|
1420 |
|
|
|
1421 |
/**
|
|
|
1422 |
* extend eventData for ionic.Gestures.gestures
|
|
|
1423 |
* @param {Object} ev
|
|
|
1424 |
* @returns {Object} ev
|
|
|
1425 |
*/
|
|
|
1426 |
extendEventData: function extendEventData(ev) {
|
|
|
1427 |
var startEv = this.current.startEvent;
|
|
|
1428 |
|
|
|
1429 |
// if the touches change, set the new touches over the startEvent touches
|
|
|
1430 |
// this because touchevents don't have all the touches on touchstart, or the
|
|
|
1431 |
// user must place his fingers at the EXACT same time on the screen, which is not realistic
|
|
|
1432 |
// but, sometimes it happens that both fingers are touching at the EXACT same time
|
|
|
1433 |
if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
|
|
|
1434 |
// extend 1 level deep to get the touchlist with the touch objects
|
|
|
1435 |
startEv.touches = [];
|
|
|
1436 |
for(var i = 0, len = ev.touches.length; i < len; i++) {
|
|
|
1437 |
startEv.touches.push(ionic.Gestures.utils.extend({}, ev.touches[i]));
|
|
|
1438 |
}
|
|
|
1439 |
}
|
|
|
1440 |
|
|
|
1441 |
var delta_time = ev.timeStamp - startEv.timeStamp,
|
|
|
1442 |
delta_x = ev.center.pageX - startEv.center.pageX,
|
|
|
1443 |
delta_y = ev.center.pageY - startEv.center.pageY,
|
|
|
1444 |
velocity = ionic.Gestures.utils.getVelocity(delta_time, delta_x, delta_y);
|
|
|
1445 |
|
|
|
1446 |
ionic.Gestures.utils.extend(ev, {
|
|
|
1447 |
deltaTime: delta_time,
|
|
|
1448 |
deltaX: delta_x,
|
|
|
1449 |
deltaY: delta_y,
|
|
|
1450 |
|
|
|
1451 |
velocityX: velocity.x,
|
|
|
1452 |
velocityY: velocity.y,
|
|
|
1453 |
|
|
|
1454 |
distance: ionic.Gestures.utils.getDistance(startEv.center, ev.center),
|
|
|
1455 |
angle: ionic.Gestures.utils.getAngle(startEv.center, ev.center),
|
|
|
1456 |
direction: ionic.Gestures.utils.getDirection(startEv.center, ev.center),
|
|
|
1457 |
|
|
|
1458 |
scale: ionic.Gestures.utils.getScale(startEv.touches, ev.touches),
|
|
|
1459 |
rotation: ionic.Gestures.utils.getRotation(startEv.touches, ev.touches),
|
|
|
1460 |
|
|
|
1461 |
startEvent: startEv
|
|
|
1462 |
});
|
|
|
1463 |
|
|
|
1464 |
return ev;
|
|
|
1465 |
},
|
|
|
1466 |
|
|
|
1467 |
|
|
|
1468 |
/**
|
|
|
1469 |
* register new gesture
|
|
|
1470 |
* @param {Object} gesture object, see gestures.js for documentation
|
|
|
1471 |
* @returns {Array} gestures
|
|
|
1472 |
*/
|
|
|
1473 |
register: function register(gesture) {
|
|
|
1474 |
// add an enable gesture options if there is no given
|
|
|
1475 |
var options = gesture.defaults || {};
|
|
|
1476 |
if(options[gesture.name] === undefined) {
|
|
|
1477 |
options[gesture.name] = true;
|
|
|
1478 |
}
|
|
|
1479 |
|
|
|
1480 |
// extend ionic.Gestures default options with the ionic.Gestures.gesture options
|
|
|
1481 |
ionic.Gestures.utils.extend(ionic.Gestures.defaults, options, true);
|
|
|
1482 |
|
|
|
1483 |
// set its index
|
|
|
1484 |
gesture.index = gesture.index || 1000;
|
|
|
1485 |
|
|
|
1486 |
// add ionic.Gestures.gesture to the list
|
|
|
1487 |
this.gestures.push(gesture);
|
|
|
1488 |
|
|
|
1489 |
// sort the list by index
|
|
|
1490 |
this.gestures.sort(function(a, b) {
|
|
|
1491 |
if (a.index < b.index) {
|
|
|
1492 |
return -1;
|
|
|
1493 |
}
|
|
|
1494 |
if (a.index > b.index) {
|
|
|
1495 |
return 1;
|
|
|
1496 |
}
|
|
|
1497 |
return 0;
|
|
|
1498 |
});
|
|
|
1499 |
|
|
|
1500 |
return this.gestures;
|
|
|
1501 |
}
|
|
|
1502 |
};
|
|
|
1503 |
|
|
|
1504 |
|
|
|
1505 |
ionic.Gestures.gestures = ionic.Gestures.gestures || {};
|
|
|
1506 |
|
|
|
1507 |
/**
|
|
|
1508 |
* Custom gestures
|
|
|
1509 |
* ==============================
|
|
|
1510 |
*
|
|
|
1511 |
* Gesture object
|
|
|
1512 |
* --------------------
|
|
|
1513 |
* The object structure of a gesture:
|
|
|
1514 |
*
|
|
|
1515 |
* { name: 'mygesture',
|
|
|
1516 |
* index: 1337,
|
|
|
1517 |
* defaults: {
|
|
|
1518 |
* mygesture_option: true
|
|
|
1519 |
* }
|
|
|
1520 |
* handler: function(type, ev, inst) {
|
|
|
1521 |
* // trigger gesture event
|
|
|
1522 |
* inst.trigger(this.name, ev);
|
|
|
1523 |
* }
|
|
|
1524 |
* }
|
|
|
1525 |
|
|
|
1526 |
* @param {String} name
|
|
|
1527 |
* this should be the name of the gesture, lowercase
|
|
|
1528 |
* it is also being used to disable/enable the gesture per instance config.
|
|
|
1529 |
*
|
|
|
1530 |
* @param {Number} [index=1000]
|
|
|
1531 |
* the index of the gesture, where it is going to be in the stack of gestures detection
|
|
|
1532 |
* like when you build an gesture that depends on the drag gesture, it is a good
|
|
|
1533 |
* idea to place it after the index of the drag gesture.
|
|
|
1534 |
*
|
|
|
1535 |
* @param {Object} [defaults={}]
|
|
|
1536 |
* the default settings of the gesture. these are added to the instance settings,
|
|
|
1537 |
* and can be overruled per instance. you can also add the name of the gesture,
|
|
|
1538 |
* but this is also added by default (and set to true).
|
|
|
1539 |
*
|
|
|
1540 |
* @param {Function} handler
|
|
|
1541 |
* this handles the gesture detection of your custom gesture and receives the
|
|
|
1542 |
* following arguments:
|
|
|
1543 |
*
|
|
|
1544 |
* @param {Object} eventData
|
|
|
1545 |
* event data containing the following properties:
|
|
|
1546 |
* timeStamp {Number} time the event occurred
|
|
|
1547 |
* target {HTMLElement} target element
|
|
|
1548 |
* touches {Array} touches (fingers, pointers, mouse) on the screen
|
|
|
1549 |
* pointerType {String} kind of pointer that was used. matches ionic.Gestures.POINTER_MOUSE|TOUCH
|
|
|
1550 |
* center {Object} center position of the touches. contains pageX and pageY
|
|
|
1551 |
* deltaTime {Number} the total time of the touches in the screen
|
|
|
1552 |
* deltaX {Number} the delta on x axis we haved moved
|
|
|
1553 |
* deltaY {Number} the delta on y axis we haved moved
|
|
|
1554 |
* velocityX {Number} the velocity on the x
|
|
|
1555 |
* velocityY {Number} the velocity on y
|
|
|
1556 |
* angle {Number} the angle we are moving
|
|
|
1557 |
* direction {String} the direction we are moving. matches ionic.Gestures.DIRECTION_UP|DOWN|LEFT|RIGHT
|
|
|
1558 |
* distance {Number} the distance we haved moved
|
|
|
1559 |
* scale {Number} scaling of the touches, needs 2 touches
|
|
|
1560 |
* rotation {Number} rotation of the touches, needs 2 touches *
|
|
|
1561 |
* eventType {String} matches ionic.Gestures.EVENT_START|MOVE|END
|
|
|
1562 |
* srcEvent {Object} the source event, like TouchStart or MouseDown *
|
|
|
1563 |
* startEvent {Object} contains the same properties as above,
|
|
|
1564 |
* but from the first touch. this is used to calculate
|
|
|
1565 |
* distances, deltaTime, scaling etc
|
|
|
1566 |
*
|
|
|
1567 |
* @param {ionic.Gestures.Instance} inst
|
|
|
1568 |
* the instance we are doing the detection for. you can get the options from
|
|
|
1569 |
* the inst.options object and trigger the gesture event by calling inst.trigger
|
|
|
1570 |
*
|
|
|
1571 |
*
|
|
|
1572 |
* Handle gestures
|
|
|
1573 |
* --------------------
|
|
|
1574 |
* inside the handler you can get/set ionic.Gestures.detectionic.current. This is the current
|
|
|
1575 |
* detection sessionic. It has the following properties
|
|
|
1576 |
* @param {String} name
|
|
|
1577 |
* contains the name of the gesture we have detected. it has not a real function,
|
|
|
1578 |
* only to check in other gestures if something is detected.
|
|
|
1579 |
* like in the drag gesture we set it to 'drag' and in the swipe gesture we can
|
|
|
1580 |
* check if the current gesture is 'drag' by accessing ionic.Gestures.detectionic.current.name
|
|
|
1581 |
*
|
|
|
1582 |
* readonly
|
|
|
1583 |
* @param {ionic.Gestures.Instance} inst
|
|
|
1584 |
* the instance we do the detection for
|
|
|
1585 |
*
|
|
|
1586 |
* readonly
|
|
|
1587 |
* @param {Object} startEvent
|
|
|
1588 |
* contains the properties of the first gesture detection in this sessionic.
|
|
|
1589 |
* Used for calculations about timing, distance, etc.
|
|
|
1590 |
*
|
|
|
1591 |
* readonly
|
|
|
1592 |
* @param {Object} lastEvent
|
|
|
1593 |
* contains all the properties of the last gesture detect in this sessionic.
|
|
|
1594 |
*
|
|
|
1595 |
* after the gesture detection session has been completed (user has released the screen)
|
|
|
1596 |
* the ionic.Gestures.detectionic.current object is copied into ionic.Gestures.detectionic.previous,
|
|
|
1597 |
* this is usefull for gestures like doubletap, where you need to know if the
|
|
|
1598 |
* previous gesture was a tap
|
|
|
1599 |
*
|
|
|
1600 |
* options that have been set by the instance can be received by calling inst.options
|
|
|
1601 |
*
|
|
|
1602 |
* You can trigger a gesture event by calling inst.trigger("mygesture", event).
|
|
|
1603 |
* The first param is the name of your gesture, the second the event argument
|
|
|
1604 |
*
|
|
|
1605 |
*
|
|
|
1606 |
* Register gestures
|
|
|
1607 |
* --------------------
|
|
|
1608 |
* When an gesture is added to the ionic.Gestures.gestures object, it is auto registered
|
|
|
1609 |
* at the setup of the first ionic.Gestures instance. You can also call ionic.Gestures.detectionic.register
|
|
|
1610 |
* manually and pass your gesture object as a param
|
|
|
1611 |
*
|
|
|
1612 |
*/
|
|
|
1613 |
|
|
|
1614 |
/**
|
|
|
1615 |
* Hold
|
|
|
1616 |
* Touch stays at the same place for x time
|
|
|
1617 |
* events hold
|
|
|
1618 |
*/
|
|
|
1619 |
ionic.Gestures.gestures.Hold = {
|
|
|
1620 |
name: 'hold',
|
|
|
1621 |
index: 10,
|
|
|
1622 |
defaults: {
|
|
|
1623 |
hold_timeout: 500,
|
|
|
1624 |
hold_threshold: 1
|
|
|
1625 |
},
|
|
|
1626 |
timer: null,
|
|
|
1627 |
handler: function holdGesture(ev, inst) {
|
|
|
1628 |
switch(ev.eventType) {
|
|
|
1629 |
case ionic.Gestures.EVENT_START:
|
|
|
1630 |
// clear any running timers
|
|
|
1631 |
clearTimeout(this.timer);
|
|
|
1632 |
|
|
|
1633 |
// set the gesture so we can check in the timeout if it still is
|
|
|
1634 |
ionic.Gestures.detection.current.name = this.name;
|
|
|
1635 |
|
|
|
1636 |
// set timer and if after the timeout it still is hold,
|
|
|
1637 |
// we trigger the hold event
|
|
|
1638 |
this.timer = setTimeout(function() {
|
|
|
1639 |
if(ionic.Gestures.detection.current.name == 'hold') {
|
|
|
1640 |
ionic.tap.cancelClick();
|
|
|
1641 |
inst.trigger('hold', ev);
|
|
|
1642 |
}
|
|
|
1643 |
}, inst.options.hold_timeout);
|
|
|
1644 |
break;
|
|
|
1645 |
|
|
|
1646 |
// when you move or end we clear the timer
|
|
|
1647 |
case ionic.Gestures.EVENT_MOVE:
|
|
|
1648 |
if(ev.distance > inst.options.hold_threshold) {
|
|
|
1649 |
clearTimeout(this.timer);
|
|
|
1650 |
}
|
|
|
1651 |
break;
|
|
|
1652 |
|
|
|
1653 |
case ionic.Gestures.EVENT_END:
|
|
|
1654 |
clearTimeout(this.timer);
|
|
|
1655 |
break;
|
|
|
1656 |
}
|
|
|
1657 |
}
|
|
|
1658 |
};
|
|
|
1659 |
|
|
|
1660 |
|
|
|
1661 |
/**
|
|
|
1662 |
* Tap/DoubleTap
|
|
|
1663 |
* Quick touch at a place or double at the same place
|
|
|
1664 |
* events tap, doubletap
|
|
|
1665 |
*/
|
|
|
1666 |
ionic.Gestures.gestures.Tap = {
|
|
|
1667 |
name: 'tap',
|
|
|
1668 |
index: 100,
|
|
|
1669 |
defaults: {
|
|
|
1670 |
tap_max_touchtime: 250,
|
|
|
1671 |
tap_max_distance: 10,
|
|
|
1672 |
tap_always: true,
|
|
|
1673 |
doubletap_distance: 20,
|
|
|
1674 |
doubletap_interval: 300
|
|
|
1675 |
},
|
|
|
1676 |
handler: function tapGesture(ev, inst) {
|
|
|
1677 |
if(ev.eventType == ionic.Gestures.EVENT_END && ev.srcEvent.type != 'touchcancel') {
|
|
|
1678 |
// previous gesture, for the double tap since these are two different gesture detections
|
|
|
1679 |
var prev = ionic.Gestures.detection.previous,
|
|
|
1680 |
did_doubletap = false;
|
|
|
1681 |
|
|
|
1682 |
// when the touchtime is higher then the max touch time
|
|
|
1683 |
// or when the moving distance is too much
|
|
|
1684 |
if(ev.deltaTime > inst.options.tap_max_touchtime ||
|
|
|
1685 |
ev.distance > inst.options.tap_max_distance) {
|
|
|
1686 |
return;
|
|
|
1687 |
}
|
|
|
1688 |
|
|
|
1689 |
// check if double tap
|
|
|
1690 |
if(prev && prev.name == 'tap' &&
|
|
|
1691 |
(ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
|
|
|
1692 |
ev.distance < inst.options.doubletap_distance) {
|
|
|
1693 |
inst.trigger('doubletap', ev);
|
|
|
1694 |
did_doubletap = true;
|
|
|
1695 |
}
|
|
|
1696 |
|
|
|
1697 |
// do a single tap
|
|
|
1698 |
if(!did_doubletap || inst.options.tap_always) {
|
|
|
1699 |
ionic.Gestures.detection.current.name = 'tap';
|
|
|
1700 |
inst.trigger('tap', ev);
|
|
|
1701 |
}
|
|
|
1702 |
}
|
|
|
1703 |
}
|
|
|
1704 |
};
|
|
|
1705 |
|
|
|
1706 |
|
|
|
1707 |
/**
|
|
|
1708 |
* Swipe
|
|
|
1709 |
* triggers swipe events when the end velocity is above the threshold
|
|
|
1710 |
* events swipe, swipeleft, swiperight, swipeup, swipedown
|
|
|
1711 |
*/
|
|
|
1712 |
ionic.Gestures.gestures.Swipe = {
|
|
|
1713 |
name: 'swipe',
|
|
|
1714 |
index: 40,
|
|
|
1715 |
defaults: {
|
|
|
1716 |
// set 0 for unlimited, but this can conflict with transform
|
|
|
1717 |
swipe_max_touches: 1,
|
|
|
1718 |
swipe_velocity: 0.4
|
|
|
1719 |
},
|
|
|
1720 |
handler: function swipeGesture(ev, inst) {
|
|
|
1721 |
if(ev.eventType == ionic.Gestures.EVENT_END) {
|
|
|
1722 |
// max touches
|
|
|
1723 |
if(inst.options.swipe_max_touches > 0 &&
|
|
|
1724 |
ev.touches.length > inst.options.swipe_max_touches) {
|
|
|
1725 |
return;
|
|
|
1726 |
}
|
|
|
1727 |
|
|
|
1728 |
// when the distance we moved is too small we skip this gesture
|
|
|
1729 |
// or we can be already in dragging
|
|
|
1730 |
if(ev.velocityX > inst.options.swipe_velocity ||
|
|
|
1731 |
ev.velocityY > inst.options.swipe_velocity) {
|
|
|
1732 |
// trigger swipe events
|
|
|
1733 |
inst.trigger(this.name, ev);
|
|
|
1734 |
inst.trigger(this.name + ev.direction, ev);
|
|
|
1735 |
}
|
|
|
1736 |
}
|
|
|
1737 |
}
|
|
|
1738 |
};
|
|
|
1739 |
|
|
|
1740 |
|
|
|
1741 |
/**
|
|
|
1742 |
* Drag
|
|
|
1743 |
* Move with x fingers (default 1) around on the page. Blocking the scrolling when
|
|
|
1744 |
* moving left and right is a good practice. When all the drag events are blocking
|
|
|
1745 |
* you disable scrolling on that area.
|
|
|
1746 |
* events drag, drapleft, dragright, dragup, dragdown
|
|
|
1747 |
*/
|
|
|
1748 |
ionic.Gestures.gestures.Drag = {
|
|
|
1749 |
name: 'drag',
|
|
|
1750 |
index: 50,
|
|
|
1751 |
defaults: {
|
|
|
1752 |
drag_min_distance: 10,
|
|
|
1753 |
// Set correct_for_drag_min_distance to true to make the starting point of the drag
|
|
|
1754 |
// be calculated from where the drag was triggered, not from where the touch started.
|
|
|
1755 |
// Useful to avoid a jerk-starting drag, which can make fine-adjustments
|
|
|
1756 |
// through dragging difficult, and be visually unappealing.
|
|
|
1757 |
correct_for_drag_min_distance: true,
|
|
|
1758 |
// set 0 for unlimited, but this can conflict with transform
|
|
|
1759 |
drag_max_touches: 1,
|
|
|
1760 |
// prevent default browser behavior when dragging occurs
|
|
|
1761 |
// be careful with it, it makes the element a blocking element
|
|
|
1762 |
// when you are using the drag gesture, it is a good practice to set this true
|
|
|
1763 |
drag_block_horizontal: true,
|
|
|
1764 |
drag_block_vertical: true,
|
|
|
1765 |
// drag_lock_to_axis keeps the drag gesture on the axis that it started on,
|
|
|
1766 |
// It disallows vertical directions if the initial direction was horizontal, and vice versa.
|
|
|
1767 |
drag_lock_to_axis: false,
|
|
|
1768 |
// drag lock only kicks in when distance > drag_lock_min_distance
|
|
|
1769 |
// This way, locking occurs only when the distance has become large enough to reliably determine the direction
|
|
|
1770 |
drag_lock_min_distance: 25,
|
|
|
1771 |
// prevent default if the gesture is going the given direction
|
|
|
1772 |
prevent_default_directions: []
|
|
|
1773 |
},
|
|
|
1774 |
triggered: false,
|
|
|
1775 |
handler: function dragGesture(ev, inst) {
|
|
|
1776 |
if (ev.srcEvent.type == 'touchstart' || ev.srcEvent.type == 'touchend') {
|
|
|
1777 |
this.preventedFirstMove = false;
|
|
|
1778 |
|
|
|
1779 |
} else if (!this.preventedFirstMove && ev.srcEvent.type == 'touchmove') {
|
|
|
1780 |
if (inst.options.prevent_default_directions.indexOf(ev.direction) != -1) {
|
|
|
1781 |
ev.srcEvent.preventDefault();
|
|
|
1782 |
}
|
|
|
1783 |
this.preventedFirstMove = true;
|
|
|
1784 |
}
|
|
|
1785 |
|
|
|
1786 |
// current gesture isnt drag, but dragged is true
|
|
|
1787 |
// this means an other gesture is busy. now call dragend
|
|
|
1788 |
if(ionic.Gestures.detection.current.name != this.name && this.triggered) {
|
|
|
1789 |
inst.trigger(this.name + 'end', ev);
|
|
|
1790 |
this.triggered = false;
|
|
|
1791 |
return;
|
|
|
1792 |
}
|
|
|
1793 |
|
|
|
1794 |
// max touches
|
|
|
1795 |
if(inst.options.drag_max_touches > 0 &&
|
|
|
1796 |
ev.touches.length > inst.options.drag_max_touches) {
|
|
|
1797 |
return;
|
|
|
1798 |
}
|
|
|
1799 |
|
|
|
1800 |
switch(ev.eventType) {
|
|
|
1801 |
case ionic.Gestures.EVENT_START:
|
|
|
1802 |
this.triggered = false;
|
|
|
1803 |
break;
|
|
|
1804 |
|
|
|
1805 |
case ionic.Gestures.EVENT_MOVE:
|
|
|
1806 |
// when the distance we moved is too small we skip this gesture
|
|
|
1807 |
// or we can be already in dragging
|
|
|
1808 |
if(ev.distance < inst.options.drag_min_distance &&
|
|
|
1809 |
ionic.Gestures.detection.current.name != this.name) {
|
|
|
1810 |
return;
|
|
|
1811 |
}
|
|
|
1812 |
|
|
|
1813 |
// we are dragging!
|
|
|
1814 |
if(ionic.Gestures.detection.current.name != this.name) {
|
|
|
1815 |
ionic.Gestures.detection.current.name = this.name;
|
|
|
1816 |
if (inst.options.correct_for_drag_min_distance) {
|
|
|
1817 |
// When a drag is triggered, set the event center to drag_min_distance pixels from the original event center.
|
|
|
1818 |
// Without this correction, the dragged distance would jumpstart at drag_min_distance pixels instead of at 0.
|
|
|
1819 |
// It might be useful to save the original start point somewhere
|
|
|
1820 |
var factor = Math.abs(inst.options.drag_min_distance / ev.distance);
|
|
|
1821 |
ionic.Gestures.detection.current.startEvent.center.pageX += ev.deltaX * factor;
|
|
|
1822 |
ionic.Gestures.detection.current.startEvent.center.pageY += ev.deltaY * factor;
|
|
|
1823 |
|
|
|
1824 |
// recalculate event data using new start point
|
|
|
1825 |
ev = ionic.Gestures.detection.extendEventData(ev);
|
|
|
1826 |
}
|
|
|
1827 |
}
|
|
|
1828 |
|
|
|
1829 |
// lock drag to axis?
|
|
|
1830 |
if(ionic.Gestures.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance <= ev.distance)) {
|
|
|
1831 |
ev.drag_locked_to_axis = true;
|
|
|
1832 |
}
|
|
|
1833 |
var last_direction = ionic.Gestures.detection.current.lastEvent.direction;
|
|
|
1834 |
if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
|
|
|
1835 |
// keep direction on the axis that the drag gesture started on
|
|
|
1836 |
if(ionic.Gestures.utils.isVertical(last_direction)) {
|
|
|
1837 |
ev.direction = (ev.deltaY < 0) ? ionic.Gestures.DIRECTION_UP : ionic.Gestures.DIRECTION_DOWN;
|
|
|
1838 |
}
|
|
|
1839 |
else {
|
|
|
1840 |
ev.direction = (ev.deltaX < 0) ? ionic.Gestures.DIRECTION_LEFT : ionic.Gestures.DIRECTION_RIGHT;
|
|
|
1841 |
}
|
|
|
1842 |
}
|
|
|
1843 |
|
|
|
1844 |
// first time, trigger dragstart event
|
|
|
1845 |
if(!this.triggered) {
|
|
|
1846 |
inst.trigger(this.name + 'start', ev);
|
|
|
1847 |
this.triggered = true;
|
|
|
1848 |
}
|
|
|
1849 |
|
|
|
1850 |
// trigger normal event
|
|
|
1851 |
inst.trigger(this.name, ev);
|
|
|
1852 |
|
|
|
1853 |
// direction event, like dragdown
|
|
|
1854 |
inst.trigger(this.name + ev.direction, ev);
|
|
|
1855 |
|
|
|
1856 |
// block the browser events
|
|
|
1857 |
if( (inst.options.drag_block_vertical && ionic.Gestures.utils.isVertical(ev.direction)) ||
|
|
|
1858 |
(inst.options.drag_block_horizontal && !ionic.Gestures.utils.isVertical(ev.direction))) {
|
|
|
1859 |
ev.preventDefault();
|
|
|
1860 |
}
|
|
|
1861 |
break;
|
|
|
1862 |
|
|
|
1863 |
case ionic.Gestures.EVENT_END:
|
|
|
1864 |
// trigger dragend
|
|
|
1865 |
if(this.triggered) {
|
|
|
1866 |
inst.trigger(this.name + 'end', ev);
|
|
|
1867 |
}
|
|
|
1868 |
|
|
|
1869 |
this.triggered = false;
|
|
|
1870 |
break;
|
|
|
1871 |
}
|
|
|
1872 |
}
|
|
|
1873 |
};
|
|
|
1874 |
|
|
|
1875 |
|
|
|
1876 |
/**
|
|
|
1877 |
* Transform
|
|
|
1878 |
* User want to scale or rotate with 2 fingers
|
|
|
1879 |
* events transform, pinch, pinchin, pinchout, rotate
|
|
|
1880 |
*/
|
|
|
1881 |
ionic.Gestures.gestures.Transform = {
|
|
|
1882 |
name: 'transform',
|
|
|
1883 |
index: 45,
|
|
|
1884 |
defaults: {
|
|
|
1885 |
// factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
|
|
|
1886 |
transform_min_scale: 0.01,
|
|
|
1887 |
// rotation in degrees
|
|
|
1888 |
transform_min_rotation: 1,
|
|
|
1889 |
// prevent default browser behavior when two touches are on the screen
|
|
|
1890 |
// but it makes the element a blocking element
|
|
|
1891 |
// when you are using the transform gesture, it is a good practice to set this true
|
|
|
1892 |
transform_always_block: false
|
|
|
1893 |
},
|
|
|
1894 |
triggered: false,
|
|
|
1895 |
handler: function transformGesture(ev, inst) {
|
|
|
1896 |
// current gesture isnt drag, but dragged is true
|
|
|
1897 |
// this means an other gesture is busy. now call dragend
|
|
|
1898 |
if(ionic.Gestures.detection.current.name != this.name && this.triggered) {
|
|
|
1899 |
inst.trigger(this.name + 'end', ev);
|
|
|
1900 |
this.triggered = false;
|
|
|
1901 |
return;
|
|
|
1902 |
}
|
|
|
1903 |
|
|
|
1904 |
// atleast multitouch
|
|
|
1905 |
if(ev.touches.length < 2) {
|
|
|
1906 |
return;
|
|
|
1907 |
}
|
|
|
1908 |
|
|
|
1909 |
// prevent default when two fingers are on the screen
|
|
|
1910 |
if(inst.options.transform_always_block) {
|
|
|
1911 |
ev.preventDefault();
|
|
|
1912 |
}
|
|
|
1913 |
|
|
|
1914 |
switch(ev.eventType) {
|
|
|
1915 |
case ionic.Gestures.EVENT_START:
|
|
|
1916 |
this.triggered = false;
|
|
|
1917 |
break;
|
|
|
1918 |
|
|
|
1919 |
case ionic.Gestures.EVENT_MOVE:
|
|
|
1920 |
var scale_threshold = Math.abs(1 - ev.scale);
|
|
|
1921 |
var rotation_threshold = Math.abs(ev.rotation);
|
|
|
1922 |
|
|
|
1923 |
// when the distance we moved is too small we skip this gesture
|
|
|
1924 |
// or we can be already in dragging
|
|
|
1925 |
if(scale_threshold < inst.options.transform_min_scale &&
|
|
|
1926 |
rotation_threshold < inst.options.transform_min_rotation) {
|
|
|
1927 |
return;
|
|
|
1928 |
}
|
|
|
1929 |
|
|
|
1930 |
// we are transforming!
|
|
|
1931 |
ionic.Gestures.detection.current.name = this.name;
|
|
|
1932 |
|
|
|
1933 |
// first time, trigger dragstart event
|
|
|
1934 |
if(!this.triggered) {
|
|
|
1935 |
inst.trigger(this.name + 'start', ev);
|
|
|
1936 |
this.triggered = true;
|
|
|
1937 |
}
|
|
|
1938 |
|
|
|
1939 |
inst.trigger(this.name, ev); // basic transform event
|
|
|
1940 |
|
|
|
1941 |
// trigger rotate event
|
|
|
1942 |
if(rotation_threshold > inst.options.transform_min_rotation) {
|
|
|
1943 |
inst.trigger('rotate', ev);
|
|
|
1944 |
}
|
|
|
1945 |
|
|
|
1946 |
// trigger pinch event
|
|
|
1947 |
if(scale_threshold > inst.options.transform_min_scale) {
|
|
|
1948 |
inst.trigger('pinch', ev);
|
|
|
1949 |
inst.trigger('pinch' + ((ev.scale < 1) ? 'in' : 'out'), ev);
|
|
|
1950 |
}
|
|
|
1951 |
break;
|
|
|
1952 |
|
|
|
1953 |
case ionic.Gestures.EVENT_END:
|
|
|
1954 |
// trigger dragend
|
|
|
1955 |
if(this.triggered) {
|
|
|
1956 |
inst.trigger(this.name + 'end', ev);
|
|
|
1957 |
}
|
|
|
1958 |
|
|
|
1959 |
this.triggered = false;
|
|
|
1960 |
break;
|
|
|
1961 |
}
|
|
|
1962 |
}
|
|
|
1963 |
};
|
|
|
1964 |
|
|
|
1965 |
|
|
|
1966 |
/**
|
|
|
1967 |
* Touch
|
|
|
1968 |
* Called as first, tells the user has touched the screen
|
|
|
1969 |
* events touch
|
|
|
1970 |
*/
|
|
|
1971 |
ionic.Gestures.gestures.Touch = {
|
|
|
1972 |
name: 'touch',
|
|
|
1973 |
index: -Infinity,
|
|
|
1974 |
defaults: {
|
|
|
1975 |
// call preventDefault at touchstart, and makes the element blocking by
|
|
|
1976 |
// disabling the scrolling of the page, but it improves gestures like
|
|
|
1977 |
// transforming and dragging.
|
|
|
1978 |
// be careful with using this, it can be very annoying for users to be stuck
|
|
|
1979 |
// on the page
|
|
|
1980 |
prevent_default: false,
|
|
|
1981 |
|
|
|
1982 |
// disable mouse events, so only touch (or pen!) input triggers events
|
|
|
1983 |
prevent_mouseevents: false
|
|
|
1984 |
},
|
|
|
1985 |
handler: function touchGesture(ev, inst) {
|
|
|
1986 |
if(inst.options.prevent_mouseevents && ev.pointerType == ionic.Gestures.POINTER_MOUSE) {
|
|
|
1987 |
ev.stopDetect();
|
|
|
1988 |
return;
|
|
|
1989 |
}
|
|
|
1990 |
|
|
|
1991 |
if(inst.options.prevent_default) {
|
|
|
1992 |
ev.preventDefault();
|
|
|
1993 |
}
|
|
|
1994 |
|
|
|
1995 |
if(ev.eventType == ionic.Gestures.EVENT_START) {
|
|
|
1996 |
inst.trigger(this.name, ev);
|
|
|
1997 |
}
|
|
|
1998 |
}
|
|
|
1999 |
};
|
|
|
2000 |
|
|
|
2001 |
|
|
|
2002 |
/**
|
|
|
2003 |
* Release
|
|
|
2004 |
* Called as last, tells the user has released the screen
|
|
|
2005 |
* events release
|
|
|
2006 |
*/
|
|
|
2007 |
ionic.Gestures.gestures.Release = {
|
|
|
2008 |
name: 'release',
|
|
|
2009 |
index: Infinity,
|
|
|
2010 |
handler: function releaseGesture(ev, inst) {
|
|
|
2011 |
if(ev.eventType == ionic.Gestures.EVENT_END) {
|
|
|
2012 |
inst.trigger(this.name, ev);
|
|
|
2013 |
}
|
|
|
2014 |
}
|
|
|
2015 |
};
|
|
|
2016 |
})(window.ionic);
|
|
|
2017 |
|
|
|
2018 |
(function(window, document, ionic) {
|
|
|
2019 |
|
|
|
2020 |
function getParameterByName(name) {
|
|
|
2021 |
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
|
|
|
2022 |
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
|
|
|
2023 |
results = regex.exec(location.search);
|
|
|
2024 |
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
|
|
|
2025 |
}
|
|
|
2026 |
|
|
|
2027 |
var IOS = 'ios';
|
|
|
2028 |
var ANDROID = 'android';
|
|
|
2029 |
var WINDOWS_PHONE = 'windowsphone';
|
|
|
2030 |
var requestAnimationFrame = ionic.requestAnimationFrame;
|
|
|
2031 |
|
|
|
2032 |
/**
|
|
|
2033 |
* @ngdoc utility
|
|
|
2034 |
* @name ionic.Platform
|
|
|
2035 |
* @module ionic
|
|
|
2036 |
* @description
|
|
|
2037 |
* A set of utility methods that can be used to retrieve the device ready state and
|
|
|
2038 |
* various other information such as what kind of platform the app is currently installed on.
|
|
|
2039 |
*
|
|
|
2040 |
* @usage
|
|
|
2041 |
* ```js
|
|
|
2042 |
* angular.module('PlatformApp', ['ionic'])
|
|
|
2043 |
* .controller('PlatformCtrl', function($scope) {
|
|
|
2044 |
*
|
|
|
2045 |
* ionic.Platform.ready(function(){
|
|
|
2046 |
* // will execute when device is ready, or immediately if the device is already ready.
|
|
|
2047 |
* });
|
|
|
2048 |
*
|
|
|
2049 |
* var deviceInformation = ionic.Platform.device();
|
|
|
2050 |
*
|
|
|
2051 |
* var isWebView = ionic.Platform.isWebView();
|
|
|
2052 |
* var isIPad = ionic.Platform.isIPad();
|
|
|
2053 |
* var isIOS = ionic.Platform.isIOS();
|
|
|
2054 |
* var isAndroid = ionic.Platform.isAndroid();
|
|
|
2055 |
* var isWindowsPhone = ionic.Platform.isWindowsPhone();
|
|
|
2056 |
*
|
|
|
2057 |
* var currentPlatform = ionic.Platform.platform();
|
|
|
2058 |
* var currentPlatformVersion = ionic.Platform.version();
|
|
|
2059 |
*
|
|
|
2060 |
* ionic.Platform.exitApp(); // stops the app
|
|
|
2061 |
* });
|
|
|
2062 |
* ```
|
|
|
2063 |
*/
|
|
|
2064 |
var self = ionic.Platform = {
|
|
|
2065 |
|
|
|
2066 |
// Put navigator on platform so it can be mocked and set
|
|
|
2067 |
// the browser does not allow window.navigator to be set
|
|
|
2068 |
navigator: window.navigator,
|
|
|
2069 |
|
|
|
2070 |
/**
|
|
|
2071 |
* @ngdoc property
|
|
|
2072 |
* @name ionic.Platform#isReady
|
|
|
2073 |
* @returns {boolean} Whether the device is ready.
|
|
|
2074 |
*/
|
|
|
2075 |
isReady: false,
|
|
|
2076 |
/**
|
|
|
2077 |
* @ngdoc property
|
|
|
2078 |
* @name ionic.Platform#isFullScreen
|
|
|
2079 |
* @returns {boolean} Whether the device is fullscreen.
|
|
|
2080 |
*/
|
|
|
2081 |
isFullScreen: false,
|
|
|
2082 |
/**
|
|
|
2083 |
* @ngdoc property
|
|
|
2084 |
* @name ionic.Platform#platforms
|
|
|
2085 |
* @returns {Array(string)} An array of all platforms found.
|
|
|
2086 |
*/
|
|
|
2087 |
platforms: null,
|
|
|
2088 |
/**
|
|
|
2089 |
* @ngdoc property
|
|
|
2090 |
* @name ionic.Platform#grade
|
|
|
2091 |
* @returns {string} What grade the current platform is.
|
|
|
2092 |
*/
|
|
|
2093 |
grade: null,
|
|
|
2094 |
ua: navigator.userAgent,
|
|
|
2095 |
|
|
|
2096 |
/**
|
|
|
2097 |
* @ngdoc method
|
|
|
2098 |
* @name ionic.Platform#ready
|
|
|
2099 |
* @description
|
|
|
2100 |
* Trigger a callback once the device is ready, or immediately
|
|
|
2101 |
* if the device is already ready. This method can be run from
|
|
|
2102 |
* anywhere and does not need to be wrapped by any additonal methods.
|
|
|
2103 |
* When the app is within a WebView (Cordova), it'll fire
|
|
|
2104 |
* the callback once the device is ready. If the app is within
|
|
|
2105 |
* a web browser, it'll fire the callback after `window.load`.
|
|
|
2106 |
* Please remember that Cordova features (Camera, FileSystem, etc) still
|
|
|
2107 |
* will not work in a web browser.
|
|
|
2108 |
* @param {function} callback The function to call.
|
|
|
2109 |
*/
|
|
|
2110 |
ready: function(cb) {
|
|
|
2111 |
// run through tasks to complete now that the device is ready
|
|
|
2112 |
if (self.isReady) {
|
|
|
2113 |
cb();
|
|
|
2114 |
} else {
|
|
|
2115 |
// the platform isn't ready yet, add it to this array
|
|
|
2116 |
// which will be called once the platform is ready
|
|
|
2117 |
readyCallbacks.push(cb);
|
|
|
2118 |
}
|
|
|
2119 |
},
|
|
|
2120 |
|
|
|
2121 |
/**
|
|
|
2122 |
* @private
|
|
|
2123 |
*/
|
|
|
2124 |
detect: function() {
|
|
|
2125 |
self._checkPlatforms();
|
|
|
2126 |
|
|
|
2127 |
requestAnimationFrame(function() {
|
|
|
2128 |
// only add to the body class if we got platform info
|
|
|
2129 |
for (var i = 0; i < self.platforms.length; i++) {
|
|
|
2130 |
document.body.classList.add('platform-' + self.platforms[i]);
|
|
|
2131 |
}
|
|
|
2132 |
});
|
|
|
2133 |
},
|
|
|
2134 |
|
|
|
2135 |
/**
|
|
|
2136 |
* @ngdoc method
|
|
|
2137 |
* @name ionic.Platform#setGrade
|
|
|
2138 |
* @description Set the grade of the device: 'a', 'b', or 'c'. 'a' is the best
|
|
|
2139 |
* (most css features enabled), 'c' is the worst. By default, sets the grade
|
|
|
2140 |
* depending on the current device.
|
|
|
2141 |
* @param {string} grade The new grade to set.
|
|
|
2142 |
*/
|
|
|
2143 |
setGrade: function(grade) {
|
|
|
2144 |
var oldGrade = self.grade;
|
|
|
2145 |
self.grade = grade;
|
|
|
2146 |
requestAnimationFrame(function() {
|
|
|
2147 |
if (oldGrade) {
|
|
|
2148 |
document.body.classList.remove('grade-' + oldGrade);
|
|
|
2149 |
}
|
|
|
2150 |
document.body.classList.add('grade-' + grade);
|
|
|
2151 |
});
|
|
|
2152 |
},
|
|
|
2153 |
|
|
|
2154 |
/**
|
|
|
2155 |
* @ngdoc method
|
|
|
2156 |
* @name ionic.Platform#device
|
|
|
2157 |
* @description Return the current device (given by cordova).
|
|
|
2158 |
* @returns {object} The device object.
|
|
|
2159 |
*/
|
|
|
2160 |
device: function() {
|
|
|
2161 |
return window.device || {};
|
|
|
2162 |
},
|
|
|
2163 |
|
|
|
2164 |
_checkPlatforms: function() {
|
|
|
2165 |
self.platforms = [];
|
|
|
2166 |
var grade = 'a';
|
|
|
2167 |
|
|
|
2168 |
if (self.isWebView()) {
|
|
|
2169 |
self.platforms.push('webview');
|
|
|
2170 |
if (!(!window.cordova && !window.PhoneGap && !window.phonegap)) {
|
|
|
2171 |
self.platforms.push('cordova');
|
|
|
2172 |
} else if (window.forge) {
|
|
|
2173 |
self.platforms.push('trigger');
|
|
|
2174 |
}
|
|
|
2175 |
} else {
|
|
|
2176 |
self.platforms.push('browser');
|
|
|
2177 |
}
|
|
|
2178 |
if (self.isIPad()) self.platforms.push('ipad');
|
|
|
2179 |
|
|
|
2180 |
var platform = self.platform();
|
|
|
2181 |
if (platform) {
|
|
|
2182 |
self.platforms.push(platform);
|
|
|
2183 |
|
|
|
2184 |
var version = self.version();
|
|
|
2185 |
if (version) {
|
|
|
2186 |
var v = version.toString();
|
|
|
2187 |
if (v.indexOf('.') > 0) {
|
|
|
2188 |
v = v.replace('.', '_');
|
|
|
2189 |
} else {
|
|
|
2190 |
v += '_0';
|
|
|
2191 |
}
|
|
|
2192 |
self.platforms.push(platform + v.split('_')[0]);
|
|
|
2193 |
self.platforms.push(platform + v);
|
|
|
2194 |
|
|
|
2195 |
if (self.isAndroid() && version < 4.4) {
|
|
|
2196 |
grade = (version < 4 ? 'c' : 'b');
|
|
|
2197 |
} else if (self.isWindowsPhone()) {
|
|
|
2198 |
grade = 'b';
|
|
|
2199 |
}
|
|
|
2200 |
}
|
|
|
2201 |
}
|
|
|
2202 |
|
|
|
2203 |
self.setGrade(grade);
|
|
|
2204 |
},
|
|
|
2205 |
|
|
|
2206 |
/**
|
|
|
2207 |
* @ngdoc method
|
|
|
2208 |
* @name ionic.Platform#isWebView
|
|
|
2209 |
* @returns {boolean} Check if we are running within a WebView (such as Cordova).
|
|
|
2210 |
*/
|
|
|
2211 |
isWebView: function() {
|
|
|
2212 |
return !(!window.cordova && !window.PhoneGap && !window.phonegap && !window.forge);
|
|
|
2213 |
},
|
|
|
2214 |
/**
|
|
|
2215 |
* @ngdoc method
|
|
|
2216 |
* @name ionic.Platform#isIPad
|
|
|
2217 |
* @returns {boolean} Whether we are running on iPad.
|
|
|
2218 |
*/
|
|
|
2219 |
isIPad: function() {
|
|
|
2220 |
if (/iPad/i.test(self.navigator.platform)) {
|
|
|
2221 |
return true;
|
|
|
2222 |
}
|
|
|
2223 |
return /iPad/i.test(self.ua);
|
|
|
2224 |
},
|
|
|
2225 |
/**
|
|
|
2226 |
* @ngdoc method
|
|
|
2227 |
* @name ionic.Platform#isIOS
|
|
|
2228 |
* @returns {boolean} Whether we are running on iOS.
|
|
|
2229 |
*/
|
|
|
2230 |
isIOS: function() {
|
|
|
2231 |
return self.is(IOS);
|
|
|
2232 |
},
|
|
|
2233 |
/**
|
|
|
2234 |
* @ngdoc method
|
|
|
2235 |
* @name ionic.Platform#isAndroid
|
|
|
2236 |
* @returns {boolean} Whether we are running on Android.
|
|
|
2237 |
*/
|
|
|
2238 |
isAndroid: function() {
|
|
|
2239 |
return self.is(ANDROID);
|
|
|
2240 |
},
|
|
|
2241 |
/**
|
|
|
2242 |
* @ngdoc method
|
|
|
2243 |
* @name ionic.Platform#isWindowsPhone
|
|
|
2244 |
* @returns {boolean} Whether we are running on Windows Phone.
|
|
|
2245 |
*/
|
|
|
2246 |
isWindowsPhone: function() {
|
|
|
2247 |
return self.is(WINDOWS_PHONE);
|
|
|
2248 |
},
|
|
|
2249 |
|
|
|
2250 |
/**
|
|
|
2251 |
* @ngdoc method
|
|
|
2252 |
* @name ionic.Platform#platform
|
|
|
2253 |
* @returns {string} The name of the current platform.
|
|
|
2254 |
*/
|
|
|
2255 |
platform: function() {
|
|
|
2256 |
// singleton to get the platform name
|
|
|
2257 |
if (platformName === null) self.setPlatform(self.device().platform);
|
|
|
2258 |
return platformName;
|
|
|
2259 |
},
|
|
|
2260 |
|
|
|
2261 |
/**
|
|
|
2262 |
* @private
|
|
|
2263 |
*/
|
|
|
2264 |
setPlatform: function(n) {
|
|
|
2265 |
if (typeof n != 'undefined' && n !== null && n.length) {
|
|
|
2266 |
platformName = n.toLowerCase();
|
|
|
2267 |
} else if (getParameterByName('ionicplatform')) {
|
|
|
2268 |
platformName = getParameterByName('ionicplatform');
|
|
|
2269 |
} else if (self.ua.indexOf('Android') > 0) {
|
|
|
2270 |
platformName = ANDROID;
|
|
|
2271 |
} else if (/iPhone|iPad|iPod/.test(self.ua)) {
|
|
|
2272 |
platformName = IOS;
|
|
|
2273 |
} else if (self.ua.indexOf('Windows Phone') > -1) {
|
|
|
2274 |
platformName = WINDOWS_PHONE;
|
|
|
2275 |
} else {
|
|
|
2276 |
platformName = self.navigator.platform && navigator.platform.toLowerCase().split(' ')[0] || '';
|
|
|
2277 |
}
|
|
|
2278 |
},
|
|
|
2279 |
|
|
|
2280 |
/**
|
|
|
2281 |
* @ngdoc method
|
|
|
2282 |
* @name ionic.Platform#version
|
|
|
2283 |
* @returns {number} The version of the current device platform.
|
|
|
2284 |
*/
|
|
|
2285 |
version: function() {
|
|
|
2286 |
// singleton to get the platform version
|
|
|
2287 |
if (platformVersion === null) self.setVersion(self.device().version);
|
|
|
2288 |
return platformVersion;
|
|
|
2289 |
},
|
|
|
2290 |
|
|
|
2291 |
/**
|
|
|
2292 |
* @private
|
|
|
2293 |
*/
|
|
|
2294 |
setVersion: function(v) {
|
|
|
2295 |
if (typeof v != 'undefined' && v !== null) {
|
|
|
2296 |
v = v.split('.');
|
|
|
2297 |
v = parseFloat(v[0] + '.' + (v.length > 1 ? v[1] : 0));
|
|
|
2298 |
if (!isNaN(v)) {
|
|
|
2299 |
platformVersion = v;
|
|
|
2300 |
return;
|
|
|
2301 |
}
|
|
|
2302 |
}
|
|
|
2303 |
|
|
|
2304 |
platformVersion = 0;
|
|
|
2305 |
|
|
|
2306 |
// fallback to user-agent checking
|
|
|
2307 |
var pName = self.platform();
|
|
|
2308 |
var versionMatch = {
|
|
|
2309 |
'android': /Android (\d+).(\d+)?/,
|
|
|
2310 |
'ios': /OS (\d+)_(\d+)?/,
|
|
|
2311 |
'windowsphone': /Windows Phone (\d+).(\d+)?/
|
|
|
2312 |
};
|
|
|
2313 |
if (versionMatch[pName]) {
|
|
|
2314 |
v = self.ua.match(versionMatch[pName]);
|
|
|
2315 |
if (v && v.length > 2) {
|
|
|
2316 |
platformVersion = parseFloat(v[1] + '.' + v[2]);
|
|
|
2317 |
}
|
|
|
2318 |
}
|
|
|
2319 |
},
|
|
|
2320 |
|
|
|
2321 |
// Check if the platform is the one detected by cordova
|
|
|
2322 |
is: function(type) {
|
|
|
2323 |
type = type.toLowerCase();
|
|
|
2324 |
// check if it has an array of platforms
|
|
|
2325 |
if (self.platforms) {
|
|
|
2326 |
for (var x = 0; x < self.platforms.length; x++) {
|
|
|
2327 |
if (self.platforms[x] === type) return true;
|
|
|
2328 |
}
|
|
|
2329 |
}
|
|
|
2330 |
// exact match
|
|
|
2331 |
var pName = self.platform();
|
|
|
2332 |
if (pName) {
|
|
|
2333 |
return pName === type.toLowerCase();
|
|
|
2334 |
}
|
|
|
2335 |
|
|
|
2336 |
// A quick hack for to check userAgent
|
|
|
2337 |
return self.ua.toLowerCase().indexOf(type) >= 0;
|
|
|
2338 |
},
|
|
|
2339 |
|
|
|
2340 |
/**
|
|
|
2341 |
* @ngdoc method
|
|
|
2342 |
* @name ionic.Platform#exitApp
|
|
|
2343 |
* @description Exit the app.
|
|
|
2344 |
*/
|
|
|
2345 |
exitApp: function() {
|
|
|
2346 |
self.ready(function() {
|
|
|
2347 |
navigator.app && navigator.app.exitApp && navigator.app.exitApp();
|
|
|
2348 |
});
|
|
|
2349 |
},
|
|
|
2350 |
|
|
|
2351 |
/**
|
|
|
2352 |
* @ngdoc method
|
|
|
2353 |
* @name ionic.Platform#showStatusBar
|
|
|
2354 |
* @description Shows or hides the device status bar (in Cordova). Requires `cordova plugin add org.apache.cordova.statusbar`
|
|
|
2355 |
* @param {boolean} shouldShow Whether or not to show the status bar.
|
|
|
2356 |
*/
|
|
|
2357 |
showStatusBar: function(val) {
|
|
|
2358 |
// Only useful when run within cordova
|
|
|
2359 |
self._showStatusBar = val;
|
|
|
2360 |
self.ready(function() {
|
|
|
2361 |
// run this only when or if the platform (cordova) is ready
|
|
|
2362 |
requestAnimationFrame(function() {
|
|
|
2363 |
if (self._showStatusBar) {
|
|
|
2364 |
// they do not want it to be full screen
|
|
|
2365 |
window.StatusBar && window.StatusBar.show();
|
|
|
2366 |
document.body.classList.remove('status-bar-hide');
|
|
|
2367 |
} else {
|
|
|
2368 |
// it should be full screen
|
|
|
2369 |
window.StatusBar && window.StatusBar.hide();
|
|
|
2370 |
document.body.classList.add('status-bar-hide');
|
|
|
2371 |
}
|
|
|
2372 |
});
|
|
|
2373 |
});
|
|
|
2374 |
},
|
|
|
2375 |
|
|
|
2376 |
/**
|
|
|
2377 |
* @ngdoc method
|
|
|
2378 |
* @name ionic.Platform#fullScreen
|
|
|
2379 |
* @description
|
|
|
2380 |
* Sets whether the app is fullscreen or not (in Cordova).
|
|
|
2381 |
* @param {boolean=} showFullScreen Whether or not to set the app to fullscreen. Defaults to true. Requires `cordova plugin add org.apache.cordova.statusbar`
|
|
|
2382 |
* @param {boolean=} showStatusBar Whether or not to show the device's status bar. Defaults to false.
|
|
|
2383 |
*/
|
|
|
2384 |
fullScreen: function(showFullScreen, showStatusBar) {
|
|
|
2385 |
// showFullScreen: default is true if no param provided
|
|
|
2386 |
self.isFullScreen = (showFullScreen !== false);
|
|
|
2387 |
|
|
|
2388 |
// add/remove the fullscreen classname to the body
|
|
|
2389 |
ionic.DomUtil.ready(function() {
|
|
|
2390 |
// run this only when or if the DOM is ready
|
|
|
2391 |
requestAnimationFrame(function() {
|
|
|
2392 |
if (self.isFullScreen) {
|
|
|
2393 |
document.body.classList.add('fullscreen');
|
|
|
2394 |
} else {
|
|
|
2395 |
document.body.classList.remove('fullscreen');
|
|
|
2396 |
}
|
|
|
2397 |
});
|
|
|
2398 |
// showStatusBar: default is false if no param provided
|
|
|
2399 |
self.showStatusBar((showStatusBar === true));
|
|
|
2400 |
});
|
|
|
2401 |
}
|
|
|
2402 |
|
|
|
2403 |
};
|
|
|
2404 |
|
|
|
2405 |
var platformName = null, // just the name, like iOS or Android
|
|
|
2406 |
platformVersion = null, // a float of the major and minor, like 7.1
|
|
|
2407 |
readyCallbacks = [],
|
|
|
2408 |
windowLoadListenderAttached;
|
|
|
2409 |
|
|
|
2410 |
// setup listeners to know when the device is ready to go
|
|
|
2411 |
function onWindowLoad() {
|
|
|
2412 |
if (self.isWebView()) {
|
|
|
2413 |
// the window and scripts are fully loaded, and a cordova/phonegap
|
|
|
2414 |
// object exists then let's listen for the deviceready
|
|
|
2415 |
document.addEventListener("deviceready", onPlatformReady, false);
|
|
|
2416 |
} else {
|
|
|
2417 |
// the window and scripts are fully loaded, but the window object doesn't have the
|
|
|
2418 |
// cordova/phonegap object, so its just a browser, not a webview wrapped w/ cordova
|
|
|
2419 |
onPlatformReady();
|
|
|
2420 |
}
|
|
|
2421 |
if (windowLoadListenderAttached) {
|
|
|
2422 |
window.removeEventListener("load", onWindowLoad, false);
|
|
|
2423 |
}
|
|
|
2424 |
}
|
|
|
2425 |
if (document.readyState === 'complete') {
|
|
|
2426 |
onWindowLoad();
|
|
|
2427 |
} else {
|
|
|
2428 |
windowLoadListenderAttached = true;
|
|
|
2429 |
window.addEventListener("load", onWindowLoad, false);
|
|
|
2430 |
}
|
|
|
2431 |
|
|
|
2432 |
function onPlatformReady() {
|
|
|
2433 |
// the device is all set to go, init our own stuff then fire off our event
|
|
|
2434 |
self.isReady = true;
|
|
|
2435 |
self.detect();
|
|
|
2436 |
for (var x = 0; x < readyCallbacks.length; x++) {
|
|
|
2437 |
// fire off all the callbacks that were added before the platform was ready
|
|
|
2438 |
readyCallbacks[x]();
|
|
|
2439 |
}
|
|
|
2440 |
readyCallbacks = [];
|
|
|
2441 |
ionic.trigger('platformready', { target: document });
|
|
|
2442 |
|
|
|
2443 |
requestAnimationFrame(function() {
|
|
|
2444 |
document.body.classList.add('platform-ready');
|
|
|
2445 |
});
|
|
|
2446 |
}
|
|
|
2447 |
|
|
|
2448 |
})(this, document, ionic);
|
|
|
2449 |
|
|
|
2450 |
(function(document, ionic) {
|
|
|
2451 |
'use strict';
|
|
|
2452 |
|
|
|
2453 |
// Ionic CSS polyfills
|
|
|
2454 |
ionic.CSS = {};
|
|
|
2455 |
|
|
|
2456 |
(function() {
|
|
|
2457 |
|
|
|
2458 |
// transform
|
|
|
2459 |
var i, keys = ['webkitTransform', 'transform', '-webkit-transform', 'webkit-transform',
|
|
|
2460 |
'-moz-transform', 'moz-transform', 'MozTransform', 'mozTransform', 'msTransform'];
|
|
|
2461 |
|
|
|
2462 |
for (i = 0; i < keys.length; i++) {
|
|
|
2463 |
if (document.documentElement.style[keys[i]] !== undefined) {
|
|
|
2464 |
ionic.CSS.TRANSFORM = keys[i];
|
|
|
2465 |
break;
|
|
|
2466 |
}
|
|
|
2467 |
}
|
|
|
2468 |
|
|
|
2469 |
// transition
|
|
|
2470 |
keys = ['webkitTransition', 'mozTransition', 'msTransition', 'transition'];
|
|
|
2471 |
for (i = 0; i < keys.length; i++) {
|
|
|
2472 |
if (document.documentElement.style[keys[i]] !== undefined) {
|
|
|
2473 |
ionic.CSS.TRANSITION = keys[i];
|
|
|
2474 |
break;
|
|
|
2475 |
}
|
|
|
2476 |
}
|
|
|
2477 |
|
|
|
2478 |
// The only prefix we care about is webkit for transitions.
|
|
|
2479 |
var isWebkit = ionic.CSS.TRANSITION.indexOf('webkit') > -1;
|
|
|
2480 |
|
|
|
2481 |
// transition duration
|
|
|
2482 |
ionic.CSS.TRANSITION_DURATION = (isWebkit ? '-webkit-' : '') + 'transition-duration';
|
|
|
2483 |
|
|
|
2484 |
// To be sure transitionend works everywhere, include *both* the webkit and non-webkit events
|
|
|
2485 |
ionic.CSS.TRANSITIONEND = (isWebkit ? 'webkitTransitionEnd ' : '') + 'transitionend';
|
|
|
2486 |
})();
|
|
|
2487 |
|
|
|
2488 |
// classList polyfill for them older Androids
|
|
|
2489 |
// https://gist.github.com/devongovett/1381839
|
|
|
2490 |
if (!("classList" in document.documentElement) && Object.defineProperty && typeof HTMLElement !== 'undefined') {
|
|
|
2491 |
Object.defineProperty(HTMLElement.prototype, 'classList', {
|
|
|
2492 |
get: function() {
|
|
|
2493 |
var self = this;
|
|
|
2494 |
function update(fn) {
|
|
|
2495 |
return function() {
|
|
|
2496 |
var x, classes = self.className.split(/\s+/);
|
|
|
2497 |
|
|
|
2498 |
for (x = 0; x < arguments.length; x++) {
|
|
|
2499 |
fn(classes, classes.indexOf(arguments[x]), arguments[x]);
|
|
|
2500 |
}
|
|
|
2501 |
|
|
|
2502 |
self.className = classes.join(" ");
|
|
|
2503 |
};
|
|
|
2504 |
}
|
|
|
2505 |
|
|
|
2506 |
return {
|
|
|
2507 |
add: update(function(classes, index, value) {
|
|
|
2508 |
~index || classes.push(value);
|
|
|
2509 |
}),
|
|
|
2510 |
|
|
|
2511 |
remove: update(function(classes, index) {
|
|
|
2512 |
~index && classes.splice(index, 1);
|
|
|
2513 |
}),
|
|
|
2514 |
|
|
|
2515 |
toggle: update(function(classes, index, value) {
|
|
|
2516 |
~index ? classes.splice(index, 1) : classes.push(value);
|
|
|
2517 |
}),
|
|
|
2518 |
|
|
|
2519 |
contains: function(value) {
|
|
|
2520 |
return !!~self.className.split(/\s+/).indexOf(value);
|
|
|
2521 |
},
|
|
|
2522 |
|
|
|
2523 |
item: function(i) {
|
|
|
2524 |
return self.className.split(/\s+/)[i] || null;
|
|
|
2525 |
}
|
|
|
2526 |
};
|
|
|
2527 |
|
|
|
2528 |
}
|
|
|
2529 |
});
|
|
|
2530 |
}
|
|
|
2531 |
|
|
|
2532 |
})(document, ionic);
|
|
|
2533 |
|
|
|
2534 |
|
|
|
2535 |
/**
|
|
|
2536 |
* @ngdoc page
|
|
|
2537 |
* @name tap
|
|
|
2538 |
* @module ionic
|
|
|
2539 |
* @description
|
|
|
2540 |
* On touch devices such as a phone or tablet, some browsers implement a 300ms delay between
|
|
|
2541 |
* the time the user stops touching the display and the moment the browser executes the
|
|
|
2542 |
* click. This delay was initially introduced so the browser can know whether the user wants to
|
|
|
2543 |
* double-tap to zoom in on the webpage. Basically, the browser waits roughly 300ms to see if
|
|
|
2544 |
* the user is double-tapping, or just tapping on the display once.
|
|
|
2545 |
*
|
|
|
2546 |
* Out of the box, Ionic automatically removes the 300ms delay in order to make Ionic apps
|
|
|
2547 |
* feel more "native" like. Resultingly, other solutions such as
|
|
|
2548 |
* [fastclick](https://github.com/ftlabs/fastclick) and Angular's
|
|
|
2549 |
* [ngTouch](https://docs.angularjs.org/api/ngTouch) should not be included, to avoid conflicts.
|
|
|
2550 |
*
|
|
|
2551 |
* Some browsers already remove the delay with certain settings, such as the CSS property
|
|
|
2552 |
* `touch-events: none` or with specific meta tag viewport values. However, each of these
|
|
|
2553 |
* browsers still handle clicks differently, such as when to fire off or cancel the event
|
|
|
2554 |
* (like scrolling when the target is a button, or holding a button down).
|
|
|
2555 |
* For browsers that already remove the 300ms delay, consider Ionic's tap system as a way to
|
|
|
2556 |
* normalize how clicks are handled across the various devices so there's an expected response
|
|
|
2557 |
* no matter what the device, platform or version. Additionally, Ionic will prevent
|
|
|
2558 |
* ghostclicks which even browsers that remove the delay still experience.
|
|
|
2559 |
*
|
|
|
2560 |
* In some cases, third-party libraries may also be working with touch events which can interfere
|
|
|
2561 |
* with the tap system. For example, mapping libraries like Google or Leaflet Maps often implement
|
|
|
2562 |
* a touch detection system which conflicts with Ionic's tap system.
|
|
|
2563 |
*
|
|
|
2564 |
* ### Disabling the tap system
|
|
|
2565 |
*
|
|
|
2566 |
* To disable the tap for an element and all of its children elements,
|
|
|
2567 |
* add the attribute `data-tap-disabled="true"`.
|
|
|
2568 |
*
|
|
|
2569 |
* ```html
|
|
|
2570 |
* <div data-tap-disabled="true">
|
|
|
2571 |
* <div id="google-map"></div>
|
|
|
2572 |
* </div>
|
|
|
2573 |
* ```
|
|
|
2574 |
*
|
|
|
2575 |
* ### Additional Notes:
|
|
|
2576 |
*
|
|
|
2577 |
* - Ionic tap works with Ionic's JavaScript scrolling
|
|
|
2578 |
* - Elements can come and go from the DOM and Ionic tap doesn't keep adding and removing
|
|
|
2579 |
* listeners
|
|
|
2580 |
* - No "tap delay" after the first "tap" (you can tap as fast as you want, they all click)
|
|
|
2581 |
* - Minimal events listeners, only being added to document
|
|
|
2582 |
* - Correct focus in/out on each input type (select, textearea, range) on each platform/device
|
|
|
2583 |
* - Shows and hides virtual keyboard correctly for each platform/device
|
|
|
2584 |
* - Works with labels surrounding inputs
|
|
|
2585 |
* - Does not fire off a click if the user moves the pointer too far
|
|
|
2586 |
* - Adds and removes an 'activated' css class
|
|
|
2587 |
* - Multiple [unit tests](https://github.com/driftyco/ionic/blob/master/test/unit/utils/tap.unit.js) for each scenario
|
|
|
2588 |
*
|
|
|
2589 |
*/
|
|
|
2590 |
/*
|
|
|
2591 |
|
|
|
2592 |
IONIC TAP
|
|
|
2593 |
---------------
|
|
|
2594 |
- Both touch and mouse events are added to the document.body on DOM ready
|
|
|
2595 |
- If a touch event happens, it does not use mouse event listeners
|
|
|
2596 |
- On touchend, if the distance between start and end was small, trigger a click
|
|
|
2597 |
- In the triggered click event, add a 'isIonicTap' property
|
|
|
2598 |
- The triggered click receives the same x,y coordinates as as the end event
|
|
|
2599 |
- On document.body click listener (with useCapture=true), only allow clicks with 'isIonicTap'
|
|
|
2600 |
- Triggering clicks with mouse events work the same as touch, except with mousedown/mouseup
|
|
|
2601 |
- Tapping inputs is disabled during scrolling
|
|
|
2602 |
*/
|
|
|
2603 |
|
|
|
2604 |
var tapDoc; // the element which the listeners are on (document.body)
|
|
|
2605 |
var tapActiveEle; // the element which is active (probably has focus)
|
|
|
2606 |
var tapEnabledTouchEvents;
|
|
|
2607 |
var tapMouseResetTimer;
|
|
|
2608 |
var tapPointerMoved;
|
|
|
2609 |
var tapPointerStart;
|
|
|
2610 |
var tapTouchFocusedInput;
|
|
|
2611 |
var tapLastTouchTarget;
|
|
|
2612 |
var tapTouchMoveListener = 'touchmove';
|
|
|
2613 |
|
|
|
2614 |
// how much the coordinates can be off between start/end, but still a click
|
|
|
2615 |
var TAP_RELEASE_TOLERANCE = 12; // default tolerance
|
|
|
2616 |
var TAP_RELEASE_BUTTON_TOLERANCE = 50; // button elements should have a larger tolerance
|
|
|
2617 |
|
|
|
2618 |
var tapEventListeners = {
|
|
|
2619 |
'click': tapClickGateKeeper,
|
|
|
2620 |
|
|
|
2621 |
'mousedown': tapMouseDown,
|
|
|
2622 |
'mouseup': tapMouseUp,
|
|
|
2623 |
'mousemove': tapMouseMove,
|
|
|
2624 |
|
|
|
2625 |
'touchstart': tapTouchStart,
|
|
|
2626 |
'touchend': tapTouchEnd,
|
|
|
2627 |
'touchcancel': tapTouchCancel,
|
|
|
2628 |
'touchmove': tapTouchMove,
|
|
|
2629 |
|
|
|
2630 |
'pointerdown': tapTouchStart,
|
|
|
2631 |
'pointerup': tapTouchEnd,
|
|
|
2632 |
'pointercancel': tapTouchCancel,
|
|
|
2633 |
'pointermove': tapTouchMove,
|
|
|
2634 |
|
|
|
2635 |
'MSPointerDown': tapTouchStart,
|
|
|
2636 |
'MSPointerUp': tapTouchEnd,
|
|
|
2637 |
'MSPointerCancel': tapTouchCancel,
|
|
|
2638 |
'MSPointerMove': tapTouchMove,
|
|
|
2639 |
|
|
|
2640 |
'focusin': tapFocusIn,
|
|
|
2641 |
'focusout': tapFocusOut
|
|
|
2642 |
};
|
|
|
2643 |
|
|
|
2644 |
ionic.tap = {
|
|
|
2645 |
|
|
|
2646 |
register: function(ele) {
|
|
|
2647 |
tapDoc = ele;
|
|
|
2648 |
|
|
|
2649 |
tapEventListener('click', true, true);
|
|
|
2650 |
tapEventListener('mouseup');
|
|
|
2651 |
tapEventListener('mousedown');
|
|
|
2652 |
|
|
|
2653 |
if (window.navigator.pointerEnabled) {
|
|
|
2654 |
tapEventListener('pointerdown');
|
|
|
2655 |
tapEventListener('pointerup');
|
|
|
2656 |
tapEventListener('pointcancel');
|
|
|
2657 |
tapTouchMoveListener = 'pointermove';
|
|
|
2658 |
|
|
|
2659 |
} else if (window.navigator.msPointerEnabled) {
|
|
|
2660 |
tapEventListener('MSPointerDown');
|
|
|
2661 |
tapEventListener('MSPointerUp');
|
|
|
2662 |
tapEventListener('MSPointerCancel');
|
|
|
2663 |
tapTouchMoveListener = 'MSPointerMove';
|
|
|
2664 |
|
|
|
2665 |
} else {
|
|
|
2666 |
tapEventListener('touchstart');
|
|
|
2667 |
tapEventListener('touchend');
|
|
|
2668 |
tapEventListener('touchcancel');
|
|
|
2669 |
}
|
|
|
2670 |
|
|
|
2671 |
tapEventListener('focusin');
|
|
|
2672 |
tapEventListener('focusout');
|
|
|
2673 |
|
|
|
2674 |
return function() {
|
|
|
2675 |
for (var type in tapEventListeners) {
|
|
|
2676 |
tapEventListener(type, false);
|
|
|
2677 |
}
|
|
|
2678 |
tapDoc = null;
|
|
|
2679 |
tapActiveEle = null;
|
|
|
2680 |
tapEnabledTouchEvents = false;
|
|
|
2681 |
tapPointerMoved = false;
|
|
|
2682 |
tapPointerStart = null;
|
|
|
2683 |
};
|
|
|
2684 |
},
|
|
|
2685 |
|
|
|
2686 |
ignoreScrollStart: function(e) {
|
|
|
2687 |
return (e.defaultPrevented) || // defaultPrevented has been assigned by another component handling the event
|
|
|
2688 |
(/^(file|range)$/i).test(e.target.type) ||
|
|
|
2689 |
(e.target.dataset ? e.target.dataset.preventScroll : e.target.getAttribute('data-prevent-scroll')) == 'true' || // manually set within an elements attributes
|
|
|
2690 |
(!!(/^(object|embed)$/i).test(e.target.tagName)) || // flash/movie/object touches should not try to scroll
|
|
|
2691 |
ionic.tap.isElementTapDisabled(e.target); // check if this element, or an ancestor, has `data-tap-disabled` attribute
|
|
|
2692 |
},
|
|
|
2693 |
|
|
|
2694 |
isTextInput: function(ele) {
|
|
|
2695 |
return !!ele &&
|
|
|
2696 |
(ele.tagName == 'TEXTAREA' ||
|
|
|
2697 |
ele.contentEditable === 'true' ||
|
|
|
2698 |
(ele.tagName == 'INPUT' && !(/^(radio|checkbox|range|file|submit|reset|color|image|button)$/i).test(ele.type)));
|
|
|
2699 |
},
|
|
|
2700 |
|
|
|
2701 |
isDateInput: function(ele) {
|
|
|
2702 |
return !!ele &&
|
|
|
2703 |
(ele.tagName == 'INPUT' && (/^(date|time|datetime-local|month|week)$/i).test(ele.type));
|
|
|
2704 |
},
|
|
|
2705 |
|
|
|
2706 |
isKeyboardElement: function(ele) {
|
|
|
2707 |
if ( !ionic.Platform.isIOS() || ionic.Platform.isIPad() ) {
|
|
|
2708 |
return ionic.tap.isTextInput(ele) && !ionic.tap.isDateInput(ele);
|
|
|
2709 |
} else {
|
|
|
2710 |
return ionic.tap.isTextInput(ele) || ( !!ele && ele.tagName == "SELECT");
|
|
|
2711 |
}
|
|
|
2712 |
},
|
|
|
2713 |
|
|
|
2714 |
isLabelWithTextInput: function(ele) {
|
|
|
2715 |
var container = tapContainingElement(ele, false);
|
|
|
2716 |
|
|
|
2717 |
return !!container &&
|
|
|
2718 |
ionic.tap.isTextInput(tapTargetElement(container));
|
|
|
2719 |
},
|
|
|
2720 |
|
|
|
2721 |
containsOrIsTextInput: function(ele) {
|
|
|
2722 |
return ionic.tap.isTextInput(ele) || ionic.tap.isLabelWithTextInput(ele);
|
|
|
2723 |
},
|
|
|
2724 |
|
|
|
2725 |
cloneFocusedInput: function(container) {
|
|
|
2726 |
if (ionic.tap.hasCheckedClone) return;
|
|
|
2727 |
ionic.tap.hasCheckedClone = true;
|
|
|
2728 |
|
|
|
2729 |
ionic.requestAnimationFrame(function() {
|
|
|
2730 |
var focusInput = container.querySelector(':focus');
|
|
|
2731 |
if (ionic.tap.isTextInput(focusInput)) {
|
|
|
2732 |
var clonedInput = focusInput.cloneNode(true);
|
|
|
2733 |
|
|
|
2734 |
clonedInput.value = focusInput.value;
|
|
|
2735 |
clonedInput.classList.add('cloned-text-input');
|
|
|
2736 |
clonedInput.readOnly = true;
|
|
|
2737 |
if (focusInput.isContentEditable) {
|
|
|
2738 |
clonedInput.contentEditable = focusInput.contentEditable;
|
|
|
2739 |
clonedInput.innerHTML = focusInput.innerHTML;
|
|
|
2740 |
}
|
|
|
2741 |
focusInput.parentElement.insertBefore(clonedInput, focusInput);
|
|
|
2742 |
focusInput.classList.add('previous-input-focus');
|
|
|
2743 |
|
|
|
2744 |
clonedInput.scrollTop = focusInput.scrollTop;
|
|
|
2745 |
}
|
|
|
2746 |
});
|
|
|
2747 |
},
|
|
|
2748 |
|
|
|
2749 |
hasCheckedClone: false,
|
|
|
2750 |
|
|
|
2751 |
removeClonedInputs: function(container) {
|
|
|
2752 |
ionic.tap.hasCheckedClone = false;
|
|
|
2753 |
|
|
|
2754 |
ionic.requestAnimationFrame(function() {
|
|
|
2755 |
var clonedInputs = container.querySelectorAll('.cloned-text-input');
|
|
|
2756 |
var previousInputFocus = container.querySelectorAll('.previous-input-focus');
|
|
|
2757 |
var x;
|
|
|
2758 |
|
|
|
2759 |
for (x = 0; x < clonedInputs.length; x++) {
|
|
|
2760 |
clonedInputs[x].parentElement.removeChild(clonedInputs[x]);
|
|
|
2761 |
}
|
|
|
2762 |
|
|
|
2763 |
for (x = 0; x < previousInputFocus.length; x++) {
|
|
|
2764 |
previousInputFocus[x].classList.remove('previous-input-focus');
|
|
|
2765 |
previousInputFocus[x].style.top = '';
|
|
|
2766 |
if ( ionic.keyboard.isOpen && !ionic.keyboard.isClosing ) previousInputFocus[x].focus();
|
|
|
2767 |
}
|
|
|
2768 |
});
|
|
|
2769 |
},
|
|
|
2770 |
|
|
|
2771 |
requiresNativeClick: function(ele) {
|
|
|
2772 |
if (!ele || ele.disabled || (/^(file|range)$/i).test(ele.type) || (/^(object|video)$/i).test(ele.tagName) || ionic.tap.isLabelContainingFileInput(ele)) {
|
|
|
2773 |
return true;
|
|
|
2774 |
}
|
|
|
2775 |
return ionic.tap.isElementTapDisabled(ele);
|
|
|
2776 |
},
|
|
|
2777 |
|
|
|
2778 |
isLabelContainingFileInput: function(ele) {
|
|
|
2779 |
var lbl = tapContainingElement(ele);
|
|
|
2780 |
if (lbl.tagName !== 'LABEL') return false;
|
|
|
2781 |
var fileInput = lbl.querySelector('input[type=file]');
|
|
|
2782 |
if (fileInput && fileInput.disabled === false) return true;
|
|
|
2783 |
return false;
|
|
|
2784 |
},
|
|
|
2785 |
|
|
|
2786 |
isElementTapDisabled: function(ele) {
|
|
|
2787 |
if (ele && ele.nodeType === 1) {
|
|
|
2788 |
var element = ele;
|
|
|
2789 |
while (element) {
|
|
|
2790 |
if ((element.dataset ? element.dataset.tapDisabled : element.getAttribute('data-tap-disabled')) == 'true') {
|
|
|
2791 |
return true;
|
|
|
2792 |
}
|
|
|
2793 |
element = element.parentElement;
|
|
|
2794 |
}
|
|
|
2795 |
}
|
|
|
2796 |
return false;
|
|
|
2797 |
},
|
|
|
2798 |
|
|
|
2799 |
setTolerance: function(releaseTolerance, releaseButtonTolerance) {
|
|
|
2800 |
TAP_RELEASE_TOLERANCE = releaseTolerance;
|
|
|
2801 |
TAP_RELEASE_BUTTON_TOLERANCE = releaseButtonTolerance;
|
|
|
2802 |
},
|
|
|
2803 |
|
|
|
2804 |
cancelClick: function() {
|
|
|
2805 |
// used to cancel any simulated clicks which may happen on a touchend/mouseup
|
|
|
2806 |
// gestures uses this method within its tap and hold events
|
|
|
2807 |
tapPointerMoved = true;
|
|
|
2808 |
},
|
|
|
2809 |
|
|
|
2810 |
pointerCoord: function(event) {
|
|
|
2811 |
// This method can get coordinates for both a mouse click
|
|
|
2812 |
// or a touch depending on the given event
|
|
|
2813 |
var c = { x: 0, y: 0 };
|
|
|
2814 |
if (event) {
|
|
|
2815 |
var touches = event.touches && event.touches.length ? event.touches : [event];
|
|
|
2816 |
var e = (event.changedTouches && event.changedTouches[0]) || touches[0];
|
|
|
2817 |
if (e) {
|
|
|
2818 |
c.x = e.clientX || e.pageX || 0;
|
|
|
2819 |
c.y = e.clientY || e.pageY || 0;
|
|
|
2820 |
}
|
|
|
2821 |
}
|
|
|
2822 |
return c;
|
|
|
2823 |
}
|
|
|
2824 |
|
|
|
2825 |
};
|
|
|
2826 |
|
|
|
2827 |
function tapEventListener(type, enable, useCapture) {
|
|
|
2828 |
if (enable !== false) {
|
|
|
2829 |
tapDoc.addEventListener(type, tapEventListeners[type], useCapture);
|
|
|
2830 |
} else {
|
|
|
2831 |
tapDoc.removeEventListener(type, tapEventListeners[type]);
|
|
|
2832 |
}
|
|
|
2833 |
}
|
|
|
2834 |
|
|
|
2835 |
function tapClick(e) {
|
|
|
2836 |
// simulate a normal click by running the element's click method then focus on it
|
|
|
2837 |
var container = tapContainingElement(e.target);
|
|
|
2838 |
var ele = tapTargetElement(container);
|
|
|
2839 |
|
|
|
2840 |
if (ionic.tap.requiresNativeClick(ele) || tapPointerMoved) return false;
|
|
|
2841 |
|
|
|
2842 |
var c = ionic.tap.pointerCoord(e);
|
|
|
2843 |
|
|
|
2844 |
//console.log('tapClick', e.type, ele.tagName, '('+c.x+','+c.y+')');
|
|
|
2845 |
triggerMouseEvent('click', ele, c.x, c.y);
|
|
|
2846 |
|
|
|
2847 |
// if it's an input, focus in on the target, otherwise blur
|
|
|
2848 |
tapHandleFocus(ele);
|
|
|
2849 |
}
|
|
|
2850 |
|
|
|
2851 |
function triggerMouseEvent(type, ele, x, y) {
|
|
|
2852 |
// using initMouseEvent instead of MouseEvent for our Android friends
|
|
|
2853 |
var clickEvent = document.createEvent("MouseEvents");
|
|
|
2854 |
clickEvent.initMouseEvent(type, true, true, window, 1, 0, 0, x, y, false, false, false, false, 0, null);
|
|
|
2855 |
clickEvent.isIonicTap = true;
|
|
|
2856 |
ele.dispatchEvent(clickEvent);
|
|
|
2857 |
}
|
|
|
2858 |
|
|
|
2859 |
function tapClickGateKeeper(e) {
|
|
|
2860 |
//console.log('click ' + Date.now() + ' isIonicTap: ' + (e.isIonicTap ? true : false));
|
|
|
2861 |
if (e.target.type == 'submit' && e.detail === 0) {
|
|
|
2862 |
// do not prevent click if it came from an "Enter" or "Go" keypress submit
|
|
|
2863 |
return null;
|
|
|
2864 |
}
|
|
|
2865 |
|
|
|
2866 |
// do not allow through any click events that were not created by ionic.tap
|
|
|
2867 |
if ((ionic.scroll.isScrolling && ionic.tap.containsOrIsTextInput(e.target)) ||
|
|
|
2868 |
(!e.isIonicTap && !ionic.tap.requiresNativeClick(e.target))) {
|
|
|
2869 |
//console.log('clickPrevent', e.target.tagName);
|
|
|
2870 |
e.stopPropagation();
|
|
|
2871 |
|
|
|
2872 |
if (!ionic.tap.isLabelWithTextInput(e.target)) {
|
|
|
2873 |
// labels clicks from native should not preventDefault othersize keyboard will not show on input focus
|
|
|
2874 |
e.preventDefault();
|
|
|
2875 |
}
|
|
|
2876 |
return false;
|
|
|
2877 |
}
|
|
|
2878 |
}
|
|
|
2879 |
|
|
|
2880 |
// MOUSE
|
|
|
2881 |
function tapMouseDown(e) {
|
|
|
2882 |
//console.log('mousedown ' + Date.now());
|
|
|
2883 |
if (e.isIonicTap || tapIgnoreEvent(e)) return null;
|
|
|
2884 |
|
|
|
2885 |
if (tapEnabledTouchEvents) {
|
|
|
2886 |
void 0;
|
|
|
2887 |
e.stopPropagation();
|
|
|
2888 |
|
|
|
2889 |
if ((!ionic.tap.isTextInput(e.target) || tapLastTouchTarget !== e.target) && !(/^(select|option)$/i).test(e.target.tagName)) {
|
|
|
2890 |
// If you preventDefault on a text input then you cannot move its text caret/cursor.
|
|
|
2891 |
// Allow through only the text input default. However, without preventDefault on an
|
|
|
2892 |
// input the 300ms delay can change focus on inputs after the keyboard shows up.
|
|
|
2893 |
// The focusin event handles the chance of focus changing after the keyboard shows.
|
|
|
2894 |
e.preventDefault();
|
|
|
2895 |
}
|
|
|
2896 |
|
|
|
2897 |
return false;
|
|
|
2898 |
}
|
|
|
2899 |
|
|
|
2900 |
tapPointerMoved = false;
|
|
|
2901 |
tapPointerStart = ionic.tap.pointerCoord(e);
|
|
|
2902 |
|
|
|
2903 |
tapEventListener('mousemove');
|
|
|
2904 |
ionic.activator.start(e);
|
|
|
2905 |
}
|
|
|
2906 |
|
|
|
2907 |
function tapMouseUp(e) {
|
|
|
2908 |
//console.log("mouseup " + Date.now());
|
|
|
2909 |
if (tapEnabledTouchEvents) {
|
|
|
2910 |
e.stopPropagation();
|
|
|
2911 |
e.preventDefault();
|
|
|
2912 |
return false;
|
|
|
2913 |
}
|
|
|
2914 |
|
|
|
2915 |
if (tapIgnoreEvent(e) || (/^(select|option)$/i).test(e.target.tagName)) return false;
|
|
|
2916 |
|
|
|
2917 |
if (!tapHasPointerMoved(e)) {
|
|
|
2918 |
tapClick(e);
|
|
|
2919 |
}
|
|
|
2920 |
tapEventListener('mousemove', false);
|
|
|
2921 |
ionic.activator.end();
|
|
|
2922 |
tapPointerMoved = false;
|
|
|
2923 |
}
|
|
|
2924 |
|
|
|
2925 |
function tapMouseMove(e) {
|
|
|
2926 |
if (tapHasPointerMoved(e)) {
|
|
|
2927 |
tapEventListener('mousemove', false);
|
|
|
2928 |
ionic.activator.end();
|
|
|
2929 |
tapPointerMoved = true;
|
|
|
2930 |
return false;
|
|
|
2931 |
}
|
|
|
2932 |
}
|
|
|
2933 |
|
|
|
2934 |
|
|
|
2935 |
// TOUCH
|
|
|
2936 |
function tapTouchStart(e) {
|
|
|
2937 |
//console.log("touchstart " + Date.now());
|
|
|
2938 |
if (tapIgnoreEvent(e)) return;
|
|
|
2939 |
|
|
|
2940 |
tapPointerMoved = false;
|
|
|
2941 |
|
|
|
2942 |
tapEnableTouchEvents();
|
|
|
2943 |
tapPointerStart = ionic.tap.pointerCoord(e);
|
|
|
2944 |
|
|
|
2945 |
tapEventListener(tapTouchMoveListener);
|
|
|
2946 |
ionic.activator.start(e);
|
|
|
2947 |
|
|
|
2948 |
if (ionic.Platform.isIOS() && ionic.tap.isLabelWithTextInput(e.target)) {
|
|
|
2949 |
// if the tapped element is a label, which has a child input
|
|
|
2950 |
// then preventDefault so iOS doesn't ugly auto scroll to the input
|
|
|
2951 |
// but do not prevent default on Android or else you cannot move the text caret
|
|
|
2952 |
// and do not prevent default on Android or else no virtual keyboard shows up
|
|
|
2953 |
|
|
|
2954 |
var textInput = tapTargetElement(tapContainingElement(e.target));
|
|
|
2955 |
if (textInput !== tapActiveEle) {
|
|
|
2956 |
// don't preventDefault on an already focused input or else iOS's text caret isn't usable
|
|
|
2957 |
e.preventDefault();
|
|
|
2958 |
}
|
|
|
2959 |
}
|
|
|
2960 |
}
|
|
|
2961 |
|
|
|
2962 |
function tapTouchEnd(e) {
|
|
|
2963 |
//console.log('touchend ' + Date.now());
|
|
|
2964 |
if (tapIgnoreEvent(e)) return;
|
|
|
2965 |
|
|
|
2966 |
tapEnableTouchEvents();
|
|
|
2967 |
if (!tapHasPointerMoved(e)) {
|
|
|
2968 |
tapClick(e);
|
|
|
2969 |
|
|
|
2970 |
if ((/^(select|option)$/i).test(e.target.tagName)) {
|
|
|
2971 |
e.preventDefault();
|
|
|
2972 |
}
|
|
|
2973 |
}
|
|
|
2974 |
|
|
|
2975 |
tapLastTouchTarget = e.target;
|
|
|
2976 |
tapTouchCancel();
|
|
|
2977 |
}
|
|
|
2978 |
|
|
|
2979 |
function tapTouchMove(e) {
|
|
|
2980 |
if (tapHasPointerMoved(e)) {
|
|
|
2981 |
tapPointerMoved = true;
|
|
|
2982 |
tapEventListener(tapTouchMoveListener, false);
|
|
|
2983 |
ionic.activator.end();
|
|
|
2984 |
return false;
|
|
|
2985 |
}
|
|
|
2986 |
}
|
|
|
2987 |
|
|
|
2988 |
function tapTouchCancel() {
|
|
|
2989 |
tapEventListener(tapTouchMoveListener, false);
|
|
|
2990 |
ionic.activator.end();
|
|
|
2991 |
tapPointerMoved = false;
|
|
|
2992 |
}
|
|
|
2993 |
|
|
|
2994 |
function tapEnableTouchEvents() {
|
|
|
2995 |
tapEnabledTouchEvents = true;
|
|
|
2996 |
clearTimeout(tapMouseResetTimer);
|
|
|
2997 |
tapMouseResetTimer = setTimeout(function() {
|
|
|
2998 |
tapEnabledTouchEvents = false;
|
|
|
2999 |
}, 600);
|
|
|
3000 |
}
|
|
|
3001 |
|
|
|
3002 |
function tapIgnoreEvent(e) {
|
|
|
3003 |
if (e.isTapHandled) return true;
|
|
|
3004 |
e.isTapHandled = true;
|
|
|
3005 |
|
|
|
3006 |
if (ionic.scroll.isScrolling && ionic.tap.containsOrIsTextInput(e.target)) {
|
|
|
3007 |
e.preventDefault();
|
|
|
3008 |
return true;
|
|
|
3009 |
}
|
|
|
3010 |
}
|
|
|
3011 |
|
|
|
3012 |
function tapHandleFocus(ele) {
|
|
|
3013 |
tapTouchFocusedInput = null;
|
|
|
3014 |
|
|
|
3015 |
var triggerFocusIn = false;
|
|
|
3016 |
|
|
|
3017 |
if (ele.tagName == 'SELECT') {
|
|
|
3018 |
// trick to force Android options to show up
|
|
|
3019 |
triggerMouseEvent('mousedown', ele, 0, 0);
|
|
|
3020 |
ele.focus && ele.focus();
|
|
|
3021 |
triggerFocusIn = true;
|
|
|
3022 |
|
|
|
3023 |
} else if (tapActiveElement() === ele) {
|
|
|
3024 |
// already is the active element and has focus
|
|
|
3025 |
triggerFocusIn = true;
|
|
|
3026 |
|
|
|
3027 |
} else if ((/^(input|textarea)$/i).test(ele.tagName) || ele.isContentEditable) {
|
|
|
3028 |
triggerFocusIn = true;
|
|
|
3029 |
ele.focus && ele.focus();
|
|
|
3030 |
ele.value = ele.value;
|
|
|
3031 |
if (tapEnabledTouchEvents) {
|
|
|
3032 |
tapTouchFocusedInput = ele;
|
|
|
3033 |
}
|
|
|
3034 |
|
|
|
3035 |
} else {
|
|
|
3036 |
tapFocusOutActive();
|
|
|
3037 |
}
|
|
|
3038 |
|
|
|
3039 |
if (triggerFocusIn) {
|
|
|
3040 |
tapActiveElement(ele);
|
|
|
3041 |
ionic.trigger('ionic.focusin', {
|
|
|
3042 |
target: ele
|
|
|
3043 |
}, true);
|
|
|
3044 |
}
|
|
|
3045 |
}
|
|
|
3046 |
|
|
|
3047 |
function tapFocusOutActive() {
|
|
|
3048 |
var ele = tapActiveElement();
|
|
|
3049 |
if (ele && ((/^(input|textarea|select)$/i).test(ele.tagName) || ele.isContentEditable)) {
|
|
|
3050 |
void 0;
|
|
|
3051 |
ele.blur();
|
|
|
3052 |
}
|
|
|
3053 |
tapActiveElement(null);
|
|
|
3054 |
}
|
|
|
3055 |
|
|
|
3056 |
function tapFocusIn(e) {
|
|
|
3057 |
//console.log('focusin ' + Date.now());
|
|
|
3058 |
// Because a text input doesn't preventDefault (so the caret still works) there's a chance
|
|
|
3059 |
// that its mousedown event 300ms later will change the focus to another element after
|
|
|
3060 |
// the keyboard shows up.
|
|
|
3061 |
|
|
|
3062 |
if (tapEnabledTouchEvents &&
|
|
|
3063 |
ionic.tap.isTextInput(tapActiveElement()) &&
|
|
|
3064 |
ionic.tap.isTextInput(tapTouchFocusedInput) &&
|
|
|
3065 |
tapTouchFocusedInput !== e.target) {
|
|
|
3066 |
|
|
|
3067 |
// 1) The pointer is from touch events
|
|
|
3068 |
// 2) There is an active element which is a text input
|
|
|
3069 |
// 3) A text input was just set to be focused on by a touch event
|
|
|
3070 |
// 4) A new focus has been set, however the target isn't the one the touch event wanted
|
|
|
3071 |
void 0;
|
|
|
3072 |
tapTouchFocusedInput.focus();
|
|
|
3073 |
tapTouchFocusedInput = null;
|
|
|
3074 |
}
|
|
|
3075 |
ionic.scroll.isScrolling = false;
|
|
|
3076 |
}
|
|
|
3077 |
|
|
|
3078 |
function tapFocusOut() {
|
|
|
3079 |
//console.log("focusout");
|
|
|
3080 |
tapActiveElement(null);
|
|
|
3081 |
}
|
|
|
3082 |
|
|
|
3083 |
function tapActiveElement(ele) {
|
|
|
3084 |
if (arguments.length) {
|
|
|
3085 |
tapActiveEle = ele;
|
|
|
3086 |
}
|
|
|
3087 |
return tapActiveEle || document.activeElement;
|
|
|
3088 |
}
|
|
|
3089 |
|
|
|
3090 |
function tapHasPointerMoved(endEvent) {
|
|
|
3091 |
if (!endEvent || endEvent.target.nodeType !== 1 || !tapPointerStart || (tapPointerStart.x === 0 && tapPointerStart.y === 0)) {
|
|
|
3092 |
return false;
|
|
|
3093 |
}
|
|
|
3094 |
var endCoordinates = ionic.tap.pointerCoord(endEvent);
|
|
|
3095 |
|
|
|
3096 |
var hasClassList = !!(endEvent.target.classList && endEvent.target.classList.contains &&
|
|
|
3097 |
typeof endEvent.target.classList.contains === 'function');
|
|
|
3098 |
var releaseTolerance = hasClassList && endEvent.target.classList.contains('button') ?
|
|
|
3099 |
TAP_RELEASE_BUTTON_TOLERANCE :
|
|
|
3100 |
TAP_RELEASE_TOLERANCE;
|
|
|
3101 |
|
|
|
3102 |
return Math.abs(tapPointerStart.x - endCoordinates.x) > releaseTolerance ||
|
|
|
3103 |
Math.abs(tapPointerStart.y - endCoordinates.y) > releaseTolerance;
|
|
|
3104 |
}
|
|
|
3105 |
|
|
|
3106 |
function tapContainingElement(ele, allowSelf) {
|
|
|
3107 |
var climbEle = ele;
|
|
|
3108 |
for (var x = 0; x < 6; x++) {
|
|
|
3109 |
if (!climbEle) break;
|
|
|
3110 |
if (climbEle.tagName === 'LABEL') return climbEle;
|
|
|
3111 |
climbEle = climbEle.parentElement;
|
|
|
3112 |
}
|
|
|
3113 |
if (allowSelf !== false) return ele;
|
|
|
3114 |
}
|
|
|
3115 |
|
|
|
3116 |
function tapTargetElement(ele) {
|
|
|
3117 |
if (ele && ele.tagName === 'LABEL') {
|
|
|
3118 |
if (ele.control) return ele.control;
|
|
|
3119 |
|
|
|
3120 |
// older devices do not support the "control" property
|
|
|
3121 |
if (ele.querySelector) {
|
|
|
3122 |
var control = ele.querySelector('input,textarea,select');
|
|
|
3123 |
if (control) return control;
|
|
|
3124 |
}
|
|
|
3125 |
}
|
|
|
3126 |
return ele;
|
|
|
3127 |
}
|
|
|
3128 |
|
|
|
3129 |
ionic.DomUtil.ready(function() {
|
|
|
3130 |
var ng = typeof angular !== 'undefined' ? angular : null;
|
|
|
3131 |
//do nothing for e2e tests
|
|
|
3132 |
if (!ng || (ng && !ng.scenario)) {
|
|
|
3133 |
ionic.tap.register(document);
|
|
|
3134 |
}
|
|
|
3135 |
});
|
|
|
3136 |
|
|
|
3137 |
(function(document, ionic) {
|
|
|
3138 |
'use strict';
|
|
|
3139 |
|
|
|
3140 |
var queueElements = {}; // elements that should get an active state in XX milliseconds
|
|
|
3141 |
var activeElements = {}; // elements that are currently active
|
|
|
3142 |
var keyId = 0; // a counter for unique keys for the above ojects
|
|
|
3143 |
var ACTIVATED_CLASS = 'activated';
|
|
|
3144 |
|
|
|
3145 |
ionic.activator = {
|
|
|
3146 |
|
|
|
3147 |
start: function(e) {
|
|
|
3148 |
var hitX = ionic.tap.pointerCoord(e).x;
|
|
|
3149 |
if (hitX > 0 && hitX < 30) {
|
|
|
3150 |
return;
|
|
|
3151 |
}
|
|
|
3152 |
|
|
|
3153 |
// when an element is touched/clicked, it climbs up a few
|
|
|
3154 |
// parents to see if it is an .item or .button element
|
|
|
3155 |
ionic.requestAnimationFrame(function() {
|
|
|
3156 |
if ((ionic.scroll && ionic.scroll.isScrolling) || ionic.tap.requiresNativeClick(e.target)) return;
|
|
|
3157 |
var ele = e.target;
|
|
|
3158 |
var eleToActivate;
|
|
|
3159 |
|
|
|
3160 |
for (var x = 0; x < 6; x++) {
|
|
|
3161 |
if (!ele || ele.nodeType !== 1) break;
|
|
|
3162 |
if (eleToActivate && ele.classList && ele.classList.contains('item')) {
|
|
|
3163 |
eleToActivate = ele;
|
|
|
3164 |
break;
|
|
|
3165 |
}
|
|
|
3166 |
if (ele.tagName == 'A' || ele.tagName == 'BUTTON' || ele.hasAttribute('ng-click')) {
|
|
|
3167 |
eleToActivate = ele;
|
|
|
3168 |
break;
|
|
|
3169 |
}
|
|
|
3170 |
if (ele.classList.contains('button')) {
|
|
|
3171 |
eleToActivate = ele;
|
|
|
3172 |
break;
|
|
|
3173 |
}
|
|
|
3174 |
// no sense climbing past these
|
|
|
3175 |
if (ele.tagName == 'ION-CONTENT' || (ele.classList && ele.classList.contains('pane')) || ele.tagName == 'BODY') {
|
|
|
3176 |
break;
|
|
|
3177 |
}
|
|
|
3178 |
ele = ele.parentElement;
|
|
|
3179 |
}
|
|
|
3180 |
|
|
|
3181 |
if (eleToActivate) {
|
|
|
3182 |
// queue that this element should be set to active
|
|
|
3183 |
queueElements[keyId] = eleToActivate;
|
|
|
3184 |
|
|
|
3185 |
// on the next frame, set the queued elements to active
|
|
|
3186 |
ionic.requestAnimationFrame(activateElements);
|
|
|
3187 |
|
|
|
3188 |
keyId = (keyId > 29 ? 0 : keyId + 1);
|
|
|
3189 |
}
|
|
|
3190 |
|
|
|
3191 |
});
|
|
|
3192 |
},
|
|
|
3193 |
|
|
|
3194 |
end: function() {
|
|
|
3195 |
// clear out any active/queued elements after XX milliseconds
|
|
|
3196 |
setTimeout(clear, 200);
|
|
|
3197 |
}
|
|
|
3198 |
|
|
|
3199 |
};
|
|
|
3200 |
|
|
|
3201 |
function clear() {
|
|
|
3202 |
// clear out any elements that are queued to be set to active
|
|
|
3203 |
queueElements = {};
|
|
|
3204 |
|
|
|
3205 |
// in the next frame, remove the active class from all active elements
|
|
|
3206 |
ionic.requestAnimationFrame(deactivateElements);
|
|
|
3207 |
}
|
|
|
3208 |
|
|
|
3209 |
function activateElements() {
|
|
|
3210 |
// activate all elements in the queue
|
|
|
3211 |
for (var key in queueElements) {
|
|
|
3212 |
if (queueElements[key]) {
|
|
|
3213 |
queueElements[key].classList.add(ACTIVATED_CLASS);
|
|
|
3214 |
activeElements[key] = queueElements[key];
|
|
|
3215 |
}
|
|
|
3216 |
}
|
|
|
3217 |
queueElements = {};
|
|
|
3218 |
}
|
|
|
3219 |
|
|
|
3220 |
function deactivateElements() {
|
|
|
3221 |
if (ionic.transition && ionic.transition.isActive) {
|
|
|
3222 |
setTimeout(deactivateElements, 400);
|
|
|
3223 |
return;
|
|
|
3224 |
}
|
|
|
3225 |
|
|
|
3226 |
for (var key in activeElements) {
|
|
|
3227 |
if (activeElements[key]) {
|
|
|
3228 |
activeElements[key].classList.remove(ACTIVATED_CLASS);
|
|
|
3229 |
delete activeElements[key];
|
|
|
3230 |
}
|
|
|
3231 |
}
|
|
|
3232 |
}
|
|
|
3233 |
|
|
|
3234 |
})(document, ionic);
|
|
|
3235 |
|
|
|
3236 |
(function(ionic) {
|
|
|
3237 |
/* for nextUid function below */
|
|
|
3238 |
var nextId = 0;
|
|
|
3239 |
|
|
|
3240 |
/**
|
|
|
3241 |
* Various utilities used throughout Ionic
|
|
|
3242 |
*
|
|
|
3243 |
* Some of these are adopted from underscore.js and backbone.js, both also MIT licensed.
|
|
|
3244 |
*/
|
|
|
3245 |
ionic.Utils = {
|
|
|
3246 |
|
|
|
3247 |
arrayMove: function(arr, oldIndex, newIndex) {
|
|
|
3248 |
if (newIndex >= arr.length) {
|
|
|
3249 |
var k = newIndex - arr.length;
|
|
|
3250 |
while ((k--) + 1) {
|
|
|
3251 |
arr.push(undefined);
|
|
|
3252 |
}
|
|
|
3253 |
}
|
|
|
3254 |
arr.splice(newIndex, 0, arr.splice(oldIndex, 1)[0]);
|
|
|
3255 |
return arr;
|
|
|
3256 |
},
|
|
|
3257 |
|
|
|
3258 |
/**
|
|
|
3259 |
* Return a function that will be called with the given context
|
|
|
3260 |
*/
|
|
|
3261 |
proxy: function(func, context) {
|
|
|
3262 |
var args = Array.prototype.slice.call(arguments, 2);
|
|
|
3263 |
return function() {
|
|
|
3264 |
return func.apply(context, args.concat(Array.prototype.slice.call(arguments)));
|
|
|
3265 |
};
|
|
|
3266 |
},
|
|
|
3267 |
|
|
|
3268 |
/**
|
|
|
3269 |
* Only call a function once in the given interval.
|
|
|
3270 |
*
|
|
|
3271 |
* @param func {Function} the function to call
|
|
|
3272 |
* @param wait {int} how long to wait before/after to allow function calls
|
|
|
3273 |
* @param immediate {boolean} whether to call immediately or after the wait interval
|
|
|
3274 |
*/
|
|
|
3275 |
debounce: function(func, wait, immediate) {
|
|
|
3276 |
var timeout, args, context, timestamp, result;
|
|
|
3277 |
return function() {
|
|
|
3278 |
context = this;
|
|
|
3279 |
args = arguments;
|
|
|
3280 |
timestamp = new Date();
|
|
|
3281 |
var later = function() {
|
|
|
3282 |
var last = (new Date()) - timestamp;
|
|
|
3283 |
if (last < wait) {
|
|
|
3284 |
timeout = setTimeout(later, wait - last);
|
|
|
3285 |
} else {
|
|
|
3286 |
timeout = null;
|
|
|
3287 |
if (!immediate) result = func.apply(context, args);
|
|
|
3288 |
}
|
|
|
3289 |
};
|
|
|
3290 |
var callNow = immediate && !timeout;
|
|
|
3291 |
if (!timeout) {
|
|
|
3292 |
timeout = setTimeout(later, wait);
|
|
|
3293 |
}
|
|
|
3294 |
if (callNow) result = func.apply(context, args);
|
|
|
3295 |
return result;
|
|
|
3296 |
};
|
|
|
3297 |
},
|
|
|
3298 |
|
|
|
3299 |
/**
|
|
|
3300 |
* Throttle the given fun, only allowing it to be
|
|
|
3301 |
* called at most every `wait` ms.
|
|
|
3302 |
*/
|
|
|
3303 |
throttle: function(func, wait, options) {
|
|
|
3304 |
var context, args, result;
|
|
|
3305 |
var timeout = null;
|
|
|
3306 |
var previous = 0;
|
|
|
3307 |
options || (options = {});
|
|
|
3308 |
var later = function() {
|
|
|
3309 |
previous = options.leading === false ? 0 : Date.now();
|
|
|
3310 |
timeout = null;
|
|
|
3311 |
result = func.apply(context, args);
|
|
|
3312 |
};
|
|
|
3313 |
return function() {
|
|
|
3314 |
var now = Date.now();
|
|
|
3315 |
if (!previous && options.leading === false) previous = now;
|
|
|
3316 |
var remaining = wait - (now - previous);
|
|
|
3317 |
context = this;
|
|
|
3318 |
args = arguments;
|
|
|
3319 |
if (remaining <= 0) {
|
|
|
3320 |
clearTimeout(timeout);
|
|
|
3321 |
timeout = null;
|
|
|
3322 |
previous = now;
|
|
|
3323 |
result = func.apply(context, args);
|
|
|
3324 |
} else if (!timeout && options.trailing !== false) {
|
|
|
3325 |
timeout = setTimeout(later, remaining);
|
|
|
3326 |
}
|
|
|
3327 |
return result;
|
|
|
3328 |
};
|
|
|
3329 |
},
|
|
|
3330 |
// Borrowed from Backbone.js's extend
|
|
|
3331 |
// Helper function to correctly set up the prototype chain, for subclasses.
|
|
|
3332 |
// Similar to `goog.inherits`, but uses a hash of prototype properties and
|
|
|
3333 |
// class properties to be extended.
|
|
|
3334 |
inherit: function(protoProps, staticProps) {
|
|
|
3335 |
var parent = this;
|
|
|
3336 |
var child;
|
|
|
3337 |
|
|
|
3338 |
// The constructor function for the new subclass is either defined by you
|
|
|
3339 |
// (the "constructor" property in your `extend` definition), or defaulted
|
|
|
3340 |
// by us to simply call the parent's constructor.
|
|
|
3341 |
if (protoProps && protoProps.hasOwnProperty('constructor')) {
|
|
|
3342 |
child = protoProps.constructor;
|
|
|
3343 |
} else {
|
|
|
3344 |
child = function() { return parent.apply(this, arguments); };
|
|
|
3345 |
}
|
|
|
3346 |
|
|
|
3347 |
// Add static properties to the constructor function, if supplied.
|
|
|
3348 |
ionic.extend(child, parent, staticProps);
|
|
|
3349 |
|
|
|
3350 |
// Set the prototype chain to inherit from `parent`, without calling
|
|
|
3351 |
// `parent`'s constructor function.
|
|
|
3352 |
var Surrogate = function() { this.constructor = child; };
|
|
|
3353 |
Surrogate.prototype = parent.prototype;
|
|
|
3354 |
child.prototype = new Surrogate();
|
|
|
3355 |
|
|
|
3356 |
// Add prototype properties (instance properties) to the subclass,
|
|
|
3357 |
// if supplied.
|
|
|
3358 |
if (protoProps) ionic.extend(child.prototype, protoProps);
|
|
|
3359 |
|
|
|
3360 |
// Set a convenience property in case the parent's prototype is needed
|
|
|
3361 |
// later.
|
|
|
3362 |
child.__super__ = parent.prototype;
|
|
|
3363 |
|
|
|
3364 |
return child;
|
|
|
3365 |
},
|
|
|
3366 |
|
|
|
3367 |
// Extend adapted from Underscore.js
|
|
|
3368 |
extend: function(obj) {
|
|
|
3369 |
var args = Array.prototype.slice.call(arguments, 1);
|
|
|
3370 |
for (var i = 0; i < args.length; i++) {
|
|
|
3371 |
var source = args[i];
|
|
|
3372 |
if (source) {
|
|
|
3373 |
for (var prop in source) {
|
|
|
3374 |
obj[prop] = source[prop];
|
|
|
3375 |
}
|
|
|
3376 |
}
|
|
|
3377 |
}
|
|
|
3378 |
return obj;
|
|
|
3379 |
},
|
|
|
3380 |
|
|
|
3381 |
nextUid: function() {
|
|
|
3382 |
return 'ion' + (nextId++);
|
|
|
3383 |
},
|
|
|
3384 |
|
|
|
3385 |
disconnectScope: function disconnectScope(scope) {
|
|
|
3386 |
if (!scope) return;
|
|
|
3387 |
|
|
|
3388 |
if (scope.$root === scope) {
|
|
|
3389 |
return; // we can't disconnect the root node;
|
|
|
3390 |
}
|
|
|
3391 |
var parent = scope.$parent;
|
|
|
3392 |
scope.$$disconnected = true;
|
|
|
3393 |
scope.$broadcast('$ionic.disconnectScope', scope);
|
|
|
3394 |
|
|
|
3395 |
// See Scope.$destroy
|
|
|
3396 |
if (parent.$$childHead === scope) {
|
|
|
3397 |
parent.$$childHead = scope.$$nextSibling;
|
|
|
3398 |
}
|
|
|
3399 |
if (parent.$$childTail === scope) {
|
|
|
3400 |
parent.$$childTail = scope.$$prevSibling;
|
|
|
3401 |
}
|
|
|
3402 |
if (scope.$$prevSibling) {
|
|
|
3403 |
scope.$$prevSibling.$$nextSibling = scope.$$nextSibling;
|
|
|
3404 |
}
|
|
|
3405 |
if (scope.$$nextSibling) {
|
|
|
3406 |
scope.$$nextSibling.$$prevSibling = scope.$$prevSibling;
|
|
|
3407 |
}
|
|
|
3408 |
scope.$$nextSibling = scope.$$prevSibling = null;
|
|
|
3409 |
},
|
|
|
3410 |
|
|
|
3411 |
reconnectScope: function reconnectScope(scope) {
|
|
|
3412 |
if (!scope) return;
|
|
|
3413 |
|
|
|
3414 |
if (scope.$root === scope) {
|
|
|
3415 |
return; // we can't disconnect the root node;
|
|
|
3416 |
}
|
|
|
3417 |
if (!scope.$$disconnected) {
|
|
|
3418 |
return;
|
|
|
3419 |
}
|
|
|
3420 |
var parent = scope.$parent;
|
|
|
3421 |
scope.$$disconnected = false;
|
|
|
3422 |
scope.$broadcast('$ionic.reconnectScope', scope);
|
|
|
3423 |
// See Scope.$new for this logic...
|
|
|
3424 |
scope.$$prevSibling = parent.$$childTail;
|
|
|
3425 |
if (parent.$$childHead) {
|
|
|
3426 |
parent.$$childTail.$$nextSibling = scope;
|
|
|
3427 |
parent.$$childTail = scope;
|
|
|
3428 |
} else {
|
|
|
3429 |
parent.$$childHead = parent.$$childTail = scope;
|
|
|
3430 |
}
|
|
|
3431 |
},
|
|
|
3432 |
|
|
|
3433 |
isScopeDisconnected: function(scope) {
|
|
|
3434 |
var climbScope = scope;
|
|
|
3435 |
while (climbScope) {
|
|
|
3436 |
if (climbScope.$$disconnected) return true;
|
|
|
3437 |
climbScope = climbScope.$parent;
|
|
|
3438 |
}
|
|
|
3439 |
return false;
|
|
|
3440 |
}
|
|
|
3441 |
};
|
|
|
3442 |
|
|
|
3443 |
// Bind a few of the most useful functions to the ionic scope
|
|
|
3444 |
ionic.inherit = ionic.Utils.inherit;
|
|
|
3445 |
ionic.extend = ionic.Utils.extend;
|
|
|
3446 |
ionic.throttle = ionic.Utils.throttle;
|
|
|
3447 |
ionic.proxy = ionic.Utils.proxy;
|
|
|
3448 |
ionic.debounce = ionic.Utils.debounce;
|
|
|
3449 |
|
|
|
3450 |
})(window.ionic);
|
|
|
3451 |
|
|
|
3452 |
/**
|
|
|
3453 |
* @ngdoc page
|
|
|
3454 |
* @name keyboard
|
|
|
3455 |
* @module ionic
|
|
|
3456 |
* @description
|
|
|
3457 |
* On both Android and iOS, Ionic will attempt to prevent the keyboard from
|
|
|
3458 |
* obscuring inputs and focusable elements when it appears by scrolling them
|
|
|
3459 |
* into view. In order for this to work, any focusable elements must be within
|
|
|
3460 |
* a [Scroll View](http://ionicframework.com/docs/api/directive/ionScroll/)
|
|
|
3461 |
* or a directive such as [Content](http://ionicframework.com/docs/api/directive/ionContent/)
|
|
|
3462 |
* that has a Scroll View.
|
|
|
3463 |
*
|
|
|
3464 |
* It will also attempt to prevent the native overflow scrolling on focus,
|
|
|
3465 |
* which can cause layout issues such as pushing headers up and out of view.
|
|
|
3466 |
*
|
|
|
3467 |
* The keyboard fixes work best in conjunction with the
|
|
|
3468 |
* [Ionic Keyboard Plugin](https://github.com/driftyco/ionic-plugins-keyboard),
|
|
|
3469 |
* although it will perform reasonably well without. However, if you are using
|
|
|
3470 |
* Cordova there is no reason not to use the plugin.
|
|
|
3471 |
*
|
|
|
3472 |
* ### Hide when keyboard shows
|
|
|
3473 |
*
|
|
|
3474 |
* To hide an element when the keyboard is open, add the class `hide-on-keyboard-open`.
|
|
|
3475 |
*
|
|
|
3476 |
* ```html
|
|
|
3477 |
* <div class="hide-on-keyboard-open">
|
|
|
3478 |
* <div id="google-map"></div>
|
|
|
3479 |
* </div>
|
|
|
3480 |
* ```
|
|
|
3481 |
* ----------
|
|
|
3482 |
*
|
|
|
3483 |
* ### Plugin Usage
|
|
|
3484 |
* Information on using the plugin can be found at
|
|
|
3485 |
* [https://github.com/driftyco/ionic-plugins-keyboard](https://github.com/driftyco/ionic-plugins-keyboard).
|
|
|
3486 |
*
|
|
|
3487 |
* ----------
|
|
|
3488 |
*
|
|
|
3489 |
* ### Android Notes
|
|
|
3490 |
* - If your app is running in fullscreen, i.e. you have
|
|
|
3491 |
* `<preference name="Fullscreen" value="true" />` in your `config.xml` file
|
|
|
3492 |
* you will need to set `ionic.Platform.isFullScreen = true` manually.
|
|
|
3493 |
*
|
|
|
3494 |
* - You can configure the behavior of the web view when the keyboard shows by setting
|
|
|
3495 |
* [android:windowSoftInputMode](http://developer.android.com/reference/android/R.attr.html#windowSoftInputMode)
|
|
|
3496 |
* to either `adjustPan`, `adjustResize` or `adjustNothing` in your app's
|
|
|
3497 |
* activity in `AndroidManifest.xml`. `adjustResize` is the recommended setting
|
|
|
3498 |
* for Ionic, but if for some reason you do use `adjustPan` you will need to
|
|
|
3499 |
* set `ionic.Platform.isFullScreen = true`.
|
|
|
3500 |
*
|
|
|
3501 |
* ```xml
|
|
|
3502 |
* <activity android:windowSoftInputMode="adjustResize">
|
|
|
3503 |
*
|
|
|
3504 |
* ```
|
|
|
3505 |
*
|
|
|
3506 |
* ### iOS Notes
|
|
|
3507 |
* - If the content of your app (including the header) is being pushed up and
|
|
|
3508 |
* out of view on input focus, try setting `cordova.plugins.Keyboard.disableScroll(true)`.
|
|
|
3509 |
* This does **not** disable scrolling in the Ionic scroll view, rather it
|
|
|
3510 |
* disables the native overflow scrolling that happens automatically as a
|
|
|
3511 |
* result of focusing on inputs below the keyboard.
|
|
|
3512 |
*
|
|
|
3513 |
*/
|
|
|
3514 |
|
|
|
3515 |
/**
|
|
|
3516 |
* The current viewport height.
|
|
|
3517 |
*/
|
|
|
3518 |
var keyboardCurrentViewportHeight = 0;
|
|
|
3519 |
|
|
|
3520 |
/**
|
|
|
3521 |
* The viewport height when in portrait orientation.
|
|
|
3522 |
*/
|
|
|
3523 |
var keyboardPortraitViewportHeight = 0;
|
|
|
3524 |
|
|
|
3525 |
/**
|
|
|
3526 |
* The viewport height when in landscape orientation.
|
|
|
3527 |
*/
|
|
|
3528 |
var keyboardLandscapeViewportHeight = 0;
|
|
|
3529 |
|
|
|
3530 |
/**
|
|
|
3531 |
* The currently focused input.
|
|
|
3532 |
*/
|
|
|
3533 |
var keyboardActiveElement;
|
|
|
3534 |
|
|
|
3535 |
/**
|
|
|
3536 |
* The scroll view containing the currently focused input.
|
|
|
3537 |
*/
|
|
|
3538 |
var scrollView;
|
|
|
3539 |
|
|
|
3540 |
/**
|
|
|
3541 |
* Timer for the setInterval that polls window.innerHeight to determine whether
|
|
|
3542 |
* the layout has updated for the keyboard showing/hiding.
|
|
|
3543 |
*/
|
|
|
3544 |
var waitForResizeTimer;
|
|
|
3545 |
|
|
|
3546 |
/**
|
|
|
3547 |
* Sometimes when switching inputs or orientations, focusout will fire before
|
|
|
3548 |
* focusin, so this timer is for the small setTimeout to determine if we should
|
|
|
3549 |
* really focusout/hide the keyboard.
|
|
|
3550 |
*/
|
|
|
3551 |
var keyboardFocusOutTimer;
|
|
|
3552 |
|
|
|
3553 |
/**
|
|
|
3554 |
* on Android, orientationchange will fire before the keyboard plugin notifies
|
|
|
3555 |
* the browser that the keyboard will show/is showing, so this flag indicates
|
|
|
3556 |
* to nativeShow that there was an orientationChange and we should update
|
|
|
3557 |
* the viewport height with an accurate keyboard height value
|
|
|
3558 |
*/
|
|
|
3559 |
var wasOrientationChange = false;
|
|
|
3560 |
|
|
|
3561 |
/**
|
|
|
3562 |
* CSS class added to the body indicating the keyboard is open.
|
|
|
3563 |
*/
|
|
|
3564 |
var KEYBOARD_OPEN_CSS = 'keyboard-open';
|
|
|
3565 |
|
|
|
3566 |
/**
|
|
|
3567 |
* CSS class that indicates a scroll container.
|
|
|
3568 |
*/
|
|
|
3569 |
var SCROLL_CONTAINER_CSS = 'scroll-content';
|
|
|
3570 |
|
|
|
3571 |
/**
|
|
|
3572 |
* Debounced keyboardFocusIn function
|
|
|
3573 |
*/
|
|
|
3574 |
var debouncedKeyboardFocusIn = ionic.debounce(keyboardFocusIn, 200, true);
|
|
|
3575 |
|
|
|
3576 |
/**
|
|
|
3577 |
* Debounced keyboardNativeShow function
|
|
|
3578 |
*/
|
|
|
3579 |
var debouncedKeyboardNativeShow = ionic.debounce(keyboardNativeShow, 100, true);
|
|
|
3580 |
|
|
|
3581 |
/**
|
|
|
3582 |
* Ionic keyboard namespace.
|
|
|
3583 |
* @namespace keyboard
|
|
|
3584 |
*/
|
|
|
3585 |
ionic.keyboard = {
|
|
|
3586 |
|
|
|
3587 |
/**
|
|
|
3588 |
* Whether the keyboard is open or not.
|
|
|
3589 |
*/
|
|
|
3590 |
isOpen: false,
|
|
|
3591 |
|
|
|
3592 |
/**
|
|
|
3593 |
* Whether the keyboard is closing or not.
|
|
|
3594 |
*/
|
|
|
3595 |
isClosing: false,
|
|
|
3596 |
|
|
|
3597 |
/**
|
|
|
3598 |
* Whether the keyboard is opening or not.
|
|
|
3599 |
*/
|
|
|
3600 |
isOpening: false,
|
|
|
3601 |
|
|
|
3602 |
/**
|
|
|
3603 |
* The height of the keyboard in pixels, as reported by the keyboard plugin.
|
|
|
3604 |
* If the plugin is not available, calculated as the difference in
|
|
|
3605 |
* window.innerHeight after the keyboard has shown.
|
|
|
3606 |
*/
|
|
|
3607 |
height: 0,
|
|
|
3608 |
|
|
|
3609 |
/**
|
|
|
3610 |
* Whether the device is in landscape orientation or not.
|
|
|
3611 |
*/
|
|
|
3612 |
isLandscape: false,
|
|
|
3613 |
|
|
|
3614 |
/**
|
|
|
3615 |
* Whether the keyboard event listeners have been added or not
|
|
|
3616 |
*/
|
|
|
3617 |
isInitialized: false,
|
|
|
3618 |
|
|
|
3619 |
/**
|
|
|
3620 |
* Hide the keyboard, if it is open.
|
|
|
3621 |
*/
|
|
|
3622 |
hide: function() {
|
|
|
3623 |
if (keyboardHasPlugin()) {
|
|
|
3624 |
cordova.plugins.Keyboard.close();
|
|
|
3625 |
}
|
|
|
3626 |
keyboardActiveElement && keyboardActiveElement.blur();
|
|
|
3627 |
},
|
|
|
3628 |
|
|
|
3629 |
/**
|
|
|
3630 |
* An alias for cordova.plugins.Keyboard.show(). If the keyboard plugin
|
|
|
3631 |
* is installed, show the keyboard.
|
|
|
3632 |
*/
|
|
|
3633 |
show: function() {
|
|
|
3634 |
if (keyboardHasPlugin()) {
|
|
|
3635 |
cordova.plugins.Keyboard.show();
|
|
|
3636 |
}
|
|
|
3637 |
},
|
|
|
3638 |
|
|
|
3639 |
/**
|
|
|
3640 |
* Remove all keyboard related event listeners, effectively disabling Ionic's
|
|
|
3641 |
* keyboard adjustments.
|
|
|
3642 |
*/
|
|
|
3643 |
disable: function() {
|
|
|
3644 |
if (keyboardHasPlugin()) {
|
|
|
3645 |
window.removeEventListener('native.keyboardshow', debouncedKeyboardNativeShow );
|
|
|
3646 |
window.removeEventListener('native.keyboardhide', keyboardFocusOut);
|
|
|
3647 |
} else {
|
|
|
3648 |
document.body.removeEventListener('focusout', keyboardFocusOut);
|
|
|
3649 |
}
|
|
|
3650 |
|
|
|
3651 |
document.body.removeEventListener('ionic.focusin', debouncedKeyboardFocusIn);
|
|
|
3652 |
document.body.removeEventListener('focusin', debouncedKeyboardFocusIn);
|
|
|
3653 |
|
|
|
3654 |
window.removeEventListener('orientationchange', keyboardOrientationChange);
|
|
|
3655 |
|
|
|
3656 |
if ( window.navigator.msPointerEnabled ) {
|
|
|
3657 |
document.removeEventListener("MSPointerDown", keyboardInit);
|
|
|
3658 |
} else {
|
|
|
3659 |
document.removeEventListener('touchstart', keyboardInit);
|
|
|
3660 |
}
|
|
|
3661 |
ionic.keyboard.isInitialized = false;
|
|
|
3662 |
},
|
|
|
3663 |
|
|
|
3664 |
/**
|
|
|
3665 |
* Alias for keyboardInit, initialize all keyboard related event listeners.
|
|
|
3666 |
*/
|
|
|
3667 |
enable: function() {
|
|
|
3668 |
keyboardInit();
|
|
|
3669 |
}
|
|
|
3670 |
};
|
|
|
3671 |
|
|
|
3672 |
// Initialize the viewport height (after ionic.keyboard.height has been
|
|
|
3673 |
// defined).
|
|
|
3674 |
keyboardCurrentViewportHeight = getViewportHeight();
|
|
|
3675 |
|
|
|
3676 |
|
|
|
3677 |
/* Event handlers */
|
|
|
3678 |
/* ------------------------------------------------------------------------- */
|
|
|
3679 |
|
|
|
3680 |
/**
|
|
|
3681 |
* Event handler for first touch event, initializes all event listeners
|
|
|
3682 |
* for keyboard related events. Also aliased by ionic.keyboard.enable.
|
|
|
3683 |
*/
|
|
|
3684 |
function keyboardInit() {
|
|
|
3685 |
|
|
|
3686 |
if (ionic.keyboard.isInitialized) return;
|
|
|
3687 |
|
|
|
3688 |
if (keyboardHasPlugin()) {
|
|
|
3689 |
window.addEventListener('native.keyboardshow', debouncedKeyboardNativeShow);
|
|
|
3690 |
window.addEventListener('native.keyboardhide', keyboardFocusOut);
|
|
|
3691 |
} else {
|
|
|
3692 |
document.body.addEventListener('focusout', keyboardFocusOut);
|
|
|
3693 |
}
|
|
|
3694 |
|
|
|
3695 |
document.body.addEventListener('ionic.focusin', debouncedKeyboardFocusIn);
|
|
|
3696 |
document.body.addEventListener('focusin', debouncedKeyboardFocusIn);
|
|
|
3697 |
|
|
|
3698 |
if (window.navigator.msPointerEnabled) {
|
|
|
3699 |
document.removeEventListener("MSPointerDown", keyboardInit);
|
|
|
3700 |
} else {
|
|
|
3701 |
document.removeEventListener('touchstart', keyboardInit);
|
|
|
3702 |
}
|
|
|
3703 |
|
|
|
3704 |
ionic.keyboard.isInitialized = true;
|
|
|
3705 |
}
|
|
|
3706 |
|
|
|
3707 |
/**
|
|
|
3708 |
* Event handler for 'native.keyboardshow' event, sets keyboard.height to the
|
|
|
3709 |
* reported height and keyboard.isOpening to true. Then calls
|
|
|
3710 |
* keyboardWaitForResize with keyboardShow or keyboardUpdateViewportHeight as
|
|
|
3711 |
* the callback depending on whether the event was triggered by a focusin or
|
|
|
3712 |
* an orientationchange.
|
|
|
3713 |
*/
|
|
|
3714 |
function keyboardNativeShow(e) {
|
|
|
3715 |
clearTimeout(keyboardFocusOutTimer);
|
|
|
3716 |
//console.log("keyboardNativeShow fired at: " + Date.now());
|
|
|
3717 |
//console.log("keyboardNativeshow window.innerHeight: " + window.innerHeight);
|
|
|
3718 |
|
|
|
3719 |
if (!ionic.keyboard.isOpen || ionic.keyboard.isClosing) {
|
|
|
3720 |
ionic.keyboard.isOpening = true;
|
|
|
3721 |
ionic.keyboard.isClosing = false;
|
|
|
3722 |
}
|
|
|
3723 |
|
|
|
3724 |
ionic.keyboard.height = e.keyboardHeight;
|
|
|
3725 |
//console.log('nativeshow keyboard height:' + e.keyboardHeight);
|
|
|
3726 |
|
|
|
3727 |
if (wasOrientationChange) {
|
|
|
3728 |
keyboardWaitForResize(keyboardUpdateViewportHeight, true);
|
|
|
3729 |
} else {
|
|
|
3730 |
keyboardWaitForResize(keyboardShow, true);
|
|
|
3731 |
}
|
|
|
3732 |
}
|
|
|
3733 |
|
|
|
3734 |
/**
|
|
|
3735 |
* Event handler for 'focusin' and 'ionic.focusin' events. Initializes
|
|
|
3736 |
* keyboard state (keyboardActiveElement and keyboard.isOpening) for the
|
|
|
3737 |
* appropriate adjustments once the window has resized. If not using the
|
|
|
3738 |
* keyboard plugin, calls keyboardWaitForResize with keyboardShow as the
|
|
|
3739 |
* callback or keyboardShow right away if the keyboard is already open. If
|
|
|
3740 |
* using the keyboard plugin does nothing and lets keyboardNativeShow handle
|
|
|
3741 |
* adjustments with a more accurate keyboard height.
|
|
|
3742 |
*/
|
|
|
3743 |
function keyboardFocusIn(e) {
|
|
|
3744 |
clearTimeout(keyboardFocusOutTimer);
|
|
|
3745 |
//console.log("keyboardFocusIn from: " + e.type + " at: " + Date.now());
|
|
|
3746 |
|
|
|
3747 |
if (!e.target ||
|
|
|
3748 |
e.target.readOnly ||
|
|
|
3749 |
!ionic.tap.isKeyboardElement(e.target) ||
|
|
|
3750 |
!(scrollView = inputScrollView(e.target))) {
|
|
|
3751 |
keyboardActiveElement = null;
|
|
|
3752 |
return;
|
|
|
3753 |
}
|
|
|
3754 |
|
|
|
3755 |
keyboardActiveElement = e.target;
|
|
|
3756 |
|
|
|
3757 |
// if using JS scrolling, undo the effects of native overflow scroll so the
|
|
|
3758 |
// scroll view is positioned correctly
|
|
|
3759 |
document.body.scrollTop = 0;
|
|
|
3760 |
scrollView.scrollTop = 0;
|
|
|
3761 |
ionic.requestAnimationFrame(function(){
|
|
|
3762 |
document.body.scrollTop = 0;
|
|
|
3763 |
scrollView.scrollTop = 0;
|
|
|
3764 |
});
|
|
|
3765 |
|
|
|
3766 |
if (!ionic.keyboard.isOpen || ionic.keyboard.isClosing) {
|
|
|
3767 |
ionic.keyboard.isOpening = true;
|
|
|
3768 |
ionic.keyboard.isClosing = false;
|
|
|
3769 |
}
|
|
|
3770 |
|
|
|
3771 |
// attempt to prevent browser from natively scrolling input into view while
|
|
|
3772 |
// we are trying to do the same (while we are scrolling) if the user taps the
|
|
|
3773 |
// keyboard
|
|
|
3774 |
document.addEventListener('keydown', keyboardOnKeyDown, false);
|
|
|
3775 |
|
|
|
3776 |
// any showing part of the document that isn't within the scroll the user
|
|
|
3777 |
// could touchmove and cause some ugly changes to the app, so disable
|
|
|
3778 |
// any touchmove events while the keyboard is open using e.preventDefault()
|
|
|
3779 |
if (window.navigator.msPointerEnabled) {
|
|
|
3780 |
document.addEventListener("MSPointerMove", keyboardPreventDefault, false);
|
|
|
3781 |
} else {
|
|
|
3782 |
document.addEventListener('touchmove', keyboardPreventDefault, false);
|
|
|
3783 |
}
|
|
|
3784 |
|
|
|
3785 |
// if we aren't using the plugin and the keyboard isn't open yet, wait for the
|
|
|
3786 |
// window to resize so we can get an accurate estimate of the keyboard size,
|
|
|
3787 |
// otherwise we do nothing and let nativeShow call keyboardShow once we have
|
|
|
3788 |
// an exact keyboard height
|
|
|
3789 |
// if the keyboard is already open, go ahead and scroll the input into view
|
|
|
3790 |
// if necessary
|
|
|
3791 |
if (!ionic.keyboard.isOpen && !keyboardHasPlugin()) {
|
|
|
3792 |
keyboardWaitForResize(keyboardShow, true);
|
|
|
3793 |
|
|
|
3794 |
} else if (ionic.keyboard.isOpen) {
|
|
|
3795 |
keyboardShow();
|
|
|
3796 |
}
|
|
|
3797 |
}
|
|
|
3798 |
|
|
|
3799 |
/**
|
|
|
3800 |
* Event handler for 'focusout' events. Sets keyboard.isClosing to true and
|
|
|
3801 |
* calls keyboardWaitForResize with keyboardHide as the callback after a small
|
|
|
3802 |
* timeout.
|
|
|
3803 |
*/
|
|
|
3804 |
function keyboardFocusOut() {
|
|
|
3805 |
clearTimeout(keyboardFocusOutTimer);
|
|
|
3806 |
//console.log("keyboardFocusOut fired at: " + Date.now());
|
|
|
3807 |
//console.log("keyboardFocusOut event type: " + e.type);
|
|
|
3808 |
|
|
|
3809 |
if (ionic.keyboard.isOpen || ionic.keyboard.isOpening) {
|
|
|
3810 |
ionic.keyboard.isClosing = true;
|
|
|
3811 |
ionic.keyboard.isOpening = false;
|
|
|
3812 |
}
|
|
|
3813 |
|
|
|
3814 |
// Call keyboardHide with a slight delay because sometimes on focus or
|
|
|
3815 |
// orientation change focusin is called immediately after, so we give it time
|
|
|
3816 |
// to cancel keyboardHide
|
|
|
3817 |
keyboardFocusOutTimer = setTimeout(function() {
|
|
|
3818 |
ionic.requestAnimationFrame(function() {
|
|
|
3819 |
// focusOut during or right after an orientationchange, so we didn't get
|
|
|
3820 |
// a chance to update the viewport height yet, do it and keyboardHide
|
|
|
3821 |
//console.log("focusOut, wasOrientationChange: " + wasOrientationChange);
|
|
|
3822 |
if (wasOrientationChange) {
|
|
|
3823 |
keyboardWaitForResize(function(){
|
|
|
3824 |
keyboardUpdateViewportHeight();
|
|
|
3825 |
keyboardHide();
|
|
|
3826 |
}, false);
|
|
|
3827 |
} else {
|
|
|
3828 |
keyboardWaitForResize(keyboardHide, false);
|
|
|
3829 |
}
|
|
|
3830 |
});
|
|
|
3831 |
}, 50);
|
|
|
3832 |
}
|
|
|
3833 |
|
|
|
3834 |
/**
|
|
|
3835 |
* Event handler for 'orientationchange' events. If using the keyboard plugin
|
|
|
3836 |
* and the keyboard is open on Android, sets wasOrientationChange to true so
|
|
|
3837 |
* nativeShow can update the viewport height with an accurate keyboard height.
|
|
|
3838 |
* If the keyboard isn't open or keyboard plugin isn't being used,
|
|
|
3839 |
* waits for the window to resize before updating the viewport height.
|
|
|
3840 |
*
|
|
|
3841 |
* On iOS, where orientationchange fires after the keyboard has already shown,
|
|
|
3842 |
* updates the viewport immediately, regardless of if the keyboard is already
|
|
|
3843 |
* open.
|
|
|
3844 |
*/
|
|
|
3845 |
function keyboardOrientationChange() {
|
|
|
3846 |
//console.log("orientationchange fired at: " + Date.now());
|
|
|
3847 |
//console.log("orientation was: " + (ionic.keyboard.isLandscape ? "landscape" : "portrait"));
|
|
|
3848 |
|
|
|
3849 |
// toggle orientation
|
|
|
3850 |
ionic.keyboard.isLandscape = !ionic.keyboard.isLandscape;
|
|
|
3851 |
// //console.log("now orientation is: " + (ionic.keyboard.isLandscape ? "landscape" : "portrait"));
|
|
|
3852 |
|
|
|
3853 |
// no need to wait for resizing on iOS, and orientationchange always fires
|
|
|
3854 |
// after the keyboard has opened, so it doesn't matter if it's open or not
|
|
|
3855 |
if (ionic.Platform.isIOS()) {
|
|
|
3856 |
keyboardUpdateViewportHeight();
|
|
|
3857 |
}
|
|
|
3858 |
|
|
|
3859 |
// On Android, if the keyboard isn't open or we aren't using the keyboard
|
|
|
3860 |
// plugin, update the viewport height once everything has resized. If the
|
|
|
3861 |
// keyboard is open and we are using the keyboard plugin do nothing and let
|
|
|
3862 |
// nativeShow handle it using an accurate keyboard height.
|
|
|
3863 |
if ( ionic.Platform.isAndroid()) {
|
|
|
3864 |
if (!ionic.keyboard.isOpen || !keyboardHasPlugin()) {
|
|
|
3865 |
keyboardWaitForResize(keyboardUpdateViewportHeight, false);
|
|
|
3866 |
} else {
|
|
|
3867 |
wasOrientationChange = true;
|
|
|
3868 |
}
|
|
|
3869 |
}
|
|
|
3870 |
}
|
|
|
3871 |
|
|
|
3872 |
/**
|
|
|
3873 |
* Event handler for 'keydown' event. Tries to prevent browser from natively
|
|
|
3874 |
* scrolling an input into view when a user taps the keyboard while we are
|
|
|
3875 |
* scrolling the input into view ourselves with JS.
|
|
|
3876 |
*/
|
|
|
3877 |
function keyboardOnKeyDown(e) {
|
|
|
3878 |
if (ionic.scroll.isScrolling) {
|
|
|
3879 |
keyboardPreventDefault(e);
|
|
|
3880 |
}
|
|
|
3881 |
}
|
|
|
3882 |
|
|
|
3883 |
/**
|
|
|
3884 |
* Event for 'touchmove' or 'MSPointerMove'. Prevents native scrolling on
|
|
|
3885 |
* elements outside the scroll view while the keyboard is open.
|
|
|
3886 |
*/
|
|
|
3887 |
function keyboardPreventDefault(e) {
|
|
|
3888 |
if (e.target.tagName !== 'TEXTAREA') {
|
|
|
3889 |
e.preventDefault();
|
|
|
3890 |
}
|
|
|
3891 |
}
|
|
|
3892 |
|
|
|
3893 |
/* Private API */
|
|
|
3894 |
/* -------------------------------------------------------------------------- */
|
|
|
3895 |
|
|
|
3896 |
/**
|
|
|
3897 |
* Polls window.innerHeight until it has updated to an expected value (or
|
|
|
3898 |
* sufficient time has passed) before calling the specified callback function.
|
|
|
3899 |
* Only necessary for non-fullscreen Android which sometimes reports multiple
|
|
|
3900 |
* window.innerHeight values during interim layouts while it is resizing.
|
|
|
3901 |
*
|
|
|
3902 |
* On iOS, the window.innerHeight will already be updated, but we use the 50ms
|
|
|
3903 |
* delay as essentially a timeout so that scroll view adjustments happen after
|
|
|
3904 |
* the keyboard has shown so there isn't a white flash from us resizing too
|
|
|
3905 |
* quickly.
|
|
|
3906 |
*
|
|
|
3907 |
* @param {Function} callback the function to call once the window has resized
|
|
|
3908 |
* @param {boolean} isOpening whether the resize is from the keyboard opening
|
|
|
3909 |
* or not
|
|
|
3910 |
*/
|
|
|
3911 |
function keyboardWaitForResize(callback, isOpening) {
|
|
|
3912 |
clearInterval(waitForResizeTimer);
|
|
|
3913 |
var count = 0;
|
|
|
3914 |
var maxCount;
|
|
|
3915 |
var initialHeight = getViewportHeight();
|
|
|
3916 |
var viewportHeight = initialHeight;
|
|
|
3917 |
|
|
|
3918 |
//console.log("waitForResize initial viewport height: " + viewportHeight);
|
|
|
3919 |
//var start = Date.now();
|
|
|
3920 |
//console.log("start: " + start);
|
|
|
3921 |
|
|
|
3922 |
// want to fail relatively quickly on modern android devices, since it's much
|
|
|
3923 |
// more likely we just have a bad keyboard height
|
|
|
3924 |
if (ionic.Platform.isAndroid() && ionic.Platform.version() < 4.4) {
|
|
|
3925 |
maxCount = 30;
|
|
|
3926 |
} else if (ionic.Platform.isAndroid()) {
|
|
|
3927 |
maxCount = 10;
|
|
|
3928 |
} else {
|
|
|
3929 |
maxCount = 1;
|
|
|
3930 |
}
|
|
|
3931 |
|
|
|
3932 |
// poll timer
|
|
|
3933 |
waitForResizeTimer = setInterval(function(){
|
|
|
3934 |
viewportHeight = getViewportHeight();
|
|
|
3935 |
|
|
|
3936 |
// height hasn't updated yet, try again in 50ms
|
|
|
3937 |
// if not using plugin, wait for maxCount to ensure we have waited long enough
|
|
|
3938 |
// to get an accurate keyboard height
|
|
|
3939 |
if (++count < maxCount &&
|
|
|
3940 |
((!isPortraitViewportHeight(viewportHeight) &&
|
|
|
3941 |
!isLandscapeViewportHeight(viewportHeight)) ||
|
|
|
3942 |
!ionic.keyboard.height)) {
|
|
|
3943 |
return;
|
|
|
3944 |
}
|
|
|
3945 |
|
|
|
3946 |
// infer the keyboard height from the resize if not using the keyboard plugin
|
|
|
3947 |
if (!keyboardHasPlugin()) {
|
|
|
3948 |
ionic.keyboard.height = Math.abs(initialHeight - window.innerHeight);
|
|
|
3949 |
}
|
|
|
3950 |
|
|
|
3951 |
// set to true if we were waiting for the keyboard to open
|
|
|
3952 |
ionic.keyboard.isOpen = isOpening;
|
|
|
3953 |
|
|
|
3954 |
clearInterval(waitForResizeTimer);
|
|
|
3955 |
//var end = Date.now();
|
|
|
3956 |
//console.log("waitForResize count: " + count);
|
|
|
3957 |
//console.log("end: " + end);
|
|
|
3958 |
//console.log("difference: " + ( end - start ) + "ms");
|
|
|
3959 |
|
|
|
3960 |
//console.log("callback: " + callback.name);
|
|
|
3961 |
callback();
|
|
|
3962 |
|
|
|
3963 |
}, 50);
|
|
|
3964 |
|
|
|
3965 |
return maxCount; //for tests
|
|
|
3966 |
}
|
|
|
3967 |
|
|
|
3968 |
/**
|
|
|
3969 |
* On keyboard close sets keyboard state to closed, resets the scroll view,
|
|
|
3970 |
* removes CSS from body indicating keyboard was open, removes any event
|
|
|
3971 |
* listeners for when the keyboard is open and on Android blurs the active
|
|
|
3972 |
* element (which in some cases will still have focus even if the keyboard
|
|
|
3973 |
* is closed and can cause it to reappear on subsequent taps).
|
|
|
3974 |
*/
|
|
|
3975 |
function keyboardHide() {
|
|
|
3976 |
clearTimeout(keyboardFocusOutTimer);
|
|
|
3977 |
//console.log("keyboardHide");
|
|
|
3978 |
|
|
|
3979 |
ionic.keyboard.isOpen = false;
|
|
|
3980 |
ionic.keyboard.isClosing = false;
|
|
|
3981 |
|
|
|
3982 |
if (keyboardActiveElement) {
|
|
|
3983 |
ionic.trigger('resetScrollView', {
|
|
|
3984 |
target: keyboardActiveElement
|
|
|
3985 |
}, true);
|
|
|
3986 |
}
|
|
|
3987 |
|
|
|
3988 |
ionic.requestAnimationFrame(function(){
|
|
|
3989 |
document.body.classList.remove(KEYBOARD_OPEN_CSS);
|
|
|
3990 |
});
|
|
|
3991 |
|
|
|
3992 |
// the keyboard is gone now, remove the touchmove that disables native scroll
|
|
|
3993 |
if (window.navigator.msPointerEnabled) {
|
|
|
3994 |
document.removeEventListener("MSPointerMove", keyboardPreventDefault);
|
|
|
3995 |
} else {
|
|
|
3996 |
document.removeEventListener('touchmove', keyboardPreventDefault);
|
|
|
3997 |
}
|
|
|
3998 |
document.removeEventListener('keydown', keyboardOnKeyDown);
|
|
|
3999 |
|
|
|
4000 |
if (ionic.Platform.isAndroid()) {
|
|
|
4001 |
// on android closing the keyboard with the back/dismiss button won't remove
|
|
|
4002 |
// focus and keyboard can re-appear on subsequent taps (like scrolling)
|
|
|
4003 |
if (keyboardHasPlugin()) cordova.plugins.Keyboard.close();
|
|
|
4004 |
keyboardActiveElement && keyboardActiveElement.blur();
|
|
|
4005 |
}
|
|
|
4006 |
|
|
|
4007 |
keyboardActiveElement = null;
|
|
|
4008 |
}
|
|
|
4009 |
|
|
|
4010 |
/**
|
|
|
4011 |
* On keyboard open sets keyboard state to open, adds CSS to the body
|
|
|
4012 |
* indicating the keyboard is open and tells the scroll view to resize and
|
|
|
4013 |
* the currently focused input into view if necessary.
|
|
|
4014 |
*/
|
|
|
4015 |
function keyboardShow() {
|
|
|
4016 |
|
|
|
4017 |
ionic.keyboard.isOpen = true;
|
|
|
4018 |
ionic.keyboard.isOpening = false;
|
|
|
4019 |
|
|
|
4020 |
var details = {
|
|
|
4021 |
keyboardHeight: keyboardGetHeight(),
|
|
|
4022 |
viewportHeight: keyboardCurrentViewportHeight
|
|
|
4023 |
};
|
|
|
4024 |
|
|
|
4025 |
if (keyboardActiveElement) {
|
|
|
4026 |
details.target = keyboardActiveElement;
|
|
|
4027 |
|
|
|
4028 |
var elementBounds = keyboardActiveElement.getBoundingClientRect();
|
|
|
4029 |
|
|
|
4030 |
details.elementTop = Math.round(elementBounds.top);
|
|
|
4031 |
details.elementBottom = Math.round(elementBounds.bottom);
|
|
|
4032 |
|
|
|
4033 |
details.windowHeight = details.viewportHeight - details.keyboardHeight;
|
|
|
4034 |
//console.log("keyboardShow viewportHeight: " + details.viewportHeight +
|
|
|
4035 |
//", windowHeight: " + details.windowHeight +
|
|
|
4036 |
//", keyboardHeight: " + details.keyboardHeight);
|
|
|
4037 |
|
|
|
4038 |
// figure out if the element is under the keyboard
|
|
|
4039 |
details.isElementUnderKeyboard = (details.elementBottom > details.windowHeight);
|
|
|
4040 |
//console.log("isUnderKeyboard: " + details.isElementUnderKeyboard);
|
|
|
4041 |
//console.log("elementBottom: " + details.elementBottom);
|
|
|
4042 |
|
|
|
4043 |
// send event so the scroll view adjusts
|
|
|
4044 |
ionic.trigger('scrollChildIntoView', details, true);
|
|
|
4045 |
}
|
|
|
4046 |
|
|
|
4047 |
setTimeout(function(){
|
|
|
4048 |
document.body.classList.add(KEYBOARD_OPEN_CSS);
|
|
|
4049 |
}, 400);
|
|
|
4050 |
|
|
|
4051 |
return details; //for testing
|
|
|
4052 |
}
|
|
|
4053 |
|
|
|
4054 |
/* eslint no-unused-vars:0 */
|
|
|
4055 |
function keyboardGetHeight() {
|
|
|
4056 |
// check if we already have a keyboard height from the plugin or resize calculations
|
|
|
4057 |
if (ionic.keyboard.height) {
|
|
|
4058 |
return ionic.keyboard.height;
|
|
|
4059 |
}
|
|
|
4060 |
|
|
|
4061 |
if (ionic.Platform.isAndroid()) {
|
|
|
4062 |
// should be using the plugin, no way to know how big the keyboard is, so guess
|
|
|
4063 |
if ( ionic.Platform.isFullScreen ) {
|
|
|
4064 |
return 275;
|
|
|
4065 |
}
|
|
|
4066 |
// otherwise just calculate it
|
|
|
4067 |
var contentHeight = window.innerHeight;
|
|
|
4068 |
if (contentHeight < keyboardCurrentViewportHeight) {
|
|
|
4069 |
return keyboardCurrentViewportHeight - contentHeight;
|
|
|
4070 |
} else {
|
|
|
4071 |
return 0;
|
|
|
4072 |
}
|
|
|
4073 |
}
|
|
|
4074 |
|
|
|
4075 |
// fallback for when it's the webview without the plugin
|
|
|
4076 |
// or for just the standard web browser
|
|
|
4077 |
// TODO: have these be based on device
|
|
|
4078 |
if (ionic.Platform.isIOS()) {
|
|
|
4079 |
if (ionic.keyboard.isLandscape) {
|
|
|
4080 |
return 206;
|
|
|
4081 |
}
|
|
|
4082 |
|
|
|
4083 |
if (!ionic.Platform.isWebView()) {
|
|
|
4084 |
return 216;
|
|
|
4085 |
}
|
|
|
4086 |
|
|
|
4087 |
return 260;
|
|
|
4088 |
}
|
|
|
4089 |
|
|
|
4090 |
// safe guess
|
|
|
4091 |
return 275;
|
|
|
4092 |
}
|
|
|
4093 |
|
|
|
4094 |
function isPortraitViewportHeight(viewportHeight) {
|
|
|
4095 |
return !!(!ionic.keyboard.isLandscape &&
|
|
|
4096 |
keyboardPortraitViewportHeight &&
|
|
|
4097 |
(Math.abs(keyboardPortraitViewportHeight - viewportHeight) < 2));
|
|
|
4098 |
}
|
|
|
4099 |
|
|
|
4100 |
function isLandscapeViewportHeight(viewportHeight) {
|
|
|
4101 |
return !!(ionic.keyboard.isLandscape &&
|
|
|
4102 |
keyboardLandscapeViewportHeight &&
|
|
|
4103 |
(Math.abs(keyboardLandscapeViewportHeight - viewportHeight) < 2));
|
|
|
4104 |
}
|
|
|
4105 |
|
|
|
4106 |
function keyboardUpdateViewportHeight() {
|
|
|
4107 |
wasOrientationChange = false;
|
|
|
4108 |
keyboardCurrentViewportHeight = getViewportHeight();
|
|
|
4109 |
|
|
|
4110 |
if (ionic.keyboard.isLandscape && !keyboardLandscapeViewportHeight) {
|
|
|
4111 |
//console.log("saved landscape: " + keyboardCurrentViewportHeight);
|
|
|
4112 |
keyboardLandscapeViewportHeight = keyboardCurrentViewportHeight;
|
|
|
4113 |
|
|
|
4114 |
} else if (!ionic.keyboard.isLandscape && !keyboardPortraitViewportHeight) {
|
|
|
4115 |
//console.log("saved portrait: " + keyboardCurrentViewportHeight);
|
|
|
4116 |
keyboardPortraitViewportHeight = keyboardCurrentViewportHeight;
|
|
|
4117 |
}
|
|
|
4118 |
|
|
|
4119 |
if (keyboardActiveElement) {
|
|
|
4120 |
ionic.trigger('resetScrollView', {
|
|
|
4121 |
target: keyboardActiveElement
|
|
|
4122 |
}, true);
|
|
|
4123 |
}
|
|
|
4124 |
|
|
|
4125 |
if (ionic.keyboard.isOpen && ionic.tap.isTextInput(keyboardActiveElement)) {
|
|
|
4126 |
keyboardShow();
|
|
|
4127 |
}
|
|
|
4128 |
}
|
|
|
4129 |
|
|
|
4130 |
function keyboardInitViewportHeight() {
|
|
|
4131 |
var viewportHeight = getViewportHeight();
|
|
|
4132 |
//console.log("Keyboard init VP: " + viewportHeight + " " + window.innerWidth);
|
|
|
4133 |
// can't just use window.innerHeight in case the keyboard is opened immediately
|
|
|
4134 |
if ((viewportHeight / window.innerWidth) < 1) {
|
|
|
4135 |
ionic.keyboard.isLandscape = true;
|
|
|
4136 |
}
|
|
|
4137 |
//console.log("ionic.keyboard.isLandscape is: " + ionic.keyboard.isLandscape);
|
|
|
4138 |
|
|
|
4139 |
// initialize or update the current viewport height values
|
|
|
4140 |
keyboardCurrentViewportHeight = viewportHeight;
|
|
|
4141 |
if (ionic.keyboard.isLandscape && !keyboardLandscapeViewportHeight) {
|
|
|
4142 |
keyboardLandscapeViewportHeight = keyboardCurrentViewportHeight;
|
|
|
4143 |
} else if (!ionic.keyboard.isLandscape && !keyboardPortraitViewportHeight) {
|
|
|
4144 |
keyboardPortraitViewportHeight = keyboardCurrentViewportHeight;
|
|
|
4145 |
}
|
|
|
4146 |
}
|
|
|
4147 |
|
|
|
4148 |
function getViewportHeight() {
|
|
|
4149 |
var windowHeight = window.innerHeight;
|
|
|
4150 |
//console.log('window.innerHeight is: ' + windowHeight);
|
|
|
4151 |
//console.log('kb height is: ' + ionic.keyboard.height);
|
|
|
4152 |
//console.log('kb isOpen: ' + ionic.keyboard.isOpen);
|
|
|
4153 |
|
|
|
4154 |
//TODO: add iPad undocked/split kb once kb plugin supports it
|
|
|
4155 |
// the keyboard overlays the window on Android fullscreen
|
|
|
4156 |
if (!(ionic.Platform.isAndroid() && ionic.Platform.isFullScreen) &&
|
|
|
4157 |
(ionic.keyboard.isOpen || ionic.keyboard.isOpening) &&
|
|
|
4158 |
!ionic.keyboard.isClosing) {
|
|
|
4159 |
|
|
|
4160 |
return windowHeight + keyboardGetHeight();
|
|
|
4161 |
}
|
|
|
4162 |
return windowHeight;
|
|
|
4163 |
}
|
|
|
4164 |
|
|
|
4165 |
function inputScrollView(ele) {
|
|
|
4166 |
while(ele) {
|
|
|
4167 |
if (ele.classList.contains(SCROLL_CONTAINER_CSS)) {
|
|
|
4168 |
return ele;
|
|
|
4169 |
}
|
|
|
4170 |
ele = ele.parentElement;
|
|
|
4171 |
}
|
|
|
4172 |
return null;
|
|
|
4173 |
}
|
|
|
4174 |
|
|
|
4175 |
function keyboardHasPlugin() {
|
|
|
4176 |
return !!(window.cordova && cordova.plugins && cordova.plugins.Keyboard);
|
|
|
4177 |
}
|
|
|
4178 |
|
|
|
4179 |
ionic.Platform.ready(function() {
|
|
|
4180 |
keyboardInitViewportHeight();
|
|
|
4181 |
|
|
|
4182 |
window.addEventListener('orientationchange', keyboardOrientationChange);
|
|
|
4183 |
|
|
|
4184 |
// if orientation changes while app is in background, update on resuming
|
|
|
4185 |
/*
|
|
|
4186 |
if ( ionic.Platform.isWebView() ) {
|
|
|
4187 |
document.addEventListener('resume', keyboardInitViewportHeight);
|
|
|
4188 |
|
|
|
4189 |
if (ionic.Platform.isAndroid()) {
|
|
|
4190 |
//TODO: onbackpressed to detect keyboard close without focusout or plugin
|
|
|
4191 |
}
|
|
|
4192 |
}
|
|
|
4193 |
*/
|
|
|
4194 |
|
|
|
4195 |
// if orientation changes while app is in background, update on resuming
|
|
|
4196 |
/* if ( ionic.Platform.isWebView() ) {
|
|
|
4197 |
document.addEventListener('pause', function() {
|
|
|
4198 |
window.removeEventListener('orientationchange', keyboardOrientationChange);
|
|
|
4199 |
})
|
|
|
4200 |
document.addEventListener('resume', function() {
|
|
|
4201 |
keyboardInitViewportHeight();
|
|
|
4202 |
window.addEventListener('orientationchange', keyboardOrientationChange)
|
|
|
4203 |
});
|
|
|
4204 |
}*/
|
|
|
4205 |
|
|
|
4206 |
// Android sometimes reports bad innerHeight on window.load
|
|
|
4207 |
// try it again in a lil bit to play it safe
|
|
|
4208 |
setTimeout(keyboardInitViewportHeight, 999);
|
|
|
4209 |
|
|
|
4210 |
// only initialize the adjustments for the virtual keyboard
|
|
|
4211 |
// if a touchstart event happens
|
|
|
4212 |
if (window.navigator.msPointerEnabled) {
|
|
|
4213 |
document.addEventListener("MSPointerDown", keyboardInit, false);
|
|
|
4214 |
} else {
|
|
|
4215 |
document.addEventListener('touchstart', keyboardInit, false);
|
|
|
4216 |
}
|
|
|
4217 |
});
|
|
|
4218 |
|
|
|
4219 |
|
|
|
4220 |
|
|
|
4221 |
var viewportTag;
|
|
|
4222 |
var viewportProperties = {};
|
|
|
4223 |
|
|
|
4224 |
ionic.viewport = {
|
|
|
4225 |
orientation: function() {
|
|
|
4226 |
// 0 = Portrait
|
|
|
4227 |
// 90 = Landscape
|
|
|
4228 |
// not using window.orientation because each device has a different implementation
|
|
|
4229 |
return (window.innerWidth > window.innerHeight ? 90 : 0);
|
|
|
4230 |
}
|
|
|
4231 |
};
|
|
|
4232 |
|
|
|
4233 |
function viewportLoadTag() {
|
|
|
4234 |
var x;
|
|
|
4235 |
|
|
|
4236 |
for (x = 0; x < document.head.children.length; x++) {
|
|
|
4237 |
if (document.head.children[x].name == 'viewport') {
|
|
|
4238 |
viewportTag = document.head.children[x];
|
|
|
4239 |
break;
|
|
|
4240 |
}
|
|
|
4241 |
}
|
|
|
4242 |
|
|
|
4243 |
if (viewportTag) {
|
|
|
4244 |
var props = viewportTag.content.toLowerCase().replace(/\s+/g, '').split(',');
|
|
|
4245 |
var keyValue;
|
|
|
4246 |
for (x = 0; x < props.length; x++) {
|
|
|
4247 |
if (props[x]) {
|
|
|
4248 |
keyValue = props[x].split('=');
|
|
|
4249 |
viewportProperties[ keyValue[0] ] = (keyValue.length > 1 ? keyValue[1] : '_');
|
|
|
4250 |
}
|
|
|
4251 |
}
|
|
|
4252 |
viewportUpdate();
|
|
|
4253 |
}
|
|
|
4254 |
}
|
|
|
4255 |
|
|
|
4256 |
function viewportUpdate() {
|
|
|
4257 |
// unit tests in viewport.unit.js
|
|
|
4258 |
|
|
|
4259 |
var initWidth = viewportProperties.width;
|
|
|
4260 |
var initHeight = viewportProperties.height;
|
|
|
4261 |
var p = ionic.Platform;
|
|
|
4262 |
var version = p.version();
|
|
|
4263 |
var DEVICE_WIDTH = 'device-width';
|
|
|
4264 |
var DEVICE_HEIGHT = 'device-height';
|
|
|
4265 |
var orientation = ionic.viewport.orientation();
|
|
|
4266 |
|
|
|
4267 |
// Most times we're removing the height and adding the width
|
|
|
4268 |
// So this is the default to start with, then modify per platform/version/oreintation
|
|
|
4269 |
delete viewportProperties.height;
|
|
|
4270 |
viewportProperties.width = DEVICE_WIDTH;
|
|
|
4271 |
|
|
|
4272 |
if (p.isIPad()) {
|
|
|
4273 |
// iPad
|
|
|
4274 |
|
|
|
4275 |
if (version > 7) {
|
|
|
4276 |
// iPad >= 7.1
|
|
|
4277 |
// https://issues.apache.org/jira/browse/CB-4323
|
|
|
4278 |
delete viewportProperties.width;
|
|
|
4279 |
|
|
|
4280 |
} else {
|
|
|
4281 |
// iPad <= 7.0
|
|
|
4282 |
|
|
|
4283 |
if (p.isWebView()) {
|
|
|
4284 |
// iPad <= 7.0 WebView
|
|
|
4285 |
|
|
|
4286 |
if (orientation == 90) {
|
|
|
4287 |
// iPad <= 7.0 WebView Landscape
|
|
|
4288 |
viewportProperties.height = '0';
|
|
|
4289 |
|
|
|
4290 |
} else if (version == 7) {
|
|
|
4291 |
// iPad <= 7.0 WebView Portait
|
|
|
4292 |
viewportProperties.height = DEVICE_HEIGHT;
|
|
|
4293 |
}
|
|
|
4294 |
} else {
|
|
|
4295 |
// iPad <= 6.1 Browser
|
|
|
4296 |
if (version < 7) {
|
|
|
4297 |
viewportProperties.height = '0';
|
|
|
4298 |
}
|
|
|
4299 |
}
|
|
|
4300 |
}
|
|
|
4301 |
|
|
|
4302 |
} else if (p.isIOS()) {
|
|
|
4303 |
// iPhone
|
|
|
4304 |
|
|
|
4305 |
if (p.isWebView()) {
|
|
|
4306 |
// iPhone WebView
|
|
|
4307 |
|
|
|
4308 |
if (version > 7) {
|
|
|
4309 |
// iPhone >= 7.1 WebView
|
|
|
4310 |
delete viewportProperties.width;
|
|
|
4311 |
|
|
|
4312 |
} else if (version < 7) {
|
|
|
4313 |
// iPhone <= 6.1 WebView
|
|
|
4314 |
// if height was set it needs to get removed with this hack for <= 6.1
|
|
|
4315 |
if (initHeight) viewportProperties.height = '0';
|
|
|
4316 |
|
|
|
4317 |
} else if (version == 7) {
|
|
|
4318 |
//iPhone == 7.0 WebView
|
|
|
4319 |
viewportProperties.height = DEVICE_HEIGHT;
|
|
|
4320 |
}
|
|
|
4321 |
|
|
|
4322 |
} else {
|
|
|
4323 |
// iPhone Browser
|
|
|
4324 |
|
|
|
4325 |
if (version < 7) {
|
|
|
4326 |
// iPhone <= 6.1 Browser
|
|
|
4327 |
// if height was set it needs to get removed with this hack for <= 6.1
|
|
|
4328 |
if (initHeight) viewportProperties.height = '0';
|
|
|
4329 |
}
|
|
|
4330 |
}
|
|
|
4331 |
|
|
|
4332 |
}
|
|
|
4333 |
|
|
|
4334 |
// only update the viewport tag if there was a change
|
|
|
4335 |
if (initWidth !== viewportProperties.width || initHeight !== viewportProperties.height) {
|
|
|
4336 |
viewportTagUpdate();
|
|
|
4337 |
}
|
|
|
4338 |
}
|
|
|
4339 |
|
|
|
4340 |
function viewportTagUpdate() {
|
|
|
4341 |
var key, props = [];
|
|
|
4342 |
for (key in viewportProperties) {
|
|
|
4343 |
if (viewportProperties[key]) {
|
|
|
4344 |
props.push(key + (viewportProperties[key] == '_' ? '' : '=' + viewportProperties[key]));
|
|
|
4345 |
}
|
|
|
4346 |
}
|
|
|
4347 |
|
|
|
4348 |
viewportTag.content = props.join(', ');
|
|
|
4349 |
}
|
|
|
4350 |
|
|
|
4351 |
ionic.Platform.ready(function() {
|
|
|
4352 |
viewportLoadTag();
|
|
|
4353 |
|
|
|
4354 |
window.addEventListener("orientationchange", function() {
|
|
|
4355 |
setTimeout(viewportUpdate, 1000);
|
|
|
4356 |
}, false);
|
|
|
4357 |
});
|
|
|
4358 |
|
|
|
4359 |
(function(ionic) {
|
|
|
4360 |
'use strict';
|
|
|
4361 |
ionic.views.View = function() {
|
|
|
4362 |
this.initialize.apply(this, arguments);
|
|
|
4363 |
};
|
|
|
4364 |
|
|
|
4365 |
ionic.views.View.inherit = ionic.inherit;
|
|
|
4366 |
|
|
|
4367 |
ionic.extend(ionic.views.View.prototype, {
|
|
|
4368 |
initialize: function() {}
|
|
|
4369 |
});
|
|
|
4370 |
|
|
|
4371 |
})(window.ionic);
|
|
|
4372 |
|
|
|
4373 |
/*
|
|
|
4374 |
* Scroller
|
|
|
4375 |
* http://github.com/zynga/scroller
|
|
|
4376 |
*
|
|
|
4377 |
* Copyright 2011, Zynga Inc.
|
|
|
4378 |
* Licensed under the MIT License.
|
|
|
4379 |
* https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt
|
|
|
4380 |
*
|
|
|
4381 |
* Based on the work of: Unify Project (unify-project.org)
|
|
|
4382 |
* http://unify-project.org
|
|
|
4383 |
* Copyright 2011, Deutsche Telekom AG
|
|
|
4384 |
* License: MIT + Apache (V2)
|
|
|
4385 |
*/
|
|
|
4386 |
|
|
|
4387 |
/* jshint eqnull: true */
|
|
|
4388 |
|
|
|
4389 |
/**
|
|
|
4390 |
* Generic animation class with support for dropped frames both optional easing and duration.
|
|
|
4391 |
*
|
|
|
4392 |
* Optional duration is useful when the lifetime is defined by another condition than time
|
|
|
4393 |
* e.g. speed of an animating object, etc.
|
|
|
4394 |
*
|
|
|
4395 |
* Dropped frame logic allows to keep using the same updater logic independent from the actual
|
|
|
4396 |
* rendering. This eases a lot of cases where it might be pretty complex to break down a state
|
|
|
4397 |
* based on the pure time difference.
|
|
|
4398 |
*/
|
|
|
4399 |
var zyngaCore = { effect: {} };
|
|
|
4400 |
(function(global) {
|
|
|
4401 |
var time = Date.now || function() {
|
|
|
4402 |
return +new Date();
|
|
|
4403 |
};
|
|
|
4404 |
var desiredFrames = 60;
|
|
|
4405 |
var millisecondsPerSecond = 1000;
|
|
|
4406 |
var running = {};
|
|
|
4407 |
var counter = 1;
|
|
|
4408 |
|
|
|
4409 |
zyngaCore.effect.Animate = {
|
|
|
4410 |
|
|
|
4411 |
/**
|
|
|
4412 |
* A requestAnimationFrame wrapper / polyfill.
|
|
|
4413 |
*
|
|
|
4414 |
* @param callback {Function} The callback to be invoked before the next repaint.
|
|
|
4415 |
* @param root {HTMLElement} The root element for the repaint
|
|
|
4416 |
*/
|
|
|
4417 |
requestAnimationFrame: (function() {
|
|
|
4418 |
|
|
|
4419 |
// Check for request animation Frame support
|
|
|
4420 |
var requestFrame = global.requestAnimationFrame || global.webkitRequestAnimationFrame || global.mozRequestAnimationFrame || global.oRequestAnimationFrame;
|
|
|
4421 |
var isNative = !!requestFrame;
|
|
|
4422 |
|
|
|
4423 |
if (requestFrame && !/requestAnimationFrame\(\)\s*\{\s*\[native code\]\s*\}/i.test(requestFrame.toString())) {
|
|
|
4424 |
isNative = false;
|
|
|
4425 |
}
|
|
|
4426 |
|
|
|
4427 |
if (isNative) {
|
|
|
4428 |
return function(callback, root) {
|
|
|
4429 |
requestFrame(callback, root);
|
|
|
4430 |
};
|
|
|
4431 |
}
|
|
|
4432 |
|
|
|
4433 |
var TARGET_FPS = 60;
|
|
|
4434 |
var requests = {};
|
|
|
4435 |
var requestCount = 0;
|
|
|
4436 |
var rafHandle = 1;
|
|
|
4437 |
var intervalHandle = null;
|
|
|
4438 |
var lastActive = +new Date();
|
|
|
4439 |
|
|
|
4440 |
return function(callback) {
|
|
|
4441 |
var callbackHandle = rafHandle++;
|
|
|
4442 |
|
|
|
4443 |
// Store callback
|
|
|
4444 |
requests[callbackHandle] = callback;
|
|
|
4445 |
requestCount++;
|
|
|
4446 |
|
|
|
4447 |
// Create timeout at first request
|
|
|
4448 |
if (intervalHandle === null) {
|
|
|
4449 |
|
|
|
4450 |
intervalHandle = setInterval(function() {
|
|
|
4451 |
|
|
|
4452 |
var time = +new Date();
|
|
|
4453 |
var currentRequests = requests;
|
|
|
4454 |
|
|
|
4455 |
// Reset data structure before executing callbacks
|
|
|
4456 |
requests = {};
|
|
|
4457 |
requestCount = 0;
|
|
|
4458 |
|
|
|
4459 |
for(var key in currentRequests) {
|
|
|
4460 |
if (currentRequests.hasOwnProperty(key)) {
|
|
|
4461 |
currentRequests[key](time);
|
|
|
4462 |
lastActive = time;
|
|
|
4463 |
}
|
|
|
4464 |
}
|
|
|
4465 |
|
|
|
4466 |
// Disable the timeout when nothing happens for a certain
|
|
|
4467 |
// period of time
|
|
|
4468 |
if (time - lastActive > 2500) {
|
|
|
4469 |
clearInterval(intervalHandle);
|
|
|
4470 |
intervalHandle = null;
|
|
|
4471 |
}
|
|
|
4472 |
|
|
|
4473 |
}, 1000 / TARGET_FPS);
|
|
|
4474 |
}
|
|
|
4475 |
|
|
|
4476 |
return callbackHandle;
|
|
|
4477 |
};
|
|
|
4478 |
|
|
|
4479 |
})(),
|
|
|
4480 |
|
|
|
4481 |
|
|
|
4482 |
/**
|
|
|
4483 |
* Stops the given animation.
|
|
|
4484 |
*
|
|
|
4485 |
* @param id {Integer} Unique animation ID
|
|
|
4486 |
* @return {Boolean} Whether the animation was stopped (aka, was running before)
|
|
|
4487 |
*/
|
|
|
4488 |
stop: function(id) {
|
|
|
4489 |
var cleared = running[id] != null;
|
|
|
4490 |
if (cleared) {
|
|
|
4491 |
running[id] = null;
|
|
|
4492 |
}
|
|
|
4493 |
|
|
|
4494 |
return cleared;
|
|
|
4495 |
},
|
|
|
4496 |
|
|
|
4497 |
|
|
|
4498 |
/**
|
|
|
4499 |
* Whether the given animation is still running.
|
|
|
4500 |
*
|
|
|
4501 |
* @param id {Integer} Unique animation ID
|
|
|
4502 |
* @return {Boolean} Whether the animation is still running
|
|
|
4503 |
*/
|
|
|
4504 |
isRunning: function(id) {
|
|
|
4505 |
return running[id] != null;
|
|
|
4506 |
},
|
|
|
4507 |
|
|
|
4508 |
|
|
|
4509 |
/**
|
|
|
4510 |
* Start the animation.
|
|
|
4511 |
*
|
|
|
4512 |
* @param stepCallback {Function} Pointer to function which is executed on every step.
|
|
|
4513 |
* Signature of the method should be `function(percent, now, virtual) { return continueWithAnimation; }`
|
|
|
4514 |
* @param verifyCallback {Function} Executed before every animation step.
|
|
|
4515 |
* Signature of the method should be `function() { return continueWithAnimation; }`
|
|
|
4516 |
* @param completedCallback {Function}
|
|
|
4517 |
* Signature of the method should be `function(droppedFrames, finishedAnimation) {}`
|
|
|
4518 |
* @param duration {Integer} Milliseconds to run the animation
|
|
|
4519 |
* @param easingMethod {Function} Pointer to easing function
|
|
|
4520 |
* Signature of the method should be `function(percent) { return modifiedValue; }`
|
|
|
4521 |
* @param root {Element} Render root, when available. Used for internal
|
|
|
4522 |
* usage of requestAnimationFrame.
|
|
|
4523 |
* @return {Integer} Identifier of animation. Can be used to stop it any time.
|
|
|
4524 |
*/
|
|
|
4525 |
start: function(stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) {
|
|
|
4526 |
|
|
|
4527 |
var start = time();
|
|
|
4528 |
var lastFrame = start;
|
|
|
4529 |
var percent = 0;
|
|
|
4530 |
var dropCounter = 0;
|
|
|
4531 |
var id = counter++;
|
|
|
4532 |
|
|
|
4533 |
if (!root) {
|
|
|
4534 |
root = document.body;
|
|
|
4535 |
}
|
|
|
4536 |
|
|
|
4537 |
// Compacting running db automatically every few new animations
|
|
|
4538 |
if (id % 20 === 0) {
|
|
|
4539 |
var newRunning = {};
|
|
|
4540 |
for (var usedId in running) {
|
|
|
4541 |
newRunning[usedId] = true;
|
|
|
4542 |
}
|
|
|
4543 |
running = newRunning;
|
|
|
4544 |
}
|
|
|
4545 |
|
|
|
4546 |
// This is the internal step method which is called every few milliseconds
|
|
|
4547 |
var step = function(virtual) {
|
|
|
4548 |
|
|
|
4549 |
// Normalize virtual value
|
|
|
4550 |
var render = virtual !== true;
|
|
|
4551 |
|
|
|
4552 |
// Get current time
|
|
|
4553 |
var now = time();
|
|
|
4554 |
|
|
|
4555 |
// Verification is executed before next animation step
|
|
|
4556 |
if (!running[id] || (verifyCallback && !verifyCallback(id))) {
|
|
|
4557 |
|
|
|
4558 |
running[id] = null;
|
|
|
4559 |
completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, false);
|
|
|
4560 |
return;
|
|
|
4561 |
|
|
|
4562 |
}
|
|
|
4563 |
|
|
|
4564 |
// For the current rendering to apply let's update omitted steps in memory.
|
|
|
4565 |
// This is important to bring internal state variables up-to-date with progress in time.
|
|
|
4566 |
if (render) {
|
|
|
4567 |
|
|
|
4568 |
var droppedFrames = Math.round((now - lastFrame) / (millisecondsPerSecond / desiredFrames)) - 1;
|
|
|
4569 |
for (var j = 0; j < Math.min(droppedFrames, 4); j++) {
|
|
|
4570 |
step(true);
|
|
|
4571 |
dropCounter++;
|
|
|
4572 |
}
|
|
|
4573 |
|
|
|
4574 |
}
|
|
|
4575 |
|
|
|
4576 |
// Compute percent value
|
|
|
4577 |
if (duration) {
|
|
|
4578 |
percent = (now - start) / duration;
|
|
|
4579 |
if (percent > 1) {
|
|
|
4580 |
percent = 1;
|
|
|
4581 |
}
|
|
|
4582 |
}
|
|
|
4583 |
|
|
|
4584 |
// Execute step callback, then...
|
|
|
4585 |
var value = easingMethod ? easingMethod(percent) : percent;
|
|
|
4586 |
if ((stepCallback(value, now, render) === false || percent === 1) && render) {
|
|
|
4587 |
running[id] = null;
|
|
|
4588 |
completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, percent === 1 || duration == null);
|
|
|
4589 |
} else if (render) {
|
|
|
4590 |
lastFrame = now;
|
|
|
4591 |
zyngaCore.effect.Animate.requestAnimationFrame(step, root);
|
|
|
4592 |
}
|
|
|
4593 |
};
|
|
|
4594 |
|
|
|
4595 |
// Mark as running
|
|
|
4596 |
running[id] = true;
|
|
|
4597 |
|
|
|
4598 |
// Init first step
|
|
|
4599 |
zyngaCore.effect.Animate.requestAnimationFrame(step, root);
|
|
|
4600 |
|
|
|
4601 |
// Return unique animation ID
|
|
|
4602 |
return id;
|
|
|
4603 |
}
|
|
|
4604 |
};
|
|
|
4605 |
})(this);
|
|
|
4606 |
|
|
|
4607 |
/*
|
|
|
4608 |
* Scroller
|
|
|
4609 |
* http://github.com/zynga/scroller
|
|
|
4610 |
*
|
|
|
4611 |
* Copyright 2011, Zynga Inc.
|
|
|
4612 |
* Licensed under the MIT License.
|
|
|
4613 |
* https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt
|
|
|
4614 |
*
|
|
|
4615 |
* Based on the work of: Unify Project (unify-project.org)
|
|
|
4616 |
* http://unify-project.org
|
|
|
4617 |
* Copyright 2011, Deutsche Telekom AG
|
|
|
4618 |
* License: MIT + Apache (V2)
|
|
|
4619 |
*/
|
|
|
4620 |
|
|
|
4621 |
(function(ionic) {
|
|
|
4622 |
var NOOP = function(){};
|
|
|
4623 |
|
|
|
4624 |
// Easing Equations (c) 2003 Robert Penner, all rights reserved.
|
|
|
4625 |
// Open source under the BSD License.
|
|
|
4626 |
|
|
|
4627 |
/**
|
|
|
4628 |
* @param pos {Number} position between 0 (start of effect) and 1 (end of effect)
|
|
|
4629 |
**/
|
|
|
4630 |
var easeOutCubic = function(pos) {
|
|
|
4631 |
return (Math.pow((pos - 1), 3) + 1);
|
|
|
4632 |
};
|
|
|
4633 |
|
|
|
4634 |
/**
|
|
|
4635 |
* @param pos {Number} position between 0 (start of effect) and 1 (end of effect)
|
|
|
4636 |
**/
|
|
|
4637 |
var easeInOutCubic = function(pos) {
|
|
|
4638 |
if ((pos /= 0.5) < 1) {
|
|
|
4639 |
return 0.5 * Math.pow(pos, 3);
|
|
|
4640 |
}
|
|
|
4641 |
|
|
|
4642 |
return 0.5 * (Math.pow((pos - 2), 3) + 2);
|
|
|
4643 |
};
|
|
|
4644 |
|
|
|
4645 |
|
|
|
4646 |
/**
|
|
|
4647 |
* ionic.views.Scroll
|
|
|
4648 |
* A powerful scroll view with support for bouncing, pull to refresh, and paging.
|
|
|
4649 |
* @param {Object} options options for the scroll view
|
|
|
4650 |
* @class A scroll view system
|
|
|
4651 |
* @memberof ionic.views
|
|
|
4652 |
*/
|
|
|
4653 |
ionic.views.Scroll = ionic.views.View.inherit({
|
|
|
4654 |
initialize: function(options) {
|
|
|
4655 |
var self = this;
|
|
|
4656 |
|
|
|
4657 |
self.__container = options.el;
|
|
|
4658 |
self.__content = options.el.firstElementChild;
|
|
|
4659 |
|
|
|
4660 |
//Remove any scrollTop attached to these elements; they are virtual scroll now
|
|
|
4661 |
//This also stops on-load-scroll-to-window.location.hash that the browser does
|
|
|
4662 |
setTimeout(function() {
|
|
|
4663 |
if (self.__container && self.__content) {
|
|
|
4664 |
self.__container.scrollTop = 0;
|
|
|
4665 |
self.__content.scrollTop = 0;
|
|
|
4666 |
}
|
|
|
4667 |
});
|
|
|
4668 |
|
|
|
4669 |
self.options = {
|
|
|
4670 |
|
|
|
4671 |
/** Disable scrolling on x-axis by default */
|
|
|
4672 |
scrollingX: false,
|
|
|
4673 |
scrollbarX: true,
|
|
|
4674 |
|
|
|
4675 |
/** Enable scrolling on y-axis */
|
|
|
4676 |
scrollingY: true,
|
|
|
4677 |
scrollbarY: true,
|
|
|
4678 |
|
|
|
4679 |
startX: 0,
|
|
|
4680 |
startY: 0,
|
|
|
4681 |
|
|
|
4682 |
/** The amount to dampen mousewheel events */
|
|
|
4683 |
wheelDampen: 6,
|
|
|
4684 |
|
|
|
4685 |
/** The minimum size the scrollbars scale to while scrolling */
|
|
|
4686 |
minScrollbarSizeX: 5,
|
|
|
4687 |
minScrollbarSizeY: 5,
|
|
|
4688 |
|
|
|
4689 |
/** Scrollbar fading after scrolling */
|
|
|
4690 |
scrollbarsFade: true,
|
|
|
4691 |
scrollbarFadeDelay: 300,
|
|
|
4692 |
/** The initial fade delay when the pane is resized or initialized */
|
|
|
4693 |
scrollbarResizeFadeDelay: 1000,
|
|
|
4694 |
|
|
|
4695 |
/** Enable animations for deceleration, snap back, zooming and scrolling */
|
|
|
4696 |
animating: true,
|
|
|
4697 |
|
|
|
4698 |
/** duration for animations triggered by scrollTo/zoomTo */
|
|
|
4699 |
animationDuration: 250,
|
|
|
4700 |
|
|
|
4701 |
/** The velocity required to make the scroll view "slide" after touchend */
|
|
|
4702 |
decelVelocityThreshold: 4,
|
|
|
4703 |
|
|
|
4704 |
/** The velocity required to make the scroll view "slide" after touchend when using paging */
|
|
|
4705 |
decelVelocityThresholdPaging: 4,
|
|
|
4706 |
|
|
|
4707 |
/** Enable bouncing (content can be slowly moved outside and jumps back after releasing) */
|
|
|
4708 |
bouncing: true,
|
|
|
4709 |
|
|
|
4710 |
/** Enable locking to the main axis if user moves only slightly on one of them at start */
|
|
|
4711 |
locking: true,
|
|
|
4712 |
|
|
|
4713 |
/** Enable pagination mode (switching between full page content panes) */
|
|
|
4714 |
paging: false,
|
|
|
4715 |
|
|
|
4716 |
/** Enable snapping of content to a configured pixel grid */
|
|
|
4717 |
snapping: false,
|
|
|
4718 |
|
|
|
4719 |
/** Enable zooming of content via API, fingers and mouse wheel */
|
|
|
4720 |
zooming: false,
|
|
|
4721 |
|
|
|
4722 |
/** Minimum zoom level */
|
|
|
4723 |
minZoom: 0.5,
|
|
|
4724 |
|
|
|
4725 |
/** Maximum zoom level */
|
|
|
4726 |
maxZoom: 3,
|
|
|
4727 |
|
|
|
4728 |
/** Multiply or decrease scrolling speed **/
|
|
|
4729 |
speedMultiplier: 1,
|
|
|
4730 |
|
|
|
4731 |
deceleration: 0.97,
|
|
|
4732 |
|
|
|
4733 |
/** Whether to prevent default on a scroll operation to capture drag events **/
|
|
|
4734 |
preventDefault: false,
|
|
|
4735 |
|
|
|
4736 |
/** Callback that is fired on the later of touch end or deceleration end,
|
|
|
4737 |
provided that another scrolling action has not begun. Used to know
|
|
|
4738 |
when to fade out a scrollbar. */
|
|
|
4739 |
scrollingComplete: NOOP,
|
|
|
4740 |
|
|
|
4741 |
/** This configures the amount of change applied to deceleration when reaching boundaries **/
|
|
|
4742 |
penetrationDeceleration: 0.03,
|
|
|
4743 |
|
|
|
4744 |
/** This configures the amount of change applied to acceleration when reaching boundaries **/
|
|
|
4745 |
penetrationAcceleration: 0.08,
|
|
|
4746 |
|
|
|
4747 |
// The ms interval for triggering scroll events
|
|
|
4748 |
scrollEventInterval: 10,
|
|
|
4749 |
|
|
|
4750 |
freeze: false,
|
|
|
4751 |
|
|
|
4752 |
getContentWidth: function() {
|
|
|
4753 |
return Math.max(self.__content.scrollWidth, self.__content.offsetWidth);
|
|
|
4754 |
},
|
|
|
4755 |
getContentHeight: function() {
|
|
|
4756 |
return Math.max(self.__content.scrollHeight, self.__content.offsetHeight + (self.__content.offsetTop * 2));
|
|
|
4757 |
}
|
|
|
4758 |
};
|
|
|
4759 |
|
|
|
4760 |
for (var key in options) {
|
|
|
4761 |
self.options[key] = options[key];
|
|
|
4762 |
}
|
|
|
4763 |
|
|
|
4764 |
self.hintResize = ionic.debounce(function() {
|
|
|
4765 |
self.resize();
|
|
|
4766 |
}, 1000, true);
|
|
|
4767 |
|
|
|
4768 |
self.onScroll = function() {
|
|
|
4769 |
|
|
|
4770 |
if (!ionic.scroll.isScrolling) {
|
|
|
4771 |
setTimeout(self.setScrollStart, 50);
|
|
|
4772 |
} else {
|
|
|
4773 |
clearTimeout(self.scrollTimer);
|
|
|
4774 |
self.scrollTimer = setTimeout(self.setScrollStop, 80);
|
|
|
4775 |
}
|
|
|
4776 |
|
|
|
4777 |
};
|
|
|
4778 |
|
|
|
4779 |
self.freeze = function(shouldFreeze) {
|
|
|
4780 |
if (arguments.length) {
|
|
|
4781 |
self.options.freeze = shouldFreeze;
|
|
|
4782 |
}
|
|
|
4783 |
return self.options.freeze;
|
|
|
4784 |
};
|
|
|
4785 |
|
|
|
4786 |
self.setScrollStart = function() {
|
|
|
4787 |
ionic.scroll.isScrolling = Math.abs(ionic.scroll.lastTop - self.__scrollTop) > 1;
|
|
|
4788 |
clearTimeout(self.scrollTimer);
|
|
|
4789 |
self.scrollTimer = setTimeout(self.setScrollStop, 80);
|
|
|
4790 |
};
|
|
|
4791 |
|
|
|
4792 |
self.setScrollStop = function() {
|
|
|
4793 |
ionic.scroll.isScrolling = false;
|
|
|
4794 |
ionic.scroll.lastTop = self.__scrollTop;
|
|
|
4795 |
};
|
|
|
4796 |
|
|
|
4797 |
self.triggerScrollEvent = ionic.throttle(function() {
|
|
|
4798 |
self.onScroll();
|
|
|
4799 |
ionic.trigger('scroll', {
|
|
|
4800 |
scrollTop: self.__scrollTop,
|
|
|
4801 |
scrollLeft: self.__scrollLeft,
|
|
|
4802 |
target: self.__container
|
|
|
4803 |
});
|
|
|
4804 |
}, self.options.scrollEventInterval);
|
|
|
4805 |
|
|
|
4806 |
self.triggerScrollEndEvent = function() {
|
|
|
4807 |
ionic.trigger('scrollend', {
|
|
|
4808 |
scrollTop: self.__scrollTop,
|
|
|
4809 |
scrollLeft: self.__scrollLeft,
|
|
|
4810 |
target: self.__container
|
|
|
4811 |
});
|
|
|
4812 |
};
|
|
|
4813 |
|
|
|
4814 |
self.__scrollLeft = self.options.startX;
|
|
|
4815 |
self.__scrollTop = self.options.startY;
|
|
|
4816 |
|
|
|
4817 |
// Get the render update function, initialize event handlers,
|
|
|
4818 |
// and calculate the size of the scroll container
|
|
|
4819 |
self.__callback = self.getRenderFn();
|
|
|
4820 |
self.__initEventHandlers();
|
|
|
4821 |
self.__createScrollbars();
|
|
|
4822 |
|
|
|
4823 |
},
|
|
|
4824 |
|
|
|
4825 |
run: function() {
|
|
|
4826 |
this.resize();
|
|
|
4827 |
|
|
|
4828 |
// Fade them out
|
|
|
4829 |
this.__fadeScrollbars('out', this.options.scrollbarResizeFadeDelay);
|
|
|
4830 |
},
|
|
|
4831 |
|
|
|
4832 |
|
|
|
4833 |
|
|
|
4834 |
/*
|
|
|
4835 |
---------------------------------------------------------------------------
|
|
|
4836 |
INTERNAL FIELDS :: STATUS
|
|
|
4837 |
---------------------------------------------------------------------------
|
|
|
4838 |
*/
|
|
|
4839 |
|
|
|
4840 |
/** Whether only a single finger is used in touch handling */
|
|
|
4841 |
__isSingleTouch: false,
|
|
|
4842 |
|
|
|
4843 |
/** Whether a touch event sequence is in progress */
|
|
|
4844 |
__isTracking: false,
|
|
|
4845 |
|
|
|
4846 |
/** Whether a deceleration animation went to completion. */
|
|
|
4847 |
__didDecelerationComplete: false,
|
|
|
4848 |
|
|
|
4849 |
/**
|
|
|
4850 |
* Whether a gesture zoom/rotate event is in progress. Activates when
|
|
|
4851 |
* a gesturestart event happens. This has higher priority than dragging.
|
|
|
4852 |
*/
|
|
|
4853 |
__isGesturing: false,
|
|
|
4854 |
|
|
|
4855 |
/**
|
|
|
4856 |
* Whether the user has moved by such a distance that we have enabled
|
|
|
4857 |
* dragging mode. Hint: It's only enabled after some pixels of movement to
|
|
|
4858 |
* not interrupt with clicks etc.
|
|
|
4859 |
*/
|
|
|
4860 |
__isDragging: false,
|
|
|
4861 |
|
|
|
4862 |
/**
|
|
|
4863 |
* Not touching and dragging anymore, and smoothly animating the
|
|
|
4864 |
* touch sequence using deceleration.
|
|
|
4865 |
*/
|
|
|
4866 |
__isDecelerating: false,
|
|
|
4867 |
|
|
|
4868 |
/**
|
|
|
4869 |
* Smoothly animating the currently configured change
|
|
|
4870 |
*/
|
|
|
4871 |
__isAnimating: false,
|
|
|
4872 |
|
|
|
4873 |
|
|
|
4874 |
|
|
|
4875 |
/*
|
|
|
4876 |
---------------------------------------------------------------------------
|
|
|
4877 |
INTERNAL FIELDS :: DIMENSIONS
|
|
|
4878 |
---------------------------------------------------------------------------
|
|
|
4879 |
*/
|
|
|
4880 |
|
|
|
4881 |
/** Available outer left position (from document perspective) */
|
|
|
4882 |
__clientLeft: 0,
|
|
|
4883 |
|
|
|
4884 |
/** Available outer top position (from document perspective) */
|
|
|
4885 |
__clientTop: 0,
|
|
|
4886 |
|
|
|
4887 |
/** Available outer width */
|
|
|
4888 |
__clientWidth: 0,
|
|
|
4889 |
|
|
|
4890 |
/** Available outer height */
|
|
|
4891 |
__clientHeight: 0,
|
|
|
4892 |
|
|
|
4893 |
/** Outer width of content */
|
|
|
4894 |
__contentWidth: 0,
|
|
|
4895 |
|
|
|
4896 |
/** Outer height of content */
|
|
|
4897 |
__contentHeight: 0,
|
|
|
4898 |
|
|
|
4899 |
/** Snapping width for content */
|
|
|
4900 |
__snapWidth: 100,
|
|
|
4901 |
|
|
|
4902 |
/** Snapping height for content */
|
|
|
4903 |
__snapHeight: 100,
|
|
|
4904 |
|
|
|
4905 |
/** Height to assign to refresh area */
|
|
|
4906 |
__refreshHeight: null,
|
|
|
4907 |
|
|
|
4908 |
/** Whether the refresh process is enabled when the event is released now */
|
|
|
4909 |
__refreshActive: false,
|
|
|
4910 |
|
|
|
4911 |
/** Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release */
|
|
|
4912 |
__refreshActivate: null,
|
|
|
4913 |
|
|
|
4914 |
/** Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled */
|
|
|
4915 |
__refreshDeactivate: null,
|
|
|
4916 |
|
|
|
4917 |
/** Callback to execute to start the actual refresh. Call {@link #refreshFinish} when done */
|
|
|
4918 |
__refreshStart: null,
|
|
|
4919 |
|
|
|
4920 |
/** Zoom level */
|
|
|
4921 |
__zoomLevel: 1,
|
|
|
4922 |
|
|
|
4923 |
/** Scroll position on x-axis */
|
|
|
4924 |
__scrollLeft: 0,
|
|
|
4925 |
|
|
|
4926 |
/** Scroll position on y-axis */
|
|
|
4927 |
__scrollTop: 0,
|
|
|
4928 |
|
|
|
4929 |
/** Maximum allowed scroll position on x-axis */
|
|
|
4930 |
__maxScrollLeft: 0,
|
|
|
4931 |
|
|
|
4932 |
/** Maximum allowed scroll position on y-axis */
|
|
|
4933 |
__maxScrollTop: 0,
|
|
|
4934 |
|
|
|
4935 |
/* Scheduled left position (final position when animating) */
|
|
|
4936 |
__scheduledLeft: 0,
|
|
|
4937 |
|
|
|
4938 |
/* Scheduled top position (final position when animating) */
|
|
|
4939 |
__scheduledTop: 0,
|
|
|
4940 |
|
|
|
4941 |
/* Scheduled zoom level (final scale when animating) */
|
|
|
4942 |
__scheduledZoom: 0,
|
|
|
4943 |
|
|
|
4944 |
|
|
|
4945 |
|
|
|
4946 |
/*
|
|
|
4947 |
---------------------------------------------------------------------------
|
|
|
4948 |
INTERNAL FIELDS :: LAST POSITIONS
|
|
|
4949 |
---------------------------------------------------------------------------
|
|
|
4950 |
*/
|
|
|
4951 |
|
|
|
4952 |
/** Left position of finger at start */
|
|
|
4953 |
__lastTouchLeft: null,
|
|
|
4954 |
|
|
|
4955 |
/** Top position of finger at start */
|
|
|
4956 |
__lastTouchTop: null,
|
|
|
4957 |
|
|
|
4958 |
/** Timestamp of last move of finger. Used to limit tracking range for deceleration speed. */
|
|
|
4959 |
__lastTouchMove: null,
|
|
|
4960 |
|
|
|
4961 |
/** List of positions, uses three indexes for each state: left, top, timestamp */
|
|
|
4962 |
__positions: null,
|
|
|
4963 |
|
|
|
4964 |
|
|
|
4965 |
|
|
|
4966 |
/*
|
|
|
4967 |
---------------------------------------------------------------------------
|
|
|
4968 |
INTERNAL FIELDS :: DECELERATION SUPPORT
|
|
|
4969 |
---------------------------------------------------------------------------
|
|
|
4970 |
*/
|
|
|
4971 |
|
|
|
4972 |
/** Minimum left scroll position during deceleration */
|
|
|
4973 |
__minDecelerationScrollLeft: null,
|
|
|
4974 |
|
|
|
4975 |
/** Minimum top scroll position during deceleration */
|
|
|
4976 |
__minDecelerationScrollTop: null,
|
|
|
4977 |
|
|
|
4978 |
/** Maximum left scroll position during deceleration */
|
|
|
4979 |
__maxDecelerationScrollLeft: null,
|
|
|
4980 |
|
|
|
4981 |
/** Maximum top scroll position during deceleration */
|
|
|
4982 |
__maxDecelerationScrollTop: null,
|
|
|
4983 |
|
|
|
4984 |
/** Current factor to modify horizontal scroll position with on every step */
|
|
|
4985 |
__decelerationVelocityX: null,
|
|
|
4986 |
|
|
|
4987 |
/** Current factor to modify vertical scroll position with on every step */
|
|
|
4988 |
__decelerationVelocityY: null,
|
|
|
4989 |
|
|
|
4990 |
|
|
|
4991 |
/** the browser-specific property to use for transforms */
|
|
|
4992 |
__transformProperty: null,
|
|
|
4993 |
__perspectiveProperty: null,
|
|
|
4994 |
|
|
|
4995 |
/** scrollbar indicators */
|
|
|
4996 |
__indicatorX: null,
|
|
|
4997 |
__indicatorY: null,
|
|
|
4998 |
|
|
|
4999 |
/** Timeout for scrollbar fading */
|
|
|
5000 |
__scrollbarFadeTimeout: null,
|
|
|
5001 |
|
|
|
5002 |
/** whether we've tried to wait for size already */
|
|
|
5003 |
__didWaitForSize: null,
|
|
|
5004 |
__sizerTimeout: null,
|
|
|
5005 |
|
|
|
5006 |
__initEventHandlers: function() {
|
|
|
5007 |
var self = this;
|
|
|
5008 |
|
|
|
5009 |
// Event Handler
|
|
|
5010 |
var container = self.__container;
|
|
|
5011 |
|
|
|
5012 |
// save height when scroll view is shrunk so we don't need to reflow
|
|
|
5013 |
var scrollViewOffsetHeight;
|
|
|
5014 |
|
|
|
5015 |
/**
|
|
|
5016 |
* Shrink the scroll view when the keyboard is up if necessary and if the
|
|
|
5017 |
* focused input is below the bottom of the shrunk scroll view, scroll it
|
|
|
5018 |
* into view.
|
|
|
5019 |
*/
|
|
|
5020 |
self.scrollChildIntoView = function(e) {
|
|
|
5021 |
//console.log("scrollChildIntoView at: " + Date.now());
|
|
|
5022 |
|
|
|
5023 |
// D
|
|
|
5024 |
var scrollBottomOffsetToTop = container.getBoundingClientRect().bottom;
|
|
|
5025 |
// D - A
|
|
|
5026 |
scrollViewOffsetHeight = container.offsetHeight;
|
|
|
5027 |
var alreadyShrunk = self.isShrunkForKeyboard;
|
|
|
5028 |
|
|
|
5029 |
var isModal = container.parentNode.classList.contains('modal');
|
|
|
5030 |
// 680px is when the media query for 60% modal width kicks in
|
|
|
5031 |
var isInsetModal = isModal && window.innerWidth >= 680;
|
|
|
5032 |
|
|
|
5033 |
/*
|
|
|
5034 |
* _______
|
|
|
5035 |
* |---A---| <- top of scroll view
|
|
|
5036 |
* | |
|
|
|
5037 |
* |---B---| <- keyboard
|
|
|
5038 |
* | C | <- input
|
|
|
5039 |
* |---D---| <- initial bottom of scroll view
|
|
|
5040 |
* |___E___| <- bottom of viewport
|
|
|
5041 |
*
|
|
|
5042 |
* All commented calculations relative to the top of the viewport (ie E
|
|
|
5043 |
* is the viewport height, not 0)
|
|
|
5044 |
*/
|
|
|
5045 |
if (!alreadyShrunk) {
|
|
|
5046 |
// shrink scrollview so we can actually scroll if the input is hidden
|
|
|
5047 |
// if it isn't shrink so we can scroll to inputs under the keyboard
|
|
|
5048 |
// inset modals won't shrink on Android on their own when the keyboard appears
|
|
|
5049 |
if ( ionic.Platform.isIOS() || ionic.Platform.isFullScreen || isInsetModal ) {
|
|
|
5050 |
// if there are things below the scroll view account for them and
|
|
|
5051 |
// subtract them from the keyboard height when resizing
|
|
|
5052 |
// E - D E D
|
|
|
5053 |
var scrollBottomOffsetToBottom = e.detail.viewportHeight - scrollBottomOffsetToTop;
|
|
|
5054 |
|
|
|
5055 |
// 0 or D - B if D > B E - B E - D
|
|
|
5056 |
var keyboardOffset = Math.max(0, e.detail.keyboardHeight - scrollBottomOffsetToBottom);
|
|
|
5057 |
|
|
|
5058 |
ionic.requestAnimationFrame(function(){
|
|
|
5059 |
// D - A or B - A if D > B D - A max(0, D - B)
|
|
|
5060 |
scrollViewOffsetHeight = scrollViewOffsetHeight - keyboardOffset;
|
|
|
5061 |
container.style.height = scrollViewOffsetHeight + "px";
|
|
|
5062 |
container.style.overflow = "visible";
|
|
|
5063 |
|
|
|
5064 |
//update scroll view
|
|
|
5065 |
self.resize();
|
|
|
5066 |
});
|
|
|
5067 |
}
|
|
|
5068 |
|
|
|
5069 |
self.isShrunkForKeyboard = true;
|
|
|
5070 |
}
|
|
|
5071 |
|
|
|
5072 |
/*
|
|
|
5073 |
* _______
|
|
|
5074 |
* |---A---| <- top of scroll view
|
|
|
5075 |
* | * | <- where we want to scroll to
|
|
|
5076 |
* |--B-D--| <- keyboard, bottom of scroll view
|
|
|
5077 |
* | C | <- input
|
|
|
5078 |
* | |
|
|
|
5079 |
* |___E___| <- bottom of viewport
|
|
|
5080 |
*
|
|
|
5081 |
* All commented calculations relative to the top of the viewport (ie E
|
|
|
5082 |
* is the viewport height, not 0)
|
|
|
5083 |
*/
|
|
|
5084 |
// if the element is positioned under the keyboard scroll it into view
|
|
|
5085 |
if (e.detail.isElementUnderKeyboard) {
|
|
|
5086 |
|
|
|
5087 |
ionic.requestAnimationFrame(function(){
|
|
|
5088 |
container.scrollTop = 0;
|
|
|
5089 |
// update D if we shrunk
|
|
|
5090 |
if (self.isShrunkForKeyboard && !alreadyShrunk) {
|
|
|
5091 |
scrollBottomOffsetToTop = container.getBoundingClientRect().bottom;
|
|
|
5092 |
}
|
|
|
5093 |
|
|
|
5094 |
// middle of the scrollview, this is where we want to scroll to
|
|
|
5095 |
// (D - A) / 2
|
|
|
5096 |
var scrollMidpointOffset = scrollViewOffsetHeight * 0.5;
|
|
|
5097 |
//console.log("container.offsetHeight: " + scrollViewOffsetHeight);
|
|
|
5098 |
|
|
|
5099 |
// middle of the input we want to scroll into view
|
|
|
5100 |
// C
|
|
|
5101 |
var inputMidpoint = ((e.detail.elementBottom + e.detail.elementTop) / 2);
|
|
|
5102 |
|
|
|
5103 |
// distance from middle of input to the bottom of the scroll view
|
|
|
5104 |
// C - D C D
|
|
|
5105 |
var inputMidpointOffsetToScrollBottom = inputMidpoint - scrollBottomOffsetToTop;
|
|
|
5106 |
|
|
|
5107 |
//C - D + (D - A)/2 C - D (D - A)/ 2
|
|
|
5108 |
var scrollTop = inputMidpointOffsetToScrollBottom + scrollMidpointOffset;
|
|
|
5109 |
|
|
|
5110 |
if ( scrollTop > 0) {
|
|
|
5111 |
if (ionic.Platform.isIOS()) ionic.tap.cloneFocusedInput(container, self);
|
|
|
5112 |
self.scrollBy(0, scrollTop, true);
|
|
|
5113 |
self.onScroll();
|
|
|
5114 |
}
|
|
|
5115 |
});
|
|
|
5116 |
}
|
|
|
5117 |
|
|
|
5118 |
// Only the first scrollView parent of the element that broadcasted this event
|
|
|
5119 |
// (the active element that needs to be shown) should receive this event
|
|
|
5120 |
e.stopPropagation();
|
|
|
5121 |
};
|
|
|
5122 |
|
|
|
5123 |
self.resetScrollView = function() {
|
|
|
5124 |
//return scrollview to original height once keyboard has hidden
|
|
|
5125 |
if ( self.isShrunkForKeyboard ) {
|
|
|
5126 |
self.isShrunkForKeyboard = false;
|
|
|
5127 |
container.style.height = "";
|
|
|
5128 |
container.style.overflow = "";
|
|
|
5129 |
}
|
|
|
5130 |
self.resize();
|
|
|
5131 |
};
|
|
|
5132 |
|
|
|
5133 |
//Broadcasted when keyboard is shown on some platforms.
|
|
|
5134 |
//See js/utils/keyboard.js
|
|
|
5135 |
container.addEventListener('scrollChildIntoView', self.scrollChildIntoView);
|
|
|
5136 |
|
|
|
5137 |
// Listen on document because container may not have had the last
|
|
|
5138 |
// keyboardActiveElement, for example after closing a modal with a focused
|
|
|
5139 |
// input and returning to a previously resized scroll view in an ion-content.
|
|
|
5140 |
// Since we can only resize scroll views that are currently visible, just resize
|
|
|
5141 |
// the current scroll view when the keyboard is closed.
|
|
|
5142 |
document.addEventListener('resetScrollView', self.resetScrollView);
|
|
|
5143 |
|
|
|
5144 |
function getEventTouches(e) {
|
|
|
5145 |
return e.touches && e.touches.length ? e.touches : [{
|
|
|
5146 |
pageX: e.pageX,
|
|
|
5147 |
pageY: e.pageY
|
|
|
5148 |
}];
|
|
|
5149 |
}
|
|
|
5150 |
|
|
|
5151 |
self.touchStart = function(e) {
|
|
|
5152 |
self.startCoordinates = ionic.tap.pointerCoord(e);
|
|
|
5153 |
|
|
|
5154 |
if ( ionic.tap.ignoreScrollStart(e) ) {
|
|
|
5155 |
return;
|
|
|
5156 |
}
|
|
|
5157 |
|
|
|
5158 |
self.__isDown = true;
|
|
|
5159 |
|
|
|
5160 |
if ( ionic.tap.containsOrIsTextInput(e.target) || e.target.tagName === 'SELECT' ) {
|
|
|
5161 |
// do not start if the target is a text input
|
|
|
5162 |
// if there is a touchmove on this input, then we can start the scroll
|
|
|
5163 |
self.__hasStarted = false;
|
|
|
5164 |
return;
|
|
|
5165 |
}
|
|
|
5166 |
|
|
|
5167 |
self.__isSelectable = true;
|
|
|
5168 |
self.__enableScrollY = true;
|
|
|
5169 |
self.__hasStarted = true;
|
|
|
5170 |
self.doTouchStart(getEventTouches(e), e.timeStamp);
|
|
|
5171 |
e.preventDefault();
|
|
|
5172 |
};
|
|
|
5173 |
|
|
|
5174 |
self.touchMove = function(e) {
|
|
|
5175 |
if (self.options.freeze || !self.__isDown ||
|
|
|
5176 |
(!self.__isDown && e.defaultPrevented) ||
|
|
|
5177 |
(e.target.tagName === 'TEXTAREA' && e.target.parentElement.querySelector(':focus')) ) {
|
|
|
5178 |
return;
|
|
|
5179 |
}
|
|
|
5180 |
|
|
|
5181 |
if ( !self.__hasStarted && ( ionic.tap.containsOrIsTextInput(e.target) || e.target.tagName === 'SELECT' ) ) {
|
|
|
5182 |
// the target is a text input and scroll has started
|
|
|
5183 |
// since the text input doesn't start on touchStart, do it here
|
|
|
5184 |
self.__hasStarted = true;
|
|
|
5185 |
self.doTouchStart(getEventTouches(e), e.timeStamp);
|
|
|
5186 |
e.preventDefault();
|
|
|
5187 |
return;
|
|
|
5188 |
}
|
|
|
5189 |
|
|
|
5190 |
if (self.startCoordinates) {
|
|
|
5191 |
// we have start coordinates, so get this touch move's current coordinates
|
|
|
5192 |
var currentCoordinates = ionic.tap.pointerCoord(e);
|
|
|
5193 |
|
|
|
5194 |
if ( self.__isSelectable &&
|
|
|
5195 |
ionic.tap.isTextInput(e.target) &&
|
|
|
5196 |
Math.abs(self.startCoordinates.x - currentCoordinates.x) > 20 ) {
|
|
|
5197 |
// user slid the text input's caret on its x axis, disable any future y scrolling
|
|
|
5198 |
self.__enableScrollY = false;
|
|
|
5199 |
self.__isSelectable = true;
|
|
|
5200 |
}
|
|
|
5201 |
|
|
|
5202 |
if ( self.__enableScrollY && Math.abs(self.startCoordinates.y - currentCoordinates.y) > 10 ) {
|
|
|
5203 |
// user scrolled the entire view on the y axis
|
|
|
5204 |
// disabled being able to select text on an input
|
|
|
5205 |
// hide the input which has focus, and show a cloned one that doesn't have focus
|
|
|
5206 |
self.__isSelectable = false;
|
|
|
5207 |
ionic.tap.cloneFocusedInput(container, self);
|
|
|
5208 |
}
|
|
|
5209 |
}
|
|
|
5210 |
|
|
|
5211 |
self.doTouchMove(getEventTouches(e), e.timeStamp, e.scale);
|
|
|
5212 |
self.__isDown = true;
|
|
|
5213 |
};
|
|
|
5214 |
|
|
|
5215 |
self.touchMoveBubble = function(e) {
|
|
|
5216 |
if(self.__isDown && self.options.preventDefault) {
|
|
|
5217 |
e.preventDefault();
|
|
|
5218 |
}
|
|
|
5219 |
};
|
|
|
5220 |
|
|
|
5221 |
self.touchEnd = function(e) {
|
|
|
5222 |
if (!self.__isDown) return;
|
|
|
5223 |
|
|
|
5224 |
self.doTouchEnd(e, e.timeStamp);
|
|
|
5225 |
self.__isDown = false;
|
|
|
5226 |
self.__hasStarted = false;
|
|
|
5227 |
self.__isSelectable = true;
|
|
|
5228 |
self.__enableScrollY = true;
|
|
|
5229 |
|
|
|
5230 |
if ( !self.__isDragging && !self.__isDecelerating && !self.__isAnimating ) {
|
|
|
5231 |
ionic.tap.removeClonedInputs(container, self);
|
|
|
5232 |
}
|
|
|
5233 |
};
|
|
|
5234 |
|
|
|
5235 |
self.mouseWheel = ionic.animationFrameThrottle(function(e) {
|
|
|
5236 |
var scrollParent = ionic.DomUtil.getParentOrSelfWithClass(e.target, 'ionic-scroll');
|
|
|
5237 |
if (!self.options.freeze && scrollParent === self.__container) {
|
|
|
5238 |
|
|
|
5239 |
self.hintResize();
|
|
|
5240 |
self.scrollBy(
|
|
|
5241 |
(e.wheelDeltaX || e.deltaX || 0) / self.options.wheelDampen,
|
|
|
5242 |
(-e.wheelDeltaY || e.deltaY || 0) / self.options.wheelDampen
|
|
|
5243 |
);
|
|
|
5244 |
|
|
|
5245 |
self.__fadeScrollbars('in');
|
|
|
5246 |
clearTimeout(self.__wheelHideBarTimeout);
|
|
|
5247 |
self.__wheelHideBarTimeout = setTimeout(function() {
|
|
|
5248 |
self.__fadeScrollbars('out');
|
|
|
5249 |
}, 100);
|
|
|
5250 |
}
|
|
|
5251 |
});
|
|
|
5252 |
|
|
|
5253 |
if ('ontouchstart' in window) {
|
|
|
5254 |
// Touch Events
|
|
|
5255 |
container.addEventListener("touchstart", self.touchStart, false);
|
|
|
5256 |
if(self.options.preventDefault) container.addEventListener("touchmove", self.touchMoveBubble, false);
|
|
|
5257 |
document.addEventListener("touchmove", self.touchMove, false);
|
|
|
5258 |
document.addEventListener("touchend", self.touchEnd, false);
|
|
|
5259 |
document.addEventListener("touchcancel", self.touchEnd, false);
|
|
|
5260 |
|
|
|
5261 |
} else if (window.navigator.pointerEnabled) {
|
|
|
5262 |
// Pointer Events
|
|
|
5263 |
container.addEventListener("pointerdown", self.touchStart, false);
|
|
|
5264 |
if(self.options.preventDefault) container.addEventListener("pointermove", self.touchMoveBubble, false);
|
|
|
5265 |
document.addEventListener("pointermove", self.touchMove, false);
|
|
|
5266 |
document.addEventListener("pointerup", self.touchEnd, false);
|
|
|
5267 |
document.addEventListener("pointercancel", self.touchEnd, false);
|
|
|
5268 |
document.addEventListener("wheel", self.mouseWheel, false);
|
|
|
5269 |
|
|
|
5270 |
} else if (window.navigator.msPointerEnabled) {
|
|
|
5271 |
// IE10, WP8 (Pointer Events)
|
|
|
5272 |
container.addEventListener("MSPointerDown", self.touchStart, false);
|
|
|
5273 |
if(self.options.preventDefault) container.addEventListener("MSPointerMove", self.touchMoveBubble, false);
|
|
|
5274 |
document.addEventListener("MSPointerMove", self.touchMove, false);
|
|
|
5275 |
document.addEventListener("MSPointerUp", self.touchEnd, false);
|
|
|
5276 |
document.addEventListener("MSPointerCancel", self.touchEnd, false);
|
|
|
5277 |
document.addEventListener("wheel", self.mouseWheel, false);
|
|
|
5278 |
|
|
|
5279 |
} else {
|
|
|
5280 |
// Mouse Events
|
|
|
5281 |
var mousedown = false;
|
|
|
5282 |
|
|
|
5283 |
self.mouseDown = function(e) {
|
|
|
5284 |
if ( ionic.tap.ignoreScrollStart(e) || e.target.tagName === 'SELECT' ) {
|
|
|
5285 |
return;
|
|
|
5286 |
}
|
|
|
5287 |
self.doTouchStart(getEventTouches(e), e.timeStamp);
|
|
|
5288 |
|
|
|
5289 |
if ( !ionic.tap.isTextInput(e.target) ) {
|
|
|
5290 |
e.preventDefault();
|
|
|
5291 |
}
|
|
|
5292 |
mousedown = true;
|
|
|
5293 |
};
|
|
|
5294 |
|
|
|
5295 |
self.mouseMove = function(e) {
|
|
|
5296 |
if (self.options.freeze || !mousedown || (!mousedown && e.defaultPrevented)) {
|
|
|
5297 |
return;
|
|
|
5298 |
}
|
|
|
5299 |
|
|
|
5300 |
self.doTouchMove(getEventTouches(e), e.timeStamp);
|
|
|
5301 |
|
|
|
5302 |
mousedown = true;
|
|
|
5303 |
};
|
|
|
5304 |
|
|
|
5305 |
self.mouseMoveBubble = function(e) {
|
|
|
5306 |
if (mousedown && self.options.preventDefault) {
|
|
|
5307 |
e.preventDefault();
|
|
|
5308 |
}
|
|
|
5309 |
};
|
|
|
5310 |
|
|
|
5311 |
self.mouseUp = function(e) {
|
|
|
5312 |
if (!mousedown) {
|
|
|
5313 |
return;
|
|
|
5314 |
}
|
|
|
5315 |
|
|
|
5316 |
self.doTouchEnd(e, e.timeStamp);
|
|
|
5317 |
|
|
|
5318 |
mousedown = false;
|
|
|
5319 |
};
|
|
|
5320 |
|
|
|
5321 |
container.addEventListener("mousedown", self.mouseDown, false);
|
|
|
5322 |
if(self.options.preventDefault) container.addEventListener("mousemove", self.mouseMoveBubble, false);
|
|
|
5323 |
document.addEventListener("mousemove", self.mouseMove, false);
|
|
|
5324 |
document.addEventListener("mouseup", self.mouseUp, false);
|
|
|
5325 |
document.addEventListener('mousewheel', self.mouseWheel, false);
|
|
|
5326 |
document.addEventListener('wheel', self.mouseWheel, false);
|
|
|
5327 |
}
|
|
|
5328 |
},
|
|
|
5329 |
|
|
|
5330 |
__cleanup: function() {
|
|
|
5331 |
var self = this;
|
|
|
5332 |
var container = self.__container;
|
|
|
5333 |
|
|
|
5334 |
container.removeEventListener('touchstart', self.touchStart);
|
|
|
5335 |
container.removeEventListener('touchmove', self.touchMoveBubble);
|
|
|
5336 |
document.removeEventListener('touchmove', self.touchMove);
|
|
|
5337 |
document.removeEventListener('touchend', self.touchEnd);
|
|
|
5338 |
document.removeEventListener('touchcancel', self.touchCancel);
|
|
|
5339 |
|
|
|
5340 |
container.removeEventListener("pointerdown", self.touchStart);
|
|
|
5341 |
container.removeEventListener("pointermove", self.touchMoveBubble);
|
|
|
5342 |
document.removeEventListener("pointermove", self.touchMove);
|
|
|
5343 |
document.removeEventListener("pointerup", self.touchEnd);
|
|
|
5344 |
document.removeEventListener("pointercancel", self.touchEnd);
|
|
|
5345 |
|
|
|
5346 |
container.removeEventListener("MSPointerDown", self.touchStart);
|
|
|
5347 |
container.removeEventListener("MSPointerMove", self.touchMoveBubble);
|
|
|
5348 |
document.removeEventListener("MSPointerMove", self.touchMove);
|
|
|
5349 |
document.removeEventListener("MSPointerUp", self.touchEnd);
|
|
|
5350 |
document.removeEventListener("MSPointerCancel", self.touchEnd);
|
|
|
5351 |
|
|
|
5352 |
container.removeEventListener("mousedown", self.mouseDown);
|
|
|
5353 |
container.removeEventListener("mousemove", self.mouseMoveBubble);
|
|
|
5354 |
document.removeEventListener("mousemove", self.mouseMove);
|
|
|
5355 |
document.removeEventListener("mouseup", self.mouseUp);
|
|
|
5356 |
document.removeEventListener('mousewheel', self.mouseWheel);
|
|
|
5357 |
document.removeEventListener('wheel', self.mouseWheel);
|
|
|
5358 |
|
|
|
5359 |
container.removeEventListener('scrollChildIntoView', self.scrollChildIntoView);
|
|
|
5360 |
document.removeEventListener('resetScrollView', self.resetScrollView);
|
|
|
5361 |
|
|
|
5362 |
ionic.tap.removeClonedInputs(container, self);
|
|
|
5363 |
|
|
|
5364 |
delete self.__container;
|
|
|
5365 |
delete self.__content;
|
|
|
5366 |
delete self.__indicatorX;
|
|
|
5367 |
delete self.__indicatorY;
|
|
|
5368 |
delete self.options.el;
|
|
|
5369 |
|
|
|
5370 |
self.__callback = self.scrollChildIntoView = self.resetScrollView = NOOP;
|
|
|
5371 |
|
|
|
5372 |
self.mouseMove = self.mouseDown = self.mouseUp = self.mouseWheel =
|
|
|
5373 |
self.touchStart = self.touchMove = self.touchEnd = self.touchCancel = NOOP;
|
|
|
5374 |
|
|
|
5375 |
self.resize = self.scrollTo = self.zoomTo =
|
|
|
5376 |
self.__scrollingComplete = NOOP;
|
|
|
5377 |
container = null;
|
|
|
5378 |
},
|
|
|
5379 |
|
|
|
5380 |
/** Create a scroll bar div with the given direction **/
|
|
|
5381 |
__createScrollbar: function(direction) {
|
|
|
5382 |
var bar = document.createElement('div'),
|
|
|
5383 |
indicator = document.createElement('div');
|
|
|
5384 |
|
|
|
5385 |
indicator.className = 'scroll-bar-indicator scroll-bar-fade-out';
|
|
|
5386 |
|
|
|
5387 |
if (direction == 'h') {
|
|
|
5388 |
bar.className = 'scroll-bar scroll-bar-h';
|
|
|
5389 |
} else {
|
|
|
5390 |
bar.className = 'scroll-bar scroll-bar-v';
|
|
|
5391 |
}
|
|
|
5392 |
|
|
|
5393 |
bar.appendChild(indicator);
|
|
|
5394 |
return bar;
|
|
|
5395 |
},
|
|
|
5396 |
|
|
|
5397 |
__createScrollbars: function() {
|
|
|
5398 |
var self = this;
|
|
|
5399 |
var indicatorX, indicatorY;
|
|
|
5400 |
|
|
|
5401 |
if (self.options.scrollingX) {
|
|
|
5402 |
indicatorX = {
|
|
|
5403 |
el: self.__createScrollbar('h'),
|
|
|
5404 |
sizeRatio: 1
|
|
|
5405 |
};
|
|
|
5406 |
indicatorX.indicator = indicatorX.el.children[0];
|
|
|
5407 |
|
|
|
5408 |
if (self.options.scrollbarX) {
|
|
|
5409 |
self.__container.appendChild(indicatorX.el);
|
|
|
5410 |
}
|
|
|
5411 |
self.__indicatorX = indicatorX;
|
|
|
5412 |
}
|
|
|
5413 |
|
|
|
5414 |
if (self.options.scrollingY) {
|
|
|
5415 |
indicatorY = {
|
|
|
5416 |
el: self.__createScrollbar('v'),
|
|
|
5417 |
sizeRatio: 1
|
|
|
5418 |
};
|
|
|
5419 |
indicatorY.indicator = indicatorY.el.children[0];
|
|
|
5420 |
|
|
|
5421 |
if (self.options.scrollbarY) {
|
|
|
5422 |
self.__container.appendChild(indicatorY.el);
|
|
|
5423 |
}
|
|
|
5424 |
self.__indicatorY = indicatorY;
|
|
|
5425 |
}
|
|
|
5426 |
},
|
|
|
5427 |
|
|
|
5428 |
__resizeScrollbars: function() {
|
|
|
5429 |
var self = this;
|
|
|
5430 |
|
|
|
5431 |
// Update horiz bar
|
|
|
5432 |
if (self.__indicatorX) {
|
|
|
5433 |
var width = Math.max(Math.round(self.__clientWidth * self.__clientWidth / (self.__contentWidth)), 20);
|
|
|
5434 |
if (width > self.__contentWidth) {
|
|
|
5435 |
width = 0;
|
|
|
5436 |
}
|
|
|
5437 |
if (width !== self.__indicatorX.size) {
|
|
|
5438 |
ionic.requestAnimationFrame(function(){
|
|
|
5439 |
self.__indicatorX.indicator.style.width = width + 'px';
|
|
|
5440 |
});
|
|
|
5441 |
}
|
|
|
5442 |
self.__indicatorX.size = width;
|
|
|
5443 |
self.__indicatorX.minScale = self.options.minScrollbarSizeX / width;
|
|
|
5444 |
self.__indicatorX.maxPos = self.__clientWidth - width;
|
|
|
5445 |
self.__indicatorX.sizeRatio = self.__maxScrollLeft ? self.__indicatorX.maxPos / self.__maxScrollLeft : 1;
|
|
|
5446 |
}
|
|
|
5447 |
|
|
|
5448 |
// Update vert bar
|
|
|
5449 |
if (self.__indicatorY) {
|
|
|
5450 |
var height = Math.max(Math.round(self.__clientHeight * self.__clientHeight / (self.__contentHeight)), 20);
|
|
|
5451 |
if (height > self.__contentHeight) {
|
|
|
5452 |
height = 0;
|
|
|
5453 |
}
|
|
|
5454 |
if (height !== self.__indicatorY.size) {
|
|
|
5455 |
ionic.requestAnimationFrame(function(){
|
|
|
5456 |
self.__indicatorY && (self.__indicatorY.indicator.style.height = height + 'px');
|
|
|
5457 |
});
|
|
|
5458 |
}
|
|
|
5459 |
self.__indicatorY.size = height;
|
|
|
5460 |
self.__indicatorY.minScale = self.options.minScrollbarSizeY / height;
|
|
|
5461 |
self.__indicatorY.maxPos = self.__clientHeight - height;
|
|
|
5462 |
self.__indicatorY.sizeRatio = self.__maxScrollTop ? self.__indicatorY.maxPos / self.__maxScrollTop : 1;
|
|
|
5463 |
}
|
|
|
5464 |
},
|
|
|
5465 |
|
|
|
5466 |
/**
|
|
|
5467 |
* Move and scale the scrollbars as the page scrolls.
|
|
|
5468 |
*/
|
|
|
5469 |
__repositionScrollbars: function() {
|
|
|
5470 |
var self = this,
|
|
|
5471 |
heightScale, widthScale,
|
|
|
5472 |
widthDiff, heightDiff,
|
|
|
5473 |
x, y,
|
|
|
5474 |
xstop = 0, ystop = 0;
|
|
|
5475 |
|
|
|
5476 |
if (self.__indicatorX) {
|
|
|
5477 |
// Handle the X scrollbar
|
|
|
5478 |
|
|
|
5479 |
// Don't go all the way to the right if we have a vertical scrollbar as well
|
|
|
5480 |
if (self.__indicatorY) xstop = 10;
|
|
|
5481 |
|
|
|
5482 |
x = Math.round(self.__indicatorX.sizeRatio * self.__scrollLeft) || 0;
|
|
|
5483 |
|
|
|
5484 |
// The the difference between the last content X position, and our overscrolled one
|
|
|
5485 |
widthDiff = self.__scrollLeft - (self.__maxScrollLeft - xstop);
|
|
|
5486 |
|
|
|
5487 |
if (self.__scrollLeft < 0) {
|
|
|
5488 |
|
|
|
5489 |
widthScale = Math.max(self.__indicatorX.minScale,
|
|
|
5490 |
(self.__indicatorX.size - Math.abs(self.__scrollLeft)) / self.__indicatorX.size);
|
|
|
5491 |
|
|
|
5492 |
// Stay at left
|
|
|
5493 |
x = 0;
|
|
|
5494 |
|
|
|
5495 |
// Make sure scale is transformed from the left/center origin point
|
|
|
5496 |
self.__indicatorX.indicator.style[self.__transformOriginProperty] = 'left center';
|
|
|
5497 |
} else if (widthDiff > 0) {
|
|
|
5498 |
|
|
|
5499 |
widthScale = Math.max(self.__indicatorX.minScale,
|
|
|
5500 |
(self.__indicatorX.size - widthDiff) / self.__indicatorX.size);
|
|
|
5501 |
|
|
|
5502 |
// Stay at the furthest x for the scrollable viewport
|
|
|
5503 |
x = self.__indicatorX.maxPos - xstop;
|
|
|
5504 |
|
|
|
5505 |
// Make sure scale is transformed from the right/center origin point
|
|
|
5506 |
self.__indicatorX.indicator.style[self.__transformOriginProperty] = 'right center';
|
|
|
5507 |
|
|
|
5508 |
} else {
|
|
|
5509 |
|
|
|
5510 |
// Normal motion
|
|
|
5511 |
x = Math.min(self.__maxScrollLeft, Math.max(0, x));
|
|
|
5512 |
widthScale = 1;
|
|
|
5513 |
|
|
|
5514 |
}
|
|
|
5515 |
|
|
|
5516 |
var translate3dX = 'translate3d(' + x + 'px, 0, 0) scaleX(' + widthScale + ')';
|
|
|
5517 |
if (self.__indicatorX.transformProp !== translate3dX) {
|
|
|
5518 |
self.__indicatorX.indicator.style[self.__transformProperty] = translate3dX;
|
|
|
5519 |
self.__indicatorX.transformProp = translate3dX;
|
|
|
5520 |
}
|
|
|
5521 |
}
|
|
|
5522 |
|
|
|
5523 |
if (self.__indicatorY) {
|
|
|
5524 |
|
|
|
5525 |
y = Math.round(self.__indicatorY.sizeRatio * self.__scrollTop) || 0;
|
|
|
5526 |
|
|
|
5527 |
// Don't go all the way to the right if we have a vertical scrollbar as well
|
|
|
5528 |
if (self.__indicatorX) ystop = 10;
|
|
|
5529 |
|
|
|
5530 |
heightDiff = self.__scrollTop - (self.__maxScrollTop - ystop);
|
|
|
5531 |
|
|
|
5532 |
if (self.__scrollTop < 0) {
|
|
|
5533 |
|
|
|
5534 |
heightScale = Math.max(self.__indicatorY.minScale, (self.__indicatorY.size - Math.abs(self.__scrollTop)) / self.__indicatorY.size);
|
|
|
5535 |
|
|
|
5536 |
// Stay at top
|
|
|
5537 |
y = 0;
|
|
|
5538 |
|
|
|
5539 |
// Make sure scale is transformed from the center/top origin point
|
|
|
5540 |
if (self.__indicatorY.originProp !== 'center top') {
|
|
|
5541 |
self.__indicatorY.indicator.style[self.__transformOriginProperty] = 'center top';
|
|
|
5542 |
self.__indicatorY.originProp = 'center top';
|
|
|
5543 |
}
|
|
|
5544 |
|
|
|
5545 |
} else if (heightDiff > 0) {
|
|
|
5546 |
|
|
|
5547 |
heightScale = Math.max(self.__indicatorY.minScale, (self.__indicatorY.size - heightDiff) / self.__indicatorY.size);
|
|
|
5548 |
|
|
|
5549 |
// Stay at bottom of scrollable viewport
|
|
|
5550 |
y = self.__indicatorY.maxPos - ystop;
|
|
|
5551 |
|
|
|
5552 |
// Make sure scale is transformed from the center/bottom origin point
|
|
|
5553 |
if (self.__indicatorY.originProp !== 'center bottom') {
|
|
|
5554 |
self.__indicatorY.indicator.style[self.__transformOriginProperty] = 'center bottom';
|
|
|
5555 |
self.__indicatorY.originProp = 'center bottom';
|
|
|
5556 |
}
|
|
|
5557 |
|
|
|
5558 |
} else {
|
|
|
5559 |
|
|
|
5560 |
// Normal motion
|
|
|
5561 |
y = Math.min(self.__maxScrollTop, Math.max(0, y));
|
|
|
5562 |
heightScale = 1;
|
|
|
5563 |
|
|
|
5564 |
}
|
|
|
5565 |
|
|
|
5566 |
var translate3dY = 'translate3d(0,' + y + 'px, 0) scaleY(' + heightScale + ')';
|
|
|
5567 |
if (self.__indicatorY.transformProp !== translate3dY) {
|
|
|
5568 |
self.__indicatorY.indicator.style[self.__transformProperty] = translate3dY;
|
|
|
5569 |
self.__indicatorY.transformProp = translate3dY;
|
|
|
5570 |
}
|
|
|
5571 |
}
|
|
|
5572 |
},
|
|
|
5573 |
|
|
|
5574 |
__fadeScrollbars: function(direction, delay) {
|
|
|
5575 |
var self = this;
|
|
|
5576 |
|
|
|
5577 |
if (!self.options.scrollbarsFade) {
|
|
|
5578 |
return;
|
|
|
5579 |
}
|
|
|
5580 |
|
|
|
5581 |
var className = 'scroll-bar-fade-out';
|
|
|
5582 |
|
|
|
5583 |
if (self.options.scrollbarsFade === true) {
|
|
|
5584 |
clearTimeout(self.__scrollbarFadeTimeout);
|
|
|
5585 |
|
|
|
5586 |
if (direction == 'in') {
|
|
|
5587 |
if (self.__indicatorX) { self.__indicatorX.indicator.classList.remove(className); }
|
|
|
5588 |
if (self.__indicatorY) { self.__indicatorY.indicator.classList.remove(className); }
|
|
|
5589 |
} else {
|
|
|
5590 |
self.__scrollbarFadeTimeout = setTimeout(function() {
|
|
|
5591 |
if (self.__indicatorX) { self.__indicatorX.indicator.classList.add(className); }
|
|
|
5592 |
if (self.__indicatorY) { self.__indicatorY.indicator.classList.add(className); }
|
|
|
5593 |
}, delay || self.options.scrollbarFadeDelay);
|
|
|
5594 |
}
|
|
|
5595 |
}
|
|
|
5596 |
},
|
|
|
5597 |
|
|
|
5598 |
__scrollingComplete: function() {
|
|
|
5599 |
this.options.scrollingComplete();
|
|
|
5600 |
ionic.tap.removeClonedInputs(this.__container, this);
|
|
|
5601 |
this.__fadeScrollbars('out');
|
|
|
5602 |
},
|
|
|
5603 |
|
|
|
5604 |
resize: function(continueScrolling) {
|
|
|
5605 |
var self = this;
|
|
|
5606 |
if (!self.__container || !self.options) return;
|
|
|
5607 |
|
|
|
5608 |
// Update Scroller dimensions for changed content
|
|
|
5609 |
// Add padding to bottom of content
|
|
|
5610 |
self.setDimensions(
|
|
|
5611 |
self.__container.clientWidth,
|
|
|
5612 |
self.__container.clientHeight,
|
|
|
5613 |
self.options.getContentWidth(),
|
|
|
5614 |
self.options.getContentHeight(),
|
|
|
5615 |
continueScrolling
|
|
|
5616 |
);
|
|
|
5617 |
},
|
|
|
5618 |
/*
|
|
|
5619 |
---------------------------------------------------------------------------
|
|
|
5620 |
PUBLIC API
|
|
|
5621 |
---------------------------------------------------------------------------
|
|
|
5622 |
*/
|
|
|
5623 |
|
|
|
5624 |
getRenderFn: function() {
|
|
|
5625 |
var self = this;
|
|
|
5626 |
|
|
|
5627 |
var content = self.__content;
|
|
|
5628 |
|
|
|
5629 |
var docStyle = document.documentElement.style;
|
|
|
5630 |
|
|
|
5631 |
var engine;
|
|
|
5632 |
if ('MozAppearance' in docStyle) {
|
|
|
5633 |
engine = 'gecko';
|
|
|
5634 |
} else if ('WebkitAppearance' in docStyle) {
|
|
|
5635 |
engine = 'webkit';
|
|
|
5636 |
} else if (typeof navigator.cpuClass === 'string') {
|
|
|
5637 |
engine = 'trident';
|
|
|
5638 |
}
|
|
|
5639 |
|
|
|
5640 |
var vendorPrefix = {
|
|
|
5641 |
trident: 'ms',
|
|
|
5642 |
gecko: 'Moz',
|
|
|
5643 |
webkit: 'Webkit',
|
|
|
5644 |
presto: 'O'
|
|
|
5645 |
}[engine];
|
|
|
5646 |
|
|
|
5647 |
var helperElem = document.createElement("div");
|
|
|
5648 |
var undef;
|
|
|
5649 |
|
|
|
5650 |
var perspectiveProperty = vendorPrefix + "Perspective";
|
|
|
5651 |
var transformProperty = vendorPrefix + "Transform";
|
|
|
5652 |
var transformOriginProperty = vendorPrefix + 'TransformOrigin';
|
|
|
5653 |
|
|
|
5654 |
self.__perspectiveProperty = transformProperty;
|
|
|
5655 |
self.__transformProperty = transformProperty;
|
|
|
5656 |
self.__transformOriginProperty = transformOriginProperty;
|
|
|
5657 |
|
|
|
5658 |
if (helperElem.style[perspectiveProperty] !== undef) {
|
|
|
5659 |
|
|
|
5660 |
return function(left, top, zoom, wasResize) {
|
|
|
5661 |
var translate3d = 'translate3d(' + (-left) + 'px,' + (-top) + 'px,0) scale(' + zoom + ')';
|
|
|
5662 |
if (translate3d !== self.contentTransform) {
|
|
|
5663 |
content.style[transformProperty] = translate3d;
|
|
|
5664 |
self.contentTransform = translate3d;
|
|
|
5665 |
}
|
|
|
5666 |
self.__repositionScrollbars();
|
|
|
5667 |
if (!wasResize) {
|
|
|
5668 |
self.triggerScrollEvent();
|
|
|
5669 |
}
|
|
|
5670 |
};
|
|
|
5671 |
|
|
|
5672 |
} else if (helperElem.style[transformProperty] !== undef) {
|
|
|
5673 |
|
|
|
5674 |
return function(left, top, zoom, wasResize) {
|
|
|
5675 |
content.style[transformProperty] = 'translate(' + (-left) + 'px,' + (-top) + 'px) scale(' + zoom + ')';
|
|
|
5676 |
self.__repositionScrollbars();
|
|
|
5677 |
if (!wasResize) {
|
|
|
5678 |
self.triggerScrollEvent();
|
|
|
5679 |
}
|
|
|
5680 |
};
|
|
|
5681 |
|
|
|
5682 |
} else {
|
|
|
5683 |
|
|
|
5684 |
return function(left, top, zoom, wasResize) {
|
|
|
5685 |
content.style.marginLeft = left ? (-left / zoom) + 'px' : '';
|
|
|
5686 |
content.style.marginTop = top ? (-top / zoom) + 'px' : '';
|
|
|
5687 |
content.style.zoom = zoom || '';
|
|
|
5688 |
self.__repositionScrollbars();
|
|
|
5689 |
if (!wasResize) {
|
|
|
5690 |
self.triggerScrollEvent();
|
|
|
5691 |
}
|
|
|
5692 |
};
|
|
|
5693 |
|
|
|
5694 |
}
|
|
|
5695 |
},
|
|
|
5696 |
|
|
|
5697 |
|
|
|
5698 |
/**
|
|
|
5699 |
* Configures the dimensions of the client (outer) and content (inner) elements.
|
|
|
5700 |
* Requires the available space for the outer element and the outer size of the inner element.
|
|
|
5701 |
* All values which are falsy (null or zero etc.) are ignored and the old value is kept.
|
|
|
5702 |
*
|
|
|
5703 |
* @param clientWidth {Integer} Inner width of outer element
|
|
|
5704 |
* @param clientHeight {Integer} Inner height of outer element
|
|
|
5705 |
* @param contentWidth {Integer} Outer width of inner element
|
|
|
5706 |
* @param contentHeight {Integer} Outer height of inner element
|
|
|
5707 |
*/
|
|
|
5708 |
setDimensions: function(clientWidth, clientHeight, contentWidth, contentHeight, continueScrolling) {
|
|
|
5709 |
var self = this;
|
|
|
5710 |
|
|
|
5711 |
if (!clientWidth && !clientHeight && !contentWidth && !contentHeight) {
|
|
|
5712 |
// this scrollview isn't rendered, don't bother
|
|
|
5713 |
return;
|
|
|
5714 |
}
|
|
|
5715 |
|
|
|
5716 |
// Only update values which are defined
|
|
|
5717 |
if (clientWidth === +clientWidth) {
|
|
|
5718 |
self.__clientWidth = clientWidth;
|
|
|
5719 |
}
|
|
|
5720 |
|
|
|
5721 |
if (clientHeight === +clientHeight) {
|
|
|
5722 |
self.__clientHeight = clientHeight;
|
|
|
5723 |
}
|
|
|
5724 |
|
|
|
5725 |
if (contentWidth === +contentWidth) {
|
|
|
5726 |
self.__contentWidth = contentWidth;
|
|
|
5727 |
}
|
|
|
5728 |
|
|
|
5729 |
if (contentHeight === +contentHeight) {
|
|
|
5730 |
self.__contentHeight = contentHeight;
|
|
|
5731 |
}
|
|
|
5732 |
|
|
|
5733 |
// Refresh maximums
|
|
|
5734 |
self.__computeScrollMax();
|
|
|
5735 |
self.__resizeScrollbars();
|
|
|
5736 |
|
|
|
5737 |
// Refresh scroll position
|
|
|
5738 |
if (!continueScrolling) {
|
|
|
5739 |
self.scrollTo(self.__scrollLeft, self.__scrollTop, true, null, true);
|
|
|
5740 |
}
|
|
|
5741 |
|
|
|
5742 |
},
|
|
|
5743 |
|
|
|
5744 |
|
|
|
5745 |
/**
|
|
|
5746 |
* Sets the client coordinates in relation to the document.
|
|
|
5747 |
*
|
|
|
5748 |
* @param left {Integer} Left position of outer element
|
|
|
5749 |
* @param top {Integer} Top position of outer element
|
|
|
5750 |
*/
|
|
|
5751 |
setPosition: function(left, top) {
|
|
|
5752 |
this.__clientLeft = left || 0;
|
|
|
5753 |
this.__clientTop = top || 0;
|
|
|
5754 |
},
|
|
|
5755 |
|
|
|
5756 |
|
|
|
5757 |
/**
|
|
|
5758 |
* Configures the snapping (when snapping is active)
|
|
|
5759 |
*
|
|
|
5760 |
* @param width {Integer} Snapping width
|
|
|
5761 |
* @param height {Integer} Snapping height
|
|
|
5762 |
*/
|
|
|
5763 |
setSnapSize: function(width, height) {
|
|
|
5764 |
this.__snapWidth = width;
|
|
|
5765 |
this.__snapHeight = height;
|
|
|
5766 |
},
|
|
|
5767 |
|
|
|
5768 |
|
|
|
5769 |
/**
|
|
|
5770 |
* Activates pull-to-refresh. A special zone on the top of the list to start a list refresh whenever
|
|
|
5771 |
* the user event is released during visibility of this zone. This was introduced by some apps on iOS like
|
|
|
5772 |
* the official Twitter client.
|
|
|
5773 |
*
|
|
|
5774 |
* @param height {Integer} Height of pull-to-refresh zone on top of rendered list
|
|
|
5775 |
* @param activateCallback {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release.
|
|
|
5776 |
* @param deactivateCallback {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled.
|
|
|
5777 |
* @param startCallback {Function} Callback to execute to start the real async refresh action. Call {@link #finishPullToRefresh} after finish of refresh.
|
|
|
5778 |
* @param showCallback {Function} Callback to execute when the refresher should be shown. This is for showing the refresher during a negative scrollTop.
|
|
|
5779 |
* @param hideCallback {Function} Callback to execute when the refresher should be hidden. This is for hiding the refresher when it's behind the nav bar.
|
|
|
5780 |
* @param tailCallback {Function} Callback to execute just before the refresher returns to it's original state. This is for zooming out the refresher.
|
|
|
5781 |
* @param pullProgressCallback Callback to state the progress while pulling to refresh
|
|
|
5782 |
*/
|
|
|
5783 |
activatePullToRefresh: function(height, refresherMethods) {
|
|
|
5784 |
var self = this;
|
|
|
5785 |
|
|
|
5786 |
self.__refreshHeight = height;
|
|
|
5787 |
self.__refreshActivate = function() { ionic.requestAnimationFrame(refresherMethods.activate); };
|
|
|
5788 |
self.__refreshDeactivate = function() { ionic.requestAnimationFrame(refresherMethods.deactivate); };
|
|
|
5789 |
self.__refreshStart = function() { ionic.requestAnimationFrame(refresherMethods.start); };
|
|
|
5790 |
self.__refreshShow = function() { ionic.requestAnimationFrame(refresherMethods.show); };
|
|
|
5791 |
self.__refreshHide = function() { ionic.requestAnimationFrame(refresherMethods.hide); };
|
|
|
5792 |
self.__refreshTail = function() { ionic.requestAnimationFrame(refresherMethods.tail); };
|
|
|
5793 |
self.__refreshTailTime = 100;
|
|
|
5794 |
self.__minSpinTime = 600;
|
|
|
5795 |
},
|
|
|
5796 |
|
|
|
5797 |
|
|
|
5798 |
/**
|
|
|
5799 |
* Starts pull-to-refresh manually.
|
|
|
5800 |
*/
|
|
|
5801 |
triggerPullToRefresh: function() {
|
|
|
5802 |
// Use publish instead of scrollTo to allow scrolling to out of boundary position
|
|
|
5803 |
// We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
|
|
|
5804 |
this.__publish(this.__scrollLeft, -this.__refreshHeight, this.__zoomLevel, true);
|
|
|
5805 |
|
|
|
5806 |
var d = new Date();
|
|
|
5807 |
this.refreshStartTime = d.getTime();
|
|
|
5808 |
|
|
|
5809 |
if (this.__refreshStart) {
|
|
|
5810 |
this.__refreshStart();
|
|
|
5811 |
}
|
|
|
5812 |
},
|
|
|
5813 |
|
|
|
5814 |
|
|
|
5815 |
/**
|
|
|
5816 |
* Signalizes that pull-to-refresh is finished.
|
|
|
5817 |
*/
|
|
|
5818 |
finishPullToRefresh: function() {
|
|
|
5819 |
var self = this;
|
|
|
5820 |
// delay to make sure the spinner has a chance to spin for a split second before it's dismissed
|
|
|
5821 |
var d = new Date();
|
|
|
5822 |
var delay = 0;
|
|
|
5823 |
if (self.refreshStartTime + self.__minSpinTime > d.getTime()) {
|
|
|
5824 |
delay = self.refreshStartTime + self.__minSpinTime - d.getTime();
|
|
|
5825 |
}
|
|
|
5826 |
setTimeout(function() {
|
|
|
5827 |
if (self.__refreshTail) {
|
|
|
5828 |
self.__refreshTail();
|
|
|
5829 |
}
|
|
|
5830 |
setTimeout(function() {
|
|
|
5831 |
self.__refreshActive = false;
|
|
|
5832 |
if (self.__refreshDeactivate) {
|
|
|
5833 |
self.__refreshDeactivate();
|
|
|
5834 |
}
|
|
|
5835 |
if (self.__refreshHide) {
|
|
|
5836 |
self.__refreshHide();
|
|
|
5837 |
}
|
|
|
5838 |
|
|
|
5839 |
self.scrollTo(self.__scrollLeft, self.__scrollTop, true);
|
|
|
5840 |
}, self.__refreshTailTime);
|
|
|
5841 |
}, delay);
|
|
|
5842 |
},
|
|
|
5843 |
|
|
|
5844 |
|
|
|
5845 |
/**
|
|
|
5846 |
* Returns the scroll position and zooming values
|
|
|
5847 |
*
|
|
|
5848 |
* @return {Map} `left` and `top` scroll position and `zoom` level
|
|
|
5849 |
*/
|
|
|
5850 |
getValues: function() {
|
|
|
5851 |
return {
|
|
|
5852 |
left: this.__scrollLeft,
|
|
|
5853 |
top: this.__scrollTop,
|
|
|
5854 |
zoom: this.__zoomLevel
|
|
|
5855 |
};
|
|
|
5856 |
},
|
|
|
5857 |
|
|
|
5858 |
|
|
|
5859 |
/**
|
|
|
5860 |
* Returns the maximum scroll values
|
|
|
5861 |
*
|
|
|
5862 |
* @return {Map} `left` and `top` maximum scroll values
|
|
|
5863 |
*/
|
|
|
5864 |
getScrollMax: function() {
|
|
|
5865 |
return {
|
|
|
5866 |
left: this.__maxScrollLeft,
|
|
|
5867 |
top: this.__maxScrollTop
|
|
|
5868 |
};
|
|
|
5869 |
},
|
|
|
5870 |
|
|
|
5871 |
|
|
|
5872 |
/**
|
|
|
5873 |
* Zooms to the given level. Supports optional animation. Zooms
|
|
|
5874 |
* the center when no coordinates are given.
|
|
|
5875 |
*
|
|
|
5876 |
* @param level {Number} Level to zoom to
|
|
|
5877 |
* @param animate {Boolean} Whether to use animation
|
|
|
5878 |
* @param originLeft {Number} Zoom in at given left coordinate
|
|
|
5879 |
* @param originTop {Number} Zoom in at given top coordinate
|
|
|
5880 |
*/
|
|
|
5881 |
zoomTo: function(level, animate, originLeft, originTop) {
|
|
|
5882 |
var self = this;
|
|
|
5883 |
|
|
|
5884 |
if (!self.options.zooming) {
|
|
|
5885 |
throw new Error("Zooming is not enabled!");
|
|
|
5886 |
}
|
|
|
5887 |
|
|
|
5888 |
// Stop deceleration
|
|
|
5889 |
if (self.__isDecelerating) {
|
|
|
5890 |
zyngaCore.effect.Animate.stop(self.__isDecelerating);
|
|
|
5891 |
self.__isDecelerating = false;
|
|
|
5892 |
}
|
|
|
5893 |
|
|
|
5894 |
var oldLevel = self.__zoomLevel;
|
|
|
5895 |
|
|
|
5896 |
// Normalize input origin to center of viewport if not defined
|
|
|
5897 |
if (originLeft == null) {
|
|
|
5898 |
originLeft = self.__clientWidth / 2;
|
|
|
5899 |
}
|
|
|
5900 |
|
|
|
5901 |
if (originTop == null) {
|
|
|
5902 |
originTop = self.__clientHeight / 2;
|
|
|
5903 |
}
|
|
|
5904 |
|
|
|
5905 |
// Limit level according to configuration
|
|
|
5906 |
level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);
|
|
|
5907 |
|
|
|
5908 |
// Recompute maximum values while temporary tweaking maximum scroll ranges
|
|
|
5909 |
self.__computeScrollMax(level);
|
|
|
5910 |
|
|
|
5911 |
// Recompute left and top coordinates based on new zoom level
|
|
|
5912 |
var left = ((originLeft + self.__scrollLeft) * level / oldLevel) - originLeft;
|
|
|
5913 |
var top = ((originTop + self.__scrollTop) * level / oldLevel) - originTop;
|
|
|
5914 |
|
|
|
5915 |
// Limit x-axis
|
|
|
5916 |
if (left > self.__maxScrollLeft) {
|
|
|
5917 |
left = self.__maxScrollLeft;
|
|
|
5918 |
} else if (left < 0) {
|
|
|
5919 |
left = 0;
|
|
|
5920 |
}
|
|
|
5921 |
|
|
|
5922 |
// Limit y-axis
|
|
|
5923 |
if (top > self.__maxScrollTop) {
|
|
|
5924 |
top = self.__maxScrollTop;
|
|
|
5925 |
} else if (top < 0) {
|
|
|
5926 |
top = 0;
|
|
|
5927 |
}
|
|
|
5928 |
|
|
|
5929 |
// Push values out
|
|
|
5930 |
self.__publish(left, top, level, animate);
|
|
|
5931 |
|
|
|
5932 |
},
|
|
|
5933 |
|
|
|
5934 |
|
|
|
5935 |
/**
|
|
|
5936 |
* Zooms the content by the given factor.
|
|
|
5937 |
*
|
|
|
5938 |
* @param factor {Number} Zoom by given factor
|
|
|
5939 |
* @param animate {Boolean} Whether to use animation
|
|
|
5940 |
* @param originLeft {Number} Zoom in at given left coordinate
|
|
|
5941 |
* @param originTop {Number} Zoom in at given top coordinate
|
|
|
5942 |
*/
|
|
|
5943 |
zoomBy: function(factor, animate, originLeft, originTop) {
|
|
|
5944 |
this.zoomTo(this.__zoomLevel * factor, animate, originLeft, originTop);
|
|
|
5945 |
},
|
|
|
5946 |
|
|
|
5947 |
|
|
|
5948 |
/**
|
|
|
5949 |
* Scrolls to the given position. Respect limitations and snapping automatically.
|
|
|
5950 |
*
|
|
|
5951 |
* @param left {Number} Horizontal scroll position, keeps current if value is <code>null</code>
|
|
|
5952 |
* @param top {Number} Vertical scroll position, keeps current if value is <code>null</code>
|
|
|
5953 |
* @param animate {Boolean} Whether the scrolling should happen using an animation
|
|
|
5954 |
* @param zoom {Number} Zoom level to go to
|
|
|
5955 |
*/
|
|
|
5956 |
scrollTo: function(left, top, animate, zoom, wasResize) {
|
|
|
5957 |
var self = this;
|
|
|
5958 |
|
|
|
5959 |
// Stop deceleration
|
|
|
5960 |
if (self.__isDecelerating) {
|
|
|
5961 |
zyngaCore.effect.Animate.stop(self.__isDecelerating);
|
|
|
5962 |
self.__isDecelerating = false;
|
|
|
5963 |
}
|
|
|
5964 |
|
|
|
5965 |
// Correct coordinates based on new zoom level
|
|
|
5966 |
if (zoom != null && zoom !== self.__zoomLevel) {
|
|
|
5967 |
|
|
|
5968 |
if (!self.options.zooming) {
|
|
|
5969 |
throw new Error("Zooming is not enabled!");
|
|
|
5970 |
}
|
|
|
5971 |
|
|
|
5972 |
left *= zoom;
|
|
|
5973 |
top *= zoom;
|
|
|
5974 |
|
|
|
5975 |
// Recompute maximum values while temporary tweaking maximum scroll ranges
|
|
|
5976 |
self.__computeScrollMax(zoom);
|
|
|
5977 |
|
|
|
5978 |
} else {
|
|
|
5979 |
|
|
|
5980 |
// Keep zoom when not defined
|
|
|
5981 |
zoom = self.__zoomLevel;
|
|
|
5982 |
|
|
|
5983 |
}
|
|
|
5984 |
|
|
|
5985 |
if (!self.options.scrollingX) {
|
|
|
5986 |
|
|
|
5987 |
left = self.__scrollLeft;
|
|
|
5988 |
|
|
|
5989 |
} else {
|
|
|
5990 |
|
|
|
5991 |
if (self.options.paging) {
|
|
|
5992 |
left = Math.round(left / self.__clientWidth) * self.__clientWidth;
|
|
|
5993 |
} else if (self.options.snapping) {
|
|
|
5994 |
left = Math.round(left / self.__snapWidth) * self.__snapWidth;
|
|
|
5995 |
}
|
|
|
5996 |
|
|
|
5997 |
}
|
|
|
5998 |
|
|
|
5999 |
if (!self.options.scrollingY) {
|
|
|
6000 |
|
|
|
6001 |
top = self.__scrollTop;
|
|
|
6002 |
|
|
|
6003 |
} else {
|
|
|
6004 |
|
|
|
6005 |
if (self.options.paging) {
|
|
|
6006 |
top = Math.round(top / self.__clientHeight) * self.__clientHeight;
|
|
|
6007 |
} else if (self.options.snapping) {
|
|
|
6008 |
top = Math.round(top / self.__snapHeight) * self.__snapHeight;
|
|
|
6009 |
}
|
|
|
6010 |
|
|
|
6011 |
}
|
|
|
6012 |
|
|
|
6013 |
// Limit for allowed ranges
|
|
|
6014 |
left = Math.max(Math.min(self.__maxScrollLeft, left), 0);
|
|
|
6015 |
top = Math.max(Math.min(self.__maxScrollTop, top), 0);
|
|
|
6016 |
|
|
|
6017 |
// Don't animate when no change detected, still call publish to make sure
|
|
|
6018 |
// that rendered position is really in-sync with internal data
|
|
|
6019 |
if (left === self.__scrollLeft && top === self.__scrollTop) {
|
|
|
6020 |
animate = false;
|
|
|
6021 |
}
|
|
|
6022 |
|
|
|
6023 |
// Publish new values
|
|
|
6024 |
self.__publish(left, top, zoom, animate, wasResize);
|
|
|
6025 |
|
|
|
6026 |
},
|
|
|
6027 |
|
|
|
6028 |
|
|
|
6029 |
/**
|
|
|
6030 |
* Scroll by the given offset
|
|
|
6031 |
*
|
|
|
6032 |
* @param left {Number} Scroll x-axis by given offset
|
|
|
6033 |
* @param top {Number} Scroll y-axis by given offset
|
|
|
6034 |
* @param animate {Boolean} Whether to animate the given change
|
|
|
6035 |
*/
|
|
|
6036 |
scrollBy: function(left, top, animate) {
|
|
|
6037 |
var self = this;
|
|
|
6038 |
|
|
|
6039 |
var startLeft = self.__isAnimating ? self.__scheduledLeft : self.__scrollLeft;
|
|
|
6040 |
var startTop = self.__isAnimating ? self.__scheduledTop : self.__scrollTop;
|
|
|
6041 |
|
|
|
6042 |
self.scrollTo(startLeft + (left || 0), startTop + (top || 0), animate);
|
|
|
6043 |
},
|
|
|
6044 |
|
|
|
6045 |
|
|
|
6046 |
|
|
|
6047 |
/*
|
|
|
6048 |
---------------------------------------------------------------------------
|
|
|
6049 |
EVENT CALLBACKS
|
|
|
6050 |
---------------------------------------------------------------------------
|
|
|
6051 |
*/
|
|
|
6052 |
|
|
|
6053 |
/**
|
|
|
6054 |
* Mouse wheel handler for zooming support
|
|
|
6055 |
*/
|
|
|
6056 |
doMouseZoom: function(wheelDelta, timeStamp, pageX, pageY) {
|
|
|
6057 |
var change = wheelDelta > 0 ? 0.97 : 1.03;
|
|
|
6058 |
return this.zoomTo(this.__zoomLevel * change, false, pageX - this.__clientLeft, pageY - this.__clientTop);
|
|
|
6059 |
},
|
|
|
6060 |
|
|
|
6061 |
/**
|
|
|
6062 |
* Touch start handler for scrolling support
|
|
|
6063 |
*/
|
|
|
6064 |
doTouchStart: function(touches, timeStamp) {
|
|
|
6065 |
var self = this;
|
|
|
6066 |
|
|
|
6067 |
// remember if the deceleration was just stopped
|
|
|
6068 |
self.__decStopped = !!(self.__isDecelerating || self.__isAnimating);
|
|
|
6069 |
|
|
|
6070 |
self.hintResize();
|
|
|
6071 |
|
|
|
6072 |
if (timeStamp instanceof Date) {
|
|
|
6073 |
timeStamp = timeStamp.valueOf();
|
|
|
6074 |
}
|
|
|
6075 |
if (typeof timeStamp !== "number") {
|
|
|
6076 |
timeStamp = Date.now();
|
|
|
6077 |
}
|
|
|
6078 |
|
|
|
6079 |
// Reset interruptedAnimation flag
|
|
|
6080 |
self.__interruptedAnimation = true;
|
|
|
6081 |
|
|
|
6082 |
// Stop deceleration
|
|
|
6083 |
if (self.__isDecelerating) {
|
|
|
6084 |
zyngaCore.effect.Animate.stop(self.__isDecelerating);
|
|
|
6085 |
self.__isDecelerating = false;
|
|
|
6086 |
self.__interruptedAnimation = true;
|
|
|
6087 |
}
|
|
|
6088 |
|
|
|
6089 |
// Stop animation
|
|
|
6090 |
if (self.__isAnimating) {
|
|
|
6091 |
zyngaCore.effect.Animate.stop(self.__isAnimating);
|
|
|
6092 |
self.__isAnimating = false;
|
|
|
6093 |
self.__interruptedAnimation = true;
|
|
|
6094 |
}
|
|
|
6095 |
|
|
|
6096 |
// Use center point when dealing with two fingers
|
|
|
6097 |
var currentTouchLeft, currentTouchTop;
|
|
|
6098 |
var isSingleTouch = touches.length === 1;
|
|
|
6099 |
if (isSingleTouch) {
|
|
|
6100 |
currentTouchLeft = touches[0].pageX;
|
|
|
6101 |
currentTouchTop = touches[0].pageY;
|
|
|
6102 |
} else {
|
|
|
6103 |
currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2;
|
|
|
6104 |
currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
|
|
|
6105 |
}
|
|
|
6106 |
|
|
|
6107 |
// Store initial positions
|
|
|
6108 |
self.__initialTouchLeft = currentTouchLeft;
|
|
|
6109 |
self.__initialTouchTop = currentTouchTop;
|
|
|
6110 |
|
|
|
6111 |
// Store initial touchList for scale calculation
|
|
|
6112 |
self.__initialTouches = touches;
|
|
|
6113 |
|
|
|
6114 |
// Store current zoom level
|
|
|
6115 |
self.__zoomLevelStart = self.__zoomLevel;
|
|
|
6116 |
|
|
|
6117 |
// Store initial touch positions
|
|
|
6118 |
self.__lastTouchLeft = currentTouchLeft;
|
|
|
6119 |
self.__lastTouchTop = currentTouchTop;
|
|
|
6120 |
|
|
|
6121 |
// Store initial move time stamp
|
|
|
6122 |
self.__lastTouchMove = timeStamp;
|
|
|
6123 |
|
|
|
6124 |
// Reset initial scale
|
|
|
6125 |
self.__lastScale = 1;
|
|
|
6126 |
|
|
|
6127 |
// Reset locking flags
|
|
|
6128 |
self.__enableScrollX = !isSingleTouch && self.options.scrollingX;
|
|
|
6129 |
self.__enableScrollY = !isSingleTouch && self.options.scrollingY;
|
|
|
6130 |
|
|
|
6131 |
// Reset tracking flag
|
|
|
6132 |
self.__isTracking = true;
|
|
|
6133 |
|
|
|
6134 |
// Reset deceleration complete flag
|
|
|
6135 |
self.__didDecelerationComplete = false;
|
|
|
6136 |
|
|
|
6137 |
// Dragging starts directly with two fingers, otherwise lazy with an offset
|
|
|
6138 |
self.__isDragging = !isSingleTouch;
|
|
|
6139 |
|
|
|
6140 |
// Some features are disabled in multi touch scenarios
|
|
|
6141 |
self.__isSingleTouch = isSingleTouch;
|
|
|
6142 |
|
|
|
6143 |
// Clearing data structure
|
|
|
6144 |
self.__positions = [];
|
|
|
6145 |
|
|
|
6146 |
},
|
|
|
6147 |
|
|
|
6148 |
|
|
|
6149 |
/**
|
|
|
6150 |
* Touch move handler for scrolling support
|
|
|
6151 |
*/
|
|
|
6152 |
doTouchMove: function(touches, timeStamp, scale) {
|
|
|
6153 |
if (timeStamp instanceof Date) {
|
|
|
6154 |
timeStamp = timeStamp.valueOf();
|
|
|
6155 |
}
|
|
|
6156 |
if (typeof timeStamp !== "number") {
|
|
|
6157 |
timeStamp = Date.now();
|
|
|
6158 |
}
|
|
|
6159 |
|
|
|
6160 |
var self = this;
|
|
|
6161 |
|
|
|
6162 |
// Ignore event when tracking is not enabled (event might be outside of element)
|
|
|
6163 |
if (!self.__isTracking) {
|
|
|
6164 |
return;
|
|
|
6165 |
}
|
|
|
6166 |
|
|
|
6167 |
var currentTouchLeft, currentTouchTop;
|
|
|
6168 |
|
|
|
6169 |
// Compute move based around of center of fingers
|
|
|
6170 |
if (touches.length === 2) {
|
|
|
6171 |
currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2;
|
|
|
6172 |
currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
|
|
|
6173 |
|
|
|
6174 |
// Calculate scale when not present and only when touches are used
|
|
|
6175 |
if (!scale && self.options.zooming) {
|
|
|
6176 |
scale = self.__getScale(self.__initialTouches, touches);
|
|
|
6177 |
}
|
|
|
6178 |
} else {
|
|
|
6179 |
currentTouchLeft = touches[0].pageX;
|
|
|
6180 |
currentTouchTop = touches[0].pageY;
|
|
|
6181 |
}
|
|
|
6182 |
|
|
|
6183 |
var positions = self.__positions;
|
|
|
6184 |
|
|
|
6185 |
// Are we already is dragging mode?
|
|
|
6186 |
if (self.__isDragging) {
|
|
|
6187 |
self.__decStopped = false;
|
|
|
6188 |
|
|
|
6189 |
// Compute move distance
|
|
|
6190 |
var moveX = currentTouchLeft - self.__lastTouchLeft;
|
|
|
6191 |
var moveY = currentTouchTop - self.__lastTouchTop;
|
|
|
6192 |
|
|
|
6193 |
// Read previous scroll position and zooming
|
|
|
6194 |
var scrollLeft = self.__scrollLeft;
|
|
|
6195 |
var scrollTop = self.__scrollTop;
|
|
|
6196 |
var level = self.__zoomLevel;
|
|
|
6197 |
|
|
|
6198 |
// Work with scaling
|
|
|
6199 |
if (scale != null && self.options.zooming) {
|
|
|
6200 |
|
|
|
6201 |
var oldLevel = level;
|
|
|
6202 |
|
|
|
6203 |
// Recompute level based on previous scale and new scale
|
|
|
6204 |
level = level / self.__lastScale * scale;
|
|
|
6205 |
|
|
|
6206 |
// Limit level according to configuration
|
|
|
6207 |
level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);
|
|
|
6208 |
|
|
|
6209 |
// Only do further compution when change happened
|
|
|
6210 |
if (oldLevel !== level) {
|
|
|
6211 |
|
|
|
6212 |
// Compute relative event position to container
|
|
|
6213 |
var currentTouchLeftRel = currentTouchLeft - self.__clientLeft;
|
|
|
6214 |
var currentTouchTopRel = currentTouchTop - self.__clientTop;
|
|
|
6215 |
|
|
|
6216 |
// Recompute left and top coordinates based on new zoom level
|
|
|
6217 |
scrollLeft = ((currentTouchLeftRel + scrollLeft) * level / oldLevel) - currentTouchLeftRel;
|
|
|
6218 |
scrollTop = ((currentTouchTopRel + scrollTop) * level / oldLevel) - currentTouchTopRel;
|
|
|
6219 |
|
|
|
6220 |
// Recompute max scroll values
|
|
|
6221 |
self.__computeScrollMax(level);
|
|
|
6222 |
|
|
|
6223 |
}
|
|
|
6224 |
}
|
|
|
6225 |
|
|
|
6226 |
if (self.__enableScrollX) {
|
|
|
6227 |
|
|
|
6228 |
scrollLeft -= moveX * self.options.speedMultiplier;
|
|
|
6229 |
var maxScrollLeft = self.__maxScrollLeft;
|
|
|
6230 |
|
|
|
6231 |
if (scrollLeft > maxScrollLeft || scrollLeft < 0) {
|
|
|
6232 |
|
|
|
6233 |
// Slow down on the edges
|
|
|
6234 |
if (self.options.bouncing) {
|
|
|
6235 |
|
|
|
6236 |
scrollLeft += (moveX / 2 * self.options.speedMultiplier);
|
|
|
6237 |
|
|
|
6238 |
} else if (scrollLeft > maxScrollLeft) {
|
|
|
6239 |
|
|
|
6240 |
scrollLeft = maxScrollLeft;
|
|
|
6241 |
|
|
|
6242 |
} else {
|
|
|
6243 |
|
|
|
6244 |
scrollLeft = 0;
|
|
|
6245 |
|
|
|
6246 |
}
|
|
|
6247 |
}
|
|
|
6248 |
}
|
|
|
6249 |
|
|
|
6250 |
// Compute new vertical scroll position
|
|
|
6251 |
if (self.__enableScrollY) {
|
|
|
6252 |
|
|
|
6253 |
scrollTop -= moveY * self.options.speedMultiplier;
|
|
|
6254 |
var maxScrollTop = self.__maxScrollTop;
|
|
|
6255 |
|
|
|
6256 |
if (scrollTop > maxScrollTop || scrollTop < 0) {
|
|
|
6257 |
|
|
|
6258 |
// Slow down on the edges
|
|
|
6259 |
if (self.options.bouncing || (self.__refreshHeight && scrollTop < 0)) {
|
|
|
6260 |
|
|
|
6261 |
scrollTop += (moveY / 2 * self.options.speedMultiplier);
|
|
|
6262 |
|
|
|
6263 |
// Support pull-to-refresh (only when only y is scrollable)
|
|
|
6264 |
if (!self.__enableScrollX && self.__refreshHeight != null) {
|
|
|
6265 |
|
|
|
6266 |
// hide the refresher when it's behind the header bar in case of header transparency
|
|
|
6267 |
if (scrollTop < 0) {
|
|
|
6268 |
self.__refreshHidden = false;
|
|
|
6269 |
self.__refreshShow();
|
|
|
6270 |
} else {
|
|
|
6271 |
self.__refreshHide();
|
|
|
6272 |
self.__refreshHidden = true;
|
|
|
6273 |
}
|
|
|
6274 |
|
|
|
6275 |
if (!self.__refreshActive && scrollTop <= -self.__refreshHeight) {
|
|
|
6276 |
|
|
|
6277 |
self.__refreshActive = true;
|
|
|
6278 |
if (self.__refreshActivate) {
|
|
|
6279 |
self.__refreshActivate();
|
|
|
6280 |
}
|
|
|
6281 |
|
|
|
6282 |
} else if (self.__refreshActive && scrollTop > -self.__refreshHeight) {
|
|
|
6283 |
|
|
|
6284 |
self.__refreshActive = false;
|
|
|
6285 |
if (self.__refreshDeactivate) {
|
|
|
6286 |
self.__refreshDeactivate();
|
|
|
6287 |
}
|
|
|
6288 |
|
|
|
6289 |
}
|
|
|
6290 |
}
|
|
|
6291 |
|
|
|
6292 |
} else if (scrollTop > maxScrollTop) {
|
|
|
6293 |
|
|
|
6294 |
scrollTop = maxScrollTop;
|
|
|
6295 |
|
|
|
6296 |
} else {
|
|
|
6297 |
|
|
|
6298 |
scrollTop = 0;
|
|
|
6299 |
|
|
|
6300 |
}
|
|
|
6301 |
} else if (self.__refreshHeight && !self.__refreshHidden) {
|
|
|
6302 |
// if a positive scroll value and the refresher is still not hidden, hide it
|
|
|
6303 |
self.__refreshHide();
|
|
|
6304 |
self.__refreshHidden = true;
|
|
|
6305 |
}
|
|
|
6306 |
}
|
|
|
6307 |
|
|
|
6308 |
// Keep list from growing infinitely (holding min 10, max 20 measure points)
|
|
|
6309 |
if (positions.length > 60) {
|
|
|
6310 |
positions.splice(0, 30);
|
|
|
6311 |
}
|
|
|
6312 |
|
|
|
6313 |
// Track scroll movement for decleration
|
|
|
6314 |
positions.push(scrollLeft, scrollTop, timeStamp);
|
|
|
6315 |
|
|
|
6316 |
// Sync scroll position
|
|
|
6317 |
self.__publish(scrollLeft, scrollTop, level);
|
|
|
6318 |
|
|
|
6319 |
// Otherwise figure out whether we are switching into dragging mode now.
|
|
|
6320 |
} else {
|
|
|
6321 |
|
|
|
6322 |
var minimumTrackingForScroll = self.options.locking ? 3 : 0;
|
|
|
6323 |
var minimumTrackingForDrag = 5;
|
|
|
6324 |
|
|
|
6325 |
var distanceX = Math.abs(currentTouchLeft - self.__initialTouchLeft);
|
|
|
6326 |
var distanceY = Math.abs(currentTouchTop - self.__initialTouchTop);
|
|
|
6327 |
|
|
|
6328 |
self.__enableScrollX = self.options.scrollingX && distanceX >= minimumTrackingForScroll;
|
|
|
6329 |
self.__enableScrollY = self.options.scrollingY && distanceY >= minimumTrackingForScroll;
|
|
|
6330 |
|
|
|
6331 |
positions.push(self.__scrollLeft, self.__scrollTop, timeStamp);
|
|
|
6332 |
|
|
|
6333 |
self.__isDragging = (self.__enableScrollX || self.__enableScrollY) && (distanceX >= minimumTrackingForDrag || distanceY >= minimumTrackingForDrag);
|
|
|
6334 |
if (self.__isDragging) {
|
|
|
6335 |
self.__interruptedAnimation = false;
|
|
|
6336 |
self.__fadeScrollbars('in');
|
|
|
6337 |
}
|
|
|
6338 |
|
|
|
6339 |
}
|
|
|
6340 |
|
|
|
6341 |
// Update last touch positions and time stamp for next event
|
|
|
6342 |
self.__lastTouchLeft = currentTouchLeft;
|
|
|
6343 |
self.__lastTouchTop = currentTouchTop;
|
|
|
6344 |
self.__lastTouchMove = timeStamp;
|
|
|
6345 |
self.__lastScale = scale;
|
|
|
6346 |
|
|
|
6347 |
},
|
|
|
6348 |
|
|
|
6349 |
|
|
|
6350 |
/**
|
|
|
6351 |
* Touch end handler for scrolling support
|
|
|
6352 |
*/
|
|
|
6353 |
doTouchEnd: function(e, timeStamp) {
|
|
|
6354 |
if (timeStamp instanceof Date) {
|
|
|
6355 |
timeStamp = timeStamp.valueOf();
|
|
|
6356 |
}
|
|
|
6357 |
if (typeof timeStamp !== "number") {
|
|
|
6358 |
timeStamp = Date.now();
|
|
|
6359 |
}
|
|
|
6360 |
|
|
|
6361 |
var self = this;
|
|
|
6362 |
|
|
|
6363 |
// Ignore event when tracking is not enabled (no touchstart event on element)
|
|
|
6364 |
// This is required as this listener ('touchmove') sits on the document and not on the element itself.
|
|
|
6365 |
if (!self.__isTracking) {
|
|
|
6366 |
return;
|
|
|
6367 |
}
|
|
|
6368 |
|
|
|
6369 |
// Not touching anymore (when two finger hit the screen there are two touch end events)
|
|
|
6370 |
self.__isTracking = false;
|
|
|
6371 |
|
|
|
6372 |
// Be sure to reset the dragging flag now. Here we also detect whether
|
|
|
6373 |
// the finger has moved fast enough to switch into a deceleration animation.
|
|
|
6374 |
if (self.__isDragging) {
|
|
|
6375 |
|
|
|
6376 |
// Reset dragging flag
|
|
|
6377 |
self.__isDragging = false;
|
|
|
6378 |
|
|
|
6379 |
// Start deceleration
|
|
|
6380 |
// Verify that the last move detected was in some relevant time frame
|
|
|
6381 |
if (self.__isSingleTouch && self.options.animating && (timeStamp - self.__lastTouchMove) <= 100) {
|
|
|
6382 |
|
|
|
6383 |
// Then figure out what the scroll position was about 100ms ago
|
|
|
6384 |
var positions = self.__positions;
|
|
|
6385 |
var endPos = positions.length - 1;
|
|
|
6386 |
var startPos = endPos;
|
|
|
6387 |
|
|
|
6388 |
// Move pointer to position measured 100ms ago
|
|
|
6389 |
for (var i = endPos; i > 0 && positions[i] > (self.__lastTouchMove - 100); i -= 3) {
|
|
|
6390 |
startPos = i;
|
|
|
6391 |
}
|
|
|
6392 |
|
|
|
6393 |
// If start and stop position is identical in a 100ms timeframe,
|
|
|
6394 |
// we cannot compute any useful deceleration.
|
|
|
6395 |
if (startPos !== endPos) {
|
|
|
6396 |
|
|
|
6397 |
// Compute relative movement between these two points
|
|
|
6398 |
var timeOffset = positions[endPos] - positions[startPos];
|
|
|
6399 |
var movedLeft = self.__scrollLeft - positions[startPos - 2];
|
|
|
6400 |
var movedTop = self.__scrollTop - positions[startPos - 1];
|
|
|
6401 |
|
|
|
6402 |
// Based on 50ms compute the movement to apply for each render step
|
|
|
6403 |
self.__decelerationVelocityX = movedLeft / timeOffset * (1000 / 60);
|
|
|
6404 |
self.__decelerationVelocityY = movedTop / timeOffset * (1000 / 60);
|
|
|
6405 |
|
|
|
6406 |
// How much velocity is required to start the deceleration
|
|
|
6407 |
var minVelocityToStartDeceleration = self.options.paging || self.options.snapping ? self.options.decelVelocityThresholdPaging : self.options.decelVelocityThreshold;
|
|
|
6408 |
|
|
|
6409 |
// Verify that we have enough velocity to start deceleration
|
|
|
6410 |
if (Math.abs(self.__decelerationVelocityX) > minVelocityToStartDeceleration || Math.abs(self.__decelerationVelocityY) > minVelocityToStartDeceleration) {
|
|
|
6411 |
|
|
|
6412 |
// Deactivate pull-to-refresh when decelerating
|
|
|
6413 |
if (!self.__refreshActive) {
|
|
|
6414 |
self.__startDeceleration(timeStamp);
|
|
|
6415 |
}
|
|
|
6416 |
}
|
|
|
6417 |
} else {
|
|
|
6418 |
self.__scrollingComplete();
|
|
|
6419 |
}
|
|
|
6420 |
} else if ((timeStamp - self.__lastTouchMove) > 100) {
|
|
|
6421 |
self.__scrollingComplete();
|
|
|
6422 |
}
|
|
|
6423 |
|
|
|
6424 |
} else if (self.__decStopped) {
|
|
|
6425 |
// the deceleration was stopped
|
|
|
6426 |
// user flicked the scroll fast, and stop dragging, then did a touchstart to stop the srolling
|
|
|
6427 |
// tell the touchend event code to do nothing, we don't want to actually send a click
|
|
|
6428 |
e.isTapHandled = true;
|
|
|
6429 |
self.__decStopped = false;
|
|
|
6430 |
}
|
|
|
6431 |
|
|
|
6432 |
// If this was a slower move it is per default non decelerated, but this
|
|
|
6433 |
// still means that we want snap back to the bounds which is done here.
|
|
|
6434 |
// This is placed outside the condition above to improve edge case stability
|
|
|
6435 |
// e.g. touchend fired without enabled dragging. This should normally do not
|
|
|
6436 |
// have modified the scroll positions or even showed the scrollbars though.
|
|
|
6437 |
if (!self.__isDecelerating) {
|
|
|
6438 |
|
|
|
6439 |
if (self.__refreshActive && self.__refreshStart) {
|
|
|
6440 |
|
|
|
6441 |
// Use publish instead of scrollTo to allow scrolling to out of boundary position
|
|
|
6442 |
// We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
|
|
|
6443 |
self.__publish(self.__scrollLeft, -self.__refreshHeight, self.__zoomLevel, true);
|
|
|
6444 |
|
|
|
6445 |
var d = new Date();
|
|
|
6446 |
self.refreshStartTime = d.getTime();
|
|
|
6447 |
|
|
|
6448 |
if (self.__refreshStart) {
|
|
|
6449 |
self.__refreshStart();
|
|
|
6450 |
}
|
|
|
6451 |
// for iOS-ey style scrolling
|
|
|
6452 |
if (!ionic.Platform.isAndroid())self.__startDeceleration();
|
|
|
6453 |
} else {
|
|
|
6454 |
|
|
|
6455 |
if (self.__interruptedAnimation || self.__isDragging) {
|
|
|
6456 |
self.__scrollingComplete();
|
|
|
6457 |
}
|
|
|
6458 |
self.scrollTo(self.__scrollLeft, self.__scrollTop, true, self.__zoomLevel);
|
|
|
6459 |
|
|
|
6460 |
// Directly signalize deactivation (nothing todo on refresh?)
|
|
|
6461 |
if (self.__refreshActive) {
|
|
|
6462 |
|
|
|
6463 |
self.__refreshActive = false;
|
|
|
6464 |
if (self.__refreshDeactivate) {
|
|
|
6465 |
self.__refreshDeactivate();
|
|
|
6466 |
}
|
|
|
6467 |
|
|
|
6468 |
}
|
|
|
6469 |
}
|
|
|
6470 |
}
|
|
|
6471 |
|
|
|
6472 |
// Fully cleanup list
|
|
|
6473 |
self.__positions.length = 0;
|
|
|
6474 |
|
|
|
6475 |
},
|
|
|
6476 |
|
|
|
6477 |
|
|
|
6478 |
|
|
|
6479 |
/*
|
|
|
6480 |
---------------------------------------------------------------------------
|
|
|
6481 |
PRIVATE API
|
|
|
6482 |
---------------------------------------------------------------------------
|
|
|
6483 |
*/
|
|
|
6484 |
|
|
|
6485 |
/**
|
|
|
6486 |
* Applies the scroll position to the content element
|
|
|
6487 |
*
|
|
|
6488 |
* @param left {Number} Left scroll position
|
|
|
6489 |
* @param top {Number} Top scroll position
|
|
|
6490 |
* @param animate {Boolean} Whether animation should be used to move to the new coordinates
|
|
|
6491 |
*/
|
|
|
6492 |
__publish: function(left, top, zoom, animate, wasResize) {
|
|
|
6493 |
|
|
|
6494 |
var self = this;
|
|
|
6495 |
|
|
|
6496 |
// Remember whether we had an animation, then we try to continue based on the current "drive" of the animation
|
|
|
6497 |
var wasAnimating = self.__isAnimating;
|
|
|
6498 |
if (wasAnimating) {
|
|
|
6499 |
zyngaCore.effect.Animate.stop(wasAnimating);
|
|
|
6500 |
self.__isAnimating = false;
|
|
|
6501 |
}
|
|
|
6502 |
|
|
|
6503 |
if (animate && self.options.animating) {
|
|
|
6504 |
|
|
|
6505 |
// Keep scheduled positions for scrollBy/zoomBy functionality
|
|
|
6506 |
self.__scheduledLeft = left;
|
|
|
6507 |
self.__scheduledTop = top;
|
|
|
6508 |
self.__scheduledZoom = zoom;
|
|
|
6509 |
|
|
|
6510 |
var oldLeft = self.__scrollLeft;
|
|
|
6511 |
var oldTop = self.__scrollTop;
|
|
|
6512 |
var oldZoom = self.__zoomLevel;
|
|
|
6513 |
|
|
|
6514 |
var diffLeft = left - oldLeft;
|
|
|
6515 |
var diffTop = top - oldTop;
|
|
|
6516 |
var diffZoom = zoom - oldZoom;
|
|
|
6517 |
|
|
|
6518 |
var step = function(percent, now, render) {
|
|
|
6519 |
|
|
|
6520 |
if (render) {
|
|
|
6521 |
|
|
|
6522 |
self.__scrollLeft = oldLeft + (diffLeft * percent);
|
|
|
6523 |
self.__scrollTop = oldTop + (diffTop * percent);
|
|
|
6524 |
self.__zoomLevel = oldZoom + (diffZoom * percent);
|
|
|
6525 |
|
|
|
6526 |
// Push values out
|
|
|
6527 |
if (self.__callback) {
|
|
|
6528 |
self.__callback(self.__scrollLeft, self.__scrollTop, self.__zoomLevel, wasResize);
|
|
|
6529 |
}
|
|
|
6530 |
|
|
|
6531 |
}
|
|
|
6532 |
};
|
|
|
6533 |
|
|
|
6534 |
var verify = function(id) {
|
|
|
6535 |
return self.__isAnimating === id;
|
|
|
6536 |
};
|
|
|
6537 |
|
|
|
6538 |
var completed = function(renderedFramesPerSecond, animationId, wasFinished) {
|
|
|
6539 |
if (animationId === self.__isAnimating) {
|
|
|
6540 |
self.__isAnimating = false;
|
|
|
6541 |
}
|
|
|
6542 |
if (self.__didDecelerationComplete || wasFinished) {
|
|
|
6543 |
self.__scrollingComplete();
|
|
|
6544 |
}
|
|
|
6545 |
|
|
|
6546 |
if (self.options.zooming) {
|
|
|
6547 |
self.__computeScrollMax();
|
|
|
6548 |
}
|
|
|
6549 |
};
|
|
|
6550 |
|
|
|
6551 |
// When continuing based on previous animation we choose an ease-out animation instead of ease-in-out
|
|
|
6552 |
self.__isAnimating = zyngaCore.effect.Animate.start(step, verify, completed, self.options.animationDuration, wasAnimating ? easeOutCubic : easeInOutCubic);
|
|
|
6553 |
|
|
|
6554 |
} else {
|
|
|
6555 |
|
|
|
6556 |
self.__scheduledLeft = self.__scrollLeft = left;
|
|
|
6557 |
self.__scheduledTop = self.__scrollTop = top;
|
|
|
6558 |
self.__scheduledZoom = self.__zoomLevel = zoom;
|
|
|
6559 |
|
|
|
6560 |
// Push values out
|
|
|
6561 |
if (self.__callback) {
|
|
|
6562 |
self.__callback(left, top, zoom, wasResize);
|
|
|
6563 |
}
|
|
|
6564 |
|
|
|
6565 |
// Fix max scroll ranges
|
|
|
6566 |
if (self.options.zooming) {
|
|
|
6567 |
self.__computeScrollMax();
|
|
|
6568 |
}
|
|
|
6569 |
}
|
|
|
6570 |
},
|
|
|
6571 |
|
|
|
6572 |
|
|
|
6573 |
/**
|
|
|
6574 |
* Recomputes scroll minimum values based on client dimensions and content dimensions.
|
|
|
6575 |
*/
|
|
|
6576 |
__computeScrollMax: function(zoomLevel) {
|
|
|
6577 |
var self = this;
|
|
|
6578 |
|
|
|
6579 |
if (zoomLevel == null) {
|
|
|
6580 |
zoomLevel = self.__zoomLevel;
|
|
|
6581 |
}
|
|
|
6582 |
|
|
|
6583 |
self.__maxScrollLeft = Math.max((self.__contentWidth * zoomLevel) - self.__clientWidth, 0);
|
|
|
6584 |
self.__maxScrollTop = Math.max((self.__contentHeight * zoomLevel) - self.__clientHeight, 0);
|
|
|
6585 |
|
|
|
6586 |
if (!self.__didWaitForSize && !self.__maxScrollLeft && !self.__maxScrollTop) {
|
|
|
6587 |
self.__didWaitForSize = true;
|
|
|
6588 |
self.__waitForSize();
|
|
|
6589 |
}
|
|
|
6590 |
},
|
|
|
6591 |
|
|
|
6592 |
|
|
|
6593 |
/**
|
|
|
6594 |
* If the scroll view isn't sized correctly on start, wait until we have at least some size
|
|
|
6595 |
*/
|
|
|
6596 |
__waitForSize: function() {
|
|
|
6597 |
var self = this;
|
|
|
6598 |
|
|
|
6599 |
clearTimeout(self.__sizerTimeout);
|
|
|
6600 |
|
|
|
6601 |
var sizer = function() {
|
|
|
6602 |
self.resize(true);
|
|
|
6603 |
};
|
|
|
6604 |
|
|
|
6605 |
sizer();
|
|
|
6606 |
self.__sizerTimeout = setTimeout(sizer, 500);
|
|
|
6607 |
},
|
|
|
6608 |
|
|
|
6609 |
/*
|
|
|
6610 |
---------------------------------------------------------------------------
|
|
|
6611 |
ANIMATION (DECELERATION) SUPPORT
|
|
|
6612 |
---------------------------------------------------------------------------
|
|
|
6613 |
*/
|
|
|
6614 |
|
|
|
6615 |
/**
|
|
|
6616 |
* Called when a touch sequence end and the speed of the finger was high enough
|
|
|
6617 |
* to switch into deceleration mode.
|
|
|
6618 |
*/
|
|
|
6619 |
__startDeceleration: function() {
|
|
|
6620 |
var self = this;
|
|
|
6621 |
|
|
|
6622 |
if (self.options.paging) {
|
|
|
6623 |
|
|
|
6624 |
var scrollLeft = Math.max(Math.min(self.__scrollLeft, self.__maxScrollLeft), 0);
|
|
|
6625 |
var scrollTop = Math.max(Math.min(self.__scrollTop, self.__maxScrollTop), 0);
|
|
|
6626 |
var clientWidth = self.__clientWidth;
|
|
|
6627 |
var clientHeight = self.__clientHeight;
|
|
|
6628 |
|
|
|
6629 |
// We limit deceleration not to the min/max values of the allowed range, but to the size of the visible client area.
|
|
|
6630 |
// Each page should have exactly the size of the client area.
|
|
|
6631 |
self.__minDecelerationScrollLeft = Math.floor(scrollLeft / clientWidth) * clientWidth;
|
|
|
6632 |
self.__minDecelerationScrollTop = Math.floor(scrollTop / clientHeight) * clientHeight;
|
|
|
6633 |
self.__maxDecelerationScrollLeft = Math.ceil(scrollLeft / clientWidth) * clientWidth;
|
|
|
6634 |
self.__maxDecelerationScrollTop = Math.ceil(scrollTop / clientHeight) * clientHeight;
|
|
|
6635 |
|
|
|
6636 |
} else {
|
|
|
6637 |
|
|
|
6638 |
self.__minDecelerationScrollLeft = 0;
|
|
|
6639 |
self.__minDecelerationScrollTop = 0;
|
|
|
6640 |
self.__maxDecelerationScrollLeft = self.__maxScrollLeft;
|
|
|
6641 |
self.__maxDecelerationScrollTop = self.__maxScrollTop;
|
|
|
6642 |
if (self.__refreshActive) self.__minDecelerationScrollTop = self.__refreshHeight * -1;
|
|
|
6643 |
}
|
|
|
6644 |
|
|
|
6645 |
// Wrap class method
|
|
|
6646 |
var step = function(percent, now, render) {
|
|
|
6647 |
self.__stepThroughDeceleration(render);
|
|
|
6648 |
};
|
|
|
6649 |
|
|
|
6650 |
// How much velocity is required to keep the deceleration running
|
|
|
6651 |
self.__minVelocityToKeepDecelerating = self.options.snapping ? 4 : 0.1;
|
|
|
6652 |
|
|
|
6653 |
// Detect whether it's still worth to continue animating steps
|
|
|
6654 |
// If we are already slow enough to not being user perceivable anymore, we stop the whole process here.
|
|
|
6655 |
var verify = function() {
|
|
|
6656 |
var shouldContinue = Math.abs(self.__decelerationVelocityX) >= self.__minVelocityToKeepDecelerating ||
|
|
|
6657 |
Math.abs(self.__decelerationVelocityY) >= self.__minVelocityToKeepDecelerating;
|
|
|
6658 |
if (!shouldContinue) {
|
|
|
6659 |
self.__didDecelerationComplete = true;
|
|
|
6660 |
|
|
|
6661 |
//Make sure the scroll values are within the boundaries after a bounce,
|
|
|
6662 |
//not below 0 or above maximum
|
|
|
6663 |
if (self.options.bouncing && !self.__refreshActive) {
|
|
|
6664 |
self.scrollTo(
|
|
|
6665 |
Math.min( Math.max(self.__scrollLeft, 0), self.__maxScrollLeft ),
|
|
|
6666 |
Math.min( Math.max(self.__scrollTop, 0), self.__maxScrollTop ),
|
|
|
6667 |
self.__refreshActive
|
|
|
6668 |
);
|
|
|
6669 |
}
|
|
|
6670 |
}
|
|
|
6671 |
return shouldContinue;
|
|
|
6672 |
};
|
|
|
6673 |
|
|
|
6674 |
var completed = function() {
|
|
|
6675 |
self.__isDecelerating = false;
|
|
|
6676 |
if (self.__didDecelerationComplete) {
|
|
|
6677 |
self.__scrollingComplete();
|
|
|
6678 |
}
|
|
|
6679 |
|
|
|
6680 |
// Animate to grid when snapping is active, otherwise just fix out-of-boundary positions
|
|
|
6681 |
if (self.options.paging) {
|
|
|
6682 |
self.scrollTo(self.__scrollLeft, self.__scrollTop, self.options.snapping);
|
|
|
6683 |
}
|
|
|
6684 |
};
|
|
|
6685 |
|
|
|
6686 |
// Start animation and switch on flag
|
|
|
6687 |
self.__isDecelerating = zyngaCore.effect.Animate.start(step, verify, completed);
|
|
|
6688 |
|
|
|
6689 |
},
|
|
|
6690 |
|
|
|
6691 |
|
|
|
6692 |
/**
|
|
|
6693 |
* Called on every step of the animation
|
|
|
6694 |
*
|
|
|
6695 |
* @param inMemory {Boolean} Whether to not render the current step, but keep it in memory only. Used internally only!
|
|
|
6696 |
*/
|
|
|
6697 |
__stepThroughDeceleration: function(render) {
|
|
|
6698 |
var self = this;
|
|
|
6699 |
|
|
|
6700 |
|
|
|
6701 |
//
|
|
|
6702 |
// COMPUTE NEXT SCROLL POSITION
|
|
|
6703 |
//
|
|
|
6704 |
|
|
|
6705 |
// Add deceleration to scroll position
|
|
|
6706 |
var scrollLeft = self.__scrollLeft + self.__decelerationVelocityX;// * self.options.deceleration);
|
|
|
6707 |
var scrollTop = self.__scrollTop + self.__decelerationVelocityY;// * self.options.deceleration);
|
|
|
6708 |
|
|
|
6709 |
|
|
|
6710 |
//
|
|
|
6711 |
// HARD LIMIT SCROLL POSITION FOR NON BOUNCING MODE
|
|
|
6712 |
//
|
|
|
6713 |
|
|
|
6714 |
if (!self.options.bouncing) {
|
|
|
6715 |
|
|
|
6716 |
var scrollLeftFixed = Math.max(Math.min(self.__maxDecelerationScrollLeft, scrollLeft), self.__minDecelerationScrollLeft);
|
|
|
6717 |
if (scrollLeftFixed !== scrollLeft) {
|
|
|
6718 |
scrollLeft = scrollLeftFixed;
|
|
|
6719 |
self.__decelerationVelocityX = 0;
|
|
|
6720 |
}
|
|
|
6721 |
|
|
|
6722 |
var scrollTopFixed = Math.max(Math.min(self.__maxDecelerationScrollTop, scrollTop), self.__minDecelerationScrollTop);
|
|
|
6723 |
if (scrollTopFixed !== scrollTop) {
|
|
|
6724 |
scrollTop = scrollTopFixed;
|
|
|
6725 |
self.__decelerationVelocityY = 0;
|
|
|
6726 |
}
|
|
|
6727 |
|
|
|
6728 |
}
|
|
|
6729 |
|
|
|
6730 |
|
|
|
6731 |
//
|
|
|
6732 |
// UPDATE SCROLL POSITION
|
|
|
6733 |
//
|
|
|
6734 |
|
|
|
6735 |
if (render) {
|
|
|
6736 |
|
|
|
6737 |
self.__publish(scrollLeft, scrollTop, self.__zoomLevel);
|
|
|
6738 |
|
|
|
6739 |
} else {
|
|
|
6740 |
|
|
|
6741 |
self.__scrollLeft = scrollLeft;
|
|
|
6742 |
self.__scrollTop = scrollTop;
|
|
|
6743 |
|
|
|
6744 |
}
|
|
|
6745 |
|
|
|
6746 |
|
|
|
6747 |
//
|
|
|
6748 |
// SLOW DOWN
|
|
|
6749 |
//
|
|
|
6750 |
|
|
|
6751 |
// Slow down velocity on every iteration
|
|
|
6752 |
if (!self.options.paging) {
|
|
|
6753 |
|
|
|
6754 |
// This is the factor applied to every iteration of the animation
|
|
|
6755 |
// to slow down the process. This should emulate natural behavior where
|
|
|
6756 |
// objects slow down when the initiator of the movement is removed
|
|
|
6757 |
var frictionFactor = self.options.deceleration;
|
|
|
6758 |
|
|
|
6759 |
self.__decelerationVelocityX *= frictionFactor;
|
|
|
6760 |
self.__decelerationVelocityY *= frictionFactor;
|
|
|
6761 |
|
|
|
6762 |
}
|
|
|
6763 |
|
|
|
6764 |
|
|
|
6765 |
//
|
|
|
6766 |
// BOUNCING SUPPORT
|
|
|
6767 |
//
|
|
|
6768 |
|
|
|
6769 |
if (self.options.bouncing) {
|
|
|
6770 |
|
|
|
6771 |
var scrollOutsideX = 0;
|
|
|
6772 |
var scrollOutsideY = 0;
|
|
|
6773 |
|
|
|
6774 |
// This configures the amount of change applied to deceleration/acceleration when reaching boundaries
|
|
|
6775 |
var penetrationDeceleration = self.options.penetrationDeceleration;
|
|
|
6776 |
var penetrationAcceleration = self.options.penetrationAcceleration;
|
|
|
6777 |
|
|
|
6778 |
// Check limits
|
|
|
6779 |
if (scrollLeft < self.__minDecelerationScrollLeft) {
|
|
|
6780 |
scrollOutsideX = self.__minDecelerationScrollLeft - scrollLeft;
|
|
|
6781 |
} else if (scrollLeft > self.__maxDecelerationScrollLeft) {
|
|
|
6782 |
scrollOutsideX = self.__maxDecelerationScrollLeft - scrollLeft;
|
|
|
6783 |
}
|
|
|
6784 |
|
|
|
6785 |
if (scrollTop < self.__minDecelerationScrollTop) {
|
|
|
6786 |
scrollOutsideY = self.__minDecelerationScrollTop - scrollTop;
|
|
|
6787 |
} else if (scrollTop > self.__maxDecelerationScrollTop) {
|
|
|
6788 |
scrollOutsideY = self.__maxDecelerationScrollTop - scrollTop;
|
|
|
6789 |
}
|
|
|
6790 |
|
|
|
6791 |
// Slow down until slow enough, then flip back to snap position
|
|
|
6792 |
if (scrollOutsideX !== 0) {
|
|
|
6793 |
var isHeadingOutwardsX = scrollOutsideX * self.__decelerationVelocityX <= self.__minDecelerationScrollLeft;
|
|
|
6794 |
if (isHeadingOutwardsX) {
|
|
|
6795 |
self.__decelerationVelocityX += scrollOutsideX * penetrationDeceleration;
|
|
|
6796 |
}
|
|
|
6797 |
var isStoppedX = Math.abs(self.__decelerationVelocityX) <= self.__minVelocityToKeepDecelerating;
|
|
|
6798 |
//If we're not heading outwards, or if the above statement got us below minDeceleration, go back towards bounds
|
|
|
6799 |
if (!isHeadingOutwardsX || isStoppedX) {
|
|
|
6800 |
self.__decelerationVelocityX = scrollOutsideX * penetrationAcceleration;
|
|
|
6801 |
}
|
|
|
6802 |
}
|
|
|
6803 |
|
|
|
6804 |
if (scrollOutsideY !== 0) {
|
|
|
6805 |
var isHeadingOutwardsY = scrollOutsideY * self.__decelerationVelocityY <= self.__minDecelerationScrollTop;
|
|
|
6806 |
if (isHeadingOutwardsY) {
|
|
|
6807 |
self.__decelerationVelocityY += scrollOutsideY * penetrationDeceleration;
|
|
|
6808 |
}
|
|
|
6809 |
var isStoppedY = Math.abs(self.__decelerationVelocityY) <= self.__minVelocityToKeepDecelerating;
|
|
|
6810 |
//If we're not heading outwards, or if the above statement got us below minDeceleration, go back towards bounds
|
|
|
6811 |
if (!isHeadingOutwardsY || isStoppedY) {
|
|
|
6812 |
self.__decelerationVelocityY = scrollOutsideY * penetrationAcceleration;
|
|
|
6813 |
}
|
|
|
6814 |
}
|
|
|
6815 |
}
|
|
|
6816 |
},
|
|
|
6817 |
|
|
|
6818 |
|
|
|
6819 |
/**
|
|
|
6820 |
* calculate the distance between two touches
|
|
|
6821 |
* @param {Touch} touch1
|
|
|
6822 |
* @param {Touch} touch2
|
|
|
6823 |
* @returns {Number} distance
|
|
|
6824 |
*/
|
|
|
6825 |
__getDistance: function getDistance(touch1, touch2) {
|
|
|
6826 |
var x = touch2.pageX - touch1.pageX,
|
|
|
6827 |
y = touch2.pageY - touch1.pageY;
|
|
|
6828 |
return Math.sqrt((x * x) + (y * y));
|
|
|
6829 |
},
|
|
|
6830 |
|
|
|
6831 |
|
|
|
6832 |
/**
|
|
|
6833 |
* calculate the scale factor between two touchLists (fingers)
|
|
|
6834 |
* no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
|
|
|
6835 |
* @param {Array} start
|
|
|
6836 |
* @param {Array} end
|
|
|
6837 |
* @returns {Number} scale
|
|
|
6838 |
*/
|
|
|
6839 |
__getScale: function getScale(start, end) {
|
|
|
6840 |
// need two fingers...
|
|
|
6841 |
if (start.length >= 2 && end.length >= 2) {
|
|
|
6842 |
return this.__getDistance(end[0], end[1]) /
|
|
|
6843 |
this.__getDistance(start[0], start[1]);
|
|
|
6844 |
}
|
|
|
6845 |
return 1;
|
|
|
6846 |
}
|
|
|
6847 |
});
|
|
|
6848 |
|
|
|
6849 |
ionic.scroll = {
|
|
|
6850 |
isScrolling: false,
|
|
|
6851 |
lastTop: 0
|
|
|
6852 |
};
|
|
|
6853 |
|
|
|
6854 |
})(ionic);
|
|
|
6855 |
|
|
|
6856 |
(function(ionic) {
|
|
|
6857 |
var NOOP = function() {};
|
|
|
6858 |
var depreciated = function(name) {
|
|
|
6859 |
void 0;
|
|
|
6860 |
};
|
|
|
6861 |
ionic.views.ScrollNative = ionic.views.View.inherit({
|
|
|
6862 |
|
|
|
6863 |
initialize: function(options) {
|
|
|
6864 |
var self = this;
|
|
|
6865 |
self.__container = self.el = options.el;
|
|
|
6866 |
self.__content = options.el.firstElementChild;
|
|
|
6867 |
self.isNative = true;
|
|
|
6868 |
|
|
|
6869 |
self.__scrollTop = self.el.scrollTop;
|
|
|
6870 |
self.__scrollLeft = self.el.scrollLeft;
|
|
|
6871 |
self.__clientHeight = self.__content.clientHeight;
|
|
|
6872 |
self.__clientWidth = self.__content.clientWidth;
|
|
|
6873 |
self.__maxScrollTop = Math.max((self.__contentHeight) - self.__clientHeight, 0);
|
|
|
6874 |
self.__maxScrollLeft = Math.max((self.__contentWidth) - self.__clientWidth, 0);
|
|
|
6875 |
|
|
|
6876 |
self.options = {
|
|
|
6877 |
|
|
|
6878 |
freeze: false,
|
|
|
6879 |
|
|
|
6880 |
getContentWidth: function() {
|
|
|
6881 |
return Math.max(self.__content.scrollWidth, self.__content.offsetWidth);
|
|
|
6882 |
},
|
|
|
6883 |
|
|
|
6884 |
getContentHeight: function() {
|
|
|
6885 |
return Math.max(self.__content.scrollHeight, self.__content.offsetHeight + (self.__content.offsetTop * 2));
|
|
|
6886 |
}
|
|
|
6887 |
|
|
|
6888 |
};
|
|
|
6889 |
|
|
|
6890 |
for (var key in options) {
|
|
|
6891 |
self.options[key] = options[key];
|
|
|
6892 |
}
|
|
|
6893 |
|
|
|
6894 |
/**
|
|
|
6895 |
* Sets isScrolling to true, and automatically deactivates if not called again in 80ms.
|
|
|
6896 |
*/
|
|
|
6897 |
self.onScroll = function() {
|
|
|
6898 |
if (!ionic.scroll.isScrolling) {
|
|
|
6899 |
ionic.scroll.isScrolling = true;
|
|
|
6900 |
}
|
|
|
6901 |
|
|
|
6902 |
clearTimeout(self.scrollTimer);
|
|
|
6903 |
self.scrollTimer = setTimeout(function() {
|
|
|
6904 |
ionic.scroll.isScrolling = false;
|
|
|
6905 |
}, 80);
|
|
|
6906 |
};
|
|
|
6907 |
|
|
|
6908 |
self.freeze = NOOP;
|
|
|
6909 |
|
|
|
6910 |
self.__initEventHandlers();
|
|
|
6911 |
},
|
|
|
6912 |
|
|
|
6913 |
/** Methods not used in native scrolling */
|
|
|
6914 |
__callback: function() { depreciated('__callback'); },
|
|
|
6915 |
zoomTo: function() { depreciated('zoomTo'); },
|
|
|
6916 |
zoomBy: function() { depreciated('zoomBy'); },
|
|
|
6917 |
activatePullToRefresh: function() { depreciated('activatePullToRefresh'); },
|
|
|
6918 |
|
|
|
6919 |
/**
|
|
|
6920 |
* Returns the scroll position and zooming values
|
|
|
6921 |
*
|
|
|
6922 |
* @return {Map} `left` and `top` scroll position and `zoom` level
|
|
|
6923 |
*/
|
|
|
6924 |
resize: function(continueScrolling) {
|
|
|
6925 |
var self = this;
|
|
|
6926 |
if (!self.__container || !self.options) return;
|
|
|
6927 |
|
|
|
6928 |
// Update Scroller dimensions for changed content
|
|
|
6929 |
// Add padding to bottom of content
|
|
|
6930 |
self.setDimensions(
|
|
|
6931 |
self.__container.clientWidth,
|
|
|
6932 |
self.__container.clientHeight,
|
|
|
6933 |
self.options.getContentWidth(),
|
|
|
6934 |
self.options.getContentHeight(),
|
|
|
6935 |
continueScrolling
|
|
|
6936 |
);
|
|
|
6937 |
},
|
|
|
6938 |
|
|
|
6939 |
/**
|
|
|
6940 |
* Initialize the scrollview
|
|
|
6941 |
* In native scrolling, this only means we need to gather size information
|
|
|
6942 |
*/
|
|
|
6943 |
run: function() {
|
|
|
6944 |
this.resize();
|
|
|
6945 |
},
|
|
|
6946 |
|
|
|
6947 |
/**
|
|
|
6948 |
* Returns the scroll position and zooming values
|
|
|
6949 |
*
|
|
|
6950 |
* @return {Map} `left` and `top` scroll position and `zoom` level
|
|
|
6951 |
*/
|
|
|
6952 |
getValues: function() {
|
|
|
6953 |
var self = this;
|
|
|
6954 |
self.update();
|
|
|
6955 |
return {
|
|
|
6956 |
left: self.__scrollLeft,
|
|
|
6957 |
top: self.__scrollTop,
|
|
|
6958 |
zoom: 1
|
|
|
6959 |
};
|
|
|
6960 |
},
|
|
|
6961 |
|
|
|
6962 |
/**
|
|
|
6963 |
* Updates the __scrollLeft and __scrollTop values to el's current value
|
|
|
6964 |
*/
|
|
|
6965 |
update: function() {
|
|
|
6966 |
var self = this;
|
|
|
6967 |
self.__scrollLeft = self.el.scrollLeft;
|
|
|
6968 |
self.__scrollTop = self.el.scrollTop;
|
|
|
6969 |
},
|
|
|
6970 |
|
|
|
6971 |
/**
|
|
|
6972 |
* Configures the dimensions of the client (outer) and content (inner) elements.
|
|
|
6973 |
* Requires the available space for the outer element and the outer size of the inner element.
|
|
|
6974 |
* All values which are falsy (null or zero etc.) are ignored and the old value is kept.
|
|
|
6975 |
*
|
|
|
6976 |
* @param clientWidth {Integer} Inner width of outer element
|
|
|
6977 |
* @param clientHeight {Integer} Inner height of outer element
|
|
|
6978 |
* @param contentWidth {Integer} Outer width of inner element
|
|
|
6979 |
* @param contentHeight {Integer} Outer height of inner element
|
|
|
6980 |
*/
|
|
|
6981 |
setDimensions: function(clientWidth, clientHeight, contentWidth, contentHeight) {
|
|
|
6982 |
var self = this;
|
|
|
6983 |
|
|
|
6984 |
if (!clientWidth && !clientHeight && !contentWidth && !contentHeight) {
|
|
|
6985 |
// this scrollview isn't rendered, don't bother
|
|
|
6986 |
return;
|
|
|
6987 |
}
|
|
|
6988 |
|
|
|
6989 |
// Only update values which are defined
|
|
|
6990 |
if (clientWidth === +clientWidth) {
|
|
|
6991 |
self.__clientWidth = clientWidth;
|
|
|
6992 |
}
|
|
|
6993 |
|
|
|
6994 |
if (clientHeight === +clientHeight) {
|
|
|
6995 |
self.__clientHeight = clientHeight;
|
|
|
6996 |
}
|
|
|
6997 |
|
|
|
6998 |
if (contentWidth === +contentWidth) {
|
|
|
6999 |
self.__contentWidth = contentWidth;
|
|
|
7000 |
}
|
|
|
7001 |
|
|
|
7002 |
if (contentHeight === +contentHeight) {
|
|
|
7003 |
self.__contentHeight = contentHeight;
|
|
|
7004 |
}
|
|
|
7005 |
|
|
|
7006 |
// Refresh maximums
|
|
|
7007 |
self.__computeScrollMax();
|
|
|
7008 |
},
|
|
|
7009 |
|
|
|
7010 |
/**
|
|
|
7011 |
* Returns the maximum scroll values
|
|
|
7012 |
*
|
|
|
7013 |
* @return {Map} `left` and `top` maximum scroll values
|
|
|
7014 |
*/
|
|
|
7015 |
getScrollMax: function() {
|
|
|
7016 |
return {
|
|
|
7017 |
left: this.__maxScrollLeft,
|
|
|
7018 |
top: this.__maxScrollTop
|
|
|
7019 |
};
|
|
|
7020 |
},
|
|
|
7021 |
|
|
|
7022 |
/**
|
|
|
7023 |
* Scrolls by the given amount in px.
|
|
|
7024 |
*
|
|
|
7025 |
* @param left {Number} Horizontal scroll position, keeps current if value is <code>null</code>
|
|
|
7026 |
* @param top {Number} Vertical scroll position, keeps current if value is <code>null</code>
|
|
|
7027 |
* @param animate {Boolean} Whether the scrolling should happen using an animation
|
|
|
7028 |
*/
|
|
|
7029 |
|
|
|
7030 |
scrollBy: function(left, top, animate) {
|
|
|
7031 |
var self = this;
|
|
|
7032 |
|
|
|
7033 |
// update scroll vars before refferencing them
|
|
|
7034 |
self.update();
|
|
|
7035 |
|
|
|
7036 |
var startLeft = self.__isAnimating ? self.__scheduledLeft : self.__scrollLeft;
|
|
|
7037 |
var startTop = self.__isAnimating ? self.__scheduledTop : self.__scrollTop;
|
|
|
7038 |
|
|
|
7039 |
self.scrollTo(startLeft + (left || 0), startTop + (top || 0), animate);
|
|
|
7040 |
},
|
|
|
7041 |
|
|
|
7042 |
/**
|
|
|
7043 |
* Scrolls to the given position in px.
|
|
|
7044 |
*
|
|
|
7045 |
* @param left {Number} Horizontal scroll position, keeps current if value is <code>null</code>
|
|
|
7046 |
* @param top {Number} Vertical scroll position, keeps current if value is <code>null</code>
|
|
|
7047 |
* @param animate {Boolean} Whether the scrolling should happen using an animation
|
|
|
7048 |
*/
|
|
|
7049 |
scrollTo: function(left, top, animate) {
|
|
|
7050 |
var self = this;
|
|
|
7051 |
if (!animate) {
|
|
|
7052 |
self.el.scrollTop = top;
|
|
|
7053 |
self.el.scrollLeft = left;
|
|
|
7054 |
self.resize();
|
|
|
7055 |
return;
|
|
|
7056 |
}
|
|
|
7057 |
animateScroll(top, left);
|
|
|
7058 |
|
|
|
7059 |
function animateScroll(Y, X) {
|
|
|
7060 |
// scroll animation loop w/ easing
|
|
|
7061 |
// credit https://gist.github.com/dezinezync/5487119
|
|
|
7062 |
var start = Date.now(),
|
|
|
7063 |
duration = 1000, //milliseconds
|
|
|
7064 |
fromY = self.el.scrollTop,
|
|
|
7065 |
fromX = self.el.scrollLeft;
|
|
|
7066 |
|
|
|
7067 |
if (fromY === Y && fromX === X) {
|
|
|
7068 |
self.resize();
|
|
|
7069 |
return; /* Prevent scrolling to the Y point if already there */
|
|
|
7070 |
}
|
|
|
7071 |
|
|
|
7072 |
// decelerating to zero velocity
|
|
|
7073 |
function easeOutCubic(t) {
|
|
|
7074 |
return (--t) * t * t + 1;
|
|
|
7075 |
}
|
|
|
7076 |
|
|
|
7077 |
// scroll loop
|
|
|
7078 |
function animateScrollStep() {
|
|
|
7079 |
var currentTime = Date.now(),
|
|
|
7080 |
time = Math.min(1, ((currentTime - start) / duration)),
|
|
|
7081 |
// where .5 would be 50% of time on a linear scale easedT gives a
|
|
|
7082 |
// fraction based on the easing method
|
|
|
7083 |
easedT = easeOutCubic(time);
|
|
|
7084 |
|
|
|
7085 |
if (fromY != Y) {
|
|
|
7086 |
self.el.scrollTop = parseInt((easedT * (Y - fromY)) + fromY, 10);
|
|
|
7087 |
}
|
|
|
7088 |
if (fromX != X) {
|
|
|
7089 |
self.el.scrollLeft = parseInt((easedT * (X - fromX)) + fromX, 10);
|
|
|
7090 |
}
|
|
|
7091 |
|
|
|
7092 |
if (time < 1) {
|
|
|
7093 |
ionic.requestAnimationFrame(animateScrollStep);
|
|
|
7094 |
|
|
|
7095 |
} else {
|
|
|
7096 |
// done
|
|
|
7097 |
self.resize();
|
|
|
7098 |
}
|
|
|
7099 |
}
|
|
|
7100 |
|
|
|
7101 |
// start scroll loop
|
|
|
7102 |
ionic.requestAnimationFrame(animateScrollStep);
|
|
|
7103 |
}
|
|
|
7104 |
},
|
|
|
7105 |
|
|
|
7106 |
|
|
|
7107 |
|
|
|
7108 |
/*
|
|
|
7109 |
---------------------------------------------------------------------------
|
|
|
7110 |
PRIVATE API
|
|
|
7111 |
---------------------------------------------------------------------------
|
|
|
7112 |
*/
|
|
|
7113 |
|
|
|
7114 |
/**
|
|
|
7115 |
* If the scroll view isn't sized correctly on start, wait until we have at least some size
|
|
|
7116 |
*/
|
|
|
7117 |
__waitForSize: function() {
|
|
|
7118 |
var self = this;
|
|
|
7119 |
|
|
|
7120 |
clearTimeout(self.__sizerTimeout);
|
|
|
7121 |
|
|
|
7122 |
var sizer = function() {
|
|
|
7123 |
self.resize(true);
|
|
|
7124 |
};
|
|
|
7125 |
|
|
|
7126 |
sizer();
|
|
|
7127 |
self.__sizerTimeout = setTimeout(sizer, 500);
|
|
|
7128 |
},
|
|
|
7129 |
|
|
|
7130 |
|
|
|
7131 |
/**
|
|
|
7132 |
* Recomputes scroll minimum values based on client dimensions and content dimensions.
|
|
|
7133 |
*/
|
|
|
7134 |
__computeScrollMax: function() {
|
|
|
7135 |
var self = this;
|
|
|
7136 |
|
|
|
7137 |
self.__maxScrollLeft = Math.max((self.__contentWidth) - self.__clientWidth, 0);
|
|
|
7138 |
self.__maxScrollTop = Math.max((self.__contentHeight) - self.__clientHeight, 0);
|
|
|
7139 |
|
|
|
7140 |
if (!self.__didWaitForSize && !self.__maxScrollLeft && !self.__maxScrollTop) {
|
|
|
7141 |
self.__didWaitForSize = true;
|
|
|
7142 |
self.__waitForSize();
|
|
|
7143 |
}
|
|
|
7144 |
},
|
|
|
7145 |
|
|
|
7146 |
__initEventHandlers: function() {
|
|
|
7147 |
var self = this;
|
|
|
7148 |
|
|
|
7149 |
// Event Handler
|
|
|
7150 |
var container = self.__container;
|
|
|
7151 |
|
|
|
7152 |
// should be unnecessary in native scrolling, but keep in case bugs show up
|
|
|
7153 |
self.scrollChildIntoView = NOOP;
|
|
|
7154 |
|
|
|
7155 |
self.resetScrollView = function() {
|
|
|
7156 |
//return scrollview to original height once keyboard has hidden
|
|
|
7157 |
if (self.isScrolledIntoView) {
|
|
|
7158 |
self.isScrolledIntoView = false;
|
|
|
7159 |
container.style.height = "";
|
|
|
7160 |
container.style.overflow = "";
|
|
|
7161 |
self.resize();
|
|
|
7162 |
ionic.scroll.isScrolling = false;
|
|
|
7163 |
}
|
|
|
7164 |
};
|
|
|
7165 |
|
|
|
7166 |
container.addEventListener('resetScrollView', self.resetScrollView);
|
|
|
7167 |
container.addEventListener('scroll', self.onScroll);
|
|
|
7168 |
|
|
|
7169 |
//Broadcasted when keyboard is shown on some platforms.
|
|
|
7170 |
//See js/utils/keyboard.js
|
|
|
7171 |
container.addEventListener('scrollChildIntoView', self.scrollChildIntoView);
|
|
|
7172 |
container.addEventListener('resetScrollView', self.resetScrollView);
|
|
|
7173 |
},
|
|
|
7174 |
|
|
|
7175 |
__cleanup: function() {
|
|
|
7176 |
var self = this;
|
|
|
7177 |
var container = self.__container;
|
|
|
7178 |
|
|
|
7179 |
container.removeEventListener('resetScrollView', self.resetScrollView);
|
|
|
7180 |
container.removeEventListener('scroll', self.onScroll);
|
|
|
7181 |
|
|
|
7182 |
container.removeEventListener('scrollChildIntoView', self.scrollChildIntoView);
|
|
|
7183 |
container.removeEventListener('resetScrollView', self.resetScrollView);
|
|
|
7184 |
|
|
|
7185 |
ionic.tap.removeClonedInputs(container, self);
|
|
|
7186 |
|
|
|
7187 |
delete self.__container;
|
|
|
7188 |
delete self.__content;
|
|
|
7189 |
delete self.__indicatorX;
|
|
|
7190 |
delete self.__indicatorY;
|
|
|
7191 |
delete self.options.el;
|
|
|
7192 |
|
|
|
7193 |
self.resize = self.scrollTo = self.onScroll = self.resetScrollView = NOOP;
|
|
|
7194 |
container = null;
|
|
|
7195 |
}
|
|
|
7196 |
});
|
|
|
7197 |
|
|
|
7198 |
})(ionic);
|
|
|
7199 |
|
|
|
7200 |
|
|
|
7201 |
(function(ionic) {
|
|
|
7202 |
'use strict';
|
|
|
7203 |
|
|
|
7204 |
var ITEM_CLASS = 'item';
|
|
|
7205 |
var ITEM_CONTENT_CLASS = 'item-content';
|
|
|
7206 |
var ITEM_SLIDING_CLASS = 'item-sliding';
|
|
|
7207 |
var ITEM_OPTIONS_CLASS = 'item-options';
|
|
|
7208 |
var ITEM_PLACEHOLDER_CLASS = 'item-placeholder';
|
|
|
7209 |
var ITEM_REORDERING_CLASS = 'item-reordering';
|
|
|
7210 |
var ITEM_REORDER_BTN_CLASS = 'item-reorder';
|
|
|
7211 |
|
|
|
7212 |
var DragOp = function() {};
|
|
|
7213 |
DragOp.prototype = {
|
|
|
7214 |
start: function(){},
|
|
|
7215 |
drag: function(){},
|
|
|
7216 |
end: function(){},
|
|
|
7217 |
isSameItem: function() {
|
|
|
7218 |
return false;
|
|
|
7219 |
}
|
|
|
7220 |
};
|
|
|
7221 |
|
|
|
7222 |
var SlideDrag = function(opts) {
|
|
|
7223 |
this.dragThresholdX = opts.dragThresholdX || 10;
|
|
|
7224 |
this.el = opts.el;
|
|
|
7225 |
this.item = opts.item;
|
|
|
7226 |
this.canSwipe = opts.canSwipe;
|
|
|
7227 |
};
|
|
|
7228 |
|
|
|
7229 |
SlideDrag.prototype = new DragOp();
|
|
|
7230 |
|
|
|
7231 |
SlideDrag.prototype.start = function(e) {
|
|
|
7232 |
var content, buttons, offsetX, buttonsWidth;
|
|
|
7233 |
|
|
|
7234 |
if (!this.canSwipe()) {
|
|
|
7235 |
return;
|
|
|
7236 |
}
|
|
|
7237 |
|
|
|
7238 |
if (e.target.classList.contains(ITEM_CONTENT_CLASS)) {
|
|
|
7239 |
content = e.target;
|
|
|
7240 |
} else if (e.target.classList.contains(ITEM_CLASS)) {
|
|
|
7241 |
content = e.target.querySelector('.' + ITEM_CONTENT_CLASS);
|
|
|
7242 |
} else {
|
|
|
7243 |
content = ionic.DomUtil.getParentWithClass(e.target, ITEM_CONTENT_CLASS);
|
|
|
7244 |
}
|
|
|
7245 |
|
|
|
7246 |
// If we don't have a content area as one of our children (or ourselves), skip
|
|
|
7247 |
if (!content) {
|
|
|
7248 |
return;
|
|
|
7249 |
}
|
|
|
7250 |
|
|
|
7251 |
// Make sure we aren't animating as we slide
|
|
|
7252 |
content.classList.remove(ITEM_SLIDING_CLASS);
|
|
|
7253 |
|
|
|
7254 |
// Grab the starting X point for the item (for example, so we can tell whether it is open or closed to start)
|
|
|
7255 |
offsetX = parseFloat(content.style[ionic.CSS.TRANSFORM].replace('translate3d(', '').split(',')[0]) || 0;
|
|
|
7256 |
|
|
|
7257 |
// Grab the buttons
|
|
|
7258 |
buttons = content.parentNode.querySelector('.' + ITEM_OPTIONS_CLASS);
|
|
|
7259 |
if (!buttons) {
|
|
|
7260 |
return;
|
|
|
7261 |
}
|
|
|
7262 |
buttons.classList.remove('invisible');
|
|
|
7263 |
|
|
|
7264 |
buttonsWidth = buttons.offsetWidth;
|
|
|
7265 |
|
|
|
7266 |
this._currentDrag = {
|
|
|
7267 |
buttons: buttons,
|
|
|
7268 |
buttonsWidth: buttonsWidth,
|
|
|
7269 |
content: content,
|
|
|
7270 |
startOffsetX: offsetX
|
|
|
7271 |
};
|
|
|
7272 |
};
|
|
|
7273 |
|
|
|
7274 |
/**
|
|
|
7275 |
* Check if this is the same item that was previously dragged.
|
|
|
7276 |
*/
|
|
|
7277 |
SlideDrag.prototype.isSameItem = function(op) {
|
|
|
7278 |
if (op._lastDrag && this._currentDrag) {
|
|
|
7279 |
return this._currentDrag.content == op._lastDrag.content;
|
|
|
7280 |
}
|
|
|
7281 |
return false;
|
|
|
7282 |
};
|
|
|
7283 |
|
|
|
7284 |
SlideDrag.prototype.clean = function(isInstant) {
|
|
|
7285 |
var lastDrag = this._lastDrag;
|
|
|
7286 |
|
|
|
7287 |
if (!lastDrag || !lastDrag.content) return;
|
|
|
7288 |
|
|
|
7289 |
lastDrag.content.style[ionic.CSS.TRANSITION] = '';
|
|
|
7290 |
lastDrag.content.style[ionic.CSS.TRANSFORM] = '';
|
|
|
7291 |
if (isInstant) {
|
|
|
7292 |
lastDrag.content.style[ionic.CSS.TRANSITION] = 'none';
|
|
|
7293 |
makeInvisible();
|
|
|
7294 |
ionic.requestAnimationFrame(function() {
|
|
|
7295 |
lastDrag.content.style[ionic.CSS.TRANSITION] = '';
|
|
|
7296 |
});
|
|
|
7297 |
} else {
|
|
|
7298 |
ionic.requestAnimationFrame(function() {
|
|
|
7299 |
setTimeout(makeInvisible, 250);
|
|
|
7300 |
});
|
|
|
7301 |
}
|
|
|
7302 |
function makeInvisible() {
|
|
|
7303 |
lastDrag.buttons && lastDrag.buttons.classList.add('invisible');
|
|
|
7304 |
}
|
|
|
7305 |
};
|
|
|
7306 |
|
|
|
7307 |
SlideDrag.prototype.drag = ionic.animationFrameThrottle(function(e) {
|
|
|
7308 |
var buttonsWidth;
|
|
|
7309 |
|
|
|
7310 |
// We really aren't dragging
|
|
|
7311 |
if (!this._currentDrag) {
|
|
|
7312 |
return;
|
|
|
7313 |
}
|
|
|
7314 |
|
|
|
7315 |
// Check if we should start dragging. Check if we've dragged past the threshold,
|
|
|
7316 |
// or we are starting from the open state.
|
|
|
7317 |
if (!this._isDragging &&
|
|
|
7318 |
((Math.abs(e.gesture.deltaX) > this.dragThresholdX) ||
|
|
|
7319 |
(Math.abs(this._currentDrag.startOffsetX) > 0))) {
|
|
|
7320 |
this._isDragging = true;
|
|
|
7321 |
}
|
|
|
7322 |
|
|
|
7323 |
if (this._isDragging) {
|
|
|
7324 |
buttonsWidth = this._currentDrag.buttonsWidth;
|
|
|
7325 |
|
|
|
7326 |
// Grab the new X point, capping it at zero
|
|
|
7327 |
var newX = Math.min(0, this._currentDrag.startOffsetX + e.gesture.deltaX);
|
|
|
7328 |
|
|
|
7329 |
// If the new X position is past the buttons, we need to slow down the drag (rubber band style)
|
|
|
7330 |
if (newX < -buttonsWidth) {
|
|
|
7331 |
// Calculate the new X position, capped at the top of the buttons
|
|
|
7332 |
newX = Math.min(-buttonsWidth, -buttonsWidth + (((e.gesture.deltaX + buttonsWidth) * 0.4)));
|
|
|
7333 |
}
|
|
|
7334 |
|
|
|
7335 |
this._currentDrag.content.$$ionicOptionsOpen = newX !== 0;
|
|
|
7336 |
|
|
|
7337 |
this._currentDrag.content.style[ionic.CSS.TRANSFORM] = 'translate3d(' + newX + 'px, 0, 0)';
|
|
|
7338 |
this._currentDrag.content.style[ionic.CSS.TRANSITION] = 'none';
|
|
|
7339 |
}
|
|
|
7340 |
});
|
|
|
7341 |
|
|
|
7342 |
SlideDrag.prototype.end = function(e, doneCallback) {
|
|
|
7343 |
var self = this;
|
|
|
7344 |
|
|
|
7345 |
// There is no drag, just end immediately
|
|
|
7346 |
if (!self._currentDrag) {
|
|
|
7347 |
doneCallback && doneCallback();
|
|
|
7348 |
return;
|
|
|
7349 |
}
|
|
|
7350 |
|
|
|
7351 |
// If we are currently dragging, we want to snap back into place
|
|
|
7352 |
// The final resting point X will be the width of the exposed buttons
|
|
|
7353 |
var restingPoint = -self._currentDrag.buttonsWidth;
|
|
|
7354 |
|
|
|
7355 |
// Check if the drag didn't clear the buttons mid-point
|
|
|
7356 |
// and we aren't moving fast enough to swipe open
|
|
|
7357 |
if (e.gesture.deltaX > -(self._currentDrag.buttonsWidth / 2)) {
|
|
|
7358 |
|
|
|
7359 |
// If we are going left but too slow, or going right, go back to resting
|
|
|
7360 |
if (e.gesture.direction == "left" && Math.abs(e.gesture.velocityX) < 0.3) {
|
|
|
7361 |
restingPoint = 0;
|
|
|
7362 |
|
|
|
7363 |
} else if (e.gesture.direction == "right") {
|
|
|
7364 |
restingPoint = 0;
|
|
|
7365 |
}
|
|
|
7366 |
|
|
|
7367 |
}
|
|
|
7368 |
|
|
|
7369 |
ionic.requestAnimationFrame(function() {
|
|
|
7370 |
if (restingPoint === 0) {
|
|
|
7371 |
self._currentDrag.content.style[ionic.CSS.TRANSFORM] = '';
|
|
|
7372 |
var buttons = self._currentDrag.buttons;
|
|
|
7373 |
setTimeout(function() {
|
|
|
7374 |
buttons && buttons.classList.add('invisible');
|
|
|
7375 |
}, 250);
|
|
|
7376 |
} else {
|
|
|
7377 |
self._currentDrag.content.style[ionic.CSS.TRANSFORM] = 'translate3d(' + restingPoint + 'px,0,0)';
|
|
|
7378 |
}
|
|
|
7379 |
self._currentDrag.content.style[ionic.CSS.TRANSITION] = '';
|
|
|
7380 |
|
|
|
7381 |
|
|
|
7382 |
// Kill the current drag
|
|
|
7383 |
if (!self._lastDrag) {
|
|
|
7384 |
self._lastDrag = {};
|
|
|
7385 |
}
|
|
|
7386 |
ionic.extend(self._lastDrag, self._currentDrag);
|
|
|
7387 |
if (self._currentDrag) {
|
|
|
7388 |
self._currentDrag.buttons = null;
|
|
|
7389 |
self._currentDrag.content = null;
|
|
|
7390 |
}
|
|
|
7391 |
self._currentDrag = null;
|
|
|
7392 |
|
|
|
7393 |
// We are done, notify caller
|
|
|
7394 |
doneCallback && doneCallback();
|
|
|
7395 |
});
|
|
|
7396 |
};
|
|
|
7397 |
|
|
|
7398 |
var ReorderDrag = function(opts) {
|
|
|
7399 |
var self = this;
|
|
|
7400 |
|
|
|
7401 |
self.dragThresholdY = opts.dragThresholdY || 0;
|
|
|
7402 |
self.onReorder = opts.onReorder;
|
|
|
7403 |
self.listEl = opts.listEl;
|
|
|
7404 |
self.el = self.item = opts.el;
|
|
|
7405 |
self.scrollEl = opts.scrollEl;
|
|
|
7406 |
self.scrollView = opts.scrollView;
|
|
|
7407 |
// Get the True Top of the list el http://www.quirksmode.org/js/findpos.html
|
|
|
7408 |
self.listElTrueTop = 0;
|
|
|
7409 |
if (self.listEl.offsetParent) {
|
|
|
7410 |
var obj = self.listEl;
|
|
|
7411 |
do {
|
|
|
7412 |
self.listElTrueTop += obj.offsetTop;
|
|
|
7413 |
obj = obj.offsetParent;
|
|
|
7414 |
} while (obj);
|
|
|
7415 |
}
|
|
|
7416 |
};
|
|
|
7417 |
|
|
|
7418 |
ReorderDrag.prototype = new DragOp();
|
|
|
7419 |
|
|
|
7420 |
ReorderDrag.prototype._moveElement = function(e) {
|
|
|
7421 |
var y = e.gesture.center.pageY +
|
|
|
7422 |
this.scrollView.getValues().top -
|
|
|
7423 |
(this._currentDrag.elementHeight / 2) -
|
|
|
7424 |
this.listElTrueTop;
|
|
|
7425 |
this.el.style[ionic.CSS.TRANSFORM] = 'translate3d(0, ' + y + 'px, 0)';
|
|
|
7426 |
};
|
|
|
7427 |
|
|
|
7428 |
ReorderDrag.prototype.deregister = function() {
|
|
|
7429 |
this.listEl = this.el = this.scrollEl = this.scrollView = null;
|
|
|
7430 |
};
|
|
|
7431 |
|
|
|
7432 |
ReorderDrag.prototype.start = function(e) {
|
|
|
7433 |
|
|
|
7434 |
var startIndex = ionic.DomUtil.getChildIndex(this.el, this.el.nodeName.toLowerCase());
|
|
|
7435 |
var elementHeight = this.el.scrollHeight;
|
|
|
7436 |
var placeholder = this.el.cloneNode(true);
|
|
|
7437 |
|
|
|
7438 |
placeholder.classList.add(ITEM_PLACEHOLDER_CLASS);
|
|
|
7439 |
|
|
|
7440 |
this.el.parentNode.insertBefore(placeholder, this.el);
|
|
|
7441 |
this.el.classList.add(ITEM_REORDERING_CLASS);
|
|
|
7442 |
|
|
|
7443 |
this._currentDrag = {
|
|
|
7444 |
elementHeight: elementHeight,
|
|
|
7445 |
startIndex: startIndex,
|
|
|
7446 |
placeholder: placeholder,
|
|
|
7447 |
scrollHeight: scroll,
|
|
|
7448 |
list: placeholder.parentNode
|
|
|
7449 |
};
|
|
|
7450 |
|
|
|
7451 |
this._moveElement(e);
|
|
|
7452 |
};
|
|
|
7453 |
|
|
|
7454 |
ReorderDrag.prototype.drag = ionic.animationFrameThrottle(function(e) {
|
|
|
7455 |
// We really aren't dragging
|
|
|
7456 |
var self = this;
|
|
|
7457 |
if (!this._currentDrag) {
|
|
|
7458 |
return;
|
|
|
7459 |
}
|
|
|
7460 |
|
|
|
7461 |
var scrollY = 0;
|
|
|
7462 |
var pageY = e.gesture.center.pageY;
|
|
|
7463 |
var offset = this.listElTrueTop;
|
|
|
7464 |
|
|
|
7465 |
//If we have a scrollView, check scroll boundaries for dragged element and scroll if necessary
|
|
|
7466 |
if (this.scrollView) {
|
|
|
7467 |
|
|
|
7468 |
var container = this.scrollView.__container;
|
|
|
7469 |
scrollY = this.scrollView.getValues().top;
|
|
|
7470 |
|
|
|
7471 |
var containerTop = container.offsetTop;
|
|
|
7472 |
var pixelsPastTop = containerTop - pageY + this._currentDrag.elementHeight / 2;
|
|
|
7473 |
var pixelsPastBottom = pageY + this._currentDrag.elementHeight / 2 - containerTop - container.offsetHeight;
|
|
|
7474 |
|
|
|
7475 |
if (e.gesture.deltaY < 0 && pixelsPastTop > 0 && scrollY > 0) {
|
|
|
7476 |
this.scrollView.scrollBy(null, -pixelsPastTop);
|
|
|
7477 |
//Trigger another drag so the scrolling keeps going
|
|
|
7478 |
ionic.requestAnimationFrame(function() {
|
|
|
7479 |
self.drag(e);
|
|
|
7480 |
});
|
|
|
7481 |
}
|
|
|
7482 |
if (e.gesture.deltaY > 0 && pixelsPastBottom > 0) {
|
|
|
7483 |
if (scrollY < this.scrollView.getScrollMax().top) {
|
|
|
7484 |
this.scrollView.scrollBy(null, pixelsPastBottom);
|
|
|
7485 |
//Trigger another drag so the scrolling keeps going
|
|
|
7486 |
ionic.requestAnimationFrame(function() {
|
|
|
7487 |
self.drag(e);
|
|
|
7488 |
});
|
|
|
7489 |
}
|
|
|
7490 |
}
|
|
|
7491 |
}
|
|
|
7492 |
|
|
|
7493 |
// Check if we should start dragging. Check if we've dragged past the threshold,
|
|
|
7494 |
// or we are starting from the open state.
|
|
|
7495 |
if (!this._isDragging && Math.abs(e.gesture.deltaY) > this.dragThresholdY) {
|
|
|
7496 |
this._isDragging = true;
|
|
|
7497 |
}
|
|
|
7498 |
|
|
|
7499 |
if (this._isDragging) {
|
|
|
7500 |
this._moveElement(e);
|
|
|
7501 |
|
|
|
7502 |
this._currentDrag.currentY = scrollY + pageY - offset;
|
|
|
7503 |
|
|
|
7504 |
// this._reorderItems();
|
|
|
7505 |
}
|
|
|
7506 |
});
|
|
|
7507 |
|
|
|
7508 |
// When an item is dragged, we need to reorder any items for sorting purposes
|
|
|
7509 |
ReorderDrag.prototype._getReorderIndex = function() {
|
|
|
7510 |
var self = this;
|
|
|
7511 |
|
|
|
7512 |
var siblings = Array.prototype.slice.call(self._currentDrag.placeholder.parentNode.children)
|
|
|
7513 |
.filter(function(el) {
|
|
|
7514 |
return el.nodeName === self.el.nodeName && el !== self.el;
|
|
|
7515 |
});
|
|
|
7516 |
|
|
|
7517 |
var dragOffsetTop = self._currentDrag.currentY;
|
|
|
7518 |
var el;
|
|
|
7519 |
for (var i = 0, len = siblings.length; i < len; i++) {
|
|
|
7520 |
el = siblings[i];
|
|
|
7521 |
if (i === len - 1) {
|
|
|
7522 |
if (dragOffsetTop > el.offsetTop) {
|
|
|
7523 |
return i;
|
|
|
7524 |
}
|
|
|
7525 |
} else if (i === 0) {
|
|
|
7526 |
if (dragOffsetTop < el.offsetTop + el.offsetHeight) {
|
|
|
7527 |
return i;
|
|
|
7528 |
}
|
|
|
7529 |
} else if (dragOffsetTop > el.offsetTop - el.offsetHeight / 2 &&
|
|
|
7530 |
dragOffsetTop < el.offsetTop + el.offsetHeight) {
|
|
|
7531 |
return i;
|
|
|
7532 |
}
|
|
|
7533 |
}
|
|
|
7534 |
return self._currentDrag.startIndex;
|
|
|
7535 |
};
|
|
|
7536 |
|
|
|
7537 |
ReorderDrag.prototype.end = function(e, doneCallback) {
|
|
|
7538 |
if (!this._currentDrag) {
|
|
|
7539 |
doneCallback && doneCallback();
|
|
|
7540 |
return;
|
|
|
7541 |
}
|
|
|
7542 |
|
|
|
7543 |
var placeholder = this._currentDrag.placeholder;
|
|
|
7544 |
var finalIndex = this._getReorderIndex();
|
|
|
7545 |
|
|
|
7546 |
// Reposition the element
|
|
|
7547 |
this.el.classList.remove(ITEM_REORDERING_CLASS);
|
|
|
7548 |
this.el.style[ionic.CSS.TRANSFORM] = '';
|
|
|
7549 |
|
|
|
7550 |
placeholder.parentNode.insertBefore(this.el, placeholder);
|
|
|
7551 |
placeholder.parentNode.removeChild(placeholder);
|
|
|
7552 |
|
|
|
7553 |
this.onReorder && this.onReorder(this.el, this._currentDrag.startIndex, finalIndex);
|
|
|
7554 |
|
|
|
7555 |
this._currentDrag = {
|
|
|
7556 |
placeholder: null,
|
|
|
7557 |
content: null
|
|
|
7558 |
};
|
|
|
7559 |
this._currentDrag = null;
|
|
|
7560 |
doneCallback && doneCallback();
|
|
|
7561 |
};
|
|
|
7562 |
|
|
|
7563 |
|
|
|
7564 |
|
|
|
7565 |
/**
|
|
|
7566 |
* The ListView handles a list of items. It will process drag animations, edit mode,
|
|
|
7567 |
* and other operations that are common on mobile lists or table views.
|
|
|
7568 |
*/
|
|
|
7569 |
ionic.views.ListView = ionic.views.View.inherit({
|
|
|
7570 |
initialize: function(opts) {
|
|
|
7571 |
var self = this;
|
|
|
7572 |
|
|
|
7573 |
opts = ionic.extend({
|
|
|
7574 |
onReorder: function() {},
|
|
|
7575 |
virtualRemoveThreshold: -200,
|
|
|
7576 |
virtualAddThreshold: 200,
|
|
|
7577 |
canSwipe: function() {
|
|
|
7578 |
return true;
|
|
|
7579 |
}
|
|
|
7580 |
}, opts);
|
|
|
7581 |
|
|
|
7582 |
ionic.extend(self, opts);
|
|
|
7583 |
|
|
|
7584 |
if (!self.itemHeight && self.listEl) {
|
|
|
7585 |
self.itemHeight = self.listEl.children[0] && parseInt(self.listEl.children[0].style.height, 10);
|
|
|
7586 |
}
|
|
|
7587 |
|
|
|
7588 |
self.onRefresh = opts.onRefresh || function() {};
|
|
|
7589 |
self.onRefreshOpening = opts.onRefreshOpening || function() {};
|
|
|
7590 |
self.onRefreshHolding = opts.onRefreshHolding || function() {};
|
|
|
7591 |
|
|
|
7592 |
var gestureOpts = {};
|
|
|
7593 |
// don't prevent native scrolling
|
|
|
7594 |
if (ionic.DomUtil.getParentOrSelfWithClass(self.el, 'overflow-scroll')) {
|
|
|
7595 |
gestureOpts.prevent_default_directions = ['left', 'right'];
|
|
|
7596 |
}
|
|
|
7597 |
|
|
|
7598 |
window.ionic.onGesture('release', function(e) {
|
|
|
7599 |
self._handleEndDrag(e);
|
|
|
7600 |
}, self.el, gestureOpts);
|
|
|
7601 |
|
|
|
7602 |
window.ionic.onGesture('drag', function(e) {
|
|
|
7603 |
self._handleDrag(e);
|
|
|
7604 |
}, self.el, gestureOpts);
|
|
|
7605 |
// Start the drag states
|
|
|
7606 |
self._initDrag();
|
|
|
7607 |
},
|
|
|
7608 |
|
|
|
7609 |
/**
|
|
|
7610 |
* Be sure to cleanup references.
|
|
|
7611 |
*/
|
|
|
7612 |
deregister: function() {
|
|
|
7613 |
this.el = this.listEl = this.scrollEl = this.scrollView = null;
|
|
|
7614 |
|
|
|
7615 |
// ensure no scrolls have been left frozen
|
|
|
7616 |
if (this.isScrollFreeze) {
|
|
|
7617 |
self.scrollView.freeze(false);
|
|
|
7618 |
}
|
|
|
7619 |
},
|
|
|
7620 |
|
|
|
7621 |
/**
|
|
|
7622 |
* Called to tell the list to stop refreshing. This is useful
|
|
|
7623 |
* if you are refreshing the list and are done with refreshing.
|
|
|
7624 |
*/
|
|
|
7625 |
stopRefreshing: function() {
|
|
|
7626 |
var refresher = this.el.querySelector('.list-refresher');
|
|
|
7627 |
refresher.style.height = '0';
|
|
|
7628 |
},
|
|
|
7629 |
|
|
|
7630 |
/**
|
|
|
7631 |
* If we scrolled and have virtual mode enabled, compute the window
|
|
|
7632 |
* of active elements in order to figure out the viewport to render.
|
|
|
7633 |
*/
|
|
|
7634 |
didScroll: function(e) {
|
|
|
7635 |
var self = this;
|
|
|
7636 |
|
|
|
7637 |
if (self.isVirtual) {
|
|
|
7638 |
var itemHeight = self.itemHeight;
|
|
|
7639 |
|
|
|
7640 |
// Grab the total height of the list
|
|
|
7641 |
var scrollHeight = e.target.scrollHeight;
|
|
|
7642 |
|
|
|
7643 |
// Get the viewport height
|
|
|
7644 |
var viewportHeight = self.el.parentNode.offsetHeight;
|
|
|
7645 |
|
|
|
7646 |
// High water is the pixel position of the first element to include (everything before
|
|
|
7647 |
// that will be removed)
|
|
|
7648 |
var highWater = Math.max(0, e.scrollTop + self.virtualRemoveThreshold);
|
|
|
7649 |
|
|
|
7650 |
// Low water is the pixel position of the last element to include (everything after
|
|
|
7651 |
// that will be removed)
|
|
|
7652 |
var lowWater = Math.min(scrollHeight, Math.abs(e.scrollTop) + viewportHeight + self.virtualAddThreshold);
|
|
|
7653 |
|
|
|
7654 |
// Get the first and last elements in the list based on how many can fit
|
|
|
7655 |
// between the pixel range of lowWater and highWater
|
|
|
7656 |
var first = parseInt(Math.abs(highWater / itemHeight), 10);
|
|
|
7657 |
var last = parseInt(Math.abs(lowWater / itemHeight), 10);
|
|
|
7658 |
|
|
|
7659 |
// Get the items we need to remove
|
|
|
7660 |
self._virtualItemsToRemove = Array.prototype.slice.call(self.listEl.children, 0, first);
|
|
|
7661 |
|
|
|
7662 |
self.renderViewport && self.renderViewport(highWater, lowWater, first, last);
|
|
|
7663 |
}
|
|
|
7664 |
},
|
|
|
7665 |
|
|
|
7666 |
didStopScrolling: function() {
|
|
|
7667 |
if (this.isVirtual) {
|
|
|
7668 |
for (var i = 0; i < this._virtualItemsToRemove.length; i++) {
|
|
|
7669 |
//el.parentNode.removeChild(el);
|
|
|
7670 |
this.didHideItem && this.didHideItem(i);
|
|
|
7671 |
}
|
|
|
7672 |
// Once scrolling stops, check if we need to remove old items
|
|
|
7673 |
|
|
|
7674 |
}
|
|
|
7675 |
},
|
|
|
7676 |
|
|
|
7677 |
/**
|
|
|
7678 |
* Clear any active drag effects on the list.
|
|
|
7679 |
*/
|
|
|
7680 |
clearDragEffects: function(isInstant) {
|
|
|
7681 |
if (this._lastDragOp) {
|
|
|
7682 |
this._lastDragOp.clean && this._lastDragOp.clean(isInstant);
|
|
|
7683 |
this._lastDragOp.deregister && this._lastDragOp.deregister();
|
|
|
7684 |
this._lastDragOp = null;
|
|
|
7685 |
}
|
|
|
7686 |
},
|
|
|
7687 |
|
|
|
7688 |
_initDrag: function() {
|
|
|
7689 |
// Store the last one
|
|
|
7690 |
if (this._lastDragOp) {
|
|
|
7691 |
this._lastDragOp.deregister && this._lastDragOp.deregister();
|
|
|
7692 |
}
|
|
|
7693 |
this._lastDragOp = this._dragOp;
|
|
|
7694 |
|
|
|
7695 |
this._dragOp = null;
|
|
|
7696 |
},
|
|
|
7697 |
|
|
|
7698 |
// Return the list item from the given target
|
|
|
7699 |
_getItem: function(target) {
|
|
|
7700 |
while (target) {
|
|
|
7701 |
if (target.classList && target.classList.contains(ITEM_CLASS)) {
|
|
|
7702 |
return target;
|
|
|
7703 |
}
|
|
|
7704 |
target = target.parentNode;
|
|
|
7705 |
}
|
|
|
7706 |
return null;
|
|
|
7707 |
},
|
|
|
7708 |
|
|
|
7709 |
|
|
|
7710 |
_startDrag: function(e) {
|
|
|
7711 |
var self = this;
|
|
|
7712 |
|
|
|
7713 |
self._isDragging = false;
|
|
|
7714 |
|
|
|
7715 |
var lastDragOp = self._lastDragOp;
|
|
|
7716 |
var item;
|
|
|
7717 |
|
|
|
7718 |
// If we have an open SlideDrag and we're scrolling the list. Clear it.
|
|
|
7719 |
if (self._didDragUpOrDown && lastDragOp instanceof SlideDrag) {
|
|
|
7720 |
lastDragOp.clean && lastDragOp.clean();
|
|
|
7721 |
}
|
|
|
7722 |
|
|
|
7723 |
// Check if this is a reorder drag
|
|
|
7724 |
if (ionic.DomUtil.getParentOrSelfWithClass(e.target, ITEM_REORDER_BTN_CLASS) && (e.gesture.direction == 'up' || e.gesture.direction == 'down')) {
|
|
|
7725 |
item = self._getItem(e.target);
|
|
|
7726 |
|
|
|
7727 |
if (item) {
|
|
|
7728 |
self._dragOp = new ReorderDrag({
|
|
|
7729 |
listEl: self.el,
|
|
|
7730 |
el: item,
|
|
|
7731 |
scrollEl: self.scrollEl,
|
|
|
7732 |
scrollView: self.scrollView,
|
|
|
7733 |
onReorder: function(el, start, end) {
|
|
|
7734 |
self.onReorder && self.onReorder(el, start, end);
|
|
|
7735 |
}
|
|
|
7736 |
});
|
|
|
7737 |
self._dragOp.start(e);
|
|
|
7738 |
e.preventDefault();
|
|
|
7739 |
}
|
|
|
7740 |
}
|
|
|
7741 |
|
|
|
7742 |
// Or check if this is a swipe to the side drag
|
|
|
7743 |
else if (!self._didDragUpOrDown && (e.gesture.direction == 'left' || e.gesture.direction == 'right') && Math.abs(e.gesture.deltaX) > 5) {
|
|
|
7744 |
|
|
|
7745 |
// Make sure this is an item with buttons
|
|
|
7746 |
item = self._getItem(e.target);
|
|
|
7747 |
if (item && item.querySelector('.item-options')) {
|
|
|
7748 |
self._dragOp = new SlideDrag({
|
|
|
7749 |
el: self.el,
|
|
|
7750 |
item: item,
|
|
|
7751 |
canSwipe: self.canSwipe
|
|
|
7752 |
});
|
|
|
7753 |
self._dragOp.start(e);
|
|
|
7754 |
e.preventDefault();
|
|
|
7755 |
self.isScrollFreeze = self.scrollView.freeze(true);
|
|
|
7756 |
}
|
|
|
7757 |
}
|
|
|
7758 |
|
|
|
7759 |
// If we had a last drag operation and this is a new one on a different item, clean that last one
|
|
|
7760 |
if (lastDragOp && self._dragOp && !self._dragOp.isSameItem(lastDragOp) && e.defaultPrevented) {
|
|
|
7761 |
lastDragOp.clean && lastDragOp.clean();
|
|
|
7762 |
}
|
|
|
7763 |
},
|
|
|
7764 |
|
|
|
7765 |
|
|
|
7766 |
_handleEndDrag: function(e) {
|
|
|
7767 |
var self = this;
|
|
|
7768 |
|
|
|
7769 |
if (self.scrollView) {
|
|
|
7770 |
self.isScrollFreeze = self.scrollView.freeze(false);
|
|
|
7771 |
}
|
|
|
7772 |
|
|
|
7773 |
self._didDragUpOrDown = false;
|
|
|
7774 |
|
|
|
7775 |
if (!self._dragOp) {
|
|
|
7776 |
return;
|
|
|
7777 |
}
|
|
|
7778 |
|
|
|
7779 |
self._dragOp.end(e, function() {
|
|
|
7780 |
self._initDrag();
|
|
|
7781 |
});
|
|
|
7782 |
},
|
|
|
7783 |
|
|
|
7784 |
/**
|
|
|
7785 |
* Process the drag event to move the item to the left or right.
|
|
|
7786 |
*/
|
|
|
7787 |
_handleDrag: function(e) {
|
|
|
7788 |
var self = this;
|
|
|
7789 |
|
|
|
7790 |
if (Math.abs(e.gesture.deltaY) > 5) {
|
|
|
7791 |
self._didDragUpOrDown = true;
|
|
|
7792 |
}
|
|
|
7793 |
|
|
|
7794 |
// If we get a drag event, make sure we aren't in another drag, then check if we should
|
|
|
7795 |
// start one
|
|
|
7796 |
if (!self.isDragging && !self._dragOp) {
|
|
|
7797 |
self._startDrag(e);
|
|
|
7798 |
}
|
|
|
7799 |
|
|
|
7800 |
// No drag still, pass it up
|
|
|
7801 |
if (!self._dragOp) {
|
|
|
7802 |
return;
|
|
|
7803 |
}
|
|
|
7804 |
|
|
|
7805 |
e.gesture.srcEvent.preventDefault();
|
|
|
7806 |
self._dragOp.drag(e);
|
|
|
7807 |
}
|
|
|
7808 |
|
|
|
7809 |
});
|
|
|
7810 |
|
|
|
7811 |
})(ionic);
|
|
|
7812 |
|
|
|
7813 |
(function(ionic) {
|
|
|
7814 |
'use strict';
|
|
|
7815 |
|
|
|
7816 |
ionic.views.Modal = ionic.views.View.inherit({
|
|
|
7817 |
initialize: function(opts) {
|
|
|
7818 |
opts = ionic.extend({
|
|
|
7819 |
focusFirstInput: false,
|
|
|
7820 |
unfocusOnHide: true,
|
|
|
7821 |
focusFirstDelay: 600,
|
|
|
7822 |
backdropClickToClose: true,
|
|
|
7823 |
hardwareBackButtonClose: true,
|
|
|
7824 |
}, opts);
|
|
|
7825 |
|
|
|
7826 |
ionic.extend(this, opts);
|
|
|
7827 |
|
|
|
7828 |
this.el = opts.el;
|
|
|
7829 |
},
|
|
|
7830 |
show: function() {
|
|
|
7831 |
var self = this;
|
|
|
7832 |
|
|
|
7833 |
if(self.focusFirstInput) {
|
|
|
7834 |
// Let any animations run first
|
|
|
7835 |
window.setTimeout(function() {
|
|
|
7836 |
var input = self.el.querySelector('input, textarea');
|
|
|
7837 |
input && input.focus && input.focus();
|
|
|
7838 |
}, self.focusFirstDelay);
|
|
|
7839 |
}
|
|
|
7840 |
},
|
|
|
7841 |
hide: function() {
|
|
|
7842 |
// Unfocus all elements
|
|
|
7843 |
if(this.unfocusOnHide) {
|
|
|
7844 |
var inputs = this.el.querySelectorAll('input, textarea');
|
|
|
7845 |
// Let any animations run first
|
|
|
7846 |
window.setTimeout(function() {
|
|
|
7847 |
for(var i = 0; i < inputs.length; i++) {
|
|
|
7848 |
inputs[i].blur && inputs[i].blur();
|
|
|
7849 |
}
|
|
|
7850 |
});
|
|
|
7851 |
}
|
|
|
7852 |
}
|
|
|
7853 |
});
|
|
|
7854 |
|
|
|
7855 |
})(ionic);
|
|
|
7856 |
|
|
|
7857 |
(function(ionic) {
|
|
|
7858 |
'use strict';
|
|
|
7859 |
|
|
|
7860 |
/**
|
|
|
7861 |
* The side menu view handles one of the side menu's in a Side Menu Controller
|
|
|
7862 |
* configuration.
|
|
|
7863 |
* It takes a DOM reference to that side menu element.
|
|
|
7864 |
*/
|
|
|
7865 |
ionic.views.SideMenu = ionic.views.View.inherit({
|
|
|
7866 |
initialize: function(opts) {
|
|
|
7867 |
this.el = opts.el;
|
|
|
7868 |
this.isEnabled = (typeof opts.isEnabled === 'undefined') ? true : opts.isEnabled;
|
|
|
7869 |
this.setWidth(opts.width);
|
|
|
7870 |
},
|
|
|
7871 |
getFullWidth: function() {
|
|
|
7872 |
return this.width;
|
|
|
7873 |
},
|
|
|
7874 |
setWidth: function(width) {
|
|
|
7875 |
this.width = width;
|
|
|
7876 |
this.el.style.width = width + 'px';
|
|
|
7877 |
},
|
|
|
7878 |
setIsEnabled: function(isEnabled) {
|
|
|
7879 |
this.isEnabled = isEnabled;
|
|
|
7880 |
},
|
|
|
7881 |
bringUp: function() {
|
|
|
7882 |
if(this.el.style.zIndex !== '0') {
|
|
|
7883 |
this.el.style.zIndex = '0';
|
|
|
7884 |
}
|
|
|
7885 |
},
|
|
|
7886 |
pushDown: function() {
|
|
|
7887 |
if(this.el.style.zIndex !== '-1') {
|
|
|
7888 |
this.el.style.zIndex = '-1';
|
|
|
7889 |
}
|
|
|
7890 |
}
|
|
|
7891 |
});
|
|
|
7892 |
|
|
|
7893 |
ionic.views.SideMenuContent = ionic.views.View.inherit({
|
|
|
7894 |
initialize: function(opts) {
|
|
|
7895 |
ionic.extend(this, {
|
|
|
7896 |
animationClass: 'menu-animated',
|
|
|
7897 |
onDrag: function() {},
|
|
|
7898 |
onEndDrag: function() {}
|
|
|
7899 |
}, opts);
|
|
|
7900 |
|
|
|
7901 |
ionic.onGesture('drag', ionic.proxy(this._onDrag, this), this.el);
|
|
|
7902 |
ionic.onGesture('release', ionic.proxy(this._onEndDrag, this), this.el);
|
|
|
7903 |
},
|
|
|
7904 |
_onDrag: function(e) {
|
|
|
7905 |
this.onDrag && this.onDrag(e);
|
|
|
7906 |
},
|
|
|
7907 |
_onEndDrag: function(e) {
|
|
|
7908 |
this.onEndDrag && this.onEndDrag(e);
|
|
|
7909 |
},
|
|
|
7910 |
disableAnimation: function() {
|
|
|
7911 |
this.el.classList.remove(this.animationClass);
|
|
|
7912 |
},
|
|
|
7913 |
enableAnimation: function() {
|
|
|
7914 |
this.el.classList.add(this.animationClass);
|
|
|
7915 |
},
|
|
|
7916 |
getTranslateX: function() {
|
|
|
7917 |
return parseFloat(this.el.style[ionic.CSS.TRANSFORM].replace('translate3d(', '').split(',')[0]);
|
|
|
7918 |
},
|
|
|
7919 |
setTranslateX: ionic.animationFrameThrottle(function(x) {
|
|
|
7920 |
this.el.style[ionic.CSS.TRANSFORM] = 'translate3d(' + x + 'px, 0, 0)';
|
|
|
7921 |
})
|
|
|
7922 |
});
|
|
|
7923 |
|
|
|
7924 |
})(ionic);
|
|
|
7925 |
|
|
|
7926 |
/*
|
|
|
7927 |
* Adapted from Swipe.js 2.0
|
|
|
7928 |
*
|
|
|
7929 |
* Brad Birdsall
|
|
|
7930 |
* Copyright 2013, MIT License
|
|
|
7931 |
*
|
|
|
7932 |
*/
|
|
|
7933 |
|
|
|
7934 |
(function(ionic) {
|
|
|
7935 |
'use strict';
|
|
|
7936 |
|
|
|
7937 |
ionic.views.Slider = ionic.views.View.inherit({
|
|
|
7938 |
initialize: function (options) {
|
|
|
7939 |
var slider = this;
|
|
|
7940 |
|
|
|
7941 |
// utilities
|
|
|
7942 |
var noop = function() {}; // simple no operation function
|
|
|
7943 |
var offloadFn = function(fn) { setTimeout(fn || noop, 0); }; // offload a functions execution
|
|
|
7944 |
|
|
|
7945 |
// check browser capabilities
|
|
|
7946 |
var browser = {
|
|
|
7947 |
addEventListener: !!window.addEventListener,
|
|
|
7948 |
touch: ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch,
|
|
|
7949 |
transitions: (function(temp) {
|
|
|
7950 |
var props = ['transitionProperty', 'WebkitTransition', 'MozTransition', 'OTransition', 'msTransition'];
|
|
|
7951 |
for ( var i in props ) if (temp.style[ props[i] ] !== undefined) return true;
|
|
|
7952 |
return false;
|
|
|
7953 |
})(document.createElement('swipe'))
|
|
|
7954 |
};
|
|
|
7955 |
|
|
|
7956 |
|
|
|
7957 |
var container = options.el;
|
|
|
7958 |
|
|
|
7959 |
// quit if no root element
|
|
|
7960 |
if (!container) return;
|
|
|
7961 |
var element = container.children[0];
|
|
|
7962 |
var slides, slidePos, width, length;
|
|
|
7963 |
options = options || {};
|
|
|
7964 |
var index = parseInt(options.startSlide, 10) || 0;
|
|
|
7965 |
var speed = options.speed || 300;
|
|
|
7966 |
options.continuous = options.continuous !== undefined ? options.continuous : true;
|
|
|
7967 |
|
|
|
7968 |
function setup() {
|
|
|
7969 |
|
|
|
7970 |
// do not setup if the container has no width
|
|
|
7971 |
if (!container.offsetWidth) {
|
|
|
7972 |
return;
|
|
|
7973 |
}
|
|
|
7974 |
|
|
|
7975 |
// cache slides
|
|
|
7976 |
slides = element.children;
|
|
|
7977 |
length = slides.length;
|
|
|
7978 |
|
|
|
7979 |
// set continuous to false if only one slide
|
|
|
7980 |
if (slides.length < 2) options.continuous = false;
|
|
|
7981 |
|
|
|
7982 |
//special case if two slides
|
|
|
7983 |
if (browser.transitions && options.continuous && slides.length < 3) {
|
|
|
7984 |
element.appendChild(slides[0].cloneNode(true));
|
|
|
7985 |
element.appendChild(element.children[1].cloneNode(true));
|
|
|
7986 |
slides = element.children;
|
|
|
7987 |
}
|
|
|
7988 |
|
|
|
7989 |
// create an array to store current positions of each slide
|
|
|
7990 |
slidePos = new Array(slides.length);
|
|
|
7991 |
|
|
|
7992 |
// determine width of each slide
|
|
|
7993 |
width = container.offsetWidth || container.getBoundingClientRect().width;
|
|
|
7994 |
|
|
|
7995 |
element.style.width = (slides.length * width) + 'px';
|
|
|
7996 |
|
|
|
7997 |
// stack elements
|
|
|
7998 |
var pos = slides.length;
|
|
|
7999 |
while(pos--) {
|
|
|
8000 |
|
|
|
8001 |
var slide = slides[pos];
|
|
|
8002 |
|
|
|
8003 |
slide.style.width = width + 'px';
|
|
|
8004 |
slide.setAttribute('data-index', pos);
|
|
|
8005 |
|
|
|
8006 |
if (browser.transitions) {
|
|
|
8007 |
slide.style.left = (pos * -width) + 'px';
|
|
|
8008 |
move(pos, index > pos ? -width : (index < pos ? width : 0), 0);
|
|
|
8009 |
}
|
|
|
8010 |
|
|
|
8011 |
}
|
|
|
8012 |
|
|
|
8013 |
// reposition elements before and after index
|
|
|
8014 |
if (options.continuous && browser.transitions) {
|
|
|
8015 |
move(circle(index - 1), -width, 0);
|
|
|
8016 |
move(circle(index + 1), width, 0);
|
|
|
8017 |
}
|
|
|
8018 |
|
|
|
8019 |
if (!browser.transitions) element.style.left = (index * -width) + 'px';
|
|
|
8020 |
|
|
|
8021 |
container.style.visibility = 'visible';
|
|
|
8022 |
|
|
|
8023 |
options.slidesChanged && options.slidesChanged();
|
|
|
8024 |
}
|
|
|
8025 |
|
|
|
8026 |
function prev(slideSpeed) {
|
|
|
8027 |
|
|
|
8028 |
if (options.continuous) slide(index - 1, slideSpeed);
|
|
|
8029 |
else if (index) slide(index - 1, slideSpeed);
|
|
|
8030 |
|
|
|
8031 |
}
|
|
|
8032 |
|
|
|
8033 |
function next(slideSpeed) {
|
|
|
8034 |
|
|
|
8035 |
if (options.continuous) slide(index + 1, slideSpeed);
|
|
|
8036 |
else if (index < slides.length - 1) slide(index + 1, slideSpeed);
|
|
|
8037 |
|
|
|
8038 |
}
|
|
|
8039 |
|
|
|
8040 |
function circle(index) {
|
|
|
8041 |
|
|
|
8042 |
// a simple positive modulo using slides.length
|
|
|
8043 |
return (slides.length + (index % slides.length)) % slides.length;
|
|
|
8044 |
|
|
|
8045 |
}
|
|
|
8046 |
|
|
|
8047 |
function slide(to, slideSpeed) {
|
|
|
8048 |
|
|
|
8049 |
// do nothing if already on requested slide
|
|
|
8050 |
if (index == to) return;
|
|
|
8051 |
|
|
|
8052 |
if (browser.transitions) {
|
|
|
8053 |
|
|
|
8054 |
var direction = Math.abs(index - to) / (index - to); // 1: backward, -1: forward
|
|
|
8055 |
|
|
|
8056 |
// get the actual position of the slide
|
|
|
8057 |
if (options.continuous) {
|
|
|
8058 |
var naturalDirection = direction;
|
|
|
8059 |
direction = -slidePos[circle(to)] / width;
|
|
|
8060 |
|
|
|
8061 |
// if going forward but to < index, use to = slides.length + to
|
|
|
8062 |
// if going backward but to > index, use to = -slides.length + to
|
|
|
8063 |
if (direction !== naturalDirection) to = -direction * slides.length + to;
|
|
|
8064 |
|
|
|
8065 |
}
|
|
|
8066 |
|
|
|
8067 |
var diff = Math.abs(index - to) - 1;
|
|
|
8068 |
|
|
|
8069 |
// move all the slides between index and to in the right direction
|
|
|
8070 |
while (diff--) move( circle((to > index ? to : index) - diff - 1), width * direction, 0);
|
|
|
8071 |
|
|
|
8072 |
to = circle(to);
|
|
|
8073 |
|
|
|
8074 |
move(index, width * direction, slideSpeed || speed);
|
|
|
8075 |
move(to, 0, slideSpeed || speed);
|
|
|
8076 |
|
|
|
8077 |
if (options.continuous) move(circle(to - direction), -(width * direction), 0); // we need to get the next in place
|
|
|
8078 |
|
|
|
8079 |
} else {
|
|
|
8080 |
|
|
|
8081 |
to = circle(to);
|
|
|
8082 |
animate(index * -width, to * -width, slideSpeed || speed);
|
|
|
8083 |
//no fallback for a circular continuous if the browser does not accept transitions
|
|
|
8084 |
}
|
|
|
8085 |
|
|
|
8086 |
index = to;
|
|
|
8087 |
offloadFn(options.callback && options.callback(index, slides[index]));
|
|
|
8088 |
}
|
|
|
8089 |
|
|
|
8090 |
function move(index, dist, speed) {
|
|
|
8091 |
|
|
|
8092 |
translate(index, dist, speed);
|
|
|
8093 |
slidePos[index] = dist;
|
|
|
8094 |
|
|
|
8095 |
}
|
|
|
8096 |
|
|
|
8097 |
function translate(index, dist, speed) {
|
|
|
8098 |
|
|
|
8099 |
var slide = slides[index];
|
|
|
8100 |
var style = slide && slide.style;
|
|
|
8101 |
|
|
|
8102 |
if (!style) return;
|
|
|
8103 |
|
|
|
8104 |
style.webkitTransitionDuration =
|
|
|
8105 |
style.MozTransitionDuration =
|
|
|
8106 |
style.msTransitionDuration =
|
|
|
8107 |
style.OTransitionDuration =
|
|
|
8108 |
style.transitionDuration = speed + 'ms';
|
|
|
8109 |
|
|
|
8110 |
style.webkitTransform = 'translate(' + dist + 'px,0)' + 'translateZ(0)';
|
|
|
8111 |
style.msTransform =
|
|
|
8112 |
style.MozTransform =
|
|
|
8113 |
style.OTransform = 'translateX(' + dist + 'px)';
|
|
|
8114 |
|
|
|
8115 |
}
|
|
|
8116 |
|
|
|
8117 |
function animate(from, to, speed) {
|
|
|
8118 |
|
|
|
8119 |
// if not an animation, just reposition
|
|
|
8120 |
if (!speed) {
|
|
|
8121 |
|
|
|
8122 |
element.style.left = to + 'px';
|
|
|
8123 |
return;
|
|
|
8124 |
|
|
|
8125 |
}
|
|
|
8126 |
|
|
|
8127 |
var start = +new Date();
|
|
|
8128 |
|
|
|
8129 |
var timer = setInterval(function() {
|
|
|
8130 |
|
|
|
8131 |
var timeElap = +new Date() - start;
|
|
|
8132 |
|
|
|
8133 |
if (timeElap > speed) {
|
|
|
8134 |
|
|
|
8135 |
element.style.left = to + 'px';
|
|
|
8136 |
|
|
|
8137 |
if (delay) begin();
|
|
|
8138 |
|
|
|
8139 |
options.transitionEnd && options.transitionEnd.call(event, index, slides[index]);
|
|
|
8140 |
|
|
|
8141 |
clearInterval(timer);
|
|
|
8142 |
return;
|
|
|
8143 |
|
|
|
8144 |
}
|
|
|
8145 |
|
|
|
8146 |
element.style.left = (( (to - from) * (Math.floor((timeElap / speed) * 100) / 100) ) + from) + 'px';
|
|
|
8147 |
|
|
|
8148 |
}, 4);
|
|
|
8149 |
|
|
|
8150 |
}
|
|
|
8151 |
|
|
|
8152 |
// setup auto slideshow
|
|
|
8153 |
var delay = options.auto || 0;
|
|
|
8154 |
var interval;
|
|
|
8155 |
|
|
|
8156 |
function begin() {
|
|
|
8157 |
|
|
|
8158 |
interval = setTimeout(next, delay);
|
|
|
8159 |
|
|
|
8160 |
}
|
|
|
8161 |
|
|
|
8162 |
function stop() {
|
|
|
8163 |
|
|
|
8164 |
delay = options.auto || 0;
|
|
|
8165 |
clearTimeout(interval);
|
|
|
8166 |
|
|
|
8167 |
}
|
|
|
8168 |
|
|
|
8169 |
|
|
|
8170 |
// setup initial vars
|
|
|
8171 |
var start = {};
|
|
|
8172 |
var delta = {};
|
|
|
8173 |
var isScrolling;
|
|
|
8174 |
|
|
|
8175 |
// setup event capturing
|
|
|
8176 |
var events = {
|
|
|
8177 |
|
|
|
8178 |
handleEvent: function(event) {
|
|
|
8179 |
if(event.type == 'mousedown' || event.type == 'mouseup' || event.type == 'mousemove') {
|
|
|
8180 |
event.touches = [{
|
|
|
8181 |
pageX: event.pageX,
|
|
|
8182 |
pageY: event.pageY
|
|
|
8183 |
}];
|
|
|
8184 |
}
|
|
|
8185 |
|
|
|
8186 |
switch (event.type) {
|
|
|
8187 |
case 'mousedown': this.start(event); break;
|
|
|
8188 |
case 'touchstart': this.start(event); break;
|
|
|
8189 |
case 'touchmove': this.touchmove(event); break;
|
|
|
8190 |
case 'mousemove': this.touchmove(event); break;
|
|
|
8191 |
case 'touchend': offloadFn(this.end(event)); break;
|
|
|
8192 |
case 'mouseup': offloadFn(this.end(event)); break;
|
|
|
8193 |
case 'webkitTransitionEnd':
|
|
|
8194 |
case 'msTransitionEnd':
|
|
|
8195 |
case 'oTransitionEnd':
|
|
|
8196 |
case 'otransitionend':
|
|
|
8197 |
case 'transitionend': offloadFn(this.transitionEnd(event)); break;
|
|
|
8198 |
case 'resize': offloadFn(setup); break;
|
|
|
8199 |
}
|
|
|
8200 |
|
|
|
8201 |
if (options.stopPropagation) event.stopPropagation();
|
|
|
8202 |
|
|
|
8203 |
},
|
|
|
8204 |
start: function(event) {
|
|
|
8205 |
|
|
|
8206 |
var touches = event.touches[0];
|
|
|
8207 |
|
|
|
8208 |
// measure start values
|
|
|
8209 |
start = {
|
|
|
8210 |
|
|
|
8211 |
// get initial touch coords
|
|
|
8212 |
x: touches.pageX,
|
|
|
8213 |
y: touches.pageY,
|
|
|
8214 |
|
|
|
8215 |
// store time to determine touch duration
|
|
|
8216 |
time: +new Date()
|
|
|
8217 |
|
|
|
8218 |
};
|
|
|
8219 |
|
|
|
8220 |
// used for testing first move event
|
|
|
8221 |
isScrolling = undefined;
|
|
|
8222 |
|
|
|
8223 |
// reset delta and end measurements
|
|
|
8224 |
delta = {};
|
|
|
8225 |
|
|
|
8226 |
// attach touchmove and touchend listeners
|
|
|
8227 |
if(browser.touch) {
|
|
|
8228 |
element.addEventListener('touchmove', this, false);
|
|
|
8229 |
element.addEventListener('touchend', this, false);
|
|
|
8230 |
} else {
|
|
|
8231 |
element.addEventListener('mousemove', this, false);
|
|
|
8232 |
element.addEventListener('mouseup', this, false);
|
|
|
8233 |
document.addEventListener('mouseup', this, false);
|
|
|
8234 |
}
|
|
|
8235 |
},
|
|
|
8236 |
touchmove: function(event) {
|
|
|
8237 |
|
|
|
8238 |
// ensure swiping with one touch and not pinching
|
|
|
8239 |
// ensure sliding is enabled
|
|
|
8240 |
if (event.touches.length > 1 ||
|
|
|
8241 |
event.scale && event.scale !== 1 ||
|
|
|
8242 |
slider.slideIsDisabled) {
|
|
|
8243 |
return;
|
|
|
8244 |
}
|
|
|
8245 |
|
|
|
8246 |
if (options.disableScroll) event.preventDefault();
|
|
|
8247 |
|
|
|
8248 |
var touches = event.touches[0];
|
|
|
8249 |
|
|
|
8250 |
// measure change in x and y
|
|
|
8251 |
delta = {
|
|
|
8252 |
x: touches.pageX - start.x,
|
|
|
8253 |
y: touches.pageY - start.y
|
|
|
8254 |
};
|
|
|
8255 |
|
|
|
8256 |
// determine if scrolling test has run - one time test
|
|
|
8257 |
if ( typeof isScrolling == 'undefined') {
|
|
|
8258 |
isScrolling = !!( isScrolling || Math.abs(delta.x) < Math.abs(delta.y) );
|
|
|
8259 |
}
|
|
|
8260 |
|
|
|
8261 |
// if user is not trying to scroll vertically
|
|
|
8262 |
if (!isScrolling) {
|
|
|
8263 |
|
|
|
8264 |
// prevent native scrolling
|
|
|
8265 |
event.preventDefault();
|
|
|
8266 |
|
|
|
8267 |
// stop slideshow
|
|
|
8268 |
stop();
|
|
|
8269 |
|
|
|
8270 |
// increase resistance if first or last slide
|
|
|
8271 |
if (options.continuous) { // we don't add resistance at the end
|
|
|
8272 |
|
|
|
8273 |
translate(circle(index - 1), delta.x + slidePos[circle(index - 1)], 0);
|
|
|
8274 |
translate(index, delta.x + slidePos[index], 0);
|
|
|
8275 |
translate(circle(index + 1), delta.x + slidePos[circle(index + 1)], 0);
|
|
|
8276 |
|
|
|
8277 |
} else {
|
|
|
8278 |
|
|
|
8279 |
delta.x =
|
|
|
8280 |
delta.x /
|
|
|
8281 |
( (!index && delta.x > 0 || // if first slide and sliding left
|
|
|
8282 |
index == slides.length - 1 && // or if last slide and sliding right
|
|
|
8283 |
delta.x < 0 // and if sliding at all
|
|
|
8284 |
) ?
|
|
|
8285 |
( Math.abs(delta.x) / width + 1 ) // determine resistance level
|
|
|
8286 |
: 1 ); // no resistance if false
|
|
|
8287 |
|
|
|
8288 |
// translate 1:1
|
|
|
8289 |
translate(index - 1, delta.x + slidePos[index - 1], 0);
|
|
|
8290 |
translate(index, delta.x + slidePos[index], 0);
|
|
|
8291 |
translate(index + 1, delta.x + slidePos[index + 1], 0);
|
|
|
8292 |
}
|
|
|
8293 |
|
|
|
8294 |
options.onDrag && options.onDrag();
|
|
|
8295 |
}
|
|
|
8296 |
|
|
|
8297 |
},
|
|
|
8298 |
end: function() {
|
|
|
8299 |
|
|
|
8300 |
// measure duration
|
|
|
8301 |
var duration = +new Date() - start.time;
|
|
|
8302 |
|
|
|
8303 |
// determine if slide attempt triggers next/prev slide
|
|
|
8304 |
var isValidSlide =
|
|
|
8305 |
Number(duration) < 250 && // if slide duration is less than 250ms
|
|
|
8306 |
Math.abs(delta.x) > 20 || // and if slide amt is greater than 20px
|
|
|
8307 |
Math.abs(delta.x) > width / 2; // or if slide amt is greater than half the width
|
|
|
8308 |
|
|
|
8309 |
// determine if slide attempt is past start and end
|
|
|
8310 |
var isPastBounds = (!index && delta.x > 0) || // if first slide and slide amt is greater than 0
|
|
|
8311 |
(index == slides.length - 1 && delta.x < 0); // or if last slide and slide amt is less than 0
|
|
|
8312 |
|
|
|
8313 |
if (options.continuous) isPastBounds = false;
|
|
|
8314 |
|
|
|
8315 |
// determine direction of swipe (true:right, false:left)
|
|
|
8316 |
var direction = delta.x < 0;
|
|
|
8317 |
|
|
|
8318 |
// if not scrolling vertically
|
|
|
8319 |
if (!isScrolling) {
|
|
|
8320 |
|
|
|
8321 |
if (isValidSlide && !isPastBounds) {
|
|
|
8322 |
|
|
|
8323 |
if (direction) {
|
|
|
8324 |
|
|
|
8325 |
if (options.continuous) { // we need to get the next in this direction in place
|
|
|
8326 |
|
|
|
8327 |
move(circle(index - 1), -width, 0);
|
|
|
8328 |
move(circle(index + 2), width, 0);
|
|
|
8329 |
|
|
|
8330 |
} else {
|
|
|
8331 |
move(index - 1, -width, 0);
|
|
|
8332 |
}
|
|
|
8333 |
|
|
|
8334 |
move(index, slidePos[index] - width, speed);
|
|
|
8335 |
move(circle(index + 1), slidePos[circle(index + 1)] - width, speed);
|
|
|
8336 |
index = circle(index + 1);
|
|
|
8337 |
|
|
|
8338 |
} else {
|
|
|
8339 |
if (options.continuous) { // we need to get the next in this direction in place
|
|
|
8340 |
|
|
|
8341 |
move(circle(index + 1), width, 0);
|
|
|
8342 |
move(circle(index - 2), -width, 0);
|
|
|
8343 |
|
|
|
8344 |
} else {
|
|
|
8345 |
move(index + 1, width, 0);
|
|
|
8346 |
}
|
|
|
8347 |
|
|
|
8348 |
move(index, slidePos[index] + width, speed);
|
|
|
8349 |
move(circle(index - 1), slidePos[circle(index - 1)] + width, speed);
|
|
|
8350 |
index = circle(index - 1);
|
|
|
8351 |
|
|
|
8352 |
}
|
|
|
8353 |
|
|
|
8354 |
options.callback && options.callback(index, slides[index]);
|
|
|
8355 |
|
|
|
8356 |
} else {
|
|
|
8357 |
|
|
|
8358 |
if (options.continuous) {
|
|
|
8359 |
|
|
|
8360 |
move(circle(index - 1), -width, speed);
|
|
|
8361 |
move(index, 0, speed);
|
|
|
8362 |
move(circle(index + 1), width, speed);
|
|
|
8363 |
|
|
|
8364 |
} else {
|
|
|
8365 |
|
|
|
8366 |
move(index - 1, -width, speed);
|
|
|
8367 |
move(index, 0, speed);
|
|
|
8368 |
move(index + 1, width, speed);
|
|
|
8369 |
}
|
|
|
8370 |
|
|
|
8371 |
}
|
|
|
8372 |
|
|
|
8373 |
}
|
|
|
8374 |
|
|
|
8375 |
// kill touchmove and touchend event listeners until touchstart called again
|
|
|
8376 |
if(browser.touch) {
|
|
|
8377 |
element.removeEventListener('touchmove', events, false);
|
|
|
8378 |
element.removeEventListener('touchend', events, false);
|
|
|
8379 |
} else {
|
|
|
8380 |
element.removeEventListener('mousemove', events, false);
|
|
|
8381 |
element.removeEventListener('mouseup', events, false);
|
|
|
8382 |
document.removeEventListener('mouseup', events, false);
|
|
|
8383 |
}
|
|
|
8384 |
|
|
|
8385 |
options.onDragEnd && options.onDragEnd();
|
|
|
8386 |
},
|
|
|
8387 |
transitionEnd: function(event) {
|
|
|
8388 |
|
|
|
8389 |
if (parseInt(event.target.getAttribute('data-index'), 10) == index) {
|
|
|
8390 |
|
|
|
8391 |
if (delay) begin();
|
|
|
8392 |
|
|
|
8393 |
options.transitionEnd && options.transitionEnd.call(event, index, slides[index]);
|
|
|
8394 |
|
|
|
8395 |
}
|
|
|
8396 |
|
|
|
8397 |
}
|
|
|
8398 |
|
|
|
8399 |
};
|
|
|
8400 |
|
|
|
8401 |
// Public API
|
|
|
8402 |
this.update = function() {
|
|
|
8403 |
setTimeout(setup);
|
|
|
8404 |
};
|
|
|
8405 |
this.setup = function() {
|
|
|
8406 |
setup();
|
|
|
8407 |
};
|
|
|
8408 |
|
|
|
8409 |
this.loop = function(value) {
|
|
|
8410 |
if (arguments.length) options.continuous = !!value;
|
|
|
8411 |
return options.continuous;
|
|
|
8412 |
};
|
|
|
8413 |
|
|
|
8414 |
this.enableSlide = function(shouldEnable) {
|
|
|
8415 |
if (arguments.length) {
|
|
|
8416 |
this.slideIsDisabled = !shouldEnable;
|
|
|
8417 |
}
|
|
|
8418 |
return !this.slideIsDisabled;
|
|
|
8419 |
};
|
|
|
8420 |
|
|
|
8421 |
this.slide = this.select = function(to, speed) {
|
|
|
8422 |
// cancel slideshow
|
|
|
8423 |
stop();
|
|
|
8424 |
|
|
|
8425 |
slide(to, speed);
|
|
|
8426 |
};
|
|
|
8427 |
|
|
|
8428 |
this.prev = this.previous = function() {
|
|
|
8429 |
// cancel slideshow
|
|
|
8430 |
stop();
|
|
|
8431 |
|
|
|
8432 |
prev();
|
|
|
8433 |
};
|
|
|
8434 |
|
|
|
8435 |
this.next = function() {
|
|
|
8436 |
// cancel slideshow
|
|
|
8437 |
stop();
|
|
|
8438 |
|
|
|
8439 |
next();
|
|
|
8440 |
};
|
|
|
8441 |
|
|
|
8442 |
this.stop = function() {
|
|
|
8443 |
// cancel slideshow
|
|
|
8444 |
stop();
|
|
|
8445 |
};
|
|
|
8446 |
|
|
|
8447 |
this.start = function() {
|
|
|
8448 |
begin();
|
|
|
8449 |
};
|
|
|
8450 |
|
|
|
8451 |
this.autoPlay = function(newDelay) {
|
|
|
8452 |
if (!delay || delay < 0) {
|
|
|
8453 |
stop();
|
|
|
8454 |
} else {
|
|
|
8455 |
delay = newDelay;
|
|
|
8456 |
begin();
|
|
|
8457 |
}
|
|
|
8458 |
};
|
|
|
8459 |
|
|
|
8460 |
this.currentIndex = this.selected = function() {
|
|
|
8461 |
// return current index position
|
|
|
8462 |
return index;
|
|
|
8463 |
};
|
|
|
8464 |
|
|
|
8465 |
this.slidesCount = this.count = function() {
|
|
|
8466 |
// return total number of slides
|
|
|
8467 |
return length;
|
|
|
8468 |
};
|
|
|
8469 |
|
|
|
8470 |
this.kill = function() {
|
|
|
8471 |
// cancel slideshow
|
|
|
8472 |
stop();
|
|
|
8473 |
|
|
|
8474 |
// reset element
|
|
|
8475 |
element.style.width = '';
|
|
|
8476 |
element.style.left = '';
|
|
|
8477 |
|
|
|
8478 |
// reset slides so no refs are held on to
|
|
|
8479 |
slides && (slides = []);
|
|
|
8480 |
|
|
|
8481 |
// removed event listeners
|
|
|
8482 |
if (browser.addEventListener) {
|
|
|
8483 |
|
|
|
8484 |
// remove current event listeners
|
|
|
8485 |
element.removeEventListener('touchstart', events, false);
|
|
|
8486 |
element.removeEventListener('webkitTransitionEnd', events, false);
|
|
|
8487 |
element.removeEventListener('msTransitionEnd', events, false);
|
|
|
8488 |
element.removeEventListener('oTransitionEnd', events, false);
|
|
|
8489 |
element.removeEventListener('otransitionend', events, false);
|
|
|
8490 |
element.removeEventListener('transitionend', events, false);
|
|
|
8491 |
window.removeEventListener('resize', events, false);
|
|
|
8492 |
|
|
|
8493 |
}
|
|
|
8494 |
else {
|
|
|
8495 |
|
|
|
8496 |
window.onresize = null;
|
|
|
8497 |
|
|
|
8498 |
}
|
|
|
8499 |
};
|
|
|
8500 |
|
|
|
8501 |
this.load = function() {
|
|
|
8502 |
// trigger setup
|
|
|
8503 |
setup();
|
|
|
8504 |
|
|
|
8505 |
// start auto slideshow if applicable
|
|
|
8506 |
if (delay) begin();
|
|
|
8507 |
|
|
|
8508 |
|
|
|
8509 |
// add event listeners
|
|
|
8510 |
if (browser.addEventListener) {
|
|
|
8511 |
|
|
|
8512 |
// set touchstart event on element
|
|
|
8513 |
if (browser.touch) {
|
|
|
8514 |
element.addEventListener('touchstart', events, false);
|
|
|
8515 |
} else {
|
|
|
8516 |
element.addEventListener('mousedown', events, false);
|
|
|
8517 |
}
|
|
|
8518 |
|
|
|
8519 |
if (browser.transitions) {
|
|
|
8520 |
element.addEventListener('webkitTransitionEnd', events, false);
|
|
|
8521 |
element.addEventListener('msTransitionEnd', events, false);
|
|
|
8522 |
element.addEventListener('oTransitionEnd', events, false);
|
|
|
8523 |
element.addEventListener('otransitionend', events, false);
|
|
|
8524 |
element.addEventListener('transitionend', events, false);
|
|
|
8525 |
}
|
|
|
8526 |
|
|
|
8527 |
// set resize event on window
|
|
|
8528 |
window.addEventListener('resize', events, false);
|
|
|
8529 |
|
|
|
8530 |
} else {
|
|
|
8531 |
|
|
|
8532 |
window.onresize = function () { setup(); }; // to play nice with old IE
|
|
|
8533 |
|
|
|
8534 |
}
|
|
|
8535 |
};
|
|
|
8536 |
|
|
|
8537 |
}
|
|
|
8538 |
});
|
|
|
8539 |
|
|
|
8540 |
})(ionic);
|
|
|
8541 |
|
|
|
8542 |
(function(ionic) {
|
|
|
8543 |
'use strict';
|
|
|
8544 |
|
|
|
8545 |
ionic.views.Toggle = ionic.views.View.inherit({
|
|
|
8546 |
initialize: function(opts) {
|
|
|
8547 |
var self = this;
|
|
|
8548 |
|
|
|
8549 |
this.el = opts.el;
|
|
|
8550 |
this.checkbox = opts.checkbox;
|
|
|
8551 |
this.track = opts.track;
|
|
|
8552 |
this.handle = opts.handle;
|
|
|
8553 |
this.openPercent = -1;
|
|
|
8554 |
this.onChange = opts.onChange || function() {};
|
|
|
8555 |
|
|
|
8556 |
this.triggerThreshold = opts.triggerThreshold || 20;
|
|
|
8557 |
|
|
|
8558 |
this.dragStartHandler = function(e) {
|
|
|
8559 |
self.dragStart(e);
|
|
|
8560 |
};
|
|
|
8561 |
this.dragHandler = function(e) {
|
|
|
8562 |
self.drag(e);
|
|
|
8563 |
};
|
|
|
8564 |
this.holdHandler = function(e) {
|
|
|
8565 |
self.hold(e);
|
|
|
8566 |
};
|
|
|
8567 |
this.releaseHandler = function(e) {
|
|
|
8568 |
self.release(e);
|
|
|
8569 |
};
|
|
|
8570 |
|
|
|
8571 |
this.dragStartGesture = ionic.onGesture('dragstart', this.dragStartHandler, this.el);
|
|
|
8572 |
this.dragGesture = ionic.onGesture('drag', this.dragHandler, this.el);
|
|
|
8573 |
this.dragHoldGesture = ionic.onGesture('hold', this.holdHandler, this.el);
|
|
|
8574 |
this.dragReleaseGesture = ionic.onGesture('release', this.releaseHandler, this.el);
|
|
|
8575 |
},
|
|
|
8576 |
|
|
|
8577 |
destroy: function() {
|
|
|
8578 |
ionic.offGesture(this.dragStartGesture, 'dragstart', this.dragStartGesture);
|
|
|
8579 |
ionic.offGesture(this.dragGesture, 'drag', this.dragGesture);
|
|
|
8580 |
ionic.offGesture(this.dragHoldGesture, 'hold', this.holdHandler);
|
|
|
8581 |
ionic.offGesture(this.dragReleaseGesture, 'release', this.releaseHandler);
|
|
|
8582 |
},
|
|
|
8583 |
|
|
|
8584 |
tap: function() {
|
|
|
8585 |
if(this.el.getAttribute('disabled') !== 'disabled') {
|
|
|
8586 |
this.val( !this.checkbox.checked );
|
|
|
8587 |
}
|
|
|
8588 |
},
|
|
|
8589 |
|
|
|
8590 |
dragStart: function(e) {
|
|
|
8591 |
if(this.checkbox.disabled) return;
|
|
|
8592 |
|
|
|
8593 |
this._dragInfo = {
|
|
|
8594 |
width: this.el.offsetWidth,
|
|
|
8595 |
left: this.el.offsetLeft,
|
|
|
8596 |
right: this.el.offsetLeft + this.el.offsetWidth,
|
|
|
8597 |
triggerX: this.el.offsetWidth / 2,
|
|
|
8598 |
initialState: this.checkbox.checked
|
|
|
8599 |
};
|
|
|
8600 |
|
|
|
8601 |
// Stop any parent dragging
|
|
|
8602 |
e.gesture.srcEvent.preventDefault();
|
|
|
8603 |
|
|
|
8604 |
// Trigger hold styles
|
|
|
8605 |
this.hold(e);
|
|
|
8606 |
},
|
|
|
8607 |
|
|
|
8608 |
drag: function(e) {
|
|
|
8609 |
var self = this;
|
|
|
8610 |
if(!this._dragInfo) { return; }
|
|
|
8611 |
|
|
|
8612 |
// Stop any parent dragging
|
|
|
8613 |
e.gesture.srcEvent.preventDefault();
|
|
|
8614 |
|
|
|
8615 |
ionic.requestAnimationFrame(function () {
|
|
|
8616 |
if (!self._dragInfo) { return; }
|
|
|
8617 |
|
|
|
8618 |
var px = e.gesture.touches[0].pageX - self._dragInfo.left;
|
|
|
8619 |
var mx = self._dragInfo.width - self.triggerThreshold;
|
|
|
8620 |
|
|
|
8621 |
// The initial state was on, so "tend towards" on
|
|
|
8622 |
if(self._dragInfo.initialState) {
|
|
|
8623 |
if(px < self.triggerThreshold) {
|
|
|
8624 |
self.setOpenPercent(0);
|
|
|
8625 |
} else if(px > self._dragInfo.triggerX) {
|
|
|
8626 |
self.setOpenPercent(100);
|
|
|
8627 |
}
|
|
|
8628 |
} else {
|
|
|
8629 |
// The initial state was off, so "tend towards" off
|
|
|
8630 |
if(px < self._dragInfo.triggerX) {
|
|
|
8631 |
self.setOpenPercent(0);
|
|
|
8632 |
} else if(px > mx) {
|
|
|
8633 |
self.setOpenPercent(100);
|
|
|
8634 |
}
|
|
|
8635 |
}
|
|
|
8636 |
});
|
|
|
8637 |
},
|
|
|
8638 |
|
|
|
8639 |
endDrag: function() {
|
|
|
8640 |
this._dragInfo = null;
|
|
|
8641 |
},
|
|
|
8642 |
|
|
|
8643 |
hold: function() {
|
|
|
8644 |
this.el.classList.add('dragging');
|
|
|
8645 |
},
|
|
|
8646 |
release: function(e) {
|
|
|
8647 |
this.el.classList.remove('dragging');
|
|
|
8648 |
this.endDrag(e);
|
|
|
8649 |
},
|
|
|
8650 |
|
|
|
8651 |
|
|
|
8652 |
setOpenPercent: function(openPercent) {
|
|
|
8653 |
// only make a change if the new open percent has changed
|
|
|
8654 |
if(this.openPercent < 0 || (openPercent < (this.openPercent - 3) || openPercent > (this.openPercent + 3) ) ) {
|
|
|
8655 |
this.openPercent = openPercent;
|
|
|
8656 |
|
|
|
8657 |
if(openPercent === 0) {
|
|
|
8658 |
this.val(false);
|
|
|
8659 |
} else if(openPercent === 100) {
|
|
|
8660 |
this.val(true);
|
|
|
8661 |
} else {
|
|
|
8662 |
var openPixel = Math.round( (openPercent / 100) * this.track.offsetWidth - (this.handle.offsetWidth) );
|
|
|
8663 |
openPixel = (openPixel < 1 ? 0 : openPixel);
|
|
|
8664 |
this.handle.style[ionic.CSS.TRANSFORM] = 'translate3d(' + openPixel + 'px,0,0)';
|
|
|
8665 |
}
|
|
|
8666 |
}
|
|
|
8667 |
},
|
|
|
8668 |
|
|
|
8669 |
val: function(value) {
|
|
|
8670 |
if(value === true || value === false) {
|
|
|
8671 |
if(this.handle.style[ionic.CSS.TRANSFORM] !== "") {
|
|
|
8672 |
this.handle.style[ionic.CSS.TRANSFORM] = "";
|
|
|
8673 |
}
|
|
|
8674 |
this.checkbox.checked = value;
|
|
|
8675 |
this.openPercent = (value ? 100 : 0);
|
|
|
8676 |
this.onChange && this.onChange();
|
|
|
8677 |
}
|
|
|
8678 |
return this.checkbox.checked;
|
|
|
8679 |
}
|
|
|
8680 |
|
|
|
8681 |
});
|
|
|
8682 |
|
|
|
8683 |
})(ionic);
|
|
|
8684 |
|
|
|
8685 |
})();
|