From b3ed4d16b231631b67323956af3c52cdebd17ae6 Mon Sep 17 00:00:00 2001 From: hulin <1289739946@qq.com> Date: Mon, 19 Aug 2024 19:57:18 +0800 Subject: [PATCH] add table block (#407) * add table block add table block * update table data field * add advance table * hide contextmenu * update table operation: delete selected columns * latest --- demo/src/pages/Editor/index.tsx | 5 +- .../src/blocks/advanced/blocks/index.ts | 13 +- .../advanced/generateAdvancedTableBlock.tsx | 97 ++++++ .../src/blocks/advanced/index.ts | 2 + .../src/blocks/standard/Table/index.tsx | 14 +- packages/easy-email-core/src/constants.ts | 1 + .../src/utils/HtmlStringToReactNodes.tsx | 84 ++++-- .../src/utils/isTableBlock.ts | 5 + .../src/AttributePanel/AttributePanel.tsx | 2 + .../blocks/AdvancedTable/Operation/index.tsx | 59 ++++ .../blocks/AdvancedTable/Operation/menu.scss | 57 ++++ .../Operation/tableMenuConfig.ts | 279 ++++++++++++++++++ .../Operation/tableOperationMenu.ts | 134 +++++++++ .../AdvancedTable/Operation/tableTool.ts | 254 ++++++++++++++++ .../blocks/AdvancedTable/Operation/type.ts | 24 ++ .../blocks/AdvancedTable/Operation/util.ts | 272 +++++++++++++++++ .../components/blocks/AdvancedTable/index.tsx | 83 ++++++ .../components/blocks/Table/index.tsx | 24 +- .../AttributePanel/components/blocks/index.ts | 2 + 19 files changed, 1370 insertions(+), 41 deletions(-) create mode 100644 packages/easy-email-core/src/blocks/advanced/generateAdvancedTableBlock.tsx create mode 100644 packages/easy-email-editor/src/utils/isTableBlock.ts create mode 100644 packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/index.tsx create mode 100644 packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/menu.scss create mode 100644 packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/tableMenuConfig.ts create mode 100644 packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/tableOperationMenu.ts create mode 100644 packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/tableTool.ts create mode 100644 packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/type.ts create mode 100644 packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/util.ts create mode 100644 packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/index.tsx diff --git a/demo/src/pages/Editor/index.tsx b/demo/src/pages/Editor/index.tsx index 064dbcd9a..1f93df2e1 100644 --- a/demo/src/pages/Editor/index.tsx +++ b/demo/src/pages/Editor/index.tsx @@ -35,7 +35,7 @@ import { pushEvent } from '@demo/utils/pushEvent'; import { FormApi } from 'final-form'; import { UserStorage } from '@demo/utils/user-storage'; -import { AdvancedType, IBlockData, JsonToMjml } from 'easy-email-core'; +import { AdvancedType, BasicType, IBlockData, JsonToMjml } from 'easy-email-core'; import { ExtensionProps, MjmlToJson, StandardLayout } from 'easy-email-extensions'; import { AutoSaveAndRestoreEmail } from '@demo/components/AutoSaveAndRestoreEmail'; @@ -87,6 +87,9 @@ const defaultCategories: ExtensionProps['categories'] = [ { type: AdvancedType.WRAPPER, }, + { + type: AdvancedType.TABLE, + }, ], }, { diff --git a/packages/easy-email-core/src/blocks/advanced/blocks/index.ts b/packages/easy-email-core/src/blocks/advanced/blocks/index.ts index 589d83ca5..d78d4742c 100644 --- a/packages/easy-email-core/src/blocks/advanced/blocks/index.ts +++ b/packages/easy-email-core/src/blocks/advanced/blocks/index.ts @@ -13,10 +13,12 @@ import { IGroup, IColumn, IHero, + ITable, } from '../../standard'; import { AdvancedType, BasicType } from '@core/constants'; import { generateAdvancedContentBlock } from '../generateAdvancedContentBlock'; import { generateAdvancedLayoutBlock } from '../generateAdvancedLayoutBlock'; +import { generateAdvancedTableBlock } from '../generateAdvancedTableBlock'; export const AdvancedText = generateAdvancedContentBlock({ type: AdvancedType.TEXT, @@ -63,6 +65,11 @@ export const AdvancedSocial = generateAdvancedContentBlock({ baseType: BasicType.SOCIAL, }); +export const AdvancedTable = generateAdvancedTableBlock({ + type: AdvancedType.TABLE, + baseType: BasicType.TABLE, +}); + // export const AdvancedWrapper = generateAdvancedLayoutBlock({ @@ -97,9 +104,5 @@ export const AdvancedColumn = generateAdvancedLayoutBlock({ export const AdvancedHero = generateAdvancedLayoutBlock({ type: AdvancedType.HERO, baseType: BasicType.HERO, - validParentType: [ - BasicType.WRAPPER, - AdvancedType.WRAPPER, - BasicType.PAGE, - ], + validParentType: [BasicType.WRAPPER, AdvancedType.WRAPPER, BasicType.PAGE], }); diff --git a/packages/easy-email-core/src/blocks/advanced/generateAdvancedTableBlock.tsx b/packages/easy-email-core/src/blocks/advanced/generateAdvancedTableBlock.tsx new file mode 100644 index 000000000..3cee5ceb3 --- /dev/null +++ b/packages/easy-email-core/src/blocks/advanced/generateAdvancedTableBlock.tsx @@ -0,0 +1,97 @@ +import { BasicType } from '@core/constants'; +import { IBlock, IBlockData } from '@core/typings'; +import { createCustomBlock } from '@core/utils/createCustomBlock'; +import { TemplateEngineManager, createBlock, t } from '@core/utils'; +import { merge } from 'lodash'; +import React from 'react'; +import { IPage, standardBlocks } from '../standard'; +import { BasicBlock } from '@core/components/BasicBlock'; + +export function generateAdvancedTableBlock(option: { + type: string; + baseType: BasicType; +}) { + return createCustomBlock({ + get name() { + return t('Table'); + }, + type: option.type, + validParentType: [BasicType.COLUMN], + create: payload => { + const defaultData: AdvancedTableBlock = { + type: option.type, + data: { + value: { + tableSource: [ + [{ content: 'header1' }, { content: 'header2' }, { content: 'header3' }], + [{ content: 'body1-1' }, { content: 'body1-2' }, { content: 'body1-3' }], + [{ content: 'body2-1' }, { content: 'body2-2' }, { content: 'body2-3' }], + ], + }, + }, + attributes: { + cellBorderColor: '#000000', + cellPadding: '8px', + 'text-align': 'center', + }, + children: [], + }; + return merge(defaultData, payload); + }, + render: params => { + const { data } = params; + const { cellPadding, cellBorderColor } = data.attributes; + const textAlign = data.attributes['text-align']; + const fontStyle = data.attributes['font-style']; + + const content = data.data.value.tableSource + .map((tr, index) => { + const styles = [] as any[]; + if (cellPadding) { + styles.push(`padding: ${cellPadding}`); + } + if (cellBorderColor) { + styles.push(`border: 1px solid ${cellBorderColor}`); + } + const _trString = tr.map( + e => + `${e.content}`, + ); + return `${_trString.join( + '\n', + )}`; + }) + .join('\n'); + + return ( + + {content} + + ); + }, + }); +} + +export interface ITableData { + content: string; + colSpan?: number; + rowSpan?: number; +} + +export type AdvancedTableBlock = IBlockData< + { + cellPadding?: string; + cellBorderColor?: string; + 'font-style'?: string; + 'text-align'?: string; + }, + { + content?: string; + tableSource: ITableData[][]; + } +>; diff --git a/packages/easy-email-core/src/blocks/advanced/index.ts b/packages/easy-email-core/src/blocks/advanced/index.ts index 6a2584376..9d2afdc3b 100644 --- a/packages/easy-email-core/src/blocks/advanced/index.ts +++ b/packages/easy-email-core/src/blocks/advanced/index.ts @@ -22,6 +22,7 @@ import { AdvancedGroup, AdvancedColumn, AdvancedHero, + AdvancedTable, } from './blocks'; export const advancedBlocks = { @@ -34,6 +35,7 @@ export const advancedBlocks = { [AdvancedType.ACCORDION]: AdvancedAccordion, [AdvancedType.CAROUSEL]: AdvancedCarousel, [AdvancedType.SOCIAL]: AdvancedSocial, + [AdvancedType.TABLE]: AdvancedTable, [AdvancedType.WRAPPER]: AdvancedWrapper, [AdvancedType.SECTION]: AdvancedSection, diff --git a/packages/easy-email-core/src/blocks/standard/Table/index.tsx b/packages/easy-email-core/src/blocks/standard/Table/index.tsx index 95dba5d1c..515bc7d02 100644 --- a/packages/easy-email-core/src/blocks/standard/Table/index.tsx +++ b/packages/easy-email-core/src/blocks/standard/Table/index.tsx @@ -6,14 +6,14 @@ import { merge } from 'lodash'; import { BasicBlock } from '@core/components/BasicBlock'; import { t } from '@core/utils'; -export type ITable = IBlockData<{}, { content: string; }>; +export type ITable = IBlockData<{}, { content: string }>; export const Table = createBlock({ get name() { return t('Table'); }, type: BasicType.TABLE, - create: (payload) => { + create: payload => { const defaultData: ITable = { type: BasicType.TABLE, data: { @@ -29,7 +29,13 @@ export const Table = createBlock({ validParentType: [BasicType.COLUMN], render(params) { const { data } = params; - return {data.data.value.content}; + return ( + + {data.data.value.content} + + ); }, - }); diff --git a/packages/easy-email-core/src/constants.ts b/packages/easy-email-core/src/constants.ts index 6484a6216..23a8d1c15 100644 --- a/packages/easy-email-core/src/constants.ts +++ b/packages/easy-email-core/src/constants.ts @@ -38,6 +38,7 @@ export enum AdvancedType { SOCIAL = 'advanced_social', ACCORDION = 'advanced_accordion', CAROUSEL = 'advanced_carousel', + TABLE = 'advanced_table', WRAPPER = 'advanced_wrapper', SECTION = 'advanced_section', diff --git a/packages/easy-email-editor/src/utils/HtmlStringToReactNodes.tsx b/packages/easy-email-editor/src/utils/HtmlStringToReactNodes.tsx index 34c09832f..d80937b98 100644 --- a/packages/easy-email-editor/src/utils/HtmlStringToReactNodes.tsx +++ b/packages/easy-email-editor/src/utils/HtmlStringToReactNodes.tsx @@ -1,13 +1,26 @@ -import { BasicType, getNodeIdxFromClassName, getNodeTypeFromClassName, MERGE_TAG_CLASS_NAME } from 'easy-email-core'; +import { + BasicType, + getNodeIdxFromClassName, + getNodeTypeFromClassName, + MERGE_TAG_CLASS_NAME, +} from 'easy-email-core'; import { camelCase } from 'lodash'; import React from 'react'; import { isTextBlock } from './isTextBlock'; import { MergeTagBadge } from './MergeTagBadge'; -import { ContentEditableType, DATA_CONTENT_EDITABLE_IDX, DATA_CONTENT_EDITABLE_TYPE } from '@/constants'; +import { + ContentEditableType, + DATA_CONTENT_EDITABLE_IDX, + DATA_CONTENT_EDITABLE_TYPE, +} from '@/constants'; import { isButtonBlock } from './isButtonBlock'; -import { getContentEditableIdxFromClassName, getContentEditableTypeFromClassName } from './contenteditable'; +import { + getContentEditableIdxFromClassName, + getContentEditableTypeFromClassName, +} from './contenteditable'; import { getContentEditableClassName } from './getContentEditableClassName'; import { isNavbarBlock } from './isNavbarBlock'; +import { isTableBlock } from './isTableBlock'; const domParser = new DOMParser(); @@ -21,13 +34,13 @@ export interface HtmlStringToReactNodesOptions { export function HtmlStringToReactNodes( content: string, - option: HtmlStringToReactNodesOptions + option: HtmlStringToReactNodesOptions, ) { let doc = domParser.parseFromString(content, 'text/html'); // The average time is about 1.4 ms - [...doc.getElementsByTagName('a')].forEach((node) => { + [...doc.getElementsByTagName('a')].forEach(node => { node.setAttribute('tabIndex', '-1'); }); - [...doc.querySelectorAll(`.${MERGE_TAG_CLASS_NAME}`)].forEach((child) => { + [...doc.querySelectorAll(`.${MERGE_TAG_CLASS_NAME}`)].forEach(child => { const editNode = child.querySelector('div'); if (editNode) { if (option.enabledMergeTagsBadge) { @@ -37,7 +50,11 @@ export function HtmlStringToReactNodes( }); const reactNode = ( - + ); return reactNode; @@ -52,10 +69,10 @@ const RenderReactNode = React.memo(function ({ index: number; selector: string; }): React.ReactElement { - const attributes: { [key: string]: string; } = { + const attributes: { [key: string]: string } = { 'data-selector': selector, }; - node.getAttributeNames?.().forEach((att) => { + node.getAttributeNames?.().forEach(att => { if (att) { attributes[att] = node.getAttribute(att) || ''; } @@ -90,7 +107,6 @@ const RenderReactNode = React.memo(function ({ } if (attributes['contenteditable'] === 'true') { - return createElement(tagName, { key: performance.now(), ...attributes, @@ -107,13 +123,13 @@ const RenderReactNode = React.memo(function ({ node.childNodes.length === 0 ? null : [...node.childNodes].map((n, i) => ( - - )), + + )), }); return <>{reactNode}; @@ -143,7 +159,7 @@ function createElement( role?: string; src?: string; dangerouslySetInnerHTML?: any; - } + }, ) { if (props?.class && props.class.includes('email-block')) { const blockType = getNodeTypeFromClassName(props.class); @@ -180,21 +196,39 @@ function makeBlockNodeContentEditable(node: ChildNode) { node.setAttribute('contentEditable', 'true'); node.setAttribute(DATA_CONTENT_EDITABLE_TYPE, ContentEditableType.Text); node.setAttribute(DATA_CONTENT_EDITABLE_IDX, idx); - + } else if (isTableBlock(type)) { + const trNodes = node.querySelectorAll('tr'); + trNodes.forEach((trNode, trIndex) => { + const tdNodes = trNode.querySelectorAll('td'); + tdNodes.forEach((tdNode, tdIndex) => { + const _idx = idx.replace( + 'data.value.content', + `data.value.tableSource.${trIndex}.${tdIndex}.content`, + ); + tdNode.setAttribute('contentEditable', 'true'); + tdNode.setAttribute(DATA_CONTENT_EDITABLE_TYPE, ContentEditableType.RichText); + tdNode.setAttribute(DATA_CONTENT_EDITABLE_IDX, _idx); + }); + }); } node.childNodes.forEach(makeBlockNodeContentEditable); - } function makeStandardContentEditable(node: HTMLElement, blockType: string, idx: string) { - if (isTextBlock(blockType) || isButtonBlock(blockType)) { - node.classList.add(...getContentEditableClassName(blockType, `${idx}.data.value.content`)); + if (isTextBlock(blockType) || isButtonBlock(blockType) || isTableBlock(blockType)) { + node.classList.add( + ...getContentEditableClassName(blockType, `${idx}.data.value.content`), + ); } if (isNavbarBlock(blockType)) { node.querySelectorAll('.mj-link').forEach((anchor, index) => { - - anchor.classList.add(...getContentEditableClassName(blockType, `${idx}.data.value.links.${index}.content`)); + anchor.classList.add( + ...getContentEditableClassName( + blockType, + `${idx}.data.value.links.${index}.content`, + ), + ); }); } } @@ -204,4 +238,4 @@ function makeStandardContentEditable(node: HTMLElement, blockType: string, idx: // This has a little downside : The content is not modified at all, this means that the text won't be escaped, so if you use characters that are used to define html tags in your text, like < or >, you should use the encoded characters < and <. If you don't, sometimes the browser can be clever enough to understand that you're not really trying to open/close an html tag, and display the unescaped character as normal text, but this may cause problems in some cases. For instance, this will likely cause problems if you use the minify option, mj-html-attributes or an inline mj-style, because these require the html to be re-parsed internally. If you're just using the minify option, and really need to use the < > characters, i.e. for templating language, you can also avoid this problem by wrapping the troublesome content between two tags. -// Here is the list of all ending tags : - mj-accordion-text - mj-accordion-title - mj-button - mj-navbar-link - mj-raw - mj-social-element - mj-text - mj-table \ No newline at end of file +// Here is the list of all ending tags : - mj-accordion-text - mj-accordion-title - mj-button - mj-navbar-link - mj-raw - mj-social-element - mj-text - mj-table diff --git a/packages/easy-email-editor/src/utils/isTableBlock.ts b/packages/easy-email-editor/src/utils/isTableBlock.ts new file mode 100644 index 000000000..b097c5acd --- /dev/null +++ b/packages/easy-email-editor/src/utils/isTableBlock.ts @@ -0,0 +1,5 @@ +import { BasicType, AdvancedType } from 'easy-email-core'; + +export function isTableBlock(blockType: any) { + return blockType === AdvancedType.TABLE; +} diff --git a/packages/easy-email-extensions/src/AttributePanel/AttributePanel.tsx b/packages/easy-email-extensions/src/AttributePanel/AttributePanel.tsx index c96f1c907..92f550583 100644 --- a/packages/easy-email-extensions/src/AttributePanel/AttributePanel.tsx +++ b/packages/easy-email-extensions/src/AttributePanel/AttributePanel.tsx @@ -11,6 +11,7 @@ import { PresetColorsProvider } from './components/provider/PresetColorsProvider import ReactDOM from 'react-dom'; import { BlockAttributeConfigurationManager } from './utils/BlockAttributeConfigurationManager'; import { SelectionRangeProvider } from './components/provider/SelectionRangeProvider'; +import { TableOperation } from './components/blocks/AdvancedTable/Operation'; export interface AttributePanelProps {} @@ -40,6 +41,7 @@ export function AttributePanel() {
+ <> {shadowRoot && ReactDOM.createPortal( diff --git a/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/index.tsx b/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/index.tsx new file mode 100644 index 000000000..ec3fe58d1 --- /dev/null +++ b/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/index.tsx @@ -0,0 +1,59 @@ +import { getShadowRoot } from '@/utils'; +import { cloneDeep } from 'lodash'; +import React, { useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; +import TableColumnTool from './tableTool'; +import { useBlock, useFocusIdx } from '@'; + +export function TableOperation() { + const shadowRoot = getShadowRoot(); + const { focusIdx } = useFocusIdx(); + const { focusBlock, change } = useBlock(); + const topRef = useRef(null); + const bottomRef = useRef(null); + const leftRef = useRef(null); + const rightRef = useRef(null); + const tool = useRef(); + + useEffect(() => { + const borderTool: any = { + top: topRef.current, + bottom: bottomRef.current, + left: leftRef.current, + right: rightRef.current, + }; + tool.current = new TableColumnTool( + borderTool, + shadowRoot.querySelector('body') as any, + ); + return () => { + tool.current?.destroy(); + }; + }, []); + + useEffect(() => { + if (tool.current) { + tool.current.changeTableData = (data: any[][]) => { + change(`${focusIdx}.data.value.tableSource`, cloneDeep(data)); + }; + tool.current.tableData = cloneDeep(focusBlock?.data?.value?.tableSource || []); + } + }, [focusIdx, focusBlock]); + + return ( + <> + {shadowRoot && + createPortal( + <> +
+
+
+
+
+
+ , + shadowRoot.querySelector('body') as any, + )} + + ); +} diff --git a/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/menu.scss b/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/menu.scss new file mode 100644 index 000000000..b76f0c7fa --- /dev/null +++ b/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/menu.scss @@ -0,0 +1,57 @@ +.easy-email-table-operation-menu { + background-color: #fff; + box-shadow: 0 2px 8px rgba(0, 0, 0, .15); + font-size: 14px; + z-index: 100; + overflow: hidden; + border-radius: 4px; + padding: 4px 0; + + .easy-email-table-operation-menu-dividing { + height: 1px; + background-color: #efefef; + } + + + .easy-email-table-operation-color-picker { + display: flex; + align-items: center; + flex-wrap: wrap; + padding: 0 16px 10px; + background-color: #fff; + overflow: hidden; + + .easy-email-table-operation-color-picker-item { + width: 20px; + height: 20px; + border: 1px solid #595959; + margin-right: 5px; + margin-bottom: 5px; + cursor: pointer; + } + } + + .easy-email-table-operation-menu-item { + display: flex; + align-items: center; + padding: 10px 16px; + line-height: 18px; + background-color: #fff; + cursor: pointer; + color: #595959; + overflow: hidden; + text-overflow: ellipsis; + + &:hover { + background-color: #efefef; + } + + .easy-email-table-operation-menu-icon { + margin-right: 8px; + height: 20px; + width: 20px; + font-size: 0; + } + } +} + diff --git a/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/tableMenuConfig.ts b/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/tableMenuConfig.ts new file mode 100644 index 000000000..a20453482 --- /dev/null +++ b/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/tableMenuConfig.ts @@ -0,0 +1,279 @@ +import TableOperationMenu from './tableOperationMenu'; +import { IOperationData } from './type'; + +const MENU_CONFIG = { + insertColumnRight: { + text: 'Insert column right', + icon: ``, + handler() { + const _this = this as unknown as TableOperationMenu; + const right = _this.tableIndexBoundary.right; + + _this.tableData.forEach(tr => { + if (right === _this.maxTdCount - 1) { + tr.push({ content: '-' } as any); + return; + } + if (tr.length === 0) { + return tr.push({ content: '-' } as any); + } + for (let index = 0; index < tr.length; index++) { + const tdLeft = tr[index].left || 0; + const tdRight = tr[index].right || 0; + + if (tdRight === right) { + tr.splice(index + 1, 0, { content: '-' } as any); + break; + } + if (tdLeft <= right && tdRight > right && tr[index].colSpan) { + tr[index].colSpan = (tr[index].colSpan || 1) + 1; + break; + } + // pre cell intersect current cell. + if (tdLeft > right && tdLeft - 1 === right) { + tr.splice(index, 0, { content: '-' } as any); + break; + } + if (tdLeft > right) { + break; + } + } + }); + _this.changeTableData?.(_this.tableData); + }, + }, + insertColumnLeft: { + text: 'Insert column left', + icon: ``, + handler() { + const _this = this as unknown as TableOperationMenu; + const left = _this.tableIndexBoundary.left; + _this.tableData.forEach(tr => { + if (left === 0) { + tr.unshift({ content: '-' } as any); + return; + } + if (tr.length === 0) { + return tr.push({ content: '-' } as any); + } + for (let index = 0; index < tr.length; index++) { + const tdLeft = tr[index].left || 0; + const tdRight = tr[index].right || 0; + if (tdLeft === left) { + tr.splice(index, 0, { content: '-' } as any); + break; + } + // pre cell intersect current cell. + if (tdLeft < left && tdRight >= left && tr[index].colSpan) { + tr[index].colSpan = (tr[index].colSpan || 1) + 1; + break; + } + // pre cell intersect current cell. + if ( + tdLeft > left && + (!tr[index - 1] || (tr[index - 1].right || 0) + 1 === left) + ) { + tr.splice(index, 0, { content: '-' } as any); + break; + } + if (tdLeft > left) { + break; + } + } + }); + _this.changeTableData?.(_this.tableData); + }, + }, + insertRowUp: { + text: 'Insert row up', + icon: ``, + handler() { + const _this = this as unknown as TableOperationMenu; + const top = _this.tableIndexBoundary.top; + let maxTdCount = _this.maxTdCount; + if (_this.tableData[top].length < maxTdCount) { + // update pre cell row span + for (let index = top - 1; index > -1; index--) { + const tr = _this.tableData[index]; + tr.forEach((td, _index) => { + if (td.bottom && td.bottom >= top) { + td.rowSpan = (td.rowSpan || 1) + 1; + maxTdCount -= td.colSpan || 1; + } + }); + if (tr.length === maxTdCount) { + break; + } + } + } + _this.addRow(top, maxTdCount); + }, + }, + insertRowDown: { + text: 'Insert row down', + icon: ``, + handler() { + const _this = this as unknown as TableOperationMenu; + let addCount = _this.maxTdCount; + const bottom = _this.tableIndexBoundary.bottom; + + if (_this.tableData[bottom].length < _this.maxTdCount) { + // update pre cell row span + for (let index = bottom - 1; index > -1; index--) { + const tr = _this.tableData[index]; + if (tr.length === _this.maxTdCount) { + break; + } + tr.forEach((td, _index) => { + if (td.bottom && td.bottom > bottom) { + td.rowSpan = (td.rowSpan || 1) + 1; + addCount -= td.colSpan || 1; + } + }); + } + } + // If current Cell intersect next row, should add rowSpan and decrease tdCount + _this.tableData[bottom].forEach(e => { + if (e.rowSpan && e.rowSpan > 1) { + e.rowSpan += 1; + addCount -= e.colSpan || 1; + } + }); + + _this.addRow(bottom + 1, addCount); + }, + }, + + mergeCells: { + text: 'Merge selected cells', + icon: ``, + handler() { + const _this = this as unknown as TableOperationMenu; + const { top, left, bottom, right } = _this.tableIndexBoundary; + const leftTopItem = _this.tableData[top].find( + e => e.left === left, + ) as IOperationData; + leftTopItem.rowSpan = bottom - top + 1; + leftTopItem.colSpan = right - left + 1; + + _this.tableData.forEach((tr, trIndex) => { + if (trIndex >= top && trIndex <= bottom) { + // merge next cell, should add `
`. + if (bottom > top && trIndex > top && trIndex <= bottom) { + leftTopItem.content += '
'; + } + const deletedIndex = [] as number[]; + tr.forEach((td, tdIndex) => { + // current cell + if (top === trIndex && left === td.left) { + return; + } + // should merged cell + if (td.left >= left && td.right <= right) { + leftTopItem.content += ' ' + td.content; + deletedIndex.push(tdIndex); + } + }); + // delete cell + if (deletedIndex.length > 0) { + tr.splice( + deletedIndex[0], + deletedIndex[deletedIndex.length - 1] - deletedIndex[0] + 1, + ); + } + } + }); + + _this.changeTableData?.(_this.tableData); + }, + }, + + deleteColumn: { + text: 'Delete selected columns', + icon: ``, + handler() { + const _this = this as unknown as TableOperationMenu; + const { left, right } = _this.tableIndexBoundary; + _this.tableData.forEach(tr => { + const deleteIds = [] as number[]; + for (let index = 0; index < tr.length; index++) { + const td = tr[index]; + const tdLeft = tr[index].left || 0; + const tdRight = tr[index].right || 0; + const colSpan = td.colSpan || 1; + if (tdLeft > right) { + break; + } + if (tdLeft >= left && tdRight <= right) { + deleteIds.push(index); + continue; + } + // operate one cell + if (tdLeft <= left && tdRight >= right) { + td.colSpan = colSpan - (right - left) - 1; + continue; + } + // left insert cell + if (tdLeft > left && tdRight >= right) { + td.colSpan = colSpan - (right - tdLeft) - 1; + continue; + } + // right insert cell + if (tdLeft < left && tdRight >= left) { + td.colSpan = colSpan - (tdRight - left) - 1; + continue; + } + } + if (deleteIds.length) { + tr.splice(deleteIds[0], deleteIds[deleteIds.length - 1] - deleteIds[0] + 1); + } + }); + _this.changeTableData?.(_this.tableData); + }, + }, + + deleteRow: { + text: 'Delete selected rows', + icon: ``, + handler() { + const _this = this as unknown as TableOperationMenu; + const { top, bottom } = _this.tableIndexBoundary; + const deleteCount = bottom - top + 1; + // pre cell intersect current cell. + for (let index = bottom - 1; index > -1; index--) { + const tr = _this.tableData[index]; + tr.forEach((td, _index) => { + if (td.bottom && td.bottom >= top) { + const deleteRowSpan = td.bottom >= bottom ? deleteCount : td.bottom - top + 1; + td.rowSpan = (td.rowSpan || 1) - deleteRowSpan; + } + }); + } + + // current cell intersect next cell + for (let index = top; index <= bottom; index++) { + const tr = _this.tableData[index]; + tr.forEach((td, _index) => { + const rowSpan = td.rowSpan || 1; + if (rowSpan - 1 + top > bottom) { + const nextRowCell = { ...td, rowSpan: rowSpan - (bottom - top + 1) }; + const nextRow = _this.tableData[bottom + 1]; + if (nextRow) { + const index = Array.from({ length: _this.maxTdCount }).findIndex( + (_, index) => index === nextRowCell.left, + ); + if (index > -1) { + nextRow.splice(index, 0, nextRowCell); + } + } + } + }); + } + + _this.tableData.splice(_this.tableIndexBoundary.top, deleteCount); + _this.changeTableData?.(_this.tableData); + }, + }, +}; + +export default MENU_CONFIG; diff --git a/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/tableOperationMenu.ts b/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/tableOperationMenu.ts new file mode 100644 index 000000000..07316ee8b --- /dev/null +++ b/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/tableOperationMenu.ts @@ -0,0 +1,134 @@ +import { setStyle, getCorrectTableIndexBoundary, getMaxTdCount } from './util'; +import styleText from './menu.scss?inline'; +import { IBoundingPosition, IOperationData } from './type'; +import MENU_CONFIG from './tableMenuConfig'; + +const MENU_HEIGHT = 305; +const MENU_WIDTH = 200; + +export default class TableOperationMenu { + menuItems = MENU_CONFIG; + domNode: Element | undefined = undefined; + styleDom?: HTMLStyleElement; + visible = false; + + changeTableData?: (e: IOperationData[][]) => void; + tableData = undefined as unknown as IOperationData[][]; + tableIndexBoundary = undefined as unknown as IBoundingPosition; + maxTdCount = 0; + + constructor() { + this.menuInitial(); + this.mount(); + } + + mount() { + if (this.domNode) { + document.body.appendChild(this.domNode); + } + document.body.addEventListener('click', this.hide.bind(this)); + } + + destroy() { + this.domNode?.remove(); + if (this.styleDom) { + document.head.removeChild(this.styleDom); + } + document.body.removeEventListener('click', this.hide.bind(this)); + } + + hide() { + if (!this.visible) { + return; + } + this.visible = false; + setStyle(this.domNode, { + display: 'none', + }); + } + + addRow(insertIndex: number, colCount: number) { + const newRow = Array.from({ length: colCount }).map(() => ({ content: '-' }) as any); + this.tableData.splice(insertIndex, 0, newRow); + this.changeTableData?.(this.tableData); + } + + setTableData(tableData: IOperationData[][]) { + this.tableData = tableData || []; + this.maxTdCount = getMaxTdCount(this.tableData); + } + + setTableIndexBoundary(tableIndexBoundary: IBoundingPosition) { + // get correct boundary index and set table-td boundary + this.tableIndexBoundary = getCorrectTableIndexBoundary( + tableIndexBoundary, + this.tableData, + ); + } + + showMenu({ x, y }: { x: number; y: number }) { + this.visible = true; + const maxHeight = window.innerHeight; + const maxWidth = window.innerWidth; + if (maxWidth - MENU_WIDTH < x) { + x -= MENU_WIDTH; + } + if (maxHeight - MENU_HEIGHT < y) { + y -= MENU_HEIGHT; + } + setStyle(this.domNode, { + display: 'block', + position: 'absolute', + left: `${x}px`, + top: `${y}px`, + 'min-height': '150px', + width: `${MENU_WIDTH}px`, + Height: `${MENU_HEIGHT}px`, + }); + } + + menuInitial() { + this.styleDom = document.createElement('style'); + this.styleDom.innerText = styleText; + document.head.appendChild(this.styleDom); + + this.domNode = document.createElement('div'); + this.domNode.classList.add('easy-email-table-operation-menu'); + setStyle(this.domNode, { display: 'none' }); + + for (let name in this.menuItems) { + const itemOption = (this.menuItems as any)[name]; + if (itemOption) { + this.domNode.appendChild(this.menuItemCreator(Object.assign({}, itemOption))); + + if (['insertRowDown'].indexOf(name) > -1) { + this.domNode.appendChild(dividingCreator()); + } + } + } + + // create dividing line + function dividingCreator() { + const dividing = document.createElement('div'); + dividing.classList.add('easy-email-table-operation-menu-dividing'); + return dividing; + } + } + menuItemCreator({ text, icon, handler }: any) { + const node = document.createElement('div'); + node.classList.add('easy-email-table-operation-menu-item'); + + const iconSpan = document.createElement('span'); + iconSpan.classList.add('easy-email-table-operation-menu-icon'); + iconSpan.innerHTML = icon; + + const textSpan = document.createElement('span'); + textSpan.classList.add('easy-email-table-operation-menu-text'); + textSpan.innerText = text; + + node.appendChild(iconSpan); + node.appendChild(textSpan); + node.addEventListener('click', handler.bind(this), false); + return node; + } +} diff --git a/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/tableTool.ts b/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/tableTool.ts new file mode 100644 index 000000000..a93f4af43 --- /dev/null +++ b/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/tableTool.ts @@ -0,0 +1,254 @@ +import TableOperationMenu from './tableOperationMenu'; +import { + checkEventInBoundingRect, + setStyle, + getCurrentTable, + getElementsBoundary, + getTdBoundaryIndex, + getBoundaryRectAndElement, +} from './util'; +import { AdvancedTableBlock } from '@core/blocks/advanced/generateAdvancedTableBlock'; + +interface IBorderTool { + top: Element; + bottom: Element; + left: Element; + right: Element; +} + +class TableColumnTool { + borderTool = {} as IBorderTool; + dragging = false; + showBorderTool = false; + startRect = {} as { width: number; height: number }; // start td rect + startTdTop = 0; // update when click + startTdLeft = 0; // update when click + endTdTop = 0; // will update by mouse move + endTdLeft = 0; // will update by mouse move + width = 0; // selected section width,will update by mouse move + height = 0; // selected section height, will update by mouse move + + selectedLeftTopCell: Element | undefined = undefined; + selectedBottomRightCell: Element | undefined = undefined; + startDom: Element | undefined = undefined; + endDom: Element | undefined = undefined; + hoveringTable: ParentNode | null = null; + root: Element | undefined = undefined; + + tableMenu?: TableOperationMenu; + changeTableData?: (e: AdvancedTableBlock['data']['value']['tableSource']) => void; + tableData: AdvancedTableBlock['data']['value']['tableSource'] = []; + + constructor(borderTool: IBorderTool, root: Element) { + if (!borderTool || !root) { + return; + } + this.borderTool = borderTool; + this.root = root; + + this.initTool(); + } + + initTool() { + this.root?.addEventListener('contextmenu', this.handleContextmenu); + this.root?.addEventListener('mousedown', this.handleMousedown.bind(this)); + document.body.addEventListener('click', this.hideBorder, false); + document.body.addEventListener('contextmenu', this.hideTableMenu, false); + document.addEventListener('keydown', this.hideBorderByKeyDown); + } + + destroy() { + this.root?.removeEventListener('mousedown', this.handleMousedown.bind(this)); + this.root?.removeEventListener('contextmenu', this.handleContextmenu); + document.body.removeEventListener('click', this.hideBorder, false); + document.body.removeEventListener('contextmenu', this.hideTableMenu, false); + document.removeEventListener('keydown', this.hideBorderByKeyDown); + } + + hideBorder = (e: any) => { + if (e.target.id === 'VisualEditorEditMode') { + return; + } + this.visibleBorder(false); + }; + + hideBorderByKeyDown = () => { + this.visibleBorder(false); + }; + + hideTableMenu = (e?: any) => { + if (e?.target.id === 'VisualEditorEditMode') { + return; + } + this.tableMenu?.hide(); + }; + + visibleBorder = (show = true) => { + if (this.showBorderTool === show) { + return; + } + if (show) { + setStyle(this.borderTool.top.parentElement, { display: 'block' }); + } else { + setStyle(this.borderTool.top.parentElement, { display: 'none' }); + } + this.showBorderTool = show; + }; + + renderBorder = () => { + this.visibleBorder(true); + const result = getBoundaryRectAndElement( + this.startDom as Element, + this.endDom as Element, + ); + if (!result) { + return; + } + const { left, top, width, height } = result.boundary; + this.selectedLeftTopCell = result.leftTopCell; + this.selectedBottomRightCell = result.bottomRightCell; + + setStyle(this.borderTool.top, { + 'background-color': 'rgb(65, 68, 77)', + left: `${left}px`, + top: `${top}px`, + width: `${Math.abs(width)}px`, + height: '2px', + position: 'absolute', + 'z-index': 10, + }); + setStyle(this.borderTool.bottom, { + 'background-color': 'rgb(65, 68, 77)', + left: `${left}px`, + top: `${top + height}px`, + width: `${Math.abs(width)}px`, + height: '2px', + position: 'absolute', + 'z-index': 10, + }); + setStyle(this.borderTool.left, { + 'background-color': 'rgb(65, 68, 77)', + left: `${left}px`, + top: `${top}px`, + width: `2px`, + height: `${Math.abs(height)}px`, + position: 'absolute', + 'z-index': 10, + }); + setStyle(this.borderTool.right, { + 'background-color': 'rgb(65, 68, 77)', + left: `${left + width}px`, + top: `${top}px`, + width: `2px`, + height: `${Math.abs(height)}px`, + position: 'absolute', + 'z-index': 10, + }); + }; + + handleContextmenu = (event: any) => { + if (this.showBorderTool) { + const selectedBoundary = getElementsBoundary( + this.selectedLeftTopCell as Element, + this.selectedBottomRightCell as Element, + ); + if (checkEventInBoundingRect(selectedBoundary, event)) { + event.preventDefault(); + return; + } + } + this.hideTableMenu(); + }; + + handleMousedown(event: any) { + let target: Element = event.target; + if (event.button == 0) { + // left button click + while (target && target.parentNode) { + if ( + target.nodeName === 'TD' && + target.getAttribute('data-content_editable-type') === 'rich_text' + ) { + this.root?.addEventListener('mousemove', this.handleDrag); + this.root?.addEventListener('mouseup', this.handleMouseup); + + this.dragging = true; + this.startDom = target; + this.endDom = target; + this.hoveringTable = getCurrentTable(target); + + this.renderBorder(); + return; + } + target = target.parentNode as Element; + if (['TR', 'TABLE', 'BODY'].includes(target.nodeName)) { + this.visibleBorder(false); + return; + } + } + } else if (event.button == 2) { + if (this.showBorderTool) { + const selectedBoundary = getElementsBoundary( + this.selectedLeftTopCell as Element, + this.selectedBottomRightCell as Element, + ); + // check event position, then show table operation menu + if (checkEventInBoundingRect(selectedBoundary, event)) { + if (!this.tableMenu) { + this.tableMenu = new TableOperationMenu(); + } + + this.tableMenu.setTableData(this.tableData as any); + this.tableMenu.changeTableData = this.changeTableData; + + this.tableMenu.setTableIndexBoundary( + getTdBoundaryIndex( + this.selectedLeftTopCell as Element, + this.selectedBottomRightCell as Element, + ), + ); + this.tableMenu.showMenu(event); + + return; + } + } + } + this.visibleBorder(false); + } + + handleDrag = (e: any) => { + e.preventDefault(); + + if (this.dragging) { + let target = e.target; + + while (target && target.parentNode) { + if ( + target.nodeName === 'TD' && + target.getAttribute('data-content_editable-type') === 'rich_text' + ) { + const hoveringTable = getCurrentTable(target); + if (this.endDom === target || this.hoveringTable !== hoveringTable) { + return; + } + this.endDom = target; + this.renderBorder(); + return; + } + target = target.parentNode; + } + } + }; + + handleMouseup = (e: any) => { + e.preventDefault(); + + if (this.dragging) { + this.dragging = false; + this.root?.removeEventListener('mousemove', this.handleDrag); + this.root?.removeEventListener('mouseup', this.handleMouseup); + } + }; +} + +export default TableColumnTool; diff --git a/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/type.ts b/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/type.ts new file mode 100644 index 000000000..e751d547d --- /dev/null +++ b/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/type.ts @@ -0,0 +1,24 @@ +import { ITableData } from '@core/blocks/advanced/generateAdvancedTableBlock'; + +export interface IOperationData extends ITableData { + top: number; + bottom: number; + left: number; + right: number; +} + +export interface IBoundaryRect { + left: number; + top: number; + right: number; + bottom: number; + width: number; + height: number; +} + +export interface IBoundingPosition { + left: number; + top: number; + right: number; + bottom: number; +} diff --git a/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/util.ts b/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/util.ts new file mode 100644 index 000000000..413937fbf --- /dev/null +++ b/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/Operation/util.ts @@ -0,0 +1,272 @@ +import { DATA_CONTENT_EDITABLE_IDX } from 'easy-email-editor'; +import { IBoundaryRect, IBoundingPosition, IOperationData } from './type'; +import { AdvancedTableBlock } from '@core/blocks/advanced/generateAdvancedTableBlock'; + +const getEditorElementClientRect = (target: any) => { + let left = target.offsetLeft; + let top = target.offsetTop; + const width = target.clientWidth; + const height = target.clientHeight; + let parentNode = target.offsetParent; + while (parentNode && parentNode.offsetParent) { + if (parentNode.classList.contains('shadow-container')) { + return { left, top, height, width }; + } + left += parentNode.offsetLeft; + top += parentNode.offsetTop; + + parentNode = parentNode.offsetParent; + } + return { left, top, height, width }; +}; + +const getBoundaryFromRects = (startRect: any, endRect: any) => { + let left = Math.min( + startRect.left, + endRect.left, + startRect.left + startRect.width, + endRect.left + endRect.width, + ); + + let right = Math.max( + startRect.left, + endRect.left, + startRect.left + startRect.width, + endRect.left + endRect.width, + ); + + let top = Math.min( + startRect.top, + endRect.top, + startRect.top + startRect.height, + endRect.top + endRect.height, + ); + + let bottom = Math.max( + startRect.top, + endRect.top, + startRect.top + startRect.height, + endRect.top + endRect.height, + ); + + let width = right - left; + let height = bottom - top; + + return { top, bottom, left, right, width, height }; +}; + +const ERROR_LIMIT = 2; + +const getCorrectBoundary = (el: Element, currentBoundary: IBoundaryRect) => { + const tableEl = el.parentElement?.parentElement?.parentElement; + if (!tableEl) { + return null; + } + let leftTopCell = el; + let bottomRightCell = el; + let leftTopRect = getEditorElementClientRect(el); + let bottomRightRect = leftTopRect; + + const tableCells = tableEl.querySelectorAll('td'); + const tableCellRects = [] as any[]; + tableCells.forEach(tableCell => { + // TODO: reduce calculation: cache table rect, use table rect diff all td rect boundary + const { left, top, height, width } = getEditorElementClientRect(tableCell); + tableCellRects.push({ left, top, height, width }); + let isIntersected = + ((left + ERROR_LIMIT >= currentBoundary.left && + left + ERROR_LIMIT <= currentBoundary.right) || + (left - ERROR_LIMIT + width >= currentBoundary.left && + left - ERROR_LIMIT + width <= currentBoundary.right)) && + ((top + ERROR_LIMIT >= currentBoundary.top && + top + ERROR_LIMIT <= currentBoundary.bottom) || + (top - ERROR_LIMIT + height >= currentBoundary.top && + top - ERROR_LIMIT + height <= currentBoundary.bottom)); + if (isIntersected) { + currentBoundary = getBoundaryFromRects(currentBoundary, { + left, + top, + height, + width, + }); + } + }); + + tableCells.forEach((tableCell, index) => { + const { left, top, height, width } = tableCellRects[index]; + let isIntersected = + ((left + ERROR_LIMIT >= currentBoundary.left && + left + ERROR_LIMIT <= currentBoundary.right) || + (left - ERROR_LIMIT + width >= currentBoundary.left && + left - ERROR_LIMIT + width <= currentBoundary.right)) && + ((top + ERROR_LIMIT >= currentBoundary.top && + top + ERROR_LIMIT <= currentBoundary.bottom) || + (top - ERROR_LIMIT + height >= currentBoundary.top && + top - ERROR_LIMIT + height <= currentBoundary.bottom)); + if (!isIntersected) { + return; + } + + if (top <= leftTopRect.top && left <= leftTopRect.left) { + leftTopRect = tableCellRects[index]; + leftTopCell = tableCell; + } + if ( + top === leftTopRect.top + ERROR_LIMIT || + (top === leftTopRect.top && left <= leftTopRect.left) + ) { + leftTopRect = tableCellRects[index]; + leftTopCell = tableCell; + } + if ( + top + height > bottomRightRect.top + bottomRightRect.height + ERROR_LIMIT || + (top + height === bottomRightRect.top + bottomRightRect.height && + left + width >= bottomRightRect.left + bottomRightRect.width) + ) { + bottomRightRect = tableCellRects[index]; + bottomRightCell = tableCell; + } + }); + + return { leftTopCell, bottomRightCell, boundary: currentBoundary }; +}; + +// get selected boundary and correct let-top-dom, right-bottom-dom +export const getBoundaryRectAndElement = (el1: Element, el2: Element) => { + const rect1 = getEditorElementClientRect(el1); + const rect2 = getEditorElementClientRect(el2); + + const boundary = getBoundaryFromRects(rect1, rect2); + + return getCorrectBoundary(el1, boundary); +}; + +export function setStyle(domNode: any, rules: any) { + if (typeof rules === 'object') { + for (let prop in rules) { + domNode.style[prop] = rules[prop]; + } + } +} + +export const getCurrentTable = (target: Element) => { + let parentNode = target.parentNode; + while (parentNode) { + if (parentNode.nodeName === 'TABLE') { + return parentNode; + } + parentNode = parentNode.parentNode; + } + return parentNode; +}; + +export const getElementsBoundary = (el1: Element, el2: Element): IBoundingPosition => { + const rect1 = el1.getBoundingClientRect(); + const rect2 = el2.getBoundingClientRect(); + + const left = Math.min(rect1.left, rect2.left); + const right = Math.max(rect1.right, rect2.right); + const bottom = Math.max(rect1.bottom, rect2.bottom); + const top = Math.min(rect1.top, rect2.top); + + return { left, top, right, bottom }; +}; + +export const checkEventInBoundingRect = ( + rect: IBoundingPosition, + { x, y }: { x: number; y: number }, +) => { + return x >= rect.left && x <= rect.right && y <= rect.bottom && y >= rect.top; +}; + +export const getCellAttr = (el: Element, attrName: string) => { + const value = el.getAttribute(attrName); + + return Number(value || 0); +}; + +const getCellIndex = (cellElement: Element) => { + let idxName = cellElement.getAttribute(DATA_CONTENT_EDITABLE_IDX) as string; + idxName = idxName.split('data.value.tableSource.')[1].split('.content')[0]; + + return idxName.split('.').map(e => Number(e)); +}; + +export const getTdBoundaryIndex = (leftTopCell: Element, bottomRightCell: Element) => { + const idx1 = getCellIndex(leftTopCell); + const idx2 = getCellIndex(bottomRightCell); + + const top = idx1[0]; + const left = idx1[1]; + const right = idx2[1]; + const bottom = idx2[0]; + + return { left, top, right, bottom }; +}; + +export const getCorrectTableIndexBoundary = ( + tableIndexBoundary: IBoundingPosition, + tableData: IOperationData[][], +) => { + let { left, right, top, bottom } = tableIndexBoundary; + // set top, bottom index + tableData.forEach((tr, trIndex) => { + tr.forEach(td => { + td.top = trIndex; + td.bottom = trIndex + (td.rowSpan || 1) - 1; + }); + }); + // set right ,left index + const maxTdCount = getMaxTdCount(tableData); + const mergedCells = [] as [number, number][]; // [trIndex, tdIndex] + Array.from({ length: maxTdCount }).forEach((_, tdIndex) => { + tableData.forEach((tr, trIndex) => { + const mergedCell = mergedCells.find(e => e[0] === trIndex && e[1] === tdIndex); + if (mergedCell) { + return; + } + const mergedTds = mergedCells.filter(e => e[0] === trIndex && e[1] < tdIndex); + const _tdIndex = tdIndex - mergedTds.length; + const td = tr[_tdIndex]; + if (!td) { + console.log('error case, should fix this error.'); + return; + } + const rowSpan = td.rowSpan || 1; + const colSpan = td.colSpan || 1; + td.left = tdIndex; + td.right = tdIndex + colSpan - 1; + + if (rowSpan > 1 || colSpan > 1) { + Array.from({ length: rowSpan }).forEach((_, rowSpanIndex) => { + Array.from({ length: colSpan }).forEach((_, colSpanIndex) => { + if (rowSpanIndex === 0 && colSpanIndex === 0) { + return; + } + mergedCells.push([trIndex + rowSpanIndex, tdIndex + colSpanIndex]); + }); + }); + } + }); + }); + + tableIndexBoundary.left = tableData?.[top]?.[left]?.left || 0; + tableIndexBoundary.right = tableData?.[bottom]?.[right]?.right || 0; + tableIndexBoundary.bottom = tableData?.[bottom]?.[right]?.bottom || 0; + tableIndexBoundary.top = tableData?.[top]?.[left]?.top || 0; + + return tableIndexBoundary; +}; + +export const getMaxTdCount = ( + tableData: AdvancedTableBlock['data']['value']['tableSource'], +) => { + let tdCount = 1; + tableData.forEach(tr => { + let _tdCount = tr.reduce((count, td) => count + (td.colSpan || 1), 0); + if (_tdCount > tdCount) { + tdCount = _tdCount; + } + }); + return tdCount; +}; diff --git a/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/index.tsx b/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/index.tsx new file mode 100644 index 000000000..e7777bf7e --- /dev/null +++ b/packages/easy-email-extensions/src/AttributePanel/components/blocks/AdvancedTable/index.tsx @@ -0,0 +1,83 @@ +import { AttributesPanelWrapper } from '@extensions/AttributePanel'; +import { Collapse, Tooltip, Button } from '@arco-design/web-react'; +import { IconFont, Stack, useFocusIdx } from 'easy-email-editor'; +import React, { useState } from 'react'; +import { Border } from '../../attributes/Border'; +import { Color } from '../../attributes/Color'; +import { ContainerBackgroundColor } from '../../attributes/ContainerBackgroundColor'; +import { FontFamily } from '../../attributes/FontFamily'; +import { FontSize } from '../../attributes/FontSize'; +import { FontStyle } from '../../attributes/FontStyle'; +import { Padding } from '../../attributes/Padding'; +import { TextAlign } from '../../attributes/TextAlign'; +import { Width } from '../../attributes/Width'; +import { HtmlEditor } from '../../UI/HtmlEditor'; +import { CollapseWrapper } from '../../attributes/CollapseWrapper'; +import { + ColorPickerField, + InputWithUnitField, + NumberField, + TextField, +} from '@extensions'; +import { pixelAdapter } from '../../adapter'; + +export function AdvancedTable() { + const { focusIdx } = useFocusIdx(); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/packages/easy-email-extensions/src/AttributePanel/components/blocks/Table/index.tsx b/packages/easy-email-extensions/src/AttributePanel/components/blocks/Table/index.tsx index 3e6655deb..edfe7a16d 100644 --- a/packages/easy-email-extensions/src/AttributePanel/components/blocks/Table/index.tsx +++ b/packages/easy-email-extensions/src/AttributePanel/components/blocks/Table/index.tsx @@ -19,17 +19,20 @@ export function Table() { return (