。
页面渲染
读取 order 组的内容遍历加载,由于这里实际填写的是组名,不是具体的内容,所以还需要根据键值去找具体的内容,读取方式类似于 dataGet(rightmenus, 'plugins.elementCheck')
:
用到了可选链操作符,所以需要 Ndoe14+ 环境let dataGet = (data, keyStr) => { const keys = keyStr.split('.'); let currentData = data; for (const key of keys) { currentData = currentData?.[key]; } return currentData; }
|
拿到菜单对象后,就是根据其中内容解析成对应元素,以 event
和 link
的值做主要区分,link 属性不为空 写成 a 标签,反之就当事件型的菜单。除了通用的菜单项,额外需要特殊对待的就是独立处理就行了:好在不多,也就是横向的导航栏和音乐控制模块。
接下来就是这些菜单项的事件响应部分,主题用一个公共的配置对象,将配置文件中的 rightmenus
以 JSON 对象的方式存储在程序中,在 JS 代码里读取这个对象就可以拿到相应内容。所以最开始是打算只判断 event
中的值是否为内置事件列表中的,如果不是就简单的使用 eval()
或者 new Function()
,套个 try catch
直接执行得了,可是后来主题增加了内容安全策略(CSP),这两个方法都在禁用列表中,所以只能妥协,先行将 event
的内容封装成函数写到页面中,JS 文件中再根据相应的属性名调用相关函数,如此才不算是动态插入的函数,这一部分放到了 RightMenusFunction
对象中。
<% rightMenu.rederFunction = item => { %> <% if (!!item && !!item['event'] && item['group'] !== 'navigation') { %> <% if (rightMenu.defaultEvent.some(value => { return value === item['event'] })) { %> <!-- RightMenusFunction['<%- item.id %>'] = (fun) => {fun()} --> <% } else if (item['group'] === 'seletctText') { %> RightMenusFunction['<%- item.id %>'] = (__text__) => {<%- item.event %>} <% } else if (item['group'] === 'elementCheck' || item['group'] === 'elementImage') { %> RightMenusFunction['<%- item.id %>'] = (__link__) => {<%- item.event %>} <% } else { %> RightMenusFunction['<%- item.id %>'] = () => {<%- item.event %>} <% } %> <% } %> <% } %>
|
上面代码基础的一般性的执行的是 RightMenusFunction['<%- item.id %>'] = () => {<%- item.event %>}
这一句,利用菜单项的 id
调用它的具体实现,其它的如 seletctText/elementCheck
的特殊处理是因为得传递相关参数,例如事件输入 window.open(__link__)
,需要将 __link__
传递成真实的值。
功能实现
在 JS 环境中,我可以拿到 volantis.GLOBAL_CONFIG.plugins.rightmenus
, RightMenusFunction
两个相关对象,前者提供了配置文件中的设定,后者是一个函数封装。
元素位置控制
在正式决定哪些菜单项显示之前,得先让整个右键菜单显示出来(并且在正确的位置上),右键菜单 rightmenu-wrapper
默认隐藏、绝对定位,在不设置 top
, left
属性前,将其设置 block
就能获取到菜单的宽高,与右键时的位置和屏幕显示宽高做对比,就能决定显示位置了。
let mouseClientX = event.clientX; let mouseClientY = event.clientY; let screenWidth = document.documentElement.clientWidth || document.body.clientWidth; let screenHeight = document.documentElement.clientHeight || document.body.clientHeight; _rightMenuWrapper.style.display = 'block'; let menuWidth = _rightMenuContent.offsetWidth; let menuHeight = _rightMenuContent.offsetHeight; let showLeft = mouseClientX + menuWidth > screenWidth ? mouseClientX - menuWidth + 10 : mouseClientX; let showTop = mouseClientY + menuHeight > screenHeight ? mouseClientY - menuHeight + 10 : mouseClientY; showTop = mouseClientY + menuHeight > screenHeight && showTop < menuHeight && mouseClientY < menuHeight ? showTop + (screenHeight - menuHeight - showTop - 10) : showTop; _rightMenuWrapper.style.left = `${showLeft}px`; _rightMenuWrapper.style.top = `${showTop}px`;
|
元素状态判断
通过对右键事前的内容进行处理,判断当前是在什么元素上打开右键菜单,处于什么状态。
globalData.selectText = window.getSelection().toString();
if (event.target.tagName.toLowerCase() === 'input' || event.target.tagName.toLowerCase() === 'textarea') { }
if (!!event.target.href && RightMenus.urlRegx.test(event.target.href)) { }
if (!!event.target.currentSrc && RightMenus.urlRegx.test(event.target.currentSrc)) { globalData.isMediaLink = true; globalData.mediaLinkUrl = event.target.currentSrc; }
if (globalData.isMediaLink && RightMenus.imgRegx.test(globalData.mediaLinkUrl)) { }
if (!!(document.querySelector('#post.article') || null)) { }
|
菜单项显示控制
在确定了右键状态后就可以着手控制元素显隐了,利用 document.querySelectorAll
获取所有的菜单项,遍历菜单根据状态设置 display
属性。此时之前菜单对象的 group
属性就派上用场了,通过对内置组匹配,成组的决定菜单项的显示。
菜单项的动态显隐带来了一个问题,本来用于间隔的分割线(hr)可能在实际显示上相邻了,如此还需要重新处理下这个:主要思路为默认分割线需要隐藏,遍历菜单,如果两个分割线之间至少存在一个可以显示的菜单,那么分割线就能显示,反之隐藏。
let elementHrItem = { item: null, hide: true }; _rightMenuListWithHr.forEach((item) => { if (item.nodeName === "HR") { item.style.display = 'block'; if (!elementHrItem.item) { elementHrItem.item = item; return; } if (elementHrItem.hide || elementHrItem.item.nextElementSibling.nodeName === "hr") { elementHrItem.item.style.display = 'none'; } elementHrItem.item = item; elementHrItem.hide = true; } else { if (item.style.display === 'block' && elementHrItem.hide) { elementHrItem.hide = false; } } }) if (!!elementHrItem.item && elementHrItem.hide) elementHrItem.item.style.display = 'none';
|
菜单事件处理
本次右键重构另一特点就是事件绑定与元素绑定分离了,上一版的右键每次打开右键时都需要经历一次事件绑定,本次重构将事件处理部分拆分,核心的实现如下,如果是内置事件,调用 fn[eventName]()
,其余的视作用户自定义脚本,除特殊需要判断的其余根据 id
调用 RightMenusFunction
函数。
item.addEventListener('click', () => { if (RightMenus.defaultEvent.every(item => { return eventName !== item })) { if (groupName === 'seletctText') { RightMenusFunction[id](globalData.selectText) } else if (groupName === 'elementCheck') { RightMenusFunction[id](globalData.isLink ? globalData.linkUrl : globalData.mediaLinkUrl) } else if (groupName === 'elementImage') { RightMenusFunction[id](globalData.mediaLinkUrl) } else { RightMenusFunction[id]() } } else { fn[eventName]() } })
|
内置事件改进
与上一版自定义右键相比,最大的改动是重写了图片复制函数,上一版只能复制 PNG 格式的图片,局限性很大,这一版理论上支持复制所有图片。改动点在于将图片利用 canvas
画出来了,转换成 canvas 后利用 toBlob()
函数获得到了 image/png
的数据,如此完美。
注:虽然设置了 crossOrigin,但是实际使用时发现相同访问地址下会触发浏览器缓存文件,还是可能会出现 CORS 问题,解决方案是地址后面加参数刷新一下,视作不同链接。
{ writeClipImg: async (link, success, error) => { const image = new Image; image.crossOrigin = "Anonymous"; image.addEventListener('load', () => { let canvas = document.createElement("canvas"); let context = canvas.getContext("2d"); canvas.width = image.width; canvas.height = image.height; context.drawImage(image, 0, 0); canvas.toBlob(blob => { navigator.clipboard.write([ new ClipboardItem({ 'image/png': blob }) ]).then(e => { success(e) }).catch(e => { error(e) }) }, 'image/png') }, false) image.src = `${link}?(lll¬ω¬)`; } }
|
四、自定义
额外的,按照自定义事件方式实现了文章页下的『查看上/下一篇』,文本选中时的『复制为 Markdown』功能。
事件实现<script type="text/javascript" src="/js/h2m.js"></script> <script> window.onkeydown = e => { if (e.ctrlKey && e.key === 'a') e.preventDefault(); }
volantis.rightmenu.jump = (type) => { const item = document.querySelector(type === 'prev' ? 'article .prev-next a.prev' : 'article .prev-next a.next'); if(!!item) { if(typeof pjax !== 'undefined') { pjax.loadUrl(item.href) } else { window.location.href = item.href; } } }
volantis.rightmenu.html2md = () => { const selectionObj = window.getSelection(); const rangObj = selectionObj.getRangeAt(0); const docFragment = rangObj.cloneContents(); const htmlStr = volantis.rightmenu.htmlHandle(docFragment); const markdown = h2m(htmlStr); VolantisApp.utilWriteClipText(markdown); }
volantis.rightmenu.htmlHandle = documentFragment => { const element = document.createElement("div"); element.appendChild(documentFragment); volantis.rightmenu.removeElement([ 'div#bottom', 'div.new-meta-box', 'div.prev-next', 'div.atk-avatar', 'div.atk-footer', 'div.atk-main-editor', 'div.atk-list-header', 'div.atk-height-limit-btn', 'div.atk-list-read-more', 'div.references', '#l_cover', '#rightmenu-wrapper', '#s-top', 'head', 'header', 'footer', 'script', 'pjax' ], element) window.selectHtml = element.innerHTML; return element.innerHTML.replace(/(<button(.*?)btn-copy(.*?)<\/button>)/g, '') .replace(/<figcaption>(.*?)<\/figcaption>/g, '') }
volantis.rightmenu.removeElement = (selects = [], element = document) => { selects.forEach(select => { element.querySelectorAll(select).forEach(item => { item.remove(); }) }) }
volantis.rightmenu.handle(() => { const prev = document.querySelector('#prev').parentElement, next = document.querySelector('#next').parentElement, articlePrev = document.querySelector('article .prev-next a.prev p.title'), articleNext = document.querySelector('article .prev-next a.next p.title');
prev.style.display = articlePrev ? 'block' : 'none'; prev.title = articlePrev ? articlePrev.innerText : null; next.style.display = articleNext ? 'block' : 'none'; next.title = articleNext ? articleNext.innerText : null; }, 'prevNext', false) </script>
|