bgneal@45: /** bgneal@183: * editor_plugin_src.js bgneal@45: * bgneal@183: * Copyright 2009, Moxiecode Systems AB bgneal@183: * Released under LGPL License. bgneal@183: * bgneal@183: * License: http://tinymce.moxiecode.com/license bgneal@183: * Contributing: http://tinymce.moxiecode.com/contributing bgneal@45: */ bgneal@45: bgneal@183: (function(tinymce) { bgneal@45: var each = tinymce.each; bgneal@45: bgneal@247: // Checks if the selection/caret is at the start of the specified block element bgneal@247: function isAtStart(rng, par) { bgneal@247: var doc = par.ownerDocument, rng2 = doc.createRange(), elm; bgneal@247: bgneal@247: rng2.setStartBefore(par); bgneal@247: rng2.setEnd(rng.endContainer, rng.endOffset); bgneal@247: bgneal@247: elm = doc.createElement('body'); bgneal@247: elm.appendChild(rng2.cloneContents()); bgneal@247: bgneal@247: // Check for text characters of other elements that should be treated as content bgneal@247: return elm.innerHTML.replace(/<(br|img|object|embed|input|textarea)[^>]*>/gi, '-').replace(/<[^>]+>/g, '').length == 0; bgneal@247: }; bgneal@247: bgneal@183: /** bgneal@183: * Table Grid class. bgneal@183: */ bgneal@183: function TableGrid(table, dom, selection) { bgneal@183: var grid, startPos, endPos, selectedCell; bgneal@183: bgneal@183: buildGrid(); bgneal@183: selectedCell = dom.getParent(selection.getStart(), 'th,td'); bgneal@183: if (selectedCell) { bgneal@183: startPos = getPos(selectedCell); bgneal@183: endPos = findEndPos(); bgneal@183: selectedCell = getCell(startPos.x, startPos.y); bgneal@183: } bgneal@183: bgneal@217: function cloneNode(node, children) { bgneal@217: node = node.cloneNode(children); bgneal@217: node.removeAttribute('id'); bgneal@217: bgneal@217: return node; bgneal@217: } bgneal@217: bgneal@183: function buildGrid() { bgneal@183: var startY = 0; bgneal@183: bgneal@183: grid = []; bgneal@183: bgneal@183: each(['thead', 'tbody', 'tfoot'], function(part) { bgneal@247: var rows = dom.select('> ' + part + ' tr', table); bgneal@183: bgneal@183: each(rows, function(tr, y) { bgneal@183: y += startY; bgneal@183: bgneal@247: each(dom.select('> td, > th', tr), function(td, x) { bgneal@183: var x2, y2, rowspan, colspan; bgneal@183: bgneal@183: // Skip over existing cells produced by rowspan bgneal@183: if (grid[y]) { bgneal@183: while (grid[y][x]) bgneal@183: x++; bgneal@183: } bgneal@183: bgneal@183: // Get col/rowspan from cell bgneal@183: rowspan = getSpanVal(td, 'rowspan'); bgneal@183: colspan = getSpanVal(td, 'colspan'); bgneal@183: bgneal@183: // Fill out rowspan/colspan right and down bgneal@183: for (y2 = y; y2 < y + rowspan; y2++) { bgneal@183: if (!grid[y2]) bgneal@183: grid[y2] = []; bgneal@183: bgneal@183: for (x2 = x; x2 < x + colspan; x2++) { bgneal@183: grid[y2][x2] = { bgneal@183: part : part, bgneal@183: real : y2 == y && x2 == x, bgneal@183: elm : td, bgneal@183: rowspan : rowspan, bgneal@183: colspan : colspan bgneal@183: }; bgneal@183: } bgneal@183: } bgneal@183: }); bgneal@183: }); bgneal@183: bgneal@183: startY += rows.length; bgneal@183: }); bgneal@183: }; bgneal@183: bgneal@183: function getCell(x, y) { bgneal@183: var row; bgneal@183: bgneal@183: row = grid[y]; bgneal@183: if (row) bgneal@183: return row[x]; bgneal@183: }; bgneal@183: bgneal@183: function getSpanVal(td, name) { bgneal@183: return parseInt(td.getAttribute(name) || 1); bgneal@183: }; bgneal@183: bgneal@183: function isCellSelected(cell) { bgneal@183: return dom.hasClass(cell.elm, 'mceSelected') || cell == selectedCell; bgneal@183: }; bgneal@183: bgneal@183: function getSelectedRows() { bgneal@183: var rows = []; bgneal@183: bgneal@183: each(table.rows, function(row) { bgneal@183: each(row.cells, function(cell) { bgneal@183: if (dom.hasClass(cell, 'mceSelected') || cell == selectedCell.elm) { bgneal@183: rows.push(row); bgneal@183: return false; bgneal@183: } bgneal@183: }); bgneal@183: }); bgneal@183: bgneal@183: return rows; bgneal@183: }; bgneal@183: bgneal@183: function deleteTable() { bgneal@183: var rng = dom.createRng(); bgneal@183: bgneal@183: rng.setStartAfter(table); bgneal@183: rng.setEndAfter(table); bgneal@183: bgneal@183: selection.setRng(rng); bgneal@183: bgneal@183: dom.remove(table); bgneal@183: }; bgneal@183: bgneal@183: function cloneCell(cell) { bgneal@183: var formatNode; bgneal@183: bgneal@183: // Clone formats bgneal@183: tinymce.walk(cell, function(node) { bgneal@183: var curNode; bgneal@183: bgneal@183: if (node.nodeType == 3) { bgneal@183: each(dom.getParents(node.parentNode, null, cell).reverse(), function(node) { bgneal@217: node = cloneNode(node, false); bgneal@183: bgneal@183: if (!formatNode) bgneal@183: formatNode = curNode = node; bgneal@183: else if (curNode) bgneal@183: curNode.appendChild(node); bgneal@183: bgneal@183: curNode = node; bgneal@183: }); bgneal@183: bgneal@183: // Add something to the inner node bgneal@183: if (curNode) bgneal@183: curNode.innerHTML = tinymce.isIE ? ' ' : '
'; bgneal@183: bgneal@183: return false; bgneal@183: } bgneal@183: }, 'childNodes'); bgneal@183: bgneal@217: cell = cloneNode(cell, false); bgneal@183: cell.rowSpan = cell.colSpan = 1; bgneal@183: bgneal@183: if (formatNode) { bgneal@183: cell.appendChild(formatNode); bgneal@183: } else { bgneal@183: if (!tinymce.isIE) bgneal@183: cell.innerHTML = '
'; bgneal@183: } bgneal@183: bgneal@183: return cell; bgneal@183: }; bgneal@183: bgneal@183: function cleanup() { bgneal@183: var rng = dom.createRng(); bgneal@183: bgneal@183: // Empty rows bgneal@183: each(dom.select('tr', table), function(tr) { bgneal@183: if (tr.cells.length == 0) bgneal@183: dom.remove(tr); bgneal@183: }); bgneal@183: bgneal@183: // Empty table bgneal@183: if (dom.select('tr', table).length == 0) { bgneal@183: rng.setStartAfter(table); bgneal@183: rng.setEndAfter(table); bgneal@183: selection.setRng(rng); bgneal@183: dom.remove(table); bgneal@183: return; bgneal@183: } bgneal@183: bgneal@183: // Empty header/body/footer bgneal@183: each(dom.select('thead,tbody,tfoot', table), function(part) { bgneal@183: if (part.rows.length == 0) bgneal@183: dom.remove(part); bgneal@183: }); bgneal@183: bgneal@183: // Restore selection to start position if it still exists bgneal@183: buildGrid(); bgneal@183: bgneal@183: // Restore the selection to the closest table position bgneal@183: row = grid[Math.min(grid.length - 1, startPos.y)]; bgneal@183: if (row) { bgneal@183: selection.select(row[Math.min(row.length - 1, startPos.x)].elm, true); bgneal@183: selection.collapse(true); bgneal@183: } bgneal@183: }; bgneal@183: bgneal@183: function fillLeftDown(x, y, rows, cols) { bgneal@183: var tr, x2, r, c, cell; bgneal@183: bgneal@183: tr = grid[y][x].elm.parentNode; bgneal@183: for (r = 1; r <= rows; r++) { bgneal@183: tr = dom.getNext(tr, 'tr'); bgneal@183: bgneal@183: if (tr) { bgneal@183: // Loop left to find real cell bgneal@183: for (x2 = x; x2 >= 0; x2--) { bgneal@183: cell = grid[y + r][x2].elm; bgneal@183: bgneal@183: if (cell.parentNode == tr) { bgneal@183: // Append clones after bgneal@183: for (c = 1; c <= cols; c++) bgneal@183: dom.insertAfter(cloneCell(cell), cell); bgneal@183: bgneal@183: break; bgneal@183: } bgneal@183: } bgneal@183: bgneal@183: if (x2 == -1) { bgneal@183: // Insert nodes before first cell bgneal@183: for (c = 1; c <= cols; c++) bgneal@183: tr.insertBefore(cloneCell(tr.cells[0]), tr.cells[0]); bgneal@183: } bgneal@183: } bgneal@183: } bgneal@183: }; bgneal@183: bgneal@183: function split() { bgneal@183: each(grid, function(row, y) { bgneal@183: each(row, function(cell, x) { bgneal@183: var colSpan, rowSpan, newCell, i; bgneal@183: bgneal@183: if (isCellSelected(cell)) { bgneal@183: cell = cell.elm; bgneal@183: colSpan = getSpanVal(cell, 'colspan'); bgneal@183: rowSpan = getSpanVal(cell, 'rowspan'); bgneal@183: bgneal@183: if (colSpan > 1 || rowSpan > 1) { bgneal@183: cell.colSpan = cell.rowSpan = 1; bgneal@183: bgneal@183: // Insert cells right bgneal@183: for (i = 0; i < colSpan - 1; i++) bgneal@183: dom.insertAfter(cloneCell(cell), cell); bgneal@183: bgneal@183: fillLeftDown(x, y, rowSpan - 1, colSpan); bgneal@183: } bgneal@183: } bgneal@183: }); bgneal@183: }); bgneal@183: }; bgneal@183: bgneal@183: function merge(cell, cols, rows) { bgneal@183: var startX, startY, endX, endY, x, y, startCell, endCell, cell, children; bgneal@183: bgneal@183: // Use specified cell and cols/rows bgneal@183: if (cell) { bgneal@183: pos = getPos(cell); bgneal@183: startX = pos.x; bgneal@183: startY = pos.y; bgneal@183: endX = startX + (cols - 1); bgneal@183: endY = startY + (rows - 1); bgneal@183: } else { bgneal@183: // Use selection bgneal@183: startX = startPos.x; bgneal@183: startY = startPos.y; bgneal@183: endX = endPos.x; bgneal@183: endY = endPos.y; bgneal@183: } bgneal@183: bgneal@183: // Find start/end cells bgneal@183: startCell = getCell(startX, startY); bgneal@183: endCell = getCell(endX, endY); bgneal@183: bgneal@183: // Check if the cells exists and if they are of the same part for example tbody = tbody bgneal@183: if (startCell && endCell && startCell.part == endCell.part) { bgneal@183: // Split and rebuild grid bgneal@183: split(); bgneal@183: buildGrid(); bgneal@183: bgneal@183: // Set row/col span to start cell bgneal@183: startCell = getCell(startX, startY).elm; bgneal@183: startCell.colSpan = (endX - startX) + 1; bgneal@183: startCell.rowSpan = (endY - startY) + 1; bgneal@183: bgneal@183: // Remove other cells and add it's contents to the start cell bgneal@183: for (y = startY; y <= endY; y++) { bgneal@183: for (x = startX; x <= endX; x++) { bgneal@183: cell = grid[y][x].elm; bgneal@183: bgneal@183: if (cell != startCell) { bgneal@183: // Move children to startCell bgneal@183: children = tinymce.grep(cell.childNodes); bgneal@183: each(children, function(node, i) { bgneal@183: // Jump over last BR element bgneal@183: if (node.nodeName != 'BR' || i != children.length - 1) bgneal@183: startCell.appendChild(node); bgneal@183: }); bgneal@183: bgneal@183: // Remove cell bgneal@183: dom.remove(cell); bgneal@183: } bgneal@183: } bgneal@183: } bgneal@183: bgneal@183: // Remove empty rows etc and restore caret location bgneal@183: cleanup(); bgneal@183: } bgneal@183: }; bgneal@183: bgneal@183: function insertRow(before) { bgneal@183: var posY, cell, lastCell, x, rowElm, newRow, newCell, otherCell; bgneal@183: bgneal@183: // Find first/last row bgneal@183: each(grid, function(row, y) { bgneal@183: each(row, function(cell, x) { bgneal@183: if (isCellSelected(cell)) { bgneal@183: cell = cell.elm; bgneal@183: rowElm = cell.parentNode; bgneal@217: newRow = cloneNode(rowElm, false); bgneal@183: posY = y; bgneal@183: bgneal@183: if (before) bgneal@183: return false; bgneal@183: } bgneal@183: }); bgneal@183: bgneal@183: if (before) bgneal@183: return !posY; bgneal@183: }); bgneal@183: bgneal@183: for (x = 0; x < grid[0].length; x++) { bgneal@183: cell = grid[posY][x].elm; bgneal@183: bgneal@183: if (cell != lastCell) { bgneal@183: if (!before) { bgneal@183: rowSpan = getSpanVal(cell, 'rowspan'); bgneal@183: if (rowSpan > 1) { bgneal@183: cell.rowSpan = rowSpan + 1; bgneal@183: continue; bgneal@183: } bgneal@183: } else { bgneal@183: // Check if cell above can be expanded bgneal@183: if (posY > 0 && grid[posY - 1][x]) { bgneal@183: otherCell = grid[posY - 1][x].elm; bgneal@183: rowSpan = getSpanVal(otherCell, 'rowspan'); bgneal@183: if (rowSpan > 1) { bgneal@183: otherCell.rowSpan = rowSpan + 1; bgneal@183: continue; bgneal@183: } bgneal@183: } bgneal@183: } bgneal@183: bgneal@183: // Insert new cell into new row bgneal@183: newCell = cloneCell(cell) bgneal@183: newCell.colSpan = cell.colSpan; bgneal@183: newRow.appendChild(newCell); bgneal@183: bgneal@183: lastCell = cell; bgneal@183: } bgneal@183: } bgneal@183: bgneal@183: if (newRow.hasChildNodes()) { bgneal@183: if (!before) bgneal@183: dom.insertAfter(newRow, rowElm); bgneal@183: else bgneal@183: rowElm.parentNode.insertBefore(newRow, rowElm); bgneal@183: } bgneal@183: }; bgneal@183: bgneal@183: function insertCol(before) { bgneal@183: var posX, lastCell; bgneal@183: bgneal@183: // Find first/last column bgneal@183: each(grid, function(row, y) { bgneal@183: each(row, function(cell, x) { bgneal@183: if (isCellSelected(cell)) { bgneal@183: posX = x; bgneal@183: bgneal@183: if (before) bgneal@183: return false; bgneal@183: } bgneal@183: }); bgneal@183: bgneal@183: if (before) bgneal@183: return !posX; bgneal@183: }); bgneal@183: bgneal@183: each(grid, function(row, y) { bgneal@183: var cell = row[posX].elm, rowSpan, colSpan; bgneal@183: bgneal@183: if (cell != lastCell) { bgneal@183: colSpan = getSpanVal(cell, 'colspan'); bgneal@183: rowSpan = getSpanVal(cell, 'rowspan'); bgneal@183: bgneal@183: if (colSpan == 1) { bgneal@183: if (!before) { bgneal@183: dom.insertAfter(cloneCell(cell), cell); bgneal@183: fillLeftDown(posX, y, rowSpan - 1, colSpan); bgneal@183: } else { bgneal@183: cell.parentNode.insertBefore(cloneCell(cell), cell); bgneal@183: fillLeftDown(posX, y, rowSpan - 1, colSpan); bgneal@183: } bgneal@183: } else bgneal@183: cell.colSpan++; bgneal@183: bgneal@183: lastCell = cell; bgneal@183: } bgneal@183: }); bgneal@183: }; bgneal@183: bgneal@183: function deleteCols() { bgneal@183: var cols = []; bgneal@183: bgneal@183: // Get selected column indexes bgneal@183: each(grid, function(row, y) { bgneal@183: each(row, function(cell, x) { bgneal@183: if (isCellSelected(cell) && tinymce.inArray(cols, x) === -1) { bgneal@183: each(grid, function(row) { bgneal@183: var cell = row[x].elm, colSpan; bgneal@183: bgneal@183: colSpan = getSpanVal(cell, 'colspan'); bgneal@183: bgneal@183: if (colSpan > 1) bgneal@183: cell.colSpan = colSpan - 1; bgneal@183: else bgneal@183: dom.remove(cell); bgneal@183: }); bgneal@183: bgneal@183: cols.push(x); bgneal@183: } bgneal@183: }); bgneal@183: }); bgneal@183: bgneal@183: cleanup(); bgneal@183: }; bgneal@183: bgneal@183: function deleteRows() { bgneal@183: var rows; bgneal@183: bgneal@183: function deleteRow(tr) { bgneal@183: var nextTr, pos, lastCell; bgneal@183: bgneal@183: nextTr = dom.getNext(tr, 'tr'); bgneal@183: bgneal@183: // Move down row spanned cells bgneal@183: each(tr.cells, function(cell) { bgneal@183: var rowSpan = getSpanVal(cell, 'rowspan'); bgneal@183: bgneal@183: if (rowSpan > 1) { bgneal@183: cell.rowSpan = rowSpan - 1; bgneal@183: pos = getPos(cell); bgneal@183: fillLeftDown(pos.x, pos.y, 1, 1); bgneal@183: } bgneal@183: }); bgneal@183: bgneal@183: // Delete cells bgneal@183: pos = getPos(tr.cells[0]); bgneal@183: each(grid[pos.y], function(cell) { bgneal@183: var rowSpan; bgneal@183: bgneal@183: cell = cell.elm; bgneal@183: bgneal@183: if (cell != lastCell) { bgneal@183: rowSpan = getSpanVal(cell, 'rowspan'); bgneal@183: bgneal@183: if (rowSpan <= 1) bgneal@183: dom.remove(cell); bgneal@183: else bgneal@183: cell.rowSpan = rowSpan - 1; bgneal@183: bgneal@183: lastCell = cell; bgneal@183: } bgneal@183: }); bgneal@183: }; bgneal@183: bgneal@183: // Get selected rows and move selection out of scope bgneal@183: rows = getSelectedRows(); bgneal@183: bgneal@183: // Delete all selected rows bgneal@183: each(rows.reverse(), function(tr) { bgneal@183: deleteRow(tr); bgneal@183: }); bgneal@183: bgneal@183: cleanup(); bgneal@183: }; bgneal@183: bgneal@183: function cutRows() { bgneal@183: var rows = getSelectedRows(); bgneal@183: bgneal@183: dom.remove(rows); bgneal@183: cleanup(); bgneal@183: bgneal@183: return rows; bgneal@183: }; bgneal@183: bgneal@183: function copyRows() { bgneal@183: var rows = getSelectedRows(); bgneal@183: bgneal@183: each(rows, function(row, i) { bgneal@217: rows[i] = cloneNode(row, true); bgneal@183: }); bgneal@183: bgneal@183: return rows; bgneal@183: }; bgneal@183: bgneal@183: function pasteRows(rows, before) { bgneal@183: var selectedRows = getSelectedRows(), bgneal@183: targetRow = selectedRows[before ? 0 : selectedRows.length - 1], bgneal@183: targetCellCount = targetRow.cells.length; bgneal@183: bgneal@183: // Calc target cell count bgneal@183: each(grid, function(row) { bgneal@183: var match; bgneal@183: bgneal@183: targetCellCount = 0; bgneal@183: each(row, function(cell, x) { bgneal@183: if (cell.real) bgneal@183: targetCellCount += cell.colspan; bgneal@183: bgneal@183: if (cell.elm.parentNode == targetRow) bgneal@183: match = 1; bgneal@183: }); bgneal@183: bgneal@183: if (match) bgneal@183: return false; bgneal@183: }); bgneal@183: bgneal@183: if (!before) bgneal@183: rows.reverse(); bgneal@183: bgneal@183: each(rows, function(row) { bgneal@183: var cellCount = row.cells.length, cell; bgneal@183: bgneal@183: // Remove col/rowspans bgneal@183: for (i = 0; i < cellCount; i++) { bgneal@183: cell = row.cells[i]; bgneal@183: cell.colSpan = cell.rowSpan = 1; bgneal@183: } bgneal@183: bgneal@183: // Needs more cells bgneal@183: for (i = cellCount; i < targetCellCount; i++) bgneal@183: row.appendChild(cloneCell(row.cells[cellCount - 1])); bgneal@183: bgneal@183: // Needs less cells bgneal@183: for (i = targetCellCount; i < cellCount; i++) bgneal@183: dom.remove(row.cells[i]); bgneal@183: bgneal@183: // Add before/after bgneal@183: if (before) bgneal@183: targetRow.parentNode.insertBefore(row, targetRow); bgneal@183: else bgneal@183: dom.insertAfter(row, targetRow); bgneal@183: }); bgneal@183: }; bgneal@183: bgneal@183: function getPos(target) { bgneal@183: var pos; bgneal@183: bgneal@183: each(grid, function(row, y) { bgneal@183: each(row, function(cell, x) { bgneal@183: if (cell.elm == target) { bgneal@183: pos = {x : x, y : y}; bgneal@183: return false; bgneal@183: } bgneal@183: }); bgneal@183: bgneal@183: return !pos; bgneal@183: }); bgneal@183: bgneal@183: return pos; bgneal@183: }; bgneal@183: bgneal@183: function setStartCell(cell) { bgneal@183: startPos = getPos(cell); bgneal@183: }; bgneal@183: bgneal@183: function findEndPos() { bgneal@183: var pos, maxX, maxY; bgneal@183: bgneal@183: maxX = maxY = 0; bgneal@183: bgneal@183: each(grid, function(row, y) { bgneal@183: each(row, function(cell, x) { bgneal@183: var colSpan, rowSpan; bgneal@183: bgneal@183: if (isCellSelected(cell)) { bgneal@183: cell = grid[y][x]; bgneal@183: bgneal@183: if (x > maxX) bgneal@183: maxX = x; bgneal@183: bgneal@183: if (y > maxY) bgneal@183: maxY = y; bgneal@183: bgneal@183: if (cell.real) { bgneal@183: colSpan = cell.colspan - 1; bgneal@183: rowSpan = cell.rowspan - 1; bgneal@183: bgneal@183: if (colSpan) { bgneal@183: if (x + colSpan > maxX) bgneal@183: maxX = x + colSpan; bgneal@183: } bgneal@183: bgneal@183: if (rowSpan) { bgneal@183: if (y + rowSpan > maxY) bgneal@183: maxY = y + rowSpan; bgneal@183: } bgneal@183: } bgneal@183: } bgneal@183: }); bgneal@183: }); bgneal@183: bgneal@183: return {x : maxX, y : maxY}; bgneal@183: }; bgneal@183: bgneal@183: function setEndCell(cell) { bgneal@183: var startX, startY, endX, endY, maxX, maxY, colSpan, rowSpan; bgneal@183: bgneal@183: endPos = getPos(cell); bgneal@183: bgneal@183: if (startPos && endPos) { bgneal@183: // Get start/end positions bgneal@183: startX = Math.min(startPos.x, endPos.x); bgneal@183: startY = Math.min(startPos.y, endPos.y); bgneal@183: endX = Math.max(startPos.x, endPos.x); bgneal@183: endY = Math.max(startPos.y, endPos.y); bgneal@183: bgneal@183: // Expand end positon to include spans bgneal@183: maxX = endX; bgneal@183: maxY = endY; bgneal@183: bgneal@183: // Expand startX bgneal@183: for (y = startY; y <= maxY; y++) { bgneal@183: cell = grid[y][startX]; bgneal@183: bgneal@183: if (!cell.real) { bgneal@183: if (startX - (cell.colspan - 1) < startX) bgneal@183: startX -= cell.colspan - 1; bgneal@183: } bgneal@183: } bgneal@183: bgneal@183: // Expand startY bgneal@183: for (x = startX; x <= maxX; x++) { bgneal@183: cell = grid[startY][x]; bgneal@183: bgneal@183: if (!cell.real) { bgneal@183: if (startY - (cell.rowspan - 1) < startY) bgneal@183: startY -= cell.rowspan - 1; bgneal@183: } bgneal@183: } bgneal@183: bgneal@183: // Find max X, Y bgneal@183: for (y = startY; y <= endY; y++) { bgneal@183: for (x = startX; x <= endX; x++) { bgneal@183: cell = grid[y][x]; bgneal@183: bgneal@183: if (cell.real) { bgneal@183: colSpan = cell.colspan - 1; bgneal@183: rowSpan = cell.rowspan - 1; bgneal@183: bgneal@183: if (colSpan) { bgneal@183: if (x + colSpan > maxX) bgneal@183: maxX = x + colSpan; bgneal@183: } bgneal@183: bgneal@183: if (rowSpan) { bgneal@183: if (y + rowSpan > maxY) bgneal@183: maxY = y + rowSpan; bgneal@183: } bgneal@183: } bgneal@183: } bgneal@183: } bgneal@183: bgneal@183: // Remove current selection bgneal@183: dom.removeClass(dom.select('td.mceSelected,th.mceSelected'), 'mceSelected'); bgneal@183: bgneal@183: // Add new selection bgneal@183: for (y = startY; y <= maxY; y++) { bgneal@183: for (x = startX; x <= maxX; x++) bgneal@183: dom.addClass(grid[y][x].elm, 'mceSelected'); bgneal@183: } bgneal@183: } bgneal@183: }; bgneal@183: bgneal@183: // Expose to public bgneal@183: tinymce.extend(this, { bgneal@183: deleteTable : deleteTable, bgneal@183: split : split, bgneal@183: merge : merge, bgneal@183: insertRow : insertRow, bgneal@183: insertCol : insertCol, bgneal@183: deleteCols : deleteCols, bgneal@183: deleteRows : deleteRows, bgneal@183: cutRows : cutRows, bgneal@183: copyRows : copyRows, bgneal@183: pasteRows : pasteRows, bgneal@183: getPos : getPos, bgneal@183: setStartCell : setStartCell, bgneal@183: setEndCell : setEndCell bgneal@183: }); bgneal@183: }; bgneal@183: bgneal@45: tinymce.create('tinymce.plugins.TablePlugin', { bgneal@45: init : function(ed, url) { bgneal@183: var winMan, clipboardRows; bgneal@45: bgneal@183: function createTableGrid(node) { bgneal@183: var selection = ed.selection, tblElm = ed.dom.getParent(node || selection.getNode(), 'table'); bgneal@183: bgneal@183: if (tblElm) bgneal@183: return new TableGrid(tblElm, ed.dom, selection); bgneal@183: }; bgneal@183: bgneal@183: function cleanup() { bgneal@183: // Restore selection possibilities bgneal@183: ed.getBody().style.webkitUserSelect = ''; bgneal@183: ed.dom.removeClass(ed.dom.select('td.mceSelected,th.mceSelected'), 'mceSelected'); bgneal@183: }; bgneal@45: bgneal@45: // Register buttons bgneal@45: each([ bgneal@45: ['table', 'table.desc', 'mceInsertTable', true], bgneal@45: ['delete_table', 'table.del', 'mceTableDelete'], bgneal@45: ['delete_col', 'table.delete_col_desc', 'mceTableDeleteCol'], bgneal@45: ['delete_row', 'table.delete_row_desc', 'mceTableDeleteRow'], bgneal@45: ['col_after', 'table.col_after_desc', 'mceTableInsertColAfter'], bgneal@45: ['col_before', 'table.col_before_desc', 'mceTableInsertColBefore'], bgneal@45: ['row_after', 'table.row_after_desc', 'mceTableInsertRowAfter'], bgneal@45: ['row_before', 'table.row_before_desc', 'mceTableInsertRowBefore'], bgneal@45: ['row_props', 'table.row_desc', 'mceTableRowProps', true], bgneal@45: ['cell_props', 'table.cell_desc', 'mceTableCellProps', true], bgneal@45: ['split_cells', 'table.split_cells_desc', 'mceTableSplitCells', true], bgneal@45: ['merge_cells', 'table.merge_cells_desc', 'mceTableMergeCells', true] bgneal@45: ], function(c) { bgneal@45: ed.addButton(c[0], {title : c[1], cmd : c[2], ui : c[3]}); bgneal@45: }); bgneal@45: bgneal@183: // Select whole table is a table border is clicked bgneal@183: if (!tinymce.isIE) { bgneal@183: ed.onClick.add(function(ed, e) { bgneal@183: e = e.target; bgneal@45: bgneal@183: if (e.nodeName === 'TABLE') bgneal@183: ed.selection.select(e); bgneal@45: }); bgneal@45: } bgneal@45: bgneal@183: // Handle node change updates bgneal@183: ed.onNodeChange.add(function(ed, cm, n) { bgneal@183: var p; bgneal@183: bgneal@183: n = ed.selection.getStart(); bgneal@183: p = ed.dom.getParent(n, 'td,th,caption'); bgneal@183: cm.setActive('table', n.nodeName === 'TABLE' || !!p); bgneal@183: bgneal@183: // Disable table tools if we are in caption bgneal@183: if (p && p.nodeName === 'CAPTION') bgneal@183: p = 0; bgneal@183: bgneal@183: cm.setDisabled('delete_table', !p); bgneal@183: cm.setDisabled('delete_col', !p); bgneal@183: cm.setDisabled('delete_table', !p); bgneal@183: cm.setDisabled('delete_row', !p); bgneal@183: cm.setDisabled('col_after', !p); bgneal@183: cm.setDisabled('col_before', !p); bgneal@183: cm.setDisabled('row_after', !p); bgneal@183: cm.setDisabled('row_before', !p); bgneal@183: cm.setDisabled('row_props', !p); bgneal@183: cm.setDisabled('cell_props', !p); bgneal@183: cm.setDisabled('split_cells', !p); bgneal@183: cm.setDisabled('merge_cells', !p); bgneal@183: }); bgneal@183: bgneal@183: ed.onInit.add(function(ed) { bgneal@183: var startTable, startCell, dom = ed.dom, tableGrid; bgneal@183: bgneal@183: winMan = ed.windowManager; bgneal@183: bgneal@183: // Add cell selection logic bgneal@183: ed.onMouseDown.add(function(ed, e) { bgneal@183: if (e.button != 2) { bgneal@183: cleanup(); bgneal@183: bgneal@183: startCell = dom.getParent(e.target, 'td,th'); bgneal@183: startTable = dom.getParent(startCell, 'table'); bgneal@183: } bgneal@183: }); bgneal@183: bgneal@183: dom.bind(ed.getDoc(), 'mouseover', function(e) { bgneal@183: var sel, table, target = e.target; bgneal@183: bgneal@183: if (startCell && (tableGrid || target != startCell) && (target.nodeName == 'TD' || target.nodeName == 'TH')) { bgneal@183: table = dom.getParent(target, 'table'); bgneal@183: if (table == startTable) { bgneal@183: if (!tableGrid) { bgneal@183: tableGrid = createTableGrid(table); bgneal@183: tableGrid.setStartCell(startCell); bgneal@183: bgneal@183: ed.getBody().style.webkitUserSelect = 'none'; bgneal@183: } bgneal@183: bgneal@183: tableGrid.setEndCell(target); bgneal@183: } bgneal@183: bgneal@183: // Remove current selection bgneal@183: sel = ed.selection.getSel(); bgneal@183: bgneal@183: if (sel.removeAllRanges) bgneal@183: sel.removeAllRanges(); bgneal@183: else bgneal@183: sel.empty(); bgneal@183: bgneal@183: e.preventDefault(); bgneal@183: } bgneal@183: }); bgneal@183: bgneal@183: ed.onMouseUp.add(function(ed, e) { bgneal@183: var rng, sel = ed.selection, selectedCells, nativeSel = sel.getSel(), walker, node, lastNode, endNode; bgneal@183: bgneal@183: // Move selection to startCell bgneal@183: if (startCell) { bgneal@183: if (tableGrid) bgneal@183: ed.getBody().style.webkitUserSelect = ''; bgneal@183: bgneal@183: function setPoint(node, start) { bgneal@183: var walker = new tinymce.dom.TreeWalker(node, node); bgneal@183: bgneal@183: do { bgneal@183: // Text node bgneal@183: if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length != 0) { bgneal@183: if (start) bgneal@183: rng.setStart(node, 0); bgneal@183: else bgneal@183: rng.setEnd(node, node.nodeValue.length); bgneal@183: bgneal@183: return; bgneal@183: } bgneal@183: bgneal@183: // BR element bgneal@183: if (node.nodeName == 'BR') { bgneal@183: if (start) bgneal@183: rng.setStartBefore(node); bgneal@183: else bgneal@183: rng.setEndBefore(node); bgneal@183: bgneal@183: return; bgneal@183: } bgneal@183: } while (node = (start ? walker.next() : walker.prev())); bgneal@183: }; bgneal@183: bgneal@183: // Try to expand text selection as much as we can only Gecko supports cell selection bgneal@183: selectedCells = dom.select('td.mceSelected,th.mceSelected'); bgneal@183: if (selectedCells.length > 0) { bgneal@183: rng = dom.createRng(); bgneal@183: node = selectedCells[0]; bgneal@183: endNode = selectedCells[selectedCells.length - 1]; bgneal@183: bgneal@183: setPoint(node, 1); bgneal@183: walker = new tinymce.dom.TreeWalker(node, dom.getParent(selectedCells[0], 'table')); bgneal@183: bgneal@183: do { bgneal@183: if (node.nodeName == 'TD' || node.nodeName == 'TH') { bgneal@183: if (!dom.hasClass(node, 'mceSelected')) bgneal@183: break; bgneal@183: bgneal@183: lastNode = node; bgneal@183: } bgneal@183: } while (node = walker.next()); bgneal@183: bgneal@183: setPoint(lastNode); bgneal@183: bgneal@183: sel.setRng(rng); bgneal@183: } bgneal@183: bgneal@183: ed.nodeChanged(); bgneal@183: startCell = tableGrid = startTable = null; bgneal@183: } bgneal@183: }); bgneal@183: bgneal@183: ed.onKeyUp.add(function(ed, e) { bgneal@183: cleanup(); bgneal@183: }); bgneal@183: bgneal@183: // Add context menu bgneal@45: if (ed && ed.plugins.contextmenu) { bgneal@45: ed.plugins.contextmenu.onContextMenu.add(function(th, m, e) { bgneal@45: var sm, se = ed.selection, el = se.getNode() || ed.getBody(); bgneal@45: bgneal@217: if (ed.dom.getParent(e, 'td') || ed.dom.getParent(e, 'th') || ed.dom.select('td.mceSelected,th.mceSelected').length) { bgneal@45: m.removeAll(); bgneal@45: bgneal@45: if (el.nodeName == 'A' && !ed.dom.getAttrib(el, 'name')) { bgneal@45: m.add({title : 'advanced.link_desc', icon : 'link', cmd : ed.plugins.advlink ? 'mceAdvLink' : 'mceLink', ui : true}); bgneal@45: m.add({title : 'advanced.unlink_desc', icon : 'unlink', cmd : 'UnLink'}); bgneal@45: m.addSeparator(); bgneal@45: } bgneal@45: bgneal@45: if (el.nodeName == 'IMG' && el.className.indexOf('mceItem') == -1) { bgneal@45: m.add({title : 'advanced.image_desc', icon : 'image', cmd : ed.plugins.advimage ? 'mceAdvImage' : 'mceImage', ui : true}); bgneal@45: m.addSeparator(); bgneal@45: } bgneal@45: bgneal@183: m.add({title : 'table.desc', icon : 'table', cmd : 'mceInsertTable', value : {action : 'insert'}}); bgneal@183: m.add({title : 'table.props_desc', icon : 'table_props', cmd : 'mceInsertTable'}); bgneal@183: m.add({title : 'table.del', icon : 'delete_table', cmd : 'mceTableDelete'}); bgneal@45: m.addSeparator(); bgneal@45: bgneal@45: // Cell menu bgneal@45: sm = m.addMenu({title : 'table.cell'}); bgneal@183: sm.add({title : 'table.cell_desc', icon : 'cell_props', cmd : 'mceTableCellProps'}); bgneal@183: sm.add({title : 'table.split_cells_desc', icon : 'split_cells', cmd : 'mceTableSplitCells'}); bgneal@183: sm.add({title : 'table.merge_cells_desc', icon : 'merge_cells', cmd : 'mceTableMergeCells'}); bgneal@45: bgneal@45: // Row menu bgneal@45: sm = m.addMenu({title : 'table.row'}); bgneal@183: sm.add({title : 'table.row_desc', icon : 'row_props', cmd : 'mceTableRowProps'}); bgneal@45: sm.add({title : 'table.row_before_desc', icon : 'row_before', cmd : 'mceTableInsertRowBefore'}); bgneal@45: sm.add({title : 'table.row_after_desc', icon : 'row_after', cmd : 'mceTableInsertRowAfter'}); bgneal@45: sm.add({title : 'table.delete_row_desc', icon : 'delete_row', cmd : 'mceTableDeleteRow'}); bgneal@45: sm.addSeparator(); bgneal@45: sm.add({title : 'table.cut_row_desc', icon : 'cut', cmd : 'mceTableCutRow'}); bgneal@45: sm.add({title : 'table.copy_row_desc', icon : 'copy', cmd : 'mceTableCopyRow'}); bgneal@183: sm.add({title : 'table.paste_row_before_desc', icon : 'paste', cmd : 'mceTablePasteRowBefore'}).setDisabled(!clipboardRows); bgneal@183: sm.add({title : 'table.paste_row_after_desc', icon : 'paste', cmd : 'mceTablePasteRowAfter'}).setDisabled(!clipboardRows); bgneal@45: bgneal@45: // Column menu bgneal@45: sm = m.addMenu({title : 'table.col'}); bgneal@45: sm.add({title : 'table.col_before_desc', icon : 'col_before', cmd : 'mceTableInsertColBefore'}); bgneal@45: sm.add({title : 'table.col_after_desc', icon : 'col_after', cmd : 'mceTableInsertColAfter'}); bgneal@45: sm.add({title : 'table.delete_col_desc', icon : 'delete_col', cmd : 'mceTableDeleteCol'}); bgneal@45: } else bgneal@183: m.add({title : 'table.desc', icon : 'table', cmd : 'mceInsertTable'}); bgneal@45: }); bgneal@45: } bgneal@183: bgneal@183: // Fixes an issue on Gecko where it's impossible to place the caret behind a table bgneal@183: // This fix will force a paragraph element after the table but only when the forced_root_block setting is enabled bgneal@183: if (!tinymce.isIE) { bgneal@183: function fixTableCaretPos() { bgneal@183: var last; bgneal@183: bgneal@183: // Skip empty text nodes form the end bgneal@183: for (last = ed.getBody().lastChild; last && last.nodeType == 3 && !last.nodeValue.length; last = last.previousSibling) ; bgneal@183: bgneal@183: if (last && last.nodeName == 'TABLE') bgneal@183: ed.dom.add(ed.getBody(), 'p', null, '
'); bgneal@183: }; bgneal@183: bgneal@183: // Fixes an bug where it's impossible to place the caret before a table in Gecko bgneal@183: // this fix solves it by detecting when the caret is at the beginning of such a table bgneal@183: // and then manually moves the caret infront of the table bgneal@183: if (tinymce.isGecko) { bgneal@183: ed.onKeyDown.add(function(ed, e) { bgneal@183: var rng, table, dom = ed.dom; bgneal@183: bgneal@183: // On gecko it's not possible to place the caret before a table bgneal@183: if (e.keyCode == 37 || e.keyCode == 38) { bgneal@183: rng = ed.selection.getRng(); bgneal@183: table = dom.getParent(rng.startContainer, 'table'); bgneal@183: bgneal@183: if (table && ed.getBody().firstChild == table) { bgneal@183: if (isAtStart(rng, table)) { bgneal@183: rng = dom.createRng(); bgneal@183: bgneal@183: rng.setStartBefore(table); bgneal@183: rng.setEndBefore(table); bgneal@183: bgneal@183: ed.selection.setRng(rng); bgneal@183: bgneal@183: e.preventDefault(); bgneal@183: } bgneal@183: } bgneal@183: } bgneal@183: }); bgneal@183: } bgneal@183: bgneal@183: ed.onKeyUp.add(fixTableCaretPos); bgneal@183: ed.onSetContent.add(fixTableCaretPos); bgneal@183: ed.onVisualAid.add(fixTableCaretPos); bgneal@183: bgneal@183: ed.onPreProcess.add(function(ed, o) { bgneal@183: var last = o.node.lastChild; bgneal@183: bgneal@183: if (last && last.childNodes.length == 1 && last.firstChild.nodeName == 'BR') bgneal@183: ed.dom.remove(last); bgneal@183: }); bgneal@183: bgneal@183: fixTableCaretPos(); bgneal@183: } bgneal@45: }); bgneal@45: bgneal@183: // Register action commands bgneal@183: each({ bgneal@183: mceTableSplitCells : function(grid) { bgneal@183: grid.split(); bgneal@183: }, bgneal@183: bgneal@183: mceTableMergeCells : function(grid) { bgneal@183: var rowSpan, colSpan, cell; bgneal@183: bgneal@183: cell = ed.dom.getParent(ed.selection.getNode(), 'th,td'); bgneal@183: if (cell) { bgneal@183: rowSpan = cell.rowSpan; bgneal@183: colSpan = cell.colSpan; bgneal@45: } bgneal@45: bgneal@183: if (!ed.dom.select('td.mceSelected,th.mceSelected').length) { bgneal@183: winMan.open({ bgneal@183: url : url + '/merge_cells.htm', bgneal@183: width : 240 + parseInt(ed.getLang('table.merge_cells_delta_width', 0)), bgneal@183: height : 110 + parseInt(ed.getLang('table.merge_cells_delta_height', 0)), bgneal@183: inline : 1 bgneal@183: }, { bgneal@183: rows : rowSpan, bgneal@183: cols : colSpan, bgneal@183: onaction : function(data) { bgneal@183: grid.merge(cell, data.cols, data.rows); bgneal@183: }, bgneal@183: plugin_url : url bgneal@183: }); bgneal@183: } else bgneal@183: grid.merge(); bgneal@183: }, bgneal@183: bgneal@183: mceTableInsertRowBefore : function(grid) { bgneal@183: grid.insertRow(true); bgneal@183: }, bgneal@183: bgneal@183: mceTableInsertRowAfter : function(grid) { bgneal@183: grid.insertRow(); bgneal@183: }, bgneal@183: bgneal@183: mceTableInsertColBefore : function(grid) { bgneal@183: grid.insertCol(true); bgneal@183: }, bgneal@183: bgneal@183: mceTableInsertColAfter : function(grid) { bgneal@183: grid.insertCol(); bgneal@183: }, bgneal@183: bgneal@183: mceTableDeleteCol : function(grid) { bgneal@183: grid.deleteCols(); bgneal@183: }, bgneal@183: bgneal@183: mceTableDeleteRow : function(grid) { bgneal@183: grid.deleteRows(); bgneal@183: }, bgneal@183: bgneal@183: mceTableCutRow : function(grid) { bgneal@183: clipboardRows = grid.cutRows(); bgneal@183: }, bgneal@183: bgneal@183: mceTableCopyRow : function(grid) { bgneal@183: clipboardRows = grid.copyRows(); bgneal@183: }, bgneal@183: bgneal@183: mceTablePasteRowBefore : function(grid) { bgneal@183: grid.pasteRows(clipboardRows, true); bgneal@183: }, bgneal@183: bgneal@183: mceTablePasteRowAfter : function(grid) { bgneal@183: grid.pasteRows(clipboardRows); bgneal@183: }, bgneal@183: bgneal@183: mceTableDelete : function(grid) { bgneal@183: grid.deleteTable(); bgneal@45: } bgneal@183: }, function(func, name) { bgneal@183: ed.addCommand(name, function() { bgneal@183: var grid = createTableGrid(); bgneal@183: bgneal@183: if (grid) { bgneal@183: func(grid); bgneal@183: ed.execCommand('mceRepaint'); bgneal@183: cleanup(); bgneal@183: } bgneal@183: }); bgneal@45: }); bgneal@45: bgneal@183: // Register dialog commands bgneal@183: each({ bgneal@183: mceInsertTable : function(val) { bgneal@183: winMan.open({ bgneal@183: url : url + '/table.htm', bgneal@183: width : 400 + parseInt(ed.getLang('table.table_delta_width', 0)), bgneal@183: height : 320 + parseInt(ed.getLang('table.table_delta_height', 0)), bgneal@183: inline : 1 bgneal@183: }, { bgneal@183: plugin_url : url, bgneal@183: action : val ? val.action : 0 bgneal@183: }); bgneal@183: }, bgneal@45: bgneal@183: mceTableRowProps : function() { bgneal@183: winMan.open({ bgneal@183: url : url + '/row.htm', bgneal@183: width : 400 + parseInt(ed.getLang('table.rowprops_delta_width', 0)), bgneal@183: height : 295 + parseInt(ed.getLang('table.rowprops_delta_height', 0)), bgneal@183: inline : 1 bgneal@183: }, { bgneal@183: plugin_url : url bgneal@183: }); bgneal@183: }, bgneal@183: bgneal@183: mceTableCellProps : function() { bgneal@183: winMan.open({ bgneal@183: url : url + '/cell.htm', bgneal@183: width : 400 + parseInt(ed.getLang('table.cellprops_delta_width', 0)), bgneal@183: height : 295 + parseInt(ed.getLang('table.cellprops_delta_height', 0)), bgneal@183: inline : 1 bgneal@183: }, { bgneal@183: plugin_url : url bgneal@45: }); bgneal@45: } bgneal@183: }, function(func, name) { bgneal@183: ed.addCommand(name, function(ui, val) { bgneal@183: func(val); bgneal@183: }); bgneal@45: }); bgneal@45: } bgneal@45: }); bgneal@45: bgneal@45: // Register plugin bgneal@45: tinymce.PluginManager.add('table', tinymce.plugins.TablePlugin); bgneal@183: })(tinymce);