adhara
Version:
foundation for any kind of website: microframework
1,448 lines (1,364 loc) • 162 kB
JavaScript
/**
* @typedef {Object} ElementAttributes
* @description attributes that can be set in a element using `addAttr` helper for Handlebar files.
* */
class TemplateEngineHelpers{
/**
* @namespace
* @description
* Helpers written for handlebars. Can also be used by static references.
* */
static getHelpers(currentHelpers){
let if_helper = currentHelpers.if;
return {
/**
* @function
* @static
* @param {ElementAttributes} attributes - html element attributes.
* @param {String|Object} default_attributes - html element default attributes.
* @description
* attributes override default_attributes
* create attribute string and return which can be used to append
* @example
* let options = {
* attributes : { id : 'element-id', className : 'css-class' }
* }
* TemplateUtils.execute(
* '<a {{addAttr attributes "{"href":"javascript:void(0)", "route":"true"}"}}></a>',
* options
* )
* //returns <a href="javascript:void(0)" route:"true" id="element-id" class:"css-class></a>
* */
'addAttr': function(attributes, default_attributes){
if(attributes || default_attributes){
let attrData = [];
default_attributes = (default_attributes && typeof(default_attributes) === "string") ? JSON.parse(default_attributes) : {};
let class_names = attributes?(attributes.className||attributes["class"]||""):"";
class_names += default_attributes.hasOwnProperty("class")?" "+default_attributes['class']:"";
attributes = Object.assign( default_attributes, attributes );
attributes['class'] = class_names;
loop(attributes, function(key, value){
if(key && value !== undefined){
if(key === "style"){
let css_values = [];
loop(value, function(css_prop, css_value){
css_values.push(css_prop+":"+css_value+";");
});
value = css_values.join('');
}
if(typeof(value) === "string"){
value = '"'+value.replace(/"/g, '\\"')+'"';
}
attrData.push(key+'='+value);
}
});
return new window.Handlebars.SafeString(attrData.join(' '));
}
},
'addProps': function(){
let properties = Array.prototype.slice.call(arguments);
return properties.splice(0, properties.length-1).join(' ');
},
'selectedClass': function(selected,selectedClassName){
if(selected === true){
return selectedClassName;
} else {
return '';
}
},
/**
* @function
* @static
* @param {String} template_name - precompiled handlebar template name.
* @param {Object} context - context with which the `template_name` hbs template to be called
* @return {String} `template_name` handlebar template contents after execution with provided context.
* @description
* includes one hbs template in another.
* @example
* //child HBS file - child-template.hbs
* `<div id="child">
* {{name}}
* </div>`
*
* // main HBS file - main-template.hbs
* `<div id="main">
* {{name}}
* {{include 'child-template' child_context}}
* </div>`
*
* Handlebars.templates['main-template']({name:"MAIN", child_context:{name:"CHILD"}})
* // returns
* //<div id="main">
* // MAIN
* // <div id="child">
* // CHILD
* // </div>
* //</div>
* */
'include' : function(template_name, context){
return new window.Handlebars.SafeString(TemplateUtils.execute(template_name, context));
},
/**
* @function
* @static
* @param {Number} param1 - left operand / lvalue
* @param {String} operation - can be one among "+", "-", "*", "/" and "%"
* @param {Number} param2 - right operand / rvalue
* @returns {Number} - result after operation between param1 and param2 with operation
* */
'math' : function(param1, operation, param2){
let lvalue = parseFloat(String(param1));
let rvalue = parseFloat(String(param2));
return {
"+": lvalue+rvalue,
"-": lvalue-rvalue,
"*": lvalue*rvalue,
"/": lvalue/rvalue,
"%": lvalue%rvalue
}[operation]
},
/**
* @function
* @static
* @param {String} i18nKey - application key that needs to be internationalized.
* @returns {String} internationalized application key's value
* @example
* // app.key.name = "APP Key"
* {{i18N 'app.key.name'}} //returns "APP Key"
*
* // app.key.operation = "App Operation {0} by {1}"
* {{i18N 'app.key.operation' 'operationName' 'operatedBy'}} //returns "App Operation operationName by operatedBy"
* */
'i18n' : function (i18nKey) { //~ (i18nKey, ...subs)
let subs = Array.prototype.slice.call(arguments);
subs = subs.splice(1, subs.length-2);
return Adhara.i18n.get(i18nKey, subs);
},
/**
* @function
* @static
* @param {Object} object - object in which to lookup.
* @param {String} path - dot separated keys to be looked up for in depth.
* @description
* Wrapper for {@link getValueFromJSON}
* @returns {String|Number|Boolean|Object|Array} - Value for the key.
* */
'get' : function(object, path){
return getValueFromJSON(object, path);
},
/**
* @function
* @static
* @description
* Wrapper for {@link evaluateLogic}
* @returns
* handlebar block based on logic.
* */
'if' : function(param1, operator, param2, options){
if(typeof operator === "object"){
return if_helper.call(this, param1, operator);
}
if(evaluateLogic(param1, operator, param2)){
return options.fn(this);
}else{
return options.inverse(this);
}
},
/**
* @function
* @static
* @description
* If condition with multiple equations to evaluate.
* Takes numerous arguments which will be executed in pairs.
* @returns
* handlebar block based on logic.
* */
'mIf' : function(){
let arr = [true, 'and', true];
let args_len = arguments.length-1;
let options = arguments[args_len];
for(let i=0; i<args_len; i++){
if(i === 0){
arr[0] = arguments[i];
}else if(i%2 === 1){
arr[1] = arguments[i];
}else{
arr[2] = arguments[i];
arr[0] = evaluateLogic(arr[0], arr[1], arr[2]);
}
}
if(arr[0]){
return options.fn(this);
}else{
return options.inverse(this);
}
},
/**
* @function
* @static
* @description
* If condition with multiple equations to evaluate.
* Takes numerous arguments which will be executed in pairs.
* @returns {Boolean} result of equation created by provided params.
* @see evaluateLogic
* */
'eIf' : function(param1, operator, param2){
return evaluateLogic(param1, operator, param2)
},
/**
* @function
* @static
* @returns {String|Number|Boolean|Object|Array} - Value of the global letiable.
* */
'global' : function(global_letiable){
if(global_letiable.indexOf("Adhara.") === 0){
return getValueFromJSON(Adhara, global_letiable.substring("Adhara.".length));
}
return getValueFromJSON(window, global_letiable);
},
'loop' : function(looper, options){
let structure = '';
for(let i=0; i<looper.length; i++){
structure+=options.fn(looper[i]);
}
return structure;
},
/**
* @function
* @static
* @description Takes numerous {String} arguments and appends all those strings.
* @returns {String} arguments joined by ''.
* */
'append' : function () {
let args = Array.prototype.slice.call(arguments);
args.pop();
return args.map(arg => {
if(typeof arg === "object"){
return JSON.stringify(arg);
}
return arg;
}).join('');
},
/**
* @function
* @static
* @description Takes a {String} and converts it to JSON object.
* @returns {Object}
* */
'make_json' : function (str) {
return JSON.parse(str);
},
/**
* @function
* @static
* @description Takes a {String} and converts it to JSON object.
* @returns {Object}
* */
'make_context' : function () {
let context = {};
for(let i=0; i<arguments.length-1; i+=2){
context[arguments[i]] = arguments[i+1];
}
return context;
},
/**
* @function
* @static
* @param {Function} fn
* @description Takes numerous and calls the fn with arguments from 2nd position since 1st argument is fn itself.
* @returns {String|Number|Boolean|Object|Array} content returned by fn
* */
'fn_call' : function(fn){
return call_fn.apply(this, Array.prototype.slice.call(arguments).slice(0, -1));
},
/**
* @function
* @static
* @param {Object} looper
* @param {Object} options - Handlebars options that has access to blocks.
* @returns {String} Block string.
* @description blocks will be called with a context ~ {"key":k, "value":v}
* where `k` is key in the looper object and `v` is value corresponding to `k`.
* */
'loopObject' : function(looper, options){
let structure = '';
loop(looper, function(key, value){
structure+=options.fn( {key: key, value: value} );
});
return structure;
},
/**
* @function
* @static
* @param {Number} start
* @param {Number} step
* @param {Number} end
* @param {Object} options - Handlebars options that has access to blocks.
* @returns {String} Block string appended n times where n is the number of times step-looped.
* @description Block will be called with just a number {Number} as context which keeps increasing in steps, where the step height is equal to step param.
* */
'iterate_range' : function(start, step, end, options){
let str_buf = '';
for(let i = start; i <= end; i += step){
str_buf += options.fn(i);
}
return str_buf;
},
'route': function(url){
return new Handlebars.SafeString(`href="${Adhara.router.transformURL(url)}" route`);
}
/*
TODO handle better!
'generate': function(context, options){
let generatorInstance = context(),
ret = '', done = false, index = 0,
data = {};
function execIteration(field, index, last) {
if (data) {
data.key = field;
data.index = index;
data.first = index === 0;
data.last = !!last;
}
ret = ret + options.fn(context[field], {
data: data,
// blockParams: _utils.blockParams([context[field], field], [contextPath + field, null])
});
}
do{
let yi = generatorInstance.next();
done = yi.done;
execIteration(yi.value, ++index, done===true);
}while(done===false);
console.log(context, options);
return ret;
}*/
};
}
static registerHelpers(){
window.Handlebars.registerHelper(TemplateEngineHelpers.getHelpers(window.Handlebars.helpers));
}
}
/**------------------------------------------------------------------------------------------------------------------**/
function handleForm(form){
if(form.submitting){
return;
}else{
form.submitting = true;
}
let hasFiles = !!form.querySelector('input[type="file"]');
let apiData;
if(hasFiles){
apiData = new FormData(form);
}else{
let formData = jQuery(form).serializeArray();
apiData = {};
jQuery.each(formData, function (i, fieldData) {
apiData[fieldData.name] = fieldData.value;
});
}
let format_fn = form.getAttribute('format-data');
if(format_fn){
apiData = call_fn(format_fn, apiData);
}
if(apiData===false){return;}
RestAPI[form.getAttribute('api-method')]({
url: form.action.split(window.location.host)[1],
data: apiData,
successMessage: form.getAttribute('success-message'),
handleError: form.getAttribute('handle-error')!=="false",
success: function(data){
if(form.getAttribute('form-clear')==="true") {
form.reset();
}
jQuery(form).trigger('success', data);
},
failure: function(message){
jQuery(form).trigger('failure', message);
}
});
}
function registerAdharaUtils(){
//Register templateEngine helpers
Adhara.templateEngine.helpersHandler.registerHelpers();
//Form listeners
jQuery(document).on('submit', 'form.api-form', function (event) {
event.preventDefault();
handleForm(this);
});
//Form listeners
jQuery(document).on('success', 'form.dialog-form', function (/*e, d*/) {
this.close.click();
});
}
/**
* @function
* @global
* @param {String|Number|Boolean} param1
* @param {String} operator - Operator can be one among these.
* "in", "not_in", "contains", "not_contains", "has", "||", "&&", "|", "&", "==", "===", "!=", "!==", ">", "<", ">=", "<=", "equals", "and", "or".
* @param {String|Number|Boolean} param2
* @Returns {Boolean}
*
* "in" - param2 must be an {Array}. Whether param1's existence in that array will be checked for
*
* "not_in" - param2 must be an {Array} and param1's non-existence in that array will be checked for
*
* "contains" - param1 must be an {Array} and param2's existence in that array will be checked for
*
* "not_contains" - param1 must be an {Array} and param2's non-existence in that array will be checked for
*
* "has" - param1 must be an {Object}. Whether param2 is a key of param1
*
* All other operations will be applied as javaScript evaluates them.
* `"equals" is equivalent to "=="`,
* `"and" is equivalent to "&&"`
* and `"or" is equivalent to "||"`.
*
* */
function evaluateLogic(param1, operator, param2){
if(operator === "in"){
return param2 && param2.indexOf(param1) !== -1;
}else if(operator === "not_in"){
return param2 && param2.indexOf(param1) === -1;
}else if(operator === "contains"){
return param1 && param1.indexOf(param2) !== -1;
}else if(operator === "not_contains"){
return param1 && param1.indexOf(param2) === -1;
}else if(operator === "has"){
return param1 && param1.hasOwnProperty(param2);
}else{
return {
"||" : param1||param2,
"&&" : param1&¶m2,
"|" : param1|param2,
"&" : param1¶m2,
"==" : param1==param2,
"===": param1===param2,
"!=" : param1!=param2,
"!==": param1!==param2,
">" : param1>param2,
"<" : param1<param2,
">=" : param1>=param2,
"<=" : param1<=param2,
"equals" : param1==param2,
"and" : param1&¶m2,
"or" : param1||param2
}[operator];
}
}
/**
* @function
* @global
* @param {Object} object - object in which to lookup.
* @param {String} path - dot separated keys to be looked up for in depth.
* @param {String} [identifier=null] - token that helps to lookup inside arrays.
* @returns {String|Number|Boolean|Object|Array} - Value for the key.
* @description
* Looks up a JSON object in depth and returns the value for the key provided.
* @example
* let obj = {
* task: {
* id: "12341234",
* status: {
* color: "#fff"
* }
* }
* };
* getValueFromJSON(obj, "task.status.color"); //returns "#fff"
*
* let objX = {
* tasks: [
* {
* id: "12341234",
* status: {
* color: "#fff"
* }
* },
* {
* id: "55424",
* status: {
* color: "#f5f5f5"
* }
* },
* {
* id: "90898080",
* status: {
* color: "#eee"
* }
* },
* ]
* };
* getValueFromJSON(objX, "task[1].status.color"); //returns "#f5f5f5"
* getValueFromJSON(objX, "task[$, ].status.color", "$"); //returns "#fff, #f5f5f5, #eee"
* // Elaborately,
* // In `[$, ]` --split into identifier and separator
* // '$'=identifier(from params).
* // Remaining part inside [] is the separator i.e., `', '` will be used to join the results from the array
* */
function getValueFromJSON(object, path, identifier){
try{
if(!path){
return object;
}
let keys = path.split('.');
for(let i=0; i<keys.length; i++){
let key_in = keys[i];
if(key_in.indexOf('[') !== -1){
let arr = key_in.split('[');
let arrName = arr[0];
let index = arr[1].split(']')[0];
if(isNaN(index)){
let rite = object[arrName];
let separator = index.substring(identifier.length, index.length);
let formattedVal = '';
for(let j=0; j<rite.length; j++){
if(j>0){
formattedVal += separator;
}
formattedVal += getValueFromJSON(rite[j], path.split('.').splice(i+1).join('.'), identifier);
}
object = formattedVal;
}else{
object = object[arrName][index];
}
break;
}else{
object = object[key_in];
}
}
return object;
}catch (e) {
return undefined;
}
}
/**
* @function
* @global
* @param {Object} object - object to set value to
* @param {String} path - dot separated path.
* @param {String|Number|Boolean|Object|Array} value - value to be set fot the given key
* @description
* Sets value to provided object at specified depth in the path be using dot separators.
* Creates objects at depths if no object already exists at specified path.
* @example
* let kit = {'has_bat': true}
* setValueToJson(kit, 'has_ball', true); //let kit = {'has_bat': true, 'has_ball': true}
* setValueToJson(kit, 'has_bat', false); //let kit = {'has_bat': false, 'has_ball': true}
*
* let aeroplane = {'name': 'B747'}
* setValueToJson(aeroplane, 'tank.capacity', '4000'); //aeroplane = {'name': 'B747', 'tank': { 'capacity': '4000' }}
* setValueToJson(aeroplane, 'tank.shape', 'amoeba'); //aeroplane = {'name': 'B747', 'tank': { 'capacity': '4000', 'shape': 'amoeba' }}
* */
function setValueToJson(object, path, value){
let keys = path.split('.');
loop(keys, function(i, key){
if(i+1 < keys.length){
if(!object.hasOwnProperty(key) || !object[key] || typeof object[key] !== "object"){
object[key]={}
}
object=object[key];
}else{
object[key]=value
}
});
}
/**
* @typedef {Object|Array|String|Boolean|Number|null|undefined} _MultiParams - numerous arguments of any kind
* @description
* A function if accepts multiple params, can use this as param type.
* @example
* //def
* function mpFn({String} param1, {_MultiParams} m_params){}
* //call
* mpFn("Str_param1", "p1", "p2", 1, {'key':'val'}, ['m1', 'm2', ...], ...);
* //def2
* function mp2Fn({String} param1, {_MultiParams} m_params, {String} param2){}
* //call2
* mp2Fn("Str_param1", "p1", "p2", 1, {'key':'val'}, ['m1', 'm2', ...], ..., "StrX_param2");
*
* */
/**
* @function
* @global
* @param {Function|String} fn - function to be called or global path to a function
* @param {_MultiParams} params - any params that to be passed to function
* @description
* calls the function with params passed
* @example
* let gv = {}; //global_letiable
* gv.sample_fn = function(param1, param2, param3){
* //Do Something...
* return "Hello from SampleFN"
* };
* call_fn('gv.sample_fn', param1, param2, param3); //returns "Hello from SampleFN"
* */
function call_fn(fn){
if(!fn){return;}
let args = Array.prototype.slice.call(arguments);
args.splice(0,1);
if(typeof(fn) === "function"){
return fn.apply(fn, args);
}else{
try{
fn = getValueFromJSON(window, fn);
if(fn && fn !== window){
return fn.apply(window[fn], args);
}
}catch(e){
if(e.name === "TypeError" && e.message.indexOf("is not a function") !== -1){
throw new Error("error executing function : "+fn);
}else{
throw e;
}
}
}
}
/**
* Can be used if required to call a function, and if it is unavailable, call a default fn
* Usage : call_fn_or_def(fn, arg1, arg2, arg3, def_fn)
* Result : fn(arg1, arg2, arg3) if fn is not undefined else def_fn(arg1, arg2, arg3)
* */
/**
* @function
* @global
* @param {Function|String} fn - function to be called or global path to a function
* @param {_MultiParams} params - any params that to be passed to function
* @param {Function|String} fn - function to be called or global path to a function
* @description
* calls the function with params passed
* @example
* let gv = {}; //global_letiable
* gv.sample_fn = function(param1, param2, param3){
* //Do Something...
* return "Hello from SampleFN"
* };
* call_fn('gv.sample_fn', param1, param2, param3, defaultFunction); //returns "Hello from SampleFN"
* */
function call_fn_or_def(fn){
let args = Array.prototype.slice.call(arguments);
let def_fn = args.pop();
if( ! ( typeof(def_fn) === "function" || typeof(window[def_fn]) === "function" ) ){
args.push(def_fn);
def_fn = undefined;
}
if(fn){
return call_fn.apply(call_fn, args);
}else if(def_fn){
args[0] = def_fn;
return call_fn.apply(call_fn, args);
}
}
/**
* Loop - looper iterator function
* @callback LoopIteratorCallback
* @param {String|Number|RegExp} key - key will be string in case of an object and Number will be an Array while looping through an Array.
* @param {*} value - Value of iterable for key.
* @returns {Boolean}
* return `false` to stop iteration.
* */
/**
* @function
* @global
* @param {Object|Array} object - Iterable.
* @param {LoopIteratorCallback} cbk - callback for each value in iterable.
* @param {Boolean} [reverseIterate=false] - iterate in reverse if it is an array.
* */
function loop(object, cbk, reverseIterate) {
let i, loop_size;
if(object instanceof Array){
loop_size = object.length;
if(reverseIterate){
for(i=loop_size-1; i>=0; i--){
if(cbk(i, object[i]) === false){
break;
}
}
}else{
for(i=0; i<loop_size; i++){
if(cbk(i, object[i]) === false){
break;
}
}
}
}else{
let keys = Object.keys(object);
loop_size = keys.length;
for(i=0; i<loop_size; i++){
if(cbk(keys[i], object[keys[i]]) === false){
break;
}
}
}
}
/**
* @typedef {String} HandlebarTemplate
* @description handlebar template can be an inline template or a pre compiled template that is created in a HBS file.
* */
/**
* @namespace
* @description
* TemplateUtils is a set of utilities related to handlebars
* */
let TemplateUtils = {};
(function(){
let preCompiledCache = {};
/**
* @function
* @static
* @param {HandlebarTemplate} template_or_template_string - Handlebar template name or template string.
* @param {Object} context - content to be passed t provided handlebar template/string.
* @param {Boolean} [cache=true] - whether to cache if the provided template is a Handlebar string template.
* @returns {String} compiled adn executed handlebar template
* @example
* //using template
* TemplateUtils.execute('template-file-name', {context_key1: "value1", ...});
*
* //using template string
* TemplateUtils.execute('Hello {{name}}', {name: "Adhara"}); //returns "Hello Adhara"
* */
TemplateUtils.execute = function(template_or_template_string, context, cache){
//Check if it is a pre compiled hbs template
let template = window.Handlebars.templates[template_or_template_string];
if(!template && window.Handlebars.hasOwnProperty("compile")){
//If template not found in precompiled hbs list => template is a handlebar string template
// check if cacheable
if(cache !== false){
//check if the template is already cached.
template = preCompiledCache[template_or_template_string];
if(!template){
//else compile, store it in cache and proceed to execution
template = preCompiledCache[template_or_template_string] = window.Handlebars.compile(template_or_template_string);
}
}else{
//else compile and proceed to execution
template = window.Handlebars.compile(template_or_template_string);
}
}
//execute with the provided context and return the output content...
return template(context);
};
})();
class Internationalize{
/**
* @constructor
* @param {Object<String, String>} key_map - i18n key map
* */
constructor(key_map){
this.key_map = key_map;
}
/**
* @instance
* @function
* @param {String} key - key
* @param {Array<String>} subs - substitutes
* @param {String} default_value - will be returned if key is not availalbe in the keymap
* */
getValue(key, subs, default_value){
if(!key){return;}
let value = this.key_map[key];
if(value===undefined){
value = getValueFromJSON(this.key_map, key);
}
if(value===undefined){
return default_value;
}
subs = subs || [];
let placeholders = value.match(/{[0-9]+}/g) || [];
for(let i=0; i<placeholders.length; i++){
let sub = subs[i] || "";
try {
if (sub.indexOf('.') !== -1) {
sub = this.get(sub);
}
}catch(e){/*Do Nothing*/}
value = value.replace( new RegExp( "\\{"+i+"\\}","g"), sub );
}
return value.trim();
}
/**
* @instance
* @function
* @param {String} key - key
* @param {Array<String>} [subs=[]] - substitutes
* */
get(key, subs){
return this.getValue(key, subs, key);
}
}
// Enable multi extend functionality https://stackoverflow.com/a/45332959
function MutateViews(baseClass, ...mixins){
class base extends baseClass {
constructor (...args) {
super(...args);
mixins.forEach((mixin) => {
copyProps(this,(new mixin(...args)));
});
}
}
function copyProps(target, source){ // this function copies all properties and symbols, filtering out some special ones
//TODO "Object.getOwnPropertyNames" and "Object.getOwnPropertySymbols" Doesn't work in IE even after transpilation to ES5...
Object.getOwnPropertyNames(source)
.concat(Object.getOwnPropertySymbols(source))
.forEach((prop) => {
if (!prop.match(/^(?:constructor|prototype|arguments|caller|name|bind|call|apply|toString|length)$/))
Object.defineProperty(target, prop, Object.getOwnPropertyDescriptor(source, prop));
})
}
mixins.forEach((mixin) => { // outside contructor() to allow aggregation(A,B,C).staticFunction() to be called etc.
copyProps(base.prototype, mixin.prototype);
copyProps(base, mixin);
});
return base;
}
function cloneObject(obj) {
let clone = (obj instanceof Array)?[]:{};
for(let i in obj) {
if(obj[i] != null && typeof(obj[i])==="object"){
clone[i] = cloneObject(obj[i]);
}else{
clone[i] = obj[i];
}
}
return clone;
}
class Time{
static async sleep(millis){
return new Promise((resolve, reject)=>{
setTimeout(resolve, millis);
})
}
}
/**------------------------------------------------------------------------------------------------------------------**/
/**
* @class
* @classdesc Ticker class that handles repetitive, time based queueing
* @param {Number} [interval=2000] - polling interval
* @param {Number} [exponential_factor=1] - factor by which polling interval should be multiplied
* @param {Number} [min_interval=0] - minimum value configured for Poller's timer to work
* */
class AdharaTicker{
constructor(interval, exponential_factor, min_interval){
this.interval = ( interval===0 ? interval: (interval || 2000) );
this.exponential_factor = exponential_factor || 1;
this.min_interval = min_interval || 0;
this.initial_interval = interval;
this.timeoutId = null;
}
scheduleNextTick(){
this.pause();
if(!(this.interval <= this.min_interval)) {
this.timeoutId = window.setTimeout(()=>{
this.onExecute()
}, this.interval);
}
}
next(){
this.scheduleNextTick();
}
onExecute(){
this.interval *= this.exponential_factor;
this.on_execute(this.next);
}
/**
* @function
* @description
* Starts polling
* */
start(execute){
this.on_execute = execute;
this.scheduleNextTick();
}
/**
* @function
* @description
* stops current timeout
* doesn't call next or on-execute function
* */
pause(){
window.clearTimeout(this.timeoutId);
};
/**
* @function
* @description
* stops current timeout
* doesn't call next or on-execute function
* start new timeout
* */
resume(){
this.scheduleNextTick();
};
/**
* @function
* @description
* stops current timeout
* doesn't call next or on-execute function
* reset's interval to initial interval
* */
stop(){
this.pause();
this.interval = this.initial_interval;
};
/**
* @function
* @description
* stops current timeout
* doesn't call next or on-execute function
* reset's interval to initial interval
* start new timeout
* */
restart(){
this.stop();
this.scheduleNextTick();
};
}
/**------------------------------------------------------------------------------------------------------------------**/
/**
* @namespace
* @description
* Client routing logic.
* All the client URL pattern's should be registered with the router.
*
* On visiting a URL, if called {@link AdharaRouter.route}, it calls the registered view function,
* which takes care of rendering the page.
*
* Any HTML element can be used as a router. For this purpose,
*
*
* > Add this attribute `href="/url/to/be/navigated/to"` and this property `route` to any html element to make behave as a router.
*
* > By default routing happening this way will execute the view function even if the current URL is same as provided href.
*
* > In order to disable this behaviour, provide the attribute `data-force="false"`
*
* > In order to use {@link AdharaRouter.goBack} functionality provide `data-force="true"`. See example below.
*
* @example
* //Registering
* AdharaRouter.register("^/adhara/{{instance_id}}([0-9]+)/{{tab}}(details|tokens|history)$", function(instance_id, tab){
* console.log(instance_id, tab);
* AdharaRouter.getPathParam("instance_id"); //Returns same as instance_id
* AdharaRouter.getPathParam("tab"); //Returns same as tab
* });
*
* //Navigating
* AdharaRouter.navigateTo("/adhara/123412341234/details");
*
* //Routing - In case if URL is already set in address bar and not via Router, call this function to execute the registered view funciton.
* this.route();
*
* //HTML Example
*
* <a href="/adhara/123412341234/tokens" />
*
* <button href="/adhara/123412341234/tokens" data-force="false">tokens</button>
*
* <div href="/adhara/123412341234/details" data-back="true"></div>
*
* */
let AdharaRouter = null;
(() => {
"use strict";
/**
* @private
* @member {Object<String, Object>}
* @description
* Stores all the registered URL's along with other view parameters.
* */
let registeredUrlPatterns = {};
/**
* @private
* @member {String}
* @description
* Stores base URI for regex matches
* */
let baseURI = "";
let defaultTitle = document.title;
/**
* @private
* @member {String}
* @description
* App Name to be used as first half of document title
* */
let appName = "";
/**
* @private
* @member {Object<String, String>}
* @description
* Stores the current URL's search query parameters.
* */
let queryParams = {};
/**
* @private
* @member {Object<String, String>}
* @description
* Stores the current URL's path variables.
* */
let pathParams = {};
/**
* @private
* @member {Array<String>}
* @description
* Stores list of visited URL's
* */
let historyStack = [];
/**
* @private
* @member {String|undefined}
* @description
* Stores current page URL.
* */
let currentUrl = undefined;
/**
* @private
* @member {RouterURLConf}
* @description
* Stores route which matches with the current URL against registered URL patterns.
* */
let currentRoute = undefined;
/**
* @private
* @member {Object<String, Function|undefined>}
* @description
* Stores listeners that will be called on routing.
* */
let listeners = {};
/**
* @typedef {Function} AdharaRouterMiddleware
* @param {Object} params - url parameters
* @param {String } params.view_name - name of the page that is being routed to
* @param {String} params.path - path that is being routed to
* @param {Object} params.query_params - url query parameters
* @param {Object} params.path_params - url path parameters
* @param {Function} route - Proceed with routing after making necessary checks in middleware
* */
/**
* @private
* @member {Array<AdharaRouterMiddleware>}
* @description
* Stores middlewares that will be called on routing.
* */
let middlewares = [];
/**
* @function
* @private
* @returns {String} Current window URL. IE compatibility provided by Hostory.js (protocol://domain/path?search_query).
* */
function getFullUrl(){
return window.location.href;
// return History.getState().cleanUrl;
}
/**
* @function
* @private
* @returns {String} Full URL without search query (protocol://domain/path).
* */
function getBaseUrl(){
return getFullUrl().split('?')[0];
}
/**
* @function
* @private
* @returns {String} URL Path (path).
* */
function getPathName(){
return AdharaRouter.transformURL(window.location.pathname);
}
/**
* @function
* @private
* @returns {String} URL Path with search query (/path?search_query).
* */
function getFullPath(){
return getPathName()+window.location.search+window.location.hash;
// return "/"+getFullUrl().split('://')[1].substring(window.location.host.length+1);
}
/**
* @function
* @private
* @returns {String} Search query (search_query).
* */
function getSearchString(){
return window.location.search.substring(1);
}
/**
* @function
* @private
* @returns {String} Hash (text after `#` from the url).
* */
function getHash(){
return window.location.hash.substring(1);
}
/**
* @function
* @private
* @param {String} new_path - URL path.
* @returns {Boolean} Whether new_path matches with current URL path.
* */
function isCurrentPath(new_path){
function stripSlash(path){
if(path.indexOf('/') === 0){
return path.substring(1);
}
return path;
}
return stripSlash(getFullPath()) === stripSlash(new_path);
}
function callMiddlewares(params, proceed){
let i=0;
(function _proceed(){
let middleware_fn = middlewares[i++];
if(middleware_fn){
call_fn(middleware_fn, params, _proceed);
}else{
proceed();
}
})();
}
/**
* @function
* @private
* @description matches the current URL and returns the configuration, path and params
* @returns Object
* */
function matchUrl(){
let path = getPathName();
let matchFound = false;
for(let regex in registeredUrlPatterns){
if(registeredUrlPatterns.hasOwnProperty(regex) && !matchFound) {
let formed_regex = new RegExp(regex);
if (formed_regex.test(path)) {
matchFound = true;
let opts = registeredUrlPatterns[regex];
let params = formed_regex.exec(path);
return {opts, path, params, meta: opts.meta};
}
}
}
}
/**
* @function
* @private
* @description
* Routes to the current URL.
* Looks up in the registered URL patterns for a match to current URL.
* If match found, view function will be called with regex path matches in the matched order and query param's as the last argument.
* Current view name will be set to the view name configured against view URL.
* @returns {Boolean} Whether any view function is found or not.
* */
function matchAndCall(){
let matchOptions = matchUrl();
if(matchOptions){
let {opts, path, params, meta} = matchOptions;
params.splice(0,1);
if(opts && opts.fn){
let _pathParams = {};
for(let [index, param] of params.entries()){
_pathParams[opts.path_param_keys[index]] = param;
}
currentRoute = opts;
//Setting current routing params...
pathParams = _pathParams;
currentUrl = getFullUrl();
fetchQueryParams();
callMiddlewares({
view_name: opts.view_name,
path: path,
query_params: getQueryParams(),
path_params: _pathParams
}, () => {
params.push(queryParams);
document.title = [(appName || defaultTitle), meta.title].filter(_=>_).join(" | ");
if(opts.fn.constructor instanceof AdharaView.constructor){
Adhara.onRoute(opts.fn, params);
}else{
opts.fn.apply(this, params);
}
});
}
}
return !!matchOptions;
}
let curr_path;
function updateHistoryStack(){
if(curr_path){
historyStack.push(curr_path);
}
curr_path = getFullPath();
}
/**
* @private
* @member {Boolean}
* @description
* This flag will be set to true when set to true, {@link Router.route} will not call the view function.
* */
let settingURL = false;
/**
* @function
* @private
* @description
* Updates the {AdharaRouter~queryParams} with the current URL parameters
* */
function updateParams(){
if(getFullUrl() !== currentUrl){
currentUrl = getFullUrl();
fetchQueryParams();
}
}
/**
* @function
* @private
* @description
* Fetches the search query from current URL and transforms it to query param's object.
* @returns {Object<String, String>} The search query will be decoded and returned as an object.
* */
function getQueryParams(){
let qp = {};
if(getSearchString()){
loop(getSearchString().split('&'), function(i, paramPair){
paramPair = paramPair.split('=');
qp[decodeURIComponent(paramPair[0])] = decodeURIComponent(paramPair[1]);
});
}else{
qp = {};
}
return qp;
}
/**
* @function
* @private
* @description
* Fetches the search query from current URL and transforms it to query param's.
* The search query will be decoded and stored.
* */
function fetchQueryParams(){
queryParams = getQueryParams();
}
/**
* @function
* @private
* @returns {String} URL constructed by current base URL and current Query Parameters.
* */
function generateCurrentUrl(){
let url = getPathName();
let params = [];
loop(queryParams, function(key, value){
params.push(encodeURIComponent(key)+"="+encodeURIComponent(value));
});
if(params.length){
url+="?"+params.join("&");
}
return url;
}
/**
* @function
* @private
* @param {RouteType} [route_type=this.routeTypes.NAVIGATE] - How to route.
* @param {Object} route_options - Route options.
* @param {boolean} [route_options.force=false] - Whether to force navigate URL or not. Will call view function even if current URL is same. Applies for {@link RouteType.NAVIGATE}.
* @see {@link RouteType}
* */
function _route(route_type, route_options){
if(!route_type || route_type===AdharaRouter.RouteTypes.SET){
AdharaRouter.setURL(generateCurrentUrl());
}else if(route_type===AdharaRouter.RouteTypes.NAVIGATE){
AdharaRouter.navigateTo(generateCurrentUrl(), (route_options && route_options.force)||false);
}else if(route_type===AdharaRouter.RouteTypes.OVERRIDE){
AdharaRouter.overrideURL(generateCurrentUrl());
}else if(route_type===AdharaRouter.RouteTypes.UPDATE){
AdharaRouter.updateURL(generateCurrentUrl());
}
}
const STATE_KEY = "__adhara_router__";
class Router{
/**
* @function
* @private
* @param {String} url - path
* @description
* modified the URL and returns the new value. Default transformer just returns the passed url/path.
* */
static transformURL(url){
if(url.startsWith(baseURI)){
return url;
}
return baseURI+url;
}
/**
* @callback ViewFunction
* @param {String} regexMatchedParams - matched regex params.
* @param {String} searchQuery - search query parameters converted to object form.
* @description
* View callback function.
* Parameters passed will be the regex matches and last parameter will be searchQueryParameters.
* In case of more than 1 path regex match, ViewFunction will be called back with (match1, match2, ..., searchQuery)
* */
/**
* @function
* @static
* @param {String} pattern - URL Pattern that is to be registered.
* @param {String} view_name - Name of the view that is mapped to this URL.
* @param {ViewFunction|Adhara} fn - View function that will be called when the pattern matches window URL.
* @param {Object} meta - Route meta which can be accessible for current route. This can be used for miscellaneous operations like finding which tab to highlight on a sidebar.
routes * */
static register_one(pattern, view_name, fn, meta){
let path_param_keys = [];
let regex = /{{([a-zA-Z$_][a-zA-Z0-9$_]*)}}/g;
let match = regex.exec(pattern);
while (match != null) {
path_param_keys.push(match[1]);
match = regex.exec(pattern);
}
pattern = pattern.replace(new RegExp("\\{\\{[a-zA-Z$_][a-zA-Z0-9$_]*\\}\\}", "g"), '');
//Removing all {{XYZ}} iff, XYZ matches first character as an alphabet ot $ or _
pattern = "^"+this.transformURL(pattern.substring(1));
pattern = pattern.replace(/[?]/g, '\\?'); //Converting ? to \? as RegExp(pattern) dosen't handle that
meta = meta || {};
registeredUrlPatterns[pattern] = { view_name, fn, path_param_keys, meta };
}
/**
* @function
* @static
* @param {RegExp} regex - URL Pattern that is to be unregistered.
* @description
* Once unregistered, this URL regex will be removed from registered URL patterns and on hitting that URL,
* will be routed to a 404.
* */
static deRegister(regex){
registeredUrlPatterns[regex] = undefined;
}
/**
* Bulk url conf
* @typedef {Object} RouterURLConf
* @property {string} url - url regex
* @property {String} view_name - view name
* @property {Object} path_params - Path params
* @property {Function|class} view - view
* */
/**
* @function
* @static
* @param {Array<RouterURLConf>} list - list of url configurations
* @description
* Register's urls in the list iteratively...
* */
static register(list){
for(let conf of list){
this.register_one(conf.url, conf.view_name, conf.view, conf.meta);
}
}
/**
* @typedef {Object} AdharaRouterConfiguration - Adhara router configuration
* @property {Array<RouterURLConf>} routes - Route configurations
* @property {String} base_uri - base url after which the matches will be taken care of.
* For example, if '/ui' is given as base URL, then /ui/home will match a route regex with '/home' only
* @property {Object<String, Function>} on_route_listeners - On Route listeners
* @property {Array<AdharaRouterMiddleware>} middlewares - Middleware functions
* */
/**
* @function
* @static
* @param {AdharaRouterConfiguration} router_configuration
* */
static configure(router_configuration){
if(router_