PageSourceSearch

https://zryyyy.github.io/js/plugins/rightMenus.js

js zryyyy.github.io collected 2026-10-03 10:15:24 UTC 21,606 bytes, 620 lines download raw bytes

1
2const RightMenus = {
3  defaultEvent: ['copyText', 'copyLink', 'copyPaste', 'copyAll', 'copyCut', 'copyImg', 'printMode', 'readMode'],
4  defaultGroup: ['navigation', 'inputBox', 'seletctText', 'elementCheck', 'elementImage', 'articlePage'],
5  messageRightMenu: volantis.GLOBAL_CONFIG.plugins.message.enable && volantis.GLOBAL_CONFIG.plugins.message.rightmenu.enable,
6  corsAnywhere: volantis.GLOBAL_CONFIG.plugins.rightmenus.options.corsAnywhere,
7  urlRegx: /^((https|http)?:\/\/)+[A-Za-z0-9]+\.[A-Za-z0-9]+[\/=\?%\-&_~`@[\]\':+!]*([^<>\"\"])*$/,
8  imgRegx: /\.(jpe?g|png|webp|svg|gif|jifi)(-|_|!|\?|\/)?.*$/,
9
10  /**
11   * 加载右键菜单
12   */
13  initialMenu: () => {
14    RightMenus.fun.init();
15  },
16
17  /**
18   * 读取剪切板
19   * @returns text
20   */
21  readClipboard: async () => {
22    let clipboardText;
23    const result = await navigator.permissions.query({ name: 'clipboard-read' });
24    switch (result.state) {
25      case 'granted':
26      case 'prompt':
27        clipboardText = await navigator.clipboard.readText()
28        break;
29      default:
30        window.clipboardRead = false;
31        break;
32    }
33    return clipboardText;
34  },
35
36  /**
37   * 写入文本到剪切板
38   * @param {String} text
39   */
40  writeClipText: text => {
41    return navigator.clipboard
42      .writeText(text)
43      .then(() => {
44        return Promise.resolve()
45      })
46      .catch(err => {
47        return Promise.reject(err)
48      })
49  },
50
51  /**
52   * 写入图片到剪切板
53   * @param {*} link
54   * @param {*} success
55   * @param {*} error
56   */
57  writeClipImg: async (link, success, error) => {
58    const image = new Image;
59    image.crossOrigin = "Anonymous";
60    image.addEventListener('load', () => {
61      let canvas = document.createElement("canvas");
62      let context = canvas.getContext("2d");
63      canvas.width = image.width;
64      canvas.height = image.height;
65      context.drawImage(image, 0, 0);
66      canvas.toBlob(blob => {
67        navigator.clipboard.write([
68          new ClipboardItem({ 'image/png': blob })
69        ]).then(e => {
70          success(e)
71        }).catch(e => {
72          error(e)
73        })
74      }, 'image/png')
75    }, false)
76    image.src = `${link}?(lll¬ω¬)`;
77  },
78
79  /**
80   * 粘贴文本到剪切板
81   * @param {*} elemt
82   * @param {*} value
83   */
84  insertAtCaret: (elemt, value) => {
85    const startPos = elemt.selectionStart,
86      endPos = elemt.selectionEnd;
87    if (document.selection) {
88      elemt.focus();
89      var sel = document.selection.createRange();
90      sel.text = value;
91      elemt.focus();
92    } else {
93      if (startPos || startPos == '0') {
94        var scrollTop = elemt.scrollTop;
95        elemt.value = elemt.value.substring(0, startPos) + value + elemt.value.substring(endPos, elemt.value.length);
96        elemt.focus();
97        elemt.selectionStart = startPos + value.length;
98        elemt.selectionEnd = startPos + value.length;
99        elemt.scrollTop = scrollTop;
100      } else {
101        elemt.value += value;
102        elemt.focus();
103      }
104    }
105  }
106}
107
108/**
109 * 事件处理区域
110 */
111RightMenus.fun = (() => {
112  const rightMenuConfig = volantis.GLOBAL_CONFIG.plugins.rightmenus;
113
114  const
115    fn = {},
116    _rightMenuWrapper = document.getElementById('rightmenu-wrapper'),
117    _rightMenuContent = document.getElementById('rightmenu-content'),
118    _rightMenuList = document.querySelectorAll('#rightmenu-content li.menuLoad-Content'),
119    _rightMenuListWithHr = document.querySelectorAll('#rightmenu-content li, #rightmenu-content hr, #menuMusic'),
120    _readBkg = document.getElementById('read_bkg'),
121    _menuMusic = document.getElementById('menuMusic'),
122    _backward = document.querySelector('#menuMusic .backward'),
123    _toggle = document.querySelector('#menuMusic .toggle'),
124    _forward = document.querySelector('#menuMusic .forward');
125
126  // 公共数据
127  let globalData = {
128    mouseEvent: null,
129    isInputBox: false,
130    selectText: '',
131    inputValue: '',
132    isLink: false,
133    linkUrl: '',
134    isMediaLink: false,
135    mediaLinkUrl: '',
136    isImage: false,
137    isArticle: false,
138    pathName: '',
139    isReadClipboard: true,
140    isShowMusic: false,
141    statusCheck: false
142  }
143  const globalDataBackup = Object.assign({}, globalData);
144
145  /**
146   * 初始化监听事件处理
147   */
148  fn.initEvent = () => {
149    fn.elementAppend();
150    fn.contextmenu();
151    fn.menuEvent();
152  }
153
154  /**
155   * 预置元素设定
156   */
157  fn.elementAppend = () => {
158    // 阅读模式
159    if (_readBkg) _readBkg.parentNode.removeChild(_readBkg);
160    const readBkg = document.createElement("div");
161    readBkg.className = "common_read_bkg common_read_hide";
162    readBkg.id = "read_bkg";
163    window.document.body.appendChild(readBkg);
164  }
165
166  /**
167   * 右键菜单位置设定
168   * @param {*} event
169   */
170  fn.menuPosition = (event) => {
171    try {
172      let mouseClientX = event.clientX;
173      let mouseClientY = event.clientY;
174      let screenWidth = document.documentElement.clientWidth || document.body.clientWidth;
175      let screenHeight = document.documentElement.clientHeight || document.body.clientHeight;
176
177      _rightMenuWrapper.style.display = 'block';
178      fn.menuControl(event);
179
180      let menuWidth = _rightMenuContent.offsetWidth;
181      let menuHeight = _rightMenuContent.offsetHeight;
182      let showLeft = mouseClientX + menuWidth > screenWidth ? mouseClientX - menuWidth + 10 : mouseClientX;
183      let showTop = mouseClientY + menuHeight > screenHeight ? mouseClientY - menuHeight + 10 : mouseClientY;
184      showTop = mouseClientY + menuHeight > screenHeight && showTop < menuHeight && mouseClientY < menuHeight ?
185        showTop + (screenHeight - menuHeight - showTop - 10) : showTop;
186      _rightMenuWrapper.style.left = `${showLeft}px`;
187      _rightMenuWrapper.style.top = `${showTop}px`;
188      if (volantis.GLOBAL_CONFIG.plugins.message.rightmenu.notice) fn.menuNotic();
189    } catch (error) {
190      console.error(error);
191      fn.hideMenu();
192      return true;
193    }
194    return false;
195  }
196
197  /**
198   * 菜单项控制
199   * @param {*} event
200   */
201  fn.menuControl = (event) => {
202    fn.globalDataSet(event);
203    if (!!_menuMusic) _menuMusic.style.display = globalData.isShowMusic ? 'block' : 'none';
204    _rightMenuList.forEach(item => {
205      item.style.display = 'none';
206      const nodeName = item.firstElementChild.nodeName;
207      const groupName = item.firstElementChild.getAttribute('data-group');
208      const itemEvent = item.firstElementChild.getAttribute('data-event');
209      if (globalData.statusCheck || globalData.isArticle) {
210        switch (groupName) {
211          case 'inputBox':
212            if (globalData.isInputBox) {
213              item.style.display = 'block';
214              if (itemEvent === 'copyCut' && !globalData.selectText) item.style.display = 'none';
215              if (itemEvent === 'copyAll' && !globalData.inputValue) item.style.display = 'none';
216              if (itemEvent === 'copyPaste' && !globalData.isReadClipboard) item.style.display = 'none';
217            }
218            break;
219          case 'seletctText':
220            if (!!globalData.selectText) item.style.display = 'block';
221            break;
222          case 'elementCheck':
223            if (globalData.isLink || globalData.isMediaLink) item.style.display = 'block';
224            break;
225          case 'elementImage':
226            if (globalData.isImage) item.style.display = 'block';
227            break;
228          case 'articlePage':
229            if (globalData.isArticle) item.style.display = 'block';
230            break;
231          default:
232            item.style.display = nodeName === 'A'
233              ? globalData.isArticle && !globalData.statusCheck && rightMenuConfig.options.articleShowLink
234                ? 'block'
235                : 'none'
236              : 'block';
237            break;
238        }
239      } else if (nodeName === 'A' || RightMenus.defaultGroup.every(item => { return groupName !== item })) {
240        item.style.display = 'block';
241      }
242    })
243
244    // 执行外部事件
245    volantis.mouseEvent = event;
246    volantis.rightmenu.method.handle.start()
247
248    // 过滤 HR 元素
249    let elementHrItem = { item: null, hide: true };
250    _rightMenuListWithHr.forEach((item) => {
251      if (item.nodeName === "HR") {
252        item.style.display = 'block';
253        if (!elementHrItem.item) {
254          elementHrItem.item = item;
255          return;
256        }
257        if (elementHrItem.hide || elementHrItem.item.nextElementSibling.nodeName === "hr") {
258          elementHrItem.item.style.display = 'none';
259        }
260        elementHrItem.item = item;
261        elementHrItem.hide = true;
262      } else {
263        if (item.style.display === 'block' && elementHrItem.hide) {
264          elementHrItem.hide = false;
265        }
266      }
267    })
268    if (!!elementHrItem.item && elementHrItem.hide) elementHrItem.item.style.display = 'none';
269  }
270
271  /**
272   * 元素状态判断/全局数据设置
273   * @param {*} event
274   */
275  fn.globalDataSet = (event) => {
276    globalData = Object.assign({}, globalDataBackup);
277    globalData.mouseEvent = event;
278    globalData.selectText = window.getSelection().toString();
279
280    // 判断是否为输入框
281    if (event.target.tagName.toLowerCase() === 'input' || event.target.tagName.toLowerCase() === 'textarea') {
282      globalData.isInputBox = true;
283      globalData.inputValue = event.target.value;
284    }
285
286    // 判断是否允许读取剪切板
287    if (globalData.isInputBox && window.clipboardRead === false) {
288      globalData.isReadClipboard = false;
289    }
290
291    // 判断是否包含链接
292    if (!!event.target.href && RightMenus.urlRegx.test(event.target.href)) {
293      globalData.isLink = true;
294      globalData.linkUrl = event.target.href;
295    }
296
297    // 判断是否包含媒体链接
298    if (!!event.target.currentSrc && RightMenus.urlRegx.test(event.target.currentSrc)) {
299      globalData.isMediaLink = true;
300      globalData.mediaLinkUrl = event.target.currentSrc;
301    }
302
303    // 判断是否为图片地址
304    if (globalData.isMediaLink && RightMenus.imgRegx.test(globalData.mediaLinkUrl)) {
305      globalData.isImage = true;
306    }
307
308    // 判断是否为文章页面
309    if (!!(document.querySelector('#post.article') || null)) {
310      globalData.isArticle = true;
311      globalData.pathName = window.location.pathname;
312    }
313
314    // 判断是否显示音乐控制器
315    if (volantis.GLOBAL_CONFIG.plugins.aplayer?.enable
316      && typeof RightMenuAplayer !== 'undefined'
317      && RightMenuAplayer.APlayer.player !== undefined) {
318      if (rightMenuConfig.options.musicAlwaysShow
319        || RightMenuAplayer.APlayer.status === 'play'
320        || RightMenuAplayer.APlayer.status === 'undefined') {
321        globalData.isShowMusic = true;
322      }
323    }
324
325    // 设定校验状态
326    if (!!globalData.selectText || globalData.isInputBox || globalData.isLink || globalData.isMediaLink) {
327      globalData.statusCheck = true;
328    }
329  }
330
331  /**
332   * 全局右键监听函数
333   */
334  fn.contextmenu = () => {
335    window.document.oncontextmenu = (event) => {
336      if (event.ctrlKey || document.body.offsetWidth <= 500) {
337        fn.hideMenu();
338        return true;
339      }
340      return fn.menuPosition(event);
341    }
342
343    _rightMenuWrapper.oncontextmenu = (event) => {
344      event.stopPropagation();
345      event.preventDefault();
346      return false;
347    }
348
349    window.removeEventListener('blur', fn.hideMenu);
350    window.addEventListener('blur', fn.hideMenu);
351    document.body.removeEventListener('click', fn.hideMenu);
352    document.body.addEventListener('click', fn.hideMenu);
353  }
354
355  /**
356   * 菜单项事件处理函数
357   */
358  fn.menuEvent = () => {
359    _rightMenuList.forEach(item => {
360      let eventName = item.firstElementChild.getAttribute('data-event');
361      const id = item.firstElementChild.getAttribute('id');
362      const groupName = item.firstElementChild.getAttribute('data-group');
363      if (item.firstElementChild.nodeName === "A") return;
364      item.addEventListener('click', () => {
365        try {
366          if (RightMenus.defaultEvent.every(item => { return eventName !== item })) {
367            if (groupName === 'seletctText') {
368              RightMenusFunction[id](globalData.selectText)
369            } else if (groupName === 'elementCheck') {
370              RightMenusFunction[id](globalData.isLink ? globalData.linkUrl : globalData.mediaLinkUrl)
371            } else if (groupName === 'elementImage') {
372              RightMenusFunction[id](globalData.mediaLinkUrl)
373            } else {
374              RightMenusFunction[id]()
375            }
376          } else {
377            fn[eventName]()
378          }
379        } catch (error) {
380          if (volantis.GLOBAL_CONFIG.debug === "rightMenus") {
381            console.error({
382              id: id,
383              error: error,
384              globalData: globalData,
385              groupName: groupName,
386              eventName: eventName
387            });
388          }
389          if (RightMenus.messageRightMenu) {
390            VolantisApp.message('错误提示', error, {
391              icon: rightMenuConfig.options.iconPrefix + ' fa-exclamation-square red',
392              time: '15000'
393            });
394          }
395        }
396      })
397    })
398
399    if (_forward && _toggle && _forward) {
400      _backward.onclick = (e) => {
401        e.preventDefault();
402        e.stopPropagation();
403        RightMenuAplayer.aplayerBackward();
404      }
405      _toggle.onclick = (e) => {
406        e.preventDefault();
407        e.stopPropagation();
408        RightMenuAplayer.aplayerToggle();
409      }
410      _forward.onclick = (e) => {
411        e.preventDefault();
412        e.stopPropagation();
413        RightMenuAplayer.aplayerForward();
414      }
415    }
416  }
417
418  /**
419   * 隐藏菜单显示
420   */
421  fn.hideMenu = () => {
422    _rightMenuWrapper.style.display = null;
423    _rightMenuWrapper.style.left = null;
424    _rightMenuWrapper.style.top = null;
425  }
426
427  /**
428   * 右键菜单覆盖提示
429   */
430  fn.menuNotic = () => {
431    const NoticeRightMenu = localStorage.getItem('NoticeRightMenu') === 'true';
432    if (RightMenus.messageRightMenu && !NoticeRightMenu)
433      VolantisApp.message('右键菜单', '唤醒原系统菜单请使用:<kbd>Ctrl</kbd> + <kbd>右键</kbd>', {
434        icon: rightMenuConfig.options.iconPrefix + ' fa-exclamation-square red',
435        displayMode: 1,
436        time: 9000
437      }, () => {
438        localStorage.setItem('NoticeRightMenu', 'true')
439      });
440  }
441
442  fn.copyText = () => {
443    VolantisApp.utilWriteClipText(globalData.selectText)
444      .then(() => {
445        if (RightMenus.messageRightMenu) {
446          VolantisApp.messageCopyright();
447        }
448      }).catch(e => {
449        if (RightMenus.messageRightMenu) {
450          VolantisApp.message('系统提示', e, {
451            icon: rightMenuConfig.options.iconPrefix + ' fa-exclamation-square red',
452            displayMode: 1,
453            time: 9000
454          });
455        }
456      })
457  }
458
459  fn.copyLink = () => {
460    VolantisApp.utilWriteClipText(globalData.linkUrl || globalData.mediaLinkUrl)
461      .then(() => {
462        if (RightMenus.messageRightMenu) {
463          VolantisApp.messageCopyright();
464        }
465      }).catch(e => {
466        if (RightMenus.messageRightMenu) {
467          VolantisApp.message('系统提示', e, {
468            icon: rightMenuConfig.options.iconPrefix + ' fa-exclamation-square red',
469            displayMode: 1,
470            time: 9000
471          });
472        }
473      })
474  }
475
476  fn.copyAll = () => {
477    globalData.mouseEvent.target.select();
478  }
479
480  fn.copyPaste = async () => {
481    const result = await RightMenus.readClipboard() || '';
482    if (RightMenus.messageRightMenu && window.clipboardRead === false) {
483      VolantisApp.message('系统提示', '未授予剪切板读取权限!');
484    } else if (RightMenus.messageRightMenu && result === '') {
485      VolantisApp.message('系统提示', '仅支持复制文本内容!');
486    } else {
487      RightMenus.insertAtCaret(globalData.mouseEvent.target, result);
488    }
489  }
490
491  fn.copyCut = () => {
492    const statrPos = globalData.mouseEvent.target.selectionStart;
493    const endPos = globalData.mouseEvent.target.selectionEnd;
494    const inputStr = globalData.inputValue;
495    fn.copyText(globalData.selectText);
496    globalData.mouseEvent.target.value = inputStr.substring(0, statrPos) + inputStr.substring(endPos, inputStr.length);
497    globalData.mouseEvent.target.selectionStart = statrPos;
498    globalData.mouseEvent.target.selectionEnd = statrPos;
499    globalData.mouseEvent.target.focus();
500  }
501
502  fn.copyImg = () => {
503    if (volantis.GLOBAL_CONFIG.plugins.message.rightmenu.notice) {
504      VolantisApp.message('系统提示', '复制中,请等待。', {
505        icon: rightMenuConfig.options.iconPrefix + ' fa-images'
506      })
507    }
508    RightMenus.writeClipImg(globalData.mediaLinkUrl, e => {
509      if (RightMenus.messageRightMenu) {
510        VolantisApp.hideMessage();
511        VolantisApp.message('系统提示', '图片复制成功!', {
512          icon: rightMenuConfig.options.iconPrefix + ' fa-images'
513        });
514      }
515    }, (e) => {
516      console.error(e);
517      if (RightMenus.messageRightMenu) {
518        VolantisApp.hideMessage();
519        VolantisApp.message('系统提示', '复制失败:' + e, {
520          icon: rightMenuConfig.options.iconPrefix + ' fa-exclamation-square red',
521          time: 9000
522        });
523      }
524    })
525  }
526
527  fn.printMode = () => {
528    if (window.location.pathname === globalData.pathName) {
529      if (RightMenus.messageRightMenu) {
530        const message = '是否打印当前页面?<br><em style="font-size: 80%">建议打印时勾选背景图形</em><br>'
531        VolantisApp.question('', message, { time: 9000 }, () => { fn.printHtml() })
532      } else {
533        fn.printHtml()
534      }
535    }
536  }
537
538  fn.printHtml = () => {
539    if (volantis.isReadModel) fn.readMode();
540    DOMController.setAttribute('details', 'open', 'true');
541    DOMController.removeList([
542      '.cus-article-bkg', '.iziToast-overlay', '.iziToast-wrapper', '.prev-next',
543      'footer', '#l_header', '#l_cover', '#l_side', '#comments', '#s-top', '#BKG',
544      '#rightmenu-wrapper', '.nav-tabs', '.parallax-mirror', '.new-meta-item.share', 
545      '.new-meta-box', 'button.btn-copy', 'iframe'
546    ]);
547    DOMController.setStyleList([
548      ['body', 'backgroundColor', 'unset'], ['#l_main, .copyright.license', 'width', '100%'],  
549      ['#post', 'boxShadow', 'none'], ['#post', 'background', 'none'], ['#post', 'padding', '0'],  
550      ['h1', 'textAlign', 'center'], ['h1', 'fontWeight', '600'], ['h1', 'fontSize', '2rem'], ['h1', 'marginBottom', '20px'], 
551      ['.tab-pane', 'display', 'block'], ['.tab-content', 'borderTop', 'none'], ['.highlight>table pre', 'whiteSpace', 'pre-wrap'], 
552      ['.highlight>table pre', 'wordBreak', 'break-all'], ['.fancybox img', 'height', 'auto'], ['.fancybox img', 'weight', 'auto'], 
553      ['.copyright.license', 'margin', '0'], ['.copyright.license', 'padding', '1.25em 20px'],  
554      ['figure.highlight, .copyright.license', 'display', 'inline-block'], 
555    ]);
556    setTimeout(() => {
557      window.print();
558      document.body.innerHTML = '';
559      window.location.reload();
560    }, 50);
561  }
562
563  fn.readMode = () => {
564    if (typeof ScrollReveal === 'function') ScrollReveal().clean('#comments');
565    DOMController.setStyle('#l_header', 'opacity', 0);
566    DOMController.fadeToggleList([
567      document.querySelector('#l_cover'), document.querySelector('footer'),
568      document.querySelector('#s-top'), document.querySelector('.article-meta#bottom'),
569      document.querySelector('.prev-next'), document.querySelector('#l_side'),
570      document.querySelector('#comments'), 
571    ]);
572    DOMController.toggleClassList([
573      [document.querySelector('#l_main'), 'common_read'], [document.querySelector('#l_main'), 'common_read_main'],
574      [document.querySelector('#l_body'), 'common_read'], [document.querySelector('#safearea'), 'common_read'],
575      [document.querySelector('#read_bkg'), 'common_read_hide'], 
576      [document.querySelector('h1'), 'common_read_h1'], [document.querySelector('#post'), 'post_read'],
577      [document.querySelector('#l_cover'), 'read_cover'], [document.querySelector('.widget.toc-wrapper'), 'post_read']
578    ]);
579    DOMController.setStyle('.copyright.license', 'margin', '15px 0');
580    volantis.isReadModel = volantis.isReadModel === undefined ? true : !volantis.isReadModel;
581    if (volantis.isReadModel) {
582      if (RightMenus.messageRightMenu) VolantisApp.message('系统提示', '阅读模式已开启,您可以点击屏幕空白处退出。', {
583        backgroundColor: 'var(--color-read-post)',
584        icon: rightMenuConfig.options.iconPrefix + ' fa-book-reader',
585        displayMode: 1,
586        time: 5000
587      });
588      document.querySelector('#l_body').removeEventListener('click', fn.readMode);
589      document.querySelector('#l_body').addEventListener('click', (event) => {
590        if (DOMController.hasClass(event.target, 'common_read')) {
591          fn.readMode();
592        }
593      });
594    } else {
595      document.querySelector('#l_body').removeEventListener('click', fn.readMode);
596      document.querySelector('#post').removeEventListener('click', fn.readMode);
597      DOMController.setStyle('.prev-next', 'display', 'flex');
598      DOMController.setStyle('.copyright.license', 'margin', '15px -40px');
599      // 恢复 header 透明度
600      DOMController.setStyle('#l_header', 'opacity', 1);
601    }
602  }
603
604  return {
605    init: fn.initEvent,
606    hideMenu: fn.hideMenu,
607    readMode: fn.readMode
608  }
609})()
610
611Object.freeze(RightMenus);
612volantis.requestAnimationFrame(() => {
613  if (document.readyState !== 'loading') {
614    RightMenus.initialMenu();
615  } else {
616    document.addEventListener("DOMContentLoaded", function () {
617      RightMenus.initialMenu();
618    })
619  }
620});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.