@ -102,6 +102,7 @@ import { PUNCTUATION_REG } from '../../dataset/constant/Regular'
import { LineBreakParticle } from './particle/LineBreakParticle'
import { MouseObserver } from '../observer/MouseObserver'
import { LineNumber } from './frame/LineNumber'
import { ITd } from '../../interface/table/Td'
import { PageBorder } from './frame/PageBorder'
export class Draw {
@ -314,6 +315,8 @@ export class Draw {
public isReadonly() {
switch ( this . mode ) {
case EditorMode . DESIGN :
return false
case EditorMode . READONLY :
case EditorMode . PRINT :
return true
@ -325,6 +328,7 @@ export class Draw {
}
public isDisabled() {
if ( this . mode === EditorMode . DESIGN ) return false
const { startIndex , endIndex } = this . range . getRange ( )
const elementList = this . getElementList ( )
if ( startIndex === endIndex ) {
@ -341,6 +345,10 @@ export class Draw {
)
}
public isDesignMode() {
return this . mode === EditorMode . DESIGN
}
public getOriginalWidth ( ) : number {
const { paperDirection , width , height } = this . options
return paperDirection === PaperDirection . VERTICAL ? width : height
@ -698,6 +706,7 @@ export class Draw {
deleteCount : number ,
. . . items : IElement [ ]
) {
const isDesignMode = this . isDesignMode ( )
if ( deleteCount > 0 ) {
// 当最后元素与开始元素列表信息不一致时:清除当前列表信息
const endIndex = start + deleteCount
@ -728,8 +737,9 @@ export class Draw {
while ( deleteIndex >= start ) {
const deleteElement = elementList [ deleteIndex ]
if (
deleteElement ? . control ? . deletable !== false &&
deleteElement ? . title ? . deletable !== false
isDesignMode ||
( deleteElement ? . control ? . deletable !== false &&
deleteElement ? . title ? . deletable !== false )
) {
elementList . splice ( deleteIndex , 1 )
}
@ -1205,6 +1215,7 @@ export class Draw {
table : { tdPadding } ,
defaultTabWidth
} = this . options
let curIndex = payload . curIndex
const defaultBasicRowMarginHeight = this . getDefaultBasicRowMarginHeight ( )
const canvas = document . createElement ( 'canvas' )
const ctx = canvas . getContext ( '2d' ) as CanvasRenderingContext2D
@ -1278,9 +1289,30 @@ export class Draw {
} else if ( element . type === ElementType . TABLE ) {
const tdPaddingWidth = tdPadding [ 1 ] + tdPadding [ 3 ]
const tdPaddingHeight = tdPadding [ 0 ] + tdPadding [ 2 ]
const height = this . getHeight ( )
const marginHeight = this . getMainOuterHeight ( )
const emptyMainHeight = ( height - marginHeight - rowMargin * 2 ) / scale
// 表格分页处理进度: https://github.com/Hufe921/canvas-editor/issues/41
// 查看后续表格是否属于同一个源表格-存在即合并
if ( element . pagingId ) {
// 为当前表格构建一个虚拟表格
const virtualTable = Array . from (
{ length : element.trList ! . length } ,
( ) = > new Array ( element . colgroup ! . length )
) as Array < Array < ITd | undefined | null > >
element . trList ! . forEach ( ( tr , trIndex ) = > {
let tdIndex = 0
tr . tdList . forEach ( td = > {
while ( virtualTable [ trIndex ] [ tdIndex ] === null ) {
tdIndex ++
}
virtualTable [ trIndex ] [ tdIndex ] = td
for ( let i = 1 ; i < td . rowspan ; i ++ ) {
virtualTable [ trIndex + i ] [ tdIndex ] = null
}
tdIndex += td . colspan
} )
} )
let tableIndex = i + 1
let combineCount = 0
while ( tableIndex < elementList . length ) {
@ -1289,8 +1321,65 @@ export class Draw {
const nexTrList = nextElement . trList ! . filter (
tr = > ! tr . pagingRepeat
)
element . trList ! . push ( . . . nexTrList )
element . height ! += nextElement . height !
// 判断后续表格第一行是拆分出来的还是从原表格挪到下一页的
const isNextTrSplit =
element . trList ! [ element . trList ! . length - 1 ] . id ===
nexTrList [ 0 ] . pagingOriginId
let tdIndex = 0
const mergedTds : ITd [ ] = [ ]
nexTrList [ 0 ] . tdList . forEach ( td = > {
let targetTd
// 如果虚拟表格最后一行对应位置有单元格,则其就为目标单元格,否则向上查找
if ( virtualTable [ virtualTable . length - 1 ] [ tdIndex ] ) {
targetTd = virtualTable [ virtualTable . length - 1 ] [ tdIndex ]
} else {
for ( let i = virtualTable . length - 2 ; i >= 0 ; i -- ) {
if ( virtualTable [ i ] [ tdIndex ] ) {
targetTd = virtualTable [ i ] [ tdIndex ]
break
}
}
}
if ( targetTd ) {
if ( targetTd . id === td . pagingOriginId ) {
targetTd . value . push ( . . . td . value )
if ( isNextTrSplit ) {
targetTd . rowspan = targetTd . rowspan + td . rowspan - 1
} else {
targetTd . rowspan = targetTd . rowspan + td . rowspan
mergedTds . push ( td )
}
}
tdIndex += targetTd . colspan
}
} )
nexTrList [ 0 ] . tdList = nexTrList [ 0 ] . tdList . filter ( td = > {
const isNotMerged = mergedTds . every (
mergedTd = > mergedTd . id !== td . id
)
delete td . pagingOriginId
return isNotMerged
} )
while ( nexTrList . length > 0 ) {
const lastTr = element . trList ! [ element . trList ! . length - 1 ]
const nextTr = nexTrList . shift ( ) !
if ( lastTr . id === nextTr . pagingOriginId ) {
lastTr . height += nextTr . pagingOriginHeight || 0
// 更新id关联
lastTr . tdList . forEach ( td = > {
td . value . forEach ( v = > {
v . tdId = td . id
v . trId = lastTr . id
v . tableId = element . id
} )
} )
} else {
nextTr . height = nextTr . pagingOriginHeight || nextTr . height
element . trList ! . push ( nextTr )
}
delete nextTr . pagingOriginHeight
delete nextTr . pagingOriginId
}
tableIndex ++
combineCount ++
} else {
@ -1308,9 +1397,17 @@ export class Draw {
const trList = element . trList !
for ( let t = 0 ; t < trList . length ; t ++ ) {
const tr = trList [ t ]
// 行的minHeight最大只能为页面主内容区域高度( 如果表格前第一行是空行, 由于此时表格不能换页, 因此需减掉空行的高度)
if ( tr . minHeight && tr . minHeight > emptyMainHeight ) {
if ( i === 1 && elementList [ 0 ] . value === ZERO ) {
tr . minHeight = emptyMainHeight - rowList [ 0 ] . height
} else {
tr . minHeight = emptyMainHeight
}
}
for ( let d = 0 ; d < tr . tdList . length ; d ++ ) {
const td = tr . tdList [ d ]
const rowList = this . computeRowList ( {
const { rowList } = this . computeRowList ( {
innerWidth : ( td . width ! - tdPaddingWidth ) * scale ,
elementList : td.value ,
isPagingMode
@ -1385,8 +1482,6 @@ export class Draw {
metrics . boundingBoxAscent = - rowMargin
// 表格分页处理(拆分表格)
if ( isPagingMode ) {
const height = this . getHeight ( )
const marginHeight = this . getMainOuterHeight ( )
let curPagePreHeight = marginHeight
for ( let r = 0 ; r < rowList . length ; r ++ ) {
const row = rowList [ r ]
@ -1400,9 +1495,11 @@ export class Draw {
}
}
// 当前剩余高度是否能容下当前表格第一行(可拆分)的高度,排除掉表头类型
const rowMarginHeight = rowMargin * 2 * scale
const rowMarginHeight = rowMargin * 2
if (
curPagePreHeight + element . trList ! [ 0 ] . height ! + rowMarginHeight >
curPagePreHeight +
element . trList ! [ 0 ] . height ! * scale +
rowMarginHeight >
height ||
( element . pagingIndex !== 0 && element . trList ! [ 0 ] . pagingRepeat )
) {
@ -1412,50 +1509,262 @@ export class Draw {
// 表格高度超过页面高度开始截断行
if ( curPagePreHeight + rowMarginHeight + elementHeight > height ) {
const trList = element . trList !
// 计算需要移除的行数
let deleteStart = 0
let deleteCount = 0
let preTrHeight = 0
// 大于一行时再拆分避免循环
if ( trList . length > 1 ) {
for ( let r = 0 ; r < trList . length ; r ++ ) {
const tr = trList [ r ]
const trHeight = tr . height * scale
if (
curPagePreHeight + rowMarginHeight + preTrHeight + trHeight >
height
) {
// 当前行存在跨行中断-暂时忽略分页
const rowColCount = tr . tdList . reduce (
( pre , cur ) = > pre + cur . colspan ,
0
)
if ( element . colgroup ? . length !== rowColCount ) {
deleteCount = 0
/** 在哪一行截断表格 */
let splitTrIndex = - 1
/** 被截断的行在截断线之前的高度 */
let splitTrPreHeight = 0
// 根据表格行列数构造出一个虚拟表格
const virtualTable = Array . from ( { length : trList.length } , ( ) = >
new Array ( element . colgroup ? . length ) . fill ( undefined )
) as Array < Array < ITd | undefined > >
trList . forEach ( tr = > {
tr . tdList . forEach ( td = > {
virtualTable [ td . rowIndex ! ] [ td . colIndex ! ] = td
} )
} )
// 加上表格上方的行间距
curPagePreHeight += rowMargin
for ( const [ trIndex , tr ] of trList . entries ( ) ) {
// 找到需要截断的行,以及该行在截断线之前的高度
if (
! tr . pagingRepeat && // 标题行不可截断
curPagePreHeight + rowMargin + tr . height * scale > height
) {
splitTrIndex = trIndex
splitTrPreHeight = height - curPagePreHeight - rowMargin // 额外减去被拆分的表格到下方的行间距
break
} else {
curPagePreHeight += tr . height * scale
}
}
if ( splitTrIndex > - 1 ) {
// 构建出一个行作为被截断出来的新行
const cloneTr = new Array ( element . colgroup ! . length ) as Array <
ITd | undefined
>
// 判断目标行是否可截断
const allowSplitTr =
splitTrPreHeight >= trList [ splitTrIndex ] . minHeight ! * scale && // 最小行高区间内不可截断
trList [ splitTrIndex ] . tdList . every (
// 如果截断线穿过该行所有单元格中第一个排版行,此时该行也不可截断
td = > td . rowList ! [ 0 ] . height < splitTrPreHeight
)
if ( ! allowSplitTr ) {
splitTrPreHeight = 0
}
// 遍历虚拟表格中的截断行, 如果某个列的位置有td, 表明截断的是这个td; 如果该位置没有td, 且该位置不是被列合并单元格所占, 此时向上查找实际被截断的td
let preMergedEndIndex = - 1
let splitTrReduceHeight =
trList [ splitTrIndex ] . height - splitTrPreHeight / scale // 被拆分的行减少的高度
virtualTable [ splitTrIndex ] . forEach ( ( td , tdIndex ) = > {
if ( tdIndex <= preMergedEndIndex ) {
return
}
// 虚拟表格中截断行的当前索引位置是否有td
let hasTdAtCurIndex = false
// 找到被截断的td
let splitTd
if ( td ) {
splitTd = td
hasTdAtCurIndex = true
preMergedEndIndex = td . colIndex ! + td . colspan - 1
} else {
for ( let i = splitTrIndex ; i >= 0 ; i -- ) {
if ( virtualTable [ i ] [ tdIndex ] ) {
splitTd = virtualTable [ i ] [ tdIndex ]
preMergedEndIndex =
splitTd ! . colIndex ! + splitTd ! . colspan - 1
break
}
}
}
if ( splitTd ) {
cloneTr [ tdIndex ] = deepClone ( splitTd )
// 如果tr可拆分, 根据截断位置, 将td中的内容拆分到新行中
// 如果tr不可拆分, 但当前位置td是跨行单元格, 同样需要拆分
if ( allowSplitTr || splitTd . rowspan > 1 ) {
cloneTr [ tdIndex ] ! . pagingOriginId =
splitTd . pagingOriginId || splitTd . id
cloneTr [ tdIndex ] ! . id = getUUID ( )
// 计算当前td在截断线之前的高度( 默认为被截断行在截断线之前的高度, 若td跨行, 则加上其他行高度)
let splitTdPreHeight = splitTrPreHeight
for ( let i = splitTd . rowIndex ! ; i < splitTrIndex ; i ++ ) {
splitTdPreHeight += trList [ i ] . height * scale
}
// 根据td中已排版的内容的高度, 计算出哪些内容需要拆分到新行中
let splitTdPreRowHeight = 0
let splitTdRowIndex = - 1
for ( const [ rowIndex , row ] of splitTd . rowList ! . entries ( ) ) {
if (
row . height +
splitTdPreRowHeight +
tdPaddingHeight * scale >
splitTdPreHeight
) {
splitTdRowIndex = rowIndex
break
} else {
splitTdPreRowHeight += row . height
}
}
if ( splitTdRowIndex > - 1 ) {
// 拆分td中的内容
cloneTr [ tdIndex ] ! . rowList =
splitTd . rowList ! . splice ( splitTdRowIndex )
cloneTr [ tdIndex ] ! . value = cloneTr [ tdIndex ] ! . rowList ! . map (
row = > row . elementList
) . flat ( )
splitTd . value = splitTd
. rowList ! . map ( row = > row . elementList )
. flat ( )
// 计算被拆分的单元格高度减少了多少
const splitTdReduceMainHeight = cloneTr [
tdIndex
] ! . rowList ! . reduce ( ( ret , row ) = > {
return ret + row . height / scale
} , 0 )
splitTd . mainHeight ! -= splitTdReduceMainHeight
// 如果目标行可拆分,计算出行减小的高度
if (
allowSplitTr &&
splitTd . rowspan === 1 &&
splitTdRowIndex > 0
) {
splitTrReduceHeight = Math . min (
splitTd . height ! - splitTdPreRowHeight / scale ,
splitTrReduceHeight
)
}
} else {
// 执行到这里表示当前td虽然被截断, 但是内容高度未达到截断线位置
// FIXME: 此时拆分出来的单元格为空, value中不应有元素, 否则会影响单元格可编辑性以及单元格尺寸
cloneTr [ tdIndex ] ! . value = [ { value : ZERO } ]
if ( allowSplitTr && splitTd . rowspan === 1 ) {
splitTrReduceHeight = Math . min (
splitTd . height ! - splitTdPreHeight / scale ,
splitTrReduceHeight
)
}
}
// 更新td的rowspan
if ( hasTdAtCurIndex ) {
cloneTr [ tdIndex ] ! . rowspan = splitTd . rowspan
splitTd . rowspan = 1
} else {
cloneTr [ tdIndex ] ! . rowspan =
splitTd . rowspan - splitTrIndex + splitTd . rowIndex !
splitTd . rowspan = splitTrIndex - splitTd . rowIndex ! + 1
// 如果tr不可截断, 且当前td是跨行单元格, 由于tr会被挪到后续表格, 因此需要将跨行单元格的rowspan减1
if ( ! allowSplitTr && splitTd . rowspan > 1 ) {
splitTd . rowspan --
}
}
}
break
}
} )
// 构造新行
const newTr = deepClone ( trList [ splitTrIndex ] )
newTr . tdList = cloneTr . filter ( td = > td !== undefined ) as ITd [ ]
if ( allowSplitTr ) {
newTr . pagingOriginId = newTr . pagingOriginId || newTr . id
newTr . id = getUUID ( )
trList [ splitTrIndex ] . height -= splitTrReduceHeight
trList [ splitTrIndex ] . tdList . forEach ( td = > {
td . realHeight ! -= splitTrReduceHeight
td . height ! -= splitTrReduceHeight
} )
// 记录拆分出来的新行的原始高度
newTr . pagingOriginHeight = splitTrReduceHeight
newTr . height = Math . max (
splitTrReduceHeight + tdPaddingHeight ,
newTr . minHeight !
)
} else {
// 记录被挪到下一页的行的原始高度
newTr . pagingOriginHeight = newTr . height
}
// 构造出后续表格的行
const cloneTrList = trList . splice ( splitTrIndex + 1 )
cloneTrList . unshift ( newTr )
// 如果当前表格行不可从中间拆分,此行将被挪至后续表格,因此将其从当前表格中移除
if ( ! allowSplitTr ) {
trList . splice ( splitTrIndex , 1 )
}
let totalHeight = 0
// 更新后续表格首行行高
const crossRowTds : ITd [ ] = [ ]
newTr . tdList . forEach ( td = > {
if ( td . rowspan === 1 ) {
const mainHeight =
td . rowList ! . reduce (
( ret , row ) = > ret + row . height / scale ,
0
) + tdPaddingHeight
newTr . height = Math . max ( mainHeight , newTr . height ! )
} else {
deleteStart = r + 1
deleteCount = trList . length - deleteStart
preTrHeight += trHeight
crossRowTds . push ( td )
}
} )
totalHeight += newTr . height
const groupTds = crossRowTds . reduce ( ( ret , td ) = > {
const key = td . rowspan
if ( ret [ key ] ) {
ret [ key ] . push ( td )
} else {
ret [ key ] = [ td ]
}
return ret
} , { } as { [ key : number ] : ITd [ ] } )
const maxRowspan = Math . max (
. . . Object . keys ( groupTds ) . map ( parseInt )
)
// 计算出跨行单元格最大内容高度, 如果高于其rowspan区间的行高之和, 增加rowspan区间中最后一行的行高
for ( let i = 2 ; i <= maxRowspan ; i ++ ) {
const tds = groupTds [ i ]
if ( tds ) {
let maxMainHeight = 0
tds . forEach ( td = > {
const mainHeight =
td . rowList ! . reduce (
( ret , row ) = > ret + row . height / scale ,
0
) + tdPaddingHeight
maxMainHeight = Math . max ( mainHeight , maxMainHeight )
} )
if ( maxMainHeight > totalHeight ) {
// 记录此行原始高度,便于合并时还原行高
cloneTrList [ i - 1 ] . pagingOriginHeight =
cloneTrList [ i - 1 ] . height
cloneTrList [ i - 1 ] . height += maxMainHeight - totalHeight
}
totalHeight += maxMainHeight
}
}
}
if ( deleteCount ) {
const cloneTrList = trList . splice ( deleteStart , deleteCount )
const cloneTrHeight = cloneTrList . reduce (
( pre , cur ) = > pre + cur . height ,
const totalOriginHeight = cloneTrList . reduce (
( ret , tr ) = > ret + ( tr . pagingOriginHeight || tr . height ) ,
0
)
// 追加拆分表格
const pagingId = element . pagingId || getUUID ( )
element . pagingId = pagingId
element . height -= cloneTrHeight
metrics . height -= cloneTrHeight
metrics . boundingBoxDescent -= cloneTrHeight
// 追加拆分表格
element . height -= totalOriginHeight
metrics . height -= totalOriginHeight * scale
metrics . boundingBoxDescent -= totalOriginHeight * scale
const cloneElement = deepClone ( element )
cloneElement . id = getUUID ( )
cloneElement . pagingId = pagingId
cloneElement . pagingIndex = element . pagingIndex ! + 1
this . tableParticle . computeRowColInfo ( element )
// 更新拆分出来的新表格中的id关联信息
cloneTrList . forEach ( tr = > {
tr . tdList . forEach ( td = > {
td . value . forEach ( v = > {
v . tdId = td . id
v . trId = tr . id
v . tableId = cloneElement . id
} )
} )
} )
// 处理分页重复表头
const repeatTrList = trList . filter ( tr = > tr . pagingRepeat )
if ( repeatTrList . length ) {
@ -1464,35 +1773,46 @@ export class Draw {
cloneTrList . unshift ( . . . cloneRepeatTrList )
}
cloneElement . trList = cloneTrList
cloneElement . id = getUUID ( )
this . spliceElementList ( elementList , i + 1 , 0 , cloneElement )
}
}
// 表格经过分页处理-需要处理上下文
// 表格经过分页处理-需要处理上下文 和选区
if ( element . pagingId ) {
const positionContext = this . position . getPositionContext ( )
if ( positionContext . isTable ) {
// 查找光标所在表格索引( 根据trId搜索)
let newPositionContextIndex = - 1
let newPositionContextTrIndex = - 1
let tableIndex = i
while ( tableIndex < elementList . length ) {
const curElement = elementList [ tableIndex ]
if ( curElement . pagingId !== element . pagingId ) break
const trIndex = curElement . trList ! . findIndex (
r = > r . id === positionContext . trId
if (
positionContext . isTable &&
positionContext . tableId === element . id
) {
const trIndex = element . trList ! . findIndex (
r = >
r . pagingOriginId === positionContext . trId ||
r . id === positionContext . trId
)
if ( ~ trIndex ) {
const tr = element . trList ! [ trIndex ]
const tdIndex = tr . tdList ! . findIndex (
d = >
d . pagingOriginId === positionContext . tdId ||
d . id === positionContext . tdId
)
if ( ~ trIndex ) {
newPositionContextIndex = tableIndex
newPositionContextTrIndex = trIndex
break
if ( ~ tdIndex ) {
const td = tr . tdList ! [ tdIndex ]
if ( curIndex !== undefined && curIndex > - 1 ) {
if ( td . value [ curIndex ] ) {
positionContext . index = i
positionContext . trIndex = trIndex
positionContext . tdIndex = tdIndex
positionContext . trId = tr . id
positionContext . tdId = td . id
this . range . setRange ( curIndex , curIndex )
} else {
positionContext . tableId = elementList [ i + 1 ] . id
curIndex -= td . value . length
}
}
}
tableIndex ++
}
if ( ~ newPositionContextIndex ) {
positionContext . index = newPositionContextIndex
positionContext . trIndex = newPositionContextTrIndex
this . position . setPositionContext ( positionContext )
} else {
positionContext . tableId = elementList [ i + 1 ] . id
}
}
}
@ -1739,7 +2059,7 @@ export class Draw {
}
}
}
return rowList
return { rowList , curIndex }
}
private _computePageList ( ) : IRow [ ] [ ] {
@ -2372,11 +2692,14 @@ export class Draw {
}
}
// 行信息
this . rowList = this . computeRowList ( {
const { rowList , curIndex : newIndex } = this . computeRowList ( {
isPagingMode ,
innerWidth ,
elementList : this.elementList
elementList : this.elementList ,
curIndex
} )
this . rowList = rowList
curIndex = newIndex
// 页面信息
this . pageRowList = this . _computePageList ( )
// 位置信息