feat: cursor following page scrolling #229
This commit is contained in:
@@ -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 editor(for 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
|
||||
}
|
||||
|
||||
+2
-1
@@ -430,5 +430,6 @@ export const options: IEditorOption = {
|
||||
},
|
||||
placeholder: {
|
||||
data: '请输入正文'
|
||||
}
|
||||
},
|
||||
maskMargin: [60, 0, 30, 0] // 菜单栏高度60,底部工具栏30为遮盖层
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user