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