UNPKG

suneditor

Version:

Vanilla JavaScript based WYSIWYG web editor

513 lines (446 loc) 15.6 kB
import { dom, numbers } from '../../../../helper'; import { SelectMenu } from '../../../../modules/ui'; import { CreateCellsHTML, CreateCellsString, InvalidateTableCache } from '../shared/table.utils'; import { CreateColumnMenu, CreateRowMenu } from '../render/table.menu'; /** * @description Manages table grid operations including row and column insertion, deletion, and header toggling. */ export class TableGridService { #main; #$; #state; /** * @param {import('../index').default} main Table index * @param {{ * columnButton: HTMLButtonElement, * rowButton: HTMLButtonElement, * openCellMenuFunc: ()=>void, * closeCellMenuFunc: ()=>void * }} options Options for table service */ constructor(main, { columnButton, rowButton, openCellMenuFunc, closeCellMenuFunc }) { this.#main = main; this.#$ = main.$; this.#state = main.state; // members - SelectMenu - column const columnMenu = CreateColumnMenu(this.#$.lang, this.#$.icons); this.selectMenu_column = new SelectMenu(this.#$, { checkList: false, position: 'bottom-center', openMethod: openCellMenuFunc, closeMethod: closeCellMenuFunc }); this.selectMenu_column.on(columnButton, this.#OnColumnEdit.bind(this)); this.selectMenu_column.create(columnMenu.items, columnMenu.menus); // members - SelectMenu - row const rownMenu = CreateRowMenu(this.#$.lang, this.#$.icons); this.selectMenu_row = new SelectMenu(this.#$, { checkList: false, position: 'bottom-center', openMethod: openCellMenuFunc, closeMethod: closeCellMenuFunc }); this.selectMenu_row.on(rowButton, this.#OnRowEdit.bind(this)); this.selectMenu_row.create(rownMenu.items, rownMenu.menus); } get #selectionService() { return this.#main.selectionService; } /** * @description Opens the column menu. */ openColumnMenu() { this.selectMenu_column.open(); } /** * @description Opens the row menu. */ openRowMenu() { this.selectMenu_row.menus[0].style.display = this.selectMenu_row.menus[1].style.display = /^TH$/i.test(this.#state.tdElement?.nodeName) ? 'none' : ''; this.selectMenu_row.open(); } /** * @description Edits the table by adding, removing, or modifying rows and cells, based on the provided options. Supports both single and multi-cell/row editing. * @param {"row"|"cell"} type The type of element to edit (`row` or `cell`). * @param {?"up"|"down"|"left"|"right"} option The action to perform: `up`, `down`, `left`, `right`, or `null` for removing. */ editTable(type, option) { const table = this.#main._element; const isRow = type === 'row'; if (isRow) { const tableAttr = this.#state.trElement.parentElement; if (/^THEAD$/i.test(tableAttr.nodeName)) { if (option === 'up') { return; } else if (!tableAttr.nextElementSibling || !/^TBODY$/i.test(tableAttr.nextElementSibling.nodeName)) { if (!option) { dom.utils.removeItem(this.#state.figureElement); this.#main._closeTableSelectInfo(); } else { table.innerHTML += '<tbody><tr>' + CreateCellsString('td', this.#state.logical_cellCnt) + '</tr></tbody>'; } return; } } } // multi if (this.#state.ref) { const positionCell = this.#state.tdElement; const selectedCells = this.#state.selectedCells; // multi - row if (isRow) { // remove row if (!option) { let row = selectedCells[0].parentNode; const removeCells = [selectedCells[0]]; for (let i = 1, len = selectedCells.length, cell; i < len; i++) { cell = selectedCells[i]; if (row !== cell.parentNode) { removeCells.push(cell); row = cell.parentNode; } } for (let i = 0, len = removeCells.length; i < len; i++) { this.#main.setCellInfo(removeCells[i], true); this.editRow(option); } } else { // edit row this.#main.setCellInfo(option === 'up' ? selectedCells[0] : selectedCells.at(-1), true); this.editRow(option, null, positionCell); } } else { // multi - cell const firstRow = selectedCells[0].parentNode; // remove cell if (!option) { const removeCells = [selectedCells[0]]; for (let i = 1, len = selectedCells.length, cell; i < len; i++) { cell = selectedCells[i]; if (firstRow === cell.parentNode) { removeCells.push(cell); } else { break; } } for (let i = 0, len = removeCells.length; i < len; i++) { this.#main.setCellInfo(removeCells[i], true); this.editColumn(option); } } else { // edit cell let rightCell = null; for (let i = 0, len = selectedCells.length - 1; i < len; i++) { if (firstRow !== selectedCells[i + 1].parentNode) { rightCell = selectedCells[i]; break; } } this.#main.setCellInfo(option === 'left' ? selectedCells[0] : rightCell || selectedCells[0], true); this.editColumn(option, null, positionCell); } } if (!option) this.#main.resetInfo(); } // one else { this[isRow ? 'editRow' : 'editColumn'](option); } // after remove if (!option) { const children = table.children; for (let i = 0; i < children.length; i++) { if (children[i].children.length === 0) { dom.utils.removeItem(children[i]); i--; } } if (table.children.length === 0) dom.utils.removeItem(table); } } /** * @description Edits a table cell(column), either adding, removing, or modifying the cell based on the provided option. * @param {?string} option The action to perform on the cell (`left`|`right`|`null`) * - `null`: to remove the cell * - `left`: to insert a new cell to the left * - `right`: to insert a new cell to the right * @param {?HTMLTableCellElement} [targetCell] Target cell, (default: current selected cell) * @param {?HTMLTableCellElement} [positionResetElement] The element to reset the position of (optional). This can be the cell that triggered the column edit. * @returns {HTMLTableCellElement} Target table cell */ editColumn(option, targetCell, positionResetElement) { if (targetCell) this.#main.setCellInfo(targetCell, true); const remove = !option; const left = option === 'left'; const colSpan = this.#state.current_colSpan; const cellIndex = this.#state.logical_cellIndex + (remove || left ? 0 : colSpan + 1); const rows = this.#state.trElements; let rowSpanArr = []; let spanIndex = []; let passCell = 0; let insertIndex; const removeCell = []; const removeSpanArr = []; for (let i = 0, len = this.#state.rowCnt, row, cells, newCell, applySpan, cellColSpan; i < len; i++) { row = rows[i]; insertIndex = cellIndex; applySpan = false; cells = row.cells; cellColSpan = 0; for (let c = 0, cell, cLen = cells.length, rs, cs, removeIndex; c < cLen; c++) { cell = cells[c]; if (!cell) break; rs = cell.rowSpan - 1; cs = cell.colSpan - 1; if (!remove) { if (c >= insertIndex) break; if (cs > 0) { if (passCell < 1 && cs + c >= insertIndex) { cell.colSpan += 1; insertIndex = null; passCell = rs + 1; break; } insertIndex -= cs; } if (!applySpan) { for (let r = 0, arr; r < spanIndex.length; r++) { arr = spanIndex[r]; insertIndex -= arr.cs; arr.rs -= 1; if (arr.rs < 1) { spanIndex.splice(r, 1); r--; } } applySpan = true; } } else { removeIndex = c + cellColSpan; if (spanIndex.length > 0) { const lastCell = !cells[c + 1]; for (let r = 0, arr; r < spanIndex.length; r++) { arr = spanIndex[r]; if (arr.row > i) continue; if (removeIndex >= arr.index) { cellColSpan += arr.cs; removeIndex = c + cellColSpan; arr.rs -= 1; arr.row = i + 1; if (arr.rs < 1) { spanIndex.splice(r, 1); r--; } } else if (lastCell) { arr.rs -= 1; arr.row = i + 1; if (arr.rs < 1) { spanIndex.splice(r, 1); r--; } } } } if (rs > 0) { rowSpanArr.push({ rs: rs, cs: cs + 1, index: removeIndex, row: -1, }); } if (removeIndex >= insertIndex && removeIndex + cs <= insertIndex + colSpan) { removeCell.push(cell); } else if (removeIndex <= insertIndex + colSpan && removeIndex + cs >= insertIndex) { cell.colSpan -= numbers.getOverlapRangeAtIndex(cellIndex, cellIndex + colSpan, removeIndex, removeIndex + cs); } else if (rs > 0 && (removeIndex < insertIndex || removeIndex + cs > insertIndex + colSpan)) { removeSpanArr.push({ cell: cell, i: i, rs: i + rs, }); } cellColSpan += cs; } } spanIndex = spanIndex.concat(rowSpanArr).sort(function (a, b) { return a.index - b.index; }); rowSpanArr = []; if (!remove) { if (passCell > 0) { passCell -= 1; continue; } if (insertIndex !== null && cells.length > 0) { newCell = CreateCellsHTML(cells[0].nodeName); newCell = row.insertBefore(newCell, cells[insertIndex]); } } } const colgroup = this.#main._element.querySelector('colgroup'); if (colgroup) { const cols = colgroup.querySelectorAll('col'); if (remove) { dom.utils.removeItem(cols[insertIndex]); } else { const isAutoLayout = !dom.utils.hasClass(this.#main._element, 'se-table-layout-fixed') && this.#main._element.style.tableLayout !== 'fixed'; const hasWidth = !isAutoLayout && Array.prototype.some.call(cols, (col) => numbers.get(col.style.width) > 0); if (hasWidth) { let totalW = 0; for (let i = 0, len = cols.length, w; i < len; i++) { w = numbers.get(cols[i].style.width); w -= Math.round((w * len * 0.1) / 2); totalW += w; cols[i].style.width = `${w}%`; } const newCol = dom.utils.createElement('col', { style: `width:${100 - totalW}%` }); colgroup.insertBefore(newCol, cols[insertIndex]); } else { // auto layout or no explicit widths — add bare col, let browser distribute colgroup.insertBefore(dom.utils.createElement('col'), cols[insertIndex] || null); } } } if (remove) { let removeFirst, removeEnd; for (let r = 0, rLen = removeCell.length, row; r < rLen; r++) { row = /** @type {HTMLTableRowElement} */ (removeCell[r].parentNode); dom.utils.removeItem(removeCell[r]); if (row.cells.length === 0) { removeFirst ||= dom.utils.getArrayIndex(rows, row); removeEnd = dom.utils.getArrayIndex(rows, row); dom.utils.removeItem(row); } } for (let c = 0, cLen = removeSpanArr.length, rowSpanCell; c < cLen; c++) { rowSpanCell = removeSpanArr[c]; rowSpanCell.cell.rowSpan = numbers.getOverlapRangeAtIndex(removeFirst, removeEnd, rowSpanCell.i, rowSpanCell.rs); } this.#main._closeController(); } else { this.#main._setCellControllerPosition(positionResetElement || this.#state.tdElement, true); } return positionResetElement || this.#state.tdElement; } /** * @description Edits a table row, either adding, removing, the row * @param {?string} option The action to perform on the row (`up`|`down`|`null`) * - `null`: to remove the row * - `up`: to insert the row up * - `down`: to insert the row down, or `null` to remove. * @param {?HTMLTableCellElement} [targetCell] Target cell, (default: current selected cell) * @param {?HTMLTableCellElement} [positionResetElement] The element to reset the position of (optional). This can be the cell that triggered the row edit. */ editRow(option, targetCell, positionResetElement) { this.#selectionService.deleteStyleSelectedCells(); if (targetCell) this.#main.setCellInfo(targetCell, true); const remove = !option; const up = option === 'up'; const originRowIndex = this.#state.rowIndex; const rowIndex = remove || up ? originRowIndex : originRowIndex + this.#state.current_rowSpan + 1; const sign = remove ? -1 : 1; const rows = this.#state.trElements; let cellCnt = this.#state.logical_cellCnt; for (let i = 0, len = originRowIndex + (remove ? -1 : 0), cell; i <= len; i++) { cell = rows[i].cells; if (cell.length === 0) return; for (let c = 0, cLen = cell.length, rs, cs; c < cLen; c++) { rs = cell[c].rowSpan; cs = cell[c].colSpan; if (rs < 2 && cs < 2) continue; if (rs + i > rowIndex && rowIndex > i) { cell[c].rowSpan = rs + sign; cellCnt -= cs; } } } if (remove) { const next = rows[originRowIndex + 1]; if (next) { const spanCells = []; let cells = rows[originRowIndex].cells; let colSpan = 0; for (let i = 0, len = cells.length, cell, logcalIndex; i < len; i++) { cell = cells[i]; logcalIndex = i + colSpan; colSpan += cell.colSpan - 1; if (cell.rowSpan > 1) { cell.rowSpan -= 1; spanCells.push({ cell: /** @type {HTMLTableCellElement} */ (cell.cloneNode(true)), index: logcalIndex }); } } if (spanCells.length > 0) { let spanCell = spanCells.shift(); cells = next.cells; colSpan = 0; for (let i = 0, len = cells.length, cell, logcalIndex; i < len; i++) { cell = cells[i]; logcalIndex = i + colSpan; colSpan += cell.colSpan - 1; if (logcalIndex >= spanCell.index) { i--; colSpan--; colSpan += spanCell.cell.colSpan - 1; next.insertBefore(spanCell.cell, cell); spanCell = spanCells.shift(); if (!spanCell) break; } } if (spanCell) { next.appendChild(spanCell.cell); for (let i = 0, len = spanCells.length; i < len; i++) { next.appendChild(spanCells[i].cell); } } } } this.#main._element.deleteRow(rowIndex); } else { this.insertBodyRow(this.#main._element, rowIndex, cellCnt); } if (!remove) { this.#main._setCellControllerPosition(positionResetElement || this.#state.tdElement, true); } else { this.#main._closeController(); } } /** * @description Inserts a new row into the table at the specified index to it. * @param {HTMLTableElement} table The table element to insert the row into. * @param {number} rowIndex The index at which to insert the new row. * @param {number} cellCnt The number of cells to create in the new row. * @returns {HTMLTableRowElement} The newly inserted row element. */ insertBodyRow(table, rowIndex, cellCnt) { const newRow = table.insertRow(rowIndex); newRow.innerHTML = CreateCellsString('td', cellCnt); return newRow; } /** * @description Handles column operations such as insert and delete. * @param {"insert-left"|"insert-right"|"delete"} command The column operation to perform. */ #OnColumnEdit(command) { InvalidateTableCache(this.#main._element); switch (command) { case 'insert-left': this.editTable('cell', 'left'); break; case 'insert-right': this.editTable('cell', 'right'); break; case 'delete': this.editTable('cell', null); } this.#main.historyPush(); } /** * @description Handles row operations such as insert and delete. * @param {"insert-above"|"insert-below"|"delete"} command The row operation to perform. */ #OnRowEdit(command) { InvalidateTableCache(this.#main._element); switch (command) { case 'insert-above': this.editTable('row', 'up'); break; case 'insert-below': this.editTable('row', 'down'); break; case 'delete': this.editTable('row', null); } this.#main.historyPush(); } } export default TableGridService;