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.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", " "); } 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(); })();