feat:merge cell & cancel merge cell

pr675
黄云飞 4 years ago
parent e383896ca3
commit 98534a2d9e

@ -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;

@ -0,0 +1 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="#3D4757" fill-rule="evenodd"><path d="M6 1v1H2v11h4v1H2a1 1 0 01-1-1V2a1 1 0 011-1h4zm3 0h4a1 1 0 011 1v11a1 1 0 01-1 1H9v-1h4V2H9V1z"/><path fill-rule="nonzero" d="M6 1h1v4H6zm2 0h1v4H8z"/><path d="M3 7.5L5 6v3zm9 0L10 6v3z"/><path d="M4 7h3v1H4zm4 0h3v1H8z"/><path fill-rule="nonzero" d="M8 10h1v4H8zm-2 0h1v4H6z"/></g></svg>

After

Width:  |  Height:  |  Size: 399 B

@ -0,0 +1 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="#3D4757" fill-rule="evenodd"><path d="M6 1v1H2v11h4v1H2a1 1 0 01-1-1V2a1 1 0 011-1h4zm3 0h4a1 1 0 011 1v11a1 1 0 01-1 1H9v-1h4V2H9V1z"/><path fill-rule="nonzero" d="M6 1h1v4H6zm2 0h1v4H8z"/><path d="M8 7.5L10 6v3zm-1 0L5 6v3z"/><path d="M9 7h3v1H9zM3 7h3v1H3z"/><path fill-rule="nonzero" d="M8 10h1v4H8zm-2 0h1v4H6z"/></g></svg>

After

Width:  |  Height:  |  Size: 400 B

@ -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)
}

@ -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) {

@ -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 {

@ -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()
}
}
]

@ -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
}

@ -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<IEditorOption>
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

@ -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) {

@ -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)

@ -1,4 +1,10 @@
export interface IRange {
startIndex: number;
endIndex: number;
isCrossRowCol?: boolean;
tableId?: string;
startTdIndex?: number;
endTdIndex?: number;
startTrIndex?: number;
endTrIndex?: number;
}

@ -2,6 +2,7 @@ export interface IContextMenuContext {
editorHasSelection: boolean;
editorTextFocus: boolean;
isInTable: boolean;
isCrossRowCol: boolean;
}
export interface IRegisterContextMenu {

Loading…
Cancel
Save