define([
'crossroads',
'module',
'core/js/api/ListenerBuilder',
'core/js/api/router/RouterState',
'pages/js/api/NavigationProvider'
], function(
crossroads,
module,
ListenerBuilder,
RouterState,
NavigationProvider
) {
'use strict';
/**
* Base Router class inherited by RT and DT routers.
*
* @returns {AbstractRouter}
*/
var AbstractRouter = function() {
AbcsLib.checkThis(this);
var self = this;
//observers
self._observers = {};
//initialization promise
self._initializationPromiseFulfil;
self._initializationPromise = new Promise(function(fulfil) {
self._initializationPromiseFulfil = fulfil;
});
//stores temporary the last state
self.__latestPageState;
};
AbstractRouter._LOGGER = Logger.get(module.id);
AbstractRouter._NAVIGATION_CHANGE_HANDLER = function(event) {
var state = event.state;
if (state !== null && state !== undefined) {
// call the state restoration implementation
this.__restoreRouterState(state);
} else {
this.__navigateTo(AbstractRouter._getCurrentHash());
}
};
/**
* Gets the promise which is fulfiled once the application routing is successfully initialized.
*
* @returns {Promise}
*/
AbstractRouter.prototype.getInitializationPromise = function() {
return this._initializationPromise;
};
/**
* Starts the initialization process of the Routing service.
*/
AbstractRouter.prototype.initialise = function() {
// our routing is stateless
crossroads.ignoreState = true;
// initialize routers/routes and listeners
this._initializeListeners();
this.__initialiseRoutes();
this.__postInitialise();
};
/**
* Registers a RouterStateObserver.
* <p>
* Such an observer is called during the route's push-, replace-
* state or before the state is used for restoration of UI context.
*
* @param {String} id identifier of the observer to be registered
* @param {RouterStateObserver} observer observer to be registered into the router
*/
AbstractRouter.prototype.registerRouterStateObserver = function(id, observer) {
AbcsLib.checkParameterCount(arguments, 2, 0);
AbcsLib.checkDefined(id, 'id');
AbcsLib.checkDefined(observer, 'observer');
var existingObserver = this._observers[id];
if (existingObserver && existingObserver !== observer) {
AbstractRouter._LOGGER.warn('RouterStateObserver with id "' + id + '" is already registered.');
return false;
}
this._observers[id] = observer;
return true;
};
/**
* Unregisters previously registered RouterStateObserver from the Router.
*
* @param {String} id RouterStateObserver's identifier
*/
AbstractRouter.prototype.unregisterRouterStateObserver = function(id) {
AbcsLib.checkDefined(id, 'id');
if (this._observers[id]) {
delete this._observers[id];
return true;
}
return false;
};
/**
* Stores the new state of the router.
* <p>
* It changes the browser's address bar and saves the state.
*
* @param {RouterState} state state to store
*/
AbstractRouter.prototype.__storeRouterState = function(state) {
this.__latestPageState = AbcsLib.clone(state);
for (var i in this._observers) {
this._observers[i].pushState(this.__latestPageState);
}
};
/**
* Updates the lastest state of the router.
* <p>
* It can change the browser's address bar and replaces the latest state.
*
* @param {RouterState} state state to use as replacement
*/
AbstractRouter.prototype.__updateRouterState = function(state) {
for (var i in this._observers) {
this._observers[i].updateState(AbcsLib.clone(state));
}
};
// XXX - call the beforeRestoreState from the AbstractRouter directly
AbstractRouter.prototype.__handleBeforeRestoreState = function(state, viewModel) {
// call observers first
for (var i in this._observers) {
this._observers[i].beforeRestoreState(state, viewModel);
}
};
/**
* Restores the given router state into submodule's configurations.
*
* @param {String} state state to restore within ABCS
*/
AbstractRouter.prototype.__restoreRouterState = function(/*state*/) {
AbcsLib.throwMustBeOverriddenError('Implementation of the Router needs provide state restoration method.');
};
AbstractRouter.prototype.__initialiseRoutes = function() {
AbcsLib.throwMustBeOverriddenError('Implementation of the Router needs to introduce/register its own routes.');
};
/**
* Calls the crossroad routing to invoke callbacks registered for given path.
*
* @param {String} path to navigate to (calls callback bound to that path)
*/
AbstractRouter.prototype.__navigateTo = function(path) {
// treat the beggining hash if any
path = path.indexOf('#') === 0 ? path.substring(1) : path;
crossroads.parse(path);
AbstractRouter._LOGGER.debug('Navigation changed to \'' + path + '\'');
};
AbstractRouter.__registerNewRouter = function(isTopLevelRouter) {
var router = crossroads.create();
router.ignoreState = true;
//top level routers are piped with the main crossroad's one
if (isTopLevelRouter) {
crossroads.pipe(router);
}
return router;
};
AbstractRouter.__registerPagesRouter = function(isTopLevelRouter, pageActivatedCallback) {
var pagesRouter = AbstractRouter.__registerNewRouter(isTopLevelRouter);
pagesRouter.addRoute('{page}/:rest*:{?qs}').matched.add(pageActivatedCallback);
pagesRouter.addRoute('{page}/:rest*:').matched.add(pageActivatedCallback);
return pagesRouter;
};
AbstractRouter.__registerUrlChangeInterceptorRouter = function(router) {
var urlChangeInterceptorRouter = AbstractRouter.__registerNewRouter(true);
var hookRoute = urlChangeInterceptorRouter.addRoute(':rest*:');
hookRoute.greedy = true;
hookRoute.matched.add(function(uri) {
var state = RouterState.create({url: uri || ''});
for (var i in router._observers) {
router._observers[i].pushState(AbcsLib.clone(state));
}
});
return urlChangeInterceptorRouter;
};
AbstractRouter.prototype.__postInitialise = function() {
if (window.history) {
var initialState = this.pathToState(location.hash);
for (var i in this._observers) {
this._observers[i].pushState(AbcsLib.clone(initialState));
}
// do the initial navigation action
var currentNavigationUrl = AbstractRouter._getCurrentHash();
this.__navigateTo(AbstractRouter.__treatPath(currentNavigationUrl));
// enable popstate and hashchange listening
window.addEventListener('popstate', AbstractRouter._NAVIGATION_CHANGE_HANDLER.bind(this));
// window.addEventListener('hashchange', AbstractRouter._NAVIGATION_CHANGE_HANDLER.bind(this));
// confirm the Router initialization as successfully done
AbstractRouter._LOGGER.info('Router was successfully initialized.');
this._initializationPromiseFulfil();
} else {
AbstractRouter._LOGGER.error('Browser doesn\'t support HTML5 history API (window.history not found).');
}
};
AbstractRouter.prototype.pathToState = function(path) {
return RouterState.create({url: path});
};
AbstractRouter.__treatPath = function(path) {
var targetUrl = '';
if (path === undefined || path.length === 0) {
targetUrl = '#/';
} else if (path.indexOf('#') === 0) {
targetUrl = path;
} else if (path.indexOf('/') !== 0) {
targetUrl = '#/' + path;
} else {
targetUrl = '#' + path;
}
return targetUrl;
};
AbstractRouter.prototype._initializeListeners = function() {
var self = this;
var navigation = NavigationProvider.getNavigation();
navigation.addListener(new ListenerBuilder(navigation.EVENT_PAGE_DEACTIVATED, function(pageId, pageViewModel) {
// update the current state during the page leave
if (pageViewModel) {
var latestState = RouterState.create({
url: location.hash ? location.hash.substring(2) : '',
pageId: pageId,
pageModelVersion: pageViewModel ? pageViewModel[navigation.PAGE_VIEWMODEL_VERSION_PROPERTY] : undefined,
contextualData: pageViewModel.getContextualData() ? pageViewModel.getContextualData().toJSON() : undefined,
bookmarkData: pageViewModel.getBookmark() ? pageViewModel.getBookmark().toJSON() : undefined,
complete: true
});
self.__updateRouterState(latestState);
}
}).build());
};
AbstractRouter._getCurrentHash = function() {
return document.location.hash;
};
return AbstractRouter;
});