1
0
mirror of https://tt-rss.org/git/tt-rss.git synced 2024-06-28 12:10:52 +02:00
ttrss/lib/dojo/parser.js
2011-11-08 20:40:44 +04:00

563 lines
21 KiB
JavaScript

/*
Copyright (c) 2004-2011, The Dojo Foundation All Rights Reserved.
Available via Academic Free License >= 2.1 OR the modified BSD license.
see: http://dojotoolkit.org/license for details
*/
if(!dojo._hasResource["dojo.parser"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dojo.parser"] = true;
dojo.provide("dojo.parser");
dojo.require("dojo.date.stamp");
new Date("X"); // workaround for #11279, new Date("") == NaN
dojo.parser = new function(){
// summary:
// The Dom/Widget parsing package
var d = dojo;
function val2type(/*Object*/ value){
// summary:
// Returns name of type of given value.
if(d.isString(value)){ return "string"; }
if(typeof value == "number"){ return "number"; }
if(typeof value == "boolean"){ return "boolean"; }
if(d.isFunction(value)){ return "function"; }
if(d.isArray(value)){ return "array"; } // typeof [] == "object"
if(value instanceof Date) { return "date"; } // assume timestamp
if(value instanceof d._Url){ return "url"; }
return "object";
}
function str2obj(/*String*/ value, /*String*/ type){
// summary:
// Convert given string value to given type
switch(type){
case "string":
return value;
case "number":
return value.length ? Number(value) : NaN;
case "boolean":
// for checked/disabled value might be "" or "checked". interpret as true.
return typeof value == "boolean" ? value : !(value.toLowerCase()=="false");
case "function":
if(d.isFunction(value)){
// IE gives us a function, even when we say something like onClick="foo"
// (in which case it gives us an invalid function "function(){ foo }").
// Therefore, convert to string
value=value.toString();
value=d.trim(value.substring(value.indexOf('{')+1, value.length-1));
}
try{
if(value === "" || value.search(/[^\w\.]+/i) != -1){
// The user has specified some text for a function like "return x+5"
return new Function(value);
}else{
// The user has specified the name of a function like "myOnClick"
// or a single word function "return"
return d.getObject(value, false) || new Function(value);
}
}catch(e){ return new Function(); }
case "array":
return value ? value.split(/\s*,\s*/) : [];
case "date":
switch(value){
case "": return new Date(""); // the NaN of dates
case "now": return new Date(); // current date
default: return d.date.stamp.fromISOString(value);
}
case "url":
return d.baseUrl + value;
default:
return d.fromJson(value);
}
}
var dummyClass = {}, instanceClasses = {
// map from fully qualified name (like "dijit.Button") to structure like
// { cls: dijit.Button, params: {label: "string", disabled: "boolean"} }
};
// 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).
// TODO: remove this in 2.0, when we stop caching parameters.
d.connect(d, "extend", function(){
instanceClasses = {};
});
function getProtoInfo(cls, params){
// cls: A prototype
// The prototype of the class to check props on
// params: Object
// The parameters object to mix found parameters onto.
for(var name in cls){
if(name.charAt(0)=="_"){ continue; } // skip internal properties
if(name in dummyClass){ continue; } // skip "constructor" and "toString"
params[name] = val2type(cls[name]);
}
return params;
}
function getClassInfo(/*String*/ className, /*Boolean*/ skipParamsLookup){
// summary:
// Maps a widget name string like "dijit.form.Button" to the widget constructor itself,
// and a list of that widget's parameters and their types
// className:
// fully qualified name (like "dijit.form.Button")
// returns:
// structure like
// {
// cls: dijit.Button,
// params: { label: "string", disabled: "boolean"}
// }
var c = instanceClasses[className];
if(!c){
// get pointer to widget class
var cls = d.getObject(className), params = null;
if(!cls){ return null; } // class not defined [yet]
if(!skipParamsLookup){ // from fastpath, we don't need to lookup the attrs on the proto because they are explicit
params = getProtoInfo(cls.prototype, {})
}
c = { cls: cls, params: params };
}else if(!skipParamsLookup && !c.params){
// if we're calling getClassInfo and have a cls proto, but no params info, scan that cls for params now
// and update the pointer in instanceClasses[className]. This happens when a widget appears in another
// widget's template which still uses dojoType, but an instance of the widget appears prior with a data-dojo-type,
// skipping this lookup the first time.
c.params = getProtoInfo(c.cls.prototype, {});
}
return c;
}
this._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 = "";
var suffix = "";
var argsStr = (script.getAttribute(attrData + "args") || script.getAttribute("args"));
if(argsStr){
d.forEach(argsStr.split(/\s*,\s*/), function(part, idx){
preamble += "var "+part+" = arguments["+idx+"]; ";
});
}
var withStr = script.getAttribute("with");
if(withStr && withStr.length){
d.forEach(withStr.split(/\s*,\s*/), function(part){
preamble += "with("+part+"){";
suffix += "}";
});
}
return new Function(preamble+script.innerHTML+suffix);
};
this.instantiate = function(/* Array */nodes, /* Object? */mixin, /* Object? */args){
// 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 nodes or objects like
// | {
// | type: "dijit.form.Button",
// | 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.
// args: Object?
// An object used to hold kwArgs for instantiation.
// See parse.args argument for details.
var thelist = [],
mixin = mixin||{};
args = args||{};
// TODO: for 2.0 default to data-dojo- regardless of scopeName (or maybe scopeName won't exist in 2.0)
var attrName = (args.scope || d._scopeName) + "Type", // typically "dojoType"
attrData = "data-" + (args.scope || d._scopeName) + "-"; // typically "data-dojo-"
d.forEach(nodes, function(obj){
if(!obj){ return; }
// Get pointers to DOMNode, dojoType string, and clsInfo (metadata about the dojoType), etc.
var node, type, clsInfo, clazz, scripts, fastpath;
if(obj.node){
// new format of nodes[] array, object w/lots of properties pre-computed for me
node = obj.node;
type = obj.type;
fastpath = obj.fastpath;
clsInfo = obj.clsInfo || (type && getClassInfo(type, fastpath));
clazz = clsInfo && clsInfo.cls;
scripts = obj.scripts;
}else{
// old (backwards compatible) format of nodes[] array, simple array of DOMNodes. no fastpath/data-dojo-type support here.
node = obj;
type = attrName in mixin ? mixin[attrName] : node.getAttribute(attrName);
clsInfo = type && getClassInfo(type);
clazz = clsInfo && clsInfo.cls;
scripts = (clazz && (clazz._noScript || clazz.prototype._noScript) ? [] :
d.query("> script[type^='dojo/']", node));
}
if(!clsInfo){
throw new Error("Could not load class '" + type);
}
// 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(args.defaults){
// settings for the document itself (or whatever subtree is being parsed)
d._mixin(params, args.defaults);
}
if(obj.inherited){
// settings from dir=rtl or lang=... on a node above this node
d._mixin(params, obj.inherited);
}
// mix things found in data-dojo-props into the params
if(fastpath){
var extra = node.getAttribute(attrData + "props");
if(extra && extra.length){
try{
extra = d.fromJson.call(args.propsThis, "{" + extra + "}");
d._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 + "'");
}
}
// For the benefit of _Templated, check if node has data-dojo-attach-point/data-dojo-attach-event
// and mix those in as though they were parameters
var attachPoint = node.getAttribute(attrData + "attach-point");
if(attachPoint){
params.dojoAttachPoint = attachPoint;
}
var attachEvent = node.getAttribute(attrData + "attach-event");
if(attachEvent){
params.dojoAttachEvent = attachEvent;
}
dojo.mixin(params, mixin);
}else{
// FIXME: we need something like "deprecateOnce()" to throw dojo.deprecation for something.
// remove this logic in 2.0
// read parameters (ie, attributes) specified on DOMNode
var attributes = node.attributes;
// clsInfo.params lists expected params like {"checked": "boolean", "n": "number"}
for(var name in clsInfo.params){
var item = name in mixin ? { value:mixin[name], specified:true } : attributes.getNamedItem(name);
if(!item || (!item.specified && (!dojo.isIE || name.toLowerCase()!="value"))){ continue; }
var value = item.value;
// Deal with IE quirks for 'class' and 'style'
switch(name){
case "class":
value = "className" in mixin ? mixin.className : node.className;
break;
case "style":
value = "style" in mixin ? mixin.style : (node.style && node.style.cssText); // FIXME: Opera?
}
var _type = clsInfo.params[name];
if(typeof value == "string"){
params[name] = str2obj(value, _type);
}else{
params[name] = value;
}
}
}
// Process <script type="dojo/*"> script tags
// <script type="dojo/method" 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" event="foo"> tags are dojo.connected after instantiation
// note: dojo/* script tags cannot exist in self closing widgets, like <input />
var connects = [], // functions to connect after instantiation
calls = []; // functions to call after instantiation
d.forEach(scripts, function(script){
node.removeChild(script);
// FIXME: drop event="" support in 2.0. use data-dojo-event="" instead
var event = (script.getAttribute(attrData + "event") || script.getAttribute("event")),
type = script.getAttribute("type"),
nf = d.parser._functionFromScript(script, attrData);
if(event){
if(type == "dojo/connect"){
connects.push({event: event, func: nf});
}else{
params[event] = nf;
}
}else{
calls.push(nf);
}
});
var markupFactory = clazz.markupFactory || clazz.prototype && clazz.prototype.markupFactory;
// create the instance
var instance = markupFactory ? markupFactory(params, node, clazz) : new clazz(params, node);
thelist.push(instance);
// map it to the JS namespace if that makes sense
// FIXME: in 2.0, drop jsId support. use data-dojo-id instead
var jsname = (node.getAttribute(attrData + "id") || node.getAttribute("jsId"));
if(jsname){
d.setObject(jsname, instance);
}
// process connections and startup functions
d.forEach(connects, function(connect){
d.connect(instance, connect.event, null, connect.func);
});
d.forEach(calls, function(func){
func.call(instance);
});
});
// 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
if(!mixin._started){
// TODO: for 2.0, when old instantiate() API is desupported, store parent-child
// relationships in the nodes[] array so that no getParent() call is needed.
// Note that will require a parse() call from ContentPane setting a param that the
// ContentPane is the parent widget (so that the parse doesn't call startup() on the
// ContentPane's children)
d.forEach(thelist, function(instance){
if( !args.noStart && instance &&
dojo.isFunction(instance.startup) &&
!instance._started &&
(!instance.getParent || !instance.getParent())
){
instance.startup();
}
});
}
return thelist;
};
this.parse = function(rootNode, args){
// 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 instantitate 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 `args`
// object can be passed in this place. If the `args` object has a
// `rootNode` member, that is used.
//
// args: Object
// a kwArgs object passed along to instantiate()
//
// * 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 `args 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._Templated`
//
// example:
// Parse all widgets on a page:
// | dojo.parser.parse();
//
// example:
// Parse all classes within the node with id="foo"
// | dojo.parser.parse(dojo.byId('foo'));
//
// example:
// Parse all classes in a page, but do not call .startup() on any
// child
// | dojo.parser.parse({ noStart: true })
//
// example:
// Parse all classes in a node, but do not call .startup()
// | dojo.parser.parse(someNode, { noStart:true });
// | // or
// | dojo.parser.parse({ noStart:true, rootNode: someNode });
// determine the root node based on the passed arguments.
var root;
if(!args && rootNode && rootNode.rootNode){
args = rootNode;
root = args.rootNode;
}else{
root = rootNode;
}
root = root ? dojo.byId(root) : dojo.body();
args = args || {};
var attrName = (args.scope || d._scopeName) + "Type", // typically "dojoType"
attrData = "data-" + (args.scope || d._scopeName) + "-"; // typically "data-dojo-"
function scan(parent, list){
// summary:
// Parent is an Object representing a DOMNode, with or without a dojoType specified.
// Scan parent's children looking for nodes with dojoType specified, storing in list[].
// If parent has a dojoType, also collects <script type=dojo/*> children and stores in parent.scripts[].
// parent: Object
// Object representing the parent node, like
// | {
// | node: DomNode, // scan children of this node
// | inherited: {dir: "rtl"}, // dir/lang setting inherited from above node
// |
// | // attributes only set if node has dojoType specified
// | scripts: [], // empty array, put <script type=dojo/*> in here
// | clsInfo: { cls: dijit.form.Button, ...}
// | }
// list: DomNode[]
// Output array of objects (same format as parent) representing nodes to be turned into widgets
// Effective dir and lang settings on parent node, either set directly or inherited from grandparent
var inherited = dojo.clone(parent.inherited);
dojo.forEach(["dir", "lang"], function(name){
// TODO: what if this is a widget and dir/lang are declared in data-dojo-props?
var val = parent.node.getAttribute(name);
if(val){
inherited[name] = val;
}
});
// if parent is a widget, then search for <script type=dojo/*> tags and put them in scripts[].
var scripts = parent.clsInfo && !parent.clsInfo.cls.prototype._noScript ? parent.scripts : null;
// unless parent is a widget with the stopParser flag set, continue search for dojoType, recursively
var recurse = (!parent.clsInfo || !parent.clsInfo.cls.prototype.stopParser) || (args && args.template);
// scan parent's children looking for dojoType and <script type=dojo/*>
for(var child = parent.node.firstChild; child; child = child.nextSibling){
if(child.nodeType == 1){
// FIXME: desupport dojoType in 2.0. use data-dojo-type instead
var type, html5 = recurse && child.getAttribute(attrData + "type");
if(html5){
type = html5;
}else{
// fallback to backward compatible mode, using dojoType. remove in 2.0
type = recurse && child.getAttribute(attrName);
}
var fastpath = html5 == type;
if(type){
// if dojoType/data-dojo-type specified, add to output array of nodes to instantiate
var params = {
"type": type,
fastpath: fastpath,
clsInfo: getClassInfo(type, fastpath), // note: won't find classes declared via dojo.Declaration
node: child,
scripts: [], // <script> nodes that are parent's children
inherited: inherited // dir & lang attributes inherited from parent
};
list.push(params);
// Recurse, collecting <script type="dojo/..."> children, and also looking for
// descendant nodes with dojoType specified (unless the widget has the stopParser flag),
scan(params, list);
}else if(scripts && child.nodeName.toLowerCase() == "script"){
// if <script type="dojo/...">, save in scripts[]
type = child.getAttribute("type");
if (type && /^dojo\/\w/i.test(type)) {
scripts.push(child);
}
}else if(recurse){
// Recurse, looking for grandchild nodes with dojoType specified
scan({
node: child,
inherited: inherited
}, list);
}
}
}
}
// Ignore bogus entries in inherited hash like {dir: ""}
var inherited = {};
if(args && args.inherited){
for(var key in args.inherited){
if(args.inherited[key]){ inherited[key] = args.inherited[key]; }
}
}
// Make list of all nodes on page w/dojoType specified
var list = [];
scan({
node: root,
inherited: inherited
}, list);
// go build the object instances
var mixin = args && args.template ? {template: true} : null;
return this.instantiate(list, mixin, args); // Array
};
}();
//Register the parser callback. It should be the first callback
//after the a11y test.
(function(){
var parseRunner = function(){
if(dojo.config.parseOnLoad){
dojo.parser.parse();
}
};
// FIXME: need to clobber cross-dependency!!
if(dojo.getObject("dijit.wai.onload") === dojo._loaders[0]){
dojo._loaders.splice(1, 0, parseRunner);
}else{
dojo._loaders.unshift(parseRunner);
}
})();
}