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.8
  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. let isCounted = false;
  29.  
  30. main();
  31.  
  32. /**
  33. * Main function to execute when the script is loaded.
  34. */
  35. function main() {
  36. ready(() => {
  37. poll(navSelector, handler, 500);
  38. });
  39. }
  40.  
  41. const wrapperId = 'x-menu-wrapper';
  42. const wrapperClassName = 'x-menu-wrapper';
  43. const toggleClassName = 'x-toggle';
  44. const bodyStateClassName = 'x-menu--open';
  45.  
  46. /**
  47. * Handler function to execute when the script is loaded.
  48. * 1. Create a toggle element
  49. * 2. Add event listener to the toggle element
  50. * 3. Add event listener to the navbar
  51. * 4. Add event listener to the component item
  52. * 5. Set the menu wrapper element
  53. * 6. Count the components
  54. * 7. Handle the component page class
  55. */
  56. function handler() {
  57. const toggleElement = document.createElement('span');
  58. toggleElement.className = toggleClassName;
  59. toggleElement.innerHTML = icon();
  60.  
  61. toggleElement.addEventListener('click', (event) => {
  62. // hold shift key to reset
  63. if (event.shiftKey) {
  64. reset();
  65. return;
  66. }
  67.  
  68. // init
  69. if (!wrapperElement || wrapperElement.id !== wrapperId) {
  70. // wrapper element add id
  71. wrapperElement = setMenuWrapper();
  72. // add event listener to component item
  73. componentItemClickEventListener(wrapperElement, componentItemSelector);
  74. // handle component page class (hide 'overview' menu item)
  75. handleComponentPageClass(wrapperElement);
  76. // count components
  77. countComponents(wrapperElement);
  78. // open the menu dashboard
  79. open();
  80. return;
  81. }
  82.  
  83. toggle();
  84. });
  85.  
  86. document.body.appendChild(toggleElement);
  87. // add event listener to navbar
  88. navClickEventListener();
  89. }
  90.  
  91. /**
  92. * Click the navbar menu element, handle the component page (hide 'overview' menu item).
  93. */
  94. function navClickEventListener() {
  95. const navElement = document.querySelector(navSelector);
  96. if (navElement) {
  97. navElement.addEventListener('click', () => {
  98. wrapperElement = document.querySelector(menuSelector);
  99. setTimeout(() => {
  100. if (wrapperElement) {
  101. handleComponentPageClass(wrapperElement);
  102. countComponents(wrapperElement);
  103. }
  104. }, 100);
  105. });
  106. }
  107. }
  108.  
  109. /**
  110. * Handle the component page class.
  111. * @param {Element} wrapperElement - The wrapper element
  112. */
  113. function handleComponentPageClass(wrapperElement) {
  114. if (isComponentPage()) {
  115. wrapperElement.classList.add(wrapperClassName);
  116. } else {
  117. wrapperElement.classList.remove(wrapperClassName);
  118. }
  119. }
  120.  
  121. /**
  122. * Click the component item, hide the menu wrapper.
  123. * @param {Element} wrapperElement - The wrapper element
  124. * @param {string} componentItemSelector - The selector of the component item
  125. */
  126. function componentItemClickEventListener(wrapperElement, componentItemSelector) {
  127. wrapperElement.addEventListener('click', (event) => {
  128. if (matches(event.target, componentItemSelector)) {
  129. close();
  130. }
  131. });
  132. }
  133.  
  134. /**
  135. * Set the menu wrapper element.
  136. * @returns {Element} - The menu wrapper element
  137. */
  138. function setMenuWrapper() {
  139. wrapperElement = document.querySelector(menuSelector);
  140. wrapperElement.setAttribute('id', wrapperId);
  141. return wrapperElement;
  142. }
  143.  
  144. /**
  145. * Count the components in the wrapper element.
  146. * @param {Element} wrapperElement - The wrapper element
  147. */
  148. function countComponents(wrapperElement) {
  149. // 获取所有 sidebar-group 元素(排除第一个)
  150. const groupElements = Array.from(wrapperElement.querySelectorAll(groupSelector));
  151. const componentCounts = [];
  152.  
  153. groupElements.forEach((item) => {
  154. const itemSelector = 'a.link';
  155. const itemElements = Array.from(item.querySelectorAll(itemSelector));
  156. const length = itemElements.length;
  157. const titleElement = item.querySelector(titleSelector);
  158. const title = titleElement.textContent;
  159. titleElement.textContent = `${title} (${length})`;
  160. componentCounts.push(length);
  161. });
  162.  
  163. const totalCount = componentCounts.reduce((acc, curr) => acc + curr, 0);
  164. const totalText = `🚀 共有组件 ${totalCount} 个`;
  165. const logoElement = document.querySelector(logoSelector);
  166. if (logoElement) {
  167. logoElement.title = totalText;
  168. }
  169. console.log(totalText);
  170. isCounted = true;
  171. }
  172.  
  173. function isComponentPage() {
  174. return window.location.href.includes('component');
  175. }
  176.  
  177. function reset() {
  178. wrapperElement.removeAttribute('id');
  179. wrapperElement.style.display = 'block';
  180. }
  181.  
  182. function open() {
  183. wrapperElement.style.display = 'grid';
  184. document.body.classList.add(bodyStateClassName);
  185. }
  186.  
  187. function close() {
  188. wrapperElement.style.display = 'none';
  189. document.body.classList.remove(bodyStateClassName);
  190. }
  191.  
  192. function toggle() {
  193. if (wrapperElement.style.display === 'none') {
  194. open();
  195. } else {
  196. close();
  197. }
  198. }
  199.  
  200. /**
  201. * Execute a function when the document is ready.
  202. * @param {function} eventHandler - Function to execute when the document is ready
  203. */
  204. function ready(eventHandler) {
  205. if (document.readyState !== 'loading') {
  206. eventHandler();
  207. } else {
  208. document.addEventListener('DOMContentLoaded', eventHandler);
  209. }
  210. }
  211.  
  212. /**
  213. * Wait for an element to be found on the page using polling.
  214. * @param {string} selector - CSS selector for the element to wait for
  215. * @param {function} callback - Function to execute when the element is found
  216. * @param {number} maxAttempts - Maximum number of attempts to find the element
  217. * @returns {number} intervalId - ID of the interval used to poll for the element
  218. */
  219. function poll(selector, callback, maxAttempts = 10) {
  220. let attempts = 0;
  221.  
  222. const intervalId = setInterval(() => {
  223. attempts++;
  224. const element = document.querySelector(selector);
  225.  
  226. if (element) {
  227. clearInterval(intervalId);
  228. if (callback && typeof callback === 'function') {
  229. callback(element);
  230. }
  231. } else if (attempts >= maxAttempts) {
  232. clearInterval(intervalId);
  233. console.log(`Element ${selector} not found after ${maxAttempts} attempts.`);
  234. }
  235. }, 1000);
  236.  
  237. return intervalId;
  238. }
  239.  
  240. /**
  241. * Check if an element matches a CSS selector.
  242. * @param {Element} currentElement - The element to check for a match
  243. * @param {string} selector - CSS selector to match against
  244. * @returns {boolean} - True if the selector matches, false otherwise
  245. */
  246. function matches(currentElement, selector) {
  247. while (currentElement !== null && currentElement !== document.body) {
  248. if (currentElement.matches(selector)) {
  249. return true;
  250. }
  251. currentElement = currentElement.parentElement;
  252. }
  253.  
  254. // 检查 body 元素
  255. return document.body.matches(selector);
  256. }
  257.  
  258. function icon() {
  259. 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>`;
  260. }
  261.  
  262. const style = `
  263. .${bodyStateClassName} {
  264. height: 100vh !important;
  265. overflow: hidden !important;
  266. }
  267.  
  268. .${toggleClassName} {
  269. position: fixed;
  270. top: 18px;
  271. right: 16px;
  272. z-index: 99999;
  273. cursor: pointer;
  274. opacity: 0.8;
  275. transition: opacity 0.3s ease-in-out;
  276. }
  277.  
  278. .${toggleClassName}:hover {
  279. opacity: 1;
  280. }
  281.  
  282. #${wrapperId} {
  283. position: fixed !important;
  284. top: 55px !important;
  285. right: 0 !important;
  286. bottom: 0 !important;
  287. left: 0 !important;
  288. z-index: 9999 !important;
  289. max-width: 100% !important;
  290. width: 100% !important;
  291. max-height: calc(100vh - 55px) !important;
  292. padding: 0 !important;
  293. background: #fff !important;
  294. /* border-block-start: 1px solid rgba(5, 5, 5, 0.06) !important; */
  295. }
  296.  
  297. #${wrapperId} .sidebar-groups {
  298. display: grid !important;
  299. grid-auto-flow: column !important;
  300. grid-auto-columns: 220px !important;
  301. max-width: max-content !important;
  302. gap: 16px !important;
  303. overflow: auto;
  304. margin-inline: auto !important;
  305. padding-block-end: 0 !important;
  306. border-inline-end: none !important;
  307. }
  308.  
  309. #${wrapperId} .doc-content-side {
  310. display: none !important;
  311. }
  312.  
  313. #${wrapperId} .sidebar-group__title {
  314. font-size: 12px !important;
  315. margin-block-end: 4px !important;
  316. }
  317.  
  318. #${wrapperId}.${wrapperClassName} .sidebar-group:nth-child(1) {
  319. display: none !important;
  320. }
  321.  
  322. #${wrapperId} .sidebar-group {
  323. padding-block-start: 16px !important;
  324. }
  325.  
  326. #${wrapperId} .sidebar-group .link {
  327. padding: 6px 8px !important;
  328. }
  329.  
  330. #${wrapperId} .sidebar-group .link-text {
  331. font-size: 12px !important;
  332. font-weight: 400 !important;
  333. }
  334. `;
  335. GM_addStyle(style);
  336. })();