JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
vanilla ckeditor-3.6.1
[ckeditor.git] / _source / plugins / tabletools / plugin.js
1 /*\r
2 Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.\r
3 For licensing, see LICENSE.html or http://ckeditor.com/license\r
4 */\r
5 \r
6 (function()\r
7 {\r
8         var cellNodeRegex = /^(?:td|th)$/;\r
9 \r
10         function getSelectedCells( selection )\r
11         {\r
12                 // Walker will try to split text nodes, which will make the current selection\r
13                 // invalid. So save bookmarks before doing anything.\r
14                 var bookmarks = selection.createBookmarks();\r
15 \r
16                 var ranges = selection.getRanges();\r
17                 var retval = [];\r
18                 var database = {};\r
19 \r
20                 function moveOutOfCellGuard( node )\r
21                 {\r
22                         // Apply to the first cell only.\r
23                         if ( retval.length > 0 )\r
24                                 return;\r
25 \r
26                         // If we are exiting from the first </td>, then the td should definitely be\r
27                         // included.\r
28                         if ( node.type == CKEDITOR.NODE_ELEMENT && cellNodeRegex.test( node.getName() )\r
29                                         && !node.getCustomData( 'selected_cell' ) )\r
30                         {\r
31                                 CKEDITOR.dom.element.setMarker( database, node, 'selected_cell', true );\r
32                                 retval.push( node );\r
33                         }\r
34                 }\r
35 \r
36                 for ( var i = 0 ; i < ranges.length ; i++ )\r
37                 {\r
38                         var range = ranges[ i ];\r
39 \r
40                         if ( range.collapsed )\r
41                         {\r
42                                 // Walker does not handle collapsed ranges yet - fall back to old API.\r
43                                 var startNode = range.getCommonAncestor();\r
44                                 var nearestCell = startNode.getAscendant( 'td', true ) || startNode.getAscendant( 'th', true );\r
45                                 if ( nearestCell )\r
46                                         retval.push( nearestCell );\r
47                         }\r
48                         else\r
49                         {\r
50                                 var walker = new CKEDITOR.dom.walker( range );\r
51                                 var node;\r
52                                 walker.guard = moveOutOfCellGuard;\r
53 \r
54                                 while ( ( node = walker.next() ) )\r
55                                 {\r
56                                         // If may be possible for us to have a range like this:\r
57                                         // <td>^1</td><td>^2</td>\r
58                                         // The 2nd td shouldn't be included.\r
59                                         //\r
60                                         // So we have to take care to include a td we've entered only when we've\r
61                                         // walked into its children.\r
62 \r
63                                         var parent = node.getAscendant( 'td' ) || node.getAscendant( 'th' );\r
64                                         if ( parent && !parent.getCustomData( 'selected_cell' ) )\r
65                                         {\r
66                                                 CKEDITOR.dom.element.setMarker( database, parent, 'selected_cell', true );\r
67                                                 retval.push( parent );\r
68                                         }\r
69                                 }\r
70                         }\r
71                 }\r
72 \r
73                 CKEDITOR.dom.element.clearAllMarkers( database );\r
74 \r
75                 // Restore selection position.\r
76                 selection.selectBookmarks( bookmarks );\r
77 \r
78                 return retval;\r
79         }\r
80 \r
81         function getFocusElementAfterDelCells( cellsToDelete ) {\r
82                 var i = 0,\r
83                         last = cellsToDelete.length - 1,\r
84                         database = {},\r
85                         cell,focusedCell,\r
86                         tr;\r
87 \r
88                 while ( ( cell = cellsToDelete[ i++ ] ) )\r
89                         CKEDITOR.dom.element.setMarker( database, cell, 'delete_cell', true );\r
90 \r
91                 // 1.first we check left or right side focusable cell row by row;\r
92                 i = 0;\r
93                 while ( ( cell = cellsToDelete[ i++ ] ) )\r
94                 {\r
95                         if ( ( focusedCell = cell.getPrevious() ) && !focusedCell.getCustomData( 'delete_cell' )\r
96                           || ( focusedCell = cell.getNext()     ) && !focusedCell.getCustomData( 'delete_cell' ) )\r
97                         {\r
98                                 CKEDITOR.dom.element.clearAllMarkers( database );\r
99                                 return focusedCell;\r
100                         }\r
101                 }\r
102 \r
103                 CKEDITOR.dom.element.clearAllMarkers( database );\r
104 \r
105                 // 2. then we check the toppest row (outside the selection area square) focusable cell\r
106                 tr = cellsToDelete[ 0 ].getParent();\r
107                 if ( ( tr = tr.getPrevious() ) )\r
108                         return tr.getLast();\r
109 \r
110                 // 3. last we check the lowerest  row focusable cell\r
111                 tr = cellsToDelete[ last ].getParent();\r
112                 if ( ( tr = tr.getNext() ) )\r
113                         return tr.getChild( 0 );\r
114 \r
115                 return null;\r
116         }\r
117 \r
118         function insertRow( selection, insertBefore )\r
119         {\r
120                 var cells = getSelectedCells( selection ),\r
121                                 firstCell = cells[ 0 ],\r
122                                 table = firstCell.getAscendant( 'table' ),\r
123                                 doc = firstCell.getDocument(),\r
124                                 startRow = cells[ 0 ].getParent(),\r
125                                 startRowIndex = startRow.$.rowIndex,\r
126                                 lastCell = cells[ cells.length - 1 ],\r
127                                 endRowIndex = lastCell.getParent().$.rowIndex + lastCell.$.rowSpan - 1,\r
128                                 endRow = new CKEDITOR.dom.element( table.$.rows[ endRowIndex ] ),\r
129                                 rowIndex = insertBefore ? startRowIndex : endRowIndex,\r
130                                 row = insertBefore ? startRow : endRow;\r
131 \r
132                 var map = CKEDITOR.tools.buildTableMap( table ),\r
133                                 cloneRow = map[ rowIndex ],\r
134                                 nextRow = insertBefore ? map[ rowIndex - 1 ] : map[ rowIndex + 1 ],\r
135                                 width = map[0].length;\r
136 \r
137                 var newRow = doc.createElement( 'tr' );\r
138                 for ( var i = 0; i < width; i++ )\r
139                 {\r
140                         var cell;\r
141                         // Check whether there's a spanning row here, do not break it.\r
142                         if ( cloneRow[ i ].rowSpan > 1 && nextRow && cloneRow[ i ] == nextRow[ i ] )\r
143                         {\r
144                                 cell = cloneRow[ i ];\r
145                                 cell.rowSpan += 1;\r
146                         }\r
147                         else\r
148                         {\r
149                                 cell = new CKEDITOR.dom.element( cloneRow[ i ] ).clone();\r
150                                 cell.removeAttribute( 'rowSpan' );\r
151                                 !CKEDITOR.env.ie && cell.appendBogus();\r
152                                 newRow.append( cell );\r
153                                 cell = cell.$;\r
154                         }\r
155 \r
156                         i += cell.colSpan - 1;\r
157                 }\r
158 \r
159                 insertBefore ?\r
160                 newRow.insertBefore( row ) :\r
161                 newRow.insertAfter( row );\r
162         }\r
163 \r
164         function deleteRows( selectionOrRow )\r
165         {\r
166                 if ( selectionOrRow instanceof CKEDITOR.dom.selection )\r
167                 {\r
168                         var cells = getSelectedCells( selectionOrRow ),\r
169                                         firstCell = cells[ 0 ],\r
170                                         table = firstCell.getAscendant( 'table' ),\r
171                                         map = CKEDITOR.tools.buildTableMap( table ),\r
172                                         startRow = cells[ 0 ].getParent(),\r
173                                         startRowIndex = startRow.$.rowIndex,\r
174                                         lastCell = cells[ cells.length - 1 ],\r
175                                         endRowIndex = lastCell.getParent().$.rowIndex + lastCell.$.rowSpan - 1,\r
176                                         rowsToDelete = [];\r
177 \r
178                         // Delete cell or reduce cell spans by checking through the table map.\r
179                         for ( var i = startRowIndex; i <= endRowIndex; i++ )\r
180                         {\r
181                                 var mapRow = map[ i ],\r
182                                                 row = new CKEDITOR.dom.element( table.$.rows[ i ] );\r
183 \r
184                                 for ( var j = 0; j < mapRow.length; j++ )\r
185                                 {\r
186                                         var cell = new CKEDITOR.dom.element( mapRow[ j ] ),\r
187                                                         cellRowIndex = cell.getParent().$.rowIndex;\r
188 \r
189                                         if ( cell.$.rowSpan == 1 )\r
190                                                 cell.remove();\r
191                                         // Row spanned cell.\r
192                                         else\r
193                                         {\r
194                                                 // Span row of the cell, reduce spanning.\r
195                                                 cell.$.rowSpan -= 1;\r
196                                                 // Root row of the cell, root cell to next row.\r
197                                                 if ( cellRowIndex == i )\r
198                                                 {\r
199                                                         var nextMapRow = map[ i + 1 ];\r
200                                                         nextMapRow[ j - 1 ] ?\r
201                                                         cell.insertAfter( new CKEDITOR.dom.element( nextMapRow[ j - 1 ] ) )\r
202                                                                         : new CKEDITOR.dom.element( table.$.rows[ i + 1 ] ).append( cell, 1 );\r
203                                                 }\r
204                                         }\r
205 \r
206                                         j += cell.$.colSpan - 1;\r
207                                 }\r
208 \r
209                                 rowsToDelete.push( row );\r
210                         }\r
211 \r
212                         var rows = table.$.rows;\r
213 \r
214                         // Where to put the cursor after rows been deleted?\r
215                         // 1. Into next sibling row if any;\r
216                         // 2. Into previous sibling row if any;\r
217                         // 3. Into table's parent element if it's the very last row.\r
218                         var cursorPosition =  new CKEDITOR.dom.element( rows[ endRowIndex + 1 ] || ( startRowIndex > 0 ? rows[  startRowIndex - 1 ] : null ) || table.$.parentNode );\r
219 \r
220                         for ( i = rowsToDelete.length ; i >= 0 ; i-- )\r
221                                 deleteRows( rowsToDelete[ i ] );\r
222 \r
223                         return cursorPosition;\r
224                 }\r
225                 else if ( selectionOrRow instanceof CKEDITOR.dom.element )\r
226                 {\r
227                         table = selectionOrRow.getAscendant( 'table' );\r
228 \r
229                         if ( table.$.rows.length == 1 )\r
230                                 table.remove();\r
231                         else\r
232                                 selectionOrRow.remove();\r
233                 }\r
234 \r
235                 return null;\r
236         }\r
237 \r
238         function getCellColIndex( cell, isStart )\r
239         {\r
240                 var row = cell.getParent(),\r
241                         rowCells = row.$.cells;\r
242 \r
243                 var colIndex = 0;\r
244                 for ( var i = 0; i < rowCells.length; i++ )\r
245                 {\r
246                         var mapCell = rowCells[ i ];\r
247                         colIndex += isStart ? 1 : mapCell.colSpan;\r
248                         if ( mapCell == cell.$ )\r
249                                 break;\r
250                 }\r
251 \r
252                 return colIndex -1;\r
253         }\r
254 \r
255         function getColumnsIndices( cells, isStart )\r
256         {\r
257                 var retval = isStart ? Infinity : 0;\r
258                 for ( var i = 0; i < cells.length; i++ )\r
259                 {\r
260                         var colIndex = getCellColIndex( cells[ i ], isStart );\r
261                         if ( isStart ? colIndex < retval  : colIndex > retval )\r
262                                 retval = colIndex;\r
263                 }\r
264                 return retval;\r
265         }\r
266 \r
267         function insertColumn( selection, insertBefore )\r
268         {\r
269                 var cells = getSelectedCells( selection ),\r
270                         firstCell = cells[ 0 ],\r
271                         table = firstCell.getAscendant( 'table' ),\r
272                         startCol =  getColumnsIndices( cells, 1 ),\r
273                         lastCol =  getColumnsIndices( cells ),\r
274                         colIndex = insertBefore? startCol : lastCol;\r
275 \r
276                 var map = CKEDITOR.tools.buildTableMap( table ),\r
277                         cloneCol = [],\r
278                         nextCol = [],\r
279                         height = map.length;\r
280 \r
281                 for ( var i = 0; i < height; i++ )\r
282                 {\r
283                         cloneCol.push( map[ i ][ colIndex ] );\r
284                         var nextCell = insertBefore ? map[ i ][ colIndex - 1 ] : map[ i ][ colIndex + 1 ];\r
285                         nextCell && nextCol.push( nextCell );\r
286                 }\r
287 \r
288                 for ( i = 0; i < height; i++ )\r
289                 {\r
290                         var cell;\r
291                         // Check whether there's a spanning column here, do not break it.\r
292                         if ( cloneCol[ i ].colSpan > 1\r
293                                 && nextCol.length\r
294                                 && nextCol[ i ] == cloneCol[ i ] )\r
295                         {\r
296                                 cell = cloneCol[ i ];\r
297                                 cell.colSpan += 1;\r
298                         }\r
299                         else\r
300                         {\r
301                                 cell = new CKEDITOR.dom.element( cloneCol[ i ] ).clone();\r
302                                 cell.removeAttribute( 'colSpan' );\r
303                                 !CKEDITOR.env.ie && cell.appendBogus();\r
304                                 cell[ insertBefore? 'insertBefore' : 'insertAfter' ].call( cell, new CKEDITOR.dom.element ( cloneCol[ i ] ) );\r
305                                 cell = cell.$;\r
306                         }\r
307 \r
308                         i += cell.rowSpan - 1;\r
309                 }\r
310         }\r
311 \r
312         function deleteColumns( selectionOrCell )\r
313         {\r
314                 var cells = getSelectedCells( selectionOrCell ),\r
315                                 firstCell = cells[ 0 ],\r
316                                 lastCell = cells[ cells.length - 1 ],\r
317                                 table = firstCell.getAscendant( 'table' ),\r
318                                 map = CKEDITOR.tools.buildTableMap( table ),\r
319                                 startColIndex,\r
320                                 endColIndex,\r
321                                 rowsToDelete = [];\r
322 \r
323                 // Figure out selected cells' column indices.\r
324                 for ( var i = 0, rows = map.length; i < rows; i++ )\r
325                 {\r
326                         for ( var j = 0, cols = map[ i ].length; j < cols; j++ )\r
327                         {\r
328                                 if ( map[ i ][ j ] == firstCell.$ )\r
329                                         startColIndex = j;\r
330                                 if ( map[ i ][ j ] == lastCell.$ )\r
331                                         endColIndex = j;\r
332                         }\r
333                 }\r
334 \r
335                 // Delete cell or reduce cell spans by checking through the table map.\r
336                 for ( i = startColIndex; i <= endColIndex; i++ )\r
337                 {\r
338                         for ( j = 0; j < map.length; j++ )\r
339                         {\r
340                                 var mapRow = map[ j ],\r
341                                         row = new CKEDITOR.dom.element( table.$.rows[ j ] ),\r
342                                         cell = new CKEDITOR.dom.element( mapRow[ i ] );\r
343 \r
344                                 if ( cell.$ )\r
345                                 {\r
346                                         if ( cell.$.colSpan == 1 )\r
347                                                 cell.remove();\r
348                                         // Reduce the col spans.\r
349                                         else\r
350                                                 cell.$.colSpan -= 1;\r
351 \r
352                                         j += cell.$.rowSpan - 1;\r
353 \r
354                                         if ( !row.$.cells.length )\r
355                                                 rowsToDelete.push( row );\r
356                                 }\r
357                         }\r
358                 }\r
359 \r
360                 var firstRowCells = table.$.rows[ 0 ] && table.$.rows[ 0 ].cells;\r
361 \r
362                 // Where to put the cursor after columns been deleted?\r
363                 // 1. Into next cell of the first row if any;\r
364                 // 2. Into previous cell of the first row if any;\r
365                 // 3. Into table's parent element;\r
366                 var cursorPosition =  new CKEDITOR.dom.element( firstRowCells[ startColIndex ] || ( startColIndex ? firstRowCells[ startColIndex - 1 ] : table.$.parentNode ) );\r
367 \r
368                 // Delete table rows only if all columns are gone (do not remove empty row).\r
369                 if ( rowsToDelete.length == rows )\r
370                         table.remove();\r
371 \r
372                 return cursorPosition;\r
373         }\r
374 \r
375         function getFocusElementAfterDelCols( cells )\r
376         {\r
377                 var cellIndexList = [],\r
378                         table = cells[ 0 ] && cells[ 0 ].getAscendant( 'table' ),\r
379                         i, length,\r
380                         targetIndex, targetCell;\r
381 \r
382                 // get the cellIndex list of delete cells\r
383                 for ( i = 0, length = cells.length; i < length; i++ )\r
384                         cellIndexList.push( cells[i].$.cellIndex );\r
385 \r
386                 // get the focusable column index\r
387                 cellIndexList.sort();\r
388                 for ( i = 1, length = cellIndexList.length; i < length; i++ )\r
389                 {\r
390                         if ( cellIndexList[ i ] - cellIndexList[ i - 1 ] > 1 )\r
391                         {\r
392                                 targetIndex = cellIndexList[ i - 1 ] + 1;\r
393                                 break;\r
394                         }\r
395                 }\r
396 \r
397                 if ( !targetIndex )\r
398                         targetIndex = cellIndexList[ 0 ] > 0 ? ( cellIndexList[ 0 ] - 1 )\r
399                                                         : ( cellIndexList[ cellIndexList.length - 1 ] + 1 );\r
400 \r
401                 // scan row by row to get the target cell\r
402                 var rows = table.$.rows;\r
403                 for ( i = 0, length = rows.length; i < length ; i++ )\r
404                 {\r
405                         targetCell = rows[ i ].cells[ targetIndex ];\r
406                         if ( targetCell )\r
407                                 break;\r
408                 }\r
409 \r
410                 return targetCell ?  new CKEDITOR.dom.element( targetCell ) :  table.getPrevious();\r
411         }\r
412 \r
413         function insertCell( selection, insertBefore )\r
414         {\r
415                 var startElement = selection.getStartElement();\r
416                 var cell = startElement.getAscendant( 'td', 1 ) || startElement.getAscendant( 'th', 1 );\r
417 \r
418                 if ( !cell )\r
419                         return;\r
420 \r
421                 // Create the new cell element to be added.\r
422                 var newCell = cell.clone();\r
423                 if ( !CKEDITOR.env.ie )\r
424                         newCell.appendBogus();\r
425 \r
426                 if ( insertBefore )\r
427                         newCell.insertBefore( cell );\r
428                 else\r
429                         newCell.insertAfter( cell );\r
430         }\r
431 \r
432         function deleteCells( selectionOrCell )\r
433         {\r
434                 if ( selectionOrCell instanceof CKEDITOR.dom.selection )\r
435                 {\r
436                         var cellsToDelete = getSelectedCells( selectionOrCell );\r
437                         var table = cellsToDelete[ 0 ] && cellsToDelete[ 0 ].getAscendant( 'table' );\r
438                         var cellToFocus   = getFocusElementAfterDelCells( cellsToDelete );\r
439 \r
440                         for ( var i = cellsToDelete.length - 1 ; i >= 0 ; i-- )\r
441                                 deleteCells( cellsToDelete[ i ] );\r
442 \r
443                         if ( cellToFocus )\r
444                                 placeCursorInCell( cellToFocus, true );\r
445                         else if ( table )\r
446                                 table.remove();\r
447                 }\r
448                 else if ( selectionOrCell instanceof CKEDITOR.dom.element )\r
449                 {\r
450                         var tr = selectionOrCell.getParent();\r
451                         if ( tr.getChildCount() == 1 )\r
452                                 tr.remove();\r
453                         else\r
454                                 selectionOrCell.remove();\r
455                 }\r
456         }\r
457 \r
458         // Remove filler at end and empty spaces around the cell content.\r
459         function trimCell( cell )\r
460         {\r
461                 var bogus = cell.getBogus();\r
462                 bogus && bogus.remove();\r
463                 cell.trim();\r
464         }\r
465 \r
466         function placeCursorInCell( cell, placeAtEnd )\r
467         {\r
468                 var range = new CKEDITOR.dom.range( cell.getDocument() );\r
469                 if ( !range[ 'moveToElementEdit' + ( placeAtEnd ? 'End' : 'Start' ) ]( cell ) )\r
470                 {\r
471                         range.selectNodeContents( cell );\r
472                         range.collapse( placeAtEnd ? false : true );\r
473                 }\r
474                 range.select( true );\r
475         }\r
476 \r
477         function cellInRow( tableMap, rowIndex, cell )\r
478         {\r
479                 var oRow = tableMap[ rowIndex ];\r
480                 if ( typeof cell == 'undefined' )\r
481                         return oRow;\r
482 \r
483                 for ( var c = 0 ; oRow && c < oRow.length ; c++ )\r
484                 {\r
485                         if ( cell.is && oRow[c] == cell.$ )\r
486                                 return c;\r
487                         else if ( c == cell )\r
488                                 return new CKEDITOR.dom.element( oRow[ c ] );\r
489                 }\r
490                 return cell.is ? -1 : null;\r
491         }\r
492 \r
493         function cellInCol( tableMap, colIndex, cell )\r
494         {\r
495                 var oCol = [];\r
496                 for ( var r = 0; r < tableMap.length; r++ )\r
497                 {\r
498                         var row = tableMap[ r ];\r
499                         if ( typeof cell == 'undefined' )\r
500                                 oCol.push( row[ colIndex ] );\r
501                         else if ( cell.is && row[ colIndex ] == cell.$ )\r
502                                 return r;\r
503                         else if ( r == cell )\r
504                                 return new CKEDITOR.dom.element( row[ colIndex ] );\r
505                 }\r
506 \r
507                 return ( typeof cell == 'undefined' )? oCol : cell.is ? -1 :  null;\r
508         }\r
509 \r
510         function mergeCells( selection, mergeDirection, isDetect )\r
511         {\r
512                 var cells = getSelectedCells( selection );\r
513 \r
514                 // Invalid merge request if:\r
515                 // 1. In batch mode despite that less than two selected.\r
516                 // 2. In solo mode while not exactly only one selected.\r
517                 // 3. Cells distributed in different table groups (e.g. from both thead and tbody).\r
518                 var commonAncestor;\r
519                 if ( ( mergeDirection ? cells.length != 1 : cells.length < 2 )\r
520                                 || ( commonAncestor = selection.getCommonAncestor() )\r
521                                 && commonAncestor.type == CKEDITOR.NODE_ELEMENT\r
522                                 && commonAncestor.is( 'table' ) )\r
523                 {\r
524                         return false;\r
525                 }\r
526 \r
527                 var     cell,\r
528                         firstCell = cells[ 0 ],\r
529                         table = firstCell.getAscendant( 'table' ),\r
530                         map = CKEDITOR.tools.buildTableMap( table ),\r
531                         mapHeight = map.length,\r
532                         mapWidth = map[ 0 ].length,\r
533                         startRow = firstCell.getParent().$.rowIndex,\r
534                         startColumn = cellInRow( map, startRow, firstCell );\r
535 \r
536                 if ( mergeDirection )\r
537                 {\r
538                         var targetCell;\r
539                         try\r
540                         {\r
541                                 var rowspan = parseInt( firstCell.getAttribute( 'rowspan' ), 10 ) || 1;\r
542                                 var colspan = parseInt( firstCell.getAttribute( 'colspan' ), 10 ) || 1;\r
543 \r
544                                 targetCell =\r
545                                         map[ mergeDirection == 'up' ?\r
546                                                         ( startRow - rowspan ):\r
547                                                         mergeDirection == 'down' ? ( startRow + rowspan ) : startRow  ] [\r
548                                                 mergeDirection == 'left' ?\r
549                                                         ( startColumn - colspan ):\r
550                                                 mergeDirection == 'right' ?  ( startColumn + colspan ) : startColumn ];\r
551 \r
552                         }\r
553                         catch( er )\r
554                         {\r
555                                 return false;\r
556                         }\r
557 \r
558                         // 1. No cell could be merged.\r
559                         // 2. Same cell actually.\r
560                         if ( !targetCell || firstCell.$ == targetCell  )\r
561                                 return false;\r
562 \r
563                         // Sort in map order regardless of the DOM sequence.\r
564                         cells[ ( mergeDirection == 'up' || mergeDirection == 'left' ) ?\r
565                                  'unshift' : 'push' ]( new CKEDITOR.dom.element( targetCell ) );\r
566                 }\r
567 \r
568                 // Start from here are merging way ignorance (merge up/right, batch merge).\r
569                 var     doc = firstCell.getDocument(),\r
570                         lastRowIndex = startRow,\r
571                         totalRowSpan = 0,\r
572                         totalColSpan = 0,\r
573                         // Use a documentFragment as buffer when appending cell contents.\r
574                         frag = !isDetect && new CKEDITOR.dom.documentFragment( doc ),\r
575                         dimension = 0;\r
576 \r
577                 for ( var i = 0; i < cells.length; i++ )\r
578                 {\r
579                         cell = cells[ i ];\r
580 \r
581                         var tr = cell.getParent(),\r
582                                 cellFirstChild = cell.getFirst(),\r
583                                 colSpan = cell.$.colSpan,\r
584                                 rowSpan = cell.$.rowSpan,\r
585                                 rowIndex = tr.$.rowIndex,\r
586                                 colIndex = cellInRow( map, rowIndex, cell );\r
587 \r
588                         // Accumulated the actual places taken by all selected cells.\r
589                         dimension += colSpan * rowSpan;\r
590                         // Accumulated the maximum virtual spans from column and row.\r
591                         totalColSpan = Math.max( totalColSpan, colIndex - startColumn + colSpan ) ;\r
592                         totalRowSpan = Math.max( totalRowSpan, rowIndex - startRow + rowSpan );\r
593 \r
594                         if ( !isDetect )\r
595                         {\r
596                                 // Trim all cell fillers and check to remove empty cells.\r
597                                 if ( trimCell( cell ), cell.getChildren().count() )\r
598                                 {\r
599                                         // Merge vertically cells as two separated paragraphs.\r
600                                         if ( rowIndex != lastRowIndex\r
601                                                 && cellFirstChild\r
602                                                 && !( cellFirstChild.isBlockBoundary\r
603                                                           && cellFirstChild.isBlockBoundary( { br : 1 } ) ) )\r
604                                         {\r
605                                                 var last = frag.getLast( CKEDITOR.dom.walker.whitespaces( true ) );\r
606                                                 if ( last && !( last.is && last.is( 'br' ) ) )\r
607                                                         frag.append( 'br' );\r
608                                         }\r
609 \r
610                                         cell.moveChildren( frag );\r
611                                 }\r
612                                 i ? cell.remove() : cell.setHtml( '' );\r
613                         }\r
614                         lastRowIndex = rowIndex;\r
615                 }\r
616 \r
617                 if ( !isDetect )\r
618                 {\r
619                         frag.moveChildren( firstCell );\r
620 \r
621                         if ( !CKEDITOR.env.ie )\r
622                                 firstCell.appendBogus();\r
623 \r
624                         if ( totalColSpan >= mapWidth )\r
625                                 firstCell.removeAttribute( 'rowSpan' );\r
626                         else\r
627                                 firstCell.$.rowSpan = totalRowSpan;\r
628 \r
629                         if ( totalRowSpan >= mapHeight )\r
630                                 firstCell.removeAttribute( 'colSpan' );\r
631                         else\r
632                                 firstCell.$.colSpan = totalColSpan;\r
633 \r
634                         // Swip empty <tr> left at the end of table due to the merging.\r
635                         var trs = new CKEDITOR.dom.nodeList( table.$.rows ),\r
636                                 count = trs.count();\r
637 \r
638                         for ( i = count - 1; i >= 0; i-- )\r
639                         {\r
640                                 var tailTr = trs.getItem( i );\r
641                                 if ( !tailTr.$.cells.length )\r
642                                 {\r
643                                         tailTr.remove();\r
644                                         count++;\r
645                                         continue;\r
646                                 }\r
647                         }\r
648 \r
649                         return firstCell;\r
650                 }\r
651                 // Be able to merge cells only if actual dimension of selected\r
652                 // cells equals to the caculated rectangle.\r
653                 else\r
654                         return ( totalRowSpan * totalColSpan ) == dimension;\r
655         }\r
656 \r
657         function verticalSplitCell ( selection, isDetect )\r
658         {\r
659                 var cells = getSelectedCells( selection );\r
660                 if ( cells.length > 1 )\r
661                         return false;\r
662                 else if ( isDetect )\r
663                         return true;\r
664 \r
665                 var cell = cells[ 0 ],\r
666                         tr = cell.getParent(),\r
667                         table = tr.getAscendant( 'table' ),\r
668                         map = CKEDITOR.tools.buildTableMap( table ),\r
669                         rowIndex = tr.$.rowIndex,\r
670                         colIndex = cellInRow( map, rowIndex, cell ),\r
671                         rowSpan = cell.$.rowSpan,\r
672                         newCell,\r
673                         newRowSpan,\r
674                         newCellRowSpan,\r
675                         newRowIndex;\r
676 \r
677                 if ( rowSpan > 1 )\r
678                 {\r
679                         newRowSpan = Math.ceil( rowSpan / 2 );\r
680                         newCellRowSpan = Math.floor( rowSpan / 2 );\r
681                         newRowIndex = rowIndex + newRowSpan;\r
682                         var newCellTr = new CKEDITOR.dom.element( table.$.rows[ newRowIndex ] ),\r
683                                 newCellRow = cellInRow( map, newRowIndex ),\r
684                                 candidateCell;\r
685 \r
686                         newCell = cell.clone();\r
687 \r
688                         // Figure out where to insert the new cell by checking the vitual row.\r
689                         for ( var c = 0; c < newCellRow.length; c++ )\r
690                         {\r
691                                 candidateCell = newCellRow[ c ];\r
692                                 // Catch first cell actually following the column.\r
693                                 if ( candidateCell.parentNode == newCellTr.$\r
694                                         && c > colIndex )\r
695                                 {\r
696                                         newCell.insertBefore( new CKEDITOR.dom.element( candidateCell ) );\r
697                                         break;\r
698                                 }\r
699                                 else\r
700                                         candidateCell = null;\r
701                         }\r
702 \r
703                         // The destination row is empty, append at will.\r
704                         if ( !candidateCell )\r
705                                 newCellTr.append( newCell, true );\r
706                 }\r
707                 else\r
708                 {\r
709                         newCellRowSpan = newRowSpan = 1;\r
710 \r
711                         newCellTr = tr.clone();\r
712                         newCellTr.insertAfter( tr );\r
713                         newCellTr.append( newCell = cell.clone() );\r
714 \r
715                         var cellsInSameRow = cellInRow( map, rowIndex );\r
716                         for ( var i = 0; i < cellsInSameRow.length; i++ )\r
717                                 cellsInSameRow[ i ].rowSpan++;\r
718                 }\r
719 \r
720                 if ( !CKEDITOR.env.ie )\r
721                         newCell.appendBogus();\r
722 \r
723                 cell.$.rowSpan = newRowSpan;\r
724                 newCell.$.rowSpan = newCellRowSpan;\r
725                 if ( newRowSpan == 1 )\r
726                         cell.removeAttribute( 'rowSpan' );\r
727                 if ( newCellRowSpan == 1 )\r
728                         newCell.removeAttribute( 'rowSpan' );\r
729 \r
730                 return newCell;\r
731         }\r
732 \r
733         function horizontalSplitCell( selection, isDetect )\r
734         {\r
735                 var cells = getSelectedCells( selection );\r
736                 if ( cells.length > 1 )\r
737                         return false;\r
738                 else if ( isDetect )\r
739                         return true;\r
740 \r
741                 var cell = cells[ 0 ],\r
742                         tr = cell.getParent(),\r
743                         table = tr.getAscendant( 'table' ),\r
744                         map = CKEDITOR.tools.buildTableMap( table ),\r
745                         rowIndex = tr.$.rowIndex,\r
746                         colIndex = cellInRow( map, rowIndex, cell ),\r
747                         colSpan = cell.$.colSpan,\r
748                         newCell,\r
749                         newColSpan,\r
750                         newCellColSpan;\r
751 \r
752                 if ( colSpan > 1 )\r
753                 {\r
754                         newColSpan = Math.ceil( colSpan / 2 );\r
755                         newCellColSpan = Math.floor( colSpan / 2 );\r
756                 }\r
757                 else\r
758                 {\r
759                         newCellColSpan = newColSpan = 1;\r
760                         var cellsInSameCol = cellInCol( map, colIndex );\r
761                         for ( var i = 0; i < cellsInSameCol.length; i++ )\r
762                                 cellsInSameCol[ i ].colSpan++;\r
763                 }\r
764                 newCell = cell.clone();\r
765                 newCell.insertAfter( cell );\r
766                 if ( !CKEDITOR.env.ie )\r
767                         newCell.appendBogus();\r
768 \r
769                 cell.$.colSpan = newColSpan;\r
770                 newCell.$.colSpan = newCellColSpan;\r
771                 if ( newColSpan == 1 )\r
772                         cell.removeAttribute( 'colSpan' );\r
773                 if ( newCellColSpan == 1 )\r
774                         newCell.removeAttribute( 'colSpan' );\r
775 \r
776                 return newCell;\r
777         }\r
778         // Context menu on table caption incorrect (#3834)\r
779         var contextMenuTags = { thead : 1, tbody : 1, tfoot : 1, td : 1, tr : 1, th : 1 };\r
780 \r
781         CKEDITOR.plugins.tabletools =\r
782         {\r
783                 init : function( editor )\r
784                 {\r
785                         var lang = editor.lang.table;\r
786 \r
787                         editor.addCommand( 'cellProperties', new CKEDITOR.dialogCommand( 'cellProperties' ) );\r
788                         CKEDITOR.dialog.add( 'cellProperties', this.path + 'dialogs/tableCell.js' );\r
789 \r
790                         editor.addCommand( 'tableDelete',\r
791                                 {\r
792                                         exec : function( editor )\r
793                                         {\r
794                                                 var selection = editor.getSelection(),\r
795                                                         startElement = selection && selection.getStartElement(),\r
796                                                         table = startElement && startElement.getAscendant( 'table', 1 );\r
797 \r
798                                                 if ( !table )\r
799                                                         return;\r
800 \r
801                                                 // If the table's parent has only one child remove it as well (unless it's the body or a table cell) (#5416, #6289)\r
802                                                 var parent = table.getParent();\r
803                                                 if ( parent.getChildCount() == 1 && !parent.is( 'body', 'td', 'th' ) )\r
804                                                         table = parent;\r
805 \r
806                                                 var range = new CKEDITOR.dom.range( editor.document );\r
807                                                 range.moveToPosition( table, CKEDITOR.POSITION_BEFORE_START );\r
808                                                 table.remove();\r
809                                                 range.select();\r
810                                         }\r
811                                 } );\r
812 \r
813                         editor.addCommand( 'rowDelete',\r
814                                 {\r
815                                         exec : function( editor )\r
816                                         {\r
817                                                 var selection = editor.getSelection();\r
818                                                 placeCursorInCell( deleteRows( selection ) );\r
819                                         }\r
820                                 } );\r
821 \r
822                         editor.addCommand( 'rowInsertBefore',\r
823                                 {\r
824                                         exec : function( editor )\r
825                                         {\r
826                                                 var selection = editor.getSelection();\r
827                                                 insertRow( selection, true );\r
828                                         }\r
829                                 } );\r
830 \r
831                         editor.addCommand( 'rowInsertAfter',\r
832                                 {\r
833                                         exec : function( editor )\r
834                                         {\r
835                                                 var selection = editor.getSelection();\r
836                                                 insertRow( selection );\r
837                                         }\r
838                                 } );\r
839 \r
840                         editor.addCommand( 'columnDelete',\r
841                                 {\r
842                                         exec : function( editor )\r
843                                         {\r
844                                                 var selection = editor.getSelection();\r
845                                                 var element = deleteColumns( selection );\r
846                                                 element &&  placeCursorInCell( element, true );\r
847                                         }\r
848                                 } );\r
849 \r
850                         editor.addCommand( 'columnInsertBefore',\r
851                                 {\r
852                                         exec : function( editor )\r
853                                         {\r
854                                                 var selection = editor.getSelection();\r
855                                                 insertColumn( selection, true );\r
856                                         }\r
857                                 } );\r
858 \r
859                         editor.addCommand( 'columnInsertAfter',\r
860                                 {\r
861                                         exec : function( editor )\r
862                                         {\r
863                                                 var selection = editor.getSelection();\r
864                                                 insertColumn( selection );\r
865                                         }\r
866                                 } );\r
867 \r
868                         editor.addCommand( 'cellDelete',\r
869                                 {\r
870                                         exec : function( editor )\r
871                                         {\r
872                                                 var selection = editor.getSelection();\r
873                                                 deleteCells( selection );\r
874                                         }\r
875                                 } );\r
876 \r
877                         editor.addCommand( 'cellMerge',\r
878                                 {\r
879                                         exec : function( editor )\r
880                                         {\r
881                                                 placeCursorInCell( mergeCells( editor.getSelection() ), true );\r
882                                         }\r
883                                 } );\r
884 \r
885                         editor.addCommand( 'cellMergeRight',\r
886                                 {\r
887                                         exec : function( editor )\r
888                                         {\r
889                                                 placeCursorInCell( mergeCells( editor.getSelection(), 'right' ), true );\r
890                                         }\r
891                                 } );\r
892 \r
893                         editor.addCommand( 'cellMergeDown',\r
894                                 {\r
895                                         exec : function( editor )\r
896                                         {\r
897                                                 placeCursorInCell( mergeCells( editor.getSelection(), 'down' ), true );\r
898                                         }\r
899                                 } );\r
900 \r
901                         editor.addCommand( 'cellVerticalSplit',\r
902                                 {\r
903                                         exec : function( editor )\r
904                                         {\r
905                                                 placeCursorInCell( verticalSplitCell( editor.getSelection() ) );\r
906                                         }\r
907                                 } );\r
908 \r
909                         editor.addCommand( 'cellHorizontalSplit',\r
910                                 {\r
911                                         exec : function( editor )\r
912                                         {\r
913                                                 placeCursorInCell( horizontalSplitCell( editor.getSelection() ) );\r
914                                         }\r
915                                 } );\r
916 \r
917                         editor.addCommand( 'cellInsertBefore',\r
918                                 {\r
919                                         exec : function( editor )\r
920                                         {\r
921                                                 var selection = editor.getSelection();\r
922                                                 insertCell( selection, true );\r
923                                         }\r
924                                 } );\r
925 \r
926                         editor.addCommand( 'cellInsertAfter',\r
927                                 {\r
928                                         exec : function( editor )\r
929                                         {\r
930                                                 var selection = editor.getSelection();\r
931                                                 insertCell( selection );\r
932                                         }\r
933                                 } );\r
934 \r
935                         // If the "menu" plugin is loaded, register the menu items.\r
936                         if ( editor.addMenuItems )\r
937                         {\r
938                                 editor.addMenuItems(\r
939                                         {\r
940                                                 tablecell :\r
941                                                 {\r
942                                                         label : lang.cell.menu,\r
943                                                         group : 'tablecell',\r
944                                                         order : 1,\r
945                                                         getItems : function()\r
946                                                         {\r
947                                                                 var selection = editor.getSelection(),\r
948                                                                         cells = getSelectedCells( selection );\r
949                                                                 return {\r
950                                                                         tablecell_insertBefore : CKEDITOR.TRISTATE_OFF,\r
951                                                                         tablecell_insertAfter : CKEDITOR.TRISTATE_OFF,\r
952                                                                         tablecell_delete : CKEDITOR.TRISTATE_OFF,\r
953                                                                         tablecell_merge : mergeCells( selection, null, true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED,\r
954                                                                         tablecell_merge_right : mergeCells( selection, 'right', true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED,\r
955                                                                         tablecell_merge_down : mergeCells( selection, 'down', true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED,\r
956                                                                         tablecell_split_vertical : verticalSplitCell( selection, true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED,\r
957                                                                         tablecell_split_horizontal : horizontalSplitCell( selection, true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED,\r
958                                                                         tablecell_properties : cells.length > 0 ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED\r
959                                                                 };\r
960                                                         }\r
961                                                 },\r
962 \r
963                                                 tablecell_insertBefore :\r
964                                                 {\r
965                                                         label : lang.cell.insertBefore,\r
966                                                         group : 'tablecell',\r
967                                                         command : 'cellInsertBefore',\r
968                                                         order : 5\r
969                                                 },\r
970 \r
971                                                 tablecell_insertAfter :\r
972                                                 {\r
973                                                         label : lang.cell.insertAfter,\r
974                                                         group : 'tablecell',\r
975                                                         command : 'cellInsertAfter',\r
976                                                         order : 10\r
977                                                 },\r
978 \r
979                                                 tablecell_delete :\r
980                                                 {\r
981                                                         label : lang.cell.deleteCell,\r
982                                                         group : 'tablecell',\r
983                                                         command : 'cellDelete',\r
984                                                         order : 15\r
985                                                 },\r
986 \r
987                                                 tablecell_merge :\r
988                                                 {\r
989                                                         label : lang.cell.merge,\r
990                                                         group : 'tablecell',\r
991                                                         command : 'cellMerge',\r
992                                                         order : 16\r
993                                                 },\r
994 \r
995                                                 tablecell_merge_right :\r
996                                                 {\r
997                                                         label : lang.cell.mergeRight,\r
998                                                         group : 'tablecell',\r
999                                                         command : 'cellMergeRight',\r
1000                                                         order : 17\r
1001                                                 },\r
1002 \r
1003                                                 tablecell_merge_down :\r
1004                                                 {\r
1005                                                         label : lang.cell.mergeDown,\r
1006                                                         group : 'tablecell',\r
1007                                                         command : 'cellMergeDown',\r
1008                                                         order : 18\r
1009                                                 },\r
1010 \r
1011                                                 tablecell_split_horizontal :\r
1012                                                 {\r
1013                                                         label : lang.cell.splitHorizontal,\r
1014                                                         group : 'tablecell',\r
1015                                                         command : 'cellHorizontalSplit',\r
1016                                                         order : 19\r
1017                                                 },\r
1018 \r
1019                                                 tablecell_split_vertical :\r
1020                                                 {\r
1021                                                         label : lang.cell.splitVertical,\r
1022                                                         group : 'tablecell',\r
1023                                                         command : 'cellVerticalSplit',\r
1024                                                         order : 20\r
1025                                                 },\r
1026 \r
1027                                                 tablecell_properties :\r
1028                                                 {\r
1029                                                         label : lang.cell.title,\r
1030                                                         group : 'tablecellproperties',\r
1031                                                         command : 'cellProperties',\r
1032                                                         order : 21\r
1033                                                 },\r
1034 \r
1035                                                 tablerow :\r
1036                                                 {\r
1037                                                         label : lang.row.menu,\r
1038                                                         group : 'tablerow',\r
1039                                                         order : 1,\r
1040                                                         getItems : function()\r
1041                                                         {\r
1042                                                                 return {\r
1043                                                                         tablerow_insertBefore : CKEDITOR.TRISTATE_OFF,\r
1044                                                                         tablerow_insertAfter : CKEDITOR.TRISTATE_OFF,\r
1045                                                                         tablerow_delete : CKEDITOR.TRISTATE_OFF\r
1046                                                                 };\r
1047                                                         }\r
1048                                                 },\r
1049 \r
1050                                                 tablerow_insertBefore :\r
1051                                                 {\r
1052                                                         label : lang.row.insertBefore,\r
1053                                                         group : 'tablerow',\r
1054                                                         command : 'rowInsertBefore',\r
1055                                                         order : 5\r
1056                                                 },\r
1057 \r
1058                                                 tablerow_insertAfter :\r
1059                                                 {\r
1060                                                         label : lang.row.insertAfter,\r
1061                                                         group : 'tablerow',\r
1062                                                         command : 'rowInsertAfter',\r
1063                                                         order : 10\r
1064                                                 },\r
1065 \r
1066                                                 tablerow_delete :\r
1067                                                 {\r
1068                                                         label : lang.row.deleteRow,\r
1069                                                         group : 'tablerow',\r
1070                                                         command : 'rowDelete',\r
1071                                                         order : 15\r
1072                                                 },\r
1073 \r
1074                                                 tablecolumn :\r
1075                                                 {\r
1076                                                         label : lang.column.menu,\r
1077                                                         group : 'tablecolumn',\r
1078                                                         order : 1,\r
1079                                                         getItems : function()\r
1080                                                         {\r
1081                                                                 return {\r
1082                                                                         tablecolumn_insertBefore : CKEDITOR.TRISTATE_OFF,\r
1083                                                                         tablecolumn_insertAfter : CKEDITOR.TRISTATE_OFF,\r
1084                                                                         tablecolumn_delete : CKEDITOR.TRISTATE_OFF\r
1085                                                                 };\r
1086                                                         }\r
1087                                                 },\r
1088 \r
1089                                                 tablecolumn_insertBefore :\r
1090                                                 {\r
1091                                                         label : lang.column.insertBefore,\r
1092                                                         group : 'tablecolumn',\r
1093                                                         command : 'columnInsertBefore',\r
1094                                                         order : 5\r
1095                                                 },\r
1096 \r
1097                                                 tablecolumn_insertAfter :\r
1098                                                 {\r
1099                                                         label : lang.column.insertAfter,\r
1100                                                         group : 'tablecolumn',\r
1101                                                         command : 'columnInsertAfter',\r
1102                                                         order : 10\r
1103                                                 },\r
1104 \r
1105                                                 tablecolumn_delete :\r
1106                                                 {\r
1107                                                         label : lang.column.deleteColumn,\r
1108                                                         group : 'tablecolumn',\r
1109                                                         command : 'columnDelete',\r
1110                                                         order : 15\r
1111                                                 }\r
1112                                         });\r
1113                         }\r
1114 \r
1115                         // If the "contextmenu" plugin is laoded, register the listeners.\r
1116                         if ( editor.contextMenu )\r
1117                         {\r
1118                                 editor.contextMenu.addListener( function( element, selection )\r
1119                                         {\r
1120                                                 if ( !element || element.isReadOnly() )\r
1121                                                         return null;\r
1122 \r
1123                                                 while ( element )\r
1124                                                 {\r
1125                                                         if ( element.getName() in contextMenuTags )\r
1126                                                         {\r
1127                                                                 return {\r
1128                                                                         tablecell : CKEDITOR.TRISTATE_OFF,\r
1129                                                                         tablerow : CKEDITOR.TRISTATE_OFF,\r
1130                                                                         tablecolumn : CKEDITOR.TRISTATE_OFF\r
1131                                                                 };\r
1132                                                         }\r
1133                                                         element = element.getParent();\r
1134                                                 }\r
1135 \r
1136                                                 return null;\r
1137                                         } );\r
1138                         }\r
1139                 },\r
1140 \r
1141                 getSelectedCells : getSelectedCells\r
1142 \r
1143         };\r
1144         CKEDITOR.plugins.add( 'tabletools', CKEDITOR.plugins.tabletools );\r
1145 })();\r
1146 \r
1147 /**\r
1148  * Create a two-dimension array that reflects the actual layout of table cells,\r
1149  * with cell spans, with mappings to the original td elements.\r
1150  * @param table {CKEDITOR.dom.element}\r
1151  */\r
1152 CKEDITOR.tools.buildTableMap = function ( table )\r
1153 {\r
1154         var aRows = table.$.rows ;\r
1155 \r
1156         // Row and Column counters.\r
1157         var r = -1 ;\r
1158 \r
1159         var aMap = [];\r
1160 \r
1161         for ( var i = 0 ; i < aRows.length ; i++ )\r
1162         {\r
1163                 r++ ;\r
1164                 !aMap[r] && ( aMap[r] = [] );\r
1165 \r
1166                 var c = -1 ;\r
1167 \r
1168                 for ( var j = 0 ; j < aRows[i].cells.length ; j++ )\r
1169                 {\r
1170                         var oCell = aRows[i].cells[j] ;\r
1171 \r
1172                         c++ ;\r
1173                         while ( aMap[r][c] )\r
1174                                 c++ ;\r
1175 \r
1176                         var iColSpan = isNaN( oCell.colSpan ) ? 1 : oCell.colSpan ;\r
1177                         var iRowSpan = isNaN( oCell.rowSpan ) ? 1 : oCell.rowSpan ;\r
1178 \r
1179                         for ( var rs = 0 ; rs < iRowSpan ; rs++ )\r
1180                         {\r
1181                                 if ( !aMap[r + rs] )\r
1182                                         aMap[r + rs] = [];\r
1183 \r
1184                                 for ( var cs = 0 ; cs < iColSpan ; cs++ )\r
1185                                 {\r
1186                                         aMap[r + rs][c + cs] = aRows[i].cells[j] ;\r
1187                                 }\r
1188                         }\r
1189 \r
1190                         c += iColSpan - 1 ;\r
1191                 }\r
1192         }\r
1193         return aMap ;\r
1194 };\r