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.

592 lines
19 KiB

import { deepClone, getUUID, splitText } from '.'
import { ElementType, IEditorOption, IElement, RowFlex } from '..'
import { LaTexParticle } from '../core/draw/particle/latex/LaTexParticle'
import { defaultCheckboxOption } from '../dataset/constant/Checkbox'
import { ZERO } from '../dataset/constant/Common'
import { defaultControlOption } from '../dataset/constant/Control'
import { EDITOR_ELEMENT_CONTEXT_ATTR, EDITOR_ELEMENT_ZIP_ATTR, TEXTLIKE_ELEMENT_TYPE } from '../dataset/constant/Element'
import { titleSizeMapping } from '../dataset/constant/Title'
import { ControlComponent, ControlType } from '../dataset/enum/Control'
import { ITd } from '../interface/table/Td'
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: Required<IEditorOption>;
}
export function formatElementList(elementList: IElement[], options: IFormatElementListOption) {
const { isHandleFirstElement, editorOptions } = <IFormatElementListOption>{
isHandleFirstElement: true,
...options
}
const startElement = elementList[0]
if (isHandleFirstElement && startElement?.value !== ZERO && startElement?.value !== '\n') {
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]
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
})
// 追加节点
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) {
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 < editorOptions.defaultTrMinHeight) {
tr.minHeight = editorOptions.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) {
const { prefix, postfix, value, placeholder, code, type, valueSets } = el.control!
const controlId = getUUID()
// 移除父节点
elementList.splice(i, 1)
// 前后缀个性化设置
const thePrePostfixArgs: Pick<IElement, 'color'> = {}
if (editorOptions && editorOptions.control) {
thePrePostfixArgs.color = editorOptions.control.bracketColor
}
// 前缀
const prefixStrList = splitText(prefix || defaultControlOption.prefix)
for (let p = 0; p < prefixStrList.length; p++) {
const value = prefixStrList[p]
elementList.splice(i, 0, {
controlId,
value,
type: el.type,
control: el.control,
controlComponent: ControlComponent.PREFIX,
...thePrePostfixArgs
})
i++
}
// 值
if (
(value && value.length) ||
type === ControlType.CHECKBOX ||
(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, {
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, {
...valueStyleList[valueStyleIndex],
controlId,
value,
type: el.type,
letterSpacing: isLastLetter ? defaultCheckboxOption.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
}]
}
}
}
for (let v = 0; v < valueList.length; v++) {
const element = valueList[v]
const valueStrList = splitText(element.value)
for (let e = 0; e < valueStrList.length; e++) {
const value = valueStrList[e]
elementList.splice(i, 0, {
...element,
controlId,
value,
type: el.type,
control: el.control,
controlComponent: ControlComponent.VALUE
})
i++
}
}
}
} else if (placeholder) {
// placeholder
const thePlaceholderArgs: Pick<IElement, 'color'> = {}
if (editorOptions && editorOptions.control) {
thePlaceholderArgs.color = editorOptions.control.placeholderColor
}
const placeholderStrList = splitText(placeholder)
for (let p = 0; p < placeholderStrList.length; p++) {
const value = placeholderStrList[p]
elementList.splice(i, 0, {
controlId,
value,
type: el.type,
control: el.control,
controlComponent: ControlComponent.PLACEHOLDER,
...thePlaceholderArgs
})
i++
}
}
// 后缀
const postfixStrList = splitText(postfix || defaultControlOption.postfix)
for (let p = 0; p < postfixStrList.length; p++) {
const value = postfixStrList[p]
elementList.splice(i, 0, {
controlId,
value,
type: el.type,
control: el.control,
controlComponent: ControlComponent.POSTFIX,
...thePrePostfixArgs
})
i++
}
i--
} else if ((!el.type || el.type === ElementType.TEXT) && 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 = 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
if (source[key] !== target[key]) {
return false
}
}
return true
}
export function pickElementAttr(payload: IElement): IElement {
const element: IElement = {
value: payload.value === ZERO ? `\n` : payload.value,
}
EDITOR_ELEMENT_ZIP_ATTR.forEach(attr => {
const value = payload[attr] as never
if (value !== undefined) {
element[attr] = value
}
})
return element
}
export function zipElementList(payload: IElement[]): IElement[] {
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.type || element.type === ElementType.TEXT)) {
e++
continue
}
// 优先处理虚拟元素,后表格、超链接、日期、控件特殊处理
if (element.titleId && element.level) {
// 标题处理
const titleId = element.titleId
const level = element.level
const titleElement: IElement = {
type: ElementType.TITLE,
value: '',
level
}
const valueList: IElement[] = []
while (e < elementList.length) {
const titleE = elementList[e]
if (titleId !== titleE.titleId) {
e--
break
}
delete titleE.level
valueList.push(titleE)
e++
}
titleElement.valueList = zipElementList(valueList)
element = titleElement
} else if (element.listId && element.listType) {
// 列表处理
const listId = element.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)
element = listElement
} else if (element.type === ElementType.TABLE) {
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)
}
if (td.verticalAlign) {
zipTd.verticalAlign = td.verticalAlign
}
if (td.backgroundColor) {
zipTd.backgroundColor = td.backgroundColor
}
tr.tdList[d] = zipTd
}
}
}
} else if (element.type === ElementType.HYPERLINK) {
// 超链接处理
const hyperlinkId = element.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)
element = hyperlinkElement
} else if (element.type === ElementType.DATE) {
const dateId = element.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)
element = dateElement
} else if (element.type === ElementType.CONTROL) {
// 控件处理
const controlId = element.controlId
const control = element.control!
const controlElement: IElement = {
type: ElementType.CONTROL,
value: '',
control
}
const valueList: IElement[] = []
while (e < elementList.length) {
const controlE = elementList[e]
if (controlId !== controlE.controlId) {
e--
break
}
if (controlE.controlComponent === ControlComponent.VALUE) {
delete controlE.type
delete controlE.control
valueList.push(controlE)
}
e++
}
controlElement.control!.value = zipElementList(valueList)
element = controlElement
}
// 组合元素
const pickElement = pickElementAttr(element)
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))
) {
const nextValue = nextElement.value === ZERO ? '\n' : nextElement.value
pickElement.value += nextValue
} else {
break
}
}
} else {
e++
}
zipElementListData.push(pickElement)
}
return zipElementListData
}
export function getElementRowFlex(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
default:
return RowFlex.LEFT
}
}
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) {
const copyElement = getAnchorElement(sourceElementList, anchorIndex)
if (!copyElement) return
const { isBreakWhenWrap = false } = options || {}
for (let e = 0; e < formatElementList.length; e++) {
const targetElement = formatElementList[e]
if (isBreakWhenWrap && !copyElement.listId && /^\n/.test(targetElement.value)) break
// 定位元素非列表,无需处理粘贴列表的上下文
if (!copyElement.listId && targetElement.type === ElementType.LIST) continue
if (targetElement.valueList && targetElement.valueList.length) {
formatElementContext(sourceElementList, targetElement.valueList, anchorIndex)
}
for (let i = 0; i < EDITOR_ELEMENT_CONTEXT_ATTR.length; i++) {
const attr = EDITOR_ELEMENT_CONTEXT_ATTR[i]
const value = copyElement[attr] as never
if (value !== undefined) {
targetElement[attr] = value
} else {
delete targetElement[attr]
}
}
}
}