From 6b1336fc1a2866a59a3ab6979cd4e8919eb224c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BA=91=E9=A3=9E?= Date: Tue, 29 Mar 2022 18:12:14 +0800 Subject: [PATCH 01/17] feat:add control interface --- src/editor/core/draw/control/Control.ts | 3 +++ .../core/draw/control/select/SelectControl.ts | 3 +++ src/editor/core/draw/control/text/TextControl.ts | 3 +++ src/editor/dataset/enum/Control.ts | 4 ++++ src/editor/dataset/enum/Element.ts | 3 ++- src/editor/interface/Control.ts | 15 +++++++++++++++ src/editor/interface/Element.ts | 2 ++ 7 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 src/editor/core/draw/control/Control.ts create mode 100644 src/editor/core/draw/control/select/SelectControl.ts create mode 100644 src/editor/core/draw/control/text/TextControl.ts create mode 100644 src/editor/dataset/enum/Control.ts create mode 100644 src/editor/interface/Control.ts diff --git a/src/editor/core/draw/control/Control.ts b/src/editor/core/draw/control/Control.ts new file mode 100644 index 0000000..db7fd68 --- /dev/null +++ b/src/editor/core/draw/control/Control.ts @@ -0,0 +1,3 @@ +export class Control { + +} \ 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..e3608fa --- /dev/null +++ b/src/editor/core/draw/control/select/SelectControl.ts @@ -0,0 +1,3 @@ +export class SelectControl { + +} \ 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..11ae793 --- /dev/null +++ b/src/editor/core/draw/control/text/TextControl.ts @@ -0,0 +1,3 @@ +export class TextControl { + +} \ 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..2693e3c --- /dev/null +++ b/src/editor/dataset/enum/Control.ts @@ -0,0 +1,4 @@ +export enum ControlType { + TEXT = 'text', + SELECT = 'select' +} \ No newline at end of file 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/interface/Control.ts b/src/editor/interface/Control.ts new file mode 100644 index 0000000..2f110e1 --- /dev/null +++ b/src/editor/interface/Control.ts @@ -0,0 +1,15 @@ +import { ControlType } from '../dataset/enum/Control' + +export interface IValueSet { + value: string; + code: string; +} + +export interface IControl { + type: ControlType; + value: string; + conceptId: string; + prefix?: string; + postfix?: string; + valueSets?: IValueSet[] +} \ No newline at end of file diff --git a/src/editor/interface/Element.ts b/src/editor/interface/Element.ts index 7a6405c..bdde96c 100644 --- a/src/editor/interface/Element.ts +++ b/src/editor/interface/Element.ts @@ -1,5 +1,6 @@ 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' @@ -57,6 +58,7 @@ export type IElement = IElementBasic & IHyperlinkElement & ISuperscriptSubscript & ISeparator + & { control?: IControl } export interface IElementMetrics { width: number; From 66319d5405f50083081a01c18510b2b5afc03d05 Mon Sep 17 00:00:00 2001 From: Hufe921 Date: Wed, 30 Mar 2022 22:24:15 +0800 Subject: [PATCH 02/17] feat:add control options --- src/editor/dataset/constant/Control.ts | 6 ++++++ src/editor/index.ts | 11 ++++++++++- src/editor/interface/Control.ts | 13 ++++++++++--- src/editor/interface/Editor.ts | 2 ++ src/mock.ts | 15 ++++++++++++++- 5 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 src/editor/dataset/constant/Control.ts diff --git a/src/editor/dataset/constant/Control.ts b/src/editor/dataset/constant/Control.ts new file mode 100644 index 0000000..ff2ee47 --- /dev/null +++ b/src/editor/dataset/constant/Control.ts @@ -0,0 +1,6 @@ +import { IControlOption } from '../../interface/Control' + +export const defaultControlOption: Readonly> = { + placeholderColor: '#9c9b9b', + bracketColor: '#000000' +} \ No newline at end of file diff --git a/src/editor/index.ts b/src/editor/index.ts index 30dcced..ed805d5 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,7 +75,8 @@ export default class Editor { headerTop: 50, ...options, header: headerOptions, - watermark: waterMarkOptions + watermark: waterMarkOptions, + control: controlOptions } formatElementList(elementList) // 监听 @@ -95,6 +103,7 @@ export { RowFlex, EditorMode, ElementType, + ControlType, EditorComponent, EDITOR_COMPONENT } diff --git a/src/editor/interface/Control.ts b/src/editor/interface/Control.ts index 2f110e1..c4fedf4 100644 --- a/src/editor/interface/Control.ts +++ b/src/editor/interface/Control.ts @@ -1,4 +1,5 @@ import { ControlType } from '../dataset/enum/Control' +import { IElement } from './Element' export interface IValueSet { value: string; @@ -7,9 +8,15 @@ export interface IValueSet { export interface IControl { type: ControlType; - value: string; - conceptId: string; + value: IElement[] | null; + placeholder: string; + conceptId?: string; prefix?: string; postfix?: string; - valueSets?: IValueSet[] + valueSets?: IValueSet[]; +} + +export interface IControlOption { + placeholderColor?: string; + bracketColor?: string; } \ 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/mock.ts b/src/mock.ts index 144b6c9..d4b708b 100644 --- a/src/mock.ts +++ b/src/mock.ts @@ -1,4 +1,4 @@ -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其他记录:` @@ -209,6 +209,19 @@ elementList.push({ }] }) +// 文本控件 +elementList.splice(587, 0, { + type: ElementType.CONTROL, + value: '', + control: { + type: ControlType.TEXT, + value: null, + placeholder: '其他补充', + prefix: '{', + postfix: '}' + } +}) + // 模拟结尾文本 elementList.push(...[{ value: 'E', From 666a880c4f20466d5228e30aa3acb9ff9922fbef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BA=91=E9=A3=9E?= Date: Thu, 31 Mar 2022 17:46:50 +0800 Subject: [PATCH 03/17] feat:format control data --- src/editor/dataset/enum/Control.ts | 7 +++ src/editor/interface/Control.ts | 9 +++- src/editor/interface/Element.ts | 9 +++- src/editor/utils/element.ts | 69 ++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 3 deletions(-) diff --git a/src/editor/dataset/enum/Control.ts b/src/editor/dataset/enum/Control.ts index 2693e3c..15ea71a 100644 --- a/src/editor/dataset/enum/Control.ts +++ b/src/editor/dataset/enum/Control.ts @@ -1,4 +1,11 @@ export enum ControlType { TEXT = 'text', SELECT = 'select' +} + +export enum ControlComponent { + SUFFIX = 'suffix', + POSTFIX = 'postfix', + PLACEHOLDER = 'placeholder', + VALUE = 'value' } \ No newline at end of file diff --git a/src/editor/interface/Control.ts b/src/editor/interface/Control.ts index c4fedf4..a380a0d 100644 --- a/src/editor/interface/Control.ts +++ b/src/editor/interface/Control.ts @@ -6,16 +6,21 @@ export interface IValueSet { code: string; } -export interface IControl { +export interface IControlSelect { + valueSets: IValueSet[]; +} + +export interface IControlBasic { type: ControlType; value: IElement[] | null; placeholder: string; conceptId?: string; prefix?: string; postfix?: string; - valueSets?: IValueSet[]; } +export type IControl = IControlBasic & Partial + export interface IControlOption { placeholderColor?: string; bracketColor?: string; diff --git a/src/editor/interface/Element.ts b/src/editor/interface/Element.ts index bdde96c..bd2d4cd 100644 --- a/src/editor/interface/Element.ts +++ b/src/editor/interface/Element.ts @@ -1,3 +1,4 @@ +import { ControlComponent } from '../dataset/enum/Control' import { ElementType } from '../dataset/enum/Element' import { RowFlex } from '../dataset/enum/Row' import { IControl } from './Control' @@ -52,13 +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 - & { control?: IControl } + & IControlElement export interface IElementMetrics { width: number; diff --git a/src/editor/utils/element.ts b/src/editor/utils/element.ts index 5c7ee4b..491f6bd 100644 --- a/src/editor/utils/element.ts +++ b/src/editor/utils/element.ts @@ -2,6 +2,7 @@ import { deepClone, getUUID } from '.' import { ElementType, IElement } from '..' import { ZERO } from '../dataset/constant/Common' import { EDITOR_ELEMENT_ZIP_ATTR } from '../dataset/constant/Element' +import { ControlComponent } from '../dataset/enum/Control' export function formatElementList(elementList: IElement[], isHandleFirstElement = true) { if (isHandleFirstElement && elementList[0]?.value !== ZERO) { @@ -59,6 +60,74 @@ export function formatElementList(elementList: IElement[], isHandleFirstElement } } i-- + } else if (el.type === ElementType.CONTROL) { + const { prefix, postfix, value, placeholder } = el.control! + const controlId = getUUID() + // 移除父节点 + elementList.splice(i, 1) + // 前缀 + if (prefix) { + const prefixStrList = 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.SUFFIX + }) + i++ + } + } + // 值 + if (value && value.length) { + for (let v = 0; v < value.length; v++) { + const element = value[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 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 + }) + i++ + } + } + // 后缀 + if (postfix) { + const postfixStrList = 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 + }) + i++ + } + } + i-- } else if ((!el.type || el.type === ElementType.TEXT) && el.value.length > 1) { elementList.splice(i, 1) const valueList = el.value.split('') From befb39f26b3c120e28b0586556b51940780d2d8d Mon Sep 17 00:00:00 2001 From: Hufe921 Date: Thu, 31 Mar 2022 22:03:08 +0800 Subject: [PATCH 04/17] feat:format control output data --- src/editor/dataset/constant/Element.ts | 3 ++- src/editor/utils/element.ts | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/editor/dataset/constant/Element.ts b/src/editor/dataset/constant/Element.ts index 38c598c..9c108fa 100644 --- a/src/editor/dataset/constant/Element.ts +++ b/src/editor/dataset/constant/Element.ts @@ -45,7 +45,8 @@ export const EDITOR_ELEMENT_ZIP_ATTR: Array = [ 'height', 'url', 'colgroup', - 'valueList' + 'valueList', + 'control' ] export const TEXTLIKE_ELEMENT_TYPE: ElementType[] = [ diff --git a/src/editor/utils/element.ts b/src/editor/utils/element.ts index 491f6bd..d03254b 100644 --- a/src/editor/utils/element.ts +++ b/src/editor/utils/element.ts @@ -222,6 +222,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) From 54cbd835775df375dd0df76d6aca32d6ceaf13ed Mon Sep 17 00:00:00 2001 From: Hufe921 Date: Fri, 1 Apr 2022 21:13:47 +0800 Subject: [PATCH 05/17] feat:format control data option --- src/editor/core/command/CommandAdapt.ts | 5 +++- src/editor/index.ts | 4 ++- src/editor/utils/element.ts | 33 ++++++++++++++++++++----- 3 files changed, 34 insertions(+), 8 deletions(-) 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/index.ts b/src/editor/index.ts index ed805d5..b4a5f34 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -78,7 +78,9 @@ export default class Editor { watermark: waterMarkOptions, control: controlOptions } - formatElementList(elementList) + formatElementList(elementList, { + editorOptions + }) // 监听 this.listener = new Listener() // 启动 diff --git a/src/editor/utils/element.ts b/src/editor/utils/element.ts index d03254b..507f3d3 100644 --- a/src/editor/utils/element.ts +++ b/src/editor/utils/element.ts @@ -1,10 +1,19 @@ import { deepClone, getUUID } from '.' -import { ElementType, IElement } from '..' +import { ElementType, IEditorOption, IElement } from '..' import { ZERO } from '../dataset/constant/Common' import { EDITOR_ELEMENT_ZIP_ATTR } from '../dataset/constant/Element' import { ControlComponent } 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 @@ -25,7 +34,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 @@ -65,6 +74,11 @@ export function formatElementList(elementList: IElement[], isHandleFirstElement const controlId = getUUID() // 移除父节点 elementList.splice(i, 1) + // 前后缀个性化设置 + const thePreSuffixArgs: Pick = {} + if (editorOptions && editorOptions.control) { + thePreSuffixArgs.color = editorOptions.control.bracketColor + } // 前缀 if (prefix) { const prefixStrList = prefix.split('') @@ -75,7 +89,8 @@ export function formatElementList(elementList: IElement[], isHandleFirstElement value, type: el.type, control: el.control, - controlComponent: ControlComponent.SUFFIX + controlComponent: ControlComponent.SUFFIX, + ...thePreSuffixArgs }) i++ } @@ -99,6 +114,10 @@ export function formatElementList(elementList: IElement[], isHandleFirstElement } } 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] @@ -107,7 +126,8 @@ export function formatElementList(elementList: IElement[], isHandleFirstElement value, type: el.type, control: el.control, - controlComponent: ControlComponent.PLACEHOLDER + controlComponent: ControlComponent.PLACEHOLDER, + ...thePlaceholderArgs }) i++ } @@ -122,7 +142,8 @@ export function formatElementList(elementList: IElement[], isHandleFirstElement value, type: el.type, control: el.control, - controlComponent: ControlComponent.POSTFIX + controlComponent: ControlComponent.POSTFIX, + ...thePreSuffixArgs }) i++ } From 2420a035bd83e29a36de2b8cc05992f0e252809f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BA=91=E9=A3=9E?= Date: Sat, 2 Apr 2022 17:12:42 +0800 Subject: [PATCH 06/17] feat:set control cursor --- src/editor/core/draw/Draw.ts | 7 +++ src/editor/core/draw/control/Control.ts | 70 ++++++++++++++++++++++++- src/editor/core/event/CanvasEvent.ts | 9 ++++ src/editor/core/position/Position.ts | 27 +++++++--- src/editor/dataset/constant/Control.ts | 4 +- src/editor/dataset/enum/Control.ts | 2 +- src/editor/interface/Control.ts | 2 + src/editor/interface/Position.ts | 2 + src/editor/utils/element.ts | 2 +- 9 files changed, 115 insertions(+), 10 deletions(-) diff --git a/src/editor/core/draw/Draw.ts b/src/editor/core/draw/Draw.ts index 736537a..3fa7524 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 } diff --git a/src/editor/core/draw/control/Control.ts b/src/editor/core/draw/control/Control.ts index db7fd68..b3b597a 100644 --- a/src/editor/core/draw/control/Control.ts +++ b/src/editor/core/draw/control/Control.ts @@ -1,3 +1,71 @@ +import { ControlComponent } from '../../../dataset/enum/Control' +import { ElementType } from '../../../dataset/enum/Element' +import { IElement } from '../../../interface/Element' +import { ICurrentPosition } from '../../../interface/Position' +import { Draw } from '../Draw' + export class Control { - + + private draw: Draw + + constructor(draw: Draw) { + this.draw = draw + } + + // 调整起始光标位置到控件合适的位置 + public moveCursorIndex(position: ICurrentPosition) { + const { index, trIndex, tdIndex, tdValueIndex } = position + let elementList = this.draw.getOriginalElementList() + let element: IElement + if (position.isTable) { + elementList = elementList[index!].trList![trIndex!].tdList[tdIndex!].value + element = elementList[tdValueIndex!] + } else { + element = elementList[index] + } + if (element.type !== ElementType.CONTROL) return + // VALUE-无需移动 + if (element.controlComponent === ControlComponent.VALUE) return + // POSTFIX-移动到最后一个后缀字符后 + if (element.controlComponent === ControlComponent.POSTFIX) { + let startIndex = index + 1 + while (startIndex < elementList.length) { + const nextElement = elementList[startIndex] + if (nextElement.controlId !== element.controlId) { + position.index = startIndex - 1 + break + } + 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 + ) { + position.index = startIndex - 1 + break + } + 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 + ) { + position.index = startIndex + break + } + 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..342d0ac 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,14 @@ export class CanvasEvent { x: evt.offsetX, y: evt.offsetY }) + // 如果是控件-光标需移动到合适位置 + if (positionResult.isControl) { + this.control.moveCursorIndex(positionResult) + } const { index, isDirectHit, + isControl, isImage, isTable, trIndex, @@ -178,6 +186,7 @@ export class CanvasEvent { // 设置位置上下文 this.position.setPositionContext({ isTable: isTable || false, + isControl: isControl || false, index, trIndex, tdIndex, 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/dataset/constant/Control.ts b/src/editor/dataset/constant/Control.ts index ff2ee47..2190bc3 100644 --- a/src/editor/dataset/constant/Control.ts +++ b/src/editor/dataset/constant/Control.ts @@ -2,5 +2,7 @@ import { IControlOption } from '../../interface/Control' export const defaultControlOption: Readonly> = { placeholderColor: '#9c9b9b', - bracketColor: '#000000' + bracketColor: '#000000', + prefix: '{', + postfix: '}' } \ No newline at end of file diff --git a/src/editor/dataset/enum/Control.ts b/src/editor/dataset/enum/Control.ts index 15ea71a..00670db 100644 --- a/src/editor/dataset/enum/Control.ts +++ b/src/editor/dataset/enum/Control.ts @@ -4,7 +4,7 @@ export enum ControlType { } export enum ControlComponent { - SUFFIX = 'suffix', + PREFIX = 'prefix', POSTFIX = 'postfix', PLACEHOLDER = 'placeholder', VALUE = 'value' diff --git a/src/editor/interface/Control.ts b/src/editor/interface/Control.ts index a380a0d..78f56a0 100644 --- a/src/editor/interface/Control.ts +++ b/src/editor/interface/Control.ts @@ -24,4 +24,6 @@ export type IControl = IControlBasic & Partial export interface IControlOption { placeholderColor?: string; bracketColor?: string; + prefix?: string; + postfix?: string; } \ No newline at end of file diff --git a/src/editor/interface/Position.ts b/src/editor/interface/Position.ts index 2461ce4..a4d9291 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 d03254b..08e8787 100644 --- a/src/editor/utils/element.ts +++ b/src/editor/utils/element.ts @@ -75,7 +75,7 @@ export function formatElementList(elementList: IElement[], isHandleFirstElement value, type: el.type, control: el.control, - controlComponent: ControlComponent.SUFFIX + controlComponent: ControlComponent.PREFIX }) i++ } From dc7b1c5cbf585c63bcee81d9e5c001476a9f375d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BA=91=E9=A3=9E?= Date: Sat, 2 Apr 2022 17:40:58 +0800 Subject: [PATCH 07/17] fix:ts syntax error --- src/editor/interface/Position.ts | 2 +- src/editor/utils/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/editor/interface/Position.ts b/src/editor/interface/Position.ts index a4d9291..4e95072 100644 --- a/src/editor/interface/Position.ts +++ b/src/editor/interface/Position.ts @@ -28,7 +28,7 @@ export interface IGetPositionByXYPayload { export interface IPositionContext { isTable: boolean; - isControl: boolean; + isControl?: boolean; index?: number; trIndex?: number; tdIndex?: number; 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]) }) From 3602093d590855e5674b6fb94094819aea334487 Mon Sep 17 00:00:00 2001 From: Hufe921 Date: Mon, 4 Apr 2022 17:01:55 +0800 Subject: [PATCH 08/17] feat:text control input data --- src/editor/core/draw/control/Control.ts | 106 +++++++++++++-- .../core/draw/control/text/TextControl.ts | 125 +++++++++++++++++- src/editor/core/event/CanvasEvent.ts | 52 ++++++-- src/editor/interface/Control.ts | 16 +++ src/editor/utils/element.ts | 53 ++++---- 5 files changed, 297 insertions(+), 55 deletions(-) diff --git a/src/editor/core/draw/control/Control.ts b/src/editor/core/draw/control/Control.ts index b3b597a..018da44 100644 --- a/src/editor/core/draw/control/Control.ts +++ b/src/editor/core/draw/control/Control.ts @@ -1,39 +1,96 @@ -import { ControlComponent } from '../../../dataset/enum/Control' +import { ControlComponent, ControlType } from '../../../dataset/enum/Control' import { ElementType } from '../../../dataset/enum/Element' +import { IControlInitOption, IControlInitResult, IControlInstance } from '../../../interface/Control' import { IElement } from '../../../interface/Element' -import { ICurrentPosition } from '../../../interface/Position' +import { RangeManager } from '../../range/RangeManager' import { Draw } from '../Draw' +import { TextControl } from './text/TextControl' +interface IMoveCursorResult { + newIndex: number; + newElement: IElement; +} export class Control { private draw: Draw + private range: RangeManager + private activeControl: IControlInstance | null constructor(draw: Draw) { this.draw = draw + this.range = draw.getRange() + this.activeControl = null + } + + public getElementList(): IElement[] { + return this.draw.getElementList() + } + + public getRange() { + return this.range.getRange() + } + + public getActiveControl(): IControlInstance | null { + return this.activeControl + } + + // 判断选区部分在控件边界外 + 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 initControl(option: IControlInitOption): IControlInitResult { + // 调整光标位置 + const { newIndex, newElement } = this.moveCursor(option) + const control = newElement.control! + // 销毁激活控件 + this.destroyControl() + // 激活控件 + if (control.type === ControlType.TEXT) { + this.activeControl = new TextControl(this) + } + return { newIndex } } // 调整起始光标位置到控件合适的位置 - public moveCursorIndex(position: ICurrentPosition) { + 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.type !== ElementType.CONTROL) return - // VALUE-无需移动 - if (element.controlComponent === ControlComponent.VALUE) return - // POSTFIX-移动到最后一个后缀字符后 - if (element.controlComponent === ControlComponent.POSTFIX) { + 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) { - position.index = startIndex - 1 - break + return { + newIndex: startIndex - 1, + newElement: elementList[startIndex - 1] + } } startIndex++ } @@ -46,8 +103,10 @@ export class Control { nextElement.controlId !== element.controlId || nextElement.controlComponent !== ControlComponent.PREFIX ) { - position.index = startIndex - 1 - break + return { + newIndex: startIndex - 1, + newElement: elementList[startIndex - 1] + } } startIndex++ } @@ -60,12 +119,31 @@ export class Control { preElement.controlId !== element.controlId || preElement.controlComponent === ControlComponent.PREFIX ) { - position.index = startIndex - break + return { + newIndex: startIndex, + newElement: elementList[startIndex] + } } startIndex-- } } + return { + newIndex, + newElement: element + } + } + + public destroyControl() { + if (this.activeControl) { + this.activeControl = null + } + } + + public setValue(data: IElement[]): number { + if (!this.activeControl) { + throw new Error('active control is null') + } + return this.activeControl.setValue(data) } } \ 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 index 11ae793..809e6a3 100644 --- a/src/editor/core/draw/control/text/TextControl.ts +++ b/src/editor/core/draw/control/text/TextControl.ts @@ -1,3 +1,126 @@ -export class TextControl { +import { ControlComponent } from '../../../../dataset/enum/Control' +import { IControlInstance } from '../../../../interface/Control' +import { IElement } from '../../../../interface/Element' +import { IRange } from '../../../../interface/Range' +import { Control } from '../Control' + +export class TextControl implements IControlInstance { + + private control: Control + + constructor(control: Control) { + this.control = control + } + + public shrinkBoundary(elementList: IElement[], range: IRange) { + const { startIndex, endIndex } = range + if (startIndex === endIndex) return + const startElement = elementList[startIndex] + const endElement = elementList[endIndex] + // 首、尾为占位符时,收缩到最后一个前缀字符后 + 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 removePlaceholder(elementList: IElement[], range: IRange) { + const { startIndex } = range + 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 setValue(data: IElement[]): number { + const elementList = this.control.getElementList() + const range = this.control.getRange() + // 收缩边界到Value内 + this.shrinkBoundary(elementList, range) + const { startIndex, endIndex } = range + // 移除选区元素 + if (startIndex !== endIndex) { + elementList.splice(startIndex + 1, endIndex - startIndex) + } else { + // 移除空白占位符 + this.removePlaceholder(elementList, range) + } + // 插入 + 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 + } } \ No newline at end of file diff --git a/src/editor/core/event/CanvasEvent.ts b/src/editor/core/event/CanvasEvent.ts index 342d0ac..1a494f6 100644 --- a/src/editor/core/event/CanvasEvent.ts +++ b/src/editor/core/event/CanvasEvent.ts @@ -166,9 +166,27 @@ export class CanvasEvent { x: evt.offsetX, y: evt.offsetY }) - // 如果是控件-光标需移动到合适位置 + // 激活控件 if (positionResult.isControl) { - this.control.moveCursorIndex(positionResult) + const { + index, + isTable, + trIndex, + tdIndex, + tdValueIndex + } = positionResult + const { newIndex } = this.control.initControl({ + index, + isTable, + trIndex, + tdIndex, + tdValueIndex + }) + if (isTable) { + positionResult.tdValueIndex = newIndex + } else { + positionResult.index = newIndex + } } const { index, @@ -439,6 +457,10 @@ export class CanvasEvent { if (!this.cursor) return const cursorPosition = this.position.getCursorPosition() if (!data || !cursorPosition || this.isCompositing) return + if (this.control.isPartRangeInControlOutside()) { + // 忽略选区部分在控件的输入 + return + } const { TEXT, HYPERLINK, SUBSCRIPT, SUPERSCRIPT } = ElementType const text = data.replaceAll(`\n`, ZERO) const elementList = this.draw.getElementList() @@ -477,18 +499,24 @@ export class CanvasEvent { } return newElement }) - let start = 0 - if (isCollapsed) { - start = index + 1 + // 控件-移除placeholder + let curIndex: number + if (positionContext.isControl) { + 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 }) } diff --git a/src/editor/interface/Control.ts b/src/editor/interface/Control.ts index 78f56a0..f0feb90 100644 --- a/src/editor/interface/Control.ts +++ b/src/editor/interface/Control.ts @@ -26,4 +26,20 @@ export interface IControlOption { 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 { + setValue(data: IElement[]): number; } \ No newline at end of file diff --git a/src/editor/utils/element.ts b/src/editor/utils/element.ts index 8af8aa7..e1e89a1 100644 --- a/src/editor/utils/element.ts +++ b/src/editor/utils/element.ts @@ -1,6 +1,7 @@ import { deepClone, getUUID } 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 } from '../dataset/enum/Control' @@ -80,20 +81,18 @@ export function formatElementList(elementList: IElement[], options: IFormatEleme thePrePostfixArgs.color = editorOptions.control.bracketColor } // 前缀 - if (prefix) { - const prefixStrList = 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++ - } + 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) { @@ -133,20 +132,18 @@ export function formatElementList(elementList: IElement[], options: IFormatEleme } } // 后缀 - if (postfix) { - const postfixStrList = 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++ - } + 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) { From 6243a8e4ca63be6964de49f101522a1811a95234 Mon Sep 17 00:00:00 2001 From: Hufe921 Date: Mon, 4 Apr 2022 22:29:17 +0800 Subject: [PATCH 09/17] feat:text control delete --- src/editor/core/draw/control/Control.ts | 14 ++ .../core/draw/control/text/TextControl.ts | 157 +++++++++++++++++- src/editor/core/event/CanvasEvent.ts | 45 +++-- src/editor/core/event/GlobalEvent.ts | 4 + src/editor/interface/Control.ts | 4 + 5 files changed, 208 insertions(+), 16 deletions(-) diff --git a/src/editor/core/draw/control/Control.ts b/src/editor/core/draw/control/Control.ts index 018da44..ed5e207 100644 --- a/src/editor/core/draw/control/Control.ts +++ b/src/editor/core/draw/control/Control.ts @@ -1,6 +1,7 @@ import { ControlComponent, ControlType } from '../../../dataset/enum/Control' import { ElementType } from '../../../dataset/enum/Element' import { IControlInitOption, IControlInitResult, IControlInstance } from '../../../interface/Control' +import { IEditorOption } from '../../../interface/Editor' import { IElement } from '../../../interface/Element' import { RangeManager } from '../../range/RangeManager' import { Draw } from '../Draw' @@ -14,14 +15,20 @@ export class Control { private draw: Draw private range: RangeManager + private options: Required private activeControl: IControlInstance | null constructor(draw: Draw) { this.draw = draw this.range = draw.getRange() + this.options = draw.getOptions() this.activeControl = null } + public getOptions(): Required { + return this.options + } + public getElementList(): IElement[] { return this.draw.getElementList() } @@ -146,4 +153,11 @@ export class Control { 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) + } + } \ 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 index 809e6a3..4b7a0dc 100644 --- a/src/editor/core/draw/control/text/TextControl.ts +++ b/src/editor/core/draw/control/text/TextControl.ts @@ -1,5 +1,7 @@ import { ControlComponent } from '../../../../dataset/enum/Control' -import { IControlInstance } from '../../../../interface/Control' +import { ElementType } from '../../../../dataset/enum/Element' +import { KeyMap } from '../../../../dataset/enum/Keymap' +import { IControlInstance, IControlOption } from '../../../../interface/Control' import { IElement } from '../../../../interface/Element' import { IRange } from '../../../../interface/Range' import { Control } from '../Control' @@ -7,9 +9,11 @@ import { Control } from '../Control' export class TextControl implements IControlInstance { private control: Control + private options: IControlOption constructor(control: Control) { this.control = control + this.options = control.getOptions().control } public shrinkBoundary(elementList: IElement[], range: IRange) { @@ -97,6 +101,94 @@ export class TextControl implements IControlInstance { } } + public removeControl(elementList: IElement[], range: IRange): number { + const { startIndex } = range + 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 -1 + // 删除元素 + elementList.splice(leftIndex + 1, rightIndex - leftIndex) + // 清除实例 + this.control.destroyControl() + return leftIndex + } + + public addPlaceholder(elementList: IElement[], startIndex: number) { + 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 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() @@ -123,4 +215,67 @@ export class TextControl implements IControlInstance { return start + data.length - 1 } + public keydown(evt: KeyboardEvent): number { + const elementList = this.control.getElementList() + const range = this.control.getRange() + // 收缩边界到Value内 + this.shrinkBoundary(elementList, range) + 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.addPlaceholder(elementList, startIndex) + } + return startIndex + } else { + if (startElement.controlComponent === ControlComponent.PREFIX) { + // 前缀 + return this.removeControl(elementList, range) + } else if (endElement.controlComponent === ControlComponent.POSTFIX) { + // 后缀 + return this.removeControl(elementList, range) + } else { + // 文本 + elementList.splice(startIndex, 1) + const value = this.getValue() + if (!value.length) { + this.addPlaceholder(elementList, 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.addPlaceholder(elementList, startIndex) + } + return startIndex + } else { + const endNextElement = elementList[endIndex + 1] + if (endNextElement.controlComponent === ControlComponent.POSTFIX) { + // 后缀 + return this.removeControl(elementList, range) + } else { + // 文本 + elementList.splice(startIndex + 1, 1) + const value = this.getValue() + if (!value.length) { + this.addPlaceholder(elementList, startIndex) + } + return startIndex + } + } + } + return -1 + } + } \ No newline at end of file diff --git a/src/editor/core/event/CanvasEvent.ts b/src/editor/core/event/CanvasEvent.ts index 1a494f6..ed7559f 100644 --- a/src/editor/core/event/CanvasEvent.ts +++ b/src/editor/core/event/CanvasEvent.ts @@ -187,6 +187,8 @@ export class CanvasEvent { } else { positionResult.index = newIndex } + } else { + this.control.destroyControl() } const { index, @@ -273,29 +275,42 @@ export class CanvasEvent { const { index } = cursorPosition const { startIndex, endIndex } = this.range.getRange() const isCollapsed = startIndex === endIndex + // 当前激活控件 + 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) { + 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) { 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/interface/Control.ts b/src/editor/interface/Control.ts index f0feb90..850c1f6 100644 --- a/src/editor/interface/Control.ts +++ b/src/editor/interface/Control.ts @@ -41,5 +41,9 @@ export interface IControlInitResult { } export interface IControlInstance { + getValue(): IElement[]; + setValue(data: IElement[]): number; + + keydown(evt: KeyboardEvent): number; } \ No newline at end of file From 37a5b6a2b28e79036796dfcc55454e9ea277626d Mon Sep 17 00:00:00 2001 From: Hufe921 Date: Mon, 4 Apr 2022 22:35:03 +0800 Subject: [PATCH 10/17] feat:update mock data --- src/mock.ts | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/mock.ts b/src/mock.ts index d4b708b..4a84fc7 100644 --- a/src/mock.ts +++ b/src/mock.ts @@ -71,8 +71,21 @@ 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(138, 0, { +elementList.splice(139, 0, { type: ElementType.HYPERLINK, value: '', valueList: [{ @@ -92,20 +105,20 @@ elementList.splice(138, 0, { }) // 模拟下标 -elementList.splice(371, 0, { +elementList.splice(372, 0, { value: '∆', color: '#FF0000', type: ElementType.SUBSCRIPT }) // 模拟上标 -elementList.splice(459, 0, { +elementList.splice(460, 0, { value: '9', type: ElementType.SUPERSCRIPT }) // 模拟图片 -elementList.splice(585, 0, { +elementList.splice(586, 0, { value: `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFkAAAAgCAYAAAB5JtSmAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAQ0SURBVGhD7dhrUSNBFAVgvKACEVjAAhJQgAIUYAABGEAABvgfAdn6UnWou01PppOZhIXNj1P9vo9zH5PK1Waz2V5wWlxIPgMuJJ8Bi0h+fn7eXl9fb29ubrYPDw/dO/8DHh8fu/vB4kym4Orqaofb29vund8OSSbhemewSrugBMnG3vlvw9vb265yn56edmtz/t/f33+5C8MkixQSZSsl9UzLOHUmcwTYAN/Rpl5eXnY+pnIB0Xd3d7s5m3rvDsrkCGszNiQ7r/tr4v39fSc/uipOqRcqufTHBiO78GGdzG5xcLtIFmVde7L9NsvXRo9s84+Pj+79pUAwn5GcD1wIz5r+fYGeJdnjGiF9hwL7iWAcfX19/evtKVHJXrtN8Rf4A3TVczqhrut5i1mSZQgnIriSWtdzP2N+EvIhi3/GWqHWtWXuy2IYbheiKarJZIZknkxyrryc2Utrgal+9S8iScUXIx/3kcxfe/jotcuDezLFlIbARDrzHpytXdKnQr4xyc74Vu9YV5Ih2Q/tT7mDSEYw5ZU4wu3nJx64k/1z9umlUG0hah/JSbC6Jzi5exDJWoTHERoBxu8uf/pT1j3HDkUIJitjbRfRA/iwVzlgy1RCfSF5ili9xj7BUWKs9wJZ3MpditYu+lsc+/PRx53cVF9Pdg/syE9Hb6cS75PkmhUEUFofmTvLGEXKimHueJP9Y3swWQwGLUiA9xEbHKuvgs4pPe1+1myTAKlw81buJ8kigjAXKauXPLQPhEYgJSEYsgdTUR0BmTVgc6C359wcvKGnBrGO8dO5VlD1ZZ519nrBHvrwKVMCas9hgL0YUI2wV98fC4FqCWizzXyqF44A0ZKLHkilgvPs1zbiTuZIdZ414KvqGCKZYx4zple+MSrrJVncAyL02/TOqncJwVMglx5zI4QDZ5WPvBGEcNP+7TlEcqJIAQFGsIdQjmZt7MlYA5yiI3pOQTCQXUm2TuVmXgmewxDJQDgl6deJJoU5y7p9uwZagmu1mCvbNoOOBfkhOf6lRZjzPb8qRjBMMiUhM9GNMZQq5/oRXBP7Mlj/i12A7EMIaJGqDcl8I79+/N1xTvdINQ2TDAQSvI9Md479vdqCHKSFQKAfEmgBqCTDkjaSgOZXQkg2jy1ti0xApnBQJo/0obQRipeQXbN3CmxKGQch5xgki4Efghl/kFqzPD//2DnXIodIRpaoETaXxcmwGNO7N4I2Oyuc6b+xK/tL9IH3kY/E+r1JdST4yM+7VUiuJbuPZHBeHZcNvXtziMMV9mRuvUOX8Vg9IFjRx9dUYM3s2oJyNx9ahFfSWwyRHKHG3nmL2q/mojyFVAWnEdi2Hg7OBXwUCCKr1QEtoe0+/9jI3xqIiuF2QRD0zqcwpfQnge9TVSI4tWrNe79shj98F0xDC0N4bTUVF5LPgAvJJ8dm+wcP2iJuZNdC5QAAAABJRU5ErkJggg==`, width: 89, height: 32, @@ -209,19 +222,6 @@ elementList.push({ }] }) -// 文本控件 -elementList.splice(587, 0, { - type: ElementType.CONTROL, - value: '', - control: { - type: ControlType.TEXT, - value: null, - placeholder: '其他补充', - prefix: '{', - postfix: '}' - } -}) - // 模拟结尾文本 elementList.push(...[{ value: 'E', From 1cee9750337564c62e3d67c9012566b8fbe4334b Mon Sep 17 00:00:00 2001 From: Hufe921 Date: Tue, 5 Apr 2022 17:36:27 +0800 Subject: [PATCH 11/17] feat:optimize control active and destroy --- src/editor/core/draw/control/Control.ts | 217 +++++++++++++++--- .../core/draw/control/text/TextControl.ts | 181 +++------------ src/editor/core/event/CanvasEvent.ts | 9 +- src/editor/core/range/RangeManager.ts | 11 + src/editor/interface/Control.ts | 2 + 5 files changed, 229 insertions(+), 191 deletions(-) diff --git a/src/editor/core/draw/control/Control.ts b/src/editor/core/draw/control/Control.ts index ed5e207..1ebdde5 100644 --- a/src/editor/core/draw/control/Control.ts +++ b/src/editor/core/draw/control/Control.ts @@ -1,7 +1,6 @@ import { ControlComponent, ControlType } from '../../../dataset/enum/Control' import { ElementType } from '../../../dataset/enum/Element' -import { IControlInitOption, IControlInitResult, IControlInstance } from '../../../interface/Control' -import { IEditorOption } from '../../../interface/Editor' +import { IControlInitOption, IControlInstance, IControlOption } from '../../../interface/Control' import { IElement } from '../../../interface/Element' import { RangeManager } from '../../range/RangeManager' import { Draw } from '../Draw' @@ -15,32 +14,16 @@ export class Control { private draw: Draw private range: RangeManager - private options: Required + private options: IControlOption private activeControl: IControlInstance | null constructor(draw: Draw) { this.draw = draw this.range = draw.getRange() - this.options = draw.getOptions() + this.options = draw.getOptions().control this.activeControl = null } - public getOptions(): Required { - return this.options - } - - public getElementList(): IElement[] { - return this.draw.getElementList() - } - - public getRange() { - return this.range.getRange() - } - - public getActiveControl(): IControlInstance | null { - return this.activeControl - } - // 判断选区部分在控件边界外 public isPartRangeInControlOutside(): boolean { const { startIndex, endIndex } = this.getRange() @@ -57,20 +40,41 @@ export class Control { return false } - public initControl(option: IControlInitOption): IControlInitResult { - // 调整光标位置 - const { newIndex, newElement } = this.moveCursor(option) - const control = newElement.control! - // 销毁激活控件 + public getElementList(): IElement[] { + return this.draw.getElementList() + } + + public getRange() { + return this.range.getRange() + } + + 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) { + const controlElement = this.activeControl.getElement() + if (element.controlId === controlElement.controlId) return + } + // 销毁旧激活控件 this.destroyControl() // 激活控件 - if (control.type === ControlType.TEXT) { - this.activeControl = new TextControl(this) + if (element.control!.type === ControlType.TEXT) { + this.activeControl = new TextControl(element, this) + } + } + + public destroyControl() { + if (this.activeControl) { + this.activeControl = null } - return { newIndex } } - // 调整起始光标位置到控件合适的位置 public moveCursor(position: IControlInitOption): IMoveCursorResult { const { index, trIndex, tdIndex, tdValueIndex } = position let elementList = this.draw.getOriginalElementList() @@ -140,9 +144,158 @@ export class Control { } } - public destroyControl() { - if (this.activeControl) { - this.activeControl = null + 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 -1 + // 删除元素 + elementList.splice(leftIndex + 1, rightIndex - leftIndex) + return leftIndex + } + + public shrinkBoundary() { + const elementList = this.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 + ) { + console.log(index) + 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 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 + }) } } diff --git a/src/editor/core/draw/control/text/TextControl.ts b/src/editor/core/draw/control/text/TextControl.ts index 4b7a0dc..b515370 100644 --- a/src/editor/core/draw/control/text/TextControl.ts +++ b/src/editor/core/draw/control/text/TextControl.ts @@ -1,154 +1,21 @@ import { ControlComponent } from '../../../../dataset/enum/Control' -import { ElementType } from '../../../../dataset/enum/Element' import { KeyMap } from '../../../../dataset/enum/Keymap' -import { IControlInstance, IControlOption } from '../../../../interface/Control' +import { IControlInstance } from '../../../../interface/Control' import { IElement } from '../../../../interface/Element' -import { IRange } from '../../../../interface/Range' import { Control } from '../Control' export class TextControl implements IControlInstance { + private element: IElement private control: Control - private options: IControlOption - constructor(control: Control) { + constructor(element: IElement, control: Control) { + this.element = element this.control = control - this.options = control.getOptions().control } - public shrinkBoundary(elementList: IElement[], range: IRange) { - const { startIndex, endIndex } = range - if (startIndex === endIndex) return - const startElement = elementList[startIndex] - const endElement = elementList[endIndex] - // 首、尾为占位符时,收缩到最后一个前缀字符后 - 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 removePlaceholder(elementList: IElement[], range: IRange) { - const { startIndex } = range - 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 removeControl(elementList: IElement[], range: IRange): number { - const { startIndex } = range - 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 -1 - // 删除元素 - elementList.splice(leftIndex + 1, rightIndex - leftIndex) - // 清除实例 - this.control.destroyControl() - return leftIndex - } - - public addPlaceholder(elementList: IElement[], startIndex: number) { - 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 getElement(): IElement { + return this.element } public getValue(): IElement[] { @@ -193,14 +60,14 @@ export class TextControl implements IControlInstance { const elementList = this.control.getElementList() const range = this.control.getRange() // 收缩边界到Value内 - this.shrinkBoundary(elementList, range) + this.control.shrinkBoundary() const { startIndex, endIndex } = range // 移除选区元素 if (startIndex !== endIndex) { elementList.splice(startIndex + 1, endIndex - startIndex) } else { // 移除空白占位符 - this.removePlaceholder(elementList, range) + this.control.removePlaceholder(startIndex) } // 插入 const startElement = elementList[startIndex] @@ -219,7 +86,7 @@ export class TextControl implements IControlInstance { const elementList = this.control.getElementList() const range = this.control.getRange() // 收缩边界到Value内 - this.shrinkBoundary(elementList, range) + this.control.shrinkBoundary() const { startIndex, endIndex } = range const startElement = elementList[startIndex] const endElement = elementList[endIndex] @@ -230,22 +97,23 @@ export class TextControl implements IControlInstance { elementList.splice(startIndex + 1, endIndex - startIndex) const value = this.getValue() if (!value.length) { - this.addPlaceholder(elementList, startIndex) + this.control.addPlaceholder(startIndex) } return startIndex } else { - if (startElement.controlComponent === ControlComponent.PREFIX) { - // 前缀 - return this.removeControl(elementList, range) - } else if (endElement.controlComponent === ControlComponent.POSTFIX) { - // 后缀 - return this.removeControl(elementList, range) + 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.addPlaceholder(elementList, startIndex - 1) + this.control.addPlaceholder(startIndex - 1) } return startIndex - 1 } @@ -256,20 +124,23 @@ export class TextControl implements IControlInstance { elementList.splice(startIndex + 1, endIndex - startIndex) const value = this.getValue() if (!value.length) { - this.addPlaceholder(elementList, startIndex) + this.control.addPlaceholder(startIndex) } return startIndex } else { const endNextElement = elementList[endIndex + 1] - if (endNextElement.controlComponent === ControlComponent.POSTFIX) { - // 后缀 - return this.removeControl(elementList, range) + 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.addPlaceholder(elementList, startIndex) + this.control.addPlaceholder(startIndex) } return startIndex } diff --git a/src/editor/core/event/CanvasEvent.ts b/src/editor/core/event/CanvasEvent.ts index ed7559f..ab11b40 100644 --- a/src/editor/core/event/CanvasEvent.ts +++ b/src/editor/core/event/CanvasEvent.ts @@ -175,7 +175,7 @@ export class CanvasEvent { tdIndex, tdValueIndex } = positionResult - const { newIndex } = this.control.initControl({ + const { newIndex } = this.control.moveCursor({ index, isTable, trIndex, @@ -187,8 +187,6 @@ export class CanvasEvent { } else { positionResult.index = newIndex } - } else { - this.control.destroyControl() } const { index, @@ -303,6 +301,8 @@ export class CanvasEvent { let curIndex: number if (activeControl) { curIndex = this.control.keydown(evt) + } else if (elementList[endIndex + 1]?.type === ElementType.CONTROL) { + curIndex = this.control.removeControl(endIndex + 1) } else { if (!isCollapsed) { elementList.splice(startIndex + 1, endIndex - startIndex) @@ -476,6 +476,7 @@ export class CanvasEvent { // 忽略选区部分在控件的输入 return } + const activeControl = this.control.getActiveControl() const { TEXT, HYPERLINK, SUBSCRIPT, SUPERSCRIPT } = ElementType const text = data.replaceAll(`\n`, ZERO) const elementList = this.draw.getElementList() @@ -516,7 +517,7 @@ export class CanvasEvent { }) // 控件-移除placeholder let curIndex: number - if (positionContext.isControl) { + if (activeControl) { curIndex = this.control.setValue(inputData) } else { let start = 0 diff --git a/src/editor/core/range/RangeManager.ts b/src/editor/core/range/RangeManager.ts index 4366aa3..3a02e5d 100644 --- a/src/editor/core/range/RangeManager.ts +++ b/src/editor/core/range/RangeManager.ts @@ -53,6 +53,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() { diff --git a/src/editor/interface/Control.ts b/src/editor/interface/Control.ts index 850c1f6..3554ecf 100644 --- a/src/editor/interface/Control.ts +++ b/src/editor/interface/Control.ts @@ -41,6 +41,8 @@ export interface IControlInitResult { } export interface IControlInstance { + getElement(): IElement; + getValue(): IElement[]; setValue(data: IElement[]): number; From eb28d2059a5f91e751e7541958ceb4f1a13092fe Mon Sep 17 00:00:00 2001 From: Hufe921 Date: Tue, 5 Apr 2022 18:44:36 +0800 Subject: [PATCH 12/17] feat:text control copy and cut --- src/editor/core/draw/control/Control.ts | 7 +++++ .../core/draw/control/text/TextControl.ts | 15 +++++++++++ src/editor/core/event/CanvasEvent.ts | 27 ++++++++++++++----- src/editor/dataset/constant/Element.ts | 3 ++- src/editor/interface/Control.ts | 2 ++ 5 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/editor/core/draw/control/Control.ts b/src/editor/core/draw/control/Control.ts index 1ebdde5..f0748ec 100644 --- a/src/editor/core/draw/control/Control.ts +++ b/src/editor/core/draw/control/Control.ts @@ -313,4 +313,11 @@ export class Control { 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/text/TextControl.ts b/src/editor/core/draw/control/text/TextControl.ts index b515370..7068ac1 100644 --- a/src/editor/core/draw/control/text/TextControl.ts +++ b/src/editor/core/draw/control/text/TextControl.ts @@ -149,4 +149,19 @@ export class TextControl implements IControlInstance { return -1 } + 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 ab11b40..dfd25df 100644 --- a/src/editor/core/event/CanvasEvent.ts +++ b/src/editor/core/event/CanvasEvent.ts @@ -314,7 +314,7 @@ export class CanvasEvent { 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 = {} @@ -326,12 +326,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) { @@ -540,12 +545,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/dataset/constant/Element.ts b/src/editor/dataset/constant/Element.ts index 9c108fa..3edd78f 100644 --- a/src/editor/dataset/constant/Element.ts +++ b/src/editor/dataset/constant/Element.ts @@ -53,5 +53,6 @@ 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/interface/Control.ts b/src/editor/interface/Control.ts index 3554ecf..b7ededd 100644 --- a/src/editor/interface/Control.ts +++ b/src/editor/interface/Control.ts @@ -48,4 +48,6 @@ export interface IControlInstance { setValue(data: IElement[]): number; keydown(evt: KeyboardEvent): number; + + cut(): number; } \ No newline at end of file From 257204ad61dd16882babb7c8846fb8adbe859bd8 Mon Sep 17 00:00:00 2001 From: Hufe921 Date: Tue, 5 Apr 2022 22:39:16 +0800 Subject: [PATCH 13/17] feat:update select control mock data --- src/mock.ts | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/mock.ts b/src/mock.ts index 4a84fc7..dd02f25 100644 --- a/src/mock.ts +++ b/src/mock.ts @@ -1,6 +1,6 @@ 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 = ['人民医院门诊病历'] @@ -84,8 +84,28 @@ elementList.splice(24, 0, { } }) +// 模拟下拉控件 +elementList.splice(112, 0, { + type: ElementType.CONTROL, + value: '', + control: { + type: ControlType.SELECT, + value: null, + placeholder: '有无', + prefix: '{', + postfix: '}', + valueSets: [{ + value: '有', + code: '98175' + }, { + value: '无', + code: '98176' + }] + } +}) + // 模拟超链接 -elementList.splice(139, 0, { +elementList.splice(138, 0, { type: ElementType.HYPERLINK, value: '', valueList: [{ @@ -105,20 +125,20 @@ elementList.splice(139, 0, { }) // 模拟下标 -elementList.splice(372, 0, { +elementList.splice(371, 0, { value: '∆', color: '#FF0000', type: ElementType.SUBSCRIPT }) // 模拟上标 -elementList.splice(460, 0, { +elementList.splice(459, 0, { value: '9', type: ElementType.SUPERSCRIPT }) // 模拟图片 -elementList.splice(586, 0, { +elementList.splice(585, 0, { value: `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFkAAAAgCAYAAAB5JtSmAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAQ0SURBVGhD7dhrUSNBFAVgvKACEVjAAhJQgAIUYAABGEAABvgfAdn6UnWou01PppOZhIXNj1P9vo9zH5PK1Waz2V5wWlxIPgMuJJ8Bi0h+fn7eXl9fb29ubrYPDw/dO/8DHh8fu/vB4kym4Orqaofb29vund8OSSbhemewSrugBMnG3vlvw9vb265yn56edmtz/t/f33+5C8MkixQSZSsl9UzLOHUmcwTYAN/Rpl5eXnY+pnIB0Xd3d7s5m3rvDsrkCGszNiQ7r/tr4v39fSc/uipOqRcqufTHBiO78GGdzG5xcLtIFmVde7L9NsvXRo9s84+Pj+79pUAwn5GcD1wIz5r+fYGeJdnjGiF9hwL7iWAcfX19/evtKVHJXrtN8Rf4A3TVczqhrut5i1mSZQgnIriSWtdzP2N+EvIhi3/GWqHWtWXuy2IYbheiKarJZIZknkxyrryc2Utrgal+9S8iScUXIx/3kcxfe/jotcuDezLFlIbARDrzHpytXdKnQr4xyc74Vu9YV5Ih2Q/tT7mDSEYw5ZU4wu3nJx64k/1z9umlUG0hah/JSbC6Jzi5exDJWoTHERoBxu8uf/pT1j3HDkUIJitjbRfRA/iwVzlgy1RCfSF5ili9xj7BUWKs9wJZ3MpditYu+lsc+/PRx53cVF9Pdg/syE9Hb6cS75PkmhUEUFofmTvLGEXKimHueJP9Y3swWQwGLUiA9xEbHKuvgs4pPe1+1myTAKlw81buJ8kigjAXKauXPLQPhEYgJSEYsgdTUR0BmTVgc6C359wcvKGnBrGO8dO5VlD1ZZ519nrBHvrwKVMCas9hgL0YUI2wV98fC4FqCWizzXyqF44A0ZKLHkilgvPs1zbiTuZIdZ414KvqGCKZYx4zple+MSrrJVncAyL02/TOqncJwVMglx5zI4QDZ5WPvBGEcNP+7TlEcqJIAQFGsIdQjmZt7MlYA5yiI3pOQTCQXUm2TuVmXgmewxDJQDgl6deJJoU5y7p9uwZagmu1mCvbNoOOBfkhOf6lRZjzPb8qRjBMMiUhM9GNMZQq5/oRXBP7Mlj/i12A7EMIaJGqDcl8I79+/N1xTvdINQ2TDAQSvI9Md479vdqCHKSFQKAfEmgBqCTDkjaSgOZXQkg2jy1ti0xApnBQJo/0obQRipeQXbN3CmxKGQch5xgki4Efghl/kFqzPD//2DnXIodIRpaoETaXxcmwGNO7N4I2Oyuc6b+xK/tL9IH3kY/E+r1JdST4yM+7VUiuJbuPZHBeHZcNvXtziMMV9mRuvUOX8Vg9IFjRx9dUYM3s2oJyNx9ahFfSWwyRHKHG3nmL2q/mojyFVAWnEdi2Hg7OBXwUCCKr1QEtoe0+/9jI3xqIiuF2QRD0zqcwpfQnge9TVSI4tWrNe79shj98F0xDC0N4bTUVF5LPgAvJJ8dm+wcP2iJuZNdC5QAAAABJRU5ErkJggg==`, width: 89, height: 32, From 526f924ff848a879225b4a7a9d512ca735748a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BA=91=E9=A3=9E?= Date: Wed, 6 Apr 2022 17:30:41 +0800 Subject: [PATCH 14/17] feat:add select control --- src/editor/assets/css/control/select.css | 44 +++ src/editor/assets/css/index.css | 2 + src/editor/core/draw/control/Control.ts | 131 +++------ .../core/draw/control/select/SelectControl.ts | 253 +++++++++++++++++- .../core/draw/control/text/TextControl.ts | 2 +- src/editor/core/range/RangeManager.ts | 85 ++++++ src/editor/dataset/enum/Editor.ts | 3 +- src/editor/interface/Control.ts | 1 + src/editor/utils/element.ts | 24 +- src/mock.ts | 4 + 10 files changed, 453 insertions(+), 96 deletions(-) create mode 100644 src/editor/assets/css/control/select.css 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/draw/control/Control.ts b/src/editor/core/draw/control/Control.ts index f0748ec..1113a21 100644 --- a/src/editor/core/draw/control/Control.ts +++ b/src/editor/core/draw/control/Control.ts @@ -1,9 +1,10 @@ import { ControlComponent, ControlType } from '../../../dataset/enum/Control' import { ElementType } from '../../../dataset/enum/Element' import { IControlInitOption, IControlInstance, IControlOption } from '../../../interface/Control' -import { IElement } from '../../../interface/Element' +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 { @@ -40,14 +41,34 @@ export class Control { 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 } @@ -58,23 +79,42 @@ export class Control { 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() // 激活控件 - if (element.control!.type === ControlType.TEXT) { + 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() @@ -169,97 +209,12 @@ export class Control { } nextIndex++ } - if (!~leftIndex || !~rightIndex) return -1 + if (!~leftIndex || !~rightIndex) return startIndex // 删除元素 elementList.splice(leftIndex + 1, rightIndex - leftIndex) return leftIndex } - public shrinkBoundary() { - const elementList = this.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 - ) { - console.log(index) - 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 removePlaceholder(startIndex: number) { const elementList = this.getElementList() const startElement = elementList[startIndex] diff --git a/src/editor/core/draw/control/select/SelectControl.ts b/src/editor/core/draw/control/select/SelectControl.ts index e3608fa..9677995 100644 --- a/src/editor/core/draw/control/select/SelectControl.ts +++ b/src/editor/core/draw/control/select/SelectControl.ts @@ -1,3 +1,254 @@ -export class SelectControl { +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 index 7068ac1..3d5521e 100644 --- a/src/editor/core/draw/control/text/TextControl.ts +++ b/src/editor/core/draw/control/text/TextControl.ts @@ -146,7 +146,7 @@ export class TextControl implements IControlInstance { } } } - return -1 + return endIndex } public cut(): number { diff --git a/src/editor/core/range/RangeManager.ts b/src/editor/core/range/RangeManager.ts index 3a02e5d..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' @@ -137,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/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/interface/Control.ts b/src/editor/interface/Control.ts index b7ededd..b0d773f 100644 --- a/src/editor/interface/Control.ts +++ b/src/editor/interface/Control.ts @@ -7,6 +7,7 @@ export interface IValueSet { } export interface IControlSelect { + code: string | null; valueSets: IValueSet[]; } diff --git a/src/editor/utils/element.ts b/src/editor/utils/element.ts index e1e89a1..89cf9f4 100644 --- a/src/editor/utils/element.ts +++ b/src/editor/utils/element.ts @@ -3,7 +3,7 @@ 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 } from '../dataset/enum/Control' +import { ControlComponent, ControlType } from '../dataset/enum/Control' interface IFormatElementListOption { isHandleFirstElement?: boolean; @@ -71,7 +71,7 @@ export function formatElementList(elementList: IElement[], options: IFormatEleme } i-- } else if (el.type === ElementType.CONTROL) { - const { prefix, postfix, value, placeholder } = el.control! + const { prefix, postfix, value, placeholder, code, type, valueSets } = el.control! const controlId = getUUID() // 移除父节点 elementList.splice(i, 1) @@ -95,9 +95,23 @@ export function formatElementList(elementList: IElement[], options: IFormatEleme i++ } // 值 - if (value && value.length) { - for (let v = 0; v < value.length; v++) { - const element = value[v] + 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] diff --git a/src/mock.ts b/src/mock.ts index dd02f25..16627b2 100644 --- a/src/mock.ts +++ b/src/mock.ts @@ -91,6 +91,7 @@ elementList.splice(112, 0, { control: { type: ControlType.SELECT, value: null, + code: null, placeholder: '有无', prefix: '{', postfix: '}', @@ -100,6 +101,9 @@ elementList.splice(112, 0, { }, { value: '无', code: '98176' + }, { + value: '不详', + code: '98177' }] } }) From c6c1936468a7e8a479854c1524d29d8e555907dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BA=91=E9=A3=9E?= Date: Wed, 6 Apr 2022 18:06:01 +0800 Subject: [PATCH 15/17] fix:event error at control boundary --- src/editor/core/event/CanvasEvent.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/editor/core/event/CanvasEvent.ts b/src/editor/core/event/CanvasEvent.ts index dfd25df..96a7f37 100644 --- a/src/editor/core/event/CanvasEvent.ts +++ b/src/editor/core/event/CanvasEvent.ts @@ -273,6 +273,7 @@ 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() @@ -299,10 +300,8 @@ export class CanvasEvent { } else if (evt.key === KeyMap.Delete) { if (isReadonly || isPartRangeInControlOutside) return let curIndex: number - if (activeControl) { + if (activeControl && elementList[endIndex + 1]?.controlId === element.controlId) { curIndex = this.control.keydown(evt) - } else if (elementList[endIndex + 1]?.type === ElementType.CONTROL) { - curIndex = this.control.removeControl(endIndex + 1) } else { if (!isCollapsed) { elementList.splice(startIndex + 1, endIndex - startIndex) @@ -522,7 +521,7 @@ export class CanvasEvent { }) // 控件-移除placeholder let curIndex: number - if (activeControl) { + if (activeControl && elementList[endIndex + 1]?.controlId === element.controlId) { curIndex = this.control.setValue(inputData) } else { let start = 0 From d94f04fec4a882585fcad9c2aaa1435b2d0348e5 Mon Sep 17 00:00:00 2001 From: Hufe921 Date: Wed, 6 Apr 2022 21:57:28 +0800 Subject: [PATCH 16/17] feat:add control test case --- cypress/integration/control/select.spec.ts | 54 ++++++++++++++++++++++ cypress/integration/control/text.spec.ts | 45 ++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 cypress/integration/control/select.spec.ts create mode 100644 cypress/integration/control/text.spec.ts 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') + }) + }) + +}) From 408b731348e9c32c2b7f244042e4947af468d3e8 Mon Sep 17 00:00:00 2001 From: Hufe921 Date: Wed, 6 Apr 2022 22:10:27 +0800 Subject: [PATCH 17/17] fix:set element list before set range --- src/editor/core/draw/Draw.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editor/core/draw/Draw.ts b/src/editor/core/draw/Draw.ts index 3fa7524..55c8f5a 100644 --- a/src/editor/core/draw/Draw.ts +++ b/src/editor/core/draw/Draw.ts @@ -887,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 }) }) }