圖片/表格:iframe 事件處理管線(xiàn)與媒體查看器觸發(fā)機(jī)制解析)
Readest 重排 EPUB 單擊打開(kāi)圖片/表格iframe 事件處理管線(xiàn)與媒體查看器觸發(fā)機(jī)制解析【免費(fèi)下載鏈接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.項(xiàng)目地址: https://gitcode.com/gh_mirrors/re/readest導(dǎo)讀本篇文章圍繞 Readest 閱讀器在重排reflowableEPUB中單擊圖片/表格直接打開(kāi)媒體查看器這一交互特性展開(kāi)它解釋了為何在單擊即翻頁(yè)的閱讀器里圖片與表格能擁有獨(dú)立的單擊語(yǔ)義并逐層剖析了從 iframe 內(nèi)部 DOM 事件、detectMediaTarget目標(biāo)判定、postSingleClick守衛(wèi)鏈、iframe-open-media消息管道到FoliateViewer消費(fèi)并驅(qū)動(dòng)ImageViewer/TableViewer的完整鏈路。讀完你將掌握 Readest 的 iframe 事件轉(zhuǎn)發(fā)模型、雙擊/長(zhǎng)按/翻頁(yè)手勢(shì)與媒體點(diǎn)擊如何仲裁以及固定版式PDF/漫畫(huà)與重排版式的行為差異。一、背景單擊即翻頁(yè)圖片與表格如何獲得第二語(yǔ)義在絕大多數(shù)閱讀器中單擊 翻頁(yè)或切換工具欄這是用戶(hù)肌肉記憶的一部分。但當(dāng)頁(yè)面里出現(xiàn)圖片、表格這類(lèi)需要放大查看的內(nèi)容時(shí)單擊翻頁(yè)就會(huì)與我想看大圖發(fā)生沖突。Readest 通過(guò) PR #4600由 [issue-4584-tap-death-investigation] 觸發(fā)的點(diǎn)擊死區(qū)調(diào)查給出了明確方案在重排 EPUB中單擊img、帶image子元素的svg、以及table現(xiàn)在會(huì)打開(kāi)ImageViewer圖片畫(huà)廊或TableViewer表格縮放而不再翻頁(yè)長(zhǎng)按仍然可以觸發(fā)相同效果在早期實(shí)現(xiàn)中與單擊并存固定版式PDF、漫畫(huà)/漫畫(huà)書(shū)以bookData.isFixedLayout為標(biāo)志中單擊保持翻頁(yè)語(yǔ)義不變——因?yàn)樵谀抢飭螕舯旧砭褪欠?yè)手勢(shì)。這份設(shè)計(jì)約束與實(shí)現(xiàn)細(xì)節(jié)記錄于倉(cāng)庫(kù)記憶文檔 tap-to-open-image-table-4600.md核心實(shí)現(xiàn)在 iframeEventHandlers.ts。二、核心判定函數(shù)detectMediaTargetiframeEventHandlers.ts中定義了統(tǒng)一的媒體目標(biāo)判定函數(shù)它同時(shí)服務(wù)于單擊與早期實(shí)現(xiàn)中的長(zhǎng)按兩條路徑避免邏輯重復(fù)type MediaTarget { elementType: image; src: string } | { elementType: table; html: string }; const detectMediaTarget (target: HTMLElement | null): MediaTarget | null { if (!target) return null; if (target.localName img) { return { elementType: image, src: (target as HTMLImageElement).src }; } const svgImage target.closest(svg)?.querySelector(image); if (svgImage) { const href svgImage.getAttribute(href) || svgImage.getAttributeNS(http://www.w3.org/1999/xlink, href); if (href) return { elementType: image, src: href }; } const table target.localName table ? target : target.closest(table); if (table) return { elementType: table, html: (table as HTMLElement).outerHTML }; return null; };判定規(guī)則可以歸納為三檔見(jiàn) iframeEventHandlers.ts目標(biāo)判定邏輯返回載荷img直接命中元素{ elementType: image, src }src為解析后的絕對(duì)地址svg內(nèi)含image向上closest(svg)后查找子級(jí)image兼容href與xlink:href兩種屬性{ elementType: image, src }table直接命中或向上closest(table)即點(diǎn)擊單元格也算{ elementType: table, html }html為整張表的outerHTML注意這是從任意子元素向上回溯的判定closest因此點(diǎn)擊表格內(nèi)的td、SVG 內(nèi)部的形狀元素都能正確命中父級(jí)媒體容器。返回的載荷恰好是ImageViewer/TableViewer消費(fèi)所需的兩種形態(tài)圖片地址與表格 HTML。三、單擊分支的守衛(wèi)鏈postSingleClick內(nèi)部順序單次單擊的真正處理在postSingleClick中完成iframeEventHandlers.ts。它不會(huì)一上來(lái)就判定媒體而是按嚴(yán)格順序通過(guò)多道守衛(wèi)防止拖拽、長(zhǎng)按、雙擊、選詞等操作與單擊媒體互相雙重觸發(fā)滑動(dòng)合成點(diǎn)擊抑制檢查consumeSuppressedDomClick把一次滑動(dòng)手勢(shì)殘留的瀏覽器合成 click 直接吞掉延遲翻頁(yè)仲裁lateLayeredClaim檢查——原生捕獲翻頁(yè)的認(rèn)領(lǐng)可能比 iframe 的 touchend/click 晚一個(gè)任務(wù)到達(dá)此時(shí)不能把 1–6px 的早認(rèn)領(lǐng)誤判為單擊腳注判定命中.js_readerFooterNote、.zhangyue-footnote、.duokan-footnote、.qqreader-footnote之一視為腳注走footnote-popup彈窗而非媒體查看媒體判定提前計(jì)算const media !isFixedLayout !footnote ? detectMediaTarget(element) : null——只有在重排版式且非腳注時(shí)才算媒體目標(biāo)鏈接守衛(wèi)element?.closest(sup, a, audio, video)命中時(shí)直接返回不翻頁(yè)不跳轉(zhuǎn)但第 4 步算出的media會(huì)被并入該守衛(wèi)!media 前綴保證被鏈接包裹的媒體能夠繞過(guò)鏈接守衛(wèi)這就是 #4757 的改動(dòng)點(diǎn)拖拽守衛(wèi)isMouseDown為真說(shuō)明鼠標(biāo)尚未抬起、正處于拖拽選擇中丟棄本次單擊對(duì)應(yīng) #4524長(zhǎng)按守衛(wèi)!longHoldTimeout時(shí)丟棄即按住不放的長(zhǎng)按預(yù)備不會(huì)提前觸發(fā)單擊早期實(shí)現(xiàn)中長(zhǎng)按隨后單獨(dú)觸發(fā)媒體查看Word Lens 選詞findGlossWord(element)命中注音詞時(shí)走wordlens-dictionary詞典查詢(xún)媒體分發(fā)if (media)時(shí)window.postMessage({ type: iframe-open-media, bookKey, ...media }, *)兜底其余情況發(fā)送iframe-single-click翻頁(yè)/工具欄切換。其中媒體判定之所以提前到鏈接守衛(wèi)之前是為了讓detectMediaTarget只執(zhí)行一次、結(jié)果在后面的分發(fā)階段復(fù)用避免二次 DOM 遍歷。雙擊去抖250ms 延遲postSingleClick并不是同步執(zhí)行的。handleClick首先檢查now - lastClickTime DOUBLE_CLICK_INTERVAL_THRESHOLD_MS若在雙擊閾值250ms常量定義于 constants.ts內(nèi)發(fā)生第二次點(diǎn)擊則走雙擊分支見(jiàn)下節(jié)。否則設(shè)置lastClickTime now并在setTimeout中延遲DOUBLE_CLICK_INTERVAL_THRESHOLD_MS毫秒后再執(zhí)行postSingleClick且執(zhí)行前還會(huì)復(fù)核Date.now() - lastClickTime DOUBLE_CLICK_INTERVAL_THRESHOLD_MS——保證第一次點(diǎn)擊不會(huì)與緊隨其后的第二次點(diǎn)擊產(chǎn)生沖突。也就是說(shuō)單擊打開(kāi)圖片/表格天然繼承了 250ms 雙擊去抖窗口快速連點(diǎn)不會(huì)反復(fù)彈出查看器。四、固定版式特例單擊翻頁(yè)、雙擊看大圖固定版式PDF/漫畫(huà)中bookData.isFixedLayout為真postSingleClick里的media恒為null因此單擊圖片仍然發(fā)送iframe-single-click用于翻頁(yè)。漫畫(huà)CBZ則有一個(gè)特例漫畫(huà)頁(yè)面是一整幅全出血圖片、沒(méi)有文本因此雙擊不會(huì)觸發(fā)選區(qū)而是被復(fù)用來(lái)打開(kāi)圖片查看器——這是唯一能放大到超過(guò)頁(yè)面適配并支持保存/分享圖片的界面const comicPage isComicBook ? detectMediaTarget(event.target as HTMLElement | null) : null; if (comicPage?.elementType image) { window.postMessage({ type: iframe-open-media, bookKey, ...comicPage }, *); return; }isComicBook與isFixedLayout兩個(gè)標(biāo)志均由FoliateViewer在綁定監(jiān)聽(tīng)器時(shí)注入見(jiàn)下文第六節(jié)handleClick的完整簽名因此是(bookKey, doubleClickDisabled, isFixedLayout, isComicBook, event)。五、消息管道iframe-long-press→iframe-open-media的更名陷阱媒體查看器運(yùn)行在主進(jìn)程/父頁(yè)面的 React 側(cè)而點(diǎn)擊事件發(fā)生在 iframe 內(nèi)部的書(shū)籍 DOM 上兩者通過(guò)window.postMessage通信。這里有一個(gè)必須注意的更名舊消息名iframe-long-press已更名為iframe-open-media它同時(shí)承載單擊與長(zhǎng)按兩種手勢(shì)的觸發(fā)消費(fèi)端的 Hook 由useLongPressEvent更名為useOpenMediaEvent內(nèi)部處理函數(shù)為handleOpenMedia長(zhǎng)按探測(cè)器addLongPressListeners/handleLongPress的名稱(chēng)被保留下來(lái)——但它現(xiàn)在只負(fù)責(zé)檢測(cè)一次真實(shí)長(zhǎng)按這一語(yǔ)義不再承擔(dān)消息命名職責(zé)。因此在源碼中檢索時(shí)應(yīng)搜索iframe-open-media而不是iframe-long-press。當(dāng)前倉(cāng)庫(kù)中該消息的發(fā)送點(diǎn)共有兩處iframeEventHandlers.ts 的漫畫(huà)雙擊分支與 L562 的重排單擊分支消費(fèi)者useOpenMediaEvent定義于 useIframeEvents.tsexport const useOpenMediaEvent ( bookKey: string, handleImagePress: (src: string) void, handleTablePress: (html: string) void, ) { const handleOpenMedia (msg: MessageEvent) { if (msg.data msg.data.bookKey bookKey msg.data.type iframe-open-media) { if (msg.data.elementType image) { handleImagePress(msg.data.src); } else if (msg.data.elementType table) { handleTablePress(msg.data.html); } } }; // ... };消息按bookKey過(guò)濾同一時(shí)間打開(kāi)的多個(gè)書(shū)籍互不串?dāng)_elementType字段直接決定分發(fā)到圖片還是表格處理器。六、FoliateViewer 的消費(fèi)側(cè)注入標(biāo)志位與打開(kāi)查看器FoliateViewer是事件鏈路的宿主組件FoliateViewer.tsx。它在docLoadHandler中把handleClick綁定到每份加載的文檔上同時(shí)注入兩個(gè)關(guān)鍵標(biāo)志detail.doc.addEventListener( click, handleClick.bind( null, bookKey, doubleClickDisabled, !!bookData?.isFixedLayout, // 固定版式 → 單擊翻頁(yè) bookData?.book?.format CBZ, // 漫畫(huà) → 雙擊開(kāi)大圖 ), );隨后通過(guò)useOpenMediaEvent(bookKey, handleImagePress, handleTablePress)訂閱消息兩個(gè)處理函數(shù)分別驅(qū)動(dòng)兩個(gè)查看器handleImagePress(src)FoliateViewer.tsx調(diào)用collectDocumentImages收集當(dāng)前文檔全部圖片定位被點(diǎn)擊圖片在畫(huà)廊中的索引記錄 alt 文本并將 blob URL 通過(guò)convertBlobUrlToDataUrl轉(zhuǎn)換為 data URL 后設(shè)置selectedImage——因此ImageViewer不僅展示單張圖還支持上一張/下一張翻看handlePreviousImage/handleNextImage還會(huì)聯(lián)動(dòng)goTo(cfi)跳轉(zhuǎn)正文位置handleTablePress(html)FoliateViewer.tsx直接把表格outerHTML存入selectedTableHtml交給TableViewer渲染縮放。兩個(gè)查看器組件分別位于 ImageViewer.tsx 與 TableViewer.tsx。七、行為矩陣與邊界情況綜合以上實(shí)現(xiàn)可以將交互行為歸納為下表場(chǎng)景版式行為單擊img/svgimage/table重排 EPUB打開(kāi)ImageViewer/TableVieweriframe-open-media單擊被a包裹的圖片重排 EPUB打開(kāi)圖片查看器不跟隨鏈接#4757單擊腳注鏈接內(nèi)圖片重排 EPUB保持腳注彈窗/跳轉(zhuǎn)不打開(kāi)查看器單擊圖片/表格固定版式PDF/CBZ翻頁(yè)iframe-single-click雙擊漫畫(huà)整頁(yè)圖片CBZ打開(kāi)圖片查看器放大/保存/分享拖拽/長(zhǎng)按/雙擊選詞途中任意被守衛(wèi)鏈攔截不誤開(kāi)查看器兩個(gè)值得一提的邊界被鏈接包裹的圖片#4757比如出版方把插圖用a href大圖包起來(lái)做點(diǎn)擊看原圖的場(chǎng)景。此前這類(lèi)元素落在sup, a, audio, video跳過(guò)守衛(wèi)里單擊會(huì)嘗試跟隨鏈接在閱讀器內(nèi)造成頁(yè)面跳轉(zhuǎn)?,F(xiàn)在media提前計(jì)算并讓鏈接守衛(wèi)帶上!media 前綴媒體目標(biāo)直接繞過(guò)鏈接守衛(wèi)進(jìn)入查看器。腳注被顯式排除!footnote條件保證腳注錨點(diǎn)內(nèi)的圖片仍走腳注彈窗/導(dǎo)航避免破壞出版物的腳注交互。八、測(cè)試佐證行為契約被固化該特性的行為契約在單元測(cè)試中被完整固化見(jiàn) iframeEventHandlers.test.ts五條核心斷言分別是重排版式單擊img→ 發(fā)送iframe-open-mediaelementType: image且不發(fā)送iframe-single-click重排版式點(diǎn)擊td表格內(nèi)部→ 發(fā)送iframe-open-mediaelementType: table載荷為table.outerHTML固定版式單擊img→ 仍然只發(fā)送iframe-single-click單擊翻頁(yè)不受影響重排版式單擊a包裹的圖片 → 打開(kāi)查看器而非跟隨鏈接#4757 回歸保護(hù)單擊duokan-footnote鏈接內(nèi)的圖片 → 不發(fā)送iframe-open-media腳注行為保留。測(cè)試通過(guò)vi.advanceTimersByTime(260)推進(jìn)虛擬時(shí)鐘越過(guò) 250ms 雙擊窗口驗(yàn)證延遲分發(fā)路徑這從側(cè)面印證了第六節(jié)描述的單擊媒體繼承雙擊去抖的實(shí)現(xiàn)細(xì)節(jié)。九、演進(jìn)線(xiàn)索從 #4600 到 #5069最后補(bǔ)充一條版本演進(jìn)脈絡(luò)幫助你理解當(dāng)前源碼與早期記憶文檔的差異#4600引入單擊打開(kāi)媒體查看器detectMediaTarget同時(shí)服務(wù)單擊與長(zhǎng)按兩條路徑消息名為iframe-open-media#4757擴(kuò)展支持被普通鏈接包裹的圖片單擊直接縮放而非跟隨鏈接#5069當(dāng)前源碼狀態(tài)長(zhǎng)按打開(kāi)查看器的能力被移除。原因記錄在測(cè)試注釋中長(zhǎng)按依賴(lài)一個(gè) 500ms 定時(shí)器只有指針移動(dòng)超過(guò) 10px 才會(huì)取消而緩慢滾動(dòng)在半秒內(nèi)的位移不足 10px導(dǎo)致定時(shí)器在滾動(dòng)中途觸發(fā)、查看器在用戶(hù)手指下方突然彈出。由于單擊#4584已經(jīng)能到達(dá)同一個(gè)查看器長(zhǎng)按不再注冊(cè)任何處理器——測(cè)試 明確斷言handlers不再暴露addLongPressListeners屬性。因此當(dāng)你閱讀倉(cāng)庫(kù)記憶文檔時(shí)需要注意文檔描述的長(zhǎng)按同樣打開(kāi)查看器是 #4600/#4757 時(shí)代的行為而當(dāng)前代碼已經(jīng)收斂為重排版式僅單擊打開(kāi)、固定版式僅單擊翻頁(yè)的簡(jiǎn)潔模型。這正是該記憶文件作為項(xiàng)目決策日志的價(jià)值所在——它記錄了每個(gè) PR 的設(shè)計(jì)動(dòng)機(jī)與改名陷阱iframe-open-media方便后續(xù)開(kāi)發(fā)者在git log與源碼之間快速對(duì)齊。【免費(fèi)下載鏈接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.項(xiàng)目地址: https://gitcode.com/gh_mirrors/re/readest創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考