NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Modules
// @namespace CCAU
// @description Automate course copies
// @match https://*.instructure.com/courses/*/modules
// @version 0.3.4
// @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));
}
const start_date = new Date(data.start_at);
start_date.setDate(start_date.getDate() + 7);
return start_date < /* @__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", " ");
}
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: {
Spring: [
"*January 12 - 18*",
"*January 19 - 25*",
"*January 26 - February 1*",
"*February 2 - 8*",
"*February 9 - 15*",
"*February 16 - 22*",
"*February 23 - March 1*",
"*March 2 - 8*",
"*March 9 - 15*",
"*March 23 - 29*",
"*March 30 - April 5*",
"*April 6 - 12*",
"*April 13 - 19*",
"*April 20 - 26*",
"*April 27 - May 3*",
"*May 4 - 10*"
]
},
ranges: {
Spring: {
"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(lenientIndexOf(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();
})();