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 pxUnit = CKEDITOR.tools.cssLength,
  9 		needsIEHacks = CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks || CKEDITOR.env.version < 7 );
 10 
 11 	function getWidth( el )
 12 	{
 13 		return CKEDITOR.env.ie ? el.$.clientWidth : parseInt( el.getComputedStyle( 'width' ), 10 );
 14 	}
 15 
 16 	function getBorderWidth( element, side )
 17 	{
 18 		var computed = element.getComputedStyle( 'border-' + side + '-width' ),
 19 			borderMap =
 20 			{
 21 				thin: '0px',
 22 				medium: '1px',
 23 				thick: '2px'
 24 			};
 25 
 26 		if ( computed.indexOf( 'px' ) < 0 )
 27 		{
 28 			// look up keywords
 29 			if ( computed in borderMap && element.getComputedStyle( 'border-style' ) != 'none' )
 30 				computed = borderMap[ computed ];
 31 			else
 32 				computed = 0;
 33 		}
 34 
 35 		return parseInt( computed, 10 );
 36 	}
 37 
 38 	// Gets the table row that contains the most columns.
 39 	function getMasterPillarRow( table )
 40 	{
 41 		var $rows = table.$.rows,
 42 			maxCells = 0, cellsCount,
 43 			$elected, $tr;
 44 
 45 		for ( var i = 0, len = $rows.length ; i < len; i++ )
 46 		{
 47 			$tr = $rows[ i ];
 48 			cellsCount = $tr.cells.length;
 49 
 50 			if ( cellsCount > maxCells )
 51 			{
 52 				maxCells = cellsCount;
 53 				$elected = $tr;
 54 			}
 55 		}
 56 
 57 		return $elected;
 58 	}
 59 
 60 	function buildTableColumnPillars( table )
 61 	{
 62 		var pillars = [],
 63 			pillarIndex = -1,
 64 			rtl = ( table.getComputedStyle( 'direction' ) == 'rtl' );
 65 
 66 		// Get the raw row element that cointains the most columns.
 67 		var $tr = getMasterPillarRow( table );
 68 
 69 		// Get the tbody element and position, which will be used to set the
 70 		// top and bottom boundaries.
 71 		var tbody = new CKEDITOR.dom.element( table.$.tBodies[ 0 ] ),
 72 			tbodyPosition = tbody.getDocumentPosition();
 73 
 74 		// Loop thorugh all cells, building pillars after each one of them.
 75 		for ( var i = 0, len = $tr.cells.length ; i < len ; i++ )
 76 		{
 77 			// Both the current cell and the successive one will be used in the
 78 			// pillar size calculation.
 79 			var td = new CKEDITOR.dom.element( $tr.cells[ i ] ),
 80 				nextTd = $tr.cells[ i + 1 ] && new CKEDITOR.dom.element( $tr.cells[ i + 1 ] );
 81 
 82 			pillarIndex += td.$.colSpan || 1;
 83 
 84 			// Calculate the pillar boundary positions.
 85 			var pillarLeft, pillarRight, pillarWidth;
 86 
 87 			var x = td.getDocumentPosition().x;
 88 
 89 			// Calculate positions based on the current cell.
 90 			rtl ?
 91 				pillarRight = x + getBorderWidth( td, 'left' ) :
 92 				pillarLeft  = x + td.$.offsetWidth - getBorderWidth( td, 'right' );
 93 
 94 			// Calculate positions based on the next cell, if available.
 95 			if ( nextTd )
 96 			{
 97 				x =  nextTd.getDocumentPosition().x;
 98 
 99 				rtl ?
100 					pillarLeft	= x + nextTd.$.offsetWidth - getBorderWidth( nextTd, 'right' ) :
101 					pillarRight	= x + getBorderWidth( nextTd, 'left' );
102 			}
103 			// Otherwise calculate positions based on the table (for last cell).
104 			else
105 			{
106 				x =  table.getDocumentPosition().x;
107 
108 				rtl ?
109 					pillarLeft	= x :
110 					pillarRight	= x + table.$.offsetWidth;
111 			}
112 
113 			pillarWidth = Math.max( pillarRight - pillarLeft, 3 );
114 
115 			// The pillar should reflects exactly the shape of the hovered
116 			// column border line.
117 			pillars.push( {
118 				table : table,
119 				index : pillarIndex,
120 				x : pillarLeft,
121 				y : tbodyPosition.y,
122 				width : pillarWidth,
123 				height : tbody.$.offsetHeight,
124 				rtl : rtl } );
125 		}
126 
127 		return pillars;
128 	}
129 
130 	function getPillarAtPosition( pillars, positionX )
131 	{
132 		for ( var i = 0, len = pillars.length ; i < len ; i++ )
133 		{
134 			var pillar = pillars[ i ];
135 
136 			if ( positionX >= pillar.x && positionX <= ( pillar.x + pillar.width ) )
137 				return pillar;
138 		}
139 
140 		return null;
141 	}
142 
143 	function cancel( evt )
144 	{
145 		( evt.data || evt ).preventDefault();
146 	}
147 
148 	function columnResizer( editor )
149 	{
150 		var pillar,
151 			document,
152 			resizer,
153 			isResizing,
154 			startOffset,
155 			currentShift;
156 
157 		var leftSideCells, rightSideCells, leftShiftBoundary, rightShiftBoundary;
158 
159 		function detach()
160 		{
161 			pillar = null;
162 			currentShift = 0;
163 			isResizing = 0;
164 
165 			document.removeListener( 'mouseup', onMouseUp );
166 			resizer.removeListener( 'mousedown', onMouseDown );
167 			resizer.removeListener( 'mousemove', onMouseMove );
168 
169 			document.getBody().setStyle( 'cursor', 'auto' );
170 
171 			// Hide the resizer (remove it on IE7 - #5890).
172 			needsIEHacks ? resizer.remove() : resizer.hide();
173 		}
174 
175 		function resizeStart()
176 		{
177 			// Before starting to resize, figure out which cells to change
178 			// and the boundaries of this resizing shift.
179 
180 			var columnIndex = pillar.index,
181 				map = CKEDITOR.tools.buildTableMap( pillar.table ),
182 				leftColumnCells = [],
183 				rightColumnCells = [],
184 				leftMinSize = Number.MAX_VALUE,
185 				rightMinSize = leftMinSize,
186 				rtl = pillar.rtl;
187 
188 			for ( var i = 0, len = map.length ; i < len ; i++ )
189 			{
190 				var row			= map[ i ],
191 					leftCell	= row[ columnIndex + ( rtl ? 1 : 0 ) ],
192 					rightCell	= row[ columnIndex + ( rtl ? 0 : 1 ) ];
193 
194 				leftCell	= leftCell && new CKEDITOR.dom.element( leftCell );
195 				rightCell	= rightCell && new CKEDITOR.dom.element( rightCell );
196 
197 				if ( !leftCell || !rightCell || !leftCell.equals( rightCell ) )
198 				{
199 					leftCell && ( leftMinSize = Math.min( leftMinSize, getWidth( leftCell ) ) );
200 					rightCell && ( rightMinSize = Math.min( rightMinSize, getWidth( rightCell ) ) );
201 
202 					leftColumnCells.push( leftCell );
203 					rightColumnCells.push( rightCell );
204 				}
205 			}
206 
207 			// Cache the list of cells to be resized.
208 			leftSideCells = leftColumnCells;
209 			rightSideCells = rightColumnCells;
210 
211 			// Cache the resize limit boundaries.
212 			leftShiftBoundary =  pillar.x - leftMinSize;
213 			rightShiftBoundary = pillar.x + rightMinSize;
214 
215 			resizer.setOpacity( 0.5 );
216 			startOffset = parseInt( resizer.getStyle( 'left' ), 10 );
217 			currentShift = 0;
218 			isResizing = 1;
219 
220 			resizer.on( 'mousemove', onMouseMove );
221 
222 			// Prevent the native drag behavior otherwise 'mousemove' won't fire.
223 			document.on( 'dragstart', cancel );
224 		}
225 
226 		function resizeEnd()
227 		{
228 			isResizing = 0;
229 
230 			resizer.setOpacity( 0 );
231 
232 			currentShift && resizeColumn();
233 
234 			var table = pillar.table;
235 			setTimeout( function () { table.removeCustomData( '_cke_table_pillars' ); }, 0 );
236 
237 			document.removeListener( 'dragstart', cancel );
238 		}
239 
240 		function resizeColumn()
241 		{
242 			var rtl = pillar.rtl,
243 				cellsCount = rtl ? rightSideCells.length : leftSideCells.length;
244 
245 			// Perform the actual resize to table cells, only for those by side of the pillar.
246 			for ( var i = 0 ; i < cellsCount ; i++ )
247 			{
248 				var leftCell = leftSideCells[ i ],
249 					rightCell = rightSideCells[ i ],
250 					table = pillar.table;
251 
252 				// Defer the resizing to avoid any interference among cells.
253 				CKEDITOR.tools.setTimeout(
254 					function( leftCell, leftOldWidth, rightCell, rightOldWidth, tableWidth, sizeShift )
255 					{
256 						leftCell && leftCell.setStyle( 'width', pxUnit( Math.max( leftOldWidth + sizeShift, 0 ) ) );
257 						rightCell && rightCell.setStyle( 'width', pxUnit( Math.max( rightOldWidth - sizeShift, 0 ) ) );
258 
259 						// If we're in the last cell, we need to resize the table as well
260 						if ( tableWidth )
261 							table.setStyle( 'width', pxUnit( tableWidth + sizeShift * ( rtl ? -1 : 1 ) ) );
262 					}
263 					, 0,
264 					this, [
265 						leftCell, leftCell && getWidth( leftCell ),
266 						rightCell, rightCell && getWidth( rightCell ),
267 						( !leftCell || !rightCell ) && ( getWidth( table ) + getBorderWidth( table, 'left' ) + getBorderWidth( table, 'right' ) ),
268 						currentShift ] );
269 			}
270 		}
271 
272 		function onMouseDown( evt )
273 		{
274 			cancel( evt );
275 
276 			resizeStart();
277 
278 			document.on( 'mouseup', onMouseUp, this );
279 		}
280 
281 		function onMouseUp( evt )
282 		{
283 			evt.removeListener();
284 
285 			resizeEnd();
286 		}
287 
288 		function onMouseMove( evt )
289 		{
290 			move( evt.data.getPageOffset().x );
291 		}
292 
293 		document = editor.document;
294 
295 		resizer = CKEDITOR.dom.element.createFromHtml(
296 			'<div data-cke-temp=1 contenteditable=false unselectable=on '+
297 			'style="position:absolute;cursor:col-resize;filter:alpha(opacity=0);opacity:0;' +
298 				'padding:0;background-color:#004;background-image:none;border:0px none;z-index:10"></div>', document );
299 
300 		// Except on IE6/7 (#5890), place the resizer after body to prevent it
301 		// from being editable.
302 		if ( !needsIEHacks )
303 			document.getDocumentElement().append( resizer );
304 
305 		this.attachTo = function( targetPillar )
306 		{
307 			// Accept only one pillar at a time.
308 			if ( isResizing )
309 				return;
310 
311 			// On IE6/7, we append the resizer everytime we need it. (#5890)
312 			if ( needsIEHacks )
313 			{
314 				document.getBody().append( resizer );
315 				currentShift = 0;
316 			}
317 
318 			pillar = targetPillar;
319 
320 			resizer.setStyles(
321 				{
322 					width: pxUnit( targetPillar.width ),
323 					height : pxUnit( targetPillar.height ),
324 					left : pxUnit( targetPillar.x ),
325 					top : pxUnit( targetPillar.y )
326 				});
327 
328 			// In IE6/7, it's not possible to have custom cursors for floating
329 			// elements in an editable document. Show the resizer in that case,
330 			// to give the user a visual clue.
331 			needsIEHacks && resizer.setOpacity( 0.25 );
332 
333 			resizer.on( 'mousedown', onMouseDown, this );
334 
335 			document.getBody().setStyle( 'cursor', 'col-resize' );
336 
337 			// Display the resizer to receive events but don't show it,
338 			// only change the cursor to resizable shape.
339 			resizer.show();
340 		};
341 
342 		var move = this.move = function( posX )
343 		{
344 			if ( !pillar )
345 				return 0;
346 
347 			if ( !isResizing && ( posX < pillar.x || posX > ( pillar.x + pillar.width ) ) )
348 			{
349 				detach();
350 				return 0;
351 			}
352 
353 			var resizerNewPosition = posX - Math.round( resizer.$.offsetWidth / 2 );
354 
355 			if ( isResizing )
356 			{
357 				if ( resizerNewPosition == leftShiftBoundary || resizerNewPosition == rightShiftBoundary )
358 					return 1;
359 
360 				resizerNewPosition = Math.max( resizerNewPosition, leftShiftBoundary );
361 				resizerNewPosition = Math.min( resizerNewPosition, rightShiftBoundary );
362 
363 				currentShift = resizerNewPosition - startOffset;
364 			}
365 
366 			resizer.setStyle( 'left', pxUnit( resizerNewPosition ) );
367 
368 			return 1;
369 		};
370 	}
371 
372 	function clearPillarsCache( evt )
373 	{
374 		var target = evt.data.getTarget();
375 
376 		if ( evt.name == 'mouseout' )
377 		{
378 			// Bypass interal mouse move.
379 			if ( !target.is ( 'table' ) )
380 				return;
381 
382 			var dest = new CKEDITOR.dom.element( evt.data.$.relatedTarget || evt.data.$.toElement );
383 			while( dest && dest.$ && !dest.equals( target ) && !dest.is( 'body' ) )
384 				dest = dest.getParent();
385 			if ( !dest || dest.equals( target ) )
386 				return;
387 		}
388 
389 		target.getAscendant( 'table', 1 ).removeCustomData( '_cke_table_pillars' );
390 		evt.removeListener();
391 	}
392 
393 	CKEDITOR.plugins.add( 'tableresize',
394 	{
395 		requires : [ 'tabletools' ],
396 		init : function( editor )
397 		{
398 			editor.on( 'contentDom', function()
399 			{
400 				var resizer;
401 
402 				editor.document.getBody().on( 'mousemove', function( evt )
403 					{
404 						evt = evt.data;
405 
406 						var pageX = evt.getPageOffset().x;
407 
408 						// If we're already attached to a pillar, simply move the
409 						// resizer.
410 						if ( resizer && resizer.move( pageX ) )
411 						{
412 							cancel( evt );
413 							return;
414 						}
415 
416 						// Considering table, tr, td, tbody but nothing else.
417 						var target = evt.getTarget(),
418 							table,
419 							pillars;
420 
421 						if ( !target.is( 'table' ) && !target.getAscendant( 'tbody', 1 ) )
422 							return;
423 
424 						table = target.getAscendant( 'table', 1 );
425 
426 						if ( !( pillars = table.getCustomData( '_cke_table_pillars' ) ) )
427 						{
428 							// Cache table pillars calculation result.
429 							table.setCustomData( '_cke_table_pillars', ( pillars = buildTableColumnPillars( table ) ) );
430 							table.on( 'mouseout', clearPillarsCache );
431 							table.on( 'mousedown', clearPillarsCache );
432 						}
433 
434 						var pillar = getPillarAtPosition( pillars, pageX );
435 						if ( pillar )
436 						{
437 							!resizer && ( resizer = new columnResizer( editor ) );
438 							resizer.attachTo( pillar );
439 						}
440 					});
441 			});
442 		}
443 	});
444 
445 })();
446