diff --git a/CHANGELOG.md b/CHANGELOG.md index f107550..e465cb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,41 @@ +## [0.9.91](https://github.com/Hufe921/canvas-editor/compare/v0.9.90...v0.9.91) (2024-08-25) + + +### Bug Fixes + +* format different types of line breaks #769 ([f65ff87](https://github.com/Hufe921/canvas-editor/commit/f65ff87e44728322417d235d2347ceca25cc6667)), closes [#769](https://github.com/Hufe921/canvas-editor/issues/769) +* format initial data boundary error #771 #784 ([f62a315](https://github.com/Hufe921/canvas-editor/commit/f62a315a7bd04e49e9e17bc7737ccd1f0e751d69)), closes [#771](https://github.com/Hufe921/canvas-editor/issues/771) [#784](https://github.com/Hufe921/canvas-editor/issues/784) +* set row margin boundary error ([5285170](https://github.com/Hufe921/canvas-editor/commit/528517094373b5fa9ca2145bb259889db865a588)) + + +### Features + +* add page border ([3f6bdf6](https://github.com/Hufe921/canvas-editor/commit/3f6bdf6fcb583ab370cb5abe3eafebf6100f415e)) +* hit checkbox/radio control when click on label #651 ([31b76b6](https://github.com/Hufe921/canvas-editor/commit/31b76b6fb6640ea75383569cb16ad1b5a2ccf25f)), closes [#651](https://github.com/Hufe921/canvas-editor/issues/651) + + + +## [0.9.90](https://github.com/Hufe921/canvas-editor/compare/v0.9.89...v0.9.90) (2024-08-18) + + +### Bug Fixes + +* float image position when scaling the page #766 ([c249b9e](https://github.com/Hufe921/canvas-editor/commit/c249b9eec215fe573b325d1430212f5f01c32099)), closes [#766](https://github.com/Hufe921/canvas-editor/issues/766) +* get range paragraph info boundary error #758 ([4653fe7](https://github.com/Hufe921/canvas-editor/commit/4653fe7427f4a9ec40700f6a86ff284d1d46088a)), closes [#758](https://github.com/Hufe921/canvas-editor/issues/758) +* insert block element row flex error #754 ([136b1ff](https://github.com/Hufe921/canvas-editor/commit/136b1ffa55b7b0b78bf3d114400489e1bbab4f17)), closes [#754](https://github.com/Hufe921/canvas-editor/issues/754) +* paper printing size setting #760 ([7a6dd75](https://github.com/Hufe921/canvas-editor/commit/7a6dd753e59bde4cb581ac215b038dd1cd08c96f)), closes [#760](https://github.com/Hufe921/canvas-editor/issues/760) +* set editor mode option error #755 ([500cec3](https://github.com/Hufe921/canvas-editor/commit/500cec3e0b63e012f6572dcedb071befa114956e)), closes [#755](https://github.com/Hufe921/canvas-editor/issues/755) +* set row flex boundary error when deleting element ([2f272de](https://github.com/Hufe921/canvas-editor/commit/2f272dee58169607783e4cfd1347534f49db0834)) + + +### Features + +* add location property to executeLocationControl api #753 ([d1a1aaa](https://github.com/Hufe921/canvas-editor/commit/d1a1aaa6ae08d8395df1df664d47bfe4fe821869)), closes [#753](https://github.com/Hufe921/canvas-editor/issues/753) +* add radio and checkbox vertical align setting ([c375466](https://github.com/Hufe921/canvas-editor/commit/c3754663cee25003a7021a6079dd46e2a3a0abd8)) +* get context content width ([187498e](https://github.com/Hufe921/canvas-editor/commit/187498ed3dfba4386375d812d41b31c057ac3af8)) + + + ## [0.9.89](https://github.com/Hufe921/canvas-editor/compare/v0.9.88...v0.9.89) (2024-08-09) diff --git a/docs/en/guide/command-execute.md b/docs/en/guide/command-execute.md index 4b0aaa6..0fdf7e4 100644 --- a/docs/en/guide/command-execute.md +++ b/docs/en/guide/command-execute.md @@ -679,7 +679,7 @@ Feature: Change how image rows are displayed Usage: ```javascript -instance.command.executeSaveAsImageElement(element: IElement, display: ImageDisplay) +instance.command.executeChangeImageDisplay(element: IElement, display: ImageDisplay) ``` ## executePageMode @@ -961,3 +961,13 @@ Usage: ```javascript instance.command.executeInsertTitle(payload: IElement) ``` + +## executeFocus + +Feature: focus + +Usage: + +```javascript +instance.command.executeFocus(payload?: IFocusOption) +``` diff --git a/docs/en/guide/option.md b/docs/en/guide/option.md index 06bfe11..5e0d853 100644 --- a/docs/en/guide/option.md +++ b/docs/en/guide/option.md @@ -14,7 +14,7 @@ new Editor(container, IEditorData | IElement[], { ```typescript interface IEditorOption { - mode?: EditorMode // Editor mode: Edit, Clean (Visual aids are not displayed, For example: page break), ReadOnly, Form (Only editable within the control), Print (Visual aids are not displayed, Unwritten content control). default: Edit + mode?: EditorMode // Editor mode: Edit, Clean (Visual aids are not displayed, For example: page break), ReadOnly, Form (Only editable within the control), Print (Visual aids are not displayed, Unwritten content control), Design (Do not handle configurations such as non deletable and read-only). default: Edit defaultType?: string // Default element type. default: TEXT defaultColor?: string // Default color. default: #000000 defaultFont?: string // Default font. default: Microsoft YaHei @@ -60,8 +60,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; strokeStyle?: string;} - radio?: IRadioOption // Radio {width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string;} + checkbox?: ICheckboxOption // Checkbox {width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string; verticalAlign?: VerticalAlign;} + radio?: IRadioOption // Radio {width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string; verticalAlign?: VerticalAlign;} 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 @@ -72,6 +72,7 @@ interface IEditorOption { lineBreak?: ILineBreakOption // LineBreak option. {disabled?:boolean; color?:string; lineWidth?:number;} separator?: ISeparatorOption // Separator option. {lineWidth?:number; strokeStyle?:string;} lineNumber?: ILineNumberOption // LineNumber option. {size?:number; font?:string; color?:string; disabled?:boolean; right?:number} + pageBorder?: IPageBorderOption // PageBorder option. {color?:string; lineWidth:number; padding?:IPadding; disabled?:boolean;} } ``` @@ -159,3 +160,14 @@ interface ILineNumberOption { type?: LineNumberType // Number type (renumber each page, consecutive numbering). default: continuity } ``` + +## PageBorder Configuration + +```typescript +interface IPageBorderOption { + color?: string // color. default: #000000 + lineWidth?: number // line width. default: 1 + padding?: IPadding // padding. default: [0, 0, 0, 0] + disabled?: boolean // Whether to disable. default: true +} +``` diff --git a/docs/en/guide/plugin-custom.md b/docs/en/guide/plugin-custom.md index 05d519f..a0df005 100644 --- a/docs/en/guide/plugin-custom.md +++ b/docs/en/guide/plugin-custom.md @@ -21,5 +21,5 @@ export function myPlugin(editor: Editor, options?: Option) { ## Use Plugin ```javascript -instance.add(myPlugin, options?: Option) +instance.use(myPlugin, options?: Option) ``` diff --git a/docs/guide/command-execute.md b/docs/guide/command-execute.md index c28090b..b7a82ff 100644 --- a/docs/guide/command-execute.md +++ b/docs/guide/command-execute.md @@ -679,7 +679,7 @@ instance.command.executeSaveAsImageElement() 用法: ```javascript -instance.command.executeSaveAsImageElement(element: IElement, display: ImageDisplay) +instance.command.executeChangeImageDisplay(element: IElement, display: ImageDisplay) ``` ## executePageMode @@ -961,3 +961,13 @@ instance.command.executeUpdateOptions(payload: IUpdateOption) ```javascript instance.command.executeInsertTitle(payload: IElement) ``` + +## executeFocus + +功能:光标聚焦 + +用法: + +```javascript +instance.command.executeFocus(payload?: IFocusOption) +``` diff --git a/docs/guide/option.md b/docs/guide/option.md index 8fdc3c3..b9161b7 100644 --- a/docs/guide/option.md +++ b/docs/guide/option.md @@ -14,7 +14,7 @@ new Editor(container, IEditorData | IElement[], { ```typescript interface IEditorOption { - mode?: EditorMode // 编辑器模式:编辑、清洁(不显示视觉辅助元素。如:分页符)、只读、表单(仅控件内可编辑)、打印(不显示辅助元素、未书写控件及前后括号)。默认:编辑 + mode?: EditorMode // 编辑器模式:编辑、清洁(不显示视觉辅助元素。如:分页符)、只读、表单(仅控件内可编辑)、打印(不显示辅助元素、未书写控件及前后括号)、设计模式(不可删除、只读等配置不控制)。默认:编辑 defaultType?: string // 默认元素类型。默认:TEXT defaultColor?: string // 默认字体颜色。默认:#000000 defaultFont?: string // 默认字体。默认:Microsoft YaHei @@ -60,8 +60,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; strokeStyle?: string;} - radio?: IRadioOption // 单选框信息。{width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string;} + checkbox?: ICheckboxOption // 复选框信息。{width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string; verticalAlign?: VerticalAlign;} + radio?: IRadioOption // 单选框信息。{width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string; verticalAlign?: VerticalAlign;} 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 // 编辑器空白占位文本 @@ -72,6 +72,7 @@ interface IEditorOption { lineBreak?: ILineBreakOption // 换行符配置。{disabled?:boolean; color?:string; lineWidth?:number;} separator?: ISeparatorOption // 分隔符配置。{lineWidth?:number; strokeStyle?:string;} lineNumber?: ILineNumberOption // 行号配置。{size?:number; font?:string; color?:string; disabled?:boolean; right?:number} + pageBorder?: IPageBorderOption // 页面边框配置。{color?:string; lineWidth:number; padding?:IPadding; disabled?:boolean;} } ``` @@ -159,3 +160,14 @@ interface ILineNumberOption { type?: LineNumberType // 编号类型(每页重新编号、连续编号)。默认:连续编号 } ``` + +## 页面边框配置 + +```typescript +interface IPageBorderOption { + color?: string // 颜色。默认:#000000 + lineWidth?: number // 宽度。默认:1 + padding?: IPadding // 距离正文内边距。默认:[0, 5, 0, 5] + disabled?: boolean // 是否禁用。默认:true +} +``` diff --git a/docs/guide/plugin-custom.md b/docs/guide/plugin-custom.md index 6bd54f6..1b104ed 100644 --- a/docs/guide/plugin-custom.md +++ b/docs/guide/plugin-custom.md @@ -21,5 +21,5 @@ export function myPlugin(editor: Editor, options?: Option) { ## 使用插件 ```javascript -instance.add(myPlugin, options?: Option) +instance.use(myPlugin, options?: Option) ``` diff --git a/index.html b/index.html index e3d64ce..a1177b3 100644 --- a/index.html +++ b/index.html @@ -365,7 +365,7 @@ 页面:1/1 字数:0 -
编辑模式
+
编辑模式
diff --git a/package.json b/package.json index 324443e..a6ebc6c 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { - "name": "@hufe921/canvas-editor", + "name": "@liushuai05/canvas-editor", "author": "Hufe", "license": "MIT", "version": "0.9.89", "description": "rich text editor by canvas/svg", "publishConfig": { - "registry": "https://registry.npmjs.org/", + "registry": "https://registry.yarnpkg.com/", "access": "public" }, "files": [ diff --git a/src/editor/core/command/Command.ts b/src/editor/core/command/Command.ts index aadab4d..38076e7 100644 --- a/src/editor/core/command/Command.ts +++ b/src/editor/core/command/Command.ts @@ -95,6 +95,7 @@ export class Command { public executeInsertControl: CommandAdapt['insertControl'] public executeUpdateOptions: CommandAdapt['updateOptions'] public executeInsertTitle: CommandAdapt['insertTitle'] + public executeFocus: CommandAdapt['focus'] public getCatalog: CommandAdapt['getCatalog'] public getImage: CommandAdapt['getImage'] public getOptions: CommandAdapt['getOptions'] @@ -214,6 +215,7 @@ export class Command { this.executeSetZone = adapt.setZone.bind(adapt) this.executeUpdateOptions = adapt.updateOptions.bind(adapt) this.executeInsertTitle = adapt.insertTitle.bind(adapt) + this.executeFocus = adapt.focus.bind(adapt) // 获取 this.getImage = adapt.getImage.bind(adapt) this.getOptions = adapt.getOptions.bind(adapt) diff --git a/src/editor/core/command/CommandAdapt.ts b/src/editor/core/command/CommandAdapt.ts index e848279..40c1479 100644 --- a/src/editor/core/command/CommandAdapt.ts +++ b/src/editor/core/command/CommandAdapt.ts @@ -22,6 +22,7 @@ import { import { ElementType } from '../../dataset/enum/Element' import { ElementStyleKey } from '../../dataset/enum/ElementStyle' import { ListStyle, ListType } from '../../dataset/enum/List' +import { MoveDirection } from '../../dataset/enum/Observer' import { RowFlex } from '../../dataset/enum/Row' import { TableBorder, TdBorder, TdSlash } from '../../dataset/enum/table/Table' import { TitleLevel } from '../../dataset/enum/Title' @@ -52,6 +53,7 @@ import { IEditorOption, IEditorResult, IEditorText, + IFocusOption, ISetValueOption, IUpdateOption } from '../../interface/Editor' @@ -789,7 +791,7 @@ export class CommandAdapt { const row = rowList[rowIndex] offsetX = row?.offsetX || 0 } - const innerWidth = this.draw.getOriginalInnerWidth() - offsetX + const innerWidth = this.draw.getContextInnerWidth() - offsetX // colgroup const colgroup: IColgroup[] = [] const colWidth = innerWidth / col @@ -810,7 +812,7 @@ export class CommandAdapt { tdList.push({ colspan: 1, rowspan: 1, - value: [{ value: ZERO, size: 16 }] + value: [] }) } trList.push(tr) @@ -825,7 +827,9 @@ export class CommandAdapt { formatElementList([element], { editorOptions: this.options }) - formatElementContext(elementList, [element], startIndex) + formatElementContext(elementList, [element], startIndex, { + editorOptions: this.options + }) const curIndex = startIndex + 1 this.draw.spliceElementList( elementList, @@ -1586,7 +1590,9 @@ export class CommandAdapt { })) if (!newElementList) return const start = startIndex + 1 - formatElementContext(elementList, newElementList, startIndex) + formatElementContext(elementList, newElementList, startIndex, { + editorOptions: this.options + }) this.draw.spliceElementList( elementList, start, @@ -1731,7 +1737,9 @@ export class CommandAdapt { dashArray: payload } // 从行头增加分割线 - formatElementContext(elementList, [newElement], startIndex) + formatElementContext(elementList, [newElement], startIndex, { + editorOptions: this.options + }) if (startIndex !== 0 && elementList[startIndex].value === ZERO) { this.draw.spliceElementList(elementList, startIndex, 1, newElement) curIndex = startIndex - 1 @@ -2236,7 +2244,8 @@ export class CommandAdapt { const { startIndex } = this.range.getRange() const elementList = this.draw.getElementList() formatElementContext(elementList, cloneElementList, startIndex, { - isBreakWhenWrap: true + isBreakWhenWrap: true, + editorOptions: this.options }) this.draw.insertElementList(cloneElementList) } @@ -2726,4 +2735,23 @@ export class CommandAdapt { // 插入标题 this.draw.insertElementList([cloneElement]) } + + public focus(payload?: IFocusOption) { + const { position = LocationPosition.AFTER } = payload || {} + const curIndex = + position === LocationPosition.BEFORE + ? 0 + : this.draw.getOriginalMainElementList().length - 1 + this.range.setRange(curIndex, curIndex) + this.draw.render({ + curIndex, + isCompute: false, + isSubmitHistory: false + }) + const positionList = this.draw.getPosition().getPositionList() + this.draw.getCursor().moveCursorToVisible({ + cursorPosition: positionList[curIndex], + direction: MoveDirection.DOWN + }) + } } diff --git a/src/editor/core/draw/Draw.ts b/src/editor/core/draw/Draw.ts index 1230c50..9fd9dbc 100644 --- a/src/editor/core/draw/Draw.ts +++ b/src/editor/core/draw/Draw.ts @@ -103,6 +103,7 @@ import { LineBreakParticle } from './particle/LineBreakParticle' import { MouseObserver } from '../observer/MouseObserver' import { LineNumber } from './frame/LineNumber' import { ITd } from '../../interface/table/Td' +import { PageBorder } from './frame/PageBorder' export class Draw { private container: HTMLDivElement @@ -157,6 +158,7 @@ export class Draw { private listParticle: ListParticle private lineBreakParticle: LineBreakParticle private control: Control + private pageBorder: PageBorder private workerManager: WorkerManager private scrollObserver: ScrollObserver private selectionObserver: SelectionObserver @@ -233,6 +235,7 @@ export class Draw { this.listParticle = new ListParticle(this) this.lineBreakParticle = new LineBreakParticle(this) this.control = new Control(this) + this.pageBorder = new PageBorder(this) this.scrollObserver = new ScrollObserver(this) this.selectionObserver = new SelectionObserver(this) @@ -312,6 +315,8 @@ export class Draw { public isReadonly() { switch (this.mode) { + case EditorMode.DESIGN: + return false case EditorMode.READONLY: case EditorMode.PRINT: return true @@ -323,6 +328,7 @@ export class Draw { } public isDisabled() { + if (this.mode === EditorMode.DESIGN) return false const { startIndex, endIndex } = this.range.getRange() const elementList = this.getElementList() if (startIndex === endIndex) { @@ -339,6 +345,10 @@ export class Draw { ) } + public isDesignMode() { + return this.mode === EditorMode.DESIGN + } + public getOriginalWidth(): number { const { paperDirection, width, height } = this.options return paperDirection === PaperDirection.VERTICAL ? width : height @@ -391,6 +401,18 @@ export class Draw { return width - margins[1] - margins[3] } + public getContextInnerWidth(): number { + const positionContext = this.position.getPositionContext() + if (positionContext.isTable) { + const { index, trIndex, tdIndex } = positionContext + const elementList = this.getOriginalElementList() + const td = elementList[index!].trList![trIndex!].tdList[tdIndex!] + const tdPadding = this.getTdPadding() + return td!.width! - tdPadding[1] - tdPadding[3] + } + return this.getOriginalInnerWidth() + } + public getMargins(): IMargin { return this.getOriginalMargins().map(m => m * this.options.scale) } @@ -684,6 +706,7 @@ export class Draw { deleteCount: number, ...items: IElement[] ) { + const isDesignMode = this.isDesignMode() if (deleteCount > 0) { // 当最后元素与开始元素列表信息不一致时:清除当前列表信息 const endIndex = start + deleteCount @@ -714,8 +737,9 @@ export class Draw { while (deleteIndex >= start) { const deleteElement = elementList[deleteIndex] if ( - deleteElement?.control?.deletable !== false && - deleteElement?.title?.deletable !== false + isDesignMode || + (deleteElement?.control?.deletable !== false && + deleteElement?.title?.deletable !== false) ) { elementList.splice(deleteIndex, 1) } @@ -1069,7 +1093,8 @@ export class Draw { pageComponentData.forEach(data => { if (!data) return formatElementList(data, { - editorOptions: this.options + editorOptions: this.options, + isForceCompensation: true }) }) this.setEditorData({ @@ -1177,7 +1202,7 @@ export class Draw { const { defaultBasicRowMarginHeight, defaultRowMargin, scale } = this.options return ( - defaultBasicRowMarginHeight * (el.rowMargin || defaultRowMargin) * scale + defaultBasicRowMarginHeight * (el.rowMargin ?? defaultRowMargin) * scale ) } @@ -1217,7 +1242,7 @@ export class Draw { const curRow: IRow = rowList[rowList.length - 1] const element = elementList[i] const rowMargin = - defaultBasicRowMarginHeight * (element.rowMargin || defaultRowMargin) + defaultBasicRowMarginHeight * (element.rowMargin ?? defaultRowMargin) const metrics: IElementMetrics = { width: 0, height: 0, @@ -2223,13 +2248,25 @@ export class Draw { element.controlComponent === ControlComponent.CHECKBOX ) { this.textParticle.complete() - this.checkboxParticle.render(ctx, element, x, y + offsetY) + this.checkboxParticle.render({ + ctx, + x, + y: y + offsetY, + index: j, + row: curRow + }) } else if ( element.type === ElementType.RADIO || element.controlComponent === ControlComponent.RADIO ) { this.textParticle.complete() - this.radioParticle.render(ctx, element, x, y + offsetY) + this.radioParticle.render({ + ctx, + x, + y: y + offsetY, + index: j, + row: curRow + }) } else if (element.type === ElementType.TAB) { this.textParticle.complete() } else if ( @@ -2470,6 +2507,7 @@ export class Draw { ctx: CanvasRenderingContext2D, payload: IDrawFloatPayload ) { + const { scale } = this.options const floatPositionList = this.position.getFloatPositionList() const { imgDisplay, pageNo } = payload for (let e = 0; e < floatPositionList.length; e++) { @@ -2486,8 +2524,8 @@ export class Draw { this.imageParticle.render( ctx, element, - imgFloatPosition.x, - imgFloatPosition.y + imgFloatPosition.x * scale, + imgFloatPosition.y * scale ) } } @@ -2507,8 +2545,15 @@ export class Draw { private _drawPage(payload: IDrawPagePayload) { const { elementList, positionList, rowList, pageNo } = payload - const { inactiveAlpha, pageMode, header, footer, pageNumber, lineNumber } = - this.options + const { + inactiveAlpha, + pageMode, + header, + footer, + pageNumber, + lineNumber, + pageBorder + } = this.options const innerWidth = this.getInnerWidth() const ctx = this.ctxList[pageNo] // 判断当前激活区域-非正文区域时元素透明度降低 @@ -2573,6 +2618,10 @@ export class Draw { if (!lineNumber.disabled) { this.lineNumber.render(ctx, pageNo) } + // 绘制页面边框 + if (!pageBorder.disabled) { + this.pageBorder.render(ctx) + } } private _disconnectLazyRender() { diff --git a/src/editor/core/draw/control/Control.ts b/src/editor/core/draw/control/Control.ts index 7c3d97f..014a0e6 100644 --- a/src/editor/core/draw/control/Control.ts +++ b/src/editor/core/draw/control/Control.ts @@ -208,7 +208,7 @@ export class Control { } public getIsDisabledControl(context: IControlContext = {}): boolean { - if (!this.activeControl) return false + if (this.draw.isDesignMode() || !this.activeControl) return false const { startIndex, endIndex } = context.range || this.range.getRange() if (startIndex === endIndex && ~startIndex && ~endIndex) { const elementList = context.elementList || this.getElementList() @@ -452,8 +452,11 @@ export class Control { ): number | null { const elementList = context.elementList || this.getElementList() const startElement = elementList[startIndex] - const { deletable = true } = startElement.control! - if (!deletable) return null + // 设计模式不验证删除权限 + if (!this.draw.isDesignMode()) { + const { deletable = true } = startElement.control! + if (!deletable) return null + } let leftIndex = -1 let rightIndex = -1 // 向左查找 @@ -538,7 +541,9 @@ export class Control { controlComponent: ControlComponent.PLACEHOLDER, color: this.controlOptions.placeholderColor } - formatElementContext(elementList, [newElement], startIndex) + formatElementContext(elementList, [newElement], startIndex, { + editorOptions: this.options + }) this.draw.spliceElementList( elementList, startIndex + p + 1, @@ -892,7 +897,8 @@ export class Control { const elementList = zipElementList(pageComponentData[pageComponentKey]!) pageComponentData[pageComponentKey] = elementList formatElementList(elementList, { - editorOptions: this.options + editorOptions: this.options, + isForceCompensation: true }) } this.draw.setEditorData(pageComponentData) diff --git a/src/editor/core/draw/control/date/DateControl.ts b/src/editor/core/draw/control/date/DateControl.ts index 6e13a99..1a84114 100644 --- a/src/editor/core/draw/control/date/DateControl.ts +++ b/src/editor/core/draw/control/date/DateControl.ts @@ -6,11 +6,13 @@ import { import { ControlComponent } from '../../../../dataset/enum/Control' import { ElementType } from '../../../../dataset/enum/Element' import { KeyMap } from '../../../../dataset/enum/KeyMap' +import { DeepRequired } from '../../../../interface/Common' import { IControlContext, IControlInstance, IControlRuleOption } from '../../../../interface/Control' +import { IEditorOption } from '../../../../interface/Editor' import { IElement } from '../../../../interface/Element' import { omitObject, pickObject } from '../../../../utils' import { formatElementContext } from '../../../../utils/element' @@ -24,10 +26,12 @@ export class DateControl implements IControlInstance { private control: Control private isPopup: boolean private datePicker: DatePicker | null + private options: DeepRequired constructor(element: IElement, control: Control) { const draw = control.getDraw() this.draw = draw + this.options = draw.getOptions() this.element = element this.control = control this.isPopup = false @@ -138,7 +142,9 @@ export class DateControl implements IControlInstance { ...data[i], controlComponent: ControlComponent.VALUE } - formatElementContext(elementList, [newElement], startIndex) + formatElementContext(elementList, [newElement], startIndex, { + editorOptions: this.options + }) draw.spliceElementList(elementList, start + i, 0, newElement) } return start + data.length - 1 @@ -207,7 +213,9 @@ export class DateControl implements IControlInstance { value: date[i], controlComponent: ControlComponent.VALUE } - formatElementContext(elementList, [newElement], prefixIndex) + formatElementContext(elementList, [newElement], prefixIndex, { + editorOptions: this.options + }) draw.spliceElementList(elementList, start + i, 0, newElement) } // 重新渲染控件 diff --git a/src/editor/core/draw/control/select/SelectControl.ts b/src/editor/core/draw/control/select/SelectControl.ts index a45de98..37365a7 100644 --- a/src/editor/core/draw/control/select/SelectControl.ts +++ b/src/editor/core/draw/control/select/SelectControl.ts @@ -10,11 +10,13 @@ import { ControlComponent } from '../../../../dataset/enum/Control' import { EditorComponent } from '../../../../dataset/enum/Editor' import { ElementType } from '../../../../dataset/enum/Element' import { KeyMap } from '../../../../dataset/enum/KeyMap' +import { DeepRequired } from '../../../../interface/Common' import { IControlContext, IControlInstance, IControlRuleOption } from '../../../../interface/Control' +import { IEditorOption } from '../../../../interface/Editor' import { IElement } from '../../../../interface/Element' import { omitObject, pickObject, splitText } from '../../../../utils' import { formatElementContext } from '../../../../utils/element' @@ -25,8 +27,10 @@ export class SelectControl implements IControlInstance { private control: Control private isPopup: boolean private selectDom: HTMLDivElement | null + private options: DeepRequired constructor(element: IElement, control: Control) { + this.options = control.getDraw().getOptions() this.element = element this.control = control this.isPopup = false @@ -270,7 +274,9 @@ export class SelectControl implements IControlInstance { value: data[i], controlComponent: ControlComponent.VALUE } - formatElementContext(elementList, [newElement], prefixIndex) + formatElementContext(elementList, [newElement], prefixIndex, { + editorOptions: this.options + }) draw.spliceElementList(elementList, start + i, 0, newElement) } // 设置状态 diff --git a/src/editor/core/draw/control/text/TextControl.ts b/src/editor/core/draw/control/text/TextControl.ts index 82c1fa6..d42c8e5 100644 --- a/src/editor/core/draw/control/text/TextControl.ts +++ b/src/editor/core/draw/control/text/TextControl.ts @@ -4,11 +4,13 @@ import { } from '../../../../dataset/constant/Element' import { ControlComponent } from '../../../../dataset/enum/Control' import { KeyMap } from '../../../../dataset/enum/KeyMap' +import { DeepRequired } from '../../../../interface/Common' import { IControlContext, IControlInstance, IControlRuleOption } from '../../../../interface/Control' +import { IEditorOption } from '../../../../interface/Editor' import { IElement } from '../../../../interface/Element' import { omitObject, pickObject } from '../../../../utils' import { formatElementContext } from '../../../../utils/element' @@ -17,8 +19,10 @@ import { Control } from '../Control' export class TextControl implements IControlInstance { private element: IElement private control: Control + private options: DeepRequired constructor(element: IElement, control: Control) { + this.options = control.getDraw().getOptions() this.element = element this.control = control } @@ -114,7 +118,9 @@ export class TextControl implements IControlInstance { ...data[i], controlComponent: ControlComponent.VALUE } - formatElementContext(elementList, [newElement], startIndex) + formatElementContext(elementList, [newElement], startIndex, { + editorOptions: this.options + }) draw.spliceElementList(elementList, start + i, 0, newElement) } return start + data.length - 1 diff --git a/src/editor/core/draw/frame/PageBorder.ts b/src/editor/core/draw/frame/PageBorder.ts new file mode 100644 index 0000000..3a6da9d --- /dev/null +++ b/src/editor/core/draw/frame/PageBorder.ts @@ -0,0 +1,47 @@ +import { DeepRequired } from '../../../interface/Common' +import { IEditorOption } from '../../../interface/Editor' +import { Draw } from '../Draw' +import { Footer } from './Footer' +import { Header } from './Header' + +export class PageBorder { + private draw: Draw + private header: Header + private footer: Footer + private options: DeepRequired + + constructor(draw: Draw) { + this.draw = draw + this.header = draw.getHeader() + this.footer = draw.getFooter() + this.options = draw.getOptions() + } + + public render(ctx: CanvasRenderingContext2D) { + const { + scale, + pageBorder: { color, lineWidth, padding } + } = this.options + ctx.save() + ctx.translate(0.5, 0.5) + ctx.strokeStyle = color + ctx.lineWidth = lineWidth * scale + const margins = this.draw.getMargins() + // x:左边距 - 左距离正文距离 + const x = margins[3] - padding[3] * scale + // y:页眉上边距 + 页眉高度 - 上距离正文距离 + const y = margins[0] + this.header.getExtraHeight() - padding[0] * scale + // width:页面宽度 + 左右距离正文距离 + const width = this.draw.getInnerWidth() + (padding[1] + padding[3]) * scale + // height:页面高度 - 正文起始位置 - 页脚高度 - 下边距 - 下距离正文距离 + const height = + this.draw.getHeight() - + y - + this.footer.getExtraHeight() - + margins[2] + + padding[2] * scale + ctx.rect(x, y, width, height) + ctx.stroke() + ctx.restore() + } +} diff --git a/src/editor/core/draw/frame/Placeholder.ts b/src/editor/core/draw/frame/Placeholder.ts index 146d82d..1aa5106 100644 --- a/src/editor/core/draw/frame/Placeholder.ts +++ b/src/editor/core/draw/frame/Placeholder.ts @@ -84,7 +84,8 @@ export class Placeholder { } ] formatElementList(this.elementList, { - editorOptions: this.options + editorOptions: this.options, + isForceCompensation: true }) // 计算 this._compute() diff --git a/src/editor/core/draw/particle/CheckboxParticle.ts b/src/editor/core/draw/particle/CheckboxParticle.ts index 1a5817b..1ec7faf 100644 --- a/src/editor/core/draw/particle/CheckboxParticle.ts +++ b/src/editor/core/draw/particle/CheckboxParticle.ts @@ -1,9 +1,19 @@ +import { NBSP, ZERO } from '../../../dataset/constant/Common' +import { VerticalAlign } from '../../../dataset/enum/VerticalAlign' import { DeepRequired } from '../../../interface/Common' import { IEditorOption } from '../../../interface/Editor' import { IElement } from '../../../interface/Element' -import { IRowElement } from '../../../interface/Row' +import { IRow, IRowElement } from '../../../interface/Row' import { Draw } from '../Draw' +interface ICheckboxRenderOption { + ctx: CanvasRenderingContext2D + x: number + y: number + row: IRow + index: number +} + export class CheckboxParticle { private draw: Draw private options: DeepRequired @@ -28,17 +38,41 @@ export class CheckboxParticle { }) } - public render( - ctx: CanvasRenderingContext2D, - element: IRowElement, - x: number, - y: number - ) { + public render(payload: ICheckboxRenderOption) { + const { ctx, x, index, row } = payload + let { y } = payload const { - checkbox: { gap, lineWidth, fillStyle, strokeStyle }, + checkbox: { gap, lineWidth, fillStyle, strokeStyle, verticalAlign }, scale } = this.options - const { metrics, checkbox } = element + const { metrics, checkbox } = row.elementList[index] + // 垂直布局设置 + if ( + verticalAlign === VerticalAlign.TOP || + verticalAlign === VerticalAlign.MIDDLE + ) { + let nextIndex = index + 1 + let nextElement: IRowElement | null = null + while (nextIndex < row.elementList.length) { + nextElement = row.elementList[nextIndex] + if (nextElement.value !== ZERO && nextElement.value !== NBSP) break + nextIndex++ + } + // 以后一个非空格元素为基准 + if (nextElement) { + const { + metrics: { boundingBoxAscent, boundingBoxDescent } + } = nextElement + const textHeight = boundingBoxAscent + boundingBoxDescent + if (textHeight > metrics.height) { + if (verticalAlign === VerticalAlign.TOP) { + y -= boundingBoxAscent - metrics.height + } else if (verticalAlign === VerticalAlign.MIDDLE) { + y -= (textHeight - metrics.height) / 2 + } + } + } + } // left top 四舍五入避免1像素问题 const left = Math.round(x + gap * scale) const top = Math.round(y - metrics.height + lineWidth) diff --git a/src/editor/core/draw/particle/ListParticle.ts b/src/editor/core/draw/particle/ListParticle.ts index 1548979..f4e1c49 100644 --- a/src/editor/core/draw/particle/ListParticle.ts +++ b/src/editor/core/draw/particle/ListParticle.ts @@ -199,9 +199,16 @@ export class ListParticle { height: height * scale } } - this.draw - .getCheckboxParticle() - .render(ctx, checkboxRowElement, x - gap * scale, y) + this.draw.getCheckboxParticle().render({ + ctx, + x: x - gap * scale, + y, + index: 0, + row: { + ...row, + elementList: [checkboxRowElement, ...row.elementList] + } + }) } else { let text = '' if (startElement.listType === ListType.UL) { diff --git a/src/editor/core/draw/particle/RadioParticle.ts b/src/editor/core/draw/particle/RadioParticle.ts index a6a2e25..1df91f3 100644 --- a/src/editor/core/draw/particle/RadioParticle.ts +++ b/src/editor/core/draw/particle/RadioParticle.ts @@ -1,9 +1,19 @@ +import { NBSP, ZERO } from '../../../dataset/constant/Common' +import { VerticalAlign } from '../../../dataset/enum/VerticalAlign' import { DeepRequired } from '../../../interface/Common' import { IEditorOption } from '../../../interface/Editor' import { IElement } from '../../../interface/Element' -import { IRowElement } from '../../../interface/Row' +import { IRow, IRowElement } from '../../../interface/Row' import { Draw } from '../Draw' +interface IRadioRenderOption { + ctx: CanvasRenderingContext2D + x: number + y: number + row: IRow + index: number +} + export class RadioParticle { private draw: Draw private options: DeepRequired @@ -28,17 +38,41 @@ export class RadioParticle { }) } - public render( - ctx: CanvasRenderingContext2D, - element: IRowElement, - x: number, - y: number - ) { + public render(payload: IRadioRenderOption) { + const { ctx, x, index, row } = payload + let { y } = payload const { - radio: { gap, lineWidth, fillStyle, strokeStyle }, + radio: { gap, lineWidth, fillStyle, strokeStyle, verticalAlign }, scale } = this.options - const { metrics, radio } = element + const { metrics, radio } = row.elementList[index] + // 垂直布局设置 + if ( + verticalAlign === VerticalAlign.TOP || + verticalAlign === VerticalAlign.MIDDLE + ) { + let nextIndex = index + 1 + let nextElement: IRowElement | null = null + while (nextIndex < row.elementList.length) { + nextElement = row.elementList[nextIndex] + if (nextElement.value !== ZERO && nextElement.value !== NBSP) break + nextIndex++ + } + // 以后一个非空格元素为基准 + if (nextElement) { + const { + metrics: { boundingBoxAscent, boundingBoxDescent } + } = nextElement + const textHeight = boundingBoxAscent + boundingBoxDescent + if (textHeight > metrics.height) { + if (verticalAlign === VerticalAlign.TOP) { + y -= boundingBoxAscent - metrics.height + } else if (verticalAlign === VerticalAlign.MIDDLE) { + y -= (textHeight - metrics.height) / 2 + } + } + } + } // left top 四舍五入避免1像素问题 const left = Math.round(x + gap * scale) const top = Math.round(y - metrics.height + lineWidth) diff --git a/src/editor/core/draw/particle/date/DateParticle.ts b/src/editor/core/draw/particle/date/DateParticle.ts index b6a9d3b..da31fd7 100644 --- a/src/editor/core/draw/particle/date/DateParticle.ts +++ b/src/editor/core/draw/particle/date/DateParticle.ts @@ -1,4 +1,6 @@ import { ElementType } from '../../../../dataset/enum/Element' +import { DeepRequired } from '../../../../interface/Common' +import { IEditorOption } from '../../../../interface/Editor' import { IElement, IElementPosition } from '../../../../interface/Element' import { formatElementContext } from '../../../../utils/element' import { RangeManager } from '../../../range/RangeManager' @@ -9,9 +11,11 @@ export class DateParticle { private draw: Draw private range: RangeManager private datePicker: DatePicker + private options: DeepRequired constructor(draw: Draw) { this.draw = draw + this.options = draw.getOptions() this.range = draw.getRange() this.datePicker = new DatePicker(draw, { onSubmit: this._setValue.bind(this) @@ -43,7 +47,9 @@ export class DateParticle { } ] } - formatElementContext(elementList, [dateElement], leftIndex) + formatElementContext(elementList, [dateElement], leftIndex, { + editorOptions: this.options + }) this.draw.insertElementList([dateElement]) } diff --git a/src/editor/core/event/handlers/input.ts b/src/editor/core/event/handlers/input.ts index 215d208..61967f6 100644 --- a/src/editor/core/event/handlers/input.ts +++ b/src/editor/core/event/handlers/input.ts @@ -30,11 +30,15 @@ export function input(data: string, host: CanvasEvent) { const elementList = draw.getElementList() const copyElement = getAnchorElement(elementList, endIndex) if (!copyElement) return + const isDesignMode = draw.isDesignMode() const inputData: IElement[] = splitText(text).map(value => { const newElement: IElement = { value } - if (!copyElement.title?.disabled && !copyElement.control?.disabled) { + if ( + isDesignMode || + (!copyElement.title?.disabled && !copyElement.control?.disabled) + ) { const nextElement = elementList[endIndex + 1] if ( !copyElement.type || @@ -69,7 +73,9 @@ export function input(data: string, host: CanvasEvent) { if (startIndex !== endIndex) { draw.spliceElementList(elementList, start, endIndex - startIndex) } - formatElementContext(elementList, inputData, startIndex) + formatElementContext(elementList, inputData, startIndex, { + editorOptions: draw.getOptions() + }) draw.spliceElementList(elementList, start, 0, ...inputData) curIndex = startIndex + inputData.length } diff --git a/src/editor/core/event/handlers/keydown/enter.ts b/src/editor/core/event/handlers/keydown/enter.ts index 5ef46d9..c7c279d 100644 --- a/src/editor/core/event/handlers/keydown/enter.ts +++ b/src/editor/core/event/handlers/keydown/enter.ts @@ -40,7 +40,8 @@ export function enter(evt: KeyboardEvent, host: CanvasEvent) { } // 格式化上下文 formatElementContext(elementList, [enterText], startIndex, { - isBreakWhenWrap: true + isBreakWhenWrap: true, + editorOptions: draw.getOptions() }) // 标题结尾处回车无需格式化及样式复制 if ( diff --git a/src/editor/core/event/handlers/keydown/tab.ts b/src/editor/core/event/handlers/keydown/tab.ts index e863910..5c71ca4 100644 --- a/src/editor/core/event/handlers/keydown/tab.ts +++ b/src/editor/core/event/handlers/keydown/tab.ts @@ -25,7 +25,9 @@ export function tab(evt: KeyboardEvent, host: CanvasEvent) { const rangeManager = draw.getRange() const { startIndex } = rangeManager.getRange() const elementList = draw.getElementList() - formatElementContext(elementList, [tabElement], startIndex) + formatElementContext(elementList, [tabElement], startIndex, { + editorOptions: draw.getOptions() + }) draw.insertElementList([tabElement]) } } diff --git a/src/editor/core/event/handlers/mousedown.ts b/src/editor/core/event/handlers/mousedown.ts index c1bf0ac..8f67026 100644 --- a/src/editor/core/event/handlers/mousedown.ts +++ b/src/editor/core/event/handlers/mousedown.ts @@ -2,12 +2,16 @@ import { ImageDisplay } from '../../../dataset/enum/Common' import { EditorMode } from '../../../dataset/enum/Editor' import { ElementType } from '../../../dataset/enum/Element' import { MouseEventButton } from '../../../dataset/enum/Event' +import { ControlComponent } from '../../../dataset/enum/Control' +import { ControlType } from '../../../dataset/enum/Control' import { IPreviewerDrawOption } from '../../../interface/Previewer' 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' +import { IElement } from '../../../interface/Element' +import { Draw } from '../../draw/Draw' export function setRangeCache(host: CanvasEvent) { const draw = host.getDraw() @@ -21,6 +25,42 @@ export function setRangeCache(host: CanvasEvent) { host.cachePositionContext = position.getPositionContext() } +export function hitCheckbox(element: IElement, draw: Draw) { + const { checkbox, control } = element + // 复选框不在控件内独立控制 + if (!control) { + draw.getCheckboxParticle().setSelect(element) + } else { + const codes = control?.code ? control.code.split(',') : [] + if (checkbox?.value) { + const codeIndex = codes.findIndex(c => c === checkbox.code) + codes.splice(codeIndex, 1) + } else { + if (checkbox?.code) { + codes.push(checkbox.code) + } + } + const activeControl = draw.getControl().getActiveControl() + if (activeControl instanceof CheckboxControl) { + activeControl.setSelect(codes) + } + } +} + +export function hitRadio(element: IElement, draw: Draw) { + const { radio, control } = element + // 单选框不在控件内独立控制 + if (!control) { + draw.getRadioParticle().setSelect(element) + } else { + const codes = radio?.code ? [radio.code] : [] + const activeControl = draw.getControl().getActiveControl() + if (activeControl instanceof RadioControl) { + activeControl.setSelect(codes) + } + } +} + export function mousedown(evt: MouseEvent, host: CanvasEvent) { if (evt.button === MouseEventButton.RIGHT) return const draw = host.getDraw() @@ -100,40 +140,27 @@ export function mousedown(evt: MouseEvent, host: CanvasEvent) { rangeManager.setRange(startIndex, endIndex) position.setCursorPosition(positionList[curIndex]) // 复选框 - const isSetCheckbox = isDirectHitCheckbox && !isReadonly - // 单选框 - const isSetRadio = isDirectHitRadio && !isReadonly - if (isSetCheckbox) { - const { checkbox, control } = curElement - // 复选框不在控件内独立控制 - if (!control) { - draw.getCheckboxParticle().setSelect(curElement) - } else { - const codes = control?.code ? control.code.split(',') : [] - if (checkbox?.value) { - const codeIndex = codes.findIndex(c => c === checkbox.code) - codes.splice(codeIndex, 1) - } else { - if (checkbox?.code) { - codes.push(checkbox.code) - } - } - const activeControl = draw.getControl().getActiveControl() - if (activeControl instanceof CheckboxControl) { - 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) + if (isDirectHitCheckbox && !isReadonly) { + hitCheckbox(curElement, draw) + } else if (isDirectHitRadio && !isReadonly) { + hitRadio(curElement, draw) + } else if ( + curElement.controlComponent === ControlComponent.VALUE && + (curElement.control?.type === ControlType.CHECKBOX || + curElement.control?.type === ControlType.RADIO) + ) { + // 向左查找 + let preIndex = curIndex + while (preIndex > 0) { + const preElement = elementList[preIndex] + if (preElement.controlComponent === ControlComponent.CHECKBOX) { + hitCheckbox(preElement, draw) + break + } else if (preElement.controlComponent === ControlComponent.RADIO) { + hitRadio(preElement, draw) + break } + preIndex-- } } else { draw.render({ diff --git a/src/editor/core/event/handlers/mouseup.ts b/src/editor/core/event/handlers/mouseup.ts index 23aac0d..81e572a 100644 --- a/src/editor/core/event/handlers/mouseup.ts +++ b/src/editor/core/event/handlers/mouseup.ts @@ -176,7 +176,9 @@ export function mouseup(evt: MouseEvent, host: CanvasEvent) { return newElement } }) - formatElementContext(elementList, replaceElementList, range.startIndex) + formatElementContext(elementList, replaceElementList, range.startIndex, { + editorOptions: draw.getOptions() + }) // 缓存拖拽选区开始元素、位置、开始结束id const cacheStartElement = cacheElementList[cacheStartIndex] const cacheStartPosition = cachePositionList[cacheStartIndex] diff --git a/src/editor/core/event/handlers/paste.ts b/src/editor/core/event/handlers/paste.ts index 6a26e67..401c558 100644 --- a/src/editor/core/event/handlers/paste.ts +++ b/src/editor/core/event/handlers/paste.ts @@ -50,7 +50,8 @@ export function pasteElement(host: CanvasEvent, elementList: IElement[]) { } } formatElementContext(originalElementList, elementList, startIndex, { - isBreakWhenWrap: true + isBreakWhenWrap: true, + editorOptions: draw.getOptions() }) } draw.insertElementList(elementList) @@ -87,7 +88,9 @@ export function pasteImage(host: CanvasEvent, file: File | Blob) { height: image.height } if (~startIndex) { - formatElementContext(elementList, [imageElement], startIndex) + formatElementContext(elementList, [imageElement], startIndex, { + editorOptions: draw.getOptions() + }) } draw.insertElementList([imageElement]) } diff --git a/src/editor/core/range/RangeManager.ts b/src/editor/core/range/RangeManager.ts index a377f24..f0ad4b8 100644 --- a/src/editor/core/range/RangeManager.ts +++ b/src/editor/core/range/RangeManager.ts @@ -430,7 +430,7 @@ export class RangeManager { const color = curElement.color || null const highlight = curElement.highlight || null const rowFlex = curElement.rowFlex || null - const rowMargin = curElement.rowMargin || this.options.defaultRowMargin + const rowMargin = curElement.rowMargin ?? this.options.defaultRowMargin const dashArray = curElement.dashArray || [] const level = curElement.level || null const listType = curElement.listType || null diff --git a/src/editor/dataset/constant/Checkbox.ts b/src/editor/dataset/constant/Checkbox.ts index 3153027..5ff7b04 100644 --- a/src/editor/dataset/constant/Checkbox.ts +++ b/src/editor/dataset/constant/Checkbox.ts @@ -1,4 +1,5 @@ import { ICheckboxOption } from '../../interface/Checkbox' +import { VerticalAlign } from '../enum/VerticalAlign' export const defaultCheckboxOption: Readonly> = { width: 14, @@ -6,5 +7,6 @@ export const defaultCheckboxOption: Readonly> = { gap: 5, lineWidth: 1, fillStyle: '#5175f4', - strokeStyle: '#ffffff' + strokeStyle: '#ffffff', + verticalAlign: VerticalAlign.BOTTOM } diff --git a/src/editor/dataset/constant/PageBorder.ts b/src/editor/dataset/constant/PageBorder.ts new file mode 100644 index 0000000..5751c37 --- /dev/null +++ b/src/editor/dataset/constant/PageBorder.ts @@ -0,0 +1,8 @@ +import { IPageBorderOption } from '../../interface/PageBorder' + +export const defaultPageBorderOption: Readonly> = { + color: '#000000', + lineWidth: 1, + padding: [0, 5, 0, 5], + disabled: true +} diff --git a/src/editor/dataset/constant/Radio.ts b/src/editor/dataset/constant/Radio.ts index 118d783..a16c0e9 100644 --- a/src/editor/dataset/constant/Radio.ts +++ b/src/editor/dataset/constant/Radio.ts @@ -1,4 +1,5 @@ import { IRadioOption } from '../../interface/Radio' +import { VerticalAlign } from '../enum/VerticalAlign' export const defaultRadioOption: Readonly> = { width: 14, @@ -6,5 +7,6 @@ export const defaultRadioOption: Readonly> = { gap: 5, lineWidth: 1, fillStyle: '#5175f4', - strokeStyle: '#000000' + strokeStyle: '#000000', + verticalAlign: VerticalAlign.BOTTOM } diff --git a/src/editor/dataset/enum/Editor.ts b/src/editor/dataset/enum/Editor.ts index c19757a..454f0a9 100644 --- a/src/editor/dataset/enum/Editor.ts +++ b/src/editor/dataset/enum/Editor.ts @@ -19,7 +19,8 @@ export enum EditorMode { CLEAN = 'clean', // 清洁模式(隐藏辅助元素) READONLY = 'readonly', // 只读模式(文档不可编辑) FORM = 'form', // 表单模式(仅控件内可编辑) - PRINT = 'print' // 打印模式(文档不可编辑、隐藏辅助元素、选区、未书写控件及边框) + PRINT = 'print', // 打印模式(文档不可编辑、隐藏辅助元素、选区、未书写控件及边框) + DESIGN = 'design' // 设计模式(不可删除、只读等配置不控制) } export enum EditorZone { diff --git a/src/editor/index.ts b/src/editor/index.ts index 4cf9a1b..c7f60a3 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -89,7 +89,8 @@ export default class Editor { ] pageComponentData.forEach(elementList => { formatElementList(elementList, { - editorOptions + editorOptions, + isForceCompensation: true }) }) // 监听 diff --git a/src/editor/interface/Checkbox.ts b/src/editor/interface/Checkbox.ts index 13ad547..456bd24 100644 --- a/src/editor/interface/Checkbox.ts +++ b/src/editor/interface/Checkbox.ts @@ -1,3 +1,5 @@ +import { VerticalAlign } from '../dataset/enum/VerticalAlign' + export interface ICheckbox { value: boolean | null code?: string @@ -11,4 +13,5 @@ export interface ICheckboxOption { lineWidth?: number fillStyle?: string strokeStyle?: string + verticalAlign?: VerticalAlign } diff --git a/src/editor/interface/Editor.ts b/src/editor/interface/Editor.ts index 8be2278..7d26670 100644 --- a/src/editor/interface/Editor.ts +++ b/src/editor/interface/Editor.ts @@ -1,4 +1,4 @@ -import { IElement } from '..' +import { IElement, LocationPosition } from '..' import { EditorMode, PageMode, @@ -25,6 +25,7 @@ import { IZoneOption } from './Zone' import { ISeparatorOption } from './Separator' import { ITableOption } from './table/Table' import { ILineNumberOption } from './LineNumber' +import { IPageBorderOption } from './PageBorder' export interface IEditorData { header?: IElement[] @@ -91,6 +92,7 @@ export interface IEditorOption { lineBreak?: ILineBreakOption separator?: ISeparatorOption lineNumber?: ILineNumberOption + pageBorder?: IPageBorderOption } export interface IEditorResult { @@ -123,3 +125,7 @@ export type IUpdateOption = Omit< export interface ISetValueOption { isSetCursor?: boolean } + +export interface IFocusOption { + position?: LocationPosition +} diff --git a/src/editor/interface/PageBorder.ts b/src/editor/interface/PageBorder.ts new file mode 100644 index 0000000..1fe8d25 --- /dev/null +++ b/src/editor/interface/PageBorder.ts @@ -0,0 +1,8 @@ +import { IPadding } from './Common' + +export interface IPageBorderOption { + color?: string + lineWidth?: number + padding?: IPadding + disabled?: boolean +} diff --git a/src/editor/interface/Radio.ts b/src/editor/interface/Radio.ts index 1f4fc61..95a42dc 100644 --- a/src/editor/interface/Radio.ts +++ b/src/editor/interface/Radio.ts @@ -1,3 +1,5 @@ +import { VerticalAlign } from '../dataset/enum/VerticalAlign' + export interface IRadio { value: boolean | null code?: string @@ -11,4 +13,5 @@ export interface IRadioOption { lineWidth?: number fillStyle?: string strokeStyle?: string + verticalAlign?: VerticalAlign } diff --git a/src/editor/utils/element.ts b/src/editor/utils/element.ts index 04ffdfe..6f3e433 100644 --- a/src/editor/utils/element.ts +++ b/src/editor/utils/element.ts @@ -9,6 +9,7 @@ import { splitText } from '.' import { + EditorMode, ElementType, IEditorOption, IElement, @@ -65,7 +66,8 @@ export function unzipElementList(elementList: IElement[]): IElement[] { } interface IFormatElementListOption { - isHandleFirstElement?: boolean + isHandleFirstElement?: boolean // 根据上下文确定首字符处理逻辑(处理首字符补偿) + isForceCompensation?: boolean // 强制补偿字符 editorOptions: DeepRequired } @@ -73,17 +75,19 @@ export function formatElementList( elementList: IElement[], options: IFormatElementListOption ) { - const { isHandleFirstElement, editorOptions } = { - isHandleFirstElement: true, - ...options - } + const { + isHandleFirstElement = true, + isForceCompensation = false, + editorOptions + } = options const startElement = elementList[0] // 非首字符零宽节点文本元素则补偿-列表元素内部会补偿此处忽略 if ( - isHandleFirstElement && - startElement?.type !== ElementType.LIST && - ((startElement?.type && startElement.type !== ElementType.TEXT) || - !START_LINE_BREAK_REG.test(startElement?.value)) + isForceCompensation || + (isHandleFirstElement && + startElement?.type !== ElementType.LIST && + ((startElement?.type && startElement.type !== ElementType.TEXT) || + !START_LINE_BREAK_REG.test(startElement?.value))) ) { elementList.unshift({ value: ZERO @@ -100,7 +104,8 @@ export function formatElementList( const valueList = el.valueList || [] formatElementList(valueList, { ...options, - isHandleFirstElement: false + isHandleFirstElement: false, + isForceCompensation: false }) // 追加节点 if (valueList.length) { @@ -134,7 +139,8 @@ export function formatElementList( const valueList = el.valueList || [] formatElementList(valueList, { ...options, - isHandleFirstElement: true + isHandleFirstElement: true, + isForceCompensation: false }) // 追加节点 if (valueList.length) { @@ -170,7 +176,8 @@ export function formatElementList( td.id = tdId formatElementList(td.value, { ...options, - isHandleFirstElement: true + isHandleFirstElement: true, + isForceCompensation: true }) for (let v = 0; v < td.value.length; v++) { const value = td.value[v] @@ -385,7 +392,8 @@ export function formatElementList( } formatElementList(valueList, { ...options, - isHandleFirstElement: false + isHandleFirstElement: false, + isForceCompensation: false }) for (let v = 0; v < valueList.length; v++) { const element = valueList[v] @@ -451,7 +459,7 @@ export function formatElementList( } el = elementList[i] } - if (el.value === '\n') { + if (el.value === '\n' || el.value == '\r\n') { el.value = ZERO } if (el.type === ElementType.IMAGE || el.type === ElementType.BLOCK) { @@ -886,7 +894,8 @@ export function getAnchorElement( } export interface IFormatElementContextOption { - isBreakWhenWrap: boolean + isBreakWhenWrap?: boolean + editorOptions?: DeepRequired } export function formatElementContext( @@ -897,11 +906,12 @@ export function formatElementContext( ) { let copyElement = getAnchorElement(sourceElementList, anchorIndex) if (!copyElement) return - // 标题元素禁用时不复制标题属性 - if (copyElement.title?.disabled) { + const { isBreakWhenWrap = false, editorOptions } = options || {} + const { mode } = editorOptions || {} + // 非设计模式时:标题元素禁用时不复制标题属性 + if (mode !== EditorMode.DESIGN && copyElement.title?.disabled) { copyElement = omitObject(copyElement, TITLE_CONTEXT_ATTR) } - const { isBreakWhenWrap = false } = options || {} // 是否已经换行 let isBreakWarped = false for (let e = 0; e < formatElementList.length; e++) { @@ -930,7 +940,8 @@ export function formatElementContext( formatElementContext( sourceElementList, targetElement.valueList, - anchorIndex + anchorIndex, + options ) } // 非块类元素,需处理行属性 diff --git a/src/editor/utils/option.ts b/src/editor/utils/option.ts index 7441fd0..c4a14a5 100644 --- a/src/editor/utils/option.ts +++ b/src/editor/utils/option.ts @@ -37,6 +37,8 @@ import { ITitleOption } from '../interface/Title' import { IWatermark } from '../interface/Watermark' import { IZoneOption } from '../interface/Zone' import { ILineNumberOption } from '../interface/LineNumber' +import { IPageBorderOption } from '../interface/PageBorder' +import { defaultPageBorderOption } from '../dataset/constant/PageBorder' import { EditorMode, PageMode, @@ -120,6 +122,10 @@ export function mergeOption( ...defaultLineNumberOption, ...options.lineNumber } + const pageBorderOptions: Required = { + ...defaultPageBorderOption, + ...options.pageBorder + } return { mode: EditorMode.EDIT, @@ -180,6 +186,7 @@ export function mergeOption( background: backgroundOptions, lineBreak: lineBreakOptions, separator: separatorOptions, - lineNumber: lineNumberOptions + lineNumber: lineNumberOptions, + pageBorder: pageBorderOptions } } diff --git a/src/main.ts b/src/main.ts index 858ffeb..ad95f3f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1412,6 +1412,10 @@ window.onload = function () { { mode: EditorMode.PRINT, name: '打印模式' + }, + { + mode: EditorMode.DESIGN, + name: '设计模式' } ] const modeElement = document.querySelector('.editor-mode')!