TextSelection.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. // @ts-nocheck
  2. import copy from 'copy-to-clipboard';
  3. import { getAbsoluteCoordinate, getInitialPoint, getActualPoint } from './annotation/utils.js';
  4. import { isMobileDevice } from './ui_utils';
  5. const markupType = isMobileDevice ? ['highlight', 'underline', 'squiggly', 'strikeout', 'selectText'] : ['highlight', 'underline', 'squiggly', 'strikeout', '', 'redaction', 'remove']
  6. const markupTypeNotNull = ['highlight', 'underline', 'squiggly', 'strikeout', 'redaction', 'remove']
  7. type Point = {
  8. x: number,
  9. y: number
  10. }
  11. type PagePtr = {
  12. doc: number
  13. pagePtr: number
  14. textPtr: number
  15. }
  16. export default class TextSelection {
  17. _selection: string | null
  18. #pagePtr: number | null
  19. #textPtr: number | null
  20. messageHandler: any
  21. container: HTMLDivElement | null
  22. textContainer: HTMLDivElement | null
  23. selected: any
  24. viewport: {
  25. width: number
  26. height: number
  27. }
  28. scale: number
  29. eventBus: any
  30. _tool: string | null = null
  31. color: string | null = null
  32. selecting: boolean = false
  33. pageViewer: any
  34. pageIndex: number
  35. constructor(options: {
  36. viewport: {
  37. width: number,
  38. height: number
  39. },
  40. scale: number,
  41. pageIndex: number,
  42. container: HTMLDivElement,
  43. pagePtr: PagePtr,
  44. messageHandler: any,
  45. eventBus: any,
  46. selected: any,
  47. tool: string,
  48. color: string,
  49. pageViewer: any
  50. }) {
  51. this._selection = null
  52. this.messageHandler = options.messageHandler
  53. this.container = options.container || null
  54. this.#textPtr = options.pagePtr.textPtr
  55. this.#pagePtr = options.pagePtr.pagePtr
  56. this.selected = options.selected
  57. this.viewport = options.viewport
  58. this.scale = options.scale
  59. this.eventBus = options.eventBus
  60. this.pageIndex = options.pageIndex
  61. this.pageViewer = options.pageViewer
  62. this.handleTool = this.handleTool.bind(this)
  63. this.handleToolMode = this.handleToolMode.bind(this)
  64. this.handleMouseDown = this.handleMouseDown.bind(this)
  65. this.handleMouseMove = this.handleMouseMove.bind(this)
  66. this.handleDobuleClick = this.handleDobuleClick.bind(this)
  67. this.handleMouseUp = this.handleMouseUp.bind(this)
  68. this.handleKeyDown = this.handleKeyDown.bind(this)
  69. this.handleTextPopup = this.handleTextPopup.bind(this)
  70. this.toolMode = options.toolMode
  71. this.tool = options.tool
  72. this.color = options.color
  73. this.eventBus._on('toolChanged', this.handleTool)
  74. this.eventBus._on('toolModeChanged', this.handleToolMode)
  75. this.eventBus._on('textPopupClicked', this.handleTextPopup)
  76. }
  77. handleToolMode(mode: string) {
  78. if (mode === this.toolMode) return
  79. this.toolMode = mode
  80. document.removeEventListener('keydown', this.handleKeyDown)
  81. document.removeEventListener('mousedown', this.handleMouseDown)
  82. document.removeEventListener('touchstart', this.handleMouseDown)
  83. document.removeEventListener('mousemove', this.handleMouseMove)
  84. document.removeEventListener('touchmove', this.handleMouseMove)
  85. document.removeEventListener('dblclick', this.handleDobuleClick)
  86. if (['view', 'annotation', 'security'].includes(this.toolMode)) {
  87. document.addEventListener('keydown', this.handleKeyDown)
  88. document.addEventListener('mousedown', this.handleMouseDown)
  89. document.addEventListener('touchstart', this.handleMouseDown)
  90. document.addEventListener('mousemove', this.handleMouseMove)
  91. document.addEventListener('touchmove', this.handleMouseMove)
  92. document.addEventListener('dblclick', this.handleDobuleClick)
  93. }
  94. }
  95. handleTool ({
  96. tool,
  97. color
  98. }: {
  99. tool: string,
  100. color: string
  101. }) {
  102. this.tool = tool
  103. this.color = color
  104. if (markupType.includes(this.tool)) {
  105. document.body.style.userSelect = 'auto'
  106. document.body.style.webkitUserSelect = 'auto'
  107. } else {
  108. document.body.style.userSelect = 'none'
  109. document.body.style.webkitUserSelect = 'none'
  110. }
  111. }
  112. get tool () {
  113. return this._tool
  114. }
  115. set tool (toolType: string) {
  116. if ((toolType === this._tool && toolType !== '')) return
  117. if (!markupType.includes(toolType) || this.toolMode === 'editor') {
  118. document.removeEventListener('keydown', this.handleKeyDown)
  119. document.removeEventListener('mousedown', this.handleMouseDown)
  120. document.removeEventListener('touchstart', this.handleMouseDown)
  121. document.removeEventListener('mousemove', this.handleMouseMove)
  122. document.removeEventListener('touchmove', this.handleMouseMove)
  123. document.removeEventListener('dblclick', this.handleDobuleClick)
  124. this._tool = toolType
  125. return
  126. }
  127. if (!(markupType.includes(toolType) && markupType.includes(this.tool))) {
  128. document.addEventListener('keydown', this.handleKeyDown)
  129. document.addEventListener('mousedown', this.handleMouseDown)
  130. document.addEventListener('touchstart', this.handleMouseDown)
  131. document.addEventListener('mousemove', this.handleMouseMove)
  132. document.addEventListener('touchmove', this.handleMouseMove)
  133. document.addEventListener('dblclick', this.handleDobuleClick)
  134. }
  135. this._tool = toolType
  136. }
  137. testPoint(event: MouseEvent) {
  138. const { x, y, width, height } = getAbsoluteCoordinate(this.container, event)
  139. if (x < 0 || y < 0 || x > width || y > height) return false
  140. return {
  141. x,
  142. y
  143. }
  144. }
  145. handleKeyDown(event: KeyboardEvent) {
  146. if (event.key.toLocaleLowerCase() === 'c' && (event.ctrlKey || event.metaKey)) {
  147. const text = this._selection?.textContent
  148. if (text) {
  149. copy(text)
  150. }
  151. }
  152. }
  153. async handleMouseDown(event: MouseEvent) {
  154. // return的几种情况:
  155. // 文本悬浮菜单下,且不清空选中文本
  156. if (this.isTargetInElement(event.target, '.text-popup')) return
  157. this.cleanSelection()
  158. const tool = this.tool
  159. // dialog弹窗显示时
  160. const dialogs = document.querySelectorAll('.dialog')
  161. if (dialogs.length) {
  162. for (let i = 0; i < dialogs.length; i++) {
  163. if (dialogs[i].contains(event.target)) return
  164. }
  165. }
  166. // 选中注释层(annotationContainer)下除Markup的元素时
  167. if (event.target.className !== 'annotationContainer' && this.isTargetInElement(event.target, '.annotationContainer') && !this.isTargetInElement(event.target, '.markup')) return
  168. // 选中text注释的编辑框时
  169. if (this.isTargetInElement(event.target, '.text-editor-container')) return
  170. // 不在PDF区域内(document-container)
  171. if (!this.isTargetInElement(event.target, '.document-container') && !this.isTargetInElement(event.target, '.compare-document-container')) return
  172. // 添加markup时
  173. if (!markupType.includes(tool)) return
  174. // 不在页面内
  175. const inPage = this.testPoint(event)
  176. if (!inPage) return
  177. const { x, y } = inPage
  178. const point = getInitialPoint({ x, y }, this.viewport, this.scale)
  179. this.startPoint = point
  180. const isText = await this.isTextAtPoint(point)
  181. this.selecting = !!isText
  182. document.addEventListener('mouseup', this.handleMouseUp)
  183. document.addEventListener('touchend', this.handleMouseUp)
  184. }
  185. async handleMouseMove(event: MouseEvent) {
  186. const inPage = this.testPoint(event)
  187. if (!inPage || document.querySelector('.annotationContainer .outline-container')) {
  188. this.container?.classList.remove('text')
  189. return
  190. }
  191. const { x, y } = inPage
  192. const point = getInitialPoint({ x, y }, this.viewport, this.scale)
  193. const isText = await this.isTextAtPoint(point)
  194. const startPoint = this.startPoint
  195. if (startPoint && (startPoint.x === point.x || startPoint.y === point.y)) return
  196. if (isText) {
  197. this.container?.classList.add('text')
  198. this.endPoint = point
  199. } else {
  200. this.container?.classList.remove('text')
  201. return
  202. }
  203. if (!this.selecting) return
  204. const selection = await this.messageHandler.sendWithPromise('GetCharsRangeAtPos', {
  205. pagePtr: this.#pagePtr,
  206. textPtr: this.#textPtr,
  207. start: this.startPoint,
  208. end: this.endPoint
  209. })
  210. this._selection = selection
  211. this.updateSelection()
  212. }
  213. async handleDobuleClick(event: MouseEvent) {
  214. event.stopPropagation()
  215. event.preventDefault()
  216. const inPage = this.testPoint(event)
  217. if (!inPage || document.querySelector('.annotationContainer .outline-container')) {
  218. this.container?.classList.remove('text')
  219. return
  220. }
  221. const { x, y } = inPage
  222. const point = getInitialPoint({ x, y }, this.viewport, this.scale)
  223. const isText = await this.isTextAtPoint(point)
  224. if (!isText) return
  225. const selection = await this.messageHandler.sendWithPromise('GetSelectionForWordAtPos', {
  226. pagePtr: this.#pagePtr,
  227. textPtr: this.#textPtr,
  228. start: point,
  229. end: point
  230. })
  231. this._selection = selection
  232. this.updateSelection()
  233. if (this._selection && this.textContainer) {
  234. this.showTextPopup()
  235. }
  236. }
  237. handleMouseUp() {
  238. if (this.selecting && this._selection && markupTypeNotNull.includes(this.tool)) {
  239. const annotationData = {
  240. operate: 'add-annot',
  241. type: this.tool,
  242. pageIndex: this.pageIndex,
  243. date: new Date(),
  244. opacity: 0.5,
  245. quadPoints: this.quadPoints,
  246. rect: this.rect,
  247. color: this.color,
  248. contents: this._selection.textContent || undefined
  249. }
  250. if (this.tool === 'redaction' || this.tool === 'remove') {
  251. for (let i = 0; i < this.rects.length; i++) {
  252. const rect = this.rects[i]
  253. const redactData = {
  254. operate: 'add-annot',
  255. type: 'redact',
  256. pageIndex: this.pageIndex,
  257. date: new Date(),
  258. rect,
  259. color: this.color,
  260. erasure: this.tool === 'remove'
  261. }
  262. this.tool === 'redaction' && (redactData.fillColor = 'rgb(0, 0, 0)')
  263. this.eventBus.dispatch('annotationChange', {
  264. type: 'add',
  265. annotation: redactData
  266. })
  267. }
  268. } else {
  269. this.eventBus.dispatch('annotationChange', {
  270. type: 'add',
  271. annotation: annotationData
  272. })
  273. }
  274. this.cleanSelection()
  275. }
  276. this.startPoint = null
  277. this.endPoint = null
  278. this.selecting = false
  279. document.removeEventListener('mouseup', this.handleMouseUp)
  280. document.removeEventListener('touchend', this.handleMouseUp)
  281. if (this._selection && this.textContainer) {
  282. this.showTextPopup()
  283. }
  284. }
  285. updateSelection() {
  286. const textRects = this._selection.textRects
  287. this.textContainer?.textContent = ''
  288. const rects = []
  289. const quadPoints = []
  290. const topArray = []
  291. const bottomArray = []
  292. const leftArray = []
  293. const rightArray = []
  294. for (let i = 0; i < textRects.length; i++) {
  295. const textRect = textRects[i]
  296. const { Left: left, Top: top, Right: right, Bottom: bottom } = textRect
  297. const rect = {
  298. left: left * this.scale,
  299. top: top * this.scale,
  300. right: right * this.scale,
  301. bottom: bottom * this.scale
  302. }
  303. topArray.push(top)
  304. bottomArray.push(bottom)
  305. leftArray.push(left)
  306. rightArray.push(right)
  307. const leftTop = {
  308. PointX: left,
  309. PointY: top
  310. }
  311. const rightTop = {
  312. PointX: right,
  313. PointY: top
  314. }
  315. const leftBottom = {
  316. PointX: left,
  317. PointY: bottom
  318. }
  319. const rightBottom = {
  320. PointX: right,
  321. PointY: bottom
  322. }
  323. quadPoints.push(leftTop, rightTop, leftBottom, rightBottom)
  324. rects.push({ top, left, right, bottom })
  325. this.drawSelection(rect)
  326. }
  327. this.quadPoints = quadPoints
  328. this.rects = rects
  329. const top = Math.min(...topArray)
  330. const bottom = Math.max(...bottomArray)
  331. const left = Math.min(...leftArray)
  332. const right = Math.max(...rightArray)
  333. this.rect = {
  334. left,
  335. top,
  336. right,
  337. bottom
  338. }
  339. }
  340. drawSelection(rect: {
  341. left: number,
  342. top: number,
  343. right: number,
  344. bottom: number
  345. }) {
  346. if (!this.textContainer) {
  347. this.textContainer = document.createElement('div')
  348. this.textContainer.classList.add('text-container')
  349. this.container?.appendChild(this.textContainer)
  350. }
  351. const { left, top, right, bottom } = rect
  352. const selection = document.createElement('div')
  353. selection.classList.add('text-selection')
  354. selection.style.left = `${left}px`
  355. selection.style.top = `${top}px`
  356. selection.style.width = `${right - left}px`
  357. selection.style.height = `${bottom - top}px`
  358. this.textContainer?.appendChild(selection)
  359. }
  360. cleanSelection() {
  361. this.textContainer?.remove()
  362. this.textContainer = null
  363. this._selection = null
  364. this.eventBus.dispatch('showTextPopup', { show: false })
  365. const data = {
  366. selectedText: null,
  367. textRects: null
  368. }
  369. }
  370. destroy() {
  371. this.cleanSelection()
  372. document.removeEventListener('keydown', this.handleKeyDown)
  373. document.removeEventListener('mousedown', this.handleMouseDown)
  374. document.removeEventListener('touchstart', this.handleMouseDown)
  375. document.removeEventListener('mousemove', this.handleMouseMove)
  376. document.removeEventListener('touchmove', this.handleMouseMove)
  377. document.removeEventListener('mouseup', this.handleMouseUp)
  378. document.removeEventListener('touchend', this.handleMouseUp)
  379. document.removeEventListener('dblclick', this.handleDobuleClick)
  380. this.eventBus._off('toolChanged', this.handleTool)
  381. this.eventBus._off('toolModeChanged', this.handleToolMode)
  382. this.eventBus._off('textPopupClicked', this.handleTextPopup)
  383. }
  384. // 计算坐标并显示文本悬浮窗
  385. showTextPopup() {
  386. const pageRect = this.container.getBoundingClientRect()
  387. const rect = {
  388. left: this.rect.left * this.scale + pageRect.left,
  389. top: this.rect.top * this.scale + pageRect.top,
  390. right: this.rect.right * this.scale + pageRect.left,
  391. bottom: this.rect.bottom * this.scale + pageRect.top
  392. }
  393. this.eventBus.dispatch('showTextPopup', { show: true, rect, text: this._selection?.textContent })
  394. const textRects = []
  395. for (let i = 0; i < this._selection.textRects.length; i++) {
  396. const textRect = this._selection.textRects[i]
  397. const { Left: left, Top: top, Right: right, Bottom: bottom } = textRect
  398. textRects.push({ left, top, right, bottom })
  399. }
  400. const data = {
  401. selectedText: this._selection?.textContent,
  402. pageNumber: this.pageIndex + 1,
  403. textRects
  404. }
  405. this.eventBus.dispatch('textSelected', data)
  406. }
  407. handleTextPopup(data) {
  408. if (!this._selection) return
  409. const { tool, color } = data
  410. const text = this._selection?.textContent
  411. if (markupTypeNotNull.includes(tool)) {
  412. const annotationData = {
  413. operate: 'add-annot',
  414. type: tool,
  415. pageIndex: this.pageIndex,
  416. date: new Date(),
  417. opacity: 0.5,
  418. quadPoints: this.quadPoints,
  419. rect: this.rect,
  420. color: color || '#ff0000',
  421. contents: text
  422. }
  423. this.eventBus.dispatch('annotationChange', {
  424. type: 'add',
  425. annotation: annotationData
  426. })
  427. this.cleanSelection()
  428. this.endPoint = null
  429. }
  430. }
  431. isTargetInElement(target: HTMLElement, query: string): boolean {
  432. const el = document.querySelector(query)
  433. if (el && el.contains(target)) return true
  434. else return false
  435. }
  436. async isTextAtPoint(point: Point): boolean {
  437. return await this.messageHandler.sendWithPromise('GetCharIndexAtPos', {
  438. pagePtr: this.#pagePtr,
  439. textPtr: this.#textPtr,
  440. point
  441. })
  442. }
  443. }