Last active
February 13, 2025 14:53
-
-
Save AndreasLonn/c5f12b1704c748a9bc18786ea295aedc to your computer and use it in GitHub Desktop.
Tools for students and teachers at Jönköping University
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name JU Tools | |
// @namespace http://tampermonkey.net/ | |
// @version 2.16 | |
// @description Tools for students and teachers at Jönköping University | |
// @author AndreasLonn | |
// @downloadURL https://gist.github.com/AndreasLonn/c5f12b1704c748a9bc18786ea295aedc/raw/JUTools.user.js | |
// @updateURL https://gist.github.com/AndreasLonn/c5f12b1704c748a9bc18786ea295aedc/raw/JUTools.user.js | |
// @run-at document-start | |
// @match https://ju.instructure.com/* | |
// @match https://ju.quiz-lti-dub-prod.instructure.com/* | |
// @match https://ju.se/* | |
// @match https://canvadocs-prod-dub.inscloudgate.net/1/sessions/* | |
// @icon https://ju.se/favicon.ico | |
// @grant GM_getValue | |
// @grant GM_setValue | |
// @grant GM_getTab | |
// @grant GM_saveTab | |
// ==/UserScript== | |
/* | |
Access the settings meny by appending "?jutools-settings" (or "&jutools-settings") | |
to the url of a matched website, e.g. "https://ju.se?jutools-settings". | |
If enabled (which it is by default), settings can also be accessed | |
by clicking "JUTools Settings" in the "Account" sidebar on Canvas. | |
*/ | |
(function() { | |
'use strict'; | |
// console.log('Origin:', window.origin); | |
function q(elem, parent=document) { return parent.querySelector(elem); } | |
function qs(elems, parent=document) { return parent.querySelectorAll(elems); } | |
function getUrlParam(key, search=location.search) { | |
return (new URLSearchParams(search)).get(key); | |
} | |
const matchingPage = { | |
hostname(regex) { return regex.test(location.hostname); }, | |
path(regex) { return regex.test(location.pathname); }, | |
param(key, value='') { return getUrlParam(key) === value; }, | |
get ju() { return this.hostname(/^ju.se$/); }, | |
get canvas() { return this.hostname(/^ju.instructure.com$/); }, | |
get canvasDocument() { return this.hostname(/canvadocs\-prod\-dub\.inscloudgate\.net/) && this.path(/^\/1\/sessions\//); }, | |
get canvasFile() { return this.canvas && this.path(/^\/courses\/[0-9]+\/files\/[0-9]+/); }, | |
get canvasFileJutools() { return this.canvasFile && this.param('jutools-file_preview'); }, | |
get canvasModules() { return this.canvas && this.path(/^\/courses\/[0-9]+\/modules/); }, | |
get canvasGrades() { return this.canvas && this.path(/^\/courses\/[0-9]+\/gradebook$/); }, | |
get canvasSpeedgrader() { return this.canvas && this.path(/^\/courses\/[0-9]+\/gradebook\/speed_grader/); }, | |
get canvasAdjustQuiz() { return this.canvas && this.path(/^\/courses\/[0-9]+\/quizzes\/[0-9]+\/moderate/); }, | |
get canvasAdjustQuizNew() { return this.canvas && this.path(/^\/courses\/[0-9]+\/assignments/) && this.param('display', 'full_width'); }, | |
get canvasAdjustQuizNewIFrame() { return this.hostname(/ju\.quiz\-lti\-dub\-prod\.instructure\.com/); }, | |
get canvasAdjustQuizNewModerate() { return this.path(/^\/moderation\/(\d)+/); }, | |
get canvasProfile() { return this.canvas && this.path(/^\/profile/); }, | |
get settings() { return this.param('jutools-settings'); }, | |
}; | |
/** | |
* Watches {@link elem} for changes and calls {@link onChange} when changes are detected. | |
* Passes "mutations" and "mutationObserver" to onChange | |
*/ | |
function watchElem(elem, onChange, options={ subtree: true, childList: true }) { | |
new MutationObserver((mutations, mutationObserver) => { | |
onChange(mutations, mutationObserver); | |
}).observe((typeof elem === 'string') ? q(elem) : elem, options); | |
} | |
/** | |
* Wait for {@link elem} to appear in the document | |
*/ | |
function waitForElem(elem) { // https://stackoverflow.com/a/61511955 | |
return new Promise(resolve => { | |
{ | |
const element = q(elem); | |
if (element) return resolve(element); | |
} | |
watchElem(document.documentElement, (mutations, observer) => { | |
const element = q(elem); | |
if (element) { | |
observer.disconnect(); | |
resolve(element); | |
} | |
}); | |
}); | |
} | |
/** | |
* Wait for the "DOMContentLoaded" event | |
*/ | |
function waitForLoad() { | |
return new Promise(resolve => { | |
document.addEventListener('DOMContentLoaded', () => resolve()); | |
}); | |
} | |
function waitForAndWatchElem(elem, onChange, options={ subtree: true, childList: true }) { | |
waitForElem(elem).then(element => { | |
onChange(element, []); | |
watchElem(element, mutations => onChange(element, mutations), options) | |
}); | |
} | |
/** | |
* Waits for and clicks the elements in order | |
* | |
* @param {...string} elems - CSS selectors to click | |
*/ | |
function waitForAndClick(...elems) { | |
return new Promise(async resolve => { | |
if(elems.length <= 0 || elems[0].length <= 0) return resolve(); | |
(await waitForElem(elems.shift())).click(); | |
await waitForAndClick(...elems); | |
resolve(); | |
}); | |
} | |
const settings = { | |
'canvasShowJuToolsSettingsInSidebar': true, | |
'canvasDocumentOpenButton': true, | |
'canvasGradesStudentInsertion': true, | |
'canvasSpeedgraderLRBtnAssignment': true, | |
'canvasModulesShortcuts': true, | |
'canvasSpeedgraderSearchStudents': true, | |
'canvasSpeedgraderSortStudents': 'firstname', | |
'canvasSpeedgraderSortStudentsGradedLast': true, | |
'canvasAdjustQuizSortStudents': 'score', | |
...JSON.parse(GM_getValue('jutools-settings', '{}')) | |
}; | |
const inIFrame = (() => { | |
try { return window.self !== window.top; } | |
catch (e) { return true; } | |
})(); | |
function sortChildren(elem, sortFunction) { | |
elem.replaceChildren(...[...elem.children].sort(sortFunction)); | |
} | |
function addCSS(css, after=true) { | |
const newCssStyle = new CSSStyleSheet(); | |
newCssStyle.replace(css); | |
document.adoptedStyleSheets = after ? [...document.adoptedStyleSheets, newCssStyle] : [newCssStyle, ...document.adoptedStyleSheets]; | |
} | |
function clickIsInsideBoundaries(event, rect) { | |
return event.clientY >= rect.top | |
&& event.clientX <= rect.right | |
&& event.clientY <= rect.bottom | |
&& event.clientX >= rect.left; | |
} | |
/** | |
* Send message to other page | |
* | |
* To send from inside iframe, use {@link window.top} as {@link receiver} | |
* | |
* To send to iframe, use `q('iframe#my-iframe').contentWindow` as {@link receiver} | |
*/ | |
function sendMessage(receiver, targetOrigin, subject, message) { | |
receiver.postMessage({'sender': 'jutools', 'subject': subject, 'message': message}, targetOrigin); | |
} | |
/** | |
* Receive message from {@link sendMessage} | |
*/ | |
function handleMessages(subject, onMessage) { | |
window.addEventListener('message', event => { | |
if(event.data | |
&& event.data.sender && event.data.sender === 'jutools' | |
&& event.data.subject && event.data.subject === subject) { | |
onMessage(event.data.message); | |
} | |
}); | |
} | |
/** | |
* Perform a regex match and return first match or undefined in no matches were found | |
*/ | |
String.prototype.matchFirst = function (regex) { return (this.match(regex) || [undefined])[0] }; | |
/** | |
* Get link to file preview. Link should be in form: | |
* [https://ju.instructure.com]/courses/{courseId}/files/{filesId} | |
* @param {boolean} jutoolsLink - Links to JUTools or directly to file preview | |
* @param {string} path - Path to the file. Defaults to location.pathname | |
* @returns string | |
*/ | |
function getFilePreviewLink(jutoolsLink, path=location.pathname) { | |
const urlParts = path.split('/'); | |
while(urlParts.length > 0 && urlParts[0] !== 'courses') urlParts.shift(); | |
if(urlParts[0] !== 'courses' || urlParts[2] !== 'files') { | |
throw `path does not match "/courses/<courseId>/files/<filesId>". path: "${path}"`; | |
} | |
const courseId = urlParts[1]; | |
const fileId = urlParts[3]; | |
return `${location.origin}/courses/${courseId}/files/${fileId}/${jutoolsLink ? '?jutools-file_preview' : 'file_preview'}`; | |
} | |
/// Add Open-button to PDF:s and other documents in Canvas JU | |
if (settings.canvasDocumentOpenButton) { | |
/// Update title | |
if(!inIFrame && matchingPage.canvasDocument) { | |
// console.log('canvasDocumentOpenButton canvasDocument'); | |
waitForLoad().then(() => { | |
try { | |
const title = decodeURIComponent(unsafeWindow.DocViewer.sessionData.pdfjs.documentName); | |
document.title = `${title} - ${document.title}`; | |
} catch { | |
console.error('Go back to Canvas and open again'); | |
GM_getTab(tab => { | |
if(tab.jutoolsFilePreviewLink) location.replace(tab.jutoolsFilePreviewLink); | |
else if(confirm('Session needs to be refreshed. Go back to Canvas?')) window.history.back(); | |
}); | |
} | |
}); | |
} | |
/// Normal files accessed through for example Modules | |
else if(matchingPage.canvasFile) { | |
// console.log('canvasDocumentOpenButton canvasFile'); | |
if(matchingPage.canvasFileJutools) { | |
// console.log('canvasDocumentOpenButton canvasFileJutools'); | |
GM_getTab(tab => { | |
const filePreviewLink = getFilePreviewLink(true); | |
tab.jutoolsFilePreviewLink = filePreviewLink; | |
GM_saveTab(tab); | |
location.replace(getFilePreviewLink(false)); | |
}); | |
} | |
waitForElem('a[download]').then(downloadElem => { | |
const docPreviewUrl = getFilePreviewLink(true); | |
const oldElem = downloadElem.parentElement.parentElement; | |
const newElem = oldElem.cloneNode(true); | |
newElem.childNodes[2].remove() // Remove file size text | |
q('a[download]', newElem).href = docPreviewUrl; | |
q('a[download]', newElem).textContent = 'Open'; | |
q('a[download]', newElem).removeAttribute('download'); | |
oldElem.parentElement.insertBefore(newElem, oldElem); | |
return; | |
}); | |
} | |
/// Files accessed through the overlay | |
waitForElem('.ef-file-preview-overlay').then(async overlay => { | |
// console.log('canvasDocumentOpenButton overlay'); | |
// Check if it already exists | |
if(q('a.jutools-open-button', overlay)) return; | |
// Find iframe and extract preview URL | |
const frame = await waitForElem('iframe.ef-file-preview-frame'); | |
const filePreviewURL = getFilePreviewLink(true, frame.src); | |
const oldElem = q('a.ef-file-preview-button', overlay); | |
const newElem = oldElem.cloneNode(true); | |
newElem.href = filePreviewURL; | |
newElem.classList.add('jutools-open-button'); | |
newElem.removeAttribute('download'); | |
q('i', newElem).className = 'icon-circle-arrow-up'; | |
q('span', newElem).textContent = 'Open'; | |
oldElem.parentElement.insertBefore(newElem, oldElem); | |
}); | |
} | |
// Insert students, from URL, into filter on the Grades page | |
if (settings.canvasGradesStudentInsertion) { | |
// console.log('canvasGradesStudentInsertion'); | |
/// Uses URL hash to enter students on the Grades page | |
/// This allows for refreshing while keeping filtered students | |
if(matchingPage.canvasGrades) { | |
// Get student_ids from URL hash (with the leading '#' removed) | |
const initialStudent_ids = getUrlParam('jutools-student_ids', location.hash.substring(1)); | |
let doneFillingStudents = false; | |
if(initialStudent_ids) { | |
const elemsToClick = []; | |
initialStudent_ids.split(',').forEach(student_id => { | |
// First click the filter element to bring up the list of students | |
elemsToClick.push('#student-names-filter:not([disabled])'); | |
// Then click the correct student | |
elemsToClick.push(`[id="${student_id}"]`); | |
// Repeat for each student_id | |
}); | |
waitForAndClick(...elemsToClick).then(() => { | |
doneFillingStudents = true | |
}); | |
} else doneFillingStudents = true; | |
// Update URL hash when user adds students to filter | |
waitForAndWatchElem('#gradebook_grid .container_0 .canvas_0', () => { | |
// Check if we are done filling students from the hash | |
if(!doneFillingStudents) return; | |
// Check that we have selected at least one student | |
if(!q('#gradebook-student-search label[for="student-names-filter"] button')) return; | |
// Collect student_ids from the gradebook grid, as it is not stored in the filter section | |
const studentElems = Array.from(qs('#gradebook_grid .student-name [data-student_id]')); | |
const newHashValue = studentElems.map(student => student.dataset.student_id).join(','); | |
if(!newHashValue) return; | |
// Create a URLSearchParams object to ensure that we don't update other values | |
const newHashParams = new URLSearchParams(location.hash.substring(1)); | |
newHashParams.set('jutools-student_ids', newHashValue); | |
location.hash = newHashParams.toString(); | |
}); | |
} | |
/// Add shortcut to the Grades page filtered on the current student in the Speedgrader | |
else if (matchingPage.canvasSpeedgrader) { | |
waitForElem('.content_box:last-child > h2').then(async elem => { | |
const linkElem = (await waitForElem('#breadcrumbs li:last-child a')).cloneNode(true); | |
linkElem.href = `${linkElem.href}#jutools-student_ids=${getUrlParam('student_id')}`; | |
elem.appendChild(linkElem); | |
}); | |
} | |
} | |
/// Make Left and Right button in SpeedGrader go to previous and next assignment instead of student | |
if (settings.canvasSpeedgraderLRBtnAssignment && matchingPage.canvasSpeedgrader) { | |
// console.log('canvasSpeedgraderLRBtnAssignment'); | |
const assignmentId = Number(getUrlParam('assignment_id')); | |
waitForElem('#prev-student-button').then(prevStudentButton => { | |
prevStudentButton.innerHTML = prevStudentButton.innerHTML; | |
prevStudentButton.addEventListener('click', event => { | |
event.stopImmediatePropagation(); | |
location.href = location.href.replace(`assignment_id=${assignmentId}`, `assignment_id=${assignmentId - 1}`); | |
}); | |
}); | |
waitForElem('#next-student-button').then(nextStudentButton => { | |
nextStudentButton.innerHTML = nextStudentButton.innerHTML; | |
nextStudentButton.addEventListener('click', event => { | |
event.stopImmediatePropagation(); | |
location.href = location.href.replace(`assignment_id=${assignmentId}`, `assignment_id=${assignmentId + 1}`); | |
}); | |
}); | |
} | |
/// Add link to SpeedGrader next to assignment in Modules | |
if (settings.canvasModulesShortcuts && matchingPage.canvasModules) { | |
// console.log('canvasModulesShortcuts'); | |
const courseId = Number(location.pathname.matchFirst(/(?<=^\/courses\/)[0-9]+/)); | |
waitForAndWatchElem('#context_modules', (modules, mutations) => { | |
// Only select assignments where user has access to admin actions, otherwise shortcut is useless | |
qs('li.attachment:not(:has(.jutools-shortcut-open))', modules).forEach(attachmentElem => { | |
const attachmentIdClass = [...attachmentElem.classList].find(className => /Attachment_(\d)+/.test(className)); | |
if(!attachmentIdClass) return; | |
const attachmentId = Number(attachmentIdClass.substring('Attachment_'.length)); | |
const openA = document.createElement('a'); | |
openA.rel = 'noopener noreferrer'; | |
openA.classList.add('jutools-shortcut-open', 'icon-circle-arrow-up'); | |
openA.href = getFilePreviewLink(true, `/courses/${courseId}/files/${attachmentId}`); | |
openA.title = 'Open file'; | |
q('div', attachmentElem).insertBefore(openA, q('.ig-admin', attachmentElem)); | |
}); | |
// Only select assignments where user has access to admin actions, otherwise shortcut is useless | |
qs('li.assignment:has(.ig-admin):not(:has(.jutools-shortcut-speedgrader))', modules).forEach(assignmentElem => { | |
const assignmentIdClass = [...assignmentElem.classList].find(className => /Assignment_(\d)+/.test(className)); | |
if(!assignmentIdClass) return; | |
const assignmentId = Number(assignmentIdClass.substring('Assignment_'.length)); | |
const speedGraderA = document.createElement('a'); | |
speedGraderA.rel = 'noopener noreferrer'; | |
speedGraderA.classList.add('jutools-shortcut-speedgrader', 'icon-speed-grader'); | |
speedGraderA.href = `/courses/${courseId}/gradebook/speed_grader?assignment_id=${assignmentId}`; | |
q('div', assignmentElem).insertBefore(speedGraderA, q('.ig-admin', assignmentElem)); | |
}); | |
// Only select assignments where user has access to admin actions, otherwise shortcut is useless | |
qs('li.lti-quiz:has(.ig-admin):not(:has(.jutools-shortcut-quiz-moderate))', modules).forEach(quizElem => { | |
const assignmentIdClass = [...quizElem.classList].find(className => /Assignment_(\d)+/.test(className)); | |
if(!assignmentIdClass) return; | |
const assignmentId = Number(assignmentIdClass.substring('Assignment_'.length)); | |
const moderateA = document.createElement('a'); | |
moderateA.rel = 'noopener noreferrer'; | |
moderateA.classList.add('jutools-shortcut-quiz-moderate', 'icon-quiz'); | |
moderateA.href = `/courses/${courseId}/assignments/${assignmentId}?display=full_width&jutools-moderate`; | |
q('div', quizElem).insertBefore(moderateA, q('.ig-admin', quizElem)); | |
}); | |
}); | |
} | |
/// Sort the list of students in SpeedGrader in a set order | |
if (settings.canvasSpeedgraderSortStudents && matchingPage.canvasSpeedgrader) { | |
// console.log('canvasSpeedgraderSortStudents'); | |
const sortFunctions = [(a,b,sortI) => 0]; | |
switch(settings.canvasSpeedgraderSortStudents) { | |
case 'firstname': | |
sortFunctions.push((a, b, sortI) => { | |
const nameA = q('li a span.ui-selectmenu-item-header', a).textContent.replace(/\n( )+/g, ''); | |
const nameB = q('li a span.ui-selectmenu-item-header', b).textContent.replace(/\n( )+/g, ''); | |
return (nameA > nameB) ? 1 : (nameA < nameB) ? -1 : sortFunctions[sortI - 1](a, b, sortI - 1); | |
}); | |
break; | |
} | |
if(settings.canvasSpeedgraderSortStudentsGradedLast) { | |
// console.log('canvasSpeedgraderSortStudentsGradedLast'); | |
addCSS(` | |
#students_selectmenu-menu .resubmitted .speedgrader-selectmenu-icon { | |
color: #0cd; | |
} | |
`); | |
sortFunctions.push((a, b, sortI) => { // not_graded > resubmitted > not_submitted > graded | |
const aState = a.classList.contains('not_graded') ? 0 : a.classList.contains('resubmitted') ? 1 : a.classList.contains('not_submitted') ? 2 : 3; | |
const bState = b.classList.contains('not_graded') ? 0 : b.classList.contains('resubmitted') ? 1 : b.classList.contains('not_submitted') ? 2 : 3; | |
if(aState === bState) return sortFunctions[sortI - 1](a, b, sortI - 1); | |
return aState - bState; | |
}); | |
} | |
waitForElem('#students_selectmenu-menu').then(listElem => { | |
if(sortFunctions.length > 1) sortChildren(listElem, (a,b) => sortFunctions[sortFunctions.length - 1](a,b,sortFunctions.length-1)); | |
}); | |
} | |
/// Search the list of students in SpeedGrader | |
if (settings.canvasSpeedgraderSearchStudents && matchingPage.canvasSpeedgrader) { | |
// console.log('canvasSpeedgraderSearchStudents'); | |
addCSS(` | |
#students_selectmenu-menu > li.hidden { | |
display: none; | |
} | |
#students_selectmenu-menu > input:first-child { | |
position: sticky; | |
top: 0; | |
width: 100%; | |
height: 2em; | |
z-index: 1; | |
} | |
`); | |
waitForElem('#students_selectmenu-menu').then(listElem => { | |
const searchField = document.createElement('input'); | |
searchField.placeholder = 'Sök...'; | |
searchField.addEventListener('mousedown', event => { | |
searchField.select(); | |
}); | |
searchField.addEventListener('keydown', event => { // Stop Canvas from capturing keys | |
event.stopImmediatePropagation(); | |
}); | |
searchField.addEventListener('input', event => { | |
const searchQuery = searchField.value.toLowerCase(); | |
qs('li', listElem).forEach((student) => { | |
const studentName = q('.ui-selectmenu-item-header', student).textContent.toLowerCase(); | |
student.classList.toggle('hidden', studentName.indexOf(searchQuery) < 0); | |
}); | |
}); | |
listElem.prepend(searchField); | |
}); | |
} | |
/// Sort the list of students in Adjust Quiz in a set order | |
if (settings.canvasAdjustQuizSortStudents) { | |
// console.log('canvasAdjustQuizSortStudents'); | |
if(matchingPage.canvasAdjustQuiz) { | |
let sortFunction = undefined; | |
switch(settings.canvasAdjustQuizSortStudents) { | |
case 'firstname': | |
sortFunction = (a, b) => { | |
const nameAText = q('td.name', a).textContent.replace(/\n( )+/g, ''); | |
const nameADivText = q('td.name div', a).textContent.replace(/\n( )+/g, ''); | |
const nameAList = nameAText.replace(nameADivText, '').split(',').map(val => val.replace(/(^( )+)|(( )+$)/g, '')); | |
const nameA = nameAList.length > 1 ? `${nameAList[1]}, ${nameAList[0]}` : nameAList[0]; | |
const nameBText = q('td.name', b).textContent.replace(/\n( )+/g, ''); | |
const nameBDivText = q('td.name div', b).textContent.replace(/\n( )+/g, ''); | |
const nameBList = nameBText.replace(nameBDivText, '').split(',').map(val => val.replace(/(^( )+)|(( )+$)/g, '')); | |
const nameB = nameBList.length > 1 ? `${nameBList[1]}, ${nameBList[0]}` : nameBList[0]; | |
return (nameA > nameB) ? 1 : (nameA < nameB) ? -1 : 0; | |
} | |
break; | |
case 'score': | |
sortFunction = (a, b) => { | |
const scoreA = Number(q('td.score_holder span', a).textContent.replace(/\n( )+/g, '')); | |
const scoreB = Number(q('td.score_holder span', b).textContent.replace(/\n( )+/g, '')); | |
return scoreB - scoreA; | |
} | |
break; | |
} | |
waitForElem('table#students tbody').then(listElem => { | |
if(sortFunction) sortChildren(listElem, sortFunction); | |
}); | |
} | |
else if(matchingPage.canvasAdjustQuizNew) { | |
if(matchingPage.param('jutools-moderate')) { | |
// console.log('jutools-moderate'); | |
let hasChanged = false; | |
handleMessages('iniframe', message => { | |
if(message !== 'load') return; | |
const frame = q('iframe.tool_launch'); | |
if(!frame) return; | |
const targetOrigin = 'https://ju.quiz-lti-dub-prod.instructure.com'; | |
frame.addEventListener('load', event => { | |
if(!hasChanged) { | |
try { | |
sendMessage(frame.contentWindow, targetOrigin, 'quiz-navigate', 'moderate'); | |
hasChanged = true; | |
} catch(error) { | |
console.error('JUTools unable to send message', error); | |
} | |
} | |
}); | |
}); | |
} | |
} | |
else if(matchingPage.canvasAdjustQuizNewIFrame) { | |
if(inIFrame) sendMessage(window.top, 'https://ju.instructure.com', 'iniframe', 'load'); | |
handleMessages('quiz-navigate', message => { | |
// console.log('Message from JU Tools, "quiz-navigate"', message); | |
switch(message) { | |
case 'moderate': { | |
const quizId = location.pathname.matchFirst(/(\d)+/); | |
location.href = `/moderation/${quizId}?jutools-filter=noattemptsleft`; | |
break; | |
} | |
} | |
}); | |
if(matchingPage.canvasAdjustQuizNewModerate) { | |
// console.log('Moderate'); | |
if(matchingPage.param('jutools-filter', 'noattemptsleft')) { | |
waitForAndClick( | |
'input[data-automation=moderate-progress-filter]', | |
'#progress-select-option-no-attempts-left' | |
); | |
} | |
waitForAndWatchElem('[data-automation="moderate-page"]', (moderatePage, mutations) => { | |
// Make sure that we have a table to sort | |
const listElem = q('table > tbody'); | |
if(!listElem) return; | |
let addedChildren = 0; | |
let removedChildren = 0; | |
mutations.forEach(mutation => { | |
addedChildren += mutation.addedNodes.length; | |
removedChildren += mutation.removedNodes.length; | |
}); | |
// If addedChildren === removedChildren, elements might have just been reordered | |
if(addedChildren === removedChildren && addedChildren === listElem.children.length) return; | |
// Make sure that at least one changed element is inside table body, otherwise changes are irrelevant | |
if (!mutations.find(mutation => mutation.target.closest('table > tbody') !== null)) return; | |
let sortFunction = undefined; | |
switch(settings.canvasAdjustQuizSortStudents) { | |
case 'score': | |
sortFunction = (a, b) => { | |
const scoreA = Number(q('td:nth-child(3) div:last-of-type', a).textContent.match(/\d+/) || -1); | |
const scoreB = Number(q('td:nth-child(3) div:last-of-type', b).textContent.match(/\d+/) || -1); | |
return scoreB - scoreA; | |
} | |
break; | |
} | |
if(sortFunction) sortChildren(listElem, sortFunction); | |
}); | |
} | |
} | |
} | |
/// Show settings page | |
if((settings.canvasShowJuToolsSettingsInSidebar && matchingPage.canvas) || matchingPage.settings) { | |
// console.log('jutools-settings'); | |
const settingsDialog = document.createElement('dialog'); | |
settingsDialog.innerHTML = ` | |
<form style="display: flex; flex-direction: column;"> | |
<h2 style="margin-top: 0;">Settings for JU Tools</h2> | |
<label> | |
<span>canvasShowJuToolsSettingsInSidebar</span> | |
<input type="checkbox" name="canvasShowJuToolsSettingsInSidebar" ${settings.canvasShowJuToolsSettingsInSidebar ? 'checked' : ''} /> | |
</label> | |
<label> | |
<span>canvasDocumentOpenButton</span> | |
<input type="checkbox" name="canvasDocumentOpenButton" ${settings.canvasDocumentOpenButton ? 'checked' : ''} /> | |
</label> | |
<label> | |
<span>canvasGradesStudentInsertion</span> | |
<input type="checkbox" name="canvasGradesStudentInsertion" ${settings.canvasGradesStudentInsertion ? 'checked' : ''} /> | |
</label> | |
<label> | |
<span>canvasSpeedgraderLRBtnAssignment</span> | |
<input type="checkbox" name="canvasSpeedgraderLRBtnAssignment" ${settings.canvasSpeedgraderLRBtnAssignment ? 'checked' : ''} /> | |
</label> | |
<label> | |
<span>canvasModulesShortcuts</span> | |
<input type="checkbox" name="canvasModulesShortcuts" ${settings.canvasModulesShortcuts ? 'checked' : ''} /> | |
</label> | |
<label> | |
<span>canvasSpeedgraderSearchStudents</span> | |
<input type="checkbox" name="canvasSpeedgraderSearchStudents" ${settings.canvasSpeedgraderSearchStudents ? 'checked' : ''} /> | |
</label> | |
<label> | |
<span>canvasSpeedgraderSortStudents</span> | |
<select name="canvasSpeedgraderSortStudents"> | |
${['firstname', 'default'].map((val) => { | |
return `<option value="${val}" ${settings.canvasSpeedgraderSortStudents === val ? 'selected' : ''}>${val}</option>`; | |
}).join('')} | |
</select> | |
</label> | |
<label> | |
<span>canvasSpeedgraderSortStudentsGradedLast</span> | |
<input type="checkbox" name="canvasSpeedgraderSortStudentsGradedLast" ${settings.canvasSpeedgraderSortStudentsGradedLast ? 'checked' : ''} /> | |
</label> | |
<label> | |
<span>canvasAdjustQuizSortStudents</span> | |
<select name="canvasAdjustQuizSortStudents"> | |
${['firstname', 'score', 'default'].map((val) => { | |
return `<option value="${val}" ${settings.canvasAdjustQuizSortStudents === val ? 'selected' : ''}>${val}</option>`; | |
}).join('')} | |
</select> | |
</label> | |
<span>Reload page after saving to apply settings</span> | |
<button type="submit">Save</button> | |
</form> | |
`; | |
q('form', settingsDialog).addEventListener('submit', event => { | |
event.preventDefault(); | |
settings.canvasShowJuToolsSettingsInSidebar = q('input[name=canvasShowJuToolsSettingsInSidebar]', settingsDialog).checked; | |
settings.canvasDocumentOpenButton = q('input[name=canvasDocumentOpenButton]', settingsDialog).checked; | |
settings.canvasGradesStudentInsertion = q('input[name=canvasGradesStudentInsertion]', settingsDialog).checked; | |
settings.canvasSpeedgraderLRBtnAssignment = q('input[name=canvasSpeedgraderLRBtnAssignment]', settingsDialog).checked; | |
settings.canvasModulesShortcuts = q('input[name=canvasModulesShortcuts]', settingsDialog).checked; | |
settings.canvasSpeedgraderSearchStudents = q('input[name=canvasSpeedgraderSearchStudents]', settingsDialog).checked; | |
settings.canvasSpeedgraderSortStudents = q('select[name=canvasSpeedgraderSortStudents]', settingsDialog).value; | |
settings.canvasSpeedgraderSortStudentsGradedLast = q('input[name=canvasSpeedgraderSortStudentsGradedLast]', settingsDialog).checked; | |
settings.canvasAdjustQuizSortStudents = q('select[name=canvasAdjustQuizSortStudents]', settingsDialog).value; | |
GM_setValue('jutools-settings', JSON.stringify(settings)); | |
settingsDialog.close(); | |
}); | |
document.addEventListener('click', event => { | |
if(event.target === settingsDialog) { | |
if (!clickIsInsideBoundaries(event, event.target.getBoundingClientRect())) { | |
event.target.close(); | |
} | |
} | |
}); | |
if(settings.canvasShowJuToolsSettingsInSidebar && matchingPage.canvas) { | |
waitForAndWatchElem('#nav-tray-portal', (navTrayDiv, mutations) => { | |
const sectionTabs = q('.navigation-tray-container.profile-tray ul', navTrayDiv); | |
if(sectionTabs && !q('#jutools-settings-button', sectionTabs)) { | |
const juToolsSettingsMenuItem = q('li', sectionTabs).cloneNode(true); | |
if(!q('a', juToolsSettingsMenuItem)) return; | |
q('a', juToolsSettingsMenuItem).id = 'jutools-settings-button'; | |
q('a', juToolsSettingsMenuItem).href = '?jutools-settings'; | |
q('a', juToolsSettingsMenuItem).textContent = 'JUTools Settings'; | |
q('a', juToolsSettingsMenuItem).addEventListener('click', event => { | |
event.preventDefault(); | |
settingsDialog.showModal(); | |
}); | |
sectionTabs.appendChild(juToolsSettingsMenuItem); | |
} | |
}); | |
} | |
waitForLoad().then(() => { | |
document.body.appendChild(settingsDialog); | |
if(matchingPage.settings) settingsDialog.showModal(); | |
}); | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment