Merge pull request #73 from Hufe921/feature/search-navigate
Feature/search navigate
This commit is contained in:
@@ -194,6 +194,12 @@
|
|||||||
<div class="menu-item__search__collapse" data-menu="search">
|
<div class="menu-item__search__collapse" data-menu="search">
|
||||||
<div class="menu-item__search__collapse__search">
|
<div class="menu-item__search__collapse__search">
|
||||||
<input type="text" />
|
<input type="text" />
|
||||||
|
<div class="arrow-left">
|
||||||
|
<i></i>
|
||||||
|
</div>
|
||||||
|
<div class="arrow-right">
|
||||||
|
<i></i>
|
||||||
|
</div>
|
||||||
<span>×</span>
|
<span>×</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-item__search__collapse__replace">
|
<div class="menu-item__search__collapse__replace">
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg width="4" height="7" xmlns="http://www.w3.org/2000/svg"><path fill="#6F6F6F" d="M0 3.5L4 0v7z" fill-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 127 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="4" height="7" xmlns="http://www.w3.org/2000/svg"><path fill="#6F6F6F" d="M4 3.5L0 0v7z" fill-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 127 B |
@@ -56,6 +56,8 @@ export class Command {
|
|||||||
private static addWatermark: CommandAdapt['addWatermark']
|
private static addWatermark: CommandAdapt['addWatermark']
|
||||||
private static deleteWatermark: CommandAdapt['deleteWatermark']
|
private static deleteWatermark: CommandAdapt['deleteWatermark']
|
||||||
private static search: CommandAdapt['search']
|
private static search: CommandAdapt['search']
|
||||||
|
private static searchNavigatePre: CommandAdapt['searchNavigatePre']
|
||||||
|
private static searchNavigateNext: CommandAdapt['searchNavigateNext']
|
||||||
private static replace: CommandAdapt['replace']
|
private static replace: CommandAdapt['replace']
|
||||||
private static print: CommandAdapt['print']
|
private static print: CommandAdapt['print']
|
||||||
private static replaceImageElement: CommandAdapt['replaceImageElement']
|
private static replaceImageElement: CommandAdapt['replaceImageElement']
|
||||||
@@ -122,6 +124,8 @@ export class Command {
|
|||||||
Command.addWatermark = adapt.addWatermark.bind(adapt)
|
Command.addWatermark = adapt.addWatermark.bind(adapt)
|
||||||
Command.deleteWatermark = adapt.deleteWatermark.bind(adapt)
|
Command.deleteWatermark = adapt.deleteWatermark.bind(adapt)
|
||||||
Command.search = adapt.search.bind(adapt)
|
Command.search = adapt.search.bind(adapt)
|
||||||
|
Command.searchNavigatePre = adapt.searchNavigatePre.bind(adapt)
|
||||||
|
Command.searchNavigateNext = adapt.searchNavigateNext.bind(adapt)
|
||||||
Command.replace = adapt.replace.bind(adapt)
|
Command.replace = adapt.replace.bind(adapt)
|
||||||
Command.print = adapt.print.bind(adapt)
|
Command.print = adapt.print.bind(adapt)
|
||||||
Command.replaceImageElement = adapt.replaceImageElement.bind(adapt)
|
Command.replaceImageElement = adapt.replaceImageElement.bind(adapt)
|
||||||
@@ -333,6 +337,14 @@ export class Command {
|
|||||||
return Command.search(payload)
|
return Command.search(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public executeSearchNavigatePre() {
|
||||||
|
return Command.searchNavigatePre()
|
||||||
|
}
|
||||||
|
|
||||||
|
public executeSearchNavigateNext() {
|
||||||
|
return Command.searchNavigateNext()
|
||||||
|
}
|
||||||
|
|
||||||
public executeReplace(payload: string) {
|
public executeReplace(payload: string) {
|
||||||
return Command.replace(payload)
|
return Command.replace(payload)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { formatElementList } from '../../utils/element'
|
|||||||
import { printImageBase64 } from '../../utils/print'
|
import { printImageBase64 } from '../../utils/print'
|
||||||
import { Control } from '../draw/control/Control'
|
import { Control } from '../draw/control/Control'
|
||||||
import { Draw } from '../draw/Draw'
|
import { Draw } from '../draw/Draw'
|
||||||
|
import { Search } from '../draw/interactive/Search'
|
||||||
import { TableTool } from '../draw/particle/table/TableTool'
|
import { TableTool } from '../draw/particle/table/TableTool'
|
||||||
import { CanvasEvent } from '../event/CanvasEvent'
|
import { CanvasEvent } from '../event/CanvasEvent'
|
||||||
import { HistoryManager } from '../history/HistoryManager'
|
import { HistoryManager } from '../history/HistoryManager'
|
||||||
@@ -40,6 +41,7 @@ export class CommandAdapt {
|
|||||||
private options: Required<IEditorOption>
|
private options: Required<IEditorOption>
|
||||||
private control: Control
|
private control: Control
|
||||||
private workerManager: WorkerManager
|
private workerManager: WorkerManager
|
||||||
|
private searchManager: Search
|
||||||
|
|
||||||
constructor(draw: Draw) {
|
constructor(draw: Draw) {
|
||||||
this.draw = draw
|
this.draw = draw
|
||||||
@@ -51,6 +53,7 @@ export class CommandAdapt {
|
|||||||
this.options = draw.getOptions()
|
this.options = draw.getOptions()
|
||||||
this.control = draw.getControl()
|
this.control = draw.getControl()
|
||||||
this.workerManager = draw.getWorkerManager()
|
this.workerManager = draw.getWorkerManager()
|
||||||
|
this.searchManager = draw.getSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
public mode(payload: EditorMode) {
|
public mode(payload: EditorMode) {
|
||||||
@@ -1211,7 +1214,25 @@ export class CommandAdapt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public search(payload: string | null) {
|
public search(payload: string | null) {
|
||||||
this.draw.setSearchKeyword(payload)
|
this.searchManager.setSearchKeyword(payload)
|
||||||
|
this.draw.render({
|
||||||
|
isSetCursor: false,
|
||||||
|
isSubmitHistory: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public searchNavigatePre() {
|
||||||
|
const index = this.searchManager.searchNavigatePre()
|
||||||
|
if (index === null) return
|
||||||
|
this.draw.render({
|
||||||
|
isSetCursor: false,
|
||||||
|
isSubmitHistory: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public searchNavigateNext() {
|
||||||
|
const index = this.searchManager.searchNavigateNext()
|
||||||
|
if (index === null) return
|
||||||
this.draw.render({
|
this.draw.render({
|
||||||
isSetCursor: false,
|
isSetCursor: false,
|
||||||
isSubmitHistory: false
|
isSubmitHistory: false
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ export class Draw {
|
|||||||
private rowList: IRow[]
|
private rowList: IRow[]
|
||||||
private painterStyle: IElementStyle | null
|
private painterStyle: IElementStyle | null
|
||||||
private painterOptions: IPainterOptions | null
|
private painterOptions: IPainterOptions | null
|
||||||
private searchKeyword: string | null
|
|
||||||
private visiblePageNoList: number[]
|
private visiblePageNoList: number[]
|
||||||
private intersectionPageNo: number
|
private intersectionPageNo: number
|
||||||
|
|
||||||
@@ -155,7 +154,6 @@ export class Draw {
|
|||||||
this.rowList = []
|
this.rowList = []
|
||||||
this.painterStyle = null
|
this.painterStyle = null
|
||||||
this.painterOptions = null
|
this.painterOptions = null
|
||||||
this.searchKeyword = null
|
|
||||||
this.visiblePageNoList = []
|
this.visiblePageNoList = []
|
||||||
this.intersectionPageNo = 0
|
this.intersectionPageNo = 0
|
||||||
|
|
||||||
@@ -417,14 +415,6 @@ export class Draw {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSearchKeyword(): string | null {
|
|
||||||
return this.searchKeyword
|
|
||||||
}
|
|
||||||
|
|
||||||
public setSearchKeyword(payload: string | null) {
|
|
||||||
this.searchKeyword = payload
|
|
||||||
}
|
|
||||||
|
|
||||||
public setDefaultRange() {
|
public setDefaultRange() {
|
||||||
if (!this.elementList.length) return
|
if (!this.elementList.length) return
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -995,7 +985,7 @@ export class Draw {
|
|||||||
// 绘制页码
|
// 绘制页码
|
||||||
this.pageNumber.render(ctx, pageNo)
|
this.pageNumber.render(ctx, pageNo)
|
||||||
// 搜索匹配绘制
|
// 搜索匹配绘制
|
||||||
if (this.searchKeyword) {
|
if (this.search.getSearchKeyword()) {
|
||||||
this.search.render(ctx, pageNo)
|
this.search.render(ctx, pageNo)
|
||||||
}
|
}
|
||||||
// 绘制水印
|
// 绘制水印
|
||||||
@@ -1017,8 +1007,9 @@ export class Draw {
|
|||||||
// 计算行信息
|
// 计算行信息
|
||||||
if (isComputeRowList) {
|
if (isComputeRowList) {
|
||||||
this.rowList = this._computeRowList(innerWidth, this.elementList)
|
this.rowList = this._computeRowList(innerWidth, this.elementList)
|
||||||
if (this.searchKeyword) {
|
const searchKeyword = this.search.getSearchKeyword()
|
||||||
this.search.compute(this.searchKeyword)
|
if (searchKeyword) {
|
||||||
|
this.search.compute(searchKeyword)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 清除光标等副作用
|
// 清除光标等副作用
|
||||||
|
|||||||
@@ -15,15 +15,107 @@ export class Search {
|
|||||||
private draw: Draw
|
private draw: Draw
|
||||||
private options: Required<IEditorOption>
|
private options: Required<IEditorOption>
|
||||||
private position: Position
|
private position: Position
|
||||||
|
private searchKeyword: string | null
|
||||||
|
private searchNavigateIndex: number | null
|
||||||
private searchMatchList: ISearchResult[]
|
private searchMatchList: ISearchResult[]
|
||||||
|
|
||||||
constructor(draw: Draw) {
|
constructor(draw: Draw) {
|
||||||
this.draw = draw
|
this.draw = draw
|
||||||
this.options = draw.getOptions()
|
this.options = draw.getOptions()
|
||||||
this.position = draw.getPosition()
|
this.position = draw.getPosition()
|
||||||
|
this.searchNavigateIndex = null
|
||||||
|
this.searchKeyword = null
|
||||||
this.searchMatchList = []
|
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[] {
|
public getSearchMatchList(): ISearchResult[] {
|
||||||
return this.searchMatchList
|
return this.searchMatchList
|
||||||
}
|
}
|
||||||
@@ -121,13 +213,12 @@ export class Search {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render(ctx: CanvasRenderingContext2D, pageIndex: number) {
|
public render(ctx: CanvasRenderingContext2D, pageIndex: number) {
|
||||||
if (!this.searchMatchList || !this.searchMatchList.length) return
|
if (!this.searchMatchList || !this.searchMatchList.length || !this.searchKeyword) return
|
||||||
const { searchMatchAlpha, searchMatchColor } = this.options
|
const { searchMatchAlpha, searchMatchColor, searchNavigateMatchColor } = this.options
|
||||||
const positionList = this.position.getOriginalPositionList()
|
const positionList = this.position.getOriginalPositionList()
|
||||||
const elementList = this.draw.getOriginalElementList()
|
const elementList = this.draw.getOriginalElementList()
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.globalAlpha = searchMatchAlpha
|
ctx.globalAlpha = searchMatchAlpha
|
||||||
ctx.fillStyle = searchMatchColor
|
|
||||||
for (let s = 0; s < this.searchMatchList.length; s++) {
|
for (let s = 0; s < this.searchMatchList.length; s++) {
|
||||||
const searchMatch = this.searchMatchList[s]
|
const searchMatch = this.searchMatchList[s]
|
||||||
let position: IElementPosition | null = null
|
let position: IElementPosition | null = null
|
||||||
@@ -140,6 +231,14 @@ export class Search {
|
|||||||
if (!position) continue
|
if (!position) continue
|
||||||
const { coordinate: { leftTop, leftBottom, rightTop }, pageNo } = position
|
const { coordinate: { leftTop, leftBottom, rightTop }, pageNo } = position
|
||||||
if (pageNo !== pageIndex) continue
|
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 x = leftTop[0]
|
||||||
const y = leftTop[1]
|
const y = leftTop[1]
|
||||||
const width = rightTop[0] - leftTop[0]
|
const width = rightTop[0] - leftTop[0]
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export default class Editor {
|
|||||||
rangeMinWidth: 5,
|
rangeMinWidth: 5,
|
||||||
searchMatchAlpha: 0.6,
|
searchMatchAlpha: 0.6,
|
||||||
searchMatchColor: '#FFFF00',
|
searchMatchColor: '#FFFF00',
|
||||||
|
searchNavigateMatchColor: '#AAD280',
|
||||||
highlightAlpha: 0.6,
|
highlightAlpha: 0.6,
|
||||||
resizerColor: '#4182D9',
|
resizerColor: '#4182D9',
|
||||||
resizerSize: 5,
|
resizerSize: 5,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface IEditorOption {
|
|||||||
rangeAlpha?: number;
|
rangeAlpha?: number;
|
||||||
rangeMinWidth?: number;
|
rangeMinWidth?: number;
|
||||||
searchMatchColor?: string;
|
searchMatchColor?: string;
|
||||||
|
searchNavigateMatchColor?: string;
|
||||||
searchMatchAlpha?: number;
|
searchMatchAlpha?: number;
|
||||||
highlightAlpha?: number;
|
highlightAlpha?: number;
|
||||||
resizerColor?: string;
|
resizerColor?: string;
|
||||||
|
|||||||
@@ -645,6 +645,12 @@ window.onload = function () {
|
|||||||
instance.command.executeReplace(replaceValue)
|
instance.command.executeReplace(replaceValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
searchCollapseDom.querySelector<HTMLDivElement>('.arrow-left')!.onclick = function () {
|
||||||
|
instance.command.executeSearchNavigatePre()
|
||||||
|
}
|
||||||
|
searchCollapseDom.querySelector<HTMLDivElement>('.arrow-right')!.onclick = function () {
|
||||||
|
instance.command.executeSearchNavigateNext()
|
||||||
|
}
|
||||||
|
|
||||||
document.querySelector<HTMLDivElement>('.menu-item__print')!.onclick = function () {
|
document.querySelector<HTMLDivElement>('.menu-item__print')!.onclick = function () {
|
||||||
console.log('print')
|
console.log('print')
|
||||||
|
|||||||
@@ -540,6 +540,52 @@ ul {
|
|||||||
background: rgba(25, 55, 88, .04);
|
background: rgba(25, 55, 88, .04);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-item .menu-item__search__collapse__search {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item .menu-item__search__collapse__search>input {
|
||||||
|
width: 181px;
|
||||||
|
padding: 5px 60px 5px 5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item .menu-item__search__collapse__search>div {
|
||||||
|
width: 28px;
|
||||||
|
height: 27px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: absolute;
|
||||||
|
border-left: 1px solid #e2e6ed;
|
||||||
|
transition: all .5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item .menu-item__search__collapse__search>div:hover {
|
||||||
|
background-color: rgba(25, 55, 88, .04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item .menu-item__search__collapse__search i {
|
||||||
|
width: 6px;
|
||||||
|
height: 8px;
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item .menu-item__search__collapse__search .arrow-left {
|
||||||
|
right: 76px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item .menu-item__search__collapse__search .arrow-left i {
|
||||||
|
background: url(./assets/images/arrow-left.svg) no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item .menu-item__search__collapse__search .arrow-right {
|
||||||
|
right: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item .menu-item__search__collapse__search .arrow-right i {
|
||||||
|
background: url(./assets/images/arrow-right.svg) no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
.menu-item__print i {
|
.menu-item__print i {
|
||||||
background-image: url('./assets/images/print.svg');
|
background-image: url('./assets/images/print.svg');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user