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 | | |
| 267 | function isEmptyCell( cell ) |
| 268 | { |
| 269 | var children = cell.getChildren(), |
| 270 | lastChild; |
| 271 | |
| 272 | if( !children.count() ) |
| 273 | return true; |
| 274 | else if( !CKEDITOR.env.ie ) |
| 275 | { |
| 276 | return children.count() == 1 |
| 277 | && ( lastChild = children.getItem( 0 ) ) |
| 278 | && lastChild.is && lastChild.is( 'br' ); |
| 279 | } |
| 280 | } |
| 281 | |
| 282 | function placeCursorInCell( cell ) |
| 283 | { |
| 284 | var range = new CKEDITOR.dom.range( cell.getDocument() ); |
| 285 | range.selectNodeContents( cell ); |
| 286 | range.collapse(); |
| 287 | range.select( true ); |
| 288 | } |
| 289 | |
| 290 | function buildTableMap( table ) |
| 291 | { |
| 292 | |
| 293 | var aRows = table.$.rows ; |
| 294 | |
| 295 | // Row and Column counters. |
| 296 | var r = -1 ; |
| 297 | |
| 298 | var aMap = []; |
| 299 | |
| 300 | for ( var i = 0 ; i < aRows.length ; i++ ) |
| 301 | { |
| 302 | r++ ; |
| 303 | !aMap[r] && ( aMap[r] = [] ); |
| 304 | |
| 305 | var c = -1 ; |
| 306 | |
| 307 | for ( var j = 0 ; j < aRows[i].cells.length ; j++ ) |
| 308 | { |
| 309 | var oCell = aRows[i].cells[j] ; |
| 310 | |
| 311 | c++ ; |
| 312 | while ( aMap[r][c] ) |
| 313 | c++ ; |
| 314 | |
| 315 | var iColSpan = isNaN( oCell.colSpan ) ? 1 : oCell.colSpan ; |
| 316 | var iRowSpan = isNaN( oCell.rowSpan ) ? 1 : oCell.rowSpan ; |
| 317 | |
| 318 | for ( var rs = 0 ; rs < iRowSpan ; rs++ ) |
| 319 | { |
| 320 | if ( !aMap[r + rs] ) |
| 321 | aMap[r + rs] = new Array() ; |
| 322 | |
| 323 | for ( var cs = 0 ; cs < iColSpan ; cs++ ) |
| 324 | { |
| 325 | aMap[r + rs][c + cs] = aRows[i].cells[j] ; |
| 326 | } |
| 327 | } |
| 328 | |
| 329 | c += iColSpan - 1 ; |
| 330 | } |
| 331 | } |
| 332 | return aMap ; |
| 333 | } |
| 334 | |
| 335 | function cellInRow( tableMap, rowIndex, cell ) |
| 336 | { |
| 337 | var oRow = tableMap[ rowIndex ]; |
| 338 | if( typeof cell == 'undefined' ) |
| 339 | return oRow; |
| 340 | |
| 341 | for ( var c = 0 ; oRow && c < oRow.length ; c++ ) |
| 342 | { |
| 343 | if ( cell.is && oRow[c] == cell.$ ) |
| 344 | return c; |
| 345 | else if( c == cell ) |
| 346 | return new CKEDITOR.dom.element( oRow[ c ] ); |
| 347 | } |
| 348 | return cell.is ? -1 : null; |
| 349 | } |
| 350 | |
| 351 | function cellInCol( tableMap, colIndex, cell ) |
| 352 | { |
| 353 | var oCol = []; |
| 354 | for ( var r = 0; r < tableMap.length; r++ ) |
| 355 | { |
| 356 | var row = tableMap[ r ]; |
| 357 | if( typeof cell == 'undefined' ) |
| 358 | oCol.push( row[ colIndex ] ); |
| 359 | else if( cell.is && row[ colIndex ] == cell.$ ) |
| 360 | return r; |
| 361 | else if( r == cell ) |
| 362 | return new CKEDITOR.dom.element( row[ colIndex ] ); |
| 363 | } |
| 364 | |
| 365 | return ( typeof cell == 'undefined' )? oCol : cell.is ? -1 : null; |
| 366 | } |
| 367 | |
| 368 | function mergeCells( selection, isDetect ) |
| 369 | { |
| 370 | var cells = getSelectedCells( selection ); |
| 371 | |
| 372 | // Disable the merge if: |
| 373 | // 1. Single cell selected. |
| 374 | // 2. Cells distributed in different groups. |
| 375 | if( cells.length < 2 || selection.getCommonAncestor().is( 'table' ) ) |
| 376 | return false; |
| 377 | |
| 378 | var cell, |
| 379 | firstCell = cells[ 0 ], |
| 380 | table = firstCell.getAscendant( 'table' ), |
| 381 | doc = firstCell.getDocument(), |
| 382 | map = buildTableMap( table ), |
| 383 | mapHeight = map.length, |
| 384 | mapWidth = map[ 0 ].length, |
| 385 | startRow = firstCell.getParent().$.rowIndex, |
| 386 | startColumn = cellInRow( map, startRow, firstCell ), |
| 387 | lastRowIndex = startRow, |
| 388 | totalRowSpan = 0, |
| 389 | totalColSpan = 0, |
| 390 | // Use a documentFragment as buffer when appending cell contents. |
| 391 | frag = !isDetect && new CKEDITOR.dom.documentFragment( doc ), |
| 392 | dimension = 0; |
| 393 | |
| 394 | for ( var i = 0; i < cells.length; i++ ) |
| 395 | { |
| 396 | cell = cells[ i ]; |
| 397 | |
| 398 | var tr = cell.getParent(), |
| 399 | cellFirstChild = cell.getFirst(), |
| 400 | colSpan = cell.$.colSpan, |
| 401 | rowSpan = cell.$.rowSpan, |
| 402 | rowIndex = tr.$.rowIndex, |
| 403 | colIndex = cellInRow( map, rowIndex, cell ); |
| 404 | |
| 405 | // Accumulated the actual places taken by all selected cells. |
| 406 | dimension += colSpan * rowSpan; |
| 407 | // Accumulated the maximum virtual spans from column and row. |
| 408 | totalColSpan = Math.max( totalColSpan, colIndex - startColumn + colSpan ) ; |
| 409 | totalRowSpan = Math.max( totalRowSpan, rowIndex - startRow + rowSpan ); |
| 410 | |
| 411 | if ( !isDetect ) |
| 412 | { |
| 413 | // Simpily remove empty cells. |
| 414 | if( cell.trim(), !isEmptyCell( cell ) ) |
| 415 | { |
| 416 | // Merge vertically cells as two separated paragraphs. |
| 417 | if( rowIndex != lastRowIndex |
| 418 | && cellFirstChild |
| 419 | && !( cellFirstChild.isBlockBoundary |
| 420 | && cellFirstChild.isBlockBoundary( { br : 1 } ) ) ) |
| 421 | { |
| 422 | var last = frag.getLast( CKEDITOR.dom.walker.whitespaces( true ) ); |
| 423 | if( last && !( last.is && last.is( 'br' ) ) ) |
| 424 | frag.append( new CKEDITOR.dom.element( 'br' ) ); |
| 425 | } |
| 426 | |
| 427 | cell.moveChildren( frag ); |
| 428 | } |
| 429 | i ? cell.remove() : cell.setHtml( '' ); |
| 430 | } |
| 431 | lastRowIndex = rowIndex; |
| 432 | } |
| 433 | |
| 434 | if ( !isDetect ) |
| 435 | { |
| 436 | frag.moveChildren( firstCell ); |
| 437 | |
| 438 | if( totalColSpan >= mapWidth ) |
| 439 | firstCell.removeAttribute( 'rowSpan' ); |
| 440 | else |
| 441 | firstCell.$.rowSpan = totalRowSpan; |
| 442 | |
| 443 | if( totalRowSpan >= mapHeight ) |
| 444 | firstCell.removeAttribute( 'colSpan' ); |
| 445 | else |
| 446 | firstCell.$.colSpan = totalColSpan; |
| 447 | |
| 448 | // Swip empty <tr> left at the end of table due to the merging. |
| 449 | var trs = new CKEDITOR.dom.nodeList( table.$.rows ), |
| 450 | count = trs.count(); |
| 451 | |
| 452 | for ( var i = count - 1; i >= 0; i-- ) |
| 453 | { |
| 454 | var tailTr = trs.getItem( i ); |
| 455 | if( !tailTr.$.cells.length ) |
| 456 | { |
| 457 | tailTr.remove(); |
| 458 | count++; |
| 459 | continue; |
| 460 | } |
| 461 | else |
| 462 | break; |
| 463 | } |
| 464 | |
| 465 | return firstCell; |
| 466 | } |
| 467 | // Be able to merge cells only if actual dimension of selected |
| 468 | // cells equals to the caculated rectangle. |
| 469 | else |
| 470 | return ( totalRowSpan * totalColSpan ) == dimension; |
| 471 | } |
| 472 | |
| 473 | function verticalSplitCell ( selection, isDetect ) |
| 474 | { |
| 475 | var cells = getSelectedCells( selection ); |
| 476 | if( cells.length > 1 ) |
| 477 | return false; |
| 478 | else if( isDetect ) |
| 479 | return true; |
| 480 | |
| 481 | var cell = cells[ 0 ], |
| 482 | tr = cell.getParent(), |
| 483 | table = tr.getAscendant( 'table' ), |
| 484 | map = buildTableMap( table ), |
| 485 | rowIndex = tr.$.rowIndex, |
| 486 | colIndex = cellInRow( map, rowIndex, cell ), |
| 487 | rowSpan = cell.$.rowSpan, |
| 488 | newCell, |
| 489 | newRowSpan, |
| 490 | newCellRowSpan, |
| 491 | newRowIndex; |
| 492 | |
| 493 | if( rowSpan > 1 ) |
| 494 | { |
| 495 | newRowSpan = Math.ceil( rowSpan / 2 ); |
| 496 | newCellRowSpan = Math.floor( rowSpan / 2 ); |
| 497 | newRowIndex = rowIndex + newRowSpan; |
| 498 | var newCellTr = new CKEDITOR.dom.element( table.$.rows[ newRowIndex ] ), |
| 499 | newCellRow = cellInRow( map, newRowIndex ), |
| 500 | candidateCell; |
| 501 | |
| 502 | newCell = cell.clone(); |
| 503 | |
| 504 | // Figure out where to insert the new cell by checking the vitual row. |
| 505 | for ( var c = 0; c < newCellRow.length; c++ ) |
| 506 | { |
| 507 | candidateCell = newCellRow[ c ]; |
| 508 | // Catch first cell actually following the column. |
| 509 | if( candidateCell.parentNode == newCellTr.$ |
| 510 | && c > colIndex ) |
| 511 | { |
| 512 | newCell.insertBefore( new CKEDITOR.dom.element( candidateCell ) ); |
| 513 | break; |
| 514 | } |
| 515 | else |
| 516 | candidateCell = null; |
| 517 | } |
| 518 | |
| 519 | // The destination row is empty, append at will. |
| 520 | if( !candidateCell ) |
| 521 | newCellTr.append( newCell, true ); |
| 522 | } |
| 523 | else |
| 524 | { |
| 525 | newCellRowSpan = newRowSpan = 1; |
| 526 | var newCellTr = tr.clone(); |
| 527 | newCellTr.insertAfter( tr ); |
| 528 | newCellTr.append( newCell = cell.clone() ); |
| 529 | var cellsInSameRow = cellInRow( map, rowIndex ); |
| 530 | for ( var i = 0; i < cellsInSameRow.length; i++ ) |
| 531 | cellsInSameRow[ i ].rowSpan++; |
| 532 | } |
| 533 | |
| 534 | if( !CKEDITOR.env.ie ) |
| 535 | newCell.appendBogus(); |
| 536 | cell.$.rowSpan = newRowSpan; |
| 537 | newCell.$.rowSpan = newCellRowSpan; |
| 538 | if( newRowSpan == 1 ) |
| 539 | cell.removeAttribute( 'rowSpan' ); |
| 540 | if( newCellRowSpan == 1 ) |
| 541 | newCell.removeAttribute( 'rowSpan' ); |
| 542 | |
| 543 | return newCell; |
| 544 | } |
| 545 | |
| 546 | function horizontalSplitCell( selection, isDetect ) |
| 547 | { |
| 548 | var cells = getSelectedCells( selection ); |
| 549 | if( cells.length > 1 ) |
| 550 | return false; |
| 551 | else if( isDetect ) |
| 552 | return true; |
| 553 | |
| 554 | var cell = cells[ 0 ], |
| 555 | tr = cell.getParent(), |
| 556 | table = tr.getAscendant( 'table' ), |
| 557 | map = buildTableMap( table ), |
| 558 | rowIndex = tr.$.rowIndex, |
| 559 | colIndex = cellInRow( map, rowIndex, cell ), |
| 560 | colSpan = cell.$.colSpan, |
| 561 | newCell, |
| 562 | newColSpan, |
| 563 | newCellColSpan; |
| 564 | |
| 565 | if( colSpan > 1 ) |
| 566 | { |
| 567 | newColSpan = Math.ceil( colSpan / 2 ); |
| 568 | newCellColSpan = Math.floor( colSpan / 2 ); |
| 569 | } |
| 570 | else |
| 571 | { |
| 572 | newCellColSpan = newColSpan = 1; |
| 573 | var cellsInSameCol = cellInCol( map, colIndex ); |
| 574 | for ( var i = 0; i < cellsInSameCol.length; i++ ) |
| 575 | cellsInSameCol[ i ].colSpan++; |
| 576 | } |
| 577 | newCell = cell.clone(); |
| 578 | newCell.insertAfter( cell ); |
| 579 | if( !CKEDITOR.env.ie ) |
| 580 | newCell.appendBogus(); |
| 581 | |
| 582 | cell.$.colSpan = newColSpan; |
| 583 | newCell.$.colSpan = newCellColSpan; |
| 584 | if( newColSpan == 1 ) |
| 585 | cell.removeAttribute( 'colSpan' ); |
| 586 | if( newCellColSpan == 1 ) |
| 587 | newCell.removeAttribute( 'colSpan' ); |
| 588 | |
| 589 | return newCell; |
| 590 | } |