1 /* 2 Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved. 3 For licensing, see LICENSE.html or http://ckeditor.com/license 4 */ 5 6 (function() 7 { 8 function removeRawAttribute( $node, attr ) 9 { 10 if ( CKEDITOR.env.ie ) 11 $node.removeAttribute( attr ); 12 else 13 delete $node[ attr ]; 14 } 15 16 var cellNodeRegex = /^(?:td|th)$/; 17 18 function getSelectedCells( selection ) 19 { 20 // Walker will try to split text nodes, which will make the current selection 21 // invalid. So save bookmarks before doing anything. 22 var bookmarks = selection.createBookmarks(); 23 24 var ranges = selection.getRanges(); 25 var retval = []; 26 var database = {}; 27 28 function moveOutOfCellGuard( node ) 29 { 30 // Apply to the first cell only. 31 if ( retval.length > 0 ) 32 return; 33 34 // If we are exiting from the first </td>, then the td should definitely be 35 // included. 36 if ( node.type == CKEDITOR.NODE_ELEMENT && cellNodeRegex.test( node.getName() ) 37 && !node.getCustomData( 'selected_cell' ) ) 38 { 39 CKEDITOR.dom.element.setMarker( database, node, 'selected_cell', true ); 40 retval.push( node ); 41 } 42 } 43 44 for ( var i = 0 ; i < ranges.length ; i++ ) 45 { 46 var range = ranges[ i ]; 47 48 if ( range.collapsed ) 49 { 50 // Walker does not handle collapsed ranges yet - fall back to old API. 51 var startNode = range.getCommonAncestor(); 52 var nearestCell = startNode.getAscendant( 'td', true ) || startNode.getAscendant( 'th', true ); 53 if ( nearestCell ) 54 retval.push( nearestCell ); 55 } 56 else 57 { 58 var walker = new CKEDITOR.dom.walker( range ); 59 var node; 60 walker.guard = moveOutOfCellGuard; 61 62 while ( ( node = walker.next() ) ) 63 { 64 // If may be possible for us to have a range like this: 65 // <td>^1</td><td>^2</td> 66 // The 2nd td shouldn't be included. 67 // 68 // So we have to take care to include a td we've entered only when we've 69 // walked into its children. 70 71 var parent = node.getParent(); 72 if ( parent && cellNodeRegex.test( parent.getName() ) && !parent.getCustomData( 'selected_cell' ) ) 73 { 74 CKEDITOR.dom.element.setMarker( database, parent, 'selected_cell', true ); 75 retval.push( parent ); 76 } 77 } 78 } 79 } 80 81 CKEDITOR.dom.element.clearAllMarkers( database ); 82 83 // Restore selection position. 84 selection.selectBookmarks( bookmarks ); 85 86 return retval; 87 } 88 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 241 function clearRow( $tr ) 242 { 243 // Get the array of row's cells. 244 var $cells = $tr.cells; 245 246 // Empty all cells. 247 for ( var i = 0 ; i < $cells.length ; i++ ) 248 { 249 $cells[ i ].innerHTML = ''; 250 251 if ( !CKEDITOR.env.ie ) 252 ( new CKEDITOR.dom.element( $cells[ i ] ) ).appendBogus(); 253 } 254 } 255 256 function insertRow( selection, insertBefore ) 257 { 258 // Get the row where the selection is placed in. 259 var row = selection.getStartElement().getAscendant( 'tr' ); 260 if ( !row ) 261 return; 262 263 // Create a clone of the row. 264 var newRow = row.clone( true ); 265 266 // Insert the new row before of it. 267 newRow.insertBefore( row ); 268 269 // Clean one of the rows to produce the illusion of inserting an empty row 270 // before or after. 271 clearRow( insertBefore ? newRow.$ : row.$ ); 272 } 273 274 function deleteRows( selectionOrRow ) 275 { 276 if ( selectionOrRow instanceof CKEDITOR.dom.selection ) 277 { 278 var cells = getSelectedCells( selectionOrRow ); 279 var rowsToDelete = []; 280 281 // Queue up the rows - it's possible and likely that we have duplicates. 282 for ( var i = 0 ; i < cells.length ; i++ ) 283 { 284 var row = cells[ i ].getParent(); 285 rowsToDelete[ row.$.rowIndex ] = row; 286 } 287 288 for ( i = rowsToDelete.length ; i >= 0 ; i-- ) 289 { 290 if ( rowsToDelete[ i ] ) 291 deleteRows( rowsToDelete[ i ] ); 292 } 293 } 294 else if ( selectionOrRow instanceof CKEDITOR.dom.element ) 295 { 296 var table = selectionOrRow.getAscendant( 'table' ); 297 298 if ( table.$.rows.length == 1 ) 299 table.remove(); 300 else 301 selectionOrRow.remove(); 302 } 303 } 304 305 function insertColumn( selection, insertBefore ) 306 { 307 // Get the cell where the selection is placed in. 308 var startElement = selection.getStartElement(); 309 var cell = startElement.getAscendant( 'td', true ) || startElement.getAscendant( 'th', true ); 310 311 if ( !cell ) 312 return; 313 314 // Get the cell's table. 315 var table = cell.getAscendant( 'table' ); 316 var cellIndex = cell.$.cellIndex; 317 318 // Loop through all rows available in the table. 319 for ( var i = 0 ; i < table.$.rows.length ; i++ ) 320 { 321 var $row = table.$.rows[ i ]; 322 323 // If the row doesn't have enough cells, ignore it. 324 if ( $row.cells.length < ( cellIndex + 1 ) ) 325 continue; 326 327 cell = new CKEDITOR.dom.element( $row.cells[ cellIndex ].cloneNode( false ) ); 328 329 if ( !CKEDITOR.env.ie ) 330 cell.appendBogus(); 331 332 // Get back the currently selected cell. 333 var baseCell = new CKEDITOR.dom.element( $row.cells[ cellIndex ] ); 334 if ( insertBefore ) 335 cell.insertBefore( baseCell ); 336 else 337 cell.insertAfter( baseCell ); 338 } 339 } 340 341 function deleteColumns( selectionOrCell ) 342 { 343 if ( selectionOrCell instanceof CKEDITOR.dom.selection ) 344 { 345 var colsToDelete = getSelectedCells( selectionOrCell ); 346 for ( var i = colsToDelete.length ; i >= 0 ; i-- ) 347 { 348 if ( colsToDelete[ i ] ) 349 deleteColumns( colsToDelete[ i ] ); 350 } 351 } 352 else if ( selectionOrCell instanceof CKEDITOR.dom.element ) 353 { 354 // Get the cell's table. 355 var table = selectionOrCell.getAscendant( 'table' ); 356 357 // Get the cell index. 358 var cellIndex = selectionOrCell.$.cellIndex; 359 360 /* 361 * Loop through all rows from down to up, coz it's possible that some rows 362 * will be deleted. 363 */ 364 for ( i = table.$.rows.length - 1 ; i >= 0 ; i-- ) 365 { 366 // Get the row. 367 var row = new CKEDITOR.dom.element( table.$.rows[ i ] ); 368 369 // If the cell to be removed is the first one and the row has just one cell. 370 if ( !cellIndex && row.$.cells.length == 1 ) 371 { 372 deleteRows( row ); 373 continue; 374 } 375 376 // Else, just delete the cell. 377 if ( row.$.cells[ cellIndex ] ) 378 row.$.removeChild( row.$.cells[ cellIndex ] ); 379 } 380 } 381 } 382 383 function insertCell( selection, insertBefore ) 384 { 385 var startElement = selection.getStartElement(); 386 var cell = startElement.getAscendant( 'td', true ) || startElement.getAscendant( 'th', true ); 387 388 if ( !cell ) 389 return; 390 391 // Create the new cell element to be added. 392 var newCell = cell.clone(); 393 if ( !CKEDITOR.env.ie ) 394 newCell.appendBogus(); 395 396 if ( insertBefore ) 397 newCell.insertBefore( cell ); 398 else 399 newCell.insertAfter( cell ); 400 } 401 402 function deleteCells( selectionOrCell ) 403 { 404 if ( selectionOrCell instanceof CKEDITOR.dom.selection ) 405 { 406 var cellsToDelete = getSelectedCells( selectionOrCell ); 407 for ( var i = cellsToDelete.length - 1 ; i >= 0 ; i-- ) 408 deleteCells( cellsToDelete[ i ] ); 409 } 410 else if ( selectionOrCell instanceof CKEDITOR.dom.element ) 411 { 412 if ( selectionOrCell.getParent().getChildCount() == 1 ) 413 selectionOrCell.getParent().remove(); 414 else 415 selectionOrCell.remove(); 416 } 417 } 418 419 CKEDITOR.plugins.add( 'tabletools', 420 { 421 init : function( editor ) 422 { 423 var lang = editor.lang.table; 424 425 editor.addCommand( 'cellProperties', new CKEDITOR.dialogCommand( 'cellProperties' ) ); 426 CKEDITOR.dialog.add( 'cellProperties', this.path + 'dialogs/tableCell.js' ); 427 428 editor.addCommand( 'tableDelete', 429 { 430 exec : function( editor ) 431 { 432 var selection = editor.getSelection(); 433 var startElement = selection && selection.getStartElement(); 434 var table = startElement && startElement.getAscendant( 'table', true ); 435 436 if ( !table ) 437 return; 438 439 // Maintain the selection point at where the table was deleted. 440 selection.selectElement( table ); 441 var range = selection.getRanges()[0]; 442 range.collapse(); 443 selection.selectRanges( [ range ] ); 444 445 // If the table's parent has only one child, remove it as well. 446 if ( table.getParent().getChildCount() == 1 ) 447 table.getParent().remove(); 448 else 449 table.remove(); 450 } 451 } ); 452 453 editor.addCommand( 'rowDelete', 454 { 455 exec : function( editor ) 456 { 457 var selection = editor.getSelection(); 458 deleteRows( selection ); 459 } 460 } ); 461 462 editor.addCommand( 'rowInsertBefore', 463 { 464 exec : function( editor ) 465 { 466 var selection = editor.getSelection(); 467 insertRow( selection, true ); 468 } 469 } ); 470 471 editor.addCommand( 'rowInsertAfter', 472 { 473 exec : function( editor ) 474 { 475 var selection = editor.getSelection(); 476 insertRow( selection ); 477 } 478 } ); 479 480 editor.addCommand( 'columnDelete', 481 { 482 exec : function( editor ) 483 { 484 var selection = editor.getSelection(); 485 deleteColumns( selection ); 486 } 487 } ); 488 489 editor.addCommand( 'columnInsertBefore', 490 { 491 exec : function( editor ) 492 { 493 var selection = editor.getSelection(); 494 insertColumn( selection, true ); 495 } 496 } ); 497 498 editor.addCommand( 'columnInsertAfter', 499 { 500 exec : function( editor ) 501 { 502 var selection = editor.getSelection(); 503 insertColumn( selection ); 504 } 505 } ); 506 507 editor.addCommand( 'cellDelete', 508 { 509 exec : function( editor ) 510 { 511 var selection = editor.getSelection(); 512 deleteCells( selection ); 513 } 514 } ); 515 516 editor.addCommand( 'cellInsertBefore', 517 { 518 exec : function( editor ) 519 { 520 var selection = editor.getSelection(); 521 insertCell( selection, true ); 522 } 523 } ); 524 525 editor.addCommand( 'cellInsertAfter', 526 { 527 exec : function( editor ) 528 { 529 var selection = editor.getSelection(); 530 insertCell( selection ); 531 } 532 } ); 533 534 // If the "menu" plugin is loaded, register the menu items. 535 if ( editor.addMenuItems ) 536 { 537 editor.addMenuItems( 538 { 539 tablecell : 540 { 541 label : lang.cell.menu, 542 group : 'tablecell', 543 order : 1, 544 getItems : function() 545 { 546 var cells = getSelectedCells( editor.getSelection() ); 547 548 return { 549 tablecell_insertBefore : CKEDITOR.TRISTATE_OFF, 550 tablecell_insertAfter : CKEDITOR.TRISTATE_OFF, 551 tablecell_delete : CKEDITOR.TRISTATE_OFF, 552 tablecell_properties : cells.length == 1 ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED 553 }; 554 } 555 }, 556 557 tablecell_insertBefore : 558 { 559 label : lang.cell.insertBefore, 560 group : 'tablecell', 561 command : 'cellInsertBefore', 562 order : 5 563 }, 564 565 tablecell_insertAfter : 566 { 567 label : lang.cell.insertAfter, 568 group : 'tablecell', 569 command : 'cellInsertAfter', 570 order : 10 571 }, 572 573 tablecell_delete : 574 { 575 label : lang.cell.deleteCell, 576 group : 'tablecell', 577 command : 'cellDelete', 578 order : 15 579 }, 580 581 tablecell_properties : 582 { 583 label : lang.cell.title, 584 group : 'tablecellproperties', 585 command : 'cellProperties', 586 order : 20 587 }, 588 589 tablerow : 590 { 591 label : lang.row.menu, 592 group : 'tablerow', 593 order : 1, 594 getItems : function() 595 { 596 return { 597 tablerow_insertBefore : CKEDITOR.TRISTATE_OFF, 598 tablerow_insertAfter : CKEDITOR.TRISTATE_OFF, 599 tablerow_delete : CKEDITOR.TRISTATE_OFF 600 }; 601 } 602 }, 603 604 tablerow_insertBefore : 605 { 606 label : lang.row.insertBefore, 607 group : 'tablerow', 608 command : 'rowInsertBefore', 609 order : 5 610 }, 611 612 tablerow_insertAfter : 613 { 614 label : lang.row.insertAfter, 615 group : 'tablerow', 616 command : 'rowInsertAfter', 617 order : 10 618 }, 619 620 tablerow_delete : 621 { 622 label : lang.row.deleteRow, 623 group : 'tablerow', 624 command : 'rowDelete', 625 order : 15 626 }, 627 628 tablecolumn : 629 { 630 label : lang.column.menu, 631 group : 'tablecolumn', 632 order : 1, 633 getItems : function() 634 { 635 return { 636 tablecolumn_insertBefore : CKEDITOR.TRISTATE_OFF, 637 tablecolumn_insertAfter : CKEDITOR.TRISTATE_OFF, 638 tablecolumn_delete : CKEDITOR.TRISTATE_OFF 639 }; 640 } 641 }, 642 643 tablecolumn_insertBefore : 644 { 645 label : lang.column.insertBefore, 646 group : 'tablecolumn', 647 command : 'columnInsertBefore', 648 order : 5 649 }, 650 651 tablecolumn_insertAfter : 652 { 653 label : lang.column.insertAfter, 654 group : 'tablecolumn', 655 command : 'columnInsertAfter', 656 order : 10 657 }, 658 659 tablecolumn_delete : 660 { 661 label : lang.column.deleteColumn, 662 group : 'tablecolumn', 663 command : 'columnDelete', 664 order : 15 665 } 666 }); 667 } 668 669 // If the "contextmenu" plugin is laoded, register the listeners. 670 if ( editor.contextMenu ) 671 { 672 editor.contextMenu.addListener( function( element, selection ) 673 { 674 if ( !element ) 675 return null; 676 677 var isCell = !element.is( 'table' ) && element.hasAscendant( 'table' ) ; 678 679 if ( isCell ) 680 { 681 return { 682 tablecell : CKEDITOR.TRISTATE_OFF, 683 tablerow : CKEDITOR.TRISTATE_OFF, 684 tablecolumn : CKEDITOR.TRISTATE_OFF 685 }; 686 } 687 688 return null; 689 } ); 690 } 691 } 692 } ); 693 })(); 694