pixl-webapp
Version:
A client-side JavaScript framework, designed to be a base for web applications.
535 lines (443 loc) • 14.5 kB
JavaScript
/**
* WebApp 1.0 Page Manager
* Author: Joseph Huckaby
* Copyright (c) 2010 Joseph Huckaby
* Released under the MIT License.
**/
var Nav = {
/**
* Virtual Page Navigation System
**/
loc: '',
old_loc: '',
inited: false,
nodes: [],
init: function() {
// initialize nav system
assert( window.config, "window.config not present.");
if (!this.inited) {
this.inited = true;
this.loc = 'init';
this.monitor();
if (window.addEventListener) {
window.addEventListener("hashchange", function(event) {
Nav.monitor();
}, false);
}
else {
window.onhashchange = function() { Nav.monitor(); };
}
}
},
monitor: function() {
// monitor browser location and activate handlers as needed
var parts = window.location.href.split(/\#/);
var anchor = parts[1];
if (!anchor) anchor = config.DefaultPage || 'Main';
var full_anchor = '' + anchor;
var sub_anchor = '';
anchor = anchor.replace(/\%7C/, '|');
if (anchor.match(/\|(\w+)$/)) {
// inline section anchor after article name, pipe delimited
sub_anchor = RegExp.$1.toLowerCase();
anchor = anchor.replace(/\|(\w+)$/, '');
}
if ((anchor != this.loc) && !anchor.match(/^_/)) { // ignore doxter anchors
Debug.trace('nav', "Caught navigation anchor: " + full_anchor);
var page_name = '';
var page_args = {};
if (full_anchor.match(/^\w+\?.+/)) {
parts = full_anchor.split(/\?/);
page_name = parts[0];
page_args = parse_query_string( parts[1] );
}
else {
parts = full_anchor.split(/\//);
page_name = parts[0];
page_args = {};
}
Debug.trace('nav', "Calling page: " + page_name + ": " + JSON.stringify(page_args));
Dialog.hide();
// app.hideMessage();
var result = app.page_manager.click( page_name, page_args );
if (result) {
this.old_loc = this.loc;
if (this.old_loc == 'init') this.old_loc = config.DefaultPage || 'Main';
this.loc = anchor;
}
else {
// current page aborted navigation -- recover current page without refresh
this.go( this.loc );
}
}
else if (sub_anchor != this.sub_anchor) {
Debug.trace('nav', "Caught sub-anchor: " + sub_anchor);
$P().gosub( sub_anchor );
} // sub-anchor changed
this.sub_anchor = sub_anchor;
},
go: function(anchor, force) {
// navigate to page
anchor = anchor.replace(/^\#/, '');
if (force) {
if (anchor == this.loc) {
this.loc = 'init';
this.monitor();
}
else {
this.loc = 'init';
window.location.href = '#' + anchor;
}
}
else {
window.location.href = '#' + anchor;
}
},
prev: function() {
// return to previous page
this.go( this.old_loc || config.DefaultPage || 'Main' );
},
refresh: function() {
// re-nav to current page
this.loc = 'refresh';
this.monitor();
},
currentAnchor: function() {
// return current page anchor
var parts = window.location.href.split(/\#/);
var anchor = parts[1] || '';
var sub_anchor = '';
anchor = anchor.replace(/\%7C/, '|');
if (anchor.match(/\|(\w+)$/)) {
// inline section anchor after article name, pipe delimited
sub_anchor = RegExp.$1.toLowerCase();
anchor = anchor.replace(/\|(\w+)$/, '');
}
return anchor;
}
}; // Nav
//
// Page Base Class
//
Class.create( 'Page', {
// 'Page' class is the abstract base class for all pages
// Each web component calls this class daddy
// member variables
ID: '', // ID of DIV for component
data: null, // holds all data for freezing
active: false, // whether page is active or not
sidebar: true, // whether to show sidebar or not
// methods
__construct: function(config, div) {
if (!config) return;
// class constructor, import config into self
this.data = {};
if (!config) config = {};
for (var key in config) this[key] = config[key];
this.div = div || $('#page_' + this.ID);
assert(this.div, "Cannot find page div: page_" + this.ID);
this.tab = $('#tab_' + this.ID);
},
onInit: function() {
// called with the page is initialized
},
onActivate: function() {
// called when page is activated
return true;
},
onDeactivate: function() {
// called when page is deactivated
return true;
},
show: function() {
// show page
this.div.show();
},
hide: function() {
this.div.hide();
},
gosub: function(anchor) {
// go to sub-anchor (article section link)
},
getSidebarTabs: function(current, tabs) {
// get html for sidebar tabs
var html = '';
html += '<div style="margin-left:151px; position:relative; min-height:400px;">';
html += '<div class="side_tab_bar" style="position:absolute; left:-161px;">';
html += '<div style="height:50px;"></div>';
for (var idx = 0, len = tabs.length; idx < len; idx++) {
var tab = tabs[idx];
if (typeof(tab) == 'string') html += tab;
else {
var class_name = 'inactive';
var link = 'Nav.go(\''+this.ID+'?sub='+tab[0]+'\')';
if (tab[0] == current) {
class_name = 'active';
link = '';
}
html += '<div class="tab side '+class_name+'" onMouseUp="'+link+'"><span class="content">'+tab[1]+'</span></div>';
}
}
html += '</div>';
return html;
},
getPaginatedTable: function(resp, cols, data_type, callback) {
// get html for paginated table
// dual-calling convention: (resp, cols, data_type, callback) or (args)
var args = null;
if (arguments.length == 1) {
// custom args calling convention
args = arguments[0];
// V2 API
if (!args.resp && args.rows && args.total) {
args.resp = {
rows: args.rows,
list: { length: args.total }
};
}
}
else {
// classic calling convention
args = {
resp: arguments[0],
cols: arguments[1],
data_type: arguments[2],
callback: arguments[3],
limit: this.args.limit,
offset: this.args.offset || 0
};
}
var resp = args.resp;
var cols = args.cols;
var data_type = args.data_type;
var callback = args.callback;
var cpl = args.pagination_link || '';
var html = '';
// pagination header
html += '<div class="pagination">';
html += '<table cellspacing="0" cellpadding="0" border="0" width="100%"><tr>';
var results = {
limit: args.limit,
offset: args.offset || 0,
total: resp.list.length
};
var num_pages = Math.floor( results.total / results.limit ) + 1;
if (results.total % results.limit == 0) num_pages--;
var current_page = Math.floor( results.offset / results.limit ) + 1;
html += '<td align="left" width="33%">';
html += commify(results.total) + ' ' + pluralize(data_type, results.total) + ' found';
html += '</td>';
html += '<td align="center" width="34%">';
if (num_pages > 1) html += 'Page ' + commify(current_page) + ' of ' + commify(num_pages);
else html += ' ';
html += '</td>';
html += '<td align="right" width="33%">';
if (num_pages > 1) {
// html += 'Page: ';
if (current_page > 1) {
if (cpl) {
html += '<span class="link" onMouseUp="'+cpl+'('+Math.floor((current_page - 2) * results.limit)+')">« Prev Page</span>';
}
else {
html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
offset: (current_page - 2) * results.limit
})) + '">« Prev Page</a>';
}
}
html += ' ';
var start_page = current_page - 4;
var end_page = current_page + 5;
if (start_page < 1) {
end_page += (1 - start_page);
start_page = 1;
}
if (end_page > num_pages) {
start_page -= (end_page - num_pages);
if (start_page < 1) start_page = 1;
end_page = num_pages;
}
for (var idx = start_page; idx <= end_page; idx++) {
if (idx == current_page) {
html += '<b>' + commify(idx) + '</b>';
}
else {
if (cpl) {
html += '<span class="link" onMouseUp="'+cpl+'('+Math.floor((idx - 1) * results.limit)+')">' + commify(idx) + '</span>';
}
else {
html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
offset: (idx - 1) * results.limit
})) + '">' + commify(idx) + '</a>';
}
}
html += ' ';
}
html += ' ';
if (current_page < num_pages) {
if (cpl) {
html += '<span class="link" onMouseUp="'+cpl+'('+Math.floor((current_page + 0) * results.limit)+')">Next Page »</span>';
}
else {
html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
offset: (current_page + 0) * results.limit
})) + '">Next Page »</a>';
}
}
} // more than one page
else {
html += 'Page 1 of 1';
}
html += '</td>';
html += '</tr></table>';
html += '</div>';
html += '<div style="margin-top:5px;">';
html += '<table class="data_table" width="100%">';
html += '<tr><th>' + cols.join('</th><th>').replace(/\s+/g, ' ') + '</th></tr>';
for (var idx = 0, len = resp.rows.length; idx < len; idx++) {
var row = resp.rows[idx];
var tds = callback(row, idx);
if (tds) {
html += '<tr' + (tds.className ? (' class="'+tds.className+'"') : '') + '>';
html += '<td>' + tds.join('</td><td>') + '</td>';
html += '</tr>';
}
} // foreach row
if (!resp.rows.length) {
html += '<tr><td colspan="'+cols.length+'" align="center" style="padding-top:10px; padding-bottom:10px; font-weight:bold;">';
html += 'No '+pluralize(data_type)+' found.';
html += '</td></tr>';
}
html += '</table>';
html += '</div>';
return html;
},
getBasicTable: function(rows, cols, data_type, callback) {
// get html for sorted table (fake pagination, for looks only)
var html = '';
// pagination
html += '<div class="pagination">';
html += '<table cellspacing="0" cellpadding="0" border="0" width="100%"><tr>';
html += '<td align="left" width="33%">';
if (cols.headerLeft) html += cols.headerLeft;
else html += commify(rows.length) + ' ' + pluralize(data_type, rows.length) + '';
html += '</td>';
html += '<td align="center" width="34%">';
html += cols.headerCenter || ' ';
html += '</td>';
html += '<td align="right" width="33%">';
html += cols.headerRight || 'Page 1 of 1';
html += '</td>';
html += '</tr></table>';
html += '</div>';
html += '<div style="margin-top:5px;">';
html += '<table class="data_table" width="100%">';
html += '<tr><th style="white-space:nowrap;">' + cols.join('</th><th style="white-space:nowrap;">') + '</th></tr>';
for (var idx = 0, len = rows.length; idx < len; idx++) {
var row = rows[idx];
var tds = callback(row, idx);
if (tds.insertAbove) html += tds.insertAbove;
html += '<tr' + (tds.className ? (' class="'+tds.className+'"') : '') + '>';
html += '<td>' + tds.join('</td><td>') + '</td>';
html += '</tr>';
} // foreach row
if (!rows.length) {
html += '<tr><td colspan="'+cols.length+'" align="center" style="padding-top:10px; padding-bottom:10px; font-weight:bold;">';
html += 'No '+pluralize(data_type)+' found.';
html += '</td></tr>';
}
html += '</table>';
html += '</div>';
return html;
}
} ); // class Page
//
// Page Manager
//
Class.create( 'PageManager', {
// 'PageManager' class handles all virtual pages in the application
// member variables
pages: null, // array of pages
current_page_id: '', // current page ID
// methods
__construct: function(page_list) {
// class constructor, create all pages
// page_list should be array of components from master config
// each one should have at least a 'ID' parameter
// anything else is copied into object verbatim
this.pages = [];
this.page_list = page_list;
for (var idx = 0, len = page_list.length; idx < len; idx++) {
Debug.trace( 'page', "Initializing page: " + page_list[idx].ID );
assert(Page[ page_list[idx].ID ], "Page class not found: Page." + page_list[idx].ID);
var page = new Page[ page_list[idx].ID ]( page_list[idx] );
page.args = {};
page.onInit();
this.pages.push(page);
$('#tab_'+page.ID).click( function(event) {
// console.log( this );
// app.page_manager.click( this._page_id );
Nav.go( this._page_id );
} )[0]._page_id = page.ID;
}
},
find: function(id) {
// locate page by ID (i.e. Plugin Name)
var page = find_object( this.pages, { ID: id } );
if (!page) Debug.trace('PageManager', "Could not find page: " + id);
return page;
},
activate: function(id, old_id, args) {
// send activate event to page by id (i.e. Plugin Name)
$('#page_'+id).show();
$('#tab_'+id).removeClass('inactive').addClass('active');
var page = this.find(id);
page.active = true;
if (!args) args = {};
// if we are navigating here from a different page, AND the new sub mismatches the old sub, clear the page html
var new_sub = args.sub || '';
if (old_id && (id != old_id) && (typeof(page._old_sub) != 'undefined') && (new_sub != page._old_sub) && page.div) {
page.div.html('');
}
var result = page.onActivate.apply(page, [args]);
if (typeof(result) == 'boolean') return result;
else throw("Page " + id + " onActivate did not return a boolean!");
},
deactivate: function(id, new_id) {
// send deactivate event to page by id (i.e. Plugin Name)
var page = this.find(id);
var result = page.onDeactivate(new_id);
if (result) {
$('#page_'+id).hide();
$('#tab_'+id).removeClass('active').addClass('inactive');
// $('#d_message').hide();
page.active = false;
// if page has args.sub, save it for clearing html on reactivate, if page AND sub are different
if (page.args) page._old_sub = page.args.sub || '';
}
return result;
},
click: function(id, args) {
// exit current page and enter specified page
Debug.trace('page', "Switching pages to: " + id);
var old_id = this.current_page_id;
if (this.current_page_id) {
var result = this.deactivate( this.current_page_id, id );
if (!result) return false; // current page said no
}
this.current_page_id = id;
this.old_page_id = old_id;
window.scrollTo( 0, 0 );
var result = this.activate(id, old_id, args);
if (!result) {
// new page has rejected activation, probably because a login is required
// un-hide previous page div, but don't call activate on it
$('#page_'+id).hide();
this.current_page_id = '';
// if (old_id) {
// $('page_'+old_id).show();
// this.current_page_id = old_id;
// }
}
return true;
}
} ); // class PageManager