You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1528 lines
48 KiB

import {
cloneProperty,
deepClone,
deepCloneOmitKeys,
getUUID,
isArrayEqual,
omitObject,
pickObject,
splitText
} from '.'
import {
ElementType,
IEditorOption,
IElement,
ImageDisplay,
ListStyle,
ListType,
RowFlex,
TableBorder,
TdBorder
} from '..'
import { LaTexParticle } from '../core/draw/particle/latex/LaTexParticle'
import { NON_BREAKING_SPACE, ZERO } from '../dataset/constant/Common'
import {
BLOCK_ELEMENT_TYPE,
CONTROL_STYLE_ATTR,
EDITOR_ELEMENT_CONTEXT_ATTR,
EDITOR_ELEMENT_ZIP_ATTR,
EDITOR_ROW_ATTR,
INLINE_NODE_NAME,
TABLE_CONTEXT_ATTR,
TABLE_TD_ZIP_ATTR,
TEXTLIKE_ELEMENT_TYPE,
TITLE_CONTEXT_ATTR
} from '../dataset/constant/Element'
import {
listStyleCSSMapping,
listTypeElementMapping,
ulStyleMapping
} from '../dataset/constant/List'
import { START_LINE_BREAK_REG } from '../dataset/constant/Regular'
import {
titleNodeNameMapping,
titleOrderNumberMapping,
titleSizeMapping
} from '../dataset/constant/Title'
import { ControlComponent, ControlType } from '../dataset/enum/Control'
import { UlStyle } from '../dataset/enum/List'
import { DeepRequired } from '../interface/Common'
import { IControlSelect } from '../interface/Control'
import { IRowElement } from '../interface/Row'
import { ITd } from '../interface/table/Td'
import { ITr } from '../interface/table/Tr'
export function unzipElementList(elementList: IElement[]): IElement[] {
const result: IElement[] = []
for (let v = 0; v < elementList.length; v++) {
const valueItem = elementList[v]
const textList = splitText(valueItem.value)
for (let d = 0; d < textList.length; d++) {
result.push({ ...valueItem, value: textList[d] })
}
}
return result
}
interface IFormatElementListOption {
isHandleFirstElement?: boolean
editorOptions: DeepRequired<IEditorOption>
}
export function formatElementList(
elementList: IElement[],
options: IFormatElementListOption
) {
const { isHandleFirstElement, editorOptions } = <IFormatElementListOption>{
isHandleFirstElement: true,
...options
}
const startElement = elementList[0]
// 非首字符零宽节点文本元素则补偿-列表元素内部会补偿此处忽略
if (
isHandleFirstElement &&
startElement?.type !== ElementType.LIST &&
((startElement?.type && startElement.type !== ElementType.TEXT) ||
!START_LINE_BREAK_REG.test(startElement?.value))
) {
elementList.unshift({
value: ZERO
})
}
let i = 0
while (i < elementList.length) {
let el = elementList[i]
// 优先处理虚拟元素
if (el.type === ElementType.TITLE) {
// 移除父节点
elementList.splice(i, 1)
// 格式化元素
const valueList = el.valueList || []
formatElementList(valueList, {
...options,
isHandleFirstElement: false
})
// 追加节点
if (valueList.length) {
const titleId = getUUID()
const titleOptions = editorOptions.title
for (let v = 0; v < valueList.length; v++) {
const value = valueList[v]
value.title = el.title
if (el.level) {
value.titleId = titleId
value.level = el.level
}
// 文本型元素设置字体及加粗
if (isTextLikeElement(value)) {
if (!value.size) {
value.size = titleOptions[titleSizeMapping[value.level!]]
}
if (value.bold === undefined) {
value.bold = true
}
}
elementList.splice(i, 0, value)
i++
}
}
i--
} else if (el.type === ElementType.LIST) {
// 移除父节点
elementList.splice(i, 1)
// 格式化元素
const valueList = el.valueList || []
formatElementList(valueList, {
...options,
isHandleFirstElement: true
})
// 追加节点
if (valueList.length) {
const listId = getUUID()
for (let v = 0; v < valueList.length; v++) {
const value = valueList[v]
value.listId = listId
value.listType = el.listType
value.listStyle = el.listStyle
elementList.splice(i, 0, value)
i++
}
}
i--
} else if (el.type === ElementType.TABLE) {
const tableId = getUUID()
el.id = tableId
if (el.trList) {
const { defaultTrMinHeight } = editorOptions.table
for (let t = 0; t < el.trList.length; t++) {
const tr = el.trList[t]
const trId = getUUID()
tr.id = trId
if (!tr.minHeight || tr.minHeight < defaultTrMinHeight) {
tr.minHeight = defaultTrMinHeight
}
if (tr.height < tr.minHeight) {
tr.height = tr.minHeight
}
for (let d = 0; d < tr.tdList.length; d++) {
const td = tr.tdList[d]
const tdId = getUUID()
td.id = tdId
formatElementList(td.value, {
...options,
isHandleFirstElement: true
})
for (let v = 0; v < td.value.length; v++) {
const value = td.value[v]
value.tdId = tdId
value.trId = trId
value.tableId = tableId
}
}
}
}
} else if (el.type === ElementType.HYPERLINK) {
// 移除父节点
elementList.splice(i, 1)
// 元素展开
const valueList = unzipElementList(el.valueList || [])
// 追加节点
if (valueList.length) {
const hyperlinkId = getUUID()
for (let v = 0; v < valueList.length; v++) {
const value = valueList[v]
value.type = el.type
value.url = el.url
value.hyperlinkId = hyperlinkId
elementList.splice(i, 0, value)
i++
}
}
i--
} else if (el.type === ElementType.DATE) {
// 移除父节点
elementList.splice(i, 1)
// 元素展开
const valueList = unzipElementList(el.valueList || [])
// 追加节点
if (valueList.length) {
const dateId = getUUID()
for (let v = 0; v < valueList.length; v++) {
const value = valueList[v]
value.type = el.type
value.dateFormat = el.dateFormat
value.dateId = dateId
elementList.splice(i, 0, value)
i++
}
}
i--
} else if (el.type === ElementType.CONTROL) {
// 兼容控件内容类型错误
if (!el.control) {
i++
continue
}
const { prefix, postfix, value, placeholder, code, type, valueSets } =
el.control
const {
editorOptions: {
control: controlOption,
checkbox: checkboxOption,
radio: radioOption
}
} = options
const controlId = getUUID()
// 移除父节点
elementList.splice(i, 1)
// 控件上下文提取(压缩后的控件上下文无法提取)
const controlContext = pickObject(el, [
...EDITOR_ELEMENT_CONTEXT_ATTR,
...EDITOR_ROW_ATTR
])
// 控件设置的默认样式(以前缀为基准)
const controlDefaultStyle = pickObject(
<IElement>(<unknown>el.control),
CONTROL_STYLE_ATTR
)
// 前后缀个性化设置
const thePrePostfixArg: Omit<IElement, 'value'> = {
...controlDefaultStyle,
color: editorOptions.control.bracketColor
}
// 前缀
const prefixStrList = splitText(prefix || controlOption.prefix)
for (let p = 0; p < prefixStrList.length; p++) {
const value = prefixStrList[p]
elementList.splice(i, 0, {
...controlContext,
...thePrePostfixArg,
controlId,
value,
type: el.type,
control: el.control,
controlComponent: ControlComponent.PREFIX
})
i++
}
// 值
if (
(value && value.length) ||
type === ControlType.CHECKBOX ||
type === ControlType.RADIO ||
(type === ControlType.SELECT && code && (!value || !value.length))
) {
let valueList: IElement[] = value || []
if (type === ControlType.CHECKBOX) {
const codeList = code ? code.split(',') : []
if (Array.isArray(valueSets) && valueSets.length) {
// 拆分valueList优先使用其属性
const valueStyleList = valueList.reduce(
(pre, cur) =>
pre.concat(
cur.value.split('').map(v => ({ ...cur, value: v }))
),
[] as IElement[]
)
let valueStyleIndex = 0
for (let v = 0; v < valueSets.length; v++) {
const valueSet = valueSets[v]
// checkbox组件
elementList.splice(i, 0, {
...controlContext,
...controlDefaultStyle,
controlId,
value: '',
type: el.type,
control: el.control,
controlComponent: ControlComponent.CHECKBOX,
checkbox: {
code: valueSet.code,
value: codeList.includes(valueSet.code)
}
})
i++
// 文本
const valueStrList = splitText(valueSet.value)
for (let e = 0; e < valueStrList.length; e++) {
const value = valueStrList[e]
const isLastLetter = e === valueStrList.length - 1
elementList.splice(i, 0, {
...controlContext,
...controlDefaultStyle,
...valueStyleList[valueStyleIndex],
controlId,
value: value === '\n' ? ZERO : value,
letterSpacing: isLastLetter ? checkboxOption.gap : 0,
control: el.control,
controlComponent: ControlComponent.VALUE
})
valueStyleIndex++
i++
}
}
}
} else if (type === ControlType.RADIO) {
if (Array.isArray(valueSets) && valueSets.length) {
// 拆分valueList优先使用其属性
const valueStyleList = valueList.reduce(
(pre, cur) =>
pre.concat(
cur.value.split('').map(v => ({ ...cur, value: v }))
),
[] as IElement[]
)
let valueStyleIndex = 0
for (let v = 0; v < valueSets.length; v++) {
const valueSet = valueSets[v]
// radio组件
elementList.splice(i, 0, {
...controlContext,
...controlDefaultStyle,
controlId,
value: '',
type: el.type,
control: el.control,
controlComponent: ControlComponent.RADIO,
radio: {
code: valueSet.code,
value: code === valueSet.code
}
})
i++
// 文本
const valueStrList = splitText(valueSet.value)
for (let e = 0; e < valueStrList.length; e++) {
const value = valueStrList[e]
const isLastLetter = e === valueStrList.length - 1
elementList.splice(i, 0, {
...controlContext,
...controlDefaultStyle,
...valueStyleList[valueStyleIndex],
controlId,
value: value === '\n' ? ZERO : value,
letterSpacing: isLastLetter ? radioOption.gap : 0,
control: el.control,
controlComponent: ControlComponent.VALUE
})
valueStyleIndex++
i++
}
}
}
} else {
if (!value || !value.length) {
if (Array.isArray(valueSets) && valueSets.length) {
const valueSet = valueSets.find(v => v.code === code)
if (valueSet) {
valueList = [
{
value: valueSet.value
}
]
}
}
}
formatElementList(valueList, {
...options,
isHandleFirstElement: false
})
for (let v = 0; v < valueList.length; v++) {
const element = valueList[v]
const value = element.value
elementList.splice(i, 0, {
...controlContext,
...controlDefaultStyle,
...element,
controlId,
value: value === '\n' ? ZERO : value,
type: element.type || ElementType.TEXT,
control: el.control,
controlComponent: ControlComponent.VALUE
})
i++
}
}
} else if (placeholder) {
// placeholder
const thePlaceholderArgs: Omit<IElement, 'value'> = {
...controlDefaultStyle,
color: editorOptions.control.placeholderColor
}
const placeholderStrList = splitText(placeholder)
for (let p = 0; p < placeholderStrList.length; p++) {
const value = placeholderStrList[p]
elementList.splice(i, 0, {
...controlContext,
...thePlaceholderArgs,
controlId,
value: value === '\n' ? ZERO : value,
type: el.type,
control: el.control,
controlComponent: ControlComponent.PLACEHOLDER
})
i++
}
}
// 后缀
const postfixStrList = splitText(postfix || controlOption.postfix)
for (let p = 0; p < postfixStrList.length; p++) {
const value = postfixStrList[p]
elementList.splice(i, 0, {
...controlContext,
...thePrePostfixArg,
controlId,
value,
type: el.type,
control: el.control,
controlComponent: ControlComponent.POSTFIX
})
i++
}
i--
} else if (
(!el.type || TEXTLIKE_ELEMENT_TYPE.includes(el.type)) &&
el.value.length > 1
) {
elementList.splice(i, 1)
const valueList = splitText(el.value)
for (let v = 0; v < valueList.length; v++) {
elementList.splice(i + v, 0, { ...el, value: valueList[v] })
}
el = elementList[i]
}
if (el.value === '\n' || el.value == '\r\n') {
el.value = ZERO
}
if (el.type === ElementType.IMAGE || el.type === ElementType.BLOCK) {
el.id = getUUID()
}
if (el.type === ElementType.LATEX) {
const { svg, width, height } = LaTexParticle.convertLaTextToSVG(el.value)
el.width = el.width || width
el.height = el.height || height
el.laTexSVG = svg
el.id = getUUID()
}
i++
}
}
export function isSameElementExceptValue(
source: IElement,
target: IElement
): boolean {
const sourceKeys = Object.keys(source)
const targetKeys = Object.keys(target)
if (sourceKeys.length !== targetKeys.length) return false
for (let s = 0; s < sourceKeys.length; s++) {
const key = sourceKeys[s] as never
// 值不需要校验
if (key === 'value') continue
// groupIds数组需特殊校验数组是否相等
if (
key === 'groupIds' &&
Array.isArray(source[key]) &&
Array.isArray(target[key]) &&
isArrayEqual(source[key], target[key])
) {
continue
}
if (source[key] !== target[key]) {
return false
}
}
return true
}
interface IPickElementOption {
extraPickAttrs?: Array<keyof IElement>
}
export function pickElementAttr(
payload: IElement,
option: IPickElementOption = {}
): IElement {
const { extraPickAttrs } = option
const zipAttrs = EDITOR_ELEMENT_ZIP_ATTR
if (extraPickAttrs) {
zipAttrs.push(...extraPickAttrs)
}
const element: IElement = {
value: payload.value === ZERO ? `\n` : payload.value
}
zipAttrs.forEach(attr => {
const value = payload[attr] as never
if (value !== undefined) {
element[attr] = value
}
})
return element
}
interface IZipElementListOption {
extraPickAttrs?: Array<keyof IElement>
}
export function zipElementList(
payload: IElement[],
options: IZipElementListOption = {}
): IElement[] {
const { extraPickAttrs } = options
const elementList = deepClone(payload)
const zipElementListData: IElement[] = []
let e = 0
while (e < elementList.length) {
let element = elementList[e]
// 上下文首字符(占位符)-列表首字符要保留避免是复选框
if (
e === 0 &&
element.value === ZERO &&
!element.listId &&
(!element.type || element.type === ElementType.TEXT)
) {
e++
continue
}
// 优先处理虚拟元素,后表格、超链接、日期、控件特殊处理
if (element.titleId && element.level) {
// 标题处理
const titleId = element.titleId
if (titleId) {
const level = element.level
const titleElement: IElement = {
type: ElementType.TITLE,
title: element.title,
value: '',
level
}
const valueList: IElement[] = []
while (e < elementList.length) {
const titleE = elementList[e]
if (titleId !== titleE.titleId) {
e--
break
}
delete titleE.level
delete titleE.title
valueList.push(titleE)
e++
}
titleElement.valueList = zipElementList(valueList, options)
element = titleElement
}
} else if (element.listId && element.listType) {
// 列表处理
const listId = element.listId
if (listId) {
const listType = element.listType
const listStyle = element.listStyle
const listElement: IElement = {
type: ElementType.LIST,
value: '',
listId,
listType,
listStyle
}
const valueList: IElement[] = []
while (e < elementList.length) {
const listE = elementList[e]
if (listId !== listE.listId) {
e--
break
}
delete listE.listType
delete listE.listStyle
valueList.push(listE)
e++
}
listElement.valueList = zipElementList(valueList, options)
element = listElement
}
} else if (element.type === ElementType.TABLE) {
// 分页表格先进行合并
if (element.pagingId) {
let tableIndex = e + 1
let combineCount = 0
while (tableIndex < elementList.length) {
const nextElement = elementList[tableIndex]
if (nextElement.pagingId === element.pagingId) {
element.height! += nextElement.height!
element.trList!.push(...nextElement.trList!)
tableIndex++
combineCount++
} else {
break
}
}
e += combineCount
}
if (element.trList) {
for (let t = 0; t < element.trList.length; t++) {
const tr = element.trList[t]
delete tr.id
for (let d = 0; d < tr.tdList.length; d++) {
const td = tr.tdList[d]
const zipTd: ITd = {
colspan: td.colspan,
rowspan: td.rowspan,
value: zipElementList(td.value, options)
}
// 压缩单元格属性
TABLE_TD_ZIP_ATTR.forEach(attr => {
const value = td[attr] as never
if (value !== undefined) {
zipTd[attr] = value
}
})
tr.tdList[d] = zipTd
}
}
}
} else if (element.type === ElementType.HYPERLINK) {
// 超链接处理
const hyperlinkId = element.hyperlinkId
if (hyperlinkId) {
const hyperlinkElement: IElement = {
type: ElementType.HYPERLINK,
value: '',
url: element.url
}
const valueList: IElement[] = []
while (e < elementList.length) {
const hyperlinkE = elementList[e]
if (hyperlinkId !== hyperlinkE.hyperlinkId) {
e--
break
}
delete hyperlinkE.type
delete hyperlinkE.url
valueList.push(hyperlinkE)
e++
}
hyperlinkElement.valueList = zipElementList(valueList, options)
element = hyperlinkElement
}
} else if (element.type === ElementType.DATE) {
const dateId = element.dateId
if (dateId) {
const dateElement: IElement = {
type: ElementType.DATE,
value: '',
dateFormat: element.dateFormat
}
const valueList: IElement[] = []
while (e < elementList.length) {
const dateE = elementList[e]
if (dateId !== dateE.dateId) {
e--
break
}
delete dateE.type
delete dateE.dateFormat
valueList.push(dateE)
e++
}
dateElement.valueList = zipElementList(valueList, options)
element = dateElement
}
} else if (element.controlId) {
// 控件处理
const controlId = element.controlId
if (controlId) {
// 以前缀为基准更新控件默认样式
const controlDefaultStyle = <IControlSelect>(
(<unknown>pickObject(element, CONTROL_STYLE_ATTR))
)
const control = {
...element.control!,
...controlDefaultStyle
}
const controlElement: IElement = {
...pickObject(element, EDITOR_ROW_ATTR),
type: ElementType.CONTROL,
value: '',
control,
controlId
}
const valueList: IElement[] = []
while (e < elementList.length) {
const controlE = elementList[e]
if (controlId !== controlE.controlId) {
e--
break
}
if (controlE.controlComponent === ControlComponent.VALUE) {
delete controlE.control
delete controlE.controlId
valueList.push(controlE)
}
e++
}
controlElement.control!.value = zipElementList(valueList, options)
element = pickElementAttr(controlElement, { extraPickAttrs })
}
}
// 组合元素
const pickElement = pickElementAttr(element, { extraPickAttrs })
if (
!element.type ||
element.type === ElementType.TEXT ||
element.type === ElementType.SUBSCRIPT ||
element.type === ElementType.SUPERSCRIPT
) {
while (e < elementList.length) {
const nextElement = elementList[e + 1]
e++
if (
nextElement &&
isSameElementExceptValue(
pickElement,
pickElementAttr(nextElement, { extraPickAttrs })
)
) {
const nextValue =
nextElement.value === ZERO ? '\n' : nextElement.value
pickElement.value += nextValue
} else {
break
}
}
} else {
e++
}
zipElementListData.push(pickElement)
}
return zipElementListData
}
export function convertTextAlignToRowFlex(node: HTMLElement) {
const textAlign = window.getComputedStyle(node).textAlign
switch (textAlign) {
case 'left':
case 'start':
return RowFlex.LEFT
case 'center':
return RowFlex.CENTER
case 'right':
case 'end':
return RowFlex.RIGHT
case 'justify':
return RowFlex.ALIGNMENT
case 'justify-all':
return RowFlex.JUSTIFY
default:
return RowFlex.LEFT
}
}
export function convertRowFlexToTextAlign(rowFlex: RowFlex) {
return rowFlex === RowFlex.ALIGNMENT ? 'justify' : rowFlex
}
export function convertRowFlexToJustifyContent(rowFlex: RowFlex) {
switch (rowFlex) {
case RowFlex.LEFT:
return 'flex-start'
case RowFlex.CENTER:
return 'center'
case RowFlex.RIGHT:
return 'flex-end'
case RowFlex.ALIGNMENT:
case RowFlex.JUSTIFY:
return 'space-between'
default:
return 'flex-start'
}
}
export function isTextLikeElement(element: IElement): boolean {
return !element.type || TEXTLIKE_ELEMENT_TYPE.includes(element.type)
}
export function getAnchorElement(
elementList: IElement[],
anchorIndex: number
): IElement | null {
const anchorElement = elementList[anchorIndex]
if (!anchorElement) return null
const anchorNextElement = elementList[anchorIndex + 1]
// 非列表元素 && 当前元素是换行符 && 下一个元素不是换行符 则以下一个元素作为参考元素
return !anchorElement.listId &&
anchorElement.value === ZERO &&
anchorNextElement &&
anchorNextElement.value !== ZERO
? anchorNextElement
: anchorElement
}
export interface IFormatElementContextOption {
isBreakWhenWrap: boolean
}
export function formatElementContext(
sourceElementList: IElement[],
formatElementList: IElement[],
anchorIndex: number,
options?: IFormatElementContextOption
) {
let copyElement = getAnchorElement(sourceElementList, anchorIndex)
if (!copyElement) return
// 标题元素禁用时不复制标题属性
if (copyElement.title?.disabled) {
copyElement = omitObject(copyElement, TITLE_CONTEXT_ATTR)
}
const { isBreakWhenWrap = false } = options || {}
// 是否已经换行
let isBreakWarped = false
for (let e = 0; e < formatElementList.length; e++) {
const targetElement = formatElementList[e]
if (
isBreakWhenWrap &&
!copyElement.listId &&
START_LINE_BREAK_REG.test(targetElement.value)
) {
isBreakWarped = true
}
// 1. 即使换行停止也要处理表格上下文信息
// 2. 定位元素非列表,无需处理粘贴列表的上下文,仅处理表格及行上下文信息
if (
isBreakWarped ||
(!copyElement.listId && targetElement.type === ElementType.LIST)
) {
const cloneAttr = [...TABLE_CONTEXT_ATTR, ...EDITOR_ROW_ATTR]
cloneProperty<IElement>(cloneAttr, copyElement!, targetElement)
targetElement.valueList?.forEach(valueItem => {
cloneProperty<IElement>(cloneAttr, copyElement!, valueItem)
})
continue
}
if (targetElement.valueList?.length) {
formatElementContext(
sourceElementList,
targetElement.valueList,
anchorIndex
)
}
// 非块类元素,需处理行属性
const cloneAttr = [...EDITOR_ELEMENT_CONTEXT_ATTR]
if (!getIsBlockElement(targetElement)) {
cloneAttr.push(...EDITOR_ROW_ATTR)
}
cloneProperty<IElement>(cloneAttr, copyElement, targetElement)
}
}
export function convertElementToDom(
element: IElement,
options: DeepRequired<IEditorOption>
): HTMLElement {
let tagName: keyof HTMLElementTagNameMap = 'span'
if (element.type === ElementType.SUPERSCRIPT) {
tagName = 'sup'
} else if (element.type === ElementType.SUBSCRIPT) {
tagName = 'sub'
}
const dom = document.createElement(tagName)
dom.style.fontFamily = element.font || options.defaultFont
if (element.rowFlex) {
dom.style.textAlign = convertRowFlexToTextAlign(element.rowFlex)
}
if (element.color) {
dom.style.color = element.color
}
if (element.bold) {
dom.style.fontWeight = '600'
}
if (element.italic) {
dom.style.fontStyle = 'italic'
}
dom.style.fontSize = `${element.size || options.defaultSize}px`
if (element.highlight) {
dom.style.backgroundColor = element.highlight
}
if (element.underline) {
dom.style.textDecoration = 'underline'
}
if (element.strikeout) {
dom.style.textDecoration += ' line-through'
}
dom.innerText = element.value.replace(new RegExp(`${ZERO}`, 'g'), '\n')
return dom
}
export function splitListElement(
elementList: IElement[]
): Map<number, IElement[]> {
let curListIndex = 0
const listElementListMap: Map<number, IElement[]> = new Map()
for (let e = 0; e < elementList.length; e++) {
const element = elementList[e]
// 移除列表首行换行字符-如果是复选框直接忽略
if (e === 0) {
if (element.checkbox) continue
element.value = element.value.replace(START_LINE_BREAK_REG, '')
}
if (element.listWrap) {
const listElementList = listElementListMap.get(curListIndex) || []
listElementList.push(element)
listElementListMap.set(curListIndex, listElementList)
} else {
const valueList = element.value.split('\n')
for (let c = 0; c < valueList.length; c++) {
if (c > 0) {
curListIndex += 1
}
const value = valueList[c]
const listElementList = listElementListMap.get(curListIndex) || []
listElementList.push({
...element,
value
})
listElementListMap.set(curListIndex, listElementList)
}
}
}
return listElementListMap
}
export interface IElementListGroupRowFlex {
rowFlex: RowFlex | null
data: IElement[]
}
export function groupElementListByRowFlex(
elementList: IElement[]
): IElementListGroupRowFlex[] {
const elementListGroupList: IElementListGroupRowFlex[] = []
if (!elementList.length) return elementListGroupList
let currentRowFlex: RowFlex | null = elementList[0]?.rowFlex || null
elementListGroupList.push({
rowFlex: currentRowFlex,
data: [elementList[0]]
})
for (let e = 1; e < elementList.length; e++) {
const element = elementList[e]
const rowFlex = element.rowFlex || null
// 行布局相同&非块元素时追加数据,否则新增分组
if (
currentRowFlex === rowFlex &&
!getIsBlockElement(element) &&
!getIsBlockElement(elementList[e - 1])
) {
const lastElementListGroup =
elementListGroupList[elementListGroupList.length - 1]
lastElementListGroup.data.push(element)
} else {
elementListGroupList.push({
rowFlex,
data: [element]
})
currentRowFlex = rowFlex
}
}
// 压缩数据
for (let g = 0; g < elementListGroupList.length; g++) {
const elementListGroup = elementListGroupList[g]
elementListGroup.data = zipElementList(elementListGroup.data)
}
return elementListGroupList
}
export function createDomFromElementList(
elementList: IElement[],
options: DeepRequired<IEditorOption>
) {
function buildDom(payload: IElement[]): HTMLDivElement {
const clipboardDom = document.createElement('div')
for (let e = 0; e < payload.length; e++) {
const element = payload[e]
// 构造表格
if (element.type === ElementType.TABLE) {
const tableDom: HTMLTableElement = document.createElement('table')
tableDom.setAttribute('cellSpacing', '0')
tableDom.setAttribute('cellpadding', '0')
tableDom.setAttribute('border', '0')
const borderStyle = '1px solid #000000'
// 表格边框
if (!element.borderType || element.borderType === TableBorder.ALL) {
tableDom.style.borderTop = borderStyle
tableDom.style.borderLeft = borderStyle
} else if (element.borderType === TableBorder.EXTERNAL) {
tableDom.style.border = borderStyle
}
tableDom.style.width = `${element.width}px`
// colgroup
const colgroupDom = document.createElement('colgroup')
for (let c = 0; c < element.colgroup!.length; c++) {
const colgroup = element.colgroup![c]
const colDom = document.createElement('col')
colDom.setAttribute('width', `${colgroup.width}`)
colgroupDom.append(colDom)
}
tableDom.append(colgroupDom)
// tr
const trList = element.trList!
for (let t = 0; t < trList.length; t++) {
const trDom = document.createElement('tr')
const tr = trList[t]
trDom.style.height = `${tr.height}px`
for (let d = 0; d < tr.tdList.length; d++) {
const tdDom = document.createElement('td')
if (!element.borderType || element.borderType === TableBorder.ALL) {
tdDom.style.borderBottom = tdDom.style.borderRight = '1px solid'
}
const td = tr.tdList[d]
tdDom.colSpan = td.colspan
tdDom.rowSpan = td.rowspan
tdDom.style.verticalAlign = td.verticalAlign || 'top'
// 单元格边框
if (td.borderTypes?.includes(TdBorder.TOP)) {
tdDom.style.borderTop = borderStyle
}
if (td.borderTypes?.includes(TdBorder.RIGHT)) {
tdDom.style.borderRight = borderStyle
}
if (td.borderTypes?.includes(TdBorder.BOTTOM)) {
tdDom.style.borderBottom = borderStyle
}
if (td.borderTypes?.includes(TdBorder.LEFT)) {
tdDom.style.borderLeft = borderStyle
}
const childDom = createDomFromElementList(td.value!, options)
tdDom.innerHTML = childDom.innerHTML
if (td.backgroundColor) {
tdDom.style.backgroundColor = td.backgroundColor
}
trDom.append(tdDom)
}
tableDom.append(trDom)
}
clipboardDom.append(tableDom)
} else if (element.type === ElementType.HYPERLINK) {
const a = document.createElement('a')
a.innerText = element.valueList!.map(v => v.value).join('')
if (element.url) {
a.href = element.url
}
clipboardDom.append(a)
} else if (element.type === ElementType.TITLE) {
const h = document.createElement(
`h${titleOrderNumberMapping[element.level!]}`
)
const childDom = buildDom(element.valueList!)
h.innerHTML = childDom.innerHTML
clipboardDom.append(h)
} else if (element.type === ElementType.LIST) {
const list = document.createElement(
listTypeElementMapping[element.listType!]
)
if (element.listStyle) {
list.style.listStyleType = listStyleCSSMapping[element.listStyle]
}
// 按照换行符拆分
const zipList = zipElementList(element.valueList!)
const listElementListMap = splitListElement(zipList)
listElementListMap.forEach(listElementList => {
const li = document.createElement('li')
const childDom = buildDom(listElementList)
li.innerHTML = childDom.innerHTML
list.append(li)
})
clipboardDom.append(list)
} else if (element.type === ElementType.IMAGE) {
const img = document.createElement('img')
if (element.value) {
img.src = element.value
img.width = element.width!
img.height = element.height!
}
clipboardDom.append(img)
} else if (element.type === ElementType.SEPARATOR) {
const hr = document.createElement('hr')
clipboardDom.append(hr)
} else if (element.type === ElementType.CHECKBOX) {
const checkbox = document.createElement('input')
checkbox.type = 'checkbox'
if (element.checkbox?.value) {
checkbox.setAttribute('checked', 'true')
}
clipboardDom.append(checkbox)
} else if (element.type === ElementType.RADIO) {
const radio = document.createElement('input')
radio.type = 'radio'
if (element.radio?.value) {
radio.setAttribute('checked', 'true')
}
clipboardDom.append(radio)
} else if (element.type === ElementType.TAB) {
const tab = document.createElement('span')
tab.innerHTML = `${NON_BREAKING_SPACE}${NON_BREAKING_SPACE}`
clipboardDom.append(tab)
} else if (element.type === ElementType.CONTROL) {
const controlElement = document.createElement('span')
const childDom = buildDom(element.control?.value || [])
controlElement.innerHTML = childDom.innerHTML
clipboardDom.append(controlElement)
} else if (
!element.type ||
element.type === ElementType.LATEX ||
TEXTLIKE_ELEMENT_TYPE.includes(element.type)
) {
let text = ''
if (element.type === ElementType.DATE) {
text = element.valueList?.map(v => v.value).join('') || ''
} else {
text = element.value
}
if (!text) continue
const dom = convertElementToDom(element, options)
// 前一个元素是标题,移除首行换行符
if (payload[e - 1]?.type === ElementType.TITLE) {
text = text.replace(/^\n/, '')
}
dom.innerText = text.replace(new RegExp(`${ZERO}`, 'g'), '\n')
clipboardDom.append(dom)
}
}
return clipboardDom
}
// 按行布局分类创建dom
const clipboardDom = document.createElement('div')
const groupElementList = groupElementListByRowFlex(elementList)
for (let g = 0; g < groupElementList.length; g++) {
const elementGroupRowFlex = groupElementList[g]
// 行布局样式设置
const isDefaultRowFlex =
!elementGroupRowFlex.rowFlex ||
elementGroupRowFlex.rowFlex === RowFlex.LEFT
// 块元素使用flex否则使用text-align
const rowFlexDom = document.createElement('div')
if (!isDefaultRowFlex) {
const firstElement = elementGroupRowFlex.data[0]
if (getIsBlockElement(firstElement)) {
rowFlexDom.style.display = 'flex'
rowFlexDom.style.justifyContent = convertRowFlexToJustifyContent(
firstElement.rowFlex!
)
} else {
rowFlexDom.style.textAlign = convertRowFlexToTextAlign(
elementGroupRowFlex.rowFlex!
)
}
}
// 布局内容
rowFlexDom.innerHTML = buildDom(elementGroupRowFlex.data).innerHTML
// 未设置行布局时无需行布局容器
if (!isDefaultRowFlex) {
clipboardDom.append(rowFlexDom)
} else {
rowFlexDom.childNodes.forEach(child => {
clipboardDom.append(child.cloneNode(true))
})
}
}
return clipboardDom
}
export function convertTextNodeToElement(
textNode: Element | Node
): IElement | null {
if (!textNode || textNode.nodeType !== 3) return null
const parentNode = <HTMLElement>textNode.parentNode
const anchorNode =
parentNode.nodeName === 'FONT'
? <HTMLElement>parentNode.parentNode
: parentNode
const rowFlex = convertTextAlignToRowFlex(anchorNode)
const value = textNode.textContent
const style = window.getComputedStyle(anchorNode)
if (!value || anchorNode.nodeName === 'STYLE') return null
const element: IElement = {
value,
color: style.color,
bold: Number(style.fontWeight) > 500,
italic: style.fontStyle.includes('italic'),
size: Math.floor(parseFloat(style.fontSize))
}
// 元素类型-默认文本
if (anchorNode.nodeName === 'SUB' || style.verticalAlign === 'sub') {
element.type = ElementType.SUBSCRIPT
} else if (anchorNode.nodeName === 'SUP' || style.verticalAlign === 'super') {
element.type = ElementType.SUPERSCRIPT
}
// 行对齐
if (rowFlex !== RowFlex.LEFT) {
element.rowFlex = rowFlex
}
// 高亮色
if (style.backgroundColor !== 'rgba(0, 0, 0, 0)') {
element.highlight = style.backgroundColor
}
// 下划线
if (style.textDecorationLine.includes('underline')) {
element.underline = true
}
// 删除线
if (style.textDecorationLine.includes('line-through')) {
element.strikeout = true
}
return element
}
interface IGetElementListByHTMLOption {
innerWidth: number
}
export function getElementListByHTML(
htmlText: string,
options: IGetElementListByHTMLOption
): IElement[] {
const elementList: IElement[] = []
function findTextNode(dom: Element | Node) {
if (dom.nodeType === 3) {
const element = convertTextNodeToElement(dom)
if (element) {
elementList.push(element)
}
} else if (dom.nodeType === 1) {
const childNodes = dom.childNodes
for (let n = 0; n < childNodes.length; n++) {
const node = childNodes[n]
// br元素与display:block元素需换行
if (node.nodeName === 'BR') {
elementList.push({
value: '\n'
})
} else if (node.nodeName === 'A') {
const aElement = node as HTMLLinkElement
const value = aElement.innerText
if (value) {
elementList.push({
type: ElementType.HYPERLINK,
value: '',
valueList: [
{
value
}
],
url: aElement.href
})
}
} else if (/H[1-6]/.test(node.nodeName)) {
const hElement = node as HTMLTitleElement
const valueList = getElementListByHTML(
replaceHTMLElementTag(hElement, 'div').outerHTML,
options
)
elementList.push({
value: '',
type: ElementType.TITLE,
level: titleNodeNameMapping[node.nodeName],
valueList
})
if (
node.nextSibling &&
!INLINE_NODE_NAME.includes(node.nextSibling.nodeName)
) {
elementList.push({
value: '\n'
})
}
} else if (node.nodeName === 'UL' || node.nodeName === 'OL') {
const listNode = node as HTMLOListElement | HTMLUListElement
const listElement: IElement = {
value: '',
type: ElementType.LIST,
valueList: []
}
if (node.nodeName === 'OL') {
listElement.listType = ListType.OL
} else {
listElement.listType = ListType.UL
listElement.listStyle = <ListStyle>(
(<unknown>listNode.style.listStyleType)
)
}
listNode.querySelectorAll('li').forEach(li => {
const liValueList = getElementListByHTML(li.innerHTML, options)
liValueList.forEach(list => {
if (list.value === '\n') {
list.listWrap = true
}
})
liValueList.unshift({
value: '\n'
})
listElement.valueList!.push(...liValueList)
})
elementList.push(listElement)
} else if (node.nodeName === 'HR') {
elementList.push({
value: '\n',
type: ElementType.SEPARATOR
})
} else if (node.nodeName === 'IMG') {
const { src, width, height } = node as HTMLImageElement
if (src && width && height) {
elementList.push({
width,
height,
value: src,
type: ElementType.IMAGE
})
}
} else if (node.nodeName === 'TABLE') {
const tableElement = node as HTMLTableElement
const element: IElement = {
type: ElementType.TABLE,
value: '\n',
colgroup: [],
trList: []
}
// 基础数据
tableElement.querySelectorAll('tr').forEach(trElement => {
const trHeightStr = window
.getComputedStyle(trElement)
.height.replace('px', '')
const tr: ITr = {
height: Number(trHeightStr),
tdList: []
}
trElement.querySelectorAll('th,td').forEach(tdElement => {
const tableCell = <HTMLTableCellElement>tdElement
const valueList = getElementListByHTML(
tableCell.innerHTML,
options
)
const td: ITd = {
colspan: tableCell.colSpan,
rowspan: tableCell.rowSpan,
value: valueList
}
if (tableCell.style.backgroundColor) {
td.backgroundColor = tableCell.style.backgroundColor
}
tr.tdList.push(td)
})
element.trList!.push(tr)
})
if (element.trList!.length) {
// 列选项数据
const tdCount = element.trList![0].tdList.reduce(
(pre, cur) => pre + cur.colspan,
0
)
const width = Math.ceil(options.innerWidth / tdCount)
for (let i = 0; i < tdCount; i++) {
element.colgroup!.push({
width
})
}
elementList.push(element)
}
} else if (
node.nodeName === 'INPUT' &&
(<HTMLInputElement>node).type === ControlComponent.CHECKBOX
) {
elementList.push({
type: ElementType.CHECKBOX,
value: '',
checkbox: {
value: (<HTMLInputElement>node).checked
}
})
} else if (
node.nodeName === 'INPUT' &&
(<HTMLInputElement>node).type === ControlComponent.RADIO
) {
elementList.push({
type: ElementType.RADIO,
value: '',
radio: {
value: (<HTMLInputElement>node).checked
}
})
} else {
findTextNode(node)
if (node.nodeType === 1 && n !== childNodes.length - 1) {
const display = window.getComputedStyle(node as Element).display
if (display === 'block') {
elementList.push({
value: '\n'
})
}
}
}
}
}
}
// 追加dom
const clipboardDom = document.createElement('div')
clipboardDom.innerHTML = htmlText
document.body.appendChild(clipboardDom)
const deleteNodes: ChildNode[] = []
clipboardDom.childNodes.forEach(child => {
if (child.nodeType !== 1 && !child.textContent?.trim()) {
deleteNodes.push(child)
}
})
deleteNodes.forEach(node => node.remove())
// 搜索文本节点
findTextNode(clipboardDom)
// 移除dom
clipboardDom.remove()
return elementList
}
export function getTextFromElementList(elementList: IElement[]) {
function buildText(payload: IElement[]): string {
let text = ''
for (let e = 0; e < payload.length; e++) {
const element = payload[e]
// 构造表格
if (element.type === ElementType.TABLE) {
text += `\n`
const trList = element.trList!
for (let t = 0; t < trList.length; t++) {
const tr = trList[t]
for (let d = 0; d < tr.tdList.length; d++) {
const td = tr.tdList[d]
const tdText = buildText(zipElementList(td.value!))
const isFirst = d === 0
const isLast = tr.tdList.length - 1 === d
text += `${!isFirst ? ` ` : ``}${tdText}${isLast ? `\n` : ``}`
}
}
} else if (element.type === ElementType.TAB) {
text += `\t`
} else if (element.type === ElementType.HYPERLINK) {
text += element.valueList!.map(v => v.value).join('')
} else if (element.type === ElementType.TITLE) {
text += `${buildText(zipElementList(element.valueList!))}`
} else if (element.type === ElementType.LIST) {
// 按照换行符拆分
const zipList = zipElementList(element.valueList!)
const listElementListMap = splitListElement(zipList)
// 无序列表前缀
let ulListStyleText = ''
if (element.listType === ListType.UL) {
ulListStyleText =
ulStyleMapping[<UlStyle>(<unknown>element.listStyle)]
}
listElementListMap.forEach((listElementList, listIndex) => {
const isLast = listElementListMap.size - 1 === listIndex
text += `\n${ulListStyleText || `${listIndex + 1}.`}${buildText(
listElementList
)}${isLast ? `\n` : ``}`
})
} else if (element.type === ElementType.CHECKBOX) {
text += element.checkbox?.value ? `` : ``
} else if (element.type === ElementType.RADIO) {
text += element.radio?.value ? `` : ``
} else if (
!element.type ||
element.type === ElementType.LATEX ||
TEXTLIKE_ELEMENT_TYPE.includes(element.type)
) {
let textLike = ''
if (element.type === ElementType.CONTROL) {
textLike = element.control!.value?.[0]?.value || ''
} else if (element.type === ElementType.DATE) {
textLike = element.valueList?.map(v => v.value).join('') || ''
} else {
textLike = element.value
}
text += textLike.replace(new RegExp(`${ZERO}`, 'g'), '\n')
}
}
return text
}
return buildText(zipElementList(elementList))
}
export function getSlimCloneElementList(elementList: IElement[]) {
return deepCloneOmitKeys<IElement[], IRowElement>(elementList, [
'metrics',
'style'
])
}
export function getIsBlockElement(element?: IElement) {
return (
!!element?.type &&
(BLOCK_ELEMENT_TYPE.includes(element.type) ||
element.imgDisplay === ImageDisplay.INLINE)
)
}
export function replaceHTMLElementTag(
oldDom: HTMLElement,
tagName: keyof HTMLElementTagNameMap
): HTMLElement {
const newDom = document.createElement(tagName)
for (let i = 0; i < oldDom.attributes.length; i++) {
const attr = oldDom.attributes[i]
newDom.setAttribute(attr.name, attr.value)
}
newDom.innerHTML = oldDom.innerHTML
return newDom
}