-
Notifications
You must be signed in to change notification settings - Fork 133
Expand file tree
/
Copy pathmobile-nav.js
More file actions
351 lines (311 loc) · 11.3 KB
/
mobile-nav.js
File metadata and controls
351 lines (311 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
/*
* angular-mobile-nav by Andy Joslin
* http://github.com/ajoslin/angular-mobile-nav
* @license MIT License http://goo.gl/Z8Nlo
*/
angular.module('ajoslin.mobile-navigate', [])
.run(['$navigate', '$rootScope', function($navigate, $rootScope) {
//Android back button functionality for phonegap
document.addEventListener("deviceready", function() {
document.addEventListener("backbutton", function() {
$rootScope.$apply(function() {
var backSuccess = $navigate.back();
if (!backSuccess) {
navigator.app.exitApp();
}
});
});
});
}]);
/*
* $change
* Service to transition between two elements
*/
angular.module('ajoslin.mobile-navigate')
.provider('$change', function() {
var transitionPresets = { //[nextClass, prevClass]
//Modal: new page pops up, old page sits there until new page is over it
'modal': ['modal', ''],
'none': ['', '']
};
var defaultOptions = {
'prefix': 'mb-'
};
var IN_CLASS = "in";
var OUT_CLASS = "out";
var REVERSE_CLASS = "reverse";
var DONE_CLASS = "done";
var ANIMATION_END = "animationName" in document.documentElement.style ? "animationend" : "webkitAnimationEnd";
this.setTransitionPreset = function(transitionName, inClass, outClass) {
inClass = inClass || '';
outClass = outClass || inClass; //Default to outClass same as inClass
transitionPresets[transitionName] = [inClass, outClass];
};
this.options = function(opts) {
defaultOptions = angular.extend(defaultOptions, opts || {});
};
this.$get = ['$q', '$rootScope', function($q, $rootScope) {
return function change(next, prev, transType, reverse, options) {
options = angular.extend(options || {}, defaultOptions);
var deferred = $q.defer(),
nextTransClass, prevTransClass;
//buildClassString
//Transforms array of classes into prefixed class string
//(better for performance than multiple .addClass()
//@param classes: Array{string}
//@return string classNames
function buildClassString(classes) {
return classes.reduce(function(accumulator, cls) {
return accumulator + (cls ? (' ' + options.prefix + cls) : '');
}, '');
}
//Convert a preset (eg 'modal') to its array of preset classes if it exists
//else, just convert eg 'slide' to ['slide', 'slide'], so both elements get it
//The array layout is [nextinationClass, prevClass]
var transition = transitionPresets[transType] ?
transitionPresets[transType] :
[transType, transType];
//Hack for white flash: z-index stops flash, offsetWidth thing forces z-index to apply
next.css('z-index','-100');
next[0].offsetWidth += 0;
var nextClasses = buildClassString([
reverse ? OUT_CLASS : IN_CLASS,
(nextTransClass = transition[reverse ? 1 : 0]),
reverse && REVERSE_CLASS || ''
]);
next.addClass(nextClasses);
var prevClasses;
if (prev) {
prevClasses = buildClassString([
reverse ? IN_CLASS : OUT_CLASS,
(prevTransClass = transition[reverse ? 0 : 1]),
reverse && REVERSE_CLASS || ''
]);
prev.addClass(prevClasses);
}
next.css('z-index', '');
next[0].offsetWidth += 0;
function done() {
$rootScope.$apply(function() {
deferred.resolve();
});
}
//Find which element (sometimes none) to bind for ending
var boundElement;
if (nextTransClass && nextTransClass.length) {
(boundElement = next).bind(ANIMATION_END, done);
} else if (prev && prevTransClass && prevTransClass.length) {
(boundElement = prev).bind(ANIMATION_END, done);
} else {
deferred.resolve();
}
deferred.promise.then(function() {
boundElement && boundElement.unbind(ANIMATION_END, done);
next.removeClass(nextClasses);
prev && prev.removeClass(prevClasses);
});
//Let the user of change 'cancel' to finish transition early if they wish
deferred.promise.cancel = function() {
deferred.resolve();
};
return deferred.promise;
};
}];
});
angular.module('ajoslin.mobile-navigate')
.provider('$navigate', function() {
this.$get = ['$rootScope', '$location', '$route', function($rootScope, $location, $route) {
var nav = {},
navHistory = []; //we keep our own version of history and ignore window.history
function Page(path, transition, isReverse) {
var _path = path,
_transition = transition || 'slide',
_isReverse = isReverse,
_onceTransition;
this.transition = function() {
var trans;
if (_onceTransition) {
trans = _onceTransition;
_onceTransition = null;
} else {
trans = _transition;
}
return trans;
};
this.path = function() { return _path; };
this.reverse = function() { return _isReverse; };
//For setting a transition on a page - but only one time
//Eg say on startup, we want to transition in with 'none',
//but want to be 'slide' after that
this.transitionOnce = function(trans) {
_onceTransition = trans;
};
}
function navigate(destination, source, isBack) {
$rootScope.$broadcast('$pageTransitionStart', destination, source, isBack);
nav.current = nav.next;
}
/*
* Will listen for a route change success and call the selected callback
* Only one listen is ever active, so if you press for example
* /link1 then press back before /link1 is done, it will go listen for the back
*/
nav.onRouteSuccess = null;
//Add a default onroutesuccess for the very first page
function defaultRouteSuccess($event, next, last) {
nav.current && navHistory.push(nav.current);
nav.next = new Page($location.path());
nav.next.transitionOnce('none');
navigate(nav.next);
nav.onRouteSuccess = null;
}
$rootScope.$on('$routeChangeSuccess', function($event, next, last) {
// Only navigate if it's a valid route and it's not gonna just redirect immediately
if (!next.$$route || !next.$$route.redirectTo) {
(nav.onRouteSuccess || defaultRouteSuccess)($event, next, last);
}
});
/*
* go -transitions to new page
* @param path - new path
* @param {optional} String transition
* @param {optional} boolean isReverse, default false
*/
nav.go = function go(path, transition, isReverse) {
if (typeof transition == 'boolean') {
isReverse = transition;
transition = null;
}
$location.path(path);
//Wait for successful route change before actually doing stuff
nav.onRouteSuccess = function($event, next, last) {
nav.current && navHistory.push(nav.current);
nav.next = new Page(path, transition || (next.$$route && next.$$route.transition), isReverse);
navigate(nav.next, nav.current, false);
};
};
//Sometimes you want to erase history
nav.eraseHistory = function() {
navHistory.length = 0;
};
nav.back = function() {
if (navHistory.length > 0) {
var previous = navHistory[navHistory.length-1];
$location.path(previous.path());
nav.onRouteSuccess = function() {
navHistory.pop();
nav.next = previous;
navigate(nav.next, nav.current, true);
};
return true;
}
return false;
};
return nav;
}];
});
angular.module('ajoslin.mobile-navigate')
.directive('mobileView', ['$rootScope', '$compile', '$controller', '$route', '$change', '$q',
function($rootScope, $compile, $controller, $route, $change, $q) {
function link(scope, viewElement, attrs) {
//Insert page into dom
function insertPage(page) {
var current = $route.current,
locals = current && current.locals;
page.element = angular.element(document.createElement("div"));
page.element.html(locals.$template);
page.element.addClass('mb-page'); //always has to have page class
page.scope = scope.$new();
if (current.controller) {
locals.$scope = page.scope;
page.controller = $controller(current.controller, locals);
page.element.contents().data('$ngControllerController', page.controller);
}
$compile(page.element.contents())(page.scope);
if (locals && locals.$template) {
// only append page element if a template exists
viewElement.append(page.element);
}
page.scope.$emit('$viewContentLoaded');
page.scope.$eval(attrs.onLoad);
return page;
}
var currentTrans;
scope.$on('$pageTransitionStart', function ($event, dest, source, reverse) {
function changePage() {
var current = $route.current && $route.current.$$route || {};
var transition = reverse ? source.transition() : dest.transition();
insertPage(dest);
//If the page is marked as reverse, reverse the direction
if (dest.reverse() || current.reverse) {
reverse = !reverse;
}
function doTransition() {
var promise = $change(dest.element, (source ? source.element : null),
transition, reverse);
promise.then(function() {
if (source) {
$rootScope.$broadcast('$pageTransitionSuccess', dest, source);
source.scope.$destroy();
source.element.remove();
source = undefined;
}
});
return promise;
}
//Set next element to display: none, then wait until transition is
//ready, then show it again.
dest.element.css('display', 'none');
//Allow a deferTransition expression, which is allowed to return a promise.
//The next page will be inserted, but not transitioned in until the promise
//is fulfilled.
var deferTransitionPromise = scope.$eval(attrs.deferTransition) || $q.when();
deferTransitionPromise.cancel = function() {
cancelled = true;
//Undo display none from waiting for transition
dest.element.css('display', '');
};
var cancelled = false;
deferTransitionPromise.then(function() {
if (!cancelled) {
//Undo display none from waiting for transition
dest.element.css('display', '');
return doTransition();
}
});
return deferTransitionPromise;
}
currentTrans && currentTrans.cancel();
currentTrans = changePage(dest, source, reverse);
});
}
return {
restrict: 'EA',
link: link
};
}])
.directive('scrollable', ['$route', function($route) {
var scrollCache = {};
return {
restrict: 'EA',
link: function(scope, elm, attrs) {
var route = $route.current ? $route.current.$$route : {};
var template = route.templateUrl || route.template;
var rawElm = elm[0];
//On scope creation, see if we remembered any scroll for this templateUrl
//If we did, set it
if (template) {
//Set oldScroll after a timeout so the page has time to fully load
setTimeout(function() {
var oldScroll = scrollCache[template];
if (oldScroll) {
rawElm.scrollTop = oldScroll;
}
});
scope.$on('$destroy', function() {
scrollCache[template] = rawElm.scrollTop;
});
}
}
};
}]);