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