      1 /*
      2 Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved.
      3 For licensing, see LICENSE.html or http://ckeditor.com/license
      4 */
      5
      6 (function()
      7 {
      8 	function removeRawAttribute( $node, attr )
      9 	{
     10 		if ( CKEDITOR.env.ie )
     11 			$node.removeAttribute( attr );
     12 		else
     13 			delete $node[ attr ];
     14 	}
     15
     16 	var cellNodeRegex = /^(?:td|th)$/;
     17
     18 	function getSelectedCells( selection )
     19 	{
     20 		// Walker will try to split text nodes, which will make the current selection
     21 		// invalid. So save bookmarks before doing anything.
     22 		var bookmarks = selection.createBookmarks();
     23
     24 		var ranges = selection.getRanges();
     25 		var retval = [];
     26 		var database = {};
     27
     28 		function moveOutOfCellGuard( node )
     29 		{
     30 			// Apply to the first cell only.
     31 			if ( retval.length > 0 )
     32 				return;
     33
     34 			// If we are exiting from the first </td>, then the td should definitely be
     35 			// included.
     36 			if ( node.type == CKEDITOR.NODE_ELEMENT && cellNodeRegex.test( node.getName() )
     37 					&& !node.getCustomData( 'selected_cell' ) )
     38 			{
     39 				CKEDITOR.dom.element.setMarker( database, node, 'selected_cell', true );
     40 				retval.push( node );
     41 			}
     42 		}
     43
     44 		for ( var i = 0 ; i < ranges.length ; i++ )
     45 		{
     46 			var range = ranges[ i ];
     47
     48 			if ( range.collapsed )
     49 			{
     50 				// Walker does not handle collapsed ranges yet - fall back to old API.
     51 				var startNode = range.getCommonAncestor();
     52 				var nearestCell = startNode.getAscendant( 'td', true ) || startNode.getAscendant( 'th', true );
     53 				if ( nearestCell )
     54 					retval.push( nearestCell );
     55 			}
     56 			else
     57 			{
     58 				var walker = new CKEDITOR.dom.walker( range );
     59 				var node;
     60 				walker.guard = moveOutOfCellGuard;
     61
     62 				while ( ( node = walker.next() ) )
     63 				{
     64 					// If may be possible for us to have a range like this:
     65 					// <td>^1</td><td>^2</td>
     66 					// The 2nd td shouldn't be included.
     67 					//
     68 					// So we have to take care to include a td we've entered only when we've
     69 					// walked into its children.
     70
     71 					var parent = node.getParent();
     72 					if ( parent && cellNodeRegex.test( parent.getName() ) && !parent.getCustomData( 'selected_cell' ) )
     73 					{
     74 						CKEDITOR.dom.element.setMarker( database, parent, 'selected_cell', true );
     75 						retval.push( parent );
     76 					}
     77 				}
     78 			}
     79 		}
     80
     81 		CKEDITOR.dom.element.clearAllMarkers( database );
     82
     83 		// Restore selection position.
     84 		selection.selectBookmarks( bookmarks );
     85
     86 		return retval;
     87 	}
     88
     89 	function createTableMap( $refCell )
     90 	{
     91 		var refCell = new CKEDITOR.dom.element( $refCell );
     92 		var $table = ( refCell.getName() == 'table' ? $refCell : refCell.getAscendant( 'table' ) ).$;
     93 		var $rows = $table.rows;
     94
     95 		// Row and column counters.
     96 		var r = -1;
     97 		var map = [];
     98 		for ( var i = 0 ; i < $rows.length ; i++ )
     99 		{
    100 			r++;
    101 			if ( !map[ r ] )
    102 				map[ r ] = [];
    103
    104 			var c = -1;
    105
    106 			for ( var j = 0 ; j < $rows[ i ].cells.length ; j++ )
    107 			{
    108 				var $cell = $rows[ i ].cells[ j ];
    109
    110 				c++;
    111 				while ( map[ r ][ c ] )
    112 					c++;
    113
    114 				var colSpan = isNaN( $cell.colSpan ) ? 1 : $cell.colSpan;
    115 				var rowSpan = isNaN( $cell.rowSpan ) ? 1 : $cell.rowSpan;
    116
    117 				for ( var rs = 0 ; rs < rowSpan ; rs++ )
    118 				{
    119 					if ( !map[ r + rs ] )
    120 						map[ r + rs ] = [];
    121
    122 					for ( var cs = 0 ; cs < colSpan ; cs++ )
    123 						map [ r + rs ][ c + cs ] = $rows[ i ].cells[ j ];
    124 				}
    125
    126 				c += colSpan - 1;
    127 			}
    128 		}
    129
    130 		return map;
    131 	}
    132
    133 	function installTableMap( tableMap, $table )
    134 	{
    135 		/*
    136 		 * IE BUG: rowSpan is always 1 in IE if the cell isn't attached to a row. So
    137 		 * store is separately in another attribute. (#1917)
    138 		 */
    139 		var rowSpanAttr = CKEDITOR.env.ie ? '_cke_rowspan' : 'rowSpan';
    140
    141 		/*
    142 		 * Disconnect all the cells in tableMap from their parents, set all colSpan
    143 		 * and rowSpan attributes to 1.
    144 		 */
    145 		for ( var i = 0 ; i < tableMap.length ; i++ )
    146 		{
    147 			for ( var j = 0 ; j < tableMap[ i ].length ; j++ )
    148 			{
    149 				var $cell = tableMap[ i ][ j ];
    150 				if ( $cell.parentNode )
    151 					$cell.parentNode.removeChild( $cell );
    152 				$cell.colSpan = $cell[ rowSpanAttr ] = 1;
    153 			}
    154 		}
    155
    156 		// Scan by rows and set colSpan.
    157 		var maxCol = 0;
    158 		for ( i = 0 ; i < tableMap.length ; i++ )
    159 		{
    160 			for ( j = 0 ; j < tableMap[ i ].length ; j++ )
    161 			{
    162 				$cell = tableMap[ i ][ j ];
    163 				if ( !$cell )
    164 					continue;
    165 				if ( j > maxCol )
    166 					maxCol = j;
    167 				if ( $cell[ '_cke_colScanned' ] )
    168 					continue;
    169 				if ( tableMap[ i ][ j - 1 ] == $cell )
    170 					$cell.colSpan++;
    171 				if ( tableMap[ i ][ j + 1 ] != $cell )
    172 					$cell[ '_cke_colScanned' ] = 1;
    173 			}
    174 		}
    175
    176 		// Scan by columns and set rowSpan.
    177 		for ( i = 0 ; i <= maxCol ; i++ )
    178 		{
    179 			for ( j = 0 ; j < tableMap.length ; j++ )
    180 			{
    181 				if ( !tableMap[ j ] )
    182 					continue;
    183 				$cell = tableMap[ j ][ i ];
    184 				if ( !$cell || $cell[ '_cke_rowScanned' ] )
    185 					continue;
    186 				if ( tableMap[ j - 1 ] && tableMap[ j - 1 ][ i ] == $cell )
    187 					$cell[ rowSpanAttr ]++;
    188 				if ( !tableMap[ j + 1 ] || tableMap[ j + 1 ][ i ] != $cell  )
    189 					$cell[ '_cke_rowScanned' ] = 1;
    190 			}
    191 		}
    192
    193 		// Clear all temporary flags.
    194 		for ( i = 0 ; i < tableMap.length ; i++ )
    195 		{
    196 			for ( j = 0 ; j < tableMap[ i ].length ; j++ )
    197 			{
    198 				$cell = tableMap[ i ][ j ];
    199 				removeRawAttribute( $cell, '_cke_colScanned' );
    200 				removeRawAttribute( $cell, '_cke_rowScanned' );
    201 			}
    202 		}
    203
    204 		// Insert physical rows and columns to table.
    205 		for ( i = 0 ; i < tableMap.length ; i++ )
    206 		{
    207 			var $row = $table.ownerDocument.createElement( 'tr' );
    208 			for ( j = 0 ; j < tableMap[ i ].length ; )
    209 			{
    210 				$cell = tableMap[ i ][ j ];
    211 				if ( tableMap[ i - 1 ] && tableMap[ i - 1 ][ j ] == $cell )
    212 				{
    213 					j += $cell.colSpan;
    214 					continue;
    215 				}
    216 				$row.appendChild( $cell );
    217 				if ( rowSpanAttr != 'rowSpan' )
    218 				{
    219 					$cell.rowSpan = $cell[ rowSpanAttr ];
    220 					$cell.removeAttribute( rowSpanAttr );
    221 				}
    222 				j += $cell.colSpan;
    223 				if ( $cell.colSpan == 1 )
    224 					$cell.removeAttribute( 'colSpan' );
    225 				if ( $cell.rowSpan == 1 )
    226 					$cell.removeAttribute( 'rowSpan' );
    227 			}
    228
    229 			if ( CKEDITOR.env.ie )
    230 				$table.rows[ i ].replaceNode( $row );
    231 			else
    232 			{
    233 				var dest = new CKEDITOR.dom.element( $table.rows[ i ] );
    234 				var src = new CKEDITOR.dom.element( $row );
    235 				dest.setHtml( '' );
    236 				src.moveChildren( dest );
    237 			}
    238 		}
    239 	}
    240
    241 	function clearRow( $tr )
    242 	{
    243 		// Get the array of row's cells.
    244 		var $cells = $tr.cells;
    245
    246 		// Empty all cells.
    247 		for ( var i = 0 ; i < $cells.length ; i++ )
    248 		{
    249 			$cells[ i ].innerHTML = '';
    250
    251 			if ( !CKEDITOR.env.ie )
    252 				( new CKEDITOR.dom.element( $cells[ i ] ) ).appendBogus();
    253 		}
    254 	}
    255
    256 	function insertRow( selection, insertBefore )
    257 	{
    258 		// Get the row where the selection is placed in.
    259 		var row = selection.getStartElement().getAscendant( 'tr' );
    260 		if ( !row )
    261 			return;
    262
    263 		// Create a clone of the row.
    264 		var newRow = row.clone( true );
    265
    266 		// Insert the new row before of it.
    267 		newRow.insertBefore( row );
    268
    269 		// Clean one of the rows to produce the illusion of inserting an empty row
    270 		// before or after.
    271 		clearRow( insertBefore ? newRow.$ : row.$ );
    272 	}
    273
    274 	function deleteRows( selectionOrRow )
    275 	{
    276 		if ( selectionOrRow instanceof CKEDITOR.dom.selection )
    277 		{
    278 			var cells = getSelectedCells( selectionOrRow );
    279 			var rowsToDelete = [];
    280
    281 			// Queue up the rows - it's possible and likely that we have duplicates.
    282 			for ( var i = 0 ; i < cells.length ; i++ )
    283 			{
    284 				var row = cells[ i ].getParent();
    285 				rowsToDelete[ row.$.rowIndex ] = row;
    286 			}
    287
    288 			for ( i = rowsToDelete.length ; i >= 0 ; i-- )
    289 			{
    290 				if ( rowsToDelete[ i ] )
    291 					deleteRows( rowsToDelete[ i ] );
    292 			}
    293 		}
    294 		else if ( selectionOrRow instanceof CKEDITOR.dom.element )
    295 		{
    296 			var table = selectionOrRow.getAscendant( 'table' );
    297
    298 			if ( table.$.rows.length == 1 )
    299 				table.remove();
    300 			else
    301 				selectionOrRow.remove();
    302 		}
    303 	}
    304
    305 	function insertColumn( selection, insertBefore )
    306 	{
    307 		// Get the cell where the selection is placed in.
    308 		var startElement = selection.getStartElement();
    309 		var cell = startElement.getAscendant( 'td', true ) || startElement.getAscendant( 'th', true );
    310
    311 		if ( !cell )
    312 			return;
    313
    314 		// Get the cell's table.
    315 		var table = cell.getAscendant( 'table' );
    316 		var cellIndex = cell.$.cellIndex;
    317
    318 		// Loop through all rows available in the table.
    319 		for ( var i = 0 ; i < table.$.rows.length ; i++ )
    320 		{
    321 			var $row = table.$.rows[ i ];
    322
    323 			// If the row doesn't have enough cells, ignore it.
    324 			if ( $row.cells.length < ( cellIndex + 1 ) )
    325 				continue;
    326
    327 			cell = new CKEDITOR.dom.element( $row.cells[ cellIndex ].cloneNode( false ) );
    328
    329 			if ( !CKEDITOR.env.ie )
    330 				cell.appendBogus();
    331
    332 			// Get back the currently selected cell.
    333 			var baseCell = new CKEDITOR.dom.element( $row.cells[ cellIndex ] );
    334 			if ( insertBefore )
    335 				cell.insertBefore( baseCell );
    336 			else
    337 				cell.insertAfter( baseCell );
    338 		}
    339 	}
    340
    341 	function deleteColumns( selectionOrCell )
    342 	{
    343 		if ( selectionOrCell instanceof CKEDITOR.dom.selection )
    344 		{
    345 			var colsToDelete = getSelectedCells( selectionOrCell );
    346 			for ( var i = colsToDelete.length ; i >= 0 ; i-- )
    347 			{
    348 				if ( colsToDelete[ i ] )
    349 					deleteColumns( colsToDelete[ i ] );
    350 			}
    351 		}
    352 		else if ( selectionOrCell instanceof CKEDITOR.dom.element )
    353 		{
    354 			// Get the cell's table.
    355 			var table = selectionOrCell.getAscendant( 'table' );
    356
    357 			// Get the cell index.
    358 			var cellIndex = selectionOrCell.$.cellIndex;
    359
    360 			/*
    361 			 * Loop through all rows from down to up, coz it's possible that some rows
    362 			 * will be deleted.
    363 			 */
    364 			for ( i = table.$.rows.length - 1 ; i >= 0 ; i-- )
    365 			{
    366 				// Get the row.
    367 				var row = new CKEDITOR.dom.element( table.$.rows[ i ] );
    368
    369 				// If the cell to be removed is the first one and the row has just one cell.
    370 				if ( !cellIndex && row.$.cells.length == 1 )
    371 				{
    372 					deleteRows( row );
    373 					continue;
    374 				}
    375
    376 				// Else, just delete the cell.
    377 				if ( row.$.cells[ cellIndex ] )
    378 					row.$.removeChild( row.$.cells[ cellIndex ] );
    379 			}
    380 		}
    381 	}
    382
    383 	function insertCell( selection, insertBefore )
    384 	{
    385 		var startElement = selection.getStartElement();
    386 		var cell = startElement.getAscendant( 'td', true ) || startElement.getAscendant( 'th', true );
    387
    388 		if ( !cell )
    389 			return;
    390
    391 		// Create the new cell element to be added.
    392 		var newCell = cell.clone();
    393 		if ( !CKEDITOR.env.ie )
    394 			newCell.appendBogus();
    395
    396 		if ( insertBefore )
    397 			newCell.insertBefore( cell );
    398 		else
    399 			newCell.insertAfter( cell );
    400 	}
    401
    402 	function deleteCells( selectionOrCell )
    403 	{
    404 		if ( selectionOrCell instanceof CKEDITOR.dom.selection )
    405 		{
    406 			var cellsToDelete = getSelectedCells( selectionOrCell );
    407 			for ( var i = cellsToDelete.length - 1 ; i >= 0 ; i-- )
    408 				deleteCells( cellsToDelete[ i ] );
    409 		}
    410 		else if ( selectionOrCell instanceof CKEDITOR.dom.element )
    411 		{
    412 			if ( selectionOrCell.getParent().getChildCount() == 1 )
    413 				selectionOrCell.getParent().remove();
    414 			else
    415 				selectionOrCell.remove();
    416 		}
    417 	}
    418
    419 	CKEDITOR.plugins.add( 'tabletools',
    420 	{
    421 		init : function( editor )
    422 		{
    423 			var lang = editor.lang.table;
    424
    425 			editor.addCommand( 'cellProperties', new CKEDITOR.dialogCommand( 'cellProperties' ) );
    426 			CKEDITOR.dialog.add( 'cellProperties', this.path + 'dialogs/tableCell.js' );
    427
    428 			editor.addCommand( 'tableDelete',
    429 				{
    430 					exec : function( editor )
    431 					{
    432 						var selection = editor.getSelection();
    433 						var startElement = selection && selection.getStartElement();
    434 						var table = startElement && startElement.getAscendant( 'table', true );
    435
    436 						if ( !table )
    437 							return;
    438
    439 						// Maintain the selection point at where the table was deleted.
    440 						selection.selectElement( table );
    441 						var range = selection.getRanges()[0];
    442 						range.collapse();
    443 						selection.selectRanges( [ range ] );
    444
    445 						// If the table's parent has only one child, remove it as well.
    446 						if ( table.getParent().getChildCount() == 1 )
    447 							table.getParent().remove();
    448 						else
    449 							table.remove();
    450 					}
    451 				} );
    452
    453 			editor.addCommand( 'rowDelete',
    454 				{
    455 					exec : function( editor )
    456 					{
    457 						var selection = editor.getSelection();
    458 						deleteRows( selection );
    459 					}
    460 				} );
    461
    462 			editor.addCommand( 'rowInsertBefore',
    463 				{
    464 					exec : function( editor )
    465 					{
    466 						var selection = editor.getSelection();
    467 						insertRow( selection, true );
    468 					}
    469 				} );
    470
    471 			editor.addCommand( 'rowInsertAfter',
    472 				{
    473 					exec : function( editor )
    474 					{
    475 						var selection = editor.getSelection();
    476 						insertRow( selection );
    477 					}
    478 				} );
    479
    480 			editor.addCommand( 'columnDelete',
    481 				{
    482 					exec : function( editor )
    483 					{
    484 						var selection = editor.getSelection();
    485 						deleteColumns( selection );
    486 					}
    487 				} );
    488
    489 			editor.addCommand( 'columnInsertBefore',
    490 				{
    491 					exec : function( editor )
    492 					{
    493 						var selection = editor.getSelection();
    494 						insertColumn( selection, true );
    495 					}
    496 				} );
    497
    498 			editor.addCommand( 'columnInsertAfter',
    499 				{
    500 					exec : function( editor )
    501 					{
    502 						var selection = editor.getSelection();
    503 						insertColumn( selection );
    504 					}
    505 				} );
    506
    507 			editor.addCommand( 'cellDelete',
    508 				{
    509 					exec : function( editor )
    510 					{
    511 						var selection = editor.getSelection();
    512 						deleteCells( selection );
    513 					}
    514 				} );
    515
    516 			editor.addCommand( 'cellInsertBefore',
    517 				{
    518 					exec : function( editor )
    519 					{
    520 						var selection = editor.getSelection();
    521 						insertCell( selection, true );
    522 					}
    523 				} );
    524
    525 			editor.addCommand( 'cellInsertAfter',
    526 				{
    527 					exec : function( editor )
    528 					{
    529 						var selection = editor.getSelection();
    530 						insertCell( selection );
    531 					}
    532 				} );
    533
    534 			// If the "menu" plugin is loaded, register the menu items.
    535 			if ( editor.addMenuItems )
    536 			{
    537 				editor.addMenuItems(
    538 					{
    539 						tablecell :
    540 						{
    541 							label : lang.cell.menu,
    542 							group : 'tablecell',
    543 							order : 1,
    544 							getItems : function()
    545 							{
    546 								var cells = getSelectedCells( editor.getSelection() );
    547
    548 								return {
    549 									tablecell_insertBefore : CKEDITOR.TRISTATE_OFF,
    550 									tablecell_insertAfter : CKEDITOR.TRISTATE_OFF,
    551 									tablecell_delete : CKEDITOR.TRISTATE_OFF,
    552 									tablecell_properties : cells.length == 1 ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED
    553 								};
    554 							}
    555 						},
    556
    557 						tablecell_insertBefore :
    558 						{
    559 							label : lang.cell.insertBefore,
    560 							group : 'tablecell',
    561 							command : 'cellInsertBefore',
    562 							order : 5
    563 						},
    564
    565 						tablecell_insertAfter :
    566 						{
    567 							label : lang.cell.insertAfter,
    568 							group : 'tablecell',
    569 							command : 'cellInsertAfter',
    570 							order : 10
    571 						},
    572
    573 						tablecell_delete :
    574 						{
    575 							label : lang.cell.deleteCell,
    576 							group : 'tablecell',
    577 							command : 'cellDelete',
    578 							order : 15
    579 						},
    580
    581 						tablecell_properties :
    582 						{
    583 							label : lang.cell.title,
    584 							group : 'tablecellproperties',
    585 							command : 'cellProperties',
    586 							order : 20
    587 						},
    588
    589 						tablerow :
    590 						{
    591 							label : lang.row.menu,
    592 							group : 'tablerow',
    593 							order : 1,
    594 							getItems : function()
    595 							{
    596 								return {
    597 									tablerow_insertBefore : CKEDITOR.TRISTATE_OFF,
    598 									tablerow_insertAfter : CKEDITOR.TRISTATE_OFF,
    599 									tablerow_delete : CKEDITOR.TRISTATE_OFF
    600 								};
    601 							}
    602 						},
    603
    604 						tablerow_insertBefore :
    605 						{
    606 							label : lang.row.insertBefore,
    607 							group : 'tablerow',
    608 							command : 'rowInsertBefore',
    609 							order : 5
    610 						},
    611
    612 						tablerow_insertAfter :
    613 						{
    614 							label : lang.row.insertAfter,
    615 							group : 'tablerow',
    616 							command : 'rowInsertAfter',
    617 							order : 10
    618 						},
    619
    620 						tablerow_delete :
    621 						{
    622 							label : lang.row.deleteRow,
    623 							group : 'tablerow',
    624 							command : 'rowDelete',
    625 							order : 15
    626 						},
    627
    628 						tablecolumn :
    629 						{
    630 							label : lang.column.menu,
    631 							group : 'tablecolumn',
    632 							order : 1,
    633 							getItems : function()
    634 							{
    635 								return {
    636 									tablecolumn_insertBefore : CKEDITOR.TRISTATE_OFF,
    637 									tablecolumn_insertAfter : CKEDITOR.TRISTATE_OFF,
    638 									tablecolumn_delete : CKEDITOR.TRISTATE_OFF
    639 								};
    640 							}
    641 						},
    642
    643 						tablecolumn_insertBefore :
    644 						{
    645 							label : lang.column.insertBefore,
    646 							group : 'tablecolumn',
    647 							command : 'columnInsertBefore',
    648 							order : 5
    649 						},
    650
    651 						tablecolumn_insertAfter :
    652 						{
    653 							label : lang.column.insertAfter,
    654 							group : 'tablecolumn',
    655 							command : 'columnInsertAfter',
    656 							order : 10
    657 						},
    658
    659 						tablecolumn_delete :
    660 						{
    661 							label : lang.column.deleteColumn,
    662 							group : 'tablecolumn',
    663 							command : 'columnDelete',
    664 							order : 15
    665 						}
    666 					});
    667 			}
    668
    669 			// If the "contextmenu" plugin is laoded, register the listeners.
    670 			if ( editor.contextMenu )
    671 			{
    672 				editor.contextMenu.addListener( function( element, selection )
    673 					{
    674 						if ( !element )
    675 							return null;
    676
    677 							var isCell	= !element.is( 'table' ) && element.hasAscendant( 'table' ) ;
    678
    679 							if ( isCell )
    680 							{
    681 								return {
    682 									tablecell : CKEDITOR.TRISTATE_OFF,
    683 									tablerow : CKEDITOR.TRISTATE_OFF,
    684 									tablecolumn : CKEDITOR.TRISTATE_OFF
    685 								};
    686 							}
    687
    688 							return null;
    689 					} );
    690 			}
    691 		}
    692 	} );
    693 })();
    694