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