Wilson@思源

目 录

js实现斜杠菜单展开并支持左右方向键

js
// js实现斜杠菜单展开并支持左右方向键 (() => { // 使用兼容模式, // 如果左右箭头有问题,可以使用兼容模式,兼容模式用ctrl/cmd + 方向键移动 // 默认false,未开启,设为true开启 const useCompatibilityMode = false; // 搜索时,虚拟分组列表跳转位置 // currpos 直接跳转到下一列的相同位置 // first 跳转到下一列的第一个元素那里,默认first const searchGroupSkipPos = "first"; // 校正因子 // 虚拟分组情况下,当左右方向键跳转有错误时,可以通过微调该参数进行校正, // 此时控制台会输出当前计算的分组大小,只要输出结果和实际一致就可以了 // 可输入正值或负值或小数 const correctionFactor = 0; ////////////// 以下代码不涉及配置项,如无必要勿动 ////////////////////////// // 筛选时虚拟列表跳过元素个数 // 由于筛选列表没有分组,这里用跳过元素数代替,这个变量会自动修改,手动修改无效 let skipElementNumInSearch = 3; // 初始化最后一个分组的大小,用于分组筛选时方向键跳转的判断依据,会实时计算 let lastGroupSize = 1; // 判断是否默认主题 //const theme = siyuan.config.appearance.mode === 0 ? siyuan.config.appearance.themeLight : siyuan.config.appearance.themeDark; //if(theme !== 'midnight' && theme !== 'daylight') return; // 注入css,解决第一个元素边距导致的列表内容不对齐 const dialogStyle = document.createElement('style'); dialogStyle.textContent = ` .hint--menu:not(.fn__none) button:nth-child(1) { margin-top: 0; } `; document.head.appendChild(dialogStyle); // 设置下一个元素的焦点 function nextElementFocus(nextElement) { const menu = document.querySelector(".hint--menu:not(.fn__none)"); const currentFocus = menu.querySelector('.b3-list-item--focus'); currentFocus.classList.remove("b3-list-item--focus"); nextElement?.classList.add("b3-list-item--focus"); } // 获取下一个分组元素的焦点 function focusNextGroupButton() { const menu = document.querySelector(".hint--menu:not(.fn__none)"); const currentFocus = menu.querySelector('.b3-list-item--focus'); let nextElement = currentFocus.nextElementSibling; // 继续查找下一个元素,直到找到一个按钮或.b3-menu__separator while (nextElement && nextElement.classList.contains('b3-list-item') && !nextElement.classList.contains('b3-menu__separator')) { nextElement = nextElement.nextElementSibling; } // 如果找到了.b3-menu__separator,就聚焦到它的前一个按钮 if (nextElement && nextElement.classList.contains('b3-menu__separator')) { nextElement = nextElement.nextElementSibling; } // 如果没有找到任何按钮或.b3-menu__separator,循环到列表开头 if (!nextElement || !nextElement.classList.contains('b3-list-item')) { nextElement = menu.querySelector('.b3-list-item'); } nextElementFocus(nextElement); } // 获取上一个分组元素的焦点 function focusPreviousGroupButton() { const menu = document.querySelector(".hint--menu:not(.fn__none)"); const currentFocus = menu.querySelector('.b3-list-item--focus'); let previousElement = currentFocus.previousElementSibling; // 继续查找上一个元素,直到找到一个按钮或.b3-menu__separator while (previousElement && previousElement.classList.contains('b3-list-item') && !previousElement.classList.contains('b3-menu__separator')) { previousElement = previousElement.previousElementSibling; } // 如果找到了.b3-menu__separator,就聚焦到它的前一个按钮 if (previousElement && previousElement.classList.contains('b3-menu__separator')) { previousElement = previousElement.previousElementSibling; } // 如果没有找到任何按钮或.b3-menu__separator,循环到列表结尾 if (!previousElement || !previousElement.classList.contains('b3-list-item')) { previousElement = menu.querySelector('.b3-list-item:last-child'); } nextElementFocus(getGroupFirstElement(previousElement)); } // 获取分组的第一个元素 function getGroupFirstElement(currentFocus) { const menu = document.querySelector(".hint--menu:not(.fn__none)"); let previousElement = currentFocus.previousElementSibling; // 继续查找上一个元素,直到找到一个按钮或.b3-menu__separator while (previousElement && previousElement.classList.contains('b3-list-item') && !previousElement.classList.contains('b3-menu__separator')) { previousElement = previousElement.previousElementSibling; } // 如果找到了.b3-menu__separator,就聚焦到它的前一个按钮 if (previousElement && previousElement.classList.contains('b3-menu__separator')) { previousElement = previousElement.nextElementSibling; } // 如果没有找到任何按钮或.b3-menu__separator,循环到列表结尾 if (!previousElement || !previousElement.classList.contains('b3-list-item')) { previousElement = menu.querySelector('.b3-list-item'); } return previousElement; } // 获取上一个元素 function focusPreviousButton(type='') { const menu = document.querySelector(".hint--menu:not(.fn__none)"); const currentFocus = menu.querySelector('.b3-list-item--focus'); let previousElement = currentFocus.previousElementSibling; if(type === ''){ if(previousElement.classList.contains("b3-menu__separator")){ previousElement = previousElement.previousElementSibling; } } if(!previousElement || !previousElement.classList.contains('b3-list-item')){ previousElement = menu.querySelector('.b3-list-item:last-child'); } nextElementFocus(previousElement); } // 获取下一个元素 function focusNextButton(type='') { const menu = document.querySelector(".hint--menu:not(.fn__none)"); const currentFocus = menu.querySelector('.b3-list-item--focus'); let nextElement = currentFocus.nextElementSibling; if(type === ''){ if(nextElement.classList.contains("b3-menu__separator")){ nextElement = nextElement.nextElementSibling; } } if(!nextElement || !nextElement.classList.contains('b3-list-item')){ nextElement = menu.querySelector('.b3-list-item'); } nextElementFocus(nextElement); } // 获取上n个元素 function focusPreviousNButton() { const menu = document.querySelector(".hint--menu:not(.fn__none)"); const currentFocus = menu.querySelector('.b3-list-item--focus'); let previousElement = currentFocus.previousElementSibling; const num = menu.querySelectorAll('.b3-list-item').length; if(skipElementNumInSearch >= num) { focusPreviousButton('search'); return; } let count = 1; while (previousElement && previousElement.classList.contains('b3-list-item') && count <= skipElementNumInSearch) { previousElement = previousElement.previousElementSibling; count++; } // 如果没有找到任何按钮,循环到列表开头 if (!previousElement || !previousElement.classList.contains('b3-list-item')) { if(searchGroupSkipPos === "first") { previousElement = menu.querySelector('.b3-list-item:nth-last-child('+lastGroupSize+')'); } else { previousElement = menu.querySelector('.b3-list-item:last-child'); } } nextElementFocus(previousElement); } // 获取下n个元素 function focusNextNButton() { const menu = document.querySelector(".hint--menu:not(.fn__none)"); const currentFocus = menu.querySelector('.b3-list-item--focus'); let nextElement = currentFocus.nextElementSibling; const num = menu.querySelectorAll('.b3-list-item').length; if(skipElementNumInSearch >= num) { focusNextButton('search'); return; } let count = 1; while (nextElement && nextElement.classList.contains('b3-list-item') && count <= skipElementNumInSearch) { nextElement = nextElement.nextElementSibling; count++; } // 如果没有找到任何按钮或.b3-menu__separator,循环到列表开头 if (!nextElement || !nextElement.classList.contains('b3-list-item')) { nextElement = menu.querySelector('.b3-list-item'); } nextElementFocus(nextElement); } // 计算计算虚拟列表大小 function calcSearchGroupSize(menu, type) { // 计算方法 // 分组大小 = 父容器大小 / (button.ofoffsetHeight + button.margin) const menuContent = menu.firstElementChild; const menuContentHeiht = menuContent.offsetHeight; const button = menu.querySelector("button.b3-list-item:nth-child(2)"); const buttonStyle = getComputedStyle(button, null); const buttonMargin = parseFloat(buttonStyle.marginTop) + parseFloat(buttonStyle.marginBottom); const buttonHeight = button.offsetHeight + buttonMargin; let groupSize = Math.round((menuContentHeiht + (parseFloat(correctionFactor)||0)) / buttonHeight); if(correctionFactor !== 0) console.log("当前分组大小", groupSize, "校正因子", correctionFactor); //console.log(groupSize, menuContentHeiht, buttonHeight); // 跳转方式 if(searchGroupSkipPos === 'first') { // 跳转到下一列第一个元素位置 // 算法 // 1. 先计算当前在列表中的位置 = 当前所在条数 / 分组大小 取余 // 2 计算下一跳间隔数(下一分组首位置)= 分组大小 - 当前在列表中的位置 // 3 计算上一跳间隔数(上一分组首位置)= 当前在列表中的位置 - 1 + (分组大小 - 1) const currentFocus = menu.querySelector('.b3-list-item--focus'); const currPos = getCurrPos(currentFocus); // 当余数为0时,说明当前在列表中是最后一个元素,currPosInGroup应等于groupSize const currPosInGroup = currPos % groupSize || groupSize; if(type === 'next'){ // 下一跳 const nexPos = groupSize - currPosInGroup; skipElementNumInSearch = nexPos; } else { // 上一跳 let prevPos = currPosInGroup - 1 + (groupSize - 1); // 计算所有元素个数 const groupNum = menu.querySelectorAll('button.b3-list-item').length; if(currPos <= groupSize) { // 如果当前在第一列,则下一跳是最后一列,要计算最后一列分组的实际大小,可能不足groupSize // 所有元素数 % 分组大小 即最后一列的实际大小,当为0时应取groupSize lastGroupSize = groupNum % groupSize || groupSize; // 当没有上一个元素了,会自动取最后最一组的倒数第lastGroupSize的那个元素(即最后最一组的第一个元素) prevPos = currPosInGroup - 1 + (lastGroupSize - 1); } skipElementNumInSearch = prevPos; } } else { // 跳转到上一列/下一列相同位置 // -1 是因为跳过n个元素,不含第一个或最后一个 skipElementNumInSearch = groupSize - 1; } } // 获取焦点元素的当前位置 function getCurrPos(focusedItem) { let count = 0; // 遍历当前元素的所有前兄弟元素 for (let sibling = focusedItem.previousElementSibling; sibling; sibling = sibling.previousElementSibling) { // 如果前兄弟元素是 button 并且拥有 b3-list-item 类,则计数加一 if (sibling.tagName.toLowerCase() === 'button' && sibling.classList.contains('b3-list-item')) { count++; } } return count + 1; } // 延迟执行 function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // 等待元素渲染完成后执行 function whenElementExist(selector) { return new Promise(resolve => { const checkForElement = () => { let element = null; if (typeof selector === 'function') { element = selector(); } else { element = document.querySelector(selector); } if (element) { resolve(element); } else { requestAnimationFrame(checkForElement); } }; checkForElement(); }); } let hintMenuShow = false; let hintMenuTimer = null; function monitorHintMenu(layoutCenter) { // 定义一个回调函数处理 DOM 变化 const observerCallback = (mutationsList) => { mutationsList.forEach((mutation) => { if (mutation.type === 'childList') { // 当 layout__center 元素有新的子元素被添加或删除时触发 mutation.addedNodes.forEach((node) => { if (node.classList && node.classList.contains('hint--menu')) { // 检查新添加的节点是否是 .hint--menu checkFnNoneClass(node); } }); } else if (mutation.type === 'attributes' && mutation.target.classList.contains('hint--menu')) { // 当 .hint--menu 元素的属性发生变化时触发 checkFnNoneClass(mutation.target); } }); }; // 检查是否有 .fn_none 类 function checkFnNoneClass(node) { const hasFnNoneClass = node.classList.contains('fn__none'); if(!hasFnNoneClass){ // 显示menu hintMenuShow = true; if(hintMenuTimer) clearTimeout(hintMenuTimer); } else { // 隐藏menu if(hintMenuTimer) clearTimeout(hintMenuTimer); hintMenuTimer = setTimeout(()=>{ hintMenuShow = false; }, 100); } } // 配置 MutationObserver const config = { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] }; // 创建一个新的 MutationObserver 实例 const observer = new MutationObserver(observerCallback); // 开始观察 layout__center 元素 observer.observe(layoutCenter, config); // 返回一个函数以停止观察 return () => { observer.disconnect(); }; } // 监控menu显示状态 if(!useCompatibilityMode) { // 等待标签页容器渲染完成后开始监听 whenElementExist('.layout__center').then(async element => { monitorHintMenu(element); }); } // 监听按键 if(useCompatibilityMode) { // 使用兼容模式 document.addEventListener('keydown', function(event) { const menu = document.querySelector(".hint--menu:not(.fn__none)"); const sepEl = document.querySelector("div.b3-menu__separator"); if(menu && (event.ctrlKey || event.metaKey)){ if (event.key === 'ArrowRight') { if(sepEl){ focusNextGroupButton(); } else { // 计算虚拟列表大小 calcSearchGroupSize(menu, 'next'); // 跳过n个元素 focusNextNButton(); } event.preventDefault(); event.stopPropagation(); } else if (event.key === 'ArrowLeft') { if(sepEl){ focusPreviousGroupButton(); } else { // 计算虚拟列表大小 calcSearchGroupSize(menu, 'prev'); // 跳过n个元素 focusPreviousNButton(); } event.preventDefault(); event.stopPropagation(); } else if (event.key === 'Escape') { menu.classList.add("fn__none"); event.preventDefault(); event.stopPropagation(); } // ctrl + 上下箭头 思源监控不到 // else if (event.key === 'ArrowUp') { // focusPreviousButton(); // } // else if (event.key === 'ArrowDown') { // focusNextButton(); // } } }); } else { // 不使用兼容模式 document.addEventListener('keydown', function(event) { const menu = document.querySelector(".hint--menu"); const sepEl = document.querySelector("div.b3-menu__separator"); if(menu && hintMenuShow){ menu.classList.remove("fn__none"); if (event.key === 'ArrowRight') { if(sepEl){ focusNextGroupButton(); } else { // 计算虚拟列表大小 calcSearchGroupSize(menu, 'next'); // 跳过n个元素 focusNextNButton(); } event.preventDefault(); event.stopPropagation(); } else if (event.key === 'ArrowLeft') { if(sepEl){ focusPreviousGroupButton(); } else { // 计算虚拟列表大小 calcSearchGroupSize(menu, 'prev'); // 跳过n个元素 focusPreviousNButton(); } event.preventDefault(); event.stopPropagation(); } else if (event.key === 'Escape') { menu.classList.add("fn__none"); event.preventDefault(); event.stopPropagation(); } } }); } })();