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
|
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
|
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
|
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
|
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;}
|
watermark?: IWatermark // Watermark{data:string; color?:string; opacity?:number; size?:number; font?:string;}
|
||||||
control?: IControlOption // Control {placeholderColor?:string; bracketColor?:string; prefix?:string; postfix?:string;}
|
control?: IControlOption // Control {placeholderColor?:string; bracketColor?:string; prefix?:string; postfix?:string;}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ interface IEditorOption {
|
|||||||
inactiveAlpha?: number // 正文内容失焦时透明度。默认值:0.6
|
inactiveAlpha?: number // 正文内容失焦时透明度。默认值:0.6
|
||||||
historyMaxRecordCount: number // 历史(撤销重做)最大记录次数。默认:100次
|
historyMaxRecordCount: number // 历史(撤销重做)最大记录次数。默认:100次
|
||||||
printPixelRatio: number // 打印像素比率(值越大越清晰,但尺寸越大)。默认:3
|
printPixelRatio: number // 打印像素比率(值越大越清晰,但尺寸越大)。默认:3
|
||||||
|
maskMargin: IMargin // 编辑器上的遮盖边距(如悬浮到编辑器上的菜单栏、底部工具栏)。默认:[0, 0, 0, 0]
|
||||||
wordBreak: WordBreak // 单词与标点断行:BREAK_WORD首行不出现标点&单词不拆分、BREAK_ALL按字符宽度撑满后折行。默认:BREAK_WORD
|
wordBreak: WordBreak // 单词与标点断行:BREAK_WORD首行不出现标点&单词不拆分、BREAK_ALL按字符宽度撑满后折行。默认:BREAK_WORD
|
||||||
watermark?: IWatermark // 水印信息。{data:string; color?:string; opacity?:number; size?:number; font?:string;}
|
watermark?: IWatermark // 水印信息。{data:string; color?:string; opacity?:number; size?:number; font?:string;}
|
||||||
control?: IControlOption // 控件信息。 {placeholderColor?:string; bracketColor?:string; prefix?:string; postfix?:string;}
|
control?: IControlOption // 控件信息。 {placeholderColor?:string; bracketColor?:string; prefix?:string; postfix?:string;}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { CURSOR_AGENT_HEIGHT } from '../../dataset/constant/Cursor'
|
import { CURSOR_AGENT_HEIGHT } from '../../dataset/constant/Cursor'
|
||||||
import { EDITOR_PREFIX } from '../../dataset/constant/Editor'
|
import { EDITOR_PREFIX } from '../../dataset/constant/Editor'
|
||||||
|
import { MoveDirection } from '../../dataset/enum/Observer'
|
||||||
import { DeepRequired } from '../../interface/Common'
|
import { DeepRequired } from '../../interface/Common'
|
||||||
import { ICursorOption } from '../../interface/Cursor'
|
import { ICursorOption } from '../../interface/Cursor'
|
||||||
import { IEditorOption } from '../../interface/Editor'
|
import { IEditorOption } from '../../interface/Editor'
|
||||||
|
import { IElementPosition } from '../../interface/Element'
|
||||||
|
import { findScrollContainer } from '../../utils'
|
||||||
import { Draw } from '../draw/Draw'
|
import { Draw } from '../draw/Draw'
|
||||||
import { CanvasEvent } from '../event/CanvasEvent'
|
import { CanvasEvent } from '../event/CanvasEvent'
|
||||||
import { Position } from '../position/Position'
|
import { Position } from '../position/Position'
|
||||||
@@ -15,6 +18,11 @@ export type IDrawCursorOption = ICursorOption & {
|
|||||||
hitLineStartIndex?: number
|
hitLineStartIndex?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IMoveCursorToVisibleOption {
|
||||||
|
direction: MoveDirection
|
||||||
|
cursorPosition: IElementPosition
|
||||||
|
}
|
||||||
|
|
||||||
export class Cursor {
|
export class Cursor {
|
||||||
private readonly ANIMATION_CLASS = `${EDITOR_PREFIX}-cursor--animation`
|
private readonly ANIMATION_CLASS = `${EDITOR_PREFIX}-cursor--animation`
|
||||||
|
|
||||||
@@ -153,4 +161,53 @@ export class Cursor {
|
|||||||
this.cursorDom.style.display = 'none'
|
this.cursorDom.style.display = 'none'
|
||||||
this._clearBlinkTimeout()
|
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 { ZERO } from '../../../dataset/constant/Common'
|
||||||
import { ElementType } from '../../../dataset/enum/Element'
|
import { ElementType } from '../../../dataset/enum/Element'
|
||||||
import { KeyMap } from '../../../dataset/enum/KeyMap'
|
import { KeyMap } from '../../../dataset/enum/KeyMap'
|
||||||
|
import { MoveDirection } from '../../../dataset/enum/Observer'
|
||||||
import { IElement, IElementPosition } from '../../../interface/Element'
|
import { IElement, IElementPosition } from '../../../interface/Element'
|
||||||
import { formatElementContext } from '../../../utils/element'
|
import { formatElementContext } from '../../../utils/element'
|
||||||
import { isMod } from '../../../utils/hotkey'
|
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) {
|
} else if (evt.key === KeyMap.Up || evt.key === KeyMap.Down) {
|
||||||
if (isReadonly) return
|
if (isReadonly) return
|
||||||
let anchorPosition: IElementPosition = cursorPosition
|
let anchorPosition: IElementPosition = cursorPosition
|
||||||
const isUp = evt.key === KeyMap.Up
|
// 扩大选区时,判断移动光标点
|
||||||
if (evt.shiftKey) {
|
if (evt.shiftKey) {
|
||||||
if (startIndex === cursorPosition.index) {
|
if (startIndex === cursorPosition.index) {
|
||||||
anchorPosition = positionList[endIndex]
|
anchorPosition = positionList[endIndex]
|
||||||
@@ -213,14 +214,15 @@ export function keydown(evt: KeyboardEvent, host: CanvasEvent) {
|
|||||||
rightTop: [curRightX]
|
rightTop: [curRightX]
|
||||||
}
|
}
|
||||||
} = anchorPosition
|
} = anchorPosition
|
||||||
// 向上时在首行、向下时再最尾则忽略
|
// 向上时在首行、向下时在尾行则忽略
|
||||||
|
const isUp = evt.key === KeyMap.Up
|
||||||
if (
|
if (
|
||||||
(isUp && rowIndex === 0) ||
|
(isUp && rowIndex === 0) ||
|
||||||
(!isUp && rowIndex === draw.getRowCount() - 1)
|
(!isUp && rowIndex === draw.getRowCount() - 1)
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 查找下一行信息
|
// 查找下一行位置列表
|
||||||
const probablePosition: IElementPosition[] = []
|
const probablePosition: IElementPosition[] = []
|
||||||
if (isUp) {
|
if (isUp) {
|
||||||
let p = index - 1
|
let p = index - 1
|
||||||
@@ -269,10 +271,9 @@ export function keydown(evt: KeyboardEvent, host: CanvasEvent) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if (!nextIndex) return
|
if (!nextIndex) return
|
||||||
const curIndex = nextIndex
|
|
||||||
// shift则缩放选区
|
// shift则缩放选区
|
||||||
let anchorStartIndex = curIndex
|
let anchorStartIndex = nextIndex
|
||||||
let anchorEndIndex = curIndex
|
let anchorEndIndex = nextIndex
|
||||||
if (evt.shiftKey) {
|
if (evt.shiftKey) {
|
||||||
if (startIndex !== endIndex) {
|
if (startIndex !== endIndex) {
|
||||||
if (startIndex === cursorPosition.index) {
|
if (startIndex === cursorPosition.index) {
|
||||||
@@ -300,6 +301,11 @@ export function keydown(evt: KeyboardEvent, host: CanvasEvent) {
|
|||||||
isSubmitHistory: false,
|
isSubmitHistory: false,
|
||||||
isCompute: 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) {
|
} else if (isMod(evt) && evt.key === KeyMap.Z) {
|
||||||
if (isReadonly) return
|
if (isReadonly) return
|
||||||
historyManager.undo()
|
historyManager.undo()
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ export default class Editor {
|
|||||||
historyMaxRecordCount: 100,
|
historyMaxRecordCount: 100,
|
||||||
wordBreak: WordBreak.BREAK_WORD,
|
wordBreak: WordBreak.BREAK_WORD,
|
||||||
printPixelRatio: 3,
|
printPixelRatio: 3,
|
||||||
|
maskMargin: [0, 0, 0, 0],
|
||||||
...options,
|
...options,
|
||||||
header: headerOptions,
|
header: headerOptions,
|
||||||
footer: footerOptions,
|
footer: footerOptions,
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export interface IEditorOption {
|
|||||||
inactiveAlpha?: number
|
inactiveAlpha?: number
|
||||||
historyMaxRecordCount?: number
|
historyMaxRecordCount?: number
|
||||||
printPixelRatio?: number
|
printPixelRatio?: number
|
||||||
|
maskMargin?: IMargin
|
||||||
wordBreak?: WordBreak
|
wordBreak?: WordBreak
|
||||||
header?: IHeader
|
header?: IHeader
|
||||||
footer?: IFooter
|
footer?: IFooter
|
||||||
|
|||||||
@@ -219,3 +219,19 @@ export function convertStringToBase64(input: string) {
|
|||||||
const base64 = window.btoa(charArray.join(''))
|
const base64 = window.btoa(charArray.join(''))
|
||||||
return base64
|
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: {
|
placeholder: {
|
||||||
data: '请输入正文'
|
data: '请输入正文'
|
||||||
}
|
},
|
||||||
|
maskMargin: [60, 0, 30, 0] // 菜单栏高度60,底部工具栏30为遮盖层
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user