解决calibre阅读器无法正确获取锚点元素的href属性问题
在创建支持交叉链接且鼠标悬停显示注释框的HTML文件 一文中提供了一个js脚本通过读取锚点元素的href目标的文本内容向伪元素传递注释内容但是那个脚本如果制作成EPUB并在Calibre阅读器中打开却会失效在Thorium阅读器中有效核心原因在于 Calibre 阅读器E-book Viewer的 JavaScript 运行环境比较特殊。它并非标准的浏览器环境而是 Calibre 用 Python 和 QtWebEngine 封装的一个沙盒。在 Calibre 的阅读器里书籍内容会被加载到一个受控的 iframe 中并且对a元素的href属性做了一个封装。打开Calibre 阅读器的开发者工具在阅读器上部鼠标右键点击可弹出选项最下面一行能够找到开发者工具通过持续展开折叠的元素可以找到a元素就会发现它的href属性被改写成了“javascript:void(0)”因此该文中通过读取href属性的值找到链接目标的代码会失败导致无法正确向伪元素传递正确的注释内容。原来的href值被装进了一个自定义属性“data-XXX”中其中XXX是一个很长的随机字符串该属性的值是一个JSON字符串其中frag键的值就是正确的href的值如下图所示搞清楚原因后可以对创建支持交叉链接且鼠标悬停显示注释框的HTML文件一文中的脚本进行改造使其兼容Calibre阅读器。改造方法是对锚点元素的属性进行遍历对名字较长的以“data-”开头的自定义属性读取其值如果其值可以成功转换成JSON并且包含键“frag”则返回“frag”键对应的值否则返回null具体方法如下function getCalibreHref(anchor) { //专门用于Calibre阅读器读取a标签的真实href // 遍历该元素的所有属性 for (let i 0; i anchor.attributes.length; i) { const attr anchor.attributes[i]; const name attr.name; const value attr.value; // 只找>function getCalibreHref(anchor) { //专门用于Calibre阅读器读取a标签的href // 遍历该元素的所有属性 for (let i 0; i anchor.attributes.length; i) { const attr anchor.attributes[i]; const name attr.name; const value attr.value; // 只找>####################20260922补充###########################其实兼容性更好、代码逻辑更清晰的方案是自己将a元素的href包装到一个自定义属性中查找注释元素的id时直接从自定义属性里找具体做法如下1、HTML注释引用定义p而三生石上颇悟前因a epub:typenoteref classnoteref href#part0004_note_22 >p classkindle-cn-kai epub:typefootnote idpart0004_note_22a href#part0004_noteBack_22[22]/a而三生石上颇悟前因唐袁郊《甘泽谣·圆观》载李源与圆观和尚 十分友好圆观依据佛家因果预知自己来生将做牧童便约请李源在他死后十二年到杭州天竺寺相见。李源依约 而往在寺前听一牧童唱道“三生石上旧精魂赏月吟风不要论。惭愧情人远相访此身虽异性常存。”李源便晓 得牧童就是圆观的托身。后人附会此事把杭州天竺寺后的山石指为“三生石”。诗文中往往也以“三生石”代指因缘 前定。三生即“三世”。佛教以过去、现在、未来即前生、今生、来生为“三生”或“三世”。前因前生因果。 因梵语意译这里指因缘。/p p classkindle-cn-kai epub:typefootnote idpart0004_note_23a href#part0004_noteBack_23[23]/a放纵之言有未可概以人废者意谓所言虽然恣意放任也有可取 之处不能一概因人废言。放纵放任不循常轨。概一概全部。/p用一个带id的块级元素供注释引用跳转内部再包含一个href指向对应注释引用的a元素实现与注释引用的交叉链接。2、CSS对注释引用及伪元素的定义.noteref { /* 使用伪元素方案时为伪元素提供定位基准 */ position: relative; display: inline-block; cursor: pointer; margin: 0; padding: 0; text-indent: 0; text-align: left; /* 模拟上标显示 */ font‑size: smaller; /*不能使用transform将注释引用显示为上标这会强制建立独立局部 层叠上下文z‑index被禁锢在这个上下文内部从而引起浏览器渲染 管线发生出现绘制碎片、下层内容穿透的情况*/ /*transform: translateY(-0.4em);*/ text-decoration: none; } /* 定义伪元素样式 */ .noteref::after { /* 读取data-note属性的值作为内容 */ content: attr(data-note); /* 相对于宿主元素定为重要勿作修改删除*/ position: absolute; top: var(--note-top); left: var(--note-left); background-color: #333; color: white; padding: 8px 12px; border-radius: 4px; font-size: 1.1em; line-height: 1.4; white-space: pre-wrap; width: var(--note-width); z-index: 1000; /* 初始完全透明 */ opacity: 0; /* 初始隐藏不占空间 */ visibility: hidden; /* 添加淡入淡出效果 */ transition: opacity 0.2s ease, visibility 0s ease; /* 防止提示框干扰鼠标事件 */ pointer-events: none; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); overflow-wrap: break-word; word-break: break-word; } /* 用CSS类名show作为伪元素显示与隐藏的开关 */ .noteref.show::after { opacity: 1; visibility: visible; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); } /* 伪元素不显示时清初相关属性*/ .noteref:not(.show)::after { content: !important; opacity: 0 !important; visibility: hidden !important; box-shadow: none !important; }3、JavaScript处理显示位置计算及向CSS传值function displayNote() { /* 方案一使用伪元素显示注释 */ const noteRefs document.querySelectorAll(.noteref); // 获取内容容器元素无专门的容器时用document.body作为容器 const contentDiv document.getElementById(content); const container contentDiv ? contentDiv : document.body; Array.from(noteRefs).forEach(noteRef { noteRef.addEventListener(mouseenter, function() { // 关闭页面上其他已经打开的注释提示避免伪元素残留 document.querySelectorAll(.noteref.show).forEach(s s.classList.remove(show)); const noteId this.getAttribute(data-href); const noteEl document.getElementById(noteId); if (!noteEl) { console.warn(注释找不到id: ${noteId}); return; } let noteText noteEl.textContent.trim(); // note note.replace(/\[\d\]/, ).trim(); // 替换掉注释内容前面的序号可选 noteText noteText.replace(/(\[\d\])[\r\n\s]/, $1 ); // 替换掉序号与注释内容之间过多的空白可选 // 向CSS传递注释文本 this.setAttribute(data-note, noteText); // 创建临时隐藏元素用来测量提示框渲染尺寸性能影响有限 const measurer document.createElement(div); // 与盒模型尺寸相关的样式须与CSS文件中的定义一致 measurer.style.cssText position:fixed; visibility:hidden; pointer-events:none; box-sizing:border-box; padding:8px 12px; border-radius:4px; font-size:1.1em; line-height:1.4; white-space:pre-wrap; max-width:20em; ; measurer.textContent noteText; document.body.appendChild(measurer); const tipRect measurer.getBoundingClientRect(); const tipW tipRect.width; const tipH tipRect.height; // 在删除measurer前读取noteref和内容容器的视口矩形避免页面布局再次变脏增加一次重排 const noterefRect this.getBoundingClientRect(); const contentRect container.getBoundingClientRect(); measurer.remove(); const gap 5; let tipViewportTop, tipViewportLeft; // 垂直规则优先放在noteref上方 if (noterefRect.top tipH gap) { tipViewportTop noterefRect.top - tipH - gap; // 上方留出间隙 } else { tipViewportTop noterefRect.bottom gap; } // 水平规则基准leftnoterefRect.left右侧溢出左移最小leftgap tipViewportLeft noterefRect.left; const rightLimit Math.min(Math.floor(contentRect.left contentRect.width), window.innerWidth) - 30; if (tipViewportLeft tipW rightLimit) { tipViewportLeft rightLimit - tipW - gap; } if (tipViewportLeft gap) { tipViewportLeft gap; } // 伪元素是absolute相对于noteref。把【视口坐标】转为【相对于noteref的偏移量】 const relTop tipViewportTop - noterefRect.top; const relLeft tipViewportLeft - noterefRect.left; // 设置CSS变量给noteref供给::after使用 this.style.setProperty(--note-top, ${relTop}px); this.style.setProperty(--note-left, ${relLeft}px); this.style.setProperty(--note-width, ${tipW}px); // 添加class开启opacity/visibility显示伪元素 this.classList.add(show); }); noteRef.addEventListener(mouseleave, function() { this.classList.remove(show); }); }); }需要说明的是伪元素方案所用到的CSS属性content只支持字符串所以如果注释中带有图片或者文字格式相关信息会全部丢失。有必要的话可以用下面的注释框显示方案显示带格式的完整注释信息HTML结构相同有必要说明的是这一方案除了Calibre阅读器表现完美Thorium等阅读器都有问题方案一兼容性更好1、注释框样式/* 方案二使用提示框显示注释可以带格式显示注释 */ .note-tip { white-space: pre-wrap; max-width: 20em; overflow-wrap: break-word; background-color: #333; color: white; padding: 8px 12px; border-radius: 4px; font-size: 1.1em; line-height: 1.4; text-indent: 0; /* 添加淡入淡出效果 */ transition: opacity 0.2s ease, visibility 0.2s ease; padding:0.618em; line-height:1.8em; font-size: 16px; } /* 注释框为黑色底纹注释中的元素在注释框中也许需要调整颜色 */ .note-tip a { color: GreenYellow; text-decoration: none; } .note-tip rt { font-size: 0.8em; color: Chartreuse; }2、JavaScript处理注释框的显示内容及位置function displayNoteTip() { /* 方案二使用DOM提示框显示注释支持富文本 */ // 容器只查询一次不要放到mouseenter内重复查询 const contentDiv document.getElementById(content); const container contentDiv ? contentDiv : document.body; const noteRefs document.querySelectorAll(.noteref); Array.from(noteRefs).forEach(noteRef { noteRef.addEventListener(mouseenter, function () { const anchorId this.getAttribute(data-href); const noteEl document.getElementById(anchorId); if (!noteEl) { console.warn(注释找不到id: ${anchorId}); return; } if (!noteEl) return; // 清除旧弹窗 document.querySelectorAll(.note-tip).forEach(oldTip{ oldTip.remove(); }); // 获取注释引用在视口内矩形 const refRect this.getBoundingClientRect(); // 转为【文档绝对坐标】加上视口滚动偏移 const docTop refRect.top window.scrollY; const docLeft refRect.left window.scrollX; // 创建弹窗 const tip document.createElement(div); tip.className note-tip; tip.innerHTML noteEl.innerHTML; tip.style.opacity 0; tip.style.visibility hidden; /* MDN 明确文档如果祖先元素存在 transform、perspective、filter、will‑change: transform * 那么后代的 position:fixed 将不再以视口为包含块改为以这个祖先盒子作为包含块。由于this(a.noteref) * 使用了transform: translateY(-0.4em);显示为上标所以tip不能再挂载到this。 * 即使没有transform在EPUB阅读器里也可能引起堆叠问题。这里直接挂载到body。 */ document.body.appendChild(tip); // requestAnimationFrame等待布局完成再读取尺寸规避拿到0宽高的时序问题 requestAnimationFrame(() { if (!tip.isConnected) return; // 防止布局未完成拿到0宽高 const tipRect tip.getBoundingClientRect(); const gap 5; const tipW tipRect.width; const tipH tipRect.height; let topPos, leftPos; // 优先放引用标记上方文档坐标系 if (refRect.top tipH gap) { // 上方空间足够tip放在ref上方 topPos docTop - tipH - gap; } else { // 上方不够放下方 topPos docTop refRect.height gap; } leftPos docLeft; // 边界限制不超出内容容器 const contentRect container.getBoundingClientRect(); const contentDocRight contentRect.left contentRect.width window.scrollX; const rightLimit contentDocRight - 30; // 如果tip右侧超出视口右边界左移 if (leftPos tipW rightLimit) { leftPos rightLimit - tipW - gap; } if (leftPos gap) leftPos gap; // 强制最小值 left gap // absolute使用文档绝对坐标不是视口 tip.style.position absolute; tip.style.top ${topPos}px; tip.style.left ${leftPos}px; tip.style.opacity 1; tip.style.visibility visible; }); }); noteRef.addEventListener(mouseleave, function () { document.querySelectorAll(.note-tip).forEach(oldTip{ oldTip.remove(); }); }); }); }据AI说如果不依赖JavaScript脚本是在head中还是在文档末尾导入都确保函数在DOM就绪后才执行的比较健壮的调用方式如下function whenDOMReady(callback) { if (document.readyState loading) { document.addEventListener(DOMContentLoaded, callback, { once: true }); } else { // interactive 或 complete 都说明 DOM 已就绪 callback(); } } whenDOMReady(displayNote);
上一篇/下一篇内容由系统自动关联
返回资讯列表 →