feat: cursor following page scrolling #229

pr675
Hufe921 3 years ago
parent a96a77a237
commit 3db28cc04f

@ -53,6 +53,7 @@ interface IEditorOption {
inactiveAlpha?: number // When the body content is out of focus, transparency. default: 0.6
historyMaxRecordCount: number // History (undo redo) maximum number of records. default: 100
printPixelRatio: number // Print the pixel ratio (larger values are clearer, but larger sizes). default: 3
maskMargin: IMargin // Masking margins above the editorfor example: menu bar, bottom toolbar。default: [0, 0, 0, 0]
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;}

@ -53,6 +53,7 @@ interface IEditorOption {
inactiveAlpha?: number // 正文内容失焦时透明度。默认值0.6
historyMaxRecordCount: number // 历史撤销重做最大记录次数。默认100次
printPixelRatio: number // 打印像素比率值越大越清晰但尺寸越大。默认3
maskMargin: IMargin // 编辑器上的遮盖边距(如悬浮到编辑器上的菜单栏、底部工具栏)。默认:[0, 0, 0, 0]
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;}

@ -1,8 +1,11 @@
import { CURSOR_AGENT_HEIGHT } from '../../dataset/constant/Cursor'
import { EDITOR_PREFIX } from '../../dataset/constant/Editor'
import { MoveDirection } from '../../dataset/enum/Observer'
import { DeepRequired } from '../../interface/Common'
import { ICursorOption } from '../../interface/Cursor'
import { IEditorOption } from '../../interface/Editor'
import { IElementPosition } from '../../interface/Element'
import { findScrollContainer } from '../../utils'
import { Draw } from '../draw/Draw'
import { CanvasEvent } from '../event/CanvasEvent'
import { Position } from '../position/Position'
@ -15,6 +18,11 @@ export type IDrawCursorOption = ICursorOption & {
hitLineStartIndex?: number
}
export interface IMoveCursorToVisibleOption {
direction: MoveDirection
cursorPosition: IElementPosition
}
export class Cursor {
private readonly ANIMATION_CLASS = `${EDITOR_PREFIX}-cursor--animation`
@ -153,4 +161,53 @@ export class Cursor {
this.cursorDom.style.display = 'none'
this._clearBlinkTimeout()
}
public moveCursorToVisible(payload: IMoveCursorToVisibleOption) {
const { cursorPosition, direction } = payload
if (!cursorPosition || !direction) return
const {
pageNo,
coordinate: { leftTop, leftBottom }
} = cursorPosition
// 当前页面距离滚动容器顶部距离
const prePageY =
pageNo * (this.draw.getHeight() + this.draw.getPageGap()) +
this.container.getBoundingClientRect().top
// 向上移动时:以顶部距离为准,向下移动时:以底部位置为准
const isUp = direction === MoveDirection.UP
const x = leftBottom[0]
const y = isUp ? leftTop[1] + prePageY : leftBottom[1] + prePageY
// 查找滚动容器如果是滚动容器是document则限制范围为当前窗口
const scrollContainer = findScrollContainer(this.container)
const rect = {
left: 0,
right: 0,
top: 0,
bottom: 0
}
if (scrollContainer === document.documentElement) {
rect.right = window.innerWidth
rect.bottom = window.innerHeight
} else {
const { left, right, top, bottom } =
scrollContainer.getBoundingClientRect()
rect.left = left
rect.right = right
rect.top = top
rect.bottom = bottom
}
// 可视范围根据参数调整
const { maskMargin } = this.options
rect.top += maskMargin[0]
rect.bottom -= maskMargin[2]
// 不在可视范围时,移动滚动条到合适位置
if (
!(x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom)
) {
const { scrollLeft, scrollTop } = scrollContainer
isUp
? scrollContainer.scroll(scrollLeft, scrollTop - (rect.top - y))
: scrollContainer.scroll(scrollLeft, scrollTop + y - rect.bottom)
}
}
}

@ -2,6 +2,7 @@ import { EditorZone } from '../../..'
import { ZERO } from '../../../dataset/constant/Common'
import { ElementType } from '../../../dataset/enum/Element'
import { KeyMap } from '../../../dataset/enum/KeyMap'
import { MoveDirection } from '../../../dataset/enum/Observer'
import { IElement, IElementPosition } from '../../../interface/Element'
import { formatElementContext } from '../../../utils/element'
import { isMod } from '../../../utils/hotkey'
@ -196,7 +197,7 @@ export function keydown(evt: KeyboardEvent, host: CanvasEvent) {
} else if (evt.key === KeyMap.Up || evt.key === KeyMap.Down) {
if (isReadonly) return
let anchorPosition: IElementPosition = cursorPosition
const isUp = evt.key === KeyMap.Up
// 扩大选区时,判断移动光标点
if (evt.shiftKey) {
if (startIndex === cursorPosition.index) {
anchorPosition = positionList[endIndex]
@ -213,14 +214,15 @@ export function keydown(evt: KeyboardEvent, host: CanvasEvent) {
rightTop: [curRightX]
}
} = anchorPosition
// 向上时在首行、向下时再最尾则忽略
// 向上时在首行、向下时在尾行则忽略
const isUp = evt.key === KeyMap.Up
if (
(isUp && rowIndex === 0) ||
(!isUp && rowIndex === draw.getRowCount() - 1)
) {
return
}
// 查找下一行信息
// 查找下一行位置列表
const probablePosition: IElementPosition[] = []
if (isUp) {
let p = index - 1
@ -269,10 +271,9 @@ export function keydown(evt: KeyboardEvent, host: CanvasEvent) {
break
}
if (!nextIndex) return
const curIndex = nextIndex
// shift则缩放选区
let anchorStartIndex = curIndex
let anchorEndIndex = curIndex
let anchorStartIndex = nextIndex
let anchorEndIndex = nextIndex
if (evt.shiftKey) {
if (startIndex !== endIndex) {
if (startIndex === cursorPosition.index) {
@ -300,6 +301,11 @@ export function keydown(evt: KeyboardEvent, host: CanvasEvent) {
isSubmitHistory: false,
isCompute: false
})
// 将光标移动到可视范围内
draw.getCursor().moveCursorToVisible({
cursorPosition: positionList[isUp ? anchorStartIndex : anchorEndIndex],
direction: isUp ? MoveDirection.UP : MoveDirection.DOWN
})
} else if (isMod(evt) && evt.key === KeyMap.Z) {
if (isReadonly) return
historyManager.undo()

@ -148,6 +148,7 @@ export default class Editor {
historyMaxRecordCount: 100,
wordBreak: WordBreak.BREAK_WORD,
printPixelRatio: 3,
maskMargin: [0, 0, 0, 0],
...options,
header: headerOptions,
footer: footerOptions,

@ -59,6 +59,7 @@ export interface IEditorOption {
inactiveAlpha?: number
historyMaxRecordCount?: number
printPixelRatio?: number
maskMargin?: IMargin
wordBreak?: WordBreak
header?: IHeader
footer?: IFooter

@ -219,3 +219,19 @@ export function convertStringToBase64(input: string) {
const base64 = window.btoa(charArray.join(''))
return base64
}
export function findScrollContainer(element: HTMLElement) {
let parent = element.parentElement
while (parent) {
const style = window.getComputedStyle(parent)
const overflowY = style.getPropertyValue('overflow-y')
if (
parent.scrollHeight > parent.clientHeight &&
(overflowY === 'auto' || overflowY === 'scroll')
) {
return parent
}
parent = parent.parentElement
}
return document.documentElement
}

@ -430,5 +430,6 @@ export const options: IEditorOption = {
},
placeholder: {
data: '请输入正文'
}
},
maskMargin: [60, 0, 30, 0] // 菜单栏高度60底部工具栏30为遮盖层
}

Loading…
Cancel
Save