Hi, Element Plus Component Dashboard
Verze ze dne
// ==UserScript==
// @name Hi, Element Plus Component Dashboard🚀
// @namespace https://github.com/xianghongai/Tampermonkey-UserScript
// @version 1.0.2
// @description Hi, Element Plus Component Dashboard
// @author Nicholas Hsiang
// @match *://element-plus.org/*
// @icon https://element-plus.org/images/element-plus-logo-small.svg
// @grant GM_addStyle
// @grant GM_info
// @run-at document-end
// @grant unsafeWindow
// @license MIT
// ==/UserScript==
(function () {
'use strict';
console.log(GM_info.script.name);
const logoSelector = '.logo-container img.logo';
const menuSelector = '.sidebar';
let wrapperEl = null;
main();
/**
* Main function to execute when the script is loaded.
*/
function main() {
ready(() => {
poll(menuSelector, handler, 500);
});
}
/**
* Toggle the target element.
*/
function handler() {
const closeSpan = document.createElement('span');
closeSpan.className = 'x-toggle';
closeSpan.innerHTML = icon();
closeSpan.addEventListener('click', (event) => {
// hold shift key to reset
if (event.shiftKey) {
wrapperEl.removeAttribute('id');
wrapperEl.style.display = 'block';
return;
}
// init
if (!wrapperEl || wrapperEl.id !== 'x-menu-wrapper') {
wrapperEl = dashboard();
// add component item click event listener
componentItemClickEventListener(wrapperEl, '.link');
handleComponentPageClass(wrapperEl);
return;
}
wrapperEl.style.display = wrapperEl.style.display === 'none' ? 'grid' : 'none';
});
document.body.appendChild(closeSpan);
navClickEventListener(wrapperEl);
}
/**
* Click the navbar menu element, handle the component page.
* @param {Element} wrapperEl - The wrapper element
*/
function navClickEventListener(wrapperEl) {
const navSelector = '.navbar-menu';
const navEl = document.querySelector(navSelector);
if (navEl) {
navEl.addEventListener('click', () => {
handleComponentPageClass(wrapperEl);
});
}
}
function handleComponentPageClass(wrapperEl) {
if (window.location.href.includes('component')) {
wrapperEl.classList.add('x-dashboard-component');
} else {
wrapperEl.classList.remove('x-dashboard-component');
}
}
/**
* Click the target element.
* @param {Element} currentElement - The target element
* @param {string} selector - The selector of the target element
*/
function componentItemClickEventListener(currentElement, selector) {
currentElement.addEventListener('click', (event) => {
if (matches(event.target, selector)) {
currentElement.style.display = 'none';
}
});
}
/**
* Create the dashboard element.
* @returns {Element} - The dashboard element
*/
function dashboard() {
wrapperEl = document.querySelector(menuSelector);
wrapperEl.setAttribute('id', 'x-menu-wrapper');
// 获取所有 sidebar-group 元素(排除第一个)
const groupSelector = '.sidebar-group:not(:first-child)';
const groupEl = Array.from(wrapperEl.querySelectorAll(groupSelector));
const lengths = [];
groupEl.forEach((item) => {
const itemSelector = 'a.link';
const itemEl = Array.from(item.querySelectorAll(itemSelector));
const length = itemEl.length;
const titleSelector = '.sidebar-group__title';
const titleEl = item.querySelector(titleSelector);
const title = titleEl.textContent;
titleEl.textContent = `${title} (${length})`;
lengths.push(length);
});
const sum = lengths.reduce((acc, curr) => acc + curr, 0);
const sumText = `🚀 共有组件 ${sum} 个`;
const logoEl = document.querySelector(logoSelector);
if (logoEl) {
logoEl.title = sumText;
}
console.log(sumText);
return wrapperEl;
}
/**
* Execute a function when the document is ready.
* @param {function} eventHandler - Function to execute when the document is ready
*/
function ready(eventHandler) {
if (document.readyState !== 'loading') {
eventHandler();
} else {
document.addEventListener('DOMContentLoaded', eventHandler);
}
}
/**
* Wait for an element to be found on the page using polling.
* @param {string} selector - CSS selector for the element to wait for
* @param {function} callback - Function to execute when the element is found
* @param {number} maxAttempts - Maximum number of attempts to find the element
* @returns {number} intervalId - ID of the interval used to poll for the element
*/
function poll(selector, callback, maxAttempts = 10) {
let attempts = 0;
const intervalId = setInterval(() => {
attempts++;
const element = document.querySelector(selector);
if (element) {
clearInterval(intervalId);
if (callback && typeof callback === 'function') {
callback(element);
}
} else if (attempts >= maxAttempts) {
clearInterval(intervalId);
console.log(`Element ${selector} not found after ${maxAttempts} attempts.`);
}
}, 1000);
return intervalId;
}
/**
* Check if an element matches a CSS selector.
* @param {Element} currentElement - The element to check for a match
* @param {string} selector - CSS selector to match against
* @returns {boolean} - True if the selector matches, false otherwise
*/
function matches(currentElement, selector) {
while (currentElement !== null && currentElement !== document.body) {
if (currentElement.matches(selector)) {
return true;
}
currentElement = currentElement.parentElement;
}
// 检查 body 元素
return document.body.matches(selector);
}
function icon() {
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>`;
}
const style = `
.x-toggle {
position: fixed;
top: 18px;
right: 16px;
z-index: 99999;
cursor: pointer;
opacity: 0.8;
transition: opacity 0.3s ease-in-out;
}
.x-toggle:hover {
opacity: 1;
}
#x-menu-wrapper {
position: fixed !important;
top: 55px !important;
right: 0 !important;
bottom: 0 !important;
left: 0 !important;
z-index: 9999 !important;
max-width: 100% !important;
width: 100% !important;
max-height: calc(100vh - 55px) !important;
padding: 0 !important;
background: #fff !important;
/* border-block-start: 1px solid rgba(5, 5, 5, 0.06) !important; */
}
#x-menu-wrapper .sidebar-groups {
display: grid !important;
grid-auto-flow: column !important;
grid-auto-columns: 220px !important;
max-width: max-content !important;
gap: 16px !important;
overflow: auto;
margin-inline: auto !important;
border-inline-end: none !important;
}
#x-menu-wrapper .doc-content-side {
display: none !important;
}
#x-menu-wrapper .sidebar-group__title {
font-size: 12px !important;
margin-block-end: 4px !important;
}
#x-menu-wrapper.x-dashboard-component .sidebar-group:nth-child(1) {
display: none !important;
}
#x-menu-wrapper .sidebar-group {
padding-block-start: 16px !important;
}
#x-menu-wrapper .sidebar-group .link {
padding: 6px 0 !important;
}
#x-menu-wrapper .sidebar-group .link-text {
font-size: 12px !important;
font-weight: 400 !important;
}
`;
GM_addStyle(style);
})();