L
O
A
D
I
N
G

右键菜单


增加右键菜单

同主题的小伙伴可能发现了,博客网站的右键菜单实在是功能少的可怜,一个好的右键菜单可以让访客朋友的用户体验提高不少,所以下面我们就来就是实现给我买的博客网站添加一个功能更丰富的右键菜单😋

效果

右键菜单

实现

找到themes\matery\source\css\matery.css,加入

/* 右键菜单的基本样式 */
#rightMenu {
    display: none;
    position: fixed;
    padding: 0 .25rem;
    width: 10rem;
    height: fit-content;
    top: 10%;
    left: 10%;
    background-color: rgba(255, 255, 255, .85);
    backdrop-filter: blur(20px);
    border-radius: 12px;
    z-index: 99994;
    border: 1px solid #e3e8f7;
    user-select: none;
    box-shadow: 0 0 12px 4px rgba(0, 0, 0, .05);
    transition: border-color .3s, box-shadow .3s;
}

#rightMenu:hover {
    border-color: #177cb0;
    box-shadow: 0 8px 12px -3px rgba(66, 89, 239, .14);
}

/* 右键菜单组样式 */
#rightMenu .rightMenu-group {
    padding: .35rem .3rem;
    transition: .3s;
}

#rightMenu .rightMenu-group.rightMenu-small {
    display: flex;
    justify-content: space-between;
}

#rightMenu .rightMenu-line {
    border-top: 1px dashed rgba(66, 89, 239, .14);
}

/* 菜单项的基本样式 */
#rightMenu .rightMenu-item {
    border-radius: 8px;
    transition: .3s;
    cursor: pointer;
    color: #525f7f;
    display: flex;
    align-items: center;
    margin: .25rem 0;
    padding: .25rem 0;
}

.rightMenu-small .rightMenu-item {
    width: 30px;
    height: 30px;
    line-height: 30px;
    justify-content: center;
}

#rightMenu .rightMenu-group .rightMenu-item i {
    text-align: center;
    line-height: 1.5rem;
    width: 2rem;
    padding: 0 .25rem;
}

#rightMenu .rightMenu-line .rightMenu-item i {
    margin: 0 .25rem;
}

#rightMenu .rightMenu-group .rightMenu-item span {
    line-height: 1.5rem;
}

#rightMenu .rightMenu-item:hover {
    background-color: #177cb0;
    color: #fff;
    box-shadow: 0 8px 12px -3px rgba(66, 89, 239, .14);
}

#rightMenu .rightMenu-item:active {
    transform: scale(.97);
}

/* 遮罩层样式 */
#rightmenu-mask {
    position: fixed;
    width: 100vw;
    height: 100vh;
    background: 0 0;
    top: 0;
    left: 0;
    display: none;
    z-index: 99993;
    margin: 0 !important;
}

定位到主题文件夹下的页面布局代码,即matery/layout/layout.ejs,在<main>标签中加入:

<!-- 添加右键菜单 -->
<% if (theme.RightMenu.enable) { %>
    <div id="rightMenu" style="top: 0px; left: 0px; display: none;">
        <!-- 基本操作组 -->
        <div class="rightMenu-group rightMenu-small">
            <% const basicOps = [
                { id: 'menu-backward', title: '返回', icon: 'fas fa-arrow-left' },
                { id: 'menu-forward', title: '前进', icon: 'fas fa-arrow-right' },
                { id: 'menu-refresh', title: '刷新', icon: 'fas fa-bolt' },
                { id: 'menu-top', title: '返回顶部', icon: 'fas fa-arrow-up' }
            ]; 
            basicOps.forEach(op => { %>
                <div class="rightMenu-item" id="<%= op.id %>" title="<%= op.title %>">
                    <i class="<%= op.icon %>"></i>
                </div>
            <% }); %>
        </div>

        <!-- 插件操作组 -->
        <div class="rightMenu-group rightMenu-line rightMenuPlugin" style="display:none">
            <% const pluginOps = [
                { id: 'menu-copytext', icon: 'fas fa-copy', text: '复制选中' },
                { id: 'menu-copylink', icon: 'fas fa-link', text: '复制链接' },
                { id: 'menu-pastetext', icon: 'fas fa-paste', text: '粘贴文本' },
                { id: 'menu-commenttext', icon: 'fas fa-comment-medical', text: '引用到评论' },
                { id: 'menu-newwindow', icon: 'fas fa-window-restore', text: '新窗口打开' },
                { id: 'menu-copyimg', icon: 'fas fa-images', text: '复制此图片' },
                { id: 'menu-downloadimg', icon: 'fas fa-download', text: '下载此图片' },
                { id: 'menu-search', icon: 'fas fa-magnifying-glass', text: '站内搜索' },
                { id: 'menu-searchBing', icon: 'fas fa-magnifying-glass', text: '必应搜索' },
                { id: 'menu-music-toggle', icon: 'fas fa-play', text: '播放音乐' },
                { id: 'menu-music-back', icon: 'fas fa-backward', text: '切换到上一首' },
                { id: 'menu-music-forward', icon: 'fas fa-forward', text: '切换到下一首' },
                { id: 'menu-music-playlist', icon: 'fas fa-radio', text: '查看所有歌曲', link: 'https://music.163.com/#/playlist?app_version=8.8.36&id=5197802668' },
                { id: 'menu-music-copyMusicName', icon: 'fas fa-copy', text: '复制歌名' }
            ];
            pluginOps.forEach(op => { %>
                <div class="rightMenu-item" id="<%= op.id %>" style="display:none">
                    <i class="<%= op.icon %>"></i>
                    <span><%= op.text %></span>
                </div>
            <% }); %>
        </div>

        <!-- 其他操作组 -->
        <div class="rightMenu-group rightMenu-line rightMenuOther">
            <% const otherOps = [
                { id: 'menu-randomWebsite', icon: 'fas fa-paper-plane', text: '次元跃迁', link: 'https://travel.moe/go.html' },
                { id: 'menu-randomPost', icon: 'fas fa-shoe-prints', text: '随便逛逛' },
                { id: 'menu-categories', icon: 'fas fa-cube', text: '博客分类', link: '/categories/' },
                { id: 'menu-tags', icon: 'fas fa-tags', text: '文章标签', link: '/tags/' },
                { id: 'menu-privacy', icon: 'fas fa-hands', text: '隐私协议', link: '/privacy/' },
                { id: 'menu-cc', icon: 'fas fa-closed-captioning', text: '版权协议', link: '/cc/' },
                { id: 'menu-writing', icon: 'fas fa-feather', text: '行文规范', link: '/SEAEPOCH-WRITING-STANDARDS/' },
                { id: 'menu-copy', icon: 'fas fa-copy', text: '复制地址' },
                { id: 'menu-darkmode', icon: 'fas fa-moon', text: '深色模式', extraClass: 'menu-darkmode-text' }
            ];
            otherOps.forEach(op => { %>
                <a class="rightMenu-item menu-link" href="<%= op.link || '#' %>" id="<%= op.id %>" target="<%= op.link ? '_blank' : '_self' %>" rel="noopener">
                    <i class="<%= op.icon %>"></i>
                    <span class="<%= op.extraClass || '' %>"><%= op.text %></span>
                </a>
            <% }); %>
        </div>
    </div>
    <div id="rightmenu-mask" style="display: none"></div>
    
    <script src="/js/rightMenu.js"></script>
    <script>addRightMenuClickEvent()</script>
<% } %>

matery/source/js下新建rightMenu.js,添加

// 初始化函数
let rm = {};
const btf = {
  scrollToDest: (pos, time = 500) => {
    if (pos < 0 || time < 0) return;

    const currentPos = window.scrollY || window.screenTop;
    pos -= 70;

    if ('CSS' in window && CSS.supports('scroll-behavior', 'smooth')) {
      window.scrollTo({ top: pos, behavior: 'smooth' });
    } else {
      let start = null;
      window.requestAnimationFrame(function step(currentTime) {
        start = start || currentTime;
        const progress = currentTime - start;
        const distance = pos - currentPos;
        window.scrollTo(0, currentPos + (distance * progress) / time);
        if (progress < time) {
          window.requestAnimationFrame(step);
        } else {
          window.scrollTo(0, pos);
        }
      });
    }
  },
};

const heo = {
  downloadImage: function (imgsrc, name) {
    rm.hideRightMenu();
    if (rm.downloadimging) {
      M.toast({ html: '<span>有正在进行中的下载,请稍后再试</span>' }, 5000);
      return;
    }

    rm.downloadimging = true;
    M.toast({ html: '<span>正在下载中,请稍后</span>' }, 10000);

    setTimeout(() => {
      let image = new Image();
      image.crossOrigin = "anonymous";
      image.onload = () => {
        let canvas = document.createElement("canvas");
        canvas.width = image.width;
        canvas.height = image.height;
        let context = canvas.getContext("2d");
        context.drawImage(image, 0, 0, image.width, image.height);
        let url = canvas.toDataURL("image/png");
        let a = document.createElement("a");
        a.download = name || "photo";
        a.href = url;
        a.dispatchEvent(new MouseEvent("click"));
        M.toast({ html: '<span>图片已添加盲水印,请遵守版权协议</span>' }, 5000);
        rm.downloadimging = false;
      };
      image.src = imgsrc;
    }, 10000);
  },
  musicToggle: function () { console.log("等待开发..."); },
  musicSkipBack: function () { console.log("等待开发..."); },
  musicSkipForward: function () { console.log("等待开发..."); },
  musicGetName: function () { console.log("等待开发..."); },
};

// 禁止图片拖拽
$("img").on("dragstart", () => false);

// 显示和隐藏菜单
rm.showRightMenu = function (isTrue, x = 0, y = 0) {
  $('#rightMenu').css({ top: `${x}px`, left: `${y}px` }).toggle(isTrue);
  $('#rightmenu-mask').css('display', isTrue ? 'flex' : 'none');
};

rm.hideRightMenu = function () { rm.showRightMenu(false); };

// 更新尺寸
rm.reloadrmSize = function () {
  rm.width = $('#rightMenu').width();
  rm.height = $('#rightMenu').height();
};

let domhref = '', domImgSrc = '', globalEvent = null;

// 右键菜单监听初始化
window.oncontextmenu = function (event) {
  if (document.body.clientWidth <= 768) return true;

  let pageX = event.clientX + 10, pageY = event.clientY;
  globalEvent = event;
  let $rightMenuOther = $('.rightMenuOther'), $rightMenuPlugin = $('.rightMenuPlugin');
  let href = event.target.href, imgsrc = event.target.currentSrc;
  let pluginMode = false;

  $('.rightMenuPlugin, .rightMenuCopyText, .rightMenuCommentText, .rightMenuSearch, .rightMenuSearchBing').hide();
  if (selectTextNow && window.getSelection()) {
    pluginMode = true;
    $('.rightMenuCopyText, .rightMenuCommentText, .rightMenuSearch, .rightMenuSearchBing').show();
  }

  if (href) {
    pluginMode = true;
    $('#menu-newwindow, #menu-copylink').show();
    domhref = href;
  } else {
    $('#menu-newwindow, #menu-copylink').hide();
  }

  if (imgsrc) {
    pluginMode = true;
    $('#menu-copyimg, #menu-downloadimg').show();
    domImgSrc = imgsrc;
  } else {
    $('#menu-copyimg, #menu-downloadimg').hide();
  }

  if (['input', 'textarea'].includes(event.target.tagName.toLowerCase())) {
    pluginMode = true;
    $('#menu-pastetext').show();
  } else {
    $('#menu-pastetext').hide();
  }

  if (event.target.nodeName == "METING-JS") {
    pluginMode = true;
    $('.rightMenuMusic').show();
  } else {
    $('.rightMenuMusic').hide();
  }

  if (pluginMode) {
    $rightMenuOther.hide();
    $rightMenuPlugin.show();
  } else {
    $rightMenuPlugin.hide();
  }

  rm.reloadrmSize();

  if (pageX + rm.width > window.innerWidth) {
    pageX -= rm.width + 20;
  }
  if (pageY + rm.height > window.innerHeight) {
    pageY -= pageY + rm.height - window.innerHeight;
  }

  rm.showRightMenu(true, pageY, pageX);
  return false;
};

// 下载图片状态
rm.downloadimging = false;

// 复制图片到剪贴板
rm.writeClipImg = async function (imgsrc) {
  rm.hideRightMenu();
  M.toast({ html: '<span>正在下载中,请稍后</span>' }, 10000);
  if (rm.downloadimging) return;

  rm.downloadimging = true;
  setTimeout(async () => {
    await copyImage(imgsrc);
    M.toast({ html: '<span>复制成功!图片已添加盲水印,请遵守版权协议</span>' }, 5000);
    rm.downloadimging = false;
  }, 10000);
};

async function imageToBlob(imageURL) {
  const img = new Image();
  const c = document.createElement("canvas");
  const ctx = c.getContext("2d");
  img.crossOrigin = "";
  img.src = imageURL;
  return new Promise(resolve => {
    img.onload = function () {
      c.width = this.naturalWidth;
      c.height = this.naturalHeight;
      ctx.drawImage(this, 0, 0);
      c.toBlob(blob => resolve(blob), "image/png", 0.75);
    };
  });
}

async function copyImage(imageURL) {
  const blob = await imageToBlob(imageURL);
  const item = new ClipboardItem({ "image/png": blob });
  await navigator.clipboard.write([item]);
}

rm.switchDarkMode = function () {
  switchNightMode();
  rm.hideRightMenu();
};

rm.copyUrl = function (id) {
  const input = $("<input id='copyVal'>").appendTo('body').val(id).select();
  document.execCommand("copy");
  input.remove();
};

function stopMaskScroll() {
  $('#rightmenu-mask, #rightMenu').on("mousewheel", () => rm.hideRightMenu());
}

rm.rightmenuCopyText = function (txt) {
  if (navigator.clipboard) navigator.clipboard.writeText(txt);
  rm.hideRightMenu();
};

rm.copyPageUrl = function () {
  rm.copyUrl(window.location.href);
  M.toast({ html: '<span>复制本页链接地址成功</span>' }, 2000);
  rm.hideRightMenu();
};

rm.sharePage = function () {
  rm.copyUrl(window.location.href);
  M.toast({ html: '<span>复制本页链接地址成功</span>' }, 2000);
  rm.hideRightMenu();
};

// 复制当前选中文本
let selectTextNow = '';
document.onmouseup = document.ondblclick = () => {
  selectTextNow = window.getSelection ? window.getSelection().toString() : document.selection.createRange().text;
};

rm.readClipboard = function () {
  if (navigator.clipboard) {
    navigator.clipboard.readText().then(clipText => rm.insertAtCaret(globalEvent.target, clipText));
  }
};

rm.insertAtCaret = function (elemt, value) {
  const startPos = elemt.selectionStart, endPos = elemt.selectionEnd;
  if (document.selection) {
    elemt.focus();
    document.selection.createRange().text = value;
  } else if (startPos || startPos === '0') {
    const scrollTop = elemt.scrollTop;
    elemt.value = elemt.value.substring(0, startPos) + value + elemt.value.substring(endPos);
    elemt.focus();
    elemt.selectionStart = elemt.selectionEnd = startPos + value.length;
    elemt.scrollTop = scrollTop;
  } else {
    elemt.value += value;
    elemt.focus();
  }
};

rm.pasteText = function () { rm.readClipboard(); rm.hideRightMenu(); };

rm.rightMenuCommentText = function (txt) {
  rm.hideRightMenu();
  const input = $('#veditor');
  const inputValue = txt.split('\n').join('\n> ');
  input.val(`> ${inputValue}\n\n`).trigger('input');
  btf.scrollToDest(0, 500);
  input.focus();
};

stopMaskScroll();

window.addEventListener('load', () => setTimeout(stopMaskScroll, 1000));

现在你就可以本地部署并查看效果了

参考文章

【Hexo博客】魔改美化 Butterfly 主题右键菜单 | 百里飞洋 (meta-code.top)

SeaYJ’s Blog


文章作者: loyeh
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 loyeh !
评论
  目录