react-email-builder
Version:
A simple React drag and drop email builder.
301 lines (256 loc) • 8.1 kB
text/typescript
import type React from 'react'
import { useCallback, useContext, useMemo } from 'react'
import {
EmailBuilderConfigContext,
EmailBuilderDefaultFontStyleContext,
EmailBuilderSelectedBlockInfoContext,
EmailBuilderStateContext,
SetEmailBuilderStateContext
} from '../context'
import type { EmailBuilderBlock } from '../types'
import type { ColumnsBlockAttrs } from '../blocks/columns/types'
import { copyBlock, createPlaceholder } from '../utils'
export function useEmailBuilderConfig() {
return useContext(EmailBuilderConfigContext)
}
export function useEmailBuilderState() {
return useContext(EmailBuilderStateContext)
}
export function useSetEmailBuilderState() {
return useContext(SetEmailBuilderStateContext)
}
export function useSelectedBlock() {
return useContext(EmailBuilderSelectedBlockInfoContext)
}
export function useBlockStyle(block: EmailBuilderBlock) {
const { style } = block
const fontStyle = useContext(EmailBuilderDefaultFontStyleContext)
return useMemo<React.CSSProperties>(() => {
const p = style.padding || []
const u = undefined
return {
...fontStyle,
backgroundColor: style.bgColor,
paddingTop: p[0] ?? u,
paddingRight: p[1] ?? u,
paddingBottom: p[2] ?? u,
paddingLeft: p[3] ?? u
}
}, [fontStyle, style])
}
export function useBlockEditor<Attrs extends object = any>(blockId: string) {
const setState = useSetEmailBuilderState()
return useCallback(
(mutate: (prev: EmailBuilderBlock<Attrs>) => EmailBuilderBlock<Attrs>) => {
setState((prev) => ({
...prev,
blocks: prev.blocks.map((block) => {
if (block.id === blockId) {
return mutate(block as any) as any
}
if (block.type === 'columns') {
let columnsTouched = false
const cols = block as EmailBuilderBlock<ColumnsBlockAttrs>
const nextColumns: typeof cols = {
...cols,
attrs: {
...cols.attrs,
columns: cols.attrs.columns.map((column) => {
let columnTouched = false
const nextColumn: typeof column = {
...column,
blocks: column.blocks.map((columnBlock) => {
if (columnBlock.id === blockId) {
columnTouched = true
return mutate(columnBlock)
}
return columnBlock
})
}
if (columnTouched) {
columnsTouched = true
}
return columnTouched ? nextColumn : column
})
}
}
return columnsTouched ? nextColumns : block
}
return block
})
}))
},
[setState, blockId]
)
}
export function useBlockAttrsEditor<Attrs extends object = any>(
block: EmailBuilderBlock<Attrs>
) {
const setBlock = useBlockEditor<Attrs>(block.id)
const setAttrs = useCallback(
(attrs: Partial<Attrs>) => {
setBlock((prev) => ({
...prev,
attrs: {
...prev.attrs,
...attrs
}
}))
},
[setBlock]
)
return setAttrs
}
export function useDeleteBlock() {
const setState = useSetEmailBuilderState()
return useCallback(
(blockId: string) => {
setState((prev) => {
const filterBlocks = (blocks: EmailBuilderBlock[]) => {
const newBlocks: EmailBuilderBlock[] = []
let touched = false
blocks.forEach((block) => {
if (block.id === blockId) {
touched = true
} else {
let newBlock = block
if (newBlock.type === 'columns') {
const cols = newBlock as EmailBuilderBlock<ColumnsBlockAttrs>
let columnsTouched = false
newBlock = {
...cols,
attrs: {
...cols.attrs,
columns: cols.attrs.columns.map((column) => {
const newColumnBlocks = filterBlocks(column.blocks)
if (newColumnBlocks !== column.blocks) {
columnsTouched = true
return {
...column,
blocks: newColumnBlocks as any
}
}
return column
})
}
}
if (columnsTouched) {
touched = true
} else {
newBlock = block
}
}
newBlocks.push(newBlock)
}
})
if (touched && !newBlocks.length) {
newBlocks.push(createPlaceholder())
}
return touched ? newBlocks : blocks
}
const prevSelected = prev.selectedId === blockId
return {
...prev,
blocks: filterBlocks(prev.blocks),
selectedId: prevSelected ? undefined : prev.selectedId,
dragoverId: prev.dragoverId === blockId ? undefined : prev.dragoverId,
tab: prevSelected && prev.tab === 'settings' ? 'blocks' : prev.tab
}
})
},
[setState]
)
}
export function useMoveBlock() {
const setState = useSetEmailBuilderState()
return useCallback(
(blockId: string, direction: -1 | 1) => {
setState((prev) => {
const move = (blocks: EmailBuilderBlock[]) => {
const newBlocks = [...blocks]
const index = newBlocks.findIndex((block) => block.id === blockId)
if (index > -1) {
const max = newBlocks.length - 1
let target = index + direction
if (target < 0) {
target = 0
}
if (target > max) {
target = max
}
const old = newBlocks[target]
newBlocks[target] = newBlocks[index]
newBlocks[index] = old
return newBlocks
}
return blocks
}
return {
...prev,
blocks: move(prev.blocks).map((block) => {
if (block.type === 'columns') {
const cols = block as EmailBuilderBlock<ColumnsBlockAttrs>
return {
...block,
attrs: {
...cols.attrs,
columns: cols.attrs.columns.map((column) => ({
...column,
blocks: move(column.blocks)
}))
}
}
}
return block
})
}
})
},
[setState]
)
}
export function useCopyBlock() {
const setState = useSetEmailBuilderState()
const config = useEmailBuilderConfig()
return useCallback(
(blockId: string) => {
setState((prev) => {
let copiedBlockId: string | undefined
const copy = (blocks: EmailBuilderBlock[]) => {
const newBlocks: EmailBuilderBlock[] = []
blocks.forEach((block) => {
newBlocks.push(block)
if (block.id === blockId) {
const copied = copyBlock(block, config)
copiedBlockId = copied.id
newBlocks.push(copied)
}
})
return newBlocks
}
const blocks = copy(prev.blocks).map((block) => {
if (block.type === 'columns') {
const cols = block as EmailBuilderBlock<ColumnsBlockAttrs>
return {
...cols,
attrs: {
...cols.attrs,
columns: cols.attrs.columns.map((col) => ({
...col,
blocks: copy(col.blocks)
}))
}
}
}
return block
})
return {
...prev,
blocks,
selectedId: copiedBlockId
}
})
},
[setState, config]
)
}