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