Index: /CKEditor/trunk/_source/core/config.js
===================================================================
--- /CKEditor/trunk/_source/core/config.js	(revision 3332)
+++ /CKEditor/trunk/_source/core/config.js	(revision 3333)
@@ -151,5 +151,5 @@
 	 */
 
-	plugins : 'basicstyles,blockquote,button,clipboard,colorbutton,contextmenu,elementspath,enterkey,entities,find,flash,font,format,forms,horizontalrule,htmldataprocessor,image,indent,justify,keystrokes,link,list,newpage,pagebreak,pastefromword,pastetext,preview,print,removeformat,save,smiley,showblocks,sourcearea,stylescombo,table,specialchar,tab,templates,toolbar,undo,wysiwygarea,wsc',
+	plugins : 'basicstyles,blockquote,button,clipboard,colorbutton,contextmenu,elementspath,enterkey,entities,find,flash,font,format,forms,horizontalrule,htmldataprocessor,image,indent,justify,keystrokes,link,list,newpage,pagebreak,pastefromword,pastetext,preview,print,removeformat,save,smiley,showblocks,sourcearea,stylescombo,table,tabletools,specialchar,tab,templates,toolbar,undo,wysiwygarea,wsc',
 
 	/**
Index: /CKEditor/trunk/_source/core/dom/element.js
===================================================================
--- /CKEditor/trunk/_source/core/dom/element.js	(revision 3332)
+++ /CKEditor/trunk/_source/core/dom/element.js	(revision 3333)
@@ -1244,4 +1244,67 @@
 
 			return $ && new CKEDITOR.dom.document( $.contentWindow.document );
+		},
+
+		/**
+		 * Copy all the attributes from one node to the other, kinda like a clone
+		 * skipAttributes is an object with the attributes that must NOT be copied.
+		 * @param {CKEDITOR.dom.element} dest The destination element.
+		 * @param {Object} skipAttributes A dictionary of attributes to skip.
+		 * @example
+		 */
+		copyAttributes : function( dest, skipAttributes )
+		{
+			var attributes = this.$.attributes;
+			skipAttributes = skipAttributes || {};
+
+			for ( var n = 0 ; n < attributes.length ; n++ )
+			{
+				var attribute = attributes[n];
+
+				if ( attribute.specified )
+				{
+					var attrName = attribute.nodeName;
+					// We can set the type only once, so do it with the proper value, not copying it.
+					if ( attrName in skipAttributes )
+						continue;
+
+					var attrValue = this.getAttribute( attrName );
+					if ( attrValue === null )
+						attrValue = attribute.nodeValue;
+
+					dest.setAttribute( attrName, attrValue );
+				}
+			}
+
+			// The style:
+			if ( this.$.style.cssText !== '' )
+				dest.$.style.cssText = this.$.style.cssText;
+		},
+
+		/**
+		 * Changes the tag name of the current element.
+		 * @param {String} newTag The new tag for the element.
+		 */
+		renameNode : function( newTag )
+		{
+			// If it's already correct exit here.
+			if ( this.getName() == newTag )
+				return;
+
+			var doc = this.getDocument();
+
+			// Create the new node.
+			var newNode = new CKEDITOR.dom.element( newTag, doc );
+
+			// Copy all attributes.
+			this.copyAttributes( newNode );
+
+			// Move children to the new node.
+			this.moveChildren( newNode );
+
+			// Replace the node.
+			this.$.parentNode.replaceChild( newNode.$, this.$ );
+			newNode.$._cke_expando = this.$._cke_expando;
+			this.$ = newNode.$;
 		}
 	});
Index: /CKEditor/trunk/_source/core/dom/range.js
===================================================================
--- /CKEditor/trunk/_source/core/dom/range.js	(revision 3332)
+++ /CKEditor/trunk/_source/core/dom/range.js	(revision 3333)
@@ -602,5 +602,5 @@
 				childCount = endNode.getChildCount();
 				if ( childCount > endOffset )
-					endNode = endNode.getChild( endOffset ).getPreviousSourceNode();
+					endNode = endNode.getChild( endOffset ).getPreviousSourceNode( true );
 				else if ( childCount < 1 )
 					endNode = endNode.getPreviousSourceNode();
Index: /CKEditor/trunk/_source/lang/en.js
===================================================================
--- /CKEditor/trunk/_source/lang/en.js	(revision 3332)
+++ /CKEditor/trunk/_source/lang/en.js	(revision 3333)
@@ -236,5 +236,26 @@
 			mergeDown		: 'Merge Down',
 			splitHorizontal	: 'Split Cell Horizontally',
-			splitVertical	: 'Split Cell Vertically'
+			splitVertical	: 'Split Cell Vertically',
+			title			: 'Cell Properties',
+			cellType		: 'Cell Type',
+			rowSpan			: 'Rows Span',
+			colSpan			: 'Columns Span',
+			wordWrap		: 'Word Wrap',
+			hAlign			: 'Horizontal Alignment',
+			vAlign			: 'Vertical Alignment',
+			alignTop		: 'Top',
+			alignMiddle		: 'Middle',
+			alignBottom		: 'Bottom',
+			alignBaseline	: 'Baseline',
+			bgColor			: 'Background Color',
+			borderColor		: 'Border Color',
+			data			: 'Data',
+			header			: 'Header',
+			yes				: 'Yes',
+			no				: 'No',
+			invalidWidth	: 'Cell width must be a number.',
+			invalidHeight	: 'Cell height must be a number.',
+			invalidRowSpan	: 'Rows span must be a whole number.',
+			invalidColSpan	: 'Columns span must be a whole number.'
 		},
 
Index: /CKEditor/trunk/_source/plugins/menu/plugin.js
===================================================================
--- /CKEditor/trunk/_source/plugins/menu/plugin.js	(revision 3332)
+++ /CKEditor/trunk/_source/plugins/menu/plugin.js	(revision 3333)
@@ -327,5 +327,5 @@
 	'clipboard,' +
 	'form,' +
-	/*'tablecell,tablerow,tablecolumn,*/'table,'+
+	'tablecell,tablecellproperties,tablerow,tablecolumn,table,'+
 	'anchor,link,image,flash,' +
 	'checkbox,radio,textfield,hiddenfield,imagebutton,button,select,textarea';
Index: /CKEditor/trunk/_source/plugins/table/dialogs/table.js
===================================================================
--- /CKEditor/trunk/_source/plugins/table/dialogs/table.js	(revision 3332)
+++ /CKEditor/trunk/_source/plugins/table/dialogs/table.js	(revision 3333)
@@ -16,66 +16,4 @@
 		data.info[id] = this.getValue();
 	};
-
-	// Copy all the attributes from one node to the other, kinda like a clone
-	// skipAttributes is an object with the attributes that must NOT be copied
-	function copyAttributes( source, dest, skipAttributes )
-	{
-		var attributes = source.$.attributes;
-
-		for ( var n = 0 ; n < attributes.length ; n++ )
-		{
-			var attribute = attributes[n];
-
-			if ( attribute.specified )
-			{
-				var attrName = attribute.nodeName;
-				// We can set the type only once, so do it with the proper value, not copying it.
-				if ( attrName in skipAttributes )
-					continue;
-
-				var attrValue = source.getAttribute( attrName );
-				if ( attrValue === null )
-					attrValue = attribute.nodeValue;
-
-				dest.setAttribute( attrName, attrValue );
-			}
-		}
-		// The style:
-		if ( source.$.style.cssText !== '' )
-			dest.$.style.cssText = source.$.style.cssText;
-	}
-
-	/**
-	* Replaces a tag with another one, keeping its contents:
-	* for example TD --> TH, and TH --> TD.
-	* input: the original node, and the new tag name
-	* http://www.w3.org/TR/DOM-Level-3-Core/core.html#Document3-renameNode
-	*/
-	function renameNode( node , newTag )
-	{
-		// Only rename element nodes.
-		if ( node.type != CKEDITOR.NODE_ELEMENT )
-			return null;
-
-		// If it's already correct exit here.
-		if ( node.getName() == newTag )
-			return node;
-
-		var doc = node.getDocument();
-
-		// Create the new node
-		var newNode = new CKEDITOR.dom.element( newTag, doc );
-
-		// Copy all attributes
-		copyAttributes( node, newNode, {} );
-
-		// Move children to the new node
-		node.moveChildren( newNode );
-
-		// Finally replace the node and return the new one
-		node.$.parentNode.replaceChild( newNode.$, node.$ );
-
-		return newNode;
-	}
 
 	function tableDialog( editor, command )
@@ -174,7 +112,11 @@
 						for ( i = 0 ; i < theRow.getChildCount() ; i++ )
 						{
-							var th = renameNode( theRow.getChild( i ), 'th' );
-							if ( th )
-								th.setAttribute( 'scope', 'col' );
+							var th = theRow.getChild( i );
+							if ( th.type == CKEDITOR.NODE_ELEMENT )
+							{
+								th.renameNode( 'th' );
+								if ( i == 0 )
+									th.setAttribute( 'scope', 'col' );
+							}
 						}
 						thead.append( theRow.remove() );
@@ -193,7 +135,10 @@
 							for ( i = 0; i < theRow.getChildCount() ; i++ )
 							{
-								var newCell = renameNode( theRow.getChild( i ), 'td' );
-								if ( newCell )
+								var newCell = theRow.getChild( i );
+								if ( newCell.type == CKEDITOR.NODE_ELEMENT )
+								{
+									newCell.renameNode( 'td' );
 									newCell.removeAttribute( 'scope' );
+								}
 							}
 							theRow.insertBefore( previousFirstRow );
@@ -207,7 +152,7 @@
 						for( row = 0 ; row < table.$.rows.length ; row++ )
 						{
-							newCell = renameNode( new CKEDITOR.dom.element( table.$.rows[row].cells[0] ), 'th' );
-							if ( newCell )
-								newCell.setAttribute( 'scope', 'col' );
+							var newCell = new CKEDITOR.dom.element( table.$.rows[ row ].cells[ 0 ] );
+							newCell.renameNode( 'th' );
+							newCell.setAttribute( 'scope', 'col' );
 						}
 					}
@@ -221,7 +166,7 @@
 							if ( row.getParent().getName() == 'tbody' )
 							{
-								newCell = renameNode( new CKEDITOR.dom.element( row.$.cells[0] ), 'td' );
-								if ( newCell )
-									newCell.removeAttribute( 'scope' );
+								var newCell = new CKEDITOR.dom.element( row.$.cells[0] );
+								newCell.renameNode( 'td');
+								newCell.removeAttribute( 'scope' );
 							}
 						}
Index: /CKEditor/trunk/_source/plugins/table/plugin.js
===================================================================
--- /CKEditor/trunk/_source/plugins/table/plugin.js	(revision 3332)
+++ /CKEditor/trunk/_source/plugins/table/plugin.js	(revision 3333)
@@ -13,4 +13,5 @@
 		editor.addCommand( 'table', new CKEDITOR.dialogCommand( 'table' ) );
 		editor.addCommand( 'tableProperties', new CKEDITOR.dialogCommand( 'tableProperties' ) );
+
 		editor.ui.addButton( 'Table',
 			{
@@ -35,120 +36,12 @@
 					},
 
-//					tabledelete :
-//					{
-//						label : lang.deleteTable,
-//						command : 'tableDelete',
-//						group : 'table',
-//						order : 1
-//					},
-
-					tablecell :
+					tabledelete :
 					{
-						label : lang.cell.menu,
-						group : 'tablecell',
-						order : 1,
-						getItems : function()
-						{
-							return {
-								tablecell_insertBefore : CKEDITOR.TRISTATE_OFF,
-								tablecell_insertAfter : CKEDITOR.TRISTATE_OFF,
-								tablecell_delete : CKEDITOR.TRISTATE_OFF
-							};
-						}
-					},
-
-					tablecell_insertBefore :
-					{
-						label : lang.cell.insertBefore,
-						group : 'tablecell',
-						order : 5
-					},
-
-					tablecell_insertAfter :
-					{
-						label : lang.cell.insertAfter,
-						group : 'tablecell',
-						order : 10
-					},
-
-					tablecell_delete :
-					{
-						label : lang.cell.deleteCell,
-						group : 'tablecell',
-						order : 15
-					},
-
-					tablerow :
-					{
-						label : lang.row.menu,
-						group : 'tablerow',
-						order : 1,
-						getItems : function()
-						{
-							return {
-								tablerow_insertBefore : CKEDITOR.TRISTATE_OFF,
-								tablerow_insertAfter : CKEDITOR.TRISTATE_OFF,
-								tablerow_delete : CKEDITOR.TRISTATE_OFF
-							};
-						}
-					},
-
-					tablerow_insertBefore :
-					{
-						label : lang.row.insertBefore,
-						group : 'tablerow',
-						order : 5
-					},
-
-					tablerow_insertAfter :
-					{
-						label : lang.row.insertAfter,
-						group : 'tablerow',
-						order : 10
-					},
-
-					tablerow_delete :
-					{
-						label : lang.row.deleteRow,
-						group : 'tablerow',
-						order : 15
-					},
-
-					tablecolumn :
-					{
-						label : lang.column.menu,
-						group : 'tablecolumn',
-						order : 1,
-						getItems : function()
-						{
-							return {
-								tablecolumn_insertBefore : CKEDITOR.TRISTATE_OFF,
-								tablecolumn_insertAfter : CKEDITOR.TRISTATE_OFF,
-								tablecolumn_delete : CKEDITOR.TRISTATE_OFF
-							};
-						}
-					},
-
-					tablecolumn_insertBefore :
-					{
-						label : lang.column.insertBefore,
-						group : 'tablecolumn',
-						order : 5
-					},
-
-					tablecolumn_insertAfter :
-					{
-						label : lang.column.insertAfter,
-						group : 'tablecolumn',
-						order : 10
-					},
-
-					tablecolumn_delete :
-					{
-						label : lang.column.deleteColumn,
-						group : 'tablecolumn',
-						order : 15
+						label : lang.deleteTable,
+						command : 'tableDelete',
+						group : 'table',
+						order : 1
 					}
-				});
+				} );
 		}
 
@@ -161,25 +54,16 @@
 						return null;
 
-					var isTable	= element.is( 'table' ) ;
-					var isCell	= !isTable && element.hasAscendant( 'table' ) ;
+					var isTable	= element.is( 'table' ) || element.hasAscendant( 'table' );
 
-					if ( isTable || isCell )
+					if ( isTable )
 					{
-						var ret = isCell ?
-							{
-								tablecell : CKEDITOR.TRISTATE_OFF,
-								tablerow : CKEDITOR.TRISTATE_OFF,
-								tablecolumn : CKEDITOR.TRISTATE_OFF
-							}
-							: {};
-
-						ret.tabledelete = CKEDITOR.TRISTATE_OFF;
-						ret.table = CKEDITOR.TRISTATE_OFF;
-
-						return ret;
+						return {
+							tabledelete : CKEDITOR.TRISTATE_OFF,
+							table : CKEDITOR.TRISTATE_OFF
+						};
 					}
 
 					return null;
-				});
+				} );
 		}
 	}
Index: /CKEditor/trunk/_source/plugins/tabletools/dialogs/tableCell.js
===================================================================
--- /CKEditor/trunk/_source/plugins/tabletools/dialogs/tableCell.js	(revision 3333)
+++ /CKEditor/trunk/_source/plugins/tabletools/dialogs/tableCell.js	(revision 3333)
@@ -0,0 +1,331 @@
+/*
+Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+CKEDITOR.dialog.add( 'cellProperties', function( editor )
+	{
+		var langTable = editor.lang.table;
+		var langCell = langTable.cell;
+		var langCommon = editor.lang.common;
+		var validate = CKEDITOR.dialog.validate;
+		var widthPattern = /^(\d+(?:\.\d+)?)(px|%)$/,
+			heightPattern = /^(\d+(?:\.\d+)?)px$/;
+		var bind = CKEDITOR.tools.bind;
+
+		function spacer()
+		{
+			return { type : 'html', html : '&nbsp;' }
+		}
+
+		return {
+			title : langCell.title,
+			minWidth : 480,
+			minHeight : 140,
+			contents : [
+				{
+					id : 'info',
+					label : langCell.title,
+					accessKey : 'I',
+					elements :
+					[
+						{
+							type : 'hbox',
+							widths : [ '45%', '10%', '45%' ],
+							children :
+							[
+								{
+									type : 'vbox',
+									padding : 0,
+									children :
+									[
+										{
+											type : 'hbox',
+											widths : [ '70%', '30%' ],
+											children :
+											[
+												{
+													type : 'text',
+													id : 'width',
+													label : langTable.width,
+													widths : [ '71%', '29%' ],
+													labelLayout : 'horizontal',
+													validate : validate[ 'number' ]( langCell.invalidWidth ),
+													setup : function( selectedCell )
+													{
+														var widthMatch = widthPattern.exec( selectedCell.$.style.width );
+														if ( widthMatch )
+															this.setValue( widthMatch[1] );
+													},
+													commit : function( selectedCell )
+													{
+														var unit = this.getDialog().getValueOf( 'info', 'widthType' );
+														if ( this.getValue() != '' )
+															selectedCell.$.style.width = this.getValue() + unit;
+														else
+															selectedCell.$.style.width = '';
+													},
+													'default' : ''
+												},
+												{
+													type : 'select',
+													id : 'widthType',
+													labelLayout : 'horizontal',
+													widths : [ '0%', '100%' ],
+													label : '',
+													'default' : 'px',
+													items :
+													[
+														[ langTable.widthPx, 'px' ],
+														[ langTable.widthPc, '%' ]
+													],
+													setup : function( selectedCell )
+													{
+														var widthMatch = widthPattern.exec( selectedCell.$.style.width );
+														if ( widthMatch )
+															this.setValue( widthMatch[2] );
+													}
+												}
+											]
+										},
+										{
+											type : 'hbox',
+											widths : [ '70%', '30%' ],
+											children :
+											[
+												{
+													type : 'text',
+													id : 'height',
+													label : langTable.height,
+													'default' : '',
+													widths : [ '71%', '29%' ],
+													labelLayout : 'horizontal',
+													validate : validate[ 'number' ]( langCell.invalidHeight ),
+													setup : function( selectedCell )
+													{
+														var heightMatch = heightPattern.exec( selectedCell.$.style.height );
+														if ( heightMatch )
+															this.setValue( heightMatch[1] );
+													},
+													commit : function( selectedCell )
+													{
+														if ( this.getValue() != '' )
+															selectedCell.$.style.height = this.getValue() + 'px';
+														else
+															selectedCell.$.style.height = '';
+													}
+												},
+												{
+													type : 'html',
+													html : langTable.widthPx
+												}
+											]
+										},
+										spacer(),
+										{
+											type : 'select',
+											id : 'wordWrap',
+											labelLayout : 'horizontal',
+											label : langCell.wordWrap,
+											widths : [ '50%', '50%' ],
+											'default' : 'yes',
+											items :
+											[
+												[ langCell.yes, 'yes' ],
+												[ langCell.no, 'no' ]
+											],
+											commit : function( selectedCell )
+											{
+												if ( this.getValue() == 'no' )
+													selectedCell.setAttribute( 'noWrap', 'nowrap' );
+												else
+													selectedCell.removeAttribute( 'noWrap' );
+											}
+										},
+										spacer(),
+										{
+											type : 'select',
+											id : 'hAlign',
+											labelLayout : 'horizontal',
+											label : langCell.hAlign,
+											widths : [ '50%', '50%' ],
+											'default' : '',
+											items :
+											[
+												[ langCommon.notSet, '' ],
+												[ langTable.alignLeft, 'left' ],
+												[ langTable.alignCenter, 'center' ],
+												[ langTable.alignRight, 'right' ]
+											],
+											setup : function( selectedCell )
+											{
+												this.setValue( selectedCell.getAttribute( 'align' ) || '' );
+											},
+											commit : function( selectedCell )
+											{
+												if ( this.getValue() )
+													selectedCell.setAttribute( 'align', this.getValue() );
+												else
+													selectedCell.removeAttribute( 'align' );
+											}
+										},
+										{
+											type : 'select',
+											id : 'vAlign',
+											labelLayout : 'horizontal',
+											label : langCell.vAlign,
+											widths : [ '50%', '50%' ],
+											'default' : '',
+											items :
+											[
+												[ langCommon.notSet, '' ],
+												[ langCell.alignTop, 'top' ],
+												[ langCell.alignMiddle, 'middle' ],
+												[ langCell.alignBottom, 'bottom' ],
+												[ langCell.alignBaseline, 'baseline' ]
+											],
+											setup : function( selectedCell )
+											{
+												this.setValue( selectedCell.getAttribute( 'vAlign' ) || '' );
+											},
+											commit : function( selectedCell )
+											{
+												if ( this.getValue() )
+													selectedCell.setAttribute( 'vAlign', this.getValue() );
+												else
+													selectedCell.removeAttribute( 'vAlign' );
+											}
+										},
+
+									]
+								},
+								spacer(),
+								{
+									type : 'vbox',
+									padding : 0,
+									children :
+									[
+										{
+											type : 'select',
+											id : 'cellType',
+											label : langCell.cellType,
+											labelLayout : 'horizontal',
+											widths : [ '50%', '50%' ],
+											'default' : 'td',
+											items :
+											[
+												[ langCell.data, 'td' ],
+												[ langCell.header, 'th' ]
+											],
+											setup : function( selectedCell )
+											{
+												this.setValue( selectedCell.getName() );
+											},
+											commit : function( selectedCell )
+											{
+												selectedCell.renameNode( this.getValue() );
+											}
+										},
+										spacer(),
+										{
+											type : 'text',
+											id : 'rowSpan',
+											label : langCell.rowSpan,
+											labelLayout : 'horizontal',
+											widths : [ '50%', '50%' ],
+											'default' : '',
+											validate : validate.integer( langCell.invalidRowSpan ),
+											setup : function( selectedCell )
+											{
+												this.setValue( selectedCell.getAttribute( 'rowSpan' ) || '' );
+											},
+											commit : function( selectedCell )
+											{
+												if ( this.getValue() )
+													selectedCell.setAttribute( 'rowSpan', this.getValue() );
+												else
+													selectedCell.removeAttribute( 'rowSpan' );
+											}
+										},
+										{
+											type : 'text',
+											id : 'colSpan',
+											label : langCell.colSpan,
+											labelLayout : 'horizontal',
+											widths : [ '50%', '50%' ],
+											'default' : '',
+											validate : validate.integer( langCell.invalidColSpan ),
+											setup : function( selectedCell )
+											{
+												this.setValue( selectedCell.getAttribute( 'colSpan' ) || '' );
+											},
+											commit : function( selectedCell )
+											{
+												if ( this.getValue() )
+													selectedCell.setAttribute( 'colSpan', this.getValue() );
+												else
+													selectedCell.removeAttribute( 'colSpan' );
+											}
+										},
+										spacer(),
+										{
+											type : 'text',
+											id : 'bgColor',
+											label : langCell.bgColor,
+											labelLayout : 'horizontal',
+											widths : [ '50%', '50%' ],
+											'default' : '',
+											setup : function( selectedCell )
+											{
+												this.setValue( selectedCell.getAttribute( 'bgColor' ) || '' );
+											},
+											commit : function( selectedCell )
+											{
+												if ( this.getValue() )
+													selectedCell.setAttribute( 'bgColor', this.getValue() );
+												else
+													selectedCell.removeAttribute( 'bgColor' );
+											}
+										},
+										{
+											type : 'text',
+											id : 'borderColor',
+											label : langCell.borderColor,
+											labelLayout : 'horizontal',
+											widths : [ '50%', '50%' ],
+											'default' : '',
+											setup : function( selectedCell )
+											{
+												this.setValue( selectedCell.getAttribute( 'borderColor' ) || '' );
+											},
+											commit : function( selectedCell )
+											{
+												if ( this.getValue() )
+													selectedCell.setAttribute( 'borderColor', this.getValue() );
+												else
+													selectedCell.removeAttribute( 'borderColor' );
+											}
+										}
+									]
+								}
+							]
+						}
+					]
+				}
+			],
+			onShow : function()
+			{
+				// Get the selected table cell.
+				var startElement = editor.getSelection().getStartElement();
+				this.cell = startElement.getAscendant( 'td', true ) || startElement.getAscendant( 'th', true );
+
+				// Call setupContent().
+				this.setupContent( this.cell );
+			},
+			onOk : function()
+			{
+				// Call commitContent().
+				this.commitContent( this.cell );
+			}
+		};
+	} );
Index: /CKEditor/trunk/_source/plugins/tabletools/plugin.js
===================================================================
--- /CKEditor/trunk/_source/plugins/tabletools/plugin.js	(revision 3333)
+++ /CKEditor/trunk/_source/plugins/tabletools/plugin.js	(revision 3333)
@@ -0,0 +1,650 @@
+/*
+Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+
+(function()
+{
+	function removeRawAttribute( $node, attr )
+	{
+		if ( CKEDITOR.env.ie )
+			$node.removeAttribute( attr );
+		else
+			delete $node[ attr ];
+	}
+
+	var cellNodeRegex = /^(?:td|th)$/;
+	function getSelectedCells( selection )
+	{
+		var ranges = selection.getRanges();
+		var retval = [];
+
+		for ( var i = 0 ; i < ranges.length ; i++ )
+		{
+			var range = ranges[ i ];
+			var boundaryNodes = range.getBoundaryNodes();
+			var currentNode = boundaryNodes.startNode;
+			var endNode = boundaryNodes.endNode;
+			var startCell = currentNode.getAscendant( 'td', true ) || currentNode.getAscendant( 'th', true );
+
+			if ( startCell )
+				retval.push( startCell );
+
+			if ( range.collapsed || currentNode.equals( endNode ) )
+				continue;
+
+			while ( !( currentNode = currentNode.getNextSourceNode() ).equals( endNode ) )
+			{
+				if ( currentNode.type == CKEDITOR.NODE_ELEMENT && cellNodeRegex.test( currentNode.getName() ) )
+					retval.push( currentNode );
+			}
+		}
+
+		return retval;
+	}
+
+	function createTableMap( $refCell )
+	{
+		var refCell = new CKEDITOR.dom.element( $refCell );
+		var $table = ( refCell.getName() == 'table' ? $refCell : refCell.getAscendant( 'table' ) ).$;
+		var $rows = $table.rows;
+		
+		// Row and column counters.
+		var r = -1;
+		var map = [];
+		for ( var i = 0 ; i < $rows.length ; i++ )
+		{
+			r++;
+			if ( !map[ r ] )
+				map[ r ] = [];
+
+			var c = -1;
+
+			for ( var j = 0 ; j < $rows[ i ].cells.length ; j++ )
+			{
+				var $cell = $row[ i ].cells[ j ];
+
+				c++;
+				while ( map[ r ][ c ] )
+					c++;
+
+				var colSpan = isNaN( $cell.colSpan ) ? 1 : $cell.colSpan;
+				var rowSpan = isNaN( $cell.rowSpan ) ? 1 : $cell.rowSpan;
+
+				for ( var rs = 0 ; rs < rowSpan ; rs++ )
+				{
+					if ( !map[ r + rs ] )
+						map[ r + rs ] = [];
+
+					for ( var cs = 0 ; cs < colspan ; cs++ )
+						map [ r + rs ][ c + cs ] = $rows[ i ].cells[ j ];
+				}
+
+				c += colSpan - 1;
+			}
+		}
+
+		return map;
+	}
+
+	function installTableMap( tableMap, $table )
+	{
+		/*
+		 * IE BUG: rowSpan is always 1 in IE if the cell isn't attached to a row. So
+		 * store is separately in another attribute. (#1917)
+		 */
+		var rowSpanAttr = CKEDITOR.env.ie ? '_cke_rowspan' : 'rowSpan';
+
+		/*
+		 * Disconnect all the cells in tableMap from their parents, set all colSpan
+		 * and rowSpan attributes to 1.
+		 */
+		for ( var i = 0 ; i < tableMap.length ; i++ )
+		{
+			for ( var j = 0 ; j < tableMap[ i ].length ; j++ )
+			{
+				var $cell = tableMap[ i ][ j ];
+				if ( $cell.parentNode ) 
+					$cell.parentNode.removeChild( $cell );
+				$cell.colSpan = $cell[ rowSpanAttr ] = 1;
+			}
+		}
+
+		// Scan by rows and set colSpan.
+		var maxCol = 0;
+		for ( var i = 0 ; i < tableMap.length ; i++ )
+		{
+			for ( var j = 0 ; j < tableMap[ i ].length ; j++ )
+			{
+				var $cell = tableMap[ i ][ j ];
+				if ( !$cell )
+					continue;
+				if ( j > maxCol )
+					maxCol = j;
+				if ( $cell[ '_cke_colScanned' ] )
+					continue;
+				if ( tableMap[ i ][ j - 1 ] == $cell )
+					$cell.colSpan++;
+				if ( tableMap[ i ][ j + 1 ] != $cell )
+					$cell[ '_cke_colScanned' ] = 1;
+			}
+		}
+
+		// Scan by columns and set rowSpan.
+		for ( var i = 0 ; i <= maxCol ; i++ )
+		{
+			for ( var j = 0 ; j < tableMap.length ; j++ )
+			{
+				if ( !tableMap[ j ] )
+					continue;
+				var $cell = tableMap[ j ][ i ];
+				if ( !$cell || $cell[ '_cke_rowScanned' ] )
+					continue;
+				if ( tableMap[ j - 1 ] && tableMap[ j - 1 ][ i ] == $cell )
+					$cell[ rowSpanAttr ]++;
+				if ( !tableMap[ j + 1 ] || tableMap[ j + 1 ][ i ] != $cell  )
+					$cell[ '_cke_rowScanned' ] = 1;
+			}
+		}
+
+		// Clear all temporary flags.
+		for ( var i = 0 ; i < tableMap.length ; i++ )
+		{
+			for ( var j = 0 ; j < tableMap[ i ].length ; j++ )
+			{
+				var $cell = tableMap[ i ][ j ];
+				removeRawAttribute( $cell, '_cke_colScanned' );
+				removeRawAttribute( $cell, '_cke_rowScanned' );
+			}
+		}
+
+		// Insert physical rows and columns to table.
+		for ( var i = 0 ; i < tableMap.length ; i++ )
+		{
+			var $row = $table.ownerDocument.createElement( 'tr' );
+			for ( var j = 0 ; j < tableMap[ i ].length ; )
+			{
+				var $cell = tableMap[ i ][ j ];
+				if ( tableMap[ i - 1 ] && tableMap[ i - 1 ][ j ] == $cell )
+				{
+					j += $cell.colSpan;
+					continue;
+				}
+				$row.appendChild( $cell );
+				if ( rowSpanAttr != 'rowSpan' )
+				{
+					$cell.rowSpan = cell[ rowSpanAttr ];
+					$cell.removeAttribute( rowSpanAttr );
+				}
+				j += $cell.colSpan;
+				if ( $cell.colSpan == 1 )
+					$cell.removeAttribute( 'colSpan' );
+				if ( $cell.rowSpan == 1 )
+					$cell.removeAttribute( 'rowSpan' );
+			}
+
+			if ( CKEDITOR.env.ie )
+				$table.rows[ i ].replaceNode( $row );
+			else
+			{
+				var dest = new CKEDITOR.dom.element( $table.rows[ i ] );
+				var src = new CKEDITOR.dom.element( $row );
+				dest.setHtml( '' );
+				src.moveChildren( dest );
+			}
+		}
+	}
+
+	function clearRow( $tr )
+	{
+		// Get the array of row's cells.
+		var $cells = $tr.cells;
+
+		// Empty all cells.
+		for ( var i = 0 ; i < $cells.length ; i++ )
+		{
+			$cells[ i ].innerHTML = '';
+
+			if ( !CKEDITOR.env.ie )
+				( new CKEDITOR.dom.element( $cells[ i ] ) ).appendBogus();
+		}
+	}
+
+	function insertRow( selection, insertBefore )
+	{
+		// Get the row where the selection is placed in.
+		var row = selection.getStartElement().getAscendant( 'tr' );
+		if ( !row )
+			return;
+
+		// Create a clone of the row.
+		var newRow = row.clone( true );
+
+		// Insert the new row before of it.
+		newRow.insertBefore( row );
+
+		// Clean one of the rows to produce the illusion of inserting an empty row
+		// before or after.
+		clearRow( insertBefore ? newRow.$ : row.$ );
+	}
+
+	function deleteRows( selectionOrRow )
+	{
+		if ( selectionOrRow instanceof CKEDITOR.dom.selection )
+		{
+			var cells = getSelectedCells( selectionOrRow );
+			var rowsToDelete = [];
+
+			// Queue up the rows - it's possible and likely that we have duplicates.
+			for ( var i = 0 ; i < cells.length ; i++ )
+			{
+				var row = cells[ i ].getParent();
+				rowsToDelete[ row.$.rowIndex ] = row;
+			}
+
+			for ( var i = rowsToDelete.length ; i >= 0 ; i-- )
+			{
+				if ( rowsToDelete[ i ] )
+					deleteRows( rowsToDelete[ i ] );
+			}
+		}
+		else if ( selectionOrRow instanceof CKEDITOR.dom.element )
+		{
+			var table = selectionOrRow.getAscendant( 'table' );
+
+			if ( table.$.rows.length == 1 )
+				table.remove();
+			else
+				selectionOrRow.remove();
+		}
+	}
+
+	function insertColumn( selection, insertBefore )
+	{
+		// Get the cell where the selection is placed in.
+		var startElement = selection.getStartElement();
+		var cell = startElement.getAscendant( 'td', true ) || startElement.getAscendant( 'th', true );
+
+		if ( !cell )
+			return;
+
+		// Get the cell's table.
+		var table = cell.getAscendant( 'table' );
+		var cellIndex = cell.$.cellIndex;
+
+		// Loop through all rows available in the table.
+		for ( var i = 0 ; i < table.$.rows.length ; i++ )
+		{
+			var $row = table.$.rows[ i ];
+
+			// If the row doesn't have enough cells, ignore it.
+			if ( $row.cells.length < ( cellIndex + 1 ) )
+				continue;
+
+			cell = new CKEDITOR.dom.element( $row.cells[ cellIndex ].cloneNode( false ) );
+
+			if ( !CKEDITOR.env.ie )
+				cell.appendBogus();
+
+			// Get back the currently selected cell.
+			var baseCell = new CKEDITOR.dom.element( $row.cells[ cellIndex ] );
+			if ( insertBefore )
+				cell.insertBefore( baseCell );
+			else
+				cell.insertAfter( baseCell );
+		}
+	}
+
+	function deleteColumns( selectionOrCell )
+	{
+		if ( selectionOrCell instanceof CKEDITOR.dom.selection )
+		{
+			var colsToDelete = getSelectedCells( selectionOrCell );
+			for ( var i = colsToDelete.length ; i >= 0 ; i-- )
+			{
+				if ( colsToDelete[ i ] )
+					deleteColumns( colsToDelete[ i ] );
+			}
+		}
+		else if ( selectionOrCell instanceof CKEDITOR.dom.element )
+		{
+			// Get the cell's table.
+			var table = selectionOrCell.getAscendant( 'table' );
+
+			// Get the cell index.
+			var cellIndex = selectionOrCell.$.cellIndex;
+
+			/*
+			 * Loop through all rows from down to up, coz it's possible that some rows
+			 * will be deleted.
+			 */
+			for ( var i = table.$.rows.length - 1 ; i >= 0 ; i-- )
+			{
+				// Get the row.
+				var row = new CKEDITOR.dom.element( table.$.rows[ i ] );
+
+				// If the cell to be removed is the first one and the row has just one cell.
+				if ( cellIndex == 0 && row.$.cells.length == 1 )
+				{
+					deleteRows( row );
+					continue;
+				}
+
+				// Else, just delete the cell.
+				if ( row.$.cells[ cellIndex ] )
+					row.$.removeChild( row.$.cells[ cellIndex ] );
+			}
+		}
+	}
+
+	function insertCell( selection, insertBefore )
+	{
+		var startElement = selection.getStartElement();
+		var cell = startElement.getAscendant( 'td', true ) || startElement.getAscendant( 'th', true );
+
+		if ( !cell )
+			return;
+
+		// Create the new cell element to be added.
+		var newCell = cell.clone();
+		if ( !CKEDITOR.env.ie )
+			newCell.appendBogus();
+
+		if ( insertBefore )
+			newCell.insertBefore( cell );
+		else
+			newCell.insertAfter( cell );
+	}
+
+	function deleteCells( selectionOrCell )
+	{
+		if ( selectionOrCell instanceof CKEDITOR.dom.selection )
+		{
+			var cellsToDelete = getSelectedCells( selectionOrCell );
+			for ( var i = cellsToDelete.length - 1 ; i >= 0 ; i-- )
+				deleteCells( cellsToDelete[ i ] );
+		}
+		else if ( selectionOrCell instanceof CKEDITOR.dom.element )
+		{
+			if ( selectionOrCell.getParent().getChildCount() == 1 )
+				selectionOrCell.getParent().remove();
+			else
+				selectionOrCell.remove();
+		}
+	}
+
+	CKEDITOR.plugins.add( 'tabletools',
+	{
+		init : function( editor )
+		{
+			var lang = editor.lang.table;
+
+			editor.addCommand( 'cellProperties', new CKEDITOR.dialogCommand( 'cellProperties' ) );
+			CKEDITOR.dialog.add( 'cellProperties', this.path + 'dialogs/tableCell.js' );
+
+			editor.addCommand( 'tableDelete',
+				{
+					exec : function( editor )
+					{
+						var selection = editor.getSelection();
+						var startElement = selection && selection.getStartElement();
+						var table = startElement && startElement.getAscendant( 'table', true );
+						
+						if ( !table )
+							return;
+
+						// Maintain the selection point at where the table was deleted.
+						selection.selectElement( table );
+						var range = selection.getRanges()[0];
+						range.collapse();
+						selection.selectRanges( [ range ] );
+
+						// If the table's parent has only one child, remove it as well.
+						if ( table.getParent().getChildCount() == 1 )
+							table.getParent().remove();
+						else
+							table.remove();
+					}
+				} );
+
+			editor.addCommand( 'rowDelete',
+				{
+					exec : function( editor )
+					{
+						var selection = editor.getSelection();
+						deleteRows( selection );
+					}
+				} );
+
+			editor.addCommand( 'rowInsertBefore',
+				{
+					exec : function( editor )
+					{
+						var selection = editor.getSelection();
+						insertRow( selection, true );
+					}
+				} );
+
+			editor.addCommand( 'rowInsertAfter',
+				{
+					exec : function( editor )
+					{
+						var selection = editor.getSelection();
+						insertRow( selection );
+					}
+				} );
+
+			editor.addCommand( 'columnDelete',
+				{
+					exec : function( editor )
+					{
+						var selection = editor.getSelection();
+						deleteColumns( selection );
+					}
+				} );
+
+			editor.addCommand( 'columnInsertBefore',
+				{
+					exec : function( editor )
+					{
+						var selection = editor.getSelection();
+						insertColumn( selection, true );
+					}
+				} );
+
+			editor.addCommand( 'columnInsertAfter',
+				{
+					exec : function( editor )
+					{
+						var selection = editor.getSelection();
+						insertColumn( selection );
+					}
+				} );
+
+			editor.addCommand( 'cellDelete',
+				{
+					exec : function( editor )
+					{
+						var selection = editor.getSelection();
+						deleteCells( selection );
+					}
+				} );
+
+			editor.addCommand( 'cellInsertBefore',
+				{
+					exec : function( editor )
+					{
+						var selection = editor.getSelection();
+						insertCell( selection, true );
+					}
+				} );
+
+			editor.addCommand( 'cellInsertAfter',
+				{
+					exec : function( editor )
+					{
+						var selection = editor.getSelection();
+						insertCell( selection );
+					}
+				} );
+
+			// If the "menu" plugin is loaded, register the menu items.
+			if ( editor.addMenuItems )
+			{
+				editor.addMenuItems(
+					{
+						tablecell :
+						{
+							label : lang.cell.menu,
+							group : 'tablecell',
+							order : 1,
+							getItems : function()
+							{
+								var cells = getSelectedCells( editor.getSelection() );
+
+								return {
+									tablecell_insertBefore : CKEDITOR.TRISTATE_OFF,
+									tablecell_insertAfter : CKEDITOR.TRISTATE_OFF,
+									tablecell_delete : CKEDITOR.TRISTATE_OFF,
+									tablecell_properties : cells.length == 1 ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED
+								};
+							}
+						},
+
+						tablecell_insertBefore :
+						{
+							label : lang.cell.insertBefore,
+							group : 'tablecell',
+							command : 'cellInsertBefore',
+							order : 5
+						},
+
+						tablecell_insertAfter :
+						{
+							label : lang.cell.insertAfter,
+							group : 'tablecell',
+							command : 'cellInsertAfter',
+							order : 10
+						},
+
+						tablecell_delete :
+						{
+							label : lang.cell.deleteCell,
+							group : 'tablecell',
+							command : 'cellDelete',
+							order : 15
+						},
+
+						tablecell_properties :
+						{
+							label : lang.cell.title,
+							group : 'tablecellproperties',
+							command : 'cellProperties',
+							order : 20
+						},
+
+						tablerow :
+						{
+							label : lang.row.menu,
+							group : 'tablerow',
+							order : 1,
+							getItems : function()
+							{
+								return {
+									tablerow_insertBefore : CKEDITOR.TRISTATE_OFF,
+									tablerow_insertAfter : CKEDITOR.TRISTATE_OFF,
+									tablerow_delete : CKEDITOR.TRISTATE_OFF
+								};
+							}
+						},
+
+						tablerow_insertBefore :
+						{
+							label : lang.row.insertBefore,
+							group : 'tablerow',
+							command : 'rowInsertBefore',
+							order : 5
+						},
+
+						tablerow_insertAfter :
+						{
+							label : lang.row.insertAfter,
+							group : 'tablerow',
+							command : 'rowInsertAfter',
+							order : 10
+						},
+
+						tablerow_delete :
+						{
+							label : lang.row.deleteRow,
+							group : 'tablerow',
+							command : 'rowDelete',
+							order : 15
+						},
+
+						tablecolumn :
+						{
+							label : lang.column.menu,
+							group : 'tablecolumn',
+							order : 1,
+							getItems : function()
+							{
+								return {
+									tablecolumn_insertBefore : CKEDITOR.TRISTATE_OFF,
+									tablecolumn_insertAfter : CKEDITOR.TRISTATE_OFF,
+									tablecolumn_delete : CKEDITOR.TRISTATE_OFF
+								};
+							}
+						},
+
+						tablecolumn_insertBefore :
+						{
+							label : lang.column.insertBefore,
+							group : 'tablecolumn',
+							command : 'columnInsertBefore',
+							order : 5
+						},
+
+						tablecolumn_insertAfter :
+						{
+							label : lang.column.insertAfter,
+							group : 'tablecolumn',
+							command : 'columnInsertAfter',
+							order : 10
+						},
+
+						tablecolumn_delete :
+						{
+							label : lang.column.deleteColumn,
+							group : 'tablecolumn',
+							command : 'columnDelete',
+							order : 15
+						}
+					});
+			}
+
+			// If the "contextmenu" plugin is laoded, register the listeners.
+			if ( editor.contextMenu )
+			{
+				editor.contextMenu.addListener( function( element, selection )
+					{
+						if ( !element )
+							return null;
+
+							var isCell	= !element.is( 'table' ) && element.hasAscendant( 'table' ) ;
+
+							if ( isCell )
+							{
+								return {
+									tablecell : CKEDITOR.TRISTATE_OFF,
+									tablerow : CKEDITOR.TRISTATE_OFF,
+									tablecolumn : CKEDITOR.TRISTATE_OFF
+								};
+							}
+
+							return null;
+					} );
+			}
+		}
+	} );
+})()
