|
|
|
|
@ -15,15 +15,107 @@ export class Search {
|
|
|
|
|
private draw: Draw
|
|
|
|
|
private options: Required<IEditorOption>
|
|
|
|
|
private position: Position
|
|
|
|
|
private searchKeyword: string | null
|
|
|
|
|
private searchNavigateIndex: number | null
|
|
|
|
|
private searchMatchList: ISearchResult[]
|
|
|
|
|
|
|
|
|
|
constructor(draw: Draw) {
|
|
|
|
|
this.draw = draw
|
|
|
|
|
this.options = draw.getOptions()
|
|
|
|
|
this.position = draw.getPosition()
|
|
|
|
|
this.searchNavigateIndex = null
|
|
|
|
|
this.searchKeyword = null
|
|
|
|
|
this.searchMatchList = []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public getSearchKeyword(): string | null {
|
|
|
|
|
return this.searchKeyword
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public setSearchKeyword(payload: string | null) {
|
|
|
|
|
this.searchKeyword = payload
|
|
|
|
|
this.searchNavigateIndex = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public searchNavigatePre(): number | null {
|
|
|
|
|
if (!this.searchMatchList.length || !this.searchKeyword) return null
|
|
|
|
|
if (this.searchNavigateIndex === null) {
|
|
|
|
|
this.searchNavigateIndex = 0
|
|
|
|
|
} else {
|
|
|
|
|
let index = this.searchNavigateIndex - 1
|
|
|
|
|
let isExistPre = false
|
|
|
|
|
const searchNavigateId = this.searchMatchList[this.searchNavigateIndex].groupId
|
|
|
|
|
while (index >= 0) {
|
|
|
|
|
const match = this.searchMatchList[index]
|
|
|
|
|
if (searchNavigateId !== match.groupId) {
|
|
|
|
|
isExistPre = true
|
|
|
|
|
this.searchNavigateIndex = index - (this.searchKeyword.length - 1)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
index--
|
|
|
|
|
}
|
|
|
|
|
if (!isExistPre) {
|
|
|
|
|
const lastSearchMatch = this.searchMatchList[this.searchMatchList.length - 1]
|
|
|
|
|
if (lastSearchMatch.groupId === searchNavigateId) return null
|
|
|
|
|
this.searchNavigateIndex = this.searchMatchList.length - 1 - (this.searchKeyword.length - 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return this.searchNavigateIndex
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public searchNavigateNext(): number | null {
|
|
|
|
|
if (!this.searchMatchList.length || !this.searchKeyword) return null
|
|
|
|
|
if (this.searchNavigateIndex === null) {
|
|
|
|
|
this.searchNavigateIndex = 0
|
|
|
|
|
} else {
|
|
|
|
|
let index = this.searchNavigateIndex + 1
|
|
|
|
|
let isExistNext = false
|
|
|
|
|
const searchNavigateId = this.searchMatchList[this.searchNavigateIndex].groupId
|
|
|
|
|
while (index < this.searchMatchList.length) {
|
|
|
|
|
const match = this.searchMatchList[index]
|
|
|
|
|
if (searchNavigateId !== match.groupId) {
|
|
|
|
|
isExistNext = true
|
|
|
|
|
this.searchNavigateIndex = index
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
index++
|
|
|
|
|
}
|
|
|
|
|
if (!isExistNext) {
|
|
|
|
|
const firstSearchMatch = this.searchMatchList[0]
|
|
|
|
|
if (firstSearchMatch.groupId === searchNavigateId) return null
|
|
|
|
|
this.searchNavigateIndex = 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return this.searchNavigateIndex
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public searchNavigateScrollIntoView(position: IElementPosition) {
|
|
|
|
|
const { coordinate: { leftTop, leftBottom, rightTop }, pageNo } = position
|
|
|
|
|
const height = this.draw.getHeight()
|
|
|
|
|
const pageGap = this.draw.getPageGap()
|
|
|
|
|
const preY = pageNo * (height + pageGap)
|
|
|
|
|
// 创建定位锚点
|
|
|
|
|
const anchor = document.createElement('div')
|
|
|
|
|
anchor.style.position = 'absolute'
|
|
|
|
|
// 扩大搜索词尺寸,使可视范围更广
|
|
|
|
|
const ANCHOR_OVERFLOW_SIZE = 50
|
|
|
|
|
anchor.style.width = `${rightTop[0] - leftTop[0] + ANCHOR_OVERFLOW_SIZE}px`
|
|
|
|
|
anchor.style.height = `${leftBottom[1] - leftTop[1] + ANCHOR_OVERFLOW_SIZE}px`
|
|
|
|
|
anchor.style.left = `${leftTop[0]}px`
|
|
|
|
|
anchor.style.top = `${leftTop[1] + preY}px`
|
|
|
|
|
this.draw.getContainer().append(anchor)
|
|
|
|
|
// 移动到可视范围
|
|
|
|
|
anchor.scrollIntoView(false)
|
|
|
|
|
anchor.remove()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public getSearchNavigateIndexList() {
|
|
|
|
|
if (this.searchNavigateIndex === null || !this.searchKeyword) return []
|
|
|
|
|
return new Array(this.searchKeyword.length)
|
|
|
|
|
.fill(this.searchNavigateIndex)
|
|
|
|
|
.map((navigate, index) => navigate + index)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public getSearchMatchList(): ISearchResult[] {
|
|
|
|
|
return this.searchMatchList
|
|
|
|
|
}
|
|
|
|
|
@ -121,13 +213,12 @@ export class Search {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public render(ctx: CanvasRenderingContext2D, pageIndex: number) {
|
|
|
|
|
if (!this.searchMatchList || !this.searchMatchList.length) return
|
|
|
|
|
const { searchMatchAlpha, searchMatchColor } = this.options
|
|
|
|
|
if (!this.searchMatchList || !this.searchMatchList.length || !this.searchKeyword) return
|
|
|
|
|
const { searchMatchAlpha, searchMatchColor, searchNavigateMatchColor } = this.options
|
|
|
|
|
const positionList = this.position.getOriginalPositionList()
|
|
|
|
|
const elementList = this.draw.getOriginalElementList()
|
|
|
|
|
ctx.save()
|
|
|
|
|
ctx.globalAlpha = searchMatchAlpha
|
|
|
|
|
ctx.fillStyle = searchMatchColor
|
|
|
|
|
for (let s = 0; s < this.searchMatchList.length; s++) {
|
|
|
|
|
const searchMatch = this.searchMatchList[s]
|
|
|
|
|
let position: IElementPosition | null = null
|
|
|
|
|
@ -140,6 +231,14 @@ export class Search {
|
|
|
|
|
if (!position) continue
|
|
|
|
|
const { coordinate: { leftTop, leftBottom, rightTop }, pageNo } = position
|
|
|
|
|
if (pageNo !== pageIndex) continue
|
|
|
|
|
// 高亮并定位当前搜索词
|
|
|
|
|
const searchMatchIndexList = this.getSearchNavigateIndexList()
|
|
|
|
|
if (searchMatchIndexList.includes(s)) {
|
|
|
|
|
ctx.fillStyle = searchNavigateMatchColor
|
|
|
|
|
this.searchNavigateScrollIntoView(position)
|
|
|
|
|
} else {
|
|
|
|
|
ctx.fillStyle = searchMatchColor
|
|
|
|
|
}
|
|
|
|
|
const x = leftTop[0]
|
|
|
|
|
const y = leftTop[1]
|
|
|
|
|
const width = rightTop[0] - leftTop[0]
|
|
|
|
|
|