UNPKG

suneditor

Version:

Vanilla JavaScript based WYSIWYG web editor

464 lines (415 loc) 16.7 kB
import { dom, numbers, converter, env, keyCodeMap } from '../../../../helper'; import { _DragHandle } from '../../../../modules/ui'; import * as Constants from '../shared/table.constants'; import { CheckCellEdge, CheckRowEdge } from '../shared/table.utils'; const { _w } = env; /** * @description Manages table and cell resizing with drag-based dimension adjustments and visual guide lines. */ export class TableResizeService { #main; #$; #state; #globalEvents; #resizing = false; #resizeLine = null; #resizeLinePrev = null; /** * @param {import('../index').default} main Table index */ constructor(main) { this.#main = main; this.#$ = main.$; this.#state = main.state; // member - global events this.#globalEvents = { resize: null, resizeStop: null, resizeKeyDown: null, }; } get #selectionService() { return this.#main.selectionService; } /** * @description Checks if the table is currently being resized. * @returns {boolean} */ isResizing() { return this.#resizing; } /** * @description Display a guide line during resize operations logic. * @param {MouseEvent} event - Mouse event * @param {HTMLElement} target - Target element (table cell or row) * @returns {boolean|undefined} Returns `false` if resizing started, otherwise `undefined`. */ onResizeGuide(event, target) { const cellEdge = CheckCellEdge(event, target); if (cellEdge.is) { if (this.#main._element) this.#main._element.style.cursor = ''; this.#removeGlobalEvents(); if (this.#resizeLine?.style.display === 'block') this.#resizeLine.style.display = 'none'; this.#resizeLine = this.#$.frameContext.get('wrapper').querySelector(Constants.RESIZE_CELL_CLASS); this.#setResizeLinePosition(dom.query.getParentElement(target, dom.check.isTable), target, this.#resizeLine, cellEdge.isLeft); this.#resizeLine.style.display = 'block'; return false; } const rowEdge = CheckRowEdge(event, target); if (rowEdge.is) { this.#removeGlobalEvents(); this.#main._element = dom.query.getParentElement(target, dom.check.isTable); this.#main._element.style.cursor = 'ns-resize'; if (this.#resizeLine?.style.display === 'block') this.#resizeLine.style.display = 'none'; this.#resizeLine = this.#$.frameContext.get('wrapper').querySelector(Constants.RESIZE_ROW_CLASS); this.#setResizeRowPosition(dom.query.getParentElement(target, dom.check.isTable), target, this.#resizeLine); this.#resizeLine.style.display = 'block'; return false; } } /** * @description Hides the resize guide line. */ offResizeGuide() { this.#hideResizeLine(); } /** * @description Prepares for resizing from the edge of a cell or row. * @param {MouseEvent} event - Mouse event * @param {HTMLTableCellElement} target - Target element * @returns {boolean|undefined} Returns `false` if resizing started. */ readyResizeFromEdge(event, target) { const cellEdge = CheckCellEdge(event, target); if (cellEdge.is) { try { this.#selectionService.deleteStyleSelectedCells(); this.#main.setCellInfo(target, true); const colIndex = this.#state.logical_cellIndex + this.#state.current_colSpan - (cellEdge.isLeft ? 1 : 0); // ready this.#$.ui.enableBackWrapper('ew-resize'); this.#resizeLine ||= this.#$.frameContext.get('wrapper').querySelector(Constants.RESIZE_CELL_CLASS); this.#resizeLinePrev = this.#$.frameContext.get('wrapper').querySelector(Constants.RESIZE_CELL_PREV_CLASS); // select figure if (colIndex < 0 || colIndex === this.#state.logical_cellCnt - 1) { this._startFigureResizing(cellEdge.startX, colIndex < 0); this.#main._editorEnable(false); return false; } const col = this.#main._element.querySelector('colgroup').querySelectorAll('col')[colIndex < 0 ? 0 : colIndex]; this._startCellResizing(col, cellEdge.startX, numbers.get(_w.getComputedStyle(col).width, Constants.CELL_DECIMAL_END), cellEdge.isLeft); this.#main._editorEnable(false); } catch (err) { console.warn('[SUNEDITOR.plugins.table.error]', err); this.#main._editorEnable(true); this.#removeGlobalEvents(); } finally { this.#main.setState('fixedCell', null); this.#main.setState('selectedCell', null); this.#main.controller_table.hide(); this.#main.controller_cell.hide(); } return false; } const rowEdge = CheckRowEdge(event, target); if (rowEdge.is) { try { /** @type {HTMLTableRowElement} */ let row = dom.query.getParentElement(target, dom.check.isTableRow); let rowSpan = target.rowSpan; if (rowSpan > 1) { while (dom.check.isTableRow(row) && rowSpan > 1) { row = /** @type {HTMLTableRowElement} */ (row.nextElementSibling); --rowSpan; } } this.#selectionService.deleteStyleSelectedCells(); this.#main.setRowInfo(row); // ready this.#$.ui.enableBackWrapper('ns-resize'); this.#resizeLine ||= this.#$.frameContext.get('wrapper').querySelector(Constants.RESIZE_ROW_CLASS); this.#resizeLinePrev = this.#$.frameContext.get('wrapper').querySelector(Constants.RESIZE_ROW_PREV_CLASS); this._startRowResizing(row, rowEdge.startY, numbers.get(_w.getComputedStyle(row).height, Constants.CELL_DECIMAL_END)); this.#main._editorEnable(false); } catch (err) { console.warn('[SUNEDITOR.plugins.table.error]', err); this.#main._editorEnable(true); this.#removeGlobalEvents(); } finally { this.#main.setState('fixedCell', null); this.#main.setState('selectedCell', null); this.#main.controller_table.hide(); this.#main.controller_cell.hide(); } return false; } } /** * @description Converts the width of `<col>` elements to percentages. * @param {HTMLTableElement} target - The target table element. */ #resizePercentCol(target) { const cols = target.querySelector('colgroup').querySelectorAll('col'); const tableTotalWidth = target.offsetWidth; cols.forEach((col) => { const colWidthString = col.style.width; if (!colWidthString.endsWith('%')) { const pixelWidth = col.offsetWidth || numbers.get(colWidthString, Constants.CELL_DECIMAL_END); const percentage = (pixelWidth / tableTotalWidth) * 100; col.style.width = percentage + '%'; } }); } /** * @internal * @description Starts resizing a table cell. * @param {HTMLElement} col The column element. * @param {number} startX The starting X position. * @param {number} startWidth The initial width of the column. * @param {boolean} isLeftEdge Whether the resizing is on the left edge. */ _startCellResizing(col, startX, startWidth, isLeftEdge) { const figureElement = this.#state.figureElement; dom.utils.removeClass(figureElement, 'se-component-selected'); this.#resizePercentCol(this.#main._element); this.#setResizeLinePosition(figureElement, this.#state.tdElement, this.#resizeLinePrev, isLeftEdge); this.#resizeLinePrev.style.display = 'block'; const prevValue = col.style.width; const nextCol = /** @type {HTMLElement} */ (col.nextElementSibling); const nextColPrevValue = nextCol.style.width; const realWidth = dom.utils.hasClass(this.#main._element, 'se-table-layout-fixed') ? nextColPrevValue : converter.getWidthInPercentage(nextCol || col); if (_DragHandle.get('__dragHandler')) _DragHandle.get('__dragHandler').style.display = 'none'; this.#addResizeGlobalEvents( this.#cellResize.bind( this, col, nextCol, figureElement, this.#state.tdElement, this.#resizeLine, isLeftEdge, startX, startWidth, numbers.get(prevValue, Constants.CELL_DECIMAL_END), numbers.get(realWidth, Constants.CELL_DECIMAL_END), this.#main._element.offsetWidth, ), () => { this.#removeGlobalEvents(); this.#resizePercentCol(this.#main._element); this.#$.history.push(true); this.#$.component.hoverSelect(this.#main._element); this.#main._editorEnable(true); }, (e) => { this._stopResize(col, prevValue, 'width', e); this._stopResize(nextCol, nextColPrevValue, 'width', e); }, ); } /** * @description Resizes a table cell. * @param {HTMLElement} col The column element. * @param {HTMLElement} nextCol The next column element. * @param {HTMLElement} figure The table figure element. * @param {HTMLElement} tdEl The table cell element. * @param {HTMLElement} resizeLine The resize line element. * @param {boolean} isLeftEdge Whether the resizing is on the left edge. * @param {number} startX The starting X position. * @param {number} startWidth The initial width of the column. * @param {number} prevWidthPercent The previous width percentage. * @param {number} nextColWidthPercent The next column's width percentage. * @param {number} tableWidth The total width of the table. * @param {MouseEvent} e The mouse event. */ #cellResize(col, nextCol, figure, tdEl, resizeLine, isLeftEdge, startX, startWidth, prevWidthPercent, nextColWidthPercent, tableWidth, e) { const deltaX = e.clientX - startX; const newWidthPx = startWidth + deltaX; const newWidthPercent = (newWidthPx / tableWidth) * 100; if (newWidthPercent > 0) { col.style.width = `${newWidthPercent}%`; const delta = prevWidthPercent - newWidthPercent; nextCol.style.width = `${nextColWidthPercent + delta}%`; this.#setResizeLinePosition(figure, tdEl, resizeLine, isLeftEdge); } } /** * @internal * @description Starts resizing a table row. * @param {HTMLElement} row The table row element. * @param {number} startY The starting Y position. * @param {number} startHeight The initial height of the row. */ _startRowResizing(row, startY, startHeight) { const figureElement = this.#state.figureElement; dom.utils.removeClass(figureElement, 'se-component-selected'); this.#setResizeRowPosition(figureElement, row, this.#resizeLinePrev); this.#resizeLinePrev.style.display = 'block'; const prevValue = row.style.height; if (_DragHandle.get('__dragHandler')) _DragHandle.get('__dragHandler').style.display = 'none'; this.#addResizeGlobalEvents( this.#rowResize.bind(this, row, figureElement, this.#resizeLine, startY, startHeight), () => { this.#removeGlobalEvents(); this.#$.history.push(true); this.#$.component.hoverSelect(this.#main._element); this.#main._editorEnable(true); }, this._stopResize.bind(this, row, prevValue, 'height'), ); } /** * @description Resizes a table row. * @param {HTMLElement} row The table row element. * @param {HTMLElement} figure The table figure element. * @param {HTMLElement} resizeLine The resize line element. * @param {number} startY The starting Y position. * @param {number} startHeight The initial height of the row. * @param {MouseEvent} e The mouse event. */ #rowResize(row, figure, resizeLine, startY, startHeight, e) { const deltaY = e.clientY - startY; const newHeightPx = startHeight + deltaY; row.style.height = `${newHeightPx}px`; this.#setResizeRowPosition(figure, row, resizeLine); } /** * @internal * @description Starts resizing the table figure. * @param {number} startX The starting X position. * @param {boolean} isLeftEdge Whether the resizing is on the left edge. */ _startFigureResizing(startX, isLeftEdge) { const figure = this.#state.figureElement; dom.utils.removeClass(figure, 'se-component-selected'); this.#setResizeLinePosition(figure, figure, this.#resizeLinePrev, isLeftEdge); this.#resizeLinePrev.style.display = 'block'; const realWidth = converter.getWidthInPercentage(figure); if (_DragHandle.get('__dragHandler')) _DragHandle.get('__dragHandler').style.display = 'none'; this.#addResizeGlobalEvents( this.#figureResize.bind(this, figure, this.#resizeLine, isLeftEdge, startX, figure.offsetWidth, numbers.get(realWidth, Constants.CELL_DECIMAL_END)), () => { this.#removeGlobalEvents(); if (numbers.get(figure.style.width, 0) > 100) figure.style.width = '100%'; this.#$.history.push(true); this.#$.component.hoverSelect(this.#main._element); this.#main._editorEnable(true); }, this._stopResize.bind(this, figure, figure.style.width, 'width'), ); } /** * @description Resizes the table figure. * @param {HTMLElement} figure The table figure element. * @param {HTMLElement} resizeLine The resize line element. * @param {boolean} isLeftEdge Whether the resizing is on the left edge. * @param {number} startX The starting X position. * @param {number} startWidth The initial width of the figure. * @param {number} constNum A constant number used for width calculation. * @param {MouseEvent} e The mouse event. */ #figureResize(figure, resizeLine, isLeftEdge, startX, startWidth, constNum, e) { const deltaX = isLeftEdge ? startX - e.clientX : e.clientX - startX; const newWidthPx = startWidth + deltaX; const newWidthPercent = (newWidthPx / startWidth) * constNum; if (newWidthPercent > 0) { figure.style.width = `${newWidthPercent}%`; this.#setResizeLinePosition(figure, figure, resizeLine, isLeftEdge); } } /** * @description Sets the resize line position. * @param {HTMLElement} figure The table figure element. * @param {HTMLElement} target The target element. * @param {HTMLElement} resizeLine The resize line element. * @param {boolean} isLeftEdge Whether the resizing is on the left edge. */ #setResizeLinePosition(figure, target, resizeLine, isLeftEdge) { const tdOffset = this.#$.offset.getLocal(target); const tableOffset = this.#$.offset.getLocal(figure); resizeLine.style.left = `${tdOffset.left + (isLeftEdge ? 0 : target.offsetWidth)}px`; resizeLine.style.top = `${tableOffset.top}px`; resizeLine.style.height = `${figure.offsetHeight}px`; } /** * @description Sets the resize row position. * @param {HTMLElement} figure The table figure element. * @param {HTMLElement} target The target row element. * @param {HTMLElement} resizeLine The resize line element. */ #setResizeRowPosition(figure, target, resizeLine) { const rowOffset = this.#$.offset.getLocal(target); const tableOffset = this.#$.offset.getLocal(figure); resizeLine.style.top = `${rowOffset.top + target.offsetHeight}px`; resizeLine.style.left = `${tableOffset.left}px`; resizeLine.style.width = `${figure.offsetWidth}px`; } /** * @internal * @description Stops resizing the table. * @param {HTMLElement} target The target element. * @param {string} prevValue The previous style value. * @param {string} styleProp The CSS property being changed. * @param {KeyboardEvent} e The keyboard event. */ _stopResize(target, prevValue, styleProp, e) { if (!keyCodeMap.isEsc(e.code)) return; this.#removeGlobalEvents(); this.#$.component.hoverSelect(this.#main._element); this.#main._editorEnable(true); target.style[styleProp] = prevValue; // figure reopen if (styleProp === 'width') { this.#$.component.select(this.#main._element, this.#main.constructor['key'], { isInput: true }); } } /** * @description Adds global event listeners for resizing. * @param {(...args: *) => void} resizeFn The function handling the resize event. * @param {(...args: *) => void} stopFn The function handling the stop event. * @param {(...args: *) => void} keyDownFn The function handling the keydown event. */ #addResizeGlobalEvents(resizeFn, stopFn, keyDownFn) { this.#globalEvents.resize = this.#$.eventManager.addGlobalEvent('mousemove', resizeFn, false); this.#globalEvents.resizeStop = this.#$.eventManager.addGlobalEvent('mouseup', stopFn, false); this.#globalEvents.resizeKeyDown = this.#$.eventManager.addGlobalEvent('keydown', keyDownFn, false); this.#resizing = true; } /** * @description Removes global event listeners and resets resize-related properties. */ #removeGlobalEvents() { this.#resizing = false; this.#$.ui.disableBackWrapper(); this.#hideResizeLine(); if (this.#resizeLinePrev) { this.#resizeLinePrev.style.display = 'none'; this.#resizeLinePrev = null; } const globalEvents = this.#globalEvents; for (const k in globalEvents) { globalEvents[k] &&= this.#$.eventManager.removeGlobalEvent(globalEvents[k]); } this.#resizing = false; this.#resizeLine = null; } /** * @description Hides the resize line if it is visible. */ #hideResizeLine() { if (this.#resizeLine) { this.#resizeLine.style.display = 'none'; this.#resizeLine = null; } if (this.#main._element) { this.#main._element.style.cursor = ''; } } /** * @description Initialize the resize service (remove global events). */ init() { this.#removeGlobalEvents(); } } export default TableResizeService;