text: ﻿/*
   * FCKeditor - The text editor for Internet - http://www.fckeditor.net Copyright
   * (C) 2003-2008 Frederico Caldeira Knabben == BEGIN LICENSE ==
   * 
   * Licensed under the terms of any of the following licenses at your choice: -
   * GNU General Public License Version 2 or later (the "GPL")
   * http://www.gnu.org/licenses/gpl.html - GNU Lesser General Public License
   * Version 2.1 or later (the "LGPL") http://www.gnu.org/licenses/lgpl.html -
   * Mozilla Public License Version 1.1 or later (the "MPL")
   * http://www.mozilla.org/MPL/MPL-1.1.html == END LICENSE ==
   * 
   * Implementation for the "Insert/Remove Ordered/Unordered List" commands.
   */
  
  var FCKListCommand = function(name, tagName) {
  	this.Name = name;
  	this.TagName = tagName;
  }
  
  FCKListCommand.prototype = {
  	GetState : function() {
  		// Disabled if not WYSIWYG.
  		if (FCK.EditMode != FCK_EDITMODE_WYSIWYG || !FCK.EditorWindow)
  			return FCK_TRISTATE_DISABLED;
  
  		// We'll use the style system's convention to determine list state
  		// here...
  		// If the starting block is a descendant of an <ol> or <ul> node, then
  		// we're in a list.
  		var startContainer = FCKSelection.GetBoundaryParentElement(true);
  		var listNode = startContainer;
  		while (listNode) {
  			if (listNode.nodeName.IEquals(['ul', 'ol']))
  				break;
  			listNode = listNode.parentNode;
  		}
  		if (listNode && listNode.nodeName.IEquals(this.TagName))
  			return FCK_TRISTATE_ON;
  		else
  			return FCK_TRISTATE_OFF;
  	},
  
  	Execute : function() {
  		FCKUndo.SaveUndoStep();
  
  		var doc = FCK.EditorDocument;
  		var range = new FCKDomRange(FCK.EditorWindow);
  		range.MoveToSelection();
  		var state = this.GetState();
  
  		// Midas lists rule #1 says we can create a list even in an empty
  		// document.
  		// But FCKDomRangeIterator wouldn't run if the document is really empty.
  		// So create a paragraph if the document is empty and we're going to
  		// create a list.
  		if (state == FCK_TRISTATE_OFF) {
  			FCKDomTools.TrimNode(doc.body);
  			if (!doc.body.firstChild) {
  				var paragraph = doc.createElement('p');
  				doc.body.appendChild(paragraph);
  				range.MoveToNodeContents(paragraph);
  			}
  		}
  
  		var bookmark = range.CreateBookmark();
  
  		// Group the blocks up because there are many cases where multiple lists
  		// have to be created,
  		// or multiple lists have to be cancelled.
  		var listGroups = [];
  		var markerObj = {};
  		var iterator = new FCKDomRangeIterator(range);
  		var block;
  
  		iterator.ForceBrBreak = (state == FCK_TRISTATE_OFF);
  		var nextRangeExists = true;
  		var rangeQueue = null;
  		while (nextRangeExists) {
  			while ((block = iterator.GetNextParagraph())) {
  				var path = new FCKElementPath(block);
  				var listNode = null;
  				var processedFlag = false;
  				var blockLimit = path.BlockLimit;
  
  				// First, try to group by a list ancestor.
  				for (var i = path.Elements.length - 1; i >= 0; i--) {
  					var el = path.Elements[i];
  					if (el.nodeName.IEquals(['ol', 'ul'])) {
  						// If we've encountered a list inside a block limit
  						// The last group object of the block limit element
  						// should
  						// no longer be valid. Since paragraphs after the list
  						// should belong to a different group of paragraphs
  						// before
  						// the list. (Bug #1309)
  						if (blockLimit._FCK_ListGroupObject)
  							blockLimit._FCK_ListGroupObject = null;
  
  						var groupObj = el._FCK_ListGroupObject;
  						if (groupObj)
  							groupObj.contents.push(block);
  						else {
  							groupObj = {
  								'root' : el,
  								'contents' : [block]
  							};
  							listGroups.push(groupObj);
  							FCKDomTools.SetElementMarker(markerObj, el,
  									'_FCK_ListGroupObject', groupObj);
  						}
  						processedFlag = true;
  						break;
  					}
  				}
  
  				if (processedFlag)
  					continue;
  
  				// No list ancestor? Group by block limit.
  				var root = blockLimit;
  				if (root._FCK_ListGroupObject)
  					root._FCK_ListGroupObject.contents.push(block);
  				else {
  					var groupObj = {
  						'root' : root,
  						'contents' : [block]
  					};
  					FCKDomTools.SetElementMarker(markerObj, root,
  							'_FCK_ListGroupObject', groupObj);
  					listGroups.push(groupObj);
  				}
  			}
  
  			if (FCKBrowserInfo.IsIE)
  				nextRangeExists = false;
  			else {
  				if (rangeQueue == null) {
  					rangeQueue = [];
  					var selectionObject = FCKSelection.GetSelection();
  					if (selectionObject && listGroups.length == 0)
  						rangeQueue.push(selectionObject.getRangeAt(0));
  					for (var i = 1; selectionObject
  							&& i < selectionObject.rangeCount; i++)
  						rangeQueue.push(selectionObject.getRangeAt(i));
  				}
  				if (rangeQueue.length < 1)
  					nextRangeExists = false;
  				else {
  					var internalRange = FCKW3CRange.CreateFromRange(doc,
  							rangeQueue.shift());
  					range._Range = internalRange;
  					range._UpdateElementInfo();
  					if (range.StartNode.nodeName.IEquals('td'))
  						range.SetStart(range.StartNode, 1);
  					if (range.EndNode.nodeName.IEquals('td'))
  						range.SetEnd(range.EndNode, 2);
  					iterator = new FCKDomRangeIterator(range);
  					iterator.ForceBrBreak = (state == FCK_TRISTATE_OFF);
  				}
  			}
  		}
  
  		// Now we have two kinds of list groups, groups rooted at a list, and
  		// groups rooted at a block limit element.
  		// We either have to build lists or remove lists, for removing a list
  		// does not makes sense when we are looking
  		// at the group that's not rooted at lists. So we have three cases to
  		// handle.
  		var listsCreated = [];
  		while (listGroups.length > 0) {
  			var groupObj = listGroups.shift();
  			if (state == FCK_TRISTATE_OFF) {
  				if (groupObj.root.nodeName.IEquals(['ul', 'ol']))
  					this._ChangeListType(groupObj, markerObj, listsCreated);
  				else
  					this._CreateList(groupObj, listsCreated);
  			} else if (state == FCK_TRISTATE_ON
  					&& groupObj.root.nodeName.IEquals(['ul', 'ol']))
  				this._RemoveList(groupObj, markerObj);
  		}
  
  		// For all new lists created, merge adjacent, same type lists.
  		for (var i = 0; i < listsCreated.length; i++) {
  			var listNode = listsCreated[i];
  			var stopFlag = false;
  			var currentNode = listNode;
  			while (!stopFlag) {
  				currentNode = currentNode.nextSibling;
  				if (currentNode && currentNode.nodeType == 3
  						&& currentNode.nodeValue.search(/^[\n\r\t ]*$/) == 0)
  					continue;
  				stopFlag = true;
  			}
  
  			if (currentNode && currentNode.nodeName.IEquals(this.TagName)) {
  				currentNode.parentNode.removeChild(currentNode);
  				while (currentNode.firstChild)
  					listNode.appendChild(currentNode
  							.removeChild(currentNode.firstChild));
  			}
  
  			stopFlag = false;
  			currentNode = listNode;
  			while (!stopFlag) {
  				currentNode = currentNode.previousSibling;
  				if (currentNode && currentNode.nodeType == 3
  						&& currentNode.nodeValue.search(/^[\n\r\t ]*$/) == 0)
  					continue;
  				stopFlag = true;
  			}
  			if (currentNode && currentNode.nodeName.IEquals(this.TagName)) {
  				currentNode.parentNode.removeChild(currentNode);
  				while (currentNode.lastChild)
  					listNode.insertBefore(currentNode
  									.removeChild(currentNode.lastChild),
  							listNode.firstChild);
  			}
  		}
  
  		// Clean up, restore selection and update toolbar button states.
  		FCKDomTools.ClearAllMarkers(markerObj);
  		range.MoveToBookmark(bookmark);
  		range.Select();
  
  		FCK.Focus();
  		FCK.Events.FireEvent('OnSelectionChange');
  	},
  
  	_ChangeListType : function(groupObj, markerObj, listsCreated) {
  		// This case is easy...
  		// 1. Convert the whole list into a one-dimensional array.
  		// 2. Change the list type by modifying the array.
  		// 3. Recreate the whole list by converting the array to a list.
  		// 4. Replace the original list with the recreated list.
  		var listArray = FCKDomTools.ListToArray(groupObj.root, markerObj);
  		var selectedListItems = [];
  		for (var i = 0; i < groupObj.contents.length; i++) {
  			var itemNode = groupObj.contents[i];
  			itemNode = FCKTools.GetElementAscensor(itemNode, 'li');
  			if (!itemNode || itemNode._FCK_ListItem_Processed)
  				continue;
  			selectedListItems.push(itemNode);
  			FCKDomTools.SetElementMarker(markerObj, itemNode,
  					'_FCK_ListItem_Processed', true);
  		}
  		var fakeParent = FCKTools.GetElementDocument(groupObj.root)
  				.createElement(this.TagName);
  		for (var i = 0; i < selectedListItems.length; i++) {
  			var listIndex = selectedListItems[i]._FCK_ListArray_Index;
  			listArray[listIndex].parent = fakeParent;
  		}
  		var newList = FCKDomTools.ArrayToList(listArray, markerObj);
  		for (var i = 0; i < newList.listNode.childNodes.length; i++) {
  			if (newList.listNode.childNodes[i].nodeName.IEquals(this.TagName))
  				listsCreated.push(newList.listNode.childNodes[i]);
  		}
  		groupObj.root.parentNode.replaceChild(newList.listNode, groupObj.root);
  	},
  
  	_CreateList : function(groupObj, listsCreated) {
  		var contents = groupObj.contents;
  		var doc = FCKTools.GetElementDocument(groupObj.root);
  		var listContents = [];
  
  		// It is possible to have the contents returned by DomRangeIterator to
  		// be the same as the root.
  		// e.g. when we're running into table cells.
  		// In such a case, enclose the childNodes of contents[0] into a <div>.
  		if (contents.length == 1 && contents[0] == groupObj.root) {
  			var divBlock = doc.createElement('div');
  			while (contents[0].firstChild)
  				divBlock.appendChild(contents[0]
  						.removeChild(contents[0].firstChild));
  			contents[0].appendChild(divBlock);
  			contents[0] = divBlock;
  		}
  
  		// Calculate the common parent node of all content blocks.
  		var commonParent = groupObj.contents[0].parentNode;
  		for (var i = 0; i < contents.length; i++)
  			commonParent = FCKDomTools.GetCommonParents(commonParent,
  					contents[i].parentNode).pop();
  
  		// We want to insert things that are in the same tree level only, so
  		// calculate the contents again
  		// by expanding the selected blocks to the same tree level.
  		for (var i = 0; i < contents.length; i++) {
  			var contentNode = contents[i];
  			while (contentNode.parentNode) {
  				if (contentNode.parentNode == commonParent) {
  					listContents.push(contentNode);
  					break;
  				}
  				contentNode = contentNode.parentNode;
  			}
  		}
  
  		if (listContents.length < 1)
  			return;
  
  		// Insert the list to the DOM tree.
  		var insertAnchor = listContents[listContents.length - 1].nextSibling;
  		var listNode = doc.createElement(this.TagName);
  		listsCreated.push(listNode);
  		while (listContents.length) {
  			var contentBlock = listContents.shift();
  			var docFrag = doc.createDocumentFragment();
  			while (contentBlock.firstChild)
  				docFrag.appendChild(contentBlock
  						.removeChild(contentBlock.firstChild));
  			contentBlock.parentNode.removeChild(contentBlock);
  			var listItem = doc.createElement('li');
  			listItem.appendChild(docFrag);
  			listNode.appendChild(listItem);
  		}
  		commonParent.insertBefore(listNode, insertAnchor);
  	},
  
  	_RemoveList : function(groupObj, markerObj) {
  		// This is very much like the change list type operation.
  		// Except that we're changing the selected items' indent to -1 in the
  		// list array.
  		var listArray = FCKDomTools.ListToArray(groupObj.root, markerObj);
  		var selectedListItems = [];
  		for (var i = 0; i < groupObj.contents.length; i++) {
  			var itemNode = groupObj.contents[i];
  			itemNode = FCKTools.GetElementAscensor(itemNode, 'li');
  			if (!itemNode || itemNode._FCK_ListItem_Processed)
  				continue;
  			selectedListItems.push(itemNode);
  			FCKDomTools.SetElementMarker(markerObj, itemNode,
  					'_FCK_ListItem_Processed', true);
  		}
  
  		var lastListIndex = null;
  		for (var i = 0; i < selectedListItems.length; i++) {
  			var listIndex = selectedListItems[i]._FCK_ListArray_Index;
  			listArray[listIndex].indent = -1;
  			lastListIndex = listIndex;
  		}
  
  		// After cutting parts of the list out with indent=-1, we still have to
  		// maintain the array list
  		// model's nextItem.indent <= currentItem.indent + 1 invariant.
  		// Otherwise the array model of the
  		// list cannot be converted back to a real DOM list.
  		for (var i = lastListIndex + 1; i < listArray.length; i++) {
  			if (listArray[i].indent > listArray[i - 1].indent + 1) {
  				var indentOffset = listArray[i - 1].indent + 1
  						- listArray[i].indent;
  				var oldIndent = listArray[i].indent;
  				while (listArray[i] && listArray[i].indent >= oldIndent) {
  					listArray[i].indent += indentOffset;
  					i++;
  				}
  				i--;
  			}
  		}
  
  		var newList = FCKDomTools.ArrayToList(listArray, markerObj);
  		// If groupObj.root is the last element in its parent, or its
  		// nextSibling is a <br>, then we should
  		// not add a <br> after the final item. So, check for the cases and trim
  		// the <br>.
  		if (groupObj.root.nextSibling == null
  				|| groupObj.root.nextSibling.nodeName.IEquals('br')) {
  			if (newList.listNode.lastChild.nodeName.IEquals('br'))
  				newList.listNode.removeChild(newList.listNode.lastChild);
  		}
  		groupObj.root.parentNode.replaceChild(newList.listNode, groupObj.root);
  	}
  };
  