NGA Menu Simplify

NGA 菜单精简,顶部的菜单实在太多了!

// ==UserScript==
// @name        NGA Menu Simplify
// @name:zh-CN  NGA 菜单精简
// @namespace   https://greasyfork.org/users/263018
// @version     1.0.0
// @author      snyssss
// @description NGA 菜单精简,顶部的菜单实在太多了!
// @license     MIT

// @match       *://bbs.nga.cn/*
// @match       *://ngabbs.com/*
// @match       *://nga.178.com/*

// @require     https://update.greasyfork.org/scripts/486070/1358886/NGA%20Library.js

// @grant       GM_addStyle
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_registerMenuCommand
// @grant       unsafeWindow

// @run-at      document-start
// @noframes
// ==/UserScript==

(() => {
  // 声明泥潭主模块
  let commonui;

  // STYLE
  GM_addStyle(`
    .s-table-wrapper {
        max-height: 80vh;
        overflow-y: auto;
    }
    .s-table {
        margin: 0;
    }
    .s-table th,
    .s-table td {
        position: relative;
        white-space: nowrap;
    }
    .s-table th {
        position: sticky;
        top: 2px;
        z-index: 1;
    }
    .s-table input:not([type]), .s-table input[type="text"] {
        margin: 0;
        box-sizing: border-box;
        height: 100%;
        width: 100%;
    }
    .s-input-wrapper {
        position: absolute;
        top: 6px;
        right: 6px;
        bottom: 6px;
        left: 6px;
    }
    .s-text-ellipsis {
        display: flex;
    }
    .s-text-ellipsis > * {
        flex: 1;
        width: 1px;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    .s-button-group {
        margin: -.1em -.2em;
    }
  `);

  /**
   * UI
   */
  class UI {
    /**
     * 标签
     */
    static label = "菜单精简";

    /**
     * 弹出窗
     */
    window;

    /**
     * 视图元素
     */
    views = {};

    /**
     * 初始化
     */
    constructor() {
      this.init();
    }

    /**
     * 初始化,创建基础视图,初始化通用设置
     */
    init() {
      const tabs = this.createTabs({
        className: "right_",
      });

      const content = this.createElement("DIV", [], {
        style: "width: 400px;",
      });

      const container = this.createElement("DIV", [tabs, content]);

      this.views = {
        tabs,
        content,
        container,
      };
    }

    /**
     * 创建元素
     * @param   {String}                               tagName    标签
     * @param   {HTMLElement | HTMLElement[] | String} content    内容,元素或者 innerHTML
     * @param   {*}                                    properties 额外属性
     * @returns {HTMLElement}                                     元素
     */
    createElement(tagName, content, properties = {}) {
      const element = document.createElement(tagName);

      // 写入内容
      if (typeof content === "string") {
        element.innerHTML = content;
      } else {
        if (Array.isArray(content) === false) {
          content = [content];
        }

        content.forEach((item) => {
          if (item === null) {
            return;
          }

          if (typeof item === "string") {
            element.append(item);
            return;
          }

          element.appendChild(item);
        });
      }

      // 对 A 标签的额外处理
      if (tagName.toUpperCase() === "A") {
        if (Object.hasOwn(properties, "href") === false) {
          properties.href = "javascript: void(0);";
        }
      }

      // 附加属性
      Object.entries(properties).forEach(([key, value]) => {
        element[key] = value;
      });

      return element;
    }

    /**
     * 创建按钮
     * @param {String}   text       文字
     * @param {Function} onclick    点击事件
     * @param {*}        properties 额外属性
     */
    createButton(text, onclick, properties = {}) {
      return this.createElement("BUTTON", text, {
        ...properties,
        onclick,
      });
    }

    /**
     * 创建按钮组
     * @param {Array} buttons 按钮集合
     */
    createButtonGroup(...buttons) {
      return this.createElement("DIV", buttons, {
        className: "filter-button-group",
      });
    }

    /**
     * 创建表格
     * @param   {Array}       headers    表头集合
     * @param   {*}           properties 额外属性
     * @returns {HTMLElement}            元素和相关函数
     */
    createTable(headers, properties = {}) {
      const rows = [];

      const ths = headers.map((item, index) =>
        this.createElement("TH", item.label, {
          ...item,
          className: `c${index + 1}`,
        })
      );

      const tr =
        ths.length > 0
          ? this.createElement("TR", ths, {
              className: "block_txt_c0",
            })
          : null;

      const thead = tr !== null ? this.createElement("THEAD", tr) : null;

      const tbody = this.createElement("TBODY", []);

      const table = this.createElement("TABLE", [thead, tbody], {
        ...properties,
        className: "filter-table forumbox",
      });

      const wrapper = this.createElement("DIV", table, {
        className: "filter-table-wrapper",
      });

      const intersectionObserver = new IntersectionObserver((entries) => {
        if (entries[0].intersectionRatio <= 0) return;

        const list = rows.splice(0, 10);

        if (list.length === 0) {
          return;
        }

        intersectionObserver.disconnect();

        tbody.append(...list);

        intersectionObserver.observe(tbody.lastElementChild);
      });

      const add = (...columns) => {
        const tds = columns.map((column, index) => {
          if (ths[index]) {
            const { center, ellipsis } = ths[index];

            const properties = {};

            if (center) {
              properties.style = "text-align: center;";
            }

            if (ellipsis) {
              properties.className = "filter-text-ellipsis";
            }

            column = this.createElement("DIV", column, properties);
          }

          return this.createElement("TD", column, {
            className: `c${index + 1}`,
          });
        });

        const tr = this.createElement("TR", tds, {
          className: `row${(rows.length % 2) + 1}`,
        });

        intersectionObserver.disconnect();

        rows.push(tr);

        intersectionObserver.observe(tbody.lastElementChild || tbody);
      };

      const update = (e, ...columns) => {
        const row = e.target.closest("TR");

        if (row) {
          const tds = row.querySelectorAll("TD");

          columns.map((column, index) => {
            if (ths[index]) {
              const { center, ellipsis } = ths[index];

              const properties = {};

              if (center) {
                properties.style = "text-align: center;";
              }

              if (ellipsis) {
                properties.className = "filter-text-ellipsis";
              }

              column = this.createElement("DIV", column, properties);
            }

            if (tds[index]) {
              tds[index].innerHTML = "";
              tds[index].append(column);
            }
          });
        }
      };

      const remove = (e) => {
        const row = e.target.closest("TR");

        if (row) {
          tbody.removeChild(row);
        }
      };

      const clear = () => {
        rows.splice(0);
        intersectionObserver.disconnect();

        tbody.innerHTML = "";
      };

      Object.assign(wrapper, {
        add,
        update,
        remove,
        clear,
      });

      return wrapper;
    }

    /**
     * 创建标签组
     * @param {*} properties 额外属性
     */
    createTabs(properties = {}) {
      const tabs = this.createElement(
        "DIV",
        `<table class="stdbtn" cellspacing="0">
          <tbody>
            <tr></tr>
          </tbody>
        </table>`,
        properties
      );

      return this.createElement(
        "DIV",
        [
          tabs,
          this.createElement("DIV", [], {
            className: "clear",
          }),
        ],
        {
          style: "display: none; margin-bottom: 5px;",
        }
      );
    }

    /**
     * 创建标签
     * @param {Element} tabs       标签组
     * @param {String}  label      标签名称
     * @param {Number}  order      标签顺序,重复则跳过
     * @param {*}       properties 额外属性
     */
    createTab(tabs, label, order, properties = {}) {
      const group = tabs.querySelector("TR");

      const items = [...group.childNodes];

      if (items.find((item) => item.order === order)) {
        return;
      }

      if (items.length > 0) {
        tabs.style.removeProperty("display");
      }

      const tab = this.createElement("A", label, {
        ...properties,
        className: "nobr silver",
        onclick: () => {
          if (tab.className === "nobr") {
            return;
          }

          group.querySelectorAll("A").forEach((item) => {
            if (item === tab) {
              item.className = "nobr";
            } else {
              item.className = "nobr silver";
            }
          });

          if (properties.onclick) {
            properties.onclick();
          }
        },
      });

      const wrapper = this.createElement("TD", tab, {
        order,
      });

      const anchor = items.find((item) => item.order > order);

      group.insertBefore(wrapper, anchor || null);

      return wrapper;
    }

    /**
     * 创建对话框
     * @param {HTMLElement | null} anchor  要绑定的元素,如果为空,直接弹出
     * @param {String}             title   对话框的标题
     * @param {HTMLElement}        content 对话框的内容
     */
    createDialog(anchor, title, content) {
      let window;

      const show = () => {
        if (window === undefined) {
          window = commonui.createCommmonWindow();
        }

        window._.addContent(null);
        window._.addTitle(title);
        window._.addContent(content);
        window._.show();
      };

      if (anchor) {
        anchor.onclick = show;
      } else {
        show();
      }

      return window;
    }

    /**
     * 渲染视图
     */
    renderView() {
      // 创建或打开弹出窗
      if (this.window === undefined) {
        this.window = this.createDialog(
          this.views.anchor,
          this.constructor.label,
          this.views.container
        );
      } else {
        this.window._.show();
      }

      // 启用第一个模块
      this.views.tabs.querySelector("A").click();
    }

    /**
     * 渲染
     */
    render() {
      this.renderView();
    }
  }

  /**
   * 基础模块
   */
  class Module {
    /**
     * 模块名称
     */
    static name;

    /**
     * 模块标签
     */
    static label;

    /**
     * 顺序
     */
    static order;

    /**
     * UI
     */
    ui;

    /**
     * 视图元素
     */
    views = {};

    /**
     * 初始化并绑定UI,注册 UI
     * @param {UI} ui UI
     */
    constructor(ui) {
      this.ui = ui;

      this.init();
    }

    /**
     * 获取列表
     */
    get list() {
      return GM_getValue(this.constructor.name, []);
    }

    /**
     * 写入列表
     */
    set list(value) {
      GM_setValue(this.constructor.name, value);
    }

    /**
     * 格式化名称
     * @param   {Number} index 菜单项下标
     * @returns                格式化后的名称
     */
    formatName(index) {
      if (index === 0) {
        return "主菜单";
      }

      if (index === 7) {
        return "LOGO";
      }

      if (index === 162) {
        return "搜索";
      }

      if (commonui.mainMenuItems[index].arg) {
        const i = commonui.mainMenuItems[index].arg.indexOf("innerHTML");

        return commonui.mainMenuItems[index].arg[i + 1];
      }

      return commonui.mainMenuItems[index].innerHTML;
    }

    /**
     * 切换启用状态
     * @param {Number} index 菜单项下标
     */
    toggle(index) {
      const list = this.list;

      if (this.list.includes(index)) {
        this.list = list.filter((i) => i !== index);
      } else {
        this.list = list.concat(index);
      }

      commonui.mainMenu.init(
        unsafeWindow.__CURRENT_UNAME,
        "",
        "",
        "",
        unsafeWindow.__CURRENT_UID
      );
    }

    /**
     * 初始化,创建基础视图和组件
     */
    init() {
      if (this.views.container) {
        this.destroy();
      }

      const { ui } = this;

      const container = ui.createElement("DIV", []);

      this.views = {
        container,
      };

      this.initComponents();
    }

    /**
     * 初始化组件
     */
    initComponents() {}

    /**
     * 销毁
     */
    destroy() {
      Object.values(this.views).forEach((view) => {
        if (view.parentNode) {
          view.parentNode.removeChild(view);
        }
      });

      this.views = {};
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      container.innerHTML = "";
      container.appendChild(this.views.container);
    }
  }

  /**
   * 左侧菜单
   */
  class LeftMenu extends Module {
    /**
     * 模块名称
     */
    static name = "hisDefLeft";

    /**
     * 模块标签
     */
    static label = "左侧";

    /**
     * 顺序
     */
    static order = 10;

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [{ label: "标题" }, { label: "是否启用", center: true, width: 1 }];
    }

    /**
     * 表格项
     * @param   {Number} index 菜单项下标
     * @returns {Array}        表格项集合
     */
    column(index) {
      const { ui, list } = this;

      // 名称
      const name = this.formatName(index);

      // 标题
      const label = ui.createElement("SPAN", name, {
        className: "nobr",
      });

      // 是否启用
      const enabled = ui.createElement("INPUT", [], {
        type: "checkbox",
        checked: list.includes(index) === false,
        onchange: () => {
          this.toggle(index);
        },
      });

      return [label, enabled];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { tabs, content } = this.ui.views;

      const table = this.ui.createTable(this.columns());

      const tab = this.ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        const list = commonui.mainMenuItems.hisDefLeft;

        Object.values(list).forEach((item) => {
          const entity = commonui.mainMenuItems[item];

          if (entity) {
            const column = this.column(item);

            add(...column);
          }
        });
      }
    }
  }

  /**
   * 右侧菜单
   */
  class RightMenu extends Module {
    /**
     * 模块名称
     */
    static name = "hisDef";

    /**
     * 模块标签
     */
    static label = "右侧";

    /**
     * 顺序
     */
    static order = 20;

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [{ label: "标题" }, { label: "是否启用", center: true, width: 1 }];
    }

    /**
     * 表格项
     * @param   {Number} index 菜单项下标
     * @returns {Array}        表格项集合
     */
    column(index) {
      const { ui, list } = this;

      // 名称
      const name = this.formatName(index);

      // 标题
      const label = ui.createElement("SPAN", name, {
        className: "nobr",
      });

      // 是否启用
      const enabled = ui.createElement("INPUT", [], {
        type: "checkbox",
        checked: list.includes(index) === false,
        onchange: () => {
          this.toggle(index);
        },
      });

      return [label, enabled];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { tabs, content } = this.ui.views;

      const table = this.ui.createTable(this.columns());

      const tab = this.ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        const list = commonui.mainMenuItems.hisDef;

        Object.values(list).forEach((item) => {
          const entity = commonui.mainMenuItems[item];

          if (entity) {
            const column = this.column(item);

            add(...column);
          }
        });
      }
    }
  }

  /**
   * 处理 commonui 模块
   * @param {*} value commonui
   */
  const handleCommonui = (value) => {
    // 绑定主模块
    commonui = value;

    // 拦截 mainMenu 模块,处理 init 事件
    Tools.interceptProperty(commonui, "mainMenu", {
      afterSet: (mainMenu) => {
        let hisDefLeft, hisDef;

        // 拦截 init 事件,请求前缓存原始菜单并禁用,请求后恢复
        // TODO 此处需要优化,应有统一的获取缓存方式
        Tools.interceptProperty(mainMenu, "init", {
          beforeGet: (...args) => {
            const disabledHisDefLeft = GM_getValue("hisDefLeft", []);
            const disabledHisDef = GM_getValue("hisDef", []);

            hisDefLeft = [...mainMenu.data.hisDefLeft];
            hisDef = [...mainMenu.data.hisDef];

            mainMenu.data.hisDefLeft = mainMenu.data.hisDefLeft.filter(
              (item) => disabledHisDefLeft.includes(item) === false
            );

            mainMenu.data.hisDef = mainMenu.data.hisDef.filter(
              (item) => disabledHisDef.includes(item) === false
            );

            return args;
          },
          afterGet: () => {
            mainMenu.data.hisDefLeft = [...hisDefLeft];
            mainMenu.data.hisDef = [...hisDef];
          },
          afterSet: () => {
            if (mainMenu.dataReady === null) {
              return;
            }

            mainMenu.init(
              unsafeWindow.__CURRENT_UNAME,
              "",
              "",
              "",
              unsafeWindow.__CURRENT_UID
            );
          },
        });
      },
    });
  };

  /**
   * 注册脚本菜单
   */
  const registerMenu = () => {
    let ui;

    GM_registerMenuCommand(`设置`, () => {
      if (commonui && commonui.mainMenuItems) {
        if (ui === undefined) {
          ui = new UI();

          new LeftMenu(ui);
          new RightMenu(ui);
        }

        ui.render();
      }
    });
  };

  // 主函数
  (async () => {
    // 注册脚本菜单
    registerMenu();

    // 处理 commonui 模块
    if (unsafeWindow.commonui) {
      handleCommonui(unsafeWindow.commonui);
      return;
    }

    Tools.interceptProperty(unsafeWindow, "commonui", {
      afterSet: (value) => {
        handleCommonui(value);
      },
    });
  })();
})();