2 * FCKeditor - The text editor for Internet - http://www.fckeditor.net
\r
3 * Copyright (C) 2003-2009 Frederico Caldeira Knabben
\r
5 * == BEGIN LICENSE ==
\r
7 * Licensed under the terms of any of the following licenses at your
\r
10 * - GNU General Public License Version 2 or later (the "GPL")
\r
11 * http://www.gnu.org/licenses/gpl.html
\r
13 * - GNU Lesser General Public License Version 2.1 or later (the "LGPL")
\r
14 * http://www.gnu.org/licenses/lgpl.html
\r
16 * - Mozilla Public License Version 1.1 or later (the "MPL")
\r
17 * http://www.mozilla.org/MPL/MPL-1.1.html
\r
21 * Implementation for the "Insert/Remove Ordered/Unordered List" commands.
\r
24 var FCKListCommand = function( name, tagName )
\r
27 this.TagName = tagName ;
\r
30 FCKListCommand.prototype =
\r
32 GetState : function()
\r
34 // Disabled if not WYSIWYG.
\r
35 if ( FCK.EditMode != FCK_EDITMODE_WYSIWYG || ! FCK.EditorWindow )
\r
36 return FCK_TRISTATE_DISABLED ;
\r
38 // We'll use the style system's convention to determine list state here...
\r
39 // If the starting block is a descendant of an <ol> or <ul> node, then we're in a list.
\r
40 var startContainer = FCKSelection.GetBoundaryParentElement( true ) ;
\r
41 var listNode = startContainer ;
\r
44 if ( listNode.nodeName.IEquals( [ 'ul', 'ol' ] ) )
\r
46 listNode = listNode.parentNode ;
\r
48 if ( listNode && listNode.nodeName.IEquals( this.TagName ) )
\r
49 return FCK_TRISTATE_ON ;
\r
51 return FCK_TRISTATE_OFF ;
\r
54 Execute : function()
\r
56 FCKUndo.SaveUndoStep() ;
\r
58 var doc = FCK.EditorDocument ;
\r
59 var range = new FCKDomRange( FCK.EditorWindow ) ;
\r
60 range.MoveToSelection() ;
\r
61 var state = this.GetState() ;
\r
63 // Midas lists rule #1 says we can create a list even in an empty document.
\r
64 // But FCKDomRangeIterator wouldn't run if the document is really empty.
\r
65 // So create a paragraph if the document is empty and we're going to create a list.
\r
66 if ( state == FCK_TRISTATE_OFF )
\r
68 FCKDomTools.TrimNode( doc.body ) ;
\r
69 if ( ! doc.body.firstChild )
\r
71 var paragraph = doc.createElement( 'p' ) ;
\r
72 doc.body.appendChild( paragraph ) ;
\r
73 range.MoveToNodeContents( paragraph ) ;
\r
77 var bookmark = range.CreateBookmark() ;
\r
79 // Group the blocks up because there are many cases where multiple lists have to be created,
\r
80 // or multiple lists have to be cancelled.
\r
81 var listGroups = [] ;
\r
82 var markerObj = {} ;
\r
83 var iterator = new FCKDomRangeIterator( range ) ;
\r
86 iterator.ForceBrBreak = ( state == FCK_TRISTATE_OFF ) ;
\r
87 var nextRangeExists = true ;
\r
88 var rangeQueue = null ;
\r
89 while ( nextRangeExists )
\r
91 while ( ( block = iterator.GetNextParagraph() ) )
\r
93 var path = new FCKElementPath( block ) ;
\r
94 var listNode = null ;
\r
95 var processedFlag = false ;
\r
96 var blockLimit = path.BlockLimit ;
\r
98 // First, try to group by a list ancestor.
\r
99 for ( var i = path.Elements.length - 1 ; i >= 0 ; i-- )
\r
101 var el = path.Elements[i] ;
\r
102 if ( el.nodeName.IEquals( ['ol', 'ul'] ) )
\r
104 // If we've encountered a list inside a block limit
\r
105 // The last group object of the block limit element should
\r
106 // no longer be valid. Since paragraphs after the list
\r
107 // should belong to a different group of paragraphs before
\r
108 // the list. (Bug #1309)
\r
109 if ( blockLimit._FCK_ListGroupObject )
\r
110 blockLimit._FCK_ListGroupObject = null ;
\r
112 var groupObj = el._FCK_ListGroupObject ;
\r
114 groupObj.contents.push( block ) ;
\r
117 groupObj = { 'root' : el, 'contents' : [ block ] } ;
\r
118 listGroups.push( groupObj ) ;
\r
119 FCKDomTools.SetElementMarker( markerObj, el, '_FCK_ListGroupObject', groupObj ) ;
\r
121 processedFlag = true ;
\r
126 if ( processedFlag )
\r
129 // No list ancestor? Group by block limit.
\r
130 var root = blockLimit ;
\r
131 if ( root._FCK_ListGroupObject )
\r
132 root._FCK_ListGroupObject.contents.push( block ) ;
\r
135 var groupObj = { 'root' : root, 'contents' : [ block ] } ;
\r
136 FCKDomTools.SetElementMarker( markerObj, root, '_FCK_ListGroupObject', groupObj ) ;
\r
137 listGroups.push( groupObj ) ;
\r
141 if ( FCKBrowserInfo.IsIE )
\r
142 nextRangeExists = false ;
\r
145 if ( rangeQueue == null )
\r
148 var selectionObject = FCKSelection.GetSelection() ;
\r
149 if ( selectionObject && listGroups.length == 0 )
\r
150 rangeQueue.push( selectionObject.getRangeAt( 0 ) ) ;
\r
151 for ( var i = 1 ; selectionObject && i < selectionObject.rangeCount ; i++ )
\r
152 rangeQueue.push( selectionObject.getRangeAt( i ) ) ;
\r
154 if ( rangeQueue.length < 1 )
\r
155 nextRangeExists = false ;
\r
158 var internalRange = FCKW3CRange.CreateFromRange( doc, rangeQueue.shift() ) ;
\r
159 range._Range = internalRange ;
\r
160 range._UpdateElementInfo() ;
\r
161 if ( range.StartNode.nodeName.IEquals( 'td' ) )
\r
162 range.SetStart( range.StartNode, 1 ) ;
\r
163 if ( range.EndNode.nodeName.IEquals( 'td' ) )
\r
164 range.SetEnd( range.EndNode, 2 ) ;
\r
165 iterator = new FCKDomRangeIterator( range ) ;
\r
166 iterator.ForceBrBreak = ( state == FCK_TRISTATE_OFF ) ;
\r
171 // Now we have two kinds of list groups, groups rooted at a list, and groups rooted at a block limit element.
\r
172 // We either have to build lists or remove lists, for removing a list does not makes sense when we are looking
\r
173 // at the group that's not rooted at lists. So we have three cases to handle.
\r
174 var listsCreated = [] ;
\r
175 while ( listGroups.length > 0 )
\r
177 var groupObj = listGroups.shift() ;
\r
178 if ( state == FCK_TRISTATE_OFF )
\r
180 if ( groupObj.root.nodeName.IEquals( ['ul', 'ol'] ) )
\r
181 this._ChangeListType( groupObj, markerObj, listsCreated ) ;
\r
183 this._CreateList( groupObj, listsCreated ) ;
\r
185 else if ( state == FCK_TRISTATE_ON && groupObj.root.nodeName.IEquals( ['ul', 'ol'] ) )
\r
186 this._RemoveList( groupObj, markerObj ) ;
\r
189 // For all new lists created, merge adjacent, same type lists.
\r
190 for ( var i = 0 ; i < listsCreated.length ; i++ )
\r
192 var listNode = listsCreated[i] ;
\r
193 var stopFlag = false ;
\r
194 var currentNode = listNode ;
\r
195 while ( ! stopFlag )
\r
197 currentNode = currentNode.nextSibling ;
\r
198 if ( currentNode && currentNode.nodeType == 3 && currentNode.nodeValue.search( /^[\n\r\t ]*$/ ) == 0 )
\r
203 if ( currentNode && currentNode.nodeName.IEquals( this.TagName ) )
\r
205 currentNode.parentNode.removeChild( currentNode ) ;
\r
206 while ( currentNode.firstChild )
\r
207 listNode.appendChild( currentNode.removeChild( currentNode.firstChild ) ) ;
\r
211 currentNode = listNode ;
\r
212 while ( ! stopFlag )
\r
214 currentNode = currentNode.previousSibling ;
\r
215 if ( currentNode && currentNode.nodeType == 3 && currentNode.nodeValue.search( /^[\n\r\t ]*$/ ) == 0 )
\r
219 if ( currentNode && currentNode.nodeName.IEquals( this.TagName ) )
\r
221 currentNode.parentNode.removeChild( currentNode ) ;
\r
222 while ( currentNode.lastChild )
\r
223 listNode.insertBefore( currentNode.removeChild( currentNode.lastChild ),
\r
224 listNode.firstChild ) ;
\r
228 // Clean up, restore selection and update toolbar button states.
\r
229 FCKDomTools.ClearAllMarkers( markerObj ) ;
\r
230 range.MoveToBookmark( bookmark ) ;
\r
234 FCK.Events.FireEvent( 'OnSelectionChange' ) ;
\r
237 _ChangeListType : function( groupObj, markerObj, listsCreated )
\r
239 // This case is easy...
\r
240 // 1. Convert the whole list into a one-dimensional array.
\r
241 // 2. Change the list type by modifying the array.
\r
242 // 3. Recreate the whole list by converting the array to a list.
\r
243 // 4. Replace the original list with the recreated list.
\r
244 var listArray = FCKDomTools.ListToArray( groupObj.root, markerObj ) ;
\r
245 var selectedListItems = [] ;
\r
246 for ( var i = 0 ; i < groupObj.contents.length ; i++ )
\r
248 var itemNode = groupObj.contents[i] ;
\r
249 itemNode = FCKTools.GetElementAscensor( itemNode, 'li' ) ;
\r
250 if ( ! itemNode || itemNode._FCK_ListItem_Processed )
\r
252 selectedListItems.push( itemNode ) ;
\r
253 FCKDomTools.SetElementMarker( markerObj, itemNode, '_FCK_ListItem_Processed', true ) ;
\r
255 var fakeParent = FCKTools.GetElementDocument( groupObj.root ).createElement( this.TagName ) ;
\r
256 for ( var i = 0 ; i < selectedListItems.length ; i++ )
\r
258 var listIndex = selectedListItems[i]._FCK_ListArray_Index ;
\r
259 listArray[listIndex].parent = fakeParent ;
\r
261 var newList = FCKDomTools.ArrayToList( listArray, markerObj ) ;
\r
262 for ( var i = 0 ; i < newList.listNode.childNodes.length ; i++ )
\r
264 if ( newList.listNode.childNodes[i].nodeName.IEquals( this.TagName ) )
\r
265 listsCreated.push( newList.listNode.childNodes[i] ) ;
\r
267 groupObj.root.parentNode.replaceChild( newList.listNode, groupObj.root ) ;
\r
270 _CreateList : function( groupObj, listsCreated )
\r
272 var contents = groupObj.contents ;
\r
273 var doc = FCKTools.GetElementDocument( groupObj.root ) ;
\r
274 var listContents = [] ;
\r
276 // It is possible to have the contents returned by DomRangeIterator to be the same as the root.
\r
277 // e.g. when we're running into table cells.
\r
278 // In such a case, enclose the childNodes of contents[0] into a <div>.
\r
279 if ( contents.length == 1 && contents[0] == groupObj.root )
\r
281 var divBlock = doc.createElement( 'div' );
\r
282 while ( contents[0].firstChild )
\r
283 divBlock.appendChild( contents[0].removeChild( contents[0].firstChild ) ) ;
\r
284 contents[0].appendChild( divBlock ) ;
\r
285 contents[0] = divBlock ;
\r
288 // Calculate the common parent node of all content blocks.
\r
289 var commonParent = groupObj.contents[0].parentNode ;
\r
290 for ( var i = 0 ; i < contents.length ; i++ )
\r
291 commonParent = FCKDomTools.GetCommonParents( commonParent, contents[i].parentNode ).pop() ;
\r
293 // We want to insert things that are in the same tree level only, so calculate the contents again
\r
294 // by expanding the selected blocks to the same tree level.
\r
295 for ( var i = 0 ; i < contents.length ; i++ )
\r
297 var contentNode = contents[i] ;
\r
298 while ( contentNode.parentNode )
\r
300 if ( contentNode.parentNode == commonParent )
\r
302 listContents.push( contentNode ) ;
\r
305 contentNode = contentNode.parentNode ;
\r
309 if ( listContents.length < 1 )
\r
312 // Insert the list to the DOM tree.
\r
313 var insertAnchor = listContents[listContents.length - 1].nextSibling ;
\r
314 var listNode = doc.createElement( this.TagName ) ;
\r
315 listsCreated.push( listNode ) ;
\r
316 while ( listContents.length )
\r
318 var contentBlock = listContents.shift() ;
\r
319 var docFrag = doc.createDocumentFragment() ;
\r
320 while ( contentBlock.firstChild )
\r
321 docFrag.appendChild( contentBlock.removeChild( contentBlock.firstChild ) ) ;
\r
322 contentBlock.parentNode.removeChild( contentBlock ) ;
\r
323 var listItem = doc.createElement( 'li' ) ;
\r
324 listItem.appendChild( docFrag ) ;
\r
325 listNode.appendChild( listItem ) ;
\r
327 commonParent.insertBefore( listNode, insertAnchor ) ;
\r
330 _RemoveList : function( groupObj, markerObj )
\r
332 // This is very much like the change list type operation.
\r
333 // Except that we're changing the selected items' indent to -1 in the list array.
\r
334 var listArray = FCKDomTools.ListToArray( groupObj.root, markerObj ) ;
\r
335 var selectedListItems = [] ;
\r
336 for ( var i = 0 ; i < groupObj.contents.length ; i++ )
\r
338 var itemNode = groupObj.contents[i] ;
\r
339 itemNode = FCKTools.GetElementAscensor( itemNode, 'li' ) ;
\r
340 if ( ! itemNode || itemNode._FCK_ListItem_Processed )
\r
342 selectedListItems.push( itemNode ) ;
\r
343 FCKDomTools.SetElementMarker( markerObj, itemNode, '_FCK_ListItem_Processed', true ) ;
\r
346 var lastListIndex = null ;
\r
347 for ( var i = 0 ; i < selectedListItems.length ; i++ )
\r
349 var listIndex = selectedListItems[i]._FCK_ListArray_Index ;
\r
350 listArray[listIndex].indent = -1 ;
\r
351 lastListIndex = listIndex ;
\r
354 // After cutting parts of the list out with indent=-1, we still have to maintain the array list
\r
355 // model's nextItem.indent <= currentItem.indent + 1 invariant. Otherwise the array model of the
\r
356 // list cannot be converted back to a real DOM list.
\r
357 for ( var i = lastListIndex + 1; i < listArray.length ; i++ )
\r
359 if ( listArray[i].indent > listArray[i-1].indent + 1 )
\r
361 var indentOffset = listArray[i-1].indent + 1 - listArray[i].indent ;
\r
362 var oldIndent = listArray[i].indent ;
\r
363 while ( listArray[i] && listArray[i].indent >= oldIndent)
\r
365 listArray[i].indent += indentOffset ;
\r
372 var newList = FCKDomTools.ArrayToList( listArray, markerObj ) ;
\r
373 // If groupObj.root is the last element in its parent, or its nextSibling is a <br>, then we should
\r
374 // not add a <br> after the final item. So, check for the cases and trim the <br>.
\r
375 if ( groupObj.root.nextSibling == null || groupObj.root.nextSibling.nodeName.IEquals( 'br' ) )
\r
377 if ( newList.listNode.lastChild.nodeName.IEquals( 'br' ) )
\r
378 newList.listNode.removeChild( newList.listNode.lastChild ) ;
\r
380 groupObj.root.parentNode.replaceChild( newList.listNode, groupObj.root ) ;
\r