]>
Commit | Line | Data |
---|---|---|
f0cfe83e AD |
1 | define("dijit/_TemplatedMixin", [ |
2 | "dojo/_base/lang", // lang.getObject | |
3 | "dojo/touch", | |
4 | "./_WidgetBase", | |
5 | "dojo/string", // string.substitute string.trim | |
6 | "dojo/cache", // dojo.cache | |
7 | "dojo/_base/array", // array.forEach | |
8 | "dojo/_base/declare", // declare | |
9 | "dojo/dom-construct", // domConstruct.destroy, domConstruct.toDom | |
10 | "dojo/sniff", // has("ie") | |
11 | "dojo/_base/unload" // unload.addOnWindowUnload | |
12 | ], function(lang, touch, _WidgetBase, string, cache, array, declare, domConstruct, has, unload) { | |
13 | ||
14 | // module: | |
15 | // dijit/_TemplatedMixin | |
16 | ||
17 | var _TemplatedMixin = declare("dijit._TemplatedMixin", null, { | |
18 | // summary: | |
19 | // Mixin for widgets that are instantiated from a template | |
20 | ||
21 | // templateString: [protected] String | |
22 | // A string that represents the widget template. | |
23 | // Use in conjunction with dojo.cache() to load from a file. | |
24 | templateString: null, | |
25 | ||
26 | // templatePath: [protected deprecated] String | |
27 | // Path to template (HTML file) for this widget relative to dojo.baseUrl. | |
28 | // Deprecated: use templateString with require([... "dojo/text!..."], ...) instead | |
29 | templatePath: null, | |
30 | ||
31 | // skipNodeCache: [protected] Boolean | |
32 | // If using a cached widget template nodes poses issues for a | |
33 | // particular widget class, it can set this property to ensure | |
34 | // that its template is always re-built from a string | |
35 | _skipNodeCache: false, | |
36 | ||
37 | // _earlyTemplatedStartup: Boolean | |
38 | // A fallback to preserve the 1.0 - 1.3 behavior of children in | |
39 | // templates having their startup called before the parent widget | |
40 | // fires postCreate. Defaults to 'false', causing child widgets to | |
41 | // have their .startup() called immediately before a parent widget | |
42 | // .startup(), but always after the parent .postCreate(). Set to | |
43 | // 'true' to re-enable to previous, arguably broken, behavior. | |
44 | _earlyTemplatedStartup: false, | |
45 | ||
46 | /*===== | |
47 | // _attachPoints: [private] String[] | |
48 | // List of widget attribute names associated with data-dojo-attach-point=... in the | |
49 | // template, ex: ["containerNode", "labelNode"] | |
50 | _attachPoints: [], | |
51 | ||
52 | // _attachEvents: [private] Handle[] | |
53 | // List of connections associated with data-dojo-attach-event=... in the | |
54 | // template | |
55 | _attachEvents: [], | |
56 | =====*/ | |
57 | ||
58 | constructor: function(/*===== params, srcNodeRef =====*/){ | |
59 | // summary: | |
60 | // Create the widget. | |
61 | // params: Object|null | |
62 | // Hash of initialization parameters for widget, including scalar values (like title, duration etc.) | |
63 | // and functions, typically callbacks like onClick. | |
64 | // The hash can contain any of the widget's properties, excluding read-only properties. | |
65 | // srcNodeRef: DOMNode|String? | |
66 | // If a srcNodeRef (DOM node) is specified, replace srcNodeRef with my generated DOM tree. | |
67 | ||
68 | this._attachPoints = []; | |
69 | this._attachEvents = []; | |
70 | }, | |
71 | ||
72 | _stringRepl: function(tmpl){ | |
73 | // summary: | |
74 | // Does substitution of ${foo} type properties in template string | |
75 | // tags: | |
76 | // private | |
77 | var className = this.declaredClass, _this = this; | |
78 | // Cache contains a string because we need to do property replacement | |
79 | // do the property replacement | |
80 | return string.substitute(tmpl, this, function(value, key){ | |
81 | if(key.charAt(0) == '!'){ value = lang.getObject(key.substr(1), false, _this); } | |
82 | if(typeof value == "undefined"){ throw new Error(className+" template:"+key); } // a debugging aide | |
83 | if(value == null){ return ""; } | |
84 | ||
85 | // Substitution keys beginning with ! will skip the transform step, | |
86 | // in case a user wishes to insert unescaped markup, e.g. ${!foo} | |
87 | return key.charAt(0) == "!" ? value : | |
88 | // Safer substitution, see heading "Attribute values" in | |
89 | // http://www.w3.org/TR/REC-html40/appendix/notes.html#h-B.3.2 | |
90 | value.toString().replace(/"/g,"""); //TODO: add &? use encodeXML method? | |
91 | }, this); | |
92 | }, | |
93 | ||
94 | buildRendering: function(){ | |
95 | // summary: | |
96 | // Construct the UI for this widget from a template, setting this.domNode. | |
97 | // tags: | |
98 | // protected | |
99 | ||
100 | if(!this.templateString){ | |
101 | this.templateString = cache(this.templatePath, {sanitize: true}); | |
102 | } | |
103 | ||
104 | // Lookup cached version of template, and download to cache if it | |
105 | // isn't there already. Returns either a DomNode or a string, depending on | |
106 | // whether or not the template contains ${foo} replacement parameters. | |
107 | var cached = _TemplatedMixin.getCachedTemplate(this.templateString, this._skipNodeCache, this.ownerDocument); | |
108 | ||
109 | var node; | |
110 | if(lang.isString(cached)){ | |
111 | node = domConstruct.toDom(this._stringRepl(cached), this.ownerDocument); | |
112 | if(node.nodeType != 1){ | |
113 | // Flag common problems such as templates with multiple top level nodes (nodeType == 11) | |
114 | throw new Error("Invalid template: " + cached); | |
115 | } | |
116 | }else{ | |
117 | // if it's a node, all we have to do is clone it | |
118 | node = cached.cloneNode(true); | |
119 | } | |
120 | ||
121 | this.domNode = node; | |
122 | ||
123 | // Call down to _Widget.buildRendering() to get base classes assigned | |
124 | // TODO: change the baseClass assignment to _setBaseClassAttr | |
125 | this.inherited(arguments); | |
126 | ||
127 | // recurse through the node, looking for, and attaching to, our | |
128 | // attachment points and events, which should be defined on the template node. | |
129 | this._attachTemplateNodes(node, function(n,p){ return n.getAttribute(p); }); | |
130 | ||
131 | this._beforeFillContent(); // hook for _WidgetsInTemplateMixin | |
132 | ||
133 | this._fillContent(this.srcNodeRef); | |
134 | }, | |
135 | ||
136 | _beforeFillContent: function(){ | |
137 | }, | |
138 | ||
139 | _fillContent: function(/*DomNode*/ source){ | |
140 | // summary: | |
141 | // Relocate source contents to templated container node. | |
142 | // this.containerNode must be able to receive children, or exceptions will be thrown. | |
143 | // tags: | |
144 | // protected | |
145 | var dest = this.containerNode; | |
146 | if(source && dest){ | |
147 | while(source.hasChildNodes()){ | |
148 | dest.appendChild(source.firstChild); | |
149 | } | |
150 | } | |
151 | }, | |
152 | ||
153 | _attachTemplateNodes: function(rootNode, getAttrFunc){ | |
154 | // summary: | |
155 | // Iterate through the template and attach functions and nodes accordingly. | |
156 | // Alternately, if rootNode is an array of widgets, then will process data-dojo-attach-point | |
157 | // etc. for those widgets. | |
158 | // description: | |
159 | // Map widget properties and functions to the handlers specified in | |
160 | // the dom node and it's descendants. This function iterates over all | |
161 | // nodes and looks for these properties: | |
162 | // | |
163 | // - dojoAttachPoint/data-dojo-attach-point | |
164 | // - dojoAttachEvent/data-dojo-attach-event | |
165 | // rootNode: DomNode|Widget[] | |
166 | // the node to search for properties. All children will be searched. | |
167 | // getAttrFunc: Function | |
168 | // a function which will be used to obtain property for a given | |
169 | // DomNode/Widget | |
170 | // tags: | |
171 | // private | |
172 | ||
173 | var nodes = lang.isArray(rootNode) ? rootNode : (rootNode.all || rootNode.getElementsByTagName("*")); | |
174 | var x = lang.isArray(rootNode) ? 0 : -1; | |
175 | for(; x < 0 || nodes[x]; x++){ // don't access nodes.length on IE, see #14346 | |
176 | var baseNode = (x == -1) ? rootNode : nodes[x]; | |
177 | if(this.widgetsInTemplate && (getAttrFunc(baseNode, "dojoType") || getAttrFunc(baseNode, "data-dojo-type"))){ | |
178 | continue; | |
179 | } | |
180 | // Process data-dojo-attach-point | |
181 | var attachPoint = getAttrFunc(baseNode, "dojoAttachPoint") || getAttrFunc(baseNode, "data-dojo-attach-point"); | |
182 | if(attachPoint){ | |
183 | var point, points = attachPoint.split(/\s*,\s*/); | |
184 | while((point = points.shift())){ | |
185 | if(lang.isArray(this[point])){ | |
186 | this[point].push(baseNode); | |
187 | }else{ | |
188 | this[point]=baseNode; | |
189 | } | |
190 | this._attachPoints.push(point); | |
191 | } | |
192 | } | |
193 | ||
194 | // Process data-dojo-attach-event | |
195 | var attachEvent = getAttrFunc(baseNode, "dojoAttachEvent") || getAttrFunc(baseNode, "data-dojo-attach-event"); | |
196 | if(attachEvent){ | |
197 | // NOTE: we want to support attributes that have the form | |
198 | // "domEvent: nativeEvent; ..." | |
199 | var event, events = attachEvent.split(/\s*,\s*/); | |
200 | var trim = lang.trim; | |
201 | while((event = events.shift())){ | |
202 | if(event){ | |
203 | var thisFunc = null; | |
204 | if(event.indexOf(":") != -1){ | |
205 | // oh, if only JS had tuple assignment | |
206 | var funcNameArr = event.split(":"); | |
207 | event = trim(funcNameArr[0]); | |
208 | thisFunc = trim(funcNameArr[1]); | |
209 | }else{ | |
210 | event = trim(event); | |
211 | } | |
212 | if(!thisFunc){ | |
213 | thisFunc = event; | |
214 | } | |
215 | // Map "press", "move" and "release" to keys.touch, keys.move, keys.release | |
216 | this._attachEvents.push(this.connect(baseNode, touch[event] || event, thisFunc)); | |
217 | } | |
218 | } | |
219 | } | |
220 | } | |
221 | }, | |
222 | ||
223 | destroyRendering: function(){ | |
224 | // Delete all attach points to prevent IE6 memory leaks. | |
225 | array.forEach(this._attachPoints, function(point){ | |
226 | delete this[point]; | |
227 | }, this); | |
228 | this._attachPoints = []; | |
229 | ||
230 | // And same for event handlers | |
231 | array.forEach(this._attachEvents, this.disconnect, this); | |
232 | this._attachEvents = []; | |
233 | ||
234 | this.inherited(arguments); | |
235 | } | |
236 | }); | |
237 | ||
238 | // key is templateString; object is either string or DOM tree | |
239 | _TemplatedMixin._templateCache = {}; | |
240 | ||
241 | _TemplatedMixin.getCachedTemplate = function(templateString, alwaysUseString, doc){ | |
242 | // summary: | |
243 | // Static method to get a template based on the templatePath or | |
244 | // templateString key | |
245 | // templateString: String | |
246 | // The template | |
247 | // alwaysUseString: Boolean | |
248 | // Don't cache the DOM tree for this template, even if it doesn't have any variables | |
249 | // doc: Document? | |
250 | // The target document. Defaults to document global if unspecified. | |
251 | // returns: Mixed | |
252 | // Either string (if there are ${} variables that need to be replaced) or just | |
253 | // a DOM tree (if the node can be cloned directly) | |
254 | ||
255 | // is it already cached? | |
256 | var tmplts = _TemplatedMixin._templateCache; | |
257 | var key = templateString; | |
258 | var cached = tmplts[key]; | |
259 | if(cached){ | |
260 | try{ | |
261 | // if the cached value is an innerHTML string (no ownerDocument) or a DOM tree created within the | |
262 | // current document, then use the current cached value | |
263 | if(!cached.ownerDocument || cached.ownerDocument == (doc || document)){ | |
264 | // string or node of the same document | |
265 | return cached; | |
266 | } | |
267 | }catch(e){ /* squelch */ } // IE can throw an exception if cached.ownerDocument was reloaded | |
268 | domConstruct.destroy(cached); | |
269 | } | |
270 | ||
271 | templateString = string.trim(templateString); | |
272 | ||
273 | if(alwaysUseString || templateString.match(/\$\{([^\}]+)\}/g)){ | |
274 | // there are variables in the template so all we can do is cache the string | |
275 | return (tmplts[key] = templateString); //String | |
276 | }else{ | |
277 | // there are no variables in the template so we can cache the DOM tree | |
278 | var node = domConstruct.toDom(templateString, doc); | |
279 | if(node.nodeType != 1){ | |
280 | throw new Error("Invalid template: " + templateString); | |
281 | } | |
282 | return (tmplts[key] = node); //Node | |
283 | } | |
284 | }; | |
285 | ||
286 | if(has("ie")){ | |
287 | unload.addOnWindowUnload(function(){ | |
288 | var cache = _TemplatedMixin._templateCache; | |
289 | for(var key in cache){ | |
290 | var value = cache[key]; | |
291 | if(typeof value == "object"){ // value is either a string or a DOM node template | |
292 | domConstruct.destroy(value); | |
293 | } | |
294 | delete cache[key]; | |
295 | } | |
296 | }); | |
297 | } | |
298 | ||
299 | // These arguments can be specified for widgets which are used in templates. | |
300 | // Since any widget can be specified as sub widgets in template, mix it | |
301 | // into the base widget class. (This is a hack, but it's effective.). | |
302 | // Remove for 2.0. Also, hide from API doc parser. | |
303 | lang.extend(_WidgetBase, /*===== {} || =====*/ { | |
304 | dojoAttachEvent: "", | |
305 | dojoAttachPoint: "" | |
306 | }); | |
307 | ||
308 | return _TemplatedMixin; | |
309 | }); |