Hi, Element Plus Component Dashboard🚀

将 Element Plus 菜单转换为 Dashboard 交互 (按 Shift 键点击可还原为默认菜单)

目前为 2025-03-27 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Hi, Element Plus Component Dashboard🚀
  3. // @namespace https://github.com/xianghongai/Tampermonkey-UserScript
  4. // @version 1.0.3
  5. // @description 将 Element Plus 菜单转换为 Dashboard 交互 (按 Shift 键点击可还原为默认菜单)
  6. // @author Nicholas Hsiang
  7. // @match *://element-plus.org/*
  8. // @icon https://avatars.githubusercontent.com/u/68583457
  9. // @grant GM_addStyle
  10. // @grant GM_info
  11. // @run-at document-end
  12. // @grant unsafeWindow
  13. // @license MIT
  14. // ==/UserScript==
  15.  
  16. (function () {
  17. 'use strict';
  18. console.log(GM_info.script.name);
  19.  
  20. const logoSelector = '.logo-container img.logo';
  21. const navSelector = '.navbar-menu';
  22. const menuSelector = '.sidebar';
  23. const groupSelector = '.sidebar-group:not(:first-child)';
  24. const componentItemSelector = '.link';
  25. const titleSelector = '.sidebar-group__title';
  26.  
  27. let wrapperElement = null;
  28.  
  29. main();
  30.  
  31. /**
  32. * Main function to execute when the script is loaded.
  33. */
  34. function main() {
  35. ready(() => {
  36. poll(navSelector, handler, 500);
  37. });
  38. }
  39.  
  40. const wrapperId = 'x-menu-wrapper';
  41. const wrapperClassName = 'x-menu-wrapper';
  42.  
  43. /**
  44. * Toggle the target element.
  45. */
  46. function handler() {
  47. const toggleElement = document.createElement('span');
  48. toggleElement.className = 'x-toggle';
  49. toggleElement.innerHTML = icon();
  50.  
  51. toggleElement.addEventListener('click', (event) => {
  52. // hold shift key to reset
  53. if (event.shiftKey) {
  54. wrapperElement.removeAttribute('id');
  55. wrapperElement.style.display = 'block';
  56. return;
  57. }
  58.  
  59. // init
  60. if (!wrapperElement || wrapperElement.id !== wrapperId) {
  61. wrapperElement = setMenuWrapper();
  62. // add event listener to component item
  63. componentItemClickEventListener(wrapperElement, componentItemSelector);
  64. // handle component page class (hide 'overview' menu item)
  65. handleComponentPageClass(wrapperElement);
  66. return;
  67. }
  68. wrapperElement.style.display = wrapperElement.style.display === 'none' ? 'grid' : 'none';
  69. });
  70.  
  71. document.body.appendChild(toggleElement);
  72. // add event listener to navbar
  73. navClickEventListener();
  74. }
  75.  
  76. /**
  77. * Click the navbar menu element, handle the component page (hide 'overview' menu item).
  78. */
  79. function navClickEventListener() {
  80. const navElement = document.querySelector(navSelector);
  81. if (navElement) {
  82. navElement.addEventListener('click', () => {
  83. wrapperElement = document.querySelector(menuSelector);
  84. handleComponentPageClass(wrapperElement);
  85. });
  86. }
  87. }
  88.  
  89. /**
  90. * Handle the component page class.
  91. * @param {Element} wrapperElement - The wrapper element
  92. */
  93. function handleComponentPageClass(wrapperElement) {
  94. if (window.location.href.includes('component')) {
  95. wrapperElement.classList.add(wrapperClassName);
  96. } else {
  97. wrapperElement.classList.remove(wrapperClassName);
  98. }
  99. }
  100.  
  101. /**
  102. * Click the component item, hide the menu wrapper.
  103. * @param {Element} wrapperElement - The wrapper element
  104. * @param {string} componentItemSelector - The selector of the component item
  105. */
  106. function componentItemClickEventListener(wrapperElement, componentItemSelector) {
  107. wrapperElement.addEventListener('click', (event) => {
  108. if (matches(event.target, componentItemSelector)) {
  109. wrapperElement.style.display = 'none';
  110. }
  111. });
  112. }
  113.  
  114. /**
  115. * Set the menu wrapper element.
  116. * @returns {Element} - The menu wrapper element
  117. */
  118. function setMenuWrapper() {
  119. wrapperElement = document.querySelector(menuSelector);
  120. wrapperElement.setAttribute('id', wrapperId);
  121.  
  122. // 获取所有 sidebar-group 元素(排除第一个)
  123. const groupElements = Array.from(wrapperElement.querySelectorAll(groupSelector));
  124. const componentCounts = [];
  125.  
  126. groupElements.forEach((item) => {
  127. const itemSelector = 'a.link';
  128. const itemElements = Array.from(item.querySelectorAll(itemSelector));
  129. const length = itemElements.length;
  130. const titleElement = item.querySelector(titleSelector);
  131. const title = titleElement.textContent;
  132. titleElement.textContent = `${title} (${length})`;
  133. componentCounts.push(length);
  134. });
  135.  
  136. const totalCount = componentCounts.reduce((acc, curr) => acc + curr, 0);
  137. const totalText = `🚀 共有组件 ${totalCount} 个`;
  138. const logoElement = document.querySelector(logoSelector);
  139. if (logoElement) {
  140. logoElement.title = totalText;
  141. }
  142. console.log(totalText);
  143. return wrapperElement;
  144. }
  145.  
  146. /**
  147. * Execute a function when the document is ready.
  148. * @param {function} eventHandler - Function to execute when the document is ready
  149. */
  150. function ready(eventHandler) {
  151. if (document.readyState !== 'loading') {
  152. eventHandler();
  153. } else {
  154. document.addEventListener('DOMContentLoaded', eventHandler);
  155. }
  156. }
  157.  
  158. /**
  159. * Wait for an element to be found on the page using polling.
  160. * @param {string} selector - CSS selector for the element to wait for
  161. * @param {function} callback - Function to execute when the element is found
  162. * @param {number} maxAttempts - Maximum number of attempts to find the element
  163. * @returns {number} intervalId - ID of the interval used to poll for the element
  164. */
  165. function poll(selector, callback, maxAttempts = 10) {
  166. let attempts = 0;
  167.  
  168. const intervalId = setInterval(() => {
  169. attempts++;
  170. const element = document.querySelector(selector);
  171.  
  172. if (element) {
  173. clearInterval(intervalId);
  174. if (callback && typeof callback === 'function') {
  175. callback(element);
  176. }
  177. } else if (attempts >= maxAttempts) {
  178. clearInterval(intervalId);
  179. console.log(`Element ${selector} not found after ${maxAttempts} attempts.`);
  180. }
  181. }, 1000);
  182.  
  183. return intervalId;
  184. }
  185.  
  186. /**
  187. * Check if an element matches a CSS selector.
  188. * @param {Element} currentElement - The element to check for a match
  189. * @param {string} selector - CSS selector to match against
  190. * @returns {boolean} - True if the selector matches, false otherwise
  191. */
  192. function matches(currentElement, selector) {
  193. while (currentElement !== null && currentElement !== document.body) {
  194. if (currentElement.matches(selector)) {
  195. return true;
  196. }
  197. currentElement = currentElement.parentElement;
  198. }
  199.  
  200. // 检查 body 元素
  201. return document.body.matches(selector);
  202. }
  203.  
  204. function icon() {
  205. return `<?xml version="1.0" encoding="UTF-8"?><svg width="18" height="18" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 4H6C4.89543 4 4 4.89543 4 6V18C4 19.1046 4.89543 20 6 20H18C19.1046 20 20 19.1046 20 18V6C20 4.89543 19.1046 4 18 4Z" fill="#2F88FF" stroke="#333" stroke-width="3" stroke-linejoin="round"/><path d="M18 28H6C4.89543 28 4 28.8954 4 30V42C4 43.1046 4.89543 44 6 44H18C19.1046 44 20 43.1046 20 42V30C20 28.8954 19.1046 28 18 28Z" fill="#2F88FF" stroke="#333" stroke-width="3" stroke-linejoin="round"/><path d="M42 4H30C28.8954 4 28 4.89543 28 6V18C28 19.1046 28.8954 20 30 20H42C43.1046 20 44 19.1046 44 18V6C44 4.89543 43.1046 4 42 4Z" fill="#2F88FF" stroke="#333" stroke-width="3" stroke-linejoin="round"/><path d="M28 28H44" stroke="#333" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M36 36H44" stroke="#333" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M28 44H44" stroke="#333" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
  206. }
  207.  
  208. const style = `
  209. .x-toggle {
  210. position: fixed;
  211. top: 18px;
  212. right: 16px;
  213. z-index: 99999;
  214. cursor: pointer;
  215. opacity: 0.8;
  216. transition: opacity 0.3s ease-in-out;
  217. }
  218.  
  219. .x-toggle:hover {
  220. opacity: 1;
  221. }
  222.  
  223. #${wrapperId} {
  224. position: fixed !important;
  225. top: 55px !important;
  226. right: 0 !important;
  227. bottom: 0 !important;
  228. left: 0 !important;
  229. z-index: 9999 !important;
  230. max-width: 100% !important;
  231. width: 100% !important;
  232. max-height: calc(100vh - 55px) !important;
  233. padding: 0 !important;
  234. background: #fff !important;
  235. /* border-block-start: 1px solid rgba(5, 5, 5, 0.06) !important; */
  236. }
  237.  
  238. #${wrapperId} .sidebar-groups {
  239. display: grid !important;
  240. grid-auto-flow: column !important;
  241. grid-auto-columns: 220px !important;
  242. max-width: max-content !important;
  243. gap: 16px !important;
  244. overflow: auto;
  245. margin-inline: auto !important;
  246. border-inline-end: none !important;
  247. }
  248.  
  249. #${wrapperId} .doc-content-side {
  250. display: none !important;
  251. }
  252.  
  253. #${wrapperId} .sidebar-group__title {
  254. font-size: 12px !important;
  255. margin-block-end: 4px !important;
  256. }
  257.  
  258. #${wrapperId}.${wrapperClassName} .sidebar-group:nth-child(1) {
  259. display: none !important;
  260. }
  261.  
  262. #${wrapperId} .sidebar-group {
  263. padding-block-start: 16px !important;
  264. }
  265.  
  266. #${wrapperId} .sidebar-group .link {
  267. padding: 6px 8px !important;
  268. }
  269.  
  270. #${wrapperId} .sidebar-group .link-text {
  271. font-size: 12px !important;
  272. font-weight: 400 !important;
  273. }
  274. `;
  275. GM_addStyle(style);
  276. })();