1 /** 2 * dom utils for kissy editor,mainly from ckeditor 3 * @author yiminghe@gmail.com 4 */ 5 /* 6 Copyright (c) 2003-2010, CKSource - Frederico Knabben. All rights reserved. 7 For licensing, see LICENSE.html or http://ckeditor.com/license 8 */ 9 KISSY.add("editor/core/dom", function (S, Editor, Utils) { 10 11 var TRUE = true, 12 undefined = undefined, 13 FALSE = false, 14 NULL = null, 15 xhtml_dtd = Editor.XHTML_DTD, 16 DOM = S.DOM, 17 UA = S.UA, 18 Node = S.Node, 19 REMOVE_EMPTY = { 20 a:1, 21 abbr:1, 22 acronym:1, 23 address:1, 24 b:1, 25 bdo:1, 26 big:1, 27 cite:1, 28 code:1, 29 del:1, 30 dfn:1, 31 em:1, 32 font:1, 33 i:1, 34 ins:1, 35 label:1, 36 kbd:1, 37 q:1, 38 s:1, 39 samp:1, 40 small:1, 41 span:1, 42 strike:1, 43 strong:1, 44 sub:1, 45 sup:1, 46 tt:1, 47 u:1, 48 'var':1 49 }; 50 /** 51 * Enum for node position 52 * @enum {number} 53 */ 54 Editor.POSITION = { 55 POSITION_IDENTICAL:0, 56 POSITION_DISCONNECTED:1, 57 POSITION_FOLLOWING:2, 58 POSITION_PRECEDING:4, 59 POSITION_IS_CONTAINED:8, 60 POSITION_CONTAINS:16 61 }; 62 var KEP = Editor.POSITION; 63 64 /* 65 * Anything whose display computed style is block, list-item, table, 66 * table-row-group, table-header-group, table-footer-group, table-row, 67 * table-column-group, table-column, table-cell, table-caption, or whose node 68 * name is hr, br (when enterMode is br only) is a block boundary. 69 */ 70 var blockBoundaryDisplayMatch = { 71 block:1, 72 'list-item':1, 73 table:1, 74 'table-row-group':1, 75 'table-header-group':1, 76 'table-footer-group':1, 77 'table-row':1, 78 'table-column-group':1, 79 'table-column':1, 80 'table-cell':1, 81 'table-caption':1 82 }, 83 blockBoundaryNodeNameMatch = { hr:1 }, 84 /** 85 * @param el {(Node)} 86 */ 87 normalElDom = function (el) { 88 return el && (el[0] || el); 89 }, 90 /** 91 * @param el {(Node)} 92 */ 93 normalEl = function (el) { 94 return new Node(el); 95 }, 96 editorDom = { 97 98 /** 99 * Whether two nodes are on the same level. 100 * @param el1 101 * @param [el2] 102 * @return {Boolean} 103 * @private 104 */ 105 _4e_sameLevel:function (el1, el2) { 106 el2 = normalElDom(el2); 107 var e1p = el1.parentNode; 108 return e1p && e1p == el2.parentNode; 109 }, 110 111 /** 112 * 是否是块状元素或块状元素边界 113 * @param el 114 * @param [customNodeNames] 115 */ 116 _4e_isBlockBoundary:function (el, customNodeNames) { 117 var nodeNameMatches = S.merge(blockBoundaryNodeNameMatch, customNodeNames); 118 return !!(blockBoundaryDisplayMatch[ DOM.css(el, 'display') ] || nodeNameMatches[ DOM.nodeName(el) ]); 119 }, 120 121 /** 122 * 返回当前元素在父元素中所有儿子节点中的序号 123 * @param [el] 124 * @param [normalized] 125 */ 126 _4e_index:function (el, normalized) { 127 var siblings = el.parentNode.childNodes, 128 candidate, 129 currentIndex = -1; 130 131 for (var i = 0; i < siblings.length; i++) { 132 candidate = siblings[i]; 133 134 // 连续的字符串节点合并 135 if (normalized && 136 candidate.nodeType == 3 && 137 candidate.previousSibling && 138 candidate.previousSibling.nodeType == 3) { 139 continue; 140 } 141 142 currentIndex++; 143 144 if (candidate === el) { 145 return currentIndex; 146 } 147 } 148 return -1; 149 }, 150 151 /** 152 * 把 thisElement 移到 target 的前面或后面 153 * @param thisElement 154 * @param target 155 * @param toStart 156 */ 157 _4e_move:function (thisElement, target, toStart) { 158 target = normalElDom(target); 159 if (toStart) { 160 target.insertBefore(thisElement, target.firstChild); 161 } else { 162 target.appendChild(thisElement); 163 } 164 }, 165 166 /** 167 * 两个元素是否名称和属性都相同 168 * @param thisElement 169 * @param otherElement 170 */ 171 _4e_isIdentical:function (thisElement, otherElement) { 172 if (!otherElement) { 173 return FALSE; 174 } 175 176 otherElement = normalElDom(otherElement); 177 178 if (DOM.nodeName(thisElement) != DOM.nodeName(otherElement)) { 179 return FALSE; 180 } 181 182 var thisAttributes = thisElement.attributes, 183 otherAttributes = otherElement.attributes; 184 185 var thisLength = thisAttributes.length, 186 otherLength = otherAttributes.length; 187 188 if (thisLength != otherLength) { 189 return FALSE; 190 } 191 192 for (var i = 0; i < thisLength; i++) { 193 var attribute = thisAttributes[i], 194 name = attribute.name; 195 if (attribute.specified && 196 DOM.attr(thisElement, name) != DOM.attr(otherElement, name)) { 197 return FALSE; 198 } 199 } 200 201 // For IE, we have to for both elements, because it's difficult to 202 // know how the atttibutes collection is organized in its DOM. 203 // ie 使用版本 < 8 204 if (Utils.ieEngine < 8) { 205 for (i = 0; i < otherLength; i++) { 206 attribute = otherAttributes[ i ]; 207 name = attribute.name; 208 if (attribute.specified && 209 DOM.attr(thisElement, name) != DOM.attr(otherElement, name)) { 210 return FALSE; 211 } 212 } 213 } 214 215 return TRUE; 216 }, 217 218 /** 219 * inline 元素是否没有包含有效文字内容 220 * @param thisElement 221 */ 222 _4e_isEmptyInlineRemovable:function (thisElement) { 223 if (!xhtml_dtd.$removeEmpty[DOM.nodeName(thisElement)]) { 224 return false; 225 } 226 var children = thisElement.childNodes; 227 for (var i = 0, count = children.length; i < count; i++) { 228 var child = children[i], 229 nodeType = child.nodeType; 230 231 if (nodeType == DOM.ELEMENT_NODE && 232 child.getAttribute('_ke_bookmark')) { 233 continue; 234 } 235 236 if (nodeType == DOM.ELEMENT_NODE && !DOM._4e_isEmptyInlineRemovable(child) || 237 nodeType == DOM.TEXT_NODE && S.trim(child.nodeValue)) { 238 return FALSE; 239 } 240 } 241 return TRUE; 242 }, 243 244 /** 245 * 把 thisElement 的所有儿子节点都插入到 target 节点的前面或后面 246 * @param thisElement 247 * @param target 248 * @param toStart 249 */ 250 _4e_moveChildren:function (thisElement, target, toStart) { 251 target = normalElDom(target); 252 253 if (thisElement == target) { 254 return; 255 } 256 257 var child; 258 259 if (toStart) { 260 while (child = thisElement.lastChild) { 261 target.insertBefore(thisElement.removeChild(child), target.firstChild); 262 } 263 } else { 264 while (child = thisElement.firstChild) { 265 target.appendChild(thisElement.removeChild(child)); 266 } 267 } 268 }, 269 270 /** 271 * 将当前元素和周围的元素合并 272 * @example 273 * <code> 274 * <b><i>1</i></b><b><i>3</i></b> 275 * => 276 * <b><i>13</i></b> 277 * </code> 278 */ 279 _4e_mergeSiblings:function (thisElement) { 280 thisElement = normalEl(thisElement); 281 // 只合并空元素不占用空间的标签 282 if (REMOVE_EMPTY[thisElement.nodeName()]) { 283 mergeElements(thisElement, TRUE); 284 mergeElements(thisElement); 285 } 286 }, 287 288 /** 289 * 将一个字符串节点拆散为两个字符串节点,并返回最后一个。 290 * 如果 offset 为 0,仍然拆成两个!第一个字符串为空文字节点。 291 * @param el 292 * @param offset 293 */ 294 _4e_splitText:function (el, offset) { 295 var doc = el.ownerDocument; 296 297 if (el.nodeType != DOM.TEXT_NODE) { 298 return; 299 } 300 // If the offset is after the last char, IE creates the text node 301 // on split, but don't include it into the DOM. So, we have to do 302 // that manually here. 303 if (UA['ie'] && offset == el.nodeValue.length) { 304 var next = doc.createTextNode(""); 305 DOM.insertAfter(next, el); 306 return next; 307 } 308 309 var ret = el.splitText(offset); 310 311 // IE BUG: IE8 does not update the childNodes array in DOM after splitText(), 312 // we need to make some DOM changes to make it update. (#3436) 313 // UA['ie']==8 不对, 314 // 判断不出来:UA['ie']==7 && doc.documentMode==7 315 // 浏览器模式:当ie8处于兼容视图以及ie7时,UA['ie']==7 316 // 文本模式: mode=5 ,mode=7, mode=8 317 // ie8 浏览器有问题,而不在于是否哪个模式 318 if (!!(doc.documentMode)) { 319 var workaround = doc.createTextNode(""); 320 DOM.insertAfter(workaround, ret); 321 DOM.remove(workaround); 322 } 323 324 return ret; 325 }, 326 327 /** 328 * 得到该节点的所有附近节点集合(包括自身) 329 * @param node 330 * @param closerFirst 331 */ 332 _4e_parents:function (node, closerFirst) { 333 var parents = []; 334 parents.__IS_NODELIST = 1; 335 do { 336 parents[ closerFirst ? 'push' : 'unshift' ](node); 337 } while (node = node.parentNode); 338 return parents; 339 }, 340 341 /** 342 * 得到该节点在前序遍历下的下一个节点 343 * @param el 344 * @param [startFromSibling] 345 * @param [nodeType] 346 * @param [guard] 347 */ 348 _4e_nextSourceNode:function (el, startFromSibling, nodeType, guard) { 349 // If "guard" is a node, transform it in a function. 350 if (guard && !guard.call) { 351 var guardNode = normalElDom(guard); 352 guard = function (node) { 353 return node !== guardNode; 354 }; 355 } 356 357 var node = !startFromSibling && el.firstChild , 358 parent = el; 359 360 // Guarding when we're skipping the current element( no children or 'startFromSibling' ). 361 // send the 'moving out' signal even we don't actually dive into. 362 if (!node) { 363 if (el.nodeType == DOM.ELEMENT_NODE && 364 guard && guard(el, TRUE) === FALSE) { 365 return NULL; 366 } 367 node = el.nextSibling; 368 } 369 370 while (!node && ( parent = parent.parentNode)) { 371 // The guard check sends the "TRUE" paramenter to indicate that 372 // we are moving "out" of the element. 373 if (guard && guard(parent, TRUE) === FALSE) { 374 return NULL; 375 } 376 node = parent.nextSibling; 377 } 378 379 if (!node) { 380 return NULL; 381 } 382 383 if (guard && guard(node) === FALSE) { 384 return NULL; 385 } 386 387 if (nodeType && nodeType != node.nodeType) { 388 return DOM._4e_nextSourceNode(node, FALSE, nodeType, guard); 389 } 390 391 return node; 392 }, 393 394 /** 395 * 得到该节点在从右向左前序遍历下的下一个节点( rtl 情况) 396 * @param el 397 * @param startFromSibling 398 * @param nodeType 399 * @param guard 400 */ 401 _4e_previousSourceNode:function (el, startFromSibling, nodeType, guard) { 402 if (guard && !guard.call) { 403 var guardNode = normalElDom(guard); 404 guard = function (node) { 405 return node !== guardNode; 406 }; 407 } 408 409 var node = !startFromSibling && el.lastChild, 410 parent = el; 411 412 // Guarding when we're skipping the current element( no children or 'startFromSibling' ). 413 // send the 'moving out' signal even we don't actually dive into. 414 if (!node) { 415 if (el.nodeType == DOM.ELEMENT_NODE && 416 guard && guard(el, TRUE) === FALSE) { 417 return NULL; 418 } 419 node = el.previousSibling; 420 } 421 422 while (!node && ( parent = parent.parentNode )) { 423 // The guard check sends the "TRUE" paramenter to indicate that 424 // we are moving "out" of the element. 425 if (guard && guard(parent, TRUE) === FALSE) 426 return NULL; 427 node = parent.previousSibling; 428 } 429 430 if (!node) { 431 return NULL; 432 } 433 434 if (guard && guard(node) === FALSE) { 435 return NULL; 436 } 437 438 if (nodeType && node.nodeType != nodeType) { 439 return DOM._4e_previousSourceNode(node, FALSE, nodeType, guard); 440 } 441 442 return node; 443 }, 444 445 /** 446 * 得到两个节点的公共祖先节点 447 * @param el 448 * @param node 449 */ 450 _4e_commonAncestor:function (el, node) { 451 452 node = normalElDom(node); 453 454 if (el === node) { 455 return el; 456 } 457 458 if (DOM.contains(node, el)) { 459 return node; 460 } 461 462 var start = el; 463 464 do { 465 if (DOM.contains(start, node)) { 466 return start; 467 } 468 } while (start = start.parentNode); 469 470 return NULL; 471 }, 472 473 /** 474 * 判断当前元素是否有设置过属性 475 */ 476 _4e_hasAttributes:Utils.ieEngine < 9 ? 477 function (el) { 478 var attributes = el.attributes; 479 for (var i = 0; i < attributes.length; i++) { 480 var attribute = attributes[i]; 481 switch (attribute.name) { 482 case 'class' : 483 // IE has a strange bug. If calling removeAttribute('className'), 484 // the attributes collection will still contain the "class" 485 // attribute, which will be marked as "specified", even if the 486 // outerHTML of the element is not displaying the class attribute. 487 if (el.getAttribute('class')) { 488 return TRUE; 489 } 490 break; 491 default : 492 if (attribute.specified) { 493 return TRUE; 494 } 495 } 496 } 497 return FALSE; 498 } : function (el) { 499 // 删除firefox自己添加的标志 500 if (UA.gecko) { 501 el.removeAttribute("_moz_dirty"); 502 } 503 // 使用原生 504 // ie8 莫名其妙多个shape??specified为false 505 return el.hasAttributes(); 506 }, 507 508 /** 509 * 得到两个元素的位置关系,参见 510 * <a href='https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition'> 511 * compareDocumentPosition 512 * </a> 513 * 注意:这里的 following 和 preceding 和 mdc 相反! 514 * @param el 515 * @param otherNode 516 */ 517 _4e_position:function (el, otherNode) { 518 var $other = normalElDom(otherNode); 519 520 if (el.compareDocumentPosition) { 521 return el.compareDocumentPosition($other); 522 } 523 524 // IE and Safari have no support for compareDocumentPosition. 525 526 if (el == $other) { 527 return KEP.POSITION_IDENTICAL; 528 } 529 530 // Only element nodes support contains and sourceIndex. 531 if (el.nodeType == DOM.ELEMENT_NODE && 532 $other.nodeType == DOM.ELEMENT_NODE) { 533 if (DOM.contains(el, $other)) { 534 return KEP.POSITION_CONTAINS + KEP.POSITION_PRECEDING; 535 } 536 537 if (DOM.contains($other, el)) { 538 return KEP.POSITION_IS_CONTAINED + KEP.POSITION_FOLLOWING; 539 } 540 541 if ('sourceIndex' in el) { 542 return ( el.sourceIndex < 0 || $other.sourceIndex < 0 ) ? 543 KEP.POSITION_DISCONNECTED : 544 ( el.sourceIndex < $other.sourceIndex ) ? 545 KEP.POSITION_PRECEDING : 546 KEP.POSITION_FOLLOWING; 547 } 548 } 549 550 // For nodes that don't support compareDocumentPosition, contains 551 // or sourceIndex, their "address" is compared. 552 var addressOfThis = DOM._4e_address(el), 553 addressOfOther = DOM._4e_address($other), 554 minLevel = Math.min(addressOfThis.length, addressOfOther.length); 555 556 // Determinate preceed/follow relationship. 557 for (var i = 0; i <= minLevel - 1; i++) { 558 if (addressOfThis[ i ] != addressOfOther[ i ]) { 559 return addressOfThis[ i ] < addressOfOther[ i ] ? 560 KEP.POSITION_PRECEDING : KEP.POSITION_FOLLOWING; 561 } 562 } 563 564 // Determinate contains/contained relationship. 565 return ( addressOfThis.length < addressOfOther.length ) ? 566 KEP.POSITION_CONTAINS + KEP.POSITION_PRECEDING : 567 KEP.POSITION_IS_CONTAINED + KEP.POSITION_FOLLOWING; 568 }, 569 570 /** 571 * 得到元素及其所有祖先元素在其兄弟节点中的序号。 572 * @param el 573 * @param [normalized] 574 */ 575 _4e_address:function (el, normalized) { 576 var address = [], 577 $documentElement = el.ownerDocument.documentElement, 578 node = el; 579 580 while (node && node != $documentElement) { 581 address.unshift(DOM._4e_index(node, normalized)); 582 node = node.parentNode; 583 } 584 585 return address; 586 }, 587 588 /** 589 * 删除一个元素 590 * @param el 591 * @param preserveChildren 是否保留其子元素(将子元素插入到当前元素之前) 592 */ 593 _4e_remove:function (el, preserveChildren) { 594 var parent = el.parentNode; 595 if (parent) { 596 if (preserveChildren) { 597 // Move all children before the node. 598 for (var child; child = el.firstChild;) { 599 parent.insertBefore(el.removeChild(child), el); 600 } 601 } 602 parent.removeChild(el); 603 } 604 return el; 605 }, 606 607 /** 608 * 清除左右空的字符串节点 609 * @param el 610 */ 611 _4e_trim:function (el) { 612 DOM._4e_ltrim(el); 613 DOM._4e_rtrim(el); 614 }, 615 616 /** 617 * 清除左边空的字符串节点 618 * @param el 619 */ 620 _4e_ltrim:function (el) { 621 var child; 622 while (child = el.firstChild) { 623 if (child.nodeType == DOM.TEXT_NODE) { 624 var trimmed = Utils.ltrim(child.nodeValue), 625 originalLength = child.nodeValue.length; 626 627 if (!trimmed) { 628 el.removeChild(child); 629 continue; 630 } 631 else if (trimmed.length < originalLength) { 632 DOM._4e_splitText(child, originalLength - trimmed.length); 633 // IE BUG: child.remove() may raise JavaScript errors here. (#81) 634 el.removeChild(el.firstChild); 635 } 636 } 637 break; 638 } 639 }, 640 641 /** 642 * 清除右边空的字符串节点 643 * @param el 644 */ 645 _4e_rtrim:function (el) { 646 var child; 647 while (child = el.lastChild) { 648 if (child.type == DOM.TEXT_NODE) { 649 var trimmed = Utils.rtrim(child.nodeValue), 650 originalLength = child.nodeValue.length; 651 if (!trimmed) { 652 el.removeChild(child); 653 continue; 654 } else if (trimmed.length < originalLength) { 655 DOM._4e_splitText(child, trimmed.length); 656 // IE BUG: child.getNext().remove() may raise JavaScript errors here. 657 // (#81) 658 el.removeChild(el.lastChild); 659 } 660 } 661 break; 662 } 663 664 if (!UA['ie'] && !UA.opera) { 665 child = el.lastChild; 666 if (child && 667 child.nodeType == 1 && 668 DOM.nodeName(child) == 'br') { 669 el.removeChild(child); 670 } 671 } 672 }, 673 674 /** 675 * 将一个 bogus 元素添加到元素末尾 676 * @param el 677 */ 678 _4e_appendBogus:function (el) { 679 var lastChild = el.lastChild, bogus; 680 681 // Ignore empty/spaces text. 682 while (lastChild && 683 lastChild.nodeType == DOM.TEXT_NODE && 684 !S.trim(lastChild.nodeValue)) { 685 lastChild = lastChild.previousSibling; 686 } 687 688 if (!lastChild || 689 lastChild.nodeType == DOM.TEXT_NODE || 690 DOM.nodeName(lastChild) !== 'br') { 691 bogus = UA.opera ? 692 el.ownerDocument.createTextNode('') : 693 el.ownerDocument.createElement('br'); 694 if (UA.gecko) { 695 bogus.setAttribute('type', '_moz'); 696 } 697 el.appendChild(bogus); 698 } 699 }, 700 701 /** 702 * 得到元素的 outerHTML 703 * @param el 704 */ 705 _4e_outerHtml:function (el) { 706 if (el.outerHTML) { 707 // IE includes the <?xml:namespace> tag in the outerHTML of 708 // namespaced element. So, we must strip it here. (#3341) 709 return el.outerHTML.replace(/<\?[^>]*>/, ''); 710 } 711 712 var tmpDiv = el.ownerDocument.createElement('div'); 713 tmpDiv.appendChild(el.cloneNode(TRUE)); 714 return tmpDiv.innerHTML; 715 }, 716 717 /** 718 * 设置元素的自定义 data 值,并记录 719 * @param element 720 * @param database 721 * @param name 722 * @param value 723 */ 724 _4e_setMarker:function (element, database, name, value) { 725 element = normalEl(element); 726 var id = element.data('list_marker_id') || 727 ( element.data('list_marker_id', S.guid()).data('list_marker_id')), 728 markerNames = element.data('list_marker_names') || 729 ( element.data('list_marker_names', {}).data('list_marker_names')); 730 database[id] = element; 731 markerNames[name] = 1; 732 return element.data(name, value); 733 }, 734 735 /** 736 * 清除元素设置的自定义 data 值。 737 * @param element 738 * @param database 739 * @param removeFromDatabase 740 */ 741 _4e_clearMarkers:function (element, database, removeFromDatabase) { 742 element = normalEl(element); 743 var names = element.data('list_marker_names'), 744 id = element.data('list_marker_id'); 745 for (var i in names) { 746 if (names.hasOwnProperty(i)) { 747 element.removeData(i); 748 } 749 } 750 element.removeData('list_marker_names'); 751 if (removeFromDatabase) { 752 element.removeData('list_marker_id'); 753 delete database[id]; 754 } 755 }, 756 757 /** 758 * 把属性从 target 复制到 el 上. 759 * @param el 760 * @param target 761 * @param skipAttributes 762 */ 763 _4e_copyAttributes:function (el, target, skipAttributes) { 764 target = normalEl(target); 765 var attributes = el.attributes; 766 skipAttributes = skipAttributes || {}; 767 768 for (var n = 0; n < attributes.length; n++) { 769 // Lowercase attribute name hard rule is broken for 770 // some attribute on IE, e.g. CHECKED. 771 var attribute = attributes[n], 772 attrName = attribute.name.toLowerCase(), 773 attrValue; 774 775 // We can set the type only once, so do it with the proper value, not copying it. 776 if (attrName in skipAttributes) { 777 continue; 778 } 779 780 if (attrName == 'checked' && ( attrValue = DOM.attr(el, attrName) )) { 781 target.attr(attrName, attrValue); 782 } 783 // IE BUG: value attribute is never specified even if it exists. 784 else if (attribute.specified || 785 ( UA['ie'] && attribute.value && attrName == 'value' )) { 786 attrValue = DOM.attr(el, attrName); 787 if (attrValue === NULL) { 788 attrValue = attribute.nodeValue; 789 } 790 target.attr(attrName, attrValue); 791 } 792 } 793 794 // The style: 795 if (el.style.cssText !== '') { 796 target[0].style.cssText = el.style.cssText; 797 } 798 }, 799 800 /** 801 * 当前元素是否可以被编辑 802 * @param el 803 */ 804 _4e_isEditable:function (el) { 805 // Get the element DTD (defaults to span for unknown elements). 806 var name = DOM.nodeName(el), 807 dtd = !xhtml_dtd.$nonEditable[ name ] && 808 ( xhtml_dtd[ name ] || xhtml_dtd["span"] ); 809 // In the DTD # == text node. 810 return dtd && dtd['#text']; 811 }, 812 813 /** 814 * 根据dom路径得到某个节点 815 * @param doc 816 * @param address 817 * @param [normalized] 818 * @return {NodeList} 819 */ 820 _4e_getByAddress:function (doc, address, normalized) { 821 var $ = doc.documentElement; 822 823 for (var i = 0; $ && i < address.length; i++) { 824 var target = address[ i ]; 825 826 if (!normalized) { 827 $ = $.childNodes[ target ]; 828 continue; 829 } 830 831 var currentIndex = -1; 832 833 for (var j = 0; j < $.childNodes.length; j++) { 834 var candidate = $.childNodes[ j ]; 835 836 if (normalized === TRUE && 837 candidate.nodeType == 3 && 838 candidate.previousSibling && 839 candidate.previousSibling.nodeType == 3) { 840 continue; 841 } 842 843 currentIndex++; 844 845 if (currentIndex == target) { 846 $ = candidate; 847 break; 848 } 849 } 850 } 851 852 return $; 853 } 854 }; 855 856 857 function mergeElements(element, isNext) { 858 var sibling = element[isNext ? "next" : "prev"](undefined, 1); 859 860 if (sibling && sibling[0].nodeType == DOM.ELEMENT_NODE) { 861 862 // Jumping over bookmark nodes and empty inline elements, e.g. <b><i></i></b>, 863 // queuing them to be moved later. (#5567) 864 var pendingNodes = []; 865 866 while (sibling.attr('_ke_bookmark') || sibling._4e_isEmptyInlineRemovable(undefined)) { 867 pendingNodes.push(sibling); 868 sibling = isNext ? sibling.next(undefined, 1) : sibling.prev(undefined, 1); 869 if (!sibling) { 870 return; 871 } 872 } 873 874 if (element._4e_isIdentical(sibling, undefined)) { 875 // Save the last child to be checked too, to merge things like 876 // <b><i></i></b><b><i></i></b> => <b><i></i></b> 877 var innerSibling = new Node(isNext ? element[0].lastChild : element[0].firstChild); 878 879 // Move pending nodes first into the target element. 880 while (pendingNodes.length) { 881 pendingNodes.shift()._4e_move(element, !isNext, undefined); 882 } 883 884 sibling._4e_moveChildren(element, !isNext, undefined); 885 sibling.remove(); 886 887 // Now check the last inner child (see two comments above). 888 if (innerSibling[0] && innerSibling[0].nodeType == DOM.ELEMENT_NODE) { 889 innerSibling._4e_mergeSiblings(); 890 } 891 } 892 } 893 } 894 895 Utils.injectDom(editorDom); 896 }, { 897 requires:['./base', './utils'] 898 }); 899