Greasy Fork is available in English.

Tampermonkey

改善篡改猴管理界面的屏幕阅读器、键盘、焦点、触控与脚本删除体验。

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

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

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

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

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

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

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

Advertisement:

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

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

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

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

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

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

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

Advertisement:

// ==UserScript==
// @name         Tampermonkey
// @namespace    https://www.tampermonkey.net/
// @version      1.0.2
// @description  改善篡改猴管理界面的屏幕阅读器、键盘、焦点、触控与脚本删除体验。
// @match        https://tampermonkey.net/*
// @match        https://*.tampermonkey.net/*
// @match        https://tmnk.net/*
// @match        https://*.tmnk.net/*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function universalModule(root, factory) {
  const api = factory();
  if (typeof module === 'object' && module.exports) {
    module.exports = api;
    return;
  }
  root.TampermonkeyDashboardA11y = api;
  if (api.isSupportedDashboard(root.document, root.location)) {
    api.enhanceDashboard(root.document, root);
  }
})(typeof globalThis !== 'undefined' ? globalThis : this, function createModule() {
  'use strict';

  const ROW_SELECTOR = '.scripttr, .script-row, .script_row, [data-script-name]';
  const NAME_SELECTOR = '.script_name, .script-name, .script_name_text, [data-script-name]';
  const CONTROL_SELECTOR = 'button, a, input, select, textarea, [role="button"], [onclick], .clickable';
  const DELETE_SELECTOR = [
    '.script-delete', '.script_delete', '.delete', '.trash', '[data-action="delete"]',
    '[title*="Delete" i]', '[title*="Remove" i]', '[title*="删除"]', '[aria-label*="删除"]',
    'img[src*="delete" i]', 'img[src*="trash" i]',
  ].join(',');

  const LABEL_RULES = [
    { pattern: /delete|remove|trash|删除|移除/i, label: '删除脚本' },
    { pattern: /edit|pencil|编辑|修改/i, label: '编辑脚本' },
    { pattern: /update|refresh|更新|刷新/i, label: '检查更新' },
    { pattern: /enable|enabled|greenled|启用/i, label: '启用脚本' },
    { pattern: /disable|disabled|redled|停用|禁用/i, label: '停用脚本' },
    { pattern: /home|homepage|主页/i, label: '打开脚本主页' },
    { pattern: /close|关闭/i, label: '关闭' },
    { pattern: /save|保存/i, label: '保存' },
    { pattern: /cancel|取消/i, label: '取消' },
    { pattern: /confirm|ok|确定|确认/i, label: '确认' },
  ];

  function isSupportedDashboard(document, location) {
    if (!document || !location) return false;
    const host = String(location.hostname || '').toLowerCase();
    const trustedHost = host === 'tampermonkey.net' || host.endsWith('.tampermonkey.net') ||
      host === 'tmnk.net' || host.endsWith('.tmnk.net');
    if (!trustedHost) return false;
    const path = String(location.pathname || '').toLowerCase();
    return /options|dashboard|settings|extension/.test(path) ||
      Boolean(document.querySelector('#dashboard, #options, .main_container, .scripttr, .script-row'));
  }

  function cleanText(value) {
    return String(value || '').replace(/\s+/g, ' ').trim();
  }

  function getControlSignature(element) {
    const image = element.matches?.('img') ? element : element.querySelector?.('img');
    return [
      element.getAttribute?.('aria-label'), element.getAttribute?.('title'),
      element.getAttribute?.('alt'), element.getAttribute?.('data-action'),
      element.id, element.className, element.textContent,
      image?.getAttribute('src'), image?.getAttribute('alt'), image?.getAttribute('title'),
    ].map(cleanText).join(' ');
  }

  function inferControlLabel(element) {
    const existing = cleanText(element.getAttribute('aria-label'));
    if (existing) return existing;
    const visible = cleanText(element.textContent);
    if (visible) return visible;
    const signature = getControlSignature(element);
    return LABEL_RULES.find(rule => rule.pattern.test(signature))?.label || '';
  }

  function enhanceControl(element) {
    if (!(element instanceof element.ownerDocument.defaultView.Element)) return;
    const label = inferControlLabel(element);
    if (label) element.setAttribute('aria-label', label);
    if (!/^(BUTTON|A|INPUT|SELECT|TEXTAREA)$/.test(element.tagName)) {
      if (!element.hasAttribute('role')) element.setAttribute('role', 'button');
      if (!element.hasAttribute('tabindex')) element.setAttribute('tabindex', '0');
      if (!element.dataset.tmA11yKeyboard) {
        element.addEventListener('keydown', event => {
          if (event.key === 'Enter' || event.key === ' ') {
            event.preventDefault();
            element.click();
          }
        });
        element.dataset.tmA11yKeyboard = 'true';
      }
    }
    element.classList.add('tm-a11y-target');
  }

  function scriptName(row) {
    const nameNode = row.querySelector(NAME_SELECTOR);
    return cleanText(nameNode?.getAttribute('data-script-name') || nameNode?.textContent || row.getAttribute('data-script-name')) || '未命名脚本';
  }

  function findOriginalDelete(row) {
    const candidates = [...row.querySelectorAll(DELETE_SELECTOR)];
    return candidates.find(candidate => !candidate.classList.contains('tm-a11y-delete')) || null;
  }

  function enhanceRow(row, announce) {
    const name = scriptName(row);
    row.setAttribute('role', row.tagName === 'TR' ? 'row' : 'group');
    row.setAttribute('aria-label', `脚本:${name}`);
    row.classList.add('tm-a11y-script-row');

    const originalDelete = findOriginalDelete(row);
    if (originalDelete && !row.querySelector('.tm-a11y-delete')) {
      const button = row.ownerDocument.createElement('button');
      button.type = 'button';
      button.className = 'tm-a11y-delete tm-a11y-target';
      button.textContent = '删除';
      button.setAttribute('aria-label', `删除脚本“${name}”`);
      button.addEventListener('click', event => {
        event.preventDefault();
        event.stopPropagation();
        announce(`准备删除脚本“${name}”`);
        originalDelete.click();
      });
      const cell = row.tagName === 'TR' ? row.lastElementChild : row;
      cell?.appendChild(button);
    }
  }

  function enhanceDialog(dialog) {
    if (dialog.dataset.tmA11yDialog === 'true') return;
    dialog.dataset.tmA11yDialog = 'true';
    dialog.setAttribute('role', 'dialog');
    dialog.setAttribute('aria-modal', 'true');
    const heading = dialog.querySelector('h1, h2, h3, .title, .dialog-title, .modal-title');
    const label = cleanText(heading?.textContent) || '对话框';
    dialog.setAttribute('aria-label', label);
    dialog.querySelectorAll(CONTROL_SELECTOR).forEach(enhanceControl);
    const primary = dialog.querySelector('.primary, .confirm, [data-action="confirm"], button[type="submit"]');
    const fallback = dialog.querySelector('button, [role="button"], a');
    (primary || fallback)?.focus();
  }

  function installStyles(document) {
    if (document.querySelector('#tm-a11y-styles')) return;
    const style = document.createElement('style');
    style.id = 'tm-a11y-styles';
    style.textContent = `
      .tm-a11y-skip { position: fixed; z-index: 2147483647; top: 8px; left: 8px; padding: 12px 16px;
        background: #fff; color: #000; border: 3px solid #005fcc; border-radius: 6px; transform: translateY(-160%); }
      .tm-a11y-skip:focus { transform: translateY(0); }
      #tm-a11y-toolbar { position: sticky; z-index: 99999; top: 0; display: flex; flex-wrap: wrap; gap: 10px;
        align-items: center; padding: 12px; background: Canvas; color: CanvasText; border: 2px solid currentColor; }
      #tm-a11y-toolbar label { font-weight: 700; }
      #tm-a11y-search { min-height: 44px; min-width: min(22rem, 70vw); padding: 8px 12px; font-size: 16px; }
      .tm-a11y-target { min-width: 44px !important; min-height: 44px !important; box-sizing: border-box !important; }
      .tm-a11y-delete { margin: 4px !important; padding: 8px 12px !important; color: #fff !important;
        background: #9b1c1c !important; border: 2px solid #5f0000 !important; border-radius: 5px !important; }
      .tm-a11y-target:focus-visible, #tm-a11y-search:focus-visible, .tm-a11y-script-row:focus-within {
        outline: 4px solid #ffbf47 !important; outline-offset: 3px !important; }
      .tm-a11y-script-row[hidden] { display: none !important; }
      @media (max-width: 700px) {
        .tm-a11y-script-row { display: block !important; padding: 10px !important; border-bottom: 2px solid currentColor !important; }
        .tm-a11y-script-row > td { display: inline-flex !important; align-items: center; min-height: 44px; padding: 4px !important; }
        #tm-a11y-toolbar { position: relative; }
      }
      @media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; animation: none !important; } }
      @media (forced-colors: active) { .tm-a11y-delete { forced-color-adjust: auto; } }
    `;
    document.head.appendChild(style);
  }

  function installToolbar(document, window, rows, announce) {
    if (document.querySelector('#tm-a11y-toolbar')) return;
    const main = document.querySelector('main, #dashboard, #options, .main_container') || document.body;
    if (!main.id) main.id = 'tm-a11y-main';
    main.setAttribute('role', 'main');

    const skip = document.createElement('a');
    skip.className = 'tm-a11y-skip';
    skip.href = `#${main.id}`;
    skip.textContent = '跳到主要内容';
    document.body.prepend(skip);

    const toolbar = document.createElement('section');
    toolbar.id = 'tm-a11y-toolbar';
    toolbar.setAttribute('role', 'search');
    toolbar.setAttribute('aria-label', '脚本管理辅助工具');
    toolbar.innerHTML = `
      <label for="tm-a11y-search">搜索脚本</label>
      <input id="tm-a11y-search" type="search" autocomplete="off" placeholder="输入脚本名称,按 / 可快速定位">
      <button id="tm-a11y-clear" type="button" class="tm-a11y-target">清除搜索</button>
      <span id="tm-a11y-status" role="status" aria-live="polite"></span>
    `;
    main.prepend(toolbar);

    const search = toolbar.querySelector('#tm-a11y-search');
    const status = toolbar.querySelector('#tm-a11y-status');
    const filter = () => {
      const query = cleanText(search.value).toLocaleLowerCase('zh-CN');
      const currentRows = rows();
      let visible = 0;
      currentRows.forEach(row => {
        const matches = !query || scriptName(row).toLocaleLowerCase('zh-CN').includes(query);
        row.hidden = !matches;
        if (matches) visible++;
      });
      status.textContent = `显示 ${visible} 个脚本,共 ${currentRows.length} 个`;
    };
    search.addEventListener('input', filter);
    toolbar.querySelector('#tm-a11y-clear').addEventListener('click', () => {
      search.value = '';
      filter();
      search.focus();
    });
    document.addEventListener('keydown', event => {
      if (event.key === '/' && !/INPUT|TEXTAREA|SELECT/.test(document.activeElement?.tagName || '')) {
        event.preventDefault();
        search.focus();
      }
      if (event.altKey && event.key.toLowerCase() === 'm') {
        event.preventDefault();
        main.setAttribute('tabindex', '-1');
        main.focus();
      }
    });
    filter();
    announce('无障碍增强已启用');
  }

  function enhanceDashboard(document, window) {
    installStyles(document);
    let live = document.querySelector('#tm-a11y-live');
    if (!live) {
      live = document.createElement('div');
      live.id = 'tm-a11y-live';
      live.setAttribute('role', 'status');
      live.setAttribute('aria-live', 'assertive');
      live.style.cssText = 'position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap';
      document.body.appendChild(live);
    }
    const announce = message => { live.textContent = ''; window.setTimeout(() => { live.textContent = message; }, 0); };
    const rows = () => [...document.querySelectorAll(ROW_SELECTOR)];

    const scan = root => {
      if (root.matches?.(CONTROL_SELECTOR)) enhanceControl(root);
      root.querySelectorAll?.(CONTROL_SELECTOR).forEach(enhanceControl);
      if (root.matches?.(ROW_SELECTOR)) enhanceRow(root, announce);
      root.querySelectorAll?.(ROW_SELECTOR).forEach(row => enhanceRow(row, announce));
      if (root.matches?.('[role="dialog"], .dialog, .modal, .modal-dialog')) enhanceDialog(root);
      root.querySelectorAll?.('[role="dialog"], .dialog, .modal, .modal-dialog').forEach(enhanceDialog);
    };

    scan(document.body);
    installToolbar(document, window, rows, announce);
    const observer = new window.MutationObserver(records => {
      records.forEach(record => record.addedNodes.forEach(node => {
        if (node.nodeType === 1) scan(node);
      }));
    });
    observer.observe(document.body, { childList: true, subtree: true });
    return { disconnect: () => observer.disconnect(), rescan: () => scan(document.body) };
  }

  return { enhanceDashboard, inferControlLabel, isSupportedDashboard };
});