/*! * Tempo Template Engine 1.6 * * http://tempojs.com/ */ function TempoEvent(type, item, element) { this.type = type; this.item = item; this.element = element; return this; } TempoEvent.Types = { RENDER_STARTING : 'render_starting', ITEM_RENDER_STARTING : 'item_render_starting', ITEM_RENDER_COMPLETE : 'item_render_complete', RENDER_COMPLETE : 'render_complete' }; var Tempo = (function (tempo) { /*! * Constants */ var NUMBER_FORMAT_REGEX = /(\d+)(\d{3})/; /*! * Helpers */ var utils = { memberRegex : function (obj) { var member_regex = ''; for (var member in obj) { if (obj.hasOwnProperty(member)) { if (member_regex.length > 0) { member_regex += '|'; } member_regex += member; } } return member_regex; }, pad : function (val, pad, size) { while (val.length < size) { val = pad + val; } return val; }, trim : function (str) { return str.replace(/^\s*([\S\s]*?)\s*$/, '$1'); }, startsWith : function (str, prefix) { return (str.indexOf(prefix) === 0); }, clearContainer : function (el) { if (el !== undefined && el.childNodes !== undefined) { for (var i = el.childNodes.length; i >= 0; i--) { if (el.childNodes[i] !== undefined && el.childNodes[i].getAttribute !== undefined && el.childNodes[i].getAttribute('data-template') !== null) { el.childNodes[i].parentNode.removeChild(el.childNodes[i]); } } } }, isNested : function (el) { var p = el.parentNode; while (p) { if (p.getAttribute !== undefined && p.getAttribute('data-template') !== null) { return true; } p = p.parentNode; } return false; }, equalsIgnoreCase : function (str1, str2) { return str1.toLowerCase() === str2.toLowerCase(); }, getElement : function (template, html) { if (utils.equalsIgnoreCase(template.tagName, 'tr')) { // Wrapping to get around read-only innerHTML var el = document.createElement('div'); el.innerHTML = '' + html + '
'; var depth = 3; while (depth--) { el = el.lastChild; } return el; } else { // No need to wrap template.innerHTML = html; return template; } }, typeOf : function (obj) { if (typeof(obj) === "object") { if (obj === null) { return "null"; } if (obj.constructor === ([]).constructor) { return "array"; } if (obj.constructor === (new Date()).constructor) { return "date"; } if (obj.constructor === (new RegExp()).constructor) { return "regex"; } return "object"; } return typeof(obj); }, notify : function (listener, event) { if (listener !== undefined) { listener(event); } } }; function Templates(params, nestedItem) { this.params = params; this.defaultTemplate = null; this.namedTemplates = {}; this.container = null; this.nestedItem = nestedItem !== undefined ? nestedItem : null; this.var_brace_left = '\\{\\{'; this.var_brace_right = '\\}\\}'; this.tag_brace_left = '\\{%'; this.tag_brace_right = '%\\}'; if (typeof params !== 'undefined') { for (var prop in params) { if (prop === 'var_braces') { this.var_brace_left = params[prop].substring(0, params[prop].length / 2); this.var_brace_right = params[prop].substring(params[prop].length / 2); } else if (prop === 'tag_braces') { this.tag_brace_left = params[prop].substring(0, params[prop].length / 2); this.tag_brace_right = params[prop].substring(params[prop].length / 2); } else if (typeof this[prop] !== 'undefined') { this[prop] = params[prop]; } } } return this; } Templates.prototype = { parse: function (container) { this.container = container; var children = container.getElementsByTagName('*'); for (var i = 0; i < children.length; i++) { if (children[i].getAttribute !== undefined && children[i].getAttribute('data-template') !== null && (this.nestedItem === children[i].getAttribute('data-template') || children[i].getAttribute('data-template') === '' || children[i].getAttribute('data-template') === 'data-template' && !utils.isNested(children[i]))) { this.createTemplate(children[i]); } else if (children[i].getAttribute !== undefined && children[i].getAttribute('data-template-fallback') !== null) { // Hiding the fallback template children[i].style.display = 'none'; } } // If there is no default template (data-template) then create one from container if (this.defaultTemplate === null) { // Creating a template inside the container var el = document.createElement('div'); el.setAttribute('data-template', ''); el.innerHTML = this.container.innerHTML; // Clearing container before adding the wrapped contents this.container.innerHTML = ''; // There is now a default template present with a data-template attribute this.container.appendChild(el); this.createTemplate(el); } utils.clearContainer(this.container); }, createTemplate : function (node) { var element = node.cloneNode(true); // Clear display: none; if (element.style.removeAttribute) { element.style.removeAttribute('display'); } else { element.style.removeProperty('display'); } // Remapping container element in case template // is deep in container this.container = node.parentNode; // Element is a template var nonDefault = false; for (var a = 0; a < element.attributes.length; a++) { var attr = element.attributes[a]; // If attribute if (utils.startsWith(attr.name, 'data-if-')) { var val; if (attr.value === '') { val = true; } else { val = '\'' + attr.value + '\''; } this.namedTemplates[attr.name.substring(8, attr.name.length) + '==' + val] = element; element.removeAttribute(attr.name); nonDefault = true; } } // Setting as default template, last one wins if (!nonDefault) { this.defaultTemplate = element; } }, templateFor: function (i) { for (var templateName in this.namedTemplates) { if (eval('i.' + templateName)) { return this.namedTemplates[templateName].cloneNode(true); } } if (this.defaultTemplate) { return this.defaultTemplate.cloneNode(true); } } }; /*! * Renderer for populating containers with data using templates. */ function Renderer(templates) { this.templates = templates; this.listener = undefined; this.started = false; this.varRegex = new RegExp(this.templates.var_brace_left + '[ ]?([A-Za-z0-9$\\._\\[\\]]*?)([ ]?\\|[ ]?.*?)?[ ]?' + this.templates.var_brace_right, 'g'); this.tagRegex = new RegExp(this.templates.tag_brace_left + '[ ]?([\\s\\S]*?)( [\\s\\S]*?)?[ ]?' + this.templates.tag_brace_right + '(([\\s\\S]*?)(?=' + this.templates.tag_brace_left + '[ ]?end\\1[ ]?' + this.templates.tag_brace_right + '))?', 'g'); return this; } Renderer.prototype = { notify : function (listener) { this.listener = listener; return this; }, _replaceVariables : function (renderer, _tempo, i, str) { return str.replace(this.varRegex, function (match, variable, args) { try { var val = null; // Handling tempo_info variable if (utils.startsWith(variable, '_tempo.')) { return eval(variable); } if (utils.typeOf(i) === 'array') { val = eval('i' + variable); } else { val = eval('i.' + variable); } // Handle filters var filterSplitter = new RegExp('\\|[ ]?(?=' + utils.memberRegex(renderer.filters) + ')', 'g'); if (args !== undefined && args !== '') { var filters = utils.trim(utils.trim(args).substring(1)).split(filterSplitter); for (var p = 0; p < filters.length; p++) { var filter = utils.trim(filters[p]); var filter_args = []; // If there is a space, there must be arguments if (filter.indexOf(' ') > -1) { var f = filter.substring(filter.indexOf(' ')).replace(/^[ ']*|[ ']*$/g, ''); filter_args = f.split(/(?:[\'"])[ ]?,[ ]?(?:[\'"])/); filter = filter.substring(0, filter.indexOf(' ')); } val = renderer.filters[filter](val, filter_args); } } if (val !== undefined) { return val; } } catch (err) { } return ''; }); }, _replaceObjects : function (renderer, _tempo, i, str) { var regex = new RegExp('(?:__[\\.]?)((_tempo|\\[|' + utils.memberRegex(i) + ')([A-Za-z0-9$\\._\\[\\]]+)?)', 'g'); return str.replace(regex, function (match, variable, args) { try { var val = null; // Handling tempo_info variable if (utils.startsWith(variable, '_tempo.')) { return eval(variable); } if (utils.typeOf(i) === 'array') { val = eval('i' + variable); } else { val = eval('i.' + variable); } if (val !== undefined) { if (utils.typeOf(val) === 'string') { return '\'' + val + '\''; } else { return val; } } } catch (err) { } return undefined; }); }, _applyAttributeSetters : function (renderer, item, str) { return str.replace(/([A-z0-9]+?)(?==).*?data-\1="(.*?)"/g, function (match, attr, data_value) { if (data_value !== '') { return attr + '="' + data_value + '"'; } return match; }); }, _applyTags : function (renderer, item, str) { return str.replace(this.tagRegex, function (match, tag, args, body) { if (renderer.tags.hasOwnProperty(tag)) { args = args.substring(args.indexOf(' ')).replace(/^[ ]*|[ ]*$/g, ''); var filter_args = args.split(/(?:['"])[ ]?,[ ]?(?:['"])/); return renderer.tags[tag](renderer, item, match, filter_args, body); } else { return ''; } }); }, starting : function () { // Use this to manually fire the RENDER_STARTING event e.g. just before you issue an AJAX request // Useful if you're not calling prepare immediately before render this.started = true; utils.notify(this.listener, new TempoEvent(TempoEvent.Types.RENDER_STARTING, undefined, undefined)); return this; }, renderItem : function (renderer, tempo_info, i, fragment) { var template = renderer.templates.templateFor(i); if (template && i) { utils.notify(this.listener, new TempoEvent(TempoEvent.Types.ITEM_RENDER_STARTING, i, template)); var nestedDeclaration = template.innerHTML.match(/data-template="(.*?)"/g); if (nestedDeclaration) { for (var p = 0; p < nestedDeclaration.length; p++) { var nested = nestedDeclaration[p].match(/"(.*?)"/)[1]; var t = new Templates(renderer.templates.params, nested); t.parse(template); var r = new Renderer(t); r.render(eval('i.' + nested)); } } // Dealing with HTML as a String from now on (to be reviewed) // Attribute values are escaped in FireFox so making sure there are no escaped tags var html = template.innerHTML.replace(/%7B%7B/g, '{{').replace(/%7D%7D/g, '}}'); // Tags html = this._applyTags(this, i, html); // Content html = this._replaceVariables(this, tempo_info, i, html); // JavaScript objects html = this._replaceObjects(this, tempo_info, i, html); // Template class attribute if (template.getAttribute('class')) { template.className = this._replaceVariables(this, tempo_info, i, template.className); } // Template id if (template.getAttribute('id')) { template.id = this._replaceVariables(this, tempo_info, i, template.id); } html = this._applyAttributeSetters(this, i, html); fragment.appendChild(utils.getElement(template, html)); utils.notify(this.listener, new TempoEvent(TempoEvent.Types.ITEM_RENDER_COMPLETE, i, template)); } }, _createFragment : function (data) { if (data) { var tempo_info = {}; var fragment = document.createDocumentFragment(); // If object then wrapping in an array if (utils.typeOf(data) === 'object') { data = [data]; } for (var i = 0; i < data.length; i++) { tempo_info.index = i; this.renderItem(this, tempo_info, data[i], fragment); } return fragment; } return null; }, render : function (data) { // Check if starting event was manually fired if (!this.started) { utils.notify(this.listener, new TempoEvent(TempoEvent.Types.RENDER_STARTING, undefined, undefined)); } this.clear(); this.append(data); return this; }, append : function (data) { // Check if starting event was manually fired if (!this.started) { utils.notify(this.listener, new TempoEvent(TempoEvent.Types.RENDER_STARTING, undefined, undefined)); } var fragment = this._createFragment(data); if (fragment !== null) { this.templates.container.appendChild(fragment); } utils.notify(this.listener, new TempoEvent(TempoEvent.Types.RENDER_COMPLETE, undefined, undefined)); return this; }, prepend : function (data) { // Check if starting event was manually fired if (!this.started) { utils.notify(this.listener, new TempoEvent(TempoEvent.Types.RENDER_STARTING, undefined, undefined)); } var fragment = this._createFragment(data); if (fragment !== null) { this.templates.container.insertBefore(fragment, this.templates.container.firstChild); } utils.notify(this.listener, new TempoEvent(TempoEvent.Types.RENDER_COMPLETE, undefined, undefined)); return this; }, clear : function (data) { utils.clearContainer(this.templates.container); }, tags : { 'if' : function (renderer, i, match, args, body) { var member_regex = utils.memberRegex(i); var expr = args[0].replace(/&/g, '&'); expr = expr.replace(new RegExp(member_regex, 'gi'), function (match) { return 'i.' + match; }); var blockRegex = new RegExp(renderer.templates.tag_brace_left + '[ ]?else[ ]?' + renderer.templates.tag_brace_right, 'g'); var blocks = body.split(blockRegex); if (eval(expr)) { return blocks[0]; } else if (blocks.length > 1) { return blocks[1]; } return ''; } }, filters : { 'truncate' : function (value, args) { if (value !== undefined) { var len = 0; var rep = '...'; if (args.length > 0) { len = parseInt(args[0]); } if (args.length > 1) { rep = args[1]; } if (value.length > len - 3) { return value.substr(0, len - 3) + rep; } return value; } }, 'format' : function (value, args) { if (value !== undefined) { var x = (value + '').split('.'); var x1 = x[0]; var x2 = x.length > 1 ? '.' + x[1] : ''; while (NUMBER_FORMAT_REGEX.test(x1)) { x1 = x1.replace(NUMBER_FORMAT_REGEX, '$1' + ',' + '$2'); } return x1 + x2; } }, 'upper' : function (value, args) { return value.toUpperCase(); }, 'lower' : function (value, args) { return value.toLowerCase(); }, 'trim' : function (value, args) { return utils.trim(value); }, 'replace' : function (value, args) { if (value !== undefined && args.length === 2) { return value.replace(new RegExp(args[0], 'g'), args[1]); } return value; }, 'append' : function (value, args) { if (value !== undefined && args.length === 1) { return value + '' + args[0]; } return value; }, 'prepend' : function (value, args) { if (value !== undefined && args.length === 1) { return args[0] + '' + value; } return value; }, 'default' : function (value, args) { if (value !== undefined && value !== null) { return value; } if (args.length === 1) { return args[0]; } return value; }, 'date' : function (value, args) { if (value !== undefined && args.length === 1) { var date = new Date(value); var format = args[0]; if (format === 'localedate') { return date.toLocaleDateString(); } else if (format === 'localetime') { return date.toLocaleTimeString(); } else if (format === 'date') { return date.toDateString(); } else if (format === 'time') { return date.toTimeString(); } else { var MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; var DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; var DATE_PATTERNS = { 'YYYY' : function (date) { return date.getFullYear(); }, 'YY' : function (date) { return date.getFullYear().toFixed().substring(2); }, 'MMMM' : function (date) { return MONTHS[date.getMonth()]; }, 'MMM' : function (date) { return MONTHS[date.getMonth()].substring(0, 3); }, 'MM' : function (date) { return utils.pad((date.getMonth() + 1).toFixed(), '0', 2); }, 'M' : function (date) { return date.getMonth() + 1; }, 'DD' : function (date) { return utils.pad(date.getDate().toFixed(), '0', 2); }, 'D' : function (date) { return date.getDate(); }, 'EEEE' : function (date) { return DAYS[date.getDay()]; }, 'EEE' : function (date) { return DAYS[date.getDay()].substring(0, 3); }, 'E' : function (date) { return date.getDay(); }, 'HH' : function (date) { return utils.pad(date.getHours().toFixed(), '0', 2); }, 'H' : function (date) { return date.getHours(); }, 'mm' : function (date) { return utils.pad(date.getMinutes().toFixed(), '0', 2); }, 'm' : function (date) { return date.getMinutes(); }, 'ss' : function (date) { return utils.pad(date.getSeconds().toFixed(), '0', 2); }, 's' : function (date) { return date.getSeconds(); }, 'SSS' : function (date) { return utils.pad(date.getMilliseconds().toFixed(), '0', 3); }, 'S' : function (date) { return date.getMilliseconds(); }, 'a' : function (date) { return date.getHours() < 12 ? 'AM' : 'PM'; } }; format = format.replace(/(\\)?(Y{2,4}|M{1,4}|D{1,2}|E{1,4}|H{1,2}|m{1,2}|s{1,2}|S{1,3}|a)/g, function (match, escape, pattern) { if (!escape) { if (DATE_PATTERNS.hasOwnProperty(pattern)) { return DATE_PATTERNS[pattern](date); } } return pattern; }); return format; } } return ''; } } }; /*! * Prepare a container for rendering, gathering templates and * clearing afterwards. */ tempo.prepare = function (container, params) { if (typeof container === 'string') { container = document.getElementById(container); } var templates = new Templates(params); templates.parse(container); return new Renderer(templates); }; tempo.test = { 'utils' : utils, 'templates': new Templates({}), 'renderer' : new Renderer(new Templates({})) }; return tempo; })(Tempo || {});