HKU moodle helper

This userscript allows HKU students to show your current courses (in a semester) in a separate entry in HKU Moodle. By: Andrew Z, converted to userscript by q234rty

  1. // ==UserScript==
  2. // @name HKU moodle helper
  3. // @include http://moodle.hku.hk/*
  4. // @include https://moodle.hku.hk/*
  5. // @version 1.4.7
  6. // @description This userscript allows HKU students to show your current courses (in a semester) in a separate entry in HKU Moodle. By: Andrew Z, converted to userscript by q234rty
  7. // @author AENeuro, q234rty, taogoddd
  8. // @resource mystyle https://cdn.jsdelivr.net/gh/AENeuro/HKU-Moodle-Helper@ede423d/myStyle.css
  9. // @resource fontawesome https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css
  10. // @license CC BY-NC 4.0
  11. // @grant GM_getResourceText
  12. // @grant GM_addStyle
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @namespace https://greasyfork.org/users/78076
  16. // ==/UserScript==
  17. globalThis.addFeedbackBox = function() {
  18. function showTextArea() {
  19. document.getElementById("helperFeedbackForm").classList.add("helper-shown")
  20. document.getElementById("helperFeedbackButton").insertAdjacentHTML("beforebegin", `
  21. <div>
  22. <p id="helperFeedbackButton2" style="color: #AAAAAA;">Check the
  23. <a href="https://github.com/AENeuro/HKU-Moodle-Helper" target="_blank">
  24. <span style="color: #AAAAAA;"><u>FAQ</u></span>
  25. </a>
  26. or submit an issue or PR on
  27. <a href="https://github.com/AENeuro/HKU-Moodle-Helper" target="_blank">
  28. <span style="color: #AAAAAA;"><u>Github</u></span>
  29. </a>
  30. </p>
  31. </div>
  32. `)
  33. document.getElementById("helperFeedbackButton").remove()
  34. }
  35.  
  36. async function sendFeedback() {
  37. if (!document.getElementById("helperFeedbackInput").value) {
  38. return 0
  39. }
  40. document.getElementById("helperFeedbackSend").disabled = true
  41. try{
  42. await request({
  43. url: " https://j8n6ydl8hd.execute-api.ap-southeast-1.amazonaws.com/create",
  44. method: "POST",
  45. body: document.getElementById("helperFeedbackInput").value
  46. })
  47. } catch(e) {
  48. alert("Network error")
  49. }
  50. document.getElementById("helperFeedbackForm").classList.remove("helper-shown")
  51. document.getElementById("helperFeedbackForm").insertAdjacentHTML("beforebegin", `
  52. <p style="color: #AAAAAA">Thank you for your feedback!</p>
  53. `)
  54. document.getElementById("helperFeedbackButton2").remove()
  55. }
  56.  
  57.  
  58. // initialization
  59.  
  60. document.getElementsByClassName("course-of-sem-wrapper")[0].insertAdjacentHTML("beforeend",`
  61. <div class="helper-feedback">
  62. <p>Powered by HKU Moodle Helper ver. 1.4.7</p>
  63. <p id="helperFeedbackButton">Feedback</p>
  64. <div id="helperFeedbackForm" class="helper-hidden">
  65. <input id="helperFeedbackInput" type="text" placeholder="Email [Optional] + issue"/><br/>
  66. <button id="helperFeedbackSend">Send</button>
  67. </div>
  68. </div>
  69. `)
  70. document.getElementById("helperFeedbackButton").addEventListener("click", showTextArea)
  71. document.getElementById("helperFeedbackSend").addEventListener("click", sendFeedback)
  72. }
  73. globalThis.addMessageBox = function () {
  74. const messageBox = `
  75. <section class="helper-extension-persistent helper-message-box block_html block card mb-3" role="complementary" data-block="html" aria-labelledby="instance-330654-header">
  76. <div class="card-body p-3">
  77. <h5 class="card-title d-inline">Message from HKU Moodle Helper</h5>
  78. <div class="card-text content mt-3">
  79. <div class="no-overflow">
  80. <p><span style="font-size:11.0pt;font-family:&quot;Calibri&quot;,sans-serif;color:black">
  81. This is a message generated by the chrome extension <i>HKU Moodle Helper</i> that you installed.
  82. </span></p>
  83. <p><span style="font-size:11.0pt;font-family:&quot;Calibri&quot;,sans-serif;color:black">
  84. As many of you have noticed, moodle underwent renovation, and it's unclear just how it would affect the extension yet.
  85. </span></p>
  86. <p><span style="font-size:11.0pt;font-family:&quot;Calibri&quot;,sans-serif;color:black">
  87. The extension will still be maintained, provided it's still relevant in new semesters to come.
  88. In the meantime, please condider becoming a dev in <a href="https://github.com/AENeuro/HKU-Moodle-Helper" target="_blank">HKU Moodle Helper</a>.
  89. Any PR or suggestions are welcomed of course.
  90. </span></p>
  91. </div>
  92. <div class="footer"></div>
  93. </div>
  94. </div>
  95. </section>
  96. `
  97.  
  98. document.getElementById("block-region-side-post").firstChild.insertAdjacentHTML("beforebegin", messageBox);
  99. }
  100. const request = obj => {
  101. return new Promise((resolve, reject) => {
  102. let xhr = new XMLHttpRequest();
  103. xhr.open(obj.method || "GET", obj.url);
  104. if (obj.headers) {
  105. Object.keys(obj.headers).forEach(key => {
  106. xhr.setRequestHeader(key, obj.headers[key]);
  107. });
  108. }
  109. xhr.onload = () => {
  110. if (xhr.status >= 200 && xhr.status < 300) {
  111. resolve(xhr.response);
  112. } else {
  113. reject(xhr.statusText);
  114. }
  115. };
  116. xhr.onerror = () => reject(xhr.statusText);
  117. xhr.send(JSON.stringify(obj.body));
  118. });
  119. };
  120. (function(){
  121. // Note: every element that is to be removed during a clearing session
  122. // should be marked with a "helper-extension" classname
  123. // Otherwise it should be marked with "helper-extension-persistent"
  124.  
  125. // Code splitting was done through globalThis (which was confined within ContentScript. Thus no pollutions were made)
  126. const my_css = GM_getResourceText("mystyle");
  127. GM_addStyle(my_css);
  128. const fontawesome = GM_getResourceText("fontawesome");
  129. GM_addStyle(fontawesome)
  130. mainFunction();
  131.  
  132. async function mainFunction() {
  133. //addCssByLink("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css");
  134. await addCourseOfSem();
  135. globalThis.addFeedbackBox();
  136. globalThis.addMessageBox();
  137. }
  138.  
  139. async function addCourseOfSem() {
  140. courseElements = new Array();
  141. courseList = JSON.parse(GM_getValue("courselist", "[]"))
  142. clearAll();
  143. var courses = document.getElementsByClassName("coursebox");
  144. pagePath = window.location.pathname;
  145. for (var i = 0; i < courses.length; i++) {
  146. currentCourseID = courses[i].dataset.courseid;
  147. var included = false;
  148. if (courseList) {
  149. included = courseList
  150. .map((value, index, array) => {
  151. return value.courseID;
  152. })
  153. .includes(currentCourseID);
  154. }
  155. if (included) {
  156. //如果在列表中
  157. //复制element,存入数组
  158. if (pagePath != "/course/search.php") {
  159. courseElements.push({
  160. courseID: currentCourseID,
  161. courseHTML: courses[i].cloneNode(true),
  162. });
  163. }
  164.  
  165. // Applies to all courses on the page that is in the list (in "my courses" section)
  166. courses[i].lastChild.lastChild.insertAdjacentHTML(
  167. "beforebegin",
  168. `
  169. <button class="helper-extension helper-remove-button" id="removeCourse${currentCourseID}">
  170. Remove from this semester
  171. </button>
  172. `
  173. );
  174. document
  175. .getElementById("removeCourse" + currentCourseID)
  176. .addEventListener("click", function (e) {
  177. removeCourse(e.target.id.slice(12), courseList);
  178. });
  179. } else {
  180. // Applies to all courses on the page that is not in the list (in "my courses" section)
  181. courses[i].lastChild.lastChild.insertAdjacentHTML(
  182. "beforebegin",
  183. `
  184. <button class="helper-extension helper-add-button" id="addCourse${currentCourseID}" >
  185. Add to this semester
  186. </button>
  187. `
  188. );
  189. document
  190. .getElementById("addCourse" + currentCourseID)
  191. .addEventListener("click", function (e) {
  192. courseInfo = extractInfo(LocateCourse(currentCourseID, courses));
  193. if (pagePath == "/course/search.php")
  194. addCourse(pagePath, e.target.id.slice(9), courseList, courseInfo);
  195. else addCourse(pagePath, e.target.id.slice(9), courseList);
  196. });
  197. }
  198. }
  199. // addcourses buttons for side bars
  200.  
  201. var sidebarlist = document.getElementsByClassName("column c1");
  202. for (var i = 0; i < sidebarlist.length; i++) {
  203. // id comes from the ref: https://moodle.hku.hk/course/view.php?id=xxx
  204. var included = false;
  205. const id = sidebarlist[i].firstChild.href.slice(41);
  206. if (courseList) {
  207. included = courseList
  208. .map((value, index, array) => {
  209. return value.courseID;
  210. })
  211. .includes(id);
  212. }
  213. const text = sidebarlist[i].removeChild(sidebarlist[i].lastChild);
  214. const anchor = document.createElement("span");
  215. sidebarlist[i].appendChild(anchor);
  216. sidebarlist[i].firstChild.insertAdjacentHTML(
  217. "afterEnd",
  218. `<div class="helper-extension helper-sidebar-wrapper">
  219. <div class="helper-extension helper-sidebar-button-${
  220. included ? "minus" : "plus"
  221. }" id="sidebarbtn${id}" title='${
  222. included ? "remove from" : "add to"
  223. } this semester' >${included ? "×" : "+"}</div>
  224. </div>`
  225. );
  226. sidebarlist[i].removeChild(anchor);
  227. sidebarlist[i].lastChild.insertAdjacentElement("beforeBegin", text);
  228.  
  229. if (included) {
  230. document
  231. .getElementById("sidebarbtn" + id)
  232. .addEventListener("click", function (e) {
  233. removeCourse(e.target.id.slice(10), courseList);
  234. });
  235. } else {
  236. document
  237. .getElementById("sidebarbtn" + id)
  238. .addEventListener("click", function (e) {
  239. addCourse("/course/search.php", e.target.id.slice(10), courseList, {
  240. title: text.innerText,
  241. teachers: "",
  242. });
  243. });
  244. }
  245. }
  246.  
  247. var outerContainer = document.getElementById("frontpage-course-list");
  248.  
  249. if (courseList && courseList.length && outerContainer) {
  250. //如果有课程
  251. outerContainer.insertAdjacentHTML(
  252. "afterBegin",
  253. `
  254. <div class="helper-extension course-of-sem-wrapper">
  255. <h2>
  256. Course of this semester
  257. <div id="removeAll">×</button>
  258. </h2>
  259. <div id="courseOfSem" class="courses frontpage-course-list-enrolled has-pre has-post course-of-sem"></div>
  260. </div>
  261. `
  262. );
  263.  
  264. document.getElementById("removeAll").addEventListener("click", function () {
  265. if (confirm("Do you wish to remove all courses from this semester?")) {
  266. removeAll();
  267. }
  268. });
  269. } else {
  270. //没有课程
  271. outerContainer.insertAdjacentHTML(
  272. "afterbegin",
  273. `
  274. <div class="helper-extension course-of-sem-wrapper">
  275. <h2>Course of this semester</h2>
  276. <p><i>Please click 'Add to this semester' on a course to bring it here.</i></p>
  277. </div>
  278. `
  279. );
  280. }
  281.  
  282. var innerContainer = document.getElementById("courseOfSem");
  283. for (var i = 0; i < courseList.length; i++) {
  284. /* if (i % 2) {
  285. //注意这里是偶数 => 这里是不能整除2(i是奇数),但是在显示顺序上是“偶数”
  286. courseHTML[i].className = "coursebox clearfix even";
  287. } else {
  288. courseHTML[i].className = "coursebox clearfix odd";
  289. } */
  290. // applies to all courses in this semester (in "course of this semester" section)
  291. currentCourseID = courseList[i].courseID;
  292. if (
  293. courseElements
  294. .map((value, index, array) => {
  295. return value.courseID;
  296. })
  297. .includes(courseList[i].courseID)
  298. ) {
  299. currentCourseHTML = courseElements.filter((value, index) => {
  300. return value.courseID == currentCourseID;
  301. })[0].courseHTML;
  302. currentCourseHTML.insertAdjacentHTML(
  303. "afterbegin",
  304. `
  305. <a id="removeCourseA${currentCourseID}" style="position: absolute; top: 5px; right: 5px; font-size: 25px; color: darkgrey; cursor: pointer">
  306. ×
  307. </a>
  308. `
  309. );
  310. innerContainer.appendChild(currentCourseHTML);
  311. document
  312. .getElementById("removeCourseA" + currentCourseID)
  313. .addEventListener("click", function (e) {
  314. removeCourse(e.target.id.slice(13), courseList);
  315. });
  316. } else {
  317. let courseDoc = new DOMParser().parseFromString(
  318. createCard(courseList[i]),
  319. "text/html"
  320. );
  321. var courseElement = courseDoc.querySelector("div");
  322. courseElement.insertAdjacentHTML(
  323. "afterbegin",
  324. `
  325. <a id="removeCourseA${currentCourseID}" style="position: absolute; top: 5px; right: 5px; font-size: 25px; color: darkgrey; cursor: pointer">
  326. ×
  327. </a>
  328. `
  329. );
  330. innerContainer.appendChild(courseElement);
  331. document
  332. .getElementById("removeCourseA" + currentCourseID)
  333. .addEventListener("click", function (e) {
  334. removeCourse(e.target.id.slice(13), courseList);
  335. });
  336. }
  337. }
  338. }
  339. // ======================================
  340. // Helper functions
  341. function addCssByLink(url) {
  342. var doc = document;
  343.  
  344. var link = doc.createElement("link");
  345.  
  346. link.setAttribute("rel", "stylesheet");
  347.  
  348. link.setAttribute("href", url);
  349.  
  350. var heads = doc.getElementsByTagName("head");
  351.  
  352. if (heads.length) heads[0].appendChild(link);
  353. else doc.documentElement.appendChild(link);
  354. }
  355. function clearAll() {
  356. var clearElements = document.getElementsByClassName("helper-extension");
  357. //必须倒序删除,因为HTMLCollection会因为remove方法动态变化
  358. for (var i = clearElements.length - 1; i >= 0; --i) {
  359. clearElements[i].remove();
  360. }
  361. }
  362.  
  363. function createCard(course) {
  364. courseID = course.courseID;
  365. courseInfo = course.courseInfo;
  366. return `<div class="coursebox clearfix odd first" data-courseid=${courseID} data-type="1">
  367. <div class="info">
  368. <h3 class="coursename">
  369. <a class="aalink" href="https://moodle.hku.hk/course/view.php?id=${courseID}">
  370. <span class="highlight">${courseInfo.title}</span>
  371. </a>
  372. </h3>
  373. <div class="moreinfo"></div>
  374. </div>
  375. <div class="content">
  376. <div class="summary">
  377. <h3 class="coursename">
  378. <a style="display: inline" href="https://moodle.hku.hk/course/view.php?id=${courseID}">${courseInfo.title}</a>
  379. <div class='history'>
  380. <div class="bubble" style="background-color: #332d2d;">
  381. <i style="width: 0px;height: 0px;color: #332d2d;border-width: 14px 15px 8px 0px;border-style: solid;border-color: currentcolor transparent transparent;top: 95%;left: 0px;margin-bottom: 4px;">
  382. </i>
  383. <div class="text">This course card was constructed based on your last visit to the search page or the sidebar</div>
  384. </div>
  385. <i class="fa fa-history history-icon" ></i>
  386. </div>
  387. </h3>
  388. <div></div>
  389. </div>
  390. <div class="teachers" >
  391. Teachers:
  392. <a style="color: #966b00;">${courseInfo.teachers}</a>
  393. </div>
  394. <div class="course-btn">
  395. <p>
  396. <a
  397. class="btn btn-primary"
  398. href="https://moodle.hku.hk/course/view.php?id=${courseID}"
  399. >
  400. Click to enter this course
  401. </a>
  402. </p>
  403. </div>
  404. </div>
  405. </div>`;
  406. }
  407.  
  408. function LocateCourse(courseID, courses) {
  409. for (let i = 0; i < courses.length; i++) {
  410. if (courses[i].dataset.courseid == courseID) return courses[i];
  411. }
  412. return;
  413. }
  414.  
  415. function extractInfo(courseElement) {
  416. title = courseElement.querySelector(".aalink").innerText;
  417. teachers = courseElement.querySelector(".teachers").innerText.slice(9);
  418. return { title: title, teachers: teachers };
  419. }
  420.  
  421. async function addCourse(pageURL, courseID, courseList, courseInfo = {}) {
  422. if (pageURL == "/course/search.php") {
  423. if (courseList && courseList.length) {
  424. courseList.push({ courseID: courseID, courseInfo: courseInfo });
  425. } else {
  426. courseList = [{ courseID: courseID, courseInfo: courseInfo }];
  427. }
  428. GM_setValue("courselist", JSON.stringify(courseList));
  429. } else {
  430. if (courseList && courseList.length) {
  431. courseList.push({ courseID: courseID });
  432. } else {
  433. courseList = [{ courseID: courseID }];
  434. }
  435. GM_setValue("courselist", JSON.stringify(courseList));
  436. }
  437. mainFunction();
  438. }
  439.  
  440. async function removeCourse(courseCode, courseList) {
  441. courseList = courseList.filter(function (value, index, arr) {
  442. return value.courseID !== courseCode;
  443. });
  444. GM_setValue("courselist", JSON.stringify(courseList));
  445.  
  446. mainFunction();
  447. }
  448.  
  449. async function removeAll() {
  450. GM_setValue("courselist", "[]")
  451. mainFunction();
  452. }
  453.  
  454. })();