18C / Modules

// ==UserScript==
// @name        Modules
// @namespace   CCAU
// @description Automate course copies
// @match       https://*.instructure.com/courses/*/modules
// @version     0.1.8
// @author      CIDT
// @grant       none
// @license     GPL-3.0-or-later
// ==/UserScript==
"use strict";
(() => {
  // out/utils.js
  function addButton(name, fn, sel) {
    const bar = document.querySelector(sel);
    const btn = document.createElement("a");
    btn.textContent = name;
    btn.classList.add("btn");
    btn.setAttribute("tabindex", "0");
    btn.addEventListener("click", fn, false);
    bar?.insertAdjacentElement("afterbegin", btn);
    bar?.insertAdjacentHTML("afterbegin", " ");
  }
  function clickButton(sel) {
    const element = document.querySelector(sel);
    const btn = element;
    btn?.click();
  }
  function getChild(element, indices) {
    let cur = element;
    indices.forEach((i_) => {
      const children = cur?.children;
      const len = children?.length ?? 0;
      const i = i_ >= 0 ? i_ : len + i_;
      len > i ? cur = children[i] : null;
    });
    return cur;
  }
  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 regex = /week[^\d]*\d{1,2}(?=.?)/;
    const matches = lowerName.match(regex);
    const result = matches ? matches[0] : null;
    if (lowerName.includes("start") && lowerName.includes("here")) {
      return "START HERE";
    }
    if (!result) {
      return null;
    }
    return "Week " + result.split(" ")[1];
  }
  function moduleList() {
    const sel = ".collapse_module_link";
    const mods = Array.from(document.querySelectorAll(sel));
    return mods;
  }
  function openMenu(idx, btnIdx) {
    const mods = moduleList();
    const parent = mods[idx].parentElement;
    const btn = getChild(parent, [5, 0, btnIdx]);
    btn?.click();
  }
  function overrideConfirm() {
    const orig = window.confirm;
    window.confirm = () => true;
    return orig;
  }
  function restoreConfirm(orig) {
    window.confirm = orig;
  }

  // out/date_headers/utils.js
  function actOnDates(idc, fn) {
    const rows = document.querySelectorAll(".ig-row");
    const len = rows.length;
    for (let i = 0; i < len; i++) {
      const rowItem = rows[i];
      const label = getChild(rowItem, [2, 0]);
      const btn = getChild(rowItem, idc);
      const name = label?.innerText || "";
      const regex = /^\*?[a-z]{3,12} \d{1,2} - [a-z]{0,12} ?\d{1,2}\*?$/;
      if (!regex.test(name.toLowerCase())) {
        continue;
      }
      btn?.click();
      fn(name);
    }
  }
  function safeNestedJSON(data, keys) {
    return keys.reduce((acc, key) => {
      return acc ? acc[key] : null;
    }, data);
  }

  // out/date_headers/del.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) => {
      const len = m.children.length;
      const btn = getChild(m, [len - 1, 0]);
      btn?.click();
    });
  }
  function removeOldDates() {
    const orig = overrideConfirm();
    actOnDates([5, 2, 1, -1, 0], clickDelete);
    restoreConfirm(orig);
  }

  // out/env.js
  var ROOT_URL = "https://se.instructure.com";
  var DATE_HEADERS = { "dates": { "Spring": ["*Jan 13 - Jan 19*", "*Jan 20 - Jan 26*", "*Jan 27 - Feb 2*", "*Feb 3 - Feb 9*", "*Feb 10 - Feb 16*", "*Feb 17 - Feb 23*", "*Feb 24 - Mar 2*", "*Mar 3 - Mar 9*", "*Mar 10 - Mar 16*", "*Mar 24 - Mar 30*", "*Mar 31 - Apr 6*", "*Apr 7 - Apr 13*", "*Apr 14 - Apr 20*", "*Apr 21 - Apr 27*", "*Apr 28 - May 4*", "*May 5 - May 11*"] }, "ranges": { "Spring": { "14": "1-14", "16": "1-16", "7A": "1-7", "7B": "9-16", "8A": "1-8", "8B": "9-16" } } };

  // out/date_headers/modal.js
  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() {
    const semesters = Object.keys(DATE_HEADERS.dates);
    return semesters.map((sem) => {
      const button = document.createElement("button");
      button.textContent = sem;
      button.classList.add("ccau_semester_button");
      button.classList.add("btn");
      button.style.margin = "5px";
      return button;
    });
  }
  function termButtons(semester) {
    const data = JSON.parse(JSON.stringify(DATE_HEADERS));
    const terms = Object.keys(data["ranges"][semester]);
    return terms.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 replaceButtons(semester) {
    const sel = ".ccau_semester_button";
    const buttons = Array.from(document.querySelectorAll(sel));
    buttons.forEach((button) => button.remove());
    const newButtons = termButtons(semester);
    const modal = document.querySelector(".ccau_modal_content");
    if (!modal) {
      return;
    }
    newButtons.forEach((button) => modal.appendChild(button));
  }
  async function showModal() {
    const div = document.createElement("div");
    const buttons = semesterButtons();
    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 tCallback = (btn) => {
        btn.addEventListener("click", () => {
          term = btn.textContent;
          resolve([semester, term]);
          modal.remove();
        });
      };
      const sCallback = (btn) => {
        btn.addEventListener("click", () => {
          semester = btn.textContent;
          replaceButtons(semester || "");
          Array.from(document.querySelectorAll(".ccau_term_button")).map((e) => e).forEach(tCallback);
        });
        div.appendChild(btn);
      };
      buttons.forEach(sCallback);
      const modal = createModal(div);
    });
  }

  // out/date_headers/update.js
  function getRawDates(sem) {
    return safeNestedJSON(DATE_HEADERS, ["dates", sem]);
  }
  function getDateRange(sem, term) {
    return safeNestedJSON(DATE_HEADERS, ["ranges", sem, term]);
  }
  function datesInRange(dates, range) {
    return range.split(",").flatMap((r) => {
      const nums = r.split("-").map(Number);
      const start = nums[0];
      const end = nums[1];
      return dates.slice(start - 1, end || start);
    });
  }
  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 ([sem, term]) => {
        if (!sem || !term) {
          throw new Error("Null semester or term from modal");
        }
        const rawDates = getRawDates(sem);
        const range = getDateRange(sem, term);
        if (!rawDates || !range) {
          throw new Error("Dates or range are null");
        }
        const dates = datesInRange(rawDates, range);
        resolve(datesToWeeks(dates));
      });
    });
  }

  // out/date_headers/add.js
  function defaultToSubheader() {
    const sel = "#add_module_item_select";
    const element = document.querySelector(sel);
    const select = element;
    const options = Array.from(select.options);
    options?.forEach((opt) => opt.value = "context_module_sub_header");
  }
  function publish() {
    actOnDates([3, 1, 0], (_) => {
    });
  }
  function setInput(sel, val) {
    const element = document.querySelector(sel);
    const textBox = element;
    textBox.value = val;
  }
  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), 2);
      setInput("#sub_header_title", dates[n]);
      clickButton(".add_item_button");
    });
    setTimeout(publish, 1500);
  }
  function dateButton() {
    addButton("Add Dates", addDates, ".header-bar-right__buttons");
  }

  // out/live.js
  function getCourseID() {
    return window.location.href.match(/courses\/(\d+)/)?.[1] ?? "NO_COURSE_ID";
  }
  async function isLiveCourse() {
    const response = await fetch(ROOT_URL + "/api/v1/courses/" + getCourseID());
    const data = await response.json();
    return new Date(data["start_at"]) < /* @__PURE__ */ new Date();
  }
  async function ccau_confirm(msg) {
    return new Promise((resolve) => {
      const didConfirm = confirm("Are you sure you want to " + msg + "?");
      resolve(didConfirm);
    });
  }

  // out/modules/utils.js
  function isEmpty(m) {
    const mod = m.parentElement?.parentElement;
    return getChild(mod, [2, 0])?.children.length === 0;
  }
  function getReactHandler(obj) {
    const sel = "__reactProps";
    const keys = Object.keys(obj);
    const key = keys.find((k) => k.startsWith(sel));
    return key;
  }

  // out/modules/delete.js
  function clickDelete2() {
    const sel = ".ui-kyle-menu";
    const menus = Array.from(document.querySelectorAll(sel));
    menus.filter((m) => m.getAttribute("aria-hidden") === "false").forEach((m) => getChild(m, [5, 0])?.click());
  }
  async function removeEmpty() {
    if (!await ccau_confirm("delete empty modules")) {
      return;
    }
    const orig = overrideConfirm();
    const mods = moduleList();
    mods.filter((m) => isEmpty(m)).map((m) => mods.indexOf(m)).forEach((i) => {
      openMenu(i, 3);
      clickDelete2();
    });
    restoreConfirm(orig);
  }
  function deleteButton() {
    addButton("Remove Empty", removeEmpty, ".header-bar-right__buttons");
  }

  // out/modules/move.js
  function clickMoveContents() {
    const sel = ".ui-kyle-menu";
    const menus = Array.from(document.querySelectorAll(sel));
    menus.filter((m) => m.getAttribute("aria-hidden") === "false").forEach((m) => getChild(m, [2, 0])?.click());
  }
  function selectDestination(name) {
    const form_el = document.querySelector(".move-select-form");
    if (!form_el) {
      return false;
    }
    const form = form_el;
    const options = Array.from(form.options);
    const handlerName = getReactHandler(form);
    const handler = form[handlerName ?? ""];
    const i = options.findIndex((o) => o.text === name);
    if (i === -1 || !form) {
      return false;
    }
    form.selectedIndex = i;
    form.value = options[i].value;
    handler?.onChange({ target: { value: options[i].value } });
    return true;
  }
  async function moveAll() {
    if (!await ccau_confirm("move content into template modules")) {
      return;
    }
    const startIdx = lenientIndexOf("START HERE", 1);
    const mods = moduleList();
    if (startIdx === -1) {
      throw new Error("START HERE not found, add it and reload");
    }
    mods.slice(startIdx).filter((m) => !isEmpty(m) && lenientName(m.title)).forEach((m) => {
      const title = m.title;
      const name = lenientName(title);
      const index = indexOf(title, startIdx);
      openMenu(index, 3);
      clickMoveContents();
      if (selectDestination(name)) {
        clickButton("#move-item-tray-submit-button");
      }
    });
  }
  function moveButton() {
    addButton("Auto-Move", moveAll, ".header-bar-right__buttons");
  }

  // out/index.js
  async function main() {
    if (!document.querySelector("#global_nav_accounts_link")) {
      throw new Error("Only admins can use CCAU");
    }
    if (await isLiveCourse()) {
      throw new Error("CCAU is disabled in live courses");
    }
    dateButton();
    deleteButton();
    moveButton();
  }
  main();
})();