/**
* Singleton for page navigation with history.
*
* All page modules should be in the directory `app/js/pages`.
* Page module name and the corresponding file name should be the same.
*
* Include module to start working:
*
* ```js
* var router = require('stb/router');
* ```
*
* Init with page modules:
*
* ```js
* router.data([
* require('./pages/init'),
* require('./pages/main'),
* require('./pages/help')
* ]);
* ```
*
* Each page has its ID. The same ID should be used in HTML.
*
* Make some page active/visible by its ID:
*
* ```js
* router.navigate('pageMain');
* ```
*
* This will hide the current page, activate the `pageMain` page and put it in the tail of the history list.
*
* All subscribers of the current and `pageMain` page will be notified with `show/hide` events.
*
* Also the router emits `navigate` event to all subscribers.
*
*
* To get to the previous active page use:
*
* ```js
* router.back();
* ```
*
* The module also has methods to parse location hash address and serialize it back:
*
* ```js
* router.parse('#pageMain/some/additional/data');
* router.stringify('pageMain', ['some', 'additional', 'data']);
* ```
*
* Direct modification of the URL address should be avoided.
* The methods `router.navigate` and `router.back` should be used instead.
*
* @module stb/router
* @author Stanislav Kalashnik <sk@infomir.eu>
* @license GNU GENERAL PUBLIC LICENSE Version 3
*/
'use strict';
var Emitter = require('./emitter'),
router;
/**
* @instance
* @type {Emitter}
*/
router = new Emitter();
/**
* Current active/visible page.
*
* @readonly
* @type {Page}
*/
router.current = null;
/**
* List of all visited pages.
*
* @readonly
* @type {Page[]}
*/
router.history = [];
/**
* List of all stored pages.
*
* @readonly
* @type {Page[]}
*/
router.pages = [];
/**
* Hash table of all pages ids with links to pages.
*
* @readonly
* @type {Object.<string, Page>}
*/
router.ids = {};
/**
* Set router data event.
*
* @event module:stb/router#init
*
* @type {Object}
* @property {Page[]} pages new page list
*/
/**
* Clear and fill the router with the given list of pages.
*
* @param {Page[]} pages list of pages to add
* @return {boolean} operation status
*
* @fires module:stb/router#init
*/
router.init = function ( pages ) {
var i, l, item;
if ( pages !== undefined ) {
if ( DEBUG ) {
if ( !Array.isArray(pages) ) { throw 'wrong pages type'; }
}
// reset page list
this.pages = [];
// apply list
this.pages = pages;
// extract ids
for ( i = 0, l = pages.length; i < l; i++ ) {
item = pages[i];
this.ids[item.id] = item;
// find the currently active page
if ( item.active ) {
this.current = item;
}
}
// there are some listeners
if ( this.events['init'] !== undefined ) {
// notify listeners
this.emit('init', {pages: pages});
}
return true;
}
return false;
};
/**
* Extract the page name and data from url hash.
*
* @param {string} hash address hash part to parse
*
* @return {{name: string, data: string[]}} parsed data
*
* @example
* router.parse('#main/some/additional/data');
* // execution result
* {name: 'main', data: ['some', 'additional', 'data']}
*/
router.parse = function ( hash ) {
var page = {
name : '',
data : []
};
// get and decode all parts
page.data = hash.split('/').map(decodeURIComponent);
// the first part is a page id
// everything else is optional path
page.name = page.data.shift().slice(1);
return page;
};
/**
* Convert the given page name and its data to url hash.
*
* @param {string} name page name
* @param {string[]} [data=[]] page additional parameters
*
* @return {string} url hash
*
* @example
* router.stringify('main', ['some', 'additional', 'data']);
* // execution result
* '#main/some/additional/data'
*/
router.stringify = function ( name, data ) {
// validation
data = Array.isArray(data) ? data : [];
// encode all parts
name = encodeURIComponent(name);
data = data.map(encodeURIComponent);
// add encoded name to the beginning
data.unshift(name);
// build an uri
return data.join('/');
};
/**
* Make the given inactive/hidden page active/visible.
* Pass some data to the page and trigger the corresponding event.
*
* @param {Page} page item to show
* @param {*} [data] data to send to page
*
* @return {boolean} operation status
*/
router.show = function ( page, data ) {
// page available and can be hidden
if ( page && !page.active ) {
// apply visibility
page.$node.classList.add('active');
page.active = true;
this.current = page;
// there are some listeners
if ( page.events['show'] !== undefined ) {
// notify listeners
page.emit('show', {page: page, data: data});
}
debug.log('component ' + page.constructor.name + '.' + page.id + ' show', 'green');
return true;
}
// nothing was done
return false;
};
/**
* Make the given active/visible page inactive/hidden and trigger the corresponding event.
*
* @param {Page} page item to hide
*
* @return {boolean} operation status
*/
router.hide = function ( page ) {
// page available and can be hidden
if ( page && page.active ) {
// apply visibility
page.$node.classList.remove('active');
page.active = false;
this.current = null;
// there are some listeners
if ( page.events['hide'] !== undefined ) {
// notify listeners
page.emit('hide', {page: page});
}
debug.log('component ' + page.constructor.name + '.' + page.id + ' hide', 'grey');
return true;
}
// nothing was done
return false;
};
/**
* Browse to a page with the given name.
* Do nothing if the name is invalid. Otherwise hide the current, show new and update history.
*
* @param {string} name page id
* @param {Array} [data] options to pass to the page on show
*
* @return {boolean} operation status
*/
router.navigate = function ( name, data ) {
var pageFrom = this.current,
pageTo = this.ids[name];
if ( DEBUG ) {
if ( router.pages.length > 0 ) {
if ( !pageTo || typeof pageTo !== 'object' ) { throw 'wrong pageTo type'; }
if ( !('active' in pageTo) ) { throw 'missing field "active" in pageTo'; }
}
}
// valid not already active page
if ( pageTo && !pageTo.active ) {
debug.log('router.navigate: ' + name, pageTo === pageFrom ? 'grey' : 'green');
// update url
location.hash = this.stringify(name, data);
// apply visibility
this.hide(this.current);
this.show(pageTo, data);
// there are some listeners
if ( this.events['navigate'] !== undefined ) {
// notify listeners
this.emit('navigate', {from: pageFrom, to: pageTo});
}
// store
this.history.push(pageTo);
return true;
}
debug.log('router.navigate: ' + name, 'red');
// nothing was done
return false;
};
/**
* Go back one step in the history.
* If there is no previous page, does nothing.
*
* @return {boolean} operation status
*/
router.back = function () {
var pageFrom, pageTo;
debug.log('router.back', this.history.length > 1 ? 'green' : 'red');
// there are some pages in the history
if ( this.history.length > 1 ) {
// remove the current
pageFrom = this.history.pop();
// new tail
pageTo = this.history[this.history.length - 1];
// valid not already active page
if ( pageTo && !pageTo.active ) {
// update url
location.hash = pageTo.id;
// apply visibility
this.hide(this.current);
this.show(pageTo);
// there are some listeners
if ( this.events['navigate'] !== undefined ) {
// notify listeners
this.emit('navigate', {from: pageFrom, to: pageTo});
}
return true;
}
}
// nothing was done
return false;
};
// public
module.exports = router;