Microsoft Forms Jump

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Microsoft Forms Jump
// @namespace    https://userscript-tools
// @version      5.2
// @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
// @author       Jerry
// @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();
  })();
})();