├── docs ├── CNAME ├── feedback │ ├── logo.png │ ├── style.css │ └── index.html └── textbooks │ ├── logo.png │ ├── Acorn_white_128.png │ ├── style.css │ ├── index.html │ └── textbooks.js ├── images ├── logo.png ├── Acorn_16.png ├── Acorn_48.png ├── Acorn_128.png ├── Acorn_red_128.png ├── Acorn_red_16.png ├── Acorn_red_48.png └── Acorn_white_128.png ├── firefox-extras.json ├── src ├── contentscripts │ ├── purge.js │ ├── textbookLinker.js │ ├── infiniteScroll.js │ ├── contentScript.js │ ├── util.js │ ├── background.js │ ├── tooltip.js │ └── google.js ├── popup │ ├── popup.css │ ├── popup.html │ └── popup.js ├── settings │ ├── settings.css │ ├── settings.js │ └── settings.html ├── util │ └── updateState.js └── about │ └── index.html ├── firefox-zip.sh ├── firefox.sh ├── Makefile ├── LICENSE ├── dependencies └── tippy │ ├── light.css │ └── tippy.all.min.js ├── manifest.json ├── README.md └── data └── discounttb.json /docs/CNAME: -------------------------------------------------------------------------------- 1 | courseinfo.murad-akh.ca -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuradAkh/UofTCourseInfo/HEAD/images/logo.png -------------------------------------------------------------------------------- /images/Acorn_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuradAkh/UofTCourseInfo/HEAD/images/Acorn_16.png -------------------------------------------------------------------------------- /images/Acorn_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuradAkh/UofTCourseInfo/HEAD/images/Acorn_48.png -------------------------------------------------------------------------------- /docs/feedback/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuradAkh/UofTCourseInfo/HEAD/docs/feedback/logo.png -------------------------------------------------------------------------------- /images/Acorn_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuradAkh/UofTCourseInfo/HEAD/images/Acorn_128.png -------------------------------------------------------------------------------- /docs/textbooks/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuradAkh/UofTCourseInfo/HEAD/docs/textbooks/logo.png -------------------------------------------------------------------------------- /images/Acorn_red_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuradAkh/UofTCourseInfo/HEAD/images/Acorn_red_128.png -------------------------------------------------------------------------------- /images/Acorn_red_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuradAkh/UofTCourseInfo/HEAD/images/Acorn_red_16.png -------------------------------------------------------------------------------- /images/Acorn_red_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuradAkh/UofTCourseInfo/HEAD/images/Acorn_red_48.png -------------------------------------------------------------------------------- /images/Acorn_white_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuradAkh/UofTCourseInfo/HEAD/images/Acorn_white_128.png -------------------------------------------------------------------------------- /docs/textbooks/Acorn_white_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuradAkh/UofTCourseInfo/HEAD/docs/textbooks/Acorn_white_128.png -------------------------------------------------------------------------------- /firefox-extras.json: -------------------------------------------------------------------------------- 1 | { 2 | "options_ui": { 3 | "page": "src/settings/settings.html", 4 | "open_in_tab": true 5 | } 6 | } -------------------------------------------------------------------------------- /src/contentscripts/purge.js: -------------------------------------------------------------------------------- 1 | $(document).ready(() => { 2 | $('.corInf').each(function () { 3 | $(this).replaceWith($(this).data('title')); 4 | }) 5 | }); -------------------------------------------------------------------------------- /firefox-zip.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd ./firefox-compiled 3 | zip -r -1 firefox.zip ./data ./images ./dependencies ./src README.md ./manifest.json 4 | cd .. 5 | cp ./firefox-compiled/firefox.zip . 6 | -------------------------------------------------------------------------------- /src/popup/popup.css: -------------------------------------------------------------------------------- 1 | html{ 2 | min-width: 300px; 3 | } 4 | 5 | #icon{ 6 | max-height: 20px; 7 | } 8 | 9 | #title{ 10 | color: #11245D; !important; 11 | } 12 | 13 | #feedback-link{ 14 | color: mediumvioletred; 15 | } -------------------------------------------------------------------------------- /src/contentscripts/textbookLinker.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | $('#settings-link').attr('href', getSettingsUrl()); 3 | $('#about-link').attr('href', getAboutUrl()); 4 | $('.installed').removeAttr('hidden'); 5 | $('.not-installed').attr('hidden', ''); 6 | }); -------------------------------------------------------------------------------- /docs/feedback/style.css: -------------------------------------------------------------------------------- 1 | body{ 2 | height: 100vh; 3 | overflow: hidden; 4 | align-items: center; 5 | justify-content: center; 6 | flex-direction: column; 7 | background-color: #fcfcfc; 8 | margin: 0; 9 | display: flex; 10 | } 11 | 12 | div{ 13 | display: block; 14 | } -------------------------------------------------------------------------------- /firefox.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # remove old "binaries" 3 | rm -rf ./firefox-compiled > /dev/null # ignore the warning 4 | 5 | # make compiled dir 6 | mkdir ./firefox-compiled 7 | 8 | cp -r ./data ./images ./dependencies ./src ./README.md ./firefox-compiled 9 | 10 | # filter the manifest requires jq 11 | # will add firefox extras and remove stuff only needed for chrome 12 | jq -s add ./manifest.json ./firefox-extras.json | jq 'del(.key, .update_url, .options_page)'> ./firefox-compiled/manifest.json 13 | 14 | 15 | #rm -rf ./firefox-compiled -------------------------------------------------------------------------------- /src/contentscripts/infiniteScroll.js: -------------------------------------------------------------------------------- 1 | function onElementHeightChange(elm, callback){ 2 | let lastHeight = elm.clientHeight, newHeight; 3 | (function run(){ 4 | newHeight = elm.clientHeight; 5 | if( lastHeight !== newHeight ) 6 | callback(); 7 | lastHeight = newHeight; 8 | 9 | if( elm.onElementHeightChangeTimer ) 10 | clearTimeout(elm.onElementHeightChangeTimer); 11 | 12 | elm.onElementHeightChangeTimer = setTimeout(run, 2500); 13 | })(); 14 | } 15 | 16 | onElementHeightChange(document.body, () => { 17 | findCourses(); 18 | generateTooltips(); 19 | }); -------------------------------------------------------------------------------- /src/settings/settings.css: -------------------------------------------------------------------------------- 1 | 2 | .form-background { 3 | background-color: #f2f3f8; 4 | border-radius: 5px; 5 | padding: 25px; 6 | font-family: 'Open Sans',sans-serif; 7 | } 8 | 9 | .settings-header {} 10 | /* Adjust aesthetics of the h3 elements in settings box. */ 11 | 12 | .btn-primary { 13 | background-color: #3172b7; 14 | border: 0; 15 | } 16 | 17 | .btn-primary:hover { 18 | background-color: #0E5BAC; 19 | } 20 | 21 | .navbar { 22 | background-color: #11245D; 23 | } 24 | 25 | #logo { 26 | max-height: 35px; 27 | padding-right: 10px; 28 | } 29 | 30 | #form { 31 | width: 45%; 32 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | FILES= $(shell find src -type f) $(shell find data -type f) $(shell find dependencies -type f) $(shell find images -type f) 2 | FIREFOX_FILES= $(shell find firefox-compiled -type f) 3 | 4 | all: chrome.zip firefox.zip 5 | 6 | # Package for chrome 7 | chrome.zip: manifest.json README.md $(FILES) 8 | zip -r $@ ./data ./images ./dependencies ./src manifest.json README.md 9 | 10 | # Convert to firefox 11 | firefox-compiled: manifest.json README.md $(FILES) 12 | sh firefox.sh 13 | 14 | # zip converted 15 | firefox.zip: firefox-compiled $(FIREFOX_FILES) 16 | sh firefox-zip.sh 17 | 18 | #firefox.xpi: firefox.zip 19 | # cp $^ $@ 20 | 21 | clean: 22 | rm -f chrome.zip firefox.zip 23 | rm -rf ./firefox-compiled -------------------------------------------------------------------------------- /docs/feedback/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CourseInfo 6 | 7 | 8 | 9 | 10 |
11 | 12 |
13 |
14 |

UofT Course Info is looking for your feedback!

15 |
16 |
17 | Take the Survey! 18 | 19 | 20 |
21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Murad Akhundov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/util/updateState.js: -------------------------------------------------------------------------------- 1 | 2 | const on = { 3 | path: { 4 | "128": "/images/Acorn_128.png", 5 | "48": "/images/Acorn_48.png", 6 | "16": "/images/Acorn_16.png" 7 | } 8 | }; 9 | 10 | const off = { 11 | path: { 12 | "128": "/images/Acorn_red_128.png", 13 | "48": "/images/Acorn_red_48.png", 14 | "16": "/images/Acorn_red_16.png" 15 | } 16 | }; 17 | 18 | function updateTabs() { 19 | chrome.storage.local.get({ 20 | globoption: true, 21 | urloptions: {} 22 | }, items => { 23 | chrome.tabs.query({}, tabs => { 24 | for (let i = 0; i < tabs.length; i++) { 25 | if ((items.urloptions[new URL(tabs[i].url).hostname] !== false) && items.globoption) { 26 | if (!/.*google\....?\/search\?.*/.test(tabs[i].url)) { 27 | chrome.tabs.executeScript(tabs[i].id, {file: '/src/contentscripts/contentScript.js'}); 28 | chrome.tabs.executeScript(tabs[i].id, {file: '/dependencies/tippy/tippy.all.min.js'}); 29 | chrome.tabs.insertCSS(tabs[i].id, {file: 'dependencies/bootstrap/bootstrapcustom.min.css'}); 30 | chrome.tabs.insertCSS(tabs[i].id, {file: 'dependencies/tippy/light.css'}); 31 | chrome.tabs.executeScript(tabs[i].id, {file: '/src/contentscripts/tooltip.js'}); 32 | } 33 | } else { 34 | chrome.tabs.executeScript(tabs[i].id, {file: "/src/contentscripts/purge.js"}); 35 | } 36 | } 37 | } 38 | ); 39 | 40 | }); 41 | 42 | 43 | } -------------------------------------------------------------------------------- /src/popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UofT Course Info 6 | 7 | 8 | 9 | 10 |
11 |
12 |
UofT Course 13 | Info
14 |
15 |
16 |
17 | 18 | 19 |
20 |
21 |
22 | 23 | 24 |
25 |
26 | 30 |
31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /dependencies/tippy/light.css: -------------------------------------------------------------------------------- 1 | .tippy-popper[x-placement^=top] .tippy-tooltip.light-theme .tippy-arrow { 2 | border-top: 8px solid transparent; 3 | border-right: 8px solid transparent; 4 | border-left: 8px solid transparent 5 | } 6 | 7 | .tippy-popper[x-placement^=bottom] .tippy-tooltip.light-theme .tippy-arrow { 8 | border-bottom: 8px solid transparent; 9 | border-right: 8px solid transparent; 10 | border-left: 8px solid transparent 11 | } 12 | 13 | .tippy-popper[x-placement^=left] .tippy-tooltip.light-theme .tippy-arrow { 14 | border-left: 8px solid transparent; 15 | border-top: 8px solid transparent; 16 | border-bottom: 8px solid transparent 17 | } 18 | 19 | .tippy-popper[x-placement^=right] .tippy-tooltip.light-theme .tippy-arrow { 20 | border-right: 8px solid transparent; 21 | border-top: 8px solid transparent; 22 | border-bottom: 8px solid transparent 23 | } 24 | 25 | .tippy-tooltip.light-theme { 26 | color: transparent; 27 | background-color: transparent 28 | } 29 | 30 | .tippy-tooltip.light-theme .tippy-backdrop { 31 | background-color: transparent 32 | } 33 | 34 | .tippy-tooltip.light-theme .tippy-roundarrow { 35 | fill: navy; 36 | } 37 | 38 | .tippy-tooltip.light-theme[data-animatefill] { 39 | background-color: transparent 40 | } 41 | 42 | /* Default (sharp) arrow */ 43 | .tippy-popper[x-placement^='top'] .tippy-tooltip.light-theme .tippy-arrow { 44 | border-top-color: #11245D; 45 | } 46 | .tippy-popper[x-placement^='bottom'] .tippy-tooltip.light-theme .tippy-arrow { 47 | border-bottom-color: #11245D; 48 | } 49 | .tippy-popper[x-placement^='left'] .tippy-tooltip.light-theme .tippy-arrow { 50 | border-left-color: #11245D; 51 | } 52 | .tippy-popper[x-placement^='right'] .tippy-tooltip.light-theme .tippy-arrow { 53 | border-right-color: #11245D; 54 | } -------------------------------------------------------------------------------- /docs/textbooks/style.css: -------------------------------------------------------------------------------- 1 | /* .nav-item { 2 | transition: 0.3s; 3 | } 4 | 5 | .nav-item:hover { 6 | background-color: #3172b7; 7 | } */ 8 | 9 | .pic { 10 | max-height: 300px; 11 | float: left; 12 | width: 18% 13 | } 14 | 15 | .amazon{ 16 | background-color: #ff9900; 17 | font-family: Tahoma, Arial, serif; 18 | text-decoration: none; 19 | border: 4px solid #ff9900; 20 | color: black !important; 21 | font-size: larger; 22 | 23 | } 24 | 25 | .facebook{ 26 | background-color: #3B5998; 27 | font-family: Officina Sans, Arial, serif; 28 | text-decoration: none; 29 | border: 4px solid #3B5998; 30 | color: white !important; 31 | font-size: larger; 32 | } 33 | 34 | .ebay{ 35 | background-color: #e53238; 36 | font-family: Univers, Arial, serif; 37 | text-decoration: none; 38 | border: 4px solid #e53238; 39 | color: white !important; 40 | font-size: larger; 41 | 42 | } 43 | 44 | .google{ 45 | background-color: #34A853; 46 | font-family: Helvetica, Arial, serif; 47 | text-decoration: none; 48 | border: 4px solid #34A853; 49 | color: white !important; 50 | font-size: larger; 51 | 52 | } 53 | 54 | #content { 55 | font-family: Arial, serif; !important; 56 | font-size: large; !important; 57 | padding-left: 100px; !important; 58 | padding-right: 100px; !important; 59 | } 60 | 61 | #search-books{ 62 | margin: 0 auto; 63 | font-size: x-large; 64 | min-width: 44%; 65 | text-align: center; 66 | 67 | } 68 | 69 | #book-cont{ 70 | text-align: center; 71 | } 72 | 73 | .margined { 74 | margin-left: 1.3vw; 75 | } 76 | 77 | #addCustom{ 78 | float: right; 79 | font-size: x-small; 80 | text-decoration: underline; 81 | } 82 | 83 | .ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active, a.ui-button:active, .ui-button:active, .ui-button.ui-state-active:hover { 84 | border: 1px solid #11245D; 85 | background: #11245D; 86 | font-weight: normal; 87 | color: #ffffff; 88 | } 89 | 90 | .ui-widget-content a { 91 | color: #007bff !important; 92 | } 93 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": { 3 | "scripts": [ 4 | "src/contentscripts/background.js" 5 | ] 6 | }, 7 | "browser_action": { 8 | "default_icon": { 9 | "128": "images/Acorn_128.png", 10 | "48": "images/Acorn_48.png", 11 | "16": "images/Acorn_16.png" 12 | }, 13 | "default_title": "UofT Course Info", 14 | "default_popup":"src/popup/popup.html" 15 | 16 | }, 17 | "content_scripts": [ 18 | { 19 | "css": [ 20 | "dependencies/tippy/light.css" 21 | ], 22 | "js": [ 23 | "dependencies/jquery/jquery.min.js", 24 | "dependencies/tippy/tippy.all.min.js", 25 | "src/contentscripts/util.js" 26 | ], 27 | "matches": [ 28 | "\u003Call_urls>" 29 | ] 30 | }, 31 | { 32 | "js": [ 33 | "src/contentscripts/textbookLinker.js" 34 | ], 35 | "matches": [ 36 | "http://courseinfo.murad-akh.ca/textbooks/*" 37 | ] 38 | } 39 | ], 40 | "description": "Adds informative tooltips to University of Toronto courses mentioned across the web", 41 | "icons": { 42 | "128": "images/Acorn_128.png", 43 | "48": "images/Acorn_48.png", 44 | "16": "images/Acorn_16.png" 45 | }, 46 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4DfgytR+GzrCMqHW946oGojHg73K75sH2FUYEinr+Fn7VQkDPGZTOwc8uYzXBBamwv6x4XV1eoS17CkCS38S97bhyR0mPJ72On9HkOL1gb8RXKShwOdriDNh/AM/wLK4AEGYYRwutZBZBg3zOYl4dHpPS9Br+6iAs86J7t8Eqkvk1FMtpGrxEdPKca/oqo7+oIdIixIaq67Zn0/A09DcyJkQ+BXmtVeQWtDihTE7+5jpeTA5UHK4CDIqJ/IDupqcSWBUA0OK9XvpwRcOkrQvuZiOq0jiLjc3GdtbZYOX81+kYo7oLifXRTH1PATH9GV4Qh0qH9IzTPEevdIcJAAUpQIDAQAB", 47 | "manifest_version": 2, 48 | "name": "UofT Course Info", 49 | "permissions": [ 50 | "activeTab", 51 | "*://*/*", 52 | "tabs", 53 | "storage", 54 | "notifications" 55 | ], 56 | "update_url": "https://clients2.google.com/service/update2/crx", 57 | "version": "4.7.0", 58 | "options_page": "src/settings/settings.html", 59 | "web_accessible_resources": [ 60 | "dependencies/tipped/tipped.css", 61 | "data/directory.json", 62 | "src/settings/settings.html", 63 | "src/about/index.html" 64 | ], 65 | "content_security_policy": "script-src 'self' https://www.google-analytics.com; object-src 'self'; style-src 'self' 'unsafe-inline' https://maxcdn.bootstrapcdn.com" 66 | } 67 | -------------------------------------------------------------------------------- /src/popup/popup.js: -------------------------------------------------------------------------------- 1 | $(document).ready(() => { 2 | let url; 3 | let urloptions; 4 | let globoption; 5 | 6 | // Standard Google Universal Analytics code 7 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 8 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 9 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 10 | })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); // Note: https protocol here 11 | 12 | ga('create', 'UA-140776274-1\n', 'auto'); // Enter your GA identifier 13 | ga('set', 'checkProtocolTask', function(){}); // Removes failing protocol check. @see: http://stackoverflow.com/a/22152353/1958200 14 | ga('require', 'displayfeatures'); 15 | ga('send','pageview', 'popup.html'); 16 | 17 | chrome.storage.local.get({ 18 | globoption: true, 19 | urloptions: {} 20 | }, function (items) { 21 | urloptions = items.urloptions; 22 | globoption = items.globoption; 23 | $("#all-dom").attr('checked', globoption); 24 | setURL(); 25 | }); 26 | 27 | 28 | $("#this-dom").change(() => { 29 | urloptions[url] = $("#this-dom").is(":checked"); 30 | chrome.storage.local.set({ 31 | urloptions: urloptions 32 | }); 33 | 34 | ga('send', 'event', { 35 | 'eventCategory': 'Settings', 36 | 'eventAction': 'this-dom', 37 | 'eventLabel': url 38 | }); 39 | 40 | updateTabs(); 41 | }); 42 | 43 | $("#all-dom").change(() => { 44 | globoption = $("#all-dom").is(":checked"); 45 | chrome.storage.local.set({ 46 | globoption: globoption 47 | }); 48 | 49 | if (globoption) chrome.browserAction.setIcon(on); 50 | else chrome.browserAction.setIcon(off); 51 | 52 | ga('send', 'event', { 53 | 'eventCategory': 'Settings', 54 | 'eventAction': 'all-dom', 55 | }); 56 | 57 | updateTabs(); 58 | }); 59 | 60 | 61 | function setURL() { 62 | chrome.tabs.query({'active': true, 'currentWindow': true}, function (tabs) { 63 | url = (new URL(tabs[0].url)).hostname; 64 | $("#curr-domain").text(url); 65 | $("#this-dom").attr('checked', urloptions[url] !== false); 66 | }); 67 | } 68 | 69 | 70 | 71 | }); 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ![](https://i.imgur.com/7lorkX2.png) 3 | 4 | |Browser|Version|Rating|Users| Link | 5 | |--|--|--|--|--| 6 | |Chrome| [![](https://img.shields.io/chrome-web-store/v/jcbiiafabmhjeiepopiiajnkjhcdieme.svg?label=&style=for-the-badge)](https://chrome.google.com/webstore/detail/uoft-course-info/jcbiiafabmhjeiepopiiajnkjhcdieme) | [![](https://img.shields.io/chrome-web-store/rating/jcbiiafabmhjeiepopiiajnkjhcdieme.svg?label=&style=for-the-badge)](https://chrome.google.com/webstore/detail/uoft-course-info/jcbiiafabmhjeiepopiiajnkjhcdieme) | [![](https://img.shields.io/chrome-web-store/users/jcbiiafabmhjeiepopiiajnkjhcdieme.svg?label=&style=for-the-badge)](https://chrome.google.com/webstore/detail/uoft-course-info/jcbiiafabmhjeiepopiiajnkjhcdieme) | [![](https://i.imgur.com/5xGgaWQ.png)](https://chrome.google.com/webstore/detail/uoft-course-info/jcbiiafabmhjeiepopiiajnkjhcdieme) 7 | |Firefox|[![](https://img.shields.io/amo/v/uoft-course-info.svg?label=&style=for-the-badge)](https://addons.mozilla.org/en-US/firefox/addon/uoft-course-info/) |[![](https://img.shields.io/amo/rating/uoft-course-info.svg?label=&style=for-the-badge)](https://addons.mozilla.org/en-US/firefox/addon/uoft-course-info/) |[![](https://img.shields.io/amo/users/uoft-course-info.svg?label=&style=for-the-badge)](https://addons.mozilla.org/en-US/firefox/addon/uoft-course-info/)|[![](https://i.imgur.com/EGkkgvF.png)](https://addons.mozilla.org/en-US/firefox/addon/uoft-course-info/)| 8 | 9 | 10 | Adds tooltips to University of Toronto (U of T) courses mentioned on any webpage. Tooltips contain course information such as prerequisites, exclusions, breadths etc. Also adds info cards to Google Search. 11 | 12 |

13 | 14 |

15 |
16 | 17 |

18 | 19 | This extension is not affiliated with the University of Toronto 20 | 21 | Data for courses is provided by Cobalt API. Discount Textbook Store data was scrapped from a pdf file available on their website. Department website links were scrapped from UofT directory. 22 | 23 | ## License 24 | 25 | 26 | 27 | Any external libraries included in the code are covered by their respective licenses. 28 | Unless otherwise specified, any code included with the extension is covered by the MIT License. 29 | 30 | ANY FILES IN ./dependencies DIRECTORY ARE NOT COVERED BY THE PROJECT LICENSE 31 | 32 | ## Acknowledgements 33 | 34 | UofT Cobalt API - made creating the extension easy (Used in the earlier versions of the extension) 35 | 36 | UofT Nikel API - for brining back open data to uoft! 37 | 38 | Aniket Kali - web design help for settings, textbooks page, and about page 39 | 40 | Reddit user /u/mycrookedmouth - icon redesign 41 | -------------------------------------------------------------------------------- /src/settings/settings.js: -------------------------------------------------------------------------------- 1 | $(document).ready(() => { 2 | // Standard Google Universal Analytics code 3 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 4 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 5 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 6 | })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); // Note: https protocol here 7 | 8 | ga('create', 'UA-140776274-1\n', 'auto'); // Enter your GA identifier 9 | ga('set', 'checkProtocolTask', function(){}); // Removes failing protocol check. @see: http://stackoverflow.com/a/22152353/1958200 10 | ga('require', 'displayfeatures'); 11 | ga('send', 'pageview', '/setting.html'); 12 | 13 | chrome.storage.local.get({ 14 | // size: 'medium', 15 | link: 'website', 16 | breadths: true, 17 | highlight: false, 18 | prereq: true, 19 | inst: true, 20 | sess: true, 21 | descript: true, 22 | gsearch: true, 23 | maxtt: 1000, 24 | illegal: '', 25 | globoption: true 26 | 27 | }, items => { 28 | // $('#size').val(items.size); 29 | $('#link').val(items.link); 30 | $('#breadths').prop('checked', items.breadths); 31 | $('#highlight').prop('checked', items.highlight); 32 | $('#prerequisites').prop('checked', items.prereq); 33 | $('#sessions').prop('checked', items.sess); 34 | $('#gsearch').prop('checked', items.gsearch); 35 | $('#maxtt').val(items.maxtt); 36 | $('#instructors').prop('checked', items.inst); 37 | $('#description').prop('checked', items.descript); 38 | $('#enablepops').prop('checked', items.globoption); 39 | $('#illegal').val(items.illegal); 40 | }); 41 | 42 | $('input').change(apply); 43 | $('select').change(apply); 44 | $('#enablepops').change(() => { 45 | if ($("#enablepops").is(":checked")) chrome.browserAction.setIcon(on); 46 | else chrome.browserAction.setIcon(off); 47 | updateTabs() 48 | }); 49 | 50 | // $('#apply').click(function () { 51 | // apply(); 52 | // 53 | // alert("UofT Course Info: Settings applied successfully"); 54 | // }); 55 | 56 | function apply() { 57 | ga('send', 'event', { 58 | 'eventCategory': "Settings", 59 | 'eventAction': "Applied", 60 | }); 61 | 62 | chrome.storage.local.set({ 63 | link: $('#link').val(), 64 | // size: $('#size').val(), 65 | breadths: $('#breadths').prop('checked'), 66 | highlight: $('#highlight').prop('checked'), 67 | prereq: $('#prerequisites').prop('checked'), 68 | inst: $('#instructors').prop('checked'), 69 | sess: $('#sessions').prop('checked'), 70 | gsearch: $('#gsearch').prop('checked'), 71 | maxtt: $('#maxtt').val(), 72 | descript: $('#description').prop('checked'), 73 | illegal: $('#illegal').val(), 74 | globoption: $('#enablepops').prop('checked'), 75 | }); 76 | } 77 | }); -------------------------------------------------------------------------------- /docs/textbooks/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Textbooks 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 57 | 58 | 59 |
60 | 61 |
62 |
63 |
64 | 65 |
66 |
67 | 68 | 69 |
70 |
71 | 72 |
73 | 74 |
75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/contentscripts/contentScript.js: -------------------------------------------------------------------------------- 1 | /* DISCLAIMER: Most of the code in this file was not written by me 2 | * Original version of the code: https://stackoverflow.com/questions/40572679/change-matching-words-in-a-webpages-text-to-buttons/40576258 3 | * by stackoverflow user: https://stackoverflow.com/users/3773011/makyen 4 | * 5 | * The code was adapted to work with the rest of the extension 6 | */ 7 | 8 | (findCourses)(); 9 | 10 | function findCourses() { 11 | const T_STYLE = "style=\" font-weight:normal;color:#000080;;border: 1px #000080 double ;letter-spacing:1pt; word-spacing:2pt;font-size:12px;text-align:left;font-family:arial black, sans-serif;line-height:1; \""; 12 | 13 | let counter = 0; 14 | let highlight = false; 15 | 16 | chrome.storage.local.get({ 17 | illegal: '', 18 | highlight: false 19 | }, function (items) { 20 | let banned = []; 21 | if (items.illegal !== '') { 22 | banned = (items.illegal.replace(/\s/g, '')).split(','); 23 | } 24 | 25 | // default blacklist plus user banned 26 | const websites = ['duckduckgo', 'google', 'youtube'].concat(banned); 27 | const str = window.location.hostname; 28 | for (let i in websites) { 29 | if (str.includes(websites[i])) { 30 | return 31 | } 32 | } 33 | highlight = items.highlight; 34 | execute() 35 | } 36 | ); 37 | 38 | 39 | function delete_space(match) { 40 | if(match.length === 6) return match; 41 | return match.substring(0, 3) + match.substring(4); 42 | } 43 | 44 | function get_style() { 45 | if (highlight) return T_STYLE; 46 | return ''; 47 | } 48 | 49 | function replace(match) { 50 | const code = delete_space(match); 51 | return `${match.substring(0, 3)}${match.substring(3)}`; 52 | } 53 | 54 | 55 | function execute() { 56 | const match = new RegExp('\\b(?!for)[A-Z][A-Z][A-Z]\\s?[1-4a-d][0-9][0-9]', 'mgi'); 57 | 58 | 59 | function handleTextNode(textNode) { 60 | if (textNode.nodeName !== '#text' 61 | || textNode.parentNode.nodeName === 'SCRIPT' 62 | || textNode.parentNode.nodeName === 'STYLE' 63 | ) { 64 | return; 65 | } 66 | let origText = textNode.textContent; 67 | match.lastIndex = 0; 68 | let newHtml = origText.replace(match, replace); 69 | 70 | 71 | if (newHtml !== origText) { 72 | counter++; 73 | let newSpan = document.createElement('span'); 74 | newSpan.innerHTML = newHtml; 75 | newSpan.node; 76 | textNode.parentNode.replaceChild(newSpan, textNode); 77 | } 78 | } 79 | 80 | 81 | let textNodes = []; 82 | let nodeIter = document.createNodeIterator(document.body, NodeFilter.SHOW_TEXT); 83 | let currentNode; 84 | 85 | while (currentNode = nodeIter.nextNode()) { 86 | textNodes.push(currentNode); 87 | } 88 | 89 | textNodes.forEach(function (el) { 90 | handleTextNode(el); 91 | }); 92 | 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/contentscripts/util.js: -------------------------------------------------------------------------------- 1 | function upToDate(str) { 2 | let curr = new Date().getFullYear(); 3 | let prev = curr - 1; 4 | let next = curr + 1; 5 | return (str.includes(curr) || str.includes(prev) || str.includes(next)) 6 | 7 | } 8 | 9 | class TooManyCoursesError extends Error { 10 | } 11 | 12 | function fetchResource(url){ 13 | return new Promise((resolve, reject)=>{ 14 | chrome.runtime.sendMessage( 15 | {msg: "FETCH", url: url}, 16 | response => { 17 | console.log(response) 18 | resolve(response.response)}); 19 | }) 20 | } 21 | 22 | function getInfo(code) { 23 | return fetchResource(`https://nikel.ml/api/courses?code=${code}`) 24 | } 25 | 26 | 27 | 28 | //taken from: https://stackoverflow.com/questions/281264/remove-empty-elements-from-an-array-in-javascript 29 | function cleanArray(actual) { 30 | let newArray = []; 31 | for (let i = 0; i < actual.length; i++) { 32 | if (actual[i]) { 33 | newArray.push(actual[i]); 34 | } 35 | } 36 | return newArray; 37 | } 38 | 39 | Array.prototype.unique = function () { 40 | let a = []; 41 | for (let i = 0; i < this.length; i++) { 42 | let current = this[i]; 43 | if (a.indexOf(current) < 0) a.push(current); 44 | } 45 | return a; 46 | }; 47 | 48 | function getSettingsUrl() { 49 | return chrome.runtime.getURL( 50 | "src/settings/settings.html" 51 | ) 52 | } 53 | 54 | function getAboutUrl() { 55 | return chrome.runtime.getURL( 56 | "src/about/index.html" 57 | ) 58 | } 59 | 60 | function getTextbookUrl(code) { 61 | // if (navigator.userAgent.search("Firefox") > -1) { 62 | // return 'http://courseinfo.murad-akh.ca/textbooks/index.html?filter?q=course_code:%22' + code + '%22'; 63 | // } else { 64 | return 'http://courseinfo.murad-akh.ca/textbooks/index.html?filter?q=course_code:%22' + code + '%22'; 65 | // } 66 | } 67 | 68 | function sessionToLinks(sessions) { 69 | let output = ''; 70 | if (sessions.length === 0) return "Currently not Offered"; 71 | sessions.forEach((offering) => { 72 | let link = document.createElement('a'); 73 | link.setAttribute('href', 'http://coursefinder.utoronto.ca/course-search/search/courseInquiry?methodToCall=start&viewId=CourseDetails-InquiryView&courseId=' 74 | + offering.id); 75 | link.innerText = offering.term + '; '; 76 | output += link.outerHTML; 77 | } 78 | ); 79 | 80 | return output; 81 | } 82 | 83 | function crawlOfferings(info) { 84 | 85 | let profs = { 86 | all: [], 87 | utsg: [], 88 | utsc: [], 89 | utm: [] 90 | }; 91 | let sessions = { 92 | utsg: [], 93 | utsc: [], 94 | utm: [] 95 | }; 96 | 97 | for (let i = 0; i < info.length; i++) { 98 | if (!upToDate(info[i].term)) { 99 | continue; 100 | } 101 | 102 | let meets = info[i].meeting_sections; 103 | let curr_profs = []; 104 | for (let j = 0; j < meets.length; j++) { 105 | let instructors = meets[j].instructors; 106 | instructors.forEach(prof => { 107 | let inst_listing = prof.split(" "); 108 | for (let k = 0; k < inst_listing.length; k += 2) { 109 | curr_profs.push(inst_listing[k] + " " + inst_listing[k + 1]); 110 | } 111 | }); 112 | } 113 | profs.all = profs.all.concat(curr_profs); 114 | 115 | let campus = info[i].campus; 116 | if (campus === "St. George") { 117 | sessions.utsg.push({id: info[i].id, term: info[i].term}); 118 | profs.utsg = profs.utsg.concat(curr_profs); 119 | } else if (campus === "Scarborough") { 120 | sessions.utsc.push({id: info[i].id, term: info[i].term}); 121 | profs.utsc = profs.utsc.concat(curr_profs); 122 | } else if (campus === "Mississauga") { 123 | sessions.utm.push({id: info[i].id, term: info[i].term}); 124 | profs.utm = profs.utm.concat(curr_profs); 125 | } 126 | } 127 | 128 | $.each(profs, (index, value) => { 129 | profs[index] = value.unique(); 130 | profs[index] = cleanArray(profs[index]); 131 | }); 132 | 133 | return { 134 | profs: profs, 135 | sessions: sessions 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/about/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | About 6 | 7 | 8 | 9 | 10 | 43 |
44 |
45 |
UofT Course Info Chrome Extension. Developed by Murad Akhundov in 2017-2018.
46 |

Source code is available on my Github

47 |

If you are interested in getting involved in this or a similar project, consider joining UofT.dev

49 |

NOT AFFILIATED WITH THE UNIVERSITY OF TORONTO

50 | 51 |

52 | Data for courses is provided by Cobalt API. Discount Textbook Store data 53 | was scrapped from a pdf file available on their website. Department website links were scrapped from UofT 54 | directory. 55 |

56 | 57 |

58 | Check out my webpage if you want to see more of my stuff or get in touch. 59 | If you are on Android, consider giving 60 | T Map (For UofT) a shot! 61 |

62 | 63 |

64 | Community contributions:
65 | Aniket Kali - bootstrap
66 | Reddit user /u/mycrookedmouth - icon redesign 67 |

68 | 69 |

70 | Any external libraries included in the code are covered by their respective licenses.
71 | Unless otherwise specified, any code included with the extension is covered by the license below. 72 |

73 | 74 |

LICENSE

75 |

MIT License

76 | 77 |

Copyright (c) 2017-2018 Murad Akhundov

78 | 79 |

Permission is hereby granted, free of charge, to any person obtaining a copy 80 | of this software and associated documentation files (the "Software"), to deal 81 | in the Software without restriction, including without limitation the rights 82 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 83 | copies of the Software, and to permit persons to whom the Software is 84 | furnished to do so, subject to the following conditions:

85 | 86 |

The above copyright notice and this permission notice shall be included in all 87 | copies or substantial portions of the Software.

88 | 89 |

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 90 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 91 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 92 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 93 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 94 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 95 | SOFTWARE. 96 |

97 | 98 |
99 | 100 | 101 | -------------------------------------------------------------------------------- /src/contentscripts/background.js: -------------------------------------------------------------------------------- 1 | /*Licensed under MIT LICENSE 2 | * 3 | * Murad Akhundov 2017 4 | */ 5 | chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { 6 | if (changeInfo.status === 'complete') { 7 | gsearch(tab); 8 | execute(tab); 9 | acorn(tab) 10 | } 11 | }); 12 | 13 | 14 | function execute(tab) { 15 | chrome.storage.local.get({ 16 | globoption: true, 17 | urloptions: {} 18 | }, function (items) { 19 | if (!tab.url.includes('https://') && !tab.url.includes('http://')) return; 20 | if (items.urloptions[new URL(tab.url).hostname] !== false && items.globoption) { 21 | if (!/.*google\....?\/search\?.*/.test(tab.url)) { 22 | chrome.tabs.executeScript(tab.id, {file: '/src/contentscripts/contentScript.js'}); 23 | chrome.tabs.executeScript(tab.id, {file: '/dependencies/tippy/tippy.all.min.js'}); 24 | chrome.tabs.insertCSS(tab.id, {file: 'dependencies/bootstrap/bootstrapcustom.min.css'}); 25 | chrome.tabs.insertCSS(tab.id, {file: 'dependencies/tippy/light.css'}); 26 | chrome.tabs.executeScript(tab.id, {file: '/src/contentscripts/tooltip.js'}); 27 | chrome.tabs.executeScript(tab.id, {file: '/src/contentscripts/infiniteScroll.js'}); 28 | } 29 | } else { 30 | chrome.tabs.executeScript(tab.id, {file: "/src/contentscripts/purge.js"}); 31 | } 32 | }); 33 | } 34 | 35 | function gsearch(tab) { 36 | if (/.*google\....?\/search\?.*/.test(tab.url)) { 37 | chrome.tabs.insertCSS(tab.id, {file: 'dependencies/bootstrap/bootstrapcustom.min.css'}); 38 | chrome.tabs.executeScript(tab.id, {file: 'dependencies/bootstrap/bootstrap.bundle.min.js'}); 39 | chrome.tabs.executeScript(tab.id, {file: 'src/contentscripts/tooltip.js'}); 40 | chrome.tabs.executeScript(tab.id, {file: 'src/contentscripts/google.js'}); 41 | } 42 | } 43 | 44 | function acorn(tab) { 45 | if (/.*acorn\.utoronto\.ca.*/.test(tab.url)) { 46 | if(!localStorage.acornOne){ 47 | chrome.notifications.create('limit', { 48 | "type": "basic", 49 | "iconUrl": chrome.extension.getURL("/images/Acorn_128.png"), 50 | "title": "UofT Course Info", 51 | "message": "Don't want to see tooltips/popovers on Acorn? Click on the extension icon to disable!", 52 | }, (id) => { 53 | localStorage.acornOne = "1"; 54 | }); 55 | } 56 | } 57 | } 58 | 59 | // Standard Google Universal Analytics code 60 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 61 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 62 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 63 | })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); // Note: https protocol here 64 | 65 | ga('create', 'UA-140776274-1\n', 'auto'); // Enter your GA identifier 66 | ga('set', 'checkProtocolTask', function(){}); // Removes failing protocol check. @see: http://stackoverflow.com/a/22152353/1958200 67 | ga('require', 'displayfeatures'); 68 | 69 | chrome.runtime.onMessage.addListener( 70 | (request, sender, sendResponse) => { 71 | if (request.msg === 'TMN') { 72 | createNotification().then(sendResponse); 73 | } 74 | 75 | else if (request.msg === 'FETCH'){ 76 | console.log(request.url) 77 | fetch(request.url) 78 | .then(r => r.json()) 79 | .then(r => sendResponse(r)) 80 | .catch(console.error) 81 | } 82 | 83 | else if(request.msg === 'ANL') { 84 | ga('send', 'event', { 85 | 'eventCategory': request.eventCategory, 86 | 'eventAction': request.eventAction, 87 | 'eventLabel': request.eventLabel 88 | }); 89 | } 90 | 91 | 92 | return true; 93 | }); 94 | 95 | async function createNotification() { 96 | const buttons = [ 97 | { 98 | "title": "Ignore" 99 | }, { 100 | "title": "Settings" 101 | } 102 | ]; 103 | 104 | chrome.notifications.create('limit', { 105 | "type": "basic", 106 | "iconUrl": chrome.extension.getURL("/images/Acorn_128.png"), 107 | "title": "UofT Course Info", 108 | "message": "Could not load tooltips. Too many courses mentioned, you can change this limit in the settings or ignore on this page", 109 | "buttons": buttons 110 | }, (id) => { 111 | }); 112 | 113 | return new Promise((resolve, reject) => { 114 | 115 | chrome.notifications.onButtonClicked.addListener((id, index) => { 116 | chrome.notifications.clear(id); 117 | switch (buttons[index].title) { 118 | case "Settings": 119 | resolve({'msg': 'SETTINGS'}); 120 | break; 121 | case "Ignore": 122 | resolve({'msg': 'DISABLE'}) 123 | } 124 | }); 125 | 126 | chrome.notifications.onClosed.addListener(() => resolve({msg: 'NOTHING'})) 127 | }) 128 | } 129 | 130 | 131 | // chrome.runtime.onInstalled.addListener(function (details) { 132 | // if (details.reason === "update") { 133 | // let first_run = false; 134 | // if (!localStorage['ranb']) { 135 | // first_run = true; 136 | // localStorage['ranb'] = '1'; 137 | // } 138 | // 139 | // if (first_run) chrome.tabs.create({url: "http://courseinfo.murad-akh.ca/feedback/index.html"}); 140 | // } 141 | // }); 142 | 143 | 144 | -------------------------------------------------------------------------------- /docs/textbooks/textbooks.js: -------------------------------------------------------------------------------- 1 | const FB_URL = 'https://www.facebook.com/groups/183712131723915/for_sale_search/?forsalesearchtype=for_sale&availability=available&query='; 2 | const AMZN_URL = 'https://www.amazon.ca/s/field-keywords='; 3 | const EBAY_URL = 'https://www.ebay.ca/sch/i.html?_nkw='; 4 | const GOOG_URL = 'https://www.google.com/search?tbm=bks&q='; 5 | 6 | const cprovider = localStorage.provider !== undefined ? JSON.parse(localStorage.provider) : false; 7 | 8 | 9 | $(document).ready(function () { 10 | 11 | document.getElementById('search-books').addEventListener("keyup", function (event) { 12 | if (event.keyCode === 13) { 13 | handle(); 14 | } 15 | }); 16 | 17 | 18 | function handle() { 19 | let query = $('#search-books').val(); 20 | if (/[0-9]{13}/.test(query)) { 21 | window.location.href = `index.html?filter?q=isbn:"${query}"` 22 | } else if (/\b[a-zA-Z]{3}([1-4]|\b)([0-9]|\b)([0-9]|\b)/.test(query)) { 23 | window.location.href = `index.html?filter?q=course_code:"${query}"`; 24 | } else { 25 | window.location.href = `index.html?search?q="${query}"`; 26 | 27 | } 28 | } 29 | 30 | let url = window.location.href; 31 | let params = url.split('?'); 32 | if (params.length === 3) { 33 | let query = params[2].substring(params[2].indexOf('%') + 3, params[2].lastIndexOf('%')); 34 | displayBooks(fetcher(params[1] + '?' + params[2]), query); 35 | } 36 | }); 37 | 38 | function fetcher(query) { 39 | let json = []; 40 | let xmlhttp = new XMLHttpRequest(); 41 | xmlhttp.onreadystatechange = function () { 42 | if (this.readyState === 4 && this.status === 200) { 43 | json = JSON.parse(this.responseText); 44 | } 45 | }; 46 | xmlhttp.open("GET", `https://nikel.ml/api/textbooks${query}`, false); 47 | xmlhttp.send(); 48 | return json; 49 | } 50 | 51 | 52 | function displayBooks(json, query) { 53 | 54 | if (json.length === 0) { 55 | $('#no_results').html('No results for ' + query + ''); 56 | } 57 | 58 | json.forEach(function (item) { 59 | let courses = "Courses:"; 60 | item.courses.forEach(function (course) { 61 | courses += ` ${course.code}`; 62 | }); 63 | 64 | // monstrosity 65 | let title = `

${item.title} ${item.author}ISBN:${item.isbn}

`; 66 | let facebook = `Facebook`; 67 | let ebay = `Ebay`; 68 | let google = `Google Books`; 69 | let amazon = `Amazon`; 70 | let custom = ''; 71 | if (cprovider) custom = `${cprovider.providerNickName}`; 72 | 73 | const addCustom = `Add/Change Custom Search Provider (Advanced)`; 74 | 75 | let external = `

${facebook}${amazon}${ebay}${google}${custom}
${addCustom}`; 76 | let image = ``; 77 | let bookstore = 'UofT BookStore
'; 78 | bookstore += (`new: $${item.price}
`); 79 | bookstore += `See the listing for more information, including the price for a used item`; 80 | 81 | 82 | let content = `
${courses}

${bookstore}${addDbook(item.isbn)}${external}
`; 83 | $('#accordion').prepend(`${title}
${image}${content}
`) 84 | }); 85 | $('#addCustom').click(addProvider); 86 | 87 | $('#accordion').accordion({ 88 | collapsible: true, 89 | heightStyle: "content" 90 | }); 91 | } 92 | 93 | 94 | function getDiscount() { 95 | let json = []; 96 | let xmlhttp = new XMLHttpRequest(); 97 | xmlhttp.onreadystatechange = function () { 98 | if (this.readyState === 4 && this.status === 200) { 99 | json = JSON.parse(this.responseText); 100 | } 101 | }; 102 | xmlhttp.open("GET", 'https://courseinfo.murad-akh.ca/textbooks/discounttb.json', false); 103 | xmlhttp.send(); 104 | return json; 105 | } 106 | 107 | function addProvider(){ 108 | const providerUrl = window.prompt("Enter ISBN search provider url, in format: www.exampleprovider.com/search?q= "); 109 | const providerNickName = window.prompt("Add a nickname"); 110 | localStorage.provider = JSON.stringify({providerNickName, providerUrl}); 111 | } 112 | 113 | function addDbook(isbn) { 114 | let discount = getDiscount(); 115 | let output = 'Discount Textbooks Store
'; 116 | if (discount[isbn] == null) { 117 | output += 'Not found
'; 118 | } else { 119 | if (discount[isbn]['new'] !== 'N/A') { 120 | output += 'new: $' + discount[isbn]['new'] + '
' 121 | 122 | } else 123 | output += '
'; 124 | if (discount[isbn]['used'] !== 'N/A') { 125 | output += 'used: $ ' + discount[isbn]['used'] 126 | 127 | } else { 128 | output += '
'; 129 | 130 | } 131 | 132 | } 133 | 134 | return '

' + output + '
Visit the website for more info'; 135 | } 136 | -------------------------------------------------------------------------------- /src/settings/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Settings 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 47 | 48 |
49 |
50 |
51 |
52 | 53 |

General

54 |
55 |
56 |
Highlight Detected Courses
57 |
58 |
59 | 60 | 61 |
62 |
63 |
64 |
65 |
Google Search Info Cards
66 |
67 |
68 | 69 | 70 |
71 |
72 |
73 |
74 | 75 |
76 | 77 |
78 |
79 |
80 |
81 | 82 |
83 |
84 | 85 | Separate these by commas
ie. google.com, 86 | piazza.com 87 |
88 |
89 |
90 |
91 | 92 | 93 |

Tooltip

94 |
95 |
96 |
Enable Tooltips
97 |
98 |
99 | 100 | 101 |
102 |
103 |
104 |
105 | 106 |
107 |
108 | 112 |
Example invalid custom select feedback
113 |
114 |
115 |
116 |
117 |
Description
118 |
119 |
120 | 121 | 122 |
123 |
124 |
125 |
126 |
Sessions
127 |
128 |
129 | 130 | 131 |
132 |
133 |
134 |
135 |
Instructors
136 |
137 |
138 | 139 | 140 |
141 |
142 |
143 |
144 |
Prerequisites and Exclusions
145 |
146 |
147 | 148 | 149 |
150 |
151 |
152 |
153 |
Breadths
154 |
155 |
156 | 157 | 158 |
159 |
160 |
161 |
162 |
163 |
164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /src/contentscripts/tooltip.js: -------------------------------------------------------------------------------- 1 | 'use-strict'; 2 | /*Licensed under MIT LICENSE 3 | * 4 | * Murad Akhundov 2017 5 | */ 6 | $(document).ready(() => generateTooltips(true)); 7 | 8 | 9 | /** Generate Tooltips for previously labeled course codes 10 | * 11 | */ 12 | function generateTooltips(notify) { 13 | // let S_SIZE; 14 | let S_LINK; 15 | let S_BREADTH; 16 | let S_PREEXL; 17 | let S_INST; 18 | let S_OFFR; 19 | let S_MAXT; 20 | let S_DESRPT; 21 | 22 | let fetched = new Set(); 23 | let directory; 24 | let num = ($(".corInf").length); 25 | 26 | fetchStoredData() 27 | .then(requestForTooltips) 28 | .catch(error => { 29 | if (!error instanceof TooManyCoursesError) { 30 | console.error(error) 31 | } 32 | }); 33 | 34 | /** Fetches Stored information. Directory and settings. 35 | */ 36 | async function fetchStoredData() { 37 | let dirpromise = await fetch( 38 | chrome.runtime.getURL("/../../data/directory.json") 39 | ); 40 | directory = await dirpromise.json(); 41 | await getSettings(); 42 | if (num > S_MAXT) { 43 | try { 44 | handleTooManyCourses(); 45 | } catch (e) { 46 | console.log(e) 47 | } 48 | throw new TooManyCoursesError(); 49 | } 50 | } 51 | 52 | 53 | function getSettings() { 54 | return new Promise((resolve, reject) => { 55 | chrome.storage.local.get({ 56 | // size: 'medium', 57 | link: 'website', 58 | breadths: true, 59 | prereq: true, 60 | inst: true, 61 | sess: true, 62 | descript: true, 63 | maxtt: 300 64 | 65 | }, items => { 66 | if (chrome.runtime.lastError) { 67 | reject(chrome.runtime.lastError.message); 68 | } else { 69 | S_LINK = items.link; 70 | S_BREADTH = items.breadths; 71 | S_PREEXL = items.prereq; 72 | S_OFFR = items.sess; 73 | S_MAXT = items.maxtt; 74 | S_INST = items.inst; 75 | S_DESRPT = items.descript; 76 | resolve(); 77 | } 78 | }); 79 | }); 80 | } 81 | 82 | 83 | /** Get information from cobalt, load when done. 84 | * 85 | * @param code 86 | */ 87 | /** Get information from cobalt, load when done. 88 | * 89 | * @param code 90 | */ 91 | function buildWithinfo(code) { 92 | getInfo(code).then( 93 | response => { 94 | fetched.add(code); 95 | load(code, response) 96 | } 97 | ) 98 | } 99 | 100 | function getDepartment(key) { 101 | if (S_LINK === "artsci") { 102 | key = key.replace(/ /g, "-"); 103 | return `https://fas.calendar.utoronto.ca/section/${key}`; 104 | } else { 105 | 106 | for (let i = 0; i < directory.length; i++) { 107 | let name = directory[i].name.toString().toUpperCase(); 108 | key = key.toUpperCase(); 109 | if (name.startsWith(key)) { 110 | return directory[i].url; 111 | } 112 | } 113 | return "https://www.utoronto.ca/a-to-z-directory"; 114 | } 115 | 116 | } 117 | 118 | 119 | function getOffers(sessions) { 120 | return `UTSG: ${sessionToLinks(sessions.utsg)}
UTM: ${sessionToLinks(sessions.utm)}
UTSC: ${sessionToLinks(sessions.utsc)}`; 121 | } 122 | 123 | function getProfs(profs) { 124 | return `Instructors: ${profs.join(", ")}`; 125 | 126 | } 127 | 128 | /** extract the details: breadths, prereqs, exclusions 129 | * 130 | * @param info 131 | * @returns {string} 132 | */ 133 | function getDetails(info) { 134 | let breadths = info[0].breadths; 135 | if (!breadths || breadths.length === 0) { 136 | breadths = "N/A" 137 | } 138 | 139 | let output = ""; 140 | if (S_PREEXL) { 141 | output += 142 | "Prerequisites: " + (info[0].prerequisites || "N/A") + "
" + 143 | "Exclusions: " + (info[0].exclusions || "N/A") + "
"; 144 | } 145 | if (S_BREADTH) { 146 | output = output + "Breadths: " + breadths + "
" 147 | } 148 | return output 149 | 150 | } 151 | 152 | /** Get the title bar string 153 | * 154 | * @param info 155 | * @returns {string} 156 | */ 157 | function getTitle(info) { 158 | let dept = getDepartment(info[0].department); 159 | let deptlink = document.createElement("a"); 160 | deptlink.setAttribute('href', dept); 161 | deptlink.className = 'card-link'; 162 | deptlink.setAttribute('style', 'margin-left: 10px; float: right; color: lightgray; text-decoration: underline'); 163 | deptlink.innerHTML = '' + info[0].department + ''; 164 | 165 | 166 | return `${info[0].name}${deptlink.outerHTML}`; 167 | 168 | } 169 | 170 | /** Request each course code from cobalt 171 | * If previously requested, return it from the hashmap 172 | */ 173 | function requestForTooltips() { 174 | $('.corInf').each(function () { 175 | let title = $(this).data('code'); 176 | if($(this).data('fetched') === 'true') return; 177 | if (!fetched.has(title)) { 178 | buildWithinfo(title) 179 | } 180 | $(this).data('fetched', 'true') 181 | }) 182 | } 183 | 184 | /** Load each tooltip as cobalt responds 185 | * called asynchronously 186 | * 187 | * @param code a course code (original fetch) 188 | * @param info array fetched from cobalt 189 | */ 190 | function load(code, info) { 191 | try { 192 | let a = info[0].name; 193 | tippy(`.${code}`, { 194 | content: buildPopover(code, info), 195 | arrow: true, 196 | arrowType: 'wide', 197 | distance: 0, 198 | size: 'small', 199 | theme: 'light', 200 | interactive: 'true', 201 | maxWidth: 700, 202 | delay: [450, 20] 203 | }); 204 | 205 | chrome.runtime.sendMessage( 206 | { 207 | msg: "ANL", 208 | eventCategory: 'Courses', 209 | eventAction: 'Tooltip', 210 | eventLabel: code 211 | 212 | }, () => { 213 | }) 214 | } catch (err) { 215 | console.error(err) 216 | $('.' + code).each(function () { 217 | $(this).replaceWith($(this).data('title')); 218 | }) 219 | } 220 | 221 | 222 | } 223 | 224 | 225 | function handleTooManyCourses() { 226 | $(".corInf").each(function () { 227 | $(this).replaceWith($(this).data('title')); 228 | 229 | }); 230 | let warning = localStorage.warning || "true"; 231 | if (warning === "true" && notify) { 232 | // let show = confirm("UofT Course Info: did not load the tooltips, too many courses mentioned. " + 233 | // "\n\n" + 234 | // "The current limit is " + S_MAXT + ", you can now change it in the settings" + 235 | // "\n\n Click 'Cancel' to never see this popup again"); 236 | 237 | chrome.runtime.sendMessage({msg: "TMN"}, response => { 238 | switch (response.msg) { 239 | case 'DISABLE': 240 | localStorage.warning = "false"; 241 | break; 242 | case 'SETTINGS': 243 | window.open(getSettingsUrl(), '_blank') 244 | } 245 | }); 246 | } 247 | } 248 | 249 | /** Builds a tooltip/popover card with all the info 250 | * 251 | * @param code original course code 252 | * @param info array of fetched courses from cobalt 253 | * @returns {string} popover card HTML 254 | */ 255 | function buildPopover(code, info) { 256 | 257 | let crawled = crawlOfferings(info); 258 | 259 | let slink = document.createElement("a"); 260 | slink.setAttribute("href", getSettingsUrl()); 261 | slink.className = 'font-weight-bold'; 262 | slink.setAttribute('style', 'margin-left: 10px;'); 263 | slink.innerText = "Configure Extension"; 264 | 265 | let tlink = document.createElement("a"); 266 | tlink.className = 'font-weight-bold'; 267 | tlink.setAttribute("href", getTextbookUrl(code)); 268 | tlink.innerText = code.toUpperCase() + " Textbooks"; 269 | 270 | let main = document.createElement("div"); 271 | main.className = "bootstrapiso"; 272 | 273 | let card = document.createElement("div"); 274 | // card.setAttribute("style", "all: initial"); 275 | card.className = "card"; 276 | 277 | let body = document.createElement("div"); 278 | body.className = "card-body"; 279 | 280 | let description = document.createElement("p"); 281 | description.className = "card-text"; 282 | description.innerText = info[0].description; 283 | 284 | let details = document.createElement("p"); 285 | details.className = "card-text"; 286 | details.innerHTML = getDetails(info); 287 | 288 | let offerings = document.createElement("p"); 289 | offerings.className = "card-text"; 290 | offerings.innerHTML = getOffers(crawled.sessions); 291 | 292 | let extralinks = document.createElement("span"); 293 | extralinks.setAttribute('style', 'float: right; margin-left: 10px;'); 294 | extralinks.innerHTML = `${tlink.outerHTML} ${slink.outerHTML}`; 295 | 296 | let lastline = document.createElement("p"); 297 | lastline.className = "card-text"; 298 | if (S_INST) lastline.innerHTML = getProfs(crawled.profs.all); 299 | lastline.append(extralinks); 300 | 301 | let heading = document.createElement("div"); 302 | heading.className = "card-header bg-primary text-white"; 303 | 304 | let card_title = document.createElement("h6"); 305 | card_title.className = "card-text"; 306 | card_title.innerHTML = `${code.toUpperCase()}: ${getTitle(info)}`; 307 | 308 | card.append(heading); 309 | card.append(body); 310 | heading.append(card_title); 311 | if (S_DESRPT) body.append(description); 312 | if (S_BREADTH || S_PREEXL) body.append(details); 313 | if (S_OFFR) body.append(offerings); 314 | body.append(lastline); 315 | main.append(card); 316 | 317 | return main.outerHTML; 318 | } 319 | } -------------------------------------------------------------------------------- /src/contentscripts/google.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | let queryDict = {}; 3 | location.search.substr(1).split("&").forEach(function (item) { 4 | queryDict[item.split("=")[0]] = item.split("=")[1] 5 | }); 6 | 7 | if (!/[a-zA-Z]{3}\+?[1-4a-dA-D][0-9]{2}/.test(queryDict["q"])) return; // doesn't match 8 | 9 | checkSettings() 10 | .then(enabled => enabled ? getInfo(queryDict["q"].replace("+", "")) : null) 11 | .then(generateCards) 12 | .catch(error => console.error(error)) 13 | 14 | })(); 15 | 16 | function checkSettings() { 17 | return new Promise((resolve, reject) => { 18 | chrome.storage.local.get( 19 | { 20 | gsearch: true 21 | }, function (items) { 22 | if (chrome.runtime.lastError) { 23 | reject(chrome.runtime.lastError.message); 24 | } else { 25 | resolve(items.gsearch); 26 | } 27 | }); 28 | }) 29 | } 30 | 31 | function generateCards(response){ 32 | if (response.length > 0) { 33 | $(".card-section").parent().remove(); 34 | 35 | let boot = document.createElement("div"); 36 | boot.className = "bootstrapiso"; 37 | 38 | response[0]["crawled"] = crawlOfferings(response); 39 | const code = response[0].code.substr(0, 6); 40 | let card = createCard(code, response[0]); 41 | boot.append(card); 42 | 43 | chrome.runtime.sendMessage( 44 | {msg: "ANL", 45 | eventCategory: 'Courses', 46 | eventAction: 'Search', 47 | eventLabel: code 48 | 49 | }, () => {}); 50 | 51 | if (code.includes('csc')) { 52 | const num = parseInt(code.substring(3, 7)); 53 | const cos = document.createElement('small'); 54 | cos.appendChild(document.createTextNode(`Cosecant of ${num} is ${1/ Math.sin(num)}`)); 55 | cos.className = "text-muted"; 56 | cos.setAttribute('style', 'float: right; margin-top: 5px; margin-bottom: -10px'); 57 | boot.appendChild(cos); 58 | 59 | }else{ 60 | card.setAttribute("style", "margin-bottom: 20px"); 61 | } 62 | 63 | $("#topstuff").append(boot); 64 | 65 | generateTooltips(); 66 | 67 | $(".cinfo-link").click( function(event){ 68 | chrome.runtime.sendMessage( 69 | {msg: "ANL", 70 | eventCategory: 'Navigation', 71 | eventAction: 'Search Navigation', 72 | eventLabel: event.target.innerText 73 | 74 | }, () => {}); 75 | }); 76 | } 77 | } 78 | 79 | 80 | function createHeader(code, name, department) { 81 | let header = document.createElement("div"); 82 | header.className = "card-header"; 83 | // header.setAttribute("style", "background: navy;"); 84 | 85 | let card_title = document.createElement("h5"); 86 | card_title.className = "card-title"; 87 | card_title.innerText = code.toUpperCase() + ": " + name; 88 | 89 | let subtitle = document.createElement("h6"); 90 | subtitle.className = "card-subtitle mb-2 text-muted"; 91 | subtitle.innerText = department; 92 | 93 | let nav = document.createElement("ul"); 94 | nav.className = "nav nav-tabs card-header-tabs pull-right"; 95 | nav.setAttribute("role", "tablist"); 96 | 97 | let first = true; 98 | ["Overview", "Requirements", "Instructors", "Offerings"].forEach(name => { 99 | let row = document.createElement("li"); 100 | row.className = "nav-item"; 101 | 102 | let link = document.createElement("a"); 103 | link.innerText = name; 104 | let link_class = "nav-link cinfo-link"; 105 | if (first) link_class += " active"; 106 | link.className = link_class; 107 | link.setAttribute("id", name.toLowerCase() + "-tab"); 108 | link.setAttribute("data-toggle", "tab"); 109 | link.setAttribute("href", "#" + name.toLowerCase()); 110 | link.setAttribute("role", "tab"); 111 | link.setAttribute("aria-controls", name.toLowerCase()); 112 | link.setAttribute("aria-selected", first.toString()); 113 | link.setAttribute("style", "text-decoration: none !important;"); 114 | first = false; 115 | row.append(link); 116 | nav.append(row); 117 | 118 | }); 119 | header.append(card_title); 120 | header.append(subtitle); 121 | header.append(nav); 122 | return header; 123 | } 124 | 125 | function createOverview(parent, code, info) { 126 | let textbooks = document.createElement("a"); 127 | textbooks.className = "btn btn-primary cinfo-link"; 128 | textbooks.setAttribute("href", `http://courseinfo.murad-akh.ca/textbooks/index.html?filter?q=course_code:%22${code}%22`); 129 | textbooks.innerText = "View Textbooks"; 130 | 131 | let exams = document.createElement("a"); 132 | exams.className = "btn btn-primary cinfo-link"; 133 | exams.setAttribute("style", "margin-left: 10px;"); 134 | exams.setAttribute("href", `https://exams-library-utoronto-ca.myaccess.library.utoronto.ca/simple-search?location=%2F&query=${code}`); 135 | exams.innerText = "View Past Exams"; 136 | 137 | let description_element = document.createElement("p"); 138 | description_element.className = "card-text"; 139 | description_element.appendChild(document.createTextNode(info.description)); 140 | 141 | parent.append(description_element); 142 | parent.append(textbooks); 143 | parent.append(exams); 144 | } 145 | 146 | function createRequirements(parent, code, info) { 147 | const format = new RegExp('[A-Z][A-Z][A-Z][1-4a-d][0-9][0-9]', 'mgi'); 148 | if(info.prerequisites === null) info.prerequisites = "" 149 | let prerequisites = document.createElement("p"); 150 | prerequisites.className = "card-text"; 151 | prerequisites.innerHTML = `Prerequisites: ${info.prerequisites.replace(format, replace)}`; 152 | 153 | let exclusions = document.createElement("p"); 154 | exclusions.className = "card-text"; 155 | exclusions.innerHTML = `Exclusions: ${info.exclusions.replace(format, replace)}`; 156 | 157 | let breadths = document.createElement("p"); 158 | breadths.className = "card-text"; 159 | breadths.innerHTML = `Breadths: ${info.breadths}`; 160 | 161 | parent.append(prerequisites); 162 | parent.append(exclusions); 163 | parent.append(breadths); 164 | } 165 | 166 | function createOfferings(parent, code, info) { 167 | let utsg = document.createElement("p"); 168 | utsg.className = "card-text"; 169 | utsg.innerHTML = `UTSG: ${sessionToLinks(info.crawled.sessions.utsg)}`; 170 | 171 | let utsc = document.createElement("p"); 172 | utsc.className = "card-text"; 173 | utsc.innerHTML = `UTSC: ${sessionToLinks(info.crawled.sessions.utsc)}`; 174 | 175 | let utm = document.createElement("p"); 176 | utm.className = "card-text"; 177 | utm.innerHTML = `UTM: ${sessionToLinks(info.crawled.sessions.utm)}`; 178 | 179 | parent.append(utsg); 180 | parent.append(utsc); 181 | parent.append(utm); 182 | } 183 | 184 | function createInstructors(parent, code, info) { 185 | let utsg = document.createElement("p"); 186 | utsg.className = "card-text"; 187 | Promise.all(uoftprofsFetch(info.crawled.profs.utsg, 'S', code)) 188 | .then(response => utsg.innerHTML = `UTSG: ${response.join(', ')}`) 189 | .catch(err => console.error(err)); 190 | 191 | let utsc = document.createElement("p"); 192 | utsc.className = "card-text"; 193 | utsc.innerHTML = `UTSC: ${info.crawled.profs.utsc.join(', ')}`; 194 | 195 | let utm = document.createElement("p"); 196 | utm.className = "card-text"; 197 | Promise.all(uoftprofsFetch(info.crawled.profs.utm, 'M', code)) 198 | .then(response => utm.innerHTML = `UTM: ${response.join(', ')}`) 199 | .catch(err => console.error(err)); 200 | 201 | //commented out bit looks too ugly TODO: make not ugly 202 | 203 | // let credit = document.createElement('p'); 204 | // credit.innerText = "Official U of T course evaluations data. Analysis provided by uoftprofs.com"; 205 | // credit.className = 'card-text text-secondary'; 206 | 207 | // parent.append(credit); 208 | parent.append(utsg); 209 | parent.append(utsc); 210 | parent.append(utm); 211 | } 212 | 213 | function uoftprofsFetch(profs, campus, code) { 214 | let promises = []; 215 | profs.forEach(prof => { 216 | promises.push(new Promise(resolve => { 217 | // fetchResource(`https://uoft-course-info.firebaseio.com/profs/${campus}${prof.split(' ').join('')}.json`) 218 | // .then(response => resolve(proflink(prof, response, code, campus))) 219 | // .catch(err => { 220 | // console.error(err); 221 | // resolve(prof); 222 | // }) 223 | resolve(prof) 224 | })) 225 | }); 226 | 227 | return promises; 228 | } 229 | 230 | function proflink(prof, fullname, code, campus) { 231 | if (fullname === null) return prof; 232 | else { 233 | let link = document.createElement("a"); 234 | link.setAttribute('href', 'http://uoftprofs.com/?' 235 | + $.param({ 236 | campus: campus === 'S' ? 'St. George' : 'Mississauga', 237 | course: code.toUpperCase(), 238 | instructor: fullname.name 239 | })); 240 | link.className = 'cinfo-link'; 241 | link.innerText = fullname.name; 242 | return link.outerHTML; 243 | } 244 | } 245 | 246 | function createCard(code, info) { 247 | let card = document.createElement("div"); 248 | card.className = "card"; 249 | card.setAttribute("style", "margin-bottom: 0"); 250 | 251 | let body = document.createElement("div"); 252 | body.className = "card-body"; 253 | 254 | let extension_label = document.createElement("div"); 255 | extension_label.className = "card-footer"; 256 | 257 | let extension_text = document.createElement("small"); 258 | extension_text.className = "text-muted"; 259 | extension_text.appendChild(document.createTextNode("Provided by UofT Course Info Extension. Not affiliated with University of Toronto or Google.")); 260 | 261 | const feedback = document.createElement('a'); 262 | feedback.setAttribute("style", "float: right; color: mediumvioletred"); 263 | feedback.setAttribute("href", "https://forms.gle/eBBmuiywqHjiM8aE8"); 264 | feedback.appendChild(document.createTextNode("Feedback :)")); 265 | extension_text.appendChild(feedback); 266 | 267 | 268 | let content = document.createElement("div"); 269 | content.className = "tab-content"; 270 | 271 | let first = true; 272 | [ 273 | {n: "overview", f: createOverview}, 274 | {n: "requirements", f: createRequirements}, 275 | {n: "offerings", f: createOfferings}, 276 | {n: "instructors", f: createInstructors} 277 | 278 | ].forEach(obj => { 279 | let tab = document.createElement("div"); 280 | obj.f(tab, code, info); 281 | let tab_class = "tab-pane"; 282 | if (first) tab_class += " show active"; 283 | tab.className = tab_class; 284 | tab.setAttribute("aria-labelledby", obj.n + "-tab"); 285 | tab.setAttribute("id", obj.n); 286 | tab.setAttribute("role", "tabpanel"); 287 | first = false; 288 | content.append(tab); 289 | }); 290 | 291 | 292 | card.append(createHeader(code, info.name, info.department)); 293 | body.append(content); 294 | card.append(body); 295 | card.append(extension_label); 296 | extension_label.append(extension_text); 297 | return card; 298 | 299 | } 300 | 301 | function replace(match) { 302 | return `${match}`; 303 | } -------------------------------------------------------------------------------- /dependencies/tippy/tippy.all.min.js: -------------------------------------------------------------------------------- 1 | (function(e,t){'object'==typeof exports&&'undefined'!=typeof module?module.exports=t():'function'==typeof define&&define.amd?define(t):e.tippy=t()})(this,function(){'use strict';function e(e){return e&&'[object Function]'==={}.toString.call(e)}function t(e,t){if(1!==e.nodeType)return[];var r=e.ownerDocument.defaultView,a=r.getComputedStyle(e,null);return t?a[t]:a}function r(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function a(e){if(!e)return document.body;switch(e.nodeName){case'HTML':case'BODY':return e.ownerDocument.body;case'#document':return e.body;}var p=t(e),o=p.overflow,i=p.overflowX,n=p.overflowY;return /(auto|scroll|overlay)/.test(o+n+i)?e:a(r(e))}function p(e){return 11===e?ke:10===e?Ee:ke||Ee}function o(e){if(!e)return document.documentElement;for(var r=p(10)?document.body:null,a=e.offsetParent||null;a===r&&e.nextElementSibling;)a=(e=e.nextElementSibling).offsetParent;var i=a&&a.nodeName;return i&&'BODY'!==i&&'HTML'!==i?-1!==['TH','TD','TABLE'].indexOf(a.nodeName)&&'static'===t(a,'position')?o(a):a:e?e.ownerDocument.documentElement:document.documentElement}function n(e){var t=e.nodeName;return'BODY'!==t&&('HTML'===t||o(e.firstElementChild)===e)}function s(e){return null===e.parentNode?e:s(e.parentNode)}function l(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return document.documentElement;var r=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,a=r?e:t,p=r?t:e,i=document.createRange();i.setStart(a,0),i.setEnd(p,0);var d=i.commonAncestorContainer;if(e!==d&&t!==d||a.contains(p))return n(d)?d:o(d);var m=s(e);return m.host?l(m.host,t):l(e,s(t).host)}function d(e){var t=1=r.clientWidth&&a>=r.clientHeight}),d=0o&&(s=ee(r,window.innerWidth-o)),n&&l>o&&(l=ee(a,window.innerHeight-o));var d=q.reference.getBoundingClientRect(),m=q.props.followCursor,c='horizontal'===m,f='vertical'===m;q.popperInstance.reference={getBoundingClientRect:function(){return{width:0,height:0,top:c?d.top:l,bottom:c?d.bottom:l,left:f?d.left:s,right:f?d.right:s}},clientWidth:0,clientHeight:0},q.popperInstance.scheduleUpdate()}}function o(e){var t=pt(e.target,q.props.target);t&&!t._tippy&&($(t,ie({},q.props,{target:'',showOnInit:!0})),i(e))}function i(e){if(T(),!q.state.isVisible){if(q.props.target)return o(e);if(W=!0,q.props.wait)return q.props.wait(q,e);x()&&document.addEventListener('mousemove',p);var t=Ve(q.props.delay,0,ne.delay);t?H=setTimeout(function(){Y()},t):Y()}}function n(){if(T(),!q.state.isVisible)return s();W=!1;var e=Ve(q.props.delay,1,ne.delay);e?R=setTimeout(function(){q.state.isVisible&&S()},e):S()}function s(){document.removeEventListener('mousemove',p),N=null}function l(){document.body.removeEventListener('mouseleave',n),document.removeEventListener('mousemove',_)}function d(e){!q.state.isEnabled||y(e)||(!q.state.isVisible&&(I=e),'click'===e.type&&!1!==q.props.hideOnClick&&q.state.isVisible?n():i(e))}function m(e){var t=ot(e.target,function(e){return e._tippy}),r=pt(e.target,Xe.POPPER)===q.popper,a=t===q.reference;r||a||ut(xt(q.popper),q.popper.getBoundingClientRect(),e,q.props)&&(l(),n())}function c(e){return y(e)?void 0:q.props.interactive?(document.body.addEventListener('mouseleave',n),void document.addEventListener('mousemove',_)):void n()}function f(e){if(e.target===q.reference){if(q.props.interactive){if(!e.relatedTarget)return;if(pt(e.relatedTarget,Xe.POPPER))return}n()}}function h(e){pt(e.target,q.props.target)&&i(e)}function b(e){pt(e.target,q.props.target)&&n()}function y(e){var t=-1l[e]&&!t.escapeWithReference&&(a=ee(m[r],l[e]-('right'===e?m.width:m.height))),Le({},r,a)}};return d.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';m=Te({},m,c[t](e))}),e.offsets.popper=m,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,r=t.popper,a=t.reference,p=e.placement.split('-')[0],o=te,i=-1!==['top','bottom'].indexOf(p),n=i?'right':'bottom',s=i?'left':'top',l=i?'width':'height';return r[n]o(a[n])&&(e.offsets.popper[s]=o(a[n])),e}},arrow:{order:500,enabled:!0,fn:function(e,r){var a;if(!V(e.instance.modifiers,'arrow','keepTogether'))return e;var p=r.element;if('string'==typeof p){if(p=e.instance.popper.querySelector(p),!p)return e;}else if(!e.instance.popper.contains(p))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var o=e.placement.split('-')[0],i=e.offsets,n=i.popper,s=i.reference,l=-1!==['left','right'].indexOf(o),d=l?'height':'width',m=l?'Top':'Left',c=m.toLowerCase(),f=l?'left':'top',h=l?'bottom':'right',y=C(p)[d];s[h]-yn[h]&&(e.offsets.popper[c]+=s[c]+y-n[h]),e.offsets.popper=b(e.offsets.popper);var u=s[c]+s[d]/2-y/2,g=t(e.instance.popper),x=parseFloat(g['margin'+m],10),w=parseFloat(g['border'+m+'Width'],10),v=u-e.offsets.popper[c]-x-w;return v=ae(ee(n[d]-y,v),0),e.arrowElement=p,e.offsets.arrow=(a={},Le(a,c,re(v)),Le(a,f,''),a),e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(D(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var r=v(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed),a=e.placement.split('-')[0],p=L(a),o=e.placement.split('-')[1]||'',i=[];switch(t.behavior){case Pe.FLIP:i=[a,p];break;case Pe.CLOCKWISE:i=j(a);break;case Pe.COUNTERCLOCKWISE:i=j(a,!0);break;default:i=t.behavior;}return i.forEach(function(n,s){if(a!==n||i.length===s+1)return e;a=e.placement.split('-')[0],p=L(a);var l=e.offsets.popper,d=e.offsets.reference,m=te,c='left'===a&&m(l.right)>m(d.left)||'right'===a&&m(l.left)m(d.top)||'bottom'===a&&m(l.top)m(r.right),b=m(l.top)m(r.bottom),u='left'===a&&f||'right'===a&&h||'top'===a&&b||'bottom'===a&&y,g=-1!==['top','bottom'].indexOf(a),x=!!t.flipVariations&&(g&&'start'===o&&f||g&&'end'===o&&h||!g&&'start'===o&&b||!g&&'end'===o&&y);(c||u||x)&&(e.flipped=!0,(c||u)&&(a=i[s+1]),x&&(o=q(o)),e.placement=a+(o?'-'+o:''),e.offsets.popper=Te({},e.offsets.popper,T(e.instance.popper,e.offsets.reference,e.placement)),e=S(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport'},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,r=t.split('-')[0],a=e.offsets,p=a.popper,o=a.reference,i=-1!==['left','right'].indexOf(r),n=-1===['top','left'].indexOf(r);return p[i?'left':'top']=o[r]-(n?p[i?'width':'height']:0),e.placement=L(t),e.offsets.popper=b(p),e}},hide:{order:800,enabled:!0,fn:function(e){if(!V(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,r=A(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottomr.right||t.top>r.bottom||t.rightwindow.devicePixelRatio||!Ae),c='bottom'===r?'top':'bottom',f='right'===a?'left':'right',h=X('transform'),b=void 0,y=void 0;if(y='bottom'==c?'HTML'===s.nodeName?-s.clientHeight+m.bottom:-l.height+m.bottom:m.top,b='right'==f?'HTML'===s.nodeName?-s.clientWidth+m.right:-l.width+m.right:m.left,n&&h)d[h]='translate3d('+b+'px, '+y+'px, 0)',d[c]=0,d[f]=0,d.willChange='transform';else{var g='bottom'==c?-1:1,x='right'==f?-1:1;d[c]=y*g,d[f]=b*x,d.willChange=c+', '+f}var w={"x-placement":e.placement};return e.attributes=Te({},w,e.attributes),e.styles=Te({},d,e.styles),e.arrowStyles=Te({},e.offsets.arrow,e.arrowStyles),e},gpuAcceleration:!0,x:'bottom',y:'right'},applyStyle:{order:900,enabled:!0,fn:function(e){return _(e.instance.popper,e.styles),F(e.instance.popper,e.attributes),e.arrowElement&&Object.keys(e.arrowStyles).length&&_(e.arrowElement,e.arrowStyles),e},onLoad:function(e,t,r,a,p){var o=O(p,t,e,r.positionFixed),i=E(r.placement,o,t,e,r.modifiers.flip.boundariesElement,r.modifiers.flip.padding);return t.setAttribute('x-placement',i),_(t,{position:r.positionFixed?'fixed':'absolute'}),r},gpuAcceleration:void 0}}};var Xe={POPPER:'.tippy-popper',TOOLTIP:'.tippy-tooltip',CONTENT:'.tippy-content',BACKDROP:'.tippy-backdrop',ARROW:'.tippy-arrow',ROUND_ARROW:'.tippy-roundarrow'},Ie={x:!0},Ne=function(e){return[].slice.call(e)},He=function(e,t){t.content instanceof Element?(_e(e,''),e.appendChild(t.content)):e[t.allowHTML?'innerHTML':'textContent']=t.content},Re=function(e){return!(e instanceof Element)||at.call(e,'a[href],area[href],button,details,input,textarea,select,iframe,[tabindex]')&&!e.hasAttribute('disabled')},We=function(e,t){e.filter(Boolean).forEach(function(e){e.style.transitionDuration=t+'ms'})},Be=function(e){var t=function(t){return e.querySelector(t)};return{tooltip:t(Xe.TOOLTIP),backdrop:t(Xe.BACKDROP),content:t(Xe.CONTENT),arrow:t(Xe.ARROW)||t(Xe.ROUND_ARROW)}},Me=function(e){return'[object Object]'==={}.toString.call(e)},ze=function(){return document.createElement('div')},_e=function(e,t){e[Ie.x&&'innerHTML']=t instanceof Element?t[Ie.x&&'innerHTML']:t},Fe=function(e){if(e instanceof Element||Me(e))return[e];if(e instanceof NodeList)return Ne(e);if(Array.isArray(e))return e;try{return Ne(document.querySelectorAll(e))}catch(t){return[]}},Ue=function(e){return!isNaN(e)&&!isNaN(parseFloat(e))},Ve=function(e,t,r){if(Array.isArray(e)){var a=e[t];return null==a?r:a}return e},qe=function(e){var t=ze();return'round'===e?(t.className='tippy-roundarrow',_e(t,'')):t.className='tippy-arrow',t},je=function(){var e=ze();return e.className='tippy-backdrop',e.setAttribute('data-state','hidden'),e},Ke=function(e,t){e.setAttribute('tabindex','-1'),t.setAttribute('data-interactive','')},Ge=function(e,t){e.removeAttribute('tabindex'),t.removeAttribute('data-interactive')},Je=function(e){e.setAttribute('data-inertia','')},Ze=function(e){e.removeAttribute('data-inertia')},$e=function(e,t){var r=ze();r.className='tippy-popper',r.setAttribute('role','tooltip'),r.id='tippy-'+e,r.style.zIndex=t.zIndex;var a=ze();a.className='tippy-tooltip',a.style.maxWidth=t.maxWidth+('number'==typeof t.maxWidth?'px':''),a.setAttribute('data-size',t.size),a.setAttribute('data-animation',t.animation),a.setAttribute('data-state','hidden'),t.theme.split(' ').forEach(function(e){a.classList.add(e+'-theme')});var p=ze();return p.className='tippy-content',p.setAttribute('data-state','hidden'),t.interactive&&Ke(r,a),t.arrow&&a.appendChild(qe(t.arrowType)),t.animateFill&&(a.appendChild(je()),a.setAttribute('data-animatefill','')),t.inertia&&a.setAttribute('data-inertia',''),He(p,t),a.appendChild(p),r.appendChild(a),r.addEventListener('focusout',function(t){t.relatedTarget&&r._tippy&&!ot(t.relatedTarget,function(e){return e===r})&&t.relatedTarget!==r._tippy.reference&&r._tippy.props.shouldPopperHideOnBlur(t)&&r._tippy.hide()}),r},Qe=function(e,t,r){var a=Be(e),p=a.tooltip,o=a.content,i=a.backdrop,n=a.arrow;e.style.zIndex=r.zIndex,p.setAttribute('data-size',r.size),p.setAttribute('data-animation',r.animation),p.style.maxWidth=r.maxWidth+('number'==typeof r.maxWidth?'px':''),t.content!==r.content&&He(o,r),!t.animateFill&&r.animateFill?(p.appendChild(je()),p.setAttribute('data-animatefill','')):t.animateFill&&!r.animateFill&&(p.removeChild(i),p.removeAttribute('data-animatefill')),!t.arrow&&r.arrow?p.appendChild(qe(r.arrowType)):t.arrow&&!r.arrow&&p.removeChild(n),t.arrow&&r.arrow&&t.arrowType!==r.arrowType&&p.replaceChild(qe(r.arrowType),n),!t.interactive&&r.interactive?Ke(e,p):t.interactive&&!r.interactive&&Ge(e,p),!t.inertia&&r.inertia?Je(p):t.inertia&&!r.inertia&&Ze(p),t.theme!==r.theme&&(t.theme.split(' ').forEach(function(e){p.classList.remove(e+'-theme')}),r.theme.split(' ').forEach(function(e){p.classList.add(e+'-theme')}))},et=function(e){Ne(document.querySelectorAll(Xe.POPPER)).forEach(function(t){var r=t._tippy;r&&!0===r.props.hideOnClick&&(!e||t!==e.popper)&&r.hide()})},tt=function(e){return Object.keys(ne).reduce(function(t,r){var a=(e.getAttribute('data-tippy-'+r)||'').trim();return a?(t[r]='content'===r?a:'true'===a||'false'!==a&&(Ue(a)?+a:'['===a[0]||'{'===a[0]?JSON.parse(a):a),t):t},{})},rt=function(e){var t={isVirtual:!0,attributes:e.attributes||{},setAttribute:function(t,r){e.attributes[t]=r},getAttribute:function(t){return e.attributes[t]},removeAttribute:function(t){delete e.attributes[t]},hasAttribute:function(t){return t in e.attributes},addEventListener:function(){},removeEventListener:function(){},classList:{classNames:{},add:function(t){e.classList.classNames[t]=!0},remove:function(t){delete e.classList.classNames[t]},contains:function(t){return t in e.classList.classNames}}};for(var r in t)e[r]=t[r];return e},at=function(){if(de){var t=Element.prototype;return t.matches||t.matchesSelector||t.webkitMatchesSelector||t.mozMatchesSelector||t.msMatchesSelector}}(),pt=function(e,t){return(Element.prototype.closest||function(e){for(var t=this;t;){if(at.call(t,e))return t;t=t.parentElement}}).call(e,t)},ot=function(e,t){for(;e;){if(t(e))return e;e=e.parentElement}},it=function(e){var t=window.scrollX||window.pageXOffset,r=window.scrollY||window.pageYOffset;e.focus(),scroll(t,r)},nt=function(e){void e.offsetHeight},st=function(e,t){return(t?e:{X:'Y',Y:'X'}[e])||''},lt=function(e,t,r,p){var o=t[0],i=t[1];if(!o&&!i)return'';var n={scale:function(){return i?r?o+', '+i:i+', '+o:''+o}(),translate:function(){return i?r?p?o+'px, '+-i+'px':o+'px, '+i+'px':p?-i+'px, '+o+'px':i+'px, '+o+'px':p?-o+'px':o+'px'}()};return n[e]},dt=function(e,t){var r=e.match(new RegExp(t+'([XY])'));return r?r[1]:''},mt=function(e,t){var r=e.match(t);return r?r[1].split(',').map(parseFloat):[]},ct={translate:/translateX?Y?\(([^)]+)\)/,scale:/scaleX?Y?\(([^)]+)\)/},ft=function(e,t){var r=xt(pt(e,Xe.POPPER)),a='top'===r||'bottom'===r,p='right'===r||'bottom'===r,o={translate:{axis:dt(t,'translate'),numbers:mt(t,ct.translate)},scale:{axis:dt(t,'scale'),numbers:mt(t,ct.scale)}},i=t.replace(ct.translate,'translate'+st(o.translate.axis,a)+'('+lt('translate',o.translate.numbers,a,p)+')').replace(ct.scale,'scale'+st(o.scale.axis,a)+'('+lt('scale',o.scale.numbers,a,p)+')');e.style['undefined'==typeof document.body.style.transform?'webkitTransform':'transform']=i},ht=function(e,t){e.filter(Boolean).forEach(function(e){e.setAttribute('data-state',t)})},bt=function(e,t){var r=e.popper,a=e.options,p=a.onCreate,o=a.onUpdate;a.onCreate=a.onUpdate=function(){nt(r),t(),o(),a.onCreate=p,a.onUpdate=o}},yt=function(e){setTimeout(e,1)},ut=function(e,t,r,a){if(!e)return!0;var p=r.clientX,o=r.clientY,i=a.interactiveBorder,n=a.distance,s=t.top-o>('top'===e?i+n:i),l=o-t.bottom>('bottom'===e?i+n:i),d=t.left-p>('left'===e?i+n:i),m=p-t.right>('right'===e?i+n:i);return s||l||d||m},gt=function(e,t){return-(e-t)+'px'},xt=function(e){var t=e.getAttribute('x-placement');return t?t.split('-')[0]:''},wt=function(e,t){var r=ie({},t,t.performance?{}:tt(e));return r.arrow&&(r.animateFill=!1),'function'==typeof r.appendTo&&(r.appendTo=t.appendTo(e)),'function'==typeof r.content&&(r.content=t.content(e)),r},vt=function(e,t,r){e[t+'EventListener']('transitionend',r)},kt=function(e,t){var r;return function(){var a=this,p=arguments;clearTimeout(r),r=setTimeout(function(){return e.apply(a,p)},t)}},Et=function(e,t){for(var r in e||{})if(!(r in t))throw Error('[tippy]: `'+r+'` is not a valid option')},Ot=function(e,t){return{}.hasOwnProperty.call(e,t)},Ct=!1,Lt=function(){Ct||(Ct=!0,be&&document.body.classList.add('tippy-iOS'),window.performance&&document.addEventListener('mousemove',At))},Tt=0,At=function e(){var t=performance.now();20>t-Tt&&(Ct=!1,document.removeEventListener('mousemove',e),!be&&document.body.classList.remove('tippy-iOS')),Tt=t},Yt=function(e){var t=e.target;if(!(t instanceof Element))return et();var r=pt(t,Xe.POPPER);if(!(r&&r._tippy&&r._tippy.props.interactive)){var a=ot(t,function(e){return e._tippy&&e._tippy.reference===e});if(a){var p=a._tippy,o=-1