parser.js 34 KB

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