Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix copy table related issues #169

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

那如果table里面还有嵌套的table呢?这里是只考虑了一层嵌套关系吗?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

对,满足现有需求够用了

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里太hack了,其实我没太看懂这里把合并行和列拉平的用意

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里需要补充原始html缺失的单元格来满足当前的数据结构,这个缺失的单元格需要加一个标识让后续展示的时候有一个可识别的内容来告诉editablejs这部分需要隐藏展示,也就是对应后面的这段隐藏逻辑,
image

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

或者你那有好的思路,也可以参考下

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

或者你那有好的思路,也可以参考下

我提交了一版,整体包含了我的思路,我这儿没有测试数据。你可以先帮忙测试看看。

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

或者你那有好的思路,也可以参考下

我提交了一版,整体包含了我的思路,我这儿没有测试数据。你可以先帮忙测试看看。
我测试了基础的表格复制粘贴也不行,没有rowspan或者colspan的无法复制,

或者你那有好的思路,也可以参考下

我提交了一版,整体包含了我的思路,我这儿没有测试数据。你可以先帮忙测试看看。

我这运行报错了,复制部分表格的时候slate报错了,复制全部表格的时候,表格样式丢失了

部分表格:
image

全部表格:
image
image

测试数据参考截图,也可以多增加一些colspan 和rowspan,在里面输入回车看是否可行

}
}
}
}
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) {
big-camel marked this conversation as resolved.
Show resolved Hide resolved
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