import { DOUBLE_CLICK_INTERVAL_THRESHOLD_MS, LONG_HOLD_THRESHOLD } from '@/services/constants'; import { eventDispatcher } from '@/utils/event'; let lastClickTime = 0; let longHoldTimeout: ReturnType | null = null; let keyboardState = { key: '', code: '', ctrlKey: false, shiftKey: false, altKey: false, metaKey: false, }; const getKeyStatus = (event?: MouseEvent | WheelEvent | TouchEvent) => { if (event && 'ctrlKey' in event) { return { key: keyboardState.key, code: keyboardState.code, ctrlKey: event.ctrlKey, shiftKey: event.shiftKey, altKey: event.altKey, metaKey: event.metaKey, }; } return { ...keyboardState, }; }; export const handleKeydown = (bookKey: string, event: KeyboardEvent) => { keyboardState = { key: event.key, code: event.code, ctrlKey: event.ctrlKey, shiftKey: event.shiftKey, altKey: event.altKey, metaKey: event.metaKey, }; if (['Backspace'].includes(event.key)) { event.preventDefault(); } if (event.ctrlKey && event.key.toLowerCase() === 'f') { event.preventDefault(); } window.postMessage( { type: 'iframe-keydown', bookKey, key: event.key, code: event.code, ctrlKey: event.ctrlKey, shiftKey: event.shiftKey, altKey: event.altKey, metaKey: event.metaKey, }, '*', ); }; export const handleKeyup = (bookKey: string, event: KeyboardEvent) => { keyboardState = { key: '', code: '', ctrlKey: event.ctrlKey, shiftKey: event.shiftKey, altKey: event.altKey, metaKey: event.metaKey, }; window.postMessage( { type: 'iframe-keyup', bookKey, key: event.key, code: event.code, ctrlKey: event.ctrlKey, shiftKey: event.shiftKey, altKey: event.altKey, metaKey: event.metaKey, }, '*', ); }; export const handleMousedown = (bookKey: string, event: MouseEvent) => { longHoldTimeout = setTimeout(() => { longHoldTimeout = null; }, LONG_HOLD_THRESHOLD); window.postMessage( { type: 'iframe-mousedown', bookKey, button: event.button, screenX: event.screenX, screenY: event.screenY, clientX: event.clientX, clientY: event.clientY, offsetX: event.offsetX, offsetY: event.offsetY, ...getKeyStatus(event), }, '*', ); }; export const handleMouseup = (bookKey: string, event: MouseEvent) => { // we will handle mouse back and forward buttons ourselves if ([3, 4].includes(event.button)) { event.preventDefault(); } window.postMessage( { type: 'iframe-mouseup', bookKey, button: event.button, screenX: event.screenX, screenY: event.screenY, clientX: event.clientX, clientY: event.clientY, offsetX: event.offsetX, offsetY: event.offsetY, ...getKeyStatus(event), }, '*', ); }; export const handleWheel = (bookKey: string, event: WheelEvent) => { window.postMessage( { type: 'iframe-wheel', bookKey, deltaMode: event.deltaMode, deltaX: event.deltaX, deltaY: event.deltaY, deltaZ: event.deltaZ, screenX: event.screenX, screenY: event.screenY, clientX: event.clientX, clientY: event.clientY, offsetX: event.offsetX, offsetY: event.offsetY, ...getKeyStatus(event), }, '*', ); }; export const handleClick = ( bookKey: string, doubleClickDisabled: React.MutableRefObject, event: MouseEvent, ) => { const now = Date.now(); if (!doubleClickDisabled.current && now - lastClickTime < DOUBLE_CLICK_INTERVAL_THRESHOLD_MS) { lastClickTime = now; window.postMessage( { type: 'iframe-double-click', bookKey, screenX: event.screenX, screenY: event.screenY, clientX: event.clientX, clientY: event.clientY, offsetX: event.offsetX, offsetY: event.offsetY, ...getKeyStatus(event), }, '*', ); return; } lastClickTime = now; const postSingleClick = () => { const element = event.target as HTMLElement | null; if ( element?.closest('sup, a, audio, video') && !element?.closest('a.duokan-footnote:not([href])') ) { return; } const footnote = element?.closest('.js_readerFooterNote, .zhangyue-footnote, .duokan-footnote'); if (footnote) { eventDispatcher.dispatch('footnote-popup', { bookKey, element: footnote, footnote: footnote.getAttribute('data-wr-footernote') || footnote.getAttribute('zy-footnote') || footnote.getAttribute('alt') || element?.getAttribute('alt') || '', }); return; } // if long hold is detected, we don't want to send single click event if (!longHoldTimeout) { return; } window.postMessage( { type: 'iframe-single-click', bookKey, screenX: event.screenX, screenY: event.screenY, clientX: event.clientX, clientY: event.clientY, offsetX: event.offsetX, offsetY: event.offsetY, ...getKeyStatus(event), }, '*', ); }; if (!doubleClickDisabled.current) { setTimeout(() => { if (Date.now() - lastClickTime >= DOUBLE_CLICK_INTERVAL_THRESHOLD_MS) { postSingleClick(); } }, DOUBLE_CLICK_INTERVAL_THRESHOLD_MS); } else { postSingleClick(); } }; const handleTouchEv = (bookKey: string, event: TouchEvent, type: string) => { const touch = event.targetTouches[0]; const touches = []; if (touch) { touches.push({ clientX: touch.clientX, clientY: touch.clientY, screenX: touch.screenX, screenY: touch.screenY, }); } window.postMessage( { type: type, bookKey, timeStamp: Date.now(), targetTouches: touches, ...getKeyStatus(event), }, '*', ); }; export const handleTouchStart = (bookKey: string, event: TouchEvent) => { handleTouchEv(bookKey, event, 'iframe-touchstart'); }; export const handleTouchMove = (bookKey: string, event: TouchEvent) => { handleTouchEv(bookKey, event, 'iframe-touchmove'); }; export const handleTouchEnd = (bookKey: string, event: TouchEvent) => { handleTouchEv(bookKey, event, 'iframe-touchend'); };