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 * Implementation for the "Insert/Remove Ordered/Unordered List" commands. 22 */ 23 24var FCKListCommand = function( name, tagName ) 25{ 26 this.Name = name ; 27 this.TagName = tagName ; 28} 29 30FCKListCommand.prototype = 31{ 32 GetState : function() 33 { 34 // Disabled if not WYSIWYG. 35 if ( FCK.EditMode != FCK_EDITMODE_WYSIWYG || ! FCK.EditorWindow ) 36 return FCK_TRISTATE_DISABLED ; 37 38 // We'll use the style system's convention to determine list state here... 39 // If the starting block is a descendant of an <ol> or <ul> node, then we're in a list. 40 var startContainer = FCKSelection.GetBoundaryParentElement( true ) ; 41 var listNode = startContainer ; 42 while ( listNode ) 43 { 44 if ( listNode.nodeName.IEquals( [ 'ul', 'ol' ] ) ) 45 break ; 46 listNode = listNode.parentNode ; 47 } 48 if ( listNode && listNode.nodeName.IEquals( this.TagName ) ) 49 return FCK_TRISTATE_ON ; 50 else 51 return FCK_TRISTATE_OFF ; 52 }, 53 54 Execute : function() 55 { 56 FCKUndo.SaveUndoStep() ; 57 58 var doc = FCK.EditorDocument ; 59 var range = new FCKDomRange( FCK.EditorWindow ) ; 60 range.MoveToSelection() ; 61 var state = this.GetState() ; 62 63 // Midas lists rule #1 says we can create a list even in an empty document. 64 // But FCKDomRangeIterator wouldn't run if the document is really empty. 65 // So create a paragraph if the document is empty and we're going to create a list. 66 if ( state == FCK_TRISTATE_OFF ) 67 { 68 FCKDomTools.TrimNode( doc.body ) ; 69 if ( ! doc.body.firstChild ) 70 { 71 var paragraph = doc.createElement( 'p' ) ; 72 doc.body.appendChild( paragraph ) ; 73 range.MoveToNodeContents( paragraph ) ; 74 } 75 } 76 77 var bookmark = range.CreateBookmark() ; 78 79 // Group the blocks up because there are many cases where multiple lists have to be created, 80 // or multiple lists have to be cancelled. 81 var listGroups = [] ; 82 var markerObj = {} ; 83 var iterator = new FCKDomRangeIterator( range ) ; 84 var block ; 85 86 iterator.ForceBrBreak = ( state == FCK_TRISTATE_OFF ) ; 87 var nextRangeExists = true ; 88 var rangeQueue = null ; 89 while ( nextRangeExists ) 90 { 91 while ( ( block = iterator.GetNextParagraph() ) ) 92 { 93 var path = new FCKElementPath( block ) ; 94 var listNode = null ; 95 var processedFlag = false ; 96 var blockLimit = path.BlockLimit ; 97 98 // First, try to group by a list ancestor. 99 for ( var i = path.Elements.length - 1 ; i >= 0 ; i-- ) 100 { 101 var el = path.Elements[i] ; 102 if ( el.nodeName.IEquals( ['ol', 'ul'] ) ) 103 { 104 // If we've encountered a list inside a block limit 105 // The last group object of the block limit element should 106 // no longer be valid. Since paragraphs after the list 107 // should belong to a different group of paragraphs before 108 // the list. (Bug #1309) 109 if ( blockLimit._FCK_ListGroupObject ) 110 blockLimit._FCK_ListGroupObject = null ; 111 112 var groupObj = el._FCK_ListGroupObject ; 113 if ( groupObj ) 114 groupObj.contents.push( block ) ; 115 else 116 { 117 groupObj = { 'root' : el, 'contents' : [ block ] } ; 118 listGroups.push( groupObj ) ; 119 FCKDomTools.SetElementMarker( markerObj, el, '_FCK_ListGroupObject', groupObj ) ; 120 } 121 processedFlag = true ; 122 break ; 123 } 124 } 125 126 if ( processedFlag ) 127 continue ; 128 129 // No list ancestor? Group by block limit. 130 var root = blockLimit ; 131 if ( root._FCK_ListGroupObject ) 132 root._FCK_ListGroupObject.contents.push( block ) ; 133 else 134 { 135 var groupObj = { 'root' : root, 'contents' : [ block ] } ; 136 FCKDomTools.SetElementMarker( markerObj, root, '_FCK_ListGroupObject', groupObj ) ; 137 listGroups.push( groupObj ) ; 138 } 139 } 140 141 if ( FCKBrowserInfo.IsIE ) 142 nextRangeExists = false ; 143 else 144 { 145 if ( rangeQueue == null ) 146 { 147 rangeQueue = [] ; 148 var selectionObject = FCK.EditorWindow.getSelection() ; 149 if ( selectionObject && listGroups.length == 0 ) 150 rangeQueue.push( selectionObject.getRangeAt( 0 ) ) ; 151 for ( var i = 1 ; selectionObject && i < selectionObject.rangeCount ; i++ ) 152 rangeQueue.push( selectionObject.getRangeAt( i ) ) ; 153 } 154 if ( rangeQueue.length < 1 ) 155 nextRangeExists = false ; 156 else 157 { 158 var internalRange = FCKW3CRange.CreateFromRange( doc, rangeQueue.shift() ) ; 159 range._Range = internalRange ; 160 range._UpdateElementInfo() ; 161 if ( range.StartNode.nodeName.IEquals( 'td' ) ) 162 range.SetStart( range.StartNode, 1 ) ; 163 if ( range.EndNode.nodeName.IEquals( 'td' ) ) 164 range.SetEnd( range.EndNode, 2 ) ; 165 iterator = new FCKDomRangeIterator( range ) ; 166 iterator.ForceBrBreak = ( state == FCK_TRISTATE_OFF ) ; 167 } 168 } 169 } 170 171 // Now we have two kinds of list groups, groups rooted at a list, and groups rooted at a block limit element. 172 // We either have to build lists or remove lists, for removing a list does not makes sense when we are looking 173 // at the group that's not rooted at lists. So we have three cases to handle. 174 var listsCreated = [] ; 175 while ( listGroups.length > 0 ) 176 { 177 var groupObj = listGroups.shift() ; 178 if ( state == FCK_TRISTATE_OFF ) 179 { 180 if ( groupObj.root.nodeName.IEquals( ['ul', 'ol'] ) ) 181 this._ChangeListType( groupObj, markerObj, listsCreated ) ; 182 else 183 this._CreateList( groupObj, listsCreated ) ; 184 } 185 else if ( state == FCK_TRISTATE_ON && groupObj.root.nodeName.IEquals( ['ul', 'ol'] ) ) 186 this._RemoveList( groupObj, markerObj ) ; 187 } 188 189 // For all new lists created, merge adjacent, same type lists. 190 for ( var i = 0 ; i < listsCreated.length ; i++ ) 191 { 192 var listNode = listsCreated[i] ; 193 var stopFlag = false ; 194 var currentNode = listNode ; 195 while ( ! stopFlag ) 196 { 197 currentNode = currentNode.nextSibling ; 198 if ( currentNode && currentNode.nodeType == 3 && currentNode.nodeValue.search( /^[\n\r\t ]*$/ ) == 0 ) 199 continue ; 200 stopFlag = true ; 201 } 202 203 if ( currentNode && currentNode.nodeName.IEquals( this.TagName ) ) 204 { 205 currentNode.parentNode.removeChild( currentNode ) ; 206 while ( currentNode.firstChild ) 207 listNode.appendChild( currentNode.removeChild( currentNode.firstChild ) ) ; 208 } 209 210 stopFlag = false ; 211 currentNode = listNode ; 212 while ( ! stopFlag ) 213 { 214 currentNode = currentNode.previousSibling ; 215 if ( currentNode && currentNode.nodeType == 3 && currentNode.nodeValue.search( /^[\n\r\t ]*$/ ) == 0 ) 216 continue ; 217 stopFlag = true ; 218 } 219 if ( currentNode && currentNode.nodeName.IEquals( this.TagName ) ) 220 { 221 currentNode.parentNode.removeChild( currentNode ) ; 222 while ( currentNode.lastChild ) 223 listNode.insertBefore( currentNode.removeChild( currentNode.lastChild ), 224 listNode.firstChild ) ; 225 } 226 } 227 228 // Clean up, restore selection and update toolbar button states. 229 FCKDomTools.ClearAllMarkers( markerObj ) ; 230 range.MoveToBookmark( bookmark ) ; 231 range.Select() ; 232 233 FCK.Focus() ; 234 FCK.Events.FireEvent( 'OnSelectionChange' ) ; 235 }, 236 237 _ChangeListType : function( groupObj, markerObj, listsCreated ) 238 { 239 // This case is easy... 240 // 1. Convert the whole list into a one-dimensional array. 241 // 2. Change the list type by modifying the array. 242 // 3. Recreate the whole list by converting the array to a list. 243 // 4. Replace the original list with the recreated list. 244 var listArray = FCKDomTools.ListToArray( groupObj.root, markerObj ) ; 245 var selectedListItems = [] ; 246 for ( var i = 0 ; i < groupObj.contents.length ; i++ ) 247 { 248 var itemNode = groupObj.contents[i] ; 249 itemNode = FCKTools.GetElementAscensor( itemNode, 'li' ) ; 250 if ( ! itemNode || itemNode._FCK_ListItem_Processed ) 251 continue ; 252 selectedListItems.push( itemNode ) ; 253 FCKDomTools.SetElementMarker( markerObj, itemNode, '_FCK_ListItem_Processed', true ) ; 254 } 255 var fakeParent = groupObj.root.ownerDocument.createElement( this.TagName ) ; 256 for ( var i = 0 ; i < selectedListItems.length ; i++ ) 257 { 258 var listIndex = selectedListItems[i]._FCK_ListArray_Index ; 259 listArray[listIndex].parent = fakeParent ; 260 } 261 var newList = FCKDomTools.ArrayToList( listArray, markerObj ) ; 262 for ( var i = 0 ; i < newList.listNode.childNodes.length ; i++ ) 263 { 264 if ( newList.listNode.childNodes[i].nodeName.IEquals( this.TagName ) ) 265 listsCreated.push( newList.listNode.childNodes[i] ) ; 266 } 267 groupObj.root.parentNode.replaceChild( newList.listNode, groupObj.root ) ; 268 }, 269 270 _CreateList : function( groupObj, listsCreated ) 271 { 272 var contents = groupObj.contents ; 273 var doc = groupObj.root.ownerDocument ; 274 var listContents = [] ; 275 276 // It is possible to have the contents returned by DomRangeIterator to be the same as the root. 277 // e.g. when we're running into table cells. 278 // In such a case, enclose the childNodes of contents[0] into a <div>. 279 if ( contents.length == 1 && contents[0] == groupObj.root ) 280 { 281 var divBlock = doc.createElement( 'div' ); 282 while ( contents[0].firstChild ) 283 divBlock.appendChild( contents[0].removeChild( contents[0].firstChild ) ) ; 284 contents[0].appendChild( divBlock ) ; 285 contents[0] = divBlock ; 286 } 287 288 // Calculate the common parent node of all content blocks. 289 var commonParent = groupObj.contents[0].parentNode ; 290 for ( var i = 0 ; i < contents.length ; i++ ) 291 commonParent = FCKDomTools.GetCommonParents( commonParent, contents[i].parentNode ).pop() ; 292 293 // We want to insert things that are in the same tree level only, so calculate the contents again 294 // by expanding the selected blocks to the same tree level. 295 for ( var i = 0 ; i < contents.length ; i++ ) 296 { 297 var contentNode = contents[i] ; 298 while ( contentNode.parentNode ) 299 { 300 if ( contentNode.parentNode == commonParent ) 301 { 302 listContents.push( contentNode ) ; 303 break ; 304 } 305 contentNode = contentNode.parentNode ; 306 } 307 } 308 309 if ( listContents.length < 1 ) 310 return ; 311 312 // Insert the list to the DOM tree. 313 var insertAnchor = listContents[listContents.length - 1].nextSibling ; 314 var listNode = doc.createElement( this.TagName ) ; 315 listsCreated.push( listNode ) ; 316 while ( listContents.length ) 317 { 318 var contentBlock = listContents.shift() ; 319 var docFrag = doc.createDocumentFragment() ; 320 while ( contentBlock.firstChild ) 321 docFrag.appendChild( contentBlock.removeChild( contentBlock.firstChild ) ) ; 322 contentBlock.parentNode.removeChild( contentBlock ) ; 323 var listItem = doc.createElement( 'li' ) ; 324 listItem.appendChild( docFrag ) ; 325 listNode.appendChild( listItem ) ; 326 } 327 commonParent.insertBefore( listNode, insertAnchor ) ; 328 }, 329 330 _RemoveList : function( groupObj, markerObj ) 331 { 332 // This is very much like the change list type operation. 333 // Except that we're changing the selected items' indent to -1 in the list array. 334 var listArray = FCKDomTools.ListToArray( groupObj.root, markerObj ) ; 335 var selectedListItems = [] ; 336 for ( var i = 0 ; i < groupObj.contents.length ; i++ ) 337 { 338 var itemNode = groupObj.contents[i] ; 339 itemNode = FCKTools.GetElementAscensor( itemNode, 'li' ) ; 340 if ( ! itemNode || itemNode._FCK_ListItem_Processed ) 341 continue ; 342 selectedListItems.push( itemNode ) ; 343 FCKDomTools.SetElementMarker( markerObj, itemNode, '_FCK_ListItem_Processed', true ) ; 344 } 345 346 var lastListIndex = null ; 347 for ( var i = 0 ; i < selectedListItems.length ; i++ ) 348 { 349 var listIndex = selectedListItems[i]._FCK_ListArray_Index ; 350 listArray[listIndex].indent = -1 ; 351 lastListIndex = listIndex ; 352 } 353 354 // After cutting parts of the list out with indent=-1, we still have to maintain the array list 355 // model's nextItem.indent <= currentItem.indent + 1 invariant. Otherwise the array model of the 356 // list cannot be converted back to a real DOM list. 357 for ( var i = lastListIndex + 1; i < listArray.length ; i++ ) 358 { 359 if ( listArray[i].indent > listArray[i-1].indent + 1 ) 360 { 361 var indentOffset = listArray[i-1].indent + 1 - listArray[i].indent ; 362 var oldIndent = listArray[i].indent ; 363 while ( listArray[i] && listArray[i].indent >= oldIndent) 364 { 365 listArray[i].indent += indentOffset ; 366 i++ ; 367 } 368 i-- ; 369 } 370 } 371 372 var newList = FCKDomTools.ArrayToList( listArray, markerObj ) ; 373 // If groupObj.root is the last element in its parent, or its nextSibling is a <br>, then we should 374 // not add a <br> after the final item. So, check for the cases and trim the <br>. 375 if ( groupObj.root.nextSibling == null || groupObj.root.nextSibling.nodeName.IEquals( 'br' ) ) 376 { 377 if ( newList.listNode.lastChild.nodeName.IEquals( 'br' ) ) 378 newList.listNode.removeChild( newList.listNode.lastChild ) ; 379 } 380 groupObj.root.parentNode.replaceChild( newList.listNode, groupObj.root ) ; 381 } 382}; 383