@syncfusion/ej2-richtexteditor
Version:
Essential JS 2 RichTextEditor component
1,135 lines (1,133 loc) • 195 kB
JavaScript
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