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).

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
  })();
})();