Skip to content
6 changes: 6 additions & 0 deletions .changeset/five-melons-travel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@editablejs/deserializer': patch
'@editablejs/plugin-table': patch
---

fix: fix issues about copy tables from other html page which may cause page collapse and wrong enter press respond and missing table header and missing last empty cell.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
node_modules
.pnp
.pnp.js

.history
# testing
coverage

Expand Down
94 changes: 93 additions & 1 deletion packages/deserializer/src/html.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Editor, Descendant, Element, Text, DOMNode, isDOMText } from '@editablejs/models'
import { DOMNode, Descendant, Editor, Element, Text, isDOMText } from '@editablejs/models'

export interface HTMLDeserializerOptions {
element?: Omit<Element, 'children'>
Expand Down Expand Up @@ -79,6 +79,98 @@ export const HTMLDeserializer = {
for (const { transform, options } of transforms) {
HTMLDeserializerEditor.with(transform, options)
}


// handle table cell merging
// 对node的children进行遍历,寻找里面的children,判断children里是否有table,并对table做处理
// 如果有table,那么就对table里的cell进行遍历,判断cell的colspan和rowspan是否为1
// 如果不为1,那么就对cell的colspan和rowspan进行处理,使其都为1
// 如果为1,那么就不做处理
// 如果cell的colspan和rowspan都为1,那么就不做处理
const children = node.children;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child.nodeName === 'TABLE') {
let colCount = 0;
for (let rowIndex = 0; rowIndex < child.rows.length; rowIndex++) {
const row = child.rows[rowIndex];
// 计算第一行的列数,用colspan累加
if (rowIndex === 0) {
for (let cellIndex = 0; cellIndex < row.cells.length; cellIndex++) {
const cell = row.cells[cellIndex];
const colspan = cell.getAttribute('colspan') ?? 1
colCount += colspan - 0;
}
}


for (let cellIndex = 0; cellIndex < row.cells.length; cellIndex++) {
const cell = row.cells[cellIndex];
if (rowIndex === 0 && cell.nodeName === 'TH') {
cell.style.fontWeight = '700';
}
const colspan = cell.getAttribute('colspan') ?? 1
const rowspan = cell.getAttribute('rowspan') ?? 1
if (colspan > 1) {
for (let i = 1; i < colspan; i++) {
const newCell = document.createElement('TD')
// 设置当前newCell的colspan和rowspan都为1
newCell.setAttribute('colspan', '1')
newCell.setAttribute('rowspan', '1')
// 设置当前newCell的文本为displaynone
newCell.textContent = 'displaynone||||||' + rowIndex + '||||||' + cellIndex
row.insertBefore(newCell, cell.nextSibling)
}
}
if (rowspan > 1) {
for (let i = 1; i < rowspan; i++) {
// 获取当前行的下i行
const nextRow = child.rows[rowIndex + i]
// 获取当前行的下i行的第cellIndex个cell
for (let i = 0; i < colspan; i++) {
const newCell = nextRow.insertCell(cellIndex);
// 设置当前newCell的colspan和rowspan都为1
newCell.setAttribute('colspan', '1')
newCell.setAttribute('rowspan', '1')
// 设置当前newCell的文本为displaynone
newCell.textContent = 'displaynone||||||' + rowIndex + '||||||' + cellIndex
}
}
}
}
let cellsLength = row.cells.length;
if (cellsLength < colCount) {
for (let i = 0; i < colCount - cellsLength; i++) {
const newCell = row.insertCell();
// 设置当前newCell的colspan和rowspan都为1
newCell.setAttribute('colspan', '1')
newCell.setAttribute('rowspan', '1')
// 设置当前newCell的文本为空
newCell.textContent = '';
}
}
}
}
// 解析child的innerHTML,并循环遍历内部的所有strike,并增加style:text-decoration:line-through;
if (child.innerHTML.indexOf('<strike') !== -1) {
const strikes = child.getElementsByTagName('strike');
for (let strike of strikes) {
// 将当前的strike元素替换为span元素
const span = document.createElement('span');
span.innerHTML = strike.innerHTML;
// 将 strike 元素的所有样式复制到 span 元素
for (let property of strike.style) {
span.style.setProperty(property, strike.style.getPropertyValue(property));
}
span.style.textDecoration = 'line-through';
// 将当前元素作为兄弟元素插入到strike后面
strike.insertAdjacentElement('afterend', span);
}
while (strikes.length > 0) {
strikes[0].remove();
}
}
}

return HTMLDeserializerEditor.transform(node, options)
},
Expand Down
10 changes: 6 additions & 4 deletions packages/plugins/list/src/task/plugin/with-task-list.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { RenderElementProps, ElementAttributes, Editable, Hotkey } from '@editablejs/editor'
import { Transforms, List } from '@editablejs/models'
import tw, { styled, css, theme } from 'twin.macro'
import { ListStyles, ListLabelStyles, renderList } from '../../styles'
import { Editable, ElementAttributes, Hotkey, RenderElementProps } from '@editablejs/editor'
import { List, Transforms } from '@editablejs/models'
import tw, { css, styled, theme } from 'twin.macro'
import { ListLabelStyles, ListStyles, renderList } from '../../styles'
import { DATA_TASK_CHECKED_KEY, TASK_LIST_KEY } from '../constants'
import { TaskList } from '../interfaces/task-list'
import { TaskListHotkey, TaskListOptions } from '../options'
Expand Down Expand Up @@ -70,6 +70,8 @@ const TaskElement = ({ checked, onChange }: TaskProps) => {
)
}

// 如果不期望任务列表选中后出现下划线,去掉 &[data-task-checked='true'] {下面的 ${tw`line-through`},
// 本次revert,保留原项目设计,后期通过脚本实现调整
const StyledTask = styled(ListStyles)`
&[data-task-checked='true'] {
${tw`line-through`}
Expand Down
53 changes: 43 additions & 10 deletions packages/plugins/table/src/cell/deserializer/html.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { HTMLDeserializerWithTransform } from '@editablejs/deserializer/html'
import { Descendant, isDOMHTMLElement } from '@editablejs/models'
import { TABLE_CELL_KEY } from '../constants'
import { TableCell } from '../interfaces/table-cell'

export const withTableCellHTMLDeserializerTransform: HTMLDeserializerWithTransform = (
next,
serializer,
deserializer,
) => {
return (node, options = {}) => {
const { text } = options
if (isDOMHTMLElement(node) && node.nodeName === 'TD') {
if (isDOMHTMLElement(node) && ['TD', 'TH'].includes(node.nodeName)) {
const children: Descendant[] = []
for (const child of node.childNodes) {
const content = serializer.transform(child, {
const content = deserializer.transform(child, {
text,
matchNewline: true,
})
Expand All @@ -22,12 +21,46 @@ export const withTableCellHTMLDeserializerTransform: HTMLDeserializerWithTransfo
children.push({ children: [{ text: '' }] })
}
const { colSpan, rowSpan } = node as HTMLTableCellElement
const cell: TableCell = {
type: TABLE_CELL_KEY,
children,
colspan: colSpan,
rowspan: rowSpan,
}
// 遍历children,每个子元素再次放入到children中
let ifHiddenCell = false
const spanArray: number[] = []
children.forEach(child => {
// 2024/01/31 10:31:42@需求ID: 产品工作站代码优化@ZhaiCongrui/GW00247400:处理有的带有 链接 的单元格,编辑时回车光标跳到下个单元格的问题
// child 的 type 为 link时,会有此类问题,所以单独处理
if (child.type === 'link') {
const copyChild = { ...child }
Reflect.ownKeys(child).forEach(i => delete child[i])
child.type = 'paragraph'
child.children = []
child.children[0] = copyChild
}
if (child.children === undefined) {
if (child.text.indexOf('displaynone||||||') > -1) {
ifHiddenCell = true
const startRow = child.text.split('||||||')[1]
const startCol = child.text.split('||||||')[2]
spanArray.push(startRow - 0)
spanArray.push(startCol - 0)
}
const tempChild = [{ ...child }]
//把child的所有属性移除
Object.keys(child).forEach(key => delete child[key])
child.children = tempChild
}
})

const cell = ifHiddenCell
? {
type: TABLE_CELL_KEY,
children,
span: spanArray,
}
: {
type: TABLE_CELL_KEY,
children,
colspan: colSpan,
rowspan: rowSpan,
}
return [cell]
}
return next(node, options)
Expand Down
4 changes: 2 additions & 2 deletions packages/plugins/table/src/cell/plugin/with-table-cell.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Editable } from '@editablejs/editor'
import { GridCell, Node } from '@editablejs/models'
import { setOptions, TableCellOptions } from '../options'
import { CellInnerStyles, CellStyles } from '../../components/styles'
import { TableCellOptions, setOptions } from '../options'
import { TableCellEditor } from './table-cell-editor'

export const withTableCell = <T extends Editable>(editor: T, options: TableCellOptions = {}) => {
Expand All @@ -22,7 +22,7 @@ export const withTableCell = <T extends Editable>(editor: T, options: TableCellO
<CellStyles
rowSpan={element.rowspan ?? 1}
colSpan={element.colspan ?? 1}
style={{ ...style, display: element.span ? 'none' : '' }}
style={{ ...style, display: element.span || element.children[0]?.text?.toString().indexOf('displaynone||||||') > -1 ? 'none' : '' }}
{...rest}
>
<CellInnerStyles>{children}</CellInnerStyles>
Expand Down
139 changes: 126 additions & 13 deletions packages/plugins/table/src/components/action.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { cancellablePromise, Editable, useCancellablePromises, Slot } from '@editablejs/editor'
import { Transforms, Grid, Editor } from '@editablejs/models'
import * as React from 'react'
import { Editable, Slot, cancellablePromise, useCancellablePromises } from '@editablejs/editor'
import { Editor, Grid, Transforms } from '@editablejs/models'
import { Icon } from '@editablejs/ui'
import * as React from 'react'
import { TABLE_CELL_KEY } from '../cell/constants'
import { defaultTableMinColWidth } from '../cell/options'
import { TableDrag, useTableDragTo, useTableDragging } from '../hooks/use-drag'
import { TableRow } from '../row'
import { TABLE_ROW_KEY } from '../row/constants'
import { defaultTableMinRowHeight } from '../row/options'
import { RowStore } from '../row/store'
import { useTableOptions } from '../table/options'
import { adaptiveExpandColumnWidthInContainer } from '../table/utils'
import {
ColsInsertIconStyles,
ColsInsertLineStyles,
Expand All @@ -16,15 +25,6 @@ import {
RowsSplitLineStyles,
RowsSplitStyles,
} from './styles'
import { TableDrag, useTableDragging, useTableDragTo } from '../hooks/use-drag'
import { TABLE_CELL_KEY } from '../cell/constants'
import { TableRow } from '../row'
import { TABLE_ROW_KEY } from '../row/constants'
import { useTableOptions } from '../table/options'
import { defaultTableMinColWidth } from '../cell/options'
import { defaultTableMinRowHeight } from '../row/options'
import { adaptiveExpandColumnWidthInContainer } from '../table/utils'
import { RowStore } from '../row/store'

const TYPE_COL = 'col'
const TYPE_ROW = 'row'
Expand Down Expand Up @@ -229,13 +229,126 @@ const SplitActionDefault: React.FC<TableActionProps> = ({
const cancellablePromisesApi = useCancellablePromises()

const handleDragSplitUp = React.useCallback(() => {
if (!dragRef.current) return
const { type: type2 } = dragRef.current
const path = Editable.findPath(editor, table)

if (type2 === TYPE_COL) {
const newGrid = Grid.above(editor, path)
if (!newGrid) return
const { children: rows } = newGrid[0]
let contentHeight = 0
const heightArray: number[] = []
for (let i = 0; i < rows.length; i++) {
const row = rows[i]
const trRow = Editable.toDOMNode(editor, row)
// 2024/01/19 10:40:43 @guoxiaona/GW00234847:遍历trRow,判断所有子元素中rowSpan为1且colSpan为1,且style中的display不为none的子元素
const trRowChildrenArray = Array.from(trRow.children)
let child: any = null
trRowChildrenArray.forEach((item: any) => {
const rowspan = item.rowSpan
const colspan = item.colSpan
const style = item.style
const display = style.display
if (rowspan === 1 && colspan === 1 && display !== 'none') {
child = item
}
})
if (!child) continue
const rect = child.getBoundingClientRect()
contentHeight = Math.max(rect.height, minRowHeight)
heightArray.push(contentHeight)
}
// 2024/01/19 10:41:10 @guoxiaona/GW00234847:heightArray中当前数之前所有数值的和
const heightArrayMapOnlyPrev = heightArray.map((item, index) => {
let sum = 0
for (let i = 0; i < index; i++) {
sum += heightArray[i]
}
return sum
})
// 2024/01/19 10:41:27 @guoxiaona/GW00234847:heightArray中当前数和之前所有数值的和
const heightArrayMapAllPrev = heightArray.map((item, index) => {
let sum = 0
for (let i = 0; i <= index; i++) {
sum += heightArray[i]
}
return sum
})

const cld = Editable.toDOMNode(editor, rows[0]).firstElementChild

// 2024/01/19 10:41:43 @guoxiaona/GW00234847:获取child的祖先节点table所在的节点的父节点的第二个子节点
const t = cld?.closest('table')
const tableParent = t?.parentElement
const tableParentChildrenArray = Array.from(tableParent!.children)
const tableTopBorder = tableParentChildrenArray?.[0]
const tableLeftBorder = tableParentChildrenArray?.[1]
// 2024/01/19 10:42:07 @guoxiaona/GW00234847:获取tableLeftBorder中所有子元素带有属性data-table-row的,并按照该属性值放到一个数组中borderHeightArray
const borderHeightArray: number[] = []
const tableLeftBorderChildrenArray = Array.from(tableLeftBorder?.children)
const tableLeftBorderPerRowArray: any[] = []
tableLeftBorderChildrenArray.forEach((item: any) => {
if (item.dataset.tableRow) {
tableLeftBorderPerRowArray.push(item)
// 2024/01/19 10:42:28 @guoxiaona/GW00234847:需要从item中获取当前style中的height值,并放入borderHeightArray中
const style = item.style
const height = Number(style.height.replace('px', ''))
borderHeightArray.push(height)
}
})
// 2024/01/19 10:42:47 @guoxiaona/GW00234847:检测heightArray和borderHeightArray对应下标的数值相差是否在10(行高大于10)以内,如果是,则不做任何处理,否则更新当前行对应的高度
let ifRowHeightUpdated = false
heightArray.forEach((item, index) => {
const borderHeight = borderHeightArray[index]
const itemNumber = Number(item)
const diff = Math.abs(borderHeight - itemNumber)
// 2024/01/19 10:43:20 @guoxiaona/GW00234847:在这里更新当前行及后面行的高度及top值
if (diff > 10 || ifRowHeightUpdated) {
ifRowHeightUpdated = true
// 2024/01/19 10:43:33 @guoxiaona/GW00234847:调整当前tableLeftBorderPerRowArray[index]的高度为heightArray[index] + 1,top为heightArrayMapOnlyPrev[index]
const currentRow = tableLeftBorderPerRowArray[index]
const currentRowStyle = currentRow.style
currentRowStyle.height = `${itemNumber + 1}px`
currentRowStyle.top = `${heightArrayMapOnlyPrev[index]}px`
// 2024/01/19 10:43:47 @guoxiaona/GW00234847:调整当前tableLeftBorderPerRowArray[index]后面两个兄弟元素的top值为heightArrayMapAllPrev[index] - 1
// 2024/01/19 10:44:00 @guoxiaona/GW00234847:需要重新获取后面两个兄弟元素,这两个兄弟元素没在tableLeftBorderPerRowArray[index]里
const nextSibling = currentRow.nextElementSibling
const nextSiblingStyle = nextSibling.style
nextSiblingStyle.top = `${heightArrayMapAllPrev[index] - 1}px`
const nextNextSibling = nextSibling.nextElementSibling
const nextNextSiblingStyle = nextNextSibling.style
nextNextSiblingStyle.top = `${heightArrayMapAllPrev[index] - 1}px`
}
})
// 2024/01/19 10:44:13 @guoxiaona/GW00234847:如果行高调整过,则需要对应调整列的高度为heightArrayMapAllPrev的最后一个元素的值 + 9
if (ifRowHeightUpdated) {
// 2024/01/19 10:44:25 @guoxiaona/GW00234847:获取tableTopBorder中所有子元素带有属性data-table-col的子元素,并放到一个数组中tableTopBorderPerColArray
const tableTopBorderChildrenArray = Array.from(tableTopBorder?.children)
const tableTopBorderPerColArray: any[] = []
tableTopBorderChildrenArray.forEach((item: any) => {
if (item.dataset.tableCol) {
tableTopBorderPerColArray.push(item)
}
})
// 2024/01/19 10:44:46 @guoxiaona/GW00234847:遍历tableTopBorderPerColArray中每一个元素的兄弟节点的兄弟节点,找到后,将高度调整为heightArrayMapAllPrev的最后一个元素的值 + 9
tableTopBorderPerColArray.forEach(item => {
const nextNextSibling = item.nextElementSibling.nextElementSibling
const nextNextSiblingStyle = nextNextSibling.style
nextNextSiblingStyle.height = `${
heightArrayMapAllPrev[heightArrayMapAllPrev.length - 1] + 9
}px`
})
}
}

dragRef.current = null
isDrag.current = false
setHover(false)
cancellablePromisesApi.clearPendingPromises()
window.removeEventListener('mousemove', handleDragSplitMove)
window.removeEventListener('mouseup', handleDragSplitUp)
}, [cancellablePromisesApi, dragRef, handleDragSplitMove])
}, [cancellablePromisesApi, dragRef, handleDragSplitMove, editor, minRowHeight, table])

const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault()
Expand Down
Loading