1 | | ### |
2 | | Copyright 2013 Aral Balkan <aral@aralbalkan.com> |
3 | | Copyright 2012 mocking@gmail.com |
4 | | |
5 | | Licensed under the Apache License, Version 2.0 (the "License"); |
6 | | you may not use this file except in compliance with the License. |
7 | | You may obtain a copy of the License at |
8 | | |
9 | | http://www.apache.org/licenses/LICENSE-2.0 |
10 | | |
11 | | Unless required by applicable law or agreed to in writing, software |
12 | | distributed under the License is distributed on an "AS IS" BASIS, |
13 | | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
14 | | See the License for the specific language governing permissions and |
15 | | limitations under the License. |
16 | | |
17 | | Forked from Distal by mocking@gmail.com (https://code.google.com/p/distal/) |
18 | | ### |
19 | | |
20 | 0 | tally = (root, obj) -> |
21 | 0 | "use strict" |
22 | | |
23 | | # Create a duplicate object which we can add properties to without affecting the original. |
24 | 0 | wrapper = -> |
25 | | |
26 | 0 | wrapper:: = obj |
27 | 0 | obj = new wrapper() |
28 | 0 | resolve = tally.resolve |
29 | 0 | node = root |
30 | 0 | doc = root.ownerDocument |
31 | 0 | querySelectorAll = !!root.querySelectorAll |
32 | | |
33 | | # Create an empty options object if one was not passed so we don’t have to keep checking for it later. |
34 | 0 | obj.__tally = {} if obj.__tally is undefined |
35 | | |
36 | | # Shortcut to flag: are we running on the server? |
37 | 0 | isRunningOnServer = obj.__tally.server |
38 | | |
39 | | # Render static option. |
40 | 0 | shouldRenderStatic = isRunningOnServer and obj.__tally.renderStatic |
41 | | |
42 | | # Optimize comparison check. |
43 | 0 | innerText = (if "innerText" of root then "innerText" else "textContent") |
44 | | |
45 | | # Attributes that don't support setAttribute() |
46 | 0 | altAttr = |
47 | | className: 1 |
48 | | class: 1 |
49 | | innerHTML: 1 |
50 | | style: 1 |
51 | | src: 1 |
52 | | href: 1 |
53 | | id: 1 |
54 | | value: 1 |
55 | | checked: 1 |
56 | | selected: 1 |
57 | | label: 1 |
58 | | htmlFor: 1 |
59 | | text: 1 |
60 | | title: 1 |
61 | | disabled: 1 |
62 | | |
63 | 0 | formInputHasBody = |
64 | | BUTTON: 1 |
65 | | LABEL: 1 |
66 | | LEGEND: 1 |
67 | | FIELDSET: 1 |
68 | | OPTION: 1 |
69 | | |
70 | | |
71 | | # TAL attributes for querySelectorAll call |
72 | 0 | qdef = tally |
73 | 0 | attributeWillChange = qdef.attributeWillChange |
74 | 0 | textWillChange = qdef.textWillChange |
75 | 0 | qif = qdef.qif or "data-tally-if" |
76 | 0 | qrepeat = qdef.qrepeat or "data-tally-repeat" |
77 | 0 | qattr = qdef.qattr or "data-tally-attribute" |
78 | 0 | qtext = qdef.qtext or "data-tally-text" |
79 | 0 | qdup = qdef.qdup or "data-tally-dummy" |
80 | | |
81 | | # Output formatter. |
82 | 0 | format = qdef.format |
83 | 0 | qdef = qdef.qdef or "data-tally-alias" |
84 | 0 | TAL = "*[" + [qdef, qif, qrepeat, qattr, qtext].join("],*[") + "]" |
85 | 0 | html = undefined |
86 | 0 | getProp = (s) -> |
87 | 0 | this[s] |
88 | | |
89 | | # There may be generated nodes that are siblings to the root node if the root node |
90 | | # itself was a repeater. Remove them so we don't have to deal with them later. |
91 | 0 | tmpNode = root.parentNode |
92 | 0 | tmpNode.removeChild node while (node = root.nextSibling) and (node.qdup or (node.nodeType is 1 and node.getAttribute(qdup))) |
93 | | |
94 | | # If we generate repeat nodes and are dealing with non-live NodeLists, then |
95 | | # we add them to the listStack[] and process them first as they won't appear inline |
96 | | # due to non-live NodeLists when we traverse our tree. |
97 | 0 | listStack = undefined |
98 | 0 | posStack = [0] |
99 | 0 | list = undefined |
100 | 0 | pos = 0 |
101 | 0 | attr = undefined |
102 | 0 | attr2 = undefined |
103 | 0 | `var undefined = {}._` |
104 | | |
105 | | # Get a list of concerned nodes within this root node. If querySelectorAll is |
106 | | # supported we use that but it is treated differently because it is a non-live NodeList. |
107 | 0 | if querySelectorAll |
108 | | |
109 | | # Remove all generated nodes (repeats), so we don't have to deal with them later. |
110 | | # Only need to do this for non-live NodeLists. |
111 | 0 | list = root.querySelectorAll("*[" + qdup + "]") |
112 | 0 | node.parentNode.removeChild node while (node = list[pos++]) |
113 | 0 | pos = 0 |
114 | | |
115 | 0 | listStack = [(if querySelectorAll then root.querySelectorAll(TAL) else root.getElementsByTagName("*"))] |
116 | | |
117 | 0 | list = [root] |
118 | | |
119 | 0 | loop |
120 | 0 | node = list[pos++] |
121 | | |
122 | | # When finished with the current list, there are generated nodes and |
123 | | # their children that need to be processed. |
124 | 0 | while not node and (list = listStack.pop()) |
125 | 0 | pos = posStack.pop() |
126 | 0 | node = list[pos++] |
127 | 0 | break unless node |
128 | | |
129 | | # Creates an alias for an object |
130 | | # e.g., <section data-tally-alias='feeds main.sidebar.feeds'> |
131 | 0 | attr = node.getAttribute(qdef) |
132 | 0 | if attr |
133 | 0 | attr = attr.split(" ") |
134 | | |
135 | | # Add it to the object as a property. |
136 | 0 | html = resolve(obj, attr[1]) |
137 | | |
138 | | # The 3rd parameter, if it exists, is a numerical index into the array. |
139 | 0 | if attr2 = attr[2] |
140 | 0 | obj["#"] = parseInt(attr2) + 1 |
141 | 0 | html = html[attr2] |
142 | 0 | obj[attr[0]] = html |
143 | | |
144 | | # Shown if object is truthy. |
145 | | # e.g., <img data-tally-if='item.unread'> <img data-tally-if='item.count isGreaterThan 1'> |
146 | 0 | attr = node.getAttribute(qif) |
147 | 0 | if attr |
148 | 0 | attr = attr.split(" ") |
149 | 0 | attr = [attr[0].substr(4), "not", 0] if attr[0].indexOf("not:") is 0 |
150 | 0 | obj2 = resolve(obj, attr[0]) |
151 | | |
152 | | # If obj is empty array it is still truthy, so make it the array length. |
153 | 0 | obj2 = obj2.length if obj2 and obj2.join and obj2.length > -1 |
154 | 0 | if attr.length > 2 |
155 | 0 | attr[2] = attr.slice(2).join(" ") if attr[3] |
156 | 0 | attr[2] *= 1 if typeof obj2 is "number" |
157 | 0 | switch attr[1] |
158 | | when "not" |
159 | 0 | attr = not obj2 |
160 | | when "is" # In Distal, this is eq (equal to) |
161 | 0 | attr = (obj2 is attr[2]) |
162 | | when "isNot" # In Distal, this is ne (not equal to) |
163 | 0 | attr = (obj2 isnt attr[2]) |
164 | | when "isGreaterThan" # In Distal, this is gt (greater than) |
165 | 0 | attr = (obj2 > attr[2]) |
166 | | when "isLessThan" # In Distal, this is lt (less than) |
167 | 0 | attr = (obj2 < attr[2]) |
168 | | when "contains" # In Distal, this is cn (contains) |
169 | 0 | attr = (obj2 and obj2.indexOf(attr[2]) >= 0) |
170 | | when "doesNotContain" # In Distal this is nc (does not contain) |
171 | 0 | attr = (obj2 and obj2.indexOf(attr[2]) < 0) |
172 | | else |
173 | 0 | throw node |
174 | | else |
175 | 0 | attr = obj2 |
176 | 0 | if attr |
177 | 0 | if not shouldRenderStatic |
178 | 0 | if node.style.removeProperty |
179 | 0 | node.style.removeProperty 'display' |
180 | | else |
181 | 0 | node.style.removeAttribute 'display' |
182 | | # node.style.display = "" if not shouldRenderStatic |
183 | | else |
184 | | |
185 | | # Handle hiding differently based on whether user has flagged that |
186 | | # we should render static HTML from the server. (If so, remove the |
187 | | # nodes instead of hiding them to cut down on traffic.) |
188 | | |
189 | | # Skip over all nodes that are children of this node. |
190 | 0 | if querySelectorAll |
191 | 0 | pos += node.querySelectorAll(TAL).length |
192 | | else |
193 | 0 | pos += node.getElementsByTagName("*").length |
194 | | |
195 | 0 | if shouldRenderStatic |
196 | 0 | node.parentNode.removeChild node |
197 | | else |
198 | 0 | node.style.display = "none" |
199 | | |
200 | | # Stop processing the rest of this node as it is invisible. |
201 | 0 | continue |
202 | | |
203 | | # Duplicate the current node x number of times where x is the length |
204 | | # of the resolved array. Create a shortcut variable for each iteration |
205 | | # of the loop. |
206 | | # e.g., <div data-tally-repeat='item feeds.items'> |
207 | 0 | attr = node.getAttribute(qrepeat) |
208 | | |
209 | 0 | if attr |
210 | 0 | attr2 = attr.split(" ") |
211 | | |
212 | | #if live NodeList, remove adjacent repeated nodes |
213 | 0 | unless querySelectorAll |
214 | 0 | html = node.parentNode |
215 | 0 | html.removeChild tmpNode while (tmpNode = node.nextSibling) and (tmpNode.qdup or (tmpNode.nodeType is 1 and tmpNode.getAttribute(qdup))) |
216 | | |
217 | 0 | throw attr2 unless attr2[1] |
218 | 0 | objList = resolve(obj, attr2[1]) |
219 | | |
220 | 0 | if objList and objList.length |
221 | | |
222 | | # Don’t set the style if on the server (as we don’t on anything) |
223 | | # node.style.display = "" if not shouldRenderStatic |
224 | 0 | if not shouldRenderStatic |
225 | 0 | if node.style.removeProperty |
226 | 0 | node.style.removeProperty 'display' |
227 | | else |
228 | 0 | node.style.removeAttribute 'display' |
229 | | |
230 | | |
231 | | # Allow this node to be treated as index zero in the repeat list |
232 | | # we do this by setting the shortcut variable to array[0] |
233 | 0 | obj[attr2[0]] = objList[0] |
234 | 0 | obj["#"] = 1 |
235 | | else |
236 | | |
237 | 0 | if shouldRenderStatic |
238 | | # Delete the node |
239 | 0 | if querySelectorAll |
240 | 0 | pos += node.querySelectorAll(TAL).length |
241 | | else |
242 | 0 | pos += node.getElementsByTagName("*").length |
243 | | |
244 | | # Will this mess up pos? |
245 | 0 | node.parentNode.removeChild node |
246 | | else |
247 | | |
248 | | # Just hide the object and skip its children. |
249 | | |
250 | | # We need to hide the repeat node if the object doesn't resolve. |
251 | 0 | node.style.display = "none" |
252 | | |
253 | | # Skip over all nodes that are children of this node. |
254 | 0 | if querySelectorAll |
255 | 0 | pos += node.querySelectorAll(TAL).length |
256 | | else |
257 | 0 | pos += node.getElementsByTagName("*").length |
258 | | |
259 | | # Stop processing the rest of this node as it is invisible. |
260 | 0 | continue |
261 | | |
262 | 0 | if objList.length > 1 |
263 | | |
264 | | # We need to duplicate this node x number of times. But instead |
265 | | # of calling cloneNode x times, we get the outerHTML and repeat |
266 | | # that x times, then innerHTML it which is faster. |
267 | 0 | html = new Array(objList.length - 1) |
268 | 0 | len = html.length |
269 | 0 | i = len |
270 | | |
271 | 0 | while i > 0 |
272 | 0 | html[len - i] = i |
273 | 0 | i-- |
274 | 0 | tmpNode = node.cloneNode(true) |
275 | 0 | tmpNode.checked = false if "form" of tmpNode |
276 | 0 | tmpNode.setAttribute qdef, attr |
277 | 0 | tmpNode.removeAttribute qrepeat |
278 | 0 | tmpNode.setAttribute qdup, "1" |
279 | 0 | tmpNode = tmpNode.outerHTML or doc.createElement("div").appendChild(tmpNode).parentNode.innerHTML |
280 | | |
281 | | # We're doing something like this: |
282 | | # html = "<div data-tally-alias=' + [1,2,3].join('><div data-tally-alias=') + '>' |
283 | 0 | prefix = tmpNode.indexOf(" " + qdef + "=\"" + attr + "\"") |
284 | 0 | prefix = tmpNode.indexOf(" " + qdef + "='" + attr + "'") if prefix is -1 |
285 | 0 | prefix = prefix + qdef.length + 3 + attr.length |
286 | 0 | html = tmpNode.substr(0, prefix) + " " + html.join(tmpNode.substr(prefix) + tmpNode.substr(0, prefix) + " ") + tmpNode.substr(prefix) |
287 | 0 | tmpNode = doc.createElement("div") |
288 | | |
289 | | # Workaround for IE which can't innerHTML tables and selects. |
290 | 0 | if "cells" of node and not ("tBodies" of node) #TR |
291 | 0 | tmpNode.innerHTML = "<table>" + html + "</table>" |
292 | 0 | tmpNode = tmpNode.firstChild.tBodies[0].childNodes |
293 | | else if "cellIndex" of node #TD |
294 | 0 | tmpNode.innerHTML = "<table><tr>" + html + "</tr></table>" |
295 | 0 | tmpNode = tmpNode.firstChild.tBodies[0].firstChild.childNodes |
296 | | else if "selected" of node and "text" of node #OPTION, OPTGROUP |
297 | 0 | tmpNode.innerHTML = "<select>" + html + "</select>" |
298 | 0 | tmpNode = tmpNode.firstChild.childNodes |
299 | | else |
300 | 0 | tmpNode.innerHTML = html |
301 | 0 | tmpNode = tmpNode.childNodes |
302 | 0 | prefix = node.parentNode |
303 | 0 | attr2 = node.nextSibling |
304 | 0 | if querySelectorAll or node is root |
305 | | |
306 | | # Push the current list and index to the stack and process the repeated |
307 | | # nodes first. We need to do this inline because some variable may change |
308 | | # value later, if the become redefined. |
309 | 0 | listStack.push list |
310 | 0 | posStack.push pos |
311 | | |
312 | | # Add this node to the stack so that it is processed right before we pop the |
313 | | # main list off the stack. This will be the last node to be processed and we |
314 | | # use it to assign our repeat variable to array index 0 so that the node's |
315 | | # children, which are also at array index 0, will be processed correctly. |
316 | 0 | list = getAttribute: getProp |
317 | 0 | list[qdef] = attr + " 0" |
318 | 0 | listStack.push [list] |
319 | 0 | posStack.push 0 |
320 | | |
321 | | # Clear the current list so that in the next round we grab another list |
322 | | # off the stack. |
323 | 0 | list = [] |
324 | 0 | i = tmpNode.length - 1 |
325 | | |
326 | 0 | while i >= 0 |
327 | 0 | html = tmpNode[i] |
328 | | |
329 | | # We need to add the repeated nodes to the listStack because |
330 | | # we are either (1) dealing with a live NodeList and we are still at |
331 | | # the root node so the newly created nodes are adjacent to the root |
332 | | # and so won't appear in the NodeList, or (2) we are dealing with a |
333 | | # non-live NodeList, so we need to add them to the listStack. |
334 | 0 | listStack.push (if querySelectorAll then html.querySelectorAll(TAL) else html.getElementsByTagName("*")) |
335 | 0 | posStack.push 0 |
336 | 0 | listStack.push [html] |
337 | 0 | posStack.push 0 |
338 | 0 | html.qdup = 1 |
339 | 0 | prefix.insertBefore html, attr2 |
340 | 0 | i-- |
341 | | else |
342 | 0 | i = tmpNode.length - 1 |
343 | | |
344 | 0 | while i >= 0 |
345 | 0 | html = tmpNode[i] |
346 | 0 | html.qdup = 1 |
347 | 0 | prefix.insertBefore html, attr2 |
348 | 0 | i-- |
349 | 0 | prefix.selectedIndex = -1 |
350 | | |
351 | | # Set multiple attributes on the node. |
352 | | # e.g., <div data-tally-attribute='value item.text; disabled item.disabled'> |
353 | 0 | attr = node.getAttribute(qattr) |
354 | 0 | if attr |
355 | 0 | name = undefined |
356 | 0 | value = undefined |
357 | 0 | html = attr.split("; ") |
358 | 0 | i = html.length - 1 |
359 | | |
360 | 0 | while i >= 0 |
361 | 0 | attr = html[i].split(" ") |
362 | 0 | name = attr[0] |
363 | 0 | throw attr unless attr[1] |
364 | 0 | value = resolve(obj, attr[1]) |
365 | 0 | value = "" if value is `undefined` |
366 | 0 | attributeWillChange node, name, value if attributeWillChange |
367 | 0 | value = attr(value) if attr = attr[2] and format[attr[2]] |
368 | 0 | if altAttr[name] |
369 | 0 | switch name |
370 | | when "innerHTML" #should use "qtext" |
371 | 0 | throw node |
372 | | when "disabled", "checked", "selected" |
373 | 0 | node[name] = !!value |
374 | | when "style" |
375 | 0 | node.style.cssText = value |
376 | | when "text" #option.text unstable in IE |
377 | 0 | node[(if querySelectorAll then name else innerText)] = value |
378 | | when "class" |
379 | 0 | node["className"] = value |
380 | | else |
381 | 0 | node[name] = value |
382 | | else |
383 | 0 | node.setAttribute name, value |
384 | 0 | i-- |
385 | | |
386 | | # Sets the innerHTML on the node. |
387 | | # e.g., <div data-tally-text='html item.description'> |
388 | 0 | attr = node.getAttribute(qtext) |
389 | 0 | if attr |
390 | 0 | attr = attr.split(" ") |
391 | 0 | html = (attr[0] is "html") |
392 | 0 | attr2 = resolve(obj, attr[(if html then 1 else 0)]) |
393 | 0 | attr2 = "" if attr2 is `undefined` |
394 | 0 | textWillChange node, attr2 if textWillChange |
395 | 0 | attr2 = attr(attr2) if (attr = attr[(if html then 2 else 1)]) and (attr = format[attr]) |
396 | 0 | if html |
397 | 0 | node.innerHTML = attr2 |
398 | | else |
399 | 0 | node[(if "form" of node and not formInputHasBody[node.tagName] then "value" else innerText)] = attr2 |
400 | | #end while |
401 | | |
402 | | # Follows the dot notation path to find an object within an object: obj["a"]["b"]["1"] = c; |
403 | 0 | tally.resolve = (obj, seq, x, lastObj) -> |
404 | | |
405 | | #if fully qualified path is at top level: obj["a.b.d"] = c |
406 | 0 | return (if (typeof x is "function") then x.call(obj, seq) else x) if x = obj[seq] |
407 | | |
408 | 0 | seq = seq.split(".") |
409 | 0 | x = 0 |
410 | | |
411 | 0 | while seq[x] and (lastObj = obj) and (obj = obj[seq[x++]]) |
412 | | ; |
413 | | |
414 | 0 | (if (typeof obj is "function") then obj.call(lastObj, seq.join(".")) else obj) |
415 | | |
416 | | |
417 | | # Number formatters |
418 | 0 | tally.format = ",.": (v, i) -> |
419 | 0 | i = v * 1 |
420 | 0 | (if isNaN(i) then v else ((if i % 1 then i.toFixed(2) else parseInt(i, 10) + "")).replace(/(^\d{1,3}|\d{3})(?=(?:\d{3})+(?:$|\.))/g, "$1,")) |
421 | | |
422 | | |
423 | | # Support RequireJS module pattern |
424 | 0 | if typeof define is "function" and define.amd |
425 | 0 | define "tally", -> |
426 | 0 | tally |
427 | | else |
428 | 0 | window.tally = tally |
429 | | |