4 * Copyright, Moxiecode Systems AB
5 * Released under LGPL License.
7 * License: http://www.tinymce.com/license
8 * Contributing: http://www.tinymce.com/contributing
12 * This class contains all core logic for the table plugin.
14 * @class tinymce.tableplugin.Plugin
17 define("tinymce/tableplugin/Plugin", [
18 "tinymce/tableplugin/TableGrid",
19 "tinymce/tableplugin/Quirks",
20 "tinymce/tableplugin/CellSelection",
22 "tinymce/dom/TreeWalker",
24 "tinymce/PluginManager"
25 ], function(TableGrid, Quirks, CellSelection, Tools, TreeWalker, Env, PluginManager) {
26 var each = Tools.each;
28 function Plugin(editor) {
29 var winMan, clipboardRows, self = this; // Might be selected cells on reload
31 function removePxSuffix(size) {
32 return size ? size.replace(/px$/, '') : "";
35 function addSizeSuffix(size) {
36 if (/^[0-9]+$/.test(size)) {
43 function unApplyAlign(elm) {
44 each('left center right'.split(' '), function(name) {
45 editor.formatter.remove('align' + name, {}, elm);
49 function tableDialog() {
50 var dom = editor.dom, tableElm, data, createNewTable;
52 tableElm = editor.dom.getParent(editor.selection.getStart(), 'table');
53 createNewTable = false;
56 width: removePxSuffix(dom.getStyle(tableElm, 'width') || dom.getAttrib(tableElm, 'width')),
57 height: removePxSuffix(dom.getStyle(tableElm, 'height') || dom.getAttrib(tableElm, 'height')),
58 cellspacing: dom.getAttrib(tableElm, 'cellspacing'),
59 cellpadding: dom.getAttrib(tableElm, 'cellpadding'),
60 border: dom.getAttrib(tableElm, 'border'),
61 caption: !!dom.select('caption', tableElm)[0]
64 each('left center right'.split(' '), function(name) {
65 if (editor.formatter.matchNode(tableElm, 'align' + name)) {
70 editor.windowManager.open({
71 title: "Table properties",
82 createNewTable ? {label: 'Cols', name: 'cols', disabled: true} : null,
83 createNewTable ? {label: 'Rows', name: 'rows', disabled: true} : null,
84 {label: 'Width', name: 'width'},
85 {label: 'Height', name: 'height'},
86 {label: 'Cell spacing', name: 'cellspacing'},
87 {label: 'Cell padding', name: 'cellpadding'},
88 {label: 'Border', name: 'border'},
89 {label: 'Caption', name: 'caption', type: 'checkbox'},
98 {text: 'None', value: ''},
99 {text: 'Left', value: 'left'},
100 {text: 'Center', value: 'center'},
101 {text: 'Right', value: 'right'}
107 onsubmit: function() {
108 var data = this.toJSON(), captionElm;
110 editor.undoManager.transact(function() {
111 editor.dom.setAttribs(tableElm, {
112 cellspacing: data.cellspacing,
113 cellpadding: data.cellpadding,
117 editor.dom.setStyles(tableElm, {
118 width: addSizeSuffix(data.width),
119 height: addSizeSuffix(data.height)
122 // Toggle caption on/off
123 captionElm = dom.select('caption', tableElm)[0];
125 if (captionElm && !data.caption) {
126 dom.remove(captionElm);
129 if (!captionElm && data.caption) {
130 captionElm = dom.create('caption');
133 captionElm.innerHTML = '<br data-mce-bogus="1"/>';
136 tableElm.insertBefore(captionElm, tableElm.firstChild);
139 unApplyAlign(tableElm);
141 editor.formatter.apply('align' + data.align, {}, tableElm);
151 function mergeDialog(grid, cell) {
152 editor.windowManager.open({
153 title: "Merge cells",
155 {label: 'Cols', name: 'cols', type: 'textbox', size: 10},
156 {label: 'Rows', name: 'rows', type: 'textbox', size: 10}
158 onsubmit: function() {
159 var data = this.toJSON();
161 editor.undoManager.transact(function() {
162 grid.merge(cell, data.cols, data.rows);
168 function cellDialog() {
169 var dom = editor.dom, cellElm, data, cells = [];
171 // Get selected cells or the current cell
172 cells = editor.dom.select('td.mce-item-selected,th.mce-item-selected');
173 cellElm = editor.dom.getParent(editor.selection.getStart(), 'td,th');
174 if (!cells.length && cellElm) {
178 cellElm = cellElm || cells[0];
181 width: removePxSuffix(dom.getStyle(cellElm, 'width') || dom.getAttrib(cellElm, 'width')),
182 height: removePxSuffix(dom.getStyle(cellElm, 'height') || dom.getAttrib(cellElm, 'height')),
183 scope: dom.getAttrib(cellElm, 'scope')
186 data.type = cellElm.nodeName.toLowerCase();
188 each('left center right'.split(' '), function(name) {
189 if (editor.formatter.matchNode(cellElm, 'align' + name)) {
194 editor.windowManager.open({
195 title: "Cell properties",
206 {label: 'Width', name: 'width'},
207 {label: 'Height', name: 'height'},
216 {text: 'Cell', value: 'td'},
217 {text: 'Header cell', value: 'th'}
228 {text: 'None', value: ''},
229 {text: 'Row', value: 'row'},
230 {text: 'Column', value: 'col'},
231 {text: 'Row group', value: 'rowgroup'},
232 {text: 'Column group', value: 'colgroup'}
243 {text: 'None', value: ''},
244 {text: 'Left', value: 'left'},
245 {text: 'Center', value: 'center'},
246 {text: 'Right', value: 'right'}
252 onsubmit: function() {
253 var data = this.toJSON();
255 editor.undoManager.transact(function() {
256 each(cells, function(cellElm) {
257 editor.dom.setAttrib(cellElm, 'scope', data.scope);
259 editor.dom.setStyles(cellElm, {
260 width: addSizeSuffix(data.width),
261 height: addSizeSuffix(data.height)
265 if (data.type && cellElm.nodeName.toLowerCase() != data.type) {
266 cellElm = dom.rename(cellElm, data.type);
269 // Apply/remove alignment
270 unApplyAlign(cellElm);
272 editor.formatter.apply('align' + data.align, {}, cellElm);
282 function rowDialog() {
283 var dom = editor.dom, tableElm, cellElm, rowElm, data, rows = [];
285 tableElm = editor.dom.getParent(editor.selection.getStart(), 'table');
286 cellElm = editor.dom.getParent(editor.selection.getStart(), 'td,th');
288 each(tableElm.rows, function(row) {
289 each(row.cells, function(cell) {
290 if (dom.hasClass(cell, 'mce-item-selected') || cell == cellElm) {
300 height: removePxSuffix(dom.getStyle(rowElm, 'height') || dom.getAttrib(rowElm, 'height')),
301 scope: dom.getAttrib(rowElm, 'scope')
304 data.type = rowElm.parentNode.nodeName.toLowerCase();
306 each('left center right'.split(' '), function(name) {
307 if (editor.formatter.matchNode(rowElm, 'align' + name)) {
312 editor.windowManager.open({
313 title: "Row properties",
329 {text: 'Header', value: 'thead'},
330 {text: 'Body', value: 'tbody'},
331 {text: 'Footer', value: 'tfoot'}
341 {text: 'None', value: ''},
342 {text: 'Left', value: 'left'},
343 {text: 'Center', value: 'center'},
344 {text: 'Right', value: 'right'}
347 {label: 'Height', name: 'height'}
351 onsubmit: function() {
352 var data = this.toJSON(), tableElm, oldParentElm, parentElm;
354 editor.undoManager.transact(function() {
355 var toType = data.type;
357 each(rows, function(rowElm) {
358 editor.dom.setAttrib(rowElm, 'scope', data.scope);
360 editor.dom.setStyles(rowElm, {
361 height: addSizeSuffix(data.height)
364 if (toType != rowElm.parentNode.nodeName.toLowerCase()) {
365 tableElm = dom.getParent(rowElm, 'table');
367 oldParentElm = rowElm.parentNode;
368 parentElm = dom.select(toType, tableElm)[0];
370 parentElm = dom.create(toType);
371 if (tableElm.firstChild) {
372 tableElm.insertBefore(parentElm, tableElm.firstChild);
374 tableElm.appendChild(parentElm);
378 parentElm.appendChild(rowElm);
380 if (!oldParentElm.hasChildNodes()) {
381 dom.remove(oldParentElm);
385 // Apply/remove alignment
386 unApplyAlign(rowElm);
388 editor.formatter.apply('align' + data.align, {}, rowElm);
398 function cmd(command) {
400 editor.execCommand(command);
404 function insertTable(cols, rows) {
407 html = '<table><tbody>';
409 for (y = 0; y < rows; y++) {
412 for (x = 0; x < cols; x++) {
413 html += '<td>' + (Env.ie ? " " : '<br>') + '</td>';
419 html += '</tbody></table>';
421 editor.insertContent(html);
424 function handleDisabledState(ctrl, selector) {
425 function bindStateListener() {
426 ctrl.disabled(!editor.dom.getParent(editor.selection.getStart(), selector));
428 editor.selection.selectorChanged(selector, function(state) {
429 ctrl.disabled(!state);
433 if (editor.initialized) {
436 editor.on('init', bindStateListener);
440 function postRender() {
441 /*jshint validthis:true*/
442 handleDisabledState(this, 'table');
445 function postRenderCell() {
446 /*jshint validthis:true*/
447 handleDisabledState(this, 'td,th');
450 function generateTableGrid() {
453 html = '<table role="presentation" class="mce-grid mce-grid-border">';
455 for (var y = 0; y < 10; y++) {
458 for (var x = 0; x < 10; x++) {
459 html += '<td><a href="#" data-mce-index="' + x + ',' + y + '"></a></td>';
467 html += '<div class="mce-text-center">0 x 0</div>';
472 editor.addMenuItem('inserttable', {
473 text: 'Insert table',
477 editor.dom.removeClass(this.menu.items()[0].getEl().getElementsByTagName('a'), 'mce-active');
482 html: generateTableGrid(),
484 onmousemove: function(e) {
485 var target = e.target;
487 if (target.nodeName == 'A') {
488 var table = editor.dom.getParent(target, 'table');
489 var pos = target.getAttribute('data-mce-index');
491 if (pos != this.lastPos) {
492 pos = pos.split(',');
494 pos[0] = parseInt(pos[0], 10);
495 pos[1] = parseInt(pos[1], 10);
497 for (var y = 0; y < 10; y++) {
498 for (var x = 0; x < 10; x++) {
499 editor.dom.toggleClass(
500 table.rows[y].childNodes[x].firstChild,
502 x <= pos[0] && y <= pos[1]
507 table.nextSibling.innerHTML = (pos[0] + 1) + ' x '+ (pos[1] + 1);
513 onclick: function(e) {
514 if (e.target.nodeName == 'A' && this.lastPos) {
517 insertTable(this.lastPos[0] + 1, this.lastPos[1] + 1);
519 // TODO: Maybe rework this?
520 this.parent().cancel(); // Close parent menu as if it was a click
527 editor.addMenuItem('tableprops', {
528 text: 'Table properties',
530 onPostRender: postRender,
534 editor.addMenuItem('deletetable', {
535 text: 'Delete table',
537 onPostRender: postRender,
538 cmd: 'mceTableDelete'
541 editor.addMenuItem('cell', {
546 {text: 'Cell properties', onclick: cmd('mceTableCellProps'), onPostRender: postRenderCell},
547 {text: 'Merge cells', onclick: cmd('mceTableMergeCells'), onPostRender: postRenderCell},
548 {text: 'Split cell', onclick: cmd('mceTableSplitCells'), onPostRender: postRenderCell}
552 editor.addMenuItem('row', {
556 {text: 'Insert row before', onclick: cmd('mceTableInsertRowBefore'), onPostRender: postRenderCell},
557 {text: 'Insert row after', onclick: cmd('mceTableInsertRowAfter'), onPostRender: postRenderCell},
558 {text: 'Delete row', onclick: cmd('mceTableDeleteRow'), onPostRender: postRenderCell},
559 {text: 'Row properties', onclick: cmd('mceTableRowProps'), onPostRender: postRenderCell},
561 {text: 'Cut row', onclick: cmd('mceTableCutRow'), onPostRender: postRenderCell},
562 {text: 'Copy row', onclick: cmd('mceTableCopyRow'), onPostRender: postRenderCell},
563 {text: 'Paste row before', onclick: cmd('mceTablePasteRowBefore'), onPostRender: postRenderCell},
564 {text: 'Paste row after', onclick: cmd('mceTablePasteRowAfter'), onPostRender: postRenderCell}
568 editor.addMenuItem('column', {
572 {text: 'Insert column before', onclick: cmd('mceTableInsertColBefore'), onPostRender: postRenderCell},
573 {text: 'Insert column after', onclick: cmd('mceTableInsertColAfter'), onPostRender: postRenderCell},
574 {text: 'Delete column', onclick: cmd('mceTableDeleteCol'), onPostRender: postRenderCell}
579 each("inserttable tableprops deletetable | cell row column".split(' '), function(name) {
581 menuItems.push({text: '-'});
583 menuItems.push(editor.menuItems[name]);
587 editor.addButton("table", {
593 // Select whole table is a table border is clicked
595 editor.on('click', function(e) {
598 if (e.nodeName === 'TABLE') {
599 editor.selection.select(e);
600 editor.nodeChanged();
605 self.quirks = new Quirks(editor);
607 editor.on('Init', function() {
608 winMan = editor.windowManager;
609 self.cellSelection = new CellSelection(editor);
612 // Register action commands
614 mceTableSplitCells: function(grid) {
618 mceTableMergeCells: function(grid) {
619 var rowSpan, colSpan, cell;
621 cell = editor.dom.getParent(editor.selection.getStart(), 'th,td');
623 rowSpan = cell.rowSpan;
624 colSpan = cell.colSpan;
627 if (!editor.dom.select('td.mce-item-selected,th.mce-item-selected').length) {
628 mergeDialog(grid, cell);
634 mceTableInsertRowBefore: function(grid) {
635 grid.insertRow(true);
638 mceTableInsertRowAfter: function(grid) {
642 mceTableInsertColBefore: function(grid) {
643 grid.insertCol(true);
646 mceTableInsertColAfter: function(grid) {
650 mceTableDeleteCol: function(grid) {
654 mceTableDeleteRow: function(grid) {
658 mceTableCutRow: function(grid) {
659 clipboardRows = grid.cutRows();
662 mceTableCopyRow: function(grid) {
663 clipboardRows = grid.copyRows();
666 mceTablePasteRowBefore: function(grid) {
667 grid.pasteRows(clipboardRows, true);
670 mceTablePasteRowAfter: function(grid) {
671 grid.pasteRows(clipboardRows);
674 mceTableDelete: function(grid) {
677 }, function(func, name) {
678 editor.addCommand(name, function() {
679 var grid = new TableGrid(editor);
683 editor.execCommand('mceRepaint');
684 self.cellSelection.clear();
689 // Register dialog commands
691 mceInsertTable: function() {
695 mceTableRowProps: rowDialog,
696 mceTableCellProps: cellDialog
697 }, function(func, name) {
698 editor.addCommand(name, function(ui, val) {
704 PluginManager.add('table', Plugin);