nope/resources/w2ui/kickstart.js

478 lines
17 KiB
JavaScript
Raw Normal View History

2020-09-01 15:48:26 +00:00
/* 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);
}
}
});