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