Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / core / src / dom / DomHelper.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.core.DomHelper
17  * <p>The DomHelper class provides a layer of abstraction from DOM and transparently supports creating
18  * elements via DOM or using HTML fragments. It also has the ability to create HTML fragment templates
19  * from your DOM building code.</p>
20  *
21  * <p><b><u>DomHelper element specification object</u></b></p>
22  * <p>A specification object is used when creating elements. Attributes of this object
23  * are assumed to be element attributes, except for 4 special attributes:
24  * <div class="mdetail-params"><ul>
25  * <li><b><tt>tag</tt></b> : <div class="sub-desc">The tag name of the element</div></li>
26  * <li><b><tt>children</tt></b> : or <tt>cn</tt><div class="sub-desc">An array of the
27  * same kind of element definition objects to be created and appended. These can be nested
28  * as deep as you want.</div></li>
29  * <li><b><tt>cls</tt></b> : <div class="sub-desc">The class attribute of the element.
30  * This will end up being either the "class" attribute on a HTML fragment or className
31  * for a DOM node, depending on whether DomHelper is using fragments or DOM.</div></li>
32  * <li><b><tt>html</tt></b> : <div class="sub-desc">The innerHTML for the element</div></li>
33  * </ul></div></p>
34  * <p><b>NOTE:</b> For other arbitrary attributes, the value will currently <b>not</b> be automatically
35  * HTML-escaped prior to building the element's HTML string. This means that if your attribute value
36  * contains special characters that would not normally be allowed in a double-quoted attribute value,
37  * you <b>must</b> manually HTML-encode it beforehand (see {@link Ext.String#htmlEncode}) or risk
38  * malformed HTML being created. This behavior may change in a future release.</p>
39  *
40  * <p><b><u>Insertion methods</u></b></p>
41  * <p>Commonly used insertion methods:
42  * <div class="mdetail-params"><ul>
43  * <li><b><tt>{@link #append}</tt></b> : <div class="sub-desc"></div></li>
44  * <li><b><tt>{@link #insertBefore}</tt></b> : <div class="sub-desc"></div></li>
45  * <li><b><tt>{@link #insertAfter}</tt></b> : <div class="sub-desc"></div></li>
46  * <li><b><tt>{@link #overwrite}</tt></b> : <div class="sub-desc"></div></li>
47  * <li><b><tt>{@link #createTemplate}</tt></b> : <div class="sub-desc"></div></li>
48  * <li><b><tt>{@link #insertHtml}</tt></b> : <div class="sub-desc"></div></li>
49  * </ul></div></p>
50  *
51  * <p><b><u>Example</u></b></p>
52  * <p>This is an example, where an unordered list with 3 children items is appended to an existing
53  * element with id <tt>'my-div'</tt>:<br>
54  <pre><code>
55 var dh = Ext.core.DomHelper; // create shorthand alias
56 // specification object
57 var spec = {
58     id: 'my-ul',
59     tag: 'ul',
60     cls: 'my-list',
61     // append children after creating
62     children: [     // may also specify 'cn' instead of 'children'
63         {tag: 'li', id: 'item0', html: 'List Item 0'},
64         {tag: 'li', id: 'item1', html: 'List Item 1'},
65         {tag: 'li', id: 'item2', html: 'List Item 2'}
66     ]
67 };
68 var list = dh.append(
69     'my-div', // the context element 'my-div' can either be the id or the actual node
70     spec      // the specification object
71 );
72  </code></pre></p>
73  * <p>Element creation specification parameters in this class may also be passed as an Array of
74  * specification objects. This can be used to insert multiple sibling nodes into an existing
75  * container very efficiently. For example, to add more list items to the example above:<pre><code>
76 dh.append('my-ul', [
77     {tag: 'li', id: 'item3', html: 'List Item 3'},
78     {tag: 'li', id: 'item4', html: 'List Item 4'}
79 ]);
80  * </code></pre></p>
81  *
82  * <p><b><u>Templating</u></b></p>
83  * <p>The real power is in the built-in templating. Instead of creating or appending any elements,
84  * <tt>{@link #createTemplate}</tt> returns a Template object which can be used over and over to
85  * insert new elements. Revisiting the example above, we could utilize templating this time:
86  * <pre><code>
87 // create the node
88 var list = dh.append('my-div', {tag: 'ul', cls: 'my-list'});
89 // get template
90 var tpl = dh.createTemplate({tag: 'li', id: 'item{0}', html: 'List Item {0}'});
91
92 for(var i = 0; i < 5, i++){
93     tpl.append(list, [i]); // use template to append to the actual node
94 }
95  * </code></pre></p>
96  * <p>An example using a template:<pre><code>
97 var html = '<a id="{0}" href="{1}" class="nav">{2}</a>';
98
99 var tpl = new Ext.core.DomHelper.createTemplate(html);
100 tpl.append('blog-roll', ['link1', 'http://www.edspencer.net/', "Ed&#39;s Site"]);
101 tpl.append('blog-roll', ['link2', 'http://www.dustindiaz.com/', "Dustin&#39;s Site"]);
102  * </code></pre></p>
103  *
104  * <p>The same example using named parameters:<pre><code>
105 var html = '<a id="{id}" href="{url}" class="nav">{text}</a>';
106
107 var tpl = new Ext.core.DomHelper.createTemplate(html);
108 tpl.append('blog-roll', {
109     id: 'link1',
110     url: 'http://www.edspencer.net/',
111     text: "Ed&#39;s Site"
112 });
113 tpl.append('blog-roll', {
114     id: 'link2',
115     url: 'http://www.dustindiaz.com/',
116     text: "Dustin&#39;s Site"
117 });
118  * </code></pre></p>
119  *
120  * <p><b><u>Compiling Templates</u></b></p>
121  * <p>Templates are applied using regular expressions. The performance is great, but if
122  * you are adding a bunch of DOM elements using the same template, you can increase
123  * performance even further by {@link Ext.Template#compile "compiling"} the template.
124  * The way "{@link Ext.Template#compile compile()}" works is the template is parsed and
125  * broken up at the different variable points and a dynamic function is created and eval'ed.
126  * The generated function performs string concatenation of these parts and the passed
127  * variables instead of using regular expressions.
128  * <pre><code>
129 var html = '<a id="{id}" href="{url}" class="nav">{text}</a>';
130
131 var tpl = new Ext.core.DomHelper.createTemplate(html);
132 tpl.compile();
133
134 //... use template like normal
135  * </code></pre></p>
136  *
137  * <p><b><u>Performance Boost</u></b></p>
138  * <p>DomHelper will transparently create HTML fragments when it can. Using HTML fragments instead
139  * of DOM can significantly boost performance.</p>
140  * <p>Element creation specification parameters may also be strings. If {@link #useDom} is <tt>false</tt>,
141  * then the string is used as innerHTML. If {@link #useDom} is <tt>true</tt>, a string specification
142  * results in the creation of a text node. Usage:</p>
143  * <pre><code>
144 Ext.core.DomHelper.useDom = true; // force it to use DOM; reduces performance
145  * </code></pre>
146  * @singleton
147  */
148 Ext.ns('Ext.core');
149 Ext.core.DomHelper = function(){
150     var tempTableEl = null,
151         emptyTags = /^(?:br|frame|hr|img|input|link|meta|range|spacer|wbr|area|param|col)$/i,
152         tableRe = /^table|tbody|tr|td$/i,
153         confRe = /tag|children|cn|html$/i,
154         tableElRe = /td|tr|tbody/i,
155         endRe = /end/i,
156         pub,
157         // kill repeat to save bytes
158         afterbegin = 'afterbegin',
159         afterend = 'afterend',
160         beforebegin = 'beforebegin',
161         beforeend = 'beforeend',
162         ts = '<table>',
163         te = '</table>',
164         tbs = ts+'<tbody>',
165         tbe = '</tbody>'+te,
166         trs = tbs + '<tr>',
167         tre = '</tr>'+tbe;
168
169     // private
170     function doInsert(el, o, returnElement, pos, sibling, append){
171         el = Ext.getDom(el);
172         var newNode;
173         if (pub.useDom) {
174             newNode = createDom(o, null);
175             if (append) {
176                 el.appendChild(newNode);
177             } else {
178                 (sibling == 'firstChild' ? el : el.parentNode).insertBefore(newNode, el[sibling] || el);
179             }
180         } else {
181             newNode = Ext.core.DomHelper.insertHtml(pos, el, Ext.core.DomHelper.createHtml(o));
182         }
183         return returnElement ? Ext.get(newNode, true) : newNode;
184     }
185     
186     function createDom(o, parentNode){
187         var el,
188             doc = document,
189             useSet,
190             attr,
191             val,
192             cn;
193
194         if (Ext.isArray(o)) {                       // Allow Arrays of siblings to be inserted
195             el = doc.createDocumentFragment(); // in one shot using a DocumentFragment
196             for (var i = 0, l = o.length; i < l; i++) {
197                 createDom(o[i], el);
198             }
199         } else if (typeof o == 'string') {         // Allow a string as a child spec.
200             el = doc.createTextNode(o);
201         } else {
202             el = doc.createElement( o.tag || 'div' );
203             useSet = !!el.setAttribute; // In IE some elements don't have setAttribute
204             for (attr in o) {
205                 if(!confRe.test(attr)){
206                     val = o[attr];
207                     if(attr == 'cls'){
208                         el.className = val;
209                     }else{
210                         if(useSet){
211                             el.setAttribute(attr, val);
212                         }else{
213                             el[attr] = val;
214                         }
215                     }
216                 }
217             }
218             Ext.core.DomHelper.applyStyles(el, o.style);
219
220             if ((cn = o.children || o.cn)) {
221                 createDom(cn, el);
222             } else if (o.html) {
223                 el.innerHTML = o.html;
224             }
225         }
226         if(parentNode){
227            parentNode.appendChild(el);
228         }
229         return el;
230     }
231
232     // build as innerHTML where available
233     function createHtml(o){
234         var b = '',
235             attr,
236             val,
237             key,
238             cn,
239             i;
240
241         if(typeof o == "string"){
242             b = o;
243         } else if (Ext.isArray(o)) {
244             for (i=0; i < o.length; i++) {
245                 if(o[i]) {
246                     b += createHtml(o[i]);
247                 }
248             }
249         } else {
250             b += '<' + (o.tag = o.tag || 'div');
251             for (attr in o) {
252                 val = o[attr];
253                 if(!confRe.test(attr)){
254                     if (typeof val == "object") {
255                         b += ' ' + attr + '="';
256                         for (key in val) {
257                             b += key + ':' + val[key] + ';';
258                         }
259                         b += '"';
260                     }else{
261                         b += ' ' + ({cls : 'class', htmlFor : 'for'}[attr] || attr) + '="' + val + '"';
262                     }
263                 }
264             }
265             // Now either just close the tag or try to add children and close the tag.
266             if (emptyTags.test(o.tag)) {
267                 b += '/>';
268             } else {
269                 b += '>';
270                 if ((cn = o.children || o.cn)) {
271                     b += createHtml(cn);
272                 } else if(o.html){
273                     b += o.html;
274                 }
275                 b += '</' + o.tag + '>';
276             }
277         }
278         return b;
279     }
280
281     function ieTable(depth, s, h, e){
282         tempTableEl.innerHTML = [s, h, e].join('');
283         var i = -1,
284             el = tempTableEl,
285             ns;
286         while(++i < depth){
287             el = el.firstChild;
288         }
289 //      If the result is multiple siblings, then encapsulate them into one fragment.
290         ns = el.nextSibling;
291         if (ns){
292             var df = document.createDocumentFragment();
293             while(el){
294                 ns = el.nextSibling;
295                 df.appendChild(el);
296                 el = ns;
297             }
298             el = df;
299         }
300         return el;
301     }
302
303     /**
304      * @ignore
305      * Nasty code for IE's broken table implementation
306      */
307     function insertIntoTable(tag, where, el, html) {
308         var node,
309             before;
310
311         tempTableEl = tempTableEl || document.createElement('div');
312
313         if(tag == 'td' && (where == afterbegin || where == beforeend) ||
314            !tableElRe.test(tag) && (where == beforebegin || where == afterend)) {
315             return null;
316         }
317         before = where == beforebegin ? el :
318                  where == afterend ? el.nextSibling :
319                  where == afterbegin ? el.firstChild : null;
320
321         if (where == beforebegin || where == afterend) {
322             el = el.parentNode;
323         }
324
325         if (tag == 'td' || (tag == 'tr' && (where == beforeend || where == afterbegin))) {
326             node = ieTable(4, trs, html, tre);
327         } else if ((tag == 'tbody' && (where == beforeend || where == afterbegin)) ||
328                    (tag == 'tr' && (where == beforebegin || where == afterend))) {
329             node = ieTable(3, tbs, html, tbe);
330         } else {
331             node = ieTable(2, ts, html, te);
332         }
333         el.insertBefore(node, before);
334         return node;
335     }
336     
337     /**
338      * @ignore
339      * Fix for IE9 createContextualFragment missing method
340      */   
341     function createContextualFragment(html){
342         var div = document.createElement("div"),
343             fragment = document.createDocumentFragment(),
344             i = 0,
345             length, childNodes;
346         
347         div.innerHTML = html;
348         childNodes = div.childNodes;
349         length = childNodes.length;
350
351         for (; i < length; i++) {
352             fragment.appendChild(childNodes[i].cloneNode(true));
353         }
354
355         return fragment;
356     }
357     
358     pub = {
359         /**
360          * Returns the markup for the passed Element(s) config.
361          * @param {Object} o The DOM object spec (and children)
362          * @return {String}
363          */
364         markup : function(o){
365             return createHtml(o);
366         },
367
368         /**
369          * Applies a style specification to an element.
370          * @param {String/HTMLElement} el The element to apply styles to
371          * @param {String/Object/Function} styles A style specification string e.g. 'width:100px', or object in the form {width:'100px'}, or
372          * a function which returns such a specification.
373          */
374         applyStyles : function(el, styles){
375             if (styles) {
376                 el = Ext.fly(el);
377                 if (typeof styles == "function") {
378                     styles = styles.call();
379                 }
380                 if (typeof styles == "string") {
381                     styles = Ext.core.Element.parseStyles(styles);
382                 }
383                 if (typeof styles == "object") {
384                     el.setStyle(styles);
385                 }
386             }
387         },
388
389         /**
390          * Inserts an HTML fragment into the DOM.
391          * @param {String} where Where to insert the html in relation to el - beforeBegin, afterBegin, beforeEnd, afterEnd.
392          * @param {HTMLElement/TextNode} el The context element
393          * @param {String} html The HTML fragment
394          * @return {HTMLElement} The new node
395          */
396         insertHtml : function(where, el, html){
397             var hash = {},
398                 hashVal,
399                 range,
400                 rangeEl,
401                 setStart,
402                 frag,
403                 rs;
404
405             where = where.toLowerCase();
406             // add these here because they are used in both branches of the condition.
407             hash[beforebegin] = ['BeforeBegin', 'previousSibling'];
408             hash[afterend] = ['AfterEnd', 'nextSibling'];
409             
410             // if IE and context element is an HTMLElement
411             if (el.insertAdjacentHTML) {
412                 if(tableRe.test(el.tagName) && (rs = insertIntoTable(el.tagName.toLowerCase(), where, el, html))){
413                     return rs;
414                 }
415                 
416                 // add these two to the hash.
417                 hash[afterbegin] = ['AfterBegin', 'firstChild'];
418                 hash[beforeend] = ['BeforeEnd', 'lastChild'];
419                 if ((hashVal = hash[where])) {
420                     el.insertAdjacentHTML(hashVal[0], html);
421                     return el[hashVal[1]];
422                 }
423             // if (not IE and context element is an HTMLElement) or TextNode
424             } else {
425                 // we cannot insert anything inside a textnode so...
426                 if (Ext.isTextNode(el)) {
427                     where = where === 'afterbegin' ? 'beforebegin' : where; 
428                     where = where === 'beforeend' ? 'afterend' : where;
429                 }
430                 range = Ext.supports.CreateContextualFragment ? el.ownerDocument.createRange() : undefined;
431                 setStart = 'setStart' + (endRe.test(where) ? 'After' : 'Before');
432                 if (hash[where]) {
433                     if (range) {
434                         range[setStart](el);
435                         frag = range.createContextualFragment(html);
436                     } else {
437                         frag = createContextualFragment(html);
438                     }
439                     el.parentNode.insertBefore(frag, where == beforebegin ? el : el.nextSibling);
440                     return el[(where == beforebegin ? 'previous' : 'next') + 'Sibling'];
441                 } else {
442                     rangeEl = (where == afterbegin ? 'first' : 'last') + 'Child';
443                     if (el.firstChild) {
444                         if (range) {
445                             range[setStart](el[rangeEl]);
446                             frag = range.createContextualFragment(html);
447                         } else {
448                             frag = createContextualFragment(html);
449                         }
450                         
451                         if(where == afterbegin){
452                             el.insertBefore(frag, el.firstChild);
453                         }else{
454                             el.appendChild(frag);
455                         }
456                     } else {
457                         el.innerHTML = html;
458                     }
459                     return el[rangeEl];
460                 }
461             }
462             //<debug>
463             Ext.Error.raise({
464                 sourceClass: 'Ext.core.DomHelper',
465                 sourceMethod: 'insertHtml',
466                 htmlToInsert: html,
467                 targetElement: el,
468                 msg: 'Illegal insertion point reached: "' + where + '"'
469             });
470             //</debug>
471         },
472
473         /**
474          * Creates new DOM element(s) and inserts them before el.
475          * @param {Mixed} el The context element
476          * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
477          * @param {Boolean} returnElement (optional) true to return a Ext.core.Element
478          * @return {HTMLElement/Ext.core.Element} The new node
479          */
480         insertBefore : function(el, o, returnElement){
481             return doInsert(el, o, returnElement, beforebegin);
482         },
483
484         /**
485          * Creates new DOM element(s) and inserts them after el.
486          * @param {Mixed} el The context element
487          * @param {Object} o The DOM object spec (and children)
488          * @param {Boolean} returnElement (optional) true to return a Ext.core.Element
489          * @return {HTMLElement/Ext.core.Element} The new node
490          */
491         insertAfter : function(el, o, returnElement){
492             return doInsert(el, o, returnElement, afterend, 'nextSibling');
493         },
494
495         /**
496          * Creates new DOM element(s) and inserts them as the first child of el.
497          * @param {Mixed} el The context element
498          * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
499          * @param {Boolean} returnElement (optional) true to return a Ext.core.Element
500          * @return {HTMLElement/Ext.core.Element} The new node
501          */
502         insertFirst : function(el, o, returnElement){
503             return doInsert(el, o, returnElement, afterbegin, 'firstChild');
504         },
505
506         /**
507          * Creates new DOM element(s) and appends them to el.
508          * @param {Mixed} el The context element
509          * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
510          * @param {Boolean} returnElement (optional) true to return a Ext.core.Element
511          * @return {HTMLElement/Ext.core.Element} The new node
512          */
513         append : function(el, o, returnElement){
514             return doInsert(el, o, returnElement, beforeend, '', true);
515         },
516
517         /**
518          * Creates new DOM element(s) and overwrites the contents of el with them.
519          * @param {Mixed} el The context element
520          * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
521          * @param {Boolean} returnElement (optional) true to return a Ext.core.Element
522          * @return {HTMLElement/Ext.core.Element} The new node
523          */
524         overwrite : function(el, o, returnElement){
525             el = Ext.getDom(el);
526             el.innerHTML = createHtml(o);
527             return returnElement ? Ext.get(el.firstChild) : el.firstChild;
528         },
529
530         createHtml : createHtml,
531         
532         /**
533          * Creates new DOM element(s) without inserting them to the document.
534          * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
535          * @return {HTMLElement} The new uninserted node
536          * @method
537          */
538         createDom: createDom,
539         
540         /** True to force the use of DOM instead of html fragments @type Boolean */
541         useDom : false,
542         
543         /**
544          * Creates a new Ext.Template from the DOM object spec.
545          * @param {Object} o The DOM object spec (and children)
546          * @return {Ext.Template} The new template
547          */
548         createTemplate : function(o){
549             var html = Ext.core.DomHelper.createHtml(o);
550             return Ext.create('Ext.Template', html);
551         }
552     };
553     return pub;
554 }();
555