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