Hi, Element Plus Component Dashboard🚀

Hi, Element Plus Component Dashboard

目前为 2025-03-26 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Hi, Element Plus Component Dashboard🚀
  3. // @namespace https://github.com/xianghongai/Tampermonkey-UserScript
  4. // @version 1.0.2
  5. // @description Hi, Element Plus Component Dashboard
  6. // @author Nicholas Hsiang
  7. // @match *://element-plus.org/*
  8. // @icon https://element-plus.org/images/element-plus-logo-small.svg
  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 menuSelector = '.sidebar';
  22. let wrapperEl = null;
  23.  
  24. main();
  25.  
  26. /**
  27. * Main function to execute when the script is loaded.
  28. */
  29. function main() {
  30. ready(() => {
  31. poll(menuSelector, handler, 500);
  32. });
  33. }
  34.  
  35. /**
  36. * Toggle the target element.
  37. */
  38. function handler() {
  39. const closeSpan = document.createElement('span');
  40. closeSpan.className = 'x-toggle';
  41. closeSpan.innerHTML = icon();
  42.  
  43. closeSpan.addEventListener('click', (event) => {
  44. // hold shift key to reset
  45. if (event.shiftKey) {
  46. wrapperEl.removeAttribute('id');
  47. wrapperEl.style.display = 'block';
  48. return;
  49. }
  50.  
  51. // init
  52. if (!wrapperEl || wrapperEl.id !== 'x-menu-wrapper') {
  53. wrapperEl = dashboard();
  54. // add component item click event listener
  55. componentItemClickEventListener(wrapperEl, '.link');
  56. handleComponentPageClass(wrapperEl);
  57. return;
  58. }
  59. wrapperEl.style.display = wrapperEl.style.display === 'none' ? 'grid' : 'none';
  60. });
  61. document.body.appendChild(closeSpan);
  62.  
  63. navClickEventListener(wrapperEl);
  64. }
  65.  
  66. /**
  67. * Click the navbar menu element, handle the component page.
  68. * @param {Element} wrapperEl - The wrapper element
  69. */
  70. function navClickEventListener(wrapperEl) {
  71. const navSelector = '.navbar-menu';
  72. const navEl = document.querySelector(navSelector);
  73. if (navEl) {
  74. navEl.addEventListener('click', () => {
  75. handleComponentPageClass(wrapperEl);
  76. });
  77. }
  78. }
  79.  
  80. function handleComponentPageClass(wrapperEl) {
  81. if (window.location.href.includes('component')) {
  82. wrapperEl.classList.add('x-dashboard-component');
  83. } else {
  84. wrapperEl.classList.remove('x-dashboard-component');
  85. }
  86. }
  87.  
  88. /**
  89. * Click the target element.
  90. * @param {Element} currentElement - The target element
  91. * @param {string} selector - The selector of the target element
  92. */
  93. function componentItemClickEventListener(currentElement, selector) {
  94. currentElement.addEventListener('click', (event) => {
  95. if (matches(event.target, selector)) {
  96. currentElement.style.display = 'none';
  97. }
  98. });
  99. }
  100.  
  101. /**
  102. * Create the dashboard element.
  103. * @returns {Element} - The dashboard element
  104. */
  105. function dashboard() {
  106. wrapperEl = document.querySelector(menuSelector);
  107. wrapperEl.setAttribute('id', 'x-menu-wrapper');
  108.  
  109. // 获取所有 sidebar-group 元素(排除第一个)
  110. const groupSelector = '.sidebar-group:not(:first-child)';
  111. const groupEl = Array.from(wrapperEl.querySelectorAll(groupSelector));
  112.  
  113. const lengths = [];
  114.  
  115. groupEl.forEach((item) => {
  116. const itemSelector = 'a.link';
  117. const itemEl = Array.from(item.querySelectorAll(itemSelector));
  118. const length = itemEl.length;
  119. const titleSelector = '.sidebar-group__title';
  120. const titleEl = item.querySelector(titleSelector);
  121. const title = titleEl.textContent;
  122. titleEl.textContent = `${title} (${length})`;
  123. lengths.push(length);
  124. });
  125.  
  126. const sum = lengths.reduce((acc, curr) => acc + curr, 0);
  127. const sumText = `🚀 共有组件 ${sum} 个`;
  128. const logoEl = document.querySelector(logoSelector);
  129. if (logoEl) {
  130. logoEl.title = sumText;
  131. }
  132. console.log(sumText);
  133. return wrapperEl;
  134. }
  135.  
  136. /**
  137. * Execute a function when the document is ready.
  138. * @param {function} eventHandler - Function to execute when the document is ready
  139. */
  140. function ready(eventHandler) {
  141. if (document.readyState !== 'loading') {
  142. eventHandler();
  143. } else {
  144. document.addEventListener('DOMContentLoaded', eventHandler);
  145. }
  146. }
  147.  
  148. /**
  149. * Wait for an element to be found on the page using polling.
  150. * @param {string} selector - CSS selector for the element to wait for
  151. * @param {function} callback - Function to execute when the element is found
  152. * @param {number} maxAttempts - Maximum number of attempts to find the element
  153. * @returns {number} intervalId - ID of the interval used to poll for the element
  154. */
  155. function poll(selector, callback, maxAttempts = 10) {
  156. let attempts = 0;
  157.  
  158. const intervalId = setInterval(() => {
  159. attempts++;
  160. const element = document.querySelector(selector);
  161.  
  162. if (element) {
  163. clearInterval(intervalId);
  164. if (callback && typeof callback === 'function') {
  165. callback(element);
  166. }
  167. } else if (attempts >= maxAttempts) {
  168. clearInterval(intervalId);
  169. console.log(`Element ${selector} not found after ${maxAttempts} attempts.`);
  170. }
  171. }, 1000);
  172.  
  173. return intervalId;
  174. }
  175.  
  176. /**
  177. * Check if an element matches a CSS selector.
  178. * @param {Element} currentElement - The element to check for a match
  179. * @param {string} selector - CSS selector to match against
  180. * @returns {boolean} - True if the selector matches, false otherwise
  181. */
  182. function matches(currentElement, selector) {
  183. while (currentElement !== null && currentElement !== document.body) {
  184. if (currentElement.matches(selector)) {
  185. return true;
  186. }
  187. currentElement = currentElement.parentElement;
  188. }
  189.  
  190. // 检查 body 元素
  191. return document.body.matches(selector);
  192. }
  193.  
  194. function icon() {
  195. 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>`;
  196. }
  197.  
  198. const style = `
  199. .x-toggle {
  200. position: fixed;
  201. top: 18px;
  202. right: 16px;
  203. z-index: 99999;
  204. cursor: pointer;
  205. opacity: 0.8;
  206. transition: opacity 0.3s ease-in-out;
  207. }
  208.  
  209. .x-toggle:hover {
  210. opacity: 1;
  211. }
  212.  
  213. #x-menu-wrapper {
  214. position: fixed !important;
  215. top: 55px !important;
  216. right: 0 !important;
  217. bottom: 0 !important;
  218. left: 0 !important;
  219. z-index: 9999 !important;
  220. max-width: 100% !important;
  221. width: 100% !important;
  222. max-height: calc(100vh - 55px) !important;
  223. padding: 0 !important;
  224. background: #fff !important;
  225. /* border-block-start: 1px solid rgba(5, 5, 5, 0.06) !important; */
  226. }
  227.  
  228. #x-menu-wrapper .sidebar-groups {
  229. display: grid !important;
  230. grid-auto-flow: column !important;
  231. grid-auto-columns: 220px !important;
  232. max-width: max-content !important;
  233. gap: 16px !important;
  234. overflow: auto;
  235. margin-inline: auto !important;
  236. border-inline-end: none !important;
  237. }
  238.  
  239. #x-menu-wrapper .doc-content-side {
  240. display: none !important;
  241. }
  242.  
  243. #x-menu-wrapper .sidebar-group__title {
  244. font-size: 12px !important;
  245. margin-block-end: 4px !important;
  246. }
  247.  
  248. #x-menu-wrapper.x-dashboard-component .sidebar-group:nth-child(1) {
  249. display: none !important;
  250. }
  251.  
  252. #x-menu-wrapper .sidebar-group {
  253. padding-block-start: 16px !important;
  254. }
  255.  
  256. #x-menu-wrapper .sidebar-group .link {
  257. padding: 6px 0 !important;
  258. }
  259.  
  260. #x-menu-wrapper .sidebar-group .link-text {
  261. font-size: 12px !important;
  262. font-weight: 400 !important;
  263. }
  264. `;
  265. GM_addStyle(style);
  266. })();