Microsoft Forms: Fixed Top Nav Bar (<< < > >>) + Auto Jump to Last

Adds a fixed top navigation bar with << < > >> on Microsoft Forms Survey Results pages; keeps original left-side <; auto-jumps to last on load (no URL tricks).

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Microsoft Forms: Fixed Top Nav Bar (<< < > >>) + Auto Jump to Last
// @namespace    https://userscript-tools
// @version      5.0
// @description  Adds a fixed top navigation bar with << < > >> on Microsoft Forms Survey Results pages; keeps original left-side <; auto-jumps to last on load (no URL tricks).
// @match        https://forms.office.com/pages/designpagev2.aspx*
// @match        https://*.office.com/Forms/DesignPageV2.aspx*
// @run-at       document-idle
// @grant        none
// @homepage     https://greasyfork.org/en/scripts/556153
// ==/UserScript==

(function () {
  'use strict';

  // -------- CONFIG -------
  const DEBUG = false;
  const CLICK_INTERVAL_MS = 110;       // pacing for repeated clicks
  const MAX_STEPS = 1500;              // safety cap for << and >>
  const AUTO_INIT_DELAY = 600;         // wait after mount to build UI
  const ENABLE_AUTO_JUMP_TO_LAST = true;

  // Top bar layout: 'right' | 'center' | 'left'
  const TOPBAR_ALIGN = 'center';

  // Visuals
  const TOPBAR_STYLE = {
    background: 'rgba(255,255,255,0.95)',
    border: '1px solid rgba(0,0,0,0.15)',
    shadow: '0 2px 10px rgba(0,0,0,0.08)',
    height: 36,
    paddingX: 10,
    gap: 6,
    zIndex: 99999
  };
  // -----------------------

  const log = (...a) => { if (DEBUG) console.log('[MSForms TopBar]', ...a); };
  const sleep = (ms) => new Promise(r => setTimeout(r, ms));

  function isSurveyResultsView() {
    try {
      const url = new URL(location.href);
      return (
        (url.searchParams.get('topview') || '').toLowerCase() === 'surveyresults' ||
        url.searchParams.get('analysis') === 'true'
      );
    } catch {
      return false;
    }
  }

  function findNextButton() {
    const selectors = [
      'button[title="Next"]',
      'button[aria-label="Next"]',
      'button[data-automationid="Results_Next"]',
      'div[role="button"][aria-label="Next"]',
      'button[aria-label*="Next"]',
      'div[role="button"][aria-label*="Next"]',
      'button[aria-label*="Chevron right"]',
      'button[aria-label*="Right"]',
      'button svg[data-icon-name="ChevronRight"]',
    ];
    for (const sel of selectors) {
      const el = document.querySelector(sel);
      if (el) return el.closest('button,[role="button"]') || el;
    }
    // Heuristic: rightmost control in a toolbar/pager area
    const toolbars = Array.from(document.querySelectorAll('div[role="toolbar"],div[aria-label*="results"],div[aria-label*="pager"],div[class*="pager"]'));
    for (const tb of toolbars) {
      const btns = tb.querySelectorAll('button,div[role="button"]');
      if (btns.length) return btns[btns.length - 1];
    }
    // Fallback by text/icon
    const candidates = Array.from(document.querySelectorAll('button,div[role="button"]'));
    const byText = candidates.find(el => /next|›|»|→/i.test(el.textContent || ''));
    return byText || null;
  }

  function findPrevButton() {
    const selectors = [
      'button[title="Previous"]',
      'button[aria-label="Previous"]',
      'button[data-automationid="Results_Previous"]',
      'div[role="button"][aria-label="Previous"]',
      'button[aria-label*="Previous"]',
      'div[role="button"][aria-label*="Previous"]',
      'button[aria-label*="Chevron left"]',
      'button[aria-label*="Left"]',
      'button svg[data-icon-name="ChevronLeft"]',
    ];
    for (const sel of selectors) {
      const el = document.querySelector(sel);
      if (el) return el.closest('button,[role="button"]') || el;
    }
    // Fallback by text/icon
    const candidates = Array.from(document.querySelectorAll('button,div[role="button"]'));
    const byText = candidates.find(el => /previous|prev|‹|«|←/i.test(el.textContent || ''));
    return byText || null;
  }

  function isDisabled(el) {
    return !el ||
           el.hasAttribute('disabled') ||
           el.getAttribute('aria-disabled') === 'true' ||
           el.classList.contains('is-disabled') ||
           el.classList.contains('disabled');
  }

  async function goToLast(nextBtn) {
    if (!nextBtn) {
      log('goToLast: next not found yet');
      return;
    }
    let steps = 0;
    while (!isDisabled(nextBtn) && steps < MAX_STEPS) {
      nextBtn.click();
      steps++;
      await sleep(CLICK_INTERVAL_MS);
      nextBtn = findNextButton();
    }
    log('goToLast: done in steps =', steps);
  }

  async function goToFirst(prevBtn) {
    if (!prevBtn) {
      log('goToFirst: prev not found yet');
      return;
    }
    let steps = 0;
    while (!isDisabled(prevBtn) && steps < MAX_STEPS) {
      prevBtn.click();
      steps++;
      await sleep(CLICK_INTERVAL_MS);
      prevBtn = findPrevButton();
    }
    log('goToFirst: done in steps =', steps);
  }

  // Build a fixed top bar with nav controls
  function buildTopBar() {
    if (!isSurveyResultsView()) return;

    const nextBtn = findNextButton();
    // We can still render the bar even if next isn’t found yet; buttons will sync state
    const barId = 'msforms-fixed-topbar';
    if (document.getElementById(barId)) return; // avoid duplicates

    const bar = document.createElement('div');
    bar.id = barId;
    bar.style.position = 'fixed';
    bar.style.top = '8px';
    // Align
    if (TOPBAR_ALIGN === 'left') {
      bar.style.left = '12px';
      bar.style.right = 'auto';
      bar.style.transform = 'none';
    } else if (TOPBAR_ALIGN === 'center') {
      bar.style.left = '50%';
      bar.style.transform = 'translateX(-50%)';
    } else {
      // right
      bar.style.right = '12px';
      bar.style.left = 'auto';
      bar.style.transform = 'none';
    }
    bar.style.display = 'inline-flex';
    bar.style.alignItems = 'center';
    bar.style.gap = `${TOPBAR_STYLE.gap}px`;
    bar.style.height = `${TOPBAR_STYLE.height}px`;
    bar.style.padding = `0 ${TOPBAR_STYLE.paddingX}px`;
    bar.style.background = TOPBAR_STYLE.background;
    bar.style.border = TOPBAR_STYLE.border;
    bar.style.boxShadow = TOPBAR_STYLE.shadow;
    bar.style.borderRadius = '8px';
    bar.style.zIndex = String(TOPBAR_STYLE.zIndex);
    bar.style.backdropFilter = 'saturate(180%) blur(8px)';

    function makeBtn(label, title) {
      const b = document.createElement('button');
      b.type = 'button';
      b.textContent = label;
      b.title = title;
      b.style.padding = '4px 10px';
      b.style.lineHeight = '1';
      b.style.height = '28px';
      b.style.minWidth = '36px';
      b.style.borderRadius = '6px';
      b.style.border = '1px solid rgba(0,0,0,0.2)';
      b.style.background = 'white';
      b.style.cursor = 'pointer';
      b.style.fontSize = '13px';
      b.style.fontWeight = '600';
      b.style.userSelect = 'none';
      b.style.transition = 'background 120ms ease';
      b.onmouseenter = () => b.style.background = '#f3f2f1';
      b.onmouseleave = () => b.style.background = 'white';
      return b;
    }

    const btnFirst = makeBtn('<<', 'First response');
    const btnPrev  = makeBtn('<',  'Previous response');
    const btnNext  = makeBtn('>',  'Next response');
    const btnLast  = makeBtn('>>', 'Last response');

    // Wire handlers
    btnPrev.addEventListener('click', () => {
      const p = findPrevButton();
      if (p && !isDisabled(p)) p.click();
    });
    btnNext.addEventListener('click', () => {
      const n = findNextButton();
      if (n && !isDisabled(n)) n.click();
    });
    btnFirst.addEventListener('click', async () => {
      await goToFirst(findPrevButton());
    });
    btnLast.addEventListener('click', async () => {
      await goToLast(findNextButton());
    });

    bar.appendChild(btnFirst);
    bar.appendChild(btnPrev);
    bar.appendChild(btnNext);
    bar.appendChild(btnLast);

    document.body.appendChild(bar);

    // Keep enabled/disabled state in sync with native buttons
    const syncState = () => {
      const p = findPrevButton();
      const n = findNextButton();
      const prevDisabled = !p || isDisabled(p);
      const nextDisabled = !n || isDisabled(n);

      btnFirst.disabled = prevDisabled;
      btnPrev.disabled  = prevDisabled;
      btnLast.disabled  = nextDisabled;
      btnNext.disabled  = nextDisabled;

      [btnFirst, btnPrev, btnNext, btnLast].forEach(b => {
        b.style.opacity = b.disabled ? '0.5' : '1';
        b.style.cursor  = b.disabled ? 'default' : 'pointer';
      });
    };

    syncState();
    const int = setInterval(syncState, 300);

    // Clean up interval if bar is removed
    const observer = new MutationObserver(() => {
      if (!document.body.contains(bar)) {
        clearInterval(int);
        observer.disconnect();
      }
    });
    observer.observe(document.body, { childList: true, subtree: true });

    log('Fixed top bar injected.');
  }

  // Auto-jump controller (per Results view mount)
  let autoJumpedForThisView = false;

  async function maybeAutoJumpToLast() {
    if (!ENABLE_AUTO_JUMP_TO_LAST) return;
    if (!isSurveyResultsView()) { autoJumpedForThisView = false; return; }
    if (autoJumpedForThisView) return;

    await sleep(AUTO_INIT_DELAY);
    const nextBtn = findNextButton();
    if (nextBtn && !isDisabled(nextBtn)) {
      await goToLast(nextBtn);
    }
    autoJumpedForThisView = true;
  }

  function installObserver() {
    const tryRun = () => {
      if (!isSurveyResultsView()) { autoJumpedForThisView = false; return; }
      // Build bar and then maybe auto-jump
      setTimeout(() => {
        buildTopBar();
        maybeAutoJumpToLast();
      }, AUTO_INIT_DELAY);
    };

    // Observe SPA mounts/changes
    const obs = new MutationObserver(() => tryRun());
    obs.observe(document.documentElement, { childList: true, subtree: true });

    // Initial load
    window.addEventListener('load', () => setTimeout(tryRun, AUTO_INIT_DELAY));

    // SPA routing hooks
    const _push = history.pushState;
    history.pushState = function () {
      _push.apply(this, arguments);
      setTimeout(tryRun, 200);
    };
    window.addEventListener('popstate', () => setTimeout(tryRun, 200));
  }

  (function init() {
    installObserver();
  })();
})();