一不注意就又在捣鼓博客装修了,本来是打算继续在12号装修日志后面添加新的DLC,结果定睛一看,发现12号装修日志只差一个字就要突破7000字大关,眼看就要再创相同tag下文章总字数历史新高,于是最终还是选择新开了一篇13号报告。

自定义右键菜单

起因只是忽然觉得浏览器右键菜单的默认样式严重影响了我全方位欣赏博客的美丽(?),遂决定让ChatGPT老师帮我写一个符合博客主题的自定义菜单。新菜单除了复制、黏贴和刷新这些基础功能之外,还顺便将博客之前一直没有实装的上一篇、下一篇按钮也塞了进去,并增加了鼠标悬浮时预览目录的功能,这样配合上random按钮,就可以更方便我来随机回顾往期的博客。

基本框架弄出来之后,又总觉得是不是还可以再装饰点什么,思考了几分钟后灵机一动,在手机上临时下载了画加,约了个含有大量ff14角色sample图(?)的像素小豆丁橱窗,不过由于最初和画手老师沟通时忘记提透明底需求,收到图时才发现老师直接画成来白底,而改透明底的话还得再加3r。好在像素图本身颜色和线条都非常简单干净,我后面用Procreate微调整体颜色时就顺便把透明底一并抠出来了。和ChatGPT说明需求获得更新后的代码,按照自己的喜好调整图片的大小和位置,一只由穿着弦月睡袍母肥24小时站岗的右键菜单就诞生了!

请欣赏效果图!做了简单的叫醒母肥的差分。时至今日,博客里已经入住了三只像素母肥了,之后有机会的话,说不定还会继续加入新衣装版本的母肥酱!

菜单

下面是具体的代码存档。为了方便后续修改,我把它们全部整合到了新建的menu.html中,并将其放在了\layouts\partials目录下,最后再在\layouts\_default\baseof.html中进行渲染。

hmtl部分。

<div id="context-decor">
<img id="context-decor-img" src="睡觉母肥图片路径" alt="">
</div>

<div id="custom-context-menu">
<div class="context-item disabled" id="context-copy">复制</div>
<div class="context-item disabled" id="context-paste">粘贴</div>
<div class="context-item" id="context-refresh">刷新</div>
<div class="context-line"></div>
<div class="context-item disabled" id="context-prev">上一篇</div>
<div class="context-item disabled" id="context-next">下一篇</div>
</div>

<div id="context-preview">
<div class="context-preview-title" id="context-preview-title"></div>
<div class="context-preview-date" id="context-preview-date"></div>
<div class="context-preview-label">目录</div>
<div class="context-preview-toc" id="context-preview-toc"></div>
</div>

js部分,因为原代码行数过多,这里就不再记录经过 PJAX 包裹后的完整样式了。

(function(){
const menu=document.getElementById('custom-context-menu');
const copy=document.getElementById('context-copy');
const paste=document.getElementById('context-paste');
const refresh=document.getElementById('context-refresh');
const prev=document.getElementById('context-prev');
const next=document.getElementById('context-next');
const decor=document.getElementById('context-decor');
const decorImg=document.getElementById('context-decor-img');
const preview=document.getElementById('context-preview');
const previewTitle=document.getElementById('context-preview-title');
const previewDate=document.getElementById('context-preview-date');
const previewToc=document.getElementById('context-preview-toc');

if(!menu)return;

let selectedText='';
let activeElement=null;
let selectionStart=null;
let selectionEnd=null;
let previewTimer=null;
let previewType=null;

const defaultImage='睡着母肥图片路径';
const hoverImage='醒来母肥图片路径';

function setDecorHover(hover){
if(!decorImg)return;
decorImg.src=hover?hoverImage:defaultImage;
}

function positionDecor(){
if(menu.style.display!=='block')return;

const rect=menu.getBoundingClientRect();
const width=120;
const leftOffset=-33;
const topOffset=84;

decor.style.left=(rect.right-width-leftOffset)+'px';
decor.style.top=(rect.top-topOffset)+'px';
}

function showDecor(){
setDecorHover(false);
positionDecor();
decor.style.display='block';
}

function hideDecor(){
decor.style.display='none';
setDecorHover(false);
}

function hideMenu(){
menu.style.display='none';
hidePreview();
hideDecor();
}

function hidePreview(){
clearTimeout(previewTimer);
preview.style.display='none';
previewType=null;
}

function getData(type){
const box=document.getElementById(
type==='prev'?'context-prev-data':'context-next-data'
);

if(!box)return null;

const url=box.querySelector('.context-data-url');
const title=box.querySelector('.context-data-title');
const date=box.querySelector('.context-data-date');
const toc=box.querySelector('.context-data-toc');

if(!url)return null;

return{
url:url.textContent.trim(),
title:title?title.textContent.trim():'',
date:date?date.textContent.trim():'',
toc:toc?toc.innerHTML.trim():''
};
}

function updateNavigation(){
const main=document.querySelector('main');

if(!main)return;

const prevUrl=main.dataset.prev;
const nextUrl=main.dataset.next;

if(prevUrl){
prev.dataset.url=prevUrl;
prev.classList.remove('disabled');
}else{
delete prev.dataset.url;
prev.classList.add('disabled');
}

if(nextUrl){
next.dataset.url=nextUrl;
next.classList.remove('disabled');
}else{
delete next.dataset.url;
next.classList.add('disabled');
}
}

function showPreview(type){
const data=getData(type);

if(!data){
hidePreview();
return;
}

previewType=type;

clearTimeout(previewTimer);

previewTitle.textContent=data.title||'未命名文章';
previewDate.textContent=data.date||'';

if(data.toc){
previewToc.innerHTML=data.toc;
}else{
previewToc.innerHTML='<div class="context-preview-empty">暂无目录</div>';
}

const target=type==='prev'?prev:next;
const rect=target.getBoundingClientRect();

preview.style.display='block';

const width=preview.offsetWidth;
const height=preview.offsetHeight;
const gap=8;

let left;
let top=rect.top;

if(type==='prev'){
left=rect.left-width-gap;

if(left<8){
left=8;
}
}else{
left=rect.right+gap;

if(left+width>window.innerWidth-8){
left=window.innerWidth-width-8;
}
}

if(top+height>window.innerHeight-8){
top=window.innerHeight-height-8;
}

if(top<8){
top=8;
}

preview.style.left=Math.max(8,left)+'px';
preview.style.top=Math.max(8,top)+'px';
}

function scheduleHide(){
clearTimeout(previewTimer);

previewTimer=setTimeout(function(){
if(
!prev.matches(':hover')&&
!next.matches(':hover')&&
!preview.matches(':hover')
){
hidePreview();
}
},120);
}

async function copyText(text){
if(!text)return;

try{
await navigator.clipboard.writeText(text);
}catch(e){
const textarea=document.createElement('textarea');

textarea.value=text;
textarea.style.position='fixed';
textarea.style.left='-9999px';
textarea.style.top='0';

document.body.appendChild(textarea);

textarea.focus();
textarea.select();

try{
document.execCommand('copy');
}catch(e){}

textarea.remove();
}
}

async function updatePaste(){
if(!activeElement){
paste.classList.add('disabled');
return;
}

const editable=
activeElement.tagName==='INPUT'||
activeElement.tagName==='TEXTAREA'||
activeElement.isContentEditable;

if(!editable){
paste.classList.add('disabled');
return;
}

try{
const text=await navigator.clipboard.readText();

paste.classList.toggle('disabled',!text);
}catch(e){
paste.classList.remove('disabled');
}
}

function goTo(url){
if(!url)return;

hideMenu();

window.location.href=url;
}

function setupHover(element){
element.addEventListener('mouseenter',function(){

if(element.classList.contains('disabled')){
return;
}

setDecorHover(true);
});

element.addEventListener('mouseleave',function(){

if(element.classList.contains('disabled')){
return;
}

setDecorHover(false);
});
}

document.addEventListener('contextmenu',function(e){

e.preventDefault();

activeElement=document.activeElement;

if(
activeElement&&(
activeElement.tagName==='INPUT'||
activeElement.tagName==='TEXTAREA'||
activeElement.isContentEditable
)
){

if(typeof activeElement.selectionStart==='number'){

selectionStart=activeElement.selectionStart;
selectionEnd=activeElement.selectionEnd;

}else{

selectionStart=null;
selectionEnd=null;

}

}else{

selectionStart=null;
selectionEnd=null;

}

selectedText=window.getSelection().toString().trim();

copy.classList.toggle('disabled',!selectedText);

updateNavigation();
updatePaste();
hidePreview();

menu.style.display='block';

const width=menu.offsetWidth;
const height=menu.offsetHeight;

let left=e.clientX;
let top=e.clientY;

if(left+width>window.innerWidth-8){
left=window.innerWidth-width-8;
}

if(top+height>window.innerHeight-8){
top=window.innerHeight-height-8;
}

menu.style.left=Math.max(8,left)+'px';
menu.style.top=Math.max(8,top)+'px';

showDecor();

});

document.addEventListener('click',function(e){

if(
!menu.contains(e.target)&&
!preview.contains(e.target)
){

hideMenu();

}

});

document.addEventListener('scroll',hideMenu);

setupHover(copy);
setupHover(paste);
setupHover(refresh);
setupHover(prev);
setupHover(next);

prev.addEventListener('mouseenter',function(){

if(prev.classList.contains('disabled')){

hidePreview();

return;

}

showPreview('prev');

});

prev.addEventListener('mouseleave',scheduleHide);

next.addEventListener('mouseenter',function(){

if(next.classList.contains('disabled')){

hidePreview();

return;

}

showPreview('next');

});

next.addEventListener('mouseleave',scheduleHide);

preview.addEventListener('mouseenter',function(){

clearTimeout(previewTimer);

});

preview.addEventListener('mouseleave',function(){

hidePreview();

});

preview.addEventListener('click',function(e){

const target=e.target.closest('a');

if(!target)return;

const href=target.getAttribute('href');

if(!href)return;

const currentData=
previewType?
getData(previewType):
null;

if(!currentData)return;

e.preventDefault();
e.stopPropagation();

if(href.charAt(0)==='#'){

window.location.href=currentData.url+href;

return;

}

try{

const targetUrl=
new URL(href,window.location.href);

const currentUrl=
new URL(currentData.url,window.location.href);

if(
targetUrl.pathname===currentUrl.pathname&&
targetUrl.hash
){

window.location.href=
currentData.url+
targetUrl.hash;

return;

}

}catch(error){}

window.location.href=href;

});

copy.addEventListener('click',async function(e){

e.stopPropagation();

if(copy.classList.contains('disabled'))return;

await copyText(selectedText);

hideMenu();

});

paste.addEventListener('click',async function(e){

e.stopPropagation();

if(paste.classList.contains('disabled'))return;

if(!activeElement)return;

try{

const text=await navigator.clipboard.readText();

if(!text)return;

activeElement.focus();

if(activeElement.isContentEditable){

document.execCommand(
'insertText',
false,
text
);

}else{

const start=
selectionStart!==null?
selectionStart:
activeElement.value.length;

const end=
selectionEnd!==null?
selectionEnd:
start;

activeElement.setRangeText(
text,
start,
end,
'end'
);

activeElement.dispatchEvent(
new Event('input',{
bubbles:true
})
);

}

}catch(e){}

hideMenu();

});

refresh.addEventListener('click',function(e){

e.stopPropagation();

hideMenu();

window.location.reload();

});

prev.addEventListener('click',function(e){

e.stopPropagation();

if(prev.classList.contains('disabled'))return;

goTo(prev.dataset.url);

});

next.addEventListener('click',function(e){

e.stopPropagation();

if(next.classList.contains('disabled'))return;

goTo(next.dataset.url);

});

window.addEventListener('resize',function(){

positionDecor();

});

window.addEventListener('resize',hidePreview);

updateNavigation();

})();

css样式部分。

#custom-context-menu {
    display: none;
    position: fixed;
    z-index: 99999;
    min-width: 155px;
    padding: 5px;
    background: #ECEFF4;
    border: 2px solid #8aa2d3;
    box-shadow: 4px 4px 0 #dae0ea;
    border-radius: 0;
    font-family: ipx, ipxs, sans-serif;
    font-size: 13px;
    color: #3f4551;
    letter-spacing: .2px;
}

.context-item {
    display: block;
    padding: 7px 11px;
    white-space: nowrap;
    color: inherit;
    text-decoration: none;
    border: 1px solid transparent;
    border-radius: 0;
}

.context-item:hover {
    background: #dce5f4;
    border-color: #b9c8e2;
    color: #536b9b;
}

.context-item.disabled {
    color: #aeb7c5;
}

.context-item.disabled:hover {
    background: transparent;
    border-color: transparent;
    color: #aeb7c5;
}

.context-line {
    height: 2px;
    margin: 5px 6px;
    background: #dae0ea;
}

#context-decor {
    display: none;
    position: fixed;
    z-index: 99998;
    width: 120px;
    height: 120px;
    pointer-events: none;
}

#context-decor-img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
    image-rendering: pixelated;
}

#context-preview {
    display: none;
    position: fixed;
    z-index: 100000;
    width: 270px;
    padding: 12px 13px;
    background: #ECEFF4;
    border: 2px solid #8aa2d3;
    box-shadow: 4px 4px 0 #dae0ea;
    border-radius: 0;
    font-family: ipx, ipxs, sans-serif;
    font-size: 12px;
    line-height: 1.7;
    color: #3f4551;
}

.context-preview-title {
    margin-bottom: 5px;
    padding-bottom: 6px;
    border-bottom: 2px dotted #b9c8e2;
    font-size: 14px;
    font-weight: bold;
    line-height: 1.5;
    color: #536b9b;
}

.context-preview-date {
    margin-bottom: 9px;
    color: #87aac9;
    font-size: 11px;
}

.context-preview-label {
    margin-bottom: 5px;
    padding-bottom: 3px;
    font-size: 11px;
    color: #87aac9;
    letter-spacing: 1px;
}

.context-preview-toc {
    max-height: 220px;
    overflow: auto;
}

.context-preview-toc ul {
    list-style-type: disc;
    padding-left: 20px;
}

.context-preview-toc ul ul {
    list-style-type: circle;
}

.context-preview-toc ul ul ul {
    display: none;
}

.context-preview-toc a {
    color: inherit;
    text-decoration: none;
}

.context-preview-toc a:hover {
    color: #b17185;
    text-decoration: none;
}

.context-preview-empty {
    color: #aeb7c5;
}

.context-preview-toc::-webkit-scrollbar {
    width: 6px;
}

.context-preview-toc::-webkit-scrollbar-track {
    background: #f6f8fa;
}

.context-preview-toc::-webkit-scrollbar-thumb {
    background: #b9c8e2;
}

.context-preview-toc::-webkit-scrollbar-thumb:hover {
    background: #8aa2d3;
}