feat: add radio element #494

Co-authored-by: jeffycai <caixiaobing@live.cn>
Co-authored-by: Hufe921 <huangyunfeihufe@hotmail.com>
pr675
huaworld 2 years ago committed by GitHub
parent 2e1403507f
commit c6d9cffc27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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 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;} 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;} 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;} 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;} title?: ITitleOption // Title configuration.{ defaultFirstSize?: number; defaultSecondSize?: number; defaultThirdSize?: number defaultFourthSize?: number; defaultFifthSize?: number; defaultSixthSize?: number;}
placeholder?: IPlaceholder // Placeholder text placeholder?: IPlaceholder // Placeholder text

@ -15,6 +15,7 @@ interface IElement {
PAGE_BREAK = 'pageBreak', PAGE_BREAK = 'pageBreak',
CONTROL = 'control', CONTROL = 'control',
CHECKBOX = 'checkbox', CHECKBOX = 'checkbox',
RADIO = 'radio',
LATEX = 'latex', LATEX = 'latex',
TAB = 'tab', TAB = 'tab',
DATE = 'date', DATE = 'date',
@ -76,7 +77,8 @@ interface IElement {
type: { type: {
TEXT = 'text', TEXT = 'text',
SELECT = 'select', SELECT = 'select',
CHECKBOX = 'checkbox' CHECKBOX = 'checkbox',
RADIO = 'radio'
}; };
value: IElement[] | null; value: IElement[] | null;
placeholder?: string; placeholder?: string;
@ -98,6 +100,13 @@ interface IElement {
code: string; code: string;
}[]; }[];
checkbox?: { checkbox?: {
value: boolean | null;
code?: string;
min?: number;
max?: number;
disabled?: boolean;
};
radio?: {
value: boolean | null; value: boolean | null;
code?: string; code?: string;
disabled?: boolean; disabled?: boolean;
@ -115,7 +124,8 @@ interface IElement {
POSTFIX = 'postfix', POSTFIX = 'postfix',
PLACEHOLDER = 'placeholder', PLACEHOLDER = 'placeholder',
VALUE = 'value', VALUE = 'value',
CHECKBOX = 'checkbox' CHECKBOX = 'checkbox',
RADIO = 'radio'
}; };
// checkbox // checkbox
checkbox?: { checkbox?: {
@ -123,6 +133,12 @@ interface IElement {
code?: string; code?: string;
disabled?: boolean; disabled?: boolean;
}; };
// radio
radio?: {
value: boolean | null;
code?: string;
disabled?: boolean;
};
// LaTeX // LaTeX
laTexSVG?: string; laTexSVG?: string;
// date // date

@ -61,7 +61,8 @@ interface IEditorOption {
wordBreak?: WordBreak // 单词与标点断行BREAK_WORD首行不出现标点&单词不拆分、BREAK_ALL按字符宽度撑满后折行。默认BREAK_WORD wordBreak?: WordBreak // 单词与标点断行BREAK_WORD首行不出现标点&单词不拆分、BREAK_ALL按字符宽度撑满后折行。默认BREAK_WORD
watermark?: IWatermark // 水印信息。{data:string; color?:string; opacity?:number; size?:number; font?:string;} 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;} 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;} cursor?: ICursorOption // 光标样式。{width?: number; color?: string; dragWidth?: number; dragColor?: string;}
title?: ITitleOption // 标题配置。{ defaultFirstSize?: number; defaultSecondSize?: number; defaultThirdSize?: number defaultFourthSize?: number; defaultFifthSize?: number; defaultSixthSize?: number;} title?: ITitleOption // 标题配置。{ defaultFirstSize?: number; defaultSecondSize?: number; defaultThirdSize?: number defaultFourthSize?: number; defaultFifthSize?: number; defaultSixthSize?: number;}
placeholder?: IPlaceholder // 编辑器空白占位文本 placeholder?: IPlaceholder // 编辑器空白占位文本

@ -15,6 +15,7 @@ interface IElement {
PAGE_BREAK = 'pageBreak', PAGE_BREAK = 'pageBreak',
CONTROL = 'control', CONTROL = 'control',
CHECKBOX = 'checkbox', CHECKBOX = 'checkbox',
RADIO = 'radio',
LATEX = 'latex', LATEX = 'latex',
TAB = 'tab', TAB = 'tab',
DATE = 'date', DATE = 'date',
@ -76,7 +77,8 @@ interface IElement {
type: { type: {
TEXT = 'text', TEXT = 'text',
SELECT = 'select', SELECT = 'select',
CHECKBOX = 'checkbox' CHECKBOX = 'checkbox',
RADIO = 'radio'
}; };
value: IElement[] | null; value: IElement[] | null;
placeholder?: string; placeholder?: string;
@ -98,6 +100,13 @@ interface IElement {
code: string; code: string;
}[]; }[];
checkbox?: { checkbox?: {
value: boolean | null;
code?: string;
min?: number;
max?: number;
disabled?: boolean;
};
radio?: {
value: boolean | null; value: boolean | null;
code?: string; code?: string;
disabled?: boolean; disabled?: boolean;
@ -115,7 +124,8 @@ interface IElement {
POSTFIX = 'postfix', POSTFIX = 'postfix',
PLACEHOLDER = 'placeholder', PLACEHOLDER = 'placeholder',
VALUE = 'value', VALUE = 'value',
CHECKBOX = 'checkbox' CHECKBOX = 'checkbox',
RADIO = 'radio'
}; };
// 复选框 // 复选框
checkbox?: { checkbox?: {
@ -123,6 +133,12 @@ interface IElement {
code?: string; code?: string;
disabled?: boolean; disabled?: boolean;
}; };
// 单选框
radio?: {
value: boolean | null;
code?: string;
disabled?: boolean;
};
// LaTeX // LaTeX
laTexSVG?: string; laTexSVG?: string;
// 日期 // 日期

@ -280,12 +280,16 @@
<li data-control='text'>文本</li> <li data-control='text'>文本</li>
<li data-control="select">列举</li> <li data-control="select">列举</li>
<li data-control="checkbox">复选框</li> <li data-control="checkbox">复选框</li>
<li data-control="radio">单选框</li>
</ul> </ul>
</div> </div>
</div> </div>
<div class="menu-item__checkbox" title="复选框"> <div class="menu-item__checkbox" title="复选框">
<i></i> <i></i>
</div> </div>
<div class="menu-item__radio" title="单选框">
<i></i>
</div>
<div class="menu-item__latex" title="LateX"> <div class="menu-item__latex" title="LateX">
<i></i> <i></i>
</div> </div>

@ -0,0 +1,4 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="#3d4757" stroke-width="2" fill="none" />
<circle cx="12" cy="12" r="4" fill="#3d4757" />
</svg>

After

Width:  |  Height:  |  Size: 236 B

@ -65,6 +65,7 @@ import {
import { Control } from './control/Control' import { Control } from './control/Control'
import { getSlimCloneElementList, zipElementList } from '../../utils/element' import { getSlimCloneElementList, zipElementList } from '../../utils/element'
import { CheckboxParticle } from './particle/CheckboxParticle' import { CheckboxParticle } from './particle/CheckboxParticle'
import { RadioParticle } from './particle/RadioParticle'
import { DeepRequired, IPadding } from '../../interface/Common' import { DeepRequired, IPadding } from '../../interface/Common'
import { import {
ControlComponent, ControlComponent,
@ -142,6 +143,7 @@ export class Draw {
private superscriptParticle: SuperscriptParticle private superscriptParticle: SuperscriptParticle
private subscriptParticle: SubscriptParticle private subscriptParticle: SubscriptParticle
private checkboxParticle: CheckboxParticle private checkboxParticle: CheckboxParticle
private radioParticle: RadioParticle
private blockParticle: BlockParticle private blockParticle: BlockParticle
private listParticle: ListParticle private listParticle: ListParticle
private lineBreakParticle: LineBreakParticle private lineBreakParticle: LineBreakParticle
@ -216,6 +218,7 @@ export class Draw {
this.superscriptParticle = new SuperscriptParticle() this.superscriptParticle = new SuperscriptParticle()
this.subscriptParticle = new SubscriptParticle() this.subscriptParticle = new SubscriptParticle()
this.checkboxParticle = new CheckboxParticle(this) this.checkboxParticle = new CheckboxParticle(this)
this.radioParticle = new RadioParticle(this)
this.blockParticle = new BlockParticle(this) this.blockParticle = new BlockParticle(this)
this.listParticle = new ListParticle(this) this.listParticle = new ListParticle(this)
this.lineBreakParticle = new LineBreakParticle(this) this.lineBreakParticle = new LineBreakParticle(this)
@ -751,6 +754,10 @@ export class Draw {
return this.checkboxParticle return this.checkboxParticle
} }
public getRadioParticle(): RadioParticle {
return this.radioParticle
}
public getControl(): Control { public getControl(): Control {
return this.control return this.control
} }
@ -1414,6 +1421,15 @@ export class Draw {
element.width = availableWidth / scale element.width = availableWidth / scale
metrics.width = availableWidth metrics.width = availableWidth
metrics.height = defaultSize 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 ( } else if (
element.type === ElementType.CHECKBOX || element.type === ElementType.CHECKBOX ||
element.controlComponent === ControlComponent.CHECKBOX element.controlComponent === ControlComponent.CHECKBOX
@ -1805,6 +1821,12 @@ export class Draw {
) { ) {
this.textParticle.complete() this.textParticle.complete()
this.checkboxParticle.render(ctx, element, x, y + offsetY) 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) { } else if (element.type === ElementType.TAB) {
this.textParticle.complete() this.textParticle.complete()
} else if (element.rowFlex === RowFlex.ALIGNMENT) { } else if (element.rowFlex === RowFlex.ALIGNMENT) {

@ -32,6 +32,7 @@ import { Listener } from '../../listener/Listener'
import { RangeManager } from '../../range/RangeManager' import { RangeManager } from '../../range/RangeManager'
import { Draw } from '../Draw' import { Draw } from '../Draw'
import { CheckboxControl } from './checkbox/CheckboxControl' import { CheckboxControl } from './checkbox/CheckboxControl'
import { RadioControl } from './radio/RadioControl'
import { ControlSearch } from './interactive/ControlSearch' import { ControlSearch } from './interactive/ControlSearch'
import { ControlBorder } from './richtext/Border' import { ControlBorder } from './richtext/Border'
import { SelectControl } from './select/SelectControl' import { SelectControl } from './select/SelectControl'
@ -236,6 +237,8 @@ export class Control {
selectControl.awake() selectControl.awake()
} else if (control.type === ControlType.CHECKBOX) { } else if (control.type === ControlType.CHECKBOX) {
this.activeControl = new CheckboxControl(element, this) this.activeControl = new CheckboxControl(element, this)
} else if (control.type === ControlType.RADIO) {
this.activeControl = new RadioControl(element, this)
} }
// 激活控件回调 // 激活控件回调
nextTick(() => { nextTick(() => {
@ -522,7 +525,8 @@ export class Control {
}) })
} else if ( } else if (
type === ControlType.SELECT || type === ControlType.SELECT ||
type === ControlType.CHECKBOX type === ControlType.CHECKBOX ||
type === ControlType.RADIO
) { ) {
const innerText = code const innerText = code
?.split(',') ?.split(',')
@ -629,6 +633,10 @@ export class Control {
const checkbox = new CheckboxControl(element, this) const checkbox = new CheckboxControl(element, this)
const codes = value?.split(',') || [] const codes = value?.split(',') || []
checkbox.setSelect(codes, controlContext, controlRule) 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 let newEndIndex = i

@ -9,8 +9,8 @@ import { IElement } from '../../../../interface/Element'
import { Control } from '../Control' import { Control } from '../Control'
export class CheckboxControl implements IControlInstance { export class CheckboxControl implements IControlInstance {
private element: IElement protected element: IElement
private control: Control protected control: Control
constructor(element: IElement, control: Control) { constructor(element: IElement, control: Control) {
this.element = element this.element = element

@ -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()
}
}

@ -35,7 +35,7 @@ export class CheckboxParticle {
y: number y: number
) { ) {
const { const {
checkbox: { gap, lineWidth, fillStyle, fontStyle }, checkbox: { gap, lineWidth, fillStyle, strokeStyle },
scale scale
} = this.options } = this.options
const { metrics, checkbox } = element const { metrics, checkbox } = element
@ -60,7 +60,7 @@ export class CheckboxParticle {
ctx.fillRect(left, top, width, height) ctx.fillRect(left, top, width, height)
// 勾选对号 // 勾选对号
ctx.beginPath() ctx.beginPath()
ctx.strokeStyle = fontStyle ctx.strokeStyle = strokeStyle
ctx.lineWidth = lineWidth * 2 * scale ctx.lineWidth = lineWidth * 2 * scale
ctx.moveTo(left + 2 * scale, top + height / 2) ctx.moveTo(left + 2 * scale, top + height / 2)
ctx.lineTo(left + width / 2, top + height - 3 * scale) ctx.lineTo(left + width / 2, top + height - 3 * scale)

@ -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<IEditorOption>
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()
}
}

@ -124,8 +124,13 @@ function dblclick(host: CanvasEvent, evt: MouseEvent) {
return return
} }
} }
// 复选框双击时是切换选择状态,禁用扩选 // 复选/单选框双击时是切换选择状态,禁用扩选
if (positionContext.isCheckbox && positionContext.isDirectHit) return if (
(positionContext.isCheckbox || positionContext.isRadio) &&
positionContext.isDirectHit
) {
return
}
// 自动扩选文字-分词处理,优先使用分词器否则降级使用光标所在位置 // 自动扩选文字-分词处理,优先使用分词器否则降级使用光标所在位置
const rangeManager = draw.getRange() const rangeManager = draw.getRange()
const segmenterRange = const segmenterRange =

@ -4,6 +4,7 @@ import { MouseEventButton } from '../../../dataset/enum/Event'
import { deepClone } from '../../../utils' import { deepClone } from '../../../utils'
import { isMod } from '../../../utils/hotkey' import { isMod } from '../../../utils/hotkey'
import { CheckboxControl } from '../../draw/control/checkbox/CheckboxControl' import { CheckboxControl } from '../../draw/control/checkbox/CheckboxControl'
import { RadioControl } from '../../draw/control/radio/RadioControl'
import { CanvasEvent } from '../CanvasEvent' import { CanvasEvent } from '../CanvasEvent'
export function setRangeCache(host: CanvasEvent) { export function setRangeCache(host: CanvasEvent) {
@ -54,6 +55,7 @@ export function mousedown(evt: MouseEvent, host: CanvasEvent) {
index, index,
isDirectHit, isDirectHit,
isCheckbox, isCheckbox,
isRadio,
isImage, isImage,
isTable, isTable,
tdValueIndex, tdValueIndex,
@ -73,11 +75,14 @@ export function mousedown(evt: MouseEvent, host: CanvasEvent) {
// 绘制 // 绘制
const isDirectHitImage = !!(isDirectHit && isImage) const isDirectHitImage = !!(isDirectHit && isImage)
const isDirectHitCheckbox = !!(isDirectHit && isCheckbox) const isDirectHitCheckbox = !!(isDirectHit && isCheckbox)
const isDirectHitRadio = !!(isDirectHit && isRadio)
if (~index) { if (~index) {
rangeManager.setRange(curIndex, curIndex) rangeManager.setRange(curIndex, curIndex)
position.setCursorPosition(positionList[curIndex]) position.setCursorPosition(positionList[curIndex])
// 复选框 // 复选框
const isSetCheckbox = isDirectHitCheckbox && !isReadonly const isSetCheckbox = isDirectHitCheckbox && !isReadonly
// 单选框
const isSetRadio = isDirectHitRadio && !isReadonly
if (isSetCheckbox) { if (isSetCheckbox) {
const { checkbox, control } = curElement const { checkbox, control } = curElement
// 复选框不在控件内独立控制 // 复选框不在控件内独立控制
@ -98,12 +103,25 @@ export function mousedown(evt: MouseEvent, host: CanvasEvent) {
activeControl.setSelect(codes) 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 { } else {
draw.render({ draw.render({
curIndex, curIndex,
isCompute: false, isCompute: false,
isSubmitHistory: false, isSubmitHistory: false,
isSetCursor: !isDirectHitImage && !isDirectHitCheckbox isSetCursor:
!isDirectHitImage && !isDirectHitCheckbox && !isDirectHitRadio
}) })
} }
// 首字需定位到行首,非上一行最后一个字后 // 首字需定位到行首,非上一行最后一个字后

@ -394,6 +394,9 @@ export class Position {
tdValueElement.type === ElementType.CHECKBOX || tdValueElement.type === ElementType.CHECKBOX ||
tdValueElement.controlComponent === tdValueElement.controlComponent ===
ControlComponent.CHECKBOX, ControlComponent.CHECKBOX,
isRadio:
tdValueElement.type === ElementType.RADIO ||
tdValueElement.controlComponent === ControlComponent.RADIO,
isControl: !!tdValueElement.controlId, isControl: !!tdValueElement.controlId,
isImage: tablePosition.isImage, isImage: tablePosition.isImage,
isDirectHit: tablePosition.isDirectHit, isDirectHit: tablePosition.isDirectHit,
@ -431,6 +434,16 @@ export class Position {
isCheckbox: true isCheckbox: true
} }
} }
if (
element.type === ElementType.RADIO ||
element.controlComponent === ControlComponent.RADIO
) {
return {
index: curPositionIndex,
isDirectHit: true,
isRadio: true
}
}
let hitLineStartIndex: number | undefined let hitLineStartIndex: number | undefined
// 判断是否在文字中间前后 // 判断是否在文字中间前后
if (elementList[index].value !== ZERO) { if (elementList[index].value !== ZERO) {
@ -651,6 +664,7 @@ export class Position {
const { const {
index, index,
isCheckbox, isCheckbox,
isRadio,
isControl, isControl,
isTable, isTable,
trIndex, trIndex,
@ -663,6 +677,7 @@ export class Position {
this.setPositionContext({ this.setPositionContext({
isTable: isTable || false, isTable: isTable || false,
isCheckbox: isCheckbox || false, isCheckbox: isCheckbox || false,
isRadio: isRadio || false,
isControl: isControl || false, isControl: isControl || false,
index, index,
trIndex, trIndex,

@ -12,6 +12,7 @@ enum ElementType {
PAGE_BREAK = 'pageBreak', PAGE_BREAK = 'pageBreak',
CONTROL = 'control', CONTROL = 'control',
CHECKBOX = 'checkbox', CHECKBOX = 'checkbox',
RADIO = 'radio',
LATEX = 'latex', LATEX = 'latex',
TAB = 'tab', TAB = 'tab',
DATE = 'date', DATE = 'date',

@ -6,5 +6,5 @@ export const defaultCheckboxOption: Readonly<Required<ICheckboxOption>> = {
gap: 5, gap: 5,
lineWidth: 1, lineWidth: 1,
fillStyle: '#5175f4', fillStyle: '#5175f4',
fontStyle: '#ffffff' strokeStyle: '#ffffff'
} }

@ -60,6 +60,7 @@ export const EDITOR_ELEMENT_ZIP_ATTR: Array<keyof IElement> = [
'valueList', 'valueList',
'control', 'control',
'checkbox', 'checkbox',
'radio',
'dateFormat', 'dateFormat',
'block', 'block',
'level', 'level',

@ -0,0 +1,10 @@
import { IRadioOption } from '../../interface/Radio'
export const defaultRadioOption: Readonly<Required<IRadioOption>> = {
width: 14,
height: 14,
gap: 5,
lineWidth: 1,
fillStyle: '#5175f4',
strokeStyle: '#000000'
}

@ -1,7 +1,8 @@
export enum ControlType { export enum ControlType {
TEXT = 'text', TEXT = 'text',
SELECT = 'select', SELECT = 'select',
CHECKBOX = 'checkbox' CHECKBOX = 'checkbox',
RADIO = 'radio'
} }
export enum ControlComponent { export enum ControlComponent {
@ -9,7 +10,8 @@ export enum ControlComponent {
POSTFIX = 'postfix', POSTFIX = 'postfix',
PLACEHOLDER = 'placeholder', PLACEHOLDER = 'placeholder',
VALUE = 'value', VALUE = 'value',
CHECKBOX = 'checkbox' CHECKBOX = 'checkbox',
RADIO = 'radio'
} }
// 控件内容缩进方式 // 控件内容缩进方式

@ -9,6 +9,7 @@ export enum ElementType {
PAGE_BREAK = 'pageBreak', PAGE_BREAK = 'pageBreak',
CONTROL = 'control', CONTROL = 'control',
CHECKBOX = 'checkbox', CHECKBOX = 'checkbox',
RADIO = 'radio',
LATEX = 'latex', LATEX = 'latex',
TAB = 'tab', TAB = 'tab',
DATE = 'date', DATE = 'date',

@ -32,7 +32,9 @@ import { ControlIndentation, ControlType } from './dataset/enum/Control'
import { defaultControlOption } from './dataset/constant/Control' import { defaultControlOption } from './dataset/constant/Control'
import { IControlOption } from './interface/Control' import { IControlOption } from './interface/Control'
import { ICheckboxOption } from './interface/Checkbox' import { ICheckboxOption } from './interface/Checkbox'
import { IRadioOption } from './interface/Radio'
import { defaultCheckboxOption } from './dataset/constant/Checkbox' import { defaultCheckboxOption } from './dataset/constant/Checkbox'
import { defaultRadioOption } from './dataset/constant/Radio'
import { DeepRequired } from './interface/Common' import { DeepRequired } from './interface/Common'
import { INavigateInfo } from './core/draw/interactive/Search' import { INavigateInfo } from './core/draw/interactive/Search'
import { Shortcut } from './core/shortcut/Shortcut' import { Shortcut } from './core/shortcut/Shortcut'
@ -117,6 +119,10 @@ export default class Editor {
...defaultCheckboxOption, ...defaultCheckboxOption,
...options.checkbox ...options.checkbox
} }
const radioOptions: Required<IRadioOption> = {
...defaultRadioOption,
...options.radio
}
const cursorOptions: Required<ICursorOption> = { const cursorOptions: Required<ICursorOption> = {
...defaultCursorOption, ...defaultCursorOption,
...options.cursor ...options.cursor
@ -200,6 +206,7 @@ export default class Editor {
watermark: waterMarkOptions, watermark: waterMarkOptions,
control: controlOptions, control: controlOptions,
checkbox: checkboxOptions, checkbox: checkboxOptions,
radio: radioOptions,
cursor: cursorOptions, cursor: cursorOptions,
title: titleOptions, title: titleOptions,
placeholder: placeholderOptions, placeholder: placeholderOptions,

@ -10,5 +10,5 @@ export interface ICheckboxOption {
gap?: number gap?: number
lineWidth?: number lineWidth?: number
fillStyle?: string fillStyle?: string
fontStyle?: string strokeStyle?: string
} }

@ -2,6 +2,7 @@ import { ControlType, ControlIndentation } from '../dataset/enum/Control'
import { EditorZone } from '../dataset/enum/Editor' import { EditorZone } from '../dataset/enum/Editor'
import { ICheckbox } from './Checkbox' import { ICheckbox } from './Checkbox'
import { IElement } from './Element' import { IElement } from './Element'
import { IRadio } from './Radio'
import { IRange } from './Range' import { IRange } from './Range'
export interface IValueSet { export interface IValueSet {
@ -22,6 +23,12 @@ export interface IControlCheckbox {
checkbox?: ICheckbox checkbox?: ICheckbox
} }
export interface IControlRadio {
code: string | null
valueSets: IValueSet[]
radio?: IRadio
}
export interface IControlHighlightRule { export interface IControlHighlightRule {
keyword: string keyword: string
alpha?: number alpha?: number
@ -65,6 +72,7 @@ export type IControl = IControlBasic &
IControlRule & IControlRule &
Partial<IControlSelect> & Partial<IControlSelect> &
Partial<IControlCheckbox> & Partial<IControlCheckbox> &
Partial<IControlRadio> &
Partial<IControlStyle> Partial<IControlStyle>
export interface IControlOption { export interface IControlOption {

@ -7,6 +7,7 @@ import {
} from '../dataset/enum/Editor' } from '../dataset/enum/Editor'
import { IBackgroundOption } from './Background' import { IBackgroundOption } from './Background'
import { ICheckboxOption } from './Checkbox' import { ICheckboxOption } from './Checkbox'
import { IRadioOption } from './Radio'
import { IPadding } from './Common' import { IPadding } from './Common'
import { IControlOption } from './Control' import { IControlOption } from './Control'
import { ICursorOption } from './Cursor' import { ICursorOption } from './Cursor'
@ -77,6 +78,7 @@ export interface IEditorOption {
watermark?: IWatermark watermark?: IWatermark
control?: IControlOption control?: IControlOption
checkbox?: ICheckboxOption checkbox?: ICheckboxOption
radio?: IRadioOption
cursor?: ICursorOption cursor?: ICursorOption
title?: ITitleOption title?: ITitleOption
placeholder?: IPlaceholder placeholder?: IPlaceholder

@ -8,6 +8,7 @@ import { TableBorder } from '../dataset/enum/table/Table'
import { IBlock } from './Block' import { IBlock } from './Block'
import { ICheckbox } from './Checkbox' import { ICheckbox } from './Checkbox'
import { IControl } from './Control' import { IControl } from './Control'
import { IRadio } from './Radio'
import { ITextDecoration } from './Text' import { ITextDecoration } from './Text'
import { IColgroup } from './table/Colgroup' import { IColgroup } from './table/Colgroup'
import { ITr } from './table/Tr' import { ITr } from './table/Tr'
@ -94,6 +95,10 @@ export interface ICheckboxElement {
checkbox?: ICheckbox checkbox?: ICheckbox
} }
export interface IRadioElement {
radio?: IRadio
}
export interface ILaTexElement { export interface ILaTexElement {
laTexSVG?: string laTexSVG?: string
} }
@ -124,6 +129,7 @@ export type IElement = IElementBasic &
ISeparator & ISeparator &
IControlElement & IControlElement &
ICheckboxElement & ICheckboxElement &
IRadioElement &
ILaTexElement & ILaTexElement &
IDateElement & IDateElement &
IImageElement & IImageElement &

@ -9,6 +9,7 @@ export interface ICurrentPosition {
x?: number x?: number
y?: number y?: number
isCheckbox?: boolean isCheckbox?: boolean
isRadio?: boolean
isControl?: boolean isControl?: boolean
isImage?: boolean isImage?: boolean
isTable?: boolean isTable?: boolean
@ -41,6 +42,7 @@ export type IGetFloatPositionByXYPayload = IGetPositionByXYPayload & {
export interface IPositionContext { export interface IPositionContext {
isTable: boolean isTable: boolean
isCheckbox?: boolean isCheckbox?: boolean
isRadio?: boolean
isControl?: boolean isControl?: boolean
index?: number index?: number
trIndex?: number trIndex?: number

@ -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
}

@ -222,7 +222,11 @@ export function formatElementList(
const { prefix, postfix, value, placeholder, code, type, valueSets } = const { prefix, postfix, value, placeholder, code, type, valueSets } =
el.control el.control
const { const {
editorOptions: { control: controlOption, checkbox: checkboxOption } editorOptions: {
control: controlOption,
checkbox: checkboxOption,
radio: radioOption
}
} = options } = options
const controlId = getUUID() const controlId = getUUID()
// 移除父节点 // 移除父节点
@ -258,6 +262,7 @@ export function formatElementList(
if ( if (
(value && value.length) || (value && value.length) ||
type === ControlType.CHECKBOX || type === ControlType.CHECKBOX ||
type === ControlType.RADIO ||
(type === ControlType.SELECT && code && (!value || !value.length)) (type === ControlType.SELECT && code && (!value || !value.length))
) { ) {
let valueList: IElement[] = value || [] 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 { } else {
if (!value || !value.length) { if (!value || !value.length) {
if (Array.isArray(valueSets) && valueSets.length) { if (Array.isArray(valueSets) && valueSets.length) {
@ -955,6 +1007,13 @@ export function createDomFromElementList(
checkbox.setAttribute('checked', 'true') checkbox.setAttribute('checked', 'true')
} }
clipboardDom.append(checkbox) 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) { } else if (element.type === ElementType.TAB) {
const tab = document.createElement('span') const tab = document.createElement('span')
tab.innerHTML = `${NON_BREAKING_SPACE}${NON_BREAKING_SPACE}` tab.innerHTML = `${NON_BREAKING_SPACE}${NON_BREAKING_SPACE}`
@ -1200,6 +1259,17 @@ export function getElementListByHTML(
value: (<HTMLInputElement>node).checked value: (<HTMLInputElement>node).checked
} }
}) })
} else if (
node.nodeName === 'INPUT' &&
(<HTMLInputElement>node).type === ControlComponent.RADIO
) {
elementList.push({
type: ElementType.RADIO,
value: '',
radio: {
value: (<HTMLInputElement>node).checked
}
})
} else { } else {
findTextNode(node) findTextNode(node)
if (node.nodeType === 1 && n !== childNodes.length - 1) { if (node.nodeType === 1 && n !== childNodes.length - 1) {
@ -1275,6 +1345,8 @@ export function getTextFromElementList(elementList: IElement[]) {
}) })
} else if (element.type === ElementType.CHECKBOX) { } else if (element.type === ElementType.CHECKBOX) {
text += element.checkbox?.value ? `` : `` text += element.checkbox?.value ? `` : ``
} else if (element.type === ElementType.RADIO) {
text += element.radio?.value ? `` : ``
} else if ( } else if (
!element.type || !element.type ||
element.type === ElementType.LATEX || element.type === ElementType.LATEX ||

@ -768,6 +768,44 @@ window.onload = function () {
} }
}) })
break 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: default:
break break
} }
@ -789,6 +827,20 @@ window.onload = function () {
]) ])
} }
const radioDom = document.querySelector<HTMLDivElement>('.menu-item__radio')!
radioDom.onclick = function () {
console.log('radio')
instance.command.executeInsertElementList([
{
type: ElementType.RADIO,
checkbox: {
value: false
},
value: ''
}
])
}
const latexDom = document.querySelector<HTMLDivElement>('.menu-item__latex')! const latexDom = document.querySelector<HTMLDivElement>('.menu-item__latex')!
latexDom.onclick = function () { latexDom.onclick = function () {
console.log('LaTeX') console.log('LaTeX')

@ -540,6 +540,10 @@ ul {
background-image: url('./assets/images/checkbox.svg'); background-image: url('./assets/images/checkbox.svg');
} }
.menu-item__radio i {
background-image: url('./assets/images/radio.svg');
}
.menu-item__latex i { .menu-item__latex i {
background-image: url('./assets/images/latex.svg'); background-image: url('./assets/images/latex.svg');
} }

Loading…
Cancel
Save