Lit3Nitride / iNTULearn - Course Lists (Redundant Redundancy Removal)

// ==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

})();