本篇魔改会涉及进阶内容,对于没有基础的新手小白,择需查看进阶功能的教程。如需添加额外功能则需注意兼容性。

241226更新中,进行了优化和测试,大大减少了出现问题的情况发生。

效果预览

在本站PC端右键即可体验。

创建数据

小节开始前,提醒事项。
对于初次魔改新手,建议先过一遍:魔改前置教程:添加自定义css和js文件

  • 创建[主题目录]/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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
.js-pjax
#rightMenu
.rightMenu-group.rightMenu-small
.rightMenu-item#menu-backward
i.MeuiCat.icon-arrow-left-line
.rightMenu-item#menu-forward
i.MeuiCat.icon-arrow-right-line
.rightMenu-item#menu-refresh
i.MeuiCat.icon-restart-line
.rightMenu-item#menu-top
i.MeuiCat.icon-arrow-up-line
.rightMenu-group.rightMenuPost
if theme.readmode
.rightMenu-item#menu-reading
i.MeuiCat.icon-read-fill
span= '阅读模式'
//- .rightMenu-item#menu-commentBarrage
//- i.MeuiCat.icon-chat-fill
//- span= '评论弹窗'
.rightMenu-item#menu-postlink
i.MeuiCat.icon-external-link-fill
span= '分享本文'
.rightMenu-group.rightMenuPlugin
if theme.comments.use
.rightMenu-item#menu-commenttext
i.MeuiCat.icon-chat-new-fill
span= '引用评论'
.rightMenu-item#menu-copytext
i.MeuiCat.icon-copy-fill
span= '复制内容'
.rightMenu-item#menu-pastetext
i.MeuiCat.icon-clipboard-fill
span= '粘贴内容'
//- if theme.search.use
//- .rightMenu-item#menu-search
//- i.MeuiCat.icon-search-line
//- span= '站内搜索'
.rightMenu-item#menu-searchBaidu
i.MeuiCat.icon-baidu-fill
span= '百度搜索'
.rightMenu-item#menu-newwindow
i.MeuiCat.icon-window-fill
span= '新建窗口打开'
.rightMenu-item#menu-copylink
i.MeuiCat.icon-link-m-line
span= '复制链接地址'
.rightMenu-item#menu-copyimg
i.MeuiCat.icon-copy-fill
span= '复制此图片'
.rightMenu-item#menu-downloadimg
i.MeuiCat.icon-download-cloud-fill
span= '下载此图片'
.rightMenu-group.rightMenuOther
//- a.rightMenu-item#menu-randomPost
//- i.MeuiCat.icon-dice-fill
//- span= '随便逛逛'
if theme.darkmode.enable
.rightMenu-item#menu-darkmode
i.MeuiCat.icon-moon-clear-fill
span= '显示模式'
if theme.translate.enable
a.rightMenu-item#menu-translate
i.MeuiCat.icon-panben-line
span= '繁简转换'
.rightMenu-item#menu-asidehide
i.MeuiCat.icon-side-bar-fill
span= '边栏控制'
.rightMenu-group
a.rightMenu-item(href="/privacy/")
i.MeuiCat.icon-shield-user-fill
span= '隐私协议'
a.rightMenu-item(href="/cc/")
i.MeuiCat.icon-creative-commons-fill
span= '版权协议'
#rightMenu-mask
  • 新增[主题目录]/layout/includes/layout.pug页面内容。
1
2
3
4
5
6
7
8
  ···

else
include ./404.pug

include ./rightside.pug
+ !=partial('includes/rightmenu',{}, {cache:true})
include ./additional-js.pug
  • 新建[主题目录]/source/css/_layout/rightmenu.styl样式文件,并新增以下内容。
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
68
69
70
71
72
#rightMenu
position fixed
display none
padding 0 .25rem
background-color var(--icat-mask)
backdrop-filter blur(20px) saturate(1.5)
border var(--icat-border-always)
border-radius 8px
z-index 91
box-shadow var(--icat-shadow-border)
transition border 0.3s

&:hover
border var(--icat-border-main)
box-shadow var(--icat-shadow-main)

.rightMenu-group
padding 6px 6px

&:not(:nth-last-child(1))
border-bottom 1px dashed var(--icat-theme-op)

&.rightMenu-small
display flex
align-items center
gap .5rem

&:not(.rightMenu-small)
.rightMenu-item
padding 0 4px
height 40px

&:not(:last-child)
margin-bottom 4px

*
height 40px
line-height 40px

.rightMenu-item
display flex
align-items center
border-radius 6px
transition .3s
cursor pointer

&:hover
background-color var(--icat-theme)
color var(--icat-card-bg)

i
display inline-block
text-align center
line-height 30px
width 30px
height 30px
padding 0 5px

#rightMenu .rightMenu-group .rightMenu-item .icat-refresh,
#rightMenu .rightMenu-group .rightMenu-item .icat-changing-over,
#rightMenu .rightMenu-group .rightMenu-item .icat-simple-complex
font-weight 900

#rightMenu-mask
position fixed
width 100vw
height 100vh
background 0 0
top 0
left 0
display none
z-index 90
  • 打开[博客根目录]/source/js/meuicat.js文件,新增以下内容。
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
/**
* RightMenu Class Function
* author: MeuiCat(yife.Liang)
* date: 2024-12-09
*/

class RightMenu {
constructor() {
this.selectTextNow = ""
this.domhref = ""
this.domImgSrc = ""
this.globalEvent = null
this.downloadimging = false
this.rmWidth = 0
this.rmHeight = 0
this.toLocal = btf.saveToLocal.get('rightside-status')
this.isEnabled = !this.toLocal || this.toLocal !== 'off' ? true : false
this.initialize()
window.rightMenu = undefined
}

initialize() {
window.addEventListener('load', () => {
this.addRightMenuClickEvent()
document.onmouseup = document.ondbclick = this.selceText.bind(this)
btf.addGlobalFn('pjaxComplete', () => { this.addRightMenuClickEvent() }, 'addRightMenuClickEvent')
})

window.oncontextmenu = (e) => this.onContextMenu(e)
}

enable() {
this.isEnabled = true
typeof !window.rightMenu && (window.rightMenu = {
getSelectTextNow: () => rightMenu.getSelectTextNow()
})
}

disable() {
this.isEnabled = false
typeof window.rightMenu && (window.rightMenu = undefined)
}

addRightMenuClickEvent() {
const menuItems = {
"rightMenu-mask": [this.hideRightMenu.bind(this)],
"menu-backward": [() => { window.history.back(); this.hideRightMenu() }],
"menu-forward": [() => { window.history.forward(); this.hideRightMenu() }],
"menu-refresh": [() => { this.hideRightMenu(); window.location.reload() }],
"menu-top": [() => { this.hideRightMenu(); btf.scrollToDest(0, 500) }],
"menu-darkmode": [() => { this.hideRightMenu(); document.querySelector('#darkmode').click() }],
"menu-translate": [() => { this.hideRightMenu(); typeof rightSideFn === 'object' && rightSideFn.translateLink() }],
"menu-reading": [() => { this.hideRightMenu(); document.querySelector('#readmode').click() }],
"menu-postlink": [this.copyPostUrl.bind(this)],
"menu-search": [this.hideRightMenu.bind(this)],
"menu-asidehide": [() => { this.hideRightMenu(); document.querySelector('#hide-aside-btn').click() }],
"menu-copytext": [() => { this.rightmenuCopyText(this.selectTextNow); GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow("复制成功,复制和转载请标注本文地址") }],
"menu-commenttext": [() => { commentText(this.selectTextNow); this.hideRightMenu() }],
"menu-searchBaidu": [this.searchBaidu.bind(this)],
"menu-newwindow": [() => { window.open(this.domhref); this.hideRightMenu() }],
"menu-copylink": [() => { this.rightmenuCopyText(this.domhref); GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow("已复制链接地址") }],
"menu-pastetext": [this.pasteText.bind(this)],
"menu-copylinkimg": [() => { this.rightmenuCopyText(this.domImgSrc); GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow("已复制图片链接") }],
"menu-copyimg": [() => { this.copyImage(this.domImgSrc) }],
"menu-downloadimg": [() => { this.downloadImage(this.domImgSrc, "MeuiCat") }],
"menu-commentBarrage": [() => { this.hideRightMenu(); typeof comment === 'object' && comment.closeBarrage() }],
"menu-randomPost": [() => { this.hideRightMenu(); toRandomPost() }]
}

for (let id in menuItems) {
const element = document.getElementById(id)
if (element) menuItems[id].forEach(fn => { element.addEventListener("click", fn) })
}

document.getElementById("rightMenu-mask").addEventListener("contextmenu", () => {
this.hideRightMenu()
return false
})
}

reloadrmSize() {
const menu = document.getElementById("rightMenu")
this.rmWidth = menu.offsetWidth
this.rmHeight = menu.offsetHeight
}

showRightMenu(more, mouseY = 0, mouseX = 0) {
const rightMenu = document.getElementById("rightMenu")

if (more) {
rightMenu.style.display = "block"
this.reloadrmSize()

mouseX + this.rmWidth + 20 > window.innerWidth && (mouseX -= this.rmWidth + 20)
mouseY + this.rmHeight > window.innerHeight && (mouseY -= mouseY + this.rmHeight - window.innerHeight + 20)
rightMenu.style.top = mouseY + "px"
rightMenu.style.left = mouseX + "px"

document.getElementById("rightMenu-mask").style.display = "flex"
this.stopMaskScroll()
} else {
rightMenu.style.display = "none"
}
}

hideRightMenu() {
this.showRightMenu(false)
document.getElementById("rightMenu-mask").style.display = "none"
}

getSelectTextNow() {
return this.selectTextNow
}

stopMaskScroll() {
const elements = ["rightMenu-mask", "rightMenu"]
elements.forEach(id => {
const element = document.getElementById(id)
if (element) element.addEventListener("mousewheel", this.hideRightMenu.bind(this), false)
})
}

selceText() {
this.selectTextNow = window.getSelection ? window.getSelection().toString() : document.selection ? document.selection.createRange().text : ""
}

copyPostUrl() {
this.copyUrl(window.location.href)
GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow("复制本页链接地址成功", false, 2000)
this.hideRightMenu()
}

searchBaidu() {
GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow("即将跳转到百度搜索", false, 2000)
setTimeout(() => { window.open("https://www.baidu.com/s?wd=" + this.selectTextNow) }, 2000)
this.hideRightMenu()
}

pasteText() {
if (navigator.clipboard) navigator.clipboard.readText().then((text) => this.insertAtCaret(this.globalEvent.target, text))
this.hideRightMenu()
}

insertAtCaret(input, text) {
const start = input.selectionStart
const end = input.selectionEnd
if (document.selection) {
input.focus()
document.selection.createRange().text = text
input.focus()
} else if (start || start === 0) {
const scrollTop = input.scrollTop
input.value = input.value.substring(0, start) + text + input.value.substring(end, input.value.length)
input.focus()
input.selectionStart = start + text.length
input.selectionEnd = start + text.length
input.scrollTop = scrollTop
} else {
input.value += text
input.focus()
}
}

rightmenuCopyText(text) {
if (navigator.clipboard) navigator.clipboard.writeText(text)
this.hideRightMenu()
}

imageToBlob(imgSrc, options = {}) {
const { fileName, action = "download", callback } = options
const canvas = document.createElement("canvas")
const context = canvas.getContext("2d")
const img = new Image()

img.crossOrigin = 'anonymous'
img.src = imgSrc

return new Promise((resolve, reject) => {
img.onload = function () {
canvas.width = img.width
canvas.height = img.height
context.drawImage(this, 0, 0)

if (action === 'copy') {
canvas.toBlob((blob) => {
const item = new ClipboardItem({ "image/png": blob })
navigator.clipboard.write([item]).then(() => {
callback && callback("复制成功!图片已添加盲水印,请遵守版权协议")
resolve()
})
}, "image/png", 0.75)
} else if (action === 'download') {
const dataURL = canvas.toDataURL("image/png")
const a = document.createElement("a")
const event = new MouseEvent("click")
a.download = fileName || "image.png"
a.href = dataURL
a.dispatchEvent(event)
callback && callback("图片已添加盲水印,请遵守版权协议")
resolve()
}
}

img.onerror = () => {
callback && callback("图片加载失败,请检查跨域或网络连接")
reject(new Error("Image load error"))
}
})
}

copyImage(imgSrc) {
this.hideRightMenu()
const time = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1" ? 0 : 5000
if (this.downloadimging) return GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow("有正在进行中的任务,请稍后再试", false, time)
this.downloadimging = true

setTimeout(() => {
this.imageToBlob(imgSrc, {
action: "copy",
callback: (msg) => {
GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(msg)
this.downloadimging = false
}
})
}, time)
}

downloadImage(imgSrc, fileName) {
this.hideRightMenu()
if (this.downloadimging) return GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow("有正在进行中的下载,请稍后再试")
this.downloadimging = true
GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow("正在下载中,请稍后", false, 10000)

setTimeout(() => {
this.imageToBlob(imgSrc, {
action: "download",
fileName: fileName,
callback: (msg) => {
GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(msg)
this.downloadimging = false
}
})
}, 10000)
}

onContextMenu(e) {
if (this.isEnabled && document.body.clientWidth > 768) {
const mouseX = e.clientX + 12,
mouseY = e.clientY,
target = e.target,
menus = {
rightMenuPost: document.querySelector(".rightMenuPost"),
rightMenuPlugin: document.querySelector(".rightMenuPlugin"),
rightMenuOther: document.querySelector(".rightMenuOther"),
},
menuItems = {
menuCopyText: { element: document.getElementById("menu-copytext"), condition: () => this.selectTextNow && window.getSelection },
menuCommentText: { element: document.getElementById("menu-commenttext"), condition: () => this.selectTextNow && document.getElementById("post-comment") },
menuSearch: { element: document.getElementById("menu-search"), condition: () => this.selectTextNow },
menuSearchBaidu: { element: document.getElementById("menu-searchBaidu"), condition: () => this.selectTextNow },
menuNewWindow: { element: document.getElementById("menu-newwindow"), condition: () => !!target.href },
menuCopyLink: { element: document.getElementById("menu-copylink"), condition: () => !!target.href },
menuCopyImage: { element: document.getElementById("menu-copyimg"), condition: () => !!target.currentSrc },
menuDownloadImage: { element: document.getElementById("menu-downloadimg"), condition: () => !!target.currentSrc },
menuPasteText: { element: document.getElementById("menu-pastetext"), condition: () => ["input", "textarea"].includes(target.tagName.toLowerCase()) },
}

this.domhref = target.href || ""
this.domImgSrc = target.src || ""

Object.values(menuItems).forEach(({ element, condition }) => { if (element) element.style.display = condition() ? "block" : "none" })

const isTextSelected = Object.values(menuItems).some(({ condition }) => condition())
menus.rightMenuPost.style.display = isTextSelected || !!target.href || !!target.currentSrc ? "none" : document.querySelector("#body-wrap.post") ? "block" : "none"
menus.rightMenuPlugin.style.display = isTextSelected ? "block" : "none"
menus.rightMenuOther.style.display = isTextSelected ? "none" : "block"

const isReadMode = document.querySelector(".read-mode");
["menu-commentBarrage", "menu-postlink", "menu-randomPost", "menu-asidehide"].forEach((id) => {
const element = document.getElementById(id)
if (element) element.style.display = isReadMode ? "none" : "block"
})

this.showRightMenu(e, mouseY, mouseX)
e.preventDefault()
return false
} else {
return true
}
}
}

const rightMenu = new RightMenu()

复制图片下载图片 这两个功能都需要浏览器支持ClipboardItemAPI,并且图片服务器正确设置CORS跨域。
如遇错误提示,大概率就是涉及到跨域问题。自行设置解决。

进阶功能

rightmenu.pug文件里有几处是注释掉了的功能,需要搭配其他的魔改教程使用。择需启用。

评论弹窗

此功能需要搭配Butterfly的魔改教程:评论弹窗进行使用。

  • 移除rightmenu.pug文件里第17至19行的注释,为启用评论弹窗功能。

Butterfly的魔改教程:评论弹窗文章教程中,新增了一个配置可控制评论弹窗的开启和关闭。

  • comment.closeBarrage():开启或关闭评论弹窗功能。

择需自行放置按钮控制评论弹窗的开启和关闭。默认情况下为开启状态。
当本地存储的comment-pop值为no时,表示全站关闭评论弹窗功能。

站内搜索

你需要搭配搜索功能使用,提前配置好你的搜索功能:Butterfly官方文档 - 搜索

如果你使用的是Algolia服务,则修改algolia.js文件

  • 打开[主题目录]/source/js/search/algolia.js文件,并新增以下内容。
1
2
3
4
5
6
7
8
9
10
11
12
  const searchClickFn = () => {
btf.addEventListenerPjax(document.querySelector('#search-button > .search'), 'click', openSearch)

+ const rmSearch = () => {
+ openSearch()
+ const Searchinput = document.querySelector('.ais-SearchBox-input'), evt = document.createEvent('HTMLEvents')
+ evt.initEvent('input', true, true)
+ Searchinput.value = window.rightMenu.getSelectTextNow()
+ Searchinput.dispatchEvent(evt)
+ }
+ btf.addEventListenerPjax(document.querySelector('#menu-search'), 'click', rmSearch)
}

如果你使用的是本地搜索,则修改local-search.js文件

  • 打开[主题目录]/source/js/search/local-search.js文件,并新增以下内容。
1
2
3
4
5
6
7
8
9
10
11
12
  const searchClickFn = () => {
btf.addEventListenerPjax(document.querySelector('#search-button > .search'), 'click', openSearch)

+ const rmSearch = () => {
+ openSearch()
+ const Searchinput = document.querySelector('.local-search-box--input'), evt = document.createEvent('HTMLEvents')
+ evt.initEvent('input', true, true)
+ Searchinput.value = window.rightMenu.getSelectTextNow()
+ Searchinput.dispatchEvent(evt)
+ }
+ btf.addEventListenerPjax(document.querySelector('#menu-search'), 'click', rmSearch)
}
  • 移除rightmenu.pug文件里第34至37行的注释,为启用站内搜索功能。

随便逛逛

此功能需要搭配Butterfly的魔改教程:随机阅读一篇文章进行使用。

  • 移除rightmenu.pug文件里第54至56行的注释,为启用随便逛逛功能。

关闭右键功能

  • rightMenu.disable():关闭右键功能。

  • rightMenu.enable():开启右键功能。

新增了一个配置使得右键功能可进行控制开启和关闭。

可以自行放置按钮控制右键功能的开启和关闭。默认情况下为开启状态。
当本地存储的rightside-status值为no时,右键功能将关闭。

1
2
3
4
5
6
const rightMenu: () => {
const right = window.rightMenu
right ? rightMenu.disable() : rightMenu.enable()
btf.saveToLocal.set('rightside-status', right ? 'off' : 'no', 2)
rightSideFn.init('console-right')
}

旧版教程

参考文献

更新历史

  • 240410 更新:新增无jquery依赖的版本,使用原生js实现;修复已知图片处理的问题

  • 241226 更新:移除jquery依赖的版本教程;调整并使用使用Class(类)声明;新增进阶小节教程以及关闭右键功能