1 /**
  2  * The purpose of this templating engine is to allow for extremely easy display and
  3  * management of data within the context of the JSWidget environment. By rendering
  4  * individual tokens for your data, you can create very robust environments in which
  5  * your widgets can reside.
  6  * 
  7  * @fileoverview
  8  * @author Garth Henson (<a href="http://www.guahanweb.com">Guahan Web</a>)
  9  * @version 0.1
 10  */
 11 
 12 JSWidgets.Template = Class.extend(/** @lends Template.prototype */{
 13     /**
 14      * Basic front-end templating engine to assist in granular updates of widgets
 15      * @constructs
 16      * @class Basic front-end templating engine allowing for easy data manipulation and presentation.
 17      * @author Garth Henson (<a href="http://www.guahanweb.com">Guahan Web</a>)
 18      * @version 0.1
 19      * @requires TemplateToken
 20      * @requires ItemRegistry
 21      * @param {Object} opts Options used to configure the template
 22      */
 23     construct : function(opts) {
 24         // Set defaults
 25         this.null_highlight = (opts.null_highlight || false) ? opts.null_highlight : false;
 26         this.null_highlight_color = (opts.null_highlight_color || false) ? opts.null_highlight_color : '#fff000';
 27         this.null_tooltip = (opts.null_tooltip || false) ? opts.null_tooltip : false;
 28         if (this.null_tooltip) {
 29             this.tooltip = $("<p id=\"jswidgets-tpl-null-tooltip\">" + this.null_tooltip.msg + "</p>\n").hide().appendTo('body').css({
 30                 border : '1px solid #aaaaaa',
 31                 padding: '10px',
 32                 fontSize: '11px',
 33                 fontFamily: 'Verdana, Arial, sans-serif',
 34                 backgroundColor: '#ffffff',
 35                 position: 'absolute'
 36             });
 37         }
 38         
 39         this.tokens = new ItemRegistry({
 40             type : JSWidgets.TemplateToken
 41         });
 42         this.pattern = /\$\{\{(.+?)\}\}/gm;
 43     },
 44     
 45     /**
 46      * Loads the template from preexisting markup within the DOM. The raw template is
 47      * extracted from the innerHTML of the provided <code>id</code> element.
 48      * @public
 49      * @param {string} id The ID attribute of the container element
 50      */
 51     loadTemplateFromId : function(id) {
 52         this.tpl = $('#' + id);
 53         this.raw = this.tpl.html();
 54         this.findTokens();
 55     },
 56     
 57     /**
 58      * Loads the provided <code>txt</code> as the raw template content.
 59      * @public
 60      * @param {string} txt The template text to use
 61      */
 62     loadTemplateText : function(txt) {
 63         this.tpl = $(txt);
 64         this.raw = this.tpl.html();
 65         this.findTokens();
 66     },
 67     
 68     /**
 69      * Loads the provided <code>key => value</code> pairs as data to replace into the template.
 70      * The provided keys are expected to match to the name of the {@link Token} which is to be replaced.
 71      * @public
 72      * @param {object} obj The <code>key => value</code> pairs to be assigned
 73      */
 74     loadValues : function(obj) {
 75         for (var k in obj) {
 76             var tok = this.tokens.checkout(k);
 77             tok.setValue(obj[k]);
 78             this.tokens.checkin(tok);
 79         }
 80     },
 81     
 82     /**
 83      * Looks in the template for anything formed as ${{xxx}}
 84      * Default values can be defined as ${{xxx|default: value}}
 85      * If no default is defined, token will be left blank when rendered
 86      * @private
 87      */
 88     findTokens : function() {
 89         this.tokens.clear();
 90         var tokens = this.raw.match(this.pattern);
 91         for (var i = 0; i < tokens.length; i++) {
 92             this.tokens.checkin(new JSWidgets.TemplateToken(tokens[i]));
 93         }
 94     },
 95     
 96     /**
 97      * Using the raw template text and the assigned token values that have been loaded, render
 98      * the output text to be displayed to the user. If the <code>ret</code> parameter is passed
 99      * as a boolean <i>true</i>, the text is returned to the caller. Otherwise, this method
100      * returns <i>true</i> upon completion.
101      * @public
102      * @param {boolean} ret Whether or not to return the rendered text | defaults to <i>false</i>
103      * @returns {boolean|string} Boolean by default or rendered output upon request
104      */
105     render : function(ret) {
106         var txt  = this.raw;
107         for (var i = 0; i < this.tokens.length; i++) {
108             if (this.tokens.get(i).hasValue()) {
109                 txt = this.tokens.get(i).replaceIn(txt);
110             } else if (this.null_highlight) {
111                 // Highlight token
112                 var hl_txt = '<span style="background-color: ' + this.null_highlight_color + ';" class="tpl-null-token">' + this.tokens.get(i).raw + '</span>';
113                 txt = txt.replace(this.tokens.get(i).raw, hl_txt);
114             }
115         }
116         
117         if (ret || false) {
118             return txt;
119         }
120         
121         this.tpl.html(txt);
122         if (this.null_tooltip) {
123             var that = this;
124             $('span.tpl-null-token').hover(function(e) {
125                 that.showTooltip(e);
126             }, function(e) {
127                 that.hideTooltip(e);
128             });
129         }
130         return true;
131     },
132     
133     /**
134      * Render the template output to the specified <code>id</code> element
135      * @public
136      * @param {string} id ID attribute of the DOM element into which the output should be rendered
137      */
138     renderTo : function(id) {
139         $('#' + id).html(this.render(true));
140     },
141     
142     /**
143      * Shows this object's tooltip in relation to the hovered element
144      * @private
145      * @param {event} e The <code>mouseover</code> event
146      */
147     showTooltip : function(e) {
148         e.preventDefault();
149         var offset = $(e.target).offset();
150         var that = this;
151         this.tooltip.css({
152             top  : offset.top - that.tooltip.outerHeight() + 'px',
153             left : offset.left + 'px'
154         }).show();
155     },
156     
157     /**
158      * Hides this object's tooltip display
159      * @private
160      * @param {event} e The <code>mouseout</code> event
161      */
162     hideTooltip : function(e) {
163         e.preventDefault();
164         this.tooltip.hide();
165     }
166 });
167 
168 JSWidgets.TemplateToken = Class.extend(/** @lends TemplateToken.prototype */{
169     /**
170      * Individual token objects used by {@link Template} objects to render their appropriate
171      * content. TemplateToken syntax is expected as: <code>${{<title>|<mod_name>="<mod_value>"}}</code>
172      * where:
173      * <table>
174      *  <tr>
175      *      <td><b>title</b>:</td>
176      *      <td>The title of the current token (used to assign values)</td>
177      *  </tr>
178      *  <tr>
179      *      <td><b>mod_name</b>:</td>
180      *      <td>Name of the modifier(s). Valid modifiers are covered in {@link TemplateToken#parseToken}.</td>
181      *  </tr>
182      *  <tr>
183      *      <td><b>mod_value</b>:</td>
184      *      <td>Value of the current modifier (ie, <code>type="text"</code>)</td>
185      *  </tr>
186      * </table>
187      * @constructs
188      * @class Token objects used by {@link Template} to apply and display data granularly.
189      * @author Garth Henson (<a href="http://www.guahanweb.com">Guahan Web</a>)
190      * @version 0.1
191      * @param {Object} opts Options used to configure the template
192      */
193     construct : function(txt) {
194         // Set defaults
195         this.value = null;
196         this.type  = 'text';
197         this.decimals = 0;
198         this.parseToken(txt);
199     },
200     
201     /**
202      * Parses the provided <code>txt</code> as a token, retrieving the provided modifiers
203      * and title. Currently supported modifiers are as follows:
204      * <table>
205      *  <tr>
206      *      <td><b>default</b></td>
207      *      <td>The default value to render for this token when none is provided</td>
208      *  </tr>
209      *  <tr>
210      *      <td><b>type</b></td>
211      *      <td>The type of value this token represents ("text" or "number")</td>
212      *  </tr>
213      *  <tr>
214      *      <td><b>decimals</b></td>
215      *      <td>The number of decimals to which to round (associated with "number" type)</td>
216      *  </tr>
217      * </table>
218      * @private
219      * @param {string} txt The raw token text
220      */
221     parseToken : function(txt) {
222         this.raw = txt;
223         var inner = txt.substring(0, txt.length - 2);
224         this.inner = inner.substring(3);
225         
226         var parts = this.inner.split('|', 2);
227         this.name = parts[0];
228         if (parts.length > 1) {
229             // Options were provided, check for them
230             // Valid options supported currently: default|type
231             var pattern = '(default|type|decimals)=\"(.*?)\"';
232             var d = parts[1].match(new RegExp(pattern, 'g'));
233             if (null !== d) {
234                 for (var i = 0; i < d.length; i++) {
235                     var x = d[i].match(new RegExp(pattern));
236                     switch(x[1]) {
237                         case 'type':
238                             var valid = ['text', 'number'];
239                             for (var j = 0; j < valid.length; j++) {
240                                 if (x[2] == valid[j]) {
241                                     this[x[1]] = x[2];
242                                     break;
243                                 }
244                             }
245                             break;
246                         
247                         case 'decimals':
248                             this[x[1]] = parseInt(x[2]);
249                             break;
250                         
251                         default:
252                             this[x[1]] = x[2];    
253                     }
254                 }
255             }
256         }
257         
258         // Set modifier for rounding
259         this.modifier = 0;
260         if (this.decimals > 0) {
261             var mod = 10;
262             for (var i = 1; i < this.decimals; i++) {
263                 mod *= 10;
264             }
265             this.modifier = mod;
266         }
267     },
268     
269     /**
270      * Sets the value of this token to the provided <code>val</code>, taking into account any
271      * modifiers in its calculation.
272      * @public
273      * @param {varied} val The value to be assigned
274      */
275     setValue : function(val) {
276         switch (this.type) {
277             case 'number':
278                 val = parseFloat(val);
279                 this.value = Math.round(this.modifier * val) / this.modifier;
280                 break;
281             
282             default:
283                 this.value = val.toString();
284         }
285     },
286     
287     /**
288      * Gets the value of the token, taking into account any default values that have been defined.
289      * @public
290      * @returns {varied} The current value
291      */
292     getValue : function() {
293         if (this.value !== null) {
294             return this.value;
295         }
296         
297         if (this['default'] !== null) {
298             return this['default'];
299         }
300         
301         return false;
302     },
303     
304     /**
305      * Returns a boolean <code>true|false</code> representing whether or not there is a returnable value.
306      * @public
307      * @returns {boolean} Whether or not there is a value assigned
308      */
309     hasValue : function() {
310         return !!this.getValue();
311     },
312     
313     /**
314      * Replaces the current value for any occurrences of the raw token string in the provided <code>txt</code>
315      * @public
316      * @param {string} txt The text on which to run the replacement
317      * @returns {string} The modified text string
318      */
319     replaceIn : function(txt) {
320         return txt.replace(this.raw, this.getValue());
321     }
322 });