import _ from 'lodash'; export interface FoundWord { word: string; start: number; clickedRect: DOMRect; } export class WordPosSearch { private listener?: (e: MouseEvent) => void; private lastClicked: FoundWord | null = null; constructor() { this.start(); } stop() { if (this.listener) { document.removeEventListener('contextmenu', this.listener); } delete this.listener; } start() { this.stop(); this.listener = this.generateListener(); document.addEventListener( 'contextmenu', this.listener ); } generateListener(): ((e: MouseEvent) => void) { return (e: MouseEvent): void => { try { if (!e.target) { return; } this.lastClicked = _.reduce( (e.target as any).childNodes || [], (accum: FoundWord | null, node) => (accum ? accum : this.findClickedWord(node as any, e.clientX, e.clientY)), null ); } catch (err) { console.log('wordpos.event', err); } }; } findClickedWord(parentElt: Element, x: number, y: number): FoundWord | null { if (!parentElt.textContent) { return null; } const range = document.createRange(); const words = parentElt.textContent.split(/[\s\.\<\>\,\?\!\;\:\@\$\*\(\)]/); let start = 0; let end = 0; for(let i = 0; i < words.length; i++) { const word = words[i]; end = start + word.length; try { range.setStart(parentElt, start); range.setEnd(parentElt, end); // not getBoundingClientRect as word could wrap const rects = range.getClientRects(); const clickedRect = this.isClickInRects(x, y, rects); if (clickedRect) { return { word, start, clickedRect }; } start = end + 1; } catch (e) { // console.log('MM', 'word', word, 'start', start, 'end', end, 'i', i, words); // console.error(e); return null; } } return null; } isClickInRects(x: number, y: number, rects: DOMRectList): DOMRect | null { for(let i = 0; i < rects.length; ++i) { const r = rects[i]; if ((r.left < x) && (r.right > x) && (r.top < y) && (r.bottom > y)) { return r; } } return null; } getLastClickedWord(): string | null { return this.lastClicked ? this.lastClicked.word : null; } }