import torrus 1.0.9
[freeside.git] / rt / share / html / NoAuth / RichText / FCKeditor / editor / _source / commandclasses / fcklistcommands.js
1 /*\r
2  * FCKeditor - The text editor for Internet - http://www.fckeditor.net\r
3  * Copyright (C) 2003-2009 Frederico Caldeira Knabben\r
4  *\r
5  * == BEGIN LICENSE ==\r
6  *\r
7  * Licensed under the terms of any of the following licenses at your\r
8  * choice:\r
9  *\r
10  *  - GNU General Public License Version 2 or later (the "GPL")\r
11  *    http://www.gnu.org/licenses/gpl.html\r
12  *\r
13  *  - GNU Lesser General Public License Version 2.1 or later (the "LGPL")\r
14  *    http://www.gnu.org/licenses/lgpl.html\r
15  *\r
16  *  - Mozilla Public License Version 1.1 or later (the "MPL")\r
17  *    http://www.mozilla.org/MPL/MPL-1.1.html\r
18  *\r
19  * == END LICENSE ==\r
20  *\r
21  * Implementation for the "Insert/Remove Ordered/Unordered List" commands.\r
22  */\r
23 \r
24 var FCKListCommand = function( name, tagName )\r
25 {\r
26         this.Name = name ;\r
27         this.TagName = tagName ;\r
28 }\r
29 \r
30 FCKListCommand.prototype =\r
31 {\r
32         GetState : function()\r
33         {\r
34                 // Disabled if not WYSIWYG.\r
35                 if ( FCK.EditMode != FCK_EDITMODE_WYSIWYG || ! FCK.EditorWindow )\r
36                         return FCK_TRISTATE_DISABLED ;\r
37 \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
42                 while ( listNode )\r
43                 {\r
44                         if ( listNode.nodeName.IEquals( [ 'ul', 'ol' ] ) )\r
45                                 break ;\r
46                         listNode = listNode.parentNode ;\r
47                 }\r
48                 if ( listNode && listNode.nodeName.IEquals( this.TagName ) )\r
49                         return FCK_TRISTATE_ON ;\r
50                 else\r
51                         return FCK_TRISTATE_OFF ;\r
52         },\r
53 \r
54         Execute : function()\r
55         {\r
56                 FCKUndo.SaveUndoStep() ;\r
57 \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
62 \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
67                 {\r
68                         FCKDomTools.TrimNode( doc.body ) ;\r
69                         if ( ! doc.body.firstChild )\r
70                         {\r
71                                 var paragraph = doc.createElement( 'p' ) ;\r
72                                 doc.body.appendChild( paragraph ) ;\r
73                                 range.MoveToNodeContents( paragraph ) ;\r
74                         }\r
75                 }\r
76 \r
77                 var bookmark = range.CreateBookmark() ;\r
78 \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
84                 var block ;\r
85 \r
86                 iterator.ForceBrBreak = ( state == FCK_TRISTATE_OFF ) ;\r
87                 var nextRangeExists = true ;\r
88                 var rangeQueue = null ;\r
89                 while ( nextRangeExists )\r
90                 {\r
91                         while ( ( block = iterator.GetNextParagraph() ) )\r
92                         {\r
93                                 var path = new FCKElementPath( block ) ;\r
94                                 var listNode = null ;\r
95                                 var processedFlag = false ;\r
96                                 var blockLimit = path.BlockLimit ;\r
97 \r
98                                 // First, try to group by a list ancestor.\r
99                                 for ( var i = path.Elements.length - 1 ; i >= 0 ; i-- )\r
100                                 {\r
101                                         var el = path.Elements[i] ;\r
102                                         if ( el.nodeName.IEquals( ['ol', 'ul'] ) )\r
103                                         {\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
111 \r
112                                                 var groupObj = el._FCK_ListGroupObject ;\r
113                                                 if ( groupObj )\r
114                                                         groupObj.contents.push( block ) ;\r
115                                                 else\r
116                                                 {\r
117                                                         groupObj = { 'root' : el, 'contents' : [ block ] } ;\r
118                                                         listGroups.push( groupObj ) ;\r
119                                                         FCKDomTools.SetElementMarker( markerObj, el, '_FCK_ListGroupObject', groupObj ) ;\r
120                                                 }\r
121                                                 processedFlag = true ;\r
122                                                 break ;\r
123                                         }\r
124                                 }\r
125 \r
126                                 if ( processedFlag )\r
127                                         continue ;\r
128 \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
133                                 else\r
134                                 {\r
135                                         var groupObj = { 'root' : root, 'contents' : [ block ] } ;\r
136                                         FCKDomTools.SetElementMarker( markerObj, root, '_FCK_ListGroupObject', groupObj ) ;\r
137                                         listGroups.push( groupObj ) ;\r
138                                 }\r
139                         }\r
140 \r
141                         if ( FCKBrowserInfo.IsIE )\r
142                                 nextRangeExists = false ;\r
143                         else\r
144                         {\r
145                                 if ( rangeQueue == null )\r
146                                 {\r
147                                         rangeQueue = [] ;\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
153                                 }\r
154                                 if ( rangeQueue.length < 1 )\r
155                                         nextRangeExists = false ;\r
156                                 else\r
157                                 {\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
167                                 }\r
168                         }\r
169                 }\r
170 \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
176                 {\r
177                         var groupObj = listGroups.shift() ;\r
178                         if ( state == FCK_TRISTATE_OFF )\r
179                         {\r
180                                 if ( groupObj.root.nodeName.IEquals( ['ul', 'ol'] ) )\r
181                                         this._ChangeListType( groupObj, markerObj, listsCreated ) ;\r
182                                 else\r
183                                         this._CreateList( groupObj, listsCreated ) ;\r
184                         }\r
185                         else if ( state == FCK_TRISTATE_ON && groupObj.root.nodeName.IEquals( ['ul', 'ol'] ) )\r
186                                 this._RemoveList( groupObj, markerObj ) ;\r
187                 }\r
188 \r
189                 // For all new lists created, merge adjacent, same type lists.\r
190                 for ( var i = 0 ; i < listsCreated.length ; i++ )\r
191                 {\r
192                         var listNode = listsCreated[i] ;\r
193                         var stopFlag = false ;\r
194                         var currentNode = listNode ;\r
195                         while ( ! stopFlag )\r
196                         {\r
197                                 currentNode = currentNode.nextSibling ;\r
198                                 if ( currentNode && currentNode.nodeType == 3 && currentNode.nodeValue.search( /^[\n\r\t ]*$/ ) == 0 )\r
199                                         continue ;\r
200                                 stopFlag = true ;\r
201                         }\r
202 \r
203                         if ( currentNode && currentNode.nodeName.IEquals( this.TagName ) )\r
204                         {\r
205                                 currentNode.parentNode.removeChild( currentNode ) ;\r
206                                 while ( currentNode.firstChild )\r
207                                         listNode.appendChild( currentNode.removeChild( currentNode.firstChild ) ) ;\r
208                         }\r
209 \r
210                         stopFlag = false ;\r
211                         currentNode = listNode ;\r
212                         while ( ! stopFlag )\r
213                         {\r
214                                 currentNode = currentNode.previousSibling ;\r
215                                 if ( currentNode && currentNode.nodeType == 3 && currentNode.nodeValue.search( /^[\n\r\t ]*$/ ) == 0 )\r
216                                         continue ;\r
217                                 stopFlag = true ;\r
218                         }\r
219                         if ( currentNode && currentNode.nodeName.IEquals( this.TagName ) )\r
220                         {\r
221                                 currentNode.parentNode.removeChild( currentNode ) ;\r
222                                 while ( currentNode.lastChild )\r
223                                         listNode.insertBefore( currentNode.removeChild( currentNode.lastChild ),\r
224                                                        listNode.firstChild ) ;\r
225                         }\r
226                 }\r
227 \r
228                 // Clean up, restore selection and update toolbar button states.\r
229                 FCKDomTools.ClearAllMarkers( markerObj ) ;\r
230                 range.MoveToBookmark( bookmark ) ;\r
231                 range.Select() ;\r
232 \r
233                 FCK.Focus() ;\r
234                 FCK.Events.FireEvent( 'OnSelectionChange' ) ;\r
235         },\r
236 \r
237         _ChangeListType : function( groupObj, markerObj, listsCreated )\r
238         {\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
247                 {\r
248                         var itemNode = groupObj.contents[i] ;\r
249                         itemNode = FCKTools.GetElementAscensor( itemNode, 'li' ) ;\r
250                         if ( ! itemNode || itemNode._FCK_ListItem_Processed )\r
251                                 continue ;\r
252                         selectedListItems.push( itemNode ) ;\r
253                         FCKDomTools.SetElementMarker( markerObj, itemNode, '_FCK_ListItem_Processed', true ) ;\r
254                 }\r
255                 var fakeParent = FCKTools.GetElementDocument( groupObj.root ).createElement( this.TagName ) ;\r
256                 for ( var i = 0 ; i < selectedListItems.length ; i++ )\r
257                 {\r
258                         var listIndex = selectedListItems[i]._FCK_ListArray_Index ;\r
259                         listArray[listIndex].parent = fakeParent ;\r
260                 }\r
261                 var newList = FCKDomTools.ArrayToList( listArray, markerObj ) ;\r
262                 for ( var i = 0 ; i < newList.listNode.childNodes.length ; i++ )\r
263                 {\r
264                         if ( newList.listNode.childNodes[i].nodeName.IEquals( this.TagName ) )\r
265                                 listsCreated.push( newList.listNode.childNodes[i] ) ;\r
266                 }\r
267                 groupObj.root.parentNode.replaceChild( newList.listNode, groupObj.root ) ;\r
268         },\r
269 \r
270         _CreateList : function( groupObj, listsCreated )\r
271         {\r
272                 var contents = groupObj.contents ;\r
273                 var doc = FCKTools.GetElementDocument( groupObj.root ) ;\r
274                 var listContents = [] ;\r
275 \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
280                 {\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
286                 }\r
287 \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
292 \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
296                 {\r
297                         var contentNode = contents[i] ;\r
298                         while ( contentNode.parentNode )\r
299                         {\r
300                                 if ( contentNode.parentNode == commonParent )\r
301                                 {\r
302                                         listContents.push( contentNode ) ;\r
303                                         break ;\r
304                                 }\r
305                                 contentNode = contentNode.parentNode ;\r
306                         }\r
307                 }\r
308 \r
309                 if ( listContents.length < 1 )\r
310                         return ;\r
311 \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
317                 {\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
326                 }\r
327                 commonParent.insertBefore( listNode, insertAnchor ) ;\r
328         },\r
329 \r
330         _RemoveList : function( groupObj, markerObj )\r
331         {\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
337                 {\r
338                         var itemNode = groupObj.contents[i] ;\r
339                         itemNode = FCKTools.GetElementAscensor( itemNode, 'li' ) ;\r
340                         if ( ! itemNode || itemNode._FCK_ListItem_Processed )\r
341                                 continue ;\r
342                         selectedListItems.push( itemNode ) ;\r
343                         FCKDomTools.SetElementMarker( markerObj, itemNode, '_FCK_ListItem_Processed', true ) ;\r
344                 }\r
345 \r
346                 var lastListIndex = null ;\r
347                 for ( var i = 0 ; i < selectedListItems.length ; i++ )\r
348                 {\r
349                         var listIndex = selectedListItems[i]._FCK_ListArray_Index ;\r
350                         listArray[listIndex].indent = -1 ;\r
351                         lastListIndex = listIndex ;\r
352                 }\r
353 \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
358                 {\r
359                         if ( listArray[i].indent > listArray[i-1].indent + 1 )\r
360                         {\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
364                                 {\r
365                                         listArray[i].indent += indentOffset ;\r
366                                         i++ ;\r
367                                 }\r
368                                 i-- ;\r
369                         }\r
370                 }\r
371 \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
376                 {\r
377                         if ( newList.listNode.lastChild.nodeName.IEquals( 'br' ) )\r
378                                 newList.listNode.removeChild( newList.listNode.lastChild ) ;\r
379                 }\r
380                 groupObj.root.parentNode.replaceChild( newList.listNode, groupObj.root ) ;\r
381         }\r
382 };\r