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