table.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  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 defaultToPixel = CKEDITOR.tools.cssLength;
  7. var commitValue = function( data ) {
  8. var id = this.id;
  9. if ( !data.info )
  10. data.info = {};
  11. data.info[ id ] = this.getValue();
  12. };
  13. function tableColumns( table ) {
  14. var cols = 0,
  15. maxCols = 0;
  16. for ( var i = 0, row, rows = table.$.rows.length; i < rows; i++ ) {
  17. row = table.$.rows[ i ], cols = 0;
  18. for ( var j = 0, cell, cells = row.cells.length; j < cells; j++ ) {
  19. cell = row.cells[ j ];
  20. cols += cell.colSpan;
  21. }
  22. cols > maxCols && ( maxCols = cols );
  23. }
  24. return maxCols;
  25. }
  26. // Whole-positive-integer validator.
  27. function validatorNum( msg ) {
  28. return function() {
  29. var value = this.getValue(),
  30. pass = !!( CKEDITOR.dialog.validate.integer()( value ) && value > 0 );
  31. if ( !pass ) {
  32. alert( msg ); // jshint ignore:line
  33. this.select();
  34. }
  35. return pass;
  36. };
  37. }
  38. function tableDialog( editor, command ) {
  39. var makeElement = function( name ) {
  40. return new CKEDITOR.dom.element( name, editor.document );
  41. };
  42. var editable = editor.editable();
  43. var dialogadvtab = editor.plugins.dialogadvtab;
  44. return {
  45. title: editor.lang.table.title,
  46. minWidth: 310,
  47. minHeight: CKEDITOR.env.ie ? 310 : 280,
  48. onLoad: function() {
  49. var dialog = this;
  50. var styles = dialog.getContentElement( 'advanced', 'advStyles' );
  51. if ( styles ) {
  52. styles.on( 'change', function() {
  53. // Synchronize width value.
  54. var width = this.getStyle( 'width', '' ),
  55. txtWidth = dialog.getContentElement( 'info', 'txtWidth' );
  56. txtWidth && txtWidth.setValue( width, true );
  57. // Synchronize height value.
  58. var height = this.getStyle( 'height', '' ),
  59. txtHeight = dialog.getContentElement( 'info', 'txtHeight' );
  60. txtHeight && txtHeight.setValue( height, true );
  61. } );
  62. }
  63. },
  64. onShow: function() {
  65. // Detect if there's a selected table.
  66. var selection = editor.getSelection(),
  67. ranges = selection.getRanges(),
  68. table;
  69. var rowsInput = this.getContentElement( 'info', 'txtRows' ),
  70. colsInput = this.getContentElement( 'info', 'txtCols' ),
  71. widthInput = this.getContentElement( 'info', 'txtWidth' ),
  72. heightInput = this.getContentElement( 'info', 'txtHeight' );
  73. if ( command == 'tableProperties' ) {
  74. var selected = selection.getSelectedElement();
  75. if ( selected && selected.is( 'table' ) )
  76. table = selected;
  77. else if ( ranges.length > 0 ) {
  78. // Webkit could report the following range on cell selection (#4948):
  79. // <table><tr><td>[&nbsp;</td></tr></table>]
  80. if ( CKEDITOR.env.webkit )
  81. ranges[ 0 ].shrink( CKEDITOR.NODE_ELEMENT );
  82. table = editor.elementPath( ranges[ 0 ].getCommonAncestor( true ) ).contains( 'table', 1 );
  83. }
  84. // Save a reference to the selected table, and push a new set of default values.
  85. this._.selectedElement = table;
  86. }
  87. // Enable or disable the row, cols, width fields.
  88. if ( table ) {
  89. this.setupContent( table );
  90. rowsInput && rowsInput.disable();
  91. colsInput && colsInput.disable();
  92. } else {
  93. rowsInput && rowsInput.enable();
  94. colsInput && colsInput.enable();
  95. }
  96. // Call the onChange method for the widht and height fields so
  97. // they get reflected into the Advanced tab.
  98. widthInput && widthInput.onChange();
  99. heightInput && heightInput.onChange();
  100. },
  101. onOk: function() {
  102. var selection = editor.getSelection(),
  103. bms = this._.selectedElement && selection.createBookmarks();
  104. var table = this._.selectedElement || makeElement( 'table' ),
  105. data = {};
  106. this.commitContent( data, table );
  107. if ( data.info ) {
  108. var info = data.info;
  109. // Generate the rows and cols.
  110. if ( !this._.selectedElement ) {
  111. var tbody = table.append( makeElement( 'tbody' ) ),
  112. rows = parseInt( info.txtRows, 10 ) || 0,
  113. cols = parseInt( info.txtCols, 10 ) || 0;
  114. for ( var i = 0; i < rows; i++ ) {
  115. var row = tbody.append( makeElement( 'tr' ) );
  116. for ( var j = 0; j < cols; j++ ) {
  117. var cell = row.append( makeElement( 'td' ) );
  118. cell.appendBogus();
  119. }
  120. }
  121. }
  122. // Modify the table headers. Depends on having rows and cols generated
  123. // correctly so it can't be done in commit functions.
  124. // Should we make a <thead>?
  125. var headers = info.selHeaders;
  126. if ( !table.$.tHead && ( headers == 'row' || headers == 'both' ) ) {
  127. var thead = new CKEDITOR.dom.element( table.$.createTHead() );
  128. tbody = table.getElementsByTag( 'tbody' ).getItem( 0 );
  129. var theRow = tbody.getElementsByTag( 'tr' ).getItem( 0 );
  130. // Change TD to TH:
  131. for ( i = 0; i < theRow.getChildCount(); i++ ) {
  132. var th = theRow.getChild( i );
  133. // Skip bookmark nodes. (#6155)
  134. if ( th.type == CKEDITOR.NODE_ELEMENT && !th.data( 'cke-bookmark' ) ) {
  135. th.renameNode( 'th' );
  136. th.setAttribute( 'scope', 'col' );
  137. }
  138. }
  139. thead.append( theRow.remove() );
  140. }
  141. if ( table.$.tHead !== null && !( headers == 'row' || headers == 'both' ) ) {
  142. // Move the row out of the THead and put it in the TBody:
  143. thead = new CKEDITOR.dom.element( table.$.tHead );
  144. tbody = table.getElementsByTag( 'tbody' ).getItem( 0 );
  145. var previousFirstRow = tbody.getFirst();
  146. while ( thead.getChildCount() > 0 ) {
  147. theRow = thead.getFirst();
  148. for ( i = 0; i < theRow.getChildCount(); i++ ) {
  149. var newCell = theRow.getChild( i );
  150. if ( newCell.type == CKEDITOR.NODE_ELEMENT ) {
  151. newCell.renameNode( 'td' );
  152. newCell.removeAttribute( 'scope' );
  153. }
  154. }
  155. theRow.insertBefore( previousFirstRow );
  156. }
  157. thead.remove();
  158. }
  159. // Should we make all first cells in a row TH?
  160. if ( !this.hasColumnHeaders && ( headers == 'col' || headers == 'both' ) ) {
  161. for ( row = 0; row < table.$.rows.length; row++ ) {
  162. newCell = new CKEDITOR.dom.element( table.$.rows[ row ].cells[ 0 ] );
  163. newCell.renameNode( 'th' );
  164. newCell.setAttribute( 'scope', 'row' );
  165. }
  166. }
  167. // Should we make all first TH-cells in a row make TD? If 'yes' we do it the other way round :-)
  168. if ( ( this.hasColumnHeaders ) && !( headers == 'col' || headers == 'both' ) ) {
  169. for ( i = 0; i < table.$.rows.length; i++ ) {
  170. row = new CKEDITOR.dom.element( table.$.rows[ i ] );
  171. if ( row.getParent().getName() == 'tbody' ) {
  172. newCell = new CKEDITOR.dom.element( row.$.cells[ 0 ] );
  173. newCell.renameNode( 'td' );
  174. newCell.removeAttribute( 'scope' );
  175. }
  176. }
  177. }
  178. // Set the width and height.
  179. info.txtHeight ? table.setStyle( 'height', info.txtHeight ) : table.removeStyle( 'height' );
  180. info.txtWidth ? table.setStyle( 'width', info.txtWidth ) : table.removeStyle( 'width' );
  181. if ( !table.getAttribute( 'style' ) )
  182. table.removeAttribute( 'style' );
  183. }
  184. // Insert the table element if we're creating one.
  185. if ( !this._.selectedElement ) {
  186. editor.insertElement( table );
  187. // Override the default cursor position after insertElement to place
  188. // cursor inside the first cell (#7959), IE needs a while.
  189. setTimeout( function() {
  190. var firstCell = new CKEDITOR.dom.element( table.$.rows[ 0 ].cells[ 0 ] );
  191. var range = editor.createRange();
  192. range.moveToPosition( firstCell, CKEDITOR.POSITION_AFTER_START );
  193. range.select();
  194. }, 0 );
  195. }
  196. // Properly restore the selection, (#4822) but don't break
  197. // because of this, e.g. updated table caption.
  198. else {
  199. try {
  200. selection.selectBookmarks( bms );
  201. } catch ( er ) {
  202. }
  203. }
  204. },
  205. contents: [ {
  206. id: 'info',
  207. label: editor.lang.table.title,
  208. elements: [ {
  209. type: 'hbox',
  210. widths: [ null, null ],
  211. styles: [ 'vertical-align:top' ],
  212. children: [ {
  213. type: 'vbox',
  214. padding: 0,
  215. children: [ {
  216. type: 'text',
  217. id: 'txtRows',
  218. 'default': 3,
  219. label: editor.lang.table.rows,
  220. required: true,
  221. controlStyle: 'width:5em',
  222. validate: validatorNum( editor.lang.table.invalidRows ),
  223. setup: function( selectedElement ) {
  224. this.setValue( selectedElement.$.rows.length );
  225. },
  226. commit: commitValue
  227. },
  228. {
  229. type: 'text',
  230. id: 'txtCols',
  231. 'default': 2,
  232. label: editor.lang.table.columns,
  233. required: true,
  234. controlStyle: 'width:5em',
  235. validate: validatorNum( editor.lang.table.invalidCols ),
  236. setup: function( selectedTable ) {
  237. this.setValue( tableColumns( selectedTable ) );
  238. },
  239. commit: commitValue
  240. },
  241. {
  242. type: 'html',
  243. html: '&nbsp;'
  244. },
  245. {
  246. type: 'select',
  247. id: 'selHeaders',
  248. requiredContent: 'th',
  249. 'default': '',
  250. label: editor.lang.table.headers,
  251. items: [
  252. [ editor.lang.table.headersNone, '' ],
  253. [ editor.lang.table.headersRow, 'row' ],
  254. [ editor.lang.table.headersColumn, 'col' ],
  255. [ editor.lang.table.headersBoth, 'both' ]
  256. ],
  257. setup: function( selectedTable ) {
  258. // Fill in the headers field.
  259. var dialog = this.getDialog();
  260. dialog.hasColumnHeaders = true;
  261. // Check if all the first cells in every row are TH
  262. for ( var row = 0; row < selectedTable.$.rows.length; row++ ) {
  263. // If just one cell isn't a TH then it isn't a header column
  264. var headCell = selectedTable.$.rows[ row ].cells[ 0 ];
  265. if ( headCell && headCell.nodeName.toLowerCase() != 'th' ) {
  266. dialog.hasColumnHeaders = false;
  267. break;
  268. }
  269. }
  270. // Check if the table contains <thead>.
  271. if ( ( selectedTable.$.tHead !== null ) )
  272. this.setValue( dialog.hasColumnHeaders ? 'both' : 'row' );
  273. else
  274. this.setValue( dialog.hasColumnHeaders ? 'col' : '' );
  275. },
  276. commit: commitValue
  277. },
  278. {
  279. type: 'text',
  280. id: 'txtBorder',
  281. requiredContent: 'table[border]',
  282. // Avoid setting border which will then disappear.
  283. 'default': editor.filter.check( 'table[border]' ) ? 1 : 0,
  284. label: editor.lang.table.border,
  285. controlStyle: 'width:3em',
  286. validate: CKEDITOR.dialog.validate.number( editor.lang.table.invalidBorder ),
  287. setup: function( selectedTable ) {
  288. this.setValue( selectedTable.getAttribute( 'border' ) || '' );
  289. },
  290. commit: function( data, selectedTable ) {
  291. if ( this.getValue() )
  292. selectedTable.setAttribute( 'border', this.getValue() );
  293. else
  294. selectedTable.removeAttribute( 'border' );
  295. }
  296. },
  297. {
  298. id: 'cmbAlign',
  299. type: 'select',
  300. requiredContent: 'table[align]',
  301. 'default': '',
  302. label: editor.lang.common.align,
  303. items: [
  304. [ editor.lang.common.notSet, '' ],
  305. [ editor.lang.common.alignLeft, 'left' ],
  306. [ editor.lang.common.alignCenter, 'center' ],
  307. [ editor.lang.common.alignRight, 'right' ]
  308. ],
  309. setup: function( selectedTable ) {
  310. this.setValue( selectedTable.getAttribute( 'align' ) || '' );
  311. },
  312. commit: function( data, selectedTable ) {
  313. if ( this.getValue() )
  314. selectedTable.setAttribute( 'align', this.getValue() );
  315. else
  316. selectedTable.removeAttribute( 'align' );
  317. }
  318. } ]
  319. },
  320. {
  321. type: 'vbox',
  322. padding: 0,
  323. children: [ {
  324. type: 'hbox',
  325. widths: [ '5em' ],
  326. children: [ {
  327. type: 'text',
  328. id: 'txtWidth',
  329. requiredContent: 'table{width}',
  330. controlStyle: 'width:5em',
  331. label: editor.lang.common.width,
  332. title: editor.lang.common.cssLengthTooltip,
  333. // Smarter default table width. (#9600)
  334. 'default': editor.filter.check( 'table{width}' ) ? ( editable.getSize( 'width' ) < 500 ? '100%' : 500 ) : 0,
  335. getValue: defaultToPixel,
  336. validate: CKEDITOR.dialog.validate.cssLength( editor.lang.common.invalidCssLength.replace( '%1', editor.lang.common.width ) ),
  337. onChange: function() {
  338. var styles = this.getDialog().getContentElement( 'advanced', 'advStyles' );
  339. styles && styles.updateStyle( 'width', this.getValue() );
  340. },
  341. setup: function( selectedTable ) {
  342. var val = selectedTable.getStyle( 'width' );
  343. this.setValue( val );
  344. },
  345. commit: commitValue
  346. } ]
  347. },
  348. {
  349. type: 'hbox',
  350. widths: [ '5em' ],
  351. children: [ {
  352. type: 'text',
  353. id: 'txtHeight',
  354. requiredContent: 'table{height}',
  355. controlStyle: 'width:5em',
  356. label: editor.lang.common.height,
  357. title: editor.lang.common.cssLengthTooltip,
  358. 'default': '',
  359. getValue: defaultToPixel,
  360. validate: CKEDITOR.dialog.validate.cssLength( editor.lang.common.invalidCssLength.replace( '%1', editor.lang.common.height ) ),
  361. onChange: function() {
  362. var styles = this.getDialog().getContentElement( 'advanced', 'advStyles' );
  363. styles && styles.updateStyle( 'height', this.getValue() );
  364. },
  365. setup: function( selectedTable ) {
  366. var val = selectedTable.getStyle( 'height' );
  367. val && this.setValue( val );
  368. },
  369. commit: commitValue
  370. } ]
  371. },
  372. {
  373. type: 'html',
  374. html: '&nbsp;'
  375. },
  376. {
  377. type: 'text',
  378. id: 'txtCellSpace',
  379. requiredContent: 'table[cellspacing]',
  380. controlStyle: 'width:3em',
  381. label: editor.lang.table.cellSpace,
  382. 'default': editor.filter.check( 'table[cellspacing]' ) ? 1 : 0,
  383. validate: CKEDITOR.dialog.validate.number( editor.lang.table.invalidCellSpacing ),
  384. setup: function( selectedTable ) {
  385. this.setValue( selectedTable.getAttribute( 'cellSpacing' ) || '' );
  386. },
  387. commit: function( data, selectedTable ) {
  388. if ( this.getValue() )
  389. selectedTable.setAttribute( 'cellSpacing', this.getValue() );
  390. else
  391. selectedTable.removeAttribute( 'cellSpacing' );
  392. }
  393. },
  394. {
  395. type: 'text',
  396. id: 'txtCellPad',
  397. requiredContent: 'table[cellpadding]',
  398. controlStyle: 'width:3em',
  399. label: editor.lang.table.cellPad,
  400. 'default': editor.filter.check( 'table[cellpadding]' ) ? 1 : 0,
  401. validate: CKEDITOR.dialog.validate.number( editor.lang.table.invalidCellPadding ),
  402. setup: function( selectedTable ) {
  403. this.setValue( selectedTable.getAttribute( 'cellPadding' ) || '' );
  404. },
  405. commit: function( data, selectedTable ) {
  406. if ( this.getValue() )
  407. selectedTable.setAttribute( 'cellPadding', this.getValue() );
  408. else
  409. selectedTable.removeAttribute( 'cellPadding' );
  410. }
  411. } ]
  412. } ]
  413. },
  414. {
  415. type: 'html',
  416. align: 'right',
  417. html: ''
  418. },
  419. {
  420. type: 'vbox',
  421. padding: 0,
  422. children: [ {
  423. type: 'text',
  424. id: 'txtCaption',
  425. requiredContent: 'caption',
  426. label: editor.lang.table.caption,
  427. setup: function( selectedTable ) {
  428. this.enable();
  429. var nodeList = selectedTable.getElementsByTag( 'caption' );
  430. if ( nodeList.count() > 0 ) {
  431. var caption = nodeList.getItem( 0 );
  432. var firstElementChild = caption.getFirst( CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_ELEMENT ) );
  433. if ( firstElementChild && !firstElementChild.equals( caption.getBogus() ) ) {
  434. this.disable();
  435. this.setValue( caption.getText() );
  436. return;
  437. }
  438. caption = CKEDITOR.tools.trim( caption.getText() );
  439. this.setValue( caption );
  440. }
  441. },
  442. commit: function( data, table ) {
  443. if ( !this.isEnabled() )
  444. return;
  445. var caption = this.getValue(),
  446. captionElement = table.getElementsByTag( 'caption' );
  447. if ( caption ) {
  448. if ( captionElement.count() > 0 ) {
  449. captionElement = captionElement.getItem( 0 );
  450. captionElement.setHtml( '' );
  451. } else {
  452. captionElement = new CKEDITOR.dom.element( 'caption', editor.document );
  453. if ( table.getChildCount() )
  454. captionElement.insertBefore( table.getFirst() );
  455. else
  456. captionElement.appendTo( table );
  457. }
  458. captionElement.append( new CKEDITOR.dom.text( caption, editor.document ) );
  459. } else if ( captionElement.count() > 0 ) {
  460. for ( var i = captionElement.count() - 1; i >= 0; i-- )
  461. captionElement.getItem( i ).remove();
  462. }
  463. }
  464. },
  465. {
  466. type: 'text',
  467. id: 'txtSummary',
  468. bidi: true,
  469. requiredContent: 'table[summary]',
  470. label: editor.lang.table.summary,
  471. setup: function( selectedTable ) {
  472. this.setValue( selectedTable.getAttribute( 'summary' ) || '' );
  473. },
  474. commit: function( data, selectedTable ) {
  475. if ( this.getValue() )
  476. selectedTable.setAttribute( 'summary', this.getValue() );
  477. else
  478. selectedTable.removeAttribute( 'summary' );
  479. }
  480. } ]
  481. } ]
  482. },
  483. dialogadvtab && dialogadvtab.createAdvancedTab( editor, null, 'table' )
  484. ] };
  485. }
  486. CKEDITOR.dialog.add( 'table', function( editor ) {
  487. return tableDialog( editor, 'table' );
  488. } );
  489. CKEDITOR.dialog.add( 'tableProperties', function( editor ) {
  490. return tableDialog( editor, 'tableProperties' );
  491. } );
  492. } )();