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
})();