18C / Modules

// ==UserScript==
// @name        Modules
// @namespace   CCAU
// @description Automate course copies
// @match       https://*.instructure.com/courses/*/modules
// @version     0.3.0
// @author      Cappiebara
// @grant       none
// @license     GPL-3.0-or-later
// ==/UserScript==
"use strict";
(() => {
  // out/ccau.js
  function isAdmin() {
    const adminButton = document.querySelector("#global_nav_accounts_link");
    return adminButton ? true : false;
  }
  function getCourseID() {
    return window.location.href.match(/courses\/(\d+)/)?.[1] ?? "NO_COURSE_ID";
  }
  async function isLiveCourse() {
    const response = await fetch("https://se.instructure.com/api/v1/courses/" + getCourseID());
    const data = await response.json();
    if (!data.start_at) {
      return new Promise((resolve) => resolve(false));
    }
    return new Date(data.start_at) < /* @__PURE__ */ new Date();
  }
  async function ccauConfirm(msg) {
    return new Promise((resolve) => {
      const didConfirm = confirm("Are you sure you want to " + msg + "?");
      resolve(didConfirm);
    });
  }
  function clickButton(sel) {
    const element = document.querySelector(sel);
    const button = element;
    button?.click();
  }

  // out/utils.js
  function isEmpty(m) {
    const module = m.parentElement?.parentElement;
    const list = module?.querySelector(".content > ul");
    return list?.children.length === 0;
  }
  function getReactHandler(obj) {
    return Object.keys(obj).find((k) => k.startsWith("__reactProps"));
  }
  function openMenuItem(index, name) {
    const modules = moduleList();
    const parent = modules[index].parentElement;
    const button = parent?.querySelector(`.ig-header-admin > .al-options > li > a[title='${name}']`);
    button?.click();
  }
  function addButton(name, fn) {
    const slot = document.querySelector(".header-bar-right__buttons");
    const button = document.createElement("a");
    button.textContent = name;
    button.classList.add("btn");
    button.setAttribute("tabindex", "0");
    button.addEventListener("click", fn, false);
    slot?.insertAdjacentElement("afterbegin", button);
    slot?.insertAdjacentHTML("afterbegin", "&nbsp;");
  }
  function indexOf(name, skip = 0) {
    return moduleList().findIndex((m, i) => i >= skip && m.title.toLowerCase() === name.toLowerCase());
  }
  function lenientIndexOf(name, skip = 0) {
    return moduleList().findIndex((m, i) => i >= skip && lenientName(m.title) === lenientName(name));
  }
  function lenientName(name) {
    const lowerName = name.toLowerCase();
    const pattern = /week[^\d]*\d{1,2}(?=.?)/;
    const matches = lowerName.match(pattern);
    const result = matches ? matches[0] : null;
    if (lowerName.includes("start") && lowerName.includes("here")) {
      return "START HERE";
    }
    return result ? "Week " + result.split(" ")[1] : null;
  }
  function moduleList() {
    return Array.from(document.querySelectorAll(".collapse_module_link"));
  }
  function withOverriddenConfirm(fn) {
    const orig = window.confirm;
    window.confirm = () => true;
    fn();
    window.confirm = orig;
  }

  // out/delete_empty.js
  async function removeEmpty() {
    if (!await ccauConfirm("delete empty modules")) {
      return;
    }
    withOverriddenConfirm(() => {
      const modules = moduleList();
      modules.filter(isEmpty).map((m) => modules.indexOf(m)).forEach((i) => openMenuItem(i, "Delete this module"));
    });
  }
  function addDeleteButton() {
    addButton("Remove Empty", removeEmpty);
  }

  // out/move_modules.js
  function selectDestination(name) {
    const form = document.querySelector(".move-select-form");
    if (!form) {
      return false;
    }
    const options = Array.from(form.options);
    const handlerName = getReactHandler(form);
    const handler = form[handlerName ?? ""];
    const index = options.findIndex((o) => o.text === name);
    if (index === -1) {
      return false;
    }
    form.selectedIndex = index;
    form.value = options[index].value;
    handler?.onChange({ target: { value: options[index].value } });
    return true;
  }
  async function moveAll() {
    if (!await ccauConfirm("move content into template modules")) {
      return;
    }
    const startIndex = lenientIndexOf("START HERE", 1);
    moduleList().slice(startIndex).filter((m) => !isEmpty(m) && lenientName(m.title)).forEach((m) => {
      const name = lenientName(m.title);
      const index = indexOf(m.title, startIndex);
      openMenuItem(index, "Move module contents");
      if (name && selectDestination(name)) {
        clickButton("#move-item-tray-submit-button");
      }
    });
  }
  function addMoveButton() {
    addButton("Auto-Move", moveAll);
  }

  // out/date_modal.js
  var DATE_HEADERS = {
    dates: {
      Summer: [
        "*May 12 - May 18*",
        "*May 19 - May 25*",
        "*May 26 - Jun 1*",
        "*Jun 2 - Jun 8*",
        "*Jun 9 - Jun 15*",
        "*Jun 16 - Jun 22*",
        "*Jun 23 - Jun 29*",
        "*Jun 30 - Jul 6*",
        "*Jul 7 - Jul 13*",
        "*Jul 14 - Jul 20*",
        "*Jul 21 - Jul 27*",
        "*Jul 28 - Aug 3*",
        "*Aug 4 - Aug 10*",
        "*Aug 11 - Aug 17*"
      ],
      Fall: [
        "*Aug 18 - Aug 24*",
        "*Aug 25 - Aug 31*",
        "*Sep 1 - Sep 7*",
        "*Sep 8 - Sep 14*",
        "*Sep 15 - Sep 21*",
        "*Sep 22 - Sep 28*",
        "*Sep 29 - Oct 5*",
        "*Oct 6 - Oct 12*",
        "*Oct 13 - Oct 19*",
        "*Oct 20 - Oct 26*",
        "*Oct 27 - Nov 2*",
        "*Nov 3 - Nov 9*",
        "*Nov 10 - Nov 16*",
        "*Nov 17 - Nov 23*",
        "*Dec 1 - Dec 7*",
        "*Dec 8 - Dec 14*"
      ]
    },
    ranges: {
      Summer: {
        "14": [1, 14],
        "7A": [1, 7],
        "7B": [8, 14],
        "8": [4, 11],
        "4A": [4, 7],
        "4B": [8, 11]
      },
      Fall: {
        "16": [1, 16],
        "7A": [1, 7],
        "7B": [9, 15],
        "8A": [1, 8],
        "8B": [9, 16],
        "14": [1, 15]
      }
    }
  };
  function createModal(div) {
    const container = document.createElement("div");
    const content = document.createElement("div");
    container.className = "ccau_modal";
    container.style.position = "fixed";
    container.style.top = "0";
    container.style.left = "0";
    container.style.width = "100%";
    container.style.height = "100%";
    container.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
    container.style.display = "flex";
    container.style.justifyContent = "center";
    container.style.alignItems = "center";
    container.style.zIndex = "1000";
    content.classList.add("ccau_modal_content");
    content.classList.add("ui-dialog");
    content.classList.add("ui-widget");
    content.classList.add("ui-widget-content");
    content.classList.add("ui-corner-all");
    content.classList.add("ui-dialog-buttons");
    content.style.padding = "20px";
    content.style.textAlign = "center";
    document.body.appendChild(container);
    container.appendChild(content);
    content.appendChild(div);
    return container;
  }
  function semesterButtons() {
    return Object.keys(DATE_HEADERS.dates).map((semester) => {
      const button = document.createElement("button");
      button.textContent = semester;
      button.classList.add("ccau_semester_button");
      button.classList.add("btn");
      button.style.margin = "5px";
      return button;
    });
  }
  function termButtons(semester) {
    return Object.keys(DATE_HEADERS.ranges[semester]).map((term) => {
      const button = document.createElement("button");
      button.textContent = term;
      button.classList.add("ccau_term_button");
      button.classList.add("btn");
      button.style.margin = "5px";
      return button;
    });
  }
  function addTermButtonsForSemester(semester) {
    Array.from(document.querySelectorAll(".ccau_semester_button")).forEach((b) => b.remove());
    const buttons = termButtons(semester);
    const modal = document.querySelector(".ccau_modal_content");
    if (!modal) {
      return;
    }
    buttons.forEach((b) => modal.appendChild(b));
  }
  async function showModal() {
    const div = document.createElement("div");
    const label = document.createElement("div");
    label.textContent = "Which semester is this course?";
    div.appendChild(label);
    let semester = null;
    let term = null;
    return new Promise((resolve) => {
      const termCallback = (button) => {
        button.addEventListener("click", () => {
          term = button.textContent;
          resolve([semester, term]);
          modal.remove();
        });
      };
      const semesterCallback = (button) => {
        button.addEventListener("click", () => {
          semester = button.textContent;
          addTermButtonsForSemester(semester || "");
          Array.from(document.querySelectorAll(".ccau_term_button")).map((e) => e).forEach(termCallback);
        });
        div.appendChild(button);
      };
      semesterButtons().forEach(semesterCallback);
      const modal = createModal(div);
    });
  }

  // out/update_dates.js
  function clickDelete(_) {
    const nodes = document.querySelectorAll(".ui-kyle-menu");
    const menus = Array.from(nodes).map((e) => e);
    menus.filter((m) => m.getAttribute("aria-hidden") === "false").forEach((m) => m.querySelector("li > .delete_link")?.click());
  }
  function removeOldDates() {
    withOverriddenConfirm(() => actOnDates(".ig-admin > .cog-menu-container > ul > li > .delete_link", clickDelete));
  }
  function isDateHeader(item) {
    const label = item.querySelector(".ig-info > .module-item-title");
    const regex = /^\*?[a-z]{3,12} \d{1,2} - [a-z]{0,12} ?\d{1,2}\*?$/;
    if (!label?.innerText || !regex.test(label?.innerText.toLowerCase())) {
      return false;
    }
    return true;
  }
  function actOnDates(selector, fn) {
    const rows = document.querySelectorAll(".ig-row");
    const len = rows.length;
    for (let i = 0; i < len; i++) {
      const item = rows[i];
      if (!item || !isDateHeader(item)) {
        continue;
      }
      if (selector) {
        const button = item?.querySelector(selector);
        button?.click();
      }
      fn(item);
    }
  }
  function datesToWeeks(dates) {
    return dates.reduce((acc, date, i) => {
      acc[`Week ${i + 1}`] = date;
      return acc;
    }, {});
  }
  async function getDates() {
    return new Promise((resolve) => {
      showModal().then(async ([semester, term]) => {
        if (!semester || !term) {
          throw new Error("Null semester or term from modal");
        }
        const range = DATE_HEADERS.ranges[semester][term];
        const dates = DATE_HEADERS.dates[semester].slice(range[0] - 1, range[1]);
        resolve(datesToWeeks(dates));
      });
    });
  }
  function defaultToSubheader() {
    const sel = "#add_module_item_select";
    const element = document.querySelector(sel);
    const select = element;
    const options = Array.from(select.options);
    options?.forEach((o) => o.value = "context_module_sub_header");
  }
  function setInput(val) {
    const element = document.querySelector("#sub_header_title");
    const textBox = element;
    textBox.value = val;
  }
  function openMenu(idx) {
    const mods = moduleList();
    const parent = mods[idx].parentElement;
    const button = parent?.querySelector(".module_header_items > .ig-header-admin > .add_module_item_link");
    button?.click();
  }
  function simulate(element, type, x, y) {
    const event = new MouseEvent(type, {
      bubbles: true,
      cancelable: true,
      clientX: x,
      clientY: y,
      view: window
    });
    element.dispatchEvent(event);
  }
  async function dragItem(item) {
    const target = item.closest("ul")?.querySelector("li");
    const handle = item.querySelector(".ig-handle > .draggable-handle");
    if (!target || !handle) {
      return;
    }
    handle.scrollIntoView();
    const sourceBox = handle.getBoundingClientRect();
    const startX = sourceBox.left + sourceBox.width / 2;
    const startY = sourceBox.top + sourceBox.height / 2;
    const targetBox = target.getBoundingClientRect();
    const endX = targetBox.left + targetBox.width / 2;
    const endY = targetBox.top + targetBox.height / 2;
    simulate(handle, "mousedown", startX, startY);
    simulate(document, "mousemove", startX + 1, startY + 1);
    simulate(document, "mousemove", endX, endY);
    target.scrollIntoView();
    simulate(document, "mouseup", endX, endY);
  }
  function publish() {
    actOnDates(".ig-admin > span[title='Publish'] > i", (_) => {
    });
  }
  function moveToTop() {
    actOnDates(null, dragItem);
  }
  async function addDates() {
    removeOldDates();
    defaultToSubheader();
    const dates = await getDates();
    const mods = moduleList();
    mods.map((m) => lenientName(m.title)).filter((n) => n && dates[n]).map((n) => n).forEach((n) => {
      openMenu(indexOf(n));
      setInput(dates[n]);
      clickButton(".add_item_button");
    });
    setTimeout(moveToTop, 1e3);
    setTimeout(publish, 1e3);
  }
  function addDateButton() {
    addButton("Add Dates", addDates);
  }

  // out/index.js
  async function main() {
    if (!isAdmin()) {
      throw new Error("Only admins can use CCAU");
    }
    if (await isLiveCourse()) {
      throw new Error("CCAU is disabled in live courses");
    }
    [
      addMoveButton,
      addDeleteButton,
      addDateButton
    ].reverse().forEach((f) => f());
  }
  main();
})();