parser.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. /*
  2. Copyright (c) 2004-2012, The Dojo Foundation All Rights Reserved.
  3. Available via Academic Free License >= 2.1 OR the modified BSD license.
  4. see: http://dojotoolkit.org/license for details
  5. */
  6. if(!dojo._hasResource["dojo.parser"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dojo.parser"] = true;
  8. dojo.provide("dojo.parser");
  9. dojo.require("dojo.date.stamp");
  10. new Date("X"); // workaround for #11279, new Date("") == NaN
  11. dojo.parser = new function(){
  12. // summary:
  13. // The Dom/Widget parsing package
  14. var d = dojo;
  15. function val2type(/*Object*/ value){
  16. // summary:
  17. // Returns name of type of given value.
  18. if(d.isString(value)){ return "string"; }
  19. if(typeof value == "number"){ return "number"; }
  20. if(typeof value == "boolean"){ return "boolean"; }
  21. if(d.isFunction(value)){ return "function"; }
  22. if(d.isArray(value)){ return "array"; } // typeof [] == "object"
  23. if(value instanceof Date) { return "date"; } // assume timestamp
  24. if(value instanceof d._Url){ return "url"; }
  25. return "object";
  26. }
  27. function str2obj(/*String*/ value, /*String*/ type){
  28. // summary:
  29. // Convert given string value to given type
  30. switch(type){
  31. case "string":
  32. return value;
  33. case "number":
  34. return value.length ? Number(value) : NaN;
  35. case "boolean":
  36. // for checked/disabled value might be "" or "checked". interpret as true.
  37. return typeof value == "boolean" ? value : !(value.toLowerCase()=="false");
  38. case "function":
  39. if(d.isFunction(value)){
  40. // IE gives us a function, even when we say something like onClick="foo"
  41. // (in which case it gives us an invalid function "function(){ foo }").
  42. // Therefore, convert to string
  43. value=value.toString();
  44. value=d.trim(value.substring(value.indexOf('{')+1, value.length-1));
  45. }
  46. try{
  47. if(value === "" || value.search(/[^\w\.]+/i) != -1){
  48. // The user has specified some text for a function like "return x+5"
  49. return new Function(value);
  50. }else{
  51. // The user has specified the name of a function like "myOnClick"
  52. // or a single word function "return"
  53. return d.getObject(value, false) || new Function(value);
  54. }
  55. }catch(e){ return new Function(); }
  56. case "array":
  57. return value ? value.split(/\s*,\s*/) : [];
  58. case "date":
  59. switch(value){
  60. case "": return new Date(""); // the NaN of dates
  61. case "now": return new Date(); // current date
  62. default: return d.date.stamp.fromISOString(value);
  63. }
  64. case "url":
  65. return d.baseUrl + value;
  66. default:
  67. return d.fromJson(value);
  68. }
  69. }
  70. var dummyClass = {}, instanceClasses = {
  71. // map from fully qualified name (like "dijit.Button") to structure like
  72. // { cls: dijit.Button, params: {label: "string", disabled: "boolean"} }
  73. };
  74. // Widgets like BorderContainer add properties to _Widget via dojo.extend().
  75. // If BorderContainer is loaded after _Widget's parameter list has been cached,
  76. // we need to refresh that parameter list (for _Widget and all widgets that extend _Widget).
  77. // TODO: remove this in 2.0, when we stop caching parameters.
  78. d.connect(d, "extend", function(){
  79. instanceClasses = {};
  80. });
  81. function getProtoInfo(cls, params){
  82. // cls: A prototype
  83. // The prototype of the class to check props on
  84. // params: Object
  85. // The parameters object to mix found parameters onto.
  86. for(var name in cls){
  87. if(name.charAt(0)=="_"){ continue; } // skip internal properties
  88. if(name in dummyClass){ continue; } // skip "constructor" and "toString"
  89. params[name] = val2type(cls[name]);
  90. }
  91. return params;
  92. }
  93. function getClassInfo(/*String*/ className, /*Boolean*/ skipParamsLookup){
  94. // summary:
  95. // Maps a widget name string like "dijit.form.Button" to the widget constructor itself,
  96. // and a list of that widget's parameters and their types
  97. // className:
  98. // fully qualified name (like "dijit.form.Button")
  99. // returns:
  100. // structure like
  101. // {
  102. // cls: dijit.Button,
  103. // params: { label: "string", disabled: "boolean"}
  104. // }
  105. var c = instanceClasses[className];
  106. if(!c){
  107. // get pointer to widget class
  108. var cls = d.getObject(className), params = null;
  109. if(!cls){ return null; } // class not defined [yet]
  110. if(!skipParamsLookup){ // from fastpath, we don't need to lookup the attrs on the proto because they are explicit
  111. params = getProtoInfo(cls.prototype, {})
  112. }
  113. c = { cls: cls, params: params };
  114. }else if(!skipParamsLookup && !c.params){
  115. // if we're calling getClassInfo and have a cls proto, but no params info, scan that cls for params now
  116. // and update the pointer in instanceClasses[className]. This happens when a widget appears in another
  117. // widget's template which still uses dojoType, but an instance of the widget appears prior with a data-dojo-type,
  118. // skipping this lookup the first time.
  119. c.params = getProtoInfo(c.cls.prototype, {});
  120. }
  121. return c;
  122. }
  123. this._functionFromScript = function(script, attrData){
  124. // summary:
  125. // Convert a <script type="dojo/method" args="a, b, c"> ... </script>
  126. // into a function
  127. // script: DOMNode
  128. // The <script> DOMNode
  129. // attrData: String
  130. // For HTML5 compliance, searches for attrData + "args" (typically
  131. // "data-dojo-args") instead of "args"
  132. var preamble = "";
  133. var suffix = "";
  134. var argsStr = (script.getAttribute(attrData + "args") || script.getAttribute("args"));
  135. if(argsStr){
  136. d.forEach(argsStr.split(/\s*,\s*/), function(part, idx){
  137. preamble += "var "+part+" = arguments["+idx+"]; ";
  138. });
  139. }
  140. var withStr = script.getAttribute("with");
  141. if(withStr && withStr.length){
  142. d.forEach(withStr.split(/\s*,\s*/), function(part){
  143. preamble += "with("+part+"){";
  144. suffix += "}";
  145. });
  146. }
  147. return new Function(preamble+script.innerHTML+suffix);
  148. };
  149. this.instantiate = function(/* Array */nodes, /* Object? */mixin, /* Object? */args){
  150. // summary:
  151. // Takes array of nodes, and turns them into class instances and
  152. // potentially calls a startup method to allow them to connect with
  153. // any children.
  154. // nodes: Array
  155. // Array of nodes or objects like
  156. // | {
  157. // | type: "dijit.form.Button",
  158. // | node: DOMNode,
  159. // | scripts: [ ... ], // array of <script type="dojo/..."> children of node
  160. // | inherited: { ... } // settings inherited from ancestors like dir, theme, etc.
  161. // | }
  162. // mixin: Object?
  163. // An object that will be mixed in with each node in the array.
  164. // Values in the mixin will override values in the node, if they
  165. // exist.
  166. // args: Object?
  167. // An object used to hold kwArgs for instantiation.
  168. // See parse.args argument for details.
  169. var thelist = [],
  170. mixin = mixin||{};
  171. args = args||{};
  172. // TODO: for 2.0 default to data-dojo- regardless of scopeName (or maybe scopeName won't exist in 2.0)
  173. var attrName = (args.scope || d._scopeName) + "Type", // typically "dojoType"
  174. attrData = "data-" + (args.scope || d._scopeName) + "-"; // typically "data-dojo-"
  175. d.forEach(nodes, function(obj){
  176. if(!obj){ return; }
  177. // Get pointers to DOMNode, dojoType string, and clsInfo (metadata about the dojoType), etc.
  178. var node, type, clsInfo, clazz, scripts, fastpath;
  179. if(obj.node){
  180. // new format of nodes[] array, object w/lots of properties pre-computed for me
  181. node = obj.node;
  182. type = obj.type;
  183. fastpath = obj.fastpath;
  184. clsInfo = obj.clsInfo || (type && getClassInfo(type, fastpath));
  185. clazz = clsInfo && clsInfo.cls;
  186. scripts = obj.scripts;
  187. }else{
  188. // old (backwards compatible) format of nodes[] array, simple array of DOMNodes. no fastpath/data-dojo-type support here.
  189. node = obj;
  190. type = attrName in mixin ? mixin[attrName] : node.getAttribute(attrName);
  191. clsInfo = type && getClassInfo(type);
  192. clazz = clsInfo && clsInfo.cls;
  193. scripts = (clazz && (clazz._noScript || clazz.prototype._noScript) ? [] :
  194. d.query("> script[type^='dojo/']", node));
  195. }
  196. if(!clsInfo){
  197. throw new Error("Could not load class '" + type);
  198. }
  199. // Setup hash to hold parameter settings for this widget. Start with the parameter
  200. // settings inherited from ancestors ("dir" and "lang").
  201. // Inherited setting may later be overridden by explicit settings on node itself.
  202. var params = {};
  203. if(args.defaults){
  204. // settings for the document itself (or whatever subtree is being parsed)
  205. d._mixin(params, args.defaults);
  206. }
  207. if(obj.inherited){
  208. // settings from dir=rtl or lang=... on a node above this node
  209. d._mixin(params, obj.inherited);
  210. }
  211. // mix things found in data-dojo-props into the params
  212. if(fastpath){
  213. var extra = node.getAttribute(attrData + "props");
  214. if(extra && extra.length){
  215. try{
  216. extra = d.fromJson.call(args.propsThis, "{" + extra + "}");
  217. d._mixin(params, extra);
  218. }catch(e){
  219. // give the user a pointer to their invalid parameters. FIXME: can we kill this in production?
  220. throw new Error(e.toString() + " in data-dojo-props='" + extra + "'");
  221. }
  222. }
  223. // For the benefit of _Templated, check if node has data-dojo-attach-point/data-dojo-attach-event
  224. // and mix those in as though they were parameters
  225. var attachPoint = node.getAttribute(attrData + "attach-point");
  226. if(attachPoint){
  227. params.dojoAttachPoint = attachPoint;
  228. }
  229. var attachEvent = node.getAttribute(attrData + "attach-event");
  230. if(attachEvent){
  231. params.dojoAttachEvent = attachEvent;
  232. }
  233. dojo.mixin(params, mixin);
  234. }else{
  235. // FIXME: we need something like "deprecateOnce()" to throw dojo.deprecation for something.
  236. // remove this logic in 2.0
  237. // read parameters (ie, attributes) specified on DOMNode
  238. var attributes = node.attributes;
  239. // clsInfo.params lists expected params like {"checked": "boolean", "n": "number"}
  240. for(var name in clsInfo.params){
  241. var item = name in mixin ? { value:mixin[name], specified:true } : attributes.getNamedItem(name);
  242. if(!item || (!item.specified && (!dojo.isIE || name.toLowerCase()!="value"))){ continue; }
  243. var value = item.value;
  244. // Deal with IE quirks for 'class' and 'style'
  245. switch(name){
  246. case "class":
  247. value = "className" in mixin ? mixin.className : node.className;
  248. break;
  249. case "style":
  250. value = "style" in mixin ? mixin.style : (node.style && node.style.cssText); // FIXME: Opera?
  251. }
  252. var _type = clsInfo.params[name];
  253. if(typeof value == "string"){
  254. params[name] = str2obj(value, _type);
  255. }else{
  256. params[name] = value;
  257. }
  258. }
  259. }
  260. // Process <script type="dojo/*"> script tags
  261. // <script type="dojo/method" event="foo"> tags are added to params, and passed to
  262. // the widget on instantiation.
  263. // <script type="dojo/method"> tags (with no event) are executed after instantiation
  264. // <script type="dojo/connect" event="foo"> tags are dojo.connected after instantiation
  265. // note: dojo/* script tags cannot exist in self closing widgets, like <input />
  266. var connects = [], // functions to connect after instantiation
  267. calls = []; // functions to call after instantiation
  268. d.forEach(scripts, function(script){
  269. node.removeChild(script);
  270. // FIXME: drop event="" support in 2.0. use data-dojo-event="" instead
  271. var event = (script.getAttribute(attrData + "event") || script.getAttribute("event")),
  272. type = script.getAttribute("type"),
  273. nf = d.parser._functionFromScript(script, attrData);
  274. if(event){
  275. if(type == "dojo/connect"){
  276. connects.push({event: event, func: nf});
  277. }else{
  278. params[event] = nf;
  279. }
  280. }else{
  281. calls.push(nf);
  282. }
  283. });
  284. var markupFactory = clazz.markupFactory || clazz.prototype && clazz.prototype.markupFactory;
  285. // create the instance
  286. var instance = markupFactory ? markupFactory(params, node, clazz) : new clazz(params, node);
  287. thelist.push(instance);
  288. // map it to the JS namespace if that makes sense
  289. // FIXME: in 2.0, drop jsId support. use data-dojo-id instead
  290. var jsname = (node.getAttribute(attrData + "id") || node.getAttribute("jsId"));
  291. if(jsname){
  292. d.setObject(jsname, instance);
  293. }
  294. // process connections and startup functions
  295. d.forEach(connects, function(connect){
  296. d.connect(instance, connect.event, null, connect.func);
  297. });
  298. d.forEach(calls, function(func){
  299. func.call(instance);
  300. });
  301. });
  302. // Call startup on each top level instance if it makes sense (as for
  303. // widgets). Parent widgets will recursively call startup on their
  304. // (non-top level) children
  305. if(!mixin._started){
  306. // TODO: for 2.0, when old instantiate() API is desupported, store parent-child
  307. // relationships in the nodes[] array so that no getParent() call is needed.
  308. // Note that will require a parse() call from ContentPane setting a param that the
  309. // ContentPane is the parent widget (so that the parse doesn't call startup() on the
  310. // ContentPane's children)
  311. d.forEach(thelist, function(instance){
  312. if( !args.noStart && instance &&
  313. dojo.isFunction(instance.startup) &&
  314. !instance._started &&
  315. (!instance.getParent || !instance.getParent())
  316. ){
  317. instance.startup();
  318. }
  319. });
  320. }
  321. return thelist;
  322. };
  323. this.parse = function(rootNode, args){
  324. // summary:
  325. // Scan the DOM for class instances, and instantiate them.
  326. //
  327. // description:
  328. // Search specified node (or root node) recursively for class instances,
  329. // and instantiate them. Searches for either data-dojo-type="Class" or
  330. // dojoType="Class" where "Class" is a a fully qualified class name,
  331. // like `dijit.form.Button`
  332. //
  333. // Using `data-dojo-type`:
  334. // Attributes using can be mixed into the parameters used to instantitate the
  335. // Class by using a `data-dojo-props` attribute on the node being converted.
  336. // `data-dojo-props` should be a string attribute to be converted from JSON.
  337. //
  338. // Using `dojoType`:
  339. // Attributes are read from the original domNode and converted to appropriate
  340. // types by looking up the Class prototype values. This is the default behavior
  341. // from Dojo 1.0 to Dojo 1.5. `dojoType` support is deprecated, and will
  342. // go away in Dojo 2.0.
  343. //
  344. // rootNode: DomNode?
  345. // A default starting root node from which to start the parsing. Can be
  346. // omitted, defaulting to the entire document. If omitted, the `args`
  347. // object can be passed in this place. If the `args` object has a
  348. // `rootNode` member, that is used.
  349. //
  350. // args: Object
  351. // a kwArgs object passed along to instantiate()
  352. //
  353. // * noStart: Boolean?
  354. // when set will prevent the parser from calling .startup()
  355. // when locating the nodes.
  356. // * rootNode: DomNode?
  357. // identical to the function's `rootNode` argument, though
  358. // allowed to be passed in via this `args object.
  359. // * template: Boolean
  360. // If true, ignores ContentPane's stopParser flag and parses contents inside of
  361. // a ContentPane inside of a template. This allows dojoAttachPoint on widgets/nodes
  362. // nested inside the ContentPane to work.
  363. // * inherited: Object
  364. // Hash possibly containing dir and lang settings to be applied to
  365. // parsed widgets, unless there's another setting on a sub-node that overrides
  366. // * scope: String
  367. // Root for attribute names to search for. If scopeName is dojo,
  368. // will search for data-dojo-type (or dojoType). For backwards compatibility
  369. // reasons defaults to dojo._scopeName (which is "dojo" except when
  370. // multi-version support is used, when it will be something like dojo16, dojo20, etc.)
  371. // * propsThis: Object
  372. // If specified, "this" referenced from data-dojo-props will refer to propsThis.
  373. // Intended for use from the widgets-in-template feature of `dijit._Templated`
  374. //
  375. // example:
  376. // Parse all widgets on a page:
  377. // | dojo.parser.parse();
  378. //
  379. // example:
  380. // Parse all classes within the node with id="foo"
  381. // | dojo.parser.parse(dojo.byId('foo'));
  382. //
  383. // example:
  384. // Parse all classes in a page, but do not call .startup() on any
  385. // child
  386. // | dojo.parser.parse({ noStart: true })
  387. //
  388. // example:
  389. // Parse all classes in a node, but do not call .startup()
  390. // | dojo.parser.parse(someNode, { noStart:true });
  391. // | // or
  392. // | dojo.parser.parse({ noStart:true, rootNode: someNode });
  393. // determine the root node based on the passed arguments.
  394. var root;
  395. if(!args && rootNode && rootNode.rootNode){
  396. args = rootNode;
  397. root = args.rootNode;
  398. }else{
  399. root = rootNode;
  400. }
  401. root = root ? dojo.byId(root) : dojo.body();
  402. args = args || {};
  403. var attrName = (args.scope || d._scopeName) + "Type", // typically "dojoType"
  404. attrData = "data-" + (args.scope || d._scopeName) + "-"; // typically "data-dojo-"
  405. function scan(parent, list){
  406. // summary:
  407. // Parent is an Object representing a DOMNode, with or without a dojoType specified.
  408. // Scan parent's children looking for nodes with dojoType specified, storing in list[].
  409. // If parent has a dojoType, also collects <script type=dojo/*> children and stores in parent.scripts[].
  410. // parent: Object
  411. // Object representing the parent node, like
  412. // | {
  413. // | node: DomNode, // scan children of this node
  414. // | inherited: {dir: "rtl"}, // dir/lang setting inherited from above node
  415. // |
  416. // | // attributes only set if node has dojoType specified
  417. // | scripts: [], // empty array, put <script type=dojo/*> in here
  418. // | clsInfo: { cls: dijit.form.Button, ...}
  419. // | }
  420. // list: DomNode[]
  421. // Output array of objects (same format as parent) representing nodes to be turned into widgets
  422. // Effective dir and lang settings on parent node, either set directly or inherited from grandparent
  423. var inherited = dojo.clone(parent.inherited);
  424. dojo.forEach(["dir", "lang"], function(name){
  425. // TODO: what if this is a widget and dir/lang are declared in data-dojo-props?
  426. var val = parent.node.getAttribute(name);
  427. if(val){
  428. inherited[name] = val;
  429. }
  430. });
  431. // if parent is a widget, then search for <script type=dojo/*> tags and put them in scripts[].
  432. var scripts = parent.clsInfo && !parent.clsInfo.cls.prototype._noScript ? parent.scripts : null;
  433. // unless parent is a widget with the stopParser flag set, continue search for dojoType, recursively
  434. var recurse = (!parent.clsInfo || !parent.clsInfo.cls.prototype.stopParser) || (args && args.template);
  435. // scan parent's children looking for dojoType and <script type=dojo/*>
  436. for(var child = parent.node.firstChild; child; child = child.nextSibling){
  437. if(child.nodeType == 1){
  438. // FIXME: desupport dojoType in 2.0. use data-dojo-type instead
  439. var type, html5 = recurse && child.getAttribute(attrData + "type");
  440. if(html5){
  441. type = html5;
  442. }else{
  443. // fallback to backward compatible mode, using dojoType. remove in 2.0
  444. type = recurse && child.getAttribute(attrName);
  445. }
  446. var fastpath = html5 == type;
  447. if(type){
  448. // if dojoType/data-dojo-type specified, add to output array of nodes to instantiate
  449. var params = {
  450. "type": type,
  451. fastpath: fastpath,
  452. clsInfo: getClassInfo(type, fastpath), // note: won't find classes declared via dojo.Declaration
  453. node: child,
  454. scripts: [], // <script> nodes that are parent's children
  455. inherited: inherited // dir & lang attributes inherited from parent
  456. };
  457. list.push(params);
  458. // Recurse, collecting <script type="dojo/..."> children, and also looking for
  459. // descendant nodes with dojoType specified (unless the widget has the stopParser flag),
  460. scan(params, list);
  461. }else if(scripts && child.nodeName.toLowerCase() == "script"){
  462. // if <script type="dojo/...">, save in scripts[]
  463. type = child.getAttribute("type");
  464. if (type && /^dojo\/\w/i.test(type)) {
  465. scripts.push(child);
  466. }
  467. }else if(recurse){
  468. // Recurse, looking for grandchild nodes with dojoType specified
  469. scan({
  470. node: child,
  471. inherited: inherited
  472. }, list);
  473. }
  474. }
  475. }
  476. }
  477. // Ignore bogus entries in inherited hash like {dir: ""}
  478. var inherited = {};
  479. if(args && args.inherited){
  480. for(var key in args.inherited){
  481. if(args.inherited[key]){ inherited[key] = args.inherited[key]; }
  482. }
  483. }
  484. // Make list of all nodes on page w/dojoType specified
  485. var list = [];
  486. scan({
  487. node: root,
  488. inherited: inherited
  489. }, list);
  490. // go build the object instances
  491. var mixin = args && args.template ? {template: true} : null;
  492. return this.instantiate(list, mixin, args); // Array
  493. };
  494. }();
  495. //Register the parser callback. It should be the first callback
  496. //after the a11y test.
  497. (function(){
  498. var parseRunner = function(){
  499. if(dojo.config.parseOnLoad){
  500. dojo.parser.parse();
  501. }
  502. };
  503. // FIXME: need to clobber cross-dependency!!
  504. if(dojo.getObject("dijit.wai.onload") === dojo._loaders[0]){
  505. dojo._loaders.splice(1, 0, parseRunner);
  506. }else{
  507. dojo._loaders.unshift(parseRunner);
  508. }
  509. })();
  510. }