1 | define([ |
---|
2 | "require", "./_base/kernel", "./_base/lang", "./_base/array", "./_base/config", "./dom", "./_base/window", |
---|
3 | "./_base/url", "./aspect", "./promise/all", "./date/stamp", "./Deferred", "./has", "./query", "./on", "./ready" |
---|
4 | ], function(require, dojo, dlang, darray, config, dom, dwindow, _Url, aspect, all, dates, Deferred, has, query, don, ready){ |
---|
5 | |
---|
6 | // module: |
---|
7 | // dojo/parser |
---|
8 | |
---|
9 | new Date("X"); // workaround for #11279, new Date("") == NaN |
---|
10 | |
---|
11 | // data-dojo-props etc. is not restricted to JSON, it can be any javascript |
---|
12 | function myEval(text){ |
---|
13 | return eval("(" + text + ")"); |
---|
14 | } |
---|
15 | |
---|
16 | // Widgets like BorderContainer add properties to _Widget via dojo.extend(). |
---|
17 | // If BorderContainer is loaded after _Widget's parameter list has been cached, |
---|
18 | // we need to refresh that parameter list (for _Widget and all widgets that extend _Widget). |
---|
19 | var extendCnt = 0; |
---|
20 | aspect.after(dlang, "extend", function(){ |
---|
21 | extendCnt++; |
---|
22 | }, true); |
---|
23 | |
---|
24 | function getNameMap(ctor){ |
---|
25 | // summary: |
---|
26 | // Returns map from lowercase name to attribute name in class, ex: {onclick: "onClick"} |
---|
27 | var map = ctor._nameCaseMap, proto = ctor.prototype; |
---|
28 | |
---|
29 | // Create the map if it's undefined. |
---|
30 | // Refresh the map if a superclass was possibly extended with new methods since the map was created. |
---|
31 | if(!map || map._extendCnt < extendCnt){ |
---|
32 | map = ctor._nameCaseMap = {}; |
---|
33 | for(var name in proto){ |
---|
34 | if(name.charAt(0) === "_"){ |
---|
35 | continue; |
---|
36 | } // skip internal properties |
---|
37 | map[name.toLowerCase()] = name; |
---|
38 | } |
---|
39 | map._extendCnt = extendCnt; |
---|
40 | } |
---|
41 | return map; |
---|
42 | } |
---|
43 | |
---|
44 | // Map from widget name or list of widget names(ex: "dijit/form/Button,acme/MyMixin") to a constructor. |
---|
45 | var _ctorMap = {}; |
---|
46 | |
---|
47 | function getCtor(/*String[]*/ types, /*Function?*/ contextRequire){ |
---|
48 | // summary: |
---|
49 | // Retrieves a constructor. If the types array contains more than one class/MID then the |
---|
50 | // subsequent classes will be mixed into the first class and a unique constructor will be |
---|
51 | // returned for that array. |
---|
52 | |
---|
53 | var ts = types.join(); |
---|
54 | if(!_ctorMap[ts]){ |
---|
55 | var mixins = []; |
---|
56 | for(var i = 0, l = types.length; i < l; i++){ |
---|
57 | var t = types[i]; |
---|
58 | // TODO: Consider swapping getObject and require in the future |
---|
59 | mixins[mixins.length] = (_ctorMap[t] = _ctorMap[t] || (dlang.getObject(t) || (~t.indexOf('/') && |
---|
60 | (contextRequire ? contextRequire(t) : require(t))))); |
---|
61 | } |
---|
62 | var ctor = mixins.shift(); |
---|
63 | _ctorMap[ts] = mixins.length ? (ctor.createSubclass ? ctor.createSubclass(mixins) : ctor.extend.apply(ctor, mixins)) : ctor; |
---|
64 | } |
---|
65 | |
---|
66 | return _ctorMap[ts]; |
---|
67 | } |
---|
68 | |
---|
69 | var parser = { |
---|
70 | // summary: |
---|
71 | // The Dom/Widget parsing package |
---|
72 | |
---|
73 | _clearCache: function(){ |
---|
74 | // summary: |
---|
75 | // Clear cached data. Used mainly for benchmarking. |
---|
76 | extendCnt++; |
---|
77 | _ctorMap = {}; |
---|
78 | }, |
---|
79 | |
---|
80 | _functionFromScript: function(script, attrData){ |
---|
81 | // summary: |
---|
82 | // Convert a `<script type="dojo/method" args="a, b, c"> ... </script>` |
---|
83 | // into a function |
---|
84 | // script: DOMNode |
---|
85 | // The `<script>` DOMNode |
---|
86 | // attrData: String |
---|
87 | // For HTML5 compliance, searches for attrData + "args" (typically |
---|
88 | // "data-dojo-args") instead of "args" |
---|
89 | var preamble = "", |
---|
90 | suffix = "", |
---|
91 | argsStr = (script.getAttribute(attrData + "args") || script.getAttribute("args")), |
---|
92 | withStr = script.getAttribute("with"); |
---|
93 | |
---|
94 | // Convert any arguments supplied in script tag into an array to be passed to the |
---|
95 | var fnArgs = (argsStr || "").split(/\s*,\s*/); |
---|
96 | |
---|
97 | if(withStr && withStr.length){ |
---|
98 | darray.forEach(withStr.split(/\s*,\s*/), function(part){ |
---|
99 | preamble += "with(" + part + "){"; |
---|
100 | suffix += "}"; |
---|
101 | }); |
---|
102 | } |
---|
103 | |
---|
104 | return new Function(fnArgs, preamble + script.innerHTML + suffix); |
---|
105 | }, |
---|
106 | |
---|
107 | instantiate: function(nodes, mixin, options){ |
---|
108 | // summary: |
---|
109 | // Takes array of nodes, and turns them into class instances and |
---|
110 | // potentially calls a startup method to allow them to connect with |
---|
111 | // any children. |
---|
112 | // nodes: Array |
---|
113 | // Array of DOM nodes |
---|
114 | // mixin: Object? |
---|
115 | // An object that will be mixed in with each node in the array. |
---|
116 | // Values in the mixin will override values in the node, if they |
---|
117 | // exist. |
---|
118 | // options: Object? |
---|
119 | // An object used to hold kwArgs for instantiation. |
---|
120 | // See parse.options argument for details. |
---|
121 | // returns: |
---|
122 | // Array of instances. |
---|
123 | |
---|
124 | mixin = mixin || {}; |
---|
125 | options = options || {}; |
---|
126 | |
---|
127 | var dojoType = (options.scope || dojo._scopeName) + "Type", // typically "dojoType" |
---|
128 | attrData = "data-" + (options.scope || dojo._scopeName) + "-", // typically "data-dojo-" |
---|
129 | dataDojoType = attrData + "type", // typically "data-dojo-type" |
---|
130 | dataDojoMixins = attrData + "mixins"; // typically "data-dojo-mixins" |
---|
131 | |
---|
132 | var list = []; |
---|
133 | darray.forEach(nodes, function(node){ |
---|
134 | var type = dojoType in mixin ? mixin[dojoType] : node.getAttribute(dataDojoType) || node.getAttribute(dojoType); |
---|
135 | if(type){ |
---|
136 | var mixinsValue = node.getAttribute(dataDojoMixins), |
---|
137 | types = mixinsValue ? [type].concat(mixinsValue.split(/\s*,\s*/)) : [type]; |
---|
138 | |
---|
139 | list.push({ |
---|
140 | node: node, |
---|
141 | types: types |
---|
142 | }); |
---|
143 | } |
---|
144 | }); |
---|
145 | |
---|
146 | // Instantiate the nodes and return the list of instances. |
---|
147 | return this._instantiate(list, mixin, options); |
---|
148 | }, |
---|
149 | |
---|
150 | _instantiate: function(nodes, mixin, options, returnPromise){ |
---|
151 | // summary: |
---|
152 | // Takes array of objects representing nodes, and turns them into class instances and |
---|
153 | // potentially calls a startup method to allow them to connect with |
---|
154 | // any children. |
---|
155 | // nodes: Array |
---|
156 | // Array of objects like |
---|
157 | // | { |
---|
158 | // | ctor: Function (may be null) |
---|
159 | // | types: ["dijit/form/Button", "acme/MyMixin"] (used if ctor not specified) |
---|
160 | // | node: DOMNode, |
---|
161 | // | scripts: [ ... ], // array of <script type="dojo/..."> children of node |
---|
162 | // | inherited: { ... } // settings inherited from ancestors like dir, theme, etc. |
---|
163 | // | } |
---|
164 | // mixin: Object |
---|
165 | // An object that will be mixed in with each node in the array. |
---|
166 | // Values in the mixin will override values in the node, if they |
---|
167 | // exist. |
---|
168 | // options: Object |
---|
169 | // An options object used to hold kwArgs for instantiation. |
---|
170 | // See parse.options argument for details. |
---|
171 | // returnPromise: Boolean |
---|
172 | // Return a Promise rather than the instance; supports asynchronous widget creation. |
---|
173 | // returns: |
---|
174 | // Array of instances, or if returnPromise is true, a promise for array of instances |
---|
175 | // that resolves when instances have finished initializing. |
---|
176 | |
---|
177 | // Call widget constructors. Some may be asynchronous and return promises. |
---|
178 | var thelist = darray.map(nodes, function(obj){ |
---|
179 | var ctor = obj.ctor || getCtor(obj.types, options.contextRequire); |
---|
180 | // If we still haven't resolved a ctor, it is fatal now |
---|
181 | if(!ctor){ |
---|
182 | throw new Error("Unable to resolve constructor for: '" + obj.types.join() + "'"); |
---|
183 | } |
---|
184 | return this.construct(ctor, obj.node, mixin, options, obj.scripts, obj.inherited); |
---|
185 | }, this); |
---|
186 | |
---|
187 | // After all widget construction finishes, call startup on each top level instance if it makes sense (as for |
---|
188 | // widgets). Parent widgets will recursively call startup on their (non-top level) children |
---|
189 | function onConstruct(thelist){ |
---|
190 | if(!mixin._started && !options.noStart){ |
---|
191 | darray.forEach(thelist, function(instance){ |
---|
192 | if(typeof instance.startup === "function" && !instance._started){ |
---|
193 | instance.startup(); |
---|
194 | } |
---|
195 | }); |
---|
196 | } |
---|
197 | |
---|
198 | return thelist; |
---|
199 | } |
---|
200 | |
---|
201 | if(returnPromise){ |
---|
202 | return all(thelist).then(onConstruct); |
---|
203 | }else{ |
---|
204 | // Back-compat path, remove for 2.0 |
---|
205 | return onConstruct(thelist); |
---|
206 | } |
---|
207 | }, |
---|
208 | |
---|
209 | construct: function(ctor, node, mixin, options, scripts, inherited){ |
---|
210 | // summary: |
---|
211 | // Calls new ctor(params, node), where params is the hash of parameters specified on the node, |
---|
212 | // excluding data-dojo-type and data-dojo-mixins. Does not call startup(). |
---|
213 | // ctor: Function |
---|
214 | // Widget constructor. |
---|
215 | // node: DOMNode |
---|
216 | // This node will be replaced/attached to by the widget. It also specifies the arguments to pass to ctor. |
---|
217 | // mixin: Object? |
---|
218 | // Attributes in this object will be passed as parameters to ctor, |
---|
219 | // overriding attributes specified on the node. |
---|
220 | // options: Object? |
---|
221 | // An options object used to hold kwArgs for instantiation. See parse.options argument for details. |
---|
222 | // scripts: DomNode[]? |
---|
223 | // Array of `<script type="dojo/*">` DOMNodes. If not specified, will search for `<script>` tags inside node. |
---|
224 | // inherited: Object? |
---|
225 | // Settings from dir=rtl or lang=... on a node above this node. Overrides options.inherited. |
---|
226 | // returns: |
---|
227 | // Instance or Promise for the instance, if markupFactory() itself returned a promise |
---|
228 | |
---|
229 | var proto = ctor && ctor.prototype; |
---|
230 | options = options || {}; |
---|
231 | |
---|
232 | // Setup hash to hold parameter settings for this widget. Start with the parameter |
---|
233 | // settings inherited from ancestors ("dir" and "lang"). |
---|
234 | // Inherited setting may later be overridden by explicit settings on node itself. |
---|
235 | var params = {}; |
---|
236 | |
---|
237 | if(options.defaults){ |
---|
238 | // settings for the document itself (or whatever subtree is being parsed) |
---|
239 | dlang.mixin(params, options.defaults); |
---|
240 | } |
---|
241 | if(inherited){ |
---|
242 | // settings from dir=rtl or lang=... on a node above this node |
---|
243 | dlang.mixin(params, inherited); |
---|
244 | } |
---|
245 | |
---|
246 | // Get list of attributes explicitly listed in the markup |
---|
247 | var attributes; |
---|
248 | if(has("dom-attributes-explicit")){ |
---|
249 | // Standard path to get list of user specified attributes |
---|
250 | attributes = node.attributes; |
---|
251 | }else if(has("dom-attributes-specified-flag")){ |
---|
252 | // Special processing needed for IE8, to skip a few faux values in attributes[] |
---|
253 | attributes = darray.filter(node.attributes, function(a){ |
---|
254 | return a.specified; |
---|
255 | }); |
---|
256 | }else{ |
---|
257 | // Special path for IE6-7, avoid (sometimes >100) bogus entries in node.attributes |
---|
258 | var clone = /^input$|^img$/i.test(node.nodeName) ? node : node.cloneNode(false), |
---|
259 | attrs = clone.outerHTML.replace(/=[^\s"']+|="[^"]*"|='[^']*'/g, "").replace(/^\s*<[a-zA-Z0-9]*\s*/, "").replace(/\s*>.*$/, ""); |
---|
260 | |
---|
261 | attributes = darray.map(attrs.split(/\s+/), function(name){ |
---|
262 | var lcName = name.toLowerCase(); |
---|
263 | return { |
---|
264 | name: name, |
---|
265 | // getAttribute() doesn't work for button.value, returns innerHTML of button. |
---|
266 | // but getAttributeNode().value doesn't work for the form.encType or li.value |
---|
267 | value: (node.nodeName == "LI" && name == "value") || lcName == "enctype" ? |
---|
268 | node.getAttribute(lcName) : node.getAttributeNode(lcName).value |
---|
269 | }; |
---|
270 | }); |
---|
271 | } |
---|
272 | |
---|
273 | // Hash to convert scoped attribute name (ex: data-dojo17-params) to something friendly (ex: data-dojo-params) |
---|
274 | // TODO: remove scope for 2.0 |
---|
275 | var scope = options.scope || dojo._scopeName, |
---|
276 | attrData = "data-" + scope + "-", // typically "data-dojo-" |
---|
277 | hash = {}; |
---|
278 | if(scope !== "dojo"){ |
---|
279 | hash[attrData + "props"] = "data-dojo-props"; |
---|
280 | hash[attrData + "type"] = "data-dojo-type"; |
---|
281 | hash[attrData + "mixins"] = "data-dojo-mixins"; |
---|
282 | hash[scope + "type"] = "dojoType"; |
---|
283 | hash[attrData + "id"] = "data-dojo-id"; |
---|
284 | } |
---|
285 | |
---|
286 | // Read in attributes and process them, including data-dojo-props, data-dojo-type, |
---|
287 | // dojoAttachPoint, etc., as well as normal foo=bar attributes. |
---|
288 | var i = 0, item, funcAttrs = [], jsname, extra; |
---|
289 | while(item = attributes[i++]){ |
---|
290 | var name = item.name, |
---|
291 | lcName = name.toLowerCase(), |
---|
292 | value = item.value; |
---|
293 | |
---|
294 | switch(hash[lcName] || lcName){ |
---|
295 | // Already processed, just ignore |
---|
296 | case "data-dojo-type": |
---|
297 | case "dojotype": |
---|
298 | case "data-dojo-mixins": |
---|
299 | break; |
---|
300 | |
---|
301 | // Data-dojo-props. Save for later to make sure it overrides direct foo=bar settings |
---|
302 | case "data-dojo-props": |
---|
303 | extra = value; |
---|
304 | break; |
---|
305 | |
---|
306 | // data-dojo-id or jsId. TODO: drop jsId in 2.0 |
---|
307 | case "data-dojo-id": |
---|
308 | case "jsid": |
---|
309 | jsname = value; |
---|
310 | break; |
---|
311 | |
---|
312 | // For the benefit of _Templated |
---|
313 | case "data-dojo-attach-point": |
---|
314 | case "dojoattachpoint": |
---|
315 | params.dojoAttachPoint = value; |
---|
316 | break; |
---|
317 | case "data-dojo-attach-event": |
---|
318 | case "dojoattachevent": |
---|
319 | params.dojoAttachEvent = value; |
---|
320 | break; |
---|
321 | |
---|
322 | // Special parameter handling needed for IE |
---|
323 | case "class": |
---|
324 | params["class"] = node.className; |
---|
325 | break; |
---|
326 | case "style": |
---|
327 | params["style"] = node.style && node.style.cssText; |
---|
328 | break; |
---|
329 | default: |
---|
330 | // Normal attribute, ex: value="123" |
---|
331 | |
---|
332 | // Find attribute in widget corresponding to specified name. |
---|
333 | // May involve case conversion, ex: onclick --> onClick |
---|
334 | if(!(name in proto)){ |
---|
335 | var map = getNameMap(ctor); |
---|
336 | name = map[lcName] || name; |
---|
337 | } |
---|
338 | |
---|
339 | // Set params[name] to value, doing type conversion |
---|
340 | if(name in proto){ |
---|
341 | switch(typeof proto[name]){ |
---|
342 | case "string": |
---|
343 | params[name] = value; |
---|
344 | break; |
---|
345 | case "number": |
---|
346 | params[name] = value.length ? Number(value) : NaN; |
---|
347 | break; |
---|
348 | case "boolean": |
---|
349 | // for checked/disabled value might be "" or "checked". interpret as true. |
---|
350 | params[name] = value.toLowerCase() != "false"; |
---|
351 | break; |
---|
352 | case "function": |
---|
353 | if(value === "" || value.search(/[^\w\.]+/i) != -1){ |
---|
354 | // The user has specified some text for a function like "return x+5" |
---|
355 | params[name] = new Function(value); |
---|
356 | }else{ |
---|
357 | // The user has specified the name of a global function like "myOnClick" |
---|
358 | // or a single word function "return" |
---|
359 | params[name] = dlang.getObject(value, false) || new Function(value); |
---|
360 | } |
---|
361 | funcAttrs.push(name); // prevent "double connect", see #15026 |
---|
362 | break; |
---|
363 | default: |
---|
364 | var pVal = proto[name]; |
---|
365 | params[name] = |
---|
366 | (pVal && "length" in pVal) ? (value ? value.split(/\s*,\s*/) : []) : // array |
---|
367 | (pVal instanceof Date) ? |
---|
368 | (value == "" ? new Date("") : // the NaN of dates |
---|
369 | value == "now" ? new Date() : // current date |
---|
370 | dates.fromISOString(value)) : |
---|
371 | (pVal instanceof _Url) ? (dojo.baseUrl + value) : |
---|
372 | myEval(value); |
---|
373 | } |
---|
374 | }else{ |
---|
375 | params[name] = value; |
---|
376 | } |
---|
377 | } |
---|
378 | } |
---|
379 | |
---|
380 | // Remove function attributes from DOMNode to prevent "double connect" problem, see #15026. |
---|
381 | // Do this as a separate loop since attributes[] is often a live collection (depends on the browser though). |
---|
382 | for(var j = 0; j < funcAttrs.length; j++){ |
---|
383 | var lcfname = funcAttrs[j].toLowerCase(); |
---|
384 | node.removeAttribute(lcfname); |
---|
385 | node[lcfname] = null; |
---|
386 | } |
---|
387 | |
---|
388 | // Mix things found in data-dojo-props into the params, overriding any direct settings |
---|
389 | if(extra){ |
---|
390 | try{ |
---|
391 | extra = myEval.call(options.propsThis, "{" + extra + "}"); |
---|
392 | dlang.mixin(params, extra); |
---|
393 | }catch(e){ |
---|
394 | // give the user a pointer to their invalid parameters. FIXME: can we kill this in production? |
---|
395 | throw new Error(e.toString() + " in data-dojo-props='" + extra + "'"); |
---|
396 | } |
---|
397 | } |
---|
398 | |
---|
399 | // Any parameters specified in "mixin" override everything else. |
---|
400 | dlang.mixin(params, mixin); |
---|
401 | |
---|
402 | // Get <script> nodes associated with this widget, if they weren't specified explicitly |
---|
403 | if(!scripts){ |
---|
404 | scripts = (ctor && (ctor._noScript || proto._noScript) ? [] : query("> script[type^='dojo/']", node)); |
---|
405 | } |
---|
406 | |
---|
407 | // Process <script type="dojo/*"> script tags |
---|
408 | // <script type="dojo/method" data-dojo-event="foo"> tags are added to params, and passed to |
---|
409 | // the widget on instantiation. |
---|
410 | // <script type="dojo/method"> tags (with no event) are executed after instantiation |
---|
411 | // <script type="dojo/connect" data-dojo-event="foo"> tags are dojo.connected after instantiation, |
---|
412 | // and likewise with <script type="dojo/aspect" data-dojo-method="foo"> |
---|
413 | // <script type="dojo/watch" data-dojo-prop="foo"> tags are dojo.watch after instantiation |
---|
414 | // <script type="dojo/on" data-dojo-event="foo"> tags are dojo.on after instantiation |
---|
415 | // note: dojo/* script tags cannot exist in self closing widgets, like <input /> |
---|
416 | var aspects = [], // aspects to connect after instantiation |
---|
417 | calls = [], // functions to call after instantiation |
---|
418 | watches = [], // functions to watch after instantiation |
---|
419 | ons = []; // functions to on after instantiation |
---|
420 | |
---|
421 | if(scripts){ |
---|
422 | for(i = 0; i < scripts.length; i++){ |
---|
423 | var script = scripts[i]; |
---|
424 | node.removeChild(script); |
---|
425 | // FIXME: drop event="" support in 2.0. use data-dojo-event="" instead |
---|
426 | var event = (script.getAttribute(attrData + "event") || script.getAttribute("event")), |
---|
427 | prop = script.getAttribute(attrData + "prop"), |
---|
428 | method = script.getAttribute(attrData + "method"), |
---|
429 | advice = script.getAttribute(attrData + "advice"), |
---|
430 | scriptType = script.getAttribute("type"), |
---|
431 | nf = this._functionFromScript(script, attrData); |
---|
432 | if(event){ |
---|
433 | if(scriptType == "dojo/connect"){ |
---|
434 | aspects.push({ method: event, func: nf }); |
---|
435 | }else if(scriptType == "dojo/on"){ |
---|
436 | ons.push({ event: event, func: nf }); |
---|
437 | }else{ |
---|
438 | // <script type="dojo/method" data-dojo-event="foo"> |
---|
439 | // TODO for 2.0: use data-dojo-method="foo" instead (also affects dijit/Declaration) |
---|
440 | params[event] = nf; |
---|
441 | } |
---|
442 | }else if(scriptType == "dojo/aspect"){ |
---|
443 | aspects.push({ method: method, advice: advice, func: nf }); |
---|
444 | }else if(scriptType == "dojo/watch"){ |
---|
445 | watches.push({ prop: prop, func: nf }); |
---|
446 | }else{ |
---|
447 | calls.push(nf); |
---|
448 | } |
---|
449 | } |
---|
450 | } |
---|
451 | |
---|
452 | // create the instance |
---|
453 | var markupFactory = ctor.markupFactory || proto.markupFactory; |
---|
454 | var instance = markupFactory ? markupFactory(params, node, ctor) : new ctor(params, node); |
---|
455 | |
---|
456 | function onInstantiate(instance){ |
---|
457 | // map it to the JS namespace if that makes sense |
---|
458 | if(jsname){ |
---|
459 | dlang.setObject(jsname, instance); |
---|
460 | } |
---|
461 | |
---|
462 | // process connections and startup functions |
---|
463 | for(i = 0; i < aspects.length; i++){ |
---|
464 | aspect[aspects[i].advice || "after"](instance, aspects[i].method, dlang.hitch(instance, aspects[i].func), true); |
---|
465 | } |
---|
466 | for(i = 0; i < calls.length; i++){ |
---|
467 | calls[i].call(instance); |
---|
468 | } |
---|
469 | for(i = 0; i < watches.length; i++){ |
---|
470 | instance.watch(watches[i].prop, watches[i].func); |
---|
471 | } |
---|
472 | for(i = 0; i < ons.length; i++){ |
---|
473 | don(instance, ons[i].event, ons[i].func); |
---|
474 | } |
---|
475 | |
---|
476 | return instance; |
---|
477 | } |
---|
478 | |
---|
479 | if(instance.then){ |
---|
480 | return instance.then(onInstantiate); |
---|
481 | }else{ |
---|
482 | return onInstantiate(instance); |
---|
483 | } |
---|
484 | }, |
---|
485 | |
---|
486 | scan: function(root, options){ |
---|
487 | // summary: |
---|
488 | // Scan a DOM tree and return an array of objects representing the DOMNodes |
---|
489 | // that need to be turned into widgets. |
---|
490 | // description: |
---|
491 | // Search specified node (or document root node) recursively for class instances |
---|
492 | // and return an array of objects that represent potential widgets to be |
---|
493 | // instantiated. Searches for either data-dojo-type="MID" or dojoType="MID" where |
---|
494 | // "MID" is a module ID like "dijit/form/Button" or a fully qualified Class name |
---|
495 | // like "dijit/form/Button". If the MID is not currently available, scan will |
---|
496 | // attempt to require() in the module. |
---|
497 | // |
---|
498 | // See parser.parse() for details of markup. |
---|
499 | // root: DomNode? |
---|
500 | // A default starting root node from which to start the parsing. Can be |
---|
501 | // omitted, defaulting to the entire document. If omitted, the `options` |
---|
502 | // object can be passed in this place. If the `options` object has a |
---|
503 | // `rootNode` member, that is used. |
---|
504 | // options: Object |
---|
505 | // a kwArgs options object, see parse() for details |
---|
506 | // |
---|
507 | // returns: Promise |
---|
508 | // A promise that is resolved with the nodes that have been parsed. |
---|
509 | |
---|
510 | var list = [], // Output List |
---|
511 | mids = [], // An array of modules that are not yet loaded |
---|
512 | midsHash = {}; // Used to keep the mids array unique |
---|
513 | |
---|
514 | var dojoType = (options.scope || dojo._scopeName) + "Type", // typically "dojoType" |
---|
515 | attrData = "data-" + (options.scope || dojo._scopeName) + "-", // typically "data-dojo-" |
---|
516 | dataDojoType = attrData + "type", // typically "data-dojo-type" |
---|
517 | dataDojoTextDir = attrData + "textdir", // typically "data-dojo-textdir" |
---|
518 | dataDojoMixins = attrData + "mixins"; // typically "data-dojo-mixins" |
---|
519 | |
---|
520 | // Info on DOMNode currently being processed |
---|
521 | var node = root.firstChild; |
---|
522 | |
---|
523 | // Info on parent of DOMNode currently being processed |
---|
524 | // - inherited: dir, lang, and textDir setting of parent, or inherited by parent |
---|
525 | // - parent: pointer to identical structure for my parent (or null if no parent) |
---|
526 | // - scripts: if specified, collects <script type="dojo/..."> type nodes from children |
---|
527 | var inherited = options.inherited; |
---|
528 | if(!inherited){ |
---|
529 | function findAncestorAttr(node, attr){ |
---|
530 | return (node.getAttribute && node.getAttribute(attr)) || |
---|
531 | (node.parentNode && findAncestorAttr(node.parentNode, attr)); |
---|
532 | } |
---|
533 | |
---|
534 | inherited = { |
---|
535 | dir: findAncestorAttr(root, "dir"), |
---|
536 | lang: findAncestorAttr(root, "lang"), |
---|
537 | textDir: findAncestorAttr(root, dataDojoTextDir) |
---|
538 | }; |
---|
539 | for(var key in inherited){ |
---|
540 | if(!inherited[key]){ |
---|
541 | delete inherited[key]; |
---|
542 | } |
---|
543 | } |
---|
544 | } |
---|
545 | |
---|
546 | // Metadata about parent node |
---|
547 | var parent = { |
---|
548 | inherited: inherited |
---|
549 | }; |
---|
550 | |
---|
551 | // For collecting <script type="dojo/..."> type nodes (when null, we don't need to collect) |
---|
552 | var scripts; |
---|
553 | |
---|
554 | // when true, only look for <script type="dojo/..."> tags, and don't recurse to children |
---|
555 | var scriptsOnly; |
---|
556 | |
---|
557 | function getEffective(parent){ |
---|
558 | // summary: |
---|
559 | // Get effective dir, lang, textDir settings for specified obj |
---|
560 | // (matching "parent" object structure above), and do caching. |
---|
561 | // Take care not to return null entries. |
---|
562 | if(!parent.inherited){ |
---|
563 | parent.inherited = {}; |
---|
564 | var node = parent.node, |
---|
565 | grandparent = getEffective(parent.parent); |
---|
566 | var inherited = { |
---|
567 | dir: node.getAttribute("dir") || grandparent.dir, |
---|
568 | lang: node.getAttribute("lang") || grandparent.lang, |
---|
569 | textDir: node.getAttribute(dataDojoTextDir) || grandparent.textDir |
---|
570 | }; |
---|
571 | for(var key in inherited){ |
---|
572 | if(inherited[key]){ |
---|
573 | parent.inherited[key] = inherited[key]; |
---|
574 | } |
---|
575 | } |
---|
576 | } |
---|
577 | return parent.inherited; |
---|
578 | } |
---|
579 | |
---|
580 | // DFS on DOM tree, collecting nodes with data-dojo-type specified. |
---|
581 | while(true){ |
---|
582 | if(!node){ |
---|
583 | // Finished this level, continue to parent's next sibling |
---|
584 | if(!parent || !parent.node){ |
---|
585 | break; |
---|
586 | } |
---|
587 | node = parent.node.nextSibling; |
---|
588 | scriptsOnly = false; |
---|
589 | parent = parent.parent; |
---|
590 | scripts = parent.scripts; |
---|
591 | continue; |
---|
592 | } |
---|
593 | |
---|
594 | if(node.nodeType != 1){ |
---|
595 | // Text or comment node, skip to next sibling |
---|
596 | node = node.nextSibling; |
---|
597 | continue; |
---|
598 | } |
---|
599 | |
---|
600 | if(scripts && node.nodeName.toLowerCase() == "script"){ |
---|
601 | // Save <script type="dojo/..."> for parent, then continue to next sibling |
---|
602 | type = node.getAttribute("type"); |
---|
603 | if(type && /^dojo\/\w/i.test(type)){ |
---|
604 | scripts.push(node); |
---|
605 | } |
---|
606 | node = node.nextSibling; |
---|
607 | continue; |
---|
608 | } |
---|
609 | if(scriptsOnly){ |
---|
610 | // scriptsOnly flag is set, we have already collected scripts if the parent wants them, so now we shouldn't |
---|
611 | // continue further analysis of the node and will continue to the next sibling |
---|
612 | node = node.nextSibling; |
---|
613 | continue; |
---|
614 | } |
---|
615 | |
---|
616 | // Check for data-dojo-type attribute, fallback to backward compatible dojoType |
---|
617 | // TODO: Remove dojoType in 2.0 |
---|
618 | var type = node.getAttribute(dataDojoType) || node.getAttribute(dojoType); |
---|
619 | |
---|
620 | // Short circuit for leaf nodes containing nothing [but text] |
---|
621 | var firstChild = node.firstChild; |
---|
622 | if(!type && (!firstChild || (firstChild.nodeType == 3 && !firstChild.nextSibling))){ |
---|
623 | node = node.nextSibling; |
---|
624 | continue; |
---|
625 | } |
---|
626 | |
---|
627 | // Meta data about current node |
---|
628 | var current; |
---|
629 | |
---|
630 | var ctor = null; |
---|
631 | if(type){ |
---|
632 | // If dojoType/data-dojo-type specified, add to output array of nodes to instantiate. |
---|
633 | var mixinsValue = node.getAttribute(dataDojoMixins), |
---|
634 | types = mixinsValue ? [type].concat(mixinsValue.split(/\s*,\s*/)) : [type]; |
---|
635 | |
---|
636 | // Note: won't find classes declared via dojo/Declaration or any modules that haven't been |
---|
637 | // loaded yet so use try/catch to avoid throw from require() |
---|
638 | try{ |
---|
639 | ctor = getCtor(types, options.contextRequire); |
---|
640 | }catch(e){} |
---|
641 | |
---|
642 | // If the constructor was not found, check to see if it has modules that can be loaded |
---|
643 | if(!ctor){ |
---|
644 | darray.forEach(types, function(t){ |
---|
645 | if(~t.indexOf('/') && !midsHash[t]){ |
---|
646 | // If the type looks like a MID and it currently isn't in the array of MIDs to load, add it. |
---|
647 | midsHash[t] = true; |
---|
648 | mids[mids.length] = t; |
---|
649 | } |
---|
650 | }); |
---|
651 | } |
---|
652 | |
---|
653 | var childScripts = ctor && !ctor.prototype._noScript ? [] : null; // <script> nodes that are parent's children |
---|
654 | |
---|
655 | // Setup meta data about this widget node, and save it to list of nodes to instantiate |
---|
656 | current = { |
---|
657 | types: types, |
---|
658 | ctor: ctor, |
---|
659 | parent: parent, |
---|
660 | node: node, |
---|
661 | scripts: childScripts |
---|
662 | }; |
---|
663 | current.inherited = getEffective(current); // dir & lang settings for current node, explicit or inherited |
---|
664 | list.push(current); |
---|
665 | }else{ |
---|
666 | // Meta data about this non-widget node |
---|
667 | current = { |
---|
668 | node: node, |
---|
669 | scripts: scripts, |
---|
670 | parent: parent |
---|
671 | }; |
---|
672 | } |
---|
673 | |
---|
674 | // Recurse, collecting <script type="dojo/..."> children, and also looking for |
---|
675 | // descendant nodes with dojoType specified (unless the widget has the stopParser flag). |
---|
676 | // When finished with children, go to my next sibling. |
---|
677 | scripts = childScripts; |
---|
678 | scriptsOnly = node.stopParser || (ctor && ctor.prototype.stopParser && !(options.template)); |
---|
679 | parent = current; |
---|
680 | node = firstChild; |
---|
681 | } |
---|
682 | |
---|
683 | var d = new Deferred(); |
---|
684 | |
---|
685 | // If there are modules to load then require them in |
---|
686 | if(mids.length){ |
---|
687 | // Warn that there are modules being auto-required |
---|
688 | if(has("dojo-debug-messages")){ |
---|
689 | console.warn("WARNING: Modules being Auto-Required: " + mids.join(", ")); |
---|
690 | } |
---|
691 | var r = options.contextRequire || require; |
---|
692 | r(mids, function(){ |
---|
693 | // Go through list of widget nodes, filling in missing constructors, and filtering out nodes that shouldn't |
---|
694 | // be instantiated due to a stopParser flag on an ancestor that we belatedly learned about due to |
---|
695 | // auto-require of a module like ContentPane. Assumes list is in DFS order. |
---|
696 | d.resolve(darray.filter(list, function(widget){ |
---|
697 | if(!widget.ctor){ |
---|
698 | // Attempt to find the constructor again. Still won't find classes defined via |
---|
699 | // dijit/Declaration so need to try/catch. |
---|
700 | try{ |
---|
701 | widget.ctor = getCtor(widget.types, options.contextRequire); |
---|
702 | }catch(e){} |
---|
703 | } |
---|
704 | |
---|
705 | // Get the parent widget |
---|
706 | var parent = widget.parent; |
---|
707 | while(parent && !parent.types){ |
---|
708 | parent = parent.parent; |
---|
709 | } |
---|
710 | |
---|
711 | // Return false if this node should be skipped due to stopParser on an ancestor. |
---|
712 | // Since list[] is in DFS order, this loop will always set parent.instantiateChildren before |
---|
713 | // trying to compute widget.instantiate. |
---|
714 | var proto = widget.ctor && widget.ctor.prototype; |
---|
715 | widget.instantiateChildren = !(proto && proto.stopParser && !(options.template)); |
---|
716 | widget.instantiate = !parent || (parent.instantiate && parent.instantiateChildren); |
---|
717 | return widget.instantiate; |
---|
718 | })); |
---|
719 | }); |
---|
720 | }else{ |
---|
721 | // There were no modules to load, so just resolve with the parsed nodes. This separate code path is for |
---|
722 | // efficiency, to avoid running the require() and the callback code above. |
---|
723 | d.resolve(list); |
---|
724 | } |
---|
725 | |
---|
726 | // Return the promise |
---|
727 | return d.promise; |
---|
728 | }, |
---|
729 | |
---|
730 | _require: function(/*DOMNode*/ script, /*Object?*/ options){ |
---|
731 | // summary: |
---|
732 | // Helper for _scanAMD(). Takes a `<script type=dojo/require>bar: "acme/bar", ...</script>` node, |
---|
733 | // calls require() to load the specified modules and (asynchronously) assign them to the specified global |
---|
734 | // variables, and returns a Promise for when that operation completes. |
---|
735 | // |
---|
736 | // In the example above, it is effectively doing a require(["acme/bar", ...], function(a){ bar = a; }). |
---|
737 | |
---|
738 | var hash = myEval("{" + script.innerHTML + "}"), // can't use dojo/json::parse() because maybe no quotes |
---|
739 | vars = [], |
---|
740 | mids = [], |
---|
741 | d = new Deferred(); |
---|
742 | |
---|
743 | var contextRequire = (options && options.contextRequire) || require; |
---|
744 | |
---|
745 | for(var name in hash){ |
---|
746 | vars.push(name); |
---|
747 | mids.push(hash[name]); |
---|
748 | } |
---|
749 | |
---|
750 | contextRequire(mids, function(){ |
---|
751 | for(var i = 0; i < vars.length; i++){ |
---|
752 | dlang.setObject(vars[i], arguments[i]); |
---|
753 | } |
---|
754 | d.resolve(arguments); |
---|
755 | }); |
---|
756 | |
---|
757 | return d.promise; |
---|
758 | }, |
---|
759 | |
---|
760 | _scanAmd: function(root, options){ |
---|
761 | // summary: |
---|
762 | // Scans the DOM for any declarative requires and returns their values. |
---|
763 | // description: |
---|
764 | // Looks for `<script type=dojo/require>bar: "acme/bar", ...</script>` node, calls require() to load the |
---|
765 | // specified modules and (asynchronously) assign them to the specified global variables, |
---|
766 | // and returns a Promise for when those operations complete. |
---|
767 | // root: DomNode |
---|
768 | // The node to base the scan from. |
---|
769 | // options: Object? |
---|
770 | // a kwArgs options object, see parse() for details |
---|
771 | |
---|
772 | // Promise that resolves when all the <script type=dojo/require> nodes have finished loading. |
---|
773 | var deferred = new Deferred(), |
---|
774 | promise = deferred.promise; |
---|
775 | deferred.resolve(true); |
---|
776 | |
---|
777 | var self = this; |
---|
778 | query("script[type='dojo/require']", root).forEach(function(node){ |
---|
779 | // Fire off require() call for specified modules. Chain this require to fire after |
---|
780 | // any previous requires complete, so that layers can be loaded before individual module require()'s fire. |
---|
781 | promise = promise.then(function(){ |
---|
782 | return self._require(node, options); |
---|
783 | }); |
---|
784 | |
---|
785 | // Remove from DOM so it isn't seen again |
---|
786 | node.parentNode.removeChild(node); |
---|
787 | }); |
---|
788 | |
---|
789 | return promise; |
---|
790 | }, |
---|
791 | |
---|
792 | parse: function(rootNode, options){ |
---|
793 | // summary: |
---|
794 | // Scan the DOM for class instances, and instantiate them. |
---|
795 | // description: |
---|
796 | // Search specified node (or root node) recursively for class instances, |
---|
797 | // and instantiate them. Searches for either data-dojo-type="Class" or |
---|
798 | // dojoType="Class" where "Class" is a a fully qualified class name, |
---|
799 | // like `dijit/form/Button` |
---|
800 | // |
---|
801 | // Using `data-dojo-type`: |
---|
802 | // Attributes using can be mixed into the parameters used to instantiate the |
---|
803 | // Class by using a `data-dojo-props` attribute on the node being converted. |
---|
804 | // `data-dojo-props` should be a string attribute to be converted from JSON. |
---|
805 | // |
---|
806 | // Using `dojoType`: |
---|
807 | // Attributes are read from the original domNode and converted to appropriate |
---|
808 | // types by looking up the Class prototype values. This is the default behavior |
---|
809 | // from Dojo 1.0 to Dojo 1.5. `dojoType` support is deprecated, and will |
---|
810 | // go away in Dojo 2.0. |
---|
811 | // rootNode: DomNode? |
---|
812 | // A default starting root node from which to start the parsing. Can be |
---|
813 | // omitted, defaulting to the entire document. If omitted, the `options` |
---|
814 | // object can be passed in this place. If the `options` object has a |
---|
815 | // `rootNode` member, that is used. |
---|
816 | // options: Object? |
---|
817 | // A hash of options. |
---|
818 | // |
---|
819 | // - noStart: Boolean?: |
---|
820 | // when set will prevent the parser from calling .startup() |
---|
821 | // when locating the nodes. |
---|
822 | // - rootNode: DomNode?: |
---|
823 | // identical to the function's `rootNode` argument, though |
---|
824 | // allowed to be passed in via this `options object. |
---|
825 | // - template: Boolean: |
---|
826 | // If true, ignores ContentPane's stopParser flag and parses contents inside of |
---|
827 | // a ContentPane inside of a template. This allows dojoAttachPoint on widgets/nodes |
---|
828 | // nested inside the ContentPane to work. |
---|
829 | // - inherited: Object: |
---|
830 | // Hash possibly containing dir and lang settings to be applied to |
---|
831 | // parsed widgets, unless there's another setting on a sub-node that overrides |
---|
832 | // - scope: String: |
---|
833 | // Root for attribute names to search for. If scopeName is dojo, |
---|
834 | // will search for data-dojo-type (or dojoType). For backwards compatibility |
---|
835 | // reasons defaults to dojo._scopeName (which is "dojo" except when |
---|
836 | // multi-version support is used, when it will be something like dojo16, dojo20, etc.) |
---|
837 | // - propsThis: Object: |
---|
838 | // If specified, "this" referenced from data-dojo-props will refer to propsThis. |
---|
839 | // Intended for use from the widgets-in-template feature of `dijit._WidgetsInTemplateMixin` |
---|
840 | // - contextRequire: Function: |
---|
841 | // If specified, this require is utilised for looking resolving modules instead of the |
---|
842 | // `dojo/parser` context `require()`. Intended for use from the widgets-in-template feature of |
---|
843 | // `dijit._WidgetsInTemplateMixin`. |
---|
844 | // returns: Mixed |
---|
845 | // Returns a blended object that is an array of the instantiated objects, but also can include |
---|
846 | // a promise that is resolved with the instantiated objects. This is done for backwards |
---|
847 | // compatibility. If the parser auto-requires modules, it will always behave in a promise |
---|
848 | // fashion and `parser.parse().then(function(instances){...})` should be used. |
---|
849 | // example: |
---|
850 | // Parse all widgets on a page: |
---|
851 | // | parser.parse(); |
---|
852 | // example: |
---|
853 | // Parse all classes within the node with id="foo" |
---|
854 | // | parser.parse(dojo.byId('foo')); |
---|
855 | // example: |
---|
856 | // Parse all classes in a page, but do not call .startup() on any |
---|
857 | // child |
---|
858 | // | parser.parse({ noStart: true }) |
---|
859 | // example: |
---|
860 | // Parse all classes in a node, but do not call .startup() |
---|
861 | // | parser.parse(someNode, { noStart:true }); |
---|
862 | // | // or |
---|
863 | // | parser.parse({ noStart:true, rootNode: someNode }); |
---|
864 | |
---|
865 | // determine the root node and options based on the passed arguments. |
---|
866 | var root; |
---|
867 | if(!options && rootNode && rootNode.rootNode){ |
---|
868 | options = rootNode; |
---|
869 | root = options.rootNode; |
---|
870 | }else if(rootNode && dlang.isObject(rootNode) && !("nodeType" in rootNode)){ |
---|
871 | options = rootNode; |
---|
872 | }else{ |
---|
873 | root = rootNode; |
---|
874 | } |
---|
875 | root = root ? dom.byId(root) : dwindow.body(); |
---|
876 | |
---|
877 | options = options || {}; |
---|
878 | |
---|
879 | var mixin = options.template ? { template: true } : {}, |
---|
880 | instances = [], |
---|
881 | self = this; |
---|
882 | |
---|
883 | // First scan for any <script type=dojo/require> nodes, and execute. |
---|
884 | // Then scan for all nodes with data-dojo-type, and load any unloaded modules. |
---|
885 | // Then build the object instances. Add instances to already existing (but empty) instances[] array, |
---|
886 | // which may already have been returned to caller. Also, use otherwise to collect and throw any errors |
---|
887 | // that occur during the parse(). |
---|
888 | var p = |
---|
889 | this._scanAmd(root, options).then(function(){ |
---|
890 | return self.scan(root, options); |
---|
891 | }).then(function(parsedNodes){ |
---|
892 | return self._instantiate(parsedNodes, mixin, options, true); |
---|
893 | }).then(function(_instances){ |
---|
894 | // Copy the instances into the instances[] array we declared above, and are accessing as |
---|
895 | // our return value. |
---|
896 | return instances = instances.concat(_instances); |
---|
897 | }).otherwise(function(e){ |
---|
898 | // TODO Modify to follow better pattern for promise error management when available |
---|
899 | console.error("dojo/parser::parse() error", e); |
---|
900 | throw e; |
---|
901 | }); |
---|
902 | |
---|
903 | // Blend the array with the promise |
---|
904 | dlang.mixin(instances, p); |
---|
905 | return instances; |
---|
906 | } |
---|
907 | }; |
---|
908 | |
---|
909 | if(has("extend-dojo")){ |
---|
910 | dojo.parser = parser; |
---|
911 | } |
---|
912 | |
---|
913 | // Register the parser callback. It should be the first callback |
---|
914 | // after the a11y test. |
---|
915 | if(config.parseOnLoad){ |
---|
916 | ready(100, parser, "parse"); |
---|
917 | } |
---|
918 | |
---|
919 | return parser; |
---|
920 | }); |
---|