diff --git a/docs/en/guide/schema.md b/docs/en/guide/schema.md index dcfd5168d..392a8fcaf 100644 --- a/docs/en/guide/schema.md +++ b/docs/en/guide/schema.md @@ -81,7 +81,8 @@ interface IElement { TEXT = 'text', SELECT = 'select', CHECKBOX = 'checkbox', - RADIO = 'radio' + RADIO = 'radio', + DATE = 'date' }; value: IElement[] | null; placeholder?: string; @@ -102,18 +103,7 @@ interface IElement { value: string; code: string; }[]; - checkbox?: { - value: boolean | null; - code?: string; - min?: number; - max?: number; - disabled?: boolean; - }; - radio?: { - value: boolean | null; - code?: string; - disabled?: boolean; - }; + dateFormat?: string; font?: string; size?: number; bold?: boolean; @@ -133,14 +123,10 @@ interface IElement { // checkbox checkbox?: { value: boolean | null; - code?: string; - disabled?: boolean; }; // radio radio?: { value: boolean | null; - code?: string; - disabled?: boolean; }; // LaTeX laTexSVG?: string; diff --git a/docs/guide/schema.md b/docs/guide/schema.md index eabea54e4..9299252d0 100644 --- a/docs/guide/schema.md +++ b/docs/guide/schema.md @@ -82,6 +82,7 @@ interface IElement { SELECT = 'select', CHECKBOX = 'checkbox', RADIO = 'radio' + DATE = 'date' }; value: IElement[] | null; placeholder?: string; @@ -102,18 +103,7 @@ interface IElement { value: string; code: string; }[]; - checkbox?: { - value: boolean | null; - code?: string; - min?: number; - max?: number; - disabled?: boolean; - }; - radio?: { - value: boolean | null; - code?: string; - disabled?: boolean; - }; + dateFormat?: string; font?: string; size?: number; bold?: boolean; @@ -133,14 +123,10 @@ interface IElement { // 复选框 checkbox?: { value: boolean | null; - code?: string; - disabled?: boolean; }; // 单选框 radio?: { value: boolean | null; - code?: string; - disabled?: boolean; }; // LaTeX laTexSVG?: string; diff --git a/src/editor/core/draw/control/Control.ts b/src/editor/core/draw/control/Control.ts index 3d9c881c2..ae9b79f68 100644 --- a/src/editor/core/draw/control/Control.ts +++ b/src/editor/core/draw/control/Control.ts @@ -40,6 +40,7 @@ import { ControlSearch } from './interactive/ControlSearch' import { ControlBorder } from './richtext/Border' import { SelectControl } from './select/SelectControl' import { TextControl } from './text/TextControl' +import { DateControl } from './date/DateControl' import { MoveDirection } from '../../../dataset/enum/Observer' interface IMoveCursorResult { @@ -223,9 +224,16 @@ export class Control { const element = elementList[range.startIndex] // 判断控件是否已经激活 if (this.activeControl) { - // 列举控件唤醒下拉弹窗 - if (this.activeControl instanceof SelectControl) { - this.activeControl.awake() + // 弹窗类控件唤醒弹窗,后缀处移除弹窗 + if ( + this.activeControl instanceof SelectControl || + this.activeControl instanceof DateControl + ) { + if (element.controlComponent === ControlComponent.POSTFIX) { + this.activeControl.destroy() + } else { + this.activeControl.awake() + } } const controlElement = this.activeControl.getElement() if (element.controlId === controlElement.controlId) return @@ -244,6 +252,10 @@ export class Control { this.activeControl = new CheckboxControl(element, this) } else if (control.type === ControlType.RADIO) { this.activeControl = new RadioControl(element, this) + } else if (control.type === ControlType.DATE) { + const dateControl = new DateControl(element, this) + this.activeControl = dateControl + dateControl.awake() } // 激活控件回调 nextTick(() => { @@ -269,7 +281,10 @@ export class Control { public destroyControl() { if (this.activeControl) { - if (this.activeControl instanceof SelectControl) { + if ( + this.activeControl instanceof SelectControl || + this.activeControl instanceof DateControl + ) { this.activeControl.destroy() } this.activeControl = null @@ -316,7 +331,8 @@ export class Control { const element = elementList[range.startIndex] this.activeControl.setElement(element) if ( - this.activeControl instanceof SelectControl && + (this.activeControl instanceof DateControl || + this.activeControl instanceof SelectControl) && this.activeControl.getIsPopup() ) { this.activeControl.destroy() @@ -542,14 +558,14 @@ export class Control { const nextElement = elementList[j] if (nextElement.controlId !== element.controlId) break if ( - type === ControlType.TEXT && + (type === ControlType.TEXT || type === ControlType.DATE) && nextElement.controlComponent === ControlComponent.VALUE ) { textControlValue += nextElement.value } j++ } - if (type === ControlType.TEXT) { + if (type === ControlType.TEXT || type === ControlType.DATE) { result.push({ ...element.control, zone, @@ -674,6 +690,14 @@ export class Control { this.activeControl = radio const codes = value ? [value] : [] radio.setSelect(codes, controlContext, controlRule) + } else if (type === ControlType.DATE) { + const date = new DateControl(element, this) + this.activeControl = date + if (value) { + date.setSelect(value, controlContext, controlRule) + } else { + date.clearSelect(controlContext, controlRule) + } } // 模拟控件激活后销毁 this.activeControl = null diff --git a/src/editor/core/draw/control/date/DateControl.ts b/src/editor/core/draw/control/date/DateControl.ts new file mode 100644 index 000000000..8323724e6 --- /dev/null +++ b/src/editor/core/draw/control/date/DateControl.ts @@ -0,0 +1,361 @@ +import { + CONTROL_STYLE_ATTR, + EDITOR_ELEMENT_STYLE_ATTR, + TEXTLIKE_ELEMENT_TYPE +} from '../../../../dataset/constant/Element' +import { ControlComponent } from '../../../../dataset/enum/Control' +import { ElementType } from '../../../../dataset/enum/Element' +import { KeyMap } from '../../../../dataset/enum/KeyMap' +import { + IControlContext, + IControlInstance, + IControlRuleOption +} from '../../../../interface/Control' +import { IElement } from '../../../../interface/Element' +import { omitObject, pickObject } from '../../../../utils' +import { formatElementContext } from '../../../../utils/element' +import { Draw } from '../../Draw' +import { DatePicker } from '../../particle/date/DatePicker' +import { Control } from '../Control' + +export class DateControl implements IControlInstance { + private draw: Draw + private element: IElement + private control: Control + private isPopup: boolean + private datePicker: DatePicker | null + + constructor(element: IElement, control: Control) { + const draw = control.getDraw() + this.draw = draw + this.element = element + this.control = control + this.isPopup = false + this.datePicker = null + } + + public setElement(element: IElement) { + this.element = element + } + + public getElement(): IElement { + return this.element + } + + public getIsPopup(): boolean { + return this.isPopup + } + + public getValueRange(context: IControlContext = {}): [number, number] | null { + 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 + } + preIndex-- + } + // 向右查找 + let nextIndex = startIndex + 1 + while (nextIndex < elementList.length) { + const nextElement = elementList[nextIndex] + if ( + nextElement.controlId !== startElement.controlId || + nextElement.controlComponent === ControlComponent.POSTFIX + ) { + break + } + nextIndex++ + } + if (preIndex === nextIndex) return null + return [preIndex, nextIndex - 1] + } + + public getValue(context: IControlContext = {}): IElement[] { + const elementList = context.elementList || this.control.getElementList() + const range = this.getValueRange(context) + if (!range) return [] + const data: IElement[] = [] + const [startIndex, endIndex] = range + for (let i = startIndex; i <= endIndex; i++) { + const element = elementList[i] + if (element.controlComponent === ControlComponent.VALUE) { + data.push(element) + } + } + return data + } + + public setValue( + data: IElement[], + context: IControlContext = {}, + options: IControlRuleOption = {} + ): number { + // 校验是否可以设置 + if (!options.isIgnoreDisabledRule && this.control.getIsDisabledControl()) { + return -1 + } + const elementList = context.elementList || this.control.getElementList() + const range = context.range || this.control.getRange() + // 收缩边界到Value内 + this.control.shrinkBoundary(context) + const { startIndex, endIndex } = range + const draw = this.control.getDraw() + // 移除选区元素 + if (startIndex !== endIndex) { + draw.spliceElementList(elementList, startIndex + 1, endIndex - startIndex) + } else { + // 移除空白占位符 + this.control.removePlaceholder(startIndex, context) + } + // 非文本类元素或前缀过渡掉样式属性 + const startElement = elementList[startIndex] + const anchorElement = + (startElement.type && + !TEXTLIKE_ELEMENT_TYPE.includes(startElement.type)) || + startElement.controlComponent === ControlComponent.PREFIX + ? pickObject(startElement, [ + 'control', + 'controlId', + ...CONTROL_STYLE_ATTR + ]) + : omitObject(startElement, ['type']) + // 插入起始位置 + const start = range.startIndex + 1 + for (let i = 0; i < data.length; i++) { + const newElement: IElement = { + ...anchorElement, + ...data[i], + controlComponent: ControlComponent.VALUE + } + formatElementContext(elementList, [newElement], startIndex) + draw.spliceElementList(elementList, start + i, 0, newElement) + } + return start + data.length - 1 + } + + public clearSelect( + context: IControlContext = {}, + options: IControlRuleOption = {} + ): number { + const { isIgnoreDisabledRule = false, isAddPlaceholder = true } = options + // 校验是否可以设置 + if (!isIgnoreDisabledRule && this.control.getIsDisabledControl()) { + return -1 + } + const range = this.getValueRange(context) + if (!range) return -1 + const [leftIndex, rightIndex] = range + if (!~leftIndex || !~rightIndex) return -1 + const elementList = context.elementList || this.control.getElementList() + // 删除元素 + const draw = this.control.getDraw() + draw.spliceElementList(elementList, leftIndex + 1, rightIndex - leftIndex) + // 增加占位符 + if (isAddPlaceholder) { + this.control.addPlaceholder(leftIndex, context) + } + return leftIndex + } + + public setSelect( + date: string, + context: IControlContext = {}, + options: IControlRuleOption = {} + ) { + // 校验是否可以设置 + if (!options.isIgnoreDisabledRule && this.control.getIsDisabledControl()) { + return + } + const elementList = context.elementList || this.control.getElementList() + const range = context.range || this.control.getRange() + // 样式赋值元素-默认值的第一个字符样式,否则取默认样式 + const valueElement = this.getValue(context)[0] + const styleElement = valueElement + ? pickObject(valueElement, EDITOR_ELEMENT_STYLE_ATTR) + : pickObject(elementList[range.startIndex], CONTROL_STYLE_ATTR) + // 清空选项 + const prefixIndex = this.clearSelect(context, { + isAddPlaceholder: false + }) + if (!~prefixIndex) return + // 属性赋值元素-默认为前缀属性 + const propertyElement = omitObject( + elementList[prefixIndex], + EDITOR_ELEMENT_STYLE_ATTR + ) + const start = prefixIndex + 1 + const draw = this.control.getDraw() + for (let i = 0; i < date.length; i++) { + const newElement: IElement = { + ...styleElement, + ...propertyElement, + type: ElementType.TEXT, + value: date[i], + controlComponent: ControlComponent.VALUE + } + formatElementContext(elementList, [newElement], prefixIndex) + draw.spliceElementList(elementList, start + i, 0, newElement) + } + // 重新渲染控件 + if (!context.range) { + const newIndex = start + date.length - 1 + this.control.repaintControl({ + curIndex: newIndex + }) + this.destroy() + } + } + + public keydown(evt: KeyboardEvent): number | null { + if (this.control.getIsDisabledControl()) { + return null + } + 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] + const draw = this.control.getDraw() + // backspace + if (evt.key === KeyMap.Backspace) { + // 移除选区元素 + if (startIndex !== endIndex) { + draw.spliceElementList( + elementList, + startIndex + 1, + endIndex - startIndex + ) + const value = this.getValue() + if (!value.length) { + this.control.addPlaceholder(startIndex) + } + return startIndex + } else { + if ( + startElement.controlComponent === ControlComponent.PREFIX || + endElement.controlComponent === ControlComponent.POSTFIX || + startElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + // 前缀、后缀、占位符 + return this.control.removeControl(startIndex) + } else { + // 文本 + draw.spliceElementList(elementList, startIndex, 1) + const value = this.getValue() + if (!value.length) { + this.control.addPlaceholder(startIndex - 1) + } + return startIndex - 1 + } + } + } else if (evt.key === KeyMap.Delete) { + // 移除选区元素 + if (startIndex !== endIndex) { + draw.spliceElementList( + elementList, + startIndex + 1, + endIndex - startIndex + ) + const value = this.getValue() + if (!value.length) { + this.control.addPlaceholder(startIndex) + } + return startIndex + } else { + const endNextElement = elementList[endIndex + 1] + if ( + (startElement.controlComponent === ControlComponent.PREFIX && + endNextElement.controlComponent === ControlComponent.PLACEHOLDER) || + endNextElement.controlComponent === ControlComponent.POSTFIX || + startElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + // 前缀、后缀、占位符 + return this.control.removeControl(startIndex) + } else { + // 文本 + draw.spliceElementList(elementList, startIndex + 1, 1) + const value = this.getValue() + if (!value.length) { + this.control.addPlaceholder(startIndex) + } + return startIndex + } + } + } + return endIndex + } + + public cut(): number { + if (this.control.getIsDisabledControl()) { + return -1 + } + this.control.shrinkBoundary() + const { startIndex, endIndex } = this.control.getRange() + if (startIndex === endIndex) { + return startIndex + } + const draw = this.control.getDraw() + const elementList = this.control.getElementList() + draw.spliceElementList(elementList, startIndex + 1, endIndex - startIndex) + const value = this.getValue() + if (!value.length) { + this.control.addPlaceholder(startIndex) + } + return startIndex + } + + public awake() { + if (this.isPopup || this.control.getIsDisabledControl()) return + const position = this.control.getPosition() + if (!position) return + const elementList = this.draw.getElementList() + const { startIndex } = this.control.getRange() + if (elementList[startIndex + 1]?.controlId !== this.element.controlId) { + return + } + // 渲染日期控件 + this.datePicker = new DatePicker(this.draw, { + onSubmit: this._setDate.bind(this) + }) + const range = this.getValueRange() + const value = range + ? elementList + .slice(range[0] + 1, range[1] + 1) + .map(el => el.value) + .join('') + : '' + const dateFormat = this.element.control?.dateFormat + this.datePicker.render({ + value, + position, + dateFormat + }) + // 弹窗状态 + this.isPopup = true + } + + public destroy() { + if (!this.isPopup) return + this.datePicker?.destroy() + this.isPopup = false + } + + private _setDate(date: string) { + if (!date) { + this.clearSelect() + } else { + this.setSelect(date) + } + this.destroy() + } +} diff --git a/src/editor/core/draw/particle/date/DateParticle.ts b/src/editor/core/draw/particle/date/DateParticle.ts index 413e80092..b6a9d3b30 100644 --- a/src/editor/core/draw/particle/date/DateParticle.ts +++ b/src/editor/core/draw/particle/date/DateParticle.ts @@ -13,31 +13,8 @@ export class DateParticle { constructor(draw: Draw) { this.draw = draw this.range = draw.getRange() - const i18n = draw.getI18n() - const t = i18n.t.bind(i18n) - this.datePicker = new DatePicker({ - mountDom: draw.getContainer(), - onSubmit: this._setValue.bind(this), - getLang: () => ({ - now: t('datePicker.now'), - confirm: t('datePicker.confirm'), - return: t('datePicker.return'), - timeSelect: t('datePicker.timeSelect'), - weeks: { - sun: t('datePicker.weeks.sun'), - mon: t('datePicker.weeks.mon'), - tue: t('datePicker.weeks.tue'), - wed: t('datePicker.weeks.wed'), - thu: t('datePicker.weeks.thu'), - fri: t('datePicker.weeks.fri'), - sat: t('datePicker.weeks.sat') - }, - year: t('datePicker.year'), - month: t('datePicker.month'), - hour: t('datePicker.hour'), - minute: t('datePicker.minute'), - second: t('datePicker.second') - }) + this.datePicker = new DatePicker(draw, { + onSubmit: this._setValue.bind(this) }) } @@ -111,9 +88,6 @@ export class DateParticle { } public renderDatePicker(element: IElement, position: IElementPosition) { - const height = this.draw.getHeight() - const pageGap = this.draw.getPageGap() - const startTop = this.draw.getPageNo() * (height + pageGap) const elementList = this.draw.getElementList() const range = this.getDateElementRange() const value = range @@ -124,9 +98,8 @@ export class DateParticle { : '' this.datePicker.render({ value, - element, position, - startTop + dateFormat: element.dateFormat }) } } diff --git a/src/editor/core/draw/particle/date/DatePicker.ts b/src/editor/core/draw/particle/date/DatePicker.ts index 775596b87..3c6d87e8d 100644 --- a/src/editor/core/draw/particle/date/DatePicker.ts +++ b/src/editor/core/draw/particle/date/DatePicker.ts @@ -1,6 +1,10 @@ -import { EDITOR_PREFIX } from '../../../../dataset/constant/Editor' -import { IElement, IElementPosition } from '../../../../interface/Element' -import { datePicker } from '../../../i18n/lang/zh-CN.json' +import { + EDITOR_COMPONENT, + EDITOR_PREFIX +} from '../../../../dataset/constant/Editor' +import { EditorComponent } from '../../../../dataset/enum/Editor' +import { IElementPosition } from '../../../../interface/Element' +import { Draw } from '../../Draw' export interface IDatePickerLang { now: string @@ -24,9 +28,7 @@ export interface IDatePickerLang { } export interface IDatePickerOption { - mountDom?: HTMLElement onSubmit?: (date: string) => any - getLang?: () => IDatePickerLang } interface IDatePickerDom { @@ -56,12 +58,12 @@ interface IDatePickerDom { interface IRenderOption { value: string - element: IElement position: IElementPosition - startTop?: number + dateFormat?: string } export class DatePicker { + private draw: Draw private options: IDatePickerOption private now: Date private dom: IDatePickerDom @@ -70,12 +72,10 @@ export class DatePicker { private pickDate: Date | null private lang: IDatePickerLang - constructor(options: IDatePickerOption = {}) { - this.options = { - mountDom: document.body, - ...options - } - this.lang = datePicker + constructor(draw: Draw, options: IDatePickerOption = {}) { + this.draw = draw + this.options = options + this.lang = this._getLang() this.now = new Date() this.dom = this._createDom() this.renderOptions = null @@ -87,6 +87,7 @@ export class DatePicker { private _createDom(): IDatePickerDom { const datePickerContainer = document.createElement('div') datePickerContainer.classList.add(`${EDITOR_PREFIX}-date-container`) + datePickerContainer.setAttribute(EDITOR_COMPONENT, EditorComponent.POPUP) // title-切换年月、年月显示 const dateWrap = document.createElement('div') dateWrap.classList.add(`${EDITOR_PREFIX}-date-wrap`) @@ -181,7 +182,7 @@ export class DatePicker { datePickerContainer.append(dateWrap) datePickerContainer.append(timeWrap) datePickerContainer.append(datePickerMenu) - this.options.mountDom!.append(datePickerContainer) + this.draw.getContainer().append(datePickerContainer) return { container: datePickerContainer, dateWrap, @@ -266,13 +267,17 @@ export class DatePicker { coordinate: { leftTop: [left, top] }, - lineHeight - }, - startTop + lineHeight, + pageNo + } } = this.renderOptions + const height = this.draw.getHeight() + const pageGap = this.draw.getPageGap() + const currentPageNo = pageNo ?? this.draw.getPageNo() + const preY = currentPageNo * (height + pageGap) // 位置 this.dom.container.style.left = `${left}px` - this.dom.container.style.top = `${top + (startTop || 0) + lineHeight}px` + this.dom.container.style.top = `${top + preY + lineHeight}px` } public isInvalidDate(value: Date): boolean { @@ -290,7 +295,32 @@ export class DatePicker { this.pickDate = new Date(this.now) } - private _setLang() { + private _getLang() { + const i18n = this.draw.getI18n() + const t = i18n.t.bind(i18n) + return { + now: t('datePicker.now'), + confirm: t('datePicker.confirm'), + return: t('datePicker.return'), + timeSelect: t('datePicker.timeSelect'), + weeks: { + sun: t('datePicker.weeks.sun'), + mon: t('datePicker.weeks.mon'), + tue: t('datePicker.weeks.tue'), + wed: t('datePicker.weeks.wed'), + thu: t('datePicker.weeks.thu'), + fri: t('datePicker.weeks.fri'), + sat: t('datePicker.weeks.sat') + }, + year: t('datePicker.year'), + month: t('datePicker.month'), + hour: t('datePicker.hour'), + minute: t('datePicker.minute'), + second: t('datePicker.second') + } + } + + private _setLangChange() { this.dom.menu.time.innerText = this.lang.timeSelect this.dom.menu.now.innerText = this.lang.now this.dom.menu.submit.innerText = this.lang.confirm @@ -505,7 +535,7 @@ export class DatePicker { private _submit() { if (this.options.onSubmit && this.pickDate) { - const format = this.renderOptions?.element.dateFormat + const format = this.renderOptions?.dateFormat const pickDateString = this.formatDate(this.pickDate, format) this.options.onSubmit(pickDateString) } @@ -538,10 +568,8 @@ export class DatePicker { public render(option: IRenderOption) { this.renderOptions = option - if (this.options.getLang) { - this.lang = this.options.getLang() - this._setLang() - } + this.lang = this._getLang() + this._setLangChange() this._setValue() this._update() this._setPosition() @@ -553,4 +581,8 @@ export class DatePicker { public dispose() { this._toggleVisible(false) } + + public destroy() { + this.dom.container.remove() + } } diff --git a/src/editor/dataset/enum/Control.ts b/src/editor/dataset/enum/Control.ts index 61a52d845..c88d26369 100644 --- a/src/editor/dataset/enum/Control.ts +++ b/src/editor/dataset/enum/Control.ts @@ -2,7 +2,8 @@ export enum ControlType { TEXT = 'text', SELECT = 'select', CHECKBOX = 'checkbox', - RADIO = 'radio' + RADIO = 'radio', + DATE = 'date' } export enum ControlComponent { diff --git a/src/editor/interface/Control.ts b/src/editor/interface/Control.ts index e1126df72..6a18fd52e 100644 --- a/src/editor/interface/Control.ts +++ b/src/editor/interface/Control.ts @@ -1,11 +1,9 @@ import { ControlType, ControlIndentation } from '../dataset/enum/Control' import { EditorZone } from '../dataset/enum/Editor' import { MoveDirection } from '../dataset/enum/Observer' -import { ICheckbox } from './Checkbox' import { IDrawOption } from './Draw' import { IElement } from './Element' import { IPositionContext } from './Position' -import { IRadio } from './Radio' import { IRange } from './Range' export interface IValueSet { @@ -23,13 +21,15 @@ export interface IControlCheckbox { min?: number max?: number valueSets: IValueSet[] - checkbox?: ICheckbox } export interface IControlRadio { code: string | null valueSets: IValueSet[] - radio?: IRadio +} + +export interface IControlDate { + dateFormat?: string } export interface IControlHighlightRule { @@ -73,10 +73,11 @@ export interface IControlStyle { export type IControl = IControlBasic & IControlRule & + Partial & Partial & Partial & Partial & - Partial + Partial export interface IControlOption { placeholderColor?: string diff --git a/src/mock.ts b/src/mock.ts index c01c6b334..caac844e7 100644 --- a/src/mock.ts +++ b/src/mock.ts @@ -385,13 +385,18 @@ elementList.push( value: '签署日期:' }, { + type: ElementType.CONTROL, value: '', - valueList: [ - { - value: `2022-08-10 17:30:01` - } - ], - type: ElementType.DATE + control: { + conceptId: '5', + type: ControlType.DATE, + value: [ + { + value: `2022-08-10 17:30:01` + } + ], + placeholder: '签署日期' + } }, { value: '\n'