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