碎碎念
前几天在twikoo的交流群中,有人提到了这样一个问题:twikoo可以实现段落评论吗?我想了一下,下载了个番茄小说发现,他们都是按照每一行的内容分别进行评论的,Hexo可以实现类似于每一段落一个Url,也就是#[段落名]的格式,但是Twikoo并不能将这些段落分开,而且本来评论就很少了,(这么一分就都看不见了)。所以我想是否可以利用我的说说页面中的,点击评论按钮后后会在评论区添加一个:> + “文本”,从而实现类似引用的功能,那么也就实现了仿段落评论,同时所有的评论都会在评论区显示,避免了因为都在段落评论而导致主评论区没人的尴尬局面。
内容简述
- 实现亮暗模式适配
- 实现高分辨率适配,设置上下阈值,基本确保不会超出屏幕
- 动画效果适配
- 自动将节选段落放置在评论框中
- 解决文本中含有回车导致函数失效的问题
- 解决好友imsyy提出的弹窗中再次点击打开弹窗会导致无法关闭的问题:点击跳转
- 解决好友imsyy提出的弹窗中点击刷新按钮会退出的问题:点击跳转
实现功能
添加按钮
要实现回复功能,首先需要有回复按钮呀,我们先考虑一下逻辑,什么情况需要回复按钮?经过设计,我决定将按钮添加在右键菜单中,并且是文章页,且需要选中文字右键才有效果(因为你不选中文字回复什么段落),我们先添加按钮,如果没有进行魔改右键菜单的请按照别人的教程进行魔改,可以参考下面这些链接(以下方法不分先后,都可以使用)
以上均可以实现右键菜单的魔改,推荐百里飞洋的文章,因为有复制功能,本教程也是基于他的进行修改,这里不再详细讲解,我们直接进入添加段落回复功能。
在"[root]\themes\butterfly\layout\includes\rightmenu.pug"
文件中修改复制按钮部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| #rightMenu .rightMenu-group.rightMenu-small .rightMenu-item#menu-backward i.fa-solid.fa-arrow-left .rightMenu-item#menu-forward i.fa-solid.fa-arrow-right .rightMenu-item#menu-refresh i.fa-solid.fa-arrow-rotate-right .rightMenu-item#menu-home i.fa-solid.fa-house .rightMenu-group.rightMenu-line.hide#menu-text + a.rightMenu-item#copy(href="javascript:rm.copySelect();") - a.rightMenu-item(href="javascript:rm.copySelect();") i.fa-solid.fa-copy span='复制选中文字' + a.rightMenu-item#reply(href="javascript:rm.replySelect();") + i.fa-regular.fa-comment + span='评论选中段落' .rightMenu-group.rightMenu-line.rightMenuOther a.rightMenu-item.menu-link(href='/archives/') i.fa-solid.fa-archive span='文章时间线' a.rightMenu-item.menu-link(href='/categories/') i.fa-solid.fa-folder-open span='文章分大类' a.rightMenu-item.menu-link(href='/tags/') i.fa-solid.fa-tags span='文章小标签' .rightMenu-group.rightMenu-line.rightMenuNormal a.rightMenu-item.menu-link#menu-radompage(href='/comment/') i.fa-solid.fa-shoe-prints span='随心留言板' .rightMenu-item#menu-translate i.fa-solid.fa-earth-asia span='繁简模式切换' .rightMenu-item#menu-darkmode i.fa-solid.fa-moon span='切换亮暗模式' .rightMenu-item#menu-live2dvisibility i.fa-solid.fa-cat span='小猫显示隐藏' .rightMenu-item#menu-print i.fa-solid.fa-print.fa-fw span='打印整个页面' a.rightMenu-item.menu-link#statement(href='/statement/') i.fa-regular.fa-copyright.fa-fw span='网站声明' #rightmenu-mask
|
如上代码所示,为了更好的自定义每个元素,我们给原本的复制按钮添加了一个"#copy"
表示ID,并在复制的下面添加了回复段落功能,样式前面已经添加了,我们这里不需要进行修改了,只添加元素即可,去掉加减号即为正常缩进,rm.replySelect()
为我们的执行函数。
下面我们来控制他的显示和隐藏:
在自定义JS文件中,修改"window.oncontextmenu = function (event)"
部分代码:
下方代码2024-04-20更新:第九行添加判断,判断页面中是否存在popup元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| window.oncontextmenu = function (event) { + $('#menu-text #copy').hide(); + $('#menu-text #reply').hide(); + //如果有文字选中,则显示 文字选中相关的菜单项 + if(document.getSelection().toString()){ + $('#menu-text #copy').show(); + } + const currentPath = window.location.pathname; + if (document.getSelection().toString() && currentPath.startsWith('/posts/')) { + // 如果页面中没有弹窗元素,则显示按钮并调用函数显示弹窗 + if (!document.getElementById('popup')) { + $('#menu-text #reply').show(); + } + } - $('.rightMenu-group.hide').hide(); - //如果有文字选中,则显示 文字选中相关的菜单项 - if(document.getSelection().toString()){ - $('#menu-text').show(); - } if (document.body.clientWidth > 768) { let pageX = event.clientX + 10; let pageY = event.clientY; let $rightMenuNormal = $(".rightMenuNormal"); let $rightMenuOther = $(".rightMenuOther"); let $rightMenuReadmode = $("#menu-readmode"); $rightMenuNormal.show(); $rightMenuOther.show(); rm.reloadrmSize(); if (pageX + rmWidth > window.innerWidth) { pageX -= rmWidth; } if (pageY + rmHeight > window.innerHeight) { pageY -= rmHeight; } rm.showRightMenu(true, pageY, pageX); $('#rightmenu-mask').attr('style', 'display: flex'); return false; } };
|
此时就可以基本测试出我们的逻辑是否正常了,hexo三联,在文章页选中文字右键才能看见我们的回复段落功能按钮出现,其他方式均不能触发。
非文章页不选中文字时,右键复制及回复均无法显示
非文章页选中文字仅会触发复制
仅仅在文章页且选中文字的情况下才可以触发该动作
实现函数
这里我会咯嗦我的探索过程,请不想看只想实现功能的铁铁直接跳转到第三部分按照教程顺序实现即可。
妥协方案
下面我们需要实现该功能,刚开始我选择的时使用和说说页面类似的效果,当点击评论后,找到评论区输入框,将选中文字放到输入框中,进行类似于回复段落的效果,但是由于我设置的懒加载,当评论区没有滚入到页面视野内时不会自动加载,会导致如下查找输入框语句失败:
1
| const commentBox = document.querySelector(".el-textarea__inner");
|
这样也就无法输入到文本框了,所以我刚开始想了个妥协的方法,就是当没加载评论框时弹出提示说没有加载,需要手动滚到下方进行加载,这也是第一代方案,草草了事了~
不完美实现方案
第二天起床,我想了一下,他没加载评论,那我就自己提前加载一下呗?于是我开始翻看Twikoo的文档,找到了以下文档:
其中内容如下:
于是我写出了如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| function loadTwikooLibrary() { return new Promise((resolve, reject) => { var script = document.createElement('script'); script.src = 'https://cdn.staticfile.org/twikoo/1.6.32/twikoo.all.min.js'; script.onload = function() { console.log('Twikoo库加载成功'); resolve(); }; script.onerror = function() { reject('Twikoo库加载失败'); }; document.head.appendChild(script); }); }
async function replySelect() { removeRightMenu(); var selectedText = document.getSelection().toString(); if (selectedText.includes('\n')) { selectedText = selectedText.split('\n')[0]; }
var commentBoxTest = document.querySelector(".el-textarea__inner"); if (!commentBoxTest) { console.log("加载评论中"); try { await loadTwikooLibrary(); twikoo.init({ envId: 'ddddddddddddd地址', el: '#post-comment', }); } catch (error) { console.error(error); } }
var commentBox = document.querySelector(".el-textarea__inner"); commentBox.value = `> ${selectedText}\n\n`; commentBox.focus(); var replySelectMessage = document.createElement('div'); replySelectMessage.textContent = '不要删除空行显示效果更佳哦!'; replySelectMessage.style.position = 'fixed'; replySelectMessage.style.top = '50%'; replySelectMessage.style.left = '50%'; replySelectMessage.style.transform = 'translate(-50%, -50%)'; replySelectMessage.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; replySelectMessage.style.color = '#fff'; replySelectMessage.style.padding = '10px 20px'; replySelectMessage.style.borderRadius = '5px'; replySelectMessage.style.zIndex = '9999'; document.body.appendChild(replySelectMessage); setTimeout(function(){ document.body.removeChild(replySelectMessage); }, 2000); }
rm.replySelect = replySelect;
|
这个方法解决了评论区没有加载的问题,但是因为他的评论区是只引入了一个twikoo,而我的评论区是双评论(其实屁用没有,不过当摆设倒是很高大上),导致他的样式和主题加载出来的有些不同,这里我没有记录截图,现在回退回去也有点麻烦,所以我就放一张正常的,解释一下哪里样式有问题:
所以这个方法实现的也不是很完美,感觉怪郁闷的,因为本人为中度强迫症患者,只要我能解决的我会去解决,解决不了的我宁愿给他删掉。。。所以,我想出了最后的一种方案:弹窗法。
完美(可能)实现方案
经过了半天的思考,我在想,为什么我会被说说的评价局限住呢?我可以参考一下番茄小说,每段话后面有个按钮,点击后弹窗,那我也可以这么实现吧?再就是,我选中文字回复后,会跳转到页面底部的话,就算完美实现了,读者也需要重新跳过去才能继续阅读文章,这很大的影响了读者阅读体验,那我为什么不能原地弹窗,弹出之后不动页面,让读者评论完成后继续看呢?这样我也不需要考虑多评论了,因为这个没有什么主题自带的模板格式,完全是我自己造的,想怎么来怎么来!于是我开始使用JS实现这些功能,为了更加美观直接好理解,我将每个部分的内容封装成了函数:
JS功能实现
首先,加载twikoo的库:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| async function loadTwikooLibrary() { return new Promise((resolve, reject) => { if (window.twikoo) { resolve(); return; }
const script = document.createElement('script'); script.src = 'https://cdn.staticfile.org/twikoo/1.6.31/twikoo.all.min.js'; script.onload = () => { console.log('Twikoo库加载成功'); resolve(); }; script.onerror = () => { reject(new Error('Twikoo库加载失败')); }; document.head.appendChild(script); }); }
|
这里我解释一下,这段代码会动态加载Twikoo库。返回一个 Promise,用于处理后续的异步操作。首先,它检查窗口对象 window
上是否已经有 twikoo
属性,即 Twikoo 库是否已经加载过。如果已经加载过,比如你提前已经回复过一次或者已经翻到下面加载成功过主评论区,它直接返回一个成功的 Promise。否则,它创建一个 <script>
元素,并将其 src
属性设置为 Twikoo 库的 URL。这会让网页加载 Twikoo 库文件。当库加载成功后,会在控制台中打印 “Twikoo库加载成功”,并返回Promise;如果加载失败,会 reject Promise 并返回错误信息。最后,将 <script>
元素添加到 <head>
部分中,开始加载 Twikoo 库(听不懂没关系,直接抄代码就行)。
下面我们开始创建弹窗,我想创建一个后面是遮罩层,前台一个框的弹窗,于是先创建遮罩层,再创建弹窗,分别写出以下函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function createOverlay() { const overlay = document.createElement('div'); overlay.id = 'overlay'; overlay.classList.add('overlay'); document.addEventListener('click', handleClickOutsidePopup); return overlay; }
function createPopup() { const popup = document.createElement('div'); popup.id = 'popup'; popup.classList.add('popup'); return popup; }
|
创建了弹窗,我们还需要关闭弹窗,要不然下次就用不了了,于是我们再写一个关闭遮罩层的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function closePopup(popup, overlay) { overlay.style.opacity = 0; popup.style.opacity = 0;
setTimeout(() => { document.body.removeChild(popup); document.body.removeChild(overlay); document.removeEventListener('click', handleClickOutsidePopup); }, 300); }
function handleClickOutsidePopup(event) { const popup = document.getElementById('popup'); if (popup && !popup.contains(event.target)) { closePopup(popup, document.getElementById('overlay')); } }
|
这里我创建了一个事件,点击弹窗以外的空白位置关闭弹窗(可能是人之常情?总喜欢点别的地方来关闭它,至少我是这样),本来是有个按钮的,但是嫌弃他太丑了给删掉了,后面看看能不能加上一个更加美观的。
然后我将之前的提示消息弹窗的内容也封装成了函数(反正封了这么多不差这一个),方便其他位置直接调用即可,因为代码量还是不小的,有点占地方。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| function showMessage(message, duration = 2000) { const replySelectMessage = document.createElement('div'); replySelectMessage.innerHTML = message; replySelectMessage.classList.add('pop-message'); document.body.appendChild(replySelectMessage);
replySelectMessage.style.opacity = '0';
setTimeout(() => { replySelectMessage.style.opacity = '1'; }, 10);
function removeMessage() { replySelectMessage.style.opacity = '0'; setTimeout(() => { document.body.removeChild(replySelectMessage); }, 500); }
setTimeout(removeMessage, duration); }
|
这个函数接受两个参数,内容和时间(默认两秒),函数都封装完了,需要开始组装了!把面向对象贯彻到底,继续封装函数!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| function showPopupWithComments(envId, commentElementId) { const overlay = createOverlay();
const popup = createPopup();
const commentSection = document.createElement('div'); commentSection.id = commentElementId; popup.appendChild(commentSection);
document.body.appendChild(overlay); document.body.appendChild(popup);
twikoo.init({ envId, el: `#${commentElementId}` });
showMessage('点击弹窗外任意部分即可退出');
setTimeout(() => { overlay.style.opacity = 1; popup.style.opacity = 1; }, 0); }
|
我的注释应该已经够详细了,所以就不解释了,这个就是我们显示待评论的弹窗,然后我们将其在回复按钮的相应事件中调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| async function replySelect() { removeRightMenu(); var selectedText = document.getSelection().toString().trim();
if (selectedText.includes('\n')) { selectedText = selectedText.split('\n')[0].trim(); }
try { await loadTwikooLibrary(); showPopupWithComments('你的twikoo地址或者按照官方的腾讯环境ID', 'comment-section'); } catch (error) { console.error(error); }
const commentBox = document.querySelector("#popup .el-textarea__inner"); commentBox.value = `> ${selectedText}\n\n`; }
rm.replySelect = replySelect;
|
上面需要改你的Twikoo的地址,在倒数第二行,我修改了获取文本框的CSS路径,防止匹配到主评论区了,最后将将 replySelect 函数绑定到事件上。
CSS添加
到这里还没完,因为我们没有指定任何样式,下面是所有的CSS内容,这个比较简单我就不解释啦!直接复制到你的自定义CSS文件中即可!
下方代码2024-04-20更新:第六十四行,由于刷新按钮为Twikoo官方内部封装,为方便后续升级不想对其修改,于是将其隐藏防止误触
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
|
.overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(255, 255, 255, 0.7); z-index: 999; opacity: 0; transition: opacity 0.3s; }
[data-theme=dark] .overlay { background-color: rgba(0, 0, 0, 0.7); }
.popup { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: rgba(255, 255, 255, 0.7); padding: 20px; border: 1px solid #ccc; z-index: 1000; max-height: 80%; overflow-y: auto; border-radius: 20px; opacity: 0; transition: opacity 0.3s; scrollbar-width: none; -ms-overflow-style: none; }
[data-theme=dark] .popup { background-color: rgba(44, 44, 44, 0.7); border: 1px solid #666; }
.pop-message { position: fixed; top: 70px; right: 10px; background-color: rgba(255, 255, 255, 0.7); color: #000000; padding: 20px 30px; border-radius: 10px; z-index: 9999; border: 1px solid #000000; opacity: 0; transition: opacity 1.0s ease-in-out; }
[data-theme=dark] .pop-message { background-color: rgba(44, 44, 44, 0.7); color: #fff; border: 1px solid #ffffff; }
#popup #twikoo .tk-comments .tk-comments-container .tk-comments-title > span:nth-child(2) > span:nth-child(1) { display: none; }
|
展示效果
最终的效果我也很满意,下面是一些效果图:
其实后面想想,手机也没有右键菜单啊?算了不管了,就当是分辨率适配啦
总结
这次魔改是我最近比较大的一次尝试,作为入场一个月的小白,慢慢成长,我也能感受到我的收获,后面我会继续学习,实现更多的功能!
完结撒花!
每日一图