@@ -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 = <ElementType>'control'
|
||||
const controlType: 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')
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
@@ -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 = <ElementType>'control'
|
||||
const controlType: 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')
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
@import './control/select.css';
|
||||
|
||||
.inputarea {
|
||||
width: 0;
|
||||
height: 12px;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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树:<div><ul><li>item</li></ul></div>
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
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
|
||||
}
|
||||
// 禁止直接使用解构存在性能问题
|
||||
for (let i = 0; i < inputData.length; i++) {
|
||||
elementList.splice(start + i, 0, inputData[i])
|
||||
}
|
||||
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 })
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { IControlOption } from '../../interface/Control'
|
||||
|
||||
export const defaultControlOption: Readonly<Required<IControlOption>> = {
|
||||
placeholderColor: '#9c9b9b',
|
||||
bracketColor: '#000000',
|
||||
prefix: '{',
|
||||
postfix: '}'
|
||||
}
|
||||
@@ -45,12 +45,14 @@ export const EDITOR_ELEMENT_ZIP_ATTR: Array<keyof IElement> = [
|
||||
'height',
|
||||
'url',
|
||||
'colgroup',
|
||||
'valueList'
|
||||
'valueList',
|
||||
'control'
|
||||
]
|
||||
|
||||
export const TEXTLIKE_ELEMENT_TYPE: ElementType[] = [
|
||||
ElementType.TEXT,
|
||||
ElementType.HYPERLINK,
|
||||
ElementType.SUBSCRIPT,
|
||||
ElementType.SUPERSCRIPT
|
||||
ElementType.SUPERSCRIPT,
|
||||
ElementType.CONTROL
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
export enum ControlType {
|
||||
TEXT = 'text',
|
||||
SELECT = 'select'
|
||||
}
|
||||
|
||||
export enum ControlComponent {
|
||||
PREFIX = 'prefix',
|
||||
POSTFIX = 'postfix',
|
||||
PLACEHOLDER = 'placeholder',
|
||||
VALUE = 'value'
|
||||
}
|
||||
@@ -3,7 +3,8 @@ export enum EditorComponent {
|
||||
MENU = 'menu',
|
||||
MAIN = 'main',
|
||||
FOOTER = 'footer',
|
||||
CONTEXTMENU = 'contextmenu'
|
||||
CONTEXTMENU = 'contextmenu',
|
||||
POPUP = 'popup'
|
||||
}
|
||||
|
||||
export enum EditorContext {
|
||||
|
||||
@@ -6,5 +6,6 @@ export enum ElementType {
|
||||
SUPERSCRIPT = 'superscript',
|
||||
SUBSCRIPT = 'subscript',
|
||||
SEPARATOR = 'separator',
|
||||
PAGE_BREAK = 'pageBreak'
|
||||
PAGE_BREAK = 'pageBreak',
|
||||
CONTROL = 'control'
|
||||
}
|
||||
+13
-2
@@ -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<IControlOption> = {
|
||||
...defaultControlOption,
|
||||
...options.control
|
||||
}
|
||||
const editorOptions: Required<IEditorOption> = {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<IControlSelect>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+129
-3
@@ -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<IEditorOption>;
|
||||
}
|
||||
|
||||
export function formatElementList(elementList: IElement[], options: IFormatElementListOption = {}) {
|
||||
const { isHandleFirstElement, editorOptions } = <IFormatElementListOption>{
|
||||
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<IElement, 'color'> = {}
|
||||
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<IElement, 'color'> = {}
|
||||
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)
|
||||
|
||||
@@ -19,7 +19,7 @@ export function deepClone<T>(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])
|
||||
})
|
||||
|
||||
+39
-2
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user