Skip to content

Commit

Permalink
add table block (#407)
Browse files Browse the repository at this point in the history
* add table block

add table block

* update table data field

* add advance table

* hide contextmenu

* update table operation:  delete selected columns

* latest
  • Loading branch information
hulinNeil authored Aug 19, 2024
1 parent aceafd5 commit b3ed4d1
Show file tree
Hide file tree
Showing 19 changed files with 1,370 additions and 41 deletions.
5 changes: 4 additions & 1 deletion demo/src/pages/Editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -87,6 +87,9 @@ const defaultCategories: ExtensionProps['categories'] = [
{
type: AdvancedType.WRAPPER,
},
{
type: AdvancedType.TABLE,
},
],
},
{
Expand Down
13 changes: 8 additions & 5 deletions packages/easy-email-core/src/blocks/advanced/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IText>({
type: AdvancedType.TEXT,
Expand Down Expand Up @@ -63,6 +65,11 @@ export const AdvancedSocial = generateAdvancedContentBlock<ISocial>({
baseType: BasicType.SOCIAL,
});

export const AdvancedTable = generateAdvancedTableBlock({
type: AdvancedType.TABLE,
baseType: BasicType.TABLE,
});

//

export const AdvancedWrapper = generateAdvancedLayoutBlock<IWrapper>({
Expand Down Expand Up @@ -97,9 +104,5 @@ export const AdvancedColumn = generateAdvancedLayoutBlock<IColumn>({
export const AdvancedHero = generateAdvancedLayoutBlock<IHero>({
type: AdvancedType.HERO,
baseType: BasicType.HERO,
validParentType: [
BasicType.WRAPPER,
AdvancedType.WRAPPER,
BasicType.PAGE,
],
validParentType: [BasicType.WRAPPER, AdvancedType.WRAPPER, BasicType.PAGE],
});
Original file line number Diff line number Diff line change
@@ -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<AdvancedTableBlock>({
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 =>
`<td rowspan="${e.rowSpan || 1}" colspan="${
e.colSpan || 1
}" style="${styles.join(';')}">${e.content}</td>`,
);
return `<tr style="text-align:${textAlign};font-style:${fontStyle};">${_trString.join(
'\n',
)}</tr>`;
})
.join('\n');

return (
<BasicBlock
params={params}
tag='mj-table'
>
{content}
</BasicBlock>
);
},
});
}

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[][];
}
>;
2 changes: 2 additions & 0 deletions packages/easy-email-core/src/blocks/advanced/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
AdvancedGroup,
AdvancedColumn,
AdvancedHero,
AdvancedTable,
} from './blocks';

export const advancedBlocks = {
Expand All @@ -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,
Expand Down
14 changes: 10 additions & 4 deletions packages/easy-email-core/src/blocks/standard/Table/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ITable>({
get name() {
return t('Table');
},
type: BasicType.TABLE,
create: (payload) => {
create: payload => {
const defaultData: ITable = {
type: BasicType.TABLE,
data: {
Expand All @@ -29,7 +29,13 @@ export const Table = createBlock<ITable>({
validParentType: [BasicType.COLUMN],
render(params) {
const { data } = params;
return <BasicBlock params={params} tag="mj-table">{data.data.value.content}</BasicBlock>;
return (
<BasicBlock
params={params}
tag='mj-table'
>
{data.data.value.content}
</BasicBlock>
);
},

});
1 change: 1 addition & 0 deletions packages/easy-email-core/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
84 changes: 59 additions & 25 deletions packages/easy-email-editor/src/utils/HtmlStringToReactNodes.tsx
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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) {
Expand All @@ -37,7 +50,11 @@ export function HtmlStringToReactNodes(
});

const reactNode = (
<RenderReactNode selector={'0'} node={doc.documentElement} index={0} />
<RenderReactNode
selector={'0'}
node={doc.documentElement}
index={0}
/>
);

return reactNode;
Expand All @@ -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) || '';
}
Expand Down Expand Up @@ -90,7 +107,6 @@ const RenderReactNode = React.memo(function ({
}

if (attributes['contenteditable'] === 'true') {

return createElement(tagName, {
key: performance.now(),
...attributes,
Expand All @@ -107,13 +123,13 @@ const RenderReactNode = React.memo(function ({
node.childNodes.length === 0
? null
: [...node.childNodes].map((n, i) => (
<RenderReactNode
selector={getChildSelector(selector, i)}
key={i}
node={n as any}
index={i}
/>
)),
<RenderReactNode
selector={getChildSelector(selector, i)}
key={i}
node={n as any}
index={i}
/>
)),
});

return <>{reactNode}</>;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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`,
),
);
});
}
}
Expand All @@ -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 &lt; and &lt;. 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 <!-- htmlmin:ignore --> 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
// 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
5 changes: 5 additions & 0 deletions packages/easy-email-editor/src/utils/isTableBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { BasicType, AdvancedType } from 'easy-email-core';

export function isTableBlock(blockType: any) {
return blockType === AdvancedType.TABLE;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}

Expand Down Expand Up @@ -40,6 +41,7 @@ export function AttributePanel() {
<div style={{ position: 'absolute' }}>
<RichTextField idx={focusIdx} />
</div>
<TableOperation />
<>
{shadowRoot &&
ReactDOM.createPortal(
Expand Down
Loading

0 comments on commit b3ed4d1

Please sign in to comment.