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