userscripts/Compass QoL Enhancer.user.js

867 lines
33 KiB
JavaScript

// ==UserScript==
// @name Compass QoL Enhancer
// @namespace blankie-scripts
// @match http*://*.compass.education/*
// @version 1.30.0
// @author blankie
// @description A userscript that adds small but useful features for Compass, such as the ability to close windows by clicking on the background
// @inject-into page
// @run-at document-end
// ==/UserScript==
"use strict";
let qolTabOpened = false;
let qolLearningTaskOpened = false;
let modifyUrlHash = true;
let shownWindows = 0;
// popups should bot be blocked by the prioritised nav bar
let productNavBar = window.location.pathname !== "/ActionCentre/" ? document.querySelector("#productNavBar") : null;
// needed because .toString() adds a trailing = for empty values
URLSearchParams.prototype.laxToString = function() {
let out = "";
for (let [key, value] of this) {
if (out) {
out += "&";
}
// required for session/... or activity/...
out += encodeURIComponent(key).replaceAll("%2F", "/");
if (value) {
out += `=${encodeURIComponent(value)}`;
}
}
return out;
}
function getHashSearch() {
return new URLSearchParams(window.location.hash.substring(1));
}
function getExtClass(className) {
if (!unsafeWindow.Ext) {
return null;
}
return unsafeWindow.Ext.ClassManager.get(className);
}
function getPanelItemHash(panelId, isDefault) {
if (window.location.pathname === "/Organise/Activities/Activity.aspx") {
let query = getHashSearch();
if (panelId === "learningtasks") {
query.delete("qol_open_tab");
query.set("openLearningTaskTab", "");
} else if (isDefault) {
query.delete("qol_open_tab");
query.delete("openLearningTaskTab");
} else {
query.set("qol_open_tab", panelId);
query.delete("openLearningTaskTab");
}
query.delete("qol_learning_task");
return `#${query.laxToString()}`;
}
return `#${encodeURIComponent(panelId)}`;
}
// Prevent scrolling if a window is open, and put the nav bar over or behind masks when necessary
function handleWindowShow() {
shownWindows++;
document.documentElement.style.overflow = "hidden";
productNavBar.style.zIndex = "";
}
function handleWindowClose() {
shownWindows--;
if (shownWindows <= 0) {
document.documentElement.style.overflow = "";
productNavBar.style.zIndex = 20000;
}
}
let Window = getExtClass("Ext.window.Window");
if (Window) {
let original = Window.prototype.initComponent;
Window.prototype.initComponent = function() {
original.apply(this, arguments);
this.on("show", handleWindowShow);
this.on("close", handleWindowClose);
};
}
// example of message boxes: remove device on /Configure/LoginAndSecurity.aspx
// for some reason, listening on Ext.window.MessageBox does nothing
let Msg = unsafeWindow.Ext ? unsafeWindow.Ext.Msg : null;
if (Msg) {
let original = Msg.show;
Msg.show = function(options) {
let originalFn = options.fn;
options.fn = function() {
handleWindowClose();
if (originalFn) {
originalFn.apply(this, arguments);
}
};
handleWindowShow();
original.apply(this, arguments);
};
}
if (productNavBar) {
productNavBar.style.zIndex = 20000;
}
let UserProfileNewWidget = getExtClass("Compass.widgets.UserProfileNewWidget");
if (UserProfileNewWidget) {
// Prevent the dashboard tab in the user page being "dsh"
let original = UserProfileNewWidget.prototype.createItems;
UserProfileNewWidget.prototype.createItems = function() {
let res = original.apply(this, arguments);
delete res[0].listeners;
return res;
};
// Prevent clicking on a learning task from changing back to the Dashboard tab
UserProfileNewWidget.prototype.processHashChange = function() {};
}
function handleNewsItemClick(event) {
if (!this.moreButton.hidden && !event.target.closest("a, .newsfeed-image")) {
Compass.saveScrollPosition();
this.expandTruncated();
}
}
function handleNewsItem(newsItem) {
if (!newsItem.priority) {
return;
}
let img = document.createElement("img");
img.src = `${newsItem.m_AssetPath}Pix/16x16/flag_orange.png`;
img.classList.add("priority-flag");
img.dataset.qtip = "This news item has been flagged as important";
newsItem.newsItemDetails.el.dom.querySelector("div > span > div > span").append(img);
}
let NewsfeedItemWidget = getExtClass("Compass.widgets.NewsfeedItemWidget");
if (NewsfeedItemWidget) {
let originalRenderLayout = NewsfeedItemWidget.prototype.renderLayout;
NewsfeedItemWidget.prototype.renderLayout = function() {
originalRenderLayout.apply(this, arguments);
// prevent clicking on text from expanding the item since this disrupts selecting text, and we're gonna extend this to the entire item anyway
this.newsItemTextContainer.events.afterrender = true;
this.on("afterrender", function() {
// Expand news feed items by clicking on them
this.el.on("click", handleNewsItemClick.bind(this), this.el);
// Add important flag to priority news items
handleNewsItem(this);
});
}
// Highlight priority news items with red
let originalInitComponent = NewsfeedItemWidget.prototype.initComponent;
NewsfeedItemWidget.prototype.initComponent = function() {
if (this.priority) {
this.cls += " qol-priority-news-item";
}
originalInitComponent.apply(this, arguments);
};
let style = document.createElement("style");
style.textContent = `
.newLNF .newsfeed-newsItem.qol-priority-news-item {
background-color: rgb(255, 230, 230);
}
.newLNF .newsfeed-newsItem.qol-priority-news-item .fade {
background-image: -webkit-linear-gradient(top, transparent, rgb(255, 230, 230));
}
.qol-priority-news-item .priority-flag {
vertical-align: text-top;
margin-left: 0.25em;
}
`;
document.head.append(style);
}
// Make links in Looking Ahead actual links
let NewsfeedAlertItemWidget = getExtClass("Compass.widgets.NewsfeedAlertItemWidget");
if (NewsfeedAlertItemWidget) {
let original = NewsfeedAlertItemWidget.prototype.initComponent;
NewsfeedAlertItemWidget.prototype.initComponent = function() {
this.alertItem.Content = this.alertItem.Content.replaceAll(
/<a onclick="Compass\.postForm\('\/ActionCentre\/', { eventId:(\d+), attendeeId:(\d+)}\)"/gi,
"<a href=\"/Organise/Activities/Events/Event.aspx?eventId=$1&userId=$2\"",
);
original.apply(this, arguments);
};
}
let LearningTasksDetailsWidgetNew = getExtClass("Compass.widgets.LearningTasksDetailsWidgetNew");
if (LearningTasksDetailsWidgetNew) {
// Automatically open learning tasks specified in the URL
let originalFOLTOL = LearningTasksDetailsWidgetNew.prototype.forceOpenLearningTaskOnLoad;
LearningTasksDetailsWidgetNew.prototype.forceOpenLearningTaskOnLoad = function() {
// stole this from the function above
let toOpen = this.initialLoadComplete && !this.firstLoad;
let exit = !toOpen || this.openLearningTaskId || qolLearningTaskOpened;
originalFOLTOL.apply(this, arguments);
if (exit) {
return;
}
this.openUrlLearningTask();
qolLearningTaskOpened = true;
};
LearningTasksDetailsWidgetNew.prototype.openUrlLearningTask = function() {
let learningTask = parseInt(getHashSearch().get("qol_learning_task"), 10);
if (isNaN(learningTask)) {
return;
}
let learningTaskRecord = this.taskStore.findRecord("id", learningTask);
if (!learningTaskRecord) {
return;
}
this.launchLearningTasksSubmissionWidget(learningTaskRecord.data.gridRecordId, false);
};
// Show "Grades" and "Task" visibility for learning tasks
let originalCreateBasicTaskGrid = LearningTasksDetailsWidgetNew.prototype.createBasicTaskGrid;
LearningTasksDetailsWidgetNew.prototype.createBasicTaskGrid = function() {
let widget = this;
let taskGrid = originalCreateBasicTaskGrid.apply(this, arguments);
let originalRenderer = taskGrid.columns[0].renderer;
taskGrid.columns[0].renderer = function() {
let isStudentView = widget.isStudentView;
let isStudentOrParent = widget.isStudentOrParent;
widget.isStudentView = widget.isStudentOrParent = false;
let ret = originalRenderer.apply(widget, arguments);
widget.isStudentView = isStudentView;
widget.isStudentOrParent = isStudentOrParent;
return ret;
};
return taskGrid;
}
// Add refresh button to learning tasks
let originalInitTBar = LearningTasksDetailsWidgetNew.prototype.initTBar;
LearningTasksDetailsWidgetNew.prototype.initTBar = function() {
originalInitTBar.apply(this, arguments);
let widget = this;
let refreshButton = {
itemId: "refreshButton",
text: "Refresh",
handler: function() {
widget.taskStore.load();
}
};
this.tbar.splice(this.tbar.indexOf("->") + 1, 0, refreshButton);
};
}
// Automatically add (and remove) the currently open learning task to the URL
function handleLearningTaskShow() {
if (!modifyUrlHash) {
return;
}
let search = getHashSearch();
search.set("qol_learning_task", this.learningTask.data.id);
window.location.hash = `#${search.laxToString()}`;
}
function handleLearningTaskClose() {
if (!modifyUrlHash) {
return;
}
let search = getHashSearch();
search.delete("qol_learning_task");
window.location.hash = `#${search.laxToString()}`;
}
let LearningTasksSubmissionWidget = getExtClass("Compass.widgets.LearningTasksSubmissionWidget");
if (LearningTasksSubmissionWidget) {
let original = LearningTasksSubmissionWidget.prototype.initComponent;
LearningTasksSubmissionWidget.prototype.initComponent = function() {
original.apply(this, arguments);
this.on("show", handleLearningTaskShow.bind(this));
this.on("close", handleLearningTaskClose);
}
}
function updateInstanceButton(instanceDetails, instanceButton, offset) {
// Make previous/next session buttons links
let index = instanceDetails.instanceStore.indexOfId(instanceDetails.m_instanceId);
index += offset;
if (index >= 0 && index < instanceDetails.instanceStore.count()) {
let url = `#session/${instanceDetails.instanceStore.getAt(index).internalId}`;
instanceButton.el.dom.href = url;
} else {
instanceButton.el.dom.removeAttribute("href");
}
}
let InstanceDetailsWidget = getExtClass("Compass.widgets.InstanceDetailsWidget");
if (InstanceDetailsWidget) {
// recreation of updateURLHash to fix qol_learning_task being stripped
// example: /Organise/Activities/Activity.aspx#activity/67672&openLearningTaskTab&qol_learning_task=77334
InstanceDetailsWidget.prototype.updateURLHash = function(useInstanceId) {
let match = window.location.hash.match(/^#.*?[?&](.+)$/);
let hash = useInstanceId ? `session/${this.m_instanceId}` : `activity/${this.m_activityId}`;
if (match) {
hash += `&${match[1]}`;
}
window.location.hash = `#${hash}`;
};
let originalUpdateInstanceHeader = InstanceDetailsWidget.prototype.updateInstanceHeader;
InstanceDetailsWidget.prototype.updateInstanceHeader = function() {
originalUpdateInstanceHeader.apply(this, arguments);
let toolbar = this.getInstanceNavigatorToolbar();
updateInstanceButton(this, toolbar.getComponent("previousInstanceButton"), -1);
updateInstanceButton(this, toolbar.getComponent("nextInstanceButton"), 1);
};
let originalGetInstanceNavigatorToolbar = InstanceDetailsWidget.prototype.getInstanceNavigatorToolbar;
InstanceDetailsWidget.prototype.getInstanceNavigatorToolbar = function() {
if (this.instanceNavigatorToolbar) {
return this.instanceNavigatorToolbar;
}
let toolbar = originalGetInstanceNavigatorToolbar.apply(this, arguments);
let previousButton = toolbar.getComponent("previousInstanceButton");
let nextButton = toolbar.getComponent("nextInstanceButton");
previousButton.preventDefault = false;
let originalPreviousButtonHandler = previousButton.handler;
previousButton.handler = function(_, event) {
if (!event.ctrlKey) {
event.preventDefault();
originalPreviousButtonHandler.apply(this, arguments);
}
};
nextButton.preventDefault = false;
let originalNextButtonHandler = nextButton.handler;
nextButton.handler = function(_, event) {
if (!event.ctrlKey) {
event.preventDefault();
originalNextButtonHandler.apply(this, arguments);
}
};
return toolbar;
};
// not a typo :)
let originalGetCmbSelectIntance = InstanceDetailsWidget.prototype.getCmbSelectIntance;
InstanceDetailsWidget.prototype.getCmbSelectIntance = function() {
if (this.cmbSelectInstance) {
return this.cmbSelectInstance;
}
let combo = originalGetCmbSelectIntance.apply(this, arguments);
// Make sessions in the sessions dropdown links
combo.tpl.html = combo.tpl.html.replace(
/<div (.+?) style="(.+?)">(.+?)<\/div>/,
"<a href='#session/{id}' onclick='event.ctrlKey && event.stopImmediatePropagation()' $1 style='display: block; text-decoration: none; $2'>$3</a>"
);
return combo;
};
}
// Sort by name in wiki/resources
function isFolder(node) {
return node.data.type === unsafeWindow.Compass.enums.WikiNodeType.Folder;
}
function wikiNodeSort(lhs, rhs) {
if (isFolder(lhs) && !isFolder(rhs)) {
return -1;
} else if (!isFolder(lhs) && isFolder(rhs)) {
return 1;
}
let lhsName = lhs.data.name.toLowerCase();
let rhsName = rhs.data.name.toLowerCase();
if (lhsName < rhsName) {
return -1;
} else if (lhsName === rhsName) {
return 0;
} else {
return 1;
}
}
function wikiHandleSorting() {
// check if root node exists, otherwise pages like /Communicate/SchoolDocumentation.aspx will die
if (this.sort && this.treePanel.getRootNode()) {
this.treePanel.store.sort({sorterFn: wikiNodeSort});
} else if (!this.sort) {
this.treePanel.store.sorters.clear();
}
}
// Add "Enable/Disable sorting" button
function wikiAddSortButton() {
let refreshButton = this.treePanel.down("#refreshButton");
toolbar = refreshButton.up();
toolbar.remove("toggleSortButton");
toolbar.insert(toolbar.items.items.indexOf(refreshButton), {
xtype: "button",
itemId: "toggleSortButton",
text: `${this.sort ? "Disable" : "Enable"} Sorting`,
handler: () => {
this.sort = !this.sort;
wikiHandleSorting.call(this);
this.rebindTreePanel(true);
}
});
}
let WikiBrowserPanel = getExtClass("Compass.widgets.WikiBrowserPanel");
if (WikiBrowserPanel) {
let originalInitComponent = WikiBrowserPanel.prototype.initComponent;
WikiBrowserPanel.prototype.initComponent = function() {
this.sort = true;
this.on("treeDataLoaded", wikiHandleSorting);
originalInitComponent.apply(this, arguments);
}
let originalRenderLayout = WikiBrowserPanel.prototype.renderLayout;
WikiBrowserPanel.prototype.renderLayout = function() {
// Reveal filter and refresh toolbars
let readOnly = this.readOnly;
this.readOnly = false;
originalRenderLayout.apply(this, arguments);
this.readOnly = readOnly;
// hide tools button and context menu since it is useless to us
this.treePanel.down("#toolButton").hidden = true;
this.treePanel.events.itemcontextmenu = true;
wikiHandleSorting.call(this);
wikiAddSortButton.call(this);
};
let originalSelectedNodeChanged = WikiBrowserPanel.prototype.selectedNodeChanged;
WikiBrowserPanel.prototype.selectedNodeChanged = function() {
// Reveal "View Permissions", "Visible to", and "Created by"
let readOnly = this.readOnly;
this.readOnly = false;
originalSelectedNodeChanged.apply(this, arguments);
this.readOnly = readOnly;
// hide things we can't touch
let toolbar = this.nodeViewerPanel.down("toolbar");
for (let item of toolbar.items.items) {
if (item.disabled) {
toolbar.remove(item);
}
}
wikiAddSortButton.call(this);
};
let originalUpdateNodePermissions = WikiBrowserPanel.prototype.updateNodePermissions;
WikiBrowserPanel.prototype.updateNodePermissions = function() {
// prevent a network request from coming out to prevent arousing suspicion and to speed up things
let originalPostWithCallback = unsafeWindow.Compass.postWithCallback;
unsafeWindow.Compass.postWithCallback = function(url, data, callback) {
callback({d: false});
};
originalUpdateNodePermissions.apply(this, arguments);
unsafeWindow.Compass.postWithCallback = originalPostWithCallback;
};
}
function handleNewCalendarEvent(element, calendar) {
// Turn each calendar event into a link so that Link Hints can recognize it
let a = document.createElement("a");
for (let attr of element.attributes) {
a.setAttribute(attr.name, attr.value);
}
// fix learning tasks shrinking for some reason
a.style.display = "block";
// fix weird aboveline (underline but above the text) on hover
a.style.textDecoration = "none";
a.replaceChildren(...element.childNodes);
let calendarData = calendar.getEventRecordFromEl(element).data;
if (a.classList.contains("activity-type-1")) {
// Add a link for activities/"standard classes"
a.href = `/Organise/Activities/Activity.aspx?targetUserId=${calendarData.targetStudentId}#session/${calendarData.instanceId}`;
} else if (a.classList.contains("activity-type-2")) {
// Add a link for action centre events
a.href = `/Organise/Activities/Events/Event.aspx?eventId=${calendarData.activityId}`;
if (calendarData.targetStudentId) {
a.href += `&userId=${calendarData.targetStudentId}`;
}
} else if (a.classList.contains("activity-type-10")) {
// Add a link for learning tasks
let calendarPanel = calendar.ownerCalendarPanel;
a.href = "/Records/User.aspx"
if (calendarPanel.targetUserId !== undefined) {
a.href += `?userId=${calendarPanel.targetUserId}`;
}
// Link specifically to the learning task
a.href += `#learningTasks&qol_learning_task=${calendarData.learningTaskId}`;
// Hide user's last name on learning tasks
calendarData.longTitleWithoutTime = calendarData.longTitleWithoutTime.replace(/^.+?, /, "");
}
// Show the finish time for applicable calendar events
let startString = unsafeWindow.Ext.util.Format.date(calendarData.start, unsafeWindow.Compass.TIME_NO_PERIOD_FORMAT);
let finishString = unsafeWindow.Ext.util.Format.date(calendarData.finish, unsafeWindow.Compass.TIME_NO_PERIOD_FORMAT);
let textElement = a.querySelector(".ext-evt-bd") || a;
// yes, innerHTML. longTitleWithoutTime can apparently contain HTML. lets hope that startString and finishString don't
if (textElement.innerHTML.startsWith(`${startString}: `)) {
textElement.innerHTML = `${startString} - ${finishString}: ${calendarData.longTitleWithoutTime}`;
}
// prevent ctrl-clicking from changing the current tab
// it seems like the link thing actually effectively killed the default handler anyway
if (a.href) {
a.addEventListener("click", function(event) {
event.stopImmediatePropagation();
}, {passive: true});
}
element.replaceWith(a);
}
function handlePanel(panel) {
let tabToOpen = window.location.pathname === "/Organise/Activities/Activity.aspx"
? getHashSearch().get("qol_open_tab")
: (window.location.hash.replace(/^#(.+?)(?:&.*)?$/, "$1") || null);
for (let i = 0; i < panel.items.items.length; i++) {
handlePanelItem(panel, panel.items.items[i], i === 0, tabToOpen);
}
}
function handlePanelItem(panel, panelItem, isDefault, tabToOpen) {
let panelId = panelItem.itemId || panelItem.id;
// example of panel-<id>: /Organise/Subjects/Subject.aspx
// example of panelItem.title being undefined: /Communicate/SchoolDocumentation.aspx
if (panelId.startsWith("panel-") && panelItem.title) {
panelId = panelItem.title.toLowerCase().replaceAll(" ", "-");
}
let panelItemHash = getPanelItemHash(panelId, isDefault);
// Automatically open tab specified in fragment
if (panelId === tabToOpen) {
// if the sessions tab is automatically opened, the currently selected tab is still dashboard for some reason,
// hence this hook to open it when the dashboard finishes loading
if (!qolTabOpened && panel.setDashboardToLoading) {
let original = panel.setDashboardToLoading;
panel.setDashboardToLoading = function(loading) {
original.apply(this, arguments);
!loading && panel.setActiveTab(panelItem);
}
} else {
panel.setActiveTab(panelItem);
}
}
// /Communicate/SchoolDocumentation.aspx does not have tabs
if (!panelItem.tab) {
return;
}
panelItem.tab.preventDefault = false;
panelItem.tab.el.dom.href = panelItemHash;
panelItem.tab.el.dom.addEventListener("click", function(event) {
if (event.ctrlKey) {
event.stopImmediatePropagation();
return;
}
// prevent the browser from scrolling to the body
event.preventDefault();
if (modifyUrlHash && (window.location.pathname === "/Organise/Activities/Activity.aspx" || !window.location.hash.startsWith(`${panelItemHash}&`))) {
// Automatically add a reference to the tab when it is clicked
window.location.hash = panelItemHash;
}
});
}
function handleLearningTasksTable(element) {
// Make ctrl-clicking activities in a user's learning tasks tab no longer collapse everything else
for (let a of element.querySelectorAll(".x-grid-group-title a")) {
a.addEventListener("click", function(event) {
event.stopImmediatePropagation();
}, {passive: true});
}
// Add links to learning tasks themselves
let gridView = Ext.getCmp(element.parentElement.id);
for (let item of gridView.store.data.items) {
let data = item.data;
let a = element.querySelector(`[gridrecordid="${data.gridRecordId}"]`);
let query = getHashSearch();
query.set("qol_learning_task", data.id);
a.href = `#${query.laxToString()}`;
a.addEventListener("click", function(event) {
if (event.ctrlKey) {
event.stopImmediatePropagation();
} else {
event.preventDefault();
}
});
}
}
// Make files and links submitted to a learning task actual links
function handleLearningTaskSubmissionTable(element) {
let LearningTasksSubmissionFileType = unsafeWindow.Compass.enums.LearningTasksSubmissionFileType;
let taskId = unsafeWindow.Ext.getCmp(element.closest(".x-window").id).learningTask.data.id;
let items = unsafeWindow.Ext.getCmp(element.id).store.data.items;
let lastColumns = element.querySelectorAll(".x-grid-cell-last img");
for (let i = 0; i < items.length; i++) {
let item = items[i].data;
let img = lastColumns[i];
let url;
if (item.submissionFileType === LearningTasksSubmissionFileType.File) {
url = `/Services/FileDownload/FileRequestHandler?FileDownloadType=2&taskId=${taskId}&submissionId=${item.id}`;
} else if (item.submissionFileType === LearningTasksSubmissionFileType.SubmissionUrl) {
// Fix opening submitted links with ampersands
let parser = new DOMParser();
let document = parser.parseFromString(item.fileName, "text/html");
url = document.documentElement.textContent;
} else {
continue;
}
let a = document.createElement("a");
a.href = url;
a.addEventListener("click", function(event) {
event.stopImmediatePropagation();
}, {passive: true});
img.replaceWith(a);
a.append(img);
}
}
// Make permissions grid more visible
function handlePermissionsWindow(node) {
node.querySelector(".x-mask").remove();
node.querySelector(".x-grid-body").style.pointerEvents = "none";
}
function handleCKEditor(instance) {
instance.on("contentDom", function() {
let editable = instance.editable();
for (let element of editable.$.querySelectorAll("a")) {
handleCKEditorLink(element);
}
observer.observe(editable.$, {childList: true, subtree: true});
});
}
function handleCKEditorLink(element) {
// Make links inside lesson plans open in the parent instead of a new tab
if (element.target === "_parent") {
return;
}
element.target = "_parent";
element.addEventListener("click", function(event) {
event.stopImmediatePropagation();
}, {passive: true});
}
function handleNewNode(node, observer) {
if (node.nodeType !== 1) {
return;
}
if (node.parentElement && (node.classList.contains("ext-cal-hd-ct") || node.classList.contains("ext-cal-bg-tbl") || node.classList.contains("ext-cal-inner-ct") || node.classList.contains("ext-cal-mdv"))) {
let calendar = unsafeWindow.Ext.getCmp(node.closest(".x-component").id);
if (calendar.view) {
calendar = calendar.view;
}
for (let element of node.querySelectorAll("div.ext-cal-evt")) {
handleNewCalendarEvent(element, calendar);
}
} else if (!qolTabOpened && node.classList.contains("x-panel")) {
let topMostPanel = node;
while (topMostPanel.parentElement.closest(".x-panel")) {
topMostPanel = topMostPanel.parentElement.closest(".x-panel");
}
handlePanel(unsafeWindow.Ext.getCmp(topMostPanel.id));
qolTabOpened = true;
} else if (node.closest("[id^='wikibrowserpanel-'], #CompassWidgetsWikiBrowserPanel")) {
// Make files and folders in wiki/resources clickable for Link Hints
if (node.localName === "td") {
node.setAttribute("role", "button");
}
for (let element of node.querySelectorAll("td, .x-tree-expander")) {
element.setAttribute("role", "button");
}
} else if (node.localName === "table" && node.closest(".sel-learning-tasks-widget")) {
handleLearningTasksTable(node);
} else if (node.classList.contains("x-grid-view") && unsafeWindow.Ext.getCmp(node.id).up("#submissionsPanel")) {
handleLearningTaskSubmissionTable(node);
} else if (node.classList.contains("x-window")) {
let titleElement = node.querySelector(".x-window-header-text");
let title = titleElement ? titleElement.textContent : "";
if (title.startsWith("View Permissions for ")) {
handlePermissionsWindow(node);
}
}
if (node.classList.contains("cke")) {
let instance = unsafeWindow.CKEDITOR.instances[node.id.substring(4)];
handleCKEditor(instance);
} else if (node.localName === "a" && /\bcke_/.test(node.closest("body").className)) {
handleCKEditorLink(node);
}
}
let observer = new MutationObserver(function(mutations, observer) {
for (let mutation of mutations) {
if (mutation.type !== "childList") {
continue;
}
for (let node of mutation.addedNodes) {
handleNewNode(node, observer);
}
}
});
observer.observe(document.body, {childList: true, subtree: true});
document.body.addEventListener("click", function(event) {
if (event.target.classList.contains("x-mask") && event.target.parentElement.localName === "body") {
// Add the ability to close windows by clicking on the background
let maskZIndex = BigInt(event.target.style.zIndex);
for (let maskMsg of document.querySelectorAll(".x-mask-msg")) {
if (BigInt(maskMsg.style.zIndex) >= maskZIndex && maskMsg.style.display !== "none") {
return;
}
}
let closeButton = document.querySelector(".x-window-closable.x-window-active .x-tool-close");
if (closeButton) {
closeButton.click();
}
} else if (event.target.classList.contains("x-form-file-btn")) {
// Make Link Hints work with uploading files
event.target.querySelector("input[type=file]").click();
}
}, {passive: true});
// Allow submitting links by pressing Enter instead of clicking the button
document.body.addEventListener("keydown", function(event) {
if (event.key !== "Enter" || event.target.localName !== "input" || event.target.type !== "text") {
return;
}
let window = unsafeWindow.Ext.getCmp(event.target.closest(".x-window").id);
if (!window || window.itemId !== "urlSelectionModal") {
return;
}
window.down("#saveButton").handler();
}, {passive: true});
// Re-open panel tabs and learning tasks when the URL changes
function getLearningTaskWindow() {
let LearningTasksSubmissionWidget = getExtClass("Compass.widgets.LearningTasksSubmissionWidget");
for (let window of document.querySelectorAll(".x-window")) {
window = unsafeWindow.Ext.getCmp(window.id);
if (unsafeWindow.Ext.getClass(window) === LearningTasksSubmissionWidget) {
return window;
}
}
return null;
}
function getLearningTaskTab(panel) {
return panel.items.items.find(function(tab) {
return tab.itemId === "learningtasks";
});
}
window.addEventListener("hashchange", function(event) {
// workaround window.onhashchange being ignored by window.addEventListener("hashchange", ...)
if (window.onhashchange) {
window.onhashchange();
}
modifyUrlHash = false;
let isActivityPage = window.location.pathname === "/Organise/Activities/Activity.aspx";
let hashSearch = getHashSearch();
let panel = document.querySelector(".x-panel");
if (panel) {
panel = unsafeWindow.Ext.getCmp(panel.id);
if (isActivityPage && hashSearch.has("openLearningTaskTab")) {
panel.setActiveTab(getLearningTaskTab(panel));
} else if (hashSearch.size === 0 || (isActivityPage && hashSearch.size === 1)) {
event.stopImmediatePropagation();
panel.setActiveTab(panel.items.items[0]);
} else {
handlePanel(panel);
}
}
let learningTaskWindow = getLearningTaskWindow();
if (learningTaskWindow) {
learningTaskWindow.close();
}
let learningTaskWidget = document.querySelector(".sel-learning-tasks-widget");
if (learningTaskWidget) {
unsafeWindow.Ext.getCmp(learningTaskWidget.id).openUrlLearningTask();
}
modifyUrlHash = true;
});
// Stop the calendar and email buttons from opening in a new tab
for (let element of document.querySelectorAll("#productNavBar a[target='_blank']")) {
element.removeAttribute("target");
}
// Prevent clicking "Loading Class Items..." from reloading the current page
let loadingClassItems = document.querySelector(".toolbar-classes-loading");
if (loadingClassItems) {
loadingClassItems.addEventListener("click", function(event) {
event.preventDefault();
});
}
// Preload subjects and classes when the page loads
let teachingAndLearning = document.querySelector(".toolbar-clickable-teaching-and-learning");
if (teachingAndLearning) {
new MutationObserver(function(mutations, observer) {
observer.disconnect();
teachingAndLearning.dispatchEvent(new MouseEvent("mouseover"));
}).observe(teachingAndLearning, {attributes: true});
}
// unsafeWindow.CKEDITOR may not be set if you're on the login page, for example
if (unsafeWindow.CKEDITOR) {
// Suppress that annoying barebones context menu that only has Copy
unsafeWindow.CKEDITOR.plugins.load("contextmenu", function() {
unsafeWindow.CKEDITOR.plugins.contextMenu.prototype.addTarget = function() {};
});
// Workaround https://github.com/lydell/LinkHints/issues/86
CKEDITOR.dom.document.prototype.write = function(data) {
this.$.documentElement.innerHTML = data;
for (let script of this.$.documentElement.querySelectorAll("script")) {
// script.cloneNode() makes it not execute for some reason
let scriptClone = document.createElement("script");
for (let attr of script.attributes) {
scriptClone.setAttribute(attr.name, attr.value);
}
scriptClone.innerText = script.textContent;
script.replaceWith(scriptClone);
}
this.$.dispatchEvent(new Event("DOMContentLoaded"));
};
}
// Uncheck the Remember Me button by default
if (window.location.pathname === "/login.aspx") {
document.querySelector("#rememberMeChk").checked = false;
}