4 * Copyright 2009, Moxiecode Systems AB
5 * Released under LGPL License.
7 * License: http://tinymce.moxiecode.com/license
8 * Contributing: http://tinymce.moxiecode.com/contributing
12 var each = tinymce.each;
14 // Checks if the selection/caret is at the start of the specified block element
15 function isAtStart(rng, par) {
16 var doc = par.ownerDocument, rng2 = doc.createRange(), elm;
18 rng2.setStartBefore(par);
19 rng2.setEnd(rng.endContainer, rng.endOffset);
21 elm = doc.createElement('body');
22 elm.appendChild(rng2.cloneContents());
24 // Check for text characters of other elements that should be treated as content
25 return elm.innerHTML.replace(/<(br|img|object|embed|input|textarea)[^>]*>/gi, '-').replace(/<[^>]+>/g, '').length == 0;
28 function getSpanVal(td, name) {
29 return parseInt(td.getAttribute(name) || 1);
35 function TableGrid(table, dom, selection) {
36 var grid, startPos, endPos, selectedCell;
39 selectedCell = dom.getParent(selection.getStart(), 'th,td');
41 startPos = getPos(selectedCell);
42 endPos = findEndPos();
43 selectedCell = getCell(startPos.x, startPos.y);
46 function cloneNode(node, children) {
47 node = node.cloneNode(children);
48 node.removeAttribute('id');
53 function buildGrid() {
58 each(['thead', 'tbody', 'tfoot'], function(part) {
59 var rows = dom.select('> ' + part + ' tr', table);
61 each(rows, function(tr, y) {
64 each(dom.select('> td, > th', tr), function(td, x) {
65 var x2, y2, rowspan, colspan;
67 // Skip over existing cells produced by rowspan
73 // Get col/rowspan from cell
74 rowspan = getSpanVal(td, 'rowspan');
75 colspan = getSpanVal(td, 'colspan');
77 // Fill out rowspan/colspan right and down
78 for (y2 = y; y2 < y + rowspan; y2++) {
82 for (x2 = x; x2 < x + colspan; x2++) {
85 real : y2 == y && x2 == x,
95 startY += rows.length;
99 function getCell(x, y) {
107 function setSpanVal(td, name, val) {
112 td.removeAttribute(name, 1);
114 td.setAttribute(name, val, 1);
118 function isCellSelected(cell) {
119 return cell && (dom.hasClass(cell.elm, 'mceSelected') || cell == selectedCell);
122 function getSelectedRows() {
125 each(table.rows, function(row) {
126 each(row.cells, function(cell) {
127 if (dom.hasClass(cell, 'mceSelected') || cell == selectedCell.elm) {
137 function deleteTable() {
138 var rng = dom.createRng();
140 rng.setStartAfter(table);
141 rng.setEndAfter(table);
143 selection.setRng(rng);
148 function cloneCell(cell) {
152 tinymce.walk(cell, function(node) {
155 if (node.nodeType == 3) {
156 each(dom.getParents(node.parentNode, null, cell).reverse(), function(node) {
157 node = cloneNode(node, false);
160 formatNode = curNode = node;
162 curNode.appendChild(node);
167 // Add something to the inner node
169 curNode.innerHTML = tinymce.isIE ? ' ' : '<br data-mce-bogus="1" />';
175 cell = cloneNode(cell, false);
176 setSpanVal(cell, 'rowSpan', 1);
177 setSpanVal(cell, 'colSpan', 1);
180 cell.appendChild(formatNode);
183 cell.innerHTML = '<br data-mce-bogus="1" />';
190 var rng = dom.createRng();
193 each(dom.select('tr', table), function(tr) {
194 if (tr.cells.length == 0)
199 if (dom.select('tr', table).length == 0) {
200 rng.setStartAfter(table);
201 rng.setEndAfter(table);
202 selection.setRng(rng);
207 // Empty header/body/footer
208 each(dom.select('thead,tbody,tfoot', table), function(part) {
209 if (part.rows.length == 0)
213 // Restore selection to start position if it still exists
216 // Restore the selection to the closest table position
217 row = grid[Math.min(grid.length - 1, startPos.y)];
219 selection.select(row[Math.min(row.length - 1, startPos.x)].elm, true);
220 selection.collapse(true);
224 function fillLeftDown(x, y, rows, cols) {
225 var tr, x2, r, c, cell;
227 tr = grid[y][x].elm.parentNode;
228 for (r = 1; r <= rows; r++) {
229 tr = dom.getNext(tr, 'tr');
232 // Loop left to find real cell
233 for (x2 = x; x2 >= 0; x2--) {
234 cell = grid[y + r][x2].elm;
236 if (cell.parentNode == tr) {
237 // Append clones after
238 for (c = 1; c <= cols; c++)
239 dom.insertAfter(cloneCell(cell), cell);
246 // Insert nodes before first cell
247 for (c = 1; c <= cols; c++)
248 tr.insertBefore(cloneCell(tr.cells[0]), tr.cells[0]);
255 each(grid, function(row, y) {
256 each(row, function(cell, x) {
257 var colSpan, rowSpan, newCell, i;
259 if (isCellSelected(cell)) {
261 colSpan = getSpanVal(cell, 'colspan');
262 rowSpan = getSpanVal(cell, 'rowspan');
264 if (colSpan > 1 || rowSpan > 1) {
265 setSpanVal(cell, 'rowSpan', 1);
266 setSpanVal(cell, 'colSpan', 1);
268 // Insert cells right
269 for (i = 0; i < colSpan - 1; i++)
270 dom.insertAfter(cloneCell(cell), cell);
272 fillLeftDown(x, y, rowSpan - 1, colSpan);
279 function merge(cell, cols, rows) {
280 var startX, startY, endX, endY, x, y, startCell, endCell, cell, children, count;
282 // Use specified cell and cols/rows
287 endX = startX + (cols - 1);
288 endY = startY + (rows - 1);
290 startPos = endPos = null;
292 // Calculate start/end pos by checking for selected cells in grid works better with context menu
293 each(grid, function(row, y) {
294 each(row, function(cell, x) {
295 if (isCellSelected(cell)) {
297 startPos = {x: x, y: y};
300 endPos = {x: x, y: y};
312 // Find start/end cells
313 startCell = getCell(startX, startY);
314 endCell = getCell(endX, endY);
316 // Check if the cells exists and if they are of the same part for example tbody = tbody
317 if (startCell && endCell && startCell.part == endCell.part) {
318 // Split and rebuild grid
322 // Set row/col span to start cell
323 startCell = getCell(startX, startY).elm;
324 setSpanVal(startCell, 'colSpan', (endX - startX) + 1);
325 setSpanVal(startCell, 'rowSpan', (endY - startY) + 1);
327 // Remove other cells and add it's contents to the start cell
328 for (y = startY; y <= endY; y++) {
329 for (x = startX; x <= endX; x++) {
330 if (!grid[y] || !grid[y][x])
333 cell = grid[y][x].elm;
335 if (cell != startCell) {
336 // Move children to startCell
337 children = tinymce.grep(cell.childNodes);
338 each(children, function(node) {
339 startCell.appendChild(node);
342 // Remove bogus nodes if there is children in the target cell
343 if (children.length) {
344 children = tinymce.grep(startCell.childNodes);
346 each(children, function(node) {
347 if (node.nodeName == 'BR' && dom.getAttrib(node, 'data-mce-bogus') && count++ < children.length - 1)
348 startCell.removeChild(node);
358 // Remove empty rows etc and restore caret location
363 function insertRow(before) {
364 var posY, cell, lastCell, x, rowElm, newRow, newCell, otherCell, rowSpan;
366 // Find first/last row
367 each(grid, function(row, y) {
368 each(row, function(cell, x) {
369 if (isCellSelected(cell)) {
371 rowElm = cell.parentNode;
372 newRow = cloneNode(rowElm, false);
384 for (x = 0; x < grid[0].length; x++) {
385 // Cell not found could be because of an invalid table structure
389 cell = grid[posY][x].elm;
391 if (cell != lastCell) {
393 rowSpan = getSpanVal(cell, 'rowspan');
395 setSpanVal(cell, 'rowSpan', rowSpan + 1);
399 // Check if cell above can be expanded
400 if (posY > 0 && grid[posY - 1][x]) {
401 otherCell = grid[posY - 1][x].elm;
402 rowSpan = getSpanVal(otherCell, 'rowSpan');
404 setSpanVal(otherCell, 'rowSpan', rowSpan + 1);
410 // Insert new cell into new row
411 newCell = cloneCell(cell);
412 setSpanVal(newCell, 'colSpan', cell.colSpan);
414 newRow.appendChild(newCell);
420 if (newRow.hasChildNodes()) {
422 dom.insertAfter(newRow, rowElm);
424 rowElm.parentNode.insertBefore(newRow, rowElm);
428 function insertCol(before) {
431 // Find first/last column
432 each(grid, function(row, y) {
433 each(row, function(cell, x) {
434 if (isCellSelected(cell)) {
446 each(grid, function(row, y) {
447 var cell, rowSpan, colSpan;
452 cell = row[posX].elm;
453 if (cell != lastCell) {
454 colSpan = getSpanVal(cell, 'colspan');
455 rowSpan = getSpanVal(cell, 'rowspan');
459 dom.insertAfter(cloneCell(cell), cell);
460 fillLeftDown(posX, y, rowSpan - 1, colSpan);
462 cell.parentNode.insertBefore(cloneCell(cell), cell);
463 fillLeftDown(posX, y, rowSpan - 1, colSpan);
466 setSpanVal(cell, 'colSpan', cell.colSpan + 1);
473 function deleteCols() {
476 // Get selected column indexes
477 each(grid, function(row, y) {
478 each(row, function(cell, x) {
479 if (isCellSelected(cell) && tinymce.inArray(cols, x) === -1) {
480 each(grid, function(row) {
481 var cell = row[x].elm, colSpan;
483 colSpan = getSpanVal(cell, 'colSpan');
486 setSpanVal(cell, 'colSpan', colSpan - 1);
499 function deleteRows() {
502 function deleteRow(tr) {
503 var nextTr, pos, lastCell;
505 nextTr = dom.getNext(tr, 'tr');
507 // Move down row spanned cells
508 each(tr.cells, function(cell) {
509 var rowSpan = getSpanVal(cell, 'rowSpan');
512 setSpanVal(cell, 'rowSpan', rowSpan - 1);
514 fillLeftDown(pos.x, pos.y, 1, 1);
519 pos = getPos(tr.cells[0]);
520 each(grid[pos.y], function(cell) {
525 if (cell != lastCell) {
526 rowSpan = getSpanVal(cell, 'rowSpan');
531 setSpanVal(cell, 'rowSpan', rowSpan - 1);
538 // Get selected rows and move selection out of scope
539 rows = getSelectedRows();
541 // Delete all selected rows
542 each(rows.reverse(), function(tr) {
550 var rows = getSelectedRows();
558 function copyRows() {
559 var rows = getSelectedRows();
561 each(rows, function(row, i) {
562 rows[i] = cloneNode(row, true);
568 function pasteRows(rows, before) {
569 // If we don't have any rows in the clipboard, return immediately
573 var selectedRows = getSelectedRows(),
574 targetRow = selectedRows[before ? 0 : selectedRows.length - 1],
575 targetCellCount = targetRow.cells.length;
577 // Calc target cell count
578 each(grid, function(row) {
582 each(row, function(cell, x) {
584 targetCellCount += cell.colspan;
586 if (cell.elm.parentNode == targetRow)
597 each(rows, function(row) {
598 var cellCount = row.cells.length, cell;
600 // Remove col/rowspans
601 for (i = 0; i < cellCount; i++) {
603 setSpanVal(cell, 'colSpan', 1);
604 setSpanVal(cell, 'rowSpan', 1);
608 for (i = cellCount; i < targetCellCount; i++)
609 row.appendChild(cloneCell(row.cells[cellCount - 1]));
612 for (i = targetCellCount; i < cellCount; i++)
613 dom.remove(row.cells[i]);
617 targetRow.parentNode.insertBefore(row, targetRow);
619 dom.insertAfter(row, targetRow);
622 // Remove current selection
623 dom.removeClass(dom.select('td.mceSelected,th.mceSelected'), 'mceSelected');
626 function getPos(target) {
629 each(grid, function(row, y) {
630 each(row, function(cell, x) {
631 if (cell.elm == target) {
632 pos = {x : x, y : y};
643 function setStartCell(cell) {
644 startPos = getPos(cell);
647 function findEndPos() {
652 each(grid, function(row, y) {
653 each(row, function(cell, x) {
654 var colSpan, rowSpan;
656 if (isCellSelected(cell)) {
666 colSpan = cell.colspan - 1;
667 rowSpan = cell.rowspan - 1;
670 if (x + colSpan > maxX)
675 if (y + rowSpan > maxY)
683 return {x : maxX, y : maxY};
686 function setEndCell(cell) {
687 var startX, startY, endX, endY, maxX, maxY, colSpan, rowSpan;
689 endPos = getPos(cell);
691 if (startPos && endPos) {
692 // Get start/end positions
693 startX = Math.min(startPos.x, endPos.x);
694 startY = Math.min(startPos.y, endPos.y);
695 endX = Math.max(startPos.x, endPos.x);
696 endY = Math.max(startPos.y, endPos.y);
698 // Expand end positon to include spans
703 for (y = startY; y <= maxY; y++) {
704 cell = grid[y][startX];
707 if (startX - (cell.colspan - 1) < startX)
708 startX -= cell.colspan - 1;
713 for (x = startX; x <= maxX; x++) {
714 cell = grid[startY][x];
717 if (startY - (cell.rowspan - 1) < startY)
718 startY -= cell.rowspan - 1;
723 for (y = startY; y <= endY; y++) {
724 for (x = startX; x <= endX; x++) {
728 colSpan = cell.colspan - 1;
729 rowSpan = cell.rowspan - 1;
732 if (x + colSpan > maxX)
737 if (y + rowSpan > maxY)
744 // Remove current selection
745 dom.removeClass(dom.select('td.mceSelected,th.mceSelected'), 'mceSelected');
748 for (y = startY; y <= maxY; y++) {
749 for (x = startX; x <= maxX; x++) {
751 dom.addClass(grid[y][x].elm, 'mceSelected');
758 tinymce.extend(this, {
759 deleteTable : deleteTable,
762 insertRow : insertRow,
763 insertCol : insertCol,
764 deleteCols : deleteCols,
765 deleteRows : deleteRows,
768 pasteRows : pasteRows,
770 setStartCell : setStartCell,
771 setEndCell : setEndCell
775 tinymce.create('tinymce.plugins.TablePlugin', {
776 init : function(ed, url) {
777 var winMan, clipboardRows, hasCellSelection = true; // Might be selected cells on reload
779 function createTableGrid(node) {
780 var selection = ed.selection, tblElm = ed.dom.getParent(node || selection.getNode(), 'table');
783 return new TableGrid(tblElm, ed.dom, selection);
787 // Restore selection possibilities
788 ed.getBody().style.webkitUserSelect = '';
790 if (hasCellSelection) {
791 ed.dom.removeClass(ed.dom.select('td.mceSelected,th.mceSelected'), 'mceSelected');
792 hasCellSelection = false;
798 ['table', 'table.desc', 'mceInsertTable', true],
799 ['delete_table', 'table.del', 'mceTableDelete'],
800 ['delete_col', 'table.delete_col_desc', 'mceTableDeleteCol'],
801 ['delete_row', 'table.delete_row_desc', 'mceTableDeleteRow'],
802 ['col_after', 'table.col_after_desc', 'mceTableInsertColAfter'],
803 ['col_before', 'table.col_before_desc', 'mceTableInsertColBefore'],
804 ['row_after', 'table.row_after_desc', 'mceTableInsertRowAfter'],
805 ['row_before', 'table.row_before_desc', 'mceTableInsertRowBefore'],
806 ['row_props', 'table.row_desc', 'mceTableRowProps', true],
807 ['cell_props', 'table.cell_desc', 'mceTableCellProps', true],
808 ['split_cells', 'table.split_cells_desc', 'mceTableSplitCells', true],
809 ['merge_cells', 'table.merge_cells_desc', 'mceTableMergeCells', true]
811 ed.addButton(c[0], {title : c[1], cmd : c[2], ui : c[3]});
814 // Select whole table is a table border is clicked
816 ed.onClick.add(function(ed, e) {
819 if (e.nodeName === 'TABLE') {
820 ed.selection.select(e);
826 ed.onPreProcess.add(function(ed, args) {
827 var nodes, i, node, dom = ed.dom, value;
829 nodes = dom.select('table', args.node);
833 dom.setAttrib(node, 'data-mce-style', '');
835 if ((value = dom.getAttrib(node, 'width'))) {
836 dom.setStyle(node, 'width', value);
837 dom.setAttrib(node, 'width', '');
840 if ((value = dom.getAttrib(node, 'height'))) {
841 dom.setStyle(node, 'height', value);
842 dom.setAttrib(node, 'height', '');
847 // Handle node change updates
848 ed.onNodeChange.add(function(ed, cm, n) {
851 n = ed.selection.getStart();
852 p = ed.dom.getParent(n, 'td,th,caption');
853 cm.setActive('table', n.nodeName === 'TABLE' || !!p);
855 // Disable table tools if we are in caption
856 if (p && p.nodeName === 'CAPTION')
859 cm.setDisabled('delete_table', !p);
860 cm.setDisabled('delete_col', !p);
861 cm.setDisabled('delete_table', !p);
862 cm.setDisabled('delete_row', !p);
863 cm.setDisabled('col_after', !p);
864 cm.setDisabled('col_before', !p);
865 cm.setDisabled('row_after', !p);
866 cm.setDisabled('row_before', !p);
867 cm.setDisabled('row_props', !p);
868 cm.setDisabled('cell_props', !p);
869 cm.setDisabled('split_cells', !p);
870 cm.setDisabled('merge_cells', !p);
873 ed.onInit.add(function(ed) {
874 var startTable, startCell, dom = ed.dom, tableGrid;
876 winMan = ed.windowManager;
878 // Add cell selection logic
879 ed.onMouseDown.add(function(ed, e) {
883 startCell = dom.getParent(e.target, 'td,th');
884 startTable = dom.getParent(startCell, 'table');
888 dom.bind(ed.getDoc(), 'mouseover', function(e) {
889 var sel, table, target = e.target;
891 if (startCell && (tableGrid || target != startCell) && (target.nodeName == 'TD' || target.nodeName == 'TH')) {
892 table = dom.getParent(target, 'table');
893 if (table == startTable) {
895 tableGrid = createTableGrid(table);
896 tableGrid.setStartCell(startCell);
898 ed.getBody().style.webkitUserSelect = 'none';
901 tableGrid.setEndCell(target);
902 hasCellSelection = true;
905 // Remove current selection
906 sel = ed.selection.getSel();
909 if (sel.removeAllRanges)
910 sel.removeAllRanges();
914 // IE9 might throw errors here
921 ed.onMouseUp.add(function(ed, e) {
922 var rng, sel = ed.selection, selectedCells, nativeSel = sel.getSel(), walker, node, lastNode, endNode;
924 // Move selection to startCell
927 ed.getBody().style.webkitUserSelect = '';
929 function setPoint(node, start) {
930 var walker = new tinymce.dom.TreeWalker(node, node);
934 if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length != 0) {
936 rng.setStart(node, 0);
938 rng.setEnd(node, node.nodeValue.length);
944 if (node.nodeName == 'BR') {
946 rng.setStartBefore(node);
948 rng.setEndBefore(node);
952 } while (node = (start ? walker.next() : walker.prev()));
955 // Try to expand text selection as much as we can only Gecko supports cell selection
956 selectedCells = dom.select('td.mceSelected,th.mceSelected');
957 if (selectedCells.length > 0) {
958 rng = dom.createRng();
959 node = selectedCells[0];
960 endNode = selectedCells[selectedCells.length - 1];
961 rng.setStartBefore(node);
962 rng.setEndAfter(node);
965 walker = new tinymce.dom.TreeWalker(node, dom.getParent(selectedCells[0], 'table'));
968 if (node.nodeName == 'TD' || node.nodeName == 'TH') {
969 if (!dom.hasClass(node, 'mceSelected'))
974 } while (node = walker.next());
982 startCell = tableGrid = startTable = null;
986 ed.onKeyUp.add(function(ed, e) {
990 ed.onKeyDown.add(function (ed, e) {
991 fixTableCellSelection(ed);
994 ed.onMouseDown.add(function (ed, e) {
996 fixTableCellSelection(ed);
999 function tableCellSelected(ed, rng, n, currentCell) {
1000 // The decision of when a table cell is selected is somewhat involved. The fact that this code is
1001 // required is actually a pointer to the root cause of this bug. A cell is selected when the start
1002 // and end offsets are 0, the start container is a text, and the selection node is either a TR (most cases)
1003 // or the parent of the table (in the case of the selection containing the last cell of a table).
1004 var TEXT_NODE = 3, table = ed.dom.getParent(rng.startContainer, 'TABLE'),
1005 tableParent, allOfCellSelected, tableCellSelection;
1007 tableParent = table.parentNode;
1008 allOfCellSelected =rng.startContainer.nodeType == TEXT_NODE &&
1009 rng.startOffset == 0 &&
1010 rng.endOffset == 0 &&
1012 (n.nodeName=="TR" || n==tableParent);
1013 tableCellSelection = (n.nodeName=="TD"||n.nodeName=="TH")&& !currentCell;
1014 return allOfCellSelected || tableCellSelection;
1018 // this nasty hack is here to work around some WebKit selection bugs.
1019 function fixTableCellSelection(ed) {
1020 if (!tinymce.isWebKit)
1023 var rng = ed.selection.getRng();
1024 var n = ed.selection.getNode();
1025 var currentCell = ed.dom.getParent(rng.startContainer, 'TD,TH');
1027 if (!tableCellSelected(ed, rng, n, currentCell))
1033 // Get the very last node inside the table cell
1034 var end = currentCell.lastChild;
1035 while (end.lastChild)
1036 end = end.lastChild;
1038 // Select the entire table cell. Nothing outside of the table cell should be selected.
1039 rng.setEnd(end, end.nodeValue.length);
1040 ed.selection.setRng(rng);
1042 ed.plugins.table.fixTableCellSelection=fixTableCellSelection;
1045 if (ed && ed.plugins.contextmenu) {
1046 ed.plugins.contextmenu.onContextMenu.add(function(th, m, e) {
1047 var sm, se = ed.selection, el = se.getNode() || ed.getBody();
1049 if (ed.dom.getParent(e, 'td') || ed.dom.getParent(e, 'th') || ed.dom.select('td.mceSelected,th.mceSelected').length) {
1052 if (el.nodeName == 'A' && !ed.dom.getAttrib(el, 'name')) {
1053 m.add({title : 'advanced.link_desc', icon : 'link', cmd : ed.plugins.advlink ? 'mceAdvLink' : 'mceLink', ui : true});
1054 m.add({title : 'advanced.unlink_desc', icon : 'unlink', cmd : 'UnLink'});
1058 if (el.nodeName == 'IMG' && el.className.indexOf('mceItem') == -1) {
1059 m.add({title : 'advanced.image_desc', icon : 'image', cmd : ed.plugins.advimage ? 'mceAdvImage' : 'mceImage', ui : true});
1063 m.add({title : 'table.desc', icon : 'table', cmd : 'mceInsertTable', value : {action : 'insert'}});
1064 m.add({title : 'table.props_desc', icon : 'table_props', cmd : 'mceInsertTable'});
1065 m.add({title : 'table.del', icon : 'delete_table', cmd : 'mceTableDelete'});
1069 sm = m.addMenu({title : 'table.cell'});
1070 sm.add({title : 'table.cell_desc', icon : 'cell_props', cmd : 'mceTableCellProps'});
1071 sm.add({title : 'table.split_cells_desc', icon : 'split_cells', cmd : 'mceTableSplitCells'});
1072 sm.add({title : 'table.merge_cells_desc', icon : 'merge_cells', cmd : 'mceTableMergeCells'});
1075 sm = m.addMenu({title : 'table.row'});
1076 sm.add({title : 'table.row_desc', icon : 'row_props', cmd : 'mceTableRowProps'});
1077 sm.add({title : 'table.row_before_desc', icon : 'row_before', cmd : 'mceTableInsertRowBefore'});
1078 sm.add({title : 'table.row_after_desc', icon : 'row_after', cmd : 'mceTableInsertRowAfter'});
1079 sm.add({title : 'table.delete_row_desc', icon : 'delete_row', cmd : 'mceTableDeleteRow'});
1081 sm.add({title : 'table.cut_row_desc', icon : 'cut', cmd : 'mceTableCutRow'});
1082 sm.add({title : 'table.copy_row_desc', icon : 'copy', cmd : 'mceTableCopyRow'});
1083 sm.add({title : 'table.paste_row_before_desc', icon : 'paste', cmd : 'mceTablePasteRowBefore'}).setDisabled(!clipboardRows);
1084 sm.add({title : 'table.paste_row_after_desc', icon : 'paste', cmd : 'mceTablePasteRowAfter'}).setDisabled(!clipboardRows);
1087 sm = m.addMenu({title : 'table.col'});
1088 sm.add({title : 'table.col_before_desc', icon : 'col_before', cmd : 'mceTableInsertColBefore'});
1089 sm.add({title : 'table.col_after_desc', icon : 'col_after', cmd : 'mceTableInsertColAfter'});
1090 sm.add({title : 'table.delete_col_desc', icon : 'delete_col', cmd : 'mceTableDeleteCol'});
1092 m.add({title : 'table.desc', icon : 'table', cmd : 'mceInsertTable'});
1096 // Fix to allow navigating up and down in a table in WebKit browsers.
1097 if (tinymce.isWebKit) {
1098 function moveSelection(ed, e) {
1099 var VK = tinymce.VK;
1100 var key = e.keyCode;
1102 function handle(upBool, sourceNode, event) {
1103 var siblingDirection = upBool ? 'previousSibling' : 'nextSibling';
1104 var currentRow = ed.dom.getParent(sourceNode, 'tr');
1105 var siblingRow = currentRow[siblingDirection];
1108 moveCursorToRow(ed, sourceNode, siblingRow, upBool);
1109 tinymce.dom.Event.cancel(event);
1112 var tableNode = ed.dom.getParent(currentRow, 'table');
1113 var middleNode = currentRow.parentNode;
1114 var parentNodeName = middleNode.nodeName.toLowerCase();
1115 if (parentNodeName === 'tbody' || parentNodeName === (upBool ? 'tfoot' : 'thead')) {
1116 var targetParent = getTargetParent(upBool, tableNode, middleNode, 'tbody');
1117 if (targetParent !== null) {
1118 return moveToRowInTarget(upBool, targetParent, sourceNode, event);
1121 return escapeTable(upBool, currentRow, siblingDirection, tableNode, event);
1125 function getTargetParent(upBool, topNode, secondNode, nodeName) {
1126 var tbodies = ed.dom.select('>' + nodeName, topNode);
1127 var position = tbodies.indexOf(secondNode);
1128 if (upBool && position === 0 || !upBool && position === tbodies.length - 1) {
1129 return getFirstHeadOrFoot(upBool, topNode);
1130 } else if (position === -1) {
1131 var topOrBottom = secondNode.tagName.toLowerCase() === 'thead' ? 0 : tbodies.length - 1;
1132 return tbodies[topOrBottom];
1134 return tbodies[position + (upBool ? -1 : 1)];
1138 function getFirstHeadOrFoot(upBool, parent) {
1139 var tagName = upBool ? 'thead' : 'tfoot';
1140 var headOrFoot = ed.dom.select('>' + tagName, parent);
1141 return headOrFoot.length !== 0 ? headOrFoot[0] : null;
1144 function moveToRowInTarget(upBool, targetParent, sourceNode, event) {
1145 var targetRow = getChildForDirection(targetParent, upBool);
1146 targetRow && moveCursorToRow(ed, sourceNode, targetRow, upBool);
1147 tinymce.dom.Event.cancel(event);
1151 function escapeTable(upBool, currentRow, siblingDirection, table, event) {
1152 var tableSibling = table[siblingDirection];
1154 moveCursorToStartOfElement(tableSibling);
1157 var parentCell = ed.dom.getParent(table, 'td,th');
1159 return handle(upBool, parentCell, event);
1161 var backUpSibling = getChildForDirection(currentRow, !upBool);
1162 moveCursorToStartOfElement(backUpSibling);
1163 return tinymce.dom.Event.cancel(event);
1168 function getChildForDirection(parent, up) {
1169 var child = parent && parent[up ? 'lastChild' : 'firstChild'];
1170 // BR is not a valid table child to return in this case we return the table cell
1171 return child && child.nodeName === 'BR' ? ed.dom.getParent(child, 'td,th') : child;
1174 function moveCursorToStartOfElement(n) {
1175 ed.selection.setCursorLocation(n, 0);
1178 function isVerticalMovement() {
1179 return key == VK.UP || key == VK.DOWN;
1182 function isInTable(ed) {
1183 var node = ed.selection.getNode();
1184 var currentRow = ed.dom.getParent(node, 'tr');
1185 return currentRow !== null;
1188 function columnIndex(column) {
1191 while (c.previousSibling) {
1192 c = c.previousSibling;
1193 colIndex = colIndex + getSpanVal(c, "colspan");
1198 function findColumn(rowElement, columnIndex) {
1201 each(rowElement.children, function(cell, i) {
1202 c = c + getSpanVal(cell, "colspan");
1204 if (c > columnIndex)
1210 function moveCursorToRow(ed, node, row, upBool) {
1211 var srcColumnIndex = columnIndex(ed.dom.getParent(node, 'td,th'));
1212 var tgtColumnIndex = findColumn(row, srcColumnIndex);
1213 var tgtNode = row.childNodes[tgtColumnIndex];
1214 var rowCellTarget = getChildForDirection(tgtNode, upBool);
1215 moveCursorToStartOfElement(rowCellTarget || tgtNode);
1218 function shouldFixCaret(preBrowserNode) {
1219 var newNode = ed.selection.getNode();
1220 var newParent = ed.dom.getParent(newNode, 'td,th');
1221 var oldParent = ed.dom.getParent(preBrowserNode, 'td,th');
1222 return newParent && newParent !== oldParent && checkSameParentTable(newParent, oldParent)
1225 function checkSameParentTable(nodeOne, NodeTwo) {
1226 return ed.dom.getParent(nodeOne, 'TABLE') === ed.dom.getParent(NodeTwo, 'TABLE');
1229 if (isVerticalMovement() && isInTable(ed)) {
1230 var preBrowserNode = ed.selection.getNode();
1231 setTimeout(function() {
1232 if (shouldFixCaret(preBrowserNode)) {
1233 handle(!e.shiftKey && key === VK.UP, preBrowserNode, e);
1239 ed.onKeyDown.add(moveSelection);
1242 // Fixes an issue on Gecko where it's impossible to place the caret behind a table
1243 // This fix will force a paragraph element after the table but only when the forced_root_block setting is enabled
1244 function fixTableCaretPos() {
1247 // Skip empty text nodes form the end
1248 for (last = ed.getBody().lastChild; last && last.nodeType == 3 && !last.nodeValue.length; last = last.previousSibling) ;
1250 if (last && last.nodeName == 'TABLE') {
1251 if (ed.settings.forced_root_block)
1252 ed.dom.add(ed.getBody(), ed.settings.forced_root_block, null, tinymce.isIE ? ' ' : '<br data-mce-bogus="1" />');
1254 ed.dom.add(ed.getBody(), 'br', {'data-mce-bogus': '1'});
1258 // Fixes an bug where it's impossible to place the caret before a table in Gecko
1259 // this fix solves it by detecting when the caret is at the beginning of such a table
1260 // and then manually moves the caret infront of the table
1261 if (tinymce.isGecko) {
1262 ed.onKeyDown.add(function(ed, e) {
1263 var rng, table, dom = ed.dom;
1265 // On gecko it's not possible to place the caret before a table
1266 if (e.keyCode == 37 || e.keyCode == 38) {
1267 rng = ed.selection.getRng();
1268 table = dom.getParent(rng.startContainer, 'table');
1270 if (table && ed.getBody().firstChild == table) {
1271 if (isAtStart(rng, table)) {
1272 rng = dom.createRng();
1274 rng.setStartBefore(table);
1275 rng.setEndBefore(table);
1277 ed.selection.setRng(rng);
1286 ed.onKeyUp.add(fixTableCaretPos);
1287 ed.onSetContent.add(fixTableCaretPos);
1288 ed.onVisualAid.add(fixTableCaretPos);
1290 ed.onPreProcess.add(function(ed, o) {
1291 var last = o.node.lastChild;
1293 if (last && (last.nodeName == "BR" || (last.childNodes.length == 1 && (last.firstChild.nodeName == 'BR' || last.firstChild.nodeValue == '\u00a0'))) && last.previousSibling && last.previousSibling.nodeName == "TABLE") {
1294 ed.dom.remove(last);
1300 * Fixes bug in Gecko where shift-enter in table cell does not place caret on new line
1302 * Removed: Since the new enter logic seems to fix this one.
1305 if (tinymce.isGecko) {
1306 ed.onKeyDown.add(function(ed, e) {
1307 if (e.keyCode === tinymce.VK.ENTER && e.shiftKey) {
1308 var node = ed.selection.getRng().startContainer;
1309 var tableCell = dom.getParent(node, 'td,th');
1311 var zeroSizedNbsp = ed.getDoc().createTextNode("\uFEFF");
1312 dom.insertAfter(zeroSizedNbsp, node);
1320 ed.startContent = ed.getContent({format : 'raw'});
1323 // Register action commands
1325 mceTableSplitCells : function(grid) {
1329 mceTableMergeCells : function(grid) {
1330 var rowSpan, colSpan, cell;
1332 cell = ed.dom.getParent(ed.selection.getNode(), 'th,td');
1334 rowSpan = cell.rowSpan;
1335 colSpan = cell.colSpan;
1338 if (!ed.dom.select('td.mceSelected,th.mceSelected').length) {
1340 url : url + '/merge_cells.htm',
1341 width : 240 + parseInt(ed.getLang('table.merge_cells_delta_width', 0)),
1342 height : 110 + parseInt(ed.getLang('table.merge_cells_delta_height', 0)),
1347 onaction : function(data) {
1348 grid.merge(cell, data.cols, data.rows);
1356 mceTableInsertRowBefore : function(grid) {
1357 grid.insertRow(true);
1360 mceTableInsertRowAfter : function(grid) {
1364 mceTableInsertColBefore : function(grid) {
1365 grid.insertCol(true);
1368 mceTableInsertColAfter : function(grid) {
1372 mceTableDeleteCol : function(grid) {
1376 mceTableDeleteRow : function(grid) {
1380 mceTableCutRow : function(grid) {
1381 clipboardRows = grid.cutRows();
1384 mceTableCopyRow : function(grid) {
1385 clipboardRows = grid.copyRows();
1388 mceTablePasteRowBefore : function(grid) {
1389 grid.pasteRows(clipboardRows, true);
1392 mceTablePasteRowAfter : function(grid) {
1393 grid.pasteRows(clipboardRows);
1396 mceTableDelete : function(grid) {
1399 }, function(func, name) {
1400 ed.addCommand(name, function() {
1401 var grid = createTableGrid();
1405 ed.execCommand('mceRepaint');
1411 // Register dialog commands
1413 mceInsertTable : function(val) {
1415 url : url + '/table.htm',
1416 width : 400 + parseInt(ed.getLang('table.table_delta_width', 0)),
1417 height : 320 + parseInt(ed.getLang('table.table_delta_height', 0)),
1421 action : val ? val.action : 0
1425 mceTableRowProps : function() {
1427 url : url + '/row.htm',
1428 width : 400 + parseInt(ed.getLang('table.rowprops_delta_width', 0)),
1429 height : 295 + parseInt(ed.getLang('table.rowprops_delta_height', 0)),
1436 mceTableCellProps : function() {
1438 url : url + '/cell.htm',
1439 width : 400 + parseInt(ed.getLang('table.cellprops_delta_width', 0)),
1440 height : 295 + parseInt(ed.getLang('table.cellprops_delta_height', 0)),
1446 }, function(func, name) {
1447 ed.addCommand(name, function(ui, val) {
1455 tinymce.PluginManager.add('table', tinymce.plugins.TablePlugin);