1/* 2 * FCKeditor - The text editor for Internet - http://www.fckeditor.net 3 * Copyright (C) 2003-2007 Frederico Caldeira Knabben 4 * 5 * == BEGIN LICENSE == 6 * 7 * Licensed under the terms of any of the following licenses at your 8 * choice: 9 * 10 * - GNU General Public License Version 2 or later (the "GPL") 11 * http://www.gnu.org/licenses/gpl.html 12 * 13 * - GNU Lesser General Public License Version 2.1 or later (the "LGPL") 14 * http://www.gnu.org/licenses/lgpl.html 15 * 16 * - Mozilla Public License Version 1.1 or later (the "MPL") 17 * http://www.mozilla.org/MPL/MPL-1.1.html 18 * 19 * == END LICENSE == 20 * 21 * Utility functions to work with the DOM. 22 */ 23 24var FCKDomTools = 25{ 26 MoveChildren : function( source, target, toTargetStart ) 27 { 28 if ( source == target ) 29 return ; 30 31 var eChild ; 32 33 if ( toTargetStart ) 34 { 35 while ( (eChild = source.lastChild) ) 36 target.insertBefore( source.removeChild( eChild ), target.firstChild ) ; 37 } 38 else 39 { 40 while ( (eChild = source.firstChild) ) 41 target.appendChild( source.removeChild( eChild ) ) ; 42 } 43 }, 44 45 MoveNode : function( source, target, toTargetStart ) 46 { 47 if ( toTargetStart ) 48 target.insertBefore( FCKDomTools.RemoveNode( source ), target.firstChild ) ; 49 else 50 target.appendChild( FCKDomTools.RemoveNode( source ) ) ; 51 }, 52 53 // Remove blank spaces from the beginning and the end of the contents of a node. 54 TrimNode : function( node ) 55 { 56 this.LTrimNode( node ) ; 57 this.RTrimNode( node ) ; 58 }, 59 60 LTrimNode : function( node ) 61 { 62 var eChildNode ; 63 64 while ( (eChildNode = node.firstChild) ) 65 { 66 if ( eChildNode.nodeType == 3 ) 67 { 68 var sTrimmed = eChildNode.nodeValue.LTrim() ; 69 var iOriginalLength = eChildNode.nodeValue.length ; 70 71 if ( sTrimmed.length == 0 ) 72 { 73 node.removeChild( eChildNode ) ; 74 continue ; 75 } 76 else if ( sTrimmed.length < iOriginalLength ) 77 { 78 eChildNode.splitText( iOriginalLength - sTrimmed.length ) ; 79 node.removeChild( node.firstChild ) ; 80 } 81 } 82 break ; 83 } 84 }, 85 86 RTrimNode : function( node ) 87 { 88 var eChildNode ; 89 90 while ( (eChildNode = node.lastChild) ) 91 { 92 if ( eChildNode.nodeType == 3 ) 93 { 94 var sTrimmed = eChildNode.nodeValue.RTrim() ; 95 var iOriginalLength = eChildNode.nodeValue.length ; 96 97 if ( sTrimmed.length == 0 ) 98 { 99 // If the trimmed text node is empty, just remove it. 100 101 // Use "eChildNode.parentNode" instead of "node" to avoid IE bug (#81). 102 eChildNode.parentNode.removeChild( eChildNode ) ; 103 continue ; 104 } 105 else if ( sTrimmed.length < iOriginalLength ) 106 { 107 // If the trimmed text length is less than the original 108 // length, strip all spaces from the end by splitting 109 // the text and removing the resulting useless node. 110 111 eChildNode.splitText( sTrimmed.length ) ; 112 // Use "node.lastChild.parentNode" instead of "node" to avoid IE bug (#81). 113 node.lastChild.parentNode.removeChild( node.lastChild ) ; 114 } 115 } 116 break ; 117 } 118 119 if ( !FCKBrowserInfo.IsIE ) 120 { 121 eChildNode = node.lastChild ; 122 123 if ( eChildNode && eChildNode.nodeType == 1 && eChildNode.nodeName.toLowerCase() == 'br' ) 124 { 125 // Use "eChildNode.parentNode" instead of "node" to avoid IE bug (#324). 126 eChildNode.parentNode.removeChild( eChildNode ) ; 127 } 128 } 129 }, 130 131 RemoveNode : function( node, excludeChildren ) 132 { 133 if ( excludeChildren ) 134 { 135 // Move all children before the node. 136 var eChild ; 137 while ( (eChild = node.firstChild) ) 138 node.parentNode.insertBefore( node.removeChild( eChild ), node ) ; 139 } 140 141 return node.parentNode.removeChild( node ) ; 142 }, 143 144 GetFirstChild : function( node, childNames ) 145 { 146 // If childNames is a string, transform it in a Array. 147 if ( typeof ( childNames ) == 'string' ) 148 childNames = [ childNames ] ; 149 150 var eChild = node.firstChild ; 151 while( eChild ) 152 { 153 if ( eChild.nodeType == 1 && eChild.tagName.Equals.apply( eChild.tagName, childNames ) ) 154 return eChild ; 155 156 eChild = eChild.nextSibling ; 157 } 158 159 return null ; 160 }, 161 162 GetLastChild : function( node, childNames ) 163 { 164 // If childNames is a string, transform it in a Array. 165 if ( typeof ( childNames ) == 'string' ) 166 childNames = [ childNames ] ; 167 168 var eChild = node.lastChild ; 169 while( eChild ) 170 { 171 if ( eChild.nodeType == 1 && ( !childNames || eChild.tagName.Equals( childNames ) ) ) 172 return eChild ; 173 174 eChild = eChild.previousSibling ; 175 } 176 177 return null ; 178 }, 179 180 /* 181 * Gets the previous element (nodeType=1) in the source order. Returns 182 * "null" If no element is found. 183 * @param {Object} currentNode The node to start searching from. 184 * @param {Boolean} ignoreSpaceTextOnly Sets how text nodes will be 185 * handled. If set to "true", only white spaces text nodes 186 * will be ignored, while non white space text nodes will stop 187 * the search, returning null. If "false" or omitted, all 188 * text nodes are ignored. 189 * @param {string[]} stopSearchElements An array of element names that 190 * will cause the search to stop when found, returning null. 191 * May be omitted (or null). 192 * @param {string[]} ignoreElements An array of element names that 193 * must be ignored during the search. 194 */ 195 GetPreviousSourceElement : function( currentNode, ignoreSpaceTextOnly, stopSearchElements, ignoreElements ) 196 { 197 if ( !currentNode ) 198 return null ; 199 200 if ( stopSearchElements && currentNode.nodeType == 1 && currentNode.nodeName.IEquals( stopSearchElements ) ) 201 return null ; 202 203 if ( currentNode.previousSibling ) 204 currentNode = currentNode.previousSibling ; 205 else 206 return this.GetPreviousSourceElement( currentNode.parentNode, ignoreSpaceTextOnly, stopSearchElements, ignoreElements ) ; 207 208 while ( currentNode ) 209 { 210 if ( currentNode.nodeType == 1 ) 211 { 212 if ( stopSearchElements && currentNode.nodeName.IEquals( stopSearchElements ) ) 213 break ; 214 215 if ( !ignoreElements || !currentNode.nodeName.IEquals( ignoreElements ) ) 216 return currentNode ; 217 } 218 else if ( ignoreSpaceTextOnly && currentNode.nodeType == 3 && currentNode.nodeValue.RTrim().length > 0 ) 219 break ; 220 221 if ( currentNode.lastChild ) 222 currentNode = currentNode.lastChild ; 223 else 224 return this.GetPreviousSourceElement( currentNode, ignoreSpaceTextOnly, stopSearchElements, ignoreElements ) ; 225 } 226 227 return null ; 228 }, 229 230 /* 231 * Gets the next element (nodeType=1) in the source order. Returns 232 * "null" If no element is found. 233 * @param {Object} currentNode The node to start searching from. 234 * @param {Boolean} ignoreSpaceTextOnly Sets how text nodes will be 235 * handled. If set to "true", only white spaces text nodes 236 * will be ignored, while non white space text nodes will stop 237 * the search, returning null. If "false" or omitted, all 238 * text nodes are ignored. 239 * @param {string[]} stopSearchElements An array of element names that 240 * will cause the search to stop when found, returning null. 241 * May be omitted (or null). 242 * @param {string[]} ignoreElements An array of element names that 243 * must be ignored during the search. 244 */ 245 GetNextSourceElement : function( currentNode, ignoreSpaceTextOnly, stopSearchElements, ignoreElements ) 246 { 247 while( ( currentNode = this.GetNextSourceNode( currentNode, true ) ) ) // Only one "=". 248 { 249 if ( currentNode.nodeType == 1 ) 250 { 251 if ( stopSearchElements && currentNode.nodeName.IEquals( stopSearchElements ) ) 252 break ; 253 254 if ( !ignoreElements || !currentNode.nodeName.IEquals( ignoreElements ) ) 255 return currentNode ; 256 } 257 else if ( ignoreSpaceTextOnly && currentNode.nodeType == 3 && currentNode.nodeValue.RTrim().length > 0 ) 258 break ; 259 } 260 261 return null ; 262 }, 263 264 /* 265 * Get the next DOM node available in source order. 266 */ 267 GetNextSourceNode : function( currentNode, startFromSibling, nodeType, stopSearchElement ) 268 { 269 if ( !currentNode ) 270 return null ; 271 272 var node ; 273 274 if ( !startFromSibling && currentNode.firstChild ) 275 node = currentNode.firstChild ; 276 else 277 { 278 node = currentNode.nextSibling ; 279 280 if ( !node && ( !stopSearchElement || stopSearchElement != currentNode.parentNode ) ) 281 return this.GetNextSourceNode( currentNode.parentNode, true, nodeType, stopSearchElement ) ; 282 } 283 284 if ( nodeType && node && node.nodeType != nodeType ) 285 return this.GetNextSourceNode( node, false, nodeType, stopSearchElement ) ; 286 287 return node ; 288 }, 289 290 /* 291 * Get the next DOM node available in source order. 292 */ 293 GetPreviousSourceNode : function( currentNode, startFromSibling, nodeType ) 294 { 295 if ( !currentNode ) 296 return null ; 297 298 var node ; 299 300 if ( !startFromSibling && currentNode.lastChild ) 301 node = currentNode.lastChild ; 302 else 303 node = ( currentNode.previousSibling || this.GetPreviousSourceNode( currentNode.parentNode, true, nodeType ) ) ; 304 305 if ( nodeType && node && node.nodeType != nodeType ) 306 return this.GetPreviousSourceNode( node, false, nodeType ) ; 307 308 return node ; 309 }, 310 311 // Inserts a element after a existing one. 312 InsertAfterNode : function( existingNode, newNode ) 313 { 314 return existingNode.parentNode.insertBefore( newNode, existingNode.nextSibling ) ; 315 }, 316 317 GetParents : function( node ) 318 { 319 var parents = new Array() ; 320 321 while ( node ) 322 { 323 parents.unshift( node ) ; 324 node = node.parentNode ; 325 } 326 327 return parents ; 328 }, 329 330 GetCommonParents : function( node1, node2 ) 331 { 332 var p1 = this.GetParents( node1 ) ; 333 var p2 = this.GetParents( node2 ) ; 334 var retval = [] ; 335 for ( var i = 0 ; i < p1.length ; i++ ) 336 { 337 if ( p1[i] == p2[i] ) 338 retval.push( p1[i] ) ; 339 } 340 return retval ; 341 }, 342 343 GetCommonParentNode : function( node1, node2, tagList ) 344 { 345 var tagMap = {} ; 346 if ( ! tagList.pop ) 347 tagList = [ tagList ] ; 348 while ( tagList.length > 0 ) 349 tagMap[tagList.pop().toLowerCase()] = 1 ; 350 351 var commonParents = this.GetCommonParents( node1, node2 ) ; 352 var currentParent = null ; 353 while ( ( currentParent = commonParents.pop() ) ) 354 { 355 if ( tagMap[currentParent.nodeName.toLowerCase()] ) 356 return currentParent ; 357 } 358 return null ; 359 }, 360 361 GetIndexOf : function( node ) 362 { 363 var currentNode = node.parentNode ? node.parentNode.firstChild : null ; 364 var currentIndex = -1 ; 365 366 while ( currentNode ) 367 { 368 currentIndex++ ; 369 370 if ( currentNode == node ) 371 return currentIndex ; 372 373 currentNode = currentNode.nextSibling ; 374 } 375 376 return -1 ; 377 }, 378 379 PaddingNode : null, 380 381 EnforcePaddingNode : function( doc, tagName ) 382 { 383 this.CheckAndRemovePaddingNode( doc, tagName, true ) ; 384 if ( doc.body.lastChild && ( doc.body.lastChild.nodeType != 1 385 || doc.body.lastChild.tagName.toLowerCase() == tagName.toLowerCase() ) ) 386 return ; 387 var node = doc.createElement( tagName ) ; 388 if ( FCKBrowserInfo.IsGecko && FCKListsLib.NonEmptyBlockElements[ tagName ] ) 389 FCKTools.AppendBogusBr( node ) ; 390 this.PaddingNode = node ; 391 if ( doc.body.childNodes.length == 1 392 && doc.body.firstChild.nodeType == 1 393 && doc.body.firstChild.tagName.toLowerCase() == 'br' 394 && ( doc.body.firstChild.getAttribute( '_moz_dirty' ) != null 395 || doc.body.firstChild.getAttribute( 'type' ) == '_moz' ) ) 396 doc.body.replaceChild( node, doc.body.firstChild ) ; 397 else 398 doc.body.appendChild( node ) ; 399 }, 400 401 CheckAndRemovePaddingNode : function( doc, tagName, dontRemove ) 402 { 403 var paddingNode = this.PaddingNode ; 404 if ( ! paddingNode ) 405 return ; 406 407 // If the padding node is changed, remove its status as a padding node. 408 if ( paddingNode.parentNode != doc.body 409 || paddingNode.tagName.toLowerCase() != tagName 410 || ( paddingNode.childNodes.length > 1 ) 411 || ( paddingNode.firstChild && paddingNode.firstChild.nodeValue != '\xa0' 412 && String(paddingNode.firstChild.tagName).toLowerCase() != 'br' ) ) 413 { 414 this.PaddingNode = null ; 415 return ; 416 } 417 418 // Now we're sure the padding node exists, and it is unchanged, and it 419 // isn't the only node in doc.body, remove it. 420 if ( !dontRemove ) 421 { 422 if ( paddingNode.parentNode.childNodes.length > 1 ) 423 paddingNode.parentNode.removeChild( paddingNode ) ; 424 this.PaddingNode = null ; 425 } 426 }, 427 428 HasAttribute : function( element, attributeName ) 429 { 430 if ( element.hasAttribute ) 431 return element.hasAttribute( attributeName ) ; 432 else 433 { 434 var att = element.attributes[ attributeName ] ; 435 return ( att != undefined && att.specified ) ; 436 } 437 }, 438 439 /** 440 * Checks if an element has "specified" attributes. 441 */ 442 HasAttributes : function( element ) 443 { 444 var attributes = element.attributes ; 445 446 for ( var i = 0 ; i < attributes.length ; i++ ) 447 { 448 if ( FCKBrowserInfo.IsIE && attributes[i].nodeName == 'class' ) 449 { 450 // IE has a strange bug. If calling removeAttribute('className'), 451 // the attributes collection will still contain the "class" 452 // attribute, which will be marked as "specified", even if the 453 // outerHTML of the element is not displaying the class attribute. 454 // Note : I was not able to reproduce it outside the editor, 455 // but I've faced it while working on the TC of #1391. 456 if ( element.className.length > 0 ) 457 return true ; 458 } 459 else if ( attributes[i].specified ) 460 return true ; 461 } 462 463 return false ; 464 }, 465 466 /** 467 * Remove an attribute from an element. 468 */ 469 RemoveAttribute : function( element, attributeName ) 470 { 471 if ( FCKBrowserInfo.IsIE && attributeName.toLowerCase() == 'class' ) 472 attributeName = 'className' ; 473 474 return element.removeAttribute( attributeName, 0 ) ; 475 }, 476 477 GetAttributeValue : function( element, att ) 478 { 479 var attName = att ; 480 481 if ( typeof att == 'string' ) 482 att = element.attributes[ att ] ; 483 else 484 attName = att.nodeName ; 485 486 if ( att && att.specified ) 487 { 488 // IE returns "null" for the nodeValue of a "style" attribute. 489 if ( attName == 'style' ) 490 return element.style.cssText ; 491 // There are two cases when the nodeValue must be used: 492 // - for the "class" attribute (all browsers). 493 // - for events attributes (IE only). 494 else if ( attName == 'class' || attName.indexOf('on') == 0 ) 495 return att.nodeValue ; 496 else 497 { 498 // Use getAttribute to get its value exactly as it is 499 // defined. 500 return element.getAttribute( attName, 2 ) ; 501 } 502 } 503 return null ; 504 }, 505 506 /** 507 * Checks whether one element contains the other. 508 */ 509 Contains : function( mainElement, otherElement ) 510 { 511 // IE supports contains, but only for element nodes. 512 if ( mainElement.contains && otherElement.nodeType == 1 ) 513 return mainElement.contains( otherElement ) ; 514 515 while ( ( otherElement = otherElement.parentNode ) ) // Only one "=" 516 { 517 if ( otherElement == mainElement ) 518 return true ; 519 } 520 return false ; 521 }, 522 523 /** 524 * Breaks a parent element in the position of one of its contained elements. 525 * For example, in the following case: 526 * <b>This <i>is some<span /> sample</i> test text</b> 527 * If element = <span />, we have these results: 528 * <b>This <i>is some</i><span /><i> sample</i> test text</b> (If parent = <i>) 529 * <b>This <i>is some</i></b><span /><b<i> sample</i> test text</b> (If parent = <b>) 530 */ 531 BreakParent : function( element, parent, reusableRange ) 532 { 533 var range = reusableRange || new FCKDomRange( FCKTools.GetElementWindow( element ) ) ; 534 535 // We'll be extracting part of this element, so let's use our 536 // range to get the correct piece. 537 range.SetStart( element, 4 ) ; 538 range.SetEnd( parent, 4 ) ; 539 540 // Extract it. 541 var docFrag = range.ExtractContents() ; 542 543 // Move the element outside the broken element. 544 range.InsertNode( element.parentNode.removeChild( element ) ) ; 545 546 // Re-insert the extracted piece after the element. 547 docFrag.InsertAfterNode( element ) ; 548 549 range.Release( !!reusableRange ) ; 550 }, 551 552 /** 553 * Retrieves a uniquely identifiable tree address of a DOM tree node. 554 * The tree address returns is an array of integers, with each integer 555 * indicating a child index from a DOM tree node, starting from 556 * document.documentElement. 557 * 558 * For example, assuming <body> is the second child from <html> (<head> 559 * being the first), and we'd like to address the third child under the 560 * fourth child of body, the tree address returned would be: 561 * [1, 3, 2] 562 * 563 * The tree address cannot be used for finding back the DOM tree node once 564 * the DOM tree structure has been modified. 565 */ 566 GetNodeAddress : function( node, normalized ) 567 { 568 var retval = [] ; 569 while ( node && node != node.ownerDocument.documentElement ) 570 { 571 var parentNode = node.parentNode ; 572 var currentIndex = -1 ; 573 for( var i = 0 ; i < parentNode.childNodes.length ; i++ ) 574 { 575 var candidate = parentNode.childNodes[i] ; 576 if ( normalized === true && 577 candidate.nodeType == 3 && 578 candidate.previousSibling && 579 candidate.previousSibling.nodeType == 3 ) 580 continue; 581 currentIndex++ ; 582 if ( parentNode.childNodes[i] == node ) 583 break ; 584 } 585 retval.unshift( currentIndex ) ; 586 node = node.parentNode ; 587 } 588 return retval ; 589 }, 590 591 /** 592 * The reverse transformation of FCKDomTools.GetNodeAddress(). This 593 * function returns the DOM node pointed to by its index address. 594 */ 595 GetNodeFromAddress : function( doc, addr, normalized ) 596 { 597 var cursor = doc.documentElement ; 598 for ( var i = 0 ; i < addr.length ; i++ ) 599 { 600 var target = addr[i] ; 601 if ( ! normalized ) 602 { 603 cursor = cursor.childNodes[target] ; 604 continue ; 605 } 606 607 var currentIndex = -1 ; 608 for (var j = 0 ; j < cursor.childNodes.length ; j++ ) 609 { 610 var candidate = cursor.childNodes[j] ; 611 if ( normalized === true && 612 candidate.nodeType == 3 && 613 candidate.previousSibling && 614 candidate.previousSibling.nodeType == 3 ) 615 continue ; 616 currentIndex++ ; 617 if ( currentIndex == target ) 618 { 619 cursor = candidate ; 620 break ; 621 } 622 } 623 } 624 return cursor ; 625 }, 626 627 CloneElement : function( element ) 628 { 629 element = element.cloneNode( false ) ; 630 631 // The "id" attribute should never be cloned to avoid duplication. 632 element.removeAttribute( 'id', false ) ; 633 634 return element ; 635 }, 636 637 ClearElementJSProperty : function( element, attrName ) 638 { 639 if ( FCKBrowserInfo.IsIE ) 640 element.removeAttribute( attrName ) ; 641 else 642 delete element[attrName] ; 643 }, 644 645 SetElementMarker : function ( markerObj, element, attrName, value) 646 { 647 var id = String( parseInt( Math.random() * 0xfffffff, 10 ) ) ; 648 element._FCKMarkerId = id ; 649 element[attrName] = value ; 650 if ( ! markerObj[id] ) 651 markerObj[id] = { 'element' : element, 'markers' : {} } ; 652 markerObj[id]['markers'][attrName] = value ; 653 }, 654 655 ClearElementMarkers : function( markerObj, element, clearMarkerObj ) 656 { 657 var id = element._FCKMarkerId ; 658 if ( ! id ) 659 return ; 660 this.ClearElementJSProperty( element, '_FCKMarkerId' ) ; 661 for ( var j in markerObj[id]['markers'] ) 662 this.ClearElementJSProperty( element, j ) ; 663 if ( clearMarkerObj ) 664 delete markerObj[id] ; 665 }, 666 667 ClearAllMarkers : function( markerObj ) 668 { 669 for ( var i in markerObj ) 670 this.ClearElementMarkers( markerObj, markerObj[i]['element'], true ) ; 671 }, 672 673 /** 674 * Convert a DOM list tree into a data structure that is easier to 675 * manipulate. This operation should be non-intrusive in the sense that it 676 * does not change the DOM tree, with the exception that it may add some 677 * markers to the list item nodes when markerObj is specified. 678 */ 679 ListToArray : function( listNode, markerObj, baseArray, baseIndentLevel, grandparentNode ) 680 { 681 if ( ! listNode.nodeName.IEquals( ['ul', 'ol'] ) ) 682 return [] ; 683 684 if ( ! baseIndentLevel ) 685 baseIndentLevel = 0 ; 686 if ( ! baseArray ) 687 baseArray = [] ; 688 // Iterate over all list items to get their contents and look for inner lists. 689 for ( var i = 0 ; i < listNode.childNodes.length ; i++ ) 690 { 691 var listItem = listNode.childNodes[i] ; 692 if ( ! listItem.nodeName.IEquals( 'li' ) ) 693 continue ; 694 var itemObj = { 'parent' : listNode, 'indent' : baseIndentLevel, 'contents' : [] } ; 695 if ( ! grandparentNode ) 696 { 697 itemObj.grandparent = listNode.parentNode ; 698 if ( itemObj.grandparent && itemObj.grandparent.nodeName.IEquals( 'li' ) ) 699 itemObj.grandparent = itemObj.grandparent.parentNode ; 700 } 701 else 702 itemObj.grandparent = grandparentNode ; 703 if ( markerObj ) 704 this.SetElementMarker( markerObj, listItem, '_FCK_ListArray_Index', baseArray.length ) ; 705 baseArray.push( itemObj ) ; 706 for ( var j = 0 ; j < listItem.childNodes.length ; j++ ) 707 { 708 var child = listItem.childNodes[j] ; 709 if ( child.nodeName.IEquals( ['ul', 'ol'] ) ) 710 // Note the recursion here, it pushes inner list items with 711 // +1 indentation in the correct order. 712 this.ListToArray( child, markerObj, baseArray, baseIndentLevel + 1, itemObj.grandparent ) ; 713 else 714 itemObj.contents.push( child ) ; 715 } 716 } 717 return baseArray ; 718 }, 719 720 // Convert our internal representation of a list back to a DOM forest. 721 ArrayToList : function( listArray, markerObj, baseIndex ) 722 { 723 if ( baseIndex == undefined ) 724 baseIndex = 0 ; 725 if ( ! listArray || listArray.length < baseIndex + 1 ) 726 return null ; 727 var doc = listArray[baseIndex].parent.ownerDocument ; 728 var retval = doc.createDocumentFragment() ; 729 var rootNode = null ; 730 var currentIndex = baseIndex ; 731 var indentLevel = Math.max( listArray[baseIndex].indent, 0 ) ; 732 var currentListItem = null ; 733 while ( true ) 734 { 735 var item = listArray[currentIndex] ; 736 if ( item.indent == indentLevel ) 737 { 738 if ( ! rootNode || listArray[currentIndex].parent.nodeName != rootNode.nodeName ) 739 { 740 rootNode = listArray[currentIndex].parent.cloneNode( false ) ; 741 retval.appendChild( rootNode ) ; 742 } 743 currentListItem = doc.createElement( 'li' ) ; 744 rootNode.appendChild( currentListItem ) ; 745 for ( var i = 0 ; i < item.contents.length ; i++ ) 746 currentListItem.appendChild( item.contents[i].cloneNode( true ) ) ; 747 currentIndex++ ; 748 } 749 else if ( item.indent == Math.max( indentLevel, 0 ) + 1 ) 750 { 751 var listData = this.ArrayToList( listArray, null, currentIndex ) ; 752 currentListItem.appendChild( listData.listNode ) ; 753 currentIndex = listData.nextIndex ; 754 } 755 else if ( item.indent == -1 && baseIndex == 0 && item.grandparent ) 756 { 757 var currentListItem ; 758 if ( item.grandparent.nodeName.IEquals( ['ul', 'ol'] ) ) 759 currentListItem = doc.createElement( 'li' ) ; 760 else 761 { 762 if ( FCKConfig.EnterMode.IEquals( ['div', 'p'] ) && ! item.grandparent.nodeName.IEquals( 'td' ) ) 763 currentListItem = doc.createElement( FCKConfig.EnterMode ) ; 764 else 765 currentListItem = doc.createDocumentFragment() ; 766 } 767 for ( var i = 0 ; i < item.contents.length ; i++ ) 768 currentListItem.appendChild( item.contents[i].cloneNode( true ) ) ; 769 if ( currentListItem.nodeType == 11 ) 770 { 771 if ( currentListItem.lastChild && 772 currentListItem.lastChild.getAttribute && 773 currentListItem.lastChild.getAttribute( 'type' ) == '_moz' ) 774 currentListItem.removeChild( currentListItem.lastChild ); 775 currentListItem.appendChild( doc.createElement( 'br' ) ) ; 776 } 777 if ( currentListItem.nodeName.IEquals( FCKConfig.EnterMode ) && currentListItem.firstChild ) 778 { 779 this.TrimNode( currentListItem ) ; 780 if ( FCKListsLib.BlockBoundaries[currentListItem.firstChild.nodeName.toLowerCase()] ) 781 { 782 var tmp = doc.createDocumentFragment() ; 783 while ( currentListItem.firstChild ) 784 tmp.appendChild( currentListItem.removeChild( currentListItem.firstChild ) ) ; 785 currentListItem = tmp ; 786 } 787 } 788 if ( FCKBrowserInfo.IsGeckoLike && currentListItem.nodeName.IEquals( ['div', 'p'] ) ) 789 FCKTools.AppendBogusBr( currentListItem ) ; 790 retval.appendChild( currentListItem ) ; 791 rootNode = null ; 792 currentIndex++ ; 793 } 794 else 795 return null ; 796 797 if ( listArray.length <= currentIndex || Math.max( listArray[currentIndex].indent, 0 ) < indentLevel ) 798 { 799 break ; 800 } 801 } 802 803 // Clear marker attributes for the new list tree made of cloned nodes, if any. 804 if ( markerObj ) 805 { 806 var currentNode = retval.firstChild ; 807 while ( currentNode ) 808 { 809 if ( currentNode.nodeType == 1 ) 810 this.ClearElementMarkers( markerObj, currentNode ) ; 811 currentNode = this.GetNextSourceNode( currentNode ) ; 812 } 813 } 814 815 return { 'listNode' : retval, 'nextIndex' : currentIndex } ; 816 }, 817 818 /** 819 * Get the next sibling node for a node. If "includeEmpties" is false, 820 * only element or non empty text nodes are returned. 821 */ 822 GetNextSibling : function( node, includeEmpties ) 823 { 824 node = node.nextSibling ; 825 826 while ( node && !includeEmpties && node.nodeType != 1 && ( node.nodeType != 3 || node.nodeValue.length == 0 ) ) 827 node = node.nextSibling ; 828 829 return node ; 830 }, 831 832 /** 833 * Get the previous sibling node for a node. If "includeEmpties" is false, 834 * only element or non empty text nodes are returned. 835 */ 836 GetPreviousSibling : function( node, includeEmpties ) 837 { 838 node = node.previousSibling ; 839 840 while ( node && !includeEmpties && node.nodeType != 1 && ( node.nodeType != 3 || node.nodeValue.length == 0 ) ) 841 node = node.previousSibling ; 842 843 return node ; 844 }, 845 846 /** 847 * Checks if an element has no "useful" content inside of it 848 * node tree. No "useful" content means empty text node or a signle empty 849 * inline node. 850 * elementCheckCallback may point to a function that returns a boolean 851 * indicating that a child element must be considered in the element check. 852 */ 853 CheckIsEmptyElement : function( element, elementCheckCallback ) 854 { 855 var child = element.firstChild ; 856 var elementChild ; 857 858 while ( child ) 859 { 860 if ( child.nodeType == 1 ) 861 { 862 if ( elementChild || !FCKListsLib.InlineNonEmptyElements[ child.nodeName.toLowerCase() ] ) 863 return false ; 864 865 if ( !elementCheckCallback || elementCheckCallback( child ) === true ) 866 elementChild = child ; 867 } 868 else if ( child.nodeType == 3 && child.nodeValue.length > 0 ) 869 return false ; 870 871 child = child.nextSibling ; 872 } 873 874 return elementChild ? this.CheckIsEmptyElement( elementChild, elementCheckCallback ) : true ; 875 }, 876 877 SetElementStyles : function( element, styleDict ) 878 { 879 var style = element.style ; 880 for ( var styleName in styleDict ) 881 style[ styleName ] = styleDict[ styleName ] ; 882 } 883} ; 884 885