source: Dev/branches/rest-dojo-ui/client/dojo/parser.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

File size: 21.0 KB
Line 
1define(
2        ["./_base/kernel", "./_base/lang", "./_base/array", "./_base/html", "./_base/window", "./_base/url",
3                "./_base/json", "./aspect", "./date/stamp", "./query", "./on", "./ready"],
4        function(dojo, dlang, darray, dhtml, dwindow, _Url, djson, aspect, dates, query, don){
5
6// module:
7//              dojo/parser
8// summary:
9//              The Dom/Widget parsing package
10
11new Date("X"); // workaround for #11279, new Date("") == NaN
12
13var features = {
14        // Feature detection for when node.attributes only lists the attributes specified in the markup
15        // rather than old IE/quirks behavior where it lists every default value too
16        "dom-attributes-explicit": document.createElement("div").attributes.length < 40
17};
18function has(feature){
19        return features[feature];
20}
21
22
23dojo.parser = new function(){
24        // summary:
25        //              The Dom/Widget parsing package
26
27        var _nameMap = {
28                // Map from widget name (ex: "dijit.form.Button") to structure mapping
29                // lowercase version of attribute names to the version in the widget ex:
30                //      {
31                //              label: "label",
32                //              onclick: "onClick"
33                //      }
34        };
35        function getNameMap(proto){
36                // summary:
37                //              Returns map from lowercase name to attribute name in class, ex: {onclick: "onClick"}
38                var map = {};
39                for(var name in proto){
40                        if(name.charAt(0)=="_"){ continue; }    // skip internal properties
41                        map[name.toLowerCase()] = name;
42                }
43                return map;
44        }
45        // Widgets like BorderContainer add properties to _Widget via dojo.extend().
46        // If BorderContainer is loaded after _Widget's parameter list has been cached,
47        // we need to refresh that parameter list (for _Widget and all widgets that extend _Widget).
48        aspect.after(dlang, "extend", function(){
49                _nameMap = {};
50        }, true);
51
52        // Map from widget name (ex: "dijit.form.Button") to constructor
53        var _ctorMap = {};
54
55        this._functionFromScript = function(script, attrData){
56                // summary:
57                //              Convert a <script type="dojo/method" args="a, b, c"> ... </script>
58                //              into a function
59                // script: DOMNode
60                //              The <script> DOMNode
61                // attrData: String
62                //              For HTML5 compliance, searches for attrData + "args" (typically
63                //              "data-dojo-args") instead of "args"
64                var preamble = "";
65                var suffix = "";
66                var argsStr = (script.getAttribute(attrData + "args") || script.getAttribute("args"));
67                if(argsStr){
68                        darray.forEach(argsStr.split(/\s*,\s*/), function(part, idx){
69                                preamble += "var "+part+" = arguments["+idx+"]; ";
70                        });
71                }
72                var withStr = script.getAttribute("with");
73                if(withStr && withStr.length){
74                        darray.forEach(withStr.split(/\s*,\s*/), function(part){
75                                preamble += "with("+part+"){";
76                                suffix += "}";
77                        });
78                }
79                return new Function(preamble+script.innerHTML+suffix);
80        };
81
82        this.instantiate = /*====== dojo.parser.instantiate= ======*/function(nodes, mixin, args){
83                // summary:
84                //              Takes array of nodes, and turns them into class instances and
85                //              potentially calls a startup method to allow them to connect with
86                //              any children.
87                // nodes: Array
88                //              Array of nodes or objects like
89                //      |               {
90                //      |                       type: "dijit.form.Button",
91                //      |                       node: DOMNode,
92                //      |                       scripts: [ ... ],       // array of <script type="dojo/..."> children of node
93                //      |                       inherited: { ... }      // settings inherited from ancestors like dir, theme, etc.
94                //      |               }
95                // mixin: Object?
96                //              An object that will be mixed in with each node in the array.
97                //              Values in the mixin will override values in the node, if they
98                //              exist.
99                // args: Object?
100                //              An object used to hold kwArgs for instantiation.
101                //              See parse.args argument for details.
102
103                var thelist = [],
104                mixin = mixin||{};
105                args = args||{};
106
107                // Precompute names of special attributes we are looking for
108                // TODO: for 2.0 default to data-dojo- regardless of scopeName (or maybe scopeName won't exist in 2.0)
109                var dojoType = (args.scope || dojo._scopeName) + "Type",                // typically "dojoType"
110                        attrData = "data-" + (args.scope || dojo._scopeName) + "-",// typically "data-dojo-"
111                        dataDojoType = attrData + "type",                                               // typically "data-dojo-type"
112                        dataDojoProps = attrData + "props",                                             // typically "data-dojo-props"
113                        dataDojoAttachPoint = attrData + "attach-point",
114                        dataDojoAttachEvent = attrData + "attach-event",
115                        dataDojoId = attrData + "id";
116
117                // And make hash to quickly check if a given attribute is special, and to map the name to something friendly
118                var specialAttrs = {};
119                darray.forEach([dataDojoProps, dataDojoType, dojoType, dataDojoId, "jsId", dataDojoAttachPoint,
120                                dataDojoAttachEvent, "dojoAttachPoint", "dojoAttachEvent", "class", "style"], function(name){
121                        specialAttrs[name.toLowerCase()] = name.replace(args.scope, "dojo");
122                });
123
124                darray.forEach(nodes, function(obj){
125                        if(!obj){ return; }
126
127                        var node = obj.node || obj,
128                                type = dojoType in mixin ? mixin[dojoType] : obj.node ? obj.type : (node.getAttribute(dataDojoType) || node.getAttribute(dojoType)),
129                                ctor = _ctorMap[type] || (_ctorMap[type] = dlang.getObject(type)),
130                                proto = ctor && ctor.prototype;
131                        if(!ctor){
132                                throw new Error("Could not load class '" + type);
133                        }
134
135                        // Setup hash to hold parameter settings for this widget.       Start with the parameter
136                        // settings inherited from ancestors ("dir" and "lang").
137                        // Inherited setting may later be overridden by explicit settings on node itself.
138                        var params = {};
139
140                        if(args.defaults){
141                                // settings for the document itself (or whatever subtree is being parsed)
142                                dlang.mixin(params, args.defaults);
143                        }
144                        if(obj.inherited){
145                                // settings from dir=rtl or lang=... on a node above this node
146                                dlang.mixin(params, obj.inherited);
147                        }
148
149                        // Get list of attributes explicitly listed in the markup
150                        var attributes;
151                        if(has("dom-attributes-explicit")){
152                                // Standard path to get list of user specified attributes
153                                attributes = node.attributes;
154                        }else{
155                                // Special path for IE, avoid (sometimes >100) bogus entries in node.attributes
156                                var clone = /^input$|^img$/i.test(node.nodeName) ? node : node.cloneNode(false),
157                                        attrs = clone.outerHTML.replace(/=[^\s"']+|="[^"]*"|='[^']*'/g, "").replace(/^\s*<[a-zA-Z0-9]*/, "").replace(/>.*$/, "");
158
159                                attributes = darray.map(attrs.split(/\s+/), function(name){
160                                        var lcName = name.toLowerCase();
161                                        return {
162                                                name: name,
163                                                // getAttribute() doesn't work for button.value, returns innerHTML of button.
164                                                // but getAttributeNode().value doesn't work for the form.encType or li.value
165                                                value: (node.nodeName == "LI" && name == "value") || lcName == "enctype" ?
166                                                                node.getAttribute(lcName) : node.getAttributeNode(lcName).value,
167                                                specified: true
168                                        };
169                                });
170                        }
171
172                        // Read in attributes and process them, including data-dojo-props, data-dojo-type,
173                        // dojoAttachPoint, etc., as well as normal foo=bar attributes.
174                        var i=0, item;
175                        while(item = attributes[i++]){
176                                if(!item || !item.specified){
177                                        continue;
178                                }
179
180                                var name = item.name,
181                                        lcName = name.toLowerCase(),
182                                        value = item.value;
183
184                                if(lcName in specialAttrs){
185                                        switch(specialAttrs[lcName]){
186
187                                        // Data-dojo-props.   Save for later to make sure it overrides direct foo=bar settings
188                                        case "data-dojo-props":
189                                                var extra = value;
190                                                break;
191
192                                        // data-dojo-id or jsId. TODO: drop jsId in 2.0
193                                        case "data-dojo-id":
194                                        case "jsId":
195                                                var jsname = value;
196                                                break;
197
198                                        // For the benefit of _Templated
199                                        case "data-dojo-attach-point":
200                                        case "dojoAttachPoint":
201                                                params.dojoAttachPoint = value;
202                                                break;
203                                        case "data-dojo-attach-event":
204                                        case "dojoAttachEvent":
205                                                params.dojoAttachEvent = value;
206                                                break;
207
208                                        // Special parameter handling needed for IE
209                                        case "class":
210                                                params["class"] = node.className;
211                                                break;
212                                        case "style":
213                                                params["style"] = node.style && node.style.cssText;
214                                                break;
215                                        }
216                                }else{
217                                        // Normal attribute, ex: value="123"
218
219                                        // Find attribute in widget corresponding to specified name.
220                                        // May involve case conversion, ex: onclick --> onClick
221                                        if(!(name in proto)){
222                                                var map = (_nameMap[type] || (_nameMap[type] = getNameMap(proto)));
223                                                name = map[lcName] || name;
224                                        }
225
226                                        // Set params[name] to value, doing type conversion
227                                        if(name in proto){
228                                                switch(typeof proto[name]){
229                                                case "string":
230                                                        params[name] = value;
231                                                        break;
232                                                case "number":
233                                                        params[name] = value.length ? Number(value) : NaN;
234                                                        break;
235                                                case "boolean":
236                                                        // for checked/disabled value might be "" or "checked".  interpret as true.
237                                                        params[name] = value.toLowerCase() != "false";
238                                                        break;
239                                                case "function":
240                                                        if(value === "" || value.search(/[^\w\.]+/i) != -1){
241                                                                // The user has specified some text for a function like "return x+5"
242                                                                params[name] = new Function(value);
243                                                        }else{
244                                                                // The user has specified the name of a function like "myOnClick"
245                                                                // or a single word function "return"
246                                                                params[name] = dlang.getObject(value, false) || new Function(value);
247                                                        }
248                                                        break;
249                                                default:
250                                                        var pVal = proto[name];
251                                                        params[name] =
252                                                                (pVal && "length" in pVal) ? (value ? value.split(/\s*,\s*/) : []) :    // array
253                                                                        (pVal instanceof Date) ?
254                                                                                (value == "" ? new Date("") :   // the NaN of dates
255                                                                                value == "now" ? new Date() :   // current date
256                                                                                dates.fromISOString(value)) :
257                                                                (pVal instanceof dojo._Url) ? (dojo.baseUrl + value) :
258                                                                djson.fromJson(value);
259                                                }
260                                        }else{
261                                                params[name] = value;
262                                        }
263                                }
264                        }
265
266                        // Mix things found in data-dojo-props into the params, overriding any direct settings
267                        if(extra){
268                                try{
269                                        extra = djson.fromJson.call(args.propsThis, "{" + extra + "}");
270                                        dlang.mixin(params, extra);
271                                }catch(e){
272                                        // give the user a pointer to their invalid parameters. FIXME: can we kill this in production?
273                                        throw new Error(e.toString() + " in data-dojo-props='" + extra + "'");
274                                }
275                        }
276
277                        // Any parameters specified in "mixin" override everything else.
278                        dlang.mixin(params, mixin);
279
280                        var scripts = obj.node ? obj.scripts : (ctor && (ctor._noScript || proto._noScript) ? [] :
281                                                query("> script[type^='dojo/']", node));
282
283                        // Process <script type="dojo/*"> script tags
284                        // <script type="dojo/method" event="foo"> tags are added to params, and passed to
285                        // the widget on instantiation.
286                        // <script type="dojo/method"> tags (with no event) are executed after instantiation
287                        // <script type="dojo/connect" data-dojo-event="foo"> tags are dojo.connected after instantiation
288                        // <script type="dojo/watch" data-dojo-prop="foo"> tags are dojo.watch after instantiation
289                        // <script type="dojo/on" data-dojo-event="foo"> tags are dojo.on after instantiation
290                        // note: dojo/* script tags cannot exist in self closing widgets, like <input />
291                        var connects = [],      // functions to connect after instantiation
292                                calls = [],             // functions to call after instantiation
293                                watch = [],  //functions to watch after instantiation
294                                on = []; //functions to on after instantiation
295
296                        if(scripts){
297                                for(i=0; i<scripts.length; i++){
298                                        var script = scripts[i];
299                                        node.removeChild(script);
300                                        // FIXME: drop event="" support in 2.0. use data-dojo-event="" instead
301                                        var event = (script.getAttribute(attrData + "event") || script.getAttribute("event")),
302                                                prop = script.getAttribute(attrData + "prop"),
303                                                type = script.getAttribute("type"),
304                                                nf = this._functionFromScript(script, attrData);
305                                        if(event){
306                                                if(type == "dojo/connect"){
307                                                        connects.push({event: event, func: nf});
308                                                }else if(type == "dojo/on"){
309                                                        on.push({event: event, func: nf});
310                                                }else{
311                                                        params[event] = nf;
312                                                }
313                                        }else if(type == "dojo/watch"){
314                                                watch.push({prop: prop, func: nf});
315                                        }else{
316                                                calls.push(nf);
317                                        }
318                                }
319                        }
320
321                        // create the instance
322                        var markupFactory = ctor.markupFactory || proto.markupFactory;
323                        var instance = markupFactory ? markupFactory(params, node, ctor) : new ctor(params, node);
324                        thelist.push(instance);
325
326                        // map it to the JS namespace if that makes sense
327                        if(jsname){
328                                dlang.setObject(jsname, instance);
329                        }
330
331                        // process connections and startup functions
332                        for(i=0; i<connects.length; i++){
333                                aspect.after(instance, connects[i].event, dojo.hitch(instance, connects[i].func), true);
334                        }
335                        for(i=0; i<calls.length; i++){
336                                calls[i].call(instance);
337                        }
338                        for(i=0; i<watch.length; i++){
339                                instance.watch(watch[i].prop, watch[i].func);
340                        }
341                        for(i=0; i<on.length; i++){
342                                don(instance, on[i].event, on[i].func);
343                        }
344                }, this);
345
346                // Call startup on each top level instance if it makes sense (as for
347                // widgets).  Parent widgets will recursively call startup on their
348                // (non-top level) children
349                if(!mixin._started){
350                        darray.forEach(thelist, function(instance){
351                                if( !args.noStart && instance  &&
352                                        dlang.isFunction(instance.startup) &&
353                                        !instance._started
354                                ){
355                                        instance.startup();
356                                }
357                        });
358                }
359                return thelist;
360        };
361
362        this.parse = /*====== dojo.parser.parse= ======*/ function(rootNode, args){
363                // summary:
364                //              Scan the DOM for class instances, and instantiate them.
365                //
366                // description:
367                //              Search specified node (or root node) recursively for class instances,
368                //              and instantiate them. Searches for either data-dojo-type="Class" or
369                //              dojoType="Class" where "Class" is a a fully qualified class name,
370                //              like `dijit.form.Button`
371                //
372                //              Using `data-dojo-type`:
373                //              Attributes using can be mixed into the parameters used to instantiate the
374                //              Class by using a `data-dojo-props` attribute on the node being converted.
375                //              `data-dojo-props` should be a string attribute to be converted from JSON.
376                //
377                //              Using `dojoType`:
378                //              Attributes are read from the original domNode and converted to appropriate
379                //              types by looking up the Class prototype values. This is the default behavior
380                //              from Dojo 1.0 to Dojo 1.5. `dojoType` support is deprecated, and will
381                //              go away in Dojo 2.0.
382                //
383                // rootNode: DomNode?
384                //              A default starting root node from which to start the parsing. Can be
385                //              omitted, defaulting to the entire document. If omitted, the `args`
386                //              object can be passed in this place. If the `args` object has a
387                //              `rootNode` member, that is used.
388                //
389                // args: Object
390                //              a kwArgs object passed along to instantiate()
391                //
392                //                      * noStart: Boolean?
393                //                              when set will prevent the parser from calling .startup()
394                //                              when locating the nodes.
395                //                      * rootNode: DomNode?
396                //                              identical to the function's `rootNode` argument, though
397                //                              allowed to be passed in via this `args object.
398                //                      * template: Boolean
399                //                              If true, ignores ContentPane's stopParser flag and parses contents inside of
400                //                              a ContentPane inside of a template.   This allows dojoAttachPoint on widgets/nodes
401                //                              nested inside the ContentPane to work.
402                //                      * inherited: Object
403                //                              Hash possibly containing dir and lang settings to be applied to
404                //                              parsed widgets, unless there's another setting on a sub-node that overrides
405                //                      * scope: String
406                //                              Root for attribute names to search for.   If scopeName is dojo,
407                //                              will search for data-dojo-type (or dojoType).   For backwards compatibility
408                //                              reasons defaults to dojo._scopeName (which is "dojo" except when
409                //                              multi-version support is used, when it will be something like dojo16, dojo20, etc.)
410                //                      * propsThis: Object
411                //                              If specified, "this" referenced from data-dojo-props will refer to propsThis.
412                //                              Intended for use from the widgets-in-template feature of `dijit._WidgetsInTemplateMixin`
413                //
414                // example:
415                //              Parse all widgets on a page:
416                //      |               dojo.parser.parse();
417                //
418                // example:
419                //              Parse all classes within the node with id="foo"
420                //      |               dojo.parser.parse(dojo.byId('foo'));
421                //
422                // example:
423                //              Parse all classes in a page, but do not call .startup() on any
424                //              child
425                //      |               dojo.parser.parse({ noStart: true })
426                //
427                // example:
428                //              Parse all classes in a node, but do not call .startup()
429                //      |               dojo.parser.parse(someNode, { noStart:true });
430                //      |               // or
431                //      |               dojo.parser.parse({ noStart:true, rootNode: someNode });
432
433                // determine the root node based on the passed arguments.
434                var root;
435                if(!args && rootNode && rootNode.rootNode){
436                        args = rootNode;
437                        root = args.rootNode;
438                }else{
439                        root = rootNode;
440                }
441                root = root ? dhtml.byId(root) : dwindow.body();
442                args = args || {};
443
444                var dojoType = (args.scope || dojo._scopeName) + "Type",                // typically "dojoType"
445                        attrData = "data-" + (args.scope || dojo._scopeName) + "-",     // typically "data-dojo-"
446                        dataDojoType = attrData + "type",                                               // typically "data-dojo-type"
447                        dataDojoTextDir = attrData + "textdir";                                 // typically "data-dojo-textdir"
448
449                // List of all nodes on page w/dojoType specified
450                var list = [];
451
452                // Info on DOMNode currently being processed
453                var node = root.firstChild;
454
455                // Info on parent of DOMNode currently being processed
456                //      - inherited: dir, lang, and textDir setting of parent, or inherited by parent
457                //      - parent: pointer to identical structure for my parent (or null if no parent)
458                //      - scripts: if specified, collects <script type="dojo/..."> type nodes from children
459                var inherited = args && args.inherited;
460                if(!inherited){
461                        function findAncestorAttr(node, attr){
462                                return (node.getAttribute && node.getAttribute(attr)) ||
463                                        (node !== dwindow.doc && node !== dwindow.doc.documentElement && node.parentNode ? findAncestorAttr(node.parentNode, attr) : null);
464                        }
465                        inherited = {
466                                dir: findAncestorAttr(root, "dir"),
467                                lang: findAncestorAttr(root, "lang"),
468                                textDir: findAncestorAttr(root, dataDojoTextDir)
469                        };
470                        for(var key in inherited){
471                                if(!inherited[key]){ delete inherited[key]; }
472                        }
473                }
474                var parent = {
475                        inherited: inherited
476                };
477
478                // For collecting <script type="dojo/..."> type nodes (when null, we don't need to collect)
479                var scripts;
480
481                // when true, only look for <script type="dojo/..."> tags, and don't recurse to children
482                var scriptsOnly;
483
484                function getEffective(parent){
485                        // summary:
486                        //              Get effective dir, lang, textDir settings for specified obj
487                        //              (matching "parent" object structure above), and do caching.
488                        //              Take care not to return null entries.
489                        if(!parent.inherited){
490                                parent.inherited = {};
491                                var node = parent.node,
492                                        grandparent = getEffective(parent.parent);
493                                var inherited  = {
494                                        dir: node.getAttribute("dir") || grandparent.dir,
495                                        lang: node.getAttribute("lang") || grandparent.lang,
496                                        textDir: node.getAttribute(dataDojoTextDir) || grandparent.textDir
497                                };
498                                for(var key in inherited){
499                                        if(inherited[key]){
500                                                parent.inherited[key] = inherited[key];
501                                        }
502                                }
503                        }
504                        return parent.inherited;
505                }
506
507                // DFS on DOM tree, collecting nodes with data-dojo-type specified.
508                while(true){
509                        if(!node){
510                                // Finished this level, continue to parent's next sibling
511                                if(!parent || !parent.node){
512                                        break;
513                                }
514                                node = parent.node.nextSibling;
515                                scripts = parent.scripts;
516                                scriptsOnly = false;
517                                parent = parent.parent;
518                                continue;
519                        }
520
521                        if(node.nodeType != 1){
522                                // Text or comment node, skip to next sibling
523                                node = node.nextSibling;
524                                continue;
525                        }
526
527                        if(scripts && node.nodeName.toLowerCase() == "script"){
528                                // Save <script type="dojo/..."> for parent, then continue to next sibling
529                                type = node.getAttribute("type");
530                                if(type && /^dojo\/\w/i.test(type)){
531                                        scripts.push(node);
532                                }
533                                node = node.nextSibling;
534                                continue;
535                        }
536                        if(scriptsOnly){
537                                node = node.nextSibling;
538                                continue;
539                        }
540
541                        // Check for data-dojo-type attribute, fallback to backward compatible dojoType
542                        var type = node.getAttribute(dataDojoType) || node.getAttribute(dojoType);
543
544                        // Short circuit for leaf nodes containing nothing [but text]
545                        var firstChild = node.firstChild;
546                        if(!type && (!firstChild || (firstChild.nodeType == 3 && !firstChild.nextSibling))){
547                                node = node.nextSibling;
548                                continue;
549                        }
550
551                        // Setup data structure to save info on current node for when we return from processing descendant nodes
552                        var current = {
553                                node: node,
554                                scripts: scripts,
555                                parent: parent
556                        };
557
558                        // If dojoType/data-dojo-type specified, add to output array of nodes to instantiate
559                        var ctor = type && (_ctorMap[type] || (_ctorMap[type] = dlang.getObject(type))), // note: won't find classes declared via dojo.Declaration
560                                childScripts = ctor && !ctor.prototype._noScript ? [] : null; // <script> nodes that are parent's children
561                        if(type){
562                                list.push({
563                                        "type": type,
564                                        node: node,
565                                        scripts: childScripts,
566                                        inherited: getEffective(current) // dir & lang settings for current node, explicit or inherited
567                                });
568                        }
569
570                        // Recurse, collecting <script type="dojo/..."> children, and also looking for
571                        // descendant nodes with dojoType specified (unless the widget has the stopParser flag).
572                        // When finished with children, go to my next sibling.
573                        node = firstChild;
574                        scripts = childScripts;
575                        scriptsOnly = ctor && ctor.prototype.stopParser && !(args && args.template);
576                        parent = current;
577
578                }
579
580                // go build the object instances
581                var mixin = args && args.template ? {template: true} : null;
582                return this.instantiate(list, mixin, args); // Array
583        };
584}();
585
586
587//Register the parser callback. It should be the first callback
588//after the a11y test.
589if(dojo.config.parseOnLoad){
590        dojo.ready(100, dojo.parser, "parse");
591}
592
593return dojo.parser;
594});
Note: See TracBrowser for help on using the repository browser.