1 /**
  2  * @fileOverview dom-create
  3  * @author lifesinger@gmail.com,yiminghe@gmail.com
  4  */
  5 KISSY.add('dom/create', function (S, DOM, UA, undefined) {
  6 
  7         var doc = S.Env.host.document,
  8             ie = UA['ie'],
  9             isString = S.isString,
 10             DIV = 'div',
 11             PARENT_NODE = 'parentNode',
 12             DEFAULT_DIV = doc.createElement(DIV),
 13             rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,
 14             RE_TAG = /<([\w:]+)/,
 15             rtbody = /<tbody/i,
 16             rleadingWhitespace = /^\s+/,
 17             lostLeadingWhitespace = ie && ie < 9,
 18             rhtml = /<|&#?\w+;/,
 19             supportOuterHTML = "outerHTML" in doc.documentElement,
 20             RE_SIMPLE_TAG = /^<(\w+)\s*\/?>(?:<\/\1>)?$/;
 21 
 22         // help compression
 23         function getElementsByTagName(el, tag) {
 24             return el.getElementsByTagName(tag);
 25         }
 26 
 27         function cleanData(els) {
 28             var Event = S.require("event");
 29             if (Event) {
 30                 Event.detach(els);
 31             }
 32             DOM.removeData(els);
 33         }
 34 
 35         S.mix(DOM,
 36             /**
 37              * @lends DOM
 38              */
 39             {
 40 
 41                 /**
 42                  * Creates DOM elements on the fly from the provided string of raw HTML.
 43                  * @param {String} html A string of HTML to create on the fly. Note that this parses HTML, not XML.
 44                  * @param {Object} [props] An map of attributes on the newly-created element.
 45                  * @param {Document} [ownerDoc] A document in which the new elements will be created
 46                  * @returns {DocumentFragment|HTMLElement}
 47                  */
 48                 create:function (html, props, ownerDoc, _trim/*internal*/) {
 49 
 50                     var ret = null;
 51 
 52                     if (!html) {
 53                         return ret;
 54                     }
 55 
 56                     if (html.nodeType) {
 57                         return DOM.clone(html);
 58                     }
 59 
 60 
 61                     if (!isString(html)) {
 62                         return ret;
 63                     }
 64 
 65                     if (_trim === undefined) {
 66                         _trim = true;
 67                     }
 68 
 69                     if (_trim) {
 70                         html = S.trim(html);
 71                     }
 72 
 73 
 74                     var creators = DOM._creators,
 75                         holder,
 76                         whitespaceMatch,
 77                         context = ownerDoc || doc,
 78                         m,
 79                         tag = DIV,
 80                         k,
 81                         nodes;
 82 
 83                     if (!rhtml.test(html)) {
 84                         ret = context.createTextNode(html);
 85                     }
 86                     // 简单 tag, 比如 DOM.create('<p>')
 87                     else if ((m = RE_SIMPLE_TAG.exec(html))) {
 88                         ret = context.createElement(m[1]);
 89                     }
 90                     // 复杂情况,比如 DOM.create('<img src="sprite.png" />')
 91                     else {
 92                         // Fix "XHTML"-style tags in all browsers
 93                         html = html.replace(rxhtmlTag, "<$1><" + "/$2>");
 94 
 95                         if ((m = RE_TAG.exec(html)) && (k = m[1])) {
 96                             tag = k.toLowerCase();
 97                         }
 98 
 99                         holder = (creators[tag] || creators[DIV])(html, context);
100                         // ie 把前缀空白吃掉了
101                         if (lostLeadingWhitespace && (whitespaceMatch = html.match(rleadingWhitespace))) {
102                             holder.insertBefore(context.createTextNode(whitespaceMatch[0]), holder.firstChild);
103                         }
104                         nodes = holder.childNodes;
105 
106                         if (nodes.length === 1) {
107                             // return single node, breaking parentNode ref from "fragment"
108                             ret = nodes[0][PARENT_NODE].removeChild(nodes[0]);
109                         }
110                         else if (nodes.length) {
111                             // return multiple nodes as a fragment
112                             ret = nodeListToFragment(nodes);
113                         } else {
114                             S.error(html + " : create node error");
115                         }
116                     }
117 
118                     return attachProps(ret, props);
119                 },
120 
121                 _creators:{
122                     div:function (html, ownerDoc) {
123                         var frag = ownerDoc && ownerDoc != doc ? ownerDoc.createElement(DIV) : DEFAULT_DIV;
124                         // html 为 <style></style> 时不行,必须有其他元素?
125                         frag['innerHTML'] = "m<div>" + html + "<" + "/div>";
126                         return frag.lastChild;
127                     }
128                 },
129 
130                 /**
131                  * Get the HTML contents of the first element in the set of matched elements.
132                  * or
133                  * Set the HTML contents of each element in the set of matched elements.
134                  * @param {HTMLElement|String|HTMLElement[]} [selector] matched elements
135                  * @param {String} htmlString  A string of HTML to set as the content of each matched element.
136                  * @param {Boolean} [loadScripts=false] True to look for and process scripts
137                  */
138                 html:function (selector, htmlString, loadScripts, callback) {
139                     // supports css selector/Node/NodeList
140                     var els = DOM.query(selector),
141                         el = els[0];
142                     if (!el) {
143                         return
144                     }
145                     // getter
146                     if (htmlString === undefined) {
147                         // only gets value on the first of element nodes
148                         if (el.nodeType == DOM.ELEMENT_NODE) {
149                             return el.innerHTML;
150                         } else {
151                             return null;
152                         }
153                     }
154                     // setter
155                     else {
156 
157                         var success = false, i, elem;
158                         htmlString += "";
159 
160                         // faster
161                         // fix #103,some html element can not be set through innerHTML
162                         if (!htmlString.match(/<(?:script|style|link)/i) &&
163                             (!lostLeadingWhitespace || !htmlString.match(rleadingWhitespace)) &&
164                             !creatorsMap[ (htmlString.match(RE_TAG) || ["", ""])[1].toLowerCase() ]) {
165 
166                             try {
167                                 for (i = els.length - 1; i >= 0; i--) {
168                                     elem = els[i];
169                                     if (elem.nodeType == DOM.ELEMENT_NODE) {
170                                         cleanData(getElementsByTagName(elem, "*"));
171                                         elem.innerHTML = htmlString;
172                                     }
173                                 }
174                                 success = true;
175                             } catch (e) {
176                                 // a <= "<a>"
177                                 // a.innerHTML='<p>1</p>';
178                             }
179 
180                         }
181 
182                         if (!success) {
183                             var valNode = DOM.create(htmlString, 0, el.ownerDocument, 0);
184                             DOM.empty(els);
185                             DOM.append(valNode, els, loadScripts);
186                         }
187                         callback && callback();
188                     }
189                 },
190 
191                 /**
192                  * Get the outerHTML of the first element in the set of matched elements.
193                  * or
194                  * Set the outerHTML of each element in the set of matched elements.
195                  * @param {HTMLElement|String|HTMLElement[]} [selector] matched elements
196                  * @param {String} htmlString  A string of HTML to set as outerHTML of each matched element.
197                  * @param {Boolean} [loadScripts=false] True to look for and process scripts
198                  */
199                 outerHTML:function (selector, htmlString, loadScripts) {
200                     var els = DOM.query(selector),
201                         holder,
202                         i,
203                         valNode,
204                         length = els.length,
205                         el = els[0];
206                     if (!el) {
207                         return
208                     }
209                     // getter
210                     if (htmlString === undefined) {
211                         if (supportOuterHTML) {
212                             return el.outerHTML
213                         } else {
214                             holder = el.ownerDocument.createElement("div");
215                             holder.appendChild(DOM.clone(el, true));
216                             return holder.innerHTML;
217                         }
218                     } else {
219                         htmlString += "";
220                         if (!htmlString.match(/<(?:script|style|link)/i) && supportOuterHTML) {
221                             for (i = length - 1; i >= 0; i--) {
222                                 el = els[i];
223                                 if (el.nodeType == DOM.ELEMENT_NODE) {
224                                     cleanData(el);
225                                     cleanData(getElementsByTagName(el, "*"));
226                                     el.outerHTML = htmlString;
227                                 }
228                             }
229                         } else {
230                             valNode = DOM.create(htmlString, 0, el.ownerDocument, 0);
231                             DOM.insertBefore(valNode, els, loadScripts);
232                             DOM.remove(els);
233                         }
234                     }
235                 },
236 
237                 /**
238                  * Remove the set of matched elements from the DOM.
239                  * @param {HTMLElement|String|HTMLElement[]} [selector] matched elements
240                  * @param {Boolean} [keepData=false] whether keep bound events and jQuery data associated with the elements from removed.
241                  */
242                 remove:function (selector, keepData) {
243                     var el, els = DOM.query(selector), i;
244                     for (i = els.length - 1; i >= 0; i--) {
245                         el = els[i];
246                         if (!keepData && el.nodeType == DOM.ELEMENT_NODE) {
247                             // 清理数据
248                             var elChildren = getElementsByTagName(el, "*");
249                             cleanData(elChildren);
250                             cleanData(el);
251                         }
252 
253                         if (el.parentNode) {
254                             el.parentNode.removeChild(el);
255                         }
256                     }
257                 },
258 
259                 /**
260                  * Create a deep copy of the first of matched elements.
261                  * @param {HTMLElement|String|HTMLElement[]} [selector] matched elements
262                  * @param {Boolean|Object} [deep=false] whether perform deep copy or copy config.
263                  * @param {Boolean} [deep.deep] whether perform deep copy
264                  * @param {Boolean} [deep.withDataAndEvent=false] A Boolean indicating
265                  * whether event handlers and data should be copied along with the elements.
266                  * @param {Boolean} [deep.deepWithDataAndEvent=false]
267                  * A Boolean indicating whether event handlers and data for all children of the cloned element should be copied.
268                  * if set true then deep argument must be set true as well.
269                  * @param {Boolean} [withDataAndEvent=false] A Boolean indicating
270                  * whether event handlers and data should be copied along with the elements.
271                  * @param {Boolean} [deepWithDataAndEvent=false]
272                  * A Boolean indicating whether event handlers and data for all children of the cloned element should be copied.
273                  * if set true then deep argument must be set true as well.
274                  * @see https://developer.mozilla.org/En/DOM/Node.cloneNode
275                  * @returns {HTMLElement}
276                  */
277                 clone:function (selector, deep, withDataAndEvent, deepWithDataAndEvent) {
278 
279                     if (typeof deep === 'object') {
280                         deepWithDataAndEvent = deep['deepWithDataAndEvent'];
281                         withDataAndEvent = deep['withDataAndEvent'];
282                         deep = deep['deep'];
283                     }
284 
285                     var elem = DOM.get(selector);
286 
287                     if (!elem) {
288                         return null;
289                     }
290 
291                     // TODO
292                     // ie bug :
293                     // 1. ie<9 <script>xx</script> => <script></script>
294                     // 2. ie will execute external script
295                     var clone = elem.cloneNode(deep);
296 
297                     if (elem.nodeType == DOM.ELEMENT_NODE ||
298                         elem.nodeType == DOM.DOCUMENT_FRAGMENT_NODE) {
299                         // IE copies events bound via attachEvent when using cloneNode.
300                         // Calling detachEvent on the clone will also remove the events
301                         // from the original. In order to get around this, we use some
302                         // proprietary methods to clear the events. Thanks to MooTools
303                         // guys for this hotness.
304                         if (elem.nodeType == DOM.ELEMENT_NODE) {
305                             fixAttributes(elem, clone);
306                         }
307 
308                         if (deep) {
309                             processAll(fixAttributes, elem, clone);
310                         }
311                     }
312                     // runtime 获得事件模块
313                     if (withDataAndEvent) {
314                         cloneWithDataAndEvent(elem, clone);
315                         if (deep && deepWithDataAndEvent) {
316                             processAll(cloneWithDataAndEvent, elem, clone);
317                         }
318                     }
319                     return clone;
320                 },
321 
322                 /**
323                  * Remove(include data and event handlers) all child nodes of the set of matched elements from the DOM.
324                  * @param {HTMLElement|String|HTMLElement[]} [selector] matched elements
325                  */
326                 empty:function (selector) {
327                     var els = DOM.query(selector), el, i;
328                     for (i = els.length - 1; i >= 0; i--) {
329                         el = els[i];
330                         DOM.remove(el.childNodes);
331                     }
332                 },
333 
334                 nodeListToFragment:nodeListToFragment
335             });
336 
337         function processAll(fn, elem, clone) {
338             if (elem.nodeType == DOM.DOCUMENT_FRAGMENT_NODE) {
339                 var eCs = elem.childNodes,
340                     cloneCs = clone.childNodes,
341                     fIndex = 0;
342                 while (eCs[fIndex]) {
343                     if (cloneCs[fIndex]) {
344                         processAll(fn, eCs[fIndex], cloneCs[fIndex]);
345                     }
346                     fIndex++;
347                 }
348             } else if (elem.nodeType == DOM.ELEMENT_NODE) {
349                 var elemChildren = getElementsByTagName(elem, "*"),
350                     cloneChildren = getElementsByTagName(clone, "*"),
351                     cIndex = 0;
352                 while (elemChildren[cIndex]) {
353                     if (cloneChildren[cIndex]) {
354                         fn(elemChildren[cIndex], cloneChildren[cIndex]);
355                     }
356                     cIndex++;
357                 }
358             }
359         }
360 
361 
362         // 克隆除了事件的 data
363         function cloneWithDataAndEvent(src, dest) {
364             var Event = S.require('event');
365 
366             if (dest.nodeType == DOM.ELEMENT_NODE && !DOM.hasData(src)) {
367                 return;
368             }
369 
370             var srcData = DOM.data(src);
371 
372             // 浅克隆,data 也放在克隆节点上
373             for (var d in srcData) {
374                 DOM.data(dest, d, srcData[d]);
375             }
376 
377             // 事件要特殊点
378             if (Event) {
379                 // remove event data (but without dom attached listener) which is copied from above DOM.data
380                 Event._removeData(dest);
381                 // attach src's event data and dom attached listener to dest
382                 Event._clone(src, dest);
383             }
384         }
385 
386         // wierd ie cloneNode fix from jq
387         function fixAttributes(src, dest) {
388 
389             // clearAttributes removes the attributes, which we don't want,
390             // but also removes the attachEvent events, which we *do* want
391             if (dest.clearAttributes) {
392                 dest.clearAttributes();
393             }
394 
395             // mergeAttributes, in contrast, only merges back on the
396             // original attributes, not the events
397             if (dest.mergeAttributes) {
398                 dest.mergeAttributes(src);
399             }
400 
401             var nodeName = dest.nodeName.toLowerCase(),
402                 srcChilds = src.childNodes;
403 
404             // IE6-8 fail to clone children inside object elements that use
405             // the proprietary classid attribute value (rather than the type
406             // attribute) to identify the type of content to display
407             if (nodeName === "object" && !dest.childNodes.length) {
408                 for (var i = 0; i < srcChilds.length; i++) {
409                     dest.appendChild(srcChilds[i].cloneNode(true));
410                 }
411                 // dest.outerHTML = src.outerHTML;
412             } else if (nodeName === "input" && (src.type === "checkbox" || src.type === "radio")) {
413                 // IE6-8 fails to persist the checked state of a cloned checkbox
414                 // or radio button. Worse, IE6-7 fail to give the cloned element
415                 // a checked appearance if the defaultChecked value isn't also set
416                 if (src.checked) {
417                     dest['defaultChecked'] = dest.checked = src.checked;
418                 }
419 
420                 // IE6-7 get confused and end up setting the value of a cloned
421                 // checkbox/radio button to an empty string instead of "on"
422                 if (dest.value !== src.value) {
423                     dest.value = src.value;
424                 }
425 
426                 // IE6-8 fails to return the selected option to the default selected
427                 // state when cloning options
428             } else if (nodeName === "option") {
429                 dest.selected = src.defaultSelected;
430                 // IE6-8 fails to set the defaultValue to the correct value when
431                 // cloning other types of input fields
432             } else if (nodeName === "input" || nodeName === "textarea") {
433                 dest.defaultValue = src.defaultValue;
434             }
435 
436             // Event data gets referenced instead of copied if the expando
437             // gets copied too
438             // 自定义 data 根据参数特殊处理,expando 只是个用于引用的属性
439             dest.removeAttribute(DOM.__EXPANDO);
440         }
441 
442         // 添加成员到元素中
443         function attachProps(elem, props) {
444             if (S.isPlainObject(props)) {
445                 if (elem.nodeType == DOM.ELEMENT_NODE) {
446                     DOM.attr(elem, props, true);
447                 }
448                 // document fragment
449                 else if (elem.nodeType == DOM.DOCUMENT_FRAGMENT_NODE) {
450                     DOM.attr(elem.childNodes, props, true);
451                 }
452             }
453             return elem;
454         }
455 
456         // 将 nodeList 转换为 fragment
457         function nodeListToFragment(nodes) {
458             var ret = null,
459                 i,
460                 ownerDoc,
461                 len;
462             if (nodes && (nodes.push || nodes.item) && nodes[0]) {
463                 ownerDoc = nodes[0].ownerDocument;
464                 ret = ownerDoc.createDocumentFragment();
465                 nodes = S.makeArray(nodes);
466                 for (i = 0, len = nodes.length; i < len; i++) {
467                     ret.appendChild(nodes[i]);
468                 }
469             } else {
470                 S.log('Unable to convert ' + nodes + ' to fragment.');
471             }
472             return ret;
473         }
474 
475         // only for gecko and ie
476         // 2010-10-22: 发现 chrome 也与 gecko 的处理一致了
477         // if (ie || UA['gecko'] || UA['webkit']) {
478         // 定义 creators, 处理浏览器兼容
479         var creators = DOM._creators,
480             create = DOM.create,
481             creatorsMap = {
482                 option:'select',
483                 optgroup:'select',
484                 area:'map',
485                 thead:'table',
486                 td:'tr',
487                 th:'tr',
488                 tr:'tbody',
489                 tbody:'table',
490                 tfoot:'table',
491                 caption:'table',
492                 colgroup:'table',
493                 col:'colgroup',
494                 legend:'fieldset' // ie 支持,但 gecko 不支持
495             };
496 
497         for (var p in creatorsMap) {
498             (function (tag) {
499                 creators[p] = function (html, ownerDoc) {
500                     return create('<' + tag + '>' + html + '<' + '/' + tag + '>', null, ownerDoc);
501                 };
502             })(creatorsMap[p]);
503         }
504 
505 
506         // IE7- adds TBODY when creating thead/tfoot/caption/col/colgroup elements
507         if (ie < 8) {
508             // fix #88
509             // https://github.com/kissyteam/kissy/issues/88 : spurious tbody in ie<8
510             creators.table = function (html, ownerDoc) {
511                 var frag = creators[DIV](html, ownerDoc),
512                     hasTBody = rtbody.test(html);
513                 if (hasTBody) {
514                     return frag;
515                 }
516                 var table = frag.firstChild,
517                     tableChildren = S.makeArray(table.childNodes);
518                 S.each(tableChildren, function (c) {
519                     if (DOM.nodeName(c) == "tbody" && !c.childNodes.length) {
520                         table.removeChild(c);
521                     }
522                 });
523                 return frag;
524             };
525         }
526         //}
527         return DOM;
528     },
529     {
530         requires:["./base", "ua"]
531     });
532 
533 /**
534  * 2012-01-31
535  * remove spurious tbody
536  *
537  * 2011-10-13
538  * empty , html refactor
539  *
540  * 2011-08-22
541  * clone 实现,参考 jq
542  *
543  * 2011-08
544  *  remove 需要对子孙节点以及自身清除事件以及自定义 data
545  *  create 修改,支持 <style></style> ie 下直接创建
546  *  TODO: jquery clone ,clean 实现
547  *
548  * TODO:
549  *  - 研究 jQuery 的 buildFragment 和 clean
550  *  - 增加 cache, 完善 test cases
551  *  - 支持更多 props
552  *  - remove 时,是否需要移除事件,以避免内存泄漏?需要详细的测试。
553  */
554