1 | var Trackr = require("trackr");
|
2 | var _ = require("underscore");
|
3 | var NODE_TYPE = require("./types");
|
4 | var parse = require("./m+xml").parse;
|
5 | var utils = require("./utils");
|
6 | var View = require("./view");
|
7 | var Model = require("./model");
|
8 | var Section = require("./section");
|
9 | var $track = require("trackr-objects");
|
10 | var DOMRange = require("./domrange");
|
11 |
|
12 | var Mustache =
|
13 | module.exports = View.extend({
|
14 | constructor: function(data, options) {
|
15 | options = options || {};
|
16 |
|
17 |
|
18 | var template = options.template || _.result(this, "template");
|
19 | if (template != null) this.setTemplate(template);
|
20 |
|
21 |
|
22 | this.decorate(_.extend({}, options.decorators, _.result(this, "decorators")));
|
23 |
|
24 |
|
25 | View.call(this, data, options);
|
26 | },
|
27 |
|
28 |
|
29 | setTemplate: function(template) {
|
30 | if (_.isString(template)) template = parse(template);
|
31 |
|
32 | if (!_.isObject(template) || template.type !== NODE_TYPE.ROOT)
|
33 | throw new Error("Expecting string or parsed template.");
|
34 |
|
35 | this._template = template;
|
36 | return this;
|
37 | },
|
38 |
|
39 |
|
40 | decorate: function(name, fn, options) {
|
41 | if (typeof name === "object" && fn == null) {
|
42 | _.each(name, function(fn, n) {
|
43 | if (_.isArray(fn)) this.decorate(n, fn[0], fn[1]);
|
44 | else this.decorate(n, fn, options);
|
45 | }, this);
|
46 | return this;
|
47 | }
|
48 |
|
49 | if (typeof name !== "string" || name === "") throw new Error("Expecting non-empty string for decorator name.");
|
50 | if (typeof fn !== "function") throw new Error("Expecting function for decorator.");
|
51 |
|
52 | if (this._decorators == null) this._decorators = {};
|
53 | if (this._decorators[name] == null) this._decorators[name] = [];
|
54 | var decorators = this._decorators[name];
|
55 |
|
56 | if (!_.findWhere(decorators, { callback: fn })) {
|
57 | decorators.push({
|
58 | callback: fn,
|
59 | options: options || {}
|
60 | });
|
61 | }
|
62 |
|
63 | return this;
|
64 | },
|
65 |
|
66 |
|
67 | findDecorators: function(name) {
|
68 | var decorators = [],
|
69 | c = this;
|
70 |
|
71 |
|
72 | while (c != null) {
|
73 | if (c._decorators != null && _.isArray(c._decorators[name])) {
|
74 | c._decorators[name].forEach(function(d) {
|
75 | if (!_.findWhere(decorators, { callback: d.callback })) {
|
76 | decorators.push(_.extend({ context: c }, d));
|
77 | }
|
78 | });
|
79 | }
|
80 |
|
81 | c = c.parentRange;
|
82 | }
|
83 |
|
84 | return decorators;
|
85 | },
|
86 |
|
87 |
|
88 | stopDecorating: function(name, fn) {
|
89 | if (typeof name === "function" && fn == null) {
|
90 | fn = name;
|
91 | name = null;
|
92 | }
|
93 |
|
94 | if (this._decorators == null || (name == null && fn == null)) {
|
95 | this._decorators = {};
|
96 | }
|
97 |
|
98 | else if (fn == null) {
|
99 | delete this._decorators[name];
|
100 | }
|
101 |
|
102 | else if (name == null) {
|
103 | _.each(this._decorators, function(d, n) {
|
104 | this._decorators[n] = _.filter(d, function(_d) {
|
105 | return _d.callback !== fn;
|
106 | });
|
107 | }, this);
|
108 | }
|
109 |
|
110 | else {
|
111 | var d = this._decorators[name];
|
112 | this._decorators[name] = _.filter(d, function(_d) {
|
113 | return _d.callback !== fn;
|
114 | });
|
115 | }
|
116 |
|
117 | return this;
|
118 | },
|
119 |
|
120 |
|
121 | setPartial: function(name, partial) {
|
122 | if (_.isObject(name)) return View.prototype.setPartial.call(this, name);
|
123 |
|
124 | if (_.isString(partial)) partial = parse(partial);
|
125 | if (_.isObject(partial) && partial.type === NODE_TYPE.ROOT) partial = Mustache.extend({ template: partial });
|
126 | if (partial != null && !utils.isSubClass(View, partial))
|
127 | throw new Error("Expecting string template, parsed template, View subclass or function for partial.");
|
128 |
|
129 | return View.prototype.setPartial.call(this, name, partial);
|
130 | },
|
131 |
|
132 |
|
133 | render: function() {
|
134 | if (this._template == null)
|
135 | throw new Error("Expected a template to be set before rendering.");
|
136 |
|
137 | var toMount;
|
138 | this.setMembers(this.renderTemplate(this._template, null, toMount = []));
|
139 | _.invoke(toMount, "mount");
|
140 | },
|
141 |
|
142 |
|
143 | renderTemplate: function(template, view, toMount) {
|
144 | if (view == null) view = this;
|
145 | if (toMount == null) toMount = [];
|
146 | var self = this;
|
147 |
|
148 | if (_.isArray(template)) return template.reduce(function(r, t) {
|
149 | var b = self.renderTemplate(t, view, toMount);
|
150 | if (_.isArray(b)) r.push.apply(r, b);
|
151 | else if (b != null) r.push(b);
|
152 | return r;
|
153 | }, []);
|
154 |
|
155 | switch(template.type) {
|
156 | case NODE_TYPE.ROOT:
|
157 | return this.renderTemplate(template.children, view, toMount);
|
158 |
|
159 | case NODE_TYPE.ELEMENT:
|
160 | var part = this.renderPartial(template.name, view);
|
161 | var obj;
|
162 |
|
163 | if (part != null) {
|
164 | part.addData(obj = $track({}));
|
165 |
|
166 | template.attributes.forEach(function(attr) {
|
167 | self.autorun(function(c) {
|
168 | var val = this.renderArguments(attr.arguments, view);
|
169 | if (val.length === 1) val = val[0];
|
170 | else if (!val.length) val = void 0;
|
171 |
|
172 | if (c.firstRun) obj.defineProperty(attr.name, val);
|
173 | else obj[attr.name] = val;
|
174 | });
|
175 | });
|
176 |
|
177 | toMount.push(part);
|
178 | return part;
|
179 | }
|
180 |
|
181 | else {
|
182 | var el = document.createElement(template.name);
|
183 |
|
184 | template.attributes.forEach(function(attr) {
|
185 | if (this.renderDecorations(el, attr, view)) return;
|
186 |
|
187 | this.autorun(function() {
|
188 | el.setAttribute(attr.name, this.renderTemplateAsString(attr.children, view));
|
189 | });
|
190 | }, this);
|
191 |
|
192 | var children = this.renderTemplate(template.children, view, toMount),
|
193 | child, i;
|
194 |
|
195 | for (i in children) {
|
196 | child = children[i];
|
197 | if (child instanceof DOMRange) {
|
198 | child.parentRange = view;
|
199 | child.attach(el);
|
200 | } else {
|
201 | el.appendChild(child);
|
202 | }
|
203 | }
|
204 |
|
205 | return el;
|
206 | }
|
207 |
|
208 | case NODE_TYPE.TEXT:
|
209 | return document.createTextNode(utils.decodeEntities(template.value));
|
210 |
|
211 | case NODE_TYPE.HTML:
|
212 | return new DOMRange(utils.parseHTML(template.value));
|
213 |
|
214 | case NODE_TYPE.XCOMMENT:
|
215 | return document.createComment(template.value);
|
216 |
|
217 | case NODE_TYPE.INTERPOLATOR:
|
218 | var node = document.createTextNode("");
|
219 |
|
220 | this.autorun(function() {
|
221 | var val = view.get(template.value);
|
222 | node.nodeValue = typeof val === "string" ? val : val != null ? val.toString() : "";
|
223 | });
|
224 |
|
225 | return node;
|
226 |
|
227 | case NODE_TYPE.TRIPLE:
|
228 | var range = new DOMRange();
|
229 |
|
230 | this.autorun(function() {
|
231 | range.setMembers(utils.parseHTML(view.get(template.value)));
|
232 | });
|
233 |
|
234 | return range;
|
235 |
|
236 | case NODE_TYPE.INVERTED:
|
237 | case NODE_TYPE.SECTION:
|
238 | var section = new Section(view.model)
|
239 | .invert(template.type === NODE_TYPE.INVERTED)
|
240 | .setPath(template.value)
|
241 | .onRow(function() {
|
242 | var _toMount;
|
243 | this.setMembers(self.renderTemplate(template.children, this, _toMount = []));
|
244 | _.invoke(_toMount, "mount");
|
245 | });
|
246 |
|
247 | toMount.push(section);
|
248 | return section;
|
249 |
|
250 | case NODE_TYPE.PARTIAL:
|
251 | var partial = this.renderPartial(template.value, view);
|
252 | if (partial) toMount.push(partial);
|
253 | return partial;
|
254 | }
|
255 | },
|
256 |
|
257 |
|
258 | renderTemplateAsString: function(template, ctx) {
|
259 | if (ctx == null) ctx = this;
|
260 | if (ctx instanceof View) ctx = ctx.model;
|
261 | var self = this;
|
262 |
|
263 | if (_.isArray(template)) return template.map(function(t) {
|
264 | return self.renderTemplateAsString(t, ctx);
|
265 | }).filter(function(b) { return b != null; }).join("");
|
266 |
|
267 | switch(template.type) {
|
268 | case NODE_TYPE.ROOT:
|
269 | return this.renderTemplateAsString(template.children, ctx);
|
270 |
|
271 | case NODE_TYPE.TEXT:
|
272 | return template.value;
|
273 |
|
274 | case NODE_TYPE.INTERPOLATOR:
|
275 | case NODE_TYPE.TRIPLE:
|
276 | var val = ctx.get(template.value);
|
277 | return val != null ? val.toString() : "";
|
278 |
|
279 | case NODE_TYPE.SECTION:
|
280 | case NODE_TYPE.INVERTED:
|
281 | var inverted, model, val, isEmpty, makeRow, proxy, isList;
|
282 |
|
283 | inverted = template.type === NODE_TYPE.INVERTED;
|
284 | val = ctx.get(template.value);
|
285 | model = new Model(val, ctx);
|
286 | proxy = model.getProxyByValue(val);
|
287 | isList = model.callProxyMethod(proxy, val, "isList");
|
288 | isEmpty = Section.isEmpty(model, proxy);
|
289 |
|
290 | makeRow = function(i) {
|
291 | var row, data;
|
292 |
|
293 | if (i == null) {
|
294 | data = model;
|
295 | } else {
|
296 | data = model.callProxyMethod(proxy, val, "get", i);
|
297 | data = new Model(data, new Model({ $key: i }, ctx));
|
298 | }
|
299 |
|
300 | return self.renderTemplateAsString(template.children, data);
|
301 | }
|
302 |
|
303 | if (!(isEmpty ^ inverted)) {
|
304 | return isList && !inverted ?
|
305 | model.callProxyMethod(proxy, val, "keys").map(makeRow).join("") :
|
306 | makeRow();
|
307 | }
|
308 | }
|
309 | },
|
310 |
|
311 |
|
312 | renderArguments: function(arg, ctx) {
|
313 | if (ctx == null) ctx = this;
|
314 | if (ctx instanceof View) ctx = ctx.model;
|
315 | var self = this;
|
316 |
|
317 | if (_.isArray(arg)) return arg.map(function(a) {
|
318 | return self.renderArguments(a, ctx);
|
319 | }).filter(function(b) { return b != null; });
|
320 |
|
321 | switch(arg.type) {
|
322 | case NODE_TYPE.INTERPOLATOR:
|
323 | return ctx.get(arg.value);
|
324 |
|
325 | case NODE_TYPE.LITERAL:
|
326 | return arg.value;
|
327 | }
|
328 | },
|
329 |
|
330 |
|
331 | renderDecorations: function(el, attr, ctx) {
|
332 | var self = this;
|
333 |
|
334 |
|
335 | var decorators = this.findDecorators(attr.name);
|
336 | if (!decorators.length) return;
|
337 |
|
338 |
|
339 | if (ctx == null) ctx = this;
|
340 | if (ctx instanceof View) ctx = ctx.model;
|
341 |
|
342 |
|
343 | return this.autorun(function(_comp) {
|
344 | decorators.forEach(function(d) {
|
345 | if (d.options && d.options.defer) _.defer(execDecorator);
|
346 | else execDecorator();
|
347 |
|
348 | function execDecorator() {
|
349 | var dcomp = self.autorun(function(comp) {
|
350 |
|
351 | var args = [ {
|
352 | target: el,
|
353 | model: ctx,
|
354 | view: self,
|
355 | template: attr,
|
356 | comp: comp,
|
357 | options: d.options
|
358 | } ];
|
359 |
|
360 |
|
361 | if (d.options && d.options.parse === "string") {
|
362 | args.push(self.renderTemplateAsString(attr.children, ctx));
|
363 | } else if (d.options == null || d.options.parse !== false) {
|
364 | args = args.concat(self.renderArguments(attr.arguments, ctx));
|
365 | }
|
366 |
|
367 |
|
368 | d.callback.apply(d.context || self, args);
|
369 | });
|
370 |
|
371 |
|
372 | _comp.onInvalidate(function() {
|
373 | dcomp.stop();
|
374 | });
|
375 | }
|
376 | });
|
377 | });
|
378 | }
|
379 |
|
380 | }, {
|
381 |
|
382 | render: function(template, data, options) {
|
383 | options = _.extend({}, options || {}, {
|
384 | template: template
|
385 | });
|
386 |
|
387 | return new Mustache(data || null, options);
|
388 | }
|
389 |
|
390 | });
|