478 lines
17 KiB
JavaScript
478 lines
17 KiB
JavaScript
/* kickstart 0.2.x (nightly) (c) http://w2ui.com/kickstart, vitmalina@gmail.com */
|
|
/************************************************
|
|
* Library: KickStart - Minimalistic Framework
|
|
* - Dependencies: jQuery
|
|
**/
|
|
|
|
var kickStart = (function () {
|
|
// public scope
|
|
|
|
var app = {
|
|
_conf : {
|
|
name : 'unnamed',
|
|
baseURL : '',
|
|
cache : false,
|
|
modules : {},
|
|
verbose : true
|
|
},
|
|
define : define,
|
|
require : require,
|
|
register : register
|
|
};
|
|
if (!window.app) window.app = app;
|
|
return app;
|
|
|
|
// ===========================================
|
|
// -- Define modules
|
|
|
|
function define(mod, callBack) {
|
|
// if string - it is path to the file
|
|
if (typeof mod == 'string') {
|
|
$.ajax({
|
|
url : app._conf.baseURL + mod,
|
|
dataType : 'text',
|
|
cache : app._conf.cache,
|
|
success : function (data, success, xhr) {
|
|
if (success != 'success') {
|
|
if (app._conf.verbose) console.log('ERROR: error while loading module definition from "'+ mod +'".');
|
|
return;
|
|
}
|
|
try {
|
|
mod = JSON.parse(data);
|
|
} catch (e) {
|
|
if (app._conf.verbose) console.log('ERROR: not valid JSON file "'+ mod +'".\n'+ e);
|
|
return;
|
|
}
|
|
_process(mod);
|
|
if (typeof callBack == 'function') callBack();
|
|
|
|
},
|
|
error : function (data, err, errData) {
|
|
if (app._conf.verbose) console.log('ERROR: error while loading module definition from "'+ mod +'".');
|
|
}
|
|
});
|
|
} else {
|
|
_process(mod);
|
|
if (typeof callBack == 'function') callBack();
|
|
}
|
|
|
|
function _process(mod) {
|
|
for (var m in mod) {
|
|
if (Array.isArray(mod[m].assets)) {
|
|
if (app._conf.modules.hasOwnProperty(m)) {
|
|
if (app._conf.verbose) console.log('ERROR: module ' + m + ' is already registered.');
|
|
}
|
|
app._conf.modules[m] = $.extend({ assets: {} }, mod[m], { ready: false, files: {} });
|
|
} else {
|
|
_process(mod[m]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===========================================
|
|
// -- Register module
|
|
|
|
function register(name, moduleFunction) {
|
|
// check if modules id defined
|
|
if (app.hasOwnProperty(name)) {
|
|
if (app._conf.verbose) console.log('ERROR: Namespace '+ name +' is already registered');
|
|
return false;
|
|
}
|
|
if (!app._conf.modules.hasOwnProperty(name)) {
|
|
if (app._conf.verbose) console.log('ERROR: Namespace '+ name +' is not defined, first define it with kickStart.define');
|
|
return false;
|
|
}
|
|
// register module
|
|
var mod = app._conf.modules[name];
|
|
// init module
|
|
app[name] = moduleFunction(mod.files, mod);
|
|
app._conf.modules[name].ready = true;
|
|
return;
|
|
}
|
|
|
|
// ===========================================
|
|
// -- Load Modules
|
|
|
|
function require(names, callBack) { // returns promise
|
|
if (!$.isArray(names)) names = [names];
|
|
var modCount = names.length;
|
|
var failed = false;
|
|
var promise = {
|
|
ready: function (callBack) { // a module loaded
|
|
promise._ready = callBack;
|
|
return promise;
|
|
},
|
|
fail: function (callBack) { // a module loading failed
|
|
promise._fail = callBack;
|
|
return promise;
|
|
},
|
|
done: function (callBack) { // all loaded
|
|
promise._done = callBack;
|
|
return promise;
|
|
},
|
|
always: function (callBack) {
|
|
promise._always = callBack;
|
|
return promise;
|
|
}
|
|
};
|
|
setTimeout(function () {
|
|
for (var n in names) {
|
|
var name = names[n];
|
|
// already loaded ?
|
|
if (typeof app[name] != 'undefined') {
|
|
modCount--;
|
|
isFinished();
|
|
} else if (typeof app._conf.modules[name] == 'undefined') {
|
|
if (app._conf.verbose) console.log('ERROR: module ' + name + ' is not defined.');
|
|
} else {
|
|
(function (name) { // need closure
|
|
// load dependencies
|
|
getFiles(app._conf.modules[name].assets.concat([app._conf.modules[name].start]), function (files) {
|
|
var start = files[app._conf.modules[name].start];
|
|
delete files[app._conf.modules[name].start];
|
|
// register assets
|
|
app._conf.modules[name].files = files;
|
|
app._conf.modules[name].ready = true;
|
|
// execute start file
|
|
eval(start); // if in try block, it would not show errors properly
|
|
// check ready
|
|
if (typeof promise._ready == 'function') promise._ready(app._conf.modules[name]);
|
|
modCount--;
|
|
isFinished();
|
|
});
|
|
})(name);
|
|
}
|
|
}
|
|
}, 1);
|
|
// promise need to be returned immediately
|
|
return promise;
|
|
|
|
function isFinished() {
|
|
if (modCount == 0) {
|
|
if (failed !== true) {
|
|
// if (typeof app.conf.done == 'function') app.conf.done(app._conf.modules[name]);
|
|
if (typeof promise._done == 'function') promise._done(app._conf.modules[name]);
|
|
if (typeof callBack == 'function') callBack();
|
|
}
|
|
// if (typeof app.conf.always == 'function') app.conf.always(app._conf.modules[name]);
|
|
if (typeof promise._always == 'function') promise._always();
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===========================================
|
|
// -- Loads a set of files and returns
|
|
// -- its contents to the callBack function
|
|
|
|
function getFiles (files, callBack) {
|
|
var bufferObj = {};
|
|
var bufferLen = files.length;
|
|
|
|
for (var i in files) {
|
|
// need a closure
|
|
(function () {
|
|
var index = i;
|
|
var path = files[i];
|
|
// check if file is loaded in script tag
|
|
var tmp = $('script[path="'+ path +'"]');
|
|
if (tmp.length > 0) {
|
|
bufferObj[path] = tmp.html();
|
|
loadDone();
|
|
} else {
|
|
// load from url source
|
|
$.ajax({
|
|
url : app._conf.baseURL + path,
|
|
dataType : 'text',
|
|
cache : app._conf.cache,
|
|
success : function (data, success, xhr) {
|
|
if (success != 'success') {
|
|
if (app._conf.verbose) console.log('ERROR: error while getting a file '+ path +'.');
|
|
return;
|
|
}
|
|
bufferObj[path] = xhr.responseText;
|
|
loadDone();
|
|
|
|
},
|
|
error : function (data, err, errData) {
|
|
if (err == 'error') {
|
|
if (app._conf.verbose) console.log('ERROR: failed to load '+ files[i] +'.');
|
|
} else {
|
|
if (app._conf.verbose) console.log('ERROR: file "'+ files[i] + '" is loaded, but with a parsing error(s) in line '+ errData.line +': '+ errData.message);
|
|
bufferObj[path] = xhr.responseText;
|
|
loadDone();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
})();
|
|
}
|
|
// internal counter
|
|
function loadDone() {
|
|
bufferLen--;
|
|
if (bufferLen <= 0) callBack(bufferObj);
|
|
}
|
|
}
|
|
})();
|
|
kickStart.define({ route: { assets: [] }});
|
|
kickStart.register('route', function () {
|
|
// private scope
|
|
var app = kickStart;
|
|
var routes = {};
|
|
var routeRE = {};
|
|
|
|
addListener();
|
|
|
|
var obj = {
|
|
init : init,
|
|
add : add,
|
|
remove : remove,
|
|
go : go,
|
|
set : set,
|
|
get : get,
|
|
info : info,
|
|
process : process,
|
|
list : list,
|
|
onAdd : null,
|
|
onRemove: null,
|
|
onRoute : null
|
|
};
|
|
if (typeof w2utils != 'undefined') $.extend(obj, w2utils.event, { handlers: [] });
|
|
return obj;
|
|
|
|
/*
|
|
* Public methods
|
|
*/
|
|
|
|
function init(route) {
|
|
// default route is passed here
|
|
if (get() === '') {
|
|
go(route);
|
|
} else {
|
|
process();
|
|
}
|
|
}
|
|
|
|
function add(route, handler) {
|
|
if (typeof route == 'object') {
|
|
for (var r in route) {
|
|
var tmp = String('/'+ r).replace(/\/{2,}/g, '/');
|
|
routes[tmp] = route[r];
|
|
}
|
|
return app.route;
|
|
}
|
|
route = String('/'+route).replace(/\/{2,}/g, '/');
|
|
// if events are available
|
|
if (typeof app.route.trigger == 'function') {
|
|
var eventData = app.route.trigger({ phase: 'before', type: 'add', target: 'self', route: route, handler: handler });
|
|
if (eventData.isCancelled === true) return false;
|
|
}
|
|
// default behavior
|
|
routes[route] = handler;
|
|
// if events are available
|
|
if (typeof app.route.trigger == 'function') app.route.trigger($.extend(eventData, { phase: 'after' }));
|
|
return app.route;
|
|
}
|
|
|
|
function remove(route) {
|
|
route = String('/'+route).replace(/\/{2,}/g, '/');
|
|
// if events are available
|
|
if (typeof app.route.trigger == 'function') {
|
|
var eventData = app.route.trigger({ phase: 'before', type: 'remove', target: 'self', route: route, handler: handler });
|
|
if (eventData.isCancelled === true) return false;
|
|
}
|
|
// default behavior
|
|
delete routes[route];
|
|
delete routeRE[route];
|
|
// if events are available
|
|
if (typeof app.route.trigger == 'function') app.route.trigger($.extend(eventData, { phase: 'after' }));
|
|
return app.route;
|
|
}
|
|
|
|
function go(route) {
|
|
route = String('/'+route).replace(/\/{2,}/g, '/');
|
|
window.history.replaceState({}, document.title, '#' + route)
|
|
process()
|
|
return app.route;
|
|
}
|
|
|
|
function set(route) {
|
|
route = String('/'+route).replace(/\/{2,}/g, '/');
|
|
window.history.replaceState({}, document.title, '#' + route)
|
|
return app.route;
|
|
}
|
|
|
|
function get() {
|
|
return window.location.hash.substr(1).replace(/\/{2,}/g, '/');
|
|
}
|
|
|
|
function info() {
|
|
var matches = [];
|
|
var isFound = false;
|
|
var isExact = false;
|
|
// match routes
|
|
var hash = window.location.hash.substr(1).replace(/\/{2,}/g, '/');
|
|
if (hash == '') hash = '/';
|
|
|
|
for (var r in routeRE) {
|
|
var params = {};
|
|
var tmp = routeRE[r].path.exec(hash);
|
|
if (tmp != null) { // match
|
|
isFound = true;
|
|
if (!isExact && r.indexOf('*') === -1) {
|
|
isExact = true;
|
|
}
|
|
var i = 1;
|
|
for (var p in routeRE[r].keys) {
|
|
params[routeRE[r].keys[p].name] = tmp[i];
|
|
i++;
|
|
}
|
|
// default handler
|
|
matches.push({ name: r, path: hash, params: params });
|
|
}
|
|
}
|
|
return matches;
|
|
}
|
|
|
|
function list() {
|
|
prepare();
|
|
var res = {};
|
|
for (var r in routes) {
|
|
var tmp = routeRE[r].keys;
|
|
var keys = [];
|
|
for (var t in tmp) keys.push(tmp[t].name);
|
|
res[r] = keys;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
function process() {
|
|
prepare();
|
|
// match routes
|
|
var hash = window.location.hash.substr(1).replace(/\/{2,}/g, '/');
|
|
if (hash == '') hash = '/';
|
|
// process route
|
|
var isFound = false;
|
|
var isExact = false;
|
|
var isAutoLoad = false;
|
|
for (var r in routeRE) {
|
|
var params = {};
|
|
var tmp = routeRE[r].path.exec(hash);
|
|
if (tmp != null) { // match
|
|
isFound = true;
|
|
if (!isExact && r.indexOf('*') === -1) {
|
|
isExact = true;
|
|
}
|
|
var i = 1;
|
|
for (var p in routeRE[r].keys) {
|
|
params[routeRE[r].keys[p].name] = tmp[i];
|
|
i++;
|
|
}
|
|
// if events are available
|
|
if (typeof app.route.trigger == 'function') {
|
|
var eventData = app.route.trigger({ phase: 'before', type: 'route', target: 'self', route: r, params: params });
|
|
if (eventData.isCancelled === true) return false;
|
|
}
|
|
// default handler
|
|
var res = routes[r]({ name: r, path: hash, params: params }, params);
|
|
// if events are available
|
|
if (typeof app.route.trigger == 'function') app.route.trigger($.extend(eventData, { phase: 'after' }));
|
|
// if hash changed (for example in handler), then do not process rest of old processings
|
|
var current = window.location.hash.substr(1).replace(/\/{2,}/g, '/');
|
|
if (hash !== current) return
|
|
}
|
|
}
|
|
// find if a route matches a module route
|
|
var loadCnt = 0;
|
|
var mods = app._conf.modules;
|
|
var loading = [];
|
|
for (var name in mods) {
|
|
var mod = mods[name];
|
|
var rt = mod.route;
|
|
var nearMatch = false;
|
|
if (rt != null) {
|
|
if (typeof rt == 'string') rt = [rt];
|
|
if (Array.isArray(rt)) {
|
|
rt.forEach(function (str) { checkRoute(str) });
|
|
}
|
|
}
|
|
function checkRoute(str) {
|
|
mod.routeRE = mod.routeRE || {};
|
|
if (mod.routeRE[str] == null) mod.routeRE[str] = prepare(str);
|
|
if (!mod.ready && str && mod.routeRE[str].path.exec(hash) && loading.indexOf(name) == -1) {
|
|
if (app._conf.verbose) console.log('ROUTER: Auto Load Module "' + name + '"');
|
|
isAutoLoad = true;
|
|
loadCnt++;
|
|
loading.push(name);
|
|
app.require(name).done(function () {
|
|
loadCnt--;
|
|
if (app._conf.modules[name] && loadCnt === 0) process();
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if (!isAutoLoad && !isExact && app._conf.verbose) console.log('ROUTER: Exact route for "' + hash + '" not found');
|
|
|
|
if (!isFound) {
|
|
// path not found
|
|
if (typeof app.route.trigger == 'function') {
|
|
var eventData = app.route.trigger({ phase: 'before', type: 'error', target: 'self', hash: hash});
|
|
if (eventData.isCancelled === true) return false;
|
|
}
|
|
if (!isAutoLoad && app._conf.verbose) console.log('ROUTER: Wild card route for "' + hash + '" not found');
|
|
// if events are available
|
|
if (typeof app.route.trigger == 'function') app.route.trigger($.extend(eventData, { phase: 'after' }));
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Private methods
|
|
*/
|
|
|
|
function prepare(r) {
|
|
if (r != null) {
|
|
return _prepare(r)
|
|
}
|
|
// make sure all routes are parsed to RegEx
|
|
for (var r in routes) {
|
|
if (routeRE[r]) continue;
|
|
routeRE[r] = _prepare(r)
|
|
}
|
|
|
|
function _prepare(r) {
|
|
var keys = [];
|
|
var path = r
|
|
.replace(/\/\(/g, '(?:/')
|
|
.replace(/\+/g, '__plus__')
|
|
.replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional) {
|
|
keys.push({ name: key, optional: !! optional });
|
|
slash = slash || '';
|
|
return '' + (optional ? '' : slash) + '(?:' + (optional ? slash : '') + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' + (optional || '');
|
|
})
|
|
.replace(/([\/.])/g, '\\$1')
|
|
.replace(/__plus__/g, '(.+)')
|
|
.replace(/\*/g, '(.*)');
|
|
return {
|
|
path : new RegExp('^' + path + '$', 'i'),
|
|
keys : keys
|
|
}
|
|
}
|
|
}
|
|
|
|
function addListener() {
|
|
if (window.addEventListener) {
|
|
window.addEventListener('hashchange', process, false);
|
|
} else {
|
|
window.attachEvent('onhashchange', process);
|
|
}
|
|
}
|
|
|
|
function removeListener() {
|
|
if (window.removeEventListener) {
|
|
window.removeEventListener('hashchange', process);
|
|
} else {
|
|
window.detachEvent('onhashchange', process);
|
|
}
|
|
}
|
|
}); |