Files
ionic-framework/js/angular/service/history.js
Adam Bradley b4e4055a06 refactor(backButton): separate show/enable logic
Previously the showBack property was setting if the back button should
or should not be enabled, and it was also used if the back button
should be hidden or not for the view. Changed it so there are now two
concepts, where showBack is visual only, and enableBack determines if
it should show according to $ionicHistory and navigation info.
2014-11-19 11:38:14 -06:00

712 lines
22 KiB
JavaScript

/**
* @ngdoc service
* @name $ionicHistory
* @module ionic
* @description
* $ionicHistory is what keeps track of an app's views as the user navigates. Like browser,
* an Ionic app is able to know what the previous view was, the current view, and what the
* forward view was (if there was one). However, a typical web browser only keeps track of one
* history stack in a linear fashion.
*
* Ionic's `$ionicHistory` is able to keep track of multiple histories, and persist where the
* user is as they navigate between different views, and different histories. For example, an
* app with tabs has it's own history stack for each tab. This meaning you can navigate a few
* views in Tab A, then navigate a few in Tab B, and when you return to Tab A, the existing
* stack is maintained.
*/
IonicModule
.factory('$ionicHistory', [
'$rootScope',
'$state',
'$location',
'$window',
'$ionicViewSwitcher',
function($rootScope, $state, $location, $window, $ionicViewSwitcher) {
// history actions while navigating views
var ACTION_INITIAL_VIEW = 'initialView';
var ACTION_NEW_VIEW = 'newView';
var ACTION_MOVE_BACK = 'moveBack';
var ACTION_MOVE_FORWARD = 'moveForward';
// direction of navigation
var DIRECTION_BACK = 'back';
var DIRECTION_FORWARD = 'forward';
var DIRECTION_ENTER = 'enter';
var DIRECTION_EXIT = 'exit';
var DIRECTION_SWAP = 'swap';
var DIRECTION_NONE = 'none';
var stateChangeCounter = 0;
var lastStateId, nextViewOptions, forcedNav;
var viewHistory = {
histories: { root: { historyId: 'root', parentHistoryId: null, stack: [], cursor: -1 } },
views: {},
backView: null,
forwardView: null,
currentView: null
};
var View = function() {};
View.prototype.initialize = function(data) {
if (data) {
for (var name in data) this[name] = data[name];
return this;
}
return null;
};
View.prototype.go = function() {
if (this.stateName) {
return $state.go(this.stateName, this.stateParams);
}
if (this.url && this.url !== $location.url()) {
if (viewHistory.backView === this) {
return $window.history.go(-1);
} else if (viewHistory.forwardView === this) {
return $window.history.go(1);
}
$location.url(this.url);
return;
}
return null;
};
View.prototype.destroy = function() {
if (this.scope) {
this.scope.$destroy && this.scope.$destroy();
this.scope = null;
}
};
function getViewById(viewId) {
return (viewId ? viewHistory.views[ viewId ] : null);
}
function getBackView(view) {
return (view ? getViewById(view.backViewId) : null);
}
function getForwardView(view) {
return (view ? getViewById(view.forwardViewId) : null);
}
function getHistoryById(historyId) {
return (historyId ? viewHistory.histories[ historyId ] : null);
}
function getHistory(scope) {
var histObj = getParentHistoryObj(scope);
if (!viewHistory.histories[ histObj.historyId ]) {
// this history object exists in parent scope, but doesn't
// exist in the history data yet
viewHistory.histories[ histObj.historyId ] = {
historyId: histObj.historyId,
parentHistoryId: getParentHistoryObj(histObj.scope.$parent).historyId,
stack: [],
cursor: -1
};
}
return getHistoryById(histObj.historyId);
}
function getParentHistoryObj(scope) {
var parentScope = scope;
while (parentScope) {
if (parentScope.hasOwnProperty('$historyId')) {
// this parent scope has a historyId
return { historyId: parentScope.$historyId, scope: parentScope };
}
// nothing found keep climbing up
parentScope = parentScope.$parent;
}
// no history for for the parent, use the root
return { historyId: 'root', scope: $rootScope };
}
function setNavViews(viewId) {
viewHistory.currentView = getViewById(viewId);
viewHistory.backView = getBackView(viewHistory.currentView);
viewHistory.forwardView = getForwardView(viewHistory.currentView);
}
function getCurrentStateId() {
var id;
if ($state && $state.current && $state.current.name) {
id = $state.current.name;
if ($state.params) {
for (var key in $state.params) {
if ($state.params.hasOwnProperty(key) && $state.params[key]) {
id += "_" + key + "=" + $state.params[key];
}
}
}
return id;
}
// if something goes wrong make sure its got a unique stateId
return ionic.Utils.nextUid();
}
function getCurrentStateParams() {
var rtn;
if ($state && $state.params) {
for (var key in $state.params) {
if ($state.params.hasOwnProperty(key)) {
rtn = rtn || {};
rtn[key] = $state.params[key];
}
}
}
return rtn;
}
return {
register: function(parentScope, viewLocals) {
var currentStateId = getCurrentStateId(),
hist = getHistory(parentScope),
currentView = viewHistory.currentView,
backView = viewHistory.backView,
forwardView = viewHistory.forwardView,
viewId = null,
action = null,
direction = DIRECTION_NONE,
historyId = hist.historyId,
url = $location.url(),
tmp, x, ele;
if (lastStateId !== currentStateId) {
lastStateId = currentStateId;
stateChangeCounter++;
}
if (forcedNav) {
// we've previously set exactly what to do
viewId = forcedNav.viewId;
action = forcedNav.action;
direction = forcedNav.direction;
forcedNav = null;
} else if (backView && backView.stateId === currentStateId) {
// they went back one, set the old current view as a forward view
viewId = backView.viewId;
historyId = backView.historyId;
action = ACTION_MOVE_BACK;
if (backView.historyId === currentView.historyId) {
// went back in the same history
direction = DIRECTION_BACK;
} else if (currentView) {
direction = DIRECTION_EXIT;
tmp = getHistoryById(backView.historyId);
if (tmp && tmp.parentHistoryId === currentView.historyId) {
direction = DIRECTION_ENTER;
} else {
tmp = getHistoryById(currentView.historyId);
if (tmp && tmp.parentHistoryId === hist.parentHistoryId) {
direction = DIRECTION_SWAP;
}
}
}
} else if (forwardView && forwardView.stateId === currentStateId) {
// they went to the forward one, set the forward view to no longer a forward view
viewId = forwardView.viewId;
historyId = forwardView.historyId;
action = ACTION_MOVE_FORWARD;
if (forwardView.historyId === currentView.historyId) {
direction = DIRECTION_FORWARD;
} else if (currentView) {
direction = DIRECTION_EXIT;
if (currentView.historyId === hist.parentHistoryId) {
direction = DIRECTION_ENTER;
} else {
tmp = getHistoryById(currentView.historyId);
if (tmp && tmp.parentHistoryId === hist.parentHistoryId) {
direction = DIRECTION_SWAP;
}
}
}
tmp = getParentHistoryObj(parentScope);
if (forwardView.historyId && tmp.scope) {
// if a history has already been created by the forward view then make sure it stays the same
tmp.scope.$historyId = forwardView.historyId;
historyId = forwardView.historyId;
}
} else if (currentView && currentView.historyId !== historyId &&
hist.cursor > -1 && hist.stack.length > 0 && hist.cursor < hist.stack.length &&
hist.stack[hist.cursor].stateId === currentStateId) {
// they just changed to a different history and the history already has views in it
var switchToView = hist.stack[hist.cursor];
viewId = switchToView.viewId;
historyId = switchToView.historyId;
action = ACTION_MOVE_BACK;
direction = DIRECTION_SWAP;
tmp = getHistoryById(currentView.historyId);
if (tmp && tmp.parentHistoryId === historyId) {
direction = DIRECTION_EXIT;
} else {
tmp = getHistoryById(historyId);
if (tmp && tmp.parentHistoryId === currentView.historyId) {
direction = DIRECTION_ENTER;
}
}
// if switching to a different history, and the history of the view we're switching
// to has an existing back view from a different history than itself, then
// it's back view would be better represented using the current view as its back view
tmp = getViewById(switchToView.backViewId);
if (tmp && switchToView.historyId !== tmp.historyId) {
hist.stack[hist.cursor].backViewId = currentView.viewId;
}
} else {
// create an element from the viewLocals template
ele = $ionicViewSwitcher.createViewEle(viewLocals);
if (this.isAbstractEle(ele)) {
console.log('VIEW', 'abstractView', DIRECTION_NONE, viewHistory.currentView);
return {
action: 'abstractView',
direction: DIRECTION_NONE,
ele: ele
};
}
// set a new unique viewId
viewId = ionic.Utils.nextUid();
if (currentView) {
// set the forward view if there is a current view (ie: if its not the first view)
currentView.forwardViewId = viewId;
action = ACTION_NEW_VIEW;
// check if there is a new forward view within the same history
if (forwardView && currentView.stateId !== forwardView.stateId &&
currentView.historyId === forwardView.historyId) {
// they navigated to a new view but the stack already has a forward view
// since its a new view remove any forwards that existed
tmp = getHistoryById(forwardView.historyId);
if (tmp) {
// the forward has a history
for (x = tmp.stack.length - 1; x >= forwardView.index; x--) {
// starting from the end destroy all forwards in this history from this point
tmp.stack[x].destroy();
tmp.stack.splice(x);
}
historyId = forwardView.historyId;
}
}
// its only moving forward if its in the same history
if (hist.historyId === currentView.historyId) {
direction = DIRECTION_FORWARD;
} else if (currentView.historyId !== hist.historyId) {
direction = DIRECTION_ENTER;
tmp = getHistoryById(currentView.historyId);
if (tmp && tmp.parentHistoryId === hist.parentHistoryId) {
direction = DIRECTION_SWAP;
} else {
tmp = getHistoryById(tmp.parentHistoryId);
if (tmp && tmp.historyId === hist.historyId) {
direction = DIRECTION_EXIT;
}
}
}
} else {
// there's no current view, so this must be the initial view
action = ACTION_INITIAL_VIEW;
}
if (stateChangeCounter < 2) {
// views that were spun up on the first load should not animate
direction = DIRECTION_NONE;
}
// add the new view
viewHistory.views[viewId] = this.createView({
viewId: viewId,
index: hist.stack.length,
historyId: hist.historyId,
backViewId: (currentView && currentView.viewId ? currentView.viewId : null),
forwardViewId: null,
stateId: currentStateId,
stateName: this.currentStateName(),
stateParams: getCurrentStateParams(),
url: url
});
// add the new view to this history's stack
hist.stack.push(viewHistory.views[viewId]);
}
if (nextViewOptions) {
if (nextViewOptions.disableAnimate) direction = DIRECTION_NONE;
if (nextViewOptions.disableBack) viewHistory.views[viewId].backViewId = null;
if (nextViewOptions.historyRoot) {
for (x = 0; x < hist.stack.length; x++) {
if (hist.stack[x].viewId === viewId) {
hist.stack[x].index = 0;
hist.stack[x].backViewId = hist.stack[x].forwardViewId = null;
} else {
delete viewHistory.views[hist.stack[x].viewId];
}
}
hist.stack = [viewHistory.views[viewId]];
}
nextViewOptions = null;
}
setNavViews(viewId);
if (viewHistory.backView && historyId == viewHistory.backView.historyId && currentStateId == viewHistory.backView.stateId && url == viewHistory.backView.url) {
for (x = 0; x < hist.stack.length; x++) {
if (hist.stack[x].viewId == viewId) {
action = 'dupNav';
direction = DIRECTION_NONE;
hist.stack[x - 1].forwardViewId = viewHistory.forwardView = null;
viewHistory.currentView.index = viewHistory.backView.index;
viewHistory.currentView.backViewId = viewHistory.backView.backViewId;
viewHistory.backView = getBackView(viewHistory.backView);
hist.stack.splice(x, 1);
break;
}
}
}
console.log('VIEW', action, direction, viewHistory.currentView);
hist.cursor = viewHistory.currentView.index;
return {
viewId: viewId,
action: action,
direction: direction,
historyId: historyId,
enableBack: !!(viewHistory.backView && viewHistory.backView.historyId === viewHistory.currentView.historyId),
isHistoryRoot: (viewHistory.currentView.index === 0),
ele: ele
};
},
registerHistory: function(scope) {
scope.$historyId = ionic.Utils.nextUid();
},
createView: function(data) {
var newView = new View();
return newView.initialize(data);
},
getViewById: getViewById,
/**
* @ngdoc method
* @name $ionicHistory#viewHistory
* @description The app's view history data, such as all the views and histories, along
* with how they are ordered and linked together within the navigation stack.
* @returns {object} Returns an object containing the apps view history data.
*/
viewHistory: function() {
return viewHistory;
},
/**
* @ngdoc method
* @name $ionicHistory#currentView
* @description The app's current view.
* @returns {object} Returns the current view.
*/
currentView: function() {
return viewHistory.currentView;
},
/**
* @ngdoc method
* @name $ionicHistory#currentHistoryId
* @description The ID of the history stack which is the parent container of the current view.
* @returns {string} Returns the current history ID.
*/
currentHistoryId: function() {
return viewHistory.currentView ? viewHistory.currentView.historyId : null;
},
/**
* @ngdoc method
* @name $ionicHistory#currentTitle
* @description Gets and sets the current view's title.
* @param {string=} val The title to update the current view with.
* @returns {string} Returns the current view's title.
*/
currentTitle: function(val) {
if (viewHistory.currentView) {
if (arguments.length) {
viewHistory.currentView.title = val;
}
return viewHistory.currentView.title;
}
},
/**
* @ngdoc method
* @name $ionicHistory#backView
* @description Returns the view that was before the current view in the history stack.
* If the user navigated from View A to View B, then View A would be the back view, and
* View B would be the current view.
* @returns {string} Returns the back view.
*/
backView: function() {
return viewHistory.backView;
},
/**
* @ngdoc method
* @name $ionicHistory#backTitle
* @description Gets the back view's title.
* @returns {string} Returns the back view's title.
*/
backTitle: function() {
if (viewHistory.backView) {
return viewHistory.backView.title;
}
},
/**
* @ngdoc method
* @name $ionicHistory#forwardView
* @description Returns the view that was in front of the current view in the history stack.
* A forward view would exist if the user navigated from View A to View B, then
* navigated back to View A. At this point then View B would be the forward view, and View
* A would be the current view.
* @returns {string} Returns the back view.
*/
forwardView: function() {
return viewHistory.forwardView;
},
/**
* @ngdoc method
* @name $ionicHistory#currentStateName
* @description Returns the current state name.
* @returns {string}
*/
currentStateName: function() {
return ($state && $state.current ? $state.current.name : null);
},
isCurrentStateNavView: function(navView) {
return !!($state && $state.current && $state.current.views && $state.current.views[navView]);
},
goToHistoryRoot: function(historyId) {
if (historyId) {
var hist = getHistoryById(historyId);
if (hist && hist.stack.length) {
if (viewHistory.currentView && viewHistory.currentView.viewId === hist.stack[0].viewId) {
return;
}
forcedNav = {
viewId: hist.stack[0].viewId,
action: ACTION_MOVE_BACK,
direction: DIRECTION_BACK
};
hist.stack[0].go();
}
}
},
/**
* @ngdoc method
* @name $ionicHistory#goBack
* @description Navigates the app to the back view, if a back view exists.
*/
goBack: function() {
viewHistory.backView && viewHistory.backView.go();
},
/**
* @ngdoc method
* @name $ionicHistory#clearHistory
* @description Clears out the app's entire history, except for the current view.
*/
clearHistory: function() {
var
histories = viewHistory.histories,
currentView = viewHistory.currentView;
if (histories) {
for (var historyId in histories) {
if (histories[historyId].stack) {
histories[historyId].stack = [];
histories[historyId].cursor = -1;
}
if (currentView && currentView.historyId === historyId) {
currentView.backViewId = currentView.forwardViewId = null;
histories[historyId].stack.push(currentView);
} else if (histories[historyId].destroy) {
histories[historyId].destroy();
}
}
}
for (var viewId in viewHistory.views) {
if (viewId !== currentView.viewId) {
delete viewHistory.views[viewId];
}
}
if (currentView) {
setNavViews(currentView.viewId);
}
},
/**
* @ngdoc method
* @name $ionicHistory#nextViewOptions
* @description Sets options for the next view. This method can be useful to override
* certain view/transition defaults right before a view transition happens. For example,
* the {@link ionic.directive:menuClose} directive uses this methond internally to ensure
* an animated view transition does not happen when a menu is closed, and also sets that
* the next view should become this root of its history stack. After the next view has
* entered then these options are set back to null.
*
* Available options:
*
* * `disableAnimate`: Do not animate the next transition.
* * `disableBack`: The next view should forget its back view, and set it to null.
* * `historyRoot`: The next view should become the root view in its history stack.
*
* ```js
* $ionicHistory.nextViewOptions({
* disableAnimate: true,
* disableBack: true
* });
* ```
*/
nextViewOptions: function(opts) {
if (arguments.length) {
if (opts === null) {
nextViewOptions = opts;
} else {
nextViewOptions = nextViewOptions || {};
extend(nextViewOptions, opts);
}
}
return nextViewOptions;
},
isAbstractEle: function(ele) {
return !!(ele && (isAbstractTag(ele) || isAbstractTag(ele.children())));
}
};
function isAbstractTag(ele) {
return ele && ele.length && /ion-side-menus|ion-tabs/i.test(ele[0].tagName);
}
}])
.run([
'$rootScope',
'$state',
'$location',
'$document',
'$ionicPlatform',
'$ionicHistory',
function($rootScope, $state, $location, $document, $ionicPlatform, $ionicHistory) {
// always reset the keyboard state when change stage
$rootScope.$on('$stateChangeStart', function() {
ionic.keyboard && ionic.keyboard.hide && ionic.keyboard.hide();
});
$rootScope.$on('$ionicHistory.change', function(e, data) {
if (!data) return;
var viewHistory = $ionicHistory.viewHistory();
var hist = (data.historyId ? viewHistory.histories[ data.historyId ] : null);
if (hist && hist.cursor > -1 && hist.cursor < hist.stack.length) {
// the history they're going to already exists
// go to it's last view in its stack
var view = hist.stack[ hist.cursor ];
return view.go(data);
}
// this history does not have a URL, but it does have a uiSref
// figure out its URL from the uiSref
if (!data.url && data.uiSref) {
data.url = $state.href(data.uiSref);
}
if (data.url) {
// don't let it start with a #, messes with $location.url()
if (data.url.indexOf('#') === 0) {
data.url = data.url.replace('#', '');
}
if (data.url !== $location.url()) {
// we've got a good URL, ready GO!
$location.url(data.url);
}
}
});
$rootScope.$ionicGoBack = function() {
$ionicHistory.goBack();
};
// Set the document title when a new view is shown
$rootScope.$on('$ionicView.afterEnter', function(ev, data) {
if (data && data.title) {
$document[0].title = data.title;
}
});
// Triggered when devices with a hardware back button (Android) is clicked by the user
// This is a Cordova/Phonegap platform specifc method
function onHardwareBackButton(e) {
var backView = $ionicHistory.backView();
if (backView) {
// there is a back view, go to it
backView.go();
} else {
// there is no back view, so close the app instead
ionic.Platform.exitApp();
}
e.preventDefault();
return false;
}
$ionicPlatform.registerBackButtonAction(
onHardwareBackButton,
PLATFORM_BACK_BUTTON_PRIORITY_VIEW
);
}]);