Quizlet Reshuffle

Reshuffles in learn and flashcards with a button or keybind.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Quizlet Reshuffle
// @namespace    quizlet-reshuffle
// @version      1.0
// @description  Reshuffles in learn and flashcards with a button or keybind.
// @author       Richard Rogalski
// @match        https://quizlet.com/*/learn*
// @match        https://quizlet.com/*/flashcards*
// @grant        none
// @run-at       document-idle
// @license      0BSD
// ==/UserScript==

(function () {
  'use strict';

  // Config: how long to wait before each task (change if having problems)
  const WAIT_MS = 0;

  // Config: change key combo here (always functions)
  const KEYBIND = { ctrl: true, alt: false, shift: false, meta: false, key: 'R' }; // meta == ⌘ on macOS

  // Config: change key combo here (functions when not in a text box)
  const KEYBIND2 = 'R';

  function isTyping() {
    const el = document.activeElement;
    if (!el) return false;
    const tag = el.tagName;
    if (tag === 'INPUT' || tag === 'TEXTAREA') return true;
    if (el.isContentEditable) return true;
    return false;
  }

  const sleep = (ms) => new Promise(r => setTimeout(r, ms));

  function clickAway() {
    document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
    document.body.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
    document.body.dispatchEvent(new MouseEvent('click', { bubbles: true }));
  }

  function getOptionsButton() {
    return document.querySelector('button[aria-label="Options"]');
  }

  function getShuffleButton() {
    return document.querySelector('button[aria-label="Shuffle"]');
  }

  function isShuffleOn(btn) {
    return btn?.getAttribute('aria-pressed') === 'true';
  }

  async function forceReshuffle() {
    let shuffleBtn = getShuffleButton();
    const optionsBtn = getOptionsButton();
    let cards = true;
    if (!shuffleBtn) cards = false;
    //if (!optionsBtn) return;

    // Open Options
    if (!cards) optionsBtn.click();

    // Wait for Shuffle to exist
    for (let i = 0; i < 10; i++) {
      await sleep(WAIT_MS);
      shuffleBtn = getShuffleButton();
      if (shuffleBtn) break;
    }
    if (!shuffleBtn) return;

    // Turn shuffle OFF if ON
    if (isShuffleOn(shuffleBtn)) {
      shuffleBtn.click();
      clickAway();
      await sleep(WAIT_MS);

      // Reopen options
      if (!cards) optionsBtn.click();
      await sleep(WAIT_MS);
      shuffleBtn = getShuffleButton();
    }

    // Turn shuffle ON
    shuffleBtn?.click();

    // click-away to close menu and commit state
    await sleep(WAIT_MS);
    clickAway();

    //console.log('Deck reshuffled');
  }

  function createFloatingButton() {
    const btn = document.createElement('button');
    btn.textContent = 'Reshuffle';
    Object.assign(btn.style, {
      position: 'fixed',
      bottom: '20px',
      right: '20px',
      zIndex: 9999,
      padding: '10px 14px',
      background: '#2e3856',
      color: '#d9dde8',
      border: 'none',
      borderRadius: '12px',
      fontSize: '.875rem',
      cursor: 'pointer',
      boxShadow: '0 6px 16px rgba(0,0,0,0.25)'
    });

    btn.addEventListener('click', forceReshuffle);
    document.body.appendChild(btn);
  }

  function matchesKeybind(e) {
    if (isTyping()) {
        if (!!KEYBIND.ctrl !== !!e.ctrlKey) return false;
        if (!!KEYBIND.alt !== !!e.altKey) return false;
        if (!!KEYBIND.shift !== !!e.shiftKey) return false;
        if (!!KEYBIND.meta !== !!e.metaKey) return false;
        return e.key && e.key.toLowerCase() === KEYBIND.key.toLowerCase();
    } return e.key && e.key.toLowerCase() === KEYBIND2.toLowerCase();
  }

  window.addEventListener('keydown', function (e) {
    if (matchesKeybind(e)) {
      e.preventDefault();
      e.stopPropagation();
      forceReshuffle();
    }
  }, true);

  createFloatingButton();

})();