From c6d9cffc275ab4114a1c9a3ce931dc0843cb5585 Mon Sep 17 00:00:00 2001 From: huaworld <148570453+fangxiugao@users.noreply.github.com> Date: Thu, 2 May 2024 13:39:42 +0800 Subject: [PATCH] feat: add radio element #494 Co-authored-by: jeffycai Co-authored-by: Hufe921 --- docs/en/guide/option.md | 3 +- docs/en/guide/schema.md | 20 ++++- docs/guide/option.md | 3 +- docs/guide/schema.md | 20 ++++- index.html | 4 + src/assets/images/radio.svg | 4 + src/editor/core/draw/Draw.ts | 22 ++++++ src/editor/core/draw/control/Control.ts | 10 ++- .../draw/control/checkbox/CheckboxControl.ts | 4 +- .../core/draw/control/radio/RadioControl.ts | 57 ++++++++++++++ .../core/draw/particle/CheckboxParticle.ts | 4 +- .../core/draw/particle/RadioParticle.ts | 65 ++++++++++++++++ src/editor/core/event/handlers/click.ts | 9 ++- src/editor/core/event/handlers/mousedown.ts | 20 ++++- src/editor/core/position/Position.ts | 15 ++++ src/editor/core/worker/works/catalog.ts | 1 + src/editor/dataset/constant/Checkbox.ts | 2 +- src/editor/dataset/constant/Element.ts | 1 + src/editor/dataset/constant/Radio.ts | 10 +++ src/editor/dataset/enum/Control.ts | 6 +- src/editor/dataset/enum/Element.ts | 1 + src/editor/index.ts | 7 ++ src/editor/interface/Checkbox.ts | 2 +- src/editor/interface/Control.ts | 8 ++ src/editor/interface/Editor.ts | 2 + src/editor/interface/Element.ts | 6 ++ src/editor/interface/Position.ts | 2 + src/editor/interface/Radio.ts | 14 ++++ src/editor/utils/element.ts | 74 ++++++++++++++++++- src/main.ts | 52 +++++++++++++ src/style.css | 4 + 31 files changed, 433 insertions(+), 19 deletions(-) create mode 100644 src/assets/images/radio.svg create mode 100644 src/editor/core/draw/control/radio/RadioControl.ts create mode 100644 src/editor/core/draw/particle/RadioParticle.ts create mode 100644 src/editor/dataset/constant/Radio.ts create mode 100644 src/editor/interface/Radio.ts diff --git a/docs/en/guide/option.md b/docs/en/guide/option.md index eedf745..bd30ae8 100644 --- a/docs/en/guide/option.md +++ b/docs/en/guide/option.md @@ -61,7 +61,8 @@ interface IEditorOption { wordBreak?: WordBreak // Word and punctuation breaks: No punctuation in the first line of the BREAK_WORD &The word is not split, and the line is folded after BREAK_ALL full according to the width of the character. default: BREAK_WORD watermark?: IWatermark // Watermark{data:string; color?:string; opacity?:number; size?:number; font?:string;} control?: IControlOption // Control {placeholderColor?:string; bracketColor?:string; prefix?:string; postfix?:string; borderWidth?: number; borderColor?: string;} - checkbox?: ICheckboxOption // Checkbox {width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; fontStyle?: string;} + checkbox?: ICheckboxOption // Checkbox {width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string;} + radio?: IRadioOption // Radio {width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string;} cursor?: ICursorOption // Cursor style. {width?: number; color?: string; dragWidth?: number; dragColor?: string;} title?: ITitleOption // Title configuration.{ defaultFirstSize?: number; defaultSecondSize?: number; defaultThirdSize?: number defaultFourthSize?: number; defaultFifthSize?: number; defaultSixthSize?: number;} placeholder?: IPlaceholder // Placeholder text diff --git a/docs/en/guide/schema.md b/docs/en/guide/schema.md index 65950c6..c56bb06 100644 --- a/docs/en/guide/schema.md +++ b/docs/en/guide/schema.md @@ -15,6 +15,7 @@ interface IElement { PAGE_BREAK = 'pageBreak', CONTROL = 'control', CHECKBOX = 'checkbox', + RADIO = 'radio', LATEX = 'latex', TAB = 'tab', DATE = 'date', @@ -76,7 +77,8 @@ interface IElement { type: { TEXT = 'text', SELECT = 'select', - CHECKBOX = 'checkbox' + CHECKBOX = 'checkbox', + RADIO = 'radio' }; value: IElement[] | null; placeholder?: string; @@ -98,6 +100,13 @@ interface IElement { code: string; }[]; checkbox?: { + value: boolean | null; + code?: string; + min?: number; + max?: number; + disabled?: boolean; + }; + radio?: { value: boolean | null; code?: string; disabled?: boolean; @@ -115,7 +124,8 @@ interface IElement { POSTFIX = 'postfix', PLACEHOLDER = 'placeholder', VALUE = 'value', - CHECKBOX = 'checkbox' + CHECKBOX = 'checkbox', + RADIO = 'radio' }; // checkbox checkbox?: { @@ -123,6 +133,12 @@ interface IElement { code?: string; disabled?: boolean; }; + // radio + radio?: { + value: boolean | null; + code?: string; + disabled?: boolean; + }; // LaTeX laTexSVG?: string; // date diff --git a/docs/guide/option.md b/docs/guide/option.md index c81a1fe..3b055fc 100644 --- a/docs/guide/option.md +++ b/docs/guide/option.md @@ -61,7 +61,8 @@ interface IEditorOption { wordBreak?: WordBreak // 单词与标点断行:BREAK_WORD首行不出现标点&单词不拆分、BREAK_ALL按字符宽度撑满后折行。默认:BREAK_WORD watermark?: IWatermark // 水印信息。{data:string; color?:string; opacity?:number; size?:number; font?:string;} control?: IControlOption // 控件信息。 {placeholderColor?:string; bracketColor?:string; prefix?:string; postfix?:string; borderWidth?: number; borderColor?: string;} - checkbox?: ICheckboxOption // 复选框信息。{width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; fontStyle?: string;} + checkbox?: ICheckboxOption // 复选框信息。{width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string;} + radio?: IRadioOption // 单选框信息。{width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string;} cursor?: ICursorOption // 光标样式。{width?: number; color?: string; dragWidth?: number; dragColor?: string;} title?: ITitleOption // 标题配置。{ defaultFirstSize?: number; defaultSecondSize?: number; defaultThirdSize?: number defaultFourthSize?: number; defaultFifthSize?: number; defaultSixthSize?: number;} placeholder?: IPlaceholder // 编辑器空白占位文本 diff --git a/docs/guide/schema.md b/docs/guide/schema.md index 097c120..a2de5e5 100644 --- a/docs/guide/schema.md +++ b/docs/guide/schema.md @@ -15,6 +15,7 @@ interface IElement { PAGE_BREAK = 'pageBreak', CONTROL = 'control', CHECKBOX = 'checkbox', + RADIO = 'radio', LATEX = 'latex', TAB = 'tab', DATE = 'date', @@ -76,7 +77,8 @@ interface IElement { type: { TEXT = 'text', SELECT = 'select', - CHECKBOX = 'checkbox' + CHECKBOX = 'checkbox', + RADIO = 'radio' }; value: IElement[] | null; placeholder?: string; @@ -98,6 +100,13 @@ interface IElement { code: string; }[]; checkbox?: { + value: boolean | null; + code?: string; + min?: number; + max?: number; + disabled?: boolean; + }; + radio?: { value: boolean | null; code?: string; disabled?: boolean; @@ -115,7 +124,8 @@ interface IElement { POSTFIX = 'postfix', PLACEHOLDER = 'placeholder', VALUE = 'value', - CHECKBOX = 'checkbox' + CHECKBOX = 'checkbox', + RADIO = 'radio' }; // 复选框 checkbox?: { @@ -123,6 +133,12 @@ interface IElement { code?: string; disabled?: boolean; }; + // 单选框 + radio?: { + value: boolean | null; + code?: string; + disabled?: boolean; + }; // LaTeX laTexSVG?: string; // 日期 diff --git a/index.html b/index.html index a33a93c..2cbbdcc 100644 --- a/index.html +++ b/index.html @@ -280,12 +280,16 @@
  • 文本
  • 列举
  • 复选框
  • +
  • 单选框
  • + diff --git a/src/assets/images/radio.svg b/src/assets/images/radio.svg new file mode 100644 index 0000000..fff1439 --- /dev/null +++ b/src/assets/images/radio.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/editor/core/draw/Draw.ts b/src/editor/core/draw/Draw.ts index 9f5589d..e1c2f52 100644 --- a/src/editor/core/draw/Draw.ts +++ b/src/editor/core/draw/Draw.ts @@ -65,6 +65,7 @@ import { import { Control } from './control/Control' import { getSlimCloneElementList, zipElementList } from '../../utils/element' import { CheckboxParticle } from './particle/CheckboxParticle' +import { RadioParticle } from './particle/RadioParticle' import { DeepRequired, IPadding } from '../../interface/Common' import { ControlComponent, @@ -142,6 +143,7 @@ export class Draw { private superscriptParticle: SuperscriptParticle private subscriptParticle: SubscriptParticle private checkboxParticle: CheckboxParticle + private radioParticle: RadioParticle private blockParticle: BlockParticle private listParticle: ListParticle private lineBreakParticle: LineBreakParticle @@ -216,6 +218,7 @@ export class Draw { this.superscriptParticle = new SuperscriptParticle() this.subscriptParticle = new SubscriptParticle() this.checkboxParticle = new CheckboxParticle(this) + this.radioParticle = new RadioParticle(this) this.blockParticle = new BlockParticle(this) this.listParticle = new ListParticle(this) this.lineBreakParticle = new LineBreakParticle(this) @@ -751,6 +754,10 @@ export class Draw { return this.checkboxParticle } + public getRadioParticle(): RadioParticle { + return this.radioParticle + } + public getControl(): Control { return this.control } @@ -1414,6 +1421,15 @@ export class Draw { element.width = availableWidth / scale metrics.width = availableWidth metrics.height = defaultSize + } else if ( + element.type === ElementType.RADIO || + element.controlComponent === ControlComponent.RADIO + ) { + const { width, height, gap } = this.options.radio + const elementWidth = width + gap * 2 + element.width = elementWidth + metrics.width = elementWidth * scale + metrics.height = height * scale } else if ( element.type === ElementType.CHECKBOX || element.controlComponent === ControlComponent.CHECKBOX @@ -1805,6 +1821,12 @@ export class Draw { ) { this.textParticle.complete() this.checkboxParticle.render(ctx, element, x, y + offsetY) + } else if ( + element.type === ElementType.RADIO || + element.controlComponent === ControlComponent.RADIO + ) { + this.textParticle.complete() + this.radioParticle.render(ctx, element, x, y + offsetY) } else if (element.type === ElementType.TAB) { this.textParticle.complete() } else if (element.rowFlex === RowFlex.ALIGNMENT) { diff --git a/src/editor/core/draw/control/Control.ts b/src/editor/core/draw/control/Control.ts index c2818d0..b3f4cfc 100644 --- a/src/editor/core/draw/control/Control.ts +++ b/src/editor/core/draw/control/Control.ts @@ -32,6 +32,7 @@ import { Listener } from '../../listener/Listener' import { RangeManager } from '../../range/RangeManager' import { Draw } from '../Draw' import { CheckboxControl } from './checkbox/CheckboxControl' +import { RadioControl } from './radio/RadioControl' import { ControlSearch } from './interactive/ControlSearch' import { ControlBorder } from './richtext/Border' import { SelectControl } from './select/SelectControl' @@ -236,6 +237,8 @@ export class Control { selectControl.awake() } else if (control.type === ControlType.CHECKBOX) { this.activeControl = new CheckboxControl(element, this) + } else if (control.type === ControlType.RADIO) { + this.activeControl = new RadioControl(element, this) } // 激活控件回调 nextTick(() => { @@ -522,7 +525,8 @@ export class Control { }) } else if ( type === ControlType.SELECT || - type === ControlType.CHECKBOX + type === ControlType.CHECKBOX || + type === ControlType.RADIO ) { const innerText = code ?.split(',') @@ -629,6 +633,10 @@ export class Control { const checkbox = new CheckboxControl(element, this) const codes = value?.split(',') || [] checkbox.setSelect(codes, controlContext, controlRule) + } else if (type === ControlType.RADIO) { + const radio = new RadioControl(element, this) + const codes = value ? [value] : [] + radio.setSelect(codes, controlContext, controlRule) } // 修改后控件结束索引 let newEndIndex = i diff --git a/src/editor/core/draw/control/checkbox/CheckboxControl.ts b/src/editor/core/draw/control/checkbox/CheckboxControl.ts index a6a245f..23bdcc6 100644 --- a/src/editor/core/draw/control/checkbox/CheckboxControl.ts +++ b/src/editor/core/draw/control/checkbox/CheckboxControl.ts @@ -9,8 +9,8 @@ import { IElement } from '../../../../interface/Element' import { Control } from '../Control' export class CheckboxControl implements IControlInstance { - private element: IElement - private control: Control + protected element: IElement + protected control: Control constructor(element: IElement, control: Control) { this.element = element diff --git a/src/editor/core/draw/control/radio/RadioControl.ts b/src/editor/core/draw/control/radio/RadioControl.ts new file mode 100644 index 0000000..9965379 --- /dev/null +++ b/src/editor/core/draw/control/radio/RadioControl.ts @@ -0,0 +1,57 @@ +import { ControlComponent } from '../../../../dataset/enum/Control' +import { + IControlContext, + IControlRuleOption +} from '../../../../interface/Control' +import { CheckboxControl } from '../checkbox/CheckboxControl' + +export class RadioControl extends CheckboxControl { + public setSelect( + codes: string[], + context: IControlContext = {}, + options: IControlRuleOption = {} + ) { + // 校验是否可以设置 + if (!options.isIgnoreDisabledRule && this.control.getIsDisabledControl()) { + return + } + const { control } = this.element + const elementList = context.elementList || this.control.getElementList() + const { startIndex } = context.range || this.control.getRange() + const startElement = elementList[startIndex] + // 向左查找 + let preIndex = startIndex + while (preIndex > 0) { + const preElement = elementList[preIndex] + if ( + preElement.controlId !== startElement.controlId || + preElement.controlComponent === ControlComponent.PREFIX + ) { + break + } + if (preElement.controlComponent === ControlComponent.RADIO) { + const radio = preElement.radio! + radio.value = codes.includes(radio.code!) + } + 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.RADIO) { + const radio = nextElement.radio! + radio.value = codes.includes(radio.code!) + } + nextIndex++ + } + control!.code = codes.join(',') + this.control.repaintControl() + } +} diff --git a/src/editor/core/draw/particle/CheckboxParticle.ts b/src/editor/core/draw/particle/CheckboxParticle.ts index b16e9a3..1a5817b 100644 --- a/src/editor/core/draw/particle/CheckboxParticle.ts +++ b/src/editor/core/draw/particle/CheckboxParticle.ts @@ -35,7 +35,7 @@ export class CheckboxParticle { y: number ) { const { - checkbox: { gap, lineWidth, fillStyle, fontStyle }, + checkbox: { gap, lineWidth, fillStyle, strokeStyle }, scale } = this.options const { metrics, checkbox } = element @@ -60,7 +60,7 @@ export class CheckboxParticle { ctx.fillRect(left, top, width, height) // 勾选对号 ctx.beginPath() - ctx.strokeStyle = fontStyle + ctx.strokeStyle = strokeStyle ctx.lineWidth = lineWidth * 2 * scale ctx.moveTo(left + 2 * scale, top + height / 2) ctx.lineTo(left + width / 2, top + height - 3 * scale) diff --git a/src/editor/core/draw/particle/RadioParticle.ts b/src/editor/core/draw/particle/RadioParticle.ts new file mode 100644 index 0000000..a6a2e25 --- /dev/null +++ b/src/editor/core/draw/particle/RadioParticle.ts @@ -0,0 +1,65 @@ +import { DeepRequired } from '../../../interface/Common' +import { IEditorOption } from '../../../interface/Editor' +import { IElement } from '../../../interface/Element' +import { IRowElement } from '../../../interface/Row' +import { Draw } from '../Draw' + +export class RadioParticle { + private draw: Draw + private options: DeepRequired + + constructor(draw: Draw) { + this.draw = draw + this.options = draw.getOptions() + } + + public setSelect(element: IElement) { + const { radio } = element + if (radio) { + radio.value = !radio.value + } else { + element.radio = { + value: true + } + } + this.draw.render({ + isCompute: false, + isSetCursor: false + }) + } + + public render( + ctx: CanvasRenderingContext2D, + element: IRowElement, + x: number, + y: number + ) { + const { + radio: { gap, lineWidth, fillStyle, strokeStyle }, + scale + } = this.options + const { metrics, radio } = element + // left top 四舍五入避免1像素问题 + const left = Math.round(x + gap * scale) + const top = Math.round(y - metrics.height + lineWidth) + const width = metrics.width - gap * 2 * scale + const height = metrics.height + ctx.save() + ctx.beginPath() + ctx.translate(0.5, 0.5) + // 边框 + ctx.strokeStyle = radio?.value ? fillStyle : strokeStyle + ctx.lineWidth = lineWidth + ctx.arc(left + width / 2, top + height / 2, width / 2, 0, Math.PI * 2) + ctx.stroke() + // 填充选中色 + if (radio?.value) { + ctx.beginPath() + ctx.fillStyle = fillStyle + ctx.arc(left + width / 2, top + height / 2, width / 3, 0, Math.PI * 2) + ctx.fill() + } + ctx.closePath() + ctx.restore() + } +} diff --git a/src/editor/core/event/handlers/click.ts b/src/editor/core/event/handlers/click.ts index d2c3cc7..5c3fc05 100644 --- a/src/editor/core/event/handlers/click.ts +++ b/src/editor/core/event/handlers/click.ts @@ -124,8 +124,13 @@ function dblclick(host: CanvasEvent, evt: MouseEvent) { return } } - // 复选框双击时是切换选择状态,禁用扩选 - if (positionContext.isCheckbox && positionContext.isDirectHit) return + // 复选/单选框双击时是切换选择状态,禁用扩选 + if ( + (positionContext.isCheckbox || positionContext.isRadio) && + positionContext.isDirectHit + ) { + return + } // 自动扩选文字-分词处理,优先使用分词器否则降级使用光标所在位置 const rangeManager = draw.getRange() const segmenterRange = diff --git a/src/editor/core/event/handlers/mousedown.ts b/src/editor/core/event/handlers/mousedown.ts index 8ba08c4..3dfc1fa 100644 --- a/src/editor/core/event/handlers/mousedown.ts +++ b/src/editor/core/event/handlers/mousedown.ts @@ -4,6 +4,7 @@ import { MouseEventButton } from '../../../dataset/enum/Event' import { deepClone } from '../../../utils' import { isMod } from '../../../utils/hotkey' import { CheckboxControl } from '../../draw/control/checkbox/CheckboxControl' +import { RadioControl } from '../../draw/control/radio/RadioControl' import { CanvasEvent } from '../CanvasEvent' export function setRangeCache(host: CanvasEvent) { @@ -54,6 +55,7 @@ export function mousedown(evt: MouseEvent, host: CanvasEvent) { index, isDirectHit, isCheckbox, + isRadio, isImage, isTable, tdValueIndex, @@ -73,11 +75,14 @@ export function mousedown(evt: MouseEvent, host: CanvasEvent) { // 绘制 const isDirectHitImage = !!(isDirectHit && isImage) const isDirectHitCheckbox = !!(isDirectHit && isCheckbox) + const isDirectHitRadio = !!(isDirectHit && isRadio) if (~index) { rangeManager.setRange(curIndex, curIndex) position.setCursorPosition(positionList[curIndex]) // 复选框 const isSetCheckbox = isDirectHitCheckbox && !isReadonly + // 单选框 + const isSetRadio = isDirectHitRadio && !isReadonly if (isSetCheckbox) { const { checkbox, control } = curElement // 复选框不在控件内独立控制 @@ -98,12 +103,25 @@ export function mousedown(evt: MouseEvent, host: CanvasEvent) { activeControl.setSelect(codes) } } + } else if (isSetRadio) { + const { control, radio } = curElement + // 单选框不在控件内独立控制 + if (!control) { + draw.getRadioParticle().setSelect(curElement) + } else { + const codes = radio?.code ? [radio.code] : [] + const activeControl = draw.getControl().getActiveControl() + if (activeControl instanceof RadioControl) { + activeControl.setSelect(codes) + } + } } else { draw.render({ curIndex, isCompute: false, isSubmitHistory: false, - isSetCursor: !isDirectHitImage && !isDirectHitCheckbox + isSetCursor: + !isDirectHitImage && !isDirectHitCheckbox && !isDirectHitRadio }) } // 首字需定位到行首,非上一行最后一个字后 diff --git a/src/editor/core/position/Position.ts b/src/editor/core/position/Position.ts index c193732..3faa0fe 100644 --- a/src/editor/core/position/Position.ts +++ b/src/editor/core/position/Position.ts @@ -394,6 +394,9 @@ export class Position { tdValueElement.type === ElementType.CHECKBOX || tdValueElement.controlComponent === ControlComponent.CHECKBOX, + isRadio: + tdValueElement.type === ElementType.RADIO || + tdValueElement.controlComponent === ControlComponent.RADIO, isControl: !!tdValueElement.controlId, isImage: tablePosition.isImage, isDirectHit: tablePosition.isDirectHit, @@ -431,6 +434,16 @@ export class Position { isCheckbox: true } } + if ( + element.type === ElementType.RADIO || + element.controlComponent === ControlComponent.RADIO + ) { + return { + index: curPositionIndex, + isDirectHit: true, + isRadio: true + } + } let hitLineStartIndex: number | undefined // 判断是否在文字中间前后 if (elementList[index].value !== ZERO) { @@ -651,6 +664,7 @@ export class Position { const { index, isCheckbox, + isRadio, isControl, isTable, trIndex, @@ -663,6 +677,7 @@ export class Position { this.setPositionContext({ isTable: isTable || false, isCheckbox: isCheckbox || false, + isRadio: isRadio || false, isControl: isControl || false, index, trIndex, diff --git a/src/editor/core/worker/works/catalog.ts b/src/editor/core/worker/works/catalog.ts index dc6a2b9..c31b5f3 100644 --- a/src/editor/core/worker/works/catalog.ts +++ b/src/editor/core/worker/works/catalog.ts @@ -12,6 +12,7 @@ enum ElementType { PAGE_BREAK = 'pageBreak', CONTROL = 'control', CHECKBOX = 'checkbox', + RADIO = 'radio', LATEX = 'latex', TAB = 'tab', DATE = 'date', diff --git a/src/editor/dataset/constant/Checkbox.ts b/src/editor/dataset/constant/Checkbox.ts index 7df38b0..3153027 100644 --- a/src/editor/dataset/constant/Checkbox.ts +++ b/src/editor/dataset/constant/Checkbox.ts @@ -6,5 +6,5 @@ export const defaultCheckboxOption: Readonly> = { gap: 5, lineWidth: 1, fillStyle: '#5175f4', - fontStyle: '#ffffff' + strokeStyle: '#ffffff' } diff --git a/src/editor/dataset/constant/Element.ts b/src/editor/dataset/constant/Element.ts index 3476224..2d65579 100644 --- a/src/editor/dataset/constant/Element.ts +++ b/src/editor/dataset/constant/Element.ts @@ -60,6 +60,7 @@ export const EDITOR_ELEMENT_ZIP_ATTR: Array = [ 'valueList', 'control', 'checkbox', + 'radio', 'dateFormat', 'block', 'level', diff --git a/src/editor/dataset/constant/Radio.ts b/src/editor/dataset/constant/Radio.ts new file mode 100644 index 0000000..118d783 --- /dev/null +++ b/src/editor/dataset/constant/Radio.ts @@ -0,0 +1,10 @@ +import { IRadioOption } from '../../interface/Radio' + +export const defaultRadioOption: Readonly> = { + width: 14, + height: 14, + gap: 5, + lineWidth: 1, + fillStyle: '#5175f4', + strokeStyle: '#000000' +} diff --git a/src/editor/dataset/enum/Control.ts b/src/editor/dataset/enum/Control.ts index 80308ee..61a52d8 100644 --- a/src/editor/dataset/enum/Control.ts +++ b/src/editor/dataset/enum/Control.ts @@ -1,7 +1,8 @@ export enum ControlType { TEXT = 'text', SELECT = 'select', - CHECKBOX = 'checkbox' + CHECKBOX = 'checkbox', + RADIO = 'radio' } export enum ControlComponent { @@ -9,7 +10,8 @@ export enum ControlComponent { POSTFIX = 'postfix', PLACEHOLDER = 'placeholder', VALUE = 'value', - CHECKBOX = 'checkbox' + CHECKBOX = 'checkbox', + RADIO = 'radio' } // 控件内容缩进方式 diff --git a/src/editor/dataset/enum/Element.ts b/src/editor/dataset/enum/Element.ts index bffe15c..23a5dc6 100644 --- a/src/editor/dataset/enum/Element.ts +++ b/src/editor/dataset/enum/Element.ts @@ -9,6 +9,7 @@ export enum ElementType { PAGE_BREAK = 'pageBreak', CONTROL = 'control', CHECKBOX = 'checkbox', + RADIO = 'radio', LATEX = 'latex', TAB = 'tab', DATE = 'date', diff --git a/src/editor/index.ts b/src/editor/index.ts index a3bf4e5..d0a2380 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -32,7 +32,9 @@ import { ControlIndentation, ControlType } from './dataset/enum/Control' import { defaultControlOption } from './dataset/constant/Control' import { IControlOption } from './interface/Control' import { ICheckboxOption } from './interface/Checkbox' +import { IRadioOption } from './interface/Radio' import { defaultCheckboxOption } from './dataset/constant/Checkbox' +import { defaultRadioOption } from './dataset/constant/Radio' import { DeepRequired } from './interface/Common' import { INavigateInfo } from './core/draw/interactive/Search' import { Shortcut } from './core/shortcut/Shortcut' @@ -117,6 +119,10 @@ export default class Editor { ...defaultCheckboxOption, ...options.checkbox } + const radioOptions: Required = { + ...defaultRadioOption, + ...options.radio + } const cursorOptions: Required = { ...defaultCursorOption, ...options.cursor @@ -200,6 +206,7 @@ export default class Editor { watermark: waterMarkOptions, control: controlOptions, checkbox: checkboxOptions, + radio: radioOptions, cursor: cursorOptions, title: titleOptions, placeholder: placeholderOptions, diff --git a/src/editor/interface/Checkbox.ts b/src/editor/interface/Checkbox.ts index 71fb359..13ad547 100644 --- a/src/editor/interface/Checkbox.ts +++ b/src/editor/interface/Checkbox.ts @@ -10,5 +10,5 @@ export interface ICheckboxOption { gap?: number lineWidth?: number fillStyle?: string - fontStyle?: string + strokeStyle?: string } diff --git a/src/editor/interface/Control.ts b/src/editor/interface/Control.ts index ed80f5c..430475a 100644 --- a/src/editor/interface/Control.ts +++ b/src/editor/interface/Control.ts @@ -2,6 +2,7 @@ import { ControlType, ControlIndentation } from '../dataset/enum/Control' import { EditorZone } from '../dataset/enum/Editor' import { ICheckbox } from './Checkbox' import { IElement } from './Element' +import { IRadio } from './Radio' import { IRange } from './Range' export interface IValueSet { @@ -22,6 +23,12 @@ export interface IControlCheckbox { checkbox?: ICheckbox } +export interface IControlRadio { + code: string | null + valueSets: IValueSet[] + radio?: IRadio +} + export interface IControlHighlightRule { keyword: string alpha?: number @@ -65,6 +72,7 @@ export type IControl = IControlBasic & IControlRule & Partial & Partial & + Partial & Partial export interface IControlOption { diff --git a/src/editor/interface/Editor.ts b/src/editor/interface/Editor.ts index 3f2f440..ab491ef 100644 --- a/src/editor/interface/Editor.ts +++ b/src/editor/interface/Editor.ts @@ -7,6 +7,7 @@ import { } from '../dataset/enum/Editor' import { IBackgroundOption } from './Background' import { ICheckboxOption } from './Checkbox' +import { IRadioOption } from './Radio' import { IPadding } from './Common' import { IControlOption } from './Control' import { ICursorOption } from './Cursor' @@ -77,6 +78,7 @@ export interface IEditorOption { watermark?: IWatermark control?: IControlOption checkbox?: ICheckboxOption + radio?: IRadioOption cursor?: ICursorOption title?: ITitleOption placeholder?: IPlaceholder diff --git a/src/editor/interface/Element.ts b/src/editor/interface/Element.ts index 47cbf5b..97a7f92 100644 --- a/src/editor/interface/Element.ts +++ b/src/editor/interface/Element.ts @@ -8,6 +8,7 @@ import { TableBorder } from '../dataset/enum/table/Table' import { IBlock } from './Block' import { ICheckbox } from './Checkbox' import { IControl } from './Control' +import { IRadio } from './Radio' import { ITextDecoration } from './Text' import { IColgroup } from './table/Colgroup' import { ITr } from './table/Tr' @@ -94,6 +95,10 @@ export interface ICheckboxElement { checkbox?: ICheckbox } +export interface IRadioElement { + radio?: IRadio +} + export interface ILaTexElement { laTexSVG?: string } @@ -124,6 +129,7 @@ export type IElement = IElementBasic & ISeparator & IControlElement & ICheckboxElement & + IRadioElement & ILaTexElement & IDateElement & IImageElement & diff --git a/src/editor/interface/Position.ts b/src/editor/interface/Position.ts index 7033b7f..b5c030b 100644 --- a/src/editor/interface/Position.ts +++ b/src/editor/interface/Position.ts @@ -9,6 +9,7 @@ export interface ICurrentPosition { x?: number y?: number isCheckbox?: boolean + isRadio?: boolean isControl?: boolean isImage?: boolean isTable?: boolean @@ -41,6 +42,7 @@ export type IGetFloatPositionByXYPayload = IGetPositionByXYPayload & { export interface IPositionContext { isTable: boolean isCheckbox?: boolean + isRadio?: boolean isControl?: boolean index?: number trIndex?: number diff --git a/src/editor/interface/Radio.ts b/src/editor/interface/Radio.ts new file mode 100644 index 0000000..1f4fc61 --- /dev/null +++ b/src/editor/interface/Radio.ts @@ -0,0 +1,14 @@ +export interface IRadio { + value: boolean | null + code?: string + disabled?: boolean +} + +export interface IRadioOption { + width?: number + height?: number + gap?: number + lineWidth?: number + fillStyle?: string + strokeStyle?: string +} diff --git a/src/editor/utils/element.ts b/src/editor/utils/element.ts index 62f2294..4bf0ca3 100644 --- a/src/editor/utils/element.ts +++ b/src/editor/utils/element.ts @@ -222,7 +222,11 @@ export function formatElementList( const { prefix, postfix, value, placeholder, code, type, valueSets } = el.control const { - editorOptions: { control: controlOption, checkbox: checkboxOption } + editorOptions: { + control: controlOption, + checkbox: checkboxOption, + radio: radioOption + } } = options const controlId = getUUID() // 移除父节点 @@ -258,6 +262,7 @@ export function formatElementList( if ( (value && value.length) || type === ControlType.CHECKBOX || + type === ControlType.RADIO || (type === ControlType.SELECT && code && (!value || !value.length)) ) { let valueList: IElement[] = value || [] @@ -309,6 +314,53 @@ export function formatElementList( } } } + } else if (type === ControlType.RADIO) { + if (Array.isArray(valueSets) && valueSets.length) { + // 拆分valueList优先使用其属性 + const valueStyleList = valueList.reduce( + (pre, cur) => + pre.concat( + cur.value.split('').map(v => ({ ...cur, value: v })) + ), + [] as IElement[] + ) + let valueStyleIndex = 0 + for (let v = 0; v < valueSets.length; v++) { + const valueSet = valueSets[v] + // radio组件 + elementList.splice(i, 0, { + ...controlContext, + controlId, + value: '', + type: el.type, + control: el.control, + controlComponent: ControlComponent.RADIO, + radio: { + code: valueSet.code, + value: code === valueSet.code + } + }) + i++ + // 文本 + const valueStrList = splitText(valueSet.value) + for (let e = 0; e < valueStrList.length; e++) { + const value = valueStrList[e] + const isLastLetter = e === valueStrList.length - 1 + elementList.splice(i, 0, { + ...controlContext, + ...controlDefaultStyle, + ...valueStyleList[valueStyleIndex], + controlId, + value: value === '\n' ? ZERO : value, + letterSpacing: isLastLetter ? radioOption.gap : 0, + control: el.control, + controlComponent: ControlComponent.VALUE + }) + valueStyleIndex++ + i++ + } + } + } } else { if (!value || !value.length) { if (Array.isArray(valueSets) && valueSets.length) { @@ -955,6 +1007,13 @@ export function createDomFromElementList( checkbox.setAttribute('checked', 'true') } clipboardDom.append(checkbox) + } else if (element.type === ElementType.RADIO) { + const radio = document.createElement('input') + radio.type = 'radio' + if (element.radio?.value) { + radio.setAttribute('checked', 'true') + } + clipboardDom.append(radio) } else if (element.type === ElementType.TAB) { const tab = document.createElement('span') tab.innerHTML = `${NON_BREAKING_SPACE}${NON_BREAKING_SPACE}` @@ -1200,6 +1259,17 @@ export function getElementListByHTML( value: (node).checked } }) + } else if ( + node.nodeName === 'INPUT' && + (node).type === ControlComponent.RADIO + ) { + elementList.push({ + type: ElementType.RADIO, + value: '', + radio: { + value: (node).checked + } + }) } else { findTextNode(node) if (node.nodeType === 1 && n !== childNodes.length - 1) { @@ -1275,6 +1345,8 @@ export function getTextFromElementList(elementList: IElement[]) { }) } else if (element.type === ElementType.CHECKBOX) { text += element.checkbox?.value ? `☑` : `□` + } else if (element.type === ElementType.RADIO) { + text += element.radio?.value ? `☉` : `○` } else if ( !element.type || element.type === ElementType.LATEX || diff --git a/src/main.ts b/src/main.ts index 1ae3981..a3846f1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -768,6 +768,44 @@ window.onload = function () { } }) break + case ControlType.RADIO: + new Dialog({ + title: '单选框控件', + data: [ + { + type: 'text', + label: '默认值', + name: 'code', + placeholder: '请输入默认值' + }, + { + type: 'textarea', + label: '值集', + name: 'valueSets', + required: true, + height: 100, + placeholder: `请输入值集JSON,例:\n[{\n"value":"有",\n"code":"98175"\n}]` + } + ], + onConfirm: payload => { + const valueSets = payload.find(p => p.name === 'valueSets')?.value + if (!valueSets) return + const code = payload.find(p => p.name === 'code')?.value + instance.command.executeInsertElementList([ + { + type: ElementType.CONTROL, + value: '', + control: { + type, + code, + value: null, + valueSets: JSON.parse(valueSets) + } + } + ]) + } + }) + break default: break } @@ -789,6 +827,20 @@ window.onload = function () { ]) } + const radioDom = document.querySelector('.menu-item__radio')! + radioDom.onclick = function () { + console.log('radio') + instance.command.executeInsertElementList([ + { + type: ElementType.RADIO, + checkbox: { + value: false + }, + value: '' + } + ]) + } + const latexDom = document.querySelector('.menu-item__latex')! latexDom.onclick = function () { console.log('LaTeX') diff --git a/src/style.css b/src/style.css index 5f9ef23..959a6b5 100644 --- a/src/style.css +++ b/src/style.css @@ -540,6 +540,10 @@ ul { background-image: url('./assets/images/checkbox.svg'); } +.menu-item__radio i { + background-image: url('./assets/images/radio.svg'); +} + .menu-item__latex i { background-image: url('./assets/images/latex.svg'); }