diff --git a/Compass QoL Enhancer.user.js b/Compass QoL Enhancer.user.js index ae5b0ca..d17eb3d 100644 --- a/Compass QoL Enhancer.user.js +++ b/Compass QoL Enhancer.user.js @@ -2,7 +2,7 @@ // @name Compass QoL Enhancer // @namespace blankie-scripts // @match http*://*.compass.education/* -// @version 1.12.0 +// @version 1.13.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 @@ -12,13 +12,12 @@ "use strict"; let qolTabOpened = false; -// we make a copy of window.location.hash because the dashboard tab on a user's page would set the fragment to #dsh for some reason -let hashCopy = window.location.hash; +let qolLearningTaskOpened = false; // needed because .toString() adds a trailing = for empty values -function serializeURLSearchParams(query) { +URLSearchParams.prototype.laxToString = function() { let out = ""; - for (let [key, value] of query) { + for (let [key, value] of this) { if (out) { out += "&"; } @@ -30,9 +29,18 @@ function serializeURLSearchParams(query) { } 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 = new URLSearchParams(window.location.hash.substring(1)); + let query = getHashSearch(); if (panelId === "learningtasks") { query.delete("qol_open_tab"); query.set("openLearningTaskTab", ""); @@ -43,12 +51,52 @@ function getPanelItemHash(panelId, isDefault) { query.set("qol_open_tab", panelId); query.delete("openLearningTaskTab"); } - return `#${serializeURLSearchParams(query)}`; + query.delete("qol_learning_task"); + return `#${query.laxToString()}`; } return `#${encodeURIComponent(panelId)}`; } + + +// Prevent the dashboard tab in the user page being "dsh" +let UserProfileNewWidget = getExtClass("Compass.widgets.UserProfileNewWidget"); +if (UserProfileNewWidget) { + let original = UserProfileNewWidget.prototype.createItems; + UserProfileNewWidget.prototype.createItems = function() { + let res = original.apply(this, arguments); + delete res[0].listeners; + return res; + }; +} + +// Automatically open learning tasks specified in the URL +let LearningTasksDetailsWidgetNew = getExtClass("Compass.widgets.LearningTasksDetailsWidgetNew"); +if (LearningTasksDetailsWidgetNew) { + let original = 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; + original.apply(this, arguments); + if (exit) { + return; + } + + qolLearningTaskOpened = true; + 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); + }; +} + function updateInstanceButton(instanceDetails, instanceButton, offset) { // Make previous/next session buttons links let index = instanceDetails.instanceStore.indexOfId(instanceDetails.m_instanceId); @@ -60,42 +108,64 @@ function updateInstanceButton(instanceDetails, instanceButton, offset) { instanceButton.el.dom.removeAttribute("href"); } } - -function handleInstanceButtonClick(event) { - if (event.ctrlKey) { - event.stopImmediatePropagation(); - } -} - -function handleActivityManager(element) { - let instanceDetails = unsafeWindow.Ext.getCmp(element.id).m_InstanceDetailsWidget; - let instanceNavigatorToolbar = instanceDetails.getInstanceNavigatorToolbar(); - let previousInstanceButton = instanceNavigatorToolbar.getComponent("previousInstanceButton"); - let nextInstanceButton = instanceNavigatorToolbar.getComponent("nextInstanceButton"); - let comboSelectInstance = instanceDetails.getCmbSelectIntance(); // not a typo :) - - let realUpdateInstanceHeader = instanceDetails.updateInstanceHeader; - instanceDetails.updateInstanceHeader = function() { - realUpdateInstanceHeader.apply(this, arguments); - updateInstanceButton(instanceDetails, previousInstanceButton, -1); - updateInstanceButton(instanceDetails, nextInstanceButton, 1); +let InstanceDetailsWidget = getExtClass("Compass.widgets.InstanceDetailsWidget"); +if (InstanceDetailsWidget) { + 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); }; - for (let button of [previousInstanceButton, nextInstanceButton]) { - button.el.dom.addEventListener("click", handleInstanceButtonClick, {passive: true}); - // move all previous handlers back to the front - // new relic's browser agent would've borked this, but it did not?? - // i'll take what i can get i guess - // (note to self: unsafeWindow.NREUM = true if this breaks) - for (let handler of unsafeWindow.Ext.EventManager.getEventListenerCache(button, "click")) { - button.el.dom.removeEventListener("click", handler.wrap); - button.el.dom.addEventListener("click", handler.wrap); + 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"); - comboSelectInstance.tpl.html = comboSelectInstance.tpl.html.replace("
(.+?)<\/div>/, + "$3" + ); + return combo; + }; } + + function handleNewCalendarEvent(element, calendar) { // Turn each calendar event into a link so that Link Hints can recognize it let a = document.createElement("a"); @@ -107,26 +177,27 @@ function handleNewCalendarEvent(element, calendar) { // fix weird aboveline (underline but above the text) on hover a.style.textDecoration = "none"; a.replaceChildren(...element.childNodes); - let preventCompassHandler = false; 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}`; - preventCompassHandler = true; } 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}`; } - preventCompassHandler = true; } else if (a.classList.contains("activity-type-10")) { // Add a link for learning tasks let calendarPanel = calendar.ownerCalendarPanel; - a.href = calendarPanel.targetUserId !== undefined ? `/Records/User.aspx?userId=${calendarPanel.targetUserId}#learningTasks` : "/Records/User.aspx#learningTasks"; - preventCompassHandler = true; + 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}`; } // Show the finish time for applicable calendar events @@ -140,7 +211,7 @@ function handleNewCalendarEvent(element, calendar) { // prevent ctrl-clicking from changing the current tab // it seems like the link thing actually effectively killed the default handler anyway - if (preventCompassHandler) { + if (a.href) { a.addEventListener("click", function(event) { event.stopImmediatePropagation(); }, {passive: true}); @@ -151,10 +222,6 @@ function handleNewCalendarEvent(element, calendar) { function handlePanelItem(panel, panelItem, isDefault, tabToOpen) { let panelId = panelItem.itemId || panelItem.id; - // i don't know why but dashboard is dsh - if (window.location.pathname === "/Records/User.aspx" && panelId === "dashboard") { - panelId = "dsh"; - } let panelItemHash = getPanelItemHash(panelId, isDefault); // Automatically open tab specified in fragment @@ -164,10 +231,6 @@ function handlePanelItem(panel, panelItem, isDefault, tabToOpen) { // it does look like that this bug only manifests if the tab is activated while the initial loading thing is shown setTimeout(function() { panel.setActiveTab(panelItem); - // reference the now active tab in the fragment if necessary (cough cough the reason why hashCopy exists) - if (panelItemHash !== window.location.hash) { - history.replaceState("", "", panelItemHash); - } }, 1000); } @@ -186,7 +249,7 @@ function handlePanelItem(panel, panelItem, isDefault, tabToOpen) { } // prevent the browser from scrolling to the body event.preventDefault(); - if (panelItemHash !== window.location.hash) { + if (window.location.hash !== panelItemHash && (window.location.pathname === "/Organise/Activities/Activity.aspx" || !window.location.hash.startsWith(`${panelItemHash}&`))) { // Automatically add a reference to the tab when it is clicked history.pushState("", "", panelItemHash); } @@ -200,29 +263,24 @@ function handleLearningTasksTable(element) { event.stopImmediatePropagation(); }, {passive: true}); } -} -function handleSessionItem(element) { - // Make sessions in the sessions dropdown links - let style = getComputedStyle(element); + // 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 a = document.createElement("a"); - a.href = `#session/${element.dataset.qolSession}`; - a.style.display = "block"; - a.style.textDecoration = "none"; - a.style.padding = style.padding; - a.style.color = style.color; - a.append(...element.childNodes); - a.addEventListener("click", function(event) { - if (event.ctrlKey) { - event.stopImmediatePropagation(); - } else { - event.preventDefault(); - } - }); - - element.style.padding = 0; - element.append(a); + 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(); + } + }); + } } function handleCKEditor(instance) { @@ -252,10 +310,6 @@ function handleNewNode(node, observer) { return; } - if (node.id === "CompassManagersActivityDefaultManager") { - handleActivityManager(node); - } - 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) { @@ -266,11 +320,10 @@ function handleNewNode(node, observer) { } } else if (!qolTabOpened && node.classList.contains("x-panel")) { qolTabOpened = true; - let tabToOpen = hashCopy.substring(1) || null; - if (window.location.pathname === "/Organise/Activities/Activity.aspx") { - tabToOpen = (new URLSearchParams(tabToOpen)).get("qol_open_tab"); - } let panel = unsafeWindow.Ext.getCmp(node.id); + 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); } @@ -284,8 +337,6 @@ function handleNewNode(node, observer) { } } else if (node.localName === "table" && node.closest(".sel-learning-tasks-widget")) { handleLearningTasksTable(node); - } else if (node.classList.contains("x-boundlist-item") && node.dataset.qolSession) { - handleSessionItem(node); } else if (node.classList.contains("cke")) { let instance = unsafeWindow.CKEDITOR.instances[node.id.substring(4)]; handleCKEditor(instance); diff --git a/README.md b/README.md index 76e86d1..21b88d6 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,13 @@ A userscript that adds small but useful features for Compass. Features include: creating a new tab - Ctrl-clicking an activity in a user's learning tasks tab no longer collapses everything else +- Learning tasks now being links (you can ctrl-click them) - The previous/next buttons and sessions dropdown are now links (you can now use [Link Hints] and ctrl-click to open them) - News feed items can now be opened by simply clicking on their background - The context menu that only says "Copy" is now suppressed - The option to remember logins is unchecked by default +- The dashboard tab in a user's profile no longer points you to #dsb [Link Hints]: https://lydell.github.io/LinkHints/