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 {