plugin.js 30 KB

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