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