NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name iNTULearn - Course Lists (Redundant Redundancy Removal) // @namespace http://zaw.li // @version 4.0 // @description Clicks the redundant "LAUNCH" button when clicking a course, and enables ctrl+click for the course list // @author Lit3Nitride // @homepage https://gist.github.com/Lit3Nitride/3462d642beda63957d3301fc12e3e6d7 // @license https://creativecommons.org/licenses/by-sa/4.0/ // @match https://intulearn.ntu.edu.sg/ntu-app/app/dashboard.html // @grant none // ==/UserScript== (function() { "use strict"; /** * * ══════════════════════════ * Stuff this script does * ══════════════════════════ * 1) Enable ctrl+click for Course List, Course Table of Contents, * and Announcement/Notifications view-more ellipsis * 2) Increase clickable areas for notifications and announcements * 3) In doing (1), indirectly made permalinks for Course Sections * 4) Enhanced popup behaviour: pressing 'Esc' or clicking outside * the popup closes it * */ // URL query string function from https://stackoverflow.com/a/901144 function getParameterByName(name, url) { if (!url) url = window.location.href name = name.replace(/[\[\]]/g, "\\$&") var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), results = regex.exec(url) if (!results) return null if (!results[2]) return '' return decodeURIComponent(results[2].replace(/\+/g, " ")) } var RRR = function() { /** * ========================= * Initialisation things * ========================= */ // Everything is in this function. Defining it allows us to try // running it, and if that doesn't work, try running it again later if (typeof jQuery == "undefined") // Before we even start, we need to make sure jQuery is loaded return false // If not, we exit this function // Here we add the various styles to the header $("head").append(` <style type="text/css"> /** * * This makes invisible the course details page, * which removes the "flicker" during transition * */ .course-expand-container { opacity: 0; } /** * * This hides the folder in the side menu of the * course page, which, if you click it, *gives * you the exact same thing that the side menu * is already showing*. * */ .course-launch-leftmenu .course-launch-leftmenu-heading span { display: none; } /** * * 'touchOut' implies it's those dark background * which you click, and it closes the popup. But * 1) it doesn't work that way * 2) it makes EVERYTHING a 'pointer cursor', * making one think that every single thing * can be clicked * * So to keep ourselves sane, we make the cursor * be normal, except for the background of the * popup window * */ body.touchOut, .iNTU-modal > * { cursor: default; } .iNTU-modal { cursor: pointer; } /** * * A bunch of thing that should be and IS clickable, * but the mouse doesn't tell you that (???????) * * TODO: For some reason using the comma here doesn't * work (the browser will ignore all the styles), so * the individual rules had to be typed out * */ .view-more-ellipsis { cursor: pointer; } #bell-data ul.bell-main-list > li { cursor: pointer; } .create-content-aside .nav-list .submenu-heading { cursor: pointer; } .announcement-content { cursor: pointer; } /** * * The whole box have been made clickable, so we * wouldn't want the title to seem like it has a * different link * */ body .NotificationTitle:hover { color: #333333; } .bell-dropdown-heading a:hover { color: #337ab7; } /** * * Changes the whole box's background colour so * that we know the whole thing is clickable * */ #bell-data ul.bell-main-list > li:hover > ul { background-color: white; } </style> `) /** * ======================== * Course launch things * ======================== */ var hashTimeouts, // This declares a variable for the "check if LAUNCH // button exists" so that we can access it anywhere // in our little script bypassCourseLaunch = function() { // This function clicks "LAUNCH" once it appears if ($("a[ng-click='CourseLaunchClick()']").length < 1) { // As long as that button doesn't exist hashTimeouts = setTimeout(bypassCourseLaunch, 4) // We keep looking for it } else { // Once it appears, $("a[ng-click='CourseLaunchClick()']").click() // we click it. Simple. clearTimeout(hashTimeouts) // Just in case, we clear the timeout variable } }, hashChange = function(e) { // This function will run everytime the URL changes clearTimeout(hashTimeouts) // Clear all the things we were doing in the past // now that the situation has changed if (location.hash == "#/courses") // When we are in the dumbass page where we have // to click the redundant button hashTimeouts = setTimeout(bypassCourseLaunch, 4) // We go on the lookout for that elusive button else if (getParameterByName("section") != null) // In the case we have a query in the url called "section" initTocInit() // Means we're probably in the course page itself and we've // selected a particular section, so we call the function that // handles the sections of the "table of contents" of the course } /** * ================= * Clicky things * ================= */ var triggered = false // Attempts to prevent double-clicks $(document) .on("click", "[ng-click^='getCurrentCourse(course.Id)'], a.view-more-ellipsis", function(e) { // Here we're detecting either if any button under "My Courses", // or the ellipsis, is clicked if (e.ctrlKey) { // If the control key is currently pressed window.open(location.href) // Open a new window/tab of the current page // (by the time this runs, the current page // is already the new page, so the new // window/tab will be the new page) history.back() // Go back to the previous page, just before // the new page; the whole sequence thus // emulates the "Open in New Tab/Window" // behaviour of how links are supposed to be } }) .on("click", ".bell-main-list > li.ng-scope, .notificationContent .announcement-container .announcement-content", function() { // This detects the "boxes" of the notification dropdown/page if (!triggered) { // Only continue if it was not recently clicked // (4 milliseconds, so computer-time recently, // not human time) triggered = true // So if this function runs again within 4ms (which // sometimes happens due to the many layers of // listeners and triggers already existing on iNTULearn's= // side), it'll do nothing. $(this).find("h3.ng-binding, a.ng-binding")[0].click() // Within the "box", find the title of the box and click it setTimeout(function() { triggered = false }, 4) // Reset the trigger click after 4ms } }) .on("click", ".announcement-content .checkbox + div", function() { // This detects when a "box" of the announcements is clicked $("#" + $(this).parents(".announcement-content").find(".checkbox").children("input").attr("id")).click() // So we click the checkbox beside the box (so we won't need // to click the checkbox precisely; clicking anywhere in the // box would check it) if ($('[ng-model="selectedStatus"]').val().toLowerCase() == "unread") // If we're currently in the "unread" page $("a[ng-click='updateAnnouncementStatus(true)']").click() // Make the announcement "read" (this is like the behaviour // with the old NTU Learn) }) /** * ========================================= * Course Page - Table of Content things * ========================================= */ var tocInit, // This allows us to check whether or // not we should run the ToC checking // function: the checking function // runs constantly, so we'll set a // time limit for it to terminate so // as to not slow down the page (than // it already is) tocClickTimeout, // The timeout part of the function is // assigned here, so that we can terminate // it if needed tocClick = function() { // The checking function: it looks for the clickable // button in the ToC of the section specified in the, // query, and clicks it once it finds it, thus going // to that section $("[ng-click^='tocClick']").each(function() { // We go through each of the ToC items if (getParameterByName("section") == $(this).children("a").text()) { // If the "section" query is the item // that we are currently checking tocInit = false // Make the ToC variable false to // terminate the checking function $(this).click() // Click this item (therefore going into this section) } }) if (tocInit) // In the case the variable doesn't tell us to // terminate yet (which means that we still haven't // found the section specified in the query: either // the ToC isn't completely loaded yet, or the query // doesn't exist. We'll assume the former and keep // checking) tocClickTimeout = setTimeout(tocClick, 4) // Here we run ourselves again: keep on checking }, initTocInit = function() { // This initialises the checking function: either // when we change to another section, or when we // first run it tocInit = true // Obviously we wouldn't want the checking function // to terminate immediately after the first run tocClick() // Do the first checking run setTimeout(function() { tocInit = false }, 120000) // But we wouldn't want the checking function to // run forever in case the section actually doesn't // exist, so we set a time limit of 2 minutes } $(document) .on("click", "[ng-click^='tocClick']", function(e) { // This runs when an item in the ToC is clicked e.preventDefault() // Prevents the window from opening "javascript:;" clearTimeout(tocClickTimeout) // We stop the checking function, since the old specified // section may no longer be the one the user wants to go to var oldUrl = location.href.split("?")[0], newUrl = oldUrl + "?section=" + $(this).children("a").text() // This removes the currently specified section, and then adds // the new section (that the user just clicked) // // NOTE: This is a bit of a hack that assumes iNTULearn will // never use GET queries. Obviously we can make a parsing // function that handles it properly in the case they decide // to eventually use it, but how much of your life would you // really want to put into placing duct tape on paper cuts? if (e.ctrlKey) { // If the control key is currently pressed window.open(newUrl) // Open the selected section in a new page/tab //hashChange() if (oldUrl == location.href) // If the URL doesn't already have a specified // section (means we're at the first section) $(this).siblings().first().click() // Go to the first section else // If the specified section exists initTocInit() // Go back to the specified section } else if (decodeURIComponent(location.href) != newUrl) { // In the case that the current URL is // not the newly specified one location.href = newUrl // Change the URL to the new one // // This might seem a little odd, but what this does // is that it adds the "?section=" into the URL // even if we're not making a new table. So when // you copy and paste the URL, it opens on the // section you're currently at instead of going // back to announcements } }) /** * ======================= * Popup window things * ======================= */ $(document) .keyup(function(e) { // This detects when any key is pressed if (e.keyCode == 27 && $(".iNTU-modal:not([class*='hide']").length > 0) { // If the escape key is pressed, and an active popup exists $(".iNTU-modal:not([class*='hide']").find("[ng-click*='close'],[ng-click*='hide']").click() // We click it } }) .on("click", ".iNTU-modal", function(e) { // If we click the popup window if (e.target === this) // And the thing it click is itself and not its children // (aka the area outside the contents: the dark areas) $(this).find("[ng-click*='close'],[ng-click*='hide']").click() // We close the popup }) /** * =================== * Listener things * =================== * * Because these functions listen to changes in the * URL and run other functions that are all over the * place, we run these last so that we won't try to * run a function that didn't exist. * */ $(window).on("hashchange", hashChange) // We tell the window to run the this-function-will-run-everytime- // the-URL-changes function to run everytime the URL changes hashChange() // And for good measure, run it once the page loads; in case we're // already at the redundant redundancy page } if (!RRR()) // Some userscript extension injects our script too quickly, so we need to // try running it first, and if it false document.addEventListener("DOMContentLoaded", RRR) // means jQuery isn't loaded yet. In that case we wait for it to load })();