UNPKG

adhara

Version:

foundation for any kind of website: microframework

1,448 lines (1,364 loc) 162 kB
/** * @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&&param2, "|" : param1|param2, "&" : param1&param2, "==" : param1==param2, "===": param1===param2, "!=" : param1!=param2, "!==": param1!==param2, ">" : param1>param2, "<" : param1<param2, ">=" : param1>=param2, "<=" : param1<=param2, "equals" : param1==param2, "and" : param1&&param2, "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_