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

功能: Table 支持 Cell 合并 #31

Merged
merged 6 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all 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: 3 additions & 3 deletions packages/basic-modules/__tests__/image/render-elem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ describe('image render elem', () => {
src,
alt: 'logo',
href,
style: { width: '100', height: '80' },
style: { width: '100' },
children: [{ text: '' }], // void node 必须包含一个空 text
}

const containerVnode = renderImageConf.renderElem(elem, null, editor) as any
expect(containerVnode.sel).toBe('div')
expect(containerVnode.data.className).toBe('w-e-image-container')
expect(containerVnode.data.style.width).toBe('100')
expect(containerVnode.data.style.height).toBe('80')
// expect(containerVnode.data.style.height).toBe('80')

const imageVnode = containerVnode.children[0] as any
expect(imageVnode.sel).toBe('img')
Expand All @@ -57,7 +57,7 @@ describe('image render elem', () => {
src,
alt: 'logo',
href,
style: { width: '100', height: '80' },
style: { width: '100' },
children: [{ text: '' }], // void node 必须包含一个空 text
}

Expand Down
38 changes: 30 additions & 8 deletions packages/basic-modules/src/modules/image/render-elem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ function renderContainer(

const style: any = {}
if (width) style.width = width
if (height) style.height = height
/** 不强制设置高度 */
// if (height) style.height = height

const containerId = genContainerId(editor, elemNode)

Expand All @@ -60,6 +61,7 @@ function renderResizeContainer(
let originalX = 0
let originalWith = 0
let originalHeight = 0
let maxWidth = 0 // 最大宽度
let revers = false // 是否反转。如向右拖拽 right-top 需增加宽度(非反转),但向右拖拽 left-top 则需要减少宽度(反转)
let $container: Dom7Array | null = null

Expand All @@ -72,11 +74,12 @@ function renderResizeContainer(
/**
* 初始化。监听事件,记录原始数据
*/
function init(clientX: number) {
function init(clientX: number, parentNodeWidth: number) {
$container = getContainerElem()

// 记录当前 x 坐标值
originalX = clientX
maxWidth = parentNodeWidth

// 记录 img 原始宽高
const $img = $container.find('img')
Expand Down Expand Up @@ -104,12 +107,17 @@ function renderResizeContainer(
const newWidth = originalWith + gap
const newHeight = originalHeight * (newWidth / originalWith) // 根据 width ,按比例计算 height

/**
* 图片有左右3px margin
*/
if (newWidth > maxWidth - 6) return // 超过最大宽度,不处理

// 实时修改 img 宽高 -【注意】这里只修改 DOM ,mouseup 时再统一不修改 node
if ($container == null) return
if (newWidth <= 15 || newHeight <= 15) return // 最小就是 15px

$container.css('width', `${newWidth}px`)
$container.css('height', `${newHeight}px`)
// $container.css('height', `${newHeight}px`)
}, 100)

function onMouseup(e: Event) {
Expand All @@ -118,14 +126,14 @@ function renderResizeContainer(

if ($container == null) return
const newWidth = $container.width().toFixed(2)
const newHeight = $container.height().toFixed(2)
// const newHeight = $container.height().toFixed(2)

// 修改 node
const props: Partial<ImageElement> = {
style: {
...(elemNode as ImageElement).style,
width: `${newWidth}px`,
height: `${newHeight}px`,
// height: `${newHeight}px`,
},
}
Transforms.setNodes(editor, props, { at: DomEditor.findPath(editor, elemNode) })
Expand All @@ -136,7 +144,7 @@ function renderResizeContainer(

const style: any = {}
if (width) style.width = width
if (height) style.height = height
// if (height) style.height = height
// style.boxShadow = '0 0 0 1px #B4D5FF' // 自定义 selected 样式,因为有拖拽触手

return (
Expand All @@ -157,7 +165,21 @@ function renderResizeContainer(
if ($target.hasClass('left-top') || $target.hasClass('left-bottom')) {
revers = true // 反转。向右拖拽,减少宽度
}
init(e.clientX) // 初始化

// 获取 image 父容器宽度
const parentNode = DomEditor.getParentNode(editor, elemNode)
if (parentNode == null) return
const parentNodeDom = DomEditor.toDOMNode(editor, parentNode)
const rect = parentNodeDom.getBoundingClientRect()
// 获取元素的计算样式
const style = window.getComputedStyle(parentNodeDom)
// 获取左右 padding 和 border 的宽度
const paddingLeft = parseFloat(style.paddingLeft)
const paddingRight = parseFloat(style.paddingRight)
const borderLeft = parseFloat(style.borderLeftWidth)
const borderRight = parseFloat(style.borderRightWidth)

init(e.clientX, rect.width - paddingLeft - paddingRight - borderLeft - borderRight) // 初始化
},
}}
>
Expand All @@ -177,7 +199,7 @@ function renderImage(elemNode: SlateElement, children: VNode[] | null, editor: I
const { width = '', height = '' } = style
const selected = DomEditor.isNodeSelected(editor, elemNode) // 图片是否选中

const imageStyle: any = {}
const imageStyle: any = { maxWidth: '100%' }
if (width) imageStyle.width = '100%'
if (height) imageStyle.height = '100%'

Expand Down
11 changes: 8 additions & 3 deletions packages/core/src/editor/dom-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,14 @@ export const DomEditor = {

return (
// 祖先节点中包括 data-slate-editor 属性,即 textarea
targetEl.closest(`[data-slate-editor]`) === editorEl &&
// 通过参数 editable 控制开启是否验证是可编辑元素或零宽字符
(!editable || targetEl.isContentEditable || !!targetEl.getAttribute('data-slate-zero-width'))
(targetEl.closest(`[data-slate-editor]`) === editorEl &&
// 通过参数 editable 控制开启是否验证是可编辑元素或零宽字符
// 补全 data-slate-string 可参考本文代码
//(data-slate-zero-width、data-slate-string)判断一起出现,唯独此处欠缺,补全
(!editable ||
targetEl.isContentEditable ||
!!targetEl.getAttribute('data-slate-zero-width'))) ||
!!targetEl.getAttribute('data-slate-string')
)
},

Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/text-area/syncSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ export function DOMSelectionToEditor(textarea: TextArea, editor: IDomEditor) {
})
Transforms.select(editor, range)
} else {
Transforms.deselect(editor)
// 禁用此行,让光标选区继续生效
// Transforms.deselect(editor)
}
}
3 changes: 3 additions & 0 deletions packages/editor/src/init-default-config/config/hoverbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ const COMMON_HOVERBAR_KEYS = {
'insertTableCol',
'deleteTableCol',
'deleteTable',
/** 注册单元格合并 拆分 */
'mergeTableCell',
'splitTableCell',
],
},
divider: {
Expand Down
10 changes: 7 additions & 3 deletions packages/table-module/__tests__/elem-to-html.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe('TableModule module', () => {
.mockReturnValueOnce({ type: 'table', children: [{ text: '' }] } as Ancestor)

const res = tableCellToHtmlConf.elemToHtml(element, '<span>123</span>')
expect(res).toBe('<td colSpan="1" rowSpan="1" width="auto"><span>123</span></td>')
expect(res).toBe('<td colSpan="1" rowSpan="1" width="auto" style=""><span>123</span></td>')
})

test('tableRowToHtmlConf should return object that include "type" and "elemToHtml" property', () => {
Expand Down Expand Up @@ -87,7 +87,9 @@ describe('TableModule module', () => {
children: [],
}
const res = tableToHtmlConf.elemToHtml(element, '<tr><td>123</td></tr>')
expect(res).toBe('<table style="width: auto;"><tbody><tr><td>123</td></tr></tbody></table>')
expect(res).toBe(
'<table style="width: auto;table-layout: fixed;"><tbody><tr><td>123</td></tr></tbody></table>'
)
})

test('tableToHtmlConf should return html table string with full width style if element is set fullWith value true', () => {
Expand All @@ -97,7 +99,9 @@ describe('TableModule module', () => {
children: [],
}
const res = tableToHtmlConf.elemToHtml(element, '<tr><td>123</td></tr>')
expect(res).toBe('<table style="width: 100%;"><tbody><tr><td>123</td></tr></tbody></table>')
expect(res).toBe(
'<table style="width: 100%;table-layout: fixed;"><tbody><tr><td>123</td></tr></tbody></table>'
)
})
})
})
10 changes: 10 additions & 0 deletions packages/table-module/__tests__/menu/delete-col.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import DeleteCol from '../../src/module/menu/DeleteCol'
import createEditor from '../../../../tests/utils/create-editor'
import { DEL_COL_SVG } from '../../src/constants/svg'
import * as utils from '../../src/utils'
import locale from '../../src/locale/zh-CN'
import * as slate from 'slate'
import * as core from '@wangeditor-next/core'

jest.mock('../../src/utils', () => ({
filledMatrix: jest.fn(),
}));
const mockedUtils = utils as jest.Mocked<typeof utils>;

function setEditorSelection(
editor: core.IDomEditor,
selection: slate.Selection = {
Expand Down Expand Up @@ -139,6 +145,10 @@ describe('Table Module Delete Col Menu', () => {
}
jest.spyOn(slate.Editor, 'nodes').mockReturnValue(fn())
jest.spyOn(core.DomEditor, 'findPath').mockImplementation(() => [0, 1] as slate.Path)

mockedUtils.filledMatrix.mockImplementation(() => {
return [[[[{ "type": "table-cell", "children": [{ "text": "" }], "isHeader": false }, [0, 0, 0]], { "rtl": 1, "ltr": 1, "ttb": 1, "btt": 1 }], [[{ "type": "table-cell", "children": [{ "text": "" }], "isHeader": false }, [0, 0, 1]], { "rtl": 1, "ltr": 1, "ttb": 1, "btt": 1 }]], [[[{ "type": "table-cell", "children": [{ "text": "" }] }, [0, 1, 0]], { "rtl": 1, "ltr": 1, "ttb": 1, "btt": 1 }], [[{ "type": "table-cell", "children": [{ "text": "" }] }, [0, 1, 1]], { "rtl": 1, "ltr": 1, "ttb": 1, "btt": 1 }]]]
});
const removeNodesFn = jest.fn()
jest.spyOn(slate.Transforms, 'removeNodes').mockImplementation(removeNodesFn)

Expand Down
13 changes: 11 additions & 2 deletions packages/table-module/__tests__/menu/delete-row.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import DeleteRow from '../../src/module/menu/DeleteRow'
import createEditor from '../../../../tests/utils/create-editor'
import { DEL_ROW_SVG } from '../../src/constants/svg'
import * as utils from '../../src/utils'
import locale from '../../src/locale/zh-CN'
import * as slate from 'slate'
import * as core from '@wangeditor-next/core'

jest.mock('../../src/utils', () => ({
filledMatrix: jest.fn(),
}));
const mockedUtils = utils as jest.Mocked<typeof utils>;

function setEditorSelection(
editor: core.IDomEditor,
selection: slate.Selection = {
Expand Down Expand Up @@ -134,7 +140,7 @@ describe('Table Module Delete Row Menu', () => {
],
}))

const path = [0, 1]
const path = [0, 0, 0]
const fn = function* a() {
yield [
{
Expand All @@ -144,7 +150,10 @@ describe('Table Module Delete Row Menu', () => {
path,
] as slate.NodeEntry<slate.Element>
}
jest.spyOn(slate.Editor, 'nodes').mockReturnValue(fn())
jest.spyOn(slate.Editor, 'nodes').mockImplementation(() => fn())
mockedUtils.filledMatrix.mockImplementation(() => {
return [[[[{ "type": "table-cell", "children": [{ "text": "" }], "isHeader": false }, [0, 0, 0]], { "rtl": 1, "ltr": 1, "ttb": 1, "btt": 1 }], [[{ "type": "table-cell", "children": [{ "text": "" }], "isHeader": false }, [0, 0, 1]], { "rtl": 1, "ltr": 1, "ttb": 1, "btt": 1 }]], [[[{ "type": "table-cell", "children": [{ "text": "" }] }, [0, 1, 0]], { "rtl": 1, "ltr": 1, "ttb": 1, "btt": 1 }], [[{ "type": "table-cell", "children": [{ "text": "" }] }, [0, 1, 1]], { "rtl": 1, "ltr": 1, "ttb": 1, "btt": 1 }]]]
});
const removeNodesFn = jest.fn()
jest.spyOn(slate.Transforms, 'removeNodes').mockImplementation(removeNodesFn)

Expand Down
13 changes: 11 additions & 2 deletions packages/table-module/__tests__/menu/insert-col.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import InsertCol from '../../src/module/menu/InsertCol'
import createEditor from '../../../../tests/utils/create-editor'
import { ADD_COL_SVG } from '../../src/constants/svg'
import * as utils from '../../src/utils'
import locale from '../../src/locale/zh-CN'
import * as slate from 'slate'
import * as core from '@wangeditor-next/core'

jest.mock('../../src/utils', () => ({
filledMatrix: jest.fn(),
}));
const mockedUtils = utils as jest.Mocked<typeof utils>;

function setEditorSelection(
editor: core.IDomEditor,
selection: slate.Selection = {
Expand Down Expand Up @@ -181,13 +187,16 @@ describe('Table Module Insert Col Menu', () => {
jest.spyOn(core.DomEditor, 'findPath').mockReturnValue([0, 1])
const insertNodesFn = jest.fn()
jest.spyOn(slate.Transforms, 'insertNodes').mockImplementation(insertNodesFn)
mockedUtils.filledMatrix.mockImplementation(() => {
return [[[[{ "type": "table-cell", "children": [{ "text": "" }], "isHeader": false }, [0, 0, 0]], { "rtl": 1, "ltr": 1, "ttb": 1, "btt": 1 }], [[{ "type": "table-cell", "children": [{ "text": "" }], "isHeader": false }, [0, 0, 1]], { "rtl": 1, "ltr": 1, "ttb": 1, "btt": 1 }]], [[[{ "type": "table-cell", "children": [{ "text": "" }] }, [0, 1, 0]], { "rtl": 1, "ltr": 1, "ttb": 1, "btt": 1 }], [[{ "type": "table-cell", "children": [{ "text": "" }] }, [0, 1, 1]], { "rtl": 1, "ltr": 1, "ttb": 1, "btt": 1 }]]]
});

insertColMenu.exec(editor, '')

expect(insertNodesFn).toBeCalledWith(
editor,
{ type: 'table-cell', children: [{ text: '' }] },
{ at: [0, 1] }
{ type: 'table-cell', children: [{ text: '' }], hidden: false },
{ at: [0, 0, 0] }
)
})
})
12 changes: 10 additions & 2 deletions packages/table-module/__tests__/render-elem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,23 @@ describe('table module - render elem', () => {
expect(renderTableConf.type).toBe('table')

const elem = { type: 'table', children: [] }
const containerVnode = renderTableConf.renderElem(elem, null, editor) as any

/**
* 改变了结构,新增外层 DIV
*/
const observerVnode = renderTableConf.renderElem(elem, null, editor) as any
expect(observerVnode.sel).toBe('div')
const containerVnode = observerVnode.children[0] as any
expect(containerVnode.sel).toBe('div')
const tableVnode = containerVnode.children[0] as any
expect(tableVnode.sel).toBe('table')
})

it('render table elem with full with', () => {
const elem = { type: 'table', children: [], width: '100%' }
const containerVnode = renderTableConf.renderElem(elem, null, editor) as any

const observerVnode = renderTableConf.renderElem(elem, null, editor) as any
const containerVnode = observerVnode.children[0] as any
const tableVnode = containerVnode.children[0] as any
expect(tableVnode.data.width).toBe('100%')
})
Expand Down
3 changes: 2 additions & 1 deletion packages/table-module/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@
"peerDependencies": {
"@wangeditor-next/core": "1.x",
"dom7": "^3.0.0",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"nanoid": "^3.2.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
}
}
Loading