diff --git a/cypress/integration/control/select.spec.ts b/cypress/integration/control/select.spec.ts new file mode 100644 index 0000000..0910363 --- /dev/null +++ b/cypress/integration/control/select.spec.ts @@ -0,0 +1,54 @@ +import Editor, { ControlType, ElementType } from '../../../src/editor' + +describe('控件-列举型', () => { + + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + const text = `有` + const elementType: ElementType = 'control' + const controlType: ControlType = 'select' + + it('列举型', () => { + cy.getEditor().then((editor: Editor) => { + editor.listener.saved = function (payload) { + const data = payload.data[0] + + expect(data.control!.value![0].value).to.be.eq(text) + + expect(data.control!.code).to.be.eq('98175') + } + + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([{ + type: elementType, + value: '', + control: { + type: controlType, + value: null, + placeholder: '列举型', + valueSets: [{ + value: '有', + code: '98175' + }, { + value: '无', + code: '98176' + }] + } + }]) + + cy.get('@canvas').type(`{leftArrow}`) + + cy.get('.select-control-popup li').eq(0).click() + + cy.get('@canvas').type('{ctrl}s') + }) + }) + +}) diff --git a/cypress/integration/control/text.spec.ts b/cypress/integration/control/text.spec.ts new file mode 100644 index 0000000..fb392f6 --- /dev/null +++ b/cypress/integration/control/text.spec.ts @@ -0,0 +1,45 @@ +import Editor, { ControlType, ElementType } from '../../../src/editor' + +describe('控件-文本型', () => { + + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + const text = `canvas-editor` + const elementType: ElementType = 'control' + const controlType: ControlType = 'text' + + it('文本型', () => { + cy.getEditor().then((editor: Editor) => { + editor.listener.saved = function (payload) { + const data = payload.data[0] + + expect(data.control!.value![0].value).to.be.eq(text) + } + + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([{ + type: elementType, + value: '', + control: { + type: controlType, + value: null, + placeholder: '文本型' + } + }]) + + cy.get('@canvas').type(`{leftArrow}`) + + cy.get('.inputarea').type(text) + + cy.get('@canvas').type('{ctrl}s') + }) + }) + +}) diff --git a/src/editor/assets/css/control/select.css b/src/editor/assets/css/control/select.css new file mode 100644 index 0000000..010c9ad --- /dev/null +++ b/src/editor/assets/css/control/select.css @@ -0,0 +1,44 @@ +.select-control-popup { + max-width: 160px; + min-width: 69px; + max-height: 225px; + position: absolute; + z-index: 1; + border: 1px solid #e4e7ed; + border-radius: 4px; + background-color: #fff; + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1); + box-sizing: border-box; + margin: 5px 0; + overflow-y: auto; +} + +.select-control-popup ul { + list-style: none; + padding: 3px 0; + margin: 0; + box-sizing: border-box; +} + +.select-control-popup ul li { + font-size: 13px; + padding: 0 20px; + position: relative; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: #666; + height: 36px; + line-height: 36px; + box-sizing: border-box; + cursor: pointer; +} + +.select-control-popup ul li:hover { + background-color: #EEF2FD; +} + +.select-control-popup ul li.active { + color: var(--COLOR-HOVER, #5175f4); + font-weight: 700; +} \ No newline at end of file diff --git a/src/editor/assets/css/index.css b/src/editor/assets/css/index.css index d976bd2..2b3c201 100644 --- a/src/editor/assets/css/index.css +++ b/src/editor/assets/css/index.css @@ -1,3 +1,5 @@ +@import './control/select.css'; + .inputarea { width: 0; height: 12px; diff --git a/src/editor/core/command/CommandAdapt.ts b/src/editor/core/command/CommandAdapt.ts index bb22617..ccfa1e4 100644 --- a/src/editor/core/command/CommandAdapt.ts +++ b/src/editor/core/command/CommandAdapt.ts @@ -1236,7 +1236,10 @@ export class CommandAdapt { const { startIndex, endIndex } = this.range.getRange() if (!~startIndex && !~endIndex) return // 格式化element - formatElementList(payload, false) + formatElementList(payload, { + isHandleFirstElement: false, + editorOptions: this.options + }) const elementList = this.draw.getElementList() const isCollapsed = startIndex === endIndex const start = startIndex + 1 diff --git a/src/editor/core/draw/Draw.ts b/src/editor/core/draw/Draw.ts index 736537a..55c8f5a 100644 --- a/src/editor/core/draw/Draw.ts +++ b/src/editor/core/draw/Draw.ts @@ -34,6 +34,7 @@ import { SeparatorParticle } from './particle/Separator' import { PageBreakParticle } from './particle/PageBreak' import { Watermark } from './frame/Watermark' import { EditorMode } from '../../dataset/enum/Editor' +import { Control } from './control/Control' export class Draw { @@ -70,6 +71,7 @@ export class Draw { private pageBreakParticle: PageBreakParticle private superscriptParticle: SuperscriptParticle private subscriptParticle: SubscriptParticle + private control: Control private rowList: IRow[] private painterStyle: IElementStyle | null @@ -116,6 +118,7 @@ export class Draw { this.pageBreakParticle = new PageBreakParticle(this) this.superscriptParticle = new SuperscriptParticle() this.subscriptParticle = new SubscriptParticle() + this.control = new Control(this) new ScrollObserver(this) new SelectionObserver() @@ -301,6 +304,10 @@ export class Draw { return this.hyperlinkParticle } + public getControl(): Control { + return this.control + } + public getRowCount(): number { return this.rowList.length } @@ -880,8 +887,8 @@ export class Draw { this.historyManager.execute(function () { self.setPageNo(pageNo) self.position.setPositionContext(oldPositionContext) - self.range.setRange(startIndex, endIndex) self.elementList = deepClone(oldElementList) + self.range.setRange(startIndex, endIndex) self.render({ curIndex, isSubmitHistory: false }) }) } diff --git a/src/editor/core/draw/control/Control.ts b/src/editor/core/draw/control/Control.ts new file mode 100644 index 0000000..1113a21 --- /dev/null +++ b/src/editor/core/draw/control/Control.ts @@ -0,0 +1,278 @@ +import { ControlComponent, ControlType } from '../../../dataset/enum/Control' +import { ElementType } from '../../../dataset/enum/Element' +import { IControlInitOption, IControlInstance, IControlOption } from '../../../interface/Control' +import { IElement, IElementPosition } from '../../../interface/Element' +import { RangeManager } from '../../range/RangeManager' +import { Draw } from '../Draw' +import { SelectControl } from './select/SelectControl' +import { TextControl } from './text/TextControl' + +interface IMoveCursorResult { + newIndex: number; + newElement: IElement; +} +export class Control { + + private draw: Draw + private range: RangeManager + private options: IControlOption + private activeControl: IControlInstance | null + + constructor(draw: Draw) { + this.draw = draw + this.range = draw.getRange() + this.options = draw.getOptions().control + this.activeControl = null + } + + // 判断选区部分在控件边界外 + public isPartRangeInControlOutside(): boolean { + const { startIndex, endIndex } = this.getRange() + if (!~startIndex && !~endIndex) return false + const elementList = this.getElementList() + const startElement = elementList[startIndex] + const endElement = elementList[endIndex] + if ( + (startElement.type === ElementType.CONTROL || endElement.type === ElementType.CONTROL) + && startElement.controlId !== endElement.controlId + ) { + return true + } + return false + } + + public getContainer(): HTMLDivElement { + return this.draw.getContainer() + } + + public getElementList(): IElement[] { + return this.draw.getElementList() + } + + public getPosition(): IElementPosition | null { + const positionList = this.draw.getPosition().getPositionList() + const { endIndex } = this.range.getRange() + return positionList[endIndex] || null + } + + public getPreY(): number { + const height = this.draw.getHeight() + const pageGap = this.draw.getPageGap() + return this.draw.getPageNo() * (height + pageGap) + } + + public getRange() { + return this.range.getRange() + } + + public shrinkBoundary() { + this.range.shrinkBoundary() + } + + public getActiveControl(): IControlInstance | null { + return this.activeControl + } + + public initControl() { + const elementList = this.getElementList() + const range = this.getRange() + const element = elementList[range.startIndex] + // 判断控件是否已经激活 + if (this.activeControl) { + // 列举控件唤醒下拉弹窗 + if (this.activeControl instanceof SelectControl) { + this.activeControl.awake() + } + const controlElement = this.activeControl.getElement() + if (element.controlId === controlElement.controlId) return + } + // 销毁旧激活控件 + this.destroyControl() + // 激活控件 + const control = element.control! + if (control.type === ControlType.TEXT) { + this.activeControl = new TextControl(element, this) + } else if (control.type === ControlType.SELECT) { + const selectControl = new SelectControl(element, this) + this.activeControl = selectControl + selectControl.awake() + } + } + + public destroyControl() { + if (this.activeControl) { + if (this.activeControl instanceof SelectControl) { + this.activeControl.destroy() + } + this.activeControl = null + } + } + + public repaintControl(curIndex: number) { + this.range.setRange(curIndex, curIndex) + this.draw.render({ + curIndex + }) + } + + public moveCursor(position: IControlInitOption): IMoveCursorResult { + const { index, trIndex, tdIndex, tdValueIndex } = position + let elementList = this.draw.getOriginalElementList() + let element: IElement + const newIndex = position.isTable ? tdValueIndex! : index + if (position.isTable) { + elementList = elementList[index!].trList![trIndex!].tdList[tdIndex!].value + element = elementList[tdValueIndex!] + } else { + element = elementList[index] + } + if (element.controlComponent === ControlComponent.VALUE) { + // VALUE-无需移动 + return { + newIndex, + newElement: element + } + } else if (element.controlComponent === ControlComponent.POSTFIX) { + // POSTFIX-移动到最后一个后缀字符后 + let startIndex = index + 1 + while (startIndex < elementList.length) { + const nextElement = elementList[startIndex] + if (nextElement.controlId !== element.controlId) { + return { + newIndex: startIndex - 1, + newElement: elementList[startIndex - 1] + } + } + startIndex++ + } + } else if (element.controlComponent === ControlComponent.PREFIX) { + // PREFIX-移动到最后一个前缀字符后 + let startIndex = index + 1 + while (startIndex < elementList.length) { + const nextElement = elementList[startIndex] + if ( + nextElement.controlId !== element.controlId + || nextElement.controlComponent !== ControlComponent.PREFIX + ) { + return { + newIndex: startIndex - 1, + newElement: elementList[startIndex - 1] + } + } + startIndex++ + } + } else if (element.controlComponent === ControlComponent.PLACEHOLDER) { + // PLACEHOLDER-移动到第一个前缀后 + let startIndex = index - 1 + while (startIndex > 0) { + const preElement = elementList[startIndex] + if ( + preElement.controlId !== element.controlId + || preElement.controlComponent === ControlComponent.PREFIX + ) { + return { + newIndex: startIndex, + newElement: elementList[startIndex] + } + } + startIndex-- + } + } + return { + newIndex, + newElement: element + } + } + + public removeControl(startIndex: number): number { + const elementList = this.getElementList() + const startElement = elementList[startIndex] + let leftIndex = -1 + let rightIndex = -1 + // 向左查找 + let preIndex = startIndex + while (preIndex > 0) { + const preElement = elementList[preIndex] + if (preElement.controlId !== startElement.controlId) { + leftIndex = preIndex + break + } + preIndex-- + } + // 向右查找 + let nextIndex = startIndex + 1 + while (nextIndex < elementList.length) { + const nextElement = elementList[nextIndex] + if (nextElement.controlId !== startElement.controlId) { + rightIndex = nextIndex - 1 + break + } + nextIndex++ + } + if (!~leftIndex || !~rightIndex) return startIndex + // 删除元素 + elementList.splice(leftIndex + 1, rightIndex - leftIndex) + return leftIndex + } + + public removePlaceholder(startIndex: number) { + const elementList = this.getElementList() + const startElement = elementList[startIndex] + const nextElement = elementList[startIndex + 1] + if ( + startElement.controlComponent === ControlComponent.PLACEHOLDER || + nextElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + let index = startIndex + while (index < elementList.length) { + const curElement = elementList[index] + if (curElement.controlId !== startElement.controlId) break + if (curElement.controlComponent === ControlComponent.PLACEHOLDER) { + elementList.splice(index, 1) + } else { + index++ + } + } + } + } + + public addPlaceholder(startIndex: number) { + const elementList = this.getElementList() + const startElement = elementList[startIndex] + const control = startElement.control! + const placeholderStrList = control.placeholder.split('') + for (let p = 0; p < placeholderStrList.length; p++) { + const value = placeholderStrList[p] + elementList.splice(startIndex + p + 1, 0, { + value, + controlId: startElement.controlId, + type: ElementType.CONTROL, + control: startElement.control, + controlComponent: ControlComponent.PLACEHOLDER, + color: this.options.placeholderColor + }) + } + } + + public setValue(data: IElement[]): number { + if (!this.activeControl) { + throw new Error('active control is null') + } + return this.activeControl.setValue(data) + } + + public keydown(evt: KeyboardEvent): number { + if (!this.activeControl) { + throw new Error('active control is null') + } + return this.activeControl.keydown(evt) + } + + public cut(): number { + if (!this.activeControl) { + throw new Error('active control is null') + } + return this.activeControl.cut() + } + +} \ No newline at end of file diff --git a/src/editor/core/draw/control/select/SelectControl.ts b/src/editor/core/draw/control/select/SelectControl.ts new file mode 100644 index 0000000..9677995 --- /dev/null +++ b/src/editor/core/draw/control/select/SelectControl.ts @@ -0,0 +1,254 @@ +import { EDITOR_COMPONENT } from '../../../../dataset/constant/Editor' +import { ControlComponent } from '../../../../dataset/enum/Control' +import { EditorComponent } from '../../../../dataset/enum/Editor' +import { KeyMap } from '../../../../dataset/enum/Keymap' +import { IControlInstance } from '../../../../interface/Control' +import { IElement } from '../../../../interface/Element' +import { Control } from '../Control' + +export class SelectControl implements IControlInstance { + + private element: IElement + private control: Control + private isPopup: boolean + private selectDom: HTMLDivElement | null + + constructor(element: IElement, control: Control) { + this.element = element + this.control = control + this.isPopup = false + this.selectDom = null + } + + public getElement(): IElement { + return this.element + } + + public getCode(): string | null { + return this.element.control?.code || null + } + + public getValue(): IElement[] { + const elementList = this.control.getElementList() + const { startIndex } = this.control.getRange() + const startElement = elementList[startIndex] + const data: IElement[] = [] + // 向左查找 + let preIndex = startIndex + while (preIndex > 0) { + const preElement = elementList[preIndex] + if ( + preElement.controlId !== startElement.controlId || + preElement.controlComponent === ControlComponent.PREFIX + ) { + break + } + if (preElement.controlComponent === ControlComponent.VALUE) { + data.unshift(preElement) + } + preIndex-- + } + // 向右查找 + let nextIndex = startIndex + 1 + while (nextIndex < elementList.length) { + const nextElement = elementList[nextIndex] + if ( + nextElement.controlId !== startElement.controlId || + nextElement.controlComponent === ControlComponent.POSTFIX + ) { + break + } + if (nextElement.controlComponent === ControlComponent.VALUE) { + data.push(nextElement) + } + nextIndex++ + } + return data + } + + public setValue(): number { + const range = this.control.getRange() + return range.endIndex + } + + public keydown(evt: KeyboardEvent): number { + const elementList = this.control.getElementList() + const range = this.control.getRange() + // 收缩边界到Value内 + this.control.shrinkBoundary() + const { startIndex, endIndex } = range + const startElement = elementList[startIndex] + const endElement = elementList[endIndex] + // backspace + if (evt.key === KeyMap.Backspace) { + // 清空选项 + if (startIndex !== endIndex) { + return this.clearSelect() + } else { + if ( + startElement.controlComponent === ControlComponent.PREFIX || + endElement.controlComponent === ControlComponent.POSTFIX || + startElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + // 前缀、后缀、占位符 + return this.control.removeControl(startIndex) + } else { + // 清空选项 + return this.clearSelect() + } + } + } else if (evt.key === KeyMap.Delete) { + // 移除选区元素 + if (startIndex !== endIndex) { + // 清空选项 + return this.clearSelect() + } else { + const endNextElement = elementList[endIndex + 1] + if (startElement.controlComponent === ControlComponent.PREFIX || + endNextElement.controlComponent === ControlComponent.POSTFIX || + startElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + // 前缀、后缀、占位符 + return this.control.removeControl(startIndex) + } else { + // 清空选项 + return this.clearSelect() + } + } + } + return endIndex + } + + public cut(): number { + this.control.shrinkBoundary() + const { startIndex, endIndex } = this.control.getRange() + if (startIndex === endIndex) { + return startIndex + } + // 清空选项 + return this.clearSelect() + } + + public clearSelect(): number { + const elementList = this.control.getElementList() + const { startIndex } = this.control.getRange() + const startElement = elementList[startIndex] + let leftIndex = -1 + let rightIndex = -1 + // 向左查找 + let preIndex = startIndex + while (preIndex > 0) { + const preElement = elementList[preIndex] + if ( + preElement.controlId !== startElement.controlId || + preElement.controlComponent === ControlComponent.PREFIX + ) { + leftIndex = preIndex + break + } + preIndex-- + } + // 向右查找 + let nextIndex = startIndex + 1 + while (nextIndex < elementList.length) { + const nextElement = elementList[nextIndex] + if ( + nextElement.controlId !== startElement.controlId || + nextElement.controlComponent === ControlComponent.POSTFIX + ) { + rightIndex = nextIndex - 1 + break + } + nextIndex++ + } + if (!~leftIndex || !~rightIndex) return -1 + // 删除元素 + elementList.splice(leftIndex + 1, rightIndex - leftIndex) + // 增加占位符 + this.control.addPlaceholder(preIndex) + this.element.control!.code = null + return preIndex + } + + public setSelect(code: string) { + const control = this.element.control! + const valueSets = control.valueSets + if (!Array.isArray(valueSets) || !valueSets.length) return + // 转换code + const valueSet = valueSets.find(v => v.code === code) + if (!valueSet) return + // 清空选项 + const startIndex = this.clearSelect() + this.control.removePlaceholder(startIndex) + // 插入 + const elementList = this.control.getElementList() + const startElement = elementList[startIndex] + const start = startIndex + 1 + const data = valueSet.value.split('') + for (let i = 0; i < data.length; i++) { + elementList.splice(start + i, 0, { + ...startElement, + value: data[i], + controlComponent: ControlComponent.VALUE + }) + } + // render + const newIndex = start + data.length - 1 + this.control.repaintControl(newIndex) + // 设置状态 + this.element.control!.code = code + this.destroy() + } + + private _createSelectPopupDom() { + const control = this.element.control! + const valueSets = control.valueSets + if (!Array.isArray(valueSets) || !valueSets.length) return + const position = this.control.getPosition() + if (!position) return + // dom树:
  • item
+ const selectPopupContainer = document.createElement('div') + selectPopupContainer.classList.add('select-control-popup') + selectPopupContainer.setAttribute(EDITOR_COMPONENT, EditorComponent.POPUP) + const ul = document.createElement('ul') + for (let v = 0; v < valueSets.length; v++) { + const valueSet = valueSets[v] + const li = document.createElement('li') + const code = this.getCode() + if (code === valueSet.code) { + li.classList.add('active') + } + li.onclick = () => { + this.setSelect(valueSet.code) + } + li.append(document.createTextNode(valueSet.value)) + ul.append(li) + } + selectPopupContainer.append(ul) + // 定位 + const { coordinate: { leftTop: [left, top] }, lineHeight } = position + const preY = this.control.getPreY() + selectPopupContainer.style.left = `${left}px` + selectPopupContainer.style.top = `${top + preY + lineHeight}px` + // 追加至container + const container = this.control.getContainer() + container.append(selectPopupContainer) + this.selectDom = selectPopupContainer + } + + public awake() { + if (this.isPopup) return + const { startIndex } = this.control.getRange() + const elementList = this.control.getElementList() + if (elementList[startIndex + 1]?.controlId !== this.element.controlId) return + this._createSelectPopupDom() + this.isPopup = true + } + + public destroy() { + if (!this.isPopup) return + this.selectDom?.remove() + this.isPopup = false + } + +} \ No newline at end of file diff --git a/src/editor/core/draw/control/text/TextControl.ts b/src/editor/core/draw/control/text/TextControl.ts new file mode 100644 index 0000000..3d5521e --- /dev/null +++ b/src/editor/core/draw/control/text/TextControl.ts @@ -0,0 +1,167 @@ +import { ControlComponent } from '../../../../dataset/enum/Control' +import { KeyMap } from '../../../../dataset/enum/Keymap' +import { IControlInstance } from '../../../../interface/Control' +import { IElement } from '../../../../interface/Element' +import { Control } from '../Control' + +export class TextControl implements IControlInstance { + + private element: IElement + private control: Control + + constructor(element: IElement, control: Control) { + this.element = element + this.control = control + } + + public getElement(): IElement { + return this.element + } + + public getValue(): IElement[] { + const elementList = this.control.getElementList() + const { startIndex } = this.control.getRange() + const startElement = elementList[startIndex] + const data: IElement[] = [] + // 向左查找 + let preIndex = startIndex + while (preIndex > 0) { + const preElement = elementList[preIndex] + if ( + preElement.controlId !== startElement.controlId || + preElement.controlComponent === ControlComponent.PREFIX + ) { + break + } + if (preElement.controlComponent === ControlComponent.VALUE) { + data.unshift(preElement) + } + preIndex-- + } + // 向右查找 + let nextIndex = startIndex + 1 + while (nextIndex < elementList.length) { + const nextElement = elementList[nextIndex] + if ( + nextElement.controlId !== startElement.controlId || + nextElement.controlComponent === ControlComponent.POSTFIX + ) { + break + } + if (nextElement.controlComponent === ControlComponent.VALUE) { + data.push(nextElement) + } + nextIndex++ + } + return data + } + + public setValue(data: IElement[]): number { + const elementList = this.control.getElementList() + const range = this.control.getRange() + // 收缩边界到Value内 + this.control.shrinkBoundary() + const { startIndex, endIndex } = range + // 移除选区元素 + if (startIndex !== endIndex) { + elementList.splice(startIndex + 1, endIndex - startIndex) + } else { + // 移除空白占位符 + this.control.removePlaceholder(startIndex) + } + // 插入 + const startElement = elementList[startIndex] + const start = range.startIndex + 1 + for (let i = 0; i < data.length; i++) { + elementList.splice(start + i, 0, { + ...startElement, + ...data[i], + controlComponent: ControlComponent.VALUE + }) + } + return start + data.length - 1 + } + + public keydown(evt: KeyboardEvent): number { + const elementList = this.control.getElementList() + const range = this.control.getRange() + // 收缩边界到Value内 + this.control.shrinkBoundary() + const { startIndex, endIndex } = range + const startElement = elementList[startIndex] + const endElement = elementList[endIndex] + // backspace + if (evt.key === KeyMap.Backspace) { + // 移除选区元素 + if (startIndex !== endIndex) { + elementList.splice(startIndex + 1, endIndex - startIndex) + const value = this.getValue() + if (!value.length) { + this.control.addPlaceholder(startIndex) + } + return startIndex + } else { + if ( + startElement.controlComponent === ControlComponent.PREFIX || + endElement.controlComponent === ControlComponent.POSTFIX || + startElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + // 前缀、后缀、占位符 + return this.control.removeControl(startIndex) + } else { + // 文本 + elementList.splice(startIndex, 1) + const value = this.getValue() + if (!value.length) { + this.control.addPlaceholder(startIndex - 1) + } + return startIndex - 1 + } + } + } else if (evt.key === KeyMap.Delete) { + // 移除选区元素 + if (startIndex !== endIndex) { + elementList.splice(startIndex + 1, endIndex - startIndex) + const value = this.getValue() + if (!value.length) { + this.control.addPlaceholder(startIndex) + } + return startIndex + } else { + const endNextElement = elementList[endIndex + 1] + if (startElement.controlComponent === ControlComponent.PREFIX || + endNextElement.controlComponent === ControlComponent.POSTFIX || + startElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + // 前缀、后缀、占位符 + return this.control.removeControl(startIndex) + } else { + // 文本 + elementList.splice(startIndex + 1, 1) + const value = this.getValue() + if (!value.length) { + this.control.addPlaceholder(startIndex) + } + return startIndex + } + } + } + return endIndex + } + + public cut(): number { + this.control.shrinkBoundary() + const { startIndex, endIndex } = this.control.getRange() + if (startIndex === endIndex) { + return startIndex + } + const elementList = this.control.getElementList() + elementList.splice(startIndex + 1, endIndex - startIndex) + const value = this.getValue() + if (!value.length) { + this.control.addPlaceholder(startIndex) + } + return startIndex + } + +} \ No newline at end of file diff --git a/src/editor/core/event/CanvasEvent.ts b/src/editor/core/event/CanvasEvent.ts index dbe45a6..96a7f37 100644 --- a/src/editor/core/event/CanvasEvent.ts +++ b/src/editor/core/event/CanvasEvent.ts @@ -20,6 +20,7 @@ import { Listener } from '../listener/Listener' import { Position } from '../position/Position' import { RangeManager } from '../range/RangeManager' import { LETTER_REG, NUMBER_LIKE_REG } from '../../dataset/constant/Regular' +import { Control } from '../draw/control/Control' export class CanvasEvent { @@ -38,6 +39,7 @@ export class CanvasEvent { private tableTool: TableTool private hyperlinkParticle: HyperlinkParticle private listener: Listener + private control: Control constructor(draw: Draw) { this.isAllowDrag = false @@ -55,6 +57,7 @@ export class CanvasEvent { this.tableTool = this.draw.getTableTool() this.hyperlinkParticle = this.draw.getHyperlinkParticle() this.listener = this.draw.getListener() + this.control = this.draw.getControl() } public register() { @@ -163,9 +166,32 @@ export class CanvasEvent { x: evt.offsetX, y: evt.offsetY }) + // 激活控件 + if (positionResult.isControl) { + const { + index, + isTable, + trIndex, + tdIndex, + tdValueIndex + } = positionResult + const { newIndex } = this.control.moveCursor({ + index, + isTable, + trIndex, + tdIndex, + tdValueIndex + }) + if (isTable) { + positionResult.tdValueIndex = newIndex + } else { + positionResult.index = newIndex + } + } const { index, isDirectHit, + isControl, isImage, isTable, trIndex, @@ -178,6 +204,7 @@ export class CanvasEvent { // 设置位置上下文 this.position.setPositionContext({ isTable: isTable || false, + isControl: isControl || false, index, trIndex, tdIndex, @@ -246,33 +273,47 @@ export class CanvasEvent { const { index } = cursorPosition const { startIndex, endIndex } = this.range.getRange() const isCollapsed = startIndex === endIndex + const element = elementList[index] + // 当前激活控件 + const isPartRangeInControlOutside = this.control.isPartRangeInControlOutside() + const activeControl = this.control.getActiveControl() if (evt.key === KeyMap.Backspace) { - if (isReadonly) return - // 判断是否允许删除 - if (isCollapsed && elementList[index].value === ZERO && index === 0) { - evt.preventDefault() - return - } - if (!isCollapsed) { - elementList.splice(startIndex + 1, endIndex - startIndex) + if (isReadonly || isPartRangeInControlOutside) return + let curIndex: number + if (activeControl) { + curIndex = this.control.keydown(evt) } else { - elementList.splice(index, 1) + // 判断是否允许删除 + if (isCollapsed && elementList[index].value === ZERO && index === 0) { + evt.preventDefault() + return + } + if (!isCollapsed) { + elementList.splice(startIndex + 1, endIndex - startIndex) + } else { + elementList.splice(index, 1) + } + curIndex = isCollapsed ? index - 1 : startIndex } - const curIndex = isCollapsed ? index - 1 : startIndex this.range.setRange(curIndex, curIndex) this.draw.render({ curIndex }) } else if (evt.key === KeyMap.Delete) { - if (isReadonly) return - if (!isCollapsed) { - elementList.splice(startIndex + 1, endIndex - startIndex) + if (isReadonly || isPartRangeInControlOutside) return + let curIndex: number + if (activeControl && elementList[endIndex + 1]?.controlId === element.controlId) { + curIndex = this.control.keydown(evt) } else { - elementList.splice(index + 1, 1) + if (!isCollapsed) { + elementList.splice(startIndex + 1, endIndex - startIndex) + } else { + elementList.splice(index + 1, 1) + } + curIndex = isCollapsed ? index : startIndex } - const curIndex = isCollapsed ? index : startIndex this.range.setRange(curIndex, curIndex) this.draw.render({ curIndex }) } else if (evt.key === KeyMap.Enter) { - if (isReadonly) return + if (isReadonly || isPartRangeInControlOutside) return // 表格需要上下文信息 const positionContext = this.position.getPositionContext() let restArg = {} @@ -284,12 +325,17 @@ export class CanvasEvent { value: ZERO, ...restArg } - if (isCollapsed) { - elementList.splice(index + 1, 0, enterText) + let curIndex: number + if (activeControl) { + curIndex = this.control.setValue([enterText]) } else { - elementList.splice(startIndex + 1, endIndex - startIndex, enterText) + if (isCollapsed) { + elementList.splice(index + 1, 0, enterText) + } else { + elementList.splice(startIndex + 1, endIndex - startIndex, enterText) + } + curIndex = index + 1 } - const curIndex = index + 1 this.range.setRange(curIndex, curIndex) this.draw.render({ curIndex }) } else if (evt.key === KeyMap.Left) { @@ -430,6 +476,11 @@ export class CanvasEvent { if (!this.cursor) return const cursorPosition = this.position.getCursorPosition() if (!data || !cursorPosition || this.isCompositing) return + if (this.control.isPartRangeInControlOutside()) { + // 忽略选区部分在控件的输入 + return + } + const activeControl = this.control.getActiveControl() const { TEXT, HYPERLINK, SUBSCRIPT, SUPERSCRIPT } = ElementType const text = data.replaceAll(`\n`, ZERO) const elementList = this.draw.getElementList() @@ -468,18 +519,24 @@ export class CanvasEvent { } return newElement }) - let start = 0 - if (isCollapsed) { - start = index + 1 + // 控件-移除placeholder + let curIndex: number + if (activeControl && elementList[endIndex + 1]?.controlId === element.controlId) { + curIndex = this.control.setValue(inputData) } else { - start = startIndex + 1 - elementList.splice(startIndex + 1, endIndex - startIndex) - } - // 禁止直接使用解构存在性能问题 - for (let i = 0; i < inputData.length; i++) { - elementList.splice(start + i, 0, inputData[i]) + let start = 0 + if (isCollapsed) { + start = index + 1 + } else { + start = startIndex + 1 + elementList.splice(startIndex + 1, endIndex - startIndex) + } + // 禁止直接使用解构存在性能问题 + for (let i = 0; i < inputData.length; i++) { + elementList.splice(start + i, 0, inputData[i]) + } + curIndex = (isCollapsed ? index : startIndex) + inputData.length } - const curIndex = (isCollapsed ? index : startIndex) + inputData.length this.range.setRange(curIndex, curIndex) this.draw.render({ curIndex }) } @@ -487,12 +544,20 @@ export class CanvasEvent { public cut() { const isReadonly = this.draw.isReadonly() if (isReadonly) return + const isPartRangeInControlOutside = this.control.isPartRangeInControlOutside() + if (isPartRangeInControlOutside) return + const activeControl = this.control.getActiveControl() const { startIndex, endIndex } = this.range.getRange() const elementList = this.draw.getElementList() if (startIndex !== endIndex) { writeTextByElementList(elementList.slice(startIndex + 1, endIndex + 1)) - elementList.splice(startIndex + 1, endIndex - startIndex) - const curIndex = startIndex + let curIndex: number + if (activeControl) { + curIndex = this.control.cut() + } else { + elementList.splice(startIndex + 1, endIndex - startIndex) + curIndex = startIndex + } this.range.setRange(curIndex, curIndex) this.draw.render({ curIndex }) } diff --git a/src/editor/core/event/GlobalEvent.ts b/src/editor/core/event/GlobalEvent.ts index 4044156..c4077e9 100644 --- a/src/editor/core/event/GlobalEvent.ts +++ b/src/editor/core/event/GlobalEvent.ts @@ -2,6 +2,7 @@ import { EDITOR_COMPONENT } from '../../dataset/constant/Editor' import { IEditorOption } from '../../interface/Editor' import { findParent } from '../../utils' import { Cursor } from '../cursor/Cursor' +import { Control } from '../draw/control/Control' import { Draw } from '../draw/Draw' import { HyperlinkParticle } from '../draw/particle/HyperlinkParticle' import { ImageParticle } from '../draw/particle/ImageParticle' @@ -20,6 +21,7 @@ export class GlobalEvent { private imageParticle: ImageParticle private tableTool: TableTool private hyperlinkParticle: HyperlinkParticle + private control: Control constructor(draw: Draw, canvasEvent: CanvasEvent) { this.draw = draw @@ -31,6 +33,7 @@ export class GlobalEvent { this.imageParticle = draw.getImageParticle() this.tableTool = draw.getTableTool() this.hyperlinkParticle = draw.getHyperlinkParticle() + this.control = draw.getControl() } public register() { @@ -74,6 +77,7 @@ export class GlobalEvent { this.imageParticle.clearResizer() this.tableTool.dispose() this.hyperlinkParticle.clearHyperlinkPopup() + this.control.destroyControl() } public setDragState() { diff --git a/src/editor/core/position/Position.ts b/src/editor/core/position/Position.ts index 281bceb..cccacf9 100644 --- a/src/editor/core/position/Position.ts +++ b/src/editor/core/position/Position.ts @@ -18,7 +18,8 @@ export class Position { this.positionList = [] this.cursorPosition = null this.positionContext = { - isTable: false + isTable: false, + isControl: false } this.draw = draw @@ -92,14 +93,16 @@ export class Position { positionList: td.positionList }) if (~tablePosition.index) { + const { index: tdValueIndex } = tablePosition return { index, + isControl: td.value[tdValueIndex].type === ElementType.CONTROL, isImage: tablePosition.isImage, isDirectHit: tablePosition.isDirectHit, isTable: true, tdIndex: d, trIndex: t, - tdValueIndex: tablePosition.index, + tdValueIndex, tdId: td.id, trId: tr.id, tableId: element.id @@ -110,7 +113,11 @@ export class Position { } // 图片区域均为命中 if (element.type === ElementType.IMAGE) { - return { index: curPositionIndex, isDirectHit: true, isImage: true } + return { + index: curPositionIndex, + isDirectHit: true, + isImage: true + } } // 判断是否在文字中间前后 if (elementList[index].value !== ZERO) { @@ -119,7 +126,10 @@ export class Position { curPositionIndex = j - 1 } } - return { index: curPositionIndex } + return { + index: curPositionIndex, + isControl: element.type === ElementType.CONTROL, + } } } // 非命中区域 @@ -135,7 +145,9 @@ export class Position { const tdWidth = td.width! const tdHeight = td.height! if (!(tdX < x && x < tdX + tdWidth && tdY < y && y < tdY + tdHeight)) { - return { index: curPositionIndex } + return { + index: curPositionIndex + } } } } @@ -161,7 +173,10 @@ export class Position { // 当前页最后一行 return { index: firstLetterList[firstLetterList.length - 1]?.index || positionList.length - 1 } } - return { index: curPositionIndex } + return { + index: curPositionIndex, + isControl: elementList[curPositionIndex].type === ElementType.CONTROL + } } } \ No newline at end of file diff --git a/src/editor/core/range/RangeManager.ts b/src/editor/core/range/RangeManager.ts index 4366aa3..34d6507 100644 --- a/src/editor/core/range/RangeManager.ts +++ b/src/editor/core/range/RangeManager.ts @@ -1,4 +1,5 @@ import { ElementType } from '../..' +import { ControlComponent } from '../../dataset/enum/Control' import { IEditorOption } from '../../interface/Editor' import { IElement } from '../../interface/Element' import { IRange } from '../../interface/Range' @@ -53,6 +54,17 @@ export class RangeManager { this.range.startTrIndex = startTrIndex this.range.endTrIndex = endTrIndex this.range.isCrossRowCol = !!(startTdIndex || endTdIndex || startTrIndex || endTrIndex) + // 激活控件 + const control = this.draw.getControl() + if (~startIndex && ~endIndex && startIndex === startIndex) { + const elementList = this.draw.getElementList() + const element = elementList[startIndex] + if (element.type === ElementType.CONTROL) { + control.initControl() + return + } + } + control.destroyControl() } public setRangeStyle() { @@ -126,6 +138,90 @@ export class RangeManager { }) } + public shrinkBoundary() { + const elementList = this.draw.getElementList() + const range = this.getRange() + const { startIndex, endIndex } = range + const startElement = elementList[startIndex] + const endElement = elementList[endIndex] + if (startIndex === endIndex) { + if (startElement.controlComponent === ControlComponent.PLACEHOLDER) { + // 找到第一个placeholder字符 + let index = startIndex - 1 + while (index > 0) { + const preElement = elementList[index] + if ( + preElement.controlId !== startElement.controlId || + preElement.controlComponent === ControlComponent.PREFIX + ) { + range.startIndex = index + range.endIndex = index + break + } + index-- + } + } + } else { + // 首、尾为占位符时,收缩到最后一个前缀字符后 + if ( + startElement.controlComponent === ControlComponent.PLACEHOLDER || + endElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + let index = endIndex - 1 + while (index > 0) { + const preElement = elementList[index] + if ( + preElement.controlId !== endElement.controlId + || preElement.controlComponent === ControlComponent.PREFIX + ) { + range.startIndex = index + range.endIndex = index + return + } + index-- + } + } + // 向右查找到第一个Value + if (startElement.controlComponent === ControlComponent.PREFIX) { + let index = startIndex + 1 + while (index < elementList.length) { + const nextElement = elementList[index] + if ( + nextElement.controlId !== startElement.controlId + || nextElement.controlComponent === ControlComponent.VALUE + ) { + range.startIndex = index - 1 + break + } else if (nextElement.controlComponent === ControlComponent.PLACEHOLDER) { + range.startIndex = index - 1 + range.endIndex = index - 1 + return + } + index++ + } + } + // 向左查找到第一个Value + if (endElement.controlComponent !== ControlComponent.VALUE) { + let index = startIndex - 1 + while (index > 0) { + const preElement = elementList[index] + if ( + preElement.controlId !== startElement.controlId + || preElement.controlComponent === ControlComponent.VALUE + ) { + range.startIndex = index + break + } else if (preElement.controlComponent === ControlComponent.PLACEHOLDER) { + range.startIndex = index + range.endIndex = index + return + } + index-- + } + } + } + } + public render(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number) { ctx.save() ctx.globalAlpha = this.options.rangeAlpha diff --git a/src/editor/dataset/constant/Control.ts b/src/editor/dataset/constant/Control.ts new file mode 100644 index 0000000..2190bc3 --- /dev/null +++ b/src/editor/dataset/constant/Control.ts @@ -0,0 +1,8 @@ +import { IControlOption } from '../../interface/Control' + +export const defaultControlOption: Readonly> = { + placeholderColor: '#9c9b9b', + bracketColor: '#000000', + prefix: '{', + postfix: '}' +} \ No newline at end of file diff --git a/src/editor/dataset/constant/Element.ts b/src/editor/dataset/constant/Element.ts index 38c598c..3edd78f 100644 --- a/src/editor/dataset/constant/Element.ts +++ b/src/editor/dataset/constant/Element.ts @@ -45,12 +45,14 @@ export const EDITOR_ELEMENT_ZIP_ATTR: Array = [ 'height', 'url', 'colgroup', - 'valueList' + 'valueList', + 'control' ] export const TEXTLIKE_ELEMENT_TYPE: ElementType[] = [ ElementType.TEXT, ElementType.HYPERLINK, ElementType.SUBSCRIPT, - ElementType.SUPERSCRIPT + ElementType.SUPERSCRIPT, + ElementType.CONTROL ] \ No newline at end of file diff --git a/src/editor/dataset/enum/Control.ts b/src/editor/dataset/enum/Control.ts new file mode 100644 index 0000000..00670db --- /dev/null +++ b/src/editor/dataset/enum/Control.ts @@ -0,0 +1,11 @@ +export enum ControlType { + TEXT = 'text', + SELECT = 'select' +} + +export enum ControlComponent { + PREFIX = 'prefix', + POSTFIX = 'postfix', + PLACEHOLDER = 'placeholder', + VALUE = 'value' +} \ No newline at end of file diff --git a/src/editor/dataset/enum/Editor.ts b/src/editor/dataset/enum/Editor.ts index f483592..7624423 100644 --- a/src/editor/dataset/enum/Editor.ts +++ b/src/editor/dataset/enum/Editor.ts @@ -3,7 +3,8 @@ export enum EditorComponent { MENU = 'menu', MAIN = 'main', FOOTER = 'footer', - CONTEXTMENU = 'contextmenu' + CONTEXTMENU = 'contextmenu', + POPUP = 'popup' } export enum EditorContext { diff --git a/src/editor/dataset/enum/Element.ts b/src/editor/dataset/enum/Element.ts index 5c586a9..07c379e 100644 --- a/src/editor/dataset/enum/Element.ts +++ b/src/editor/dataset/enum/Element.ts @@ -6,5 +6,6 @@ export enum ElementType { SUPERSCRIPT = 'superscript', SUBSCRIPT = 'subscript', SEPARATOR = 'separator', - PAGE_BREAK = 'pageBreak' + PAGE_BREAK = 'pageBreak', + CONTROL = 'control' } \ No newline at end of file diff --git a/src/editor/index.ts b/src/editor/index.ts index 30dcced..b4a5f34 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -19,6 +19,9 @@ import { IHeader } from './interface/Header' import { IWatermark } from './interface/Watermark' import { defaultHeaderOption } from './dataset/constant/Header' import { defaultWatermarkOption } from './dataset/constant/Watermark' +import { ControlType } from './dataset/enum/Control' +import { defaultControlOption } from './dataset/constant/Control' +import { IControlOption } from './interface/Control' export default class Editor { @@ -35,6 +38,10 @@ export default class Editor { ...defaultWatermarkOption, ...options.watermark } + const controlOptions: Required = { + ...defaultControlOption, + ...options.control + } const editorOptions: Required = { defaultMode: EditorMode.EDIT, defaultType: 'TEXT', @@ -68,9 +75,12 @@ export default class Editor { headerTop: 50, ...options, header: headerOptions, - watermark: waterMarkOptions + watermark: waterMarkOptions, + control: controlOptions } - formatElementList(elementList) + formatElementList(elementList, { + editorOptions + }) // 监听 this.listener = new Listener() // 启动 @@ -95,6 +105,7 @@ export { RowFlex, EditorMode, ElementType, + ControlType, EditorComponent, EDITOR_COMPONENT } diff --git a/src/editor/interface/Control.ts b/src/editor/interface/Control.ts new file mode 100644 index 0000000..b0d773f --- /dev/null +++ b/src/editor/interface/Control.ts @@ -0,0 +1,54 @@ +import { ControlType } from '../dataset/enum/Control' +import { IElement } from './Element' + +export interface IValueSet { + value: string; + code: string; +} + +export interface IControlSelect { + code: string | null; + valueSets: IValueSet[]; +} + +export interface IControlBasic { + type: ControlType; + value: IElement[] | null; + placeholder: string; + conceptId?: string; + prefix?: string; + postfix?: string; +} + +export type IControl = IControlBasic & Partial + +export interface IControlOption { + placeholderColor?: string; + bracketColor?: string; + prefix?: string; + postfix?: string; +} + +export interface IControlInitOption { + index: number; + isTable?: boolean; + trIndex?: number; + tdIndex?: number; + tdValueIndex?: number; +} + +export interface IControlInitResult { + newIndex: number; +} + +export interface IControlInstance { + getElement(): IElement; + + getValue(): IElement[]; + + setValue(data: IElement[]): number; + + keydown(evt: KeyboardEvent): number; + + cut(): number; +} \ No newline at end of file diff --git a/src/editor/interface/Editor.ts b/src/editor/interface/Editor.ts index db221d5..055914b 100644 --- a/src/editor/interface/Editor.ts +++ b/src/editor/interface/Editor.ts @@ -1,5 +1,6 @@ import { IElement } from '..' import { EditorMode } from '../dataset/enum/Editor' +import { IControlOption } from './Control' import { IHeader } from './Header' import { IWatermark } from './Watermark' @@ -36,6 +37,7 @@ export interface IEditorOption { headerTop?: number; header?: IHeader; watermark?: IWatermark; + control?: IControlOption; } export interface IEditorResult { diff --git a/src/editor/interface/Element.ts b/src/editor/interface/Element.ts index 7a6405c..bd2d4cd 100644 --- a/src/editor/interface/Element.ts +++ b/src/editor/interface/Element.ts @@ -1,5 +1,7 @@ +import { ControlComponent } from '../dataset/enum/Control' import { ElementType } from '../dataset/enum/Element' import { RowFlex } from '../dataset/enum/Row' +import { IControl } from './Control' import { IColgroup } from './table/Colgroup' import { ITr } from './table/Tr' @@ -51,12 +53,19 @@ export interface ISeparator { dashArray?: number[]; } +export interface IControlElement { + control?: IControl; + controlId?: string; + controlComponent?: ControlComponent; +} + export type IElement = IElementBasic & IElementStyle & ITable & IHyperlinkElement & ISuperscriptSubscript & ISeparator + & IControlElement export interface IElementMetrics { width: number; diff --git a/src/editor/interface/Position.ts b/src/editor/interface/Position.ts index 2461ce4..4e95072 100644 --- a/src/editor/interface/Position.ts +++ b/src/editor/interface/Position.ts @@ -4,6 +4,7 @@ import { ITd } from './table/Td' export interface ICurrentPosition { index: number; + isControl?: boolean; isImage?: boolean; isTable?: boolean; isDirectHit?: boolean; @@ -27,6 +28,7 @@ export interface IGetPositionByXYPayload { export interface IPositionContext { isTable: boolean; + isControl?: boolean; index?: number; trIndex?: number; tdIndex?: number; diff --git a/src/editor/utils/element.ts b/src/editor/utils/element.ts index 5c7ee4b..89cf9f4 100644 --- a/src/editor/utils/element.ts +++ b/src/editor/utils/element.ts @@ -1,9 +1,20 @@ import { deepClone, getUUID } from '.' -import { ElementType, IElement } from '..' +import { ElementType, IEditorOption, IElement } from '..' import { ZERO } from '../dataset/constant/Common' +import { defaultControlOption } from '../dataset/constant/Control' import { EDITOR_ELEMENT_ZIP_ATTR } from '../dataset/constant/Element' +import { ControlComponent, ControlType } from '../dataset/enum/Control' -export function formatElementList(elementList: IElement[], isHandleFirstElement = true) { +interface IFormatElementListOption { + isHandleFirstElement?: boolean; + editorOptions?: Required; +} + +export function formatElementList(elementList: IElement[], options: IFormatElementListOption = {}) { + const { isHandleFirstElement, editorOptions } = { + isHandleFirstElement: true, + ...options + } if (isHandleFirstElement && elementList[0]?.value !== ZERO) { elementList.unshift({ value: ZERO @@ -24,7 +35,7 @@ export function formatElementList(elementList: IElement[], isHandleFirstElement const td = tr.tdList[d] const tdId = getUUID() td.id = tdId - formatElementList(td.value) + formatElementList(td.value, options) for (let v = 0; v < td.value.length; v++) { const value = td.value[v] value.tdId = tdId @@ -59,6 +70,96 @@ export function formatElementList(elementList: IElement[], isHandleFirstElement } } 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 = {} + if (editorOptions && editorOptions.control) { + thePrePostfixArgs.color = editorOptions.control.bracketColor + } + // 前缀 + const prefixStrList = (prefix || defaultControlOption.prefix).split('') + 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.SELECT && code && (!value || !value.length)) + ) { + let valueList: IElement[] = value || [] + 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 = element.value.split('') + for (let e = 0; e < valueStrList.length; e++) { + const value = valueStrList[e] + elementList.splice(i, 0, { + controlId, + value, + type: el.type, + control: el.control, + controlComponent: ControlComponent.VALUE + }) + i++ + } + } + } else { + // placeholder + const thePlaceholderArgs: Pick = {} + if (editorOptions && editorOptions.control) { + thePlaceholderArgs.color = editorOptions.control.placeholderColor + } + const placeholderStrList = placeholder.split('') + 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 = (postfix || defaultControlOption.postfix).split('') + 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 = el.value.split('') @@ -153,6 +254,31 @@ export function zipElementList(payload: IElement[]): IElement[] { } hyperlinkElement.valueList = zipElementList(valueList) element = hyperlinkElement + } 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) diff --git a/src/editor/utils/index.ts b/src/editor/utils/index.ts index 18578ae..f571403 100644 --- a/src/editor/utils/index.ts +++ b/src/editor/utils/index.ts @@ -19,7 +19,7 @@ export function deepClone(obj: T): T { if (Array.isArray(obj)) { newObj = obj.map(item => deepClone(item)) } else { - Object.keys(obj).forEach((key) => { + Object.keys(obj as any).forEach((key) => { // @ts-ignore return newObj[key] = deepClone(obj[key]) }) diff --git a/src/mock.ts b/src/mock.ts index 144b6c9..16627b2 100644 --- a/src/mock.ts +++ b/src/mock.ts @@ -1,6 +1,6 @@ -import { ElementType, IEditorOption, IElement, RowFlex } from './editor' +import { ControlType, ElementType, IEditorOption, IElement, RowFlex } from './editor' -const text = `人民医院门诊病历\n主诉:\n发热三天,咳嗽五天。\n现病史:\n患者于三天前无明显诱因,感冒后发现面部水肿,无皮疹,尿量减少,出现乏力,在外治疗无好转,现来我院就诊。\n既往史:\n有糖尿病10年,有高血压2年,有传染性疾病1年。没有报告其他既往疾病。\n流行病史:\n否认14天内接触过确诊患者、疑似患者、无症状感染者及其密切接触者;否认14天内去过以下场所:水产、肉类批发市场,农贸市场,集市,大型超市,夜市;否认14天内与以下场所工作人员密切接触:水产、肉类批发市场,农贸市场,集市,大型超市;否认14天内周围(如家庭、办公室)有2例以上聚集性发病;否认14天内接触过有发热或呼吸道症状的人员;否认14天内自身有发热或呼吸道症状;否认14天内接触过纳入隔离观察的人员及其他可能与新冠肺炎关联的情形;陪同家属无以上情况。\n体格检查:\nT:39.5℃,P:80bpm,R:20次/分,BP:120/80mmHg;\n辅助检查:\n2020年6月10日,普放:血细胞比容36.50%(偏低)40~50;单核细胞绝对值0.75*10/L(偏高)参考值:0.1~0.6;\n门诊诊断:\n1.高血压\n2.糖尿病\n3.病毒性感冒\n4.过敏性鼻炎\n5.过敏性鼻息肉\n处置治疗:\n1.超声引导下甲状腺细针穿刺术;\n2.乙型肝炎表面抗体测定;\n3.膜式病变细胞采集术、后颈皮下肤层;\n电子签名:【】\n其他记录:` +const text = `人民医院门诊病历\n主诉:\n发热三天,咳嗽五天。\n现病史:\n患者于三天前无明显诱因,感冒后发现面部水肿,无皮疹,尿量减少,出现乏力,在外治疗无好转,现来我院就诊。\n既往史:\n有糖尿病10年,有高血压2年,有传染性疾病1年。报告其他既往疾病。\n流行病史:\n否认14天内接触过确诊患者、疑似患者、无症状感染者及其密切接触者;否认14天内去过以下场所:水产、肉类批发市场,农贸市场,集市,大型超市,夜市;否认14天内与以下场所工作人员密切接触:水产、肉类批发市场,农贸市场,集市,大型超市;否认14天内周围(如家庭、办公室)有2例以上聚集性发病;否认14天内接触过有发热或呼吸道症状的人员;否认14天内自身有发热或呼吸道症状;否认14天内接触过纳入隔离观察的人员及其他可能与新冠肺炎关联的情形;陪同家属无以上情况。\n体格检查:\nT:39.5℃,P:80bpm,R:20次/分,BP:120/80mmHg;\n辅助检查:\n2020年6月10日,普放:血细胞比容36.50%(偏低)40~50;单核细胞绝对值0.75*10/L(偏高)参考值:0.1~0.6;\n门诊诊断:\n1.高血压\n2.糖尿病\n3.病毒性感冒\n4.过敏性鼻炎\n5.过敏性鼻息肉\n处置治疗:\n1.超声引导下甲状腺细针穿刺术;\n2.乙型肝炎表面抗体测定;\n3.膜式病变细胞采集术、后颈皮下肤层;\n电子签名:【】\n其他记录:` // 模拟行居中 const centerText = ['人民医院门诊病历'] @@ -71,6 +71,43 @@ elementList.splice(8, 0, { type: ElementType.SEPARATOR }) +// 模拟文本控件 +elementList.splice(24, 0, { + type: ElementType.CONTROL, + value: '', + control: { + type: ControlType.TEXT, + value: null, + placeholder: '其他补充', + prefix: '{', + postfix: '}' + } +}) + +// 模拟下拉控件 +elementList.splice(112, 0, { + type: ElementType.CONTROL, + value: '', + control: { + type: ControlType.SELECT, + value: null, + code: null, + placeholder: '有无', + prefix: '{', + postfix: '}', + valueSets: [{ + value: '有', + code: '98175' + }, { + value: '无', + code: '98176' + }, { + value: '不详', + code: '98177' + }] + } +}) + // 模拟超链接 elementList.splice(138, 0, { type: ElementType.HYPERLINK,