UNPKG

ionic-angular

Version:

[![Circle CI](https://circleci.com/gh/driftyco/ionic.svg?style=svg)](https://circleci.com/gh/driftyco/ionic)

602 lines (499 loc) 23.6 kB
/** * @private * TODO document */ IonicModule.factory('$ionicViewSwitcher', [ '$timeout', '$document', '$q', '$ionicClickBlock', '$ionicConfig', '$ionicNavBarDelegate', function($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDelegate) { var TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend'; var DATA_NO_CACHE = '$noCache'; var DATA_DESTROY_ELE = '$destroyEle'; var DATA_ELE_IDENTIFIER = '$eleId'; var DATA_VIEW_ACCESSED = '$accessed'; var DATA_FALLBACK_TIMER = '$fallbackTimer'; var DATA_VIEW = '$viewData'; var NAV_VIEW_ATTR = 'nav-view'; var VIEW_STATUS_ACTIVE = 'active'; var VIEW_STATUS_CACHED = 'cached'; var VIEW_STATUS_STAGED = 'stage'; var transitionCounter = 0; var nextTransition, nextDirection; ionic.transition = ionic.transition || {}; ionic.transition.isActive = false; var isActiveTimer; var cachedAttr = ionic.DomUtil.cachedAttr; var transitionPromises = []; var defaultTimeout = 1100; var ionicViewSwitcher = { create: function(navViewCtrl, viewLocals, enteringView, leavingView, renderStart, renderEnd) { // get a reference to an entering/leaving element if they exist // loop through to see if the view is already in the navViewElement var enteringEle, leavingEle; var transitionId = ++transitionCounter; var alreadyInDom; var switcher = { init: function(registerData, callback) { ionicViewSwitcher.isTransitioning(true); switcher.loadViewElements(registerData); switcher.render(registerData, function() { callback && callback(); }); }, loadViewElements: function(registerData) { var x, l, viewEle; var viewElements = navViewCtrl.getViewElements(); var enteringEleIdentifier = getViewElementIdentifier(viewLocals, enteringView); var navViewActiveEleId = navViewCtrl.activeEleId(); for (x = 0, l = viewElements.length; x < l; x++) { viewEle = viewElements.eq(x); if (viewEle.data(DATA_ELE_IDENTIFIER) === enteringEleIdentifier) { // we found an existing element in the DOM that should be entering the view if (viewEle.data(DATA_NO_CACHE)) { // the existing element should not be cached, don't use it viewEle.data(DATA_ELE_IDENTIFIER, enteringEleIdentifier + ionic.Utils.nextUid()); viewEle.data(DATA_DESTROY_ELE, true); } else { enteringEle = viewEle; } } else if (isDefined(navViewActiveEleId) && viewEle.data(DATA_ELE_IDENTIFIER) === navViewActiveEleId) { leavingEle = viewEle; } if (enteringEle && leavingEle) break; } alreadyInDom = !!enteringEle; if (!alreadyInDom) { // still no existing element to use // create it using existing template/scope/locals enteringEle = registerData.ele || ionicViewSwitcher.createViewEle(viewLocals); // existing elements in the DOM are looked up by their state name and state id enteringEle.data(DATA_ELE_IDENTIFIER, enteringEleIdentifier); } if (renderEnd) { navViewCtrl.activeEleId(enteringEleIdentifier); } registerData.ele = null; }, render: function(registerData, callback) { if (alreadyInDom) { // it was already found in the DOM, just reconnect the scope ionic.Utils.reconnectScope(enteringEle.scope()); } else { // the entering element is not already in the DOM // set that the entering element should be "staged" and its // styles of where this element will go before it hits the DOM navViewAttr(enteringEle, VIEW_STATUS_STAGED); var enteringData = getTransitionData(viewLocals, enteringEle, registerData.direction, enteringView); var transitionFn = $ionicConfig.transitions.views[enteringData.transition] || $ionicConfig.transitions.views.none; transitionFn(enteringEle, null, enteringData.direction, true).run(0); enteringEle.data(DATA_VIEW, { viewId: enteringData.viewId, historyId: enteringData.historyId, stateName: enteringData.stateName, stateParams: enteringData.stateParams }); // if the current state has cache:false // or the element has cache-view="false" attribute if (viewState(viewLocals).cache === false || viewState(viewLocals).cache === 'false' || enteringEle.attr('cache-view') == 'false' || $ionicConfig.views.maxCache() === 0) { enteringEle.data(DATA_NO_CACHE, true); } // append the entering element to the DOM, create a new scope and run link var viewScope = navViewCtrl.appendViewElement(enteringEle, viewLocals); delete enteringData.direction; delete enteringData.transition; viewScope.$emit('$ionicView.loaded', enteringData); } // update that this view was just accessed enteringEle.data(DATA_VIEW_ACCESSED, Date.now()); callback && callback(); }, transition: function(direction, enableBack, allowAnimate) { var deferred; var enteringData = getTransitionData(viewLocals, enteringEle, direction, enteringView); var leavingData = extend(extend({}, enteringData), getViewData(leavingView)); enteringData.transitionId = leavingData.transitionId = transitionId; enteringData.fromCache = !!alreadyInDom; enteringData.enableBack = !!enableBack; enteringData.renderStart = renderStart; enteringData.renderEnd = renderEnd; cachedAttr(enteringEle.parent(), 'nav-view-transition', enteringData.transition); cachedAttr(enteringEle.parent(), 'nav-view-direction', enteringData.direction); // cancel any previous transition complete fallbacks $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER)); // get the transition ready and see if it'll animate var transitionFn = $ionicConfig.transitions.views[enteringData.transition] || $ionicConfig.transitions.views.none; var viewTransition = transitionFn(enteringEle, leavingEle, enteringData.direction, enteringData.shouldAnimate && allowAnimate && renderEnd); if (viewTransition.shouldAnimate) { // attach transitionend events (and fallback timer) enteringEle.on(TRANSITIONEND_EVENT, completeOnTransitionEnd); enteringEle.data(DATA_FALLBACK_TIMER, $timeout(transitionComplete, defaultTimeout)); $ionicClickBlock.show(defaultTimeout); } if (renderStart) { // notify the views "before" the transition starts switcher.emit('before', enteringData, leavingData); // stage entering element, opacity 0, no transition duration navViewAttr(enteringEle, VIEW_STATUS_STAGED); // render the elements in the correct location for their starting point viewTransition.run(0); } if (renderEnd) { // create a promise so we can keep track of when all transitions finish // only required if this transition should complete deferred = $q.defer(); transitionPromises.push(deferred.promise); } if (renderStart && renderEnd) { // CSS "auto" transitioned, not manually transitioned // wait a frame so the styles apply before auto transitioning $timeout(function() { ionic.requestAnimationFrame(onReflow); }); } else if (!renderEnd) { // just the start of a manual transition // but it will not render the end of the transition navViewAttr(enteringEle, 'entering'); navViewAttr(leavingEle, 'leaving'); // return the transition run method so each step can be ran manually return { run: viewTransition.run, cancel: function(shouldAnimate) { if (shouldAnimate) { enteringEle.on(TRANSITIONEND_EVENT, cancelOnTransitionEnd); enteringEle.data(DATA_FALLBACK_TIMER, $timeout(cancelTransition, defaultTimeout)); $ionicClickBlock.show(defaultTimeout); } else { cancelTransition(); } viewTransition.shouldAnimate = shouldAnimate; viewTransition.run(0); viewTransition = null; } }; } else if (renderEnd) { // just the end of a manual transition // happens after the manual transition has completed // and a full history change has happened onReflow(); } function onReflow() { // remove that we're staging the entering element so it can auto transition navViewAttr(enteringEle, viewTransition.shouldAnimate ? 'entering' : VIEW_STATUS_ACTIVE); navViewAttr(leavingEle, viewTransition.shouldAnimate ? 'leaving' : VIEW_STATUS_CACHED); // start the auto transition and let the CSS take over viewTransition.run(1); // trigger auto transitions on the associated nav bars $ionicNavBarDelegate._instances.forEach(function(instance) { instance.triggerTransitionStart(transitionId); }); if (!viewTransition.shouldAnimate) { // no animated auto transition transitionComplete(); } } // Make sure that transitionend events bubbling up from children won't fire // transitionComplete. Will only go forward if ev.target == the element listening. function completeOnTransitionEnd(ev) { if (ev.target !== this) return; transitionComplete(); } function transitionComplete() { if (transitionComplete.x) return; transitionComplete.x = true; enteringEle.off(TRANSITIONEND_EVENT, completeOnTransitionEnd); $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER)); leavingEle && $timeout.cancel(leavingEle.data(DATA_FALLBACK_TIMER)); // resolve that this one transition (there could be many w/ nested views) deferred && deferred.resolve(navViewCtrl); // the most recent transition added has completed and all the active // transition promises should be added to the services array of promises if (transitionId === transitionCounter) { $q.all(transitionPromises).then(ionicViewSwitcher.transitionEnd); // emit that the views have finished transitioning // each parent nav-view will update which views are active and cached switcher.emit('after', enteringData, leavingData); switcher.cleanup(enteringData); } // tell the nav bars that the transition has ended $ionicNavBarDelegate._instances.forEach(function(instance) { instance.triggerTransitionEnd(); }); // remove any references that could cause memory issues nextTransition = nextDirection = enteringView = leavingView = enteringEle = leavingEle = null; } // Make sure that transitionend events bubbling up from children won't fire // transitionComplete. Will only go forward if ev.target == the element listening. function cancelOnTransitionEnd(ev) { if (ev.target !== this) return; cancelTransition(); } function cancelTransition() { navViewAttr(enteringEle, VIEW_STATUS_CACHED); navViewAttr(leavingEle, VIEW_STATUS_ACTIVE); enteringEle.off(TRANSITIONEND_EVENT, cancelOnTransitionEnd); $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER)); ionicViewSwitcher.transitionEnd([navViewCtrl]); } }, emit: function(step, enteringData, leavingData) { var enteringScope = getScopeForElement(enteringEle, enteringData); var leavingScope = getScopeForElement(leavingEle, leavingData); var prefixesAreEqual; if ( !enteringData.viewId || enteringData.abstractView ) { // it's an abstract view, so treat it accordingly // we only get access to the leaving scope once in the transition, // so dispatch all events right away if it exists if ( leavingScope ) { leavingScope.$emit('$ionicView.beforeLeave', leavingData); leavingScope.$emit('$ionicView.leave', leavingData); leavingScope.$emit('$ionicView.afterLeave', leavingData); leavingScope.$broadcast('$ionicParentView.beforeLeave', leavingData); leavingScope.$broadcast('$ionicParentView.leave', leavingData); leavingScope.$broadcast('$ionicParentView.afterLeave', leavingData); } } else { // it's a regular view, so do the normal process if (step == 'after') { if (enteringScope) { enteringScope.$emit('$ionicView.enter', enteringData); enteringScope.$broadcast('$ionicParentView.enter', enteringData); } if (leavingScope) { leavingScope.$emit('$ionicView.leave', leavingData); leavingScope.$broadcast('$ionicParentView.leave', leavingData); } else if (enteringScope && leavingData && leavingData.viewId && enteringData.stateName !== leavingData.stateName) { // we only want to dispatch this when we are doing a single-tier // state change such as changing a tab, so compare the state // for the same state-prefix but different suffix prefixesAreEqual = compareStatePrefixes(enteringData.stateName, leavingData.stateName); if ( prefixesAreEqual ) { enteringScope.$emit('$ionicNavView.leave', leavingData); } } } if (enteringScope) { enteringScope.$emit('$ionicView.' + step + 'Enter', enteringData); enteringScope.$broadcast('$ionicParentView.' + step + 'Enter', enteringData); } if (leavingScope) { leavingScope.$emit('$ionicView.' + step + 'Leave', leavingData); leavingScope.$broadcast('$ionicParentView.' + step + 'Leave', leavingData); } else if (enteringScope && leavingData && leavingData.viewId && enteringData.stateName !== leavingData.stateName) { // we only want to dispatch this when we are doing a single-tier // state change such as changing a tab, so compare the state // for the same state-prefix but different suffix prefixesAreEqual = compareStatePrefixes(enteringData.stateName, leavingData.stateName); if ( prefixesAreEqual ) { enteringScope.$emit('$ionicNavView.' + step + 'Leave', leavingData); } } } }, cleanup: function(transData) { // check if any views should be removed if (leavingEle && transData.direction == 'back' && !$ionicConfig.views.forwardCache()) { // if they just navigated back we can destroy the forward view // do not remove forward views if cacheForwardViews config is true destroyViewEle(leavingEle); } var viewElements = navViewCtrl.getViewElements(); var viewElementsLength = viewElements.length; var x, viewElement; var removeOldestAccess = (viewElementsLength - 1) > $ionicConfig.views.maxCache(); var removableEle; var oldestAccess = Date.now(); for (x = 0; x < viewElementsLength; x++) { viewElement = viewElements.eq(x); if (removeOldestAccess && viewElement.data(DATA_VIEW_ACCESSED) < oldestAccess) { // remember what was the oldest element to be accessed so it can be destroyed oldestAccess = viewElement.data(DATA_VIEW_ACCESSED); removableEle = viewElements.eq(x); } else if (viewElement.data(DATA_DESTROY_ELE) && navViewAttr(viewElement) != VIEW_STATUS_ACTIVE) { destroyViewEle(viewElement); } } destroyViewEle(removableEle); if (enteringEle.data(DATA_NO_CACHE)) { enteringEle.data(DATA_DESTROY_ELE, true); } }, enteringEle: function() { return enteringEle; }, leavingEle: function() { return leavingEle; } }; return switcher; }, transitionEnd: function(navViewCtrls) { forEach(navViewCtrls, function(navViewCtrl) { navViewCtrl.transitionEnd(); }); ionicViewSwitcher.isTransitioning(false); $ionicClickBlock.hide(); transitionPromises = []; }, nextTransition: function(val) { nextTransition = val; }, nextDirection: function(val) { nextDirection = val; }, isTransitioning: function(val) { if (arguments.length) { ionic.transition.isActive = !!val; $timeout.cancel(isActiveTimer); if (val) { isActiveTimer = $timeout(function() { ionicViewSwitcher.isTransitioning(false); }, 999); } } return ionic.transition.isActive; }, createViewEle: function(viewLocals) { var containerEle = $document[0].createElement('div'); if (viewLocals && viewLocals.$template) { containerEle.innerHTML = viewLocals.$template; if (containerEle.children.length === 1) { containerEle.children[0].classList.add('pane'); if ( viewLocals.$$state && viewLocals.$$state.self && viewLocals.$$state.self['abstract'] ) { angular.element(containerEle.children[0]).attr("abstract", "true"); } else { if ( viewLocals.$$state && viewLocals.$$state.self ) { angular.element(containerEle.children[0]).attr("state", viewLocals.$$state.self.name); } } return jqLite(containerEle.children[0]); } } containerEle.className = "pane"; return jqLite(containerEle); }, viewEleIsActive: function(viewEle, isActiveAttr) { navViewAttr(viewEle, isActiveAttr ? VIEW_STATUS_ACTIVE : VIEW_STATUS_CACHED); }, getTransitionData: getTransitionData, navViewAttr: navViewAttr, destroyViewEle: destroyViewEle }; return ionicViewSwitcher; function getViewElementIdentifier(locals, view) { if (viewState(locals)['abstract']) return viewState(locals).name; if (view) return view.stateId || view.viewId; return ionic.Utils.nextUid(); } function viewState(locals) { return locals && locals.$$state && locals.$$state.self || {}; } function getTransitionData(viewLocals, enteringEle, direction, view) { // Priority // 1) attribute directive on the button/link to this view // 2) entering element's attribute // 3) entering view's $state config property // 4) view registration data // 5) global config // 6) fallback value var state = viewState(viewLocals); var viewTransition = nextTransition || cachedAttr(enteringEle, 'view-transition') || state.viewTransition || $ionicConfig.views.transition() || 'ios'; var navBarTransition = $ionicConfig.navBar.transition(); direction = nextDirection || cachedAttr(enteringEle, 'view-direction') || state.viewDirection || direction || 'none'; return extend(getViewData(view), { transition: viewTransition, navBarTransition: navBarTransition === 'view' ? viewTransition : navBarTransition, direction: direction, shouldAnimate: (viewTransition !== 'none' && direction !== 'none') }); } function getViewData(view) { view = view || {}; return { viewId: view.viewId, historyId: view.historyId, stateId: view.stateId, stateName: view.stateName, stateParams: view.stateParams }; } function navViewAttr(ele, value) { if (arguments.length > 1) { cachedAttr(ele, NAV_VIEW_ATTR, value); } else { return cachedAttr(ele, NAV_VIEW_ATTR); } } function destroyViewEle(ele) { // we found an element that should be removed // destroy its scope, then remove the element if (ele && ele.length) { var viewScope = ele.scope(); if (viewScope) { viewScope.$emit('$ionicView.unloaded', ele.data(DATA_VIEW)); viewScope.$destroy(); } ele.remove(); } } function compareStatePrefixes(enteringStateName, exitingStateName) { var enteringStateSuffixIndex = enteringStateName.lastIndexOf('.'); var exitingStateSuffixIndex = exitingStateName.lastIndexOf('.'); // if either of the prefixes are empty, just return false if ( enteringStateSuffixIndex < 0 || exitingStateSuffixIndex < 0 ) { return false; } var enteringPrefix = enteringStateName.substring(0, enteringStateSuffixIndex); var exitingPrefix = exitingStateName.substring(0, exitingStateSuffixIndex); return enteringPrefix === exitingPrefix; } function getScopeForElement(element, stateData) { if ( !element ) { return null; } // check if it's abstract var attributeValue = angular.element(element).attr("abstract"); var stateValue = angular.element(element).attr("state"); if ( attributeValue !== "true" ) { // it's not an abstract view, so make sure the element // matches the state. Due to abstract view weirdness, // sometimes it doesn't. If it doesn't, don't dispatch events // so leave the scope undefined if ( stateValue === stateData.stateName ) { return angular.element(element).scope(); } return null; } else { // it is an abstract element, so look for element with the "state" attributeValue // set to the name of the stateData state var elements = aggregateNavViewChildren(element); for ( var i = 0; i < elements.length; i++ ) { var state = angular.element(elements[i]).attr("state"); if ( state === stateData.stateName ) { stateData.abstractView = true; return angular.element(elements[i]).scope(); } } // we didn't find a match, so return null return null; } } function aggregateNavViewChildren(element) { var aggregate = []; var navViews = angular.element(element).find("ion-nav-view"); for ( var i = 0; i < navViews.length; i++ ) { var children = angular.element(navViews[i]).children(); var childrenAggregated = []; for ( var j = 0; j < children.length; j++ ) { childrenAggregated = childrenAggregated.concat(children[j]); } aggregate = aggregate.concat(childrenAggregated); } return aggregate; } }]);