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