// ==UserScript==
// @name Ant Design Components Dashboard (React) (^4.0.0) / Ant Design 组件看板
// @namespace https://github.com/xianghongai/Ant-Design-Components-Dashboard-React
// @version 0.0.7
// @description Better view for Ant Design (React) / 更方便的查看 Ant Design (React) 组件
// @author Nicholas Hsiang / 山茶树和葡萄树
// @icon https://xinlu.ink/favicon.ico
// @match https://4x.ant.design/*
// @grant none
// ==/UserScript==
(function () {
"use strict";
// NOTE: 已知问题:
// 当离开“组件”页面进入“设计、文档、资源”时,会引发 DOMException 异常,需要刷新页面才会生效
const bodyContainer = document.querySelector("body");
const titleText = "Ant Design of React (4.x)";
// 有的站点能够直接从菜单 root 操作
// 有的则不能,因为他们在菜单切换时,是通过 Markdown 动态生态,需要插入到 root 层,不然报错
const gridSelector = ".aside-container.menu-site";
const columnSelector = ".ant-menu-item-group";
const columnTitleSelector = ".ant-menu-item-group-title";
const menuListSelector = ".ant-menu-item-group-list";
const menuItemSelector = ".ant-menu-item-group-list .ant-menu-item";
const helpEnable = true;
const helpSelector = ".api-container";
const removeSelector = gridSelector + ">li:not(.ant-menu-item-group)";
const cloneNodeEnable = true; // 保留原 DOM 节点? 有的站点上设置 true 会造成刷新
let interval = null;
let timeout = null;
let created = false;
// #region 点击 Nav
/** 导航菜单点击事件 */
function handleMenuSiteNav(event) {
const eventTarget = event.target;
const tagName = eventTarget.tagName.toLowerCase();
if (tagName === "a") {
enterOrLeave(eventTarget.href);
}
}
const menuSiteNavHandler = debounce(handleMenuSiteNav, 500);
/** 导航菜单绑定事件 */
function initialMenuSiteNavEvent() {
var menuSite = document.querySelector("#nav");
menuSite.addEventListener("click", menuSiteNavHandler);
}
// #endregion 点击 Nav
// #region 组件页面 24 栅格
function resetLayout(type) {
const pageSider = document.querySelector(".main-wrapper>.ant-row>.main-menu");
const pageContainer = document.querySelector(".main-wrapper>.ant-row>.ant-col+.ant-col");
if (!pageSider || !pageContainer) {
return false;
}
switch (type) {
case "in":
pageSider.classList.add("hs-hide");
pageContainer.classList.remove("ant-col-md-18", "ant-col-lg-18", "ant-col-xl-19", "ant-col-xxl-20");
pageContainer.classList.add("ant-col-md-24", "ant-col-lg-24", "ant-col-xl-24", "ant-col-xxl-24");
break;
default:
pageSider.classList.remove("hs-hide");
pageContainer.classList.remove("ant-col-md-24", "ant-col-lg-24", "ant-col-xl-24", "ant-col-xxl-24");
pageContainer.classList.add("ant-col-md-18", "ant-col-lg-18", "ant-col-xl-19", "ant-col-xxl-20");
break;
}
}
// #endregion 组件页面 24 栅格
// #region 看当前 URL 是不是组件页面
function enterOrLeave(href = window.location.href) {
if (href.includes("components")) {
console.log("Ant Design Components Dashboard (React) (^4.0.0)");
bodyContainer.classList.add("hs-page__component");
resetLayout("in");
if (created === false) {
created = true;
timeout = window.setTimeout(() => {
initialDashboard();
}, 500);
}
} else {
bodyContainer.classList.remove("hs-page__component");
resetLayout("off");
}
}
// #endregion 看当前 URL 是不是组件页面
// #region MENU
/** 生成 Menu */
function initialMenu() {
// Wrapper
const wrapperEle = document.createElement("section");
wrapperEle.classList.add("hs-dashboard__wrapper", "hs-hide");
// Header
const headerEle = document.createElement("header");
headerEle.classList.add("hs-dashboard__header");
// Title
const titleEle = document.createElement("h1");
titleEle.classList.add("hs-dashboard__title");
titleEle.innerText = titleText || "";
// Title → Header
headerEle.appendChild(titleEle);
// Menu
const containerEle = document.createElement("div");
containerEle.classList.add("hs-dashboard__container");
// 0. 移除一些不要的元素
if (removeSelector) {
const removeEle = document.querySelectorAll(removeSelector);
if (removeEle) {
removeEle.forEach((element) => {
// element.remove();
element.classList.add("hs-hide");
});
}
}
// 1. 先从页面上获取 DOM
let gridEle = null;
if (cloneNodeEnable) {
gridEle = document.querySelector(gridSelector).cloneNode(true);
} else {
gridEle = document.querySelector(gridSelector);
}
let menuEle = document.createElement("nav");
menuEle.setAttribute("class", gridEle.className);
menuEle.classList.add("hs-dashboard__grid");
let menuItemsEle = gridEle.querySelectorAll(columnSelector);
menuItemsEle.forEach((element) => {
menuEle.appendChild(element);
});
// Menu → Container
containerEle.appendChild(menuEle);
// 2. 内部元素追加新的样式
// 2.1 column
const columnEle = containerEle.querySelectorAll(columnSelector);
columnEle.forEach((element) => {
element.classList.add("hs-dashboard__column");
});
// 2.2 title
const columnTitleEle = containerEle.querySelectorAll(columnTitleSelector);
columnTitleEle.forEach((element) => {
element.classList.add("hs-dashboard__item-title");
});
// 2.3 menu list
const menuListEle = containerEle.querySelectorAll(menuListSelector);
menuListEle.forEach((element) => {
element.classList.add("hs-dashboard__list");
});
// 2.4 menu item
const menuItemEle = containerEle.querySelectorAll(menuItemSelector);
menuItemEle.forEach((element) => {
element.classList.add("hs-dashboard__item");
});
// header,container → wrapper
wrapperEle.appendChild(headerEle);
wrapperEle.appendChild(containerEle);
// wrapper → body
bodyContainer.appendChild(wrapperEle);
}
// #endregion MENU
// #region Event
/** 注册(不可用)事件 */
function handleEvent() {
const wrapperEle = document.querySelector(".hs-dashboard__wrapper");
const toggleMenuBtn = document.querySelector('.hs-dashboard__toggle-menu');
const toggleHelpBtn = document.querySelector('.hs-dashboard__toggle-help');
function handler(event) {
const targetEle = event.target;
const isItem = getParents(targetEle, ".hs-dashboard__item") || hasClass(targetEle, "hs-dashboard__item") || (getParents(targetEle, ".hs-dashboard__column") && getParents(targetEle, ".hs-dashboard__list"));
const isToggle = getParents(targetEle, ".hs-dashboard__toggle-menu") || hasClass(targetEle, "hs-dashboard__toggle-menu");
const isHelp = getParents(targetEle, ".hs-dashboard__toggle-help") || hasClass(targetEle, "hs-dashboard__toggle-help");
if (isItem) {
clearStyle(wrapperEle);
} else if (isToggle) {
wrapperEle.classList.toggle("hs-hide");
bodyContainer.classList.toggle("hs-body-overflow_hide");
} else if (isHelp) {
clearStyle(wrapperEle);
handleHelp();
}
}
bodyContainer.addEventListener("click", handler);
document.addEventListener('keydown', function (event) {
if (event.key === 'Tab' || event.code === 'Tab') {
event.preventDefault();
event.stopPropagation();
toggleMenuBtn?.click();
}
// else if (event.key === 'Escape' || event.code === 'Escape') {
// toggleMenuBtn.click();
// }
else if (event.key === 'F1' || event.code === 'F1') {
event.preventDefault();
event.stopPropagation();
toggleHelpBtn?.click();
}
});
}
function clearStyle(wrapperEle) {
wrapperEle.classList.add("hs-hide");
bodyContainer.classList.remove("hs-body-overflow_hide");
}
// #endregion Event
// #region HELP
/** 是否启用‘页面滚动至指定位置’ */
function initialHelp() {
if (!helpEnable) {
const ele = document.querySelector(".hs-dashboard__toggle-help");
ele.classList.add("hs-hide");
}
}
/** 页面滚动至指定位置 */
function handleHelp() {
if (!helpSelector) {
return false;
}
const helpEle = document.querySelector(helpSelector);
const top = helpEle.getBoundingClientRect().top + window.pageYOffset;
window.scrollTo({
top,
behavior: "smooth",
});
}
// #endregion HELP
// #region STYLE
/** 添加样式 */
function initialStyle() {
const tpl = initialStyleTpl();
const headEle = document.head || document.getElementsByTagName("head")[0];
const styleEle = document.createElement("style");
styleEle.type = "text/css";
if (styleEle.styleSheet) {
styleEle.styleSheet.cssText = tpl;
} else {
styleEle.appendChild(document.createTextNode(tpl));
}
headEle.appendChild(styleEle);
}
/** 样式表 */
function initialStyleTpl() {
return `
.hs-hide {
display: none !important;
}
.hs-body-overflow_hide {
height: 100% !important;
overflow: hidden !important;
}
/* #region toggle */
.hs-dashboard__toggle {
position: fixed;
z-index: 99999;
top: 5px;
right: 5px;
}
.hs-dashboard__toggle-item {
width: 28px;
height: 28px;
margin-top: 10px;
margin-bottom: 10px;
overflow: hidden;
line-height: 30px !important;
border-radius: 50%;
border: 1px solid #ccc;
text-align: center;
color: #555;
background-color: #fff;
cursor: pointer;
transition: all 0.2s;
}
.hs-dashboard__toggle-item:hover {
border-color: #aaa;
color: #111;
}
.hs-dashboard__toggle-icon {
font-style: normal !important;
}
/* #endregion toggle */
/* #region wrapper */
.hs-dashboard__wrapper {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 99998;
overflow-y: auto;
background-color: #fff;
font-size: 16px;
}
.hs-dashboard__wrapper::-webkit-scrollbar {
width: 8px;
height: 6px;
background: rgba(0, 0, 0, 0.1);
}
.hs-dashboard__wrapper::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.3);
}
.hs-dashboard__wrapper::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
}
/* #endregion wrapper */
.hs-dashboard__header {
padding-top: 10px;
text-align: center;
}
.hs-dashboard__title {
margin: 0;
padding-top: 10px;
padding-bottom: 10px;
font-size: 1em;
font-weight: normal;
}
/* #region grid */
.hs-dashboard__grid {
display: flex;
justify-content: space-evenly;
/* justify-content: space-around; */
margin: 0;
padding: 0;
list-style: none;
}
.hs-dashboard__column {
padding-right: 10px;
padding-left: 10px;
}
.hs-dashboard__column a {
text-decoration: none;
}
.hs-dashboard__column {
list-style: none;
}
.hs-dashboard__column ul {
padding: 0;
}
.hs-dashboard__column li {
list-style: none;
}
.hs-dashboard__column .hs-dashboard__item-title {
display: block;
margin-top: 0 !important;
}
/* #endregion grid */
/* #region custom */
.fixed-widgets {
z-index: 9;
}
body[data-theme='dark'] .hs-dashboard__wrapper,
body[data-theme='dark'] .hs-menu-wrapper.ant-menu {
color: rgba(255,255,255,0.65);
background-color: #141414;
}
body[data-theme='dark'] .hs-dashboard__title {
color: rgba(255,255,255,0.65);
}
.hs-dashboard__column .hs-dashboard__list .hs-dashboard__item,
.hs-dashboard__column .hs-dashboard__list .ant-menu-item {
height: 36px;
line-height: 36px;
margin-top: 0;
margin-bottom: 0;
}
/* #endregion custom */
`;
}
// #endregion STYLE
// #region TOGGLE
/** 生成 Dashboard 开关 */
function initialToggle() {
const tpl = initialToggleTpl();
const ele = document.createElement("section");
// ele.className = 'hs-dashboard__toggle';
// ele.setAttribute("class", "hs-dashboard__toggle");
ele.classList.add("hs-dashboard__toggle");
ele.innerHTML = tpl;
// toggle → body
bodyContainer.appendChild(ele);
}
/** Dashboard 开关 DOM */
function initialToggleTpl() {
return `
<!-- menu -->
<div class="hs-dashboard__toggle-item hs-dashboard__toggle-menu">
<i class="hs-dashboard__toggle-icon">
<svg
viewBox="64 64 896 896"
focusable="false"
data-icon="appstore"
width="1em"
height="1em"
fill="currentColor"
aria-hidden="true"
>
<path
d="M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"
></path>
</svg>
</i>
</div>
<!-- api -->
<div class="hs-dashboard__toggle-item hs-dashboard__toggle-help">
<i class="hs-dashboard__toggle-icon">
<svg
viewBox="64 64 896 896"
focusable="false"
class=""
data-icon="bulb"
width="1em"
height="1em"
fill="currentColor"
aria-hidden="true"
>
<path
d="M632 888H392c-4.4 0-8 3.6-8 8v32c0 17.7 14.3 32 32 32h192c17.7 0 32-14.3 32-32v-32c0-4.4-3.6-8-8-8zM512 64c-181.1 0-328 146.9-328 328 0 121.4 66 227.4 164 284.1V792c0 17.7 14.3 32 32 32h264c17.7 0 32-14.3 32-32V676.1c98-56.7 164-162.7 164-284.1 0-181.1-146.9-328-328-328zm127.9 549.8L604 634.6V752H420V634.6l-35.9-20.8C305.4 568.3 256 484.5 256 392c0-141.4 114.6-256 256-256s256 114.6 256 256c0 92.5-49.4 176.3-128.1 221.8z"
></path>
</svg>
</i>
</div>
`;
}
// #endregion TOGGLE
// #region COMMON
function hasClass(el, className) {
if (el.classList) {
return el.classList.contains(className);
} else {
return !!el.className.match(new RegExp("(\\s|^)" + className + "(\\s|$)"));
}
}
function getParents(elem, selector) {
// Element.matches() polyfill
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
function (s) {
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
i = matches.length;
while (--i >= 0 && matches.item(i) !== this) { }
return i > -1;
};
}
// Get the closest matching element
for (; elem && elem !== document; elem = elem.parentNode) {
if (elem.matches(selector)) return elem;
}
return null;
}
function debounce(callback, delay) {
var timer = null;
return function () {
if (timer) return;
callback.apply(this, arguments);
timer = setTimeout(() => (timer = null), delay);
};
}
function throttle(callback, delay) {
let isThrottled = false,
args,
context;
function wrapper() {
if (isThrottled) {
args = arguments;
context = this;
return;
}
isThrottled = true;
callback.apply(this, arguments);
setTimeout(() => {
isThrottled = false;
if (args) {
wrapper.apply(context, args);
args = context = null;
}
}, delay);
}
return wrapper;
}
// #endregion
function initialDashboard() {
window.clearTimeout(timeout);
initialToggle();
initialStyle();
initialMenu();
initialHelp();
handleEvent();
}
function ready() {
const originEle = document.querySelector(gridSelector);
if (originEle) {
window.clearInterval(interval);
initialMenuSiteNavEvent();
enterOrLeave();
}
}
interval = window.setInterval(ready, 1000);
})();