Greasy Fork

Greasy Fork is available in English.

班固米-条目职位自定义排序与折叠

对[动画]条目的制作人员信息进行职位的自定义排序与折叠,可在[设置-隐私]页面进行相关设置

当前为 2024-11-26 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         班固米-条目职位自定义排序与折叠
// @namespace    https://github.com/weiduhuo/scripts
// @version      1.2.3-1.1
// @description  对[动画]条目的制作人员信息进行职位的自定义排序与折叠,可在[设置-隐私]页面进行相关设置
// @author       weiduhuo
// @match        *://bgm.tv/subject/*
// @match        *://bgm.tv/settings/privacy*
// @match        *://bangumi.tv/subject/*
// @match        *://bangumi.tv/settings/privacy*
// @match        *://chii.in/subject/*
// @match        *://chii.in/settings/privacy*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';
  const SCRIPT_NAME = '班固米-职位排序组件';
  // 图标
  const ICON = {
    // 三角形顶点向右,可表展开按键
    TRIANGLE_RIGHT: `
      <svg xmlns='http://www.w3.org/2000/svg' viewbox='0 0 13 13' height=' 0.7em'>
        <polygon points='0.5,0 12.5,6.5 0.5,13'  fill='currentColor' />
      </svg>
    `,
    // 三角形顶点向下,可表折叠按键
    TRIANGLE_DOWN: `
      <svg xmlns='http://www.w3.org/2000/svg' viewbox='0 0 13 13' height=' 0.7em'>
        <polygon points='0,0.5 13,0.5 6.5,12.5'  fill='currentColor' />
      </svg>
    `,
    // 三角形顶点向上,可表折叠按键
    TRIANGLE_UP: `
      <svg xmlns='http://www.w3.org/2000/svg' viewbox='0 0 13 13' height=' 0.7em'>
        <polygon points='0,12.5 13,12.5 6.5,0.5'  fill='currentColor' />
      </svg>
    `,
  };

  // 条目类型
  const SubjectType = {
    // 所支持的类型
    ANIME: 'anime',
    // 待支持的类型
    // BOOK: 'book', MUSIC: 'music', GAME: 'game', REAL: 'real', CHARACTER: 'character', PERSON: 'person',
    getAll() {
      return Object.values(this);
    },
    prase(value) {
      if (this.getAll().includes(value)) return value;
      return null;
    },
    // needPrase(value) {
    //   return value !== this.CHARACTER && value !== this.PERSON;
    // },
  };

  /**
   * 职位的排序列表`jobOrder`与默认折叠的职位`foldableJobs`的合并信息
   * 基本类型:`
      type = [Job | [boolean | Job, ...Job[]]]
      Job = string | RegExp
   * `其中`boolean`表示子序列内的职位是否默认折叠,缺损值为`False`,需位于子序列的首位才有意义
   * (下文`,,`表示在`JSON`数组中插入`null`元素,用于输出格式化文本时标记换行)
   */
  const staffMapList = [,
    "中文名", "类型", "适合年龄", /地区/, "语言", "对白", "话数", "总话数", [true, "季数"], ,
    "放送开始", "开始", "放送星期", "放送时间", "上映年度", /上映/, "发售日", "片长", /片长/, ,
    ,
    "原作", "原案", "人物原案", "原作插图", [true, "原作协力"], ,
    "团长", "总导演", "导演", "副导演", "执行导演", "主任导演", "联合导演", "系列监督", ,
    "系列构成", "脚本", "编剧", [true, /脚本|内容|故事|文艺|主笔/], ,
    "分镜", "OP・ED 分镜", "主演出", "演出", [true, "演出助理"], ,
    "人物设定", ,
    ,
    "总作画监督", [false, "作画监督"], [true, "作画监督助理"], "动作作画监督", "机械作画监督", "特效作画监督", /.*作画.*(监|导)/, ,
    "主动画师", "主要动画师", [true, "构图"], [false, "原画"], [true, "第二原画", "补间动画"], "数码绘图", /(原画|动画|動画)(?!制|检查)/, ,
    "动画检查", [true, /动画检查/], ,
    ,
    "设定", "背景设定", "道具设计", /(?<!色彩|美术|美術)(设|設)(?!.*制)/, /Design|デザイン|Motion Graphic|モーショングラフィック/, ,
    "色彩设计", [false, "色彩指定", "色指定"], [true, "上色", /(?<!角)色/], ,
    "美术监督", /(美术|美術).*导演/, [false, "美术设计"], "概念美术", "视觉概念", "概念艺术", ,
    [false, "背景美术"], [true, /景/], /ART WORK|美术|美術|艺术|工艺|创意|绘制/, ,
    ,
    /CG.*导演/, "3DCG", [false, /2D|3D|CG|コンピュータ/], "模型监督", "绑骨监督", [/建模|模型|动作|表情|骨/], ,
    "摄影监督", "副摄影监督", "后期监督", [true, "摄影", "特效", "照明", "特技", /摄影|モニター|特效|动效|合成|拍|Effect|技术/], ,
    "現像", /タイトル|标题|字幕/, ,
    [false, "剪辑", "编集"], [true, /(?<!音.*)剪辑/], "编辑", [true, "场记"], ,
    "监修", /监修|監修/, "顾问", /顾问/, ,
    ,
    "音响监督", [true, "音响", "音响制作", "音效", "拟音", /音响/], "录音", [true, "录音助理", "混声", /录音|声/], ,
    "配音", "主演", "キャスティング", [true, /配音|((?<!歌)演出)/], ,
    "音乐", "音乐制作", [true, "音乐制作人", "音乐助理"], [true, /音/], ,
    "主题歌演出", [true, "主题歌作词", "主题歌作曲", "主题歌编曲"], "插入歌演出", ,
    [true, "插入歌作词", "插入歌作曲", "插入歌编曲"], [true, "选曲"], [true, /曲|歌/], ,
    ,
    "企画", [true, "企画协力"], "企划制作人", /企画|企划|出品|策划/, "监制", /监制/, ,
    "执行制片人", "总制片人", "制片人", "总制片", "制片", [true, "副制片人", "联合制片人", "助理制片人", /(?<!动画|動画)制片/], ,
    [true, /行政|审/, "责任编辑"], [true, /法务/], [true, "宣传", /宣传|宣伝|広報/], /市场|运营|衍生/, ,
    "制作", "製作", [true, "制作著作"], ,
    "动画制片人", [true, "制作管理", "制作统筹", "制作主任", "制作助理"], ,
    [true, "设定制作"], [true, "计划管理", "制作进行", "制作进行协力"], ,
    "制作协调", "制作协力", "制作助手", "协力", /协力|協力/, [true, /取材/], [true, "特别鸣谢", /鸣谢|Thanks/], ,
    ,
    "动画制作", [true, /制作|製作/], ,
    "别名", /.+名$/, ,
    "发行", "官方网站", "在线播放平台", "链接", "播放电视台", "其他电视台", "配给", /配(?!音)|連載|番組|版|播放(?!结束)/, ,
    "播放结束", "结束", ,
    ,
    "其他", /其他/, ,
    "===此处插入未被匹配的职位===", ,
    "Copyright",
  ];
  // 职位的排序列表
  const jobOrder = [];
  // 默认折叠的职位
  const foldableJobs = [];
  // 默认值的格式化文本的缓存
  let mapListTextBuffer = null;
  // 超过此行数的职位信息将被二次折叠 (现为固定值,在之后的版本用户可以自定义)
  let maxRefoldLines = 4;

  // 是否对职位信息进行了折叠 (依此判断 `更多制作人员` 开关的必要性)
  let hasFolded = false;
  // 职位信息的行距
  let jobLineHeight = null;

  const pathname = window.location.pathname;

  // 匹配相应 URL 类型
  const urlPatterns = [
    { type: 'subject', regex: /^\/subject\/\d+$/, handler: handlerSubject },
    // { type: 'character', regex: /^\/character\/\d+$/, handler: trySortStaff },
    // { type: 'person', regex: /^\/person\/\d+$/, handler: trySortStaff },
    { type: 'settings', regex: /^\/settings\/privacy$/, handler: handlerSettings },
  ];
  function main() {
    for (const pattern of urlPatterns) {
      if (pattern.regex.test(pathname)) {
        pattern.handler(pattern.type);
        break;
      }
    }
  }

  function handlerSettings() {
    const ui = buildSettingUI({ id: 'staff_sorting' });
    document.getElementById('columnA').appendChild(ui);
    loadSettingStyle();
    // 支持 url.hash = ID 进行导引
    if (location.hash.slice(1) === 'staff_sorting') {
      ui.scrollIntoView({ behavior: 'smooth' });
    }
  }

  function handlerSubject(subType) {
    // if (needPrase(subType))
    subType = SubjectType.prase(getSubjectType());
    if (!subType) return; // 不支持该类型条目
    loadMapList();
    const ul = document.querySelector('#infobox');
    if (staffMapList.length) {
      sortStaff(ul);
    } else {
      addFoldableTag(ul);
      console.log(`${SCRIPT_NAME}:staffMapList 设置空缺,网页实行原有的职位顺序与折叠`);
    }
    loadStaffStyle();
    changeExpandToToggleButton(ul);
    addRefoldToggleButton(ul);
  }

  /**
   * 巧妙地使用非常便捷的方法,获取当前条目的类型
   * 源自 https://bangumi.tv/dev/app/2723/gadget/1242
   * 替代了下方的原有方法
   */
  function getSubjectType() {
    const href = document.querySelector("#navMenuNeue .focus").getAttribute("href");
    return href.split("/")[1];
  }

  function sortStaff(ul) {
    // 职位信息字典
    const staffDict = getStaffDict(ul);
    // 清空原始的`staff`列表
    ul.innerHTML = '';

    // 未能匹配职位的待插入位置
    let liAfterIntsert = null;
    let insterTag = false;
    let insertFold = false;

    // 按照预定顺序添加到 DOM
    jobOrder.forEach(item => {
      const matchingRoles = [];
      // 1.正则匹配
      if (item instanceof RegExp) {
        matchingRoles.push(...Object.keys(staffDict).filter(key => item.test(key)));
        if (matchingRoles.length) {
          console.log(`${SCRIPT_NAME}:使用正则表达式 "${item}" 成功匹配 \{${matchingRoles}\}`);
        } else return;
      } else if (typeof item === 'string') {
        // 2.键值匹配
        if (item && item in staffDict) {
          matchingRoles.push(item);
        // 3.特殊关键字处理
        } else if (item.startsWith('==')) {
          // 激活待插入位置
          insterTag = true;
          insertFold = foldableJobs.includes(item);
        } else return
      // 4.其余情形均忽略(且对于意外类型不报错)
      } else return;

      // 添加职位,并判断是否默认折叠
      matchingRoles.forEach(role => {
        const li = staffDict[role];
        if (typeof item === 'string' && foldableJobs.includes(role)
          || item instanceof RegExp && foldableJobs.includes(item)) {
          li.classList.add('folded', 'foldable');
          if (!hasFolded) hasFolded = true;
        }
        ul.appendChild(li);
        delete staffDict[role]; // 从字典中删除已处理的职位
        // 保存待插入位置
        if (insterTag) {
          liAfterIntsert = li;
          insterTag = false;
        }
      });
    });

    // 将剩余未被匹配的职位按原顺序添加到待插入位置
    const unmatchedJobs = Object.keys(staffDict);
    if (unmatchedJobs.length === 0) {
      return;
    }
    unmatchedJobs.forEach(role => {
      const li = staffDict[role];
      if (insertFold) li.classList.add('folded', 'foldable');
      if (liAfterIntsert) ul.insertBefore(li, liAfterIntsert);
      // 未设置待插入位置,则默认插入到末尾,且默认不折叠
      else ul.appendChild(li);
    });
    console.log(
      `${SCRIPT_NAME}:未能匹配到的职位`,
      (Object.values(staffDict).map(v => `{\n    ${v.innerText.trim()}\n}`)).join(',')
    );
    if (liAfterIntsert) console.log(`${SCRIPT_NAME}:激活将未能匹配职位插入指定位置`);
  }

  /**
   * 获取一个字典来存储网页中的职位信息,
   * 并对职位信息进行二次折叠
   */
  function getStaffDict(ul) {
    const staffDict = {};
    const lis = ul.querySelectorAll(':scope > li');
    lis.forEach(li => {
      const tip = li.querySelector('span.tip');
      if (tip) {
        const role = tip.innerText.trim().slice(0, -1); // 去掉最后的冒号
        // 为了正确计算元素高度,需使其 display
        li.classList.remove('folded');
        refoldStaff(li, tip);
        staffDict[role] = li;
        // li.folded 属性已经失效无需还原
      }
    });
    return staffDict;
  }

  /**
   * 为网页原有的`folded`类别添加`foldable`便签,用于实现切换,
   * 并对职位信息进行二次折叠
   */
  function addFoldableTag(ul) {
    const lis = ul.querySelectorAll(':scope > li');
    lis.forEach(li => {
      let flag = li.classList.contains('folded');
      if (flag) {
        if (!hasFolded) hasFolded = true;
        // 为了正确计算元素高度,需先使其 display
        li.classList.remove('folded');
      }
      const tip = li.querySelector('span.tip');
      if (tip) refoldStaff(li, tip);
      if (flag) li.classList.add('folded', 'foldable');
    });
  }

  /**
   * 对超出限制行数的职位信息进行二次折叠,并添加开关。
   * 实现类似于`summary`但是动态摘要的功能。
   * 过滤`别名`等不定行高的`infobox`信息
   */
  function refoldStaff(li, tip) {
    if (li.classList.contains('sub_container')) return; // 不定行高的 infobox 信息
    const lineCnt = getLineCnt(li);
    if (lineCnt <= maxRefoldLines) return;
    // 添加二次折叠效果 (样式将在随后通过 loadStaffStyle 动态载入)
    nestElementWithChildren(li, 'div', {class: 'refoldable refolded'});
    // 尝试不修改 DOM 结构仅通过样式添加折叠效果,但未果,故改为内嵌一层新元素

    // 添加开关状态图标
    const icon = createElement('i');
    icon.innerHTML = ICON.TRIANGLE_RIGHT;
    /* 尝试使用<symbol><use>模板或直接使用JS构建实例的方法均失败...
     * 最终改为直接修改innerHTML */ 
    updateSubElements(tip, icon, 'prepend');
    tip.classList.add('switch');
  }

  /**
   * 为二次折叠按钮绑定开关事件,
   * 采用`事件委托`形式绑定事件 (事件冒泡机制)
   */
  function addRefoldToggleButton(ul) {
    ul.addEventListener('click', (event) => {
      /* 检查点击的元素是否是开关本身或其子元素
       * 使用 .closest('.switch') 替代 classList.contains('switch')
       * 使得子元素也能响应点击事件 */
      const tip = event.target.closest('.switch');
      if (!tip || !ul.contains(tip)) return;
      // 职位名称或开关状态图标被点击了
      const parent = tip.parentElement;
      if (parent.classList.contains('refolded')) {
        parent.classList.remove('refolded')
        tip.firstChild.innerHTML = ICON.TRIANGLE_DOWN;
      } else {
        parent.classList.add('refolded')
        tip.firstChild.innerHTML = ICON.TRIANGLE_RIGHT;
      }
    });
    /* 在 mousedown 阶段阻止用户拖动或双击时的默认选中行为。
     * 由于 span.switch 本质仍然是内容段落的一部分,
     * 不通过 user-select: none 这钟粗暴的方法禁止用户的一切选中行为
     * 而是采用温和的方法阻止部分情形下对该区域的选中行为 */
    ul.addEventListener('mousedown', (event) => {
      if (event.target.closest('.switch')) event.preventDefault();
    });
  }

  /**
   * 获取固定行高元素显示的行数
   * 经测试,职员信息除了`8px`的`padding`还有`0.03555px`的`border`因为不影响行数计算忽略
   */
  function getLineCnt(el, padding = 8, border = 0) {
    jobLineHeight ??= getLineHeight(el);
    const height = el.getBoundingClientRect().height - padding - border;
    return ~~(height / jobLineHeight);
  }
  
  /**
   * 将原本存在的`更多制作人员+`一次性按钮,转绑新事件,并改为永久性开关
   * 使用网页原有的`folded`元素类别,实现对立于`sortStaff`功能
   * 添加不存在的`更多制作人员+`按钮,否则一些职位信息将永不可见
    <div class="infobox_expand">
      <a href="javascript:void(0)">更多制作人员 +</a>
      <!-- href 属性需保留,不然鼠标悬浮效果会失效 -->
    </div>
   */
  function changeExpandToToggleButton(ul) {
    const buttonValue = { on: '更多制作人员 +', off: '更多制作人员 -' };
    let moreLink = document.querySelector('#infobox + .infobox_expand a'); // 无法实现 :scope +
    if (!hasFolded) {
      // 无必要,不进行事件绑定与可能的添加,并将原有的开关隐藏
      if (moreLink) {
        moreLink.style.display = 'none';
        console.log(`${SCRIPT_NAME} - 将原有的 '${buttonValue.on}' 隐藏`);
      }
      return;
    }
    if (!moreLink) {
      moreLink = createElement('a', { href: 'javascript:void(0)' }, buttonValue.on);
      const expand = createElement('div', { class: 'infobox_expand' }, [moreLink]);
      ul.parentElement.appendChild(expand);
      console.log(`${SCRIPT_NAME}:添加原不存在的 '${buttonValue.on}' 按钮`);
    }
    moreLink.addEventListener('click', function (event) {
      event.stopImmediatePropagation(); // 阻止其他事件的触发
      const foldedLis = document.querySelectorAll('.foldable');
      const isHidden = moreLink.innerText == buttonValue.on;
      foldedLis.forEach(li => {
        if (isHidden) {
          li.classList.remove('folded');
        } else {
          li.classList.add('folded');
        }
      });
      moreLink.innerText = isHidden ? buttonValue.off : buttonValue.on;
    }, { capture: true }); // 使事件处理函数在捕获阶段运行
  }

  /**
   * 创建用户设置`UI`界面
   * 仿照`#columnA`中的同类元素进行构建,使用原有的结构与样式
    <table class="settings">
      <tbody>
        <tr>
          <td>
            <h2 class="subtitle">条目职位排序 · 默认折叠的职位</h2>
          </td>
        </tr>
        <!-- 此处添加子模块 -->
      </tbody>
    </table>
   */
  function buildSettingUI(mainStyle) {
    const mainTitle = createElement('tr', null, [
      createElement('td', null, [
        createElement('h2', { class: 'subtitle' }, '条目职位排序 · 默认折叠的职位')
      ])
    ]);
    // const lineLimitBlock = buildLineLimitBlock();
    const animeBlock = buildAnimeBlock();
    const ui = createElement('div', mainStyle, [
      createElement('table',
        { class: 'settings' }, [
        createElement('tbody', null, [
          mainTitle,
          // lineLimitBlock, // 未完成开发,暂不启用
          animeBlock,
          // 可拓展其他类型条目的模块
        ])
      ])
    ]);
    return ui;
  }

  /**
   * 创建职位信息二次折叠的行高限制设置界面
   */
  function buildLineLimitBlock() {
    const subTitle = createElement('h2', { class: 'subtitle' }, '职位信息高度 限制');
    // 搭建滑动开关
    const [toggle, toggleCntr] = buildToggleSlider('refold_switch');
    // 搭建数字输入框与控制器,不使用 input.type = 'number' 而是自我搭建相关控制
    const label = createElement('span', { class: 'text'}, '行数');
    const inputNum = createElement('input', { class: 'inputtext input_num', type: 'text', maxlength: '2' });
    const incBtn = createElement('div', { name: 'inc_btn' });
    const decBtn = createElement('div', { name: 'dec_btn' });
    // 搭建外部框架
    const numInputCntr = createElement('fieldset', { class: 'num_input_cntr' },[
      label, inputNum,
      createElement('div', { class: 'num_ctrs' }, [incBtn, decBtn])
    ]);
    const block = createElement('tr', null, [
      createElement('td', { class: 'line_limit_block' }, [
        subTitle, createElement('div', {class: 'right_inline'}, [numInputCntr, toggleCntr])
      ])
    ]);

    // 初始化
    const minNum = {int: 1, str: '1'};
    toggle.checked = true;
    inputNum.value = '4';
    incBtn.innerHTML = ICON.TRIANGLE_UP;
    decBtn.innerHTML = ICON.TRIANGLE_DOWN;

    // 绑定事件
    toggle.addEventListener('click', () => {
      if (toggle.checked) {
        numInputCntr.style.display = 'flex';
      } else {
        numInputCntr.style.display = 'none';
      }
    });
    // 限制输入为正整数
    inputNum.addEventListener('input', () => {
      let value = inputNum.value.replace(/[^0-9]/g, '');
      if (value === '' || parseInt(value) === 0) value = minNum.str;
      inputNum.value = value;
    });
    // 限制键盘输入行为,禁止非数字键输入
    inputNum.addEventListener('keydown', (event) => {
      if (!/^[0-9]$/.test(event.key) && event.key !== 'Backspace'
        && event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') event.preventDefault();
    });
    // 增加加减按键功能
    incBtn.addEventListener('click', () => {
      let value = parseInt(inputNum.value) || minNum.int;
      inputNum.value = value + minNum.int;
    });
    decBtn.addEventListener('click', () => {
      let value = parseInt(inputNum.value) || minNum.int;
      if (value > minNum.int) inputNum.value = value - minNum.int;
    });

    return block;
  }

  /**
   * 创建`staffMapList`文本内容编辑界面
   * 对于`textarea`,`button`等控件仍然使用原有的结构与样式
  <tr>
    <td class="subject_staff_block">
      <h2 class="subtitle"><!-- subject type --></h2>
      <div class="staffMapList_editor">
        <div class="markItUp">
          <textarea class="quick markItUpEditor hasEditor codeHighlight" name="staff_map_list">
            <!-- staffMapListText -->
          </textarea>
        </div>
        <div>
          <input class="inputBtn" type="submit" name="submit_context" value="保存">
          <input class="inputBtn" type="submit" name="reset_context" value="恢复默认">
          <p class="tip_j" style="display: inline;"><!-- message --></p>
        </div>
        <!-- margin-right 为移动端预留的 mainpage 滑动空间 -->
      </div>
    </td>
  </tr>
  */
  function buildAnimeBlock() {
    // 搭建标题
    const subTitle = createElement('h2', { class: 'subtitle' });
    // 搭建滑动开关
    // const selector = new TriStateSlider('anime_staff_sort');
    // selector.build();
    // 搭建文本框
    const textArea = createElement('textarea', {
      class: 'quick markItUpEditor hasEditor codeHighlight', name: 'staff_map_list'
    });
    // 搭建提交按钮
    const submitBtn = createElement('input', {
      class: 'inputBtn', type: 'submit', name: 'submit_context', value: '保存'
    });
    // 搭建重置按钮
    const resetBtn = createElement('input', {
      class: 'inputBtn', type: 'submit', name: 'reset_context', value: '恢复默认'
    });
    // 搭建简易提示框
    const editorMsgBox = createElement('p', { class: 'tip_j'});
    const selectorMsgBox = createElement('p', { class: 'tip_j' });
    // 搭建外部结构
    const block = createElement('tr', null, [
      createElement('td', {class: 'subject_staff_block'}, [
        subTitle,
        // createElement('div', {class: 'right_inline'}, [selectorMsgBox, selector.root]), // 未完成开发,暂不启用
        // 可拓展折叠效果
        createElement('div', { class: 'staffMapList_editor'}, [
          createElement('div', { class: 'markItUp' }, textArea),
          createElement('div', null, [submitBtn, resetBtn, editorMsgBox])
        ])
      ])
    ]);
    function setToggleMsgBox(state) {
      switch (state) {
        case '1':
          setMessage(selectorMsgBox, '禁用设置,但仍可编辑保存');
          break;
        case '2':
          setMessage(selectorMsgBox, '仅启用排序,禁用折叠');
          break;
        case '3':
          setMessage(selectorMsgBox, '启用自定义 / 默认设置');
          break;
      }
    }

    // 初始化
    let hasInputted = false;
    let {text, isDefault} = getMapListText(false);
    subTitle.textContent = '动画条目';
    textArea.textContent = text;
    // selector.state = '3';
    // setToggleMsgBox(selector.state);
    if (isDefault) setMessage(editorMsgBox, '现为默认设置'); // 初始化时,提醒用户已为默认设置
    if (text.trim() === "") setMessage(editorMsgBox, '现为设置空缺'); // 网页实行原有的职位顺序与折叠

    // 绑定事件
    // selector.onStateChange = (newState) => {
    //   setToggleMsgBox(newState);
    // };
    textArea.addEventListener('input', () => {
      if (!hasInputted) hasInputted = true;
      if (isDefault) isDefault = false;
      // console.log("IS INPUTTING");
    });
    resetBtn.addEventListener('click', async () => {
      if (isDefault) return setMessage(editorMsgBox, '已为默认内容');
      await trySetText(textArea, editorMsgBox, getMapListText(true).text, '已恢复默认内容', false);
      // 需进行同步等待,由于 setText 可能会触发 input 事件
      isDefault = true;
      hasInputted = false;
    })
    submitBtn.addEventListener('click', () => {
      // 判断是否为重置后未对默认内容进行修改
      if (isDefault && !hasInputted) {
        resetMapList();
        setMessage(editorMsgBox, '保存成功!恢复默认设置');
        // 恢复初始状态
        hasInputted = false;
        return;
      }
      const [modifiedData, isModified, curCursorPos] = modifyMapListJSON(textArea);
      // 强制将用户输入的文本外层嵌套 `[]`,若为重复嵌套可在 loadMapList 中识别并去除
      const savedDate = `[${modifiedData}]`;
      const parsedData = parseMapListJSON(savedDate);
      if (parsedData) {
        // 保存数据
        saveMapListText(savedDate);
        // 页面显示
        if (modifiedData.trim() === "") setMessage(editorMsgBox, '保存成功!空缺设置');
        else if (isModified) trySetText(textArea, editorMsgBox, modifiedData, '保存成功!并自动纠错', true, curCursorPos);
        else setMessage(editorMsgBox, '保存成功!');
      } else setMessage(editorMsgBox, '保存失败!格式存在错误');
      // 恢复初始状态
      hasInputted = false;
    });

    return block;
  }

  /**
   * 三态滑动选择器
   */
  class TriStateSlider {
    // 可选状态
    static states = ['1', '2', '3'];
    // 所用样式的类名
    static _selectorCls = 'tri_state_selector';
    static _radioCls = 'radio_input';
    static _labelCls = 'radio_label';
    static _sliderCls = 'select_slider';
    static _indicatorCls = 'select_indicator';
    /**
     * @type {(newState: string) => void | null} 
     * 回调函数,当状态变化时被调用
     */
    onStateChange = null;

    /**
     * 构造函数
     * @param {string} idPref - 选择器的`ID`前缀
     * @param {'1'|'2'|'3'} [initState='1'] - 初始状态
     */
    constructor(idPref, initState = '1') {
      this.root = createElement('div', { class: 'tri_state_selector' });
      this.radios = {};
      this.idPref = idPref;
      this.initState = initState;
      this._stateHis = {pre: this.initState, pre2: this.initState};
      this._initStateHis();
    }

    /**
     * 设置选择器状态
     * @param {'1'|'2'|'3'} state - 状态
     */
    set state(state) {
      this.initState = state;
      this._initStateHis(state);
      this.radios[state].checked = true;
    }

    /**
     * 获取选择器当前的状态
     * @returns {'1'|'2'|'3'}  当前状态
     */
    get state() {
      for (const [state, radio] of Object.entries(this.radios)) {
        if (radio.checked) return state;
      }
      return this.initState;
    }

    /**
     * 构造`DOM`树,并绑定事件
     */
    build() {
      // 构建单选格,radio 本体将通过样式隐藏
      TriStateSlider.states.forEach((state) => {
        const radioId = `${this.idPref}_${state}`;
        const radio = createElement('input', {
          type: 'radio', name: `${this.idPref}_group`, id: radioId,
          value: state, class: TriStateSlider._radioCls
        });
        const label = createElement('label', { htmlFor: radioId, class: 'radio_label' });
        this.radios[state] = radio;
        this.root.append(radio, label);
      });
      // 构建滑动外观
      this.root.append(
        createElement('div', { class: 'select_slider' },
          createElement('div', { class: 'select_indicator' })
        ));
      // 初始化状态并绑定事件
      this.radios[this.initState].checked = true;
      // 1) 箭头函数每次事件触发时,都会创建一个新的匿名函数,影响性能
      // this.selector.addEventListener('click', (event) => this._onClick(event));
      // 2) 事件监听器的回调函数本身会改变 this,使得它从指向类的实例对象,变为指向事件触发的元素
      // this.selector.addEventListener('click', this._onClick);
      // 3) 使用绑定后的函数
      this.root.addEventListener('click', this._onClick.bind(this));
    }

    _initStateHis() {
      this._stateHis.pre = this.initState;
      this._stateHis.pre2 = this.initState === TriStateSlider.states[1]
        ? TriStateSlider.states[2] : TriStateSlider.states[1]; // [[1,3] 2]->[2 3]
    }

    /**
     * 采用事件委托的形式处理点击事件,
     * 将原本的`radio`操作体验处理为`ToggleSlider`手感
     */
    _onClick(event) {
      if (!event.target.classList.contains('radio_input')) return;
      let curState = event.target.value;
      // 现在与过去互异,正常不处理;现在与过去的过去互异,模拟 Toggle
      if (curState === this._stateHis.pre && curState !== this._stateHis.pre2) {
        this.radios[this._stateHis.pre2].checked = true;
        curState = this._stateHis.pre2;
      }
      this._stateHis.pre2 = this._stateHis.pre;
      this._stateHis.pre = curState;
      // 使用回调函数通知外部
      if (this.onStateChange) this.onStateChange(curState);
    }
  }

  /**
   * 创建一个滑动开关
   * @param {string} sliderId - 开关的`ID`
   * @returns {[HTMLElement, HTMLElement]} 返回`开关`与`开关容器`构成的数组
   */
  function buildToggleSlider(sliderId) {
    const toggle = createElement('input', { class: 'toggle_input', type: 'checkbox', id: sliderId });
    const toggleCntr = createElement('div', { class: 'toggle' },
      [toggle, createElement('label', { class: 'toggle_slider', htmlFor: sliderId })]
    );
    return [toggle, toggleCntr];
  }

  /**
   * 优先尝试使用`execCommand`方法改写文本框,使得改写前的用户历史记录不被浏览器清除
   * (虽然`execCommand`方法已被弃用...但仍然是实现该功能最便捷的途径)
   */
  async function trySetText(textArea, msgBox, text, msg, isRestore, setCursorPos, transTime = 100) {
    let {scrollVert, cursorPos} = getTextAreaPos(textArea);
    try {
      setMessage(msgBox);
      await clearAndSetTextarea(textArea, text, transTime);
      setMessage(msgBox, `${msg},可快捷键撤销`, 0);
    } catch (e) {
      textArea.value = '';
      await new Promise(resolve => setTimeout(resolve, transTime));
      textArea.value = text;
      setMessage(msgBox, msg, 0);
      console.log(`${SCRIPT_NAME}:浏览器不支持 execCommand 方法,改为直接重置文本框,将无法通过快捷键撤销重置`)
    }
    if (isRestore) {
      setCursorPos ??= cursorPos; // 可以使用外部计算获取的光标位置
      restorePos();
    }

    /**
     * 恢复滚动位置和光标位置
     */
    function restorePos() {
      const currentTextLen = textArea.value.length;
      if (setCursorPos > currentTextLen) setCursorPos = currentTextLen;
      textArea.scrollTop = Math.min(scrollVert, textArea.scrollHeight);
      // textArea.scrollLeft = Math.min(scrollHoriz, textArea.scrollWidth - textArea.clientWidth);
      textArea.setSelectionRange(setCursorPos, setCursorPos);
    }
  }

  /**
   * 获取文本框的滚动位置和光标位置
   */
  function getTextAreaPos(textArea) {
    return {
      scrollVert: textArea.scrollTop,
      scrollHoriz: textArea.scrollLeft,
      cursorPos: textArea.selectionStart
    };
  }

  async function clearAndSetTextarea(textarea, newText, timeout = 100) {
    textarea.focus();
    // 全选文本框内容并删除
    textarea.setSelectionRange(0, textarea.value.length);
    document.execCommand('delete');
    // 延迟一段时间后,插入新的内容
    await new Promise(resolve => setTimeout(resolve, timeout));
    document.execCommand('insertText', false, newText);
  }

  async function setMessage(container, message, timeout = 100) {
    container.style.display = 'none';
    if (!message) return; // 无信息输入,则隐藏
    // 隐藏一段时间后,展现新内容
    if (timeout) await new Promise(resolve => setTimeout(resolve, timeout));
    container.textContent = message;
    container.style.display = 'inline';
  }

  /**
   * 载入`StaffMapList`数据,并对其作最终解析
   */
  function loadMapList() {
    // 读取可能的非默认设置
    let jsonString = localStorage.getItem('BangumiStaffSorting_animeStaffMapList');
    if (jsonString) {
      let parsedData = parseMapListJSON(jsonString);
      if (parsedData) {
        // 修复外层重复嵌套 `[]` 的形式 (忽略存在的漏洞,形如:[[true, ["a"], "b"]] )
        if (parsedData.length === 1 && Array.isArray(parsedData[0]) && typeof parsedData[0][0] !== 'boolean') {
          parsedData = parsedData[0];
        }
        staffMapList.length = 0;
        staffMapList.push(...parsedData);
      } else console.log(`${SCRIPT_NAME}:自定义 staffMapList 解析失败,将使用脚本默认的数据`);
    }
    // 将数据拆解为 jobOrder 与 foldableJobs
    staffMapList.forEach(item => {
      if (Array.isArray(item) && item.length) {
        // 对数组进行完全展平,提高对非标多层数组的兼容性
        item = item.flat(Infinity);
        // 对于标准格式,仅当 Boolean 为一级子序列的首元素时,对该子序列的全部元素生效
        // 此时更广义的表述为,仅当 Boolean 为一级子序列的最左节点时,对该子序列的全部元素生效
        if (typeof item[0] === 'boolean') {
          if (item[0]) foldableJobs.push(...item.slice(1));
          jobOrder.push(...item.slice(1));
        } else {
          jobOrder.push(...item);
        }
      } else if (typeof item !== 'undefined') {
        jobOrder.push(item);
      }
    });
  }

  function resetMapList() {
    localStorage.removeItem('BangumiStaffSorting_animeStaffMapList');
    console.log(`${SCRIPT_NAME}:删除自定义 staffMapList 数据,恢复默认设置`)
  }

  function saveMapListText(jsonStr) {
    localStorage.setItem('BangumiStaffSorting_animeStaffMapList', jsonStr);
    console.log(jsonStr);
    console.log(`${SCRIPT_NAME}:保存自定义 staffMapList 数据`);
  }

  function getMapListText(useDefault) {
    let jsonStr = null;
    if (!useDefault) {
      jsonStr = localStorage.getItem('BangumiStaffSorting_animeStaffMapList');
    }
    const isDefault = jsonStr === null;
    if (jsonStr) {
      jsonStr = jsonStr.slice(1, -1); // 消除首尾的 `[]`
    } else if (mapListTextBuffer) {
      jsonStr = mapListTextBuffer;
    } else {
      // 将默认数据转化为格式化文本
      jsonStr = JSON.stringify(staffMapList, regexReplacer, 1).replace(
        /(null,\n )|(\n\s+)/g, (match, g1, g2) => {
          if (g1) return '\n';
          if (g2) return ' ';
          return match;
        }).slice(3, -2); // 消除首部 `[ \n` 与尾部 `\n]`
      // 使得 `[ `->`[` 同时 ` ]`->`]`
      // jsonStr = JSON.stringify(staffMapList, regexReplacer, 1).replace(
      //   /(null,)|(?<!\[)(\n\s+)(?!])|(\[\s+)|(\s+],)/g, (match, g1, g2, g3, g4) => {
      //   if (g1) return '\n';
      //   if (g2) return ' ';
      //   if (g3) return '[';
      //   if (g4) return '],';
      //   return match;
      // }).slice(3, -2);
      mapListTextBuffer = jsonStr;
    }
    return {text: jsonStr, isDefault: isDefault};
  }

  /**
   * 对用户输入可能的常见语法与格式错误,进行自动纠错,以满足`JSON`格式
   * 并计算文本修改后,光标的适宜位置
   * 已基本兼容`JavaScript`格式的文本数据,实现格式转化
   * `group2`与`group4`致使正则表达式中不允许出现`/'"`三种字符
   */
  function modifyMapListJSON(textArea) {
    const preCursorPos = getTextAreaPos(textArea).cursorPos;
    let curCursorPos = preCursorPos;
    let flags = new Array(5).fill(false);
    const rslt = textArea.value.replace(
      /(,\s*(?=]|$))|(,\s*)+(?=,)|(')|(?<!'|")(\/[^\/'"]+\/[gimsuy]*)(?!'|")/g,
      (match, g1, g2, g3, g4, offset) => {
        isTriggered(0, '删除序列末尾元素后的 `,` 逗号', g1);
        isTriggered(2, '删除连续重复的 `,` 逗号', g2);
        isTriggered(1, '将单引号替换为双引号', g3);
        isTriggered(3, '将正则表达式以双引号包裹', g4);
        if (g1 || g2) {
          let diff = preCursorPos - offset;
          if (diff > 0) curCursorPos -= Math.min(diff, match.length);
          return '';
        }
        if (g3) return '"';
        if (g4) {
          if (offset < preCursorPos && preCursorPos < offset + match.length) curCursorPos += 1;
          else if (preCursorPos >= offset + match.length) curCursorPos += 2;
          return `"${match}"`;
        }
        return match;
      });
    return [rslt, booleanOr(...flags), curCursorPos];

    function isTriggered(index, msg, ...groups) {
      if (!flags[index] && booleanOr(...groups)) {
        console.log(`${SCRIPT_NAME}:触发自动纠错 - ${msg}`);
        flags[index] = true;
      }
    }
    function booleanOr(...values) {
      return values.reduce((acc, val) => acc || val, false);
    }
  }

  /**
   * 初步解析`staffMapListJSON`字符串
   * 仅检查:
   *   1.是否满足`JSON`格式
   *   2.是否为数组类型
   *   3.字符串样式的正则表达式,是否满足规定格式
   * 更进一步的解析,将在`loadMapList`中进行
   */
  function parseMapListJSON(text) {
    let parsedData;
    try {
      parsedData = JSON.parse(text, regexReviver);
    } catch (e) {
      console.error(`${SCRIPT_NAME}:staffMapList 解析失败 - ${e}`);
      return null;
    }
    if (!Array.isArray(parsedData)) {
      console.error(`${SCRIPT_NAME}:staffMapList 类型错误 - 非数类型`);
      return null;
    }
    return parsedData;
  }

  /**
   * 解析`JSON`字符串中的正则表达式
   */
  function regexReviver(key, value) {
    if (typeof value === 'string' && value.startsWith('/')) {
      const regexParttern = /^\/(.+)\/([gimsuy]*)$/;
      const match = value.match(regexParttern);
      if (match) {
        try {
          return new RegExp(match[1], match[2]);
        } catch (e) {
          throw new Error(`正则表达式 "${value}" 非法 - ${e}`);
        }
      } else throw new Error(`正则表达式 "${value}" 不符合 ${regexParttern} 格式`);
    }
    return value;
  }

  /**
   * 将正则表达式转化为字符串,以满足`JSON`格式
   */
  function regexReplacer(key, value) {
    if (value instanceof RegExp) return value.toString();
    return value;
  }

  /**
   * 获取元素的行高
   */
  function getLineHeight(el) {
    // 获取元素的计算样式
    const compStyle = window.getComputedStyle(el); // 不会返回 em 单位的数据
    const fontSize = parseFloat(compStyle.fontSize);
    let lineHeight = compStyle.lineHeight;
    console.log(`${SCRIPT_NAME}:fontSize:${fontSize}px, lineHeight:${lineHeight}`);
    // normal,估算为 1.2 倍
    if (lineHeight === 'normal') return fontSize * 1.2;
    // 百分比,如 150%
    if (lineHeight.endsWith('%')) return fontSize * parseFloat(lineHeight) / 100;
    // 像素单位,如 18px
    if (lineHeight.endsWith('px')) return parseFloat(lineHeight);
    // 倍数,如 1.5
    return fontSize * parseFloat(lineHeight);
  }

  /** 
   * 在子元素的外层与父元素间嵌套一层元素
   */
  function nestElementWithChildren(parent, newTagName, options) {
    const newElement = createElement(newTagName, options, Array.from(parent.childNodes));
    parent.innerHTML = '';
    parent.appendChild(newElement);
  }

  /**
   * 创建元素实例
   * @param {string} tagName - 类名
   * @param {object} options - 属性
   * @param {Array.<HTMLElement|string>|undefined} subElements - 子元素
   * @param {object.<string, Function>} eventHandlers - 绑定的事件
   */
  function createElement(tagName, options, subElements, eventHandlers) {
    const element = document.createElement(tagName);
    if (options) {
      for (let opt in options) {
        if (opt === 'class') element.className = options[opt];
        else if (['maxlength'].includes(opt)) element.setAttribute(opt, options[opt]);
        else if (opt === 'dataset' || opt === 'style') {
          for (let key in options[opt]) {
            element[opt][key] = options[opt][key];
          }
        } else element[opt] = options[opt];
      }
    }
    if (subElements) updateSubElements(element, subElements);
    if (eventHandlers) {
      for (let e in eventHandlers) {
        element.addEventListener(e, eventHandlers[e]);
      }
    }
    return element;
  }

  /**
   * 更新子元素的内容
   * @param {HTMLElement} parent - 父元素
   * @param {Array.<HTMLElement|string>|HTMLElement|string|undefined} subElements - 要插入的子元素
   * @param {'append'|'prepend'|'replace'} [actionType='append'] - 操作类型,可以是以下之一:
   *   `prepend` - 将元素插入到父元素的首位
   *   `append`  - 将元素插入到父元素的末尾
   *   `replace` - 清空父元素内容并插入元素
   */
  function updateSubElements(parent, subElements, actionType = 'append') {
    if (actionType === 'replace') parent.innerHTML = '';
    if (!subElements) return parent;
    if (!Array.isArray(subElements)) subElements = [subElements];
    for (let e of subElements) {
      const child = typeof e === 'string' ? document.createTextNode(e) : e;
      switch (actionType) {
        case "append":
        case "replace":
          parent.appendChild(child);
          break;
        case "prepend":
          parent.insertBefore(child, parent.firstChild);
          break;
        default:
          throw new Error(`'${actionType}' is invalid action type of updateElements!`);
      }
    }
    return parent;
  }

  /**
   * 动态载入职位排序的样式,
   * 依据的职位信息行高`jobLineHeight`与设置的限制行数`maxRefoldLines`
   */
  function loadStaffStyle() {
    const style = createElement('style', {class: 'staff_sorting'});
    style.innerHTML = `
      /* 职位信息二次折叠 */
      #infobox div.refoldable {
        height: auto;
        overflow: visible;
      }
      #infobox div.refolded {
        height: ${maxRefoldLines * jobLineHeight}px;
        overflow: clip;
        mask-image: 
          linear-gradient(160deg, black 10%, transparent 90%), 
          linear-gradient(black, black);
        mask-size: 100% ${jobLineHeight}px, 100% calc(100% - ${jobLineHeight}px);
        mask-position: 0 100%, 0 0;
        mask-repeat: no-repeat;
        mask-composite: add;
      }
      #infobox .tip.switch {
        cursor: pointer;
      }
      #infobox .tip.switch:hover {
        color: #000;
      }
      html[data-theme='dark'] #infobox .tip.switch:hover {
        color: #FFF;
      }
      #infobox .tip.switch:hover i {
        color: #2ea6ff;
      }
    `;
    document.head.appendChild(style);
  }

  // 载入设置界面的样式
  function loadSettingStyle() {
    const style = createElement('style', {class: 'staff_sorting'});
    style.innerHTML = `
      /* 设置界面的样式 */
      :root {
        --tri-state-selector-size: 22px;
        --tri-state-selector-step: 19px;
      }
      #staff_sorting > .settings {
        margin-left: 5px;
      }
      #staff_sorting .right_inline {
        height: 22px;
        float: right;
        display: flex;
        align-items: center;
      }
      #staff_sorting td[class$="block"] > h2 {
        font-size: 16px;
        display: inline-block;
      }
      #staff_sorting .staffMapList_editor {
        padding-right: 10%;
        margin-bottom: 5px;
      }

      /* 各类型条目的职位设置模块 */
      .subject_staff_block textarea {
        font-size: 15px;
        line-height: 21px;
      }
      .subject_staff_block .inputBtn {
        margin-right: 5px;
      }
      .subject_staff_block .tip_j {
        display: none;
        margin: 0 5px;
      }
      .subject_staff_block .right_inline .tip_j {
        display: none;
        margin-right: 15px;
      }

      /* 滑动开关 */
      .toggle {
        position: relative;
        width: 44px;
        height: 22px;
        display: block;
        float: right;
      }
      .toggle_input {
        display: none;
      }
      .toggle_slider {
        position: absolute;
        cursor: pointer;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: #eaeaea;
        border-radius: 22px;
        box-shadow: inset 0 2px 3px rgba(0, 0, 0, 0.2);
        transition: background-color 0.2s ease-in;
      }
      html[data-theme="dark"] .toggle_slider {
        background-color: #9a9a9a;
      }
      .toggle_slider::before {
        content: "";
        position: absolute;
        height: 16px;
        width: 16px;
        left: 3px;
        bottom: 3px;
        background-color: white;
        border-radius: 50%;
        box-shadow: 0 2px 3px rgba(0, 0, 0, 0.3);
        transition: transform 0.2s ease-in;
      }
      .toggle_input:checked + .toggle_slider {
        background-color: #72b6e3;
      }
      html[data-theme="dark"] .toggle_input:checked + .toggle_slider {
        background-color: #3072dc;
      }
      .toggle_input:checked + .toggle_slider::before {
        transform: translateX(22px);
      }

      /* 数字输入框与控制器 */
      .num_input_cntr {
        display: flex;
        float: left;
        align-items: center;
        gap: 5px;
        margin-right: 30px;
      }
      .num_input_cntr .text {
        font-size: 14px;
        margin-right: 2px;
      }
      .inputtext.input_num {
        width: 30px;
        height: 12px;
        text-align: center;
        font-size: 15px;
      }
      .num_ctrs {
        display: flex;
        flex-direction: column;
        background-color: white;
        border: 1px solid #d9d9d9;
        border-radius: 4px;
        box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
        gap: 0;
      }
      html[data-theme="dark"] .num_ctrs {
        background-color: black;
        border: 1px solid #757575;
      }
      .num_ctrs div {
        display: flex;
        text-align: center;
        width: 12px;
        height: 7px;
        padding: 2px;
        cursor: pointer;
      }
      .num_ctrs div:first-child {
        border-radius: 3px 3px 0 0;
      }
      .num_ctrs div:last-child {
        border-radius: 0 0 3px 3px;
      }
      .num_ctrs div svg {
        width: 100%;
        height: 100%;
      }
      .num_ctrs div:active {
        background-color: #2ea6ff;
      }

      /* 三态滑动选择器 */
      .tri_state_selector {
        position: relative;
        width: calc(
          var(--tri-state-selector-size) + var(--tri-state-selector-step) * 2
        );
        height: var(--tri-state-selector-size);
        display: inline-block;
      }
      .radio_input {
        position: absolute;
        opacity: 0;
        z-index: 2;
      }
      .select_slider {
        position: relative;
        width: 100%;
        height: 100%;
        background-color: #eaeaea;
        border-radius: var(--tri-state-selector-size);
        box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.2);
        z-index: 1;
        overflow: hidden;
        transition: background-color 0.2s ease-in;
      }
      html[data-theme="dark"] .select_slider {
        background-color: #9a9a9a;
      }
      .select_indicator {
        position: absolute;
        width: calc(var(--tri-state-selector-size) - 4px);
        height: calc(var(--tri-state-selector-size) - 4px);
        top: 2px;
        left: 2px;
        background-color: white;
        border-radius: 50%;
        box-shadow: 0 2px 3px rgba(0, 0, 0, 0.3);
        z-index: 1;
        transition: transform 0.2s ease-in;
      }
      .radio_label {
        position: absolute;
        width: var(--tri-state-selector-step);
        height: 100%;
        top: 0;
        cursor: pointer;
        z-index: 3;
      }
      label.radio_label:nth-of-type(1) {
        left: 0;
      }
      label.radio_label:nth-of-type(2) {
        left: var(--tri-state-selector-step);
      }
      label.radio_label:nth-of-type(3) {
        width: var(--tri-state-selector-size);
        left: calc(var(--tri-state-selector-step) * 2);
      }
      input.radio_input:nth-of-type(2):checked ~ .select_slider {
        background-color: #f47a88;
      }
      input.radio_input:nth-of-type(3):checked ~ .select_slider {
        background-color: #72b6e3;
      }
      html[data-theme="dark"] input.radio_input:nth-of-type(2):checked ~ .select_slider {
        background-color: #ff668a;
      }
      html[data-theme="dark"] input.radio_input:nth-of-type(3):checked ~ .select_slider {
        background-color: #3072dc;
      }
      input.radio_input:nth-of-type(1):checked ~ .select_slider .select_indicator {
        transform: translateX(0);
      }
      input.radio_input:nth-of-type(2):checked ~ .select_slider .select_indicator {
        transform: translateX(var(--tri-state-selector-step));
      }
      input.radio_input:nth-of-type(3):checked ~ .select_slider .select_indicator {
        transform: translateX(calc(var(--tri-state-selector-step) * 2));
      }
      .select_slider::after {
        content: "";
        position: absolute;
        width: calc(var(--tri-state-selector-size) + var(--tri-state-selector-step));
        height: var(--tri-state-selector-size);
        left: var(--tri-state-selector-step);
        border-radius: calc(var(--tri-state-selector-size) / 2);
        box-shadow: 0 0 3px rgba(0, 0, 0, 0.1), inset 0 0 6px rgba(0, 0, 0, 0.3);
        transition: transform 0.2s ease-in-out;
      }
      input.radio_input:nth-of-type(1):checked ~ .select_slider::after {
        transform: translateX(calc(0px - var(--tri-state-selector-step)));
      }
    `;
    document.head.appendChild(style);
  }

  main();

})();