| 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, mergeDirection, isDetect ) |
| | 369 | { |
| | 370 | var cells = getSelectedCells( selection ); |
| | 371 | |
| | 372 | // Invalid merge request if: |
| | 373 | // 1. In batch mode despite that less than two selected. |
| | 374 | // 2. In solo mode while not exactly only one selected. |
| | 375 | // 3. Cells distributed in different table groups (e.g. from both thead and tbody). |
| | 376 | if( ( mergeDirection ? cells.length != 1 : cells.length < 2 ) |
| | 377 | || selection.getCommonAncestor().is( 'table' ) ) |
| | 378 | return false; |
| | 379 | |
| | 380 | var cell, |
| | 381 | firstCell = cells[ 0 ], |
| | 382 | table = firstCell.getAscendant( 'table' ), |
| | 383 | map = buildTableMap( table ), |
| | 384 | mapHeight = map.length, |
| | 385 | mapWidth = map[ 0 ].length, |
| | 386 | startRow = firstCell.getParent().$.rowIndex, |
| | 387 | startColumn = cellInRow( map, startRow, firstCell ); |
| | 388 | |
| | 389 | if( mergeDirection ) |
| | 390 | { |
| | 391 | var targetCell; |
| | 392 | try |
| | 393 | { |
| | 394 | targetCell = |
| | 395 | map[ mergeDirection == 'up' ? |
| | 396 | ( startRow - 1 ): |
| | 397 | mergeDirection == 'down' ? ( startRow + 1 ) : startRow ] [ |
| | 398 | mergeDirection == 'left' ? |
| | 399 | ( startColumn - 1 ): |
| | 400 | mergeDirection == 'right' ? ( startColumn + 1 ) : startColumn ]; |
| | 401 | |
| | 402 | } |
| | 403 | catch( er ) |
| | 404 | { |
| | 405 | return false; |
| | 406 | } |
| | 407 | |
| | 408 | // 1. No cell could be merged. |
| | 409 | // 2. Same cell actually. |
| | 410 | if( !targetCell || firstCell.$ == targetCell ) |
| | 411 | return false; |
| | 412 | |
| | 413 | // Sort in map order regardless of the DOM sequence. |
| | 414 | cells[ ( mergeDirection == 'up' || mergeDirection == 'left' ) ? |
| | 415 | 'unshift' : 'push' ]( new CKEDITOR.dom.element( targetCell ) ); |
| | 416 | } |
| | 417 | |
| | 418 | // Start from here are merging way ignorance (merge up/right, batch merge). |
| | 419 | var doc = firstCell.getDocument(), |
| | 420 | lastRowIndex = startRow, |
| | 421 | totalRowSpan = 0, |
| | 422 | totalColSpan = 0, |
| | 423 | // Use a documentFragment as buffer when appending cell contents. |
| | 424 | frag = !isDetect && new CKEDITOR.dom.documentFragment( doc ), |
| | 425 | dimension = 0; |
| | 426 | |
| | 427 | for ( var i = 0; i < cells.length; i++ ) |
| | 428 | { |
| | 429 | cell = cells[ i ]; |
| | 430 | |
| | 431 | var tr = cell.getParent(), |
| | 432 | cellFirstChild = cell.getFirst(), |
| | 433 | colSpan = cell.$.colSpan, |
| | 434 | rowSpan = cell.$.rowSpan, |
| | 435 | rowIndex = tr.$.rowIndex, |
| | 436 | colIndex = cellInRow( map, rowIndex, cell ); |
| | 437 | |
| | 438 | // Accumulated the actual places taken by all selected cells. |
| | 439 | dimension += colSpan * rowSpan; |
| | 440 | // Accumulated the maximum virtual spans from column and row. |
| | 441 | totalColSpan = Math.max( totalColSpan, colIndex - startColumn + colSpan ) ; |
| | 442 | totalRowSpan = Math.max( totalRowSpan, rowIndex - startRow + rowSpan ); |
| | 443 | |
| | 444 | if ( !isDetect ) |
| | 445 | { |
| | 446 | // Simpily remove empty cells. |
| | 447 | if( cell.trim(), !isEmptyCell( cell ) ) |
| | 448 | { |
| | 449 | // Merge vertically cells as two separated paragraphs. |
| | 450 | if( rowIndex != lastRowIndex |
| | 451 | && cellFirstChild |
| | 452 | && !( cellFirstChild.isBlockBoundary |
| | 453 | && cellFirstChild.isBlockBoundary( { br : 1 } ) ) ) |
| | 454 | { |
| | 455 | var last = frag.getLast( CKEDITOR.dom.walker.whitespaces( true ) ); |
| | 456 | if( last && !( last.is && last.is( 'br' ) ) ) |
| | 457 | frag.append( new CKEDITOR.dom.element( 'br' ) ); |
| | 458 | } |
| | 459 | |
| | 460 | cell.moveChildren( frag ); |
| | 461 | } |
| | 462 | i ? cell.remove() : cell.setHtml( '' ); |
| | 463 | } |
| | 464 | lastRowIndex = rowIndex; |
| | 465 | } |
| | 466 | |
| | 467 | if ( !isDetect ) |
| | 468 | { |
| | 469 | frag.moveChildren( firstCell ); |
| | 470 | |
| | 471 | if( totalColSpan >= mapWidth ) |
| | 472 | firstCell.removeAttribute( 'rowSpan' ); |
| | 473 | else |
| | 474 | firstCell.$.rowSpan = totalRowSpan; |
| | 475 | |
| | 476 | if( totalRowSpan >= mapHeight ) |
| | 477 | firstCell.removeAttribute( 'colSpan' ); |
| | 478 | else |
| | 479 | firstCell.$.colSpan = totalColSpan; |
| | 480 | |
| | 481 | // Swip empty <tr> left at the end of table due to the merging. |
| | 482 | var trs = new CKEDITOR.dom.nodeList( table.$.rows ), |
| | 483 | count = trs.count(); |
| | 484 | |
| | 485 | for ( var i = count - 1; i >= 0; i-- ) |
| | 486 | { |
| | 487 | var tailTr = trs.getItem( i ); |
| | 488 | if( !tailTr.$.cells.length ) |
| | 489 | { |
| | 490 | tailTr.remove(); |
| | 491 | count++; |
| | 492 | continue; |
| | 493 | } |
| | 494 | else |
| | 495 | break; |
| | 496 | } |
| | 497 | |
| | 498 | return firstCell; |
| | 499 | } |
| | 500 | // Be able to merge cells only if actual dimension of selected |
| | 501 | // cells equals to the caculated rectangle. |
| | 502 | else |
| | 503 | return ( totalRowSpan * totalColSpan ) == dimension; |
| | 504 | } |
| | 505 | |
| | 506 | function verticalSplitCell ( selection, isDetect ) |
| | 507 | { |
| | 508 | var cells = getSelectedCells( selection ); |
| | 509 | if( cells.length > 1 ) |
| | 510 | return false; |
| | 511 | else if( isDetect ) |
| | 512 | return true; |
| | 513 | |
| | 514 | var cell = cells[ 0 ], |
| | 515 | tr = cell.getParent(), |
| | 516 | table = tr.getAscendant( 'table' ), |
| | 517 | map = buildTableMap( table ), |
| | 518 | rowIndex = tr.$.rowIndex, |
| | 519 | colIndex = cellInRow( map, rowIndex, cell ), |
| | 520 | rowSpan = cell.$.rowSpan, |
| | 521 | newCell, |
| | 522 | newRowSpan, |
| | 523 | newCellRowSpan, |
| | 524 | newRowIndex; |
| | 525 | |
| | 526 | if( rowSpan > 1 ) |
| | 527 | { |
| | 528 | newRowSpan = Math.ceil( rowSpan / 2 ); |
| | 529 | newCellRowSpan = Math.floor( rowSpan / 2 ); |
| | 530 | newRowIndex = rowIndex + newRowSpan; |
| | 531 | var newCellTr = new CKEDITOR.dom.element( table.$.rows[ newRowIndex ] ), |
| | 532 | newCellRow = cellInRow( map, newRowIndex ), |
| | 533 | candidateCell; |
| | 534 | |
| | 535 | newCell = cell.clone(); |
| | 536 | |
| | 537 | // Figure out where to insert the new cell by checking the vitual row. |
| | 538 | for ( var c = 0; c < newCellRow.length; c++ ) |
| | 539 | { |
| | 540 | candidateCell = newCellRow[ c ]; |
| | 541 | // Catch first cell actually following the column. |
| | 542 | if( candidateCell.parentNode == newCellTr.$ |
| | 543 | && c > colIndex ) |
| | 544 | { |
| | 545 | newCell.insertBefore( new CKEDITOR.dom.element( candidateCell ) ); |
| | 546 | break; |
| | 547 | } |
| | 548 | else |
| | 549 | candidateCell = null; |
| | 550 | } |
| | 551 | |
| | 552 | // The destination row is empty, append at will. |
| | 553 | if( !candidateCell ) |
| | 554 | newCellTr.append( newCell, true ); |
| | 555 | } |
| | 556 | else |
| | 557 | { |
| | 558 | newCellRowSpan = newRowSpan = 1; |
| | 559 | var newCellTr = tr.clone(); |
| | 560 | newCellTr.insertAfter( tr ); |
| | 561 | newCellTr.append( newCell = cell.clone() ); |
| | 562 | var cellsInSameRow = cellInRow( map, rowIndex ); |
| | 563 | for ( var i = 0; i < cellsInSameRow.length; i++ ) |
| | 564 | cellsInSameRow[ i ].rowSpan++; |
| | 565 | } |
| | 566 | |
| | 567 | if( !CKEDITOR.env.ie ) |
| | 568 | newCell.appendBogus(); |
| | 569 | cell.$.rowSpan = newRowSpan; |
| | 570 | newCell.$.rowSpan = newCellRowSpan; |
| | 571 | if( newRowSpan == 1 ) |
| | 572 | cell.removeAttribute( 'rowSpan' ); |
| | 573 | if( newCellRowSpan == 1 ) |
| | 574 | newCell.removeAttribute( 'rowSpan' ); |
| | 575 | |
| | 576 | return newCell; |
| | 577 | } |
| | 578 | |
| | 579 | function horizontalSplitCell( selection, isDetect ) |
| | 580 | { |
| | 581 | var cells = getSelectedCells( selection ); |
| | 582 | if( cells.length > 1 ) |
| | 583 | return false; |
| | 584 | else if( isDetect ) |
| | 585 | return true; |
| | 586 | |
| | 587 | var cell = cells[ 0 ], |
| | 588 | tr = cell.getParent(), |
| | 589 | table = tr.getAscendant( 'table' ), |
| | 590 | map = buildTableMap( table ), |
| | 591 | rowIndex = tr.$.rowIndex, |
| | 592 | colIndex = cellInRow( map, rowIndex, cell ), |
| | 593 | colSpan = cell.$.colSpan, |
| | 594 | newCell, |
| | 595 | newColSpan, |
| | 596 | newCellColSpan; |
| | 597 | |
| | 598 | if( colSpan > 1 ) |
| | 599 | { |
| | 600 | newColSpan = Math.ceil( colSpan / 2 ); |
| | 601 | newCellColSpan = Math.floor( colSpan / 2 ); |
| | 602 | } |
| | 603 | else |
| | 604 | { |
| | 605 | newCellColSpan = newColSpan = 1; |
| | 606 | var cellsInSameCol = cellInCol( map, colIndex ); |
| | 607 | for ( var i = 0; i < cellsInSameCol.length; i++ ) |
| | 608 | cellsInSameCol[ i ].colSpan++; |
| | 609 | } |
| | 610 | newCell = cell.clone(); |
| | 611 | newCell.insertAfter( cell ); |
| | 612 | if( !CKEDITOR.env.ie ) |
| | 613 | newCell.appendBogus(); |
| | 614 | |
| | 615 | cell.$.colSpan = newColSpan; |
| | 616 | newCell.$.colSpan = newCellColSpan; |
| | 617 | if( newColSpan == 1 ) |
| | 618 | cell.removeAttribute( 'colSpan' ); |
| | 619 | if( newCellColSpan == 1 ) |
| | 620 | newCell.removeAttribute( 'colSpan' ); |
| | 621 | |
| | 622 | return newCell; |
| | 623 | } |