点击查看更新记录

更新记录

2021-01-24:内测版v0.07

  1. 实现SAO风格的右键菜单
  2. 添加了点击音效,默认使用本站同款,可以自定义配置
  3. 支持添加链接或者自定义脚本动作
  4. 添加Ctrl+右键转换原生菜单功能
  5. 几个常用脚本分享

2021-01-25:内测版v0.14

  1. 新增了3D模式开关配置项

2021-01-26:内测版v0.21

  1. 新增了3D模式下浮动效果
  2. 适配了pjax。调用了Butterfly主题自带的pjax对象。其他主题需要另外适配。

2021-01-28:内测版v0.37

  1. 正式版待发布,因为显隐逻辑完全改变,预备转至新帖;

2021-01-29:正式版v1.0

  1. 1.0版本正式版发布。最终确定此帖为SAO UI右半边菜单,适合可用菜单项较少且习惯精简风格的用户。
点击查看参考教程
参考方向教程原贴
菜单边框风格伪类样式实现方案codepen-Pure CSS SAO Menu Thing
右键菜单显隐逻辑和原生实现方案
样式风格参考,图标、音效资源采集SAO Utils
图标采集fontawesome
pjax适配参考

资源下载

由于本教程涉及的所有修改对缩进格式等有严格要求,担心自己控制不好的可以直接下载静态资源。参照教程进行修改。

写在最前

来自店长的碎碎念

2021.01.24

写一个SAO风格的右键菜单算是我的一个执念了,但是搜遍全网页找不到网页版的内容,于是我想起来多年前就用过的一款软件SAO Utils,可惜它虽然有完整的菜单逻辑,但是却是基于C语言写的。至多只能参考一些音效。

最后兜兜转转,在魔改博客时看到了Volantis的右键菜单。学习了一下右键菜单的魔改原理。决定自己来从零开始做一个SAO风格的右键菜单。

因为这个项目,魔怔了大概半个月,好在那半个月单位工作基本划水。(嘛,总之摸鱼也是为了给大家写好看的魔改教程嘛)一直被二级菜单的显隐逻辑所困扰,因为用到了相对定位,中间有一段元素是空白的,没法在不破坏菜单项显示效果的情况下直接依靠hover实现持续显示二级菜单的效果,最后是用定时函数控制绝对显隐,用hover控制持续显隐。总算是有了一个相对舒适的显隐体验。

在一开始,因为想到以前一直被一些读者喷右键菜单占用了原生菜单很讨厌(说实话这样的读者也很讨厌)。所以这次在@卓越科技建议下添加了ctrl+右键打开原生右键菜单的功能。然后考虑到菜单界面对手机不友好,干脆对手机不生效了。

在添加音效时,因为直接链接跳转的话,会来不及启动点击音效,所以只能使用超时函数设置了0.5秒的延迟,给音效播放留点时间。

也正是因为不是依赖于a标签的超链接,而是使用window.location.href来实现页面重定向,所以目前对于pjax的适配还是有些许不好。会在切换页面时打断全局音乐。
不过塞翁失马焉知非福,也正是因为如此,我又添加了自定义脚本的配置项接口。可以让读者自己开发各种各样的脚本来丰富菜单功能啦。

说到底,既然可以自定义脚本了,那应该也可以适配pjax实现无伤跳转才对。唉,果然还是太菜了

总之,这次的项目就先告一段落啦!

米娜桑,Link Start!

教程正文

点击查看教程正文
  1. 新建[Blogroot]\themes\butterfly\layout\includes\SAO-menu.pug
    #SAO-back
    #SAO-menu
    #SAO-menu-content
    #menu-list
    if theme.SAO_Utils.menu_list
    each item in theme.SAO_Utils.menu_list
    .menu-list-item(onclick='clickAudio()' onmouseover='Mouseover()' onmouseout='Mouseout()')
    if item.link
    i(class=item.icon onclick=`setTimeout(function(){linkStart('` +url_for(item.link)+ `')},500);`)= item.name
    else if item.action
    i(class=item.icon onclick=`setTimeout(function(){` + item.action + `},500);`)= item.name
    else
    i(class=item.icon)= item.name
    if item.child_list
    .menu-child(style=`top: -` + (30 * (item.child_list.length + 1) ) + `px;`)
    each child in item.child_list
    .menu-list-child(onclick='clickAudio()')
    if child.link
    i(class=child.icon onclick=`setTimeout(function(){linkStart('` +url_for(child.link)+ `')},500);`)= child.name
    else if child.action
    i(class=child.icon onclick=`setTimeout(function(){` + child.action + `},500);`)= child.name
    else
    i(class=child.icon)= child.name
    if theme.SAO_Utils.music.enable
    - var Launcher = theme.SAO_Utils.music.Launcher ? url_for(theme.SAO_Utils.music.Launcher) : 'https://cdn.jsdelivr.net/gh/Akilarlxh/Akilarlxh.github.io@bf_3.5.1_6/assets/Launcher.wav'
    - var Click = theme.SAO_Utils.music.Click ? url_for(theme.SAO_Utils.music.Click) : 'https://cdn.jsdelivr.net/gh/Akilarlxh/Akilarlxh.github.io@bf_3.5.1_6/assets/Click.wav'
    audio#SAOlauncher(src=Launcher)
    audio#SAOClick(src=Click)
    script(async src=url_for(theme.CDN.SAO_Utils))
  2. 新建[Blogroot]\themes\butterfly\source\css\_layout\SAO_Menu.styl:
    if hexo-config('SAO_Utils.enable')
    #SAO-back
    display none
    position fixed
    width 100%
    top 0
    left 0
    height 100%
    background rgba(3, 3, 3, 0.5)
    z-index 9999
    vertical-align super
    if hexo-config('SAO_Utils.ThreeD')
    .left
    transform rotate3d(-1, -1, 0, 35deg)!important
    .top
    transform rotate3d(1, 1, 0, 35deg)!important
    #SAO-menu
    display none
    position absolute
    border 1px solid
    border-image linear-gradient(to top, transparent, #f9f9f9 20%, #f9f9f9 80%, transparent) 0 0 0 1
    margin-left 30px
    width auto
    font-family Langar,-apple-system, sans-serif
    if hexo-config('SAO_Utils.ThreeD')
    transform rotate3d(-1, 1, 0, 35deg)
    animation Updown 1s linear infinite alternate
    @keyframes Updown
    from
    margin-top 20px
    to
    margin-top 10px
    &:before
    position absolute
    content ''
    border-right 25px solid #f9f9f9
    border-top 10px solid transparent
    border-bottom 10px solid transparent
    left -30px
    top calc(50% - 10px)
    &:after
    position absolute
    content ''
    border 3px solid #333
    border-radius 50%
    left -20px
    top 50%
    transform translateY(-50%)

    #SAO-menu-content
    max-height 300px
    overflow scroll
    width 430px
    padding-top 165px
    padding-bottom 15px
    &::-webkit-scrollbar
    display none

    #menu-list
    list-style-type none
    margin 0 5px
    padding 0
    width auto

    .menu-list-item
    background-color rgba(249, 249, 249, 0.79)
    width 200px
    padding 15px 25px
    margin-bottom 5px
    font-weight bolder
    color rgb(77, 72, 73)
    box-shadow 3px 3px 2px #888888
    height 50px
    border-radius 5px
    .active
    display: inline-block;
    i
    vertical-align super
    &::before
    margin-right 15px
    color: white;
    background: rgb(77, 72, 73);
    padding: 5px;
    border-radius: 50%;
    &:hover
    cursor pointer
    background-color #eda60c
    color #f9f9f9
    i
    &::before
    color: #eda60c;
    background: white;
    .menu-child
    display inline-block
    &:last-child
    margin-bottom 0

    .menu-list-child
    display display
    background-color rgba(249, 249, 249, 0.79)
    color #494748
    width 200px
    padding 15px 25px
    margin-bottom 5px
    position relative
    left -25px
    top 0px
    font-weight bolder
    color rgb(77, 72, 73)
    box-shadow 3px 3px 2px #888888
    height 50px
    border-radius 5px
    i
    vertical-align super
    &::before
    margin-right 15px
    color: white!important;
    background: rgb(77, 72, 73)!important;
    padding: 5px;
    border-radius: 50%;
    &:hover
    cursor pointer
    background-color #eda60c
    color #f9f9f9
    i
    &::before
    color: #eda60c!important;
    background: white!important;
    .menu-child
    display none
    position relative
    border 1px solid
    left 170px
    border-image linear-gradient(to top, transparent, #f9f9f9 20%, #f9f9f9 80%, transparent) 0 0 0 1
    margin-left 20px
    width auto
    height auto
    padding 10px 30px
    &:before
    position absolute
    content ''
    border-right 25px solid #f9f9f9
    border-top 10px solid transparent
    border-bottom 10px solid transparent
    left -30px
    top calc(50% - 10px)
    &:after
    position absolute
    content ''
    border 3px solid #333
    border-radius 50%
    left -20px
    top 50%
    transform translateY(-50%)
  3. 修改[Blogroot]\themes\butterfly\layout\includes\additional-js.pug,引入右键菜单网页元素,注意butterfly_v3.6.0取消了缓存配置,转为完全默认,需要将{cache:theme.fragment_cache}改为{cache: true}:
      if theme.pjax.enable
    !=partial('includes/third-party/pjax', {}, {cache:theme.fragment_cache})

    !=partial('includes/third-party/baidu_push', {}, {cache:theme.fragment_cache})

    + if theme.SAO_Utils.enable
    + !=partial('includes/SAO-menu', {}, {cache:theme.fragment_cache})
  4. 新建[Blogroot]\themes\butterfly\source\js\SAO_Menu.js,控制右键菜单的显隐。此处的脚本引入使用了async异步加载。因为全部都是触发类函数,在监听到相应的点击或悬停事件之前不会执行,所以甚至不会有加载完成后执行脚本的那段阻塞时间。
    window.document.oncontextmenu = function(event) {
    if (event.ctrlKey) return true; //ctrl+右键 使用原生右键
    if (/Android|webOS|BlackBerry/i.test(navigator.userAgent)) return true; //媒体选择
    return popMenu(event); //打开右键菜单
    };
    document.addEventListener("click", function(event) {
    var mymenu = document.getElementById('SAO-back');
    mymenu.style.display = "none";
    });
    //处理链接跳转的请求;调用了主题自带的pjax对象。其他主题需要另外适配。
    function linkStart(link){
    if (link.includes('https://') || link.includes('http://') ){
    window.location.href = link;
    }
    else{
    if (pjax){
    pjax.loadUrl(link);
    }
    else{
    window.location.href = link;
    }
    }
    }
    //点击菜单内元素播放点击音频
    function clickAudio() {
    var clickAudio = document.getElementById("SAOClick");
    if (clickAudio) {
    clickAudio.play();//有音频时播放
    }
    }
    //定义二级菜单显隐,监听鼠标悬停动作
    function Mouseover() {
    var thisChild = event.target.querySelector('.menu-child');
    if (thisChild) {
    thisChild.classList.add('active');
    }
    }
    function Mouseout() {
    var thisChild = event.target.querySelector('.menu-child');
    if (thisChild && thisChild.className.indexOf('active') > -1) {
    setTimeout(function() {
    thisChild.classList.remove('active');
    }, 100);
    }
    }
    function popMenu(event) {
    //播放菜单打开音乐
    var audio = document.getElementById("SAOlauncher");
    if (audio) {
    audio.play();//有音乐时打开
    }
    document.getElementById('SAO-back').style.display = "block";
    var mymenu = document.getElementById('SAO-menu');
    var menuContent = document.getElementById('SAO-menu-content');
    var screenWidth = document.documentElement.clientWidth || document.body.clientWidth;
    var screenHeight = document.documentElement.clientHeight || document.body.clientHeight;
    // 菜单显示
    mymenu.style.display = 'block';
    menuContent.scrollTop = '150';
    //根据当前位置决定菜单出现位置,确保菜单可完整显示
    if (event.clientX * 2 > screenWidth) {
    if ((event.clientX - menuContent.clientWidth) * 2 > screenWidth) {
    mymenu.style.left = (event.clientX - menuContent.clientWidth) + "px";//偏右时左移
    }
    else {
    mymenu.style.left = event.clientX + "px";
    }
    mymenu.classList.add('left');
    } else {
    mymenu.style.left = event.clientX + "px";
    mymenu.classList.remove('left');
    }
    if (event.clientY * 2 > screenHeight) {
    mymenu.style.top = (event.clientY - menuContent.clientHeight) + "px";//偏高时下降
    mymenu.classList.add('top');
    } else {
    mymenu.style.top = event.clientY + "px";
    mymenu.classList.remove('top');
    }
    if ((event.clientY * 2 > screenHeight) && (event.clientX * 2 > screenWidth)) {
    if (mymenu.className.indexOf('top') > -1) {
    mymenu.classList.remove('top');
    }
    if (mymenu.className.indexOf('left') > -1) {
    mymenu.classList.remove('left');
    }
    }
    return false; //屏蔽原生菜单
    }
  5. 修改[Blogroot]\_config.butterfly.yml,添加CDN配置项和菜单选项:
    • CDN配置项:
        CDN:
      # main
      main_css: /css/index.css
      jquery: https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js
      main: /js/main.js
      utils: /js/utils.js
      + # SAO_Utils
      + SAO_Utils: /js/SAO_Menu.js
    • SAO_Utils菜单配置项示例:
      # SAO_Utils右键菜单
      SAO_Utils:
      enable: true
      ThreeD: true
      music:
      enable: true
      Launcher:
      Click:
      menu_list:
      - name: Link Start
      icon: fa fa-link
      link: /link/
      action:
      child_list:
      - name: Message
      icon: fa fa-envelope
      link:
      action:
      child_list:
      - name: Tidio
      icon: fa fa-server
      link:
      action: openTidio()
      - name: Comments
      icon: fa fa-comments
      link: /comments/
      action:
  6. 因为这次的配置逻辑较为繁复,所以参数解释会比较多:
参数备选值参数释义
enabletrue , falsetrue为开启右键菜单,false为关闭右键菜单
ThreeDtrue , falsetrue为开启3D效果,false为关闭3D效果
music.enabletrue , falsetrue为开启点击音效,false为关闭点击音效
music.Launcher音乐文件的相对路径或外链右键点击打开菜单时的音效,留空则使用默认音效
music.Click音乐文件的相对路径或外链左键点击菜单选项时的音效,留空则使用默认音效
menu_list见下文菜单选项
menu_list.nametext菜单选项标题
menu_list.iconeg:fa fa-link菜单选项图标,使用fontawesome,也可以使用iconfont
menu_list.linkurl链接,站内建议使用相对路径,站外需要使用带协议的绝对路径,与action互斥,只能填写一个
menu_list.actionfunction点击动作,详见本帖拓展内容,与link互斥,只能填写一个
menu_list.child_list类似于menu_list仅一级菜单支持该配置项,其余下辖配置项与menu_list相同
child_list.nametext菜单选项标题
child_list.iconeg:fa fa-link菜单选项图标,使用fontawesome,也可以使用iconfont
child_list.linkurl链接,站内建议使用相对路径,站外需要使用带协议的绝对路径,与action互斥,只能填写一个
child_list.actionfunction点击动作,详见本帖拓展内容,与link互斥,只能填写一个

自定义脚本拓展

点击查看脚本拓展内容

糖果屋出品的右键菜单提供了自定义js配置,读者可以通过封装自己的js脚本,直接通过菜单选项调用。以下会分享几个简单示例。更多内容可以自行探索。希望可以启发读者,在评论区留下更多有趣的脚本。

使用方法:在上文的menu_list或者child_list配置项的action填写函数名即可正常调用。注意actionlink互斥。所以写了action就不要写link

- name: Mirror
icon: fa fa-indent
action: Mirror()
- name: Search
icon: fa fa-search
action: openSearch()
- name: Tidio
icon: fa fa-server
action: openTidio()

功能:针对gitee镜像站和当前站点的同篇文章跳转,记得更改链接。


function Mirror() {
let pathname;
let hostname;
let url;
pathname = window.location.pathname;
hostname = window.location.hostname;
if (hostname === 'akilar.top') {
url = "https://akilar.gitee.io" + pathname;
window.alert("即将前往糖果屋分店🍬");
window.location.href = url;
}
else if(hostname === 'akilar.gitee.io') {
url = "https://akilar.top" + pathname;
window.alert("正在返回糖果屋本部🍭!");
window.location.href = url;
}
else {
window.alert("Master,本地调试不需要跳转哦!🍫");
}
}

功能:打开local-search搜索按钮(提取自Butterfly源码,其他主题可能不生效)。


function openSearch() {
document.body.style.cssText = 'width: 100%;overflow: hidden'
document.querySelector('#local-search .search-dialog').style.display = 'block'
document.querySelector('#local-search-input input').focus()
btf.fadeIn(document.getElementById('search-mask'), 0.5)
if (!loadFlag) {
search(GLOBAL_CONFIG.localSearch.path)
loadFlag = true
}
// shortcut: ESC
document.addEventListener('keydown', function f (event) {
if (event.code === 'Escape') {
closeSearch()
document.removeEventListener('keydown', f)
}
})
}

功能:打开algolia搜索按钮(提取自Butterfly源码,其他主题可能不生效)。


function openAlgolia() {
document.body.style.cssText = 'width: 100%;overflow: hidden'
document.querySelector('#algolia-search .search-dialog').style.display = 'block'
document.querySelector('#algolia-search .ais-search-box--input').focus()
btf.fadeIn(document.getElementById('search-mask'), 0.5)
// shortcut: ESC
document.addEventListener('keydown', function f (event) {
if (event.code === 'Escape') {
closeSearch()
document.removeEventListener('keydown', f)
}
})
}

功能:打开Tidio在线聊天界面(提取自Butterfly源码,其他主题可能不生效)


function openTidio() {
window.tidioChatApi.show();window.tidioChatApi.open();
}

功能:若当前页面有评论区,则跳转评论区,若没有,则跳转到留言板页面,评论区的挂载ID和留言板路径可能不一致,请自己根据实际情况替换。


function ToComment(){
var hasComment = document.getElementById('post-comment');
if (hasComment){
window.location.href = '#post-comment'; //如果有评论区就跳转到评论区
}
else{
linkStart('/comments/');//如果没有,就跳转到留言板
//linkStart是本帖的SAO_Menu.js中带的跳转函数,用于pjax适配
}
}

功能:关闭当前页面。既然是SAO,怎么可以不致敬一下登出键呢?(对无痕窗口不生效,会提示scripts may close only the windows that were opened by them)


//关闭当前页面
function Logout(){
window.opener=null;
window.open('','_self');
window.close();
}

TO DO

SAO风格的右键菜单

二级菜单显隐逻辑适配

Ctrl+右键恢复原生菜单

适配pjax,站内跳转不打断全局音乐

补全左侧圆形列表;详见2.0

补全左侧角色属性栏样式;详见2.0

3D显示效果

添加浮动动态动画