Hi, Element Plus Component Dashboard🚀

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

  1. // ==UserScript==
  2. // @name Hi, Element Plus Component Dashboard🚀
  3. // @namespace https://github.com/xianghongai/Tampermonkey-UserScript
  4. // @version 1.0.9
  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. if (!isComponentPage() || isCounted) {
  150. return;
  151. }
  152.  
  153. // 获取所有 sidebar-group 元素(排除第一个)
  154. const groupElements = Array.from(wrapperElement.querySelectorAll(groupSelector));
  155. const componentCounts = [];
  156.  
  157. groupElements.forEach((item) => {
  158. const itemSelector = 'a.link';
  159. const itemElements = Array.from(item.querySelectorAll(itemSelector));
  160. const length = itemElements.length;
  161. const titleElement = item.querySelector(titleSelector);
  162. const title = titleElement.textContent;
  163. titleElement.textContent = `${title} (${length})`;
  164. componentCounts.push(length);
  165. });
  166.  
  167. const totalCount = componentCounts.reduce((acc, curr) => acc + curr, 0);
  168. const totalText = `🚀 共有组件 ${totalCount} 个`;
  169. const logoElement = document.querySelector(logoSelector);
  170. if (logoElement) {
  171. logoElement.title = totalText;
  172. }
  173. console.log(totalText);
  174. isCounted = true;
  175. }
  176.  
  177. function isComponentPage() {
  178. return window.location.href.includes('component');
  179. }
  180.  
  181. function reset() {
  182. wrapperElement.removeAttribute('id');
  183. wrapperElement.style.display = 'block';
  184. }
  185.  
  186. function open() {
  187. wrapperElement.style.display = 'grid';
  188. document.body.classList.add(bodyStateClassName);
  189. }
  190.  
  191. function close() {
  192. wrapperElement.style.display = 'none';
  193. document.body.classList.remove(bodyStateClassName);
  194. }
  195.  
  196. function toggle() {
  197. if (wrapperElement.style.display === 'none') {
  198. open();
  199. } else {
  200. close();
  201. }
  202. }
  203.  
  204. /**
  205. * Execute a function when the document is ready.
  206. * @param {function} eventHandler - Function to execute when the document is ready
  207. */
  208. function ready(eventHandler) {
  209. if (document.readyState !== 'loading') {
  210. eventHandler();
  211. } else {
  212. document.addEventListener('DOMContentLoaded', eventHandler);
  213. }
  214. }
  215.  
  216. /**
  217. * Wait for an element to be found on the page using polling.
  218. * @param {string} selector - CSS selector for the element to wait for
  219. * @param {function} callback - Function to execute when the element is found
  220. * @param {number} maxAttempts - Maximum number of attempts to find the element
  221. * @returns {number} intervalId - ID of the interval used to poll for the element
  222. */
  223. function poll(selector, callback, maxAttempts = 10) {
  224. let attempts = 0;
  225.  
  226. const intervalId = setInterval(() => {
  227. attempts++;
  228. const element = document.querySelector(selector);
  229.  
  230. if (element) {
  231. clearInterval(intervalId);
  232. if (callback && typeof callback === 'function') {
  233. callback(element);
  234. }
  235. } else if (attempts >= maxAttempts) {
  236. clearInterval(intervalId);
  237. console.log(`Element ${selector} not found after ${maxAttempts} attempts.`);
  238. }
  239. }, 1000);
  240.  
  241. return intervalId;
  242. }
  243.  
  244. /**
  245. * Check if an element matches a CSS selector.
  246. * @param {Element} currentElement - The element to check for a match
  247. * @param {string} selector - CSS selector to match against
  248. * @returns {boolean} - True if the selector matches, false otherwise
  249. */
  250. function matches(currentElement, selector) {
  251. while (currentElement !== null && currentElement !== document.body) {
  252. if (currentElement.matches(selector)) {
  253. return true;
  254. }
  255. currentElement = currentElement.parentElement;
  256. }
  257.  
  258. // 检查 body 元素
  259. return document.body.matches(selector);
  260. }
  261.  
  262. function icon() {
  263. 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>`;
  264. }
  265.  
  266. const style = `
  267. .${bodyStateClassName} {
  268. height: 100vh !important;
  269. overflow: hidden !important;
  270. }
  271.  
  272. .${toggleClassName} {
  273. position: fixed;
  274. top: 18px;
  275. right: 16px;
  276. z-index: 99999;
  277. cursor: pointer;
  278. opacity: 0.8;
  279. transition: opacity 0.3s ease-in-out;
  280. }
  281.  
  282. .${toggleClassName}:hover {
  283. opacity: 1;
  284. }
  285.  
  286. #${wrapperId} {
  287. position: fixed !important;
  288. top: 55px !important;
  289. right: 0 !important;
  290. bottom: 0 !important;
  291. left: 0 !important;
  292. z-index: 9999 !important;
  293. max-width: 100% !important;
  294. width: 100% !important;
  295. max-height: calc(100vh - 55px) !important;
  296. padding: 0 !important;
  297. background: #fff !important;
  298. /* border-block-start: 1px solid rgba(5, 5, 5, 0.06) !important; */
  299. }
  300.  
  301. #${wrapperId} .sidebar-groups {
  302. display: grid !important;
  303. grid-auto-flow: column !important;
  304. grid-auto-columns: max-content !important;
  305. max-width: max-content !important;
  306. gap: 16px !important;
  307. overflow: auto;
  308. margin-inline: auto !important;
  309. padding-block-end: 0 !important;
  310. border-inline-end: none !important;
  311. }
  312.  
  313. #${wrapperId} .doc-content-side {
  314. display: none !important;
  315. }
  316.  
  317. #${wrapperId} .sidebar-group__title {
  318. font-size: 12px !important;
  319. margin-block-end: 4px !important;
  320. }
  321.  
  322. #${wrapperId}.${wrapperClassName} .sidebar-group:nth-child(1) {
  323. display: none !important;
  324. }
  325.  
  326. #${wrapperId} .sidebar-group {
  327. padding-block-start: 16px !important;
  328. }
  329.  
  330. #${wrapperId} .sidebar-group .link {
  331. padding: 6px 8px !important;
  332. }
  333.  
  334. #${wrapperId} .sidebar-group .link-text {
  335. font-size: 12px !important;
  336. font-weight: 400 !important;
  337. }
  338. `;
  339. GM_addStyle(style);
  340. })();