The Instructure Community will enter a read-only state on November 22, 2025 as we prepare to migrate to our new Community platform in early December.
Read our blog post for more info about this change.
Found this content helpful? Log in or sign up to leave a like!
Now this is not my idea... I found a git on gitHub that did exactly what I needed it to do the problem was that it was beefy(12k lines)(GitHub - sdjrice/msgobs: A JavaScript modification for the Canvas learning management system which a... ). So I forked it and was going to go to work on it, convert to plain JS modify, when I realized I had no idea WTH is going on. Seriously though, @sdjrice was on another level. So I scraped the project and decided to go in from scratch. I created a baby version of that. So it has a ton of stipulations.
One stipulation that will not disappear is that a course must be selected in order to use.
I tried to embed the proof of concept no luck so here is the link:
https://www.iorad.com/player/1618642/Add-observers-to-email
Now anywhere you might see a set of // // those belong to a custom CSS animation I can share it if anyone is interested its the pacMan that you see nest to the button. Or you can use the integrated loading animation in Canvas.
function addObserverEmailButton() {
const checkIfNull = async selector => {
while (document.querySelector(selector) === null) {
await new Promise(resolve => requestAnimationFrame(resolve));
}
return document.querySelector(selector);
};
checkIfNull("#compose-btn").then(() => {
if (
ENV.current_user_roles.includes("teacher") ||
ENV.current_user_roles.includes("admin")
) {
const delay = ms => new Promise(res => setTimeout(res, ms));
let conversationsNav = document.querySelector(
"div.ui-dialog-buttonpane.ui-widget-content.ui-helper-clearfix"
);
let parentButton = document.createElement("div");
let classesToAdd = [
"ui-button",
"ui-widget",
"ui-state-default",
"ui-corner-all",
"ui-button-text-only",
"includeObserver"
];
parentButton.classList.add(...classesToAdd);
parentButton.setAttribute(
"style",
"margin:0 2px; min-width: 110px; background-color:wheat;"
);
parentButton.innerHTML = "Include Observers";
conversationsNav.insertBefore(
parentButton,
conversationsNav.childNodes[1]
);
// //let pmLoader = document.createElement("div");
// //let classesToAddPM = ["gameLoader", "pacMan"];
// //pmLoader.classList.add(...classesToAddPM);
// //conversationsNav.insertBefore(pmLoader, conversationsNav.childNodes[2]);
conversationsNav.querySelector(".includeObserver").addEventListener(
"click",
() => {
let course = document.querySelector(
'.message-header-input > input[type="hidden"]'
);
let courseNum = course.value.split("_")[1];
let parentCollection = { data: [] };
let kiddosArr = [];
let emailParents = [];
if (course.value) {
//loading animation
// //document.querySelector(".gameLoader.pacMan").style.setProperty("--playState", "play");
//loading animation
const fetchObservees = `/api/v1/courses/${courseNum}/enrollments?type[]=ObserverEnrollment&per_page=100`;
function nextURL(linkTxt) {
if (linkTxt) {
let links = linkTxt.split(",");
let nextRegEx = new RegExp('^<(.*)>; rel="next"$');
for (let i = 0; i < links.length; i++) {
let matches = nextRegEx.exec(links[i]);
if (matches && matches[1]) {
//return right away
//console.log(matches[1]);
return matches[1];
}
}
} else {
return false;
}
}
//Next define your main call:
const OPTIONS = {
credentials: "same-origin",
headers: {
accept: "application/json"
},
timeout: 5000
};
async function main() {
const response = await fetch(fetchObservees, OPTIONS);
let data = await response.json();
let res = {
data: data,
ok: response.ok,
headers: response.headers
};
parentCollection.data = res.data;
await loop(res);
}
//And then your loop
async function loop(parents) {
if (nextURL(parents.headers.get("Link")) === undefined) {
return;
//otherwise keep going.
} else {
const RESPONSE = await fetch(
nextURL(parents.headers.get("Link")),
OPTIONS
);
let data = await RESPONSE.json();
let res = {
data: data,
ok: RESPONSE.ok,
headers: RESPONSE.headers
};
if (res.ok) {
//console.log(res.data);
parentCollection.data = parentCollection.data.concat(
res.data
);
//console.log(res.data);
//console.info(Object.keys(parents.data).length);
//console.info(Object.keys(parentCollection.data).length);
}
await loop(res);
//await loop(res);
//if you want to wait for it.
//You need to call it from within an async function.
}
}
//Now all you need to do is call the main function:
const waitForMom = async () => {
await main();
};
//or:
//await main();
//if you want to wait for it.
//You need to call it from within an async function !impotanto.
waitForMom()
.then(async () => {
//console.log(parentCollection.data);
document
.querySelectorAll(
'div.message-header-input input[name="recipients[]"]'
)
.forEach(kiddo => {
kiddosArr.push(kiddo.value);
});
//console.log(kiddosArr);
})
.then(async () => {
kiddosArr = kiddosArr.map(x => {
return parseInt(x, 10);
});
})
.then(async () => {
parentCollection.data.forEach(element => {
//console.log(element.associated_user_id);
if (kiddosArr.includes(element.associated_user_id)) {
emailParents.push([element.user_id, element.user.name]);
}
});
//}
})
.then(async () => {
await delay(1000);
if (emailParents.length > 0) {
//loading animation
// //document.querySelector(".gameLoader.pacMan").style.setProperty("--playState", "paused");
//loading animation
emailParents.forEach(parent => {
console.table(parent);
document.querySelectorAll(
".ac-token-list"
)[1].innerHTML += `<li class="ac-token" style="background-color:wheat;">
${parent[1]}
<a href="#" class="ac-token-remove-btn">
<i class="icon-x icon-messageRecipient--cancel"></i>
<span class="screenreader-only">
Remove recipient ${parent[1]}
</span>
</a>
<input type="hidden" name="recipients[]" value="${parent[0]}">
</li>`;
});
} else {
popUpError(
`( ⊙0⊙) - I am not seeing an observers associated. Sorry. - (⊙▂⊙ )`
);
}
});
} else {
popUpError(
`(×_×;)-In order to add parents you must select a course first. Re-fresh to try again. -(o。o;)`
);
}
},
{ once: true }
);
}
});
//---------------------------------------------------------------------------
const popUpError = msgTxt => {
const msgHolder = document.querySelector("#flash_message_holder");
const timeout = 9000;
const daMsg = (msgHolder.innerHTML = `<div role="alert" class="ic-flash-static ic-flash-error popUp">
<div class="ic-flash__icon" aria-hidden="true">
<i class="icon-warning"></i>
</div>
<h1>${msgTxt}</h1>
</div>`);
daMsg;
setTimeout(() => {
let popUp = document.querySelector(".popUp");
popUp.parentNode.removeChild(popUp);
}, timeout);
};
//---------------------------------------------------------------------------
}
addObserverEmailButton();
Edit: 17-12-19: modified script to pull in a 100 parents if there, as well as made a few more async calls for better error handling and lengthy forEach if >50 parents(merged classes & lectures)
Edit: 10-02-20: modified script to pull in all paginated pages if there is pagination now working in large courses like grade counselors w/400+ students and 1000+ parents
Very nice. I really want to get into some observer hacks soon, but I need time to focus and do a bit of mashing up of other's ideas. I always had a problem with that hack too, boss wanted it, I said it's more lines than every other hack we have combined and I won't support it. Sometimes we just have to weigh the benefits and support.
Are you using IIFE's to wrap each of your hacks to keep them out of the global scope?
I actually use a combination of promises and mutation observers. It might not be the best way to do it but it gets the job done.
const checkIfNull = async selector => {
while (document.querySelector(selector) === null) {
await new Promise(resolve => requestAnimationFrame(resolve));
}
return document.querySelector(selector);
};
checkIfNull("#compose-btn").then(() => {So I have a ton of functions that are sitting in limbo waiting on their promises(in this case the #compose-btn which is only found in the compose message canvas function). So, every one of my "hacks" waits for a URL recognization and then it waits again for a particular element to be present to be present and then the actual function runs. SO.... this "hack" on my end is sitting in two more asynchronous functions. I think
I might have answered the question
IDK...
Not quite. The IIFE, protects the code from the global scope, ensuring that none of your declarations and Canvas declarations are interfering with each other. It's pretty benign, but extremely recommended.
ie. what if Canvas is using const checkIfNull?
Ben Alman » Immediately-Invoked Function Expression (IIFE)
URL scoping with REGEX is a good idea, but I'm not seeing it in your example. I guess I'd recommend sharing that with your example code so that people don't have to try adding it into your example when they want to test it, with the added benefit of immediately knowing where in Canvas I need to go to test it.
Here's a short example of both. ccsd-canvas/course-add-people.js at master · robert-carroll/ccsd-canvas · GitHub
Ohh that is what you mean... So I don't use var at all in my code I only use let or const, so all of my variables are blocked scoped. I actually use const checkIfNull in all of my functions without the leakage of var. So it is not something I think about anymore. I thought the question dealt with invocation and performance.
Updated for pagination and loading animation. --Also put together new script for all new endpoints.
@jsimon3 have you looked at the new refactored inbox? We've been using Stephen Rice's script to include observers but that's broken with the changes now in beta. The classes and id's in the message dialog window now appear to be dynamically generated so all the components of the script that parse/insert objects into the DOM are now broken. We have an old version of Stephen's script in our custom JS, but I've been looking at the most recent version in Git. I think the elements Map (lines 51-62) can be updated using XPath but my JS skills aren't that strong and I haven't gotten that to work. Wondering what your thoughts are since you've worked with this script. I posted in the other thread but haven't seen a response over there and this update is scheduled to go to Production with the October 15 release.
Community helpTo interact with Panda Bot, our automated chatbot, you need to sign up or log in:
Sign inTo interact with Panda Bot, our automated chatbot, you need to sign up or log in:
Sign in