- // ==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();
- })();