From 98534a2d9e4476896a592d02a4be746f06a30aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BA=91=E9=A3=9E?= Date: Sun, 26 Dec 2021 19:40:55 +0800 Subject: [PATCH] feat:merge cell & cancel merge cell --- src/editor/assets/css/index.css | 8 + .../assets/images/merge-cancel-cell.svg | 1 + src/editor/assets/images/merge-cell.svg | 1 + src/editor/core/command/Command.ts | 12 ++ src/editor/core/command/CommandAdapt.ts | 189 +++++++++++++++++- src/editor/core/contextmenu/ContextMenu.ts | 11 +- .../core/contextmenu/menus/tableMenus.ts | 32 ++- src/editor/core/draw/Draw.ts | 10 + .../core/draw/particle/table/TableParticle.ts | 58 ++++-- src/editor/core/event/CanvasEvent.ts | 54 +++-- src/editor/core/range/RangeManager.ts | 17 +- src/editor/interface/Range.ts | 6 + .../interface/contextmenu/ContextMenu.ts | 1 + 13 files changed, 358 insertions(+), 42 deletions(-) create mode 100644 src/editor/assets/images/merge-cancel-cell.svg create mode 100644 src/editor/assets/images/merge-cell.svg diff --git a/src/editor/assets/css/index.css b/src/editor/assets/css/index.css index 972bfda..b0283d8 100644 --- a/src/editor/assets/css/index.css +++ b/src/editor/assets/css/index.css @@ -308,6 +308,14 @@ background-image: url(../../assets/images/delete-table.svg); } +.contextmenu-merge-cell { + background-image: url(../../assets/images/merge-cell.svg); +} + +.contextmenu-merge-cancel-cell { + background-image: url(../../assets/images/merge-cancel-cell.svg); +} + .hyperlink-popup { min-width: 100px; font-size: 12px; diff --git a/src/editor/assets/images/merge-cancel-cell.svg b/src/editor/assets/images/merge-cancel-cell.svg new file mode 100644 index 0000000..d0f9c4b --- /dev/null +++ b/src/editor/assets/images/merge-cancel-cell.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/merge-cell.svg b/src/editor/assets/images/merge-cell.svg new file mode 100644 index 0000000..d3ea706 --- /dev/null +++ b/src/editor/assets/images/merge-cell.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/core/command/Command.ts b/src/editor/core/command/Command.ts index 36715ae..575bd5e 100644 --- a/src/editor/core/command/Command.ts +++ b/src/editor/core/command/Command.ts @@ -34,6 +34,8 @@ export class Command { private static deleteTableRow: Function private static deleteTableCol: Function private static deleteTable: Function + private static mergeTableCell: Function + private static cancelMergeTableCell: Function private static image: Function private static hyperlink: Function private static search: Function @@ -72,6 +74,8 @@ export class Command { Command.deleteTableRow = adapt.deleteTableRow.bind(adapt) Command.deleteTableCol = adapt.deleteTableCol.bind(adapt) Command.deleteTable = adapt.deleteTable.bind(adapt) + Command.mergeTableCell = adapt.mergeTableCell.bind(adapt) + Command.cancelMergeTableCell = adapt.cancelMergeTableCell.bind(adapt) Command.image = adapt.image.bind(adapt) Command.hyperlink = adapt.hyperlink.bind(adapt) Command.search = adapt.search.bind(adapt) @@ -201,6 +205,14 @@ export class Command { return Command.deleteTable() } + public executMergeTableCell() { + return Command.mergeTableCell() + } + + public executCancelMergeTableCell() { + return Command.cancelMergeTableCell() + } + public executeHyperlink(payload: IElement) { return Command.hyperlink(payload) } diff --git a/src/editor/core/command/CommandAdapt.ts b/src/editor/core/command/CommandAdapt.ts index 2309215..514da5d 100644 --- a/src/editor/core/command/CommandAdapt.ts +++ b/src/editor/core/command/CommandAdapt.ts @@ -644,18 +644,185 @@ export class CommandAdapt { public deleteTable() { const positionContext = this.position.getPositionContext() - if (positionContext.isTable) { - const originalElementList = this.draw.getOriginalElementList() - originalElementList.splice(positionContext.index!, 1) - const curIndex = positionContext.index! - 1 - this.position.setPositionContext({ - isTable: false, - index: curIndex - }) - this.range.setRange(curIndex, curIndex) - this.draw.render({ curIndex }) - this.tableTool.dispose() + if (!positionContext.isTable) return + const originalElementList = this.draw.getOriginalElementList() + originalElementList.splice(positionContext.index!, 1) + const curIndex = positionContext.index! - 1 + this.position.setPositionContext({ + isTable: false, + index: curIndex + }) + this.range.setRange(curIndex, curIndex) + this.draw.render({ curIndex }) + this.tableTool.dispose() + } + + public mergeTableCell() { + const positionContext = this.position.getPositionContext() + if (!positionContext.isTable) return + const { isCrossRowCol, startTdIndex, endTdIndex, startTrIndex, endTrIndex } = this.range.getRange() + if (!isCrossRowCol) return + const { index } = positionContext + const originalElementList = this.draw.getOriginalElementList() + const element = originalElementList[index!] + const curTrList = element.trList! + let startTd = curTrList[startTrIndex!].tdList[startTdIndex!] + let endTd = curTrList[endTrIndex!].tdList[endTdIndex!] + // 交换起始位置 + if (startTd.x! > endTd.x! || startTd.y! > endTd.y!) { + [startTd, endTd] = [endTd, startTd] + } + const startColIndex = startTd.colIndex! + const endColIndex = endTd.colIndex! + (endTd.colspan - 1) + const startRowIndex = startTd.rowIndex! + const endRowIndex = endTd.rowIndex! + (endTd.rowspan - 1) + // 选区行列 + let rowCol: ITd[][] = [] + for (let t = 0; t < curTrList.length; t++) { + const tr = curTrList[t] + const tdList: ITd[] = [] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const tdColIndex = td.colIndex! + const tdRowIndex = td.rowIndex! + if ( + tdColIndex >= startColIndex && tdColIndex <= endColIndex + && tdRowIndex >= startRowIndex && tdRowIndex <= endRowIndex + ) { + tdList.push(td) + } + } + if (tdList.length) { + rowCol.push(tdList) + } + } + if (!rowCol.length) return + // 是否是矩形 + const lastRow = rowCol[rowCol.length - 1] + const leftTop = rowCol[0][0] + const rightBottom = lastRow[lastRow.length - 1] + const startX = leftTop.x! + const startY = leftTop.y! + const endX = rightBottom.x! + rightBottom.width! + const endY = rightBottom.y! + rightBottom.height! + for (let t = 0; t < rowCol.length; t++) { + const tr = rowCol[t] + for (let d = 0; d < tr.length; d++) { + const td = tr[d] + const tdStartX = td.x! + const tdStartY = td.y! + const tdEndX = tdStartX + td.width! + const tdEndY = tdStartY + td.height! + // 存在不符合项 + if (startX > tdStartX || startY > tdStartY || endX < tdEndX || endY < tdEndY) { + return + } + } + } + // 合并单元格 + let mergeTdIdList: string[] = [] + const anchorTd = rowCol[0][0] + for (let t = 0; t < rowCol.length; t++) { + const tr = rowCol[t] + for (let d = 0; d < tr.length; d++) { + const td = tr[d] + const isAnchorTd = t === 0 && d === 0 + // 待删除单元id + if (!isAnchorTd) { + mergeTdIdList.push(td.id!) + } + // 列合并 + if (t === 0 && d !== 0) { + anchorTd.colspan += td.colspan + } + // 行合并 + if (t !== 0) { + if (anchorTd.colIndex === td.colIndex) { + anchorTd.rowspan += td.rowspan + } + } + } + } + // 移除多余单元格 + for (let t = 0; t < curTrList.length; t++) { + const tr = curTrList[t] + let d = 0 + while (d < tr.tdList.length) { + const td = tr.tdList[d] + if (mergeTdIdList.includes(td.id!)) { + tr.tdList.splice(d, 1) + d-- + } + d++ + } + } + // 重新渲染 + const { startIndex, endIndex } = this.range.getRange() + this.range.setRange(startIndex, endIndex) + this.draw.render({ + curIndex: endIndex + }) + const position = this.position.getOriginalPositionList() + this.tableTool.render(element, position[index!]) + } + + public cancelMergeTableCell() { + const positionContext = this.position.getPositionContext() + if (!positionContext.isTable) return + const { index, tdIndex, trIndex } = positionContext + const originalElementList = this.draw.getOriginalElementList() + const element = originalElementList[index!] + const curTrList = element.trList! + const curTr = curTrList[trIndex!]! + const curTd = curTr.tdList[tdIndex!] + if (curTd.rowspan === 1 && curTd.colspan === 1) return + // 设置跨列 + if (curTd.colspan > 1) { + for (let c = 1; c < curTd.colspan; c++) { + const tdId = getUUID() + curTr.tdList.splice(tdIndex! + c, 0, { + id: tdId, + rowspan: 1, + colspan: 1, + value: [{ + value: ZERO, + size: 16, + tableId: element.id, + trId: curTr.id, + tdId + }] + }) + } + curTd.colspan = 1 + } + // 设置跨行 + if (curTd.rowspan > 1) { + for (let c = 1; c < curTd.rowspan; c++) { + const tr = curTrList[trIndex! + c] + const tdId = getUUID() + tr.tdList.splice(curTd.colIndex!, 0, { + id: tdId, + rowspan: 1, + colspan: 1, + value: [{ + value: ZERO, + size: 16, + tableId: element.id, + trId: tr.id, + tdId + }] + }) + } + curTd.rowspan = 1 } + // 重新渲染 + const { startIndex, endIndex } = this.range.getRange() + this.range.setRange(startIndex, endIndex) + this.draw.render({ + curIndex: endIndex + }) + const position = this.position.getOriginalPositionList() + this.tableTool.render(element, position[index!]) } public hyperlink(payload: IElement) { diff --git a/src/editor/core/contextmenu/ContextMenu.ts b/src/editor/core/contextmenu/ContextMenu.ts index adac4fc..9f128c2 100644 --- a/src/editor/core/contextmenu/ContextMenu.ts +++ b/src/editor/core/contextmenu/ContextMenu.ts @@ -81,7 +81,7 @@ export class ContextMenu { } private _getContext(): IContextMenuContext { - const { startIndex, endIndex } = this.range.getRange() + const { isCrossRowCol: crossRowCol, startIndex, endIndex } = this.range.getRange() // 是否存在焦点 const editorTextFocus = !!(~startIndex || ~endIndex) // 是否存在选区 @@ -89,7 +89,14 @@ export class ContextMenu { // 是否在表格内 const positionContext = this.position.getPositionContext() const isInTable = positionContext.isTable - return { editorHasSelection, editorTextFocus, isInTable } + // 是否存在跨行/列 + const isCrossRowCol = isInTable && !!crossRowCol + return { + editorHasSelection, + editorTextFocus, + isInTable, + isCrossRowCol + } } private _createContextMenuContainer(): HTMLDivElement { diff --git a/src/editor/core/contextmenu/menus/tableMenus.ts b/src/editor/core/contextmenu/menus/tableMenus.ts index 9da7bdc..bafe258 100644 --- a/src/editor/core/contextmenu/menus/tableMenus.ts +++ b/src/editor/core/contextmenu/menus/tableMenus.ts @@ -19,21 +19,24 @@ export const tableMenus: IRegisterContextMenu[] = [ callback: (command: Command) => { command.executeInsertTableTopRow() } - }, { + }, + { name: '下方插入1行', icon: 'insert-bottom-row', when: () => true, callback: (command: Command) => { command.executeInsertTableBottomRow() } - }, { + }, + { name: '左侧插入1列', icon: 'insert-left-col', when: () => true, callback: (command: Command) => { command.executeInsertTableLeftCol() } - }, { + }, + { name: '右侧插入1列', icon: 'insert-right-col', when: () => true, @@ -65,7 +68,8 @@ export const tableMenus: IRegisterContextMenu[] = [ callback: (command: Command) => { command.executDeleteTableCol() } - }, { + }, + { name: '删除整个表格', icon: 'delete-table', when: () => true, @@ -74,5 +78,25 @@ export const tableMenus: IRegisterContextMenu[] = [ } } ] + }, + { + name: '合并单元格', + icon: 'merge-cell', + when: (payload) => { + return payload.isCrossRowCol + }, + callback: (command: Command) => { + command.executMergeTableCell() + } + }, + { + name: '取消合并', + icon: 'merge-cancel-cell', + when: (payload) => { + return payload.isInTable + }, + callback: (command: Command) => { + command.executCancelMergeTableCell() + } } ] \ No newline at end of file diff --git a/src/editor/core/draw/Draw.ts b/src/editor/core/draw/Draw.ts index b1c0a48..83882ad 100644 --- a/src/editor/core/draw/Draw.ts +++ b/src/editor/core/draw/Draw.ts @@ -475,6 +475,7 @@ export class Draw { private _drawRow(ctx: CanvasRenderingContext2D, payload: IDrawRowPayload): IDrawRowResult { const { positionList, rowList, pageNo, startX, startY, startIndex, innerWidth } = payload const { scale, tdPadding } = this.options + const { isCrossRowCol, tableId } = this.range.getRange() const tdGap = tdPadding * 2 let x = startX let y = startY @@ -499,6 +500,7 @@ export class Draw { width: 0, height: 0 } + let tableRangeElement: IElement | null = null for (let j = 0; j < curRow.elementList.length; j++) { const element = curRow.elementList[j] const metrics = element.metrics @@ -527,6 +529,11 @@ export class Draw { this.textParticle.complete() this.imageParticle.render(ctx, element, x, y + offsetY) } else if (element.type === ElementType.TABLE) { + if (isCrossRowCol) { + rangeRecord.x = x + rangeRecord.y = y + tableRangeElement = element + } this.tableParticle.render(ctx, element, x, y) } else if (element.type === ElementType.HYPERLINK) { this.textParticle.complete() @@ -602,6 +609,9 @@ export class Draw { const { x, y, width, height } = rangeRecord this.range.render(ctx, x, y, width, height) } + if (isCrossRowCol && tableRangeElement && tableRangeElement.id === tableId) { + this.tableParticle.drawRange(ctx, tableRangeElement, x, y) + } x = startX y += curRow.height } diff --git a/src/editor/core/draw/particle/table/TableParticle.ts b/src/editor/core/draw/particle/table/TableParticle.ts index c1a5fd5..f7cc11a 100644 --- a/src/editor/core/draw/particle/table/TableParticle.ts +++ b/src/editor/core/draw/particle/table/TableParticle.ts @@ -1,12 +1,15 @@ -import { IElement } from "../../../.." +import { ElementType, IElement } from "../../../.." import { IEditorOption } from "../../../../interface/Editor" +import { RangeManager } from "../../../range/RangeManager" import { Draw } from "../../Draw" export class TableParticle { + private range: RangeManager private options: Required constructor(draw: Draw) { + this.range = draw.getRange() this.options = draw.getOptions() } @@ -20,16 +23,6 @@ export class TableParticle { ctx.stroke() } - // @ts-ignore - private _drawRange(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number) { - const { rangeAlpha, rangeColor } = this.options - ctx.save() - ctx.globalAlpha = rangeAlpha - ctx.fillStyle = rangeColor - ctx.fillRect(x, y, width, height) - ctx.restore() - } - public computeRowColInfo(element: IElement) { const { colgroup, trList } = element if (!colgroup || !trList) return @@ -60,7 +53,7 @@ export class TableParticle { if (pTdX > x) break if (pTd.x === x && pTdY + pTdHeight > y) { x += pTdWidth - offsetXIndex += 1 + offsetXIndex += pTd.colspan } } } @@ -122,6 +115,47 @@ export class TableParticle { } } + public drawRange(ctx: CanvasRenderingContext2D, element: IElement, startX: number, startY: number) { + const { scale, rangeAlpha, rangeColor } = this.options + const { type, trList } = element + if (!trList || type !== ElementType.TABLE) return + const { isCrossRowCol, startTdIndex, endTdIndex, startTrIndex, endTrIndex } = this.range.getRange() + // 存在跨行/列 + if (!isCrossRowCol) return + let startTd = trList[startTrIndex!].tdList[startTdIndex!] + let endTd = trList[endTrIndex!].tdList[endTdIndex!] + // 交换起始位置 + if (startTd.x! > endTd.x! || startTd.y! > endTd.y!) { + [startTd, endTd] = [endTd, startTd] + } + const startColIndex = startTd.colIndex! + const endColIndex = endTd.colIndex! + (endTd.colspan - 1) + const startRowIndex = startTd.rowIndex! + const endRowIndex = endTd.rowIndex! + (endTd.rowspan - 1) + ctx.save() + for (let t = 0; t < trList.length; t++) { + const tr = trList[t] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const tdColIndex = td.colIndex! + const tdRowIndex = td.rowIndex! + if ( + tdColIndex >= startColIndex && tdColIndex <= endColIndex + && tdRowIndex >= startRowIndex && tdRowIndex <= endRowIndex + ) { + const x = td.x! * scale + const y = td.y! * scale + const width = td.width! * scale + const height = td.height! * scale + ctx.globalAlpha = rangeAlpha + ctx.fillStyle = rangeColor + ctx.fillRect(x + startX, y + startY, width, height) + } + } + } + ctx.restore() + } + public render(ctx: CanvasRenderingContext2D, element: IElement, startX: number, startY: number) { const { colgroup, trList } = element if (!colgroup || !trList) return diff --git a/src/editor/core/event/CanvasEvent.ts b/src/editor/core/event/CanvasEvent.ts index 2ee70d5..95f1f4b 100644 --- a/src/editor/core/event/CanvasEvent.ts +++ b/src/editor/core/event/CanvasEvent.ts @@ -5,6 +5,7 @@ import { ElementStyleKey } from "../../dataset/enum/ElementStyle" import { MouseEventButton } from "../../dataset/enum/Event" import { KeyMap } from "../../dataset/enum/Keymap" import { IElement } from "../../interface/Element" +import { ICurrentPosition } from "../../interface/Position" import { writeTextByElementList } from "../../utils/clipboard" import { Cursor } from "../cursor/Cursor" import { Draw } from "../draw/Draw" @@ -19,7 +20,7 @@ export class CanvasEvent { private isAllowDrag: boolean private isCompositing: boolean - private mouseDownStartIndex: number + private mouseDownStartPosition: ICurrentPosition | null private draw: Draw private pageContainer: HTMLDivElement @@ -35,7 +36,7 @@ export class CanvasEvent { constructor(draw: Draw) { this.isAllowDrag = false this.isCompositing = false - this.mouseDownStartIndex = 0 + this.mouseDownStartPosition = null this.pageContainer = draw.getPageContainer() this.pageList = draw.getPageList() @@ -81,7 +82,7 @@ export class CanvasEvent { } public mousemove(evt: MouseEvent) { - if (!this.isAllowDrag) return + if (!this.isAllowDrag || !this.mouseDownStartPosition) return const target = evt.target as HTMLDivElement const pageIndex = target.dataset.index // 设置pageNo @@ -93,16 +94,42 @@ export class CanvasEvent { x: evt.offsetX, y: evt.offsetY }) - const { index, isTable, tdValueIndex } = positionResult + const { + index, + isTable, + tdValueIndex, + tdIndex, + trIndex, + tableId + } = positionResult + const { + index: startIndex, + isTable: startIsTable, + tdIndex: startTdIndex, + trIndex: startTrIndex + } = this.mouseDownStartPosition let endIndex = isTable ? tdValueIndex! : index - let end = ~endIndex ? endIndex : 0 - // 开始位置 - let start = this.mouseDownStartIndex - if (start > end) { - [start, end] = [end, start] + // 判断是否是表格跨行/列 + if (isTable && startIsTable && (tdIndex !== startTdIndex || trIndex !== startTrIndex)) { + this.range.setRange( + endIndex, + endIndex, + tableId, + startTdIndex, + tdIndex, + startTrIndex, + trIndex + ) + } else { + let end = ~endIndex ? endIndex : 0 + // 开始位置 + let start = startIndex + if (start > end) { + [start, end] = [end, start] + } + if (start === end) return + this.range.setRange(start, end) } - if (start === end) return - this.range.setRange(start, end) // 绘制 this.draw.render({ isSubmitHistory: false, @@ -147,7 +174,10 @@ export class CanvasEvent { tableId }) // 记录选区开始位置 - this.mouseDownStartIndex = isTable ? tdValueIndex! : index + this.mouseDownStartPosition = { + ...positionResult, + index: isTable ? tdValueIndex! : index + } // 绘制 const isDirectHitImage = isDirectHit && isImage if (~index) { diff --git a/src/editor/core/range/RangeManager.ts b/src/editor/core/range/RangeManager.ts index dab76f2..51325eb 100644 --- a/src/editor/core/range/RangeManager.ts +++ b/src/editor/core/range/RangeManager.ts @@ -35,9 +35,23 @@ export class RangeManager { return elementList.slice(startIndex + 1, endIndex + 1) } - public setRange(startIndex: number, endIndex: number) { + public setRange( + startIndex: number, + endIndex: number, + tableId?: string, + startTdIndex?: number, + endTdIndex?: number, + startTrIndex?: number, + endTrIndex?: number + ) { this.range.startIndex = startIndex this.range.endIndex = endIndex + this.range.tableId = tableId + this.range.startTdIndex = startTdIndex + this.range.endTdIndex = endTdIndex + this.range.startTrIndex = startTrIndex + this.range.endTrIndex = endTrIndex + this.range.isCrossRowCol = !!(startTdIndex || endTdIndex || startTrIndex || endTrIndex) } public setRangeStyle() { @@ -50,6 +64,7 @@ export class RangeManager { curElementList = [elementList[index]] } const curElement = curElementList[curElementList.length - 1] + if (!curElement) return // 富文本 const font = curElement.font || this.options.defaultFont let bold = !~curElementList.findIndex(el => !el.bold) diff --git a/src/editor/interface/Range.ts b/src/editor/interface/Range.ts index 1412917..1382d77 100644 --- a/src/editor/interface/Range.ts +++ b/src/editor/interface/Range.ts @@ -1,4 +1,10 @@ export interface IRange { startIndex: number; endIndex: number; + isCrossRowCol?: boolean; + tableId?: string; + startTdIndex?: number; + endTdIndex?: number; + startTrIndex?: number; + endTrIndex?: number; } \ No newline at end of file diff --git a/src/editor/interface/contextmenu/ContextMenu.ts b/src/editor/interface/contextmenu/ContextMenu.ts index 419c766..9f96eff 100644 --- a/src/editor/interface/contextmenu/ContextMenu.ts +++ b/src/editor/interface/contextmenu/ContextMenu.ts @@ -2,6 +2,7 @@ export interface IContextMenuContext { editorHasSelection: boolean; editorTextFocus: boolean; isInTable: boolean; + isCrossRowCol: boolean; } export interface IRegisterContextMenu {