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