Skip to content

Instantly share code, notes, and snippets.

@GalvinGao
Created April 25, 2023 04:12
Show Gist options
  • Save GalvinGao/8860d3130c316f7abdf443a577383b41 to your computer and use it in GitHub Desktop.
Save GalvinGao/8860d3130c316f7abdf443a577383b41 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Canvas All Info
// @namespace https://theusaf.github.io
// @version 1.3.1
// @icon https://canvas.instructure.com/favicon.ico
// @copyright 2020-2021, Daniel Lau
// @license MIT
// @description Place all information on a single page (https://canvas.example.com/all or https://example.instructure.com/all)
// @author theusaf
// @include /^https:\/\/canvas\.[a-z0-9]*?\.[a-z]*?\/all\/?(\?.*)?$/
// @include /^https:\/\/[a-z0-9]*?\.instructure\.com\/all\/?(\?.*)?$/
// @grant none
// ==/UserScript==
/*
Note:
- This userscript uses public APIs accessed by canvas
- Gets class information
- Gets assignments
- Gets basic teacher information
- This userscript does not store or upload any information gathered by the script
- This userscript overwrites /all in canvas
- This userscript was originally developed for Oregon State University
*/
/* Useful Links (for use later?)
/api/v1/conversations?scope=inbox&filter_mode=and&include_private_conversation_enrollments=false
- Canvas mail
/api/v1/conversations/(mailbox_id)?include_participant_contexts=false&include_private_conversation_enrollments=false
- Specific mail
/courses/(class_id)/modules/items/assignment_info
- Module items
*/
/**
* log - logs info to console
* @param {*} str
* @param {...any} data
*/
const log = (str, ...data) => {
if (data.length > 0) {
console.log(`[CANVAS-ALL] ${str}`, data);
}
console.log(`[CANVAS-ALL] ${str}`);
};
/**
* load - Loads everything
*/
async function load() {
document.title = "Dashboard - All";
/**
* mainElement - the main application div
* iFrameLoader - The div for loading iframes for getting data
* styles - A style element
*/
const mainElement = document.getElementById("main"),
iFrameLoader = document.createElement("div");
mainElement.innerHTML = `<style>
#canvas-all-iframe-loader{
visibility: hidden;
position: fixed;
width: 100%;
height: 100%;
pointer-events: none;
left: 0;
}
#canvas-all-iframe-loader > iframe{
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
#main{
display: flex;
flex-flow: column;
padding: 1rem;
}
#main>span{
flex: 0;
margin-bottom: 1rem;
}
#main>div{
flex: 1;
}
#canvasall_main_wrapper{
display: flex;
}
#canvasall_main_wrapper>div{
flex: 75%;
}
#canvasall_class_grades{
display: flex;
flex-flow: column;
background: #111;
padding: 0.5rem;
border-radius: 0.5rem;
}
#canvasall_assignments_wrapper{
padding: 0.5rem;
background: #111;
border-radius: 0.5rem;
}
#canvasall_main_wrapper>#canvasall_announcement_wrapper{
flex: 25%;
padding: 0.5rem;
}
#canvasall_assignment_filter_list_chosen{
display: inline-block;
}
#canvasall_assignment_filter_list_chosen>option{
display: inline-block;
background: grey;
color: white;
padding: 0.25rem;
margin: 0.25rem;
cursor: pointer;
}
#canvasall_assignment_filter_list_chosen>option::before{
content: "x ";
}
.canvasall_announcement_wrapper{
margin-bottom: 0.5rem;
padding: 0.5rem;
border-radius: 0.5rem;
}
.canvasall_announcement_wrapper:nth-child(2n+1){
background: rgba(255,255,255,.1);
}
.canvasall_announcement_wrapper:nth-child(2n){
background: #111;
}
.canvasall_announcement_title{
font-size: 1.25rem;
font-weight: bold;
}
.canvasall_announcement_class,
.canvasall_announcement_when{
font-size: 0.75rem;
}
.canvasall_announcement_class>a{
color: grey;
}
.canvasall_class_grade_wrapper:nth-child(2n+1){
background: rgba(0,0,0,0.5);
}
.canvasall_class_grade_wrapper:nth-child(2n){
background: rgba(255,255,255,.1);
}
.canvasall_class_grade_wrapper{
flex: 1;
display: flex;
border-radius: 0.5rem;
}
.canvasall_class_grade_wrapper>div{
flex: 1;
padding: 0.5rem;
word-break: break-all;
}
.canvasall_assignment_wrapper:nth-child(2n+1){
background: transparent;
}
.canvasall_assignment_wrapper:nth-child(2n){
background: rgba(255,255,255,.1);
}
.canvasall_assignment_wrapper{
display: flex;
padding: 0.5rem;
border-radius: 0.5rem;
}
.canvasall_assignment_wrapper>div{
flex: 1;
padding-right: 0.25rem;
padding-left: 0.25rem;
}
.canvasall_assignment_title{
display: flex;
align-items: center;
}
.canvasall_assignment_title>img{
height: 1.5rem;
width: 1.5rem;
margin-right: 0.5rem;
}
.canvasall_assignment_icon {
min-height: 1.5rem;
min-width: 1.5rem;
margin-right: 0.5rem;
}
.canvasall_assignment_type_assignment:before {
content: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgeD0iMCIgeT0iMCIgdmlld0JveD0iMCAwIDI4MCAyNTkiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDI4MCAyNTkiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwYXRoIGQ9Ik03My4zMSwxOThjLTExLjkzLDAtMjIuMjIsOC0yNCwxOC43M2EyNi42NywyNi42NywwLDAsMC0uMywzLjYzdi4zYTIyLDIyLDAsMCwwLDUuNDQsMTQuNjUsMjIuNDcsMjIuNDcsMCwwLDAsMTcuMjIsOEgyMDBWMjI4LjE5aC0xMzRWMjEzLjA4SDIwMFYxOThabTIxLTEwNS43NGg5MC42NFY2Mkg5NC4zWk03OS4xOSwxMDcuMzRWNDYuOTJIMjAwdjYwLjQyWm03LjU1LDMwLjIxVjEyMi40NUgxOTIuNDl2MTUuMTFaTTcxLjY1LDE2LjcxQTIyLjcyLDIyLjcyLDAsMCwwLDQ5LDM5LjM2VjE5MC44OGE0MS4xMiw0MS4xMiwwLDAsMSwyNC4zMi04aDE1N1YxNi43MVpNMzMuODgsMzkuMzZBMzcuNzgsMzcuNzgsMCwwLDEsNzEuNjUsMS42SDI0NS4zNlYxOThIMjE1LjE1djQ1LjMyaDIyLjY2VjI1OC40SDcxLjY1YTM3Ljg1LDM3Ljg1LDAsMCwxLTM3Ljc2LTM3Ljc2WiI+PC9wYXRoPjwvc3ZnPgo=");
filter: invert(1);
}
.canvasall_assignment_type_quiz:before {
content: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgeD0iMCIgeT0iMCIgdmlld0JveD0iMCAwIDI4MCAyODAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDI4MCAyODAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwYXRoIGQ9Ik05MS43MiwxMjAuNzVoOTYuNTZWMTA0LjY1SDkxLjcyWm0wLDQ4LjI4aDgwLjQ3VjE1Mi45NEg5MS43MlptMC05Ni41Nmg4MC40N1Y1Ni4zN0g5MS43MlptMTYwLjk0LDM0Ljg4SDIyOC41MlYxMC43OGgtMTc3djk2LjU2SDI3LjM0QTI0LjE3LDI0LjE3LDAsMCwwLDMuMiwxMzEuNDhWMjQ0LjE0YTI0LjE3LDI0LjE3LDAsMCwwLDI0LjE0LDI0LjE0SDI1Mi42NmEyNC4xNywyNC4xNywwLDAsMCwyNC4xNC0yNC4xNFYxMzEuNDhBMjQuMTcsMjQuMTcsMCwwLDAsMjUyLjY2LDEwNy4zNFptMCwxNi4wOWE4LjA2LDguMDYsMCwwLDEsOCw4djUxLjc3bC0zMi4xOSwxOS4zMVYxMjMuNDRaTTY3LjU4LDIwMy45MXYtMTc3SDIxMi40MnYxNzdaTTI3LjM0LDEyMy40NEg1MS40OHY3OS4xM0wxOS4yOSwxODMuMjZWMTMxLjQ4QTguMDYsOC4wNiwwLDAsMSwyNy4zNCwxMjMuNDRaTTI1Mi42NiwyNTIuMTlIMjcuMzRhOC4wNiw4LjA2LDAsMCwxLTgtOFYyMDJsMzAsMThIMjMwLjc1bDMwLTE4djQyLjEyQTguMDYsOC4wNiwwLDAsMSwyNTIuNjYsMjUyLjE5WiI+PC9wYXRoPjwvc3ZnPgo=");
filter: invert(1);
}
.canvasall_assignment_type_discussion_topic:before {
content: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMWVtIiBoZWlnaHQ9IjFlbSIgcm90YXRlPSIwIiBzdHlsZT0iaGVpZ2h0OjFlbTt3aWR0aDoxZW0iIHZpZXdCb3g9IjAgMCAxOTIwIDE5MjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnPgo8cGF0aCBkPSJtNjc3LjY1IDE2djMzOC45NGgxMTIuOTR2LTIyNS44OGgxMDE2LjV2NzkwLjQ4aC0yMjZ2MjU5Ljc2bC0yNTkuNjUtMjU5Ljc2aC03OS4xNzJ2LTQ1MS42NWgtNTY0LjU5LTY3Ny42NXYxMDE2LjVoMzM4LjcxdjQxOC45bDQxOC00MTguOWg0ODUuNTN2LTQ1MS44OGgzMi43NTNsNDE5LjEyIDQxOS4xMnYtNDE5LjEyaDIyNS44OHYtMTAxNi41aC0xMjQyLjR6bS0zMzguODYgOTAzLjU2aDU2NC43MXYtMTEyLjk0aC01NjQuNzF2MTEyLjk0em0wIDIyNS44OGgzMzguOTR2LTExMy4wNWgtMzM4Ljk0djExMy4wNXptLTIyNS44NS01NjQuNzRoMTAxNi41djc5MC43aC00MTkuMDFsLTI1OC43NSAyNTkuNjV2LTI1OS42NWgtMzM4Ljcxdi03OTAuN3oiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPgo8L2c+Cjwvc3ZnPgo=")
filter: invert(1);
}
.canvasall_status_submit,
.canvasall_status_submit a{
color: green;
}
.canvasall_status_done{
text-decoration: line-through;
}
.canvasall_status_late{
background: orange !important;
color: white;
}
.canvasall_status_late a{
color: white;
}
.canvasall_status_missing{
background: red !important;
color: white;
}
.canvasall_status_missing a{
color: white;
}
.canvasall_status_feedback>.canvasall_assignment_title::after{
content: " (Feedback available)"
}
</style>
<span id="canvasall_fetching_information">Fetching information... please wait...</span>
<div id="canvasall_main_wrapper">
<div>
<div id="canvasall_class_wrapper">
<!-- Courses, Grades, etc. -->
<h3>Courses</h3>
<div id="canvasall_class_grades">
<div id="canvasall_class_grade_header" class="canvasall_class_grade_wrapper">
<div><span>Class</span></div>
<div><span>Grade</span></div>
<div><span>Professor</span></div>
</div>
</div>
</div>
<h3>Current Assignments</h3>
<div>
<span>Filters:</span>
<select id="canvasall_assignment_filter_status">
<option value="">Select</option>
<option value="submit">Hide Submitted</option>
<option value="done">Hide Graded</option>
<option value="late">Hide Late</option>
<option value="missing">Hide Missing</option>
<option value="quiz">Hide Quiz</option>
<option value="assignment">Hide Assignment</option>
<option value="calendar_event">Hide Calendar Event</option>
</select>
<div id="canvasall_assignment_filter_list_chosen">
</div>
</div>
<div id="canvasall_assignments_wrapper">
<div class="canvasall_assignment_wrapper">
<div>
<span>Assignment Name</span>
</div>
<div>
<span>Class</span>
</div>
<div>
<span>Due Date</span>
</div>
</div>
</div>
</div>
<div id="canvasall_announcement_wrapper">
<h3>Announcements</h3>
</div>
</div>`;
iFrameLoader.id = "canvas-all-iframe-loader";
mainElement.append(iFrameLoader);
/**
* courses - Class information
* classAssignments - Assignments for courses
* mainFrame - The main iframe
* ENV - Global variables with useful data
* currentUserID - The current user id
*/
log("Getting courses");
const courses = await getCourses(),
courseAssignments = [],
courseGrades = {},
{ ENV } = window,
{ currentUserID } = ENV,
courseGradeDiv = mainElement.querySelector("#canvasall_class_grades"),
assignmentsDiv = mainElement.querySelector("#canvasall_assignments_wrapper"),
announcementsDiv = mainElement.querySelector(
"#canvasall_announcement_wrapper"
),
statusFilter = mainElement.querySelector(
"#canvasall_assignment_filter_status"
),
statusFilterList = mainElement.querySelector(
"#canvasall_assignment_filter_list_chosen"
),
localStorageConfigStr = localStorage.canvasAllConfig ?? "{}",
localStorageConfig = JSON.parse(localStorageConfigStr);
function hideType(value) {
if (value === "") {
// ignore reset to empty value
return;
}
const elem = statusFilter.querySelector(
`option[value="${value}"]`
),
temp = document.createElement("template"),
now = `${Math.random()}`.substr(2);
if (!elem) {return;}
temp.innerHTML = `<style id="canvasall_filter_${now}">
.canvasall_status_${value}{
display: none;
}
</style>`;
document.body.append(temp.content.cloneNode(true));
statusFilterList.append(elem);
localStorageConfig[value] = true;
localStorage.canvasAllConfig = JSON.stringify(localStorageConfig);
statusFilter.value = "";
function click() {
localStorageConfig[value] = false;
localStorage.canvasAllConfig = JSON.stringify(localStorageConfig);
// remove style
statusFilter.append(elem);
document.querySelector(`#canvasall_filter_${now}`).remove();
statusFilter.value = "";
elem.removeEventListener("click", click);
}
elem.addEventListener("click", click);
}
// Filters
statusFilter.addEventListener("change", () => {
hideType(statusFilter.value);
});
// Begin writing class information
for (const [i, course] of Object.entries(courses)) {
const template = document.createElement("template");
template.innerHTML = `<div class="canvasall_class_grade_wrapper" canvasall-class-id="${
course.id
}">
<div class="canvasall_class_grade_name">
<span>${+i + 1}. <a href="/courses/${course.id}">${
course.name
}</a></span>
</div>
<div class="canvasall_class_grade_score">
<span>(Loading scores...)</span>
</div>
<div class="canvasall_class_grade_instructor">
<span>(Loading instructors...)</span>
</div>
</div>`;
courseGradeDiv.append(template.content.cloneNode(true));
}
log("Getting course assignments and grades");
let courseCount = 0,
finishedCollections = 0;
for (const course of courses) {
courseCount++;
getCourseAssignments(course.id, currentUserID).then(assignments => {
courseAssignments.push.apply(
courseAssignments,
assignments
);
finishedCollections++;
if (finishedCollections === courseCount) {
// Write assignments
courseAssignments.sort((a, b) => {
// sort by "due date"
// also places non-due-date at end. (+1 week)
const dueA = new Date(
a.plannable.due_at || b.plannable.created_at || Date.now() + 604800e3
),
dueB = new Date(
b.plannable.due_at || b.plannable.created_at || Date.now() + 604800e3
);
return dueA.getTime() - dueB.getTime();
});
log("Compiling assignments");
for (const assignment of courseAssignments) {
const template = document.createElement("template");
if (assignment.plannable_type === "announcement") {
// Put in announcement thing
template.innerHTML = `<div class="canvasall_announcement_wrapper">
<div class="canvasall_announcement_title">
<a href="${assignment.html_url}">${assignment.plannable.title}</a>
</div>
<div class="canvasall_announcement_class">
<a href="/courses/${assignment.course_id}">${
assignment.context_name
}</a>
</div>
<div class="canvasall_announcement_when">
<span>${new Date(assignment.plannable.created_at)
.toString()
.split(" ")
.slice(0, 5)
.join(" ")
.replace(/:\d{2}$/, "")
.replace(/\s(?=\w{3}\s\d{2})/, ", ")
.replace(/\d{4}/, "at")}</span>
</div>
</div>`;
announcementsDiv.append(template.content.cloneNode(true));
continue;
}
const divClasses = [],
{ submissions } = assignment;
if (submissions) {
const { excused, graded, has_feedback, late, missing, submitted } =
submissions;
if (excused || graded) {
divClasses.push("canvasall_status_done");
}
if (submitted) {
divClasses.push("canvasall_status_submit");
}
if (late) {
divClasses.push("canvasall_status_late");
}
if (missing) {
divClasses.push("canvasall_status_missing");
}
if (has_feedback) {
divClasses.push("canvasall_status_feedback");
}
}
divClasses.push("canvasall_status_" + assignment.plannable_type);
template.innerHTML = `<div class="canvasall_assignment_wrapper ${divClasses.join(
" "
)}" class-id="${assignment.course_id}">
<div class="canvasall_assignment_title">
<div class="canvasall_assignment_icon canvasall_assignment_type_${
assignment.plannable_type
}"></div>
<a href="${assignment.html_url}">${assignment.plannable.title}</a>
</div>
<div class="canvasall_assignment_class">
<a href="/courses/${assignment.course_id}">${
assignment.context_name
}</a>
</div>
<div class="canvasall_assignment_due">
<span>${
assignment.plannable.due_at
? new Date(assignment.plannable.due_at)
.toString()
.split(" ")
.slice(0, 5)
.join(" ")
.replace(/:\d{2}$/, "")
.replace(/\s(?=\w{3}\s\d{2})/, ", ")
.replace(/\d{4}/, "at")
: "No due date"
}</span>
</div>
</div>`;
assignmentsDiv.append(template.content.cloneNode(true));
}
// Restore hide
for (const [key, value] of Object.entries(localStorageConfig)) {
if (value && key !== "") {
hideType(key);
}
}
courseAssignments.splice(0); // Free up memory
document.querySelector("#canvasall_fetching_information").outerHTML = "";
}
});
// Get grades
loadFrame(iFrameLoader, `/courses/${course.id}/grades`).then(
(ClassGradeFrame) => {
const { document: d, window: w, frame } = ClassGradeFrame;
let overallGrade = d
.querySelector(
"#student-grades-right-content .student_assignment.final_grade > .grade"
)
?.textContent?.replace(/%|\s/g, ""),
titles = d.querySelectorAll(".title"),
possiblePoints = d.querySelectorAll(".points_possible"),
dueDates = d.querySelectorAll(".due");
courseGrades[course.id] = {
Grades: overallGrade + "%",
Titles: titles,
PossiblePoints: possiblePoints,
DueDates: dueDates,
};
// Write grades
const gradeDiv = courseGradeDiv.querySelector(
`[canvasall-class-id="${course.id}"] > .canvasall_class_grade_score`
),
{ ENV } = w,
{ grading_scheme } = ENV,
[Letter] =
grading_scheme.find((scheme) => {
try {
return +overallGrade / 100 >= scheme[1];
} catch (e) {
console.error(e);
}
}) || "?";
gradeDiv.innerHTML =
overallGrade !== "N/A"
? `<span>${Letter} (${overallGrade}%)</span>`
: `<span>N/A</span>`;
// Done using data from iframe, attempt to clean up memory usage
overallGrade = titles = possiblePoints = dueDates = null;
for (const i in courseGrades) {
delete courseGrades[i];
}
w.location = "about:blank";
setTimeout(() => {
frame.remove();
}, 500);
}
);
// Get instructors
getCourseTeacher(course.id)
.then((teacher) => {
const teacherDiv = courseGradeDiv.querySelector(
`[canvasall-class-id="${course.id}"] > .canvasall_class_grade_instructor`
);
teacherDiv.innerHTML = `<span>${teacher.name}</span>`;
})
.catch(() => {
// no teacher, or failed to get teacher
const teacherDiv = courseGradeDiv.querySelector(
`[canvasall-class-id="${course.id}"] > .canvasall_class_grade_instructor`
);
teacherDiv.innerHTML = "<span>No instructor found.</span>";
});
}
}
// /api/v1/users/:user_id/communication_channels
/**
* getCourses - Gets all the classes of the user
*
* @returns {Promise<Array>} A list of class information
*/
function getCourses() {
return new Promise((res, rej) => {
const x = new XMLHttpRequest();
x.open(
"GET",
"/api/v1/users/self/favorites/courses?include[]=term&exclude[]=enrollments&sort=nickname"
);
x.setRequestHeader("X-Requested-With", "XMLHttpRequest");
x.setRequestHeader(
"accept",
"application/json, text/javascript, application/json+canvas-string-ids, */*; q=0.01"
);
x.onload = () => {
res(JSON.parse(x.responseText));
};
x.onerror = () => {
rej();
};
x.send();
});
}
/**
* GetClassAssignments - Gets the class assignments
*
* @param {String} courseID The class id
* @param {String} userID The user id
* @returns {Promise<Array>} The list of assignments
*/
function getCourseAssignments(courseID, userID) {
const x = new XMLHttpRequest(),
now = new Date(),
offset = now.getTimezoneOffset() / 60,
nextUrlRegex = /[^<>]*(?=>; rel="next")/;
now.addDays(-14); // To get assignments from 2 weeks ago
now.setHours(8 - offset);
now.setMinutes(0);
now.setSeconds(0);
now.setMilliseconds(0);
function makeHttpRequest(url) {
return new Promise((res, rej) => {
x.open("GET", url);
x.setRequestHeader("X-Requested-With", "XMLHttpRequest");
x.setRequestHeader(
"accept",
"application/json+canvas-string-ids, application/json+canvas-string-ids, application/json, text/plain, */*"
);
x.onerror = () => {rej();}
x.onload = () => {
res(x);
}
x.send();
});
}
async function gatherRecursiveRequests(url, list = []) {
const response = await makeHttpRequest(url),
data = JSON.parse(response.responseText),
linkHeader = response.getResponseHeader("link");
list.push(...data);
if (linkHeader) {
const lastLink = linkHeader.match(nextUrlRegex);
if (lastLink) {
return gatherRecursiveRequests(lastLink[0], list);
} else {
return list;
}
} else {
return list;
}
}
return gatherRecursiveRequests(`/api/v1/planner/items?start_date=${now.toISOString()}&order=asc&context_codes[]=course_${courseID}`) //&context_codes[]=user_${userID}
.catch(() => []);
}
/**
* getCourseTeacher - Gets the teacher of the course
*
* @param {String} courseID The class id
* @returns {Object} The teacher
*/
function getCourseTeacher(courseID) {
return new Promise((res, rej) => {
const x = new XMLHttpRequest();
x.open(
"GET",
`/api/v1/search/recipients?search=&per_page=20&permissions[]=send_messages_all&messageable_only=true&synthetic_contexts=true&context=course_${courseID}_teachers`
);
x.setRequestHeader("X-Requested-With", "XMLHttpRequest");
x.setRequestHeader(
"Accept",
"application/json, text/javascript, application/json+canvas-string-ids, */*; q=0.01"
);
x.onload = () => {
res(JSON.parse(x.responseText)[0]);
};
x.onerror = () => {
rej();
};
x.send();
});
}
/**
* loadFrame - Loads an iframe
*
* @param {HTMLElement} iFrameLoader The element to append iframes to
* @param {String} src The iframe url to load
* @returns {Promise<Object>} returns an object with the "window" and "document" of the iframe
*/
function loadFrame(iFrameLoader, src) {
return new Promise((res) => {
const mainFrame = document.createElement("iframe");
mainFrame.src = src || "/";
iFrameLoader.append(mainFrame);
mainFrame.addEventListener("load", () => {
const frameContext = {
document: mainFrame.contentDocument,
window: mainFrame.contentWindow,
frame: mainFrame
};
res(frameContext);
});
});
}
load();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment