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 cellNodeRegex = /^(?:td|th)$/; 9 10 function getSelectedCells( selection ) 11 { 12 var ranges = selection.getRanges(); 13 var retval = []; 14 var database = {}; 15 16 function moveOutOfCellGuard( node ) 17 { 18 // Apply to the first cell only. 19 if ( retval.length > 0 ) 20 return; 21 22 // If we are exiting from the first </td>, then the td should definitely be 23 // included. 24 if ( node.type == CKEDITOR.NODE_ELEMENT && cellNodeRegex.test( node.getName() ) 25 && !node.getCustomData( 'selected_cell' ) ) 26 { 27 CKEDITOR.dom.element.setMarker( database, node, 'selected_cell', true ); 28 retval.push( node ); 29 } 30 } 31 32 for ( var i = 0 ; i < ranges.length ; i++ ) 33 { 34 var range = ranges[ i ]; 35 36 if ( range.collapsed ) 37 { 38 // Walker does not handle collapsed ranges yet - fall back to old API. 39 var startNode = range.getCommonAncestor(); 40 var nearestCell = startNode.getAscendant( 'td', true ) || startNode.getAscendant( 'th', true ); 41 if ( nearestCell ) 42 retval.push( nearestCell ); 43 } 44 else 45 { 46 var walker = new CKEDITOR.dom.walker( range ); 47 var node; 48 walker.guard = moveOutOfCellGuard; 49 50 while ( ( node = walker.next() ) ) 51 { 52 // If may be possible for us to have a range like this: 53 // <td>^1</td><td>^2</td> 54 // The 2nd td shouldn't be included. 55 // 56 // So we have to take care to include a td we've entered only when we've 57 // walked into its children. 58 59 var parent = node.getAscendant( 'td' ) || node.getAscendant( 'th' ); 60 if ( parent && !parent.getCustomData( 'selected_cell' ) ) 61 { 62 CKEDITOR.dom.element.setMarker( database, parent, 'selected_cell', true ); 63 retval.push( parent ); 64 } 65 } 66 } 67 } 68 69 CKEDITOR.dom.element.clearAllMarkers( database ); 70 71 return retval; 72 } 73 74 function getFocusElementAfterDelCells( cellsToDelete ) { 75 var i = 0, 76 last = cellsToDelete.length - 1, 77 database = {}, 78 cell,focusedCell, 79 tr; 80 81 while ( ( cell = cellsToDelete[ i++ ] ) ) 82 CKEDITOR.dom.element.setMarker( database, cell, 'delete_cell', true ); 83 84 // 1.first we check left or right side focusable cell row by row; 85 i = 0; 86 while ( ( cell = cellsToDelete[ i++ ] ) ) 87 { 88 if ( ( focusedCell = cell.getPrevious() ) && !focusedCell.getCustomData( 'delete_cell' ) 89 || ( focusedCell = cell.getNext() ) && !focusedCell.getCustomData( 'delete_cell' ) ) 90 { 91 CKEDITOR.dom.element.clearAllMarkers( database ); 92 return focusedCell; 93 } 94 } 95 96 CKEDITOR.dom.element.clearAllMarkers( database ); 97 98 // 2. then we check the toppest row (outside the selection area square) focusable cell 99 tr = cellsToDelete[ 0 ].getParent(); 100 if ( ( tr = tr.getPrevious() ) ) 101 return tr.getLast(); 102 103 // 3. last we check the lowerest row focusable cell 104 tr = cellsToDelete[ last ].getParent(); 105 if ( ( tr = tr.getNext() ) ) 106 return tr.getChild( 0 ); 107 108 return null; 109 } 110 111 function insertRow( selection, insertBefore ) 112 { 113 var cells = getSelectedCells( selection ), 114 firstCell = cells[ 0 ], 115 table = firstCell.getAscendant( 'table' ), 116 doc = firstCell.getDocument(), 117 startRow = cells[ 0 ].getParent(), 118 startRowIndex = startRow.$.rowIndex, 119 lastCell = cells[ cells.length - 1 ], 120 endRowIndex = lastCell.getParent().$.rowIndex + lastCell.$.rowSpan - 1, 121 endRow = new CKEDITOR.dom.element( table.$.rows[ endRowIndex ] ), 122 rowIndex = insertBefore ? startRowIndex : endRowIndex, 123 row = insertBefore ? startRow : endRow; 124 125 var map = CKEDITOR.tools.buildTableMap( table ), 126 cloneRow = map[ rowIndex ], 127 nextRow = insertBefore ? map[ rowIndex - 1 ] : map[ rowIndex + 1 ], 128 width = map[0].length; 129 130 var newRow = doc.createElement( 'tr' ); 131 for ( var i = 0; cloneRow[ i ] && i < width; i++ ) 132 { 133 var cell; 134 // Check whether there's a spanning row here, do not break it. 135 if ( cloneRow[ i ].rowSpan > 1 && nextRow && cloneRow[ i ] == nextRow[ i ] ) 136 { 137 cell = cloneRow[ i ]; 138 cell.rowSpan += 1; 139 } 140 else 141 { 142 cell = new CKEDITOR.dom.element( cloneRow[ i ] ).clone(); 143 cell.removeAttribute( 'rowSpan' ); 144 !CKEDITOR.env.ie && cell.appendBogus(); 145 newRow.append( cell ); 146 cell = cell.$; 147 } 148 149 i += cell.colSpan - 1; 150 } 151 152 insertBefore ? 153 newRow.insertBefore( row ) : 154 newRow.insertAfter( row ); 155 } 156 157 function deleteRows( selectionOrRow ) 158 { 159 if ( selectionOrRow instanceof CKEDITOR.dom.selection ) 160 { 161 var cells = getSelectedCells( selectionOrRow ), 162 firstCell = cells[ 0 ], 163 table = firstCell.getAscendant( 'table' ), 164 map = CKEDITOR.tools.buildTableMap( table ), 165 startRow = cells[ 0 ].getParent(), 166 startRowIndex = startRow.$.rowIndex, 167 lastCell = cells[ cells.length - 1 ], 168 endRowIndex = lastCell.getParent().$.rowIndex + lastCell.$.rowSpan - 1, 169 rowsToDelete = []; 170 171 // Delete cell or reduce cell spans by checking through the table map. 172 for ( var i = startRowIndex; i <= endRowIndex; i++ ) 173 { 174 var mapRow = map[ i ], 175 row = new CKEDITOR.dom.element( table.$.rows[ i ] ); 176 177 for ( var j = 0; j < mapRow.length; j++ ) 178 { 179 var cell = new CKEDITOR.dom.element( mapRow[ j ] ), 180 cellRowIndex = cell.getParent().$.rowIndex; 181 182 if ( cell.$.rowSpan == 1 ) 183 cell.remove(); 184 // Row spanned cell. 185 else 186 { 187 // Span row of the cell, reduce spanning. 188 cell.$.rowSpan -= 1; 189 // Root row of the cell, root cell to next row. 190 if ( cellRowIndex == i ) 191 { 192 var nextMapRow = map[ i + 1 ]; 193 nextMapRow[ j - 1 ] ? 194 cell.insertAfter( new CKEDITOR.dom.element( nextMapRow[ j - 1 ] ) ) 195 : new CKEDITOR.dom.element( table.$.rows[ i + 1 ] ).append( cell, 1 ); 196 } 197 } 198 199 j += cell.$.colSpan - 1; 200 } 201 202 rowsToDelete.push( row ); 203 } 204 205 var rows = table.$.rows; 206 207 // Where to put the cursor after rows been deleted? 208 // 1. Into next sibling row if any; 209 // 2. Into previous sibling row if any; 210 // 3. Into table's parent element if it's the very last row. 211 var cursorPosition = new CKEDITOR.dom.element( rows[ endRowIndex + 1 ] || ( startRowIndex > 0 ? rows[ startRowIndex - 1 ] : null ) || table.$.parentNode ); 212 213 for ( i = rowsToDelete.length ; i >= 0 ; i-- ) 214 deleteRows( rowsToDelete[ i ] ); 215 216 return cursorPosition; 217 } 218 else if ( selectionOrRow instanceof CKEDITOR.dom.element ) 219 { 220 table = selectionOrRow.getAscendant( 'table' ); 221 222 if ( table.$.rows.length == 1 ) 223 table.remove(); 224 else 225 selectionOrRow.remove(); 226 } 227 228 return null; 229 } 230 231 function getCellColIndex( cell, isStart ) 232 { 233 var row = cell.getParent(), 234 rowCells = row.$.cells; 235 236 var colIndex = 0; 237 for ( var i = 0; i < rowCells.length; i++ ) 238 { 239 var mapCell = rowCells[ i ]; 240 colIndex += isStart ? 1 : mapCell.colSpan; 241 if ( mapCell == cell.$ ) 242 break; 243 } 244 245 return colIndex -1; 246 } 247 248 function getColumnsIndices( cells, isStart ) 249 { 250 var retval = isStart ? Infinity : 0; 251 for ( var i = 0; i < cells.length; i++ ) 252 { 253 var colIndex = getCellColIndex( cells[ i ], isStart ); 254 if ( isStart ? colIndex < retval : colIndex > retval ) 255 retval = colIndex; 256 } 257 return retval; 258 } 259 260 function insertColumn( selection, insertBefore ) 261 { 262 var cells = getSelectedCells( selection ), 263 firstCell = cells[ 0 ], 264 table = firstCell.getAscendant( 'table' ), 265 startCol = getColumnsIndices( cells, 1 ), 266 lastCol = getColumnsIndices( cells ), 267 colIndex = insertBefore? startCol : lastCol; 268 269 var map = CKEDITOR.tools.buildTableMap( table ), 270 cloneCol = [], 271 nextCol = [], 272 height = map.length; 273 274 for ( var i = 0; i < height; i++ ) 275 { 276 cloneCol.push( map[ i ][ colIndex ] ); 277 var nextCell = insertBefore ? map[ i ][ colIndex - 1 ] : map[ i ][ colIndex + 1 ]; 278 nextCol.push( nextCell ); 279 } 280 281 for ( i = 0; i < height; i++ ) 282 { 283 var cell; 284 285 if ( !cloneCol[ i ] ) 286 continue; 287 288 // Check whether there's a spanning column here, do not break it. 289 if ( cloneCol[ i ].colSpan > 1 290 && nextCol[ i ] == cloneCol[ i ] ) 291 { 292 cell = cloneCol[ i ]; 293 cell.colSpan += 1; 294 } 295 else 296 { 297 cell = new CKEDITOR.dom.element( cloneCol[ i ] ).clone(); 298 cell.removeAttribute( 'colSpan' ); 299 !CKEDITOR.env.ie && cell.appendBogus(); 300 cell[ insertBefore? 'insertBefore' : 'insertAfter' ].call( cell, new CKEDITOR.dom.element ( cloneCol[ i ] ) ); 301 cell = cell.$; 302 } 303 304 i += cell.rowSpan - 1; 305 } 306 } 307 308 function deleteColumns( selectionOrCell ) 309 { 310 var cells = getSelectedCells( selectionOrCell ), 311 firstCell = cells[ 0 ], 312 lastCell = cells[ cells.length - 1 ], 313 table = firstCell.getAscendant( 'table' ), 314 map = CKEDITOR.tools.buildTableMap( table ), 315 startColIndex, 316 endColIndex, 317 rowsToDelete = []; 318 319 // Figure out selected cells' column indices. 320 for ( var i = 0, rows = map.length; i < rows; i++ ) 321 { 322 for ( var j = 0, cols = map[ i ].length; j < cols; j++ ) 323 { 324 if ( map[ i ][ j ] == firstCell.$ ) 325 startColIndex = j; 326 if ( map[ i ][ j ] == lastCell.$ ) 327 endColIndex = j; 328 } 329 } 330 331 // Delete cell or reduce cell spans by checking through the table map. 332 for ( i = startColIndex; i <= endColIndex; i++ ) 333 { 334 for ( j = 0; j < map.length; j++ ) 335 { 336 var mapRow = map[ j ], 337 row = new CKEDITOR.dom.element( table.$.rows[ j ] ), 338 cell = new CKEDITOR.dom.element( mapRow[ i ] ); 339 340 if ( cell.$ ) 341 { 342 if ( cell.$.colSpan == 1 ) 343 cell.remove(); 344 // Reduce the col spans. 345 else 346 cell.$.colSpan -= 1; 347 348 j += cell.$.rowSpan - 1; 349 350 if ( !row.$.cells.length ) 351 rowsToDelete.push( row ); 352 } 353 } 354 } 355 356 var firstRowCells = table.$.rows[ 0 ] && table.$.rows[ 0 ].cells; 357 358 // Where to put the cursor after columns been deleted? 359 // 1. Into next cell of the first row if any; 360 // 2. Into previous cell of the first row if any; 361 // 3. Into table's parent element; 362 var cursorPosition = new CKEDITOR.dom.element( firstRowCells[ startColIndex ] || ( startColIndex ? firstRowCells[ startColIndex - 1 ] : table.$.parentNode ) ); 363 364 // Delete table rows only if all columns are gone (do not remove empty row). 365 if ( rowsToDelete.length == rows ) 366 table.remove(); 367 368 return cursorPosition; 369 } 370 371 function getFocusElementAfterDelCols( cells ) 372 { 373 var cellIndexList = [], 374 table = cells[ 0 ] && cells[ 0 ].getAscendant( 'table' ), 375 i, length, 376 targetIndex, targetCell; 377 378 // get the cellIndex list of delete cells 379 for ( i = 0, length = cells.length; i < length; i++ ) 380 cellIndexList.push( cells[i].$.cellIndex ); 381 382 // get the focusable column index 383 cellIndexList.sort(); 384 for ( i = 1, length = cellIndexList.length; i < length; i++ ) 385 { 386 if ( cellIndexList[ i ] - cellIndexList[ i - 1 ] > 1 ) 387 { 388 targetIndex = cellIndexList[ i - 1 ] + 1; 389 break; 390 } 391 } 392 393 if ( !targetIndex ) 394 targetIndex = cellIndexList[ 0 ] > 0 ? ( cellIndexList[ 0 ] - 1 ) 395 : ( cellIndexList[ cellIndexList.length - 1 ] + 1 ); 396 397 // scan row by row to get the target cell 398 var rows = table.$.rows; 399 for ( i = 0, length = rows.length; i < length ; i++ ) 400 { 401 targetCell = rows[ i ].cells[ targetIndex ]; 402 if ( targetCell ) 403 break; 404 } 405 406 return targetCell ? new CKEDITOR.dom.element( targetCell ) : table.getPrevious(); 407 } 408 409 function insertCell( selection, insertBefore ) 410 { 411 var startElement = selection.getStartElement(); 412 var cell = startElement.getAscendant( 'td', 1 ) || startElement.getAscendant( 'th', 1 ); 413 414 if ( !cell ) 415 return; 416 417 // Create the new cell element to be added. 418 var newCell = cell.clone(); 419 if ( !CKEDITOR.env.ie ) 420 newCell.appendBogus(); 421 422 if ( insertBefore ) 423 newCell.insertBefore( cell ); 424 else 425 newCell.insertAfter( cell ); 426 } 427 428 function deleteCells( selectionOrCell ) 429 { 430 if ( selectionOrCell instanceof CKEDITOR.dom.selection ) 431 { 432 var cellsToDelete = getSelectedCells( selectionOrCell ); 433 var table = cellsToDelete[ 0 ] && cellsToDelete[ 0 ].getAscendant( 'table' ); 434 var cellToFocus = getFocusElementAfterDelCells( cellsToDelete ); 435 436 for ( var i = cellsToDelete.length - 1 ; i >= 0 ; i-- ) 437 deleteCells( cellsToDelete[ i ] ); 438 439 if ( cellToFocus ) 440 placeCursorInCell( cellToFocus, true ); 441 else if ( table ) 442 table.remove(); 443 } 444 else if ( selectionOrCell instanceof CKEDITOR.dom.element ) 445 { 446 var tr = selectionOrCell.getParent(); 447 if ( tr.getChildCount() == 1 ) 448 tr.remove(); 449 else 450 selectionOrCell.remove(); 451 } 452 } 453 454 // Remove filler at end and empty spaces around the cell content. 455 function trimCell( cell ) 456 { 457 var bogus = cell.getBogus(); 458 bogus && bogus.remove(); 459 cell.trim(); 460 } 461 462 function placeCursorInCell( cell, placeAtEnd ) 463 { 464 var range = new CKEDITOR.dom.range( cell.getDocument() ); 465 if ( !range[ 'moveToElementEdit' + ( placeAtEnd ? 'End' : 'Start' ) ]( cell ) ) 466 { 467 range.selectNodeContents( cell ); 468 range.collapse( placeAtEnd ? false : true ); 469 } 470 range.select( true ); 471 } 472 473 function cellInRow( tableMap, rowIndex, cell ) 474 { 475 var oRow = tableMap[ rowIndex ]; 476 if ( typeof cell == 'undefined' ) 477 return oRow; 478 479 for ( var c = 0 ; oRow && c < oRow.length ; c++ ) 480 { 481 if ( cell.is && oRow[c] == cell.$ ) 482 return c; 483 else if ( c == cell ) 484 return new CKEDITOR.dom.element( oRow[ c ] ); 485 } 486 return cell.is ? -1 : null; 487 } 488 489 function cellInCol( tableMap, colIndex ) 490 { 491 var oCol = []; 492 for ( var r = 0; r < tableMap.length; r++ ) 493 { 494 var row = tableMap[ r ]; 495 oCol.push( row[ colIndex ] ); 496 497 // Avoid adding duplicate cells. 498 if ( row[ colIndex ].rowSpan > 1 ) 499 r += row[ colIndex ].rowSpan - 1; 500 } 501 return oCol; 502 } 503 504 function mergeCells( selection, mergeDirection, isDetect ) 505 { 506 var cells = getSelectedCells( selection ); 507 508 // Invalid merge request if: 509 // 1. In batch mode despite that less than two selected. 510 // 2. In solo mode while not exactly only one selected. 511 // 3. Cells distributed in different table groups (e.g. from both thead and tbody). 512 var commonAncestor; 513 if ( ( mergeDirection ? cells.length != 1 : cells.length < 2 ) 514 || ( commonAncestor = selection.getCommonAncestor() ) 515 && commonAncestor.type == CKEDITOR.NODE_ELEMENT 516 && commonAncestor.is( 'table' ) ) 517 { 518 return false; 519 } 520 521 var cell, 522 firstCell = cells[ 0 ], 523 table = firstCell.getAscendant( 'table' ), 524 map = CKEDITOR.tools.buildTableMap( table ), 525 mapHeight = map.length, 526 mapWidth = map[ 0 ].length, 527 startRow = firstCell.getParent().$.rowIndex, 528 startColumn = cellInRow( map, startRow, firstCell ); 529 530 if ( mergeDirection ) 531 { 532 var targetCell; 533 try 534 { 535 var rowspan = parseInt( firstCell.getAttribute( 'rowspan' ), 10 ) || 1; 536 var colspan = parseInt( firstCell.getAttribute( 'colspan' ), 10 ) || 1; 537 538 targetCell = 539 map[ mergeDirection == 'up' ? 540 ( startRow - rowspan ): 541 mergeDirection == 'down' ? ( startRow + rowspan ) : startRow ] [ 542 mergeDirection == 'left' ? 543 ( startColumn - colspan ): 544 mergeDirection == 'right' ? ( startColumn + colspan ) : startColumn ]; 545 546 } 547 catch( er ) 548 { 549 return false; 550 } 551 552 // 1. No cell could be merged. 553 // 2. Same cell actually. 554 if ( !targetCell || firstCell.$ == targetCell ) 555 return false; 556 557 // Sort in map order regardless of the DOM sequence. 558 cells[ ( mergeDirection == 'up' || mergeDirection == 'left' ) ? 559 'unshift' : 'push' ]( new CKEDITOR.dom.element( targetCell ) ); 560 } 561 562 // Start from here are merging way ignorance (merge up/right, batch merge). 563 var doc = firstCell.getDocument(), 564 lastRowIndex = startRow, 565 totalRowSpan = 0, 566 totalColSpan = 0, 567 // Use a documentFragment as buffer when appending cell contents. 568 frag = !isDetect && new CKEDITOR.dom.documentFragment( doc ), 569 dimension = 0; 570 571 for ( var i = 0; i < cells.length; i++ ) 572 { 573 cell = cells[ i ]; 574 575 var tr = cell.getParent(), 576 cellFirstChild = cell.getFirst(), 577 colSpan = cell.$.colSpan, 578 rowSpan = cell.$.rowSpan, 579 rowIndex = tr.$.rowIndex, 580 colIndex = cellInRow( map, rowIndex, cell ); 581 582 // Accumulated the actual places taken by all selected cells. 583 dimension += colSpan * rowSpan; 584 // Accumulated the maximum virtual spans from column and row. 585 totalColSpan = Math.max( totalColSpan, colIndex - startColumn + colSpan ) ; 586 totalRowSpan = Math.max( totalRowSpan, rowIndex - startRow + rowSpan ); 587 588 if ( !isDetect ) 589 { 590 // Trim all cell fillers and check to remove empty cells. 591 if ( trimCell( cell ), cell.getChildren().count() ) 592 { 593 // Merge vertically cells as two separated paragraphs. 594 if ( rowIndex != lastRowIndex 595 && cellFirstChild 596 && !( cellFirstChild.isBlockBoundary 597 && cellFirstChild.isBlockBoundary( { br : 1 } ) ) ) 598 { 599 var last = frag.getLast( CKEDITOR.dom.walker.whitespaces( true ) ); 600 if ( last && !( last.is && last.is( 'br' ) ) ) 601 frag.append( 'br' ); 602 } 603 604 cell.moveChildren( frag ); 605 } 606 i ? cell.remove() : cell.setHtml( '' ); 607 } 608 lastRowIndex = rowIndex; 609 } 610 611 if ( !isDetect ) 612 { 613 frag.moveChildren( firstCell ); 614 615 if ( !CKEDITOR.env.ie ) 616 firstCell.appendBogus(); 617 618 if ( totalColSpan >= mapWidth ) 619 firstCell.removeAttribute( 'rowSpan' ); 620 else 621 firstCell.$.rowSpan = totalRowSpan; 622 623 if ( totalRowSpan >= mapHeight ) 624 firstCell.removeAttribute( 'colSpan' ); 625 else 626 firstCell.$.colSpan = totalColSpan; 627 628 // Swip empty <tr> left at the end of table due to the merging. 629 var trs = new CKEDITOR.dom.nodeList( table.$.rows ), 630 count = trs.count(); 631 632 for ( i = count - 1; i >= 0; i-- ) 633 { 634 var tailTr = trs.getItem( i ); 635 if ( !tailTr.$.cells.length ) 636 { 637 tailTr.remove(); 638 count++; 639 continue; 640 } 641 } 642 643 return firstCell; 644 } 645 // Be able to merge cells only if actual dimension of selected 646 // cells equals to the caculated rectangle. 647 else 648 return ( totalRowSpan * totalColSpan ) == dimension; 649 } 650 651 function verticalSplitCell ( selection, isDetect ) 652 { 653 var cells = getSelectedCells( selection ); 654 if ( cells.length > 1 ) 655 return false; 656 else if ( isDetect ) 657 return true; 658 659 var cell = cells[ 0 ], 660 tr = cell.getParent(), 661 table = tr.getAscendant( 'table' ), 662 map = CKEDITOR.tools.buildTableMap( table ), 663 rowIndex = tr.$.rowIndex, 664 colIndex = cellInRow( map, rowIndex, cell ), 665 rowSpan = cell.$.rowSpan, 666 newCell, 667 newRowSpan, 668 newCellRowSpan, 669 newRowIndex; 670 671 if ( rowSpan > 1 ) 672 { 673 newRowSpan = Math.ceil( rowSpan / 2 ); 674 newCellRowSpan = Math.floor( rowSpan / 2 ); 675 newRowIndex = rowIndex + newRowSpan; 676 var newCellTr = new CKEDITOR.dom.element( table.$.rows[ newRowIndex ] ), 677 newCellRow = cellInRow( map, newRowIndex ), 678 candidateCell; 679 680 newCell = cell.clone(); 681 682 // Figure out where to insert the new cell by checking the vitual row. 683 for ( var c = 0; c < newCellRow.length; c++ ) 684 { 685 candidateCell = newCellRow[ c ]; 686 // Catch first cell actually following the column. 687 if ( candidateCell.parentNode == newCellTr.$ 688 && c > colIndex ) 689 { 690 newCell.insertBefore( new CKEDITOR.dom.element( candidateCell ) ); 691 break; 692 } 693 else 694 candidateCell = null; 695 } 696 697 // The destination row is empty, append at will. 698 if ( !candidateCell ) 699 newCellTr.append( newCell, true ); 700 } 701 else 702 { 703 newCellRowSpan = newRowSpan = 1; 704 705 newCellTr = tr.clone(); 706 newCellTr.insertAfter( tr ); 707 newCellTr.append( newCell = cell.clone() ); 708 709 var cellsInSameRow = cellInRow( map, rowIndex ); 710 for ( var i = 0; i < cellsInSameRow.length; i++ ) 711 cellsInSameRow[ i ].rowSpan++; 712 } 713 714 if ( !CKEDITOR.env.ie ) 715 newCell.appendBogus(); 716 717 cell.$.rowSpan = newRowSpan; 718 newCell.$.rowSpan = newCellRowSpan; 719 if ( newRowSpan == 1 ) 720 cell.removeAttribute( 'rowSpan' ); 721 if ( newCellRowSpan == 1 ) 722 newCell.removeAttribute( 'rowSpan' ); 723 724 return newCell; 725 } 726 727 function horizontalSplitCell( selection, isDetect ) 728 { 729 var cells = getSelectedCells( selection ); 730 if ( cells.length > 1 ) 731 return false; 732 else if ( isDetect ) 733 return true; 734 735 var cell = cells[ 0 ], 736 tr = cell.getParent(), 737 table = tr.getAscendant( 'table' ), 738 map = CKEDITOR.tools.buildTableMap( table ), 739 rowIndex = tr.$.rowIndex, 740 colIndex = cellInRow( map, rowIndex, cell ), 741 colSpan = cell.$.colSpan, 742 newCell, 743 newColSpan, 744 newCellColSpan; 745 746 if ( colSpan > 1 ) 747 { 748 newColSpan = Math.ceil( colSpan / 2 ); 749 newCellColSpan = Math.floor( colSpan / 2 ); 750 } 751 else 752 { 753 newCellColSpan = newColSpan = 1; 754 var cellsInSameCol = cellInCol( map, colIndex ); 755 for ( var i = 0; i < cellsInSameCol.length; i++ ) 756 cellsInSameCol[ i ].colSpan++; 757 } 758 newCell = cell.clone(); 759 newCell.insertAfter( cell ); 760 if ( !CKEDITOR.env.ie ) 761 newCell.appendBogus(); 762 763 cell.$.colSpan = newColSpan; 764 newCell.$.colSpan = newCellColSpan; 765 if ( newColSpan == 1 ) 766 cell.removeAttribute( 'colSpan' ); 767 if ( newCellColSpan == 1 ) 768 newCell.removeAttribute( 'colSpan' ); 769 770 return newCell; 771 } 772 // Context menu on table caption incorrect (#3834) 773 var contextMenuTags = { thead : 1, tbody : 1, tfoot : 1, td : 1, tr : 1, th : 1 }; 774 775 CKEDITOR.plugins.tabletools = 776 { 777 requires : [ 'table', 'dialog' ], 778 779 init : function( editor ) 780 { 781 var lang = editor.lang.table; 782 783 editor.addCommand( 'cellProperties', new CKEDITOR.dialogCommand( 'cellProperties' ) ); 784 CKEDITOR.dialog.add( 'cellProperties', this.path + 'dialogs/tableCell.js' ); 785 786 editor.addCommand( 'tableDelete', 787 { 788 exec : function( editor ) 789 { 790 var selection = editor.getSelection(), 791 startElement = selection && selection.getStartElement(), 792 table = startElement && startElement.getAscendant( 'table', 1 ); 793 794 if ( !table ) 795 return; 796 797 // If the table's parent has only one child remove it as well (unless it's the body or a table cell) (#5416, #6289) 798 var parent = table.getParent(); 799 if ( parent.getChildCount() == 1 && !parent.is( 'body', 'td', 'th' ) ) 800 table = parent; 801 802 var range = new CKEDITOR.dom.range( editor.document ); 803 range.moveToPosition( table, CKEDITOR.POSITION_BEFORE_START ); 804 table.remove(); 805 range.select(); 806 } 807 } ); 808 809 editor.addCommand( 'rowDelete', 810 { 811 exec : function( editor ) 812 { 813 var selection = editor.getSelection(); 814 placeCursorInCell( deleteRows( selection ) ); 815 } 816 } ); 817 818 editor.addCommand( 'rowInsertBefore', 819 { 820 exec : function( editor ) 821 { 822 var selection = editor.getSelection(); 823 insertRow( selection, true ); 824 } 825 } ); 826 827 editor.addCommand( 'rowInsertAfter', 828 { 829 exec : function( editor ) 830 { 831 var selection = editor.getSelection(); 832 insertRow( selection ); 833 } 834 } ); 835 836 editor.addCommand( 'columnDelete', 837 { 838 exec : function( editor ) 839 { 840 var selection = editor.getSelection(); 841 var element = deleteColumns( selection ); 842 element && placeCursorInCell( element, true ); 843 } 844 } ); 845 846 editor.addCommand( 'columnInsertBefore', 847 { 848 exec : function( editor ) 849 { 850 var selection = editor.getSelection(); 851 insertColumn( selection, true ); 852 } 853 } ); 854 855 editor.addCommand( 'columnInsertAfter', 856 { 857 exec : function( editor ) 858 { 859 var selection = editor.getSelection(); 860 insertColumn( selection ); 861 } 862 } ); 863 864 editor.addCommand( 'cellDelete', 865 { 866 exec : function( editor ) 867 { 868 var selection = editor.getSelection(); 869 deleteCells( selection ); 870 } 871 } ); 872 873 editor.addCommand( 'cellMerge', 874 { 875 exec : function( editor ) 876 { 877 placeCursorInCell( mergeCells( editor.getSelection() ), true ); 878 } 879 } ); 880 881 editor.addCommand( 'cellMergeRight', 882 { 883 exec : function( editor ) 884 { 885 placeCursorInCell( mergeCells( editor.getSelection(), 'right' ), true ); 886 } 887 } ); 888 889 editor.addCommand( 'cellMergeDown', 890 { 891 exec : function( editor ) 892 { 893 placeCursorInCell( mergeCells( editor.getSelection(), 'down' ), true ); 894 } 895 } ); 896 897 editor.addCommand( 'cellVerticalSplit', 898 { 899 exec : function( editor ) 900 { 901 placeCursorInCell( verticalSplitCell( editor.getSelection() ) ); 902 } 903 } ); 904 905 editor.addCommand( 'cellHorizontalSplit', 906 { 907 exec : function( editor ) 908 { 909 placeCursorInCell( horizontalSplitCell( editor.getSelection() ) ); 910 } 911 } ); 912 913 editor.addCommand( 'cellInsertBefore', 914 { 915 exec : function( editor ) 916 { 917 var selection = editor.getSelection(); 918 insertCell( selection, true ); 919 } 920 } ); 921 922 editor.addCommand( 'cellInsertAfter', 923 { 924 exec : function( editor ) 925 { 926 var selection = editor.getSelection(); 927 insertCell( selection ); 928 } 929 } ); 930 931 // If the "menu" plugin is loaded, register the menu items. 932 if ( editor.addMenuItems ) 933 { 934 editor.addMenuItems( 935 { 936 tablecell : 937 { 938 label : lang.cell.menu, 939 group : 'tablecell', 940 order : 1, 941 getItems : function() 942 { 943 var selection = editor.getSelection(), 944 cells = getSelectedCells( selection ); 945 return { 946 tablecell_insertBefore : CKEDITOR.TRISTATE_OFF, 947 tablecell_insertAfter : CKEDITOR.TRISTATE_OFF, 948 tablecell_delete : CKEDITOR.TRISTATE_OFF, 949 tablecell_merge : mergeCells( selection, null, true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED, 950 tablecell_merge_right : mergeCells( selection, 'right', true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED, 951 tablecell_merge_down : mergeCells( selection, 'down', true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED, 952 tablecell_split_vertical : verticalSplitCell( selection, true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED, 953 tablecell_split_horizontal : horizontalSplitCell( selection, true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED, 954 tablecell_properties : cells.length > 0 ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED 955 }; 956 } 957 }, 958 959 tablecell_insertBefore : 960 { 961 label : lang.cell.insertBefore, 962 group : 'tablecell', 963 command : 'cellInsertBefore', 964 order : 5 965 }, 966 967 tablecell_insertAfter : 968 { 969 label : lang.cell.insertAfter, 970 group : 'tablecell', 971 command : 'cellInsertAfter', 972 order : 10 973 }, 974 975 tablecell_delete : 976 { 977 label : lang.cell.deleteCell, 978 group : 'tablecell', 979 command : 'cellDelete', 980 order : 15 981 }, 982 983 tablecell_merge : 984 { 985 label : lang.cell.merge, 986 group : 'tablecell', 987 command : 'cellMerge', 988 order : 16 989 }, 990 991 tablecell_merge_right : 992 { 993 label : lang.cell.mergeRight, 994 group : 'tablecell', 995 command : 'cellMergeRight', 996 order : 17 997 }, 998 999 tablecell_merge_down : 1000 { 1001 label : lang.cell.mergeDown, 1002 group : 'tablecell', 1003 command : 'cellMergeDown', 1004 order : 18 1005 }, 1006 1007 tablecell_split_horizontal : 1008 { 1009 label : lang.cell.splitHorizontal, 1010 group : 'tablecell', 1011 command : 'cellHorizontalSplit', 1012 order : 19 1013 }, 1014 1015 tablecell_split_vertical : 1016 { 1017 label : lang.cell.splitVertical, 1018 group : 'tablecell', 1019 command : 'cellVerticalSplit', 1020 order : 20 1021 }, 1022 1023 tablecell_properties : 1024 { 1025 label : lang.cell.title, 1026 group : 'tablecellproperties', 1027 command : 'cellProperties', 1028 order : 21 1029 }, 1030 1031 tablerow : 1032 { 1033 label : lang.row.menu, 1034 group : 'tablerow', 1035 order : 1, 1036 getItems : function() 1037 { 1038 return { 1039 tablerow_insertBefore : CKEDITOR.TRISTATE_OFF, 1040 tablerow_insertAfter : CKEDITOR.TRISTATE_OFF, 1041 tablerow_delete : CKEDITOR.TRISTATE_OFF 1042 }; 1043 } 1044 }, 1045 1046 tablerow_insertBefore : 1047 { 1048 label : lang.row.insertBefore, 1049 group : 'tablerow', 1050 command : 'rowInsertBefore', 1051 order : 5 1052 }, 1053 1054 tablerow_insertAfter : 1055 { 1056 label : lang.row.insertAfter, 1057 group : 'tablerow', 1058 command : 'rowInsertAfter', 1059 order : 10 1060 }, 1061 1062 tablerow_delete : 1063 { 1064 label : lang.row.deleteRow, 1065 group : 'tablerow', 1066 command : 'rowDelete', 1067 order : 15 1068 }, 1069 1070 tablecolumn : 1071 { 1072 label : lang.column.menu, 1073 group : 'tablecolumn', 1074 order : 1, 1075 getItems : function() 1076 { 1077 return { 1078 tablecolumn_insertBefore : CKEDITOR.TRISTATE_OFF, 1079 tablecolumn_insertAfter : CKEDITOR.TRISTATE_OFF, 1080 tablecolumn_delete : CKEDITOR.TRISTATE_OFF 1081 }; 1082 } 1083 }, 1084 1085 tablecolumn_insertBefore : 1086 { 1087 label : lang.column.insertBefore, 1088 group : 'tablecolumn', 1089 command : 'columnInsertBefore', 1090 order : 5 1091 }, 1092 1093 tablecolumn_insertAfter : 1094 { 1095 label : lang.column.insertAfter, 1096 group : 'tablecolumn', 1097 command : 'columnInsertAfter', 1098 order : 10 1099 }, 1100 1101 tablecolumn_delete : 1102 { 1103 label : lang.column.deleteColumn, 1104 group : 'tablecolumn', 1105 command : 'columnDelete', 1106 order : 15 1107 } 1108 }); 1109 } 1110 1111 // If the "contextmenu" plugin is laoded, register the listeners. 1112 if ( editor.contextMenu ) 1113 { 1114 editor.contextMenu.addListener( function( element, selection ) 1115 { 1116 if ( !element || element.isReadOnly() ) 1117 return null; 1118 1119 while ( element ) 1120 { 1121 if ( element.getName() in contextMenuTags ) 1122 { 1123 return { 1124 tablecell : CKEDITOR.TRISTATE_OFF, 1125 tablerow : CKEDITOR.TRISTATE_OFF, 1126 tablecolumn : CKEDITOR.TRISTATE_OFF 1127 }; 1128 } 1129 element = element.getParent(); 1130 } 1131 1132 return null; 1133 } ); 1134 } 1135 }, 1136 1137 getSelectedCells : getSelectedCells 1138 1139 }; 1140 CKEDITOR.plugins.add( 'tabletools', CKEDITOR.plugins.tabletools ); 1141 })(); 1142 1143 /** 1144 * Create a two-dimension array that reflects the actual layout of table cells, 1145 * with cell spans, with mappings to the original td elements. 1146 * @param table {CKEDITOR.dom.element} 1147 */ 1148 CKEDITOR.tools.buildTableMap = function ( table ) 1149 { 1150 var aRows = table.$.rows ; 1151 1152 // Row and Column counters. 1153 var r = -1 ; 1154 1155 var aMap = []; 1156 1157 for ( var i = 0 ; i < aRows.length ; i++ ) 1158 { 1159 r++ ; 1160 !aMap[r] && ( aMap[r] = [] ); 1161 1162 var c = -1 ; 1163 1164 for ( var j = 0 ; j < aRows[i].cells.length ; j++ ) 1165 { 1166 var oCell = aRows[i].cells[j] ; 1167 1168 c++ ; 1169 while ( aMap[r][c] ) 1170 c++ ; 1171 1172 var iColSpan = isNaN( oCell.colSpan ) ? 1 : oCell.colSpan ; 1173 var iRowSpan = isNaN( oCell.rowSpan ) ? 1 : oCell.rowSpan ; 1174 1175 for ( var rs = 0 ; rs < iRowSpan ; rs++ ) 1176 { 1177 if ( !aMap[r + rs] ) 1178 aMap[r + rs] = []; 1179 1180 for ( var cs = 0 ; cs < iColSpan ; cs++ ) 1181 { 1182 aMap[r + rs][c + cs] = aRows[i].cells[j] ; 1183 } 1184 } 1185 1186 c += iColSpan - 1 ; 1187 } 1188 } 1189 return aMap ; 1190 }; 1191