UNPKG

@syncfusion/ej2-richtexteditor

Version:
1,135 lines (1,133 loc) 195 kB
import { createElement, closest, detach, Browser, isNullOrUndefined as isNOU, EventHandler, addClass } from '@syncfusion/ej2-base'; import * as CONSTANT from './../base/constant'; import { InsertHtml } from './inserthtml'; import { removeClassWithAttr, getCorrespondingColumns, getCorrespondingIndex, getColGroup, insertColGroupWithSizes, convertPixelToPercentage, getCellIndex, getMaxCellCount, cleanupInternalElements } from '../../common/util'; import * as EVENTS from '../../common/constant'; import { CLS_TABLE_MULTI_CELL, CLS_TABLE_SEL, CLS_TABLE_SEL_END } from '../../common/constant'; import { TABLE_SELECTION_STATE_ALLOWED_ACTIONKEYS } from '../../common/config'; import { TablePasting } from './table-pasting'; /** * Link internal component * * @hidden */ var TableCommand = /** @class */ (function () { /** * Constructor for creating the Formats plugin * * @param {IEditorModel} parent - specifies the parent element * @param {ITableModel} tableModel - specifies the table model instance * @param {IFrameSettings} iframeSettings - specifies the table model instance * @hidden */ function TableCommand(parent, tableModel, iframeSettings) { this.isTableMoveActive = false; this.pageX = null; this.pageY = null; this.isResizeBind = true; this.currentColumnResize = ''; this.resizeEndTime = 0; this.ensureInsideTableList = true; this.parent = parent; this.tablePastingObj = new TablePasting(); this.tableModel = tableModel; this.iframeSettings = iframeSettings; this.addEventListener(); } /* * Registers all event listeners for table operations */ TableCommand.prototype.addEventListener = function () { this.parent.observer.on(CONSTANT.TABLE, this.createTable, this); this.parent.observer.on(CONSTANT.INSERT_ROW, this.insertRow, this); this.parent.observer.on(CONSTANT.INSERT_COLUMN, this.insertColumn, this); this.parent.observer.on(CONSTANT.DELETEROW, this.deleteRow, this); this.parent.observer.on(CONSTANT.DELETECOLUMN, this.deleteColumn, this); this.parent.observer.on(CONSTANT.REMOVETABLE, this.removeTable, this); this.parent.observer.on(CONSTANT.TABLEHEADER, this.tableHeader, this); this.parent.observer.on(CONSTANT.TABLE_VERTICAL_ALIGN, this.tableVerticalAlign, this); this.parent.observer.on(CONSTANT.TABLE_MERGE, this.cellMerge, this); this.parent.observer.on(CONSTANT.TABLE_HORIZONTAL_SPLIT, this.horizontalSplit, this); this.parent.observer.on(CONSTANT.TABLE_VERTICAL_SPLIT, this.verticalSplit, this); this.parent.observer.on(CONSTANT.TABLE_STYLES, this.tableStyles, this); this.parent.observer.on(CONSTANT.TABLE_BACKGROUND_COLOR, this.setBGColor, this); this.parent.observer.on(CONSTANT.TABLE_MOVE, this.tableMove, this); this.parent.observer.on(EVENTS.INTERNAL_DESTROY, this.destroy, this); }; /* * Removes all registered event listeners */ TableCommand.prototype.removeEventListener = function () { this.parent.observer.off(CONSTANT.TABLE, this.createTable); this.parent.observer.off(CONSTANT.INSERT_ROW, this.insertRow); this.parent.observer.off(CONSTANT.INSERT_COLUMN, this.insertColumn); this.parent.observer.off(CONSTANT.DELETEROW, this.deleteRow); this.parent.observer.off(CONSTANT.DELETECOLUMN, this.deleteColumn); this.parent.observer.off(CONSTANT.REMOVETABLE, this.removeTable); this.parent.observer.off(CONSTANT.TABLEHEADER, this.tableHeader); this.parent.observer.off(CONSTANT.TABLE_VERTICAL_ALIGN, this.tableVerticalAlign); this.parent.observer.off(CONSTANT.TABLE_MERGE, this.cellMerge); this.parent.observer.off(CONSTANT.TABLE_HORIZONTAL_SPLIT, this.horizontalSplit); this.parent.observer.off(CONSTANT.TABLE_VERTICAL_SPLIT, this.verticalSplit); this.parent.observer.off(CONSTANT.TABLE_STYLES, this.tableStyles); this.parent.observer.off(CONSTANT.TABLE_BACKGROUND_COLOR, this.setBGColor); this.parent.observer.off(CONSTANT.TABLE_MOVE, this.tableMove); this.parent.observer.off(EVENTS.INTERNAL_DESTROY, this.destroy); // Browser-specific event handlers for table resizing if (!Browser.isDevice && this.tableModel.tableSettings.resize) { EventHandler.remove(this.tableModel.getEditPanel(), 'mouseover', this.resizeHelper); EventHandler.remove(this.tableModel.getEditPanel(), Browser.touchStartEvent, this.resizeStart); } if (this.curTable) { EventHandler.remove(this.curTable, 'mouseleave', this.tableMouseLeave); } EventHandler.remove(this.tableModel.getDocument(), 'selectionchange', this.tableCellsKeyboardSelection); }; /** * Copies the selected table cells to clipboard. * Creates a temporary table with only the selected cells' content. * * @param {boolean} isCut - Indicates whether the operation is a cut (true) or copy (false). * @returns {void} Nothing is returned * @public * @hidden */ TableCommand.prototype.copy = function (isCut) { var copyTable = this.extractSelectedTable(this.curTable, isCut); if (copyTable) { var tableHtml = cleanupInternalElements(copyTable.outerHTML, this.tableModel.editorMode); try { var htmlBlob = new Blob([tableHtml], { type: 'text/html' }); var clipboardItem = new window.ClipboardItem({ 'text/html': htmlBlob }); navigator.clipboard.write([clipboardItem]); } catch (e) { console.error('Clipboard API not supported in this browser'); } } }; /** * Updates the table command object with the latest table model configuration and settings * * @param {ITableModel} updatedTableMode - The updated table model with latest configuration * @returns {void} - This method does not return a value * @public * @hidden */ TableCommand.prototype.updateTableModel = function (updatedTableMode) { this.tableModel = updatedTableMode; }; /* * Extracts a cloned HTMLTableElement containing only the selected cells, * preserving their original row and column positions. All non-selected * rows and cells are removed. If `isCut` is true, the original cell content * is cleared by replacing it with a <br>. */ TableCommand.prototype.extractSelectedTable = function (originalTable, isCut) { var selectedCells = originalTable.querySelectorAll('.e-cell-select.e-multi-cells-select'); if (!selectedCells || selectedCells.length === 0) { return null; } var clonedTable = originalTable.cloneNode(true); var rowsWithSelection = this.buildSelectionMap(originalTable, selectedCells, isCut); this.cleanTableToSelection(clonedTable, rowsWithSelection); this.removeColGroup(clonedTable); return clonedTable; }; /* Builds a map of selected cell coordinates and clears original cell content if cut */ TableCommand.prototype.buildSelectionMap = function (originalTable, selectedCells, isCut) { var selectionMap = new Map(); for (var i = 0; i < selectedCells.length; i++) { var cell = selectedCells[i]; var row = cell.parentElement; var rowIndex = Array.prototype.indexOf.call(originalTable.rows, row); var cellIndex = Array.prototype.indexOf.call(row.cells, cell); var rowSpan = parseInt(cell.getAttribute('rowspan') || '1', 10); for (var r = 0; r < rowSpan; r++) { var rowPosition = r + rowIndex; if (!selectionMap.has(rowPosition)) { selectionMap.set(rowPosition, new Set()); } if (r === 0) { selectionMap.get(rowPosition).add(cellIndex); } } if (isCut) { var originalCell = originalTable.rows[rowIndex].cells[cellIndex]; originalCell.innerHTML = '<br>'; } } return selectionMap; }; /* Modifies the cloned table by removing non-selected rows and cells */ TableCommand.prototype.cleanTableToSelection = function (table, selectionMap) { for (var rowIndex = table.rows.length - 1; rowIndex >= 0; rowIndex--) { var row = table.rows[rowIndex]; if (!selectionMap.has(rowIndex)) { detach(row); continue; } var selectedCellIndices = selectionMap.get(rowIndex); for (var cellIndex = row.cells.length - 1; cellIndex >= 0; cellIndex--) { if (!selectedCellIndices.has(cellIndex)) { row.deleteCell(cellIndex); } } } }; /* Removes the <colgroup> from the cloned table if it exists */ TableCommand.prototype.removeColGroup = function (table) { var colGroup = getColGroup(table); if (colGroup) { detach(colGroup); } }; /* * Creates and inserts a table based on the specified configuration. */ TableCommand.prototype.createTable = function (e) { var table = this.createTableStructure(e); this.insertTableInDocument(table, e); this.handlePostTableInsertion(table, e); return table; }; /* * Creates the table structure with rows and columns. */ TableCommand.prototype.createTableStructure = function (e) { var table = createElement('table', { className: 'e-rte-table' }); this.applyTableDimensions(table, e.item.width); var cellWidth = this.calculateCellWidth(e.item.width.width, e.item.columns); // Create colgroup with columns var colGroup = this.createInitialColgroup(e.item.columns, cellWidth); table.appendChild(colGroup); var tblBody = createElement('tbody'); this.createRowsAndCells(tblBody, e.item.rows, e.item.columns); table.appendChild(tblBody); return table; }; /* * Creates a colgroup element with evenly distributed columns */ TableCommand.prototype.createInitialColgroup = function (columnCount, cellWidth) { var colGroup = createElement('colgroup'); for (var i = 0; i < columnCount; i++) { var col = createElement('col'); col.appendChild(createElement('br')); col.style.width = cellWidth + '%'; colGroup.appendChild(col); } return colGroup; }; /* * Applies width dimensions to the table. */ TableCommand.prototype.applyTableDimensions = function (table, widthConfig) { if (!isNOU(widthConfig.width)) { table.style.width = this.calculateStyleValue(widthConfig.width); } if (!isNOU(widthConfig.minWidth)) { table.style.minWidth = this.calculateStyleValue(widthConfig.minWidth); } if (!isNOU(widthConfig.maxWidth)) { table.style.maxWidth = this.calculateStyleValue(widthConfig.maxWidth); } }; /* * Calculates appropriate cell width based on table width and column count. */ TableCommand.prototype.calculateCellWidth = function (width, columns) { return parseInt(width, 10) > 100 ? 100 / columns : parseInt(width, 10) / columns; }; /* * Creates rows and cells in the table body. */ TableCommand.prototype.createRowsAndCells = function (tblBody, rowCount, columnCount) { for (var i = 0; i < rowCount; i++) { var row = createElement('tr'); for (var j = 0; j < columnCount; j++) { var cell = createElement('td'); cell.appendChild(createElement('br')); row.appendChild(cell); } tblBody.appendChild(row); } }; /* * Inserts the table into the document. */ TableCommand.prototype.insertTableInDocument = function (table, e) { e.item.selection.restore(); InsertHtml.Insert(this.tableModel.getDocument(), table, this.tableModel.getEditPanel()); e.item.selection.setSelectionText(this.tableModel.getDocument(), table.querySelector('td'), table.querySelector('td'), 0, 0); }; /* * Handles post-insertion operations for the table. */ TableCommand.prototype.handlePostTableInsertion = function (table, e) { this.insertElementAfterTableIfNeeded(table, e.enterAction); if (table.classList.contains('ignore-table')) { removeClassWithAttr([table], ['ignore-table']); } table.querySelector('td').classList.add('e-cell-select'); if (e.callBack) { e.callBack({ requestType: 'Table', editorMode: 'HTML', event: e.event, range: this.parent.nodeSelection.getRange(this.tableModel.getDocument()), elements: [table] }); } }; /* * Inserts an appropriate element after the table if needed. */ TableCommand.prototype.insertElementAfterTableIfNeeded = function (table, enterAction) { if (table.nextElementSibling === null && !table.classList.contains('ignore-table')) { var insertElem = void 0; if (enterAction === 'DIV') { insertElem = createElement('div'); insertElem.appendChild(createElement('br')); } else if (enterAction === 'BR') { insertElem = createElement('br'); } else { insertElem = createElement('p'); insertElem.appendChild(createElement('br')); } this.insertAfter(insertElem, table); } }; /* * Calculates CSS style value by appending appropriate units. * If the value is a string with a unit (px, %, auto), it returns the original value. * Otherwise, it appends 'px' to the value. */ TableCommand.prototype.calculateStyleValue = function (value) { var styleValue; if (typeof value === 'string') { if (value.indexOf('px') >= 0 || value.indexOf('%') >= 0 || value.indexOf('auto') >= 0) { styleValue = value; } else { styleValue = value + 'px'; } } else { styleValue = value + 'px'; } return styleValue; }; /* * Inserts a node after the specified reference node. * Acts as a helper method since there's no direct insertAfter method in DOM. */ TableCommand.prototype.insertAfter = function (newNode, referenceNode) { if (!referenceNode.parentNode) { return; } referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); }; /* * Determines the minimum and maximum row/column indexes of selected cells. * This method calculates the bounding box that encloses all selected cells in the table. */ TableCommand.prototype.getSelectedCellMinMaxIndex = function (cellsMatrix) { var selectedCells = this.curTable.querySelectorAll('.e-cell-select'); var minRowIndex = cellsMatrix.length; var maxRowIndex = 0; var minColIndex = cellsMatrix[0].length; var maxColIndex = 0; for (var i = 0; i < selectedCells.length; i++) { var selectedCellPosition = getCorrespondingIndex(selectedCells[i], cellsMatrix); var cellEndPosition = this.FindIndex(selectedCellPosition[0], selectedCellPosition[1], cellsMatrix); minRowIndex = Math.min(selectedCellPosition[0], minRowIndex); maxRowIndex = Math.max(cellEndPosition[0], maxRowIndex); minColIndex = Math.min(selectedCellPosition[1], minColIndex); maxColIndex = Math.max(cellEndPosition[1], maxColIndex); } return { startRow: minRowIndex, endRow: maxRowIndex, startColumn: minColIndex, endColumn: maxColIndex }; }; /* * Inserts a new row before or after the selected row in a table. */ TableCommand.prototype.insertRow = function (e) { var isBelow = e.item.subCommand === 'InsertRowBefore' ? false : true; this.curTable = closest(this.parent.nodeSelection.range.startContainer.parentElement, 'table'); if (this.curTable.querySelectorAll('.e-cell-select').length === 0) { this.addRowWithoutCellSelection(); } else { this.addRowWithCellSelection(e, isBelow); } this.updateSelectionAfterRowInsertion(e); this.executeCallback(e); }; /* * Adds a new row when no cell is specifically selected. * Clones the last row and appends it to the table. */ TableCommand.prototype.addRowWithoutCellSelection = function () { var lastRow = this.curTable.rows[this.curTable.rows.length - 1]; var cloneRow = lastRow.cloneNode(true); cloneRow.removeAttribute('rowspan'); this.insertAfter(cloneRow, lastRow); }; /* * Adds a new row when a cell is selected, handling rowspan adjustments. */ TableCommand.prototype.addRowWithCellSelection = function (e, isBelow) { var allCells = getCorrespondingColumns(this.curTable); var minMaxIndex = this.getSelectedCellMinMaxIndex(allCells); var minVal = isBelow ? minMaxIndex.endRow : minMaxIndex.startRow; var newRow = createElement('tr'); var isHeaderSelect = this.curTable.querySelectorAll('th.e-cell-select').length > 0; this.createCellsForNewRow(allCells, minVal, isBelow, isHeaderSelect, newRow); this.insertNewRowAtPosition(e, isBelow, isHeaderSelect, minVal, newRow); }; /* * Creates cells for the new row, handling rowspan adjustments and styles. */ TableCommand.prototype.createCellsForNewRow = function (allCells, minVal, isBelow, isHeaderSelect, newRow) { for (var i = 0; i < allCells[minVal].length; i++) { if (this.isCellAffectedByRowspan(allCells, minVal, i, isBelow)) { if (this.isFirstCellInSpan(allCells, minVal, i)) { this.incrementRowspan(allCells[minVal][i]); } } else { this.createNewCellForRow(allCells, minVal, i, isHeaderSelect, isBelow, newRow); } } }; /* * Checks if a cell position is affected by a rowspan. */ TableCommand.prototype.isCellAffectedByRowspan = function (allCells, rowIndex, colIndex, isBelow) { return (isBelow && rowIndex < allCells.length - 1 && allCells[rowIndex][colIndex] === allCells[rowIndex + 1][colIndex]) || (!isBelow && 0 < rowIndex && allCells[rowIndex][colIndex] === allCells[rowIndex - 1][colIndex]); }; /* * Checks if this cell is the first cell in a rowspan/colspan.] */ TableCommand.prototype.isFirstCellInSpan = function (allCells, rowIndex, colIndex) { return 0 === colIndex || (0 < colIndex && allCells[rowIndex][colIndex] !== allCells[rowIndex][colIndex - 1]); }; /* * Increments the rowspan attribute of a cell. */ TableCommand.prototype.incrementRowspan = function (cell) { var currentRowspan = parseInt(cell.getAttribute('rowspan'), 10) || 1; cell.setAttribute('rowspan', (currentRowspan + 1).toString()); }; /* * Creates a new cell for the new row. */ TableCommand.prototype.createNewCellForRow = function (allCells, rowIndex, colIndex, isHeaderSelect, isBelow, newRow) { var tdElement = createElement('td'); tdElement.appendChild(createElement('br')); newRow.appendChild(tdElement); var referenceRowIndex = this.getReferenceRowIndex(allCells, rowIndex, isHeaderSelect, isBelow); var styleValue = allCells[referenceRowIndex][colIndex].getAttribute('style'); if (styleValue) { var updatedStyle = this.cellStyleCleanup(styleValue); tdElement.style.cssText = updatedStyle; } }; /* * Gets the appropriate reference row index for styling. */ TableCommand.prototype.getReferenceRowIndex = function (allCells, rowIndex, isHeaderSelect, isBelow) { if (isHeaderSelect && isBelow) { // If header is selected and inserting below, use first body row if available return (rowIndex + 1 < allCells.length) ? (rowIndex + 1) : rowIndex; } return rowIndex; }; /* * Inserts the new row at the appropriate position in the table. */ TableCommand.prototype.insertNewRowAtPosition = function (e, isBelow, isHeaderSelect, rowIndex, newRow) { var selectedRow; if (isHeaderSelect && isBelow) { selectedRow = this.curTable.querySelector('tbody').childNodes[0]; } else { selectedRow = this.curTable.rows[rowIndex]; } if (e.item.subCommand === 'InsertRowBefore') { selectedRow.parentElement.insertBefore(newRow, selectedRow); } else if (isHeaderSelect) { selectedRow.parentElement.insertBefore(newRow, selectedRow); } else { this.insertAfter(newRow, selectedRow); } }; /* * Updates the selection after row insertion. */ TableCommand.prototype.updateSelectionAfterRowInsertion = function (e) { e.item.selection.setSelectionText(this.tableModel.getDocument(), e.item.selection.range.startContainer, e.item.selection.range.startContainer, 0, 0); }; /* * Executes the callback function if provided. */ TableCommand.prototype.executeCallback = function (e) { if (e.callBack) { e.callBack({ requestType: e.item.subCommand, editorMode: 'HTML', event: e.event, range: this.parent.nodeSelection.getRange(this.tableModel.getDocument()), elements: this.parent.nodeSelection.getSelectedNodes(this.tableModel.getDocument()) }); } }; /* * Inserts a new column before or after the selected column in a table. */ TableCommand.prototype.insertColumn = function (e) { // Locate the selected cell var selectedCell = e.item.selection.range.startContainer; if (!(selectedCell.nodeName === 'TH' || selectedCell.nodeName === 'TD')) { selectedCell = closest(selectedCell.parentElement, 'td,th'); } var curRow = closest(selectedCell, 'tr'); var allRows = closest(curRow, 'table').rows; var colIndex = Array.prototype.slice.call(curRow.querySelectorAll(':scope > td, :scope > th')).indexOf(selectedCell); this.prepareTableForColumnInsertion(e, curRow); this.insertCellsInAllRows(e, allRows, colIndex); this.finalizeColumnInsertion(e, selectedCell); }; /* * Prepares the table for column insertion by calculating and storing widths. */ TableCommand.prototype.prepareTableForColumnInsertion = function (e, curRow) { var currentTabElm = closest(curRow, 'table'); var thTdElm = currentTabElm.querySelectorAll('th,td'); for (var i = 0; i < thTdElm.length; i++) { thTdElm[i].dataset.oldWidth = (thTdElm[i].offsetWidth / currentTabElm.offsetWidth * 100) + '%'; } if (isNOU(currentTabElm.style.width) || currentTabElm.style.width === '') { currentTabElm.style.width = currentTabElm.offsetWidth + 'px'; } }; /* * Inserts new cells in all rows at the specified column index. */ TableCommand.prototype.insertCellsInAllRows = function (e, allRows, colIndex) { // Get current table to calculate proper column width var currentTabElm = closest(allRows[0], 'table'); var thTdElm = currentTabElm.querySelectorAll('th,td'); var currentCellCount = allRows[0].querySelectorAll(':scope > td, :scope > th').length; var currentWidth = parseInt(e.item.width, 10) / (currentCellCount + 1); var previousWidth = parseInt(e.item.width, 10) / currentCellCount; // update column group var cols = this.updateColumnGroup(currentTabElm, colIndex, e.item.subCommand, currentWidth); //update the column for (var i = 0; i < allRows.length; i++) { var curCell = allRows[i].querySelectorAll(':scope > td, :scope > th')[colIndex]; var colTemplate = this.createColumnCell(curCell); if (e.item.subCommand === 'InsertColumnLeft') { curCell.parentElement.insertBefore(colTemplate, curCell); } else { this.insertAfter(colTemplate, curCell); } delete colTemplate.dataset.oldWidth; } this.redistributeCellWidths(thTdElm, previousWidth, currentWidth, cols); }; /* * Updates colgroup structure during column insertion */ TableCommand.prototype.updateColumnGroup = function (currentTabElm, colIndex, subCommand, currentWidth) { insertColGroupWithSizes(currentTabElm); var colGroup = getColGroup(currentTabElm); var newCol = createElement('col'); newCol.appendChild(createElement('br')); newCol.style.width = currentWidth.toFixed(4) + '%'; var cols = colGroup.querySelectorAll('col'); if (cols.length > 0 && colIndex < cols.length) { var curCol = cols[colIndex]; if (subCommand === 'InsertColumnLeft') { colGroup.insertBefore(newCol, curCol); } else { this.insertAfter(newCol, curCol); } } else { colGroup.appendChild(newCol); } return colGroup.querySelectorAll('col'); }; /* * Creates a new cell for column insertion with proper attributes. */ TableCommand.prototype.createColumnCell = function (referenceCell) { var colTemplate = referenceCell.cloneNode(true); var style = colTemplate.getAttribute('style'); if (style) { var updatedStyle = this.cellStyleCleanup(style); colTemplate.style.cssText = updatedStyle; } colTemplate.innerHTML = ''; colTemplate.appendChild(createElement('br')); colTemplate.removeAttribute('class'); colTemplate.removeAttribute('colspan'); colTemplate.removeAttribute('rowspan'); return colTemplate; }; /* * Redistributes cell widths after column insertion. */ TableCommand.prototype.redistributeCellWidths = function (cells, previousWidth, currentWidth, cols) { for (var i = 0; i < cells.length; i++) { if (cells[i].dataset.oldWidth) { var oldWidthValue = Number(cells[i].dataset.oldWidth.split('%')[0]); var colIndex = Array.prototype.slice.call(cells[i]. parentElement.querySelectorAll(':scope > td, :scope > th')).indexOf(cells[i]); cols[colIndex].style.width = (oldWidthValue * currentWidth / previousWidth).toFixed(4) + '%'; delete cells[i].dataset.oldWidth; } } }; /* * Finalizes column insertion by updating selection and executing callbacks. */ TableCommand.prototype.finalizeColumnInsertion = function (e, selectedCell) { e.item.selection.setSelectionText(this.tableModel.getDocument(), selectedCell, selectedCell, 0, 0); this.executeCallback(e); }; /* * Sets the background color for selected table cells. */ TableCommand.prototype.setBGColor = function (args) { var range = this.parent.nodeSelection.getRange(this.tableModel.getDocument()); var start = range.startContainer.nodeType === 3 ? range.startContainer.parentNode : range.startContainer; this.curTable = start.closest('table'); var selectedCells = this.curTable.querySelectorAll('.e-cell-select'); for (var i = 0; i < selectedCells.length; i++) { selectedCells[i].style.backgroundColor = args.value.toString(); } this.parent.undoRedoManager.saveData(); this.parent.observer.notify(EVENTS.hideTableQuickToolbar, {}); this.executeBgColorCallback(args); }; /* * Executes callback after setting background color. */ TableCommand.prototype.executeBgColorCallback = function (args) { if (args.callBack) { args.callBack({ requestType: args.subCommand, editorMode: 'HTML', event: args.event, range: this.parent.nodeSelection.getRange(this.tableModel.getDocument()), elements: this.parent.nodeSelection.getSelectedNodes(this.tableModel.getDocument()) }); } }; /** * Applies table styles. * This method handles various table styling operations like adding dashed borders, * alternating borders, or custom CSS classes. * * @param {IHtmlItem} e - The click event arguments * @returns {void} * @private */ TableCommand.prototype.tableStyles = function (e) { var args = e.event; var command = e.item.subCommand; var table = closest(args.selectParent[0], 'table'); this.applyTableStyleCommand(command, table); this.applyCustomCssClasses(args, table); this.parent.undoRedoManager.saveData(); this.parent.observer.notify(EVENTS.hideTableQuickToolbar, {}); this.parent.nodeSelection.restore(); if (e.callBack) { e.callBack({ requestType: e.item.subCommand, editorMode: 'HTML', event: args.args, range: this.parent.nodeSelection.getRange(this.parent.currentDocument), elements: this.parent.nodeSelection.getSelectedNodes(this.parent.currentDocument) }); } }; /** * Applies a specific table style command. * This helper method handles the actual application of built-in table styles * such as dashed or alternating borders. * * @param {string} command - The style command to apply * @param {HTMLTableElement} table - The table element to style * @returns {void} * @private */ TableCommand.prototype.applyTableStyleCommand = function (command, table) { if (command === 'Dashed') { var hasParentClass = this.parent.editableElement.classList.contains(EVENTS.CLS_TB_DASH_BOR); if (hasParentClass) { removeClassWithAttr([this.parent.editableElement], EVENTS.CLS_TB_DASH_BOR); } else { this.parent.editableElement.classList.add(EVENTS.CLS_TB_DASH_BOR); } var hasTableClass = table.classList.contains(EVENTS.CLS_TB_DASH_BOR); if (hasTableClass) { removeClassWithAttr([table], EVENTS.CLS_TB_DASH_BOR); } else { table.classList.add(EVENTS.CLS_TB_DASH_BOR); } } else if (command === 'Alternate') { var hasParentClass = this.parent.editableElement.classList.contains(EVENTS.CLS_TB_ALT_BOR); if (hasParentClass) { removeClassWithAttr([this.parent.editableElement], EVENTS.CLS_TB_ALT_BOR); } else { this.parent.editableElement.classList.add(EVENTS.CLS_TB_ALT_BOR); } var hasTableClass = table.classList.contains(EVENTS.CLS_TB_ALT_BOR); if (hasTableClass) { removeClassWithAttr([table], EVENTS.CLS_TB_ALT_BOR); } else { table.classList.add(EVENTS.CLS_TB_ALT_BOR); } } }; /** * Applies custom CSS classes to a table. * This helper method processes any custom CSS classes specified in the * command arguments and toggles them on the table. * * @param {ITableNotifyArgs} args - The table notification arguments * @param {HTMLTableElement} table - The table element to style * @returns {void} * @private */ TableCommand.prototype.applyCustomCssClasses = function (args, table) { var clickArgs = args.args; if (clickArgs && clickArgs.item.cssClass) { var classList = clickArgs.item.cssClass.split(' '); for (var i = 0; i < classList.length; i++) { var className = classList[i]; if (table.classList.contains(className)) { removeClassWithAttr([table], className); } else { table.classList.add(className); } } } }; /* * Deletes a column from the table. */ TableCommand.prototype.deleteColumn = function (e) { var selectedCell = e.item.selection.range.startContainer; if (selectedCell.nodeType === 3) { selectedCell = closest(selectedCell.parentElement, 'td,th'); } var tBodyHeadEle = closest(selectedCell, selectedCell.tagName === 'TH' ? 'thead' : 'tbody'); var rowIndex = tBodyHeadEle && Array.prototype.indexOf.call(tBodyHeadEle.childNodes, selectedCell.parentNode); this.curTable = closest(selectedCell, 'table'); // If only one column remains, remove the entire table var curRow = closest(selectedCell, 'tr'); if (curRow.querySelectorAll('th,td').length === 1) { this.removeEntireTable(e); } else { insertColGroupWithSizes(this.curTable); var selectedMinMaxIndex = this.removeSelectedColumns(e, tBodyHeadEle, rowIndex); // Update colgroup structure after deletion this.updateColgroupAfterColumnDeletion(this.curTable, selectedMinMaxIndex.startColumn, selectedMinMaxIndex.endColumn); } this.executeDeleteColumnCallback(e); }; /* * Updates colgroup structure after column deletion */ TableCommand.prototype.updateColgroupAfterColumnDeletion = function (table, startColIndex, endColIndex) { var colGroup = getColGroup(table); var cols = colGroup.querySelectorAll('col'); var deleteCount = endColIndex - startColIndex + 1; // Remove cols in the deleted range for (var i = 0; i < deleteCount; i++) { if (startColIndex < cols.length) { colGroup.removeChild(cols[startColIndex]); cols = colGroup.querySelectorAll('col'); } } // Redistribute widths of remaining columns var remainingCount = cols.length; var tableWidth = table.offsetWidth; var colWidths = new Array(remainingCount); // Get all column offsetWidths in one pass to avoid reflow issues for (var i = 0; i < remainingCount; i++) { colWidths[i] = cols[i].offsetWidth; } // Now apply percentage widths all at once for (var i = 0; i < remainingCount; i++) { cols[i].style.width = convertPixelToPercentage(colWidths[i], tableWidth).toFixed(4) + '%'; } }; /* * Removes the entire table when the last column is being deleted. */ TableCommand.prototype.removeEntireTable = function (e) { var selectedCell = e.item.selection.range.startContainer; detach(closest(selectedCell.parentElement, 'table')); e.item.selection.restore(); }; /* * Removes selected columns, handling colspan adjustments. */ TableCommand.prototype.removeSelectedColumns = function (e, tBodyHeadEle, rowIndex) { var deleteIndex = -1; var allCells = getCorrespondingColumns(this.curTable); var selectedMinMaxIndex = this.getSelectedCellMinMaxIndex(allCells); var minCol = selectedMinMaxIndex.startColumn; var maxCol = selectedMinMaxIndex.endColumn; for (var i = 0; i < allCells.length; i++) { var currentRow = allCells[i]; for (var j = 0; j < currentRow.length; j++) { var currentCell = currentRow[j]; var currentCellIndex = getCorrespondingIndex(currentCell, allCells); var colSpanVal = parseInt(currentCell.getAttribute('colspan'), 10) || 1; if (this.isCellAffectedByDeletedColumns(currentCellIndex[1], colSpanVal, minCol, maxCol)) { if (colSpanVal > 1) { this.adjustColspan(currentCell, colSpanVal); } else { detach(currentCell); deleteIndex = j; this.handleIESpecificSelection(e, Browser.isIE); } } } } this.updateSelectionAfterColumnDelete(e, tBodyHeadEle, rowIndex, deleteIndex); return selectedMinMaxIndex; }; /* * Checks if a cell is affected by the deleted columns. */ TableCommand.prototype.isCellAffectedByDeletedColumns = function (cellColIndex, colSpanVal, minCol, maxCol) { return cellColIndex + (colSpanVal - 1) >= minCol && cellColIndex <= maxCol; }; /* * Adjusts the colspan attribute of a cell during column deletion. */ TableCommand.prototype.adjustColspan = function (cell, currentColspan) { cell.setAttribute('colspan', (currentColspan - 1).toString()); }; /* * Handles IE-specific selection issues during column deletion. */ TableCommand.prototype.handleIESpecificSelection = function (e, isIE) { if (isIE) { var firstCell = this.curTable.querySelector('td'); e.item.selection.setSelectionText(this.tableModel.getDocument(), firstCell, firstCell, 0, 0); firstCell.classList.add('e-cell-select'); } }; /* * Updates selection after column deletion. */ TableCommand.prototype.updateSelectionAfterColumnDelete = function (e, tBodyHeadEle, rowIndex, deleteIndex) { if (deleteIndex > -1) { var rowHeadEle = tBodyHeadEle && tBodyHeadEle.children[rowIndex]; var cellIndex = deleteIndex <= (rowHeadEle && rowHeadEle.children.length - 1) ? deleteIndex : deleteIndex - 1; var nextFocusCell = rowHeadEle && rowHeadEle.children[cellIndex]; if (nextFocusCell) { e.item.selection.setSelectionText(this.tableModel.getDocument(), nextFocusCell, nextFocusCell, 0, 0); nextFocusCell.classList.add('e-cell-select'); } } }; /* * Executes the callback after column deletion with additional cursor handling. */ TableCommand.prototype.executeDeleteColumnCallback = function (e) { if (e.callBack) { var sContainer = this.parent.nodeSelection.getRange(this.tableModel.getDocument()).startContainer; // Handle selection if not directly in a TD element if (sContainer.nodeName !== 'TD') { var startChildLength = this.parent.nodeSelection. getRange(this.tableModel.getDocument()).startOffset; var focusNode = sContainer.children[startChildLength]; if (focusNode) { this.parent.nodeSelection.setCursorPoint(this.tableModel.getDocument(), focusNode, 0); } } this.executeCallback(e); } }; /* * Deletes selected rows from the table. */ TableCommand.prototype.deleteRow = function (e) { var selectedCell = e.item.selection.range.startContainer; if (selectedCell.nodeType === 3) { // Text node selectedCell = closest(selectedCell.parentElement, 'td,th'); } var colIndex = Array.prototype.indexOf.call(selectedCell.parentNode.childNodes, selectedCell); this.curTable = closest(selectedCell, 'table'); var allCells = getCorrespondingColumns(this.curTable); var minMaxIndex = this.getSelectedCellMinMaxIndex(allCells); if (this.curTable.rows.length === 1) { this.removeEntireTable(e); } else { this.deleteSelectedRows(e, minMaxIndex, allCells, colIndex); } this.executeCallback(e); }; /* * Deletes the selected rows and adjusts the table structure. */ TableCommand.prototype.deleteSelectedRows = function (e, minMaxIndex, allCells, colIndex) { for (var rowIndex = minMaxIndex.endRow; rowIndex >= minMaxIndex.startRow; rowIndex--) { var currentRow = this.curTable.rows[rowIndex]; this.adjustRowSpans(rowIndex, allCells); this.repositionSpannedCells(rowIndex, allCells); var deleteIndex = currentRow.rowIndex; this.curTable.deleteRow(deleteIndex); this.restoreFocusAfterRowDeletion(e, deleteIndex, colIndex); } }; /* * Adjusts rowspan attributes of cells when a row is deleted. */ TableCommand.prototype.adjustRowSpans = function (rowIndex, allCells) { for (var colIndex = 0; colIndex < allCells[rowIndex].length; colIndex++) { if (colIndex !== 0 && allCells[rowIndex][colIndex] === allCells[rowIndex][colIndex - 1]) { continue; } var currentCell = allCells[rowIndex][colIndex]; var rowspanAttr = currentCell.getAttribute('rowspan'); if (rowspanAttr && parseInt(rowspanAttr, 10) > 1) { var rowSpanVal = parseInt(rowspanAttr, 10) - 1; if (rowSpanVal === 1) { currentCell.removeAttribute('rowspan'); this.createReplacementCellIfNeeded(colIndex); } else { currentCell.setAttribute('rowspan', rowSpanVal.toString()); } } } }; /* * Creates a replacement cell if needed for a merged row. */ TableCommand.prototype.createReplacementCellIfNeeded = function (colIndex) { var mergedRowCells = this.getMergedRow(getCorrespondingColumns(this.curTable)); if (mergedRowCells && colIndex < mergedRowCells.length) { var cell = mergedRowCells[colIndex]; if (cell) { var cloneNode = cell.cloneNode(true); cloneNode.innerHTML = '<br>'; if (cell.parentElement) { cell.parentElement.insertBefore(cloneNode, cell); } } } }; /* * Repositions cells that span multiple rows when a row is deleted. */ TableCommand.prototype.repositionSpannedCells = function (rowIndex, allCells) { for (var colIndex = 0; colIndex < allCells[rowIndex].length; colIndex++) { var currentCell = allCells[rowIndex][colIndex]; var isSpanningToNextRow = rowIndex < allCells.length - 1 && currentCell === allCells[rowIndex + 1][colIndex]; var isBeginningOfSpan = rowIndex === 0 || currentCell !== allCells[rowIndex - 1][colIndex]; if (isSpanningToNextRow && isBeginningOfSpan) { var firstCellIndex = colIndex; while (firstCellIndex > 0 && currentCell === allCells[rowIndex][firstCellIndex - 1]) { if (firstCellIndex === 0) { this.curTable.rows[rowIndex + 1].prepend(currentCell); } else { var previousCell = allCells[rowIndex + 1][firstCellIndex - 1]; previousCell.insertAdjacentElement('afterend', currentCell); } firstCellIndex--; } } } }; /* * Restores focus to an appropriate cell after row deletion. */ TableCommand.prototype.restoreFocusAfterRowDeletion = function (e, deleteIndex, colIndex) { // Find a suitable row element (either at same index or previous one) var focusTrEle = !isNOU(this.curTable.rows[deleteIndex]) ? this.curTable.querySelectorAll('tbody tr')[deleteIndex] : this.curTable.querySelectorAll('tbody tr')[deleteIndex - 1]; // Find a suitable cell in that row var nextFocusCell = focusTrEle && focusTrEle.querySelectorAll('td')[colIndex]; if (nextFocusCell) { e.item.selection.setSelectionText(this.tableModel.getDocument(), nextFocusCell, nextFocusCell, 0, 0); nextFocusCell.classList.add('e-cell-select'); } else { var firstCell = this.curTable.querySelector('td'); e.item.selection.setSelectionText(this.tableModel.getDocument(), firstCell, firstCell, 0, 0); firstCell.classList.add('e-cell-select'); } }; /* * Finds the first row in the table that has merged cells (different cell count than the first row). */ TableCommand.prototype.getMergedRow = function (cells) { var mergedRow; var firstRowCellCount = this.curTable.rows[0].childNodes.length; for (var i = 0; i < cells.length; i++) { if (cells[i].length !== firstRowCellCount) { mergedRow = cells[i]; break; } } return mergedRow; }; /* * Removes the entire table from the document and restores selection. */ TableCommand.prototype.removeTable = function (e) { var selectedCell = e.item.selection.range.startContainer; selectedCell = (selectedCell.nodeType === 3) ? selectedCell.parentNode : selectedCell; var selectedTable = closest(selectedCell.parentElement, 'table'); if (selectedTable) { detach(selectedTable); e.item.selection.restore(); } this.executeCallback(e); }; /* * Toggles table header (THEAD) on or off in the selected table. * If the table doesn't have a header, one will be created. * If it already has a header, it will be removed. */ TableCommand.prototype.tableHeader = function (e) { var tableElement = this.getTableFromSelection(e); var hasHeader = this.checkIfTableHasHeader(tableElement); if (tableElement && !hasHeader) { this.createTableHeader(tableElement); } else { tableElement.deleteTHead(); } this.executeCallback(e); }; /* * Gets the table element from the current selection. */ TableCommand.prototype.getTableFromSelection = function (e) { var selectedCell = e.item.selection.range.startContainer; if (selectedCell.nodeName === 'TABLE') { return selectedCell; } if (selectedCell.nodeType === 3) { selectedCell = selectedCell.parentNode; } return closest(selectedCell.parentElement, 'table'); }; /* * Checks if the table already has a header element. */ TableCommand.prototype.checkIfTableHasHeader = function (table) { var headerExists = false; Array.prototype.slice.call(table.childNodes).forEach(function (childNode) { if (childNode.nodeName === 'THEAD') { headerExists = true; } }); return headerExists; }; /* * Creates a header row for the table with appropriate number of cells. */ TableCommand.prototype.createTableHeader = function (table) { var firstRow = table.querySelector('tr'); var cellCount = firstRow.childElementCount; var totalCellCount = 0; for (var i = 0; i < cellCount; i++) { var colspanValue = parseInt(firstRow.children[i].getAttribute('colspan'), 10) || 1; totalCellCount += colspanValue; } var headerSection = table.createTHead(); var headerRow = headerSection.insertRow(0); this.createHeaderCells(headerRow, totalCellCount); }; /* * Creates the appropriate number of header cells in the header row. */ TableCommand.prototype.createHeaderCells = function (headerRow, cellCount) { for (var j = 0; j < cellCount; j++) { var thElement = createElement('th'); thElement.appendChild(createElement('br')); headerRow.appendChild(thElement); } }; /* * Sets the vertical alignment for the selected table cell. */ TableCommand.prototype.tableVerticalAlign = function (e) { var alignValue = this.getVerticalAlignmentValue(e.item.subCommand); this.applyVerticalAlignment(e.item.tableCell, alignValue); this.executeCallback(e); }; /* * Determines the vertical alignment CSS value based on the subcommand. */ TableCommand.prototype.getVerticalAlignmentValue = function (subCommand) { switch (subCommand) { case 'AlignTop': return 'top'; case 'AlignMiddle': return 'middle'; case 'AlignBottom': return 'bottom'; default: return ''; } }; /* * Applies the vertical alignment to the table cell and removes any obsolete * valign attribute if necessary. */ TableCommand.prototype.applyVerticalAlignment = function (cell, valu