1 | /*
|
---|
2 | * FCKeditor - The text editor for Internet - http://www.fckeditor.net
|
---|
3 | * Copyright (C) 2003-2009 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 |
|
---|
24 | var FCKListCommand = function( name, tagName )
|
---|
25 | {
|
---|
26 | this.Name = name ;
|
---|
27 | this.TagName = tagName ;
|
---|
28 | }
|
---|
29 |
|
---|
30 | FCKListCommand.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 = FCKSelection.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 = FCKTools.GetElementDocument( groupObj.root ).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 = FCKTools.GetElementDocument( groupObj.root ) ;
|
---|
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 | };
|
---|