123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493 |
- // @ts-nocheck
- import copy from 'copy-to-clipboard';
- import { getAbsoluteCoordinate, getInitialPoint, getActualPoint } from './annotation/utils.js';
- import { isMobileDevice } from './ui_utils';
- const markupType = isMobileDevice ? ['highlight', 'underline', 'squiggly', 'strikeout', 'selectText'] : ['highlight', 'underline', 'squiggly', 'strikeout', '', 'redaction', 'remove']
- const markupTypeNotNull = ['highlight', 'underline', 'squiggly', 'strikeout', 'redaction', 'remove']
- type Point = {
- x: number,
- y: number
- }
- type PagePtr = {
- doc: number
- pagePtr: number
- textPtr: number
- }
- export default class TextSelection {
- _selection: string | null
- #pagePtr: number | null
- #textPtr: number | null
- messageHandler: any
- container: HTMLDivElement | null
- textContainer: HTMLDivElement | null
- selected: any
- viewport: {
- width: number
- height: number
- }
- scale: number
- eventBus: any
- _tool: string | null = null
- color: string | null = null
- selecting: boolean = false
- pageViewer: any
- pageIndex: number
- constructor(options: {
- viewport: {
- width: number,
- height: number
- },
- scale: number,
- pageIndex: number,
- container: HTMLDivElement,
- pagePtr: PagePtr,
- messageHandler: any,
- eventBus: any,
- selected: any,
- tool: string,
- color: string,
- pageViewer: any
- }) {
- this._selection = null
- this.messageHandler = options.messageHandler
- this.container = options.container || null
- this.#textPtr = options.pagePtr.textPtr
- this.#pagePtr = options.pagePtr.pagePtr
- this.selected = options.selected
- this.viewport = options.viewport
- this.scale = options.scale
- this.eventBus = options.eventBus
- this.pageIndex = options.pageIndex
- this.pageViewer = options.pageViewer
- this.handleTool = this.handleTool.bind(this)
- this.handleToolMode = this.handleToolMode.bind(this)
- this.handleMouseDown = this.handleMouseDown.bind(this)
- this.handleMouseMove = this.handleMouseMove.bind(this)
- this.handleDobuleClick = this.handleDobuleClick.bind(this)
- this.handleMouseUp = this.handleMouseUp.bind(this)
- this.handleKeyDown = this.handleKeyDown.bind(this)
- this.handleTextPopup = this.handleTextPopup.bind(this)
- this.toolMode = options.toolMode
- this.tool = options.tool
- this.color = options.color
- this.eventBus._on('toolChanged', this.handleTool)
- this.eventBus._on('toolModeChanged', this.handleToolMode)
- this.eventBus._on('textPopupClicked', this.handleTextPopup)
- }
- handleToolMode(mode: string) {
- if (mode === this.toolMode) return
- this.toolMode = mode
- document.removeEventListener('keydown', this.handleKeyDown)
- document.removeEventListener('mousedown', this.handleMouseDown)
- document.removeEventListener('touchstart', this.handleMouseDown)
- document.removeEventListener('mousemove', this.handleMouseMove)
- document.removeEventListener('touchmove', this.handleMouseMove)
- document.removeEventListener('dblclick', this.handleDobuleClick)
- if (['view', 'annotation', 'security'].includes(this.toolMode)) {
- document.addEventListener('keydown', this.handleKeyDown)
- document.addEventListener('mousedown', this.handleMouseDown)
- document.addEventListener('touchstart', this.handleMouseDown)
- document.addEventListener('mousemove', this.handleMouseMove)
- document.addEventListener('touchmove', this.handleMouseMove)
- document.addEventListener('dblclick', this.handleDobuleClick)
- }
- }
- handleTool ({
- tool,
- color
- }: {
- tool: string,
- color: string
- }) {
- this.tool = tool
- this.color = color
- if (markupType.includes(this.tool)) {
- document.body.style.userSelect = 'auto'
- document.body.style.webkitUserSelect = 'auto'
- } else {
- document.body.style.userSelect = 'none'
- document.body.style.webkitUserSelect = 'none'
- }
- }
- get tool () {
- return this._tool
- }
- set tool (toolType: string) {
- if ((toolType === this._tool && toolType !== '')) return
-
- if (!markupType.includes(toolType) || this.toolMode === 'editor') {
- document.removeEventListener('keydown', this.handleKeyDown)
- document.removeEventListener('mousedown', this.handleMouseDown)
- document.removeEventListener('touchstart', this.handleMouseDown)
- document.removeEventListener('mousemove', this.handleMouseMove)
- document.removeEventListener('touchmove', this.handleMouseMove)
- document.removeEventListener('dblclick', this.handleDobuleClick)
- this._tool = toolType
- return
- }
- if (!(markupType.includes(toolType) && markupType.includes(this.tool))) {
- document.addEventListener('keydown', this.handleKeyDown)
- document.addEventListener('mousedown', this.handleMouseDown)
- document.addEventListener('touchstart', this.handleMouseDown)
- document.addEventListener('mousemove', this.handleMouseMove)
- document.addEventListener('touchmove', this.handleMouseMove)
- document.addEventListener('dblclick', this.handleDobuleClick)
- }
- this._tool = toolType
- }
- testPoint(event: MouseEvent) {
- const { x, y, width, height } = getAbsoluteCoordinate(this.container, event)
- if (x < 0 || y < 0 || x > width || y > height) return false
- return {
- x,
- y
- }
- }
- handleKeyDown(event: KeyboardEvent) {
- if (event.key.toLocaleLowerCase() === 'c' && (event.ctrlKey || event.metaKey)) {
- const text = this._selection?.textContent
- if (text) {
- copy(text)
- }
- }
- }
- async handleMouseDown(event: MouseEvent) {
- // return的几种情况:
- // 文本悬浮菜单下,且不清空选中文本
- if (this.isTargetInElement(event.target, '.text-popup')) return
-
- this.cleanSelection()
- const tool = this.tool
- // dialog弹窗显示时
- const dialogs = document.querySelectorAll('.dialog')
- if (dialogs.length) {
- for (let i = 0; i < dialogs.length; i++) {
- if (dialogs[i].contains(event.target)) return
- }
- }
- // 选中注释层(annotationContainer)下除Markup的元素时
- if (event.target.className !== 'annotationContainer' && this.isTargetInElement(event.target, '.annotationContainer') && !this.isTargetInElement(event.target, '.markup')) return
- // 选中text注释的编辑框时
- if (this.isTargetInElement(event.target, '.text-editor-container')) return
- // 不在PDF区域内(document-container)
- if (!this.isTargetInElement(event.target, '.document-container') && !this.isTargetInElement(event.target, '.compare-document-container')) return
-
- // 添加markup时
- if (!markupType.includes(tool)) return
- // 不在页面内
- const inPage = this.testPoint(event)
- if (!inPage) return
- const { x, y } = inPage
- const point = getInitialPoint({ x, y }, this.viewport, this.scale)
- this.startPoint = point
- const isText = await this.isTextAtPoint(point)
- this.selecting = !!isText
- document.addEventListener('mouseup', this.handleMouseUp)
- document.addEventListener('touchend', this.handleMouseUp)
- }
- async handleMouseMove(event: MouseEvent) {
- const inPage = this.testPoint(event)
- if (!inPage || document.querySelector('.annotationContainer .outline-container')) {
- this.container?.classList.remove('text')
- return
- }
- const { x, y } = inPage
- const point = getInitialPoint({ x, y }, this.viewport, this.scale)
- const isText = await this.isTextAtPoint(point)
- const startPoint = this.startPoint
- if (startPoint && (startPoint.x === point.x || startPoint.y === point.y)) return
- if (isText) {
- this.container?.classList.add('text')
- this.endPoint = point
- } else {
- this.container?.classList.remove('text')
- return
- }
- if (!this.selecting) return
- const selection = await this.messageHandler.sendWithPromise('GetCharsRangeAtPos', {
- pagePtr: this.#pagePtr,
- textPtr: this.#textPtr,
- start: this.startPoint,
- end: this.endPoint
- })
- this._selection = selection
- this.updateSelection()
- }
- async handleDobuleClick(event: MouseEvent) {
- event.stopPropagation()
- event.preventDefault()
- const inPage = this.testPoint(event)
- if (!inPage || document.querySelector('.annotationContainer .outline-container')) {
- this.container?.classList.remove('text')
- return
- }
- const { x, y } = inPage
- const point = getInitialPoint({ x, y }, this.viewport, this.scale)
- const isText = await this.isTextAtPoint(point)
- if (!isText) return
- const selection = await this.messageHandler.sendWithPromise('GetSelectionForWordAtPos', {
- pagePtr: this.#pagePtr,
- textPtr: this.#textPtr,
- start: point,
- end: point
- })
- this._selection = selection
- this.updateSelection()
- if (this._selection && this.textContainer) {
- this.showTextPopup()
- }
- }
- handleMouseUp() {
- if (this.selecting && this._selection && markupTypeNotNull.includes(this.tool)) {
- const annotationData = {
- operate: 'add-annot',
- type: this.tool,
- pageIndex: this.pageIndex,
- date: new Date(),
- opacity: 0.5,
- quadPoints: this.quadPoints,
- rect: this.rect,
- color: this.color,
- contents: this._selection.textContent || undefined
- }
- if (this.tool === 'redaction' || this.tool === 'remove') {
- for (let i = 0; i < this.rects.length; i++) {
- const rect = this.rects[i]
- const redactData = {
- operate: 'add-annot',
- type: 'redact',
- pageIndex: this.pageIndex,
- date: new Date(),
- rect,
- color: this.color,
- erasure: this.tool === 'remove'
- }
- this.tool === 'redaction' && (redactData.fillColor = 'rgb(0, 0, 0)')
- this.eventBus.dispatch('annotationChange', {
- type: 'add',
- annotation: redactData
- })
- }
- } else {
- this.eventBus.dispatch('annotationChange', {
- type: 'add',
- annotation: annotationData
- })
- }
- this.cleanSelection()
- }
- this.startPoint = null
- this.endPoint = null
- this.selecting = false
- document.removeEventListener('mouseup', this.handleMouseUp)
- document.removeEventListener('touchend', this.handleMouseUp)
- if (this._selection && this.textContainer) {
- this.showTextPopup()
- }
- }
- updateSelection() {
- const textRects = this._selection.textRects
- this.textContainer?.textContent = ''
- const rects = []
- const quadPoints = []
- const topArray = []
- const bottomArray = []
- const leftArray = []
- const rightArray = []
- for (let i = 0; i < textRects.length; i++) {
- const textRect = textRects[i]
- const { Left: left, Top: top, Right: right, Bottom: bottom } = textRect
- const rect = {
- left: left * this.scale,
- top: top * this.scale,
- right: right * this.scale,
- bottom: bottom * this.scale
- }
- topArray.push(top)
- bottomArray.push(bottom)
- leftArray.push(left)
- rightArray.push(right)
- const leftTop = {
- PointX: left,
- PointY: top
- }
- const rightTop = {
- PointX: right,
- PointY: top
- }
- const leftBottom = {
- PointX: left,
- PointY: bottom
- }
- const rightBottom = {
- PointX: right,
- PointY: bottom
- }
- quadPoints.push(leftTop, rightTop, leftBottom, rightBottom)
- rects.push({ top, left, right, bottom })
- this.drawSelection(rect)
- }
- this.quadPoints = quadPoints
- this.rects = rects
- const top = Math.min(...topArray)
- const bottom = Math.max(...bottomArray)
- const left = Math.min(...leftArray)
- const right = Math.max(...rightArray)
- this.rect = {
- left,
- top,
- right,
- bottom
- }
- }
- drawSelection(rect: {
- left: number,
- top: number,
- right: number,
- bottom: number
- }) {
- if (!this.textContainer) {
- this.textContainer = document.createElement('div')
- this.textContainer.classList.add('text-container')
- this.container?.appendChild(this.textContainer)
- }
- const { left, top, right, bottom } = rect
- const selection = document.createElement('div')
- selection.classList.add('text-selection')
- selection.style.left = `${left}px`
- selection.style.top = `${top}px`
- selection.style.width = `${right - left}px`
- selection.style.height = `${bottom - top}px`
- this.textContainer?.appendChild(selection)
- }
- cleanSelection() {
- this.textContainer?.remove()
- this.textContainer = null
- this._selection = null
- this.eventBus.dispatch('showTextPopup', { show: false })
- const data = {
- selectedText: null,
- textRects: null
- }
- }
- destroy() {
- this.cleanSelection()
- document.removeEventListener('keydown', this.handleKeyDown)
- document.removeEventListener('mousedown', this.handleMouseDown)
- document.removeEventListener('touchstart', this.handleMouseDown)
- document.removeEventListener('mousemove', this.handleMouseMove)
- document.removeEventListener('touchmove', this.handleMouseMove)
- document.removeEventListener('mouseup', this.handleMouseUp)
- document.removeEventListener('touchend', this.handleMouseUp)
- document.removeEventListener('dblclick', this.handleDobuleClick)
- this.eventBus._off('toolChanged', this.handleTool)
- this.eventBus._off('toolModeChanged', this.handleToolMode)
- this.eventBus._off('textPopupClicked', this.handleTextPopup)
- }
- // 计算坐标并显示文本悬浮窗
- showTextPopup() {
- const pageRect = this.container.getBoundingClientRect()
- const rect = {
- left: this.rect.left * this.scale + pageRect.left,
- top: this.rect.top * this.scale + pageRect.top,
- right: this.rect.right * this.scale + pageRect.left,
- bottom: this.rect.bottom * this.scale + pageRect.top
- }
- this.eventBus.dispatch('showTextPopup', { show: true, rect, text: this._selection?.textContent })
- const textRects = []
- for (let i = 0; i < this._selection.textRects.length; i++) {
- const textRect = this._selection.textRects[i]
- const { Left: left, Top: top, Right: right, Bottom: bottom } = textRect
- textRects.push({ left, top, right, bottom })
- }
- const data = {
- selectedText: this._selection?.textContent,
- pageNumber: this.pageIndex + 1,
- textRects
- }
- this.eventBus.dispatch('textSelected', data)
- }
- handleTextPopup(data) {
- if (!this._selection) return
- const { tool, color } = data
- const text = this._selection?.textContent
-
- if (markupTypeNotNull.includes(tool)) {
- const annotationData = {
- operate: 'add-annot',
- type: tool,
- pageIndex: this.pageIndex,
- date: new Date(),
- opacity: 0.5,
- quadPoints: this.quadPoints,
- rect: this.rect,
- color: color || '#ff0000',
- contents: text
- }
- this.eventBus.dispatch('annotationChange', {
- type: 'add',
- annotation: annotationData
- })
- this.cleanSelection()
- this.endPoint = null
- }
- }
- isTargetInElement(target: HTMLElement, query: string): boolean {
- const el = document.querySelector(query)
- if (el && el.contains(target)) return true
- else return false
- }
- async isTextAtPoint(point: Point): boolean {
- return await this.messageHandler.sendWithPromise('GetCharIndexAtPos', {
- pagePtr: this.#pagePtr,
- textPtr: this.#textPtr,
- point
- })
- }
- }
|