From c6c2f981497224cc95dbdd50ee9552683bc1df6f Mon Sep 17 00:00:00 2001 From: Hufe921 Date: Wed, 22 May 2024 22:22:16 +0800 Subject: [PATCH] feat: move between controls using shortcut keys #548 --- docs/en/guide/shortcut-internal.md | 6 +- docs/guide/shortcut-internal.md | 6 +- src/editor/core/draw/control/Control.ts | 265 +++++++++++++++++- .../core/event/handlers/keydown/left.ts | 15 + .../core/event/handlers/keydown/right.ts | 15 + src/editor/core/event/handlers/keydown/tab.ts | 29 +- src/editor/interface/Control.ts | 11 + 7 files changed, 335 insertions(+), 12 deletions(-) diff --git a/docs/en/guide/shortcut-internal.md b/docs/en/guide/shortcut-internal.md index 0f76cbc..340711a 100644 --- a/docs/en/guide/shortcut-internal.md +++ b/docs/en/guide/shortcut-internal.md @@ -70,7 +70,11 @@ Feature: Exit format brush ## Tab -Feature: Increase indent +Feature: Increase indent/Move next control + +## Shift + Tab + +Feature: Move previous control ## Ctrl/Cmd + Z diff --git a/docs/guide/shortcut-internal.md b/docs/guide/shortcut-internal.md index 1bc1a3f..8434e66 100644 --- a/docs/guide/shortcut-internal.md +++ b/docs/guide/shortcut-internal.md @@ -70,7 +70,11 @@ ## Tab -功能:增加缩进 +功能:增加缩进/移动到下一个控件 + +## Shift + Tab + +功能:移动到上一个控件 ## Ctrl/Cmd + Z diff --git a/src/editor/core/draw/control/Control.ts b/src/editor/core/draw/control/Control.ts index 78d854e..c85391e 100644 --- a/src/editor/core/draw/control/Control.ts +++ b/src/editor/core/draw/control/Control.ts @@ -12,6 +12,8 @@ import { IControlRuleOption, IGetControlValueOption, IGetControlValueResult, + IInitNextControlOption, + INextControlContext, IRepaintControlOption, ISetControlExtensionOption, ISetControlProperties, @@ -38,6 +40,7 @@ import { ControlSearch } from './interactive/ControlSearch' import { ControlBorder } from './richtext/Border' import { SelectControl } from './select/SelectControl' import { TextControl } from './text/TextControl' +import { MoveDirection } from '../../../dataset/enum/Observer' interface IMoveCursorResult { newIndex: number @@ -196,7 +199,8 @@ export class Control { public getPreY(): number { const height = this.draw.getHeight() const pageGap = this.draw.getPageGap() - return this.draw.getPageNo() * (height + pageGap) + const pageNo = this.getPosition()?.pageNo ?? this.draw.getPageNo() + return pageNo * (height + pageGap) } public getRange(): IRange { @@ -809,4 +813,263 @@ export class Control { public drawBorder(ctx: CanvasRenderingContext2D) { this.controlBorder.render(ctx) } + + public getPreControlContext(): INextControlContext | null { + if (!this.activeControl) return null + const position = this.draw.getPosition() + const positionContext = position.getPositionContext() + if (!positionContext) return null + const controlElement = this.activeControl.getElement() + // 获取上一个控件上下文本信息 + function getPreContext( + elementList: IElement[], + start: number + ): INextControlContext | null { + for (let e = start; e > 0; e--) { + const element = elementList[e] + // 表格元素 + if (element.type === ElementType.TABLE) { + const trList = element.trList || [] + for (let r = trList.length - 1; r >= 0; r--) { + const tr = trList[r] + const tdList = tr.tdList + for (let d = tdList.length - 1; d >= 0; d--) { + const td = tdList[d] + const context = getPreContext(td.value, td.value.length - 1) + if (context) { + return { + positionContext: { + isTable: true, + index: e, + trIndex: r, + tdIndex: d, + tdId: td.id, + trId: tr.id, + tableId: element.id + }, + nextIndex: context.nextIndex + } + } + } + } + } + if ( + !element.controlId || + element.controlId === controlElement.controlId + ) { + continue + } + // 找到尾部第一个非占位符元素 + let nextIndex = e + while (nextIndex > 0) { + const nextElement = elementList[nextIndex] + if ( + nextElement.controlComponent === ControlComponent.VALUE || + nextElement.controlComponent === ControlComponent.PREFIX + ) { + break + } + nextIndex-- + } + return { + positionContext: { + isTable: false + }, + nextIndex + } + } + return null + } + // 当前上下文控件信息 + const { startIndex } = this.range.getRange() + const elementList = this.getElementList() + const context = getPreContext(elementList, startIndex) + if (context) { + return { + positionContext: positionContext.isTable + ? positionContext + : context.positionContext, + nextIndex: context.nextIndex + } + } + // 控件在单元内时继续循环 + if (controlElement.tableId) { + const originalElementList = this.draw.getOriginalElementList() + const { index, trIndex, tdIndex } = positionContext + const trList = originalElementList[index!].trList! + for (let r = trIndex!; r >= 0; r--) { + const tr = trList[r] + const tdList = tr.tdList + for (let d = tdList.length - 1; d >= 0; d--) { + if (trIndex === r && d >= tdIndex!) continue + const td = tdList[d] + const context = getPreContext(td.value, td.value.length - 1) + if (context) { + return { + positionContext: { + isTable: true, + index: positionContext.index, + trIndex: r, + tdIndex: d, + tdId: td.id, + trId: tr.id, + tableId: controlElement.tableId + }, + nextIndex: context.nextIndex + } + } + } + } + // 跳出表格继续循环 + const context = getPreContext(originalElementList, index! - 1) + if (context) { + return { + positionContext: { + isTable: false + }, + nextIndex: context.nextIndex + } + } + } + return null + } + + public getNextControlContext(): INextControlContext | null { + if (!this.activeControl) return null + const position = this.draw.getPosition() + const positionContext = position.getPositionContext() + if (!positionContext) return null + const controlElement = this.activeControl.getElement() + // 获取下一个控件上下文本信息 + function getNextContext( + elementList: IElement[], + start: number + ): INextControlContext | null { + for (let e = start; e < elementList.length; e++) { + const element = elementList[e] + // 表格元素 + if (element.type === ElementType.TABLE) { + const trList = element.trList || [] + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + const tdList = tr.tdList + for (let d = 0; d < tdList.length; d++) { + const td = tdList[d] + const context = getNextContext(td.value!, 0) + if (context) { + return { + positionContext: { + isTable: true, + index: e, + trIndex: r, + tdIndex: d, + tdId: td.id, + trId: tr.id, + tableId: element.id + }, + nextIndex: context.nextIndex + } + } + } + } + } + if ( + !element.controlId || + element.controlId === controlElement.controlId + ) { + continue + } + return { + positionContext: { + isTable: false + }, + nextIndex: e + } + } + return null + } + // 当前上下文控件信息 + const { endIndex } = this.range.getRange() + const elementList = this.getElementList() + const context = getNextContext(elementList, endIndex) + if (context) { + return { + positionContext: positionContext.isTable + ? positionContext + : context.positionContext, + nextIndex: context.nextIndex + } + } + // 控件在单元内时继续循环 + if (controlElement.tableId) { + const originalElementList = this.draw.getOriginalElementList() + const { index, trIndex, tdIndex } = positionContext + const trList = originalElementList[index!].trList! + for (let r = trIndex!; r < trList.length; r++) { + const tr = trList[r] + const tdList = tr.tdList + for (let d = 0; d < tdList.length; d++) { + if (trIndex === r && d <= tdIndex!) continue + const td = tdList[d] + const context = getNextContext(td.value, 0) + if (context) { + return { + positionContext: { + isTable: true, + index: positionContext.index, + trIndex: r, + tdIndex: d, + tdId: td.id, + trId: tr.id, + tableId: controlElement.tableId + }, + nextIndex: context.nextIndex + } + } + } + } + // 跳出表格继续循环 + const context = getNextContext(originalElementList, index! + 1) + if (context) { + return { + positionContext: { + isTable: false + }, + nextIndex: context.nextIndex + } + } + } + return null + } + + public initNextControl(option: IInitNextControlOption = {}) { + const { direction = MoveDirection.DOWN } = option + let context: INextControlContext | null = null + if (direction === MoveDirection.UP) { + context = this.getPreControlContext() + } else { + context = this.getNextControlContext() + } + if (!context) return + const { nextIndex, positionContext } = context + const position = this.draw.getPosition() + // 设置上下文 + position.setPositionContext(positionContext) + this.draw.getRange().replaceRange({ + startIndex: nextIndex, + endIndex: nextIndex + }) + // 重新渲染并定位 + this.draw.render({ + curIndex: nextIndex, + isCompute: false, + isSetCursor: true, + isSubmitHistory: false + }) + const positionList = position.getPositionList() + this.draw.getCursor().moveCursorToVisible({ + cursorPosition: positionList[nextIndex], + direction + }) + } } diff --git a/src/editor/core/event/handlers/keydown/left.ts b/src/editor/core/event/handlers/keydown/left.ts index f1eec34..97eaae0 100644 --- a/src/editor/core/event/handlers/keydown/left.ts +++ b/src/editor/core/event/handlers/keydown/left.ts @@ -1,4 +1,7 @@ +import { EditorMode } from '../../../..' +import { ControlComponent } from '../../../../dataset/enum/Control' import { ElementType } from '../../../../dataset/enum/Element' +import { MoveDirection } from '../../../../dataset/enum/Observer' import { isMod } from '../../../../utils/hotkey' import { CanvasEvent } from '../../CanvasEvent' @@ -16,6 +19,18 @@ export function left(evt: KeyboardEvent, host: CanvasEvent) { const { startIndex, endIndex } = rangeManager.getRange() const isCollapsed = rangeManager.getIsCollapsed() const elementList = draw.getElementList() + // 表单模式下控件移动 + const control = draw.getControl() + if ( + draw.getMode() === EditorMode.FORM && + control.getActiveControl() && + elementList[index]?.controlComponent === ControlComponent.PREFIX + ) { + control.initNextControl({ + direction: MoveDirection.UP + }) + return + } // 单词整体移动 let moveCount = 1 if (isMod(evt)) { diff --git a/src/editor/core/event/handlers/keydown/right.ts b/src/editor/core/event/handlers/keydown/right.ts index 0517014..1452f11 100644 --- a/src/editor/core/event/handlers/keydown/right.ts +++ b/src/editor/core/event/handlers/keydown/right.ts @@ -1,4 +1,7 @@ +import { ControlComponent } from '../../../../dataset/enum/Control' +import { EditorMode } from '../../../../dataset/enum/Editor' import { ElementType } from '../../../../dataset/enum/Element' +import { MoveDirection } from '../../../../dataset/enum/Observer' import { isMod } from '../../../../utils/hotkey' import { CanvasEvent } from '../../CanvasEvent' @@ -17,6 +20,18 @@ export function right(evt: KeyboardEvent, host: CanvasEvent) { const { startIndex, endIndex } = rangeManager.getRange() const isCollapsed = rangeManager.getIsCollapsed() let elementList = draw.getElementList() + // 表单模式下控件移动 + const control = draw.getControl() + if ( + draw.getMode() === EditorMode.FORM && + control.getActiveControl() && + elementList[index + 1]?.controlComponent === ControlComponent.POSTFIX + ) { + control.initNextControl({ + direction: MoveDirection.DOWN + }) + return + } // 单词整体移动 let moveCount = 1 if (isMod(evt)) { diff --git a/src/editor/core/event/handlers/keydown/tab.ts b/src/editor/core/event/handlers/keydown/tab.ts index b1f75c6..450331d 100644 --- a/src/editor/core/event/handlers/keydown/tab.ts +++ b/src/editor/core/event/handlers/keydown/tab.ts @@ -1,4 +1,5 @@ import { ElementType } from '../../../../dataset/enum/Element' +import { MoveDirection } from '../../../../dataset/enum/Observer' import { IElement } from '../../../../interface/Element' import { formatElementContext } from '../../../../utils/element' import { CanvasEvent } from '../../CanvasEvent' @@ -7,14 +8,24 @@ export function tab(evt: KeyboardEvent, host: CanvasEvent) { const draw = host.getDraw() const isReadonly = draw.isReadonly() if (isReadonly) return - const tabElement: IElement = { - type: ElementType.TAB, - value: '' - } - const rangeManager = draw.getRange() - const { startIndex } = rangeManager.getRange() - const elementList = draw.getElementList() - formatElementContext(elementList, [tabElement], startIndex) - draw.insertElementList([tabElement]) evt.preventDefault() + // 在控件上下文时,tab键控制控件之间移动 + const control = draw.getControl() + const activeControl = control.getActiveControl() + if (activeControl) { + control.initNextControl({ + direction: evt.shiftKey ? MoveDirection.UP : MoveDirection.DOWN + }) + } else { + // 插入tab符 + const tabElement: IElement = { + type: ElementType.TAB, + value: '' + } + const rangeManager = draw.getRange() + const { startIndex } = rangeManager.getRange() + const elementList = draw.getElementList() + formatElementContext(elementList, [tabElement], startIndex) + draw.insertElementList([tabElement]) + } } diff --git a/src/editor/interface/Control.ts b/src/editor/interface/Control.ts index 2468f9c..e1126df 100644 --- a/src/editor/interface/Control.ts +++ b/src/editor/interface/Control.ts @@ -1,8 +1,10 @@ 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' @@ -151,3 +153,12 @@ export type IRepaintControlOption = Pick< IDrawOption, 'curIndex' | 'isCompute' | 'isSubmitHistory' > + +export interface INextControlContext { + positionContext: IPositionContext + nextIndex: number +} + +export interface IInitNextControlOption { + direction?: MoveDirection +}