Modules

Automate course copies

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name        Modules
// @namespace   CCAU
// @description Automate course copies
// @match       https://*.instructure.com/courses/*/modules
// @version     0.1.0
// @author      CIDT
// @grant       none
// @license     BSD-3-Clause
// ==/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;
      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 ln = name.toLowerCase();
    const rgx = /^(week|module|unit) \d{1,2}(?=.?)/;
    const matches = ln.match(rgx);
    const result = matches ? matches[0] : null;
    if (ln.includes("start here")) {
      return "START HERE";
    }
    if (!result) {
      return null;
    }
    return "Week " + result.split(" ")[1];
  }
  function log(msg) {
    console.log("[CCAU] " + msg);
  }
  function moduleList() {
    const sel = ".collapse_module_link";
    const mods = Array.from(document.querySelectorAll(sel));
    return mods;
  }
  function openMenu(idx, btnIdx) {
    const mods = moduleList();
    const hpe = mods[idx].parentElement;
    const btn = getChild(hpe, [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 nm = label?.innerText || "";
      const rgx = /^\*?[a-z]{3,12} \d{1,2} - [a-z]{0,12} ?\d{1,2}\*?$/;
      if (!rgx.test(nm.toLowerCase())) {
        continue;
      }
      btn?.click();
      fn(nm);
    }
  }

  // out/date_headers/del.js
  function clickDelete(nm) {
    log(`Removing date header: ${nm}`);
    const nodes = document.querySelectorAll(".ui-kyle-menu");
    const menus = Array.from(nodes).map((e) => e);
    const len = menus.length;
    for (let i = 0; i < len; i++) {
      if (menus[i].getAttribute("aria-hidden") !== "false") {
        continue;
      }
      const miLen = menus[i].children.length;
      const btn = getChild(menus[i], [miLen - 1, 0]);
      btn?.click();
    }
  }
  function removeOldDates() {
    const orig = overrideConfirm();
    actOnDates([3, 2, 1, -1, 0], clickDelete);
    restoreConfirm(orig);
  }

  // out/env.js
  var CORS_PROXY = "https://api.allorigins.win/get?url=";
  var DATA_URL = "https://text.is/ccau_data/raw";

  // 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 cached = localStorage.getItem("ccau_data") ?? "{}";
    const data = JSON.parse(cached);
    const semesters = Object.keys(data["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(localStorage.getItem("ccau_data") || "{}");
    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) {
      throw new Error("Can't add buttons to null modal");
    }
    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 update() {
    const day = 1e3 * 60 * 60 * 24;
    const now = Date.now();
    const last = Number(localStorage.getItem("ccau_data_ts")) ?? 0;
    if (now - last < day) {
      return;
    }
    fetch(CORS_PROXY + encodeURIComponent(DATA_URL)).then((response) => response.json()).then((data) => {
      localStorage.setItem("ccau_data", data["contents"]);
      localStorage.setItem("ccau_data_ts", now.toString());
    });
  }
  function getRawDates(sem) {
    const data = JSON.parse(localStorage.getItem("ccau_data") || "{}");
    const dates = data["dates"][sem];
    if (!dates) {
      log(`No dates found for ${sem}`);
      return null;
    }
    return dates;
  }
  function getDateRange(sem, term) {
    const data = JSON.parse(localStorage.getItem("ccau_data") || "{}");
    const ret = data["ranges"][sem][term];
    if (!ret) {
      log(`No range found for ${sem} ${term}`);
      return null;
    }
    return ret;
  }
  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 mapToWeeks(dates) {
    const dict = {};
    for (let i = 0; i < dates.length; i++) {
      dict[`Week ${i + 1}`] = dates[i];
    }
    return dict;
  }
  async function getDates() {
    return new Promise((resolve) => {
      update();
      showModal().then(async ([sem, term]) => {
        if (!sem || !term) {
          resolve({});
          return;
        }
        const rawDates = getRawDates(sem);
        const range = getDateRange(sem, term);
        if (!rawDates || !range) {
          resolve({});
          return;
        }
        const dates = datesInRange(rawDates, range);
        resolve(mapToWeeks(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();
    const endIdx_ = indexOf("START HERE", 1);
    const endIdx = endIdx_ === -1 ? mods.length : endIdx_;
    for (let i = 0; i < endIdx; i++) {
      const title = mods[i].title;
      const name = lenientName(title);
      if (!name || !dates[name]) {
        log(`No date found for ${name ?? title}`);
        continue;
      }
      openMenu(indexOf(name), 2);
      setInput("#sub_header_title", dates[name]);
      clickButton(".add_item_button");
    }
    setTimeout(publish, 1500);
  }
  function dateButton() {
    addButton("Add Dates", addDates, ".header-bar-right__buttons");
  }

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

  // out/modules/del.js
  function clickDelete2() {
    const sel = ".ui-kyle-menu";
    const menus = Array.from(document.querySelectorAll(sel));
    const len = menus.length;
    for (let i = 0; i < len; i++) {
      if (menus[i].getAttribute("aria-hidden") !== "false") {
        continue;
      }
      const menuItem = menus[i];
      const btn = getChild(menuItem, [4, 0]);
      btn?.click();
    }
  }
  function removeEmpty() {
    const orig = overrideConfirm();
    const mods = moduleList();
    const len = mods.length;
    for (let i = 0; i < len - 1; i++) {
      if (!isEmpty(i)) {
        continue;
      }
      openMenu(i, 3);
      clickDelete2();
    }
    restoreConfirm(orig);
  }
  function deleteButton() {
    addButton("Remove Empty", removeEmpty, ".header-bar-right__buttons");
  }

  // out/modules/mov.js
  function clickMoveContents() {
    const sel = ".ui-kyle-menu";
    const menus = Array.from(document.querySelectorAll(sel));
    const len = menus.length;
    for (let i = 0; i < len; i++) {
      if (menus[i].getAttribute("aria-hidden") !== "false") {
        continue;
      }
      const menuItem = menus[i];
      const btn = getChild(menuItem, [2, 0]);
      btn?.click();
    }
  }
  function selectDestination(name) {
    const form = document.querySelector(".move-select-form");
    const options = Array.from(form?.options ?? []);
    const len = options.length;
    if (!form) {
      throw new Error("Could not find .move-select-form");
    }
    for (let i = 0; i < len; i++) {
      const opt = options[i];
      const handlerName = getReactHandler(form);
      const handler = form[handlerName ?? ""];
      const fakeObj = { target: { value: opt.value } };
      if (opt.text !== name) {
        continue;
      }
      form.selectedIndex = i;
      form.value = options[i].value;
      handler.onChange(fakeObj);
      return true;
    }
    return false;
  }
  function moveAll() {
    const startIdx = lenientIndexOf("START HERE", 1);
    const mods = moduleList();
    const len = mods.length;
    if (startIdx === -1) {
      throw new Error("START HERE not found, add it and reload");
    }
    for (let i = startIdx; i < len; i++) {
      const title = mods[i].title;
      const name = lenientName(title);
      const idx = indexOf(title, startIdx);
      if (!name || isEmpty(i)) {
        continue;
      }
      openMenu(idx, 3);
      clickMoveContents();
      if (!selectDestination(name)) {
        throw new Error(`No destination selected for ${name}`);
      }
      clickButton("#move-item-tray-submit-button");
    }
  }
  function moveButton() {
    addButton("Auto-Move", moveAll, ".header-bar-right__buttons");
  }

  // out/index.js
  function main() {
    if (!document.querySelector("#global_nav_accounts_link")) {
      throw new Error("Only admins can use this script");
    }
    dateButton();
    deleteButton();
    moveButton();
  }
  main();
})();