You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
936 lines
35 KiB
936 lines
35 KiB
define([ |
|
"require", "./_base/kernel", "./_base/lang", "./_base/array", "./_base/config", "./dom", "./_base/window", |
|
"./_base/url", "./aspect", "./promise/all", "./date/stamp", "./Deferred", "./has", "./json5", "./query", "./on", |
|
"./ready" |
|
], function(require, dojo, dlang, darray, config, dom, dwindow, _Url, aspect, all, dates, Deferred, has, json5, query, |
|
don, ready){ |
|
|
|
// module: |
|
// dojo/parser |
|
|
|
new Date("X"); // workaround for #11279, new Date("") == NaN |
|
|
|
var myEval; |
|
if(has('csp-restrictions')) { |
|
// JSON5 data attributes can be parsed without using eval; JS expressions will throw an error |
|
myEval = json5.parse; |
|
} |
|
else { |
|
myEval = function(text){ |
|
// data-dojo-props etc. is not restricted to JSON, it can be any javascript |
|
/* jshint -W061 */ |
|
return eval("(" + text + ")"); |
|
}; |
|
} |
|
|
|
// Widgets like BorderContainer add properties to _Widget via dojo.extend(). |
|
// If BorderContainer is loaded after _Widget's parameter list has been cached, |
|
// we need to refresh that parameter list (for _Widget and all widgets that extend _Widget). |
|
var extendCnt = 0; |
|
aspect.after(dlang, "extend", function(){ |
|
extendCnt++; |
|
}, true); |
|
|
|
function getNameMap(ctor){ |
|
// summary: |
|
// Returns map from lowercase name to attribute name in class, ex: {onclick: "onClick"} |
|
var map = ctor._nameCaseMap, proto = ctor.prototype; |
|
|
|
// Create the map if it's undefined. |
|
// Refresh the map if a superclass was possibly extended with new methods since the map was created. |
|
if(!map || map._extendCnt < extendCnt){ |
|
map = ctor._nameCaseMap = {}; |
|
for(var name in proto){ |
|
if(name.charAt(0) === "_"){ |
|
continue; |
|
} // skip internal properties |
|
map[name.toLowerCase()] = name; |
|
} |
|
map._extendCnt = extendCnt; |
|
} |
|
return map; |
|
} |
|
|
|
function getCtor(/*String[]*/ types, /*Function?*/ contextRequire){ |
|
// summary: |
|
// Retrieves a constructor. If the types array contains more than one class/MID then the |
|
// subsequent classes will be mixed into the first class and a unique constructor will be |
|
// returned for that array. |
|
|
|
if(!contextRequire){ |
|
contextRequire = require; |
|
} |
|
|
|
// Map from widget name or list of widget names(ex: "dijit/form/Button,acme/MyMixin") to a constructor. |
|
// Keep separate map for each requireContext to avoid false matches (ex: "./Foo" can mean different things |
|
// depending on context.) |
|
var ctorMap = contextRequire._dojoParserCtorMap || (contextRequire._dojoParserCtorMap = {}); |
|
|
|
var ts = types.join(); |
|
if(!ctorMap[ts]){ |
|
var mixins = []; |
|
for(var i = 0, l = types.length; i < l; i++){ |
|
var t = types[i]; |
|
// TODO: Consider swapping getObject and require in the future |
|
mixins[mixins.length] = (ctorMap[t] = ctorMap[t] || (dlang.getObject(t) || (~t.indexOf('/') && |
|
contextRequire(t)))); |
|
} |
|
var ctor = mixins.shift(); |
|
ctorMap[ts] = mixins.length ? (ctor.createSubclass ? ctor.createSubclass(mixins) : ctor.extend.apply(ctor, mixins)) : ctor; |
|
} |
|
|
|
return ctorMap[ts]; |
|
} |
|
|
|
var parser = { |
|
// summary: |
|
// The Dom/Widget parsing package |
|
|
|
_clearCache: function(){ |
|
// summary: |
|
// Clear cached data. Used mainly for benchmarking. |
|
extendCnt++; |
|
_ctorMap = {}; |
|
}, |
|
|
|
_functionFromScript: function(script, attrData){ |
|
// summary: |
|
// Convert a `<script type="dojo/method" args="a, b, c"> ... </script>` |
|
// into a function |
|
// script: DOMNode |
|
// The `<script>` DOMNode |
|
// attrData: String |
|
// For HTML5 compliance, searches for attrData + "args" (typically |
|
// "data-dojo-args") instead of "args" |
|
var preamble = "", |
|
suffix = "", |
|
argsStr = (script.getAttribute(attrData + "args") || script.getAttribute("args")), |
|
withStr = script.getAttribute("with"); |
|
|
|
// Convert any arguments supplied in script tag into an array to be passed to the |
|
var fnArgs = (argsStr || "").split(/\s*,\s*/); |
|
|
|
if(withStr && withStr.length){ |
|
darray.forEach(withStr.split(/\s*,\s*/), function(part){ |
|
preamble += "with(" + part + "){"; |
|
suffix += "}"; |
|
}); |
|
} |
|
|
|
return new Function(fnArgs, preamble + script.innerHTML + suffix); |
|
}, |
|
|
|
instantiate: function(nodes, mixin, options){ |
|
// summary: |
|
// Takes array of nodes, and turns them into class instances and |
|
// potentially calls a startup method to allow them to connect with |
|
// any children. |
|
// nodes: Array |
|
// Array of DOM nodes |
|
// mixin: Object? |
|
// An object that will be mixed in with each node in the array. |
|
// Values in the mixin will override values in the node, if they |
|
// exist. |
|
// options: Object? |
|
// An object used to hold kwArgs for instantiation. |
|
// See parse.options argument for details. |
|
// returns: |
|
// Array of instances. |
|
|
|
mixin = mixin || {}; |
|
options = options || {}; |
|
|
|
var dojoType = (options.scope || dojo._scopeName) + "Type", // typically "dojoType" |
|
attrData = "data-" + (options.scope || dojo._scopeName) + "-", // typically "data-dojo-" |
|
dataDojoType = attrData + "type", // typically "data-dojo-type" |
|
dataDojoMixins = attrData + "mixins"; // typically "data-dojo-mixins" |
|
|
|
var list = []; |
|
darray.forEach(nodes, function(node){ |
|
var type = dojoType in mixin ? mixin[dojoType] : node.getAttribute(dataDojoType) || node.getAttribute(dojoType); |
|
if(type){ |
|
var mixinsValue = node.getAttribute(dataDojoMixins), |
|
types = mixinsValue ? [type].concat(mixinsValue.split(/\s*,\s*/)) : [type]; |
|
|
|
list.push({ |
|
node: node, |
|
types: types |
|
}); |
|
} |
|
}); |
|
|
|
// Instantiate the nodes and return the list of instances. |
|
return this._instantiate(list, mixin, options); |
|
}, |
|
|
|
_instantiate: function(nodes, mixin, options, returnPromise){ |
|
// summary: |
|
// Takes array of objects representing nodes, and turns them into class instances and |
|
// potentially calls a startup method to allow them to connect with |
|
// any children. |
|
// nodes: Array |
|
// Array of objects like |
|
// | { |
|
// | ctor: Function (may be null) |
|
// | types: ["dijit/form/Button", "acme/MyMixin"] (used if ctor not specified) |
|
// | node: DOMNode, |
|
// | scripts: [ ... ], // array of <script type="dojo/..."> children of node |
|
// | inherited: { ... } // settings inherited from ancestors like dir, theme, etc. |
|
// | } |
|
// mixin: Object |
|
// An object that will be mixed in with each node in the array. |
|
// Values in the mixin will override values in the node, if they |
|
// exist. |
|
// options: Object |
|
// An options object used to hold kwArgs for instantiation. |
|
// See parse.options argument for details. |
|
// returnPromise: Boolean |
|
// Return a Promise rather than the instance; supports asynchronous widget creation. |
|
// returns: |
|
// Array of instances, or if returnPromise is true, a promise for array of instances |
|
// that resolves when instances have finished initializing. |
|
|
|
// Call widget constructors. Some may be asynchronous and return promises. |
|
var thelist = darray.map(nodes, function(obj){ |
|
var ctor = obj.ctor || getCtor(obj.types, options.contextRequire); |
|
// If we still haven't resolved a ctor, it is fatal now |
|
if(!ctor){ |
|
throw new Error("Unable to resolve constructor for: '" + obj.types.join() + "'"); |
|
} |
|
return this.construct(ctor, obj.node, mixin, options, obj.scripts, obj.inherited); |
|
}, this); |
|
|
|
// After all widget construction finishes, call startup on each top level instance if it makes sense (as for |
|
// widgets). Parent widgets will recursively call startup on their (non-top level) children |
|
function onConstruct(thelist){ |
|
if(!mixin._started && !options.noStart){ |
|
darray.forEach(thelist, function(instance){ |
|
if(typeof instance.startup === "function" && !instance._started){ |
|
instance.startup(); |
|
} |
|
}); |
|
} |
|
|
|
return thelist; |
|
} |
|
|
|
if(returnPromise){ |
|
return all(thelist).then(onConstruct); |
|
}else{ |
|
// Back-compat path, remove for 2.0 |
|
return onConstruct(thelist); |
|
} |
|
}, |
|
|
|
construct: function(ctor, node, mixin, options, scripts, inherited){ |
|
// summary: |
|
// Calls new ctor(params, node), where params is the hash of parameters specified on the node, |
|
// excluding data-dojo-type and data-dojo-mixins. Does not call startup(). |
|
// ctor: Function |
|
// Widget constructor. |
|
// node: DOMNode |
|
// This node will be replaced/attached to by the widget. It also specifies the arguments to pass to ctor. |
|
// mixin: Object? |
|
// Attributes in this object will be passed as parameters to ctor, |
|
// overriding attributes specified on the node. |
|
// options: Object? |
|
// An options object used to hold kwArgs for instantiation. See parse.options argument for details. |
|
// scripts: DomNode[]? |
|
// Array of `<script type="dojo/*">` DOMNodes. If not specified, will search for `<script>` tags inside node. |
|
// inherited: Object? |
|
// Settings from dir=rtl or lang=... on a node above this node. Overrides options.inherited. |
|
// returns: |
|
// Instance or Promise for the instance, if markupFactory() itself returned a promise |
|
|
|
var proto = ctor && ctor.prototype; |
|
options = options || {}; |
|
|
|
// Setup hash to hold parameter settings for this widget. Start with the parameter |
|
// settings inherited from ancestors ("dir" and "lang"). |
|
// Inherited setting may later be overridden by explicit settings on node itself. |
|
var params = {}; |
|
|
|
if(options.defaults){ |
|
// settings for the document itself (or whatever subtree is being parsed) |
|
dlang.mixin(params, options.defaults); |
|
} |
|
if(inherited){ |
|
// settings from dir=rtl or lang=... on a node above this node |
|
dlang.mixin(params, inherited); |
|
} |
|
|
|
// Get list of attributes explicitly listed in the markup |
|
var attributes; |
|
if(has("dom-attributes-explicit")){ |
|
// Standard path to get list of user specified attributes |
|
attributes = node.attributes; |
|
}else if(has("dom-attributes-specified-flag")){ |
|
// Special processing needed for IE8, to skip a few faux values in attributes[] |
|
attributes = darray.filter(node.attributes, function(a){ |
|
return a.specified; |
|
}); |
|
}else{ |
|
// Special path for IE6-7, avoid (sometimes >100) bogus entries in node.attributes |
|
var clone = /^input$|^img$/i.test(node.nodeName) ? node : node.cloneNode(false), |
|
attrs = clone.outerHTML.replace(/=[^\s"']+|="[^"]*"|='[^']*'/g, "").replace(/^\s*<[a-zA-Z0-9]*\s*/, "").replace(/\s*>.*$/, ""); |
|
|
|
attributes = darray.map(attrs.split(/\s+/), function(name){ |
|
var lcName = name.toLowerCase(); |
|
return { |
|
name: name, |
|
// getAttribute() doesn't work for button.value, returns innerHTML of button. |
|
// but getAttributeNode().value doesn't work for the form.encType or li.value |
|
value: (node.nodeName == "LI" && name == "value") || lcName == "enctype" ? |
|
node.getAttribute(lcName) : node.getAttributeNode(lcName).value |
|
}; |
|
}); |
|
} |
|
|
|
// Hash to convert scoped attribute name (ex: data-dojo17-params) to something friendly (ex: data-dojo-params) |
|
// TODO: remove scope for 2.0 |
|
var scope = options.scope || dojo._scopeName, |
|
attrData = "data-" + scope + "-", // typically "data-dojo-" |
|
hash = {}; |
|
if(scope !== "dojo"){ |
|
hash[attrData + "props"] = "data-dojo-props"; |
|
hash[attrData + "type"] = "data-dojo-type"; |
|
hash[attrData + "mixins"] = "data-dojo-mixins"; |
|
hash[scope + "type"] = "dojotype"; |
|
hash[attrData + "id"] = "data-dojo-id"; |
|
} |
|
|
|
// Read in attributes and process them, including data-dojo-props, data-dojo-type, |
|
// dojoAttachPoint, etc., as well as normal foo=bar attributes. |
|
var i = 0, item, funcAttrs = [], jsname, extra; |
|
while(item = attributes[i++]){ |
|
var name = item.name, |
|
lcName = name.toLowerCase(), |
|
value = item.value; |
|
|
|
switch(hash[lcName] || lcName){ |
|
// Already processed, just ignore |
|
case "data-dojo-type": |
|
case "dojotype": |
|
case "data-dojo-mixins": |
|
break; |
|
|
|
// Data-dojo-props. Save for later to make sure it overrides direct foo=bar settings |
|
case "data-dojo-props": |
|
extra = value; |
|
break; |
|
|
|
// data-dojo-id or jsId. TODO: drop jsId in 2.0 |
|
case "data-dojo-id": |
|
case "jsid": |
|
jsname = value; |
|
break; |
|
|
|
// For the benefit of _Templated |
|
case "data-dojo-attach-point": |
|
case "dojoattachpoint": |
|
params.dojoAttachPoint = value; |
|
break; |
|
case "data-dojo-attach-event": |
|
case "dojoattachevent": |
|
params.dojoAttachEvent = value; |
|
break; |
|
|
|
// Special parameter handling needed for IE |
|
case "class": |
|
params["class"] = node.className; |
|
break; |
|
case "style": |
|
params["style"] = node.style && node.style.cssText; |
|
break; |
|
default: |
|
// Normal attribute, ex: value="123" |
|
|
|
// Find attribute in widget corresponding to specified name. |
|
// May involve case conversion, ex: onclick --> onClick |
|
if(!(name in proto)){ |
|
var map = getNameMap(ctor); |
|
name = map[lcName] || name; |
|
} |
|
|
|
// Set params[name] to value, doing type conversion |
|
if(name in proto){ |
|
switch(typeof proto[name]){ |
|
case "string": |
|
params[name] = value; |
|
break; |
|
case "number": |
|
params[name] = value.length ? Number(value) : NaN; |
|
break; |
|
case "boolean": |
|
// for checked/disabled value might be "" or "checked". interpret as true. |
|
params[name] = value.toLowerCase() != "false"; |
|
break; |
|
case "function": |
|
if(value === "" || value.search(/[^\w\.]+/i) != -1){ |
|
// The user has specified some text for a function like "return x+5" |
|
params[name] = new Function(value); |
|
}else{ |
|
// The user has specified the name of a global function like "myOnClick" |
|
// or a single word function "return" |
|
params[name] = dlang.getObject(value, false) || new Function(value); |
|
} |
|
funcAttrs.push(name); // prevent "double connect", see #15026 |
|
break; |
|
default: |
|
var pVal = proto[name]; |
|
try{ |
|
params[name] = |
|
(pVal && "length" in pVal) ? (value ? value.split(/\s*,\s*/) : []) : // array |
|
(pVal instanceof Date) ? |
|
(value == "" ? new Date("") : // the NaN of dates |
|
value == "now" ? new Date() : // current date |
|
dates.fromISOString(value)) : |
|
(pVal instanceof _Url) ? (dojo.baseUrl + value) : |
|
myEval(value); |
|
} |
|
catch(error){ |
|
console.error(error); |
|
} |
|
} |
|
}else{ |
|
params[name] = value; |
|
} |
|
} |
|
} |
|
|
|
// Remove function attributes from DOMNode to prevent "double connect" problem, see #15026. |
|
// Do this as a separate loop since attributes[] is often a live collection (depends on the browser though). |
|
for(var j = 0; j < funcAttrs.length; j++){ |
|
var lcfname = funcAttrs[j].toLowerCase(); |
|
node.removeAttribute(lcfname); |
|
node[lcfname] = null; |
|
} |
|
|
|
// Mix things found in data-dojo-props into the params, overriding any direct settings |
|
if(extra){ |
|
try{ |
|
extra = myEval.call(options.propsThis, "{" + extra + "}"); |
|
dlang.mixin(params, extra); |
|
}catch(e){ |
|
// give the user a pointer to their invalid parameters. FIXME: can we kill this in production? |
|
throw new Error(e.toString() + " in data-dojo-props='" + extra + "'"); |
|
} |
|
} |
|
|
|
// Any parameters specified in "mixin" override everything else. |
|
dlang.mixin(params, mixin); |
|
|
|
// Get <script> nodes associated with this widget, if they weren't specified explicitly |
|
if(!scripts){ |
|
scripts = (ctor && (ctor._noScript || proto._noScript) ? [] : query("> script[type^='dojo/']", node)); |
|
} |
|
|
|
// Process <script type="dojo/*"> script tags |
|
// <script type="dojo/method" data-dojo-event="foo"> tags are added to params, and passed to |
|
// the widget on instantiation. |
|
// <script type="dojo/method"> tags (with no event) are executed after instantiation |
|
// <script type="dojo/connect" data-dojo-event="foo"> tags are dojo.connected after instantiation, |
|
// and likewise with <script type="dojo/aspect" data-dojo-method="foo"> |
|
// <script type="dojo/watch" data-dojo-prop="foo"> tags are dojo.watch after instantiation |
|
// <script type="dojo/on" data-dojo-event="foo"> tags are dojo.on after instantiation |
|
// note: dojo/* script tags cannot exist in self closing widgets, like <input /> |
|
var aspects = [], // aspects to connect after instantiation |
|
calls = [], // functions to call after instantiation |
|
watches = [], // functions to watch after instantiation |
|
ons = []; // functions to on after instantiation |
|
|
|
if(scripts){ |
|
for(i = 0; i < scripts.length; i++){ |
|
var script = scripts[i]; |
|
node.removeChild(script); |
|
// FIXME: drop event="" support in 2.0. use data-dojo-event="" instead |
|
var event = (script.getAttribute(attrData + "event") || script.getAttribute("event")), |
|
prop = script.getAttribute(attrData + "prop"), |
|
method = script.getAttribute(attrData + "method"), |
|
advice = script.getAttribute(attrData + "advice"), |
|
scriptType = script.getAttribute("type"), |
|
nf = this._functionFromScript(script, attrData); |
|
if(event){ |
|
if(scriptType == "dojo/connect"){ |
|
aspects.push({ method: event, func: nf }); |
|
}else if(scriptType == "dojo/on"){ |
|
ons.push({ event: event, func: nf }); |
|
}else{ |
|
// <script type="dojo/method" data-dojo-event="foo"> |
|
// TODO for 2.0: use data-dojo-method="foo" instead (also affects dijit/Declaration) |
|
params[event] = nf; |
|
} |
|
}else if(scriptType == "dojo/aspect"){ |
|
aspects.push({ method: method, advice: advice, func: nf }); |
|
}else if(scriptType == "dojo/watch"){ |
|
watches.push({ prop: prop, func: nf }); |
|
}else{ |
|
calls.push(nf); |
|
} |
|
} |
|
} |
|
|
|
// create the instance |
|
var markupFactory = ctor.markupFactory || proto.markupFactory; |
|
var instance = markupFactory ? markupFactory(params, node, ctor) : new ctor(params, node); |
|
|
|
function onInstantiate(instance){ |
|
// map it to the JS namespace if that makes sense |
|
if(jsname){ |
|
dlang.setObject(jsname, instance); |
|
} |
|
|
|
// process connections and startup functions |
|
for(i = 0; i < aspects.length; i++){ |
|
aspect[aspects[i].advice || "after"](instance, aspects[i].method, dlang.hitch(instance, aspects[i].func), true); |
|
} |
|
for(i = 0; i < calls.length; i++){ |
|
calls[i].call(instance); |
|
} |
|
for(i = 0; i < watches.length; i++){ |
|
instance.watch(watches[i].prop, watches[i].func); |
|
} |
|
for(i = 0; i < ons.length; i++){ |
|
don(instance, ons[i].event, ons[i].func); |
|
} |
|
|
|
return instance; |
|
} |
|
|
|
if(instance.then){ |
|
return instance.then(onInstantiate); |
|
}else{ |
|
return onInstantiate(instance); |
|
} |
|
}, |
|
|
|
scan: function(root, options){ |
|
// summary: |
|
// Scan a DOM tree and return an array of objects representing the DOMNodes |
|
// that need to be turned into widgets. |
|
// description: |
|
// Search specified node (or document root node) recursively for class instances |
|
// and return an array of objects that represent potential widgets to be |
|
// instantiated. Searches for either data-dojo-type="MID" or dojoType="MID" where |
|
// "MID" is a module ID like "dijit/form/Button" or a fully qualified Class name |
|
// like "dijit/form/Button". If the MID is not currently available, scan will |
|
// attempt to require() in the module. |
|
// |
|
// See parser.parse() for details of markup. |
|
// root: DomNode? |
|
// A default starting root node from which to start the parsing. Can be |
|
// omitted, defaulting to the entire document. If omitted, the `options` |
|
// object can be passed in this place. If the `options` object has a |
|
// `rootNode` member, that is used. |
|
// options: Object |
|
// a kwArgs options object, see parse() for details |
|
// |
|
// returns: Promise |
|
// A promise that is resolved with the nodes that have been parsed. |
|
|
|
var list = [], // Output List |
|
mids = [], // An array of modules that are not yet loaded |
|
midsHash = {}; // Used to keep the mids array unique |
|
|
|
var dojoType = (options.scope || dojo._scopeName) + "Type", // typically "dojoType" |
|
attrData = "data-" + (options.scope || dojo._scopeName) + "-", // typically "data-dojo-" |
|
dataDojoType = attrData + "type", // typically "data-dojo-type" |
|
dataDojoTextDir = attrData + "textdir", // typically "data-dojo-textdir" |
|
dataDojoMixins = attrData + "mixins"; // typically "data-dojo-mixins" |
|
|
|
// Info on DOMNode currently being processed |
|
var node = root.firstChild; |
|
|
|
// Info on parent of DOMNode currently being processed |
|
// - inherited: dir, lang, and textDir setting of parent, or inherited by parent |
|
// - parent: pointer to identical structure for my parent (or null if no parent) |
|
// - scripts: if specified, collects <script type="dojo/..."> type nodes from children |
|
var inherited = options.inherited; |
|
if(!inherited){ |
|
var findAncestorAttr = function findAncestorAttr(node, attr){ |
|
return (node.getAttribute && node.getAttribute(attr)) || |
|
(node.parentNode && findAncestorAttr(node.parentNode, attr)); |
|
}; |
|
|
|
inherited = { |
|
dir: findAncestorAttr(root, "dir"), |
|
lang: findAncestorAttr(root, "lang"), |
|
textDir: findAncestorAttr(root, dataDojoTextDir) |
|
}; |
|
for(var key in inherited){ |
|
if(!inherited[key]){ |
|
delete inherited[key]; |
|
} |
|
} |
|
} |
|
|
|
// Metadata about parent node |
|
var parent = { |
|
inherited: inherited |
|
}; |
|
|
|
// For collecting <script type="dojo/..."> type nodes (when null, we don't need to collect) |
|
var scripts; |
|
|
|
// when true, only look for <script type="dojo/..."> tags, and don't recurse to children |
|
var scriptsOnly; |
|
|
|
function getEffective(parent){ |
|
// summary: |
|
// Get effective dir, lang, textDir settings for specified obj |
|
// (matching "parent" object structure above), and do caching. |
|
// Take care not to return null entries. |
|
if(!parent.inherited){ |
|
parent.inherited = {}; |
|
var node = parent.node, |
|
grandparent = getEffective(parent.parent); |
|
var inherited = { |
|
dir: node.getAttribute("dir") || grandparent.dir, |
|
lang: node.getAttribute("lang") || grandparent.lang, |
|
textDir: node.getAttribute(dataDojoTextDir) || grandparent.textDir |
|
}; |
|
for(var key in inherited){ |
|
if(inherited[key]){ |
|
parent.inherited[key] = inherited[key]; |
|
} |
|
} |
|
} |
|
return parent.inherited; |
|
} |
|
|
|
// DFS on DOM tree, collecting nodes with data-dojo-type specified. |
|
while(true){ |
|
if(!node){ |
|
// Finished this level, continue to parent's next sibling |
|
if(!parent || !parent.node){ |
|
break; |
|
} |
|
node = parent.node.nextSibling; |
|
scriptsOnly = false; |
|
parent = parent.parent; |
|
scripts = parent.scripts; |
|
continue; |
|
} |
|
|
|
if(node.nodeType != 1){ |
|
// Text or comment node, skip to next sibling |
|
node = node.nextSibling; |
|
continue; |
|
} |
|
|
|
if(scripts && node.nodeName.toLowerCase() == "script"){ |
|
// Save <script type="dojo/..."> for parent, then continue to next sibling |
|
type = node.getAttribute("type"); |
|
if(type && /^dojo\/\w/i.test(type)){ |
|
scripts.push(node); |
|
} |
|
node = node.nextSibling; |
|
continue; |
|
} |
|
if(scriptsOnly){ |
|
// scriptsOnly flag is set, we have already collected scripts if the parent wants them, so now we shouldn't |
|
// continue further analysis of the node and will continue to the next sibling |
|
node = node.nextSibling; |
|
continue; |
|
} |
|
|
|
// Check for data-dojo-type attribute, fallback to backward compatible dojoType |
|
// TODO: Remove dojoType in 2.0 |
|
var type = node.getAttribute(dataDojoType) || node.getAttribute(dojoType); |
|
|
|
// Short circuit for leaf nodes containing nothing [but text] |
|
var firstChild = node.firstChild; |
|
if(!type && (!firstChild || (firstChild.nodeType == 3 && !firstChild.nextSibling))){ |
|
node = node.nextSibling; |
|
continue; |
|
} |
|
|
|
// Meta data about current node |
|
var current; |
|
|
|
var ctor = null; |
|
if(type){ |
|
// If dojoType/data-dojo-type specified, add to output array of nodes to instantiate. |
|
var mixinsValue = node.getAttribute(dataDojoMixins), |
|
types = mixinsValue ? [type].concat(mixinsValue.split(/\s*,\s*/)) : [type]; |
|
|
|
// Note: won't find classes declared via dojo/Declaration or any modules that haven't been |
|
// loaded yet so use try/catch to avoid throw from require() |
|
try{ |
|
ctor = getCtor(types, options.contextRequire); |
|
}catch(e){} |
|
|
|
// If the constructor was not found, check to see if it has modules that can be loaded |
|
if(!ctor){ |
|
darray.forEach(types, function(t){ |
|
if(~t.indexOf('/') && !midsHash[t]){ |
|
// If the type looks like a MID and it currently isn't in the array of MIDs to load, add it. |
|
midsHash[t] = true; |
|
mids[mids.length] = t; |
|
} |
|
}); |
|
} |
|
|
|
var childScripts = ctor && !ctor.prototype._noScript ? [] : null; // <script> nodes that are parent's children |
|
|
|
// Setup meta data about this widget node, and save it to list of nodes to instantiate |
|
current = { |
|
types: types, |
|
ctor: ctor, |
|
parent: parent, |
|
node: node, |
|
scripts: childScripts |
|
}; |
|
current.inherited = getEffective(current); // dir & lang settings for current node, explicit or inherited |
|
list.push(current); |
|
}else{ |
|
// Meta data about this non-widget node |
|
current = { |
|
node: node, |
|
scripts: scripts, |
|
parent: parent |
|
}; |
|
} |
|
|
|
// Recurse, collecting <script type="dojo/..."> children, and also looking for |
|
// descendant nodes with dojoType specified (unless the widget has the stopParser flag). |
|
// When finished with children, go to my next sibling. |
|
scripts = childScripts; |
|
scriptsOnly = node.stopParser || (ctor && ctor.prototype.stopParser && !(options.template)); |
|
parent = current; |
|
node = firstChild; |
|
} |
|
|
|
var d = new Deferred(); |
|
|
|
// If there are modules to load then require them in |
|
if(mids.length){ |
|
// Warn that there are modules being auto-required |
|
if(has("dojo-debug-messages")){ |
|
console.warn("WARNING: Modules being Auto-Required: " + mids.join(", ")); |
|
} |
|
var r = options.contextRequire || require; |
|
r(mids, function(){ |
|
// Go through list of widget nodes, filling in missing constructors, and filtering out nodes that shouldn't |
|
// be instantiated due to a stopParser flag on an ancestor that we belatedly learned about due to |
|
// auto-require of a module like ContentPane. Assumes list is in DFS order. |
|
d.resolve(darray.filter(list, function(widget){ |
|
if(!widget.ctor){ |
|
// Attempt to find the constructor again. Still won't find classes defined via |
|
// dijit/Declaration so need to try/catch. |
|
try{ |
|
widget.ctor = getCtor(widget.types, options.contextRequire); |
|
}catch(e){} |
|
} |
|
|
|
// Get the parent widget |
|
var parent = widget.parent; |
|
while(parent && !parent.types){ |
|
parent = parent.parent; |
|
} |
|
|
|
// Return false if this node should be skipped due to stopParser on an ancestor. |
|
// Since list[] is in DFS order, this loop will always set parent.instantiateChildren before |
|
// trying to compute widget.instantiate. |
|
var proto = widget.ctor && widget.ctor.prototype; |
|
widget.instantiateChildren = !(proto && proto.stopParser && !(options.template)); |
|
widget.instantiate = !parent || (parent.instantiate && parent.instantiateChildren); |
|
return widget.instantiate; |
|
})); |
|
}); |
|
}else{ |
|
// There were no modules to load, so just resolve with the parsed nodes. This separate code path is for |
|
// efficiency, to avoid running the require() and the callback code above. |
|
d.resolve(list); |
|
} |
|
|
|
// Return the promise |
|
return d.promise; |
|
}, |
|
|
|
_require: function(/*DOMNode*/ script, /*Object?*/ options){ |
|
// summary: |
|
// Helper for _scanAMD(). Takes a `<script type=dojo/require>bar: "acme/bar", ...</script>` node, |
|
// calls require() to load the specified modules and (asynchronously) assign them to the specified global |
|
// variables, and returns a Promise for when that operation completes. |
|
// |
|
// In the example above, it is effectively doing a require(["acme/bar", ...], function(a){ bar = a; }). |
|
|
|
var hash = myEval("{" + script.innerHTML + "}"), // can't use dojo/json::parse() because maybe no quotes |
|
vars = [], |
|
mids = [], |
|
d = new Deferred(); |
|
|
|
var contextRequire = (options && options.contextRequire) || require; |
|
|
|
for(var name in hash){ |
|
vars.push(name); |
|
mids.push(hash[name]); |
|
} |
|
|
|
contextRequire(mids, function(){ |
|
for(var i = 0; i < vars.length; i++){ |
|
dlang.setObject(vars[i], arguments[i]); |
|
} |
|
d.resolve(arguments); |
|
}); |
|
|
|
return d.promise; |
|
}, |
|
|
|
_scanAmd: function(root, options){ |
|
// summary: |
|
// Scans the DOM for any declarative requires and returns their values. |
|
// description: |
|
// Looks for `<script type=dojo/require>bar: "acme/bar", ...</script>` node, calls require() to load the |
|
// specified modules and (asynchronously) assign them to the specified global variables, |
|
// and returns a Promise for when those operations complete. |
|
// root: DomNode |
|
// The node to base the scan from. |
|
// options: Object? |
|
// a kwArgs options object, see parse() for details |
|
|
|
// Promise that resolves when all the <script type=dojo/require> nodes have finished loading. |
|
var deferred = new Deferred(), |
|
promise = deferred.promise; |
|
deferred.resolve(true); |
|
|
|
var self = this; |
|
query("script[type='dojo/require']", root).forEach(function(node){ |
|
// Fire off require() call for specified modules. Chain this require to fire after |
|
// any previous requires complete, so that layers can be loaded before individual module require()'s fire. |
|
promise = promise.then(function(){ |
|
return self._require(node, options); |
|
}); |
|
|
|
// Remove from DOM so it isn't seen again |
|
node.parentNode.removeChild(node); |
|
}); |
|
|
|
return promise; |
|
}, |
|
|
|
parse: function(rootNode, options){ |
|
// summary: |
|
// Scan the DOM for class instances, and instantiate them. |
|
// description: |
|
// Search specified node (or root node) recursively for class instances, |
|
// and instantiate them. Searches for either data-dojo-type="Class" or |
|
// dojoType="Class" where "Class" is a a fully qualified class name, |
|
// like `dijit/form/Button` |
|
// |
|
// Using `data-dojo-type`: |
|
// Attributes using can be mixed into the parameters used to instantiate the |
|
// Class by using a `data-dojo-props` attribute on the node being converted. |
|
// `data-dojo-props` should be a string attribute to be converted from JSON. |
|
// |
|
// Using `dojoType`: |
|
// Attributes are read from the original domNode and converted to appropriate |
|
// types by looking up the Class prototype values. This is the default behavior |
|
// from Dojo 1.0 to Dojo 1.5. `dojoType` support is deprecated, and will |
|
// go away in Dojo 2.0. |
|
// rootNode: DomNode? |
|
// A default starting root node from which to start the parsing. Can be |
|
// omitted, defaulting to the entire document. If omitted, the `options` |
|
// object can be passed in this place. If the `options` object has a |
|
// `rootNode` member, that is used. |
|
// options: Object? |
|
// A hash of options. |
|
// |
|
// - noStart: Boolean?: |
|
// when set will prevent the parser from calling .startup() |
|
// when locating the nodes. |
|
// - rootNode: DomNode?: |
|
// identical to the function's `rootNode` argument, though |
|
// allowed to be passed in via this `options object. |
|
// - template: Boolean: |
|
// If true, ignores ContentPane's stopParser flag and parses contents inside of |
|
// a ContentPane inside of a template. This allows dojoAttachPoint on widgets/nodes |
|
// nested inside the ContentPane to work. |
|
// - inherited: Object: |
|
// Hash possibly containing dir and lang settings to be applied to |
|
// parsed widgets, unless there's another setting on a sub-node that overrides |
|
// - scope: String: |
|
// Root for attribute names to search for. If scopeName is dojo, |
|
// will search for data-dojo-type (or dojoType). For backwards compatibility |
|
// reasons defaults to dojo._scopeName (which is "dojo" except when |
|
// multi-version support is used, when it will be something like dojo16, dojo20, etc.) |
|
// - propsThis: Object: |
|
// If specified, "this" referenced from data-dojo-props will refer to propsThis. |
|
// Intended for use from the widgets-in-template feature of `dijit._WidgetsInTemplateMixin` |
|
// - contextRequire: Function: |
|
// If specified, this require is utilised for looking resolving modules instead of the |
|
// `dojo/parser` context `require()`. Intended for use from the widgets-in-template feature of |
|
// `dijit._WidgetsInTemplateMixin`. |
|
// returns: Mixed |
|
// Returns a blended object that is an array of the instantiated objects, but also can include |
|
// a promise that is resolved with the instantiated objects. This is done for backwards |
|
// compatibility. If the parser auto-requires modules, it will always behave in a promise |
|
// fashion and `parser.parse().then(function(instances){...})` should be used. |
|
// example: |
|
// Parse all widgets on a page: |
|
// | parser.parse(); |
|
// example: |
|
// Parse all classes within the node with id="foo" |
|
// | parser.parse(dojo.byId('foo')); |
|
// example: |
|
// Parse all classes in a page, but do not call .startup() on any |
|
// child |
|
// | parser.parse({ noStart: true }) |
|
// example: |
|
// Parse all classes in a node, but do not call .startup() |
|
// | parser.parse(someNode, { noStart:true }); |
|
// | // or |
|
// | parser.parse({ noStart:true, rootNode: someNode }); |
|
|
|
// determine the root node and options based on the passed arguments. |
|
if(rootNode && typeof rootNode != "string" && !("nodeType" in rootNode)){ |
|
// If called as parse(options) rather than parse(), parse(rootNode), or parse(rootNode, options)... |
|
options = rootNode; |
|
rootNode = options.rootNode; |
|
} |
|
var root = rootNode ? dom.byId(rootNode) : dwindow.body(); |
|
options = options || {}; |
|
|
|
var mixin = options.template ? { template: true } : {}, |
|
instances = [], |
|
self = this; |
|
|
|
// First scan for any <script type=dojo/require> nodes, and execute. |
|
// Then scan for all nodes with data-dojo-type, and load any unloaded modules. |
|
// Then build the object instances. Add instances to already existing (but empty) instances[] array, |
|
// which may already have been returned to caller. Also, use otherwise to collect and throw any errors |
|
// that occur during the parse(). |
|
var p = |
|
this._scanAmd(root, options).then(function(){ |
|
return self.scan(root, options); |
|
}).then(function(parsedNodes){ |
|
return self._instantiate(parsedNodes, mixin, options, true); |
|
}).then(function(_instances){ |
|
// Copy the instances into the instances[] array we declared above, and are accessing as |
|
// our return value. |
|
return instances = instances.concat(_instances); |
|
}).otherwise(function(e){ |
|
// TODO Modify to follow better pattern for promise error management when available |
|
console.error("dojo/parser::parse() error", e); |
|
throw e; |
|
}); |
|
|
|
// Blend the array with the promise |
|
dlang.mixin(instances, p); |
|
return instances; |
|
} |
|
}; |
|
|
|
if(has("extend-dojo")){ |
|
dojo.parser = parser; |
|
} |
|
|
|
// Register the parser callback. It should be the first callback |
|
// after the a11y test. |
|
if(config.parseOnLoad){ |
|
ready(100, parser, "parse"); |
|
} |
|
|
|
return parser; |
|
});
|
|
|