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