├── CNAME ├── .gitignore ├── favicon.ico ├── assets ├── logo.png ├── arrow.png ├── imgurerror.png ├── facebookshare.png ├── icons │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── mstile-150x150.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── manifest.json │ ├── browserconfig.xml │ └── safari-pinned-tab.svg └── imgurloading.png ├── css ├── fontello │ ├── fontello.eot │ ├── fontello.ttf │ ├── fontello.woff │ ├── fontello.woff2 │ └── fontello.svg ├── bootstrap-tour.min.css ├── accordion.css ├── loading.css └── main.css ├── browserconfig.xml ├── LICENSE.md ├── js ├── core │ ├── Loading.js │ ├── Preferences.js │ ├── Welcome.js │ ├── Tutorial.js │ ├── MyCourses.js │ └── Calendar.js ├── operative.min.js ├── modernizr.js └── bootstrap-tour.min.js ├── README.md └── index.html /CNAME: -------------------------------------------------------------------------------- 1 | schedulestorm.com -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | js/node_modules 2 | .idea 3 | js/.DS_Store 4 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Step7750/ScheduleStorm/HEAD/favicon.ico -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Step7750/ScheduleStorm/HEAD/assets/logo.png -------------------------------------------------------------------------------- /assets/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Step7750/ScheduleStorm/HEAD/assets/arrow.png -------------------------------------------------------------------------------- /assets/imgurerror.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Step7750/ScheduleStorm/HEAD/assets/imgurerror.png -------------------------------------------------------------------------------- /assets/facebookshare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Step7750/ScheduleStorm/HEAD/assets/facebookshare.png -------------------------------------------------------------------------------- /assets/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Step7750/ScheduleStorm/HEAD/assets/icons/favicon.ico -------------------------------------------------------------------------------- /assets/imgurloading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Step7750/ScheduleStorm/HEAD/assets/imgurloading.png -------------------------------------------------------------------------------- /css/fontello/fontello.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Step7750/ScheduleStorm/HEAD/css/fontello/fontello.eot -------------------------------------------------------------------------------- /css/fontello/fontello.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Step7750/ScheduleStorm/HEAD/css/fontello/fontello.ttf -------------------------------------------------------------------------------- /css/fontello/fontello.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Step7750/ScheduleStorm/HEAD/css/fontello/fontello.woff -------------------------------------------------------------------------------- /css/fontello/fontello.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Step7750/ScheduleStorm/HEAD/css/fontello/fontello.woff2 -------------------------------------------------------------------------------- /assets/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Step7750/ScheduleStorm/HEAD/assets/icons/favicon-16x16.png -------------------------------------------------------------------------------- /assets/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Step7750/ScheduleStorm/HEAD/assets/icons/favicon-32x32.png -------------------------------------------------------------------------------- /assets/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Step7750/ScheduleStorm/HEAD/assets/icons/mstile-150x150.png -------------------------------------------------------------------------------- /assets/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Step7750/ScheduleStorm/HEAD/assets/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /assets/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Step7750/ScheduleStorm/HEAD/assets/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /assets/icons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Schedule Storm", 3 | "icons": [ 4 | { 5 | "src": "\/assets\/icons\/android-chrome-192x192.png", 6 | "sizes": "192x192", 7 | "type": "image\/png" 8 | } 9 | ], 10 | "theme_color": "#ffffff", 11 | "display": "standalone" 12 | } 13 | -------------------------------------------------------------------------------- /browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /assets/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #2d89ef 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /css/fontello/fontello.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright (C) 2016 by original authors @ fontello.com 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Stepan Fedorko-Bartos, Ceegan Hale 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 | -------------------------------------------------------------------------------- /js/core/Loading.js: -------------------------------------------------------------------------------- 1 | class Loading { 2 | // Creates the loading animation at the specified element 3 | 4 | constructor(element, loadingtext, styling) { 5 | this.element = element; 6 | 7 | // We need at least 150px for the animation 8 | element.css("min-height", "150px"); 9 | 10 | // TODO: We should use the user's most recent selections to generate the loading subjects 11 | this.html = $(this.createCubeHTML(["CPSC", "ART", "CHEM", "GEOG", "MATH", "STAT"], loadingtext, styling)) 12 | .hide() 13 | .appendTo(element) 14 | .fadeIn(); 15 | } 16 | 17 | /* 18 | Constructs the cube html given the subjects 19 | */ 20 | createCubeHTML(subjects, text, styling) { 21 | this.faces = ["front", "back", "left", "right", "bottom", "top"]; 22 | 23 | if (styling == undefined) var html = "
" + text +"
"; 24 | else var html = "
" + text +"
"; 25 | 26 | for (var key in subjects) { 27 | html += "
" + subjects[key] + "
"; 29 | } 30 | html += "
"; 31 | 32 | return html 33 | } 34 | 35 | /* 36 | Fade out and remove the loading animation 37 | */ 38 | remove(cb) { 39 | var self = this; 40 | 41 | // Fade out the animation 42 | this.html.fadeOut(function () { 43 | // Change the min height on the parent, remove the loader html and initiate the callback 44 | self.element.animate({"min-height": ""}, 500, function () { 45 | self.html.remove(); 46 | cb(); 47 | }); 48 | }); 49 | } 50 | 51 | /* 52 | Sets the status text to the given message 53 | */ 54 | setStatus(message) { 55 | console.log("Changing data"); 56 | this.html.find("#status:first").text(message); 57 | } 58 | } -------------------------------------------------------------------------------- /css/bootstrap-tour.min.css: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | * bootstrap-tour - v0.10.3 3 | * http://bootstraptour.com 4 | * ======================================================================== 5 | * Copyright 2012-2015 Ulrich Sossou 6 | * 7 | * ======================================================================== 8 | * Licensed under the MIT License (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * https://opensource.org/licenses/MIT 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | * ======================================================================== 20 | */ 21 | 22 | .tour-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1100;background-color:#000;opacity:.8;filter:alpha(opacity=80)}.tour-step-backdrop{position:relative;z-index:1101}.tour-step-backdrop>td{position:relative;z-index:1101}.tour-step-background{position:absolute!important;z-index:1100;background:inherit;border-radius:6px}.popover[class*=tour-]{z-index:1102}.popover[class*=tour-] .popover-navigation{padding:9px 14px;overflow:hidden}.popover[class*=tour-] .popover-navigation [data-role=end]{float:right}.popover[class*=tour-] .popover-navigation [data-role=prev],.popover[class*=tour-] .popover-navigation [data-role=next],.popover[class*=tour-] .popover-navigation [data-role=end]{cursor:pointer}.popover[class*=tour-] .popover-navigation [data-role=prev].disabled,.popover[class*=tour-] .popover-navigation [data-role=next].disabled,.popover[class*=tour-] .popover-navigation [data-role=end].disabled{cursor:default}.popover[class*=tour-].orphan{position:fixed;margin-top:0}.popover[class*=tour-].orphan .arrow{display:none} -------------------------------------------------------------------------------- /assets/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 48 | 54 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /css/accordion.css: -------------------------------------------------------------------------------- 1 | /* 2 | Adaptation of https://codyhouse.co/gem/css-multi-level-accordion-menu without CSS transformations 3 | */ 4 | 5 | /* -------------------------------- 6 | 7 | Primary style 8 | 9 | -------------------------------- */ 10 | 11 | h1 { 12 | text-align: center; 13 | width: 90%; 14 | margin: 2em auto 0; 15 | font-size: 2.4rem; 16 | font-weight: bold; 17 | } 18 | @media only screen and (min-width: 600px) { 19 | h1 { 20 | font-size: 3.2rem; 21 | } 22 | } 23 | 24 | input { 25 | font-size: 1.6rem; 26 | } 27 | 28 | /* -------------------------------- 29 | 30 | Main Components 31 | 32 | -------------------------------- */ 33 | .cd-accordion-menu { 34 | margin-top: 1em; 35 | 36 | -webkit-user-select: none; 37 | -moz-user-select: none; 38 | -ms-user-select: none; 39 | user-select: none; 40 | } 41 | 42 | .cd-accordion-menu ul { 43 | /* by default hide all sub menus */ 44 | display: none; 45 | } 46 | 47 | .cd-accordion-menu label, .cd-accordion-menu a { 48 | position: relative; 49 | display: block; 50 | padding: 18px 18px 18px 64px; 51 | background: #ffffff; 52 | box-shadow: inset 0 -1px #555960; 53 | color: black; 54 | font-size: 1.3rem; 55 | } 56 | 57 | .accordiondesc { 58 | position: relative; 59 | display: block; 60 | padding: 12px 18px 12px 90px; 61 | background: #ffffff; 62 | box-shadow: inset 0 -1px #555960; 63 | color: black; 64 | font-size: 1.15rem; 65 | } 66 | 67 | .accordionDetailButton { 68 | padding-left: 90px !important; 69 | } 70 | 71 | .accordiondetail { 72 | padding: 12px 18px 12px 110px; 73 | } 74 | 75 | .accordiontable { 76 | margin-bottom: 0px; 77 | border-bottom: 0px; 78 | } 79 | 80 | .accordiontableparent { 81 | padding-top: 0px; 82 | padding-left: 90px; 83 | padding-right: 20px; 84 | padding-bottom: 1px; 85 | background: #ffffff; 86 | box-shadow: inset 0 -1px #555960; 87 | border-bottom: 0px; 88 | } 89 | 90 | .cd-accordion-menu label { 91 | cursor: pointer; 92 | } 93 | 94 | .cd-accordion-menu label, .cd-accordion-menu a { 95 | padding: 14px 14px 14px 30px; 96 | } 97 | 98 | .cd-accordion-menu ul label, 99 | .cd-accordion-menu ul a { 100 | padding-left: 50px; 101 | } 102 | 103 | .cd-accordion-menu ul ul label, 104 | .cd-accordion-menu ul ul a { 105 | padding-left: 70px; 106 | } 107 | 108 | .cd-accordion-menu ul ul ul label, 109 | .cd-accordion-menu ul ul ul a { 110 | padding-left: 90px; 111 | } 112 | 113 | .cd-accordion-menu ul ul ul ul label, 114 | .cd-accordion-menu ul ul ul ul a { 115 | padding-left: 110px; 116 | } 117 | 118 | 119 | 120 | 121 | 122 | /* http://meyerweb.com/eric/tools/css/reset/ 123 | v2.0 | 20110126 124 | License: none (public domain) 125 | */ 126 | 127 | html, body, div, span, applet, object, iframe, 128 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 129 | a, abbr, acronym, address, big, cite, code, 130 | del, dfn, em, img, ins, kbd, q, s, samp, 131 | small, strike, strong, sub, sup, tt, var, 132 | b, u, i, center, 133 | dl, dt, dd, ol, ul, li, 134 | fieldset, form, label, legend, 135 | table, caption, tbody, tfoot, thead, tr, th, td, 136 | article, aside, canvas, details, embed, 137 | figure, figcaption, footer, header, hgroup, 138 | menu, nav, output, ruby, section, summary, 139 | time, mark, audio, video { 140 | margin: 0; 141 | padding: 0; 142 | border: 0; 143 | font-size: 100%; 144 | font: inherit; 145 | vertical-align: baseline; 146 | } 147 | /* HTML5 display-role reset for older browsers */ 148 | article, aside, details, figcaption, figure, 149 | footer, header, hgroup, menu, nav, section, main { 150 | display: block; 151 | } 152 | body { 153 | line-height: 1; 154 | } 155 | ol, ul { 156 | list-style: none; 157 | } 158 | blockquote, q { 159 | quotes: none; 160 | } 161 | blockquote:before, blockquote:after, 162 | q:before, q:after { 163 | content: ''; 164 | content: none; 165 | } 166 | table { 167 | border-collapse: collapse; 168 | border-spacing: 0; 169 | } -------------------------------------------------------------------------------- /css/loading.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2016 by Jesse (http://codepen.io/ChasingUX/pen/aOMvya) 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the "Software"), 6 | to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | and/or sell copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | DEALINGS IN THE SOFTWARE. 21 | */ 22 | 23 | #loading { 24 | font-size: 20px; 25 | } 26 | .Cube { 27 | width: 80px; 28 | height: 80px; 29 | line-height: 80px; 30 | text-align: center; 31 | font-size: 23px; 32 | -webkit-transform-style: preserve-3d; 33 | transform-style: preserve-3d; 34 | -webkit-transition: -webkit-transform 0.5s 0.1s; 35 | transition: transform 0.5s 0.1s; 36 | perspective: 9999px; 37 | color: #333; 38 | margin: -40px 0 0 -40px; 39 | margin: 0 auto; 40 | position: absolute; 41 | left: 50%; 42 | margin-left: -44px; 43 | opacity: 1; 44 | } 45 | .Cube.panelLoad { 46 | z-index: 11; 47 | margin-top: 40px; 48 | -webkit-animation: panel 4s infinite forwards; 49 | animation: panel 4s infinite forwards; 50 | } 51 | .Cube.panelLoad .cube-face { 52 | color: black; 53 | box-shadow: inset 0 0 0 1px #111, 0 0 1px 1px #111; 54 | } 55 | .Cube .cube-face { 56 | width: inherit; 57 | height: inherit; 58 | position: absolute; 59 | background: white; 60 | box-shadow: inset 0 0 0 1px #333, 0 0 1px 1px #333; 61 | opacity: 1; 62 | } 63 | .Cube .cube-face-front { 64 | transform: translate3d(0, 0, 40px); 65 | -webkit-transform: translate3d(0, 0, 40px); 66 | font-size: 25px; 67 | } 68 | .Cube .cube-face-back { 69 | -webkit-transform: rotateY(180deg) translate3d(0, 0, 40px); 70 | transform: rotateY(180deg) translate3d(0, 0, 40px); 71 | } 72 | .Cube .cube-face-left { 73 | -webkit-transform: rotateY(-90deg) translate3d(0, 0, 40px); 74 | transform: rotateY(-90deg) translate3d(0, 0, 40px); 75 | } 76 | .Cube .cube-face-right { 77 | -webkit-transform: rotateY(90deg) translate3d(0, 0, 40px); 78 | transform: rotateY(90deg) translate3d(0, 0, 40px); 79 | } 80 | .Cube .cube-face-top { 81 | -webkit-transform: rotateX(90deg) translate3d(0, 0, 40px); 82 | transform: rotateX(90deg) translate3d(0, 0, 40px); 83 | } 84 | .Cube .cube-face-bottom { 85 | -webkit-transform: rotateX(-90deg) translate3d(0, 0, 40px); 86 | transform: rotateX(-90deg) translate3d(0, 0, 40px); 87 | } 88 | 89 | @-webkit-keyframes panel { 90 | 0% { 91 | -webkit-transform: rotateY(0deg) rotateZ(0deg); 92 | } 93 | 20% { 94 | -webkit-transform: rotateY(90deg) rotateZ(0deg); 95 | } 96 | 40% { 97 | -webkit-transform: rotateX(45deg) rotateZ(45deg); 98 | } 99 | 60% { 100 | -webkit-transform: rotateX(90deg) rotateY(180deg) rotateX(90deg); 101 | } 102 | 80% { 103 | -webkit-transform: rotateX(310deg) rotateZ(230deg); 104 | } 105 | 100% { 106 | -webkit-transform: rotateX(360deg) rotateZ(360deg); 107 | } 108 | } 109 | @keyframes panel { 110 | 0% { 111 | -webkit-transform: rotateY(0deg) rotateZ(0deg); 112 | transform: rotateY(0deg) rotateZ(0deg); 113 | } 114 | 20% { 115 | -webkit-transform: rotateY(90deg) rotateZ(0deg); 116 | transform: rotateY(90deg) rotateZ(0deg); 117 | } 118 | 40% { 119 | -webkit-transform: rotateX(45deg) rotateZ(45deg); 120 | transform: rotateX(45deg) rotateZ(45deg); 121 | } 122 | 60% { 123 | -webkit-transform: rotateX(90deg) rotateY(180deg) rotateX(90deg); 124 | transform: rotateX(90deg) rotateY(180deg) rotateX(90deg); 125 | } 126 | 80% { 127 | -webkit-transform: rotateX(310deg) rotateZ(230deg); 128 | transform: rotateX(310deg) rotateZ(230deg); 129 | } 130 | 100% { 131 | -webkit-transform: rotateX(360deg) rotateZ(360deg); 132 | transform: rotateX(360deg) rotateZ(360deg); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /js/core/Preferences.js: -------------------------------------------------------------------------------- 1 | class Preferences { 2 | constructor() { 3 | this.instantiateSliders(); 4 | this.loadPreferences(); 5 | 6 | // Update the Uni, remove needless options on start 7 | this.updatedUni(); 8 | } 9 | 10 | instantiateSliders() { 11 | var self = this; 12 | 13 | self.morningslider = $('#slider_morning').slider() 14 | .on('slideStop', function () { 15 | self.savePreferences(); 16 | }); 17 | 18 | self.nightslider = $('#slider_night').slider() 19 | .on('slideStop', function () { 20 | self.savePreferences(); 21 | }); 22 | self.consecutiveslider = $('#slider_consecutive').slider() 23 | .on('slideStop', function () { 24 | self.savePreferences(); 25 | }); 26 | self.rmpslider = $('#slider_rmp').slider() 27 | .on('slideStop', function () { 28 | self.savePreferences(); 29 | }); 30 | 31 | // Bind checkbox change event 32 | $("#onlyOpenCheckbox").change(function () { 33 | self.savePreferences(); 34 | }) 35 | 36 | // Bind Engineering student change event 37 | $("#engineeringCheckbox").change(function(){ 38 | self.savePreferences(true); 39 | }); 40 | 41 | // Initialize tooltip for engineering checkbox 42 | $("#engineeringCheckboxTooltip").tooltip(); 43 | } 44 | 45 | /* 46 | Hides/shows different preferences based upon the current uni selected 47 | */ 48 | updatedUni(newuni) { 49 | $("#engineeringCheckbox").parent().hide(); 50 | 51 | if (newuni == "UAlberta") { 52 | $("#engineeringCheckbox").parent().show(); 53 | } 54 | } 55 | 56 | getMorningValue() { 57 | return this.morningslider.slider('getValue'); 58 | } 59 | 60 | getNightValue() { 61 | return this.nightslider.slider('getValue'); 62 | } 63 | 64 | getConsecutiveValue() { 65 | return this.consecutiveslider.slider('getValue'); 66 | } 67 | 68 | getRMPValue() { 69 | return this.rmpslider.slider('getValue'); 70 | } 71 | 72 | getOnlyOpenValue() { 73 | return $("#onlyOpenCheckbox").is(":checked"); 74 | } 75 | 76 | getEngineeringValue() { 77 | return $('#engineeringCheckbox').is(':checked'); 78 | } 79 | 80 | setMorningValue(value) { 81 | if (value != null) this.morningslider.slider('setValue', parseInt(value)); 82 | } 83 | 84 | setNightValue(value) { 85 | if (value != null) this.nightslider.slider('setValue', parseInt(value)); 86 | } 87 | 88 | setConsecutiveValue(value) { 89 | if (value != null) this.consecutiveslider.slider('setValue', parseInt(value)); 90 | } 91 | 92 | setRMPValue(value) { 93 | if (value != null) this.rmpslider.slider('setValue', parseInt(value)); 94 | } 95 | 96 | setOnlyOpenValue(value) { 97 | if (value != null) $("#onlyOpenCheckbox").attr("checked", (value === "true")); 98 | } 99 | 100 | setEngineeringValue(value) { 101 | if (value != null) $("#engineeringCheckbox").attr("checked", (value === "true")); 102 | } 103 | 104 | /* 105 | Saves the current slider values to localStorage 106 | */ 107 | savePreferences(regenerate) { 108 | localStorage.setItem('morningslider', this.getMorningValue()); 109 | localStorage.setItem('nightslider', this.getNightValue()); 110 | localStorage.setItem('consecutiveslider', this.getConsecutiveValue()); 111 | localStorage.setItem('rmpslider', this.getRMPValue()); 112 | localStorage.setItem('onlyOpenCheckbox', this.getOnlyOpenValue()); 113 | localStorage.setItem('engineeringCheckbox', this.getEngineeringValue()); 114 | 115 | // update any current schedule generation 116 | if (window.mycourses.generator != false) { 117 | if (regenerate != true) { 118 | // update the scores 119 | window.mycourses.generator.updateScores(); 120 | } 121 | else { 122 | window.mycourses.startGeneration(); 123 | } 124 | } 125 | } 126 | 127 | /* 128 | If there are saved preferences in localStorage, this loads them 129 | */ 130 | loadPreferences() { 131 | this.setMorningValue(localStorage.getItem('morningslider')); 132 | this.setNightValue(localStorage.getItem('nightslider')); 133 | this.setConsecutiveValue(localStorage.getItem('consecutiveslider')); 134 | this.setRMPValue(localStorage.getItem('rmpslider')); 135 | this.setOnlyOpenValue(localStorage.getItem('onlyOpenCheckbox')); 136 | this.setEngineeringValue(localStorage.getItem('engineeringCheckbox')); 137 | } 138 | } -------------------------------------------------------------------------------- /js/core/Welcome.js: -------------------------------------------------------------------------------- 1 | class Welcome { 2 | 3 | constructor() { 4 | this.baseURL = "http://api.schedulestorm.com:5000/v1/"; 5 | 6 | // We want to get the list of Unis 7 | this.getUnis(); 8 | } 9 | 10 | /* 11 | Obtains the University list from the API server 12 | */ 13 | getUnis() { 14 | // empty the parent 15 | $("#uniModalList").find("#dataList").empty(); 16 | 17 | var thisobj = this; 18 | 19 | $("#welcomeModal").modal({ 20 | backdrop: 'static', 21 | keyboard: false 22 | }); 23 | 24 | // Add the loading animation 25 | var loading = new Loading($("#uniModalList").find("#dataList"), "Loading University Data..."); 26 | 27 | $.getJSON(this.baseURL + "unis", function(data) { 28 | // remove the loading animation 29 | loading.remove(function () { 30 | // Populate the dropdown in the top right 31 | thisobj.populateUniDropdown(data); 32 | 33 | window.unis = data; 34 | thisobj.unis = data; 35 | 36 | var localUni = localStorage.getItem("uni"); 37 | var localTerm = localStorage.getItem("term"); 38 | 39 | // Check to see if they have already selected a Uni and Term in localstorage 40 | if (thisobj.unis[localUni] != undefined && thisobj.unis[localUni]["terms"][localTerm] != undefined) { 41 | // Hide the modal 42 | $("#welcomeModal").modal('hide'); 43 | 44 | // Set this uni 45 | thisobj.uni = localUni; 46 | 47 | // Populate the top right dropdown 48 | $("#MyUniversity").hide().html(thisobj.unis[thisobj.uni]["name"] + " ").fadeIn('slow'); 49 | 50 | // Load up the classes 51 | window.classList = new ClassList(localUni, localTerm); 52 | window.mycourses = new MyCourses(localUni, localTerm); 53 | } 54 | else { 55 | $("#uniModalList").find("#dataList").hide(); 56 | 57 | // New user with nothing selected, show them welcome prompts 58 | thisobj.populateUnis(data); 59 | } 60 | }); 61 | }); 62 | } 63 | 64 | /* 65 | Populates the top right university dropdown (when the modal isn't showing) and handles the click events 66 | */ 67 | populateUniDropdown(data) { 68 | var self = this; 69 | 70 | // Get the dropdown element 71 | var dropdown = $("#MyUniversity").parent().find('.dropdown-menu'); 72 | 73 | for (var uni in data) { 74 | // Add this Uni to the dropdown 75 | 76 | var uniobj = data[uni]; 77 | 78 | var uniHTML = ''; 85 | 86 | var html = $(uniHTML); 87 | 88 | // Bind an onclick event to it 89 | html.click(function () { 90 | 91 | // Get the selected uni code (UCalgary, etc...) 92 | self.uni = $(this).find("a").attr("uni"); 93 | 94 | // Change the text of the element 95 | $("#MyUniversity").hide().html(self.unis[self.uni]["name"] + " ").fadeIn('slow'); 96 | 97 | // Make sure the modal is active 98 | $("#welcomeModal").modal({ 99 | backdrop: 'static', 100 | keyboard: false 101 | }); 102 | 103 | // Let the user choose what term they want 104 | self.displayTerms(self.uni); 105 | }) 106 | 107 | // Append it 108 | dropdown.append(html); 109 | } 110 | } 111 | 112 | /* 113 | Populates the modal with the Unis 114 | */ 115 | populateUnis(unis) { 116 | var thisobj = this; 117 | 118 | var list = $("#uniModalList").find("#dataList"); 119 | var wantedText = $("#uniModalList").find("#wantedData"); 120 | 121 | wantedText.text("Please choose your University:"); 122 | 123 | // Iterate through the unis and add the buttons 124 | for (var uni in unis) { 125 | var labelText = undefined; 126 | if (this.unis[uni]["scraping"] == true) labelText = "Updating"; 127 | 128 | var button = $(this.createButton(unis[uni]["name"], uni, labelText)); 129 | button.click(function() { 130 | 131 | thisobj.uni = $(this).attr("value"); 132 | 133 | $("#MyUniversity").hide().html($(this).text() + " ").fadeIn('slow'); 134 | 135 | $("#uniModalList").slideUp(function () { 136 | thisobj.displayTerms(thisobj.uni); 137 | }); 138 | }); 139 | 140 | list.append(button); 141 | } 142 | 143 | list.append("
Don't see your school? Tell Us!"); 144 | 145 | list.slideDown(); 146 | } 147 | 148 | /* 149 | Displays the terms to the user 150 | */ 151 | displayTerms(uni) { 152 | var thisobj = this; // Keep the reference 153 | 154 | var list = $("#uniModalList").find("#dataList"); 155 | list.empty(); 156 | var wantedText = $("#uniModalList").find("#wantedData"); 157 | 158 | wantedText.text("Please choose your term:"); 159 | 160 | for (var term in this.unis[uni]["terms"]) { 161 | var button = $(this.createButton(this.unis[uni]["terms"][term], term)); 162 | 163 | button.click(function() { 164 | 165 | thisobj.term = $(this).attr("value"); 166 | 167 | window.classList = new ClassList(thisobj.uni, thisobj.term); 168 | window.mycourses = new MyCourses(thisobj.uni, thisobj.term); 169 | 170 | // reset the calendar 171 | window.calendar.resetCalendar(); 172 | 173 | // hide the modal 174 | $("#welcomeModal").modal('hide'); 175 | }); 176 | list.append(button); 177 | } 178 | 179 | $("#uniModalList").slideDown(); 180 | } 181 | 182 | /* 183 | Returns the text for an HTML button given text, value and label text 184 | */ 185 | createButton(text, value, labelText) { 186 | var html = ''; 191 | 192 | return html; 193 | } 194 | } 195 | 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 | [ScheduleStorm.com](http://schedulestorm.com) 8 | 9 | [You Can Find the Server Repo Here](https://github.com/Step7750/ScheduleStorm_Server) 10 | 11 | Schedule Storm is a schedule generator web app that lets you input your courses and preferences to generate possible schedules. 12 | 13 | Rather than just supporting one university, Schedule Storm is a platform in which you can support your university by extending the modules. 14 | 15 | ## Table of Contents 16 | * [Supported Universities](https://github.com/Step7750/ScheduleStorm#supported-universities) 17 | * [Features](https://github.com/Step7750/ScheduleStorm#features) 18 | * [How does it work?](https://github.com/Step7750/ScheduleStorm#how-does-it-work) 19 | * [Why is it better?](https://github.com/Step7750/ScheduleStorm#why-is-it-better) 20 | * [Schedule Generator Implementation](https://github.com/Step7750/ScheduleStorm#schedule-generator-implementation) 21 | * [Tech Stack](https://github.com/Step7750/ScheduleStorm#tech-stack) 22 | * [How to Transpile (using Babel)](https://github.com/Step7750/ScheduleStorm#how-to-transpile-using-babel) 23 | 24 | ## Supported Universities 25 | * University of Calgary 26 | * University of Alberta 27 | * Mount Royal University 28 | * University of Lethbridge 29 | * University of Waterloo 30 | 31 | Want us to support your university? 32 | * If you can, feel free to send pull requests with additional functionality 33 | * If not, file a new issue and we'll look into it! 34 | 35 | ## Features 36 | * Fast and responsive accordion for searching courses 37 | * Unified UI with your proposed schedules, class search, and selected courses 38 | * Add specific classes and allow the generator to fill out the rest 39 | * Fast client side schedule generation when adding courses 40 | * Inline RMP Ratings 41 | * Dynamic Scoring that takes into account your preferences 42 | * Download a photo of your schedule, share it to Facebook or Imgur, or copy it to clipboard 43 | * Create groups of classes and let the generator use "One of", "Two Of", etc.. of them 44 | * Block off timeslots by dragging on the calendar 45 | * Supports many Unis, with a framework for adding more 46 | 47 | ## How does it work? 48 | 49 | The front-end of the site is hosted on Github pages and proceeds to query a central API server that we host. The API server holds the university data and replies to the client with whatever they need. 50 | 51 | When a client chooses a specific term and uni, we send over all of the class data and ratemyprofessor ratings in a gzipped response (~350KB compressed, ~3MB uncompressed). 52 | 53 | **All requests are cached for 6 hours** 54 | 55 | Since the client has all the data they need, class searching and generation can all be done client side now. Due to this, there is no additional latency in sending requests back and forth. 56 | 57 | ## Why is it better? 58 | 59 | From the very beginning, we wanted to have a very unified experience when using Schedule Storm. When you add a new course, you can instantly see how your schedules changed. When you change your schedule scoring preferences, you can instantly see the new sorting in the background. 60 | 61 | Class searching, scoring, and generation are all client side with web workers used to minimize UI lag. 62 | 63 | When most people look for classes, they want to know how "good" the teacher is and compare that to how good the time slot is. As a result, Schedule Storm takes ratemyprofessor ratings into account when generating schedules. If you believe RMP does not give a good indication of a professor's quality, you can simply change your preferences for score weighting. 64 | 65 | Another feature that we saw lacking in other schedule generators was the ability to choose a specific class and let it figure out the proper tutorials, labs, etc... This lets you choose that specific tutorial with your friends, but let it figure out what are the best lectures and labs for it. 66 | 67 | We decided to expand upon Winston's class group features and allow you to have All of, one of, two of, etc... of a certain number of classes. This allows you to make a new group, add a bunch of options to it, and only want it to select 2 or 3 of them. 68 | 69 | For the classlist, we wanted to provide the user with a new means of browsing for possible specific classes rather than using their school's (probably) archaic class searching system. When we find a match, professors have a little number next to their name to indicate their RMP rating. We also have in-line course descriptions, requirements, and notes; you won't have to go anywhere else to check whether you have the prerequisites. 70 | 71 | 72 | ## Schedule Generator Implementation 73 | 74 | The schedule generator uses a Backtracking algorithm with Forward Checking and the Minimum Remaining Values (MRV) heuristic. The generator and sorter are both client side and in web workers to minimize UI lag during generation, but there is additional overhead with data transfer to the parent page for highly complex schedules. 75 | 76 | For the vast majority of users, schedule generation will only take a couple of milliseconds (without transport overhead). Using a deliberately intensive example, out of a search space of 4435200 schedules, the generator found the 178080 possible schedules in ~3s. 77 | 78 | Testing was done on the possiblility of using a SAT solver (such as MiniSat) and Emscripten as the LLVM to JavaScript compiler, but there was too much overhead in the creation of the object and didn't have very good solutions with the class grouping support and reusing previous results effectively. 79 | 80 | #### Generator Steps: 81 | 82 | * All duplicate classes with virtually the same attributes are removed 83 | * Class objects are modified so that time conflicts are easier to compute and some attributes are added (ex. "Manual" for manually specified classes) 84 | * A dictionary is created where the keys are the class ids and the values their objects. This minimizes the total data being thrown and copied around and when we need the attributes of a given class, we can retrieve its attributes in O(1) time complexity. 85 | * All of the relevant course data and user settings are sent to the web worker for further processing 86 | * All of the possible combinations for each course group is found and stored ("All of", "Two of", etc...) 87 | * A dictionary is created that contains, for every class id as the key, an array of class ids that conflict with this class 88 | * The recursive backtracking algo is then called which finds the domains for the current combination. The generator must satisfy at least one element in each domain for there to be a possible schedule. The domains are sorted using the MRV heuristic to reduce early on branching factor and the contents of each domain is sorted in ascending order for binary search later on. 89 | * Given the current proposed class in the current domain for a given schedule, it then removes classes from subsequent domains that conflict with this class (Forward Checking). If there is ever an empty domain, we know that a solution is impossible and backtrack. 90 | * Once the depth level is the same as the domain length, we check if we've gone through every class group for this schedule, if so, we append a copy of the current solution to a global variable. If not, we repeat the previous steps and add to the domain. 91 | * The possible schedules are returned to the parent page and sent for sorting. 92 | 93 | 94 | ## Tech Stack 95 | 96 | * MongoDB 97 | * Python Backend w/ Falcon for the API Server and threads for each University 98 | * ES6 JS OOP Frontend (transpiled to ES5 in production to support more clients) 99 | * Heavy use of JQuery to manipulate the DOM 100 | * Bootstrap, HTML2Canvas, Clipboard, Operative 101 | 102 | ### Backend 103 | 104 | Each university has an instantiated thread from it's module (ex. UCalgary.py). The backend handles API requests and necessary scraping in an all-in-one package. Each supported university must have an entry in the settings file and have an enabled flag. RMP gets a dedicated thread that looks at the currently enabled Unis and scrapes the ratings for them in the specified interval. Each university has it's settings passed into it's thread upon creation. Each university is required to handle scraping, db management, and API response handlers for itself. 105 | 106 | 107 | ## How to Transpile (using Babel) 108 | 109 | **Ensure you have Node.js and npm installed** 110 | 111 | 1. Copy the `js/core` directory to a place outside of the Github directory (unless you want to explicitly ignore the node files) 112 | 2. Change the directory to the parent of the new copied `core` directory 113 | 2. Install Babel CLI with: `npm install --save-dev babel-cli` 114 | 3. Install the ES2015 preset with: `npm install --save-dev babel-preset-es2015` 115 | 4. Transpile the core folder with: `babel core --out-file production_core.js --presets es2015` 116 | 5. Copy the resultant `production_core.js` file to the `js` folder in the Github Schedule Storm directory 117 | 118 | **Note: Ensure that any changes you make are on the original ES6 `core` file AND the transpiled `production_core.js`** 119 | 120 | ## Inspiration from: 121 | 122 | * [Hey Winston for University of Alberta](https://github.com/ahoskins/winston) 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /js/operative.min.js: -------------------------------------------------------------------------------- 1 | /** Operative v0.4.4 (c) 2013 James padolsey, MIT-licensed, http://github.com/padolsey/operative **/ 2 | (function(){function e(t,n){var o=e.getBaseURL,i=e.getSelfURL,s=e.hasWorkerSupport?e.Operative.BrowserWorker:e.Operative.Iframe;if("function"==typeof t){var a=new s({main:t},n,o,i),u=function(){return a.api.main.apply(a,arguments)};u.transfer=function(){return a.api.main.transfer.apply(a,arguments)};for(var l in a.api)r.call(a.api,l)&&(u[l]=a.api[l]);return u}return new s(t,n,o,i).api}if("undefined"!=typeof window||!self.importScripts){var r={}.hasOwnProperty,t=document.getElementsByTagName("script"),n=t[t.length-1],o=/operative/.test(n.src)&&n.src;e.pool=function(r,t,n){r=0|Math.abs(r)||1;for(var o=[],i=0,s=0;r>s;++s)o.push(e(t,n));return{terminate:function(){for(var e=0;r>e;++e)o[e].destroy()},next:function(){return i=i+1===r?0:i+1,o[i]}}},e.hasWorkerSupport=!!window.Worker,e.hasWorkerViaBlobSupport=!1,e.hasTransferSupport=!1;var i=(location.protocol+"//"+location.hostname+(location.port?":"+location.port:"")+location.pathname).replace(/[^\/]+$/,"");e.objCreate=Object.create||function(e){function r(){}return r.prototype=e,new r},e.setSelfURL=function(e){o=e},e.getSelfURL=function(){return o},e.setBaseURL=function(e){i=e},e.getBaseURL=function(){return i},window.operative=e}})(),function(){function e(e){this.value=e}function r(e,r,n,o){var i=this;e.get=e.get||function(e){return this[e]},e.set=e.set||function(e,r){return this[e]=r},this._curToken=0,this._queue=[],this._getBaseURL=n,this._getSelfURL=o,this.isDestroyed=!1,this.isContextReady=!1,this.module=e,this.dependencies=r||[],this.dataProperties={},this.api={},this.callbacks={},this.deferreds={},this._fixDependencyURLs(),this._setup();for(var s in e)t.call(e,s)&&this._createExposedMethod(s);this.api.__operative__=this,this.api.destroy=this.api.terminate=function(){return i.destroy()}}if("undefined"!=typeof window||!self.importScripts){var t={}.hasOwnProperty,n=[].slice,o={}.toString;operative.Operative=r;var i=r.Promise=window.Promise;r.prototype={_marshal:function(e){return e},_demarshal:function(e){return e},_enqueue:function(e){this._queue.push(e)},_fixDependencyURLs:function(){for(var e=this.dependencies,r=0,t=e.length;t>r;++r){var n=e[r];/\/\//.test(n)||(e[r]=n.replace(/^\/?/,this._getBaseURL().replace(/([^\/])$/,"$1/")))}},_dequeueAll:function(){for(var e=0,r=this._queue.length;r>e;++e)this._queue[e].call(this);this._queue=[]},_buildContextScript:function(e){var r,t=[],n=this.module,o=this.dataProperties;for(var i in n)r=n[i],"function"==typeof r?t.push(' self["'+i.replace(/"/g,'\\"')+'"] = '+(""+r)+";"):o[i]=r;return t.join("\n")+(e?"\n("+(""+e)+"());":"")},_createExposedMethod:function(r){var t=this,s=this.api[r]=function(){function o(){t.isContextReady?t._runMethod(r,s,a,l):t._enqueue(o)}if(t.isDestroyed)throw Error("Operative: Cannot run method. Operative has already been destroyed");var s=++t._curToken,a=n.call(arguments),u="function"==typeof a[a.length-1]&&a.pop(),l=a[a.length-1]instanceof e&&a.pop();if(!u&&!i)throw Error("Operative: No callback has been passed. Assumed that you want a promise. But `operative.Promise` is null. Please provide Promise polyfill/lib.");if(u)t.callbacks[s]=u,setTimeout(function(){o()},1);else if(i)return new i(function(e,r){var n;e.fulfil||e.fulfill?(n=e,n.fulfil=n.fulfill=e.fulfil||e.fulfill):n={fulfil:e,fulfill:e,resolve:e,reject:r,transferResolve:e,transferReject:r},t.deferreds[s]=n,o()})};s.transfer=function(){var r=[].slice.call(arguments),t="function"==typeof r[r.length-1]?r.length-2:r.length-1,n=r[t],i=o.call(n);if("[object Array]"!==i)throw Error("Operative:transfer() must be passed an Array of transfers as its last arguments (Expected: [object Array], Received: "+i+")");return r[t]=new e(n),s.apply(null,r)}},destroy:function(){this.isDestroyed=!0}}}}(),function(){function makeBlobURI(e){var r;try{r=new Blob([e],{type:"text/javascript"})}catch(t){r=new BlobBuilder,r.append(e),r=r.getBlob()}return URL.createObjectURL(r)}function workerBoilerScript(){var postMessage=self.postMessage,structuredCloningSupport=null,toString={}.toString;self.console={},self.isWorker=!0,["log","debug","error","info","warn","time","timeEnd"].forEach(function(e){self.console[e]=function(){postMessage({cmd:"console",method:e,args:[].slice.call(arguments)})}}),self.addEventListener("message",function(e){function callback(){returnResult({args:[].slice.call(arguments)})}function returnResult(e,r){postMessage({cmd:"result",token:data.token,result:e},hasTransferSupport&&r||[])}function extractTransfers(e){var r=e[e.length-1];if("[object Array]"!==toString.call(r))throw Error("Operative: callback.transfer() must be passed an Array of transfers as its last arguments");return r}var data=e.data;if("string"==typeof data&&0===data.indexOf("EVAL|"))return eval(data.substring(5)),void 0;if(null==structuredCloningSupport)return structuredCloningSupport="PING"===e.data[0],self.postMessage(structuredCloningSupport?"pingback:structuredCloningSupport=YES":"pingback:structuredCloningSupport=NO"),structuredCloningSupport||(postMessage=function(e){return self.postMessage(JSON.stringify(e))}),void 0;structuredCloningSupport||(data=JSON.parse(data));var defs=data.definitions,isDeferred=!1,args=data.args;if(defs)for(var i in defs)self[i]=defs[i];else{callback.transfer=function(){var e=[].slice.call(arguments),r=extractTransfers(e);returnResult({args:e},r)},args.push(callback),self.deferred=function(){function e(e,r){return returnResult({isDeferred:!0,action:"resolve",args:[e]},r),t}function r(e,r){returnResult({isDeferred:!0,action:"reject",args:[e]},r)}isDeferred=!0;var t={};return t.fulfil=t.fulfill=t.resolve=function(r){return e(r)},t.reject=function(e){return r(e)},t.transferResolve=function(r){var t=extractTransfers(arguments);return e(r,t)},t.transferReject=function(e){var t=extractTransfers(arguments);return r(e,t)},t};var result=self[data.method].apply(self,args);isDeferred||void 0===result||returnResult({args:[result]}),self.deferred=function(){throw Error("Operative: deferred() called at odd time")}}})}if("undefined"==typeof window&&self.importScripts)return workerBoilerScript(),void 0;var Operative=operative.Operative,URL=window.URL||window.webkitURL,BlobBuilder=window.BlobBuilder||window.WebKitBlobBuilder||window.MozBlobBuilder,workerViaBlobSupport=function(){try{new Worker(makeBlobURI(";"))}catch(e){return!1}return!0}(),transferrableObjSupport=function(){try{var e=new ArrayBuffer(1);return new Worker(makeBlobURI(";")).postMessage(e,[e]),!e.byteLength}catch(r){return!1}}();operative.hasWorkerViaBlobSupport=workerViaBlobSupport,operative.hasTransferSupport=transferrableObjSupport,Operative.BrowserWorker=function BrowserWorker(){Operative.apply(this,arguments)};var WorkerProto=Operative.BrowserWorker.prototype=operative.objCreate(Operative.prototype);WorkerProto._onWorkerMessage=function(e){var r=e.data;if("string"==typeof r&&0===r.indexOf("pingback"))return"pingback:structuredCloningSupport=NO"===r&&(this._marshal=function(e){return JSON.stringify(e)},this._demarshal=function(e){return JSON.parse(e)}),this.isContextReady=!0,this._postMessage({definitions:this.dataProperties}),this._dequeueAll(),void 0;switch(r=this._demarshal(r),r.cmd){case"console":window.console&&window.console[r.method].apply(window.console,r.args);break;case"result":var t=this.callbacks[r.token],n=this.deferreds[r.token],o=r.result&&r.result.isDeferred&&r.result.action;n&&o?n[o](r.result.args[0]):t?t.apply(this,r.result.args):n&&n.fulfil(r.result.args[0])}},WorkerProto._isWorkerViaBlobSupported=function(){return workerViaBlobSupport},WorkerProto._setup=function(){var e,r=this,t=this._getSelfURL(),n=this._isWorkerViaBlobSupported(),o=this._buildContextScript(n?workerBoilerScript:"");if(this.dependencies.length&&(o='importScripts("'+this.dependencies.join('", "')+'");\n'+o),n)e=this.worker=new Worker(makeBlobURI(o));else{if(!t)throw Error("Operaritve: No operative.js URL available. Please set via operative.setSelfURL(...)");e=this.worker=new Worker(t),e.postMessage("EVAL|"+o)}e.postMessage("EVAL|self.hasTransferSupport="+transferrableObjSupport),e.postMessage(["PING"]),e.addEventListener("message",function(e){r._onWorkerMessage(e)})},WorkerProto._postMessage=function(e){var r=transferrableObjSupport&&e.transfers;return r?this.worker.postMessage(e,r.value):this.worker.postMessage(this._marshal(e))},WorkerProto._runMethod=function(e,r,t,n){this._postMessage({method:e,args:t,token:r,transfers:n})},WorkerProto.destroy=function(){this.worker.terminate(),Operative.prototype.destroy.call(this)}}(),function(){function e(){window.__run__=function(e,r,t,n){function o(){return t.apply(this,arguments)}var i=!1;window.deferred=function(){return i=!0,n},o.transfer=function(){return t.apply(this,[].slice.call(arguments,0,arguments.length-1))},t&&r.push(o);var s=window[e].apply(window,r);window.deferred=function(){throw Error("Operative: deferred() called at odd time")},i||void 0===s||o(s)}}if("undefined"!=typeof window||!self.importScripts){var r=operative.Operative;r.Iframe=function(){r.apply(this,arguments)};var t=r.Iframe.prototype=operative.objCreate(r.prototype),n=0;t._setup=function(){var r=this,t="__operativeIFrameLoaded"+ ++n;this.module.isWorker=!1;var o=this.iframe=document.body.appendChild(document.createElement("iframe"));o.style.display="none";var i=this.iframeWindow=o.contentWindow,s=i.document;window[t]=function(){window[t]=null;var n=s.createElement("script"),o=r._buildContextScript(e);void 0!==n.text?n.text=o:n.innerHTML=o,s.documentElement.appendChild(n);for(var a in r.dataProperties)i[a]=r.dataProperties[a];r.isContextReady=!0,r._dequeueAll()},s.open();var a="";this.dependencies.length&&(a+='\n'),s.write(a+"\n"),s.close()},t._runMethod=function(e,r,t){var n=this,o=this.callbacks[r],i=this.deferreds[r];this.iframeWindow.__run__(e,t,function(e){var r=o,t=i;r?r.apply(n,arguments):t&&t.fulfil(e)},i)},t.destroy=function(){this.iframe.parentNode.removeChild(this.iframe),r.prototype.destroy.call(this)}}}(); -------------------------------------------------------------------------------- /js/core/Tutorial.js: -------------------------------------------------------------------------------- 1 | class Tutorial { 2 | constructor() { 3 | var self = this; 4 | 5 | // check localstorage to see whether we should start the tut or not 6 | if (localStorage.getItem("tour_end") == null && window.tourInProgress != true) { 7 | // Set a global defining our progress 8 | window.tourInProgress = true; 9 | 10 | // scroll to the top of the class data wraper 11 | $("#classdatawraper").scrollTop(0); 12 | 13 | // Hide scrollbars 14 | // This is for firefox since the pointer events can still move scrollbars 15 | // (which can raise events and cause the tour element to disappear) 16 | $("#classdatawraper").css('overflow', 'hidden'); 17 | $("#courseList").css('overflow', 'hidden'); 18 | 19 | // Repopulate the accordion to the default view 20 | classList.repopulateAccordion(); 21 | 22 | setTimeout(function () { 23 | self.openAccordion(); 24 | }, 500); 25 | } 26 | } 27 | 28 | /* 29 | Open the first top level for every level 30 | */ 31 | openAccordion() { 32 | this.openedAccordion = true; 33 | 34 | this.openChildRow($('#classdatawraper').children(0)); 35 | } 36 | 37 | /* 38 | Opens the first row in the child of the specified element 39 | */ 40 | openChildRow(element) { 41 | var self = this; 42 | 43 | // Get the row 44 | var row = element.parent().find('.has-children').eq(0); 45 | 46 | if (row.length > 0) { 47 | 48 | // Ensure the row isn't open already, if not, click it 49 | if (row.find("ul").length == 1 && row.find(".accordiontableparent").length == 0) row.find('label').click(); 50 | 51 | // Call the next row 52 | setTimeout(function () { 53 | self.openChildRow(row.find('label').eq(0)); 54 | }, 50); 55 | } 56 | else { 57 | // start up the tour 58 | self.createIntro(); 59 | } 60 | } 61 | 62 | /* 63 | Initialize and start the tour 64 | */ 65 | createIntro() { 66 | var self = this; 67 | 68 | window.tour = new Tour({ 69 | steps: [ 70 | { 71 | title: "What is this?", 72 | content: "Schedule Storm is a student schedule generator that lets you input your courses and preferences to generate possible schedules.

You can always restart this tour by going to preferences" 73 | }, 74 | { 75 | element: document.querySelector('#classdatawraper'), 76 | title: "Course List", 77 | content: "In this accordion, you can add and look at courses. Clicking on labels opens their contents." 78 | }, 79 | { 80 | element: $("#classdatawraper").find('.addCourseButton')[0], 81 | title: "Add Courses", 82 | content: "If you want to add a course, simply click on the 'plus' icon next to its name" 83 | }, 84 | { 85 | element: $("#classdatawraper").find('[classid]')[0], 86 | title: "Add Specific Classes", 87 | content: "If you want to add a specific class, you can click on the 'plus' icon next to it.

All other required classes will automatically be filled by the generator" 88 | }, 89 | { 90 | element: $("#classdatawraper").find("td")[1], 91 | title: "Rate My Professor Ratings", 92 | content: "If you see a number beside a teacher's name, that is their Rate My Professor rating out of 5

You can specify the weighting of the RMP rating in the generator in preferences" 93 | }, 94 | { 95 | element: $("#searchcourses")[0], 96 | title: "Search Courses", 97 | content: "Here you can search for teachers, courses, classes, rooms, descriptions, faculties, subjects, prerequisites...

Almost anything!" 98 | }, 99 | { 100 | element: $("#locationselect")[0], 101 | title: "Change Location", 102 | content: "You can limit the location for classes to specific campuses or areas" 103 | }, 104 | { 105 | element: $("#courseSelector").find(".input-group-btn")[1], 106 | title: "Change Term", 107 | content: "You can change the term you're viewing in this university" 108 | }, 109 | { 110 | element: $("#MyCourses"), 111 | title: "My Courses", 112 | content: "All of your chosen courses are displayed here", 113 | placement: "left" 114 | }, 115 | { 116 | element: $("#coursegroups"), 117 | title: "Course Groups", 118 | content: "You can create groups of courses where the generator fulfills every group. You can change/remove the group type by clicking on its 'pill'" 119 | }, 120 | { 121 | element: $("#addGroupbtn"), 122 | title: "Adding Course Groups", 123 | content: "Clicking this will create a new course group

This is useful for electives where you only want one or two of the courses selected in the group" 124 | }, 125 | { 126 | element: $("#schedule"), 127 | title: "Calendar", 128 | content: "You can look through possible schedules on this calendar", 129 | placement: "left" 130 | }, 131 | { 132 | element: $("#calendarStatus"), 133 | title: "Blocking Timeslots", 134 | content: "You can block specific timeslots for the generator by clicking and dragging on the calendar

Clicking on a banned timeslot will unban it", 135 | placement: "left" 136 | }, 137 | { 138 | element: $("#prevSchedule"), 139 | title: "Browsing Schedules", 140 | content: "You can browse possible schedules by clicking the previous and next buttons here", 141 | placement: "left" 142 | }, 143 | { 144 | element: $("#scheduleutilities"), 145 | title: "Schedule Utilities", 146 | content: "Useful schedule utilities can be found here, you can:
* Download a picture of your schedule
* Copy your schedule to clipboard
* Remove all blocked timeslots
* Share your schedule to Facebook" 147 | }, 148 | { 149 | element: $("#preferencesbutton"), 150 | title: "Preferences", 151 | content: "You can change your schedule preferences and edit settings by clicking this button

You can change your preferences for morning/night classes, consecutive classes, and teacher quality over time slots.

You can also specify that you only want the generator to allow open classes (some universities have custom settings)", 152 | placement: "left", 153 | }, 154 | { 155 | element: $("#MyUniversity"), 156 | title: "Change University", 157 | content: "You can click here to open a dropdown and change your university", 158 | placement: "left" 159 | }, 160 | { 161 | element: $("#aboutbutton"), 162 | title: "About Us", 163 | content: "We are two Computer Science students that thought there was a better way to make university schedules

Please contact us using Github or Email if you'd like to say 'Hi', file a bug report, want us to add your university, or add a new feature!", 164 | placement: "left" 165 | }, 166 | { 167 | title: "That ended too soon!", 168 | content: "It looks like thats the end of our tour, remember you can always look at it again by going to preferences.

This project is completely open-source on Github and if you want to implement your university or add a feature, please do it!" 169 | } 170 | ], 171 | backdrop: true, 172 | orphan: true, 173 | onEnd: function (tour) { 174 | window.tourInProgress = false; 175 | 176 | // Show the scrollbars again 177 | $("#classdatawraper").css('overflow', 'auto'); 178 | $("#courseList").css('overflow', 'auto'); 179 | 180 | // repopulate the accordion with the default view 181 | classList.repopulateAccordion(); 182 | }, 183 | onShown: function(tour) { 184 | // If shown, disable pointer events 185 | var step = tour._options.steps[tour._current]; 186 | $(step.element).css('pointerEvents', 'none'); 187 | }, 188 | onHidden: function(tour) { 189 | // On hide, enable pointer events 190 | var step = tour._options.steps[tour._current]; 191 | $(step.element).css('pointerEvents', ''); 192 | } 193 | }); 194 | 195 | // Initialize the tour 196 | window.tour.init(); 197 | 198 | // Start the tour 199 | window.tour.start().goTo(0); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /js/modernizr.js: -------------------------------------------------------------------------------- 1 | /* Modernizr 2.8.3 (Custom Build) | MIT & BSD 2 | * Build: http://modernizr.com/download/#-fontface-backgroundsize-borderimage-borderradius-boxshadow-flexbox-hsla-multiplebgs-opacity-rgba-textshadow-cssanimations-csscolumns-generatedcontent-cssgradients-cssreflections-csstransforms-csstransforms3d-csstransitions-applicationcache-canvas-canvastext-draganddrop-hashchange-history-audio-video-indexeddb-input-inputtypes-localstorage-postmessage-sessionstorage-websockets-websqldatabase-webworkers-geolocation-inlinesvg-smil-svg-svgclippaths-touch-webgl-shiv-cssclasses-addtest-prefixed-teststyles-testprop-testallprops-hasevent-prefixes-domprefixes-load 3 | */ 4 | ;window.Modernizr=function(a,b,c){function C(a){j.cssText=a}function D(a,b){return C(n.join(a+";")+(b||""))}function E(a,b){return typeof a===b}function F(a,b){return!!~(""+a).indexOf(b)}function G(a,b){for(var d in a){var e=a[d];if(!F(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function H(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:E(f,"function")?f.bind(d||b):f}return!1}function I(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+p.join(d+" ")+d).split(" ");return E(b,"string")||E(b,"undefined")?G(e,b):(e=(a+" "+q.join(d+" ")+d).split(" "),H(e,b,c))}function J(){e.input=function(c){for(var d=0,e=c.length;d',a,""].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},z=function(){function d(d,e){e=e||b.createElement(a[d]||"div"),d="on"+d;var f=d in e;return f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=E(e[d],"function"),E(e[d],"undefined")||(e[d]=c),e.removeAttribute(d))),e=null,f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),A={}.hasOwnProperty,B;!E(A,"undefined")&&!E(A.call,"undefined")?B=function(a,b){return A.call(a,b)}:B=function(a,b){return b in a&&E(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=w.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(w.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(w.call(arguments)))};return e}),s.flexbox=function(){return I("flexWrap")},s.canvas=function(){var a=b.createElement("canvas");return!!a.getContext&&!!a.getContext("2d")},s.canvastext=function(){return!!e.canvas&&!!E(b.createElement("canvas").getContext("2d").fillText,"function")},s.webgl=function(){return!!a.WebGLRenderingContext},s.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:y(["@media (",n.join("touch-enabled),("),h,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=a.offsetTop===9}),c},s.geolocation=function(){return"geolocation"in navigator},s.postmessage=function(){return!!a.postMessage},s.websqldatabase=function(){return!!a.openDatabase},s.indexedDB=function(){return!!I("indexedDB",a)},s.hashchange=function(){return z("hashchange",a)&&(b.documentMode===c||b.documentMode>7)},s.history=function(){return!!a.history&&!!history.pushState},s.draganddrop=function(){var a=b.createElement("div");return"draggable"in a||"ondragstart"in a&&"ondrop"in a},s.websockets=function(){return"WebSocket"in a||"MozWebSocket"in a},s.rgba=function(){return C("background-color:rgba(150,255,150,.5)"),F(j.backgroundColor,"rgba")},s.hsla=function(){return C("background-color:hsla(120,40%,100%,.5)"),F(j.backgroundColor,"rgba")||F(j.backgroundColor,"hsla")},s.multiplebgs=function(){return C("background:url(https://),url(https://),red url(https://)"),/(url\s*\(.*?){3}/.test(j.background)},s.backgroundsize=function(){return I("backgroundSize")},s.borderimage=function(){return I("borderImage")},s.borderradius=function(){return I("borderRadius")},s.boxshadow=function(){return I("boxShadow")},s.textshadow=function(){return b.createElement("div").style.textShadow===""},s.opacity=function(){return D("opacity:.55"),/^0.55$/.test(j.opacity)},s.cssanimations=function(){return I("animationName")},s.csscolumns=function(){return I("columnCount")},s.cssgradients=function(){var a="background-image:",b="gradient(linear,left top,right bottom,from(#9f9),to(white));",c="linear-gradient(left top,#9f9, white);";return C((a+"-webkit- ".split(" ").join(b+a)+n.join(c+a)).slice(0,-a.length)),F(j.backgroundImage,"gradient")},s.cssreflections=function(){return I("boxReflect")},s.csstransforms=function(){return!!I("transform")},s.csstransforms3d=function(){var a=!!I("perspective");return a&&"webkitPerspective"in g.style&&y("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(b,c){a=b.offsetLeft===9&&b.offsetHeight===3}),a},s.csstransitions=function(){return I("transition")},s.fontface=function(){var a;return y('@font-face {font-family:"font";src:url("https://")}',function(c,d){var e=b.getElementById("smodernizr"),f=e.sheet||e.styleSheet,g=f?f.cssRules&&f.cssRules[0]?f.cssRules[0].cssText:f.cssText||"":"";a=/src/i.test(g)&&g.indexOf(d.split(" ")[0])===0}),a},s.generatedcontent=function(){var a;return y(["#",h,"{font:0/0 a}#",h,':after{content:"',l,'";visibility:hidden;font:3px/1 a}'].join(""),function(b){a=b.offsetHeight>=3}),a},s.video=function(){var a=b.createElement("video"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('video/ogg; codecs="theora"').replace(/^no$/,""),c.h264=a.canPlayType('video/mp4; codecs="avc1.42E01E"').replace(/^no$/,""),c.webm=a.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,"")}catch(d){}return c},s.audio=function(){var a=b.createElement("audio"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),c.mp3=a.canPlayType("audio/mpeg;").replace(/^no$/,""),c.wav=a.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),c.m4a=(a.canPlayType("audio/x-m4a;")||a.canPlayType("audio/aac;")).replace(/^no$/,"")}catch(d){}return c},s.localstorage=function(){try{return localStorage.setItem(h,h),localStorage.removeItem(h),!0}catch(a){return!1}},s.sessionstorage=function(){try{return sessionStorage.setItem(h,h),sessionStorage.removeItem(h),!0}catch(a){return!1}},s.webworkers=function(){return!!a.Worker},s.applicationcache=function(){return!!a.applicationCache},s.svg=function(){return!!b.createElementNS&&!!b.createElementNS(r.svg,"svg").createSVGRect},s.inlinesvg=function(){var a=b.createElement("div");return a.innerHTML="",(a.firstChild&&a.firstChild.namespaceURI)==r.svg},s.smil=function(){return!!b.createElementNS&&/SVGAnimate/.test(m.call(b.createElementNS(r.svg,"animate")))},s.svgclippaths=function(){return!!b.createElementNS&&/SVGClipPath/.test(m.call(b.createElementNS(r.svg,"clipPath")))};for(var K in s)B(s,K)&&(x=K.toLowerCase(),e[x]=s[K](),v.push((e[x]?"":"no-")+x));return e.input||J(),e.addTest=function(a,b){if(typeof a=="object")for(var d in a)B(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},C(""),i=k=null,function(a,b){function l(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function m(){var a=s.elements;return typeof a=="string"?a.split(" "):a}function n(a){var b=j[a[h]];return b||(b={},i++,a[h]=i,j[i]=b),b}function o(a,c,d){c||(c=b);if(k)return c.createElement(a);d||(d=n(c));var g;return d.cache[a]?g=d.cache[a].cloneNode():f.test(a)?g=(d.cache[a]=d.createElem(a)).cloneNode():g=d.createElem(a),g.canHaveChildren&&!e.test(a)&&!g.tagUrn?d.frag.appendChild(g):g}function p(a,c){a||(a=b);if(k)return a.createDocumentFragment();c=c||n(a);var d=c.frag.cloneNode(),e=0,f=m(),g=f.length;for(;e",g="hidden"in a,k=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){g=!0,k=!0}})();var s={elements:d.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:c,shivCSS:d.shivCSS!==!1,supportsUnknownElements:k,shivMethods:d.shivMethods!==!1,type:"default",shivDocument:r,createElement:o,createDocumentFragment:p};a.html5=s,r(b)}(this,b),e._version=d,e._prefixes=n,e._domPrefixes=q,e._cssomPrefixes=p,e.hasEvent=z,e.testProp=function(a){return G([a])},e.testAllProps=I,e.testStyles=y,e.prefixed=function(a,b,c){return b?I(a,b,c):I(a,"pfx")},g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+v.join(" "):""),e}(this,this.document),function(a,b,c){function d(a){return"[object Function]"==o.call(a)}function e(a){return"string"==typeof a}function f(){}function g(a){return!a||"loaded"==a||"complete"==a||"uninitialized"==a}function h(){var a=p.shift();q=1,a?a.t?m(function(){("c"==a.t?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&&g(l.readyState)&&(u.r=o=1,!q&&h(),l.onload=l.onreadystatechange=null,b)){"img"!=a&&m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&&y[c][d].onload()}}var j=j||B.errorTimeout,l=b.createElement(a),o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};1===y[c]&&(r=1,y[c]=[]),"object"==a?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),"img"!=a&&(r||2===y[c]?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i("c"==b?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),1==p.length&&h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=s?l:n.parentNode,l=a.opera&&"[object Opera]"==o.call(a.opera),l=!!b.attachEvent&&!l,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return"[object Array]"==o.call(a)},x=[],y={},z={timeout:function(a,b){return b.length&&(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;f .row{ 231 | height: -webkit-calc(100% - 80px); 232 | height: -moz-calc(100% - 80px); 233 | height: calc(100% - 80px); 234 | } 235 | 236 | .event { 237 | color: white; 238 | border-radius: 2px; 239 | text-align: left; 240 | font-size: 0.875rem; 241 | z-index: 2; 242 | top: 2px; 243 | left: 2px; 244 | right: 2px; 245 | padding: 0.3rem; 246 | overflow-x: hidden; 247 | transition: all 0.2s; 248 | cursor: pointer; 249 | position: absolute; 250 | overflow: hidden; 251 | } 252 | 253 | .event:hover { 254 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); 255 | } 256 | 257 | .event.double { 258 | height: 550%; 259 | } 260 | 261 | .row { 262 | margin-left: 0px; 263 | margin-right: 0px; 264 | padding-right: 10px; 265 | padding-left: 10px; 266 | } 267 | 268 | 269 | .icon { 270 | font-size: 1.5rem; 271 | margin: 0 1rem; 272 | text-align: center; 273 | cursor: pointer; 274 | vertical-align: middle; 275 | position: relative; 276 | top: -2px; 277 | } 278 | .dropdown-items{ 279 | font-size: 20px; 280 | } 281 | 282 | #Brandname{ 283 | font-weight: 200; 284 | font-size: 42px; 285 | color: #ffffff; 286 | } 287 | 288 | #customSize{ 289 | margin-left: 61em; 290 | } 291 | 292 | .popover-title{ 293 | text-align: center; 294 | font-size: 17px; 295 | font-weight: bold; 296 | } 297 | 298 | .col-md-6{ 299 | background-color: #eae9e9; 300 | } 301 | 302 | .col-md-5{ 303 | margin-left: 15px; 304 | background-color: #eae9e9; 305 | 306 | width: -webkit-calc(50% - 15px); 307 | width: -moz-calc(50% - 15px); 308 | width: calc(50% - 15px); 309 | 310 | padding: 15px; 311 | } 312 | 313 | .courseColor{ 314 | background-color: #ffffff; 315 | width: 97%; 316 | border-radius: 5px !important; 317 | margin-top: 10px; 318 | height: 43px; 319 | } 320 | 321 | .course{ 322 | font-size: 24px; 323 | color: #000000; 324 | padding-left: 12px; 325 | padding-top: 10px; 326 | padding-bottom: 10px; 327 | } 328 | 329 | #schedule{ 330 | padding-top: 5px; 331 | padding-bottom: 5px; 332 | margin-top: 15px; 333 | background: #eae9e9; 334 | 335 | height: -webkit-calc(66.6666666% - 15px); 336 | height: -moz-calc(66.6666666% - 15px); 337 | height: calc(66.6666666% - 15px); 338 | } 339 | 340 | p { 341 | margin: 0 !important; 342 | } 343 | 344 | .navbar-nav>li>a{ 345 | padding: 20px; 346 | } 347 | 348 | #CourseList-title { 349 | font-size: 27px; 350 | color: #000000; 351 | } 352 | 353 | #courseList { 354 | overflow: auto; 355 | 356 | height: -webkit-calc(100% - 36px); 357 | height: -moz-calc(100% - 36px); 358 | height: calc(100% - 36px); 359 | } 360 | 361 | .navbar-default { 362 | background-color: #0275d8; 363 | border-color: #0275d8; 364 | font-size: 30px; 365 | font-weight: 500; 366 | border-radius: 0 !important; 367 | } 368 | .navbar-default .navbar-brand { 369 | color: #fbfeff; 370 | font-size: 40px; 371 | } 372 | .navbar-default .navbar-brand:hover, 373 | .navbar-default .navbar-brand:focus { 374 | color: #ffffff; 375 | } 376 | .navbar-default .navbar-text { 377 | color: #fbfeff; 378 | } 379 | .navbar-default .navbar-nav > li > a { 380 | color: #fbfeff; 381 | -webkit-transition: color 0.5s; 382 | } 383 | .navbar-default .navbar-nav > li > a:hover, 384 | .navbar-default .navbar-nav > li > a:focus { 385 | color: #e6e6e6; 386 | -webkit-transition: color 0.5s; 387 | } 388 | .navbar-default .navbar-nav > .active > a, 389 | .navbar-default .navbar-nav > .active > a:hover, 390 | .navbar-default .navbar-nav > .active > a:focus { 391 | color: #fbfeff; 392 | background-color: #0275d8; 393 | } 394 | .navbar-default .navbar-nav > .open > a, 395 | .navbar-default .navbar-nav > .open > a:hover, 396 | .navbar-default .navbar-nav > .open > a:focus { 397 | color: #e6e6e6; 398 | background-color: #0275d8; 399 | } 400 | .navbar-default .navbar-toggle { 401 | border-color: #0275d8; 402 | } 403 | .navbar-default .navbar-toggle:hover, 404 | .navbar-default .navbar-toggle:focus { 405 | background-color: #0275d8; 406 | } 407 | .navbar-default .navbar-toggle .icon-bar { 408 | background-color: #fbfeff; 409 | } 410 | .navbar-default .navbar-collapse, 411 | .navbar-default .navbar-form { 412 | border-color: #fbfeff; 413 | } 414 | .navbar-default .navbar-link { 415 | color: #fbfeff; 416 | } 417 | .navbar-default .navbar-link:hover { 418 | color: #000000; 419 | } 420 | 421 | .table>tbody>tr>td{ 422 | border-top: none; 423 | } 424 | 425 | .table>tbody>tr>td{ 426 | border-right: none; 427 | padding: 0; 428 | padding-top: 10px; 429 | } 430 | 431 | 432 | 433 | .table>tbody>tr>td:last-child{ 434 | width: 35px; 435 | } 436 | 437 | .courseColor>table>tbody>tr>td>button.btn { 438 | padding: 0 !important; 439 | border-color: #ffffff; 440 | color: red; 441 | font-size: 20px; 442 | background-color: #ffffff; 443 | } 444 | 445 | .courseColor>table>tbody>tr>td>button.btn:hover { 446 | padding: 0 !important; 447 | border-color: #ffffff; 448 | color: #a90000; 449 | font-size: 20px; 450 | background-color: #ffffff; 451 | } 452 | 453 | .courseColor>table>tbody>tr>td>button.btn:focus { 454 | padding: 0 !important; 455 | font-size: 20px; 456 | outline: none; 457 | } 458 | 459 | .accordiontable>tbody>tr>td>button{ 460 | padding: 0 !important; 461 | float: right; 462 | color: #0275d8; 463 | font-size: 27px; 464 | border-color: #ffffff; 465 | } 466 | 467 | .accordiontable>tbody>tr>td>button:hover{ 468 | background-color: #ffffff; 469 | border-color: #ffffff; 470 | color: #0256ac; 471 | } 472 | 473 | .accordiontable>tbody>tr>td>button:focus{ 474 | background-color: #ffffff; 475 | border-color: #ffffff; 476 | color: #0256ac; 477 | outline: none; 478 | } 479 | 480 | #removeClassBtn { 481 | padding: 0 !important; 482 | float: right; 483 | color: #f33d3a; 484 | font-size: 27px; 485 | border-color: #ffffff; 486 | } 487 | 488 | #removeClassBtn:hover{ 489 | background-color: #ffffff; 490 | border-color: #ffffff; 491 | color: #a01715; 492 | } 493 | 494 | #removeClassBtn:focus{ 495 | background-color: #ffffff; 496 | border-color: #ffffff; 497 | color: #a01715; 498 | outline: none; 499 | } 500 | 501 | .addCourseButton { 502 | padding: 0 !important; 503 | float: right; 504 | color: #0275d8; 505 | font-size: 35px; 506 | margin-top: -7px; 507 | border-color: #ffffff; 508 | position: relative; 509 | top: 0px; 510 | right: 20px; 511 | margin-right: -20px; 512 | } 513 | 514 | .addCourseButton:hover{ 515 | background-color: #ffffff; 516 | border-color: #ffffff; 517 | color: #0256ac; 518 | } 519 | 520 | .addCourseButton:focus{ 521 | background-color: #ffffff; 522 | border-color: #ffffff; 523 | color: #0256ac; 524 | outline: none; 525 | } 526 | 527 | .removeCourseButton { 528 | padding: 0 !important; 529 | float: right; 530 | color: #f33d3a; 531 | font-size: 35px; 532 | margin-top: -7px; 533 | border-color: #ffffff; 534 | position: relative; 535 | top: 0px; 536 | right: 20px; 537 | margin-right: -20px; 538 | } 539 | 540 | .removeCourseButton:hover{ 541 | background-color: #ffffff; 542 | border-color: #ffffff; 543 | color: #a01715; 544 | } 545 | 546 | .removeCourseButton:focus{ 547 | background-color: #ffffff; 548 | border-color: #ffffff; 549 | color: #a01715; 550 | outline: none; 551 | } 552 | 553 | 554 | /* 555 | Remove button 556 | */ 557 | 558 | .removeBtn { 559 | padding: 0 !important; 560 | float: right; 561 | color: #f33d3a; 562 | font-size: 35px; 563 | margin-top: -7px; 564 | border-color: #ffffff; 565 | position: relative; 566 | top: 0px; 567 | right: 20px; 568 | margin-right: -20px; 569 | } 570 | 571 | .removeBtn:hover{ 572 | background-color: #ffffff; 573 | border-color: #ffffff; 574 | color: #a01715; 575 | } 576 | 577 | .removeBtn:focus{ 578 | background-color: #ffffff; 579 | border-color: #ffffff; 580 | color: #a01715; 581 | outline: none; 582 | } 583 | 584 | 585 | .nav-pills>li>a.MyCourses{ 586 | font-size: 29px; 587 | font-weight: 500; 588 | cursor: pointer; 589 | padding: 3px; !important; 590 | } 591 | 592 | .noselect { 593 | -webkit-touch-callout: none; /* iOS Safari */ 594 | -webkit-user-select: none; /* Chrome/Safari/Opera */ 595 | -khtml-user-select: none; /* Konqueror */ 596 | -moz-user-select: none; /* Firefox */ 597 | -ms-user-select: none; /* Internet Explorer/Edge */ 598 | user-select: none; 599 | } 600 | 601 | #MyCourses{ 602 | height: 33.333%; 603 | } 604 | 605 | #courseSelector{ 606 | height: 100%; 607 | padding: 15px; 608 | } 609 | 610 | .largecaret { 611 | border-width: 6px; 612 | } 613 | 614 | .btn-primary{ 615 | background-color: #0275d8; 616 | } 617 | 618 | .btn-primary:focus{ 619 | background-color: #0275d8; 620 | outline: none; 621 | } 622 | 623 | .nav-pills>li.active>a, .nav-pills>li.active>a:focus, .nav-pills>li.active>a:hover { 624 | background-color: #0275d8; 625 | cursor: default; 626 | } 627 | 628 | .nav-pills>li>a:hover{ 629 | background-color: #c5c4c4; 630 | } 631 | 632 | .nav-pills>li>a.MyCourses:hover{ 633 | background-color: #eae9e9; 634 | } 635 | 636 | td{ 637 | font-size: 17px; 638 | } 639 | 640 | .form-control{ 641 | display: inline; 642 | } 643 | 644 | 645 | .headcol{ 646 | width: 45px; 647 | } 648 | 649 | .navbar { 650 | margin-bottom: 10px; 651 | } 652 | 653 | 654 | @media (max-width: 767px) { 655 | .navbar-default .navbar-nav .open .dropdown-menu > li > a { 656 | color: #fbfeff; 657 | } 658 | 659 | .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, 660 | .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { 661 | color: #000000; 662 | } 663 | 664 | .navbar-default .navbar-nav .open .dropdown-menu > .active > a, 665 | .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, 666 | .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { 667 | color: #000000; 668 | background-color: #0275d8; 669 | } 670 | } 671 | 672 | @media (max-width: 1440px) { 673 | td{ 674 | font-size: 13px; 675 | } 676 | 677 | .rmplink { 678 | font-size: 13px !important; 679 | } 680 | 681 | thead th{ 682 | font-size: 0.8rem; 683 | padding-left: 0; 684 | } 685 | 686 | #CourseList-title { 687 | font-size: 20px; 688 | } 689 | 690 | .cd-accordion-menu label, .cd-accordion-menu a{ 691 | font-size: 1.1rem; 692 | } 693 | 694 | .accordiondesc{ 695 | font-size: 1rem; 696 | } 697 | } 698 | 699 | /* 700 | Reduced accordion font size for smaller widths 701 | */ 702 | 703 | @media (max-width: 1024px){ 704 | .event{ 705 | font-size: 0.7rem; 706 | } 707 | 708 | .cd-accordion-menu label, .cd-accordion-menu a{ 709 | font-size: 1.1rem; 710 | } 711 | 712 | } 713 | 714 | /* 715 | Mobile View 716 | */ 717 | 718 | @media (max-width: 991px){ 719 | body, html{ 720 | overflow: auto; 721 | } 722 | 723 | .col-md-5{ 724 | width: 100%; 725 | margin-left: 0; 726 | } 727 | 728 | #MyCourses{ 729 | margin-top: 15px; 730 | } 731 | } 732 | 733 | @media (max-height: 860px) and (max-width: 1920px){ 734 | #calendarStatus{ 735 | font-size: 27px; 736 | } 737 | } 738 | -------------------------------------------------------------------------------- /js/bootstrap-tour.min.js: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | * bootstrap-tour - v0.10.3 3 | * http://bootstraptour.com 4 | * ======================================================================== 5 | * Copyright 2012-2015 Ulrich Sossou 6 | * 7 | * ======================================================================== 8 | * Licensed under the MIT License (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * https://opensource.org/licenses/MIT 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | * ======================================================================== 20 | */ 21 | 22 | !function(t,e){return"function"==typeof define&&define.amd?define(["jquery"],function(o){return t.Tour=e(o)}):"object"==typeof exports?module.exports=e(require("jQuery")):t.Tour=e(t.jQuery)}(window,function(t){var e,o;return o=window.document,e=function(){function e(e){var o;try{o=window.localStorage}catch(n){o=!1}this._options=t.extend({name:"tour",steps:[],container:"body",autoscroll:!0,keyboard:!0,storage:o,debug:!1,backdrop:!1,backdropContainer:"body",backdropPadding:0,redirect:!0,orphan:!1,duration:!1,delay:!1,basePath:"",template:'',afterSetState:function(){},afterGetState:function(){},afterRemoveState:function(){},onStart:function(){},onEnd:function(){},onShow:function(){},onShown:function(){},onHide:function(){},onHidden:function(){},onNext:function(){},onPrev:function(){},onPause:function(){},onResume:function(){},onRedirectError:function(){}},e),this._force=!1,this._inited=!1,this._current=null,this.backdrop={overlay:null,$element:null,$background:null,backgroundShown:!1,overlayElementShown:!1}}return e.prototype.addSteps=function(t){var e,o,n;for(o=0,n=t.length;n>o;o++)e=t[o],this.addStep(e);return this},e.prototype.addStep=function(t){return this._options.steps.push(t),this},e.prototype.getStep=function(e){return null!=this._options.steps[e]?t.extend({id:"step-"+e,path:"",host:"",placement:"right",title:"",content:"

",next:e===this._options.steps.length-1?-1:e+1,prev:e-1,animation:!0,container:this._options.container,autoscroll:this._options.autoscroll,backdrop:this._options.backdrop,backdropContainer:this._options.backdropContainer,backdropPadding:this._options.backdropPadding,redirect:this._options.redirect,reflexElement:this._options.steps[e].element,backdropElement:this._options.steps[e].element,orphan:this._options.orphan,duration:this._options.duration,delay:this._options.delay,template:this._options.template,onShow:this._options.onShow,onShown:this._options.onShown,onHide:this._options.onHide,onHidden:this._options.onHidden,onNext:this._options.onNext,onPrev:this._options.onPrev,onPause:this._options.onPause,onResume:this._options.onResume,onRedirectError:this._options.onRedirectError},this._options.steps[e]):void 0},e.prototype.init=function(t){return this._force=t,this.ended()?(this._debug("Tour ended, init prevented."),this):(this.setCurrentStep(),this._initMouseNavigation(),this._initKeyboardNavigation(),this._onResize(function(t){return function(){return t.showStep(t._current)}}(this)),null!==this._current&&this.showStep(this._current),this._inited=!0,this)},e.prototype.start=function(t){var e;return null==t&&(t=!1),this._inited||this.init(t),null===this._current&&(e=this._makePromise(null!=this._options.onStart?this._options.onStart(this):void 0),this._callOnPromiseDone(e,this.showStep,0)),this},e.prototype.next=function(){var t;return t=this.hideStep(this._current,this._current+1),this._callOnPromiseDone(t,this._showNextStep)},e.prototype.prev=function(){var t;return t=this.hideStep(this._current,this._current-1),this._callOnPromiseDone(t,this._showPrevStep)},e.prototype.goTo=function(t){var e;return e=this.hideStep(this._current,t),this._callOnPromiseDone(e,this.showStep,t)},e.prototype.end=function(){var e,n;return e=function(e){return function(){return t(o).off("click.tour-"+e._options.name),t(o).off("keyup.tour-"+e._options.name),t(window).off("resize.tour-"+e._options.name),e._setState("end","yes"),e._inited=!1,e._force=!1,e._clearTimer(),null!=e._options.onEnd?e._options.onEnd(e):void 0}}(this),n=this.hideStep(this._current),this._callOnPromiseDone(n,e)},e.prototype.ended=function(){return!this._force&&!!this._getState("end")},e.prototype.restart=function(){return this._removeState("current_step"),this._removeState("end"),this._removeState("redirect_to"),this.start()},e.prototype.pause=function(){var t;return t=this.getStep(this._current),t&&t.duration?(this._paused=!0,this._duration-=(new Date).getTime()-this._start,window.clearTimeout(this._timer),this._debug("Paused/Stopped step "+(this._current+1)+" timer ("+this._duration+" remaining)."),null!=t.onPause?t.onPause(this,this._duration):void 0):this},e.prototype.resume=function(){var t;return t=this.getStep(this._current),t&&t.duration?(this._paused=!1,this._start=(new Date).getTime(),this._duration=this._duration||t.duration,this._timer=window.setTimeout(function(t){return function(){return t._isLast()?t.next():t.end()}}(this),this._duration),this._debug("Started step "+(this._current+1)+" timer with duration "+this._duration),null!=t.onResume&&this._duration!==t.duration?t.onResume(this,this._duration):void 0):this},e.prototype.hideStep=function(e,o){var n,r,i,s;return(s=this.getStep(e))?(this._clearTimer(),i=this._makePromise(null!=s.onHide?s.onHide(this,e):void 0),r=function(n){return function(){var r,i;return r=t(s.element),r.data("bs.popover")||r.data("popover")||(r=t("body")),r.popover("destroy").removeClass("tour-"+n._options.name+"-element tour-"+n._options.name+"-"+e+"-element").removeData("bs.popover").focus(),s.reflex&&t(s.reflexElement).removeClass("tour-step-element-reflex").off(""+n._reflexEvent(s.reflex)+".tour-"+n._options.name),s.backdrop&&(i=null!=o&&n.getStep(o),i&&i.backdrop&&i.backdropElement===s.backdropElement||n._hideBackdrop()),null!=s.onHidden?s.onHidden(n):void 0}}(this),n=s.delay.hide||s.delay,"[object Number]"==={}.toString.call(n)&&n>0?(this._debug("Wait "+n+" milliseconds to hide the step "+(this._current+1)),window.setTimeout(function(t){return function(){return t._callOnPromiseDone(i,r)}}(this),n)):this._callOnPromiseDone(i,r),i):void 0},e.prototype.showStep=function(t){var e,n,r,i,s,a;return this.ended()?(this._debug("Tour ended, showStep prevented."),this):(a=this.getStep(t),a&&(s=t0?(this._debug("Wait "+r+" milliseconds to show the step "+(this._current+1)),window.setTimeout(function(t){return function(){return t._callOnPromiseDone(n,i)}}(this),r)):this._callOnPromiseDone(n,i),n):void 0)},e.prototype.getCurrentStep=function(){return this._current},e.prototype.setCurrentStep=function(t){return null!=t?(this._current=t,this._setState("current_step",t)):(this._current=this._getState("current_step"),this._current=null===this._current?null:parseInt(this._current,10)),this},e.prototype.redraw=function(){return this._showOverlayElement(this.getStep(this.getCurrentStep()).element,!0)},e.prototype._setState=function(t,e){var o,n;if(this._options.storage){n=""+this._options.name+"_"+t;try{this._options.storage.setItem(n,e)}catch(r){o=r,o.code===DOMException.QUOTA_EXCEEDED_ERR&&this._debug("LocalStorage quota exceeded. State storage failed.")}return this._options.afterSetState(n,e)}return null==this._state&&(this._state={}),this._state[t]=e},e.prototype._removeState=function(t){var e;return this._options.storage?(e=""+this._options.name+"_"+t,this._options.storage.removeItem(e),this._options.afterRemoveState(e)):null!=this._state?delete this._state[t]:void 0},e.prototype._getState=function(t){var e,o;return this._options.storage?(e=""+this._options.name+"_"+t,o=this._options.storage.getItem(e)):null!=this._state&&(o=this._state[t]),(void 0===o||"null"===o)&&(o=null),this._options.afterGetState(t,o),o},e.prototype._showNextStep=function(){var t,e,o;return o=this.getStep(this._current),e=function(t){return function(){return t.showStep(o.next)}}(this),t=this._makePromise(null!=o.onNext?o.onNext(this):void 0),this._callOnPromiseDone(t,e)},e.prototype._showPrevStep=function(){var t,e,o;return o=this.getStep(this._current),e=function(t){return function(){return t.showStep(o.prev)}}(this),t=this._makePromise(null!=o.onPrev?o.onPrev(this):void 0),this._callOnPromiseDone(t,e)},e.prototype._debug=function(t){return this._options.debug?window.console.log("Bootstrap Tour '"+this._options.name+"' | "+t):void 0},e.prototype._isRedirect=function(t,e,o){var n;return null!=t&&""!==t&&("[object RegExp]"==={}.toString.call(t)&&!t.test(o.origin)||"[object String]"==={}.toString.call(t)&&this._isHostDifferent(t,o))?!0:(n=[o.pathname,o.search,o.hash].join(""),null!=e&&""!==e&&("[object RegExp]"==={}.toString.call(e)&&!e.test(n)||"[object String]"==={}.toString.call(e)&&this._isPathDifferent(e,n)))},e.prototype._isHostDifferent=function(t,e){switch({}.toString.call(t)){case"[object RegExp]":return!t.test(e.origin);case"[object String]":return this._getProtocol(t)!==this._getProtocol(e.href)||this._getHost(t)!==this._getHost(e.href);default:return!0}},e.prototype._isPathDifferent=function(t,e){return this._getPath(t)!==this._getPath(e)||!this._equal(this._getQuery(t),this._getQuery(e))||!this._equal(this._getHash(t),this._getHash(e))},e.prototype._isJustPathHashDifferent=function(t,e,o){var n;return null!=t&&""!==t&&this._isHostDifferent(t,o)?!1:(n=[o.pathname,o.search,o.hash].join(""),"[object String]"==={}.toString.call(e)?this._getPath(e)===this._getPath(n)&&this._equal(this._getQuery(e),this._getQuery(n))&&!this._equal(this._getHash(e),this._getHash(n)):!1)},e.prototype._redirect=function(e,n,r){var i;return t.isFunction(e.redirect)?e.redirect.call(this,r):(i="[object String]"==={}.toString.call(e.host)?""+e.host+r:r,this._debug("Redirect to "+i),this._getState("redirect_to")!==""+n?(this._setState("redirect_to",""+n),o.location.href=i):(this._debug("Error redirection loop to "+r),this._removeState("redirect_to"),null!=e.onRedirectError?e.onRedirectError(this):void 0))},e.prototype._isOrphan=function(e){return null==e.element||!t(e.element).length||t(e.element).is(":hidden")&&"http://www.w3.org/2000/svg"!==t(e.element)[0].namespaceURI},e.prototype._isLast=function(){return this._current").parent().html()},e.prototype._reflexEvent=function(t){return"[object Boolean]"==={}.toString.call(t)?"click":t},e.prototype._focus=function(t,e,o){var n,r;return r=o?"end":"next",n=t.find("[data-role='"+r+"']"),e.on("shown.bs.popover",function(){return n.focus()})},e.prototype._reposition=function(e,n){var r,i,s,a,u,p,h;if(a=e[0].offsetWidth,i=e[0].offsetHeight,h=e.offset(),u=h.left,p=h.top,r=t(o).outerHeight()-h.top-e.outerHeight(),0>r&&(h.top=h.top+r),s=t("html").outerWidth()-h.left-e.outerWidth(),0>s&&(h.left=h.left+s),h.top<0&&(h.top=0),h.left<0&&(h.left=0),e.offset(h),"bottom"===n.placement||"top"===n.placement){if(u!==h.left)return this._replaceArrow(e,2*(h.left-u),a,"left")}else if(p!==h.top)return this._replaceArrow(e,2*(h.top-p),i,"top")},e.prototype._center=function(e){return e.css("top",t(window).outerHeight()/2-e.outerHeight()/2)},e.prototype._replaceArrow=function(t,e,o,n){return t.find(".arrow").css(n,e?50*(1-e/o)+"%":"")},e.prototype._scrollIntoView=function(e,o){var n,r,i,s,a,u,p;if(n=t(e.element),!n.length)return o();switch(r=t(window),a=n.offset().top,s=n.outerHeight(),p=r.height(),u=0,e.placement){case"top":u=Math.max(0,a-p/2);break;case"left":case"right":u=Math.max(0,a+s/2-p/2);break;case"bottom":u=Math.max(0,a+s-p/2)}return this._debug("Scroll into view. ScrollTop: "+u+". Element offset: "+a+". Window height: "+p+"."),i=0,t("body, html").stop(!0,!0).animate({scrollTop:Math.ceil(u)},function(t){return function(){return 2===++i?(o(),t._debug("Scroll into view.\nAnimation end element offset: "+n.offset().top+".\nWindow height: "+r.height()+".")):void 0}}(this))},e.prototype._onResize=function(e,o){return t(window).on("resize.tour-"+this._options.name,function(){return clearTimeout(o),o=setTimeout(e,100)})},e.prototype._initMouseNavigation=function(){var e;return e=this,t(o).off("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='prev']").off("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='next']").off("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='end']").off("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='pause-resume']").on("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='next']",function(t){return function(e){return e.preventDefault(),t.next()}}(this)).on("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='prev']",function(t){return function(e){return e.preventDefault(),t._current>0?t.prev():void 0}}(this)).on("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='end']",function(t){return function(e){return e.preventDefault(),t.end()}}(this)).on("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='pause-resume']",function(o){var n;return o.preventDefault(),n=t(this),n.text(e._paused?n.data("pause-text"):n.data("resume-text")),e._paused?e.resume():e.pause()})},e.prototype._initKeyboardNavigation=function(){return this._options.keyboard?t(o).on("keyup.tour-"+this._options.name,function(t){return function(e){if(e.which)switch(e.which){case 39:return e.preventDefault(),t._isLast()?t.next():t.end();case 37:if(e.preventDefault(),t._current>0)return t.prev()}}}(this)):void 0},e.prototype._makePromise=function(e){return e&&t.isFunction(e.then)?e:null},e.prototype._callOnPromiseDone=function(t,e,o){return t?t.then(function(t){return function(){return e.call(t,o)}}(this)):e.call(this,o)},e.prototype._showBackdrop=function(e){return this.backdrop.backgroundShown?void 0:(this.backdrop=t("
",{"class":"tour-backdrop"}),this.backdrop.backgroundShown=!0,t(e.backdropContainer).append(this.backdrop))},e.prototype._hideBackdrop=function(){return this._hideOverlayElement(),this._hideBackground()},e.prototype._hideBackground=function(){return this.backdrop&&this.backdrop.remove?(this.backdrop.remove(),this.backdrop.overlay=null,this.backdrop.backgroundShown=!1):void 0},e.prototype._showOverlayElement=function(e,o){var n,r,i;return r=t(e.element),n=t(e.backdropElement),!r||0===r.length||this.backdrop.overlayElementShown&&!o?void 0:(this.backdrop.overlayElementShown||(this.backdrop.$element=n.addClass("tour-step-backdrop"),this.backdrop.$background=t("
",{"class":"tour-step-background"}),this.backdrop.$background.appendTo(e.backdropContainer),this.backdrop.overlayElementShown=!0),i={width:n.innerWidth(),height:n.innerHeight(),offset:n.offset()},e.backdropPadding&&(i=this._applyBackdropPadding(e.backdropPadding,i)),this.backdrop.$background.width(i.width).height(i.height).offset(i.offset))},e.prototype._hideOverlayElement=function(){return this.backdrop.overlayElementShown?(this.backdrop.$element.removeClass("tour-step-backdrop"),this.backdrop.$background.remove(),this.backdrop.$element=null,this.backdrop.$background=null,this.backdrop.overlayElementShown=!1):void 0},e.prototype._applyBackdropPadding=function(t,e){return"object"==typeof t?(null==t.top&&(t.top=0),null==t.right&&(t.right=0),null==t.bottom&&(t.bottom=0),null==t.left&&(t.left=0),e.offset.top=e.offset.top-t.top,e.offset.left=e.offset.left-t.left,e.width=e.width+t.left+t.right,e.height=e.height+t.top+t.bottom):(e.offset.top=e.offset.top-t,e.offset.left=e.offset.left-t,e.width=e.width+2*t,e.height=e.height+2*t),e},e.prototype._clearTimer=function(){return window.clearTimeout(this._timer),this._timer=null,this._duration=null},e.prototype._getProtocol=function(t){return t=t.split("://"),t.length>1?t[0]:"http"},e.prototype._getHost=function(t){return t=t.split("//"),t=t.length>1?t[1]:t[0],t.split("/")[0]},e.prototype._getPath=function(t){return t.replace(/\/?$/,"").split("?")[0].split("#")[0]},e.prototype._getQuery=function(t){return this._getParams(t,"?")},e.prototype._getHash=function(t){return this._getParams(t,"#")},e.prototype._getParams=function(t,e){var o,n,r,i,s;if(n=t.split(e),1===n.length)return{};for(n=n[1].split("&"),r={},i=0,s=n.length;s>i;i++)o=n[i],o=o.split("="),r[o[0]]=o[1]||"";return r},e.prototype._equal=function(t,e){var o,n,r,i,s,a;if("[object Object]"==={}.toString.call(t)&&"[object Object]"==={}.toString.call(e)){if(n=Object.keys(t),r=Object.keys(e),n.length!==r.length)return!1;for(o in t)if(i=t[o],!this._equal(e[o],i))return!1;return!0}if("[object Array]"==={}.toString.call(t)&&"[object Array]"==={}.toString.call(e)){if(t.length!==e.length)return!1;for(o=s=0,a=t.length;a>s;o=++s)if(i=t[o],!this._equal(i,e[o]))return!1;return!0}return t===e},e}()}); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Schedule Storm 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 78 | 79 | 80 | 81 | 98 | 99 | 147 | 148 | 201 | 202 | 225 | 226 |
227 |
228 |
229 | 230 | 231 | 232 |
233 | 234 | 235 |
236 | 237 | 238 |
239 | 240 | 241 |
242 | 243 |
244 |
245 |
    246 |
    247 |
248 |
249 | 250 |
251 | My Courses 252 | 253 | 254 | 255 | 256 | 257 |
    258 |
    259 |
260 | 261 |
262 | 263 |
264 |
265 |
266 | 267 | 268 |
0
269 |
/
270 |
0
271 |
272 | 273 |
274 | 275 |
276 | 279 | 280 | 283 | 284 | 287 | 288 | 291 | 292 | 295 |
296 |
Score:
0
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 | 306 | 307 | -------------------------------------------------------------------------------- /js/core/MyCourses.js: -------------------------------------------------------------------------------- 1 | class MyCourses { 2 | constructor(uni, term) { 3 | this.courses = []; 4 | this.generator = false; 5 | 6 | this.uni = uni; 7 | this.term = term; 8 | 9 | $("#coursegroups").empty(); 10 | $("#courseList").empty(); 11 | 12 | // Update the preferences shown 13 | window.preferences.updatedUni(uni); 14 | 15 | this.numConvert = { 16 | "-1": "None", 17 | 0: "All", 18 | 1: "One", 19 | 2: "Two", 20 | 3: "Three", 21 | 4: "Four", 22 | 5: "Five" 23 | } 24 | } 25 | 26 | /* 27 | Creates and appends the "Add group" button 28 | */ 29 | genAddGroupBtn() { 30 | var self = this; 31 | 32 | var addGroupbtn = $(''); 34 | 35 | // Initialize the tooltip 36 | addGroupbtn.tooltip(); 37 | 38 | addGroupbtn.click(function (event) { 39 | // Add One of group 40 | self.addGroup(1); 41 | }); 42 | 43 | $("#coursegroups").append(addGroupbtn); 44 | } 45 | 46 | /* 47 | If there is a saved state, loads it and populates the courses 48 | If not, sets up the initial state 49 | 50 | Called by ClassList when done loading the class list 51 | */ 52 | loadState() { 53 | 54 | // generate the add group btn 55 | this.genAddGroupBtn(); 56 | 57 | var loadedState = localStorage.getItem(this.uni + "_" + this.term + "_saved"); 58 | 59 | // Parse it 60 | if (loadedState != null) loadedState = JSON.parse(loadedState); 61 | 62 | // Make sure it has a length > 0 63 | if (loadedState != null && loadedState.length > 0) { 64 | console.log("Loaded saved state"); 65 | 66 | this.courses = loadedState; 67 | 68 | for (var group in this.courses) { 69 | var thisgroup = this.courses[group]; 70 | 71 | if (group == 0) { 72 | // you cannot remove the first group 73 | this.generatePill(group, thisgroup["type"], true); 74 | } 75 | else { 76 | this.generatePill(group, thisgroup["type"]); 77 | } 78 | } 79 | // set the first group active 80 | this.setGroupActive(0); 81 | 82 | // start generation 83 | this.startGeneration(); 84 | } 85 | else { 86 | // add default group 87 | this.addGroup(0, true); 88 | this.setGroupActive(0); 89 | } 90 | } 91 | 92 | /* 93 | Saves the current selected courses into localStorage 94 | */ 95 | saveState() { 96 | localStorage.setItem(this.uni + "_" + this.term + "_saved", JSON.stringify(this.courses)); 97 | } 98 | 99 | /* 100 | Adds a new course group of the specified type (0 for All, 1 for one, etc..) 101 | */ 102 | addGroup(type, noremove) { 103 | // make sure we have 4 total groups or less 104 | if (this.courses.length <= 3) { 105 | var thisgroup = {"type": type, "courses": {}}; 106 | var id = this.courses.length; 107 | this.courses[id] = thisgroup; 108 | 109 | this.generatePill(id, type, noremove); 110 | } 111 | 112 | // Remove the add button if the max group amount is exceeded 113 | if (this.courses.length == 4) $("#addGroupbtn").hide(); 114 | } 115 | 116 | /* 117 | Generates, binds, and appends the given pill with the speicifed id and type 118 | */ 119 | generatePill(id, type, noremove) { 120 | var self = this; 121 | 122 | var text = this.numConvert[type] + " of"; 123 | 124 | var html = $('') 125 | 126 | html.find("a:first").click(function(e){ 127 | // If a pill is already selected, open the dropdown 128 | // If not, set the pill as active 129 | 130 | // check if this group is already active 131 | var groupid = $(this).parent().attr("groupid"); 132 | 133 | // Check if we need to set this as active 134 | if (groupid != self.activeGroup) { 135 | // we don't want the dropdown to open for this item 136 | e.stopPropagation(); 137 | 138 | // check if the dropdown for the old active pill is open 139 | // if so, close it 140 | var isopen = $('li[groupid="' + self.activeGroup + '"]').hasClass("open"); 141 | 142 | if (isopen == true) { 143 | // close it 144 | $('li[groupid="' + self.activeGroup + '"]').find('.dropdown-menu').dropdown('toggle'); 145 | } 146 | 147 | // set this group as active 148 | self.setGroupActive(groupid); 149 | } 150 | }); 151 | 152 | // Populate the dropdown 153 | html.find('.dropdown-menu').append(this.generatePillDropdown(noremove)); 154 | 155 | // Bind the dropdown click handler 156 | html.find('li').click(function (event) { 157 | // find the group type 158 | var grouptype = $(this).attr("grouptype"); 159 | // find the group id 160 | var groupid = $(this).parent().parent().attr("groupid"); 161 | 162 | if (grouptype == "remove") { 163 | // wants to remove this group 164 | self.removeGroup(groupid); 165 | } 166 | else { 167 | // Change the group type 168 | self.changeGroupType(groupid, grouptype); 169 | } 170 | }); 171 | 172 | $("#addGroupbtn").before(html); 173 | } 174 | 175 | /* 176 | Removes the specified group and removes the appropriate HTML elements 177 | */ 178 | removeGroup(groupid) { 179 | groupid = parseInt(groupid); 180 | 181 | // we need to remove this pill 182 | $('li[groupid="' + groupid + '"]').remove(); 183 | 184 | // set the previous group to active 185 | this.setGroupActive(groupid-1); 186 | 187 | // we need to change the HTML groupid tags of the groups after this one 188 | if ((groupid+1) < this.courses.length) { 189 | // this is not the last group 190 | 191 | // decrement the groupid of every subsequent group 192 | for (var x = (groupid+1); x < this.courses.length; x++) { 193 | $('li[groupid="' + x + '"]').attr("groupid", x-1); 194 | } 195 | } 196 | 197 | // now we need to splice the array 198 | this.courses.splice(groupid, 1); 199 | 200 | // Check if we can display the add button again 201 | if (this.courses.length < 4) $("#addGroupbtn").show(); 202 | 203 | // regenerate the schedules 204 | this.startGeneration(); 205 | } 206 | 207 | /* 208 | Changes the type of a group type and updates the element 209 | */ 210 | changeGroupType(id, type) { 211 | this.courses[id]["type"] = type; 212 | 213 | // Change the HTML 214 | $('li[groupid="' + id + '"]').find("a:first").html(this.numConvert[type] + ' of'); 215 | 216 | this.startGeneration(); 217 | } 218 | 219 | /* 220 | Sets the specified group to active 221 | */ 222 | setGroupActive(id) { 223 | // remove old active class 224 | if (this.activeGroup != undefined) { 225 | $('li[groupid="' + this.activeGroup + '"]').removeClass("active"); 226 | } 227 | 228 | this.activeGroup = id; 229 | $('li[groupid="' + id + '"]').addClass("active"); 230 | 231 | // now display all the courses in the group 232 | this.displayGroup(this.activeGroup); 233 | } 234 | 235 | /* 236 | Populates the courses in the specified group 237 | */ 238 | displayGroup(group) { 239 | var self = this; 240 | 241 | // empty out any current courses 242 | $("#courseList").empty(); 243 | 244 | for (var course in self.courses[self.activeGroup]["courses"]) { 245 | var course = self.courses[self.activeGroup]["courses"][course]; 246 | 247 | self.displayCourse(course["obj"], course["obj"]["path"]); 248 | } 249 | } 250 | 251 | /* 252 | Generates the dropdown HTML for a group pill 253 | */ 254 | generatePillDropdown(noremove) { 255 | var html = ''; 256 | 257 | // Add 'all of' to the top 258 | html += '
  • ' + this.numConvert[0] + ' of
  • '; 259 | 260 | for (var x of Object.keys(this.numConvert).sort()) { 261 | if (x == 0) continue; 262 | html += '
  • ' + this.numConvert[x] + ' of
  • '; 263 | } 264 | 265 | if (noremove != true) { 266 | html += ''; 267 | html += '
  • Remove
  • '; 268 | } 269 | 270 | return html; 271 | } 272 | 273 | /* 274 | Expands the type name (LEC = Lecture, TUT = Tutorial) 275 | */ 276 | typeExpand(type) { 277 | var map = { 278 | "LEC": "Lecture", 279 | "TUT": "Tutorial", 280 | "LAB": "Lab", 281 | "SEM": "Seminar", 282 | "LCL": "Lecture/Lab", 283 | "LBL": "Lab/Lecture", 284 | "CLN": "Clinic", 285 | "DD": "Distance Delivery", 286 | "BL": "Blended Delivery", 287 | "WKT": "Work Term", 288 | "FLD": "Field Work", 289 | "PRC": "Practicum", 290 | "CLI": "Clinical", 291 | "IDS": "Internship" 292 | } 293 | 294 | if (map[type] != undefined) { 295 | return map[type]; 296 | } 297 | else { 298 | return type; 299 | } 300 | } 301 | 302 | /* 303 | Deletes the given course in any group except the passed in one 304 | */ 305 | deleteCourseFromNonSafe(delcourse, safegroup) { 306 | // iterate the groups 307 | for (var group in this.courses) { 308 | if (group != safegroup) { 309 | // we can delete in this group 310 | for (var course in this.courses[group]["courses"]) { 311 | if (course == delcourse) { 312 | delete this.courses[group]["courses"][course]; 313 | } 314 | } 315 | } 316 | } 317 | } 318 | 319 | /* 320 | Adds the specified course to the current active group and populates the HTML 321 | */ 322 | addCourse(course, path, classid) { 323 | var self = this; 324 | 325 | // We want a separate copy of the obj to work on 326 | course = jQuery.extend({}, course); 327 | 328 | // add the path to the obj 329 | course["path"] = path; 330 | 331 | var subject = path.split("\\"); 332 | 333 | var coursenum = subject[subject.length-1] // 203 334 | var subject = subject[subject.length-2]; // CPSC 335 | 336 | var coursecode = subject + " " + coursenum; // CPSC 203 337 | 338 | 339 | // Add the key if it isn't there 340 | if (self.courses[self.activeGroup]["courses"][coursecode] == undefined) { 341 | self.courses[self.activeGroup]["courses"][coursecode] = {}; 342 | self.courses[self.activeGroup]["courses"][coursecode]["types"] = {}; 343 | 344 | // add the possible types 345 | for (var classv in course["classes"]) { 346 | if (course["classes"][classv]["type"] != undefined) { 347 | var thistype = course["classes"][classv]["type"]; 348 | self.courses[self.activeGroup]["courses"][coursecode]["types"][thistype] = true; 349 | } 350 | } 351 | 352 | // check to see if any other groups have this course, is so, delete the course from them 353 | self.deleteCourseFromNonSafe(coursecode, self.activeGroup); 354 | 355 | self.displayCourse(course, path, undefined, true); 356 | } 357 | 358 | var thiscourse = self.courses[self.activeGroup]["courses"][coursecode]; 359 | 360 | // set the course obj 361 | thiscourse["obj"] = course; 362 | 363 | if (classid != undefined) { 364 | var classtype = true; 365 | 366 | // figure out the class type 367 | for (var classv in course["classes"]) { 368 | if (course["classes"][classv]["id"] == classid) { 369 | classtype = course["classes"][classv]["type"]; 370 | break; 371 | } 372 | } 373 | 374 | if (thiscourse["types"][classtype] != true) { 375 | // update the class list button (remove the old class button) 376 | window.classList.updateRemovedClass(thiscourse["types"][classtype]); 377 | } 378 | 379 | thiscourse["types"][classtype] = classid; 380 | 381 | // Update the accordion if its open 382 | self.updateAccordion(coursecode); 383 | 384 | // update the classlist buttons 385 | window.classList.updateAddedCourse(coursecode); 386 | } 387 | 388 | this.startGeneration(); 389 | } 390 | 391 | /* 392 | Updates the data in the given open accordion 393 | */ 394 | updateAccordion(course) { 395 | var self = this; 396 | 397 | // get the label 398 | var label = $('label[path="' + course + '"]'); 399 | 400 | // Check if its open 401 | if (label.attr("accordopen") == "true") { 402 | // update it 403 | label.attr("accordopen", "false"); 404 | label.parent().find("ul:first").slideUp(function () { 405 | $(this).empty(); 406 | self.bindButton(label, "course"); 407 | }); 408 | } 409 | } 410 | 411 | /* 412 | Removes a course from the UI and courses obj 413 | */ 414 | removeCourse(course) { 415 | for (var group in this.courses) { 416 | var thisgroup = this.courses[group]; 417 | 418 | if (thisgroup["courses"][course] != undefined) { 419 | 420 | // Remove any remove class buttons since those classes are no longer added 421 | for (var classval in thisgroup["courses"][course]["types"]) { 422 | var thisclassval = thisgroup["courses"][course]["types"][classval]; 423 | 424 | if (thisclassval != true) { 425 | window.classList.updateRemovedClass(thisclassval); 426 | } 427 | } 428 | 429 | // Delete this course 430 | delete thisgroup["courses"][course]; 431 | 432 | // check if its the active group 433 | // if so, remove the UI element 434 | if (group == this.activeGroup) { 435 | var label = $('label[path="' + course + '"]'); 436 | label.parent().slideUp(function () { 437 | $(this).empty(); 438 | }); 439 | } 440 | } 441 | } 442 | 443 | // Restart generation 444 | this.startGeneration(); 445 | } 446 | 447 | /* 448 | Returns a boolean as to whether a specified course is in any selected group 449 | */ 450 | hasCourse(course) { 451 | for (var group in this.courses) { 452 | var thisgroup = this.courses[group]; 453 | 454 | if (thisgroup["courses"][course] != undefined) { 455 | return true; 456 | } 457 | } 458 | 459 | // We didn't find a result 460 | return false; 461 | } 462 | 463 | /* 464 | Returns a boolean as to whether the specified class id has been selected in any group 465 | */ 466 | hasClass(classid) { 467 | for (var group in this.courses) { 468 | var thisgroup = this.courses[group]; 469 | 470 | for (var course in thisgroup["courses"]) { 471 | for (var classv in thisgroup["courses"][course]["types"]) { 472 | if (thisgroup["courses"][course]["types"][classv] == classid) { 473 | return true; 474 | } 475 | } 476 | } 477 | } 478 | return false; 479 | } 480 | 481 | /* 482 | Removes the specified class from the UI and generation 483 | */ 484 | removeClass(classid) { 485 | for (var group in this.courses) { 486 | var thisgroup = this.courses[group]; 487 | 488 | for (var course in thisgroup["courses"]) { 489 | for (var classv in thisgroup["courses"][course]["types"]) { 490 | if (thisgroup["courses"][course]["types"][classv] == classid) { 491 | thisgroup["courses"][course]["types"][classv] = true; 492 | 493 | // update UI 494 | this.updateAccordion(course); 495 | 496 | // update the generation 497 | this.startGeneration(); 498 | 499 | return true; 500 | } 501 | } 502 | } 503 | } 504 | 505 | return false; 506 | } 507 | 508 | /* 509 | Appends the given course to the current courselist HTML 510 | */ 511 | displayCourse(course, path, classid, animated) { 512 | var self = this; 513 | 514 | var html = ""; 515 | if (classid == undefined) { 516 | html = $(this.generateCourseHTML(course, path)); 517 | 518 | html.find("label").click(function (event) { 519 | event.stopPropagation(); 520 | self.bindButton(this, "course"); 521 | }); 522 | 523 | // bind remove button 524 | html.find(".removeBtn").click(function (event) { 525 | event.stopPropagation(); 526 | 527 | var coursecode = $(this).parent().attr("path"); 528 | 529 | // remove the course in My Courses 530 | self.removeCourse(coursecode); 531 | 532 | // we want to update the general course remove button 533 | window.classList.updateRemovedCourse($(this).parent().attr("path")); 534 | }) 535 | } 536 | 537 | if (animated) { 538 | html.hide().prependTo("#courseList").slideDown(); 539 | } 540 | else { 541 | $("#courseList").prepend(html); 542 | } 543 | } 544 | 545 | /* 546 | Binds an accordion click 547 | */ 548 | bindButton(button, type) { 549 | var self = this; 550 | 551 | // Onclick handler 552 | 553 | // do we need to close the element? 554 | if ($(button).attr("accordopen") == "true") { 555 | // Close the element 556 | $(button).attr("accordopen", "false"); 557 | 558 | $(button).parent().find("ul").slideUp(function () { 559 | $(this).empty(); 560 | }); 561 | 562 | } 563 | else { 564 | 565 | // Open accordion 566 | var thispath = $(button).attr("path"); 567 | $(button).attr("accordopen", "true"); 568 | 569 | var element = $(button).parent().find("ul"); 570 | 571 | // Populate depending on type 572 | if (type == "course") { 573 | // Element to populate 574 | self.displayCourseDropDown(element, thispath); 575 | } 576 | } 577 | } 578 | 579 | /* 580 | Generates the dropdown when clicking on a course in MyCourses 581 | */ 582 | displayCourseDropDown(element, coursecode) { 583 | var self = this; 584 | 585 | element.slideUp(function () { 586 | 587 | var thiscourse = self.courses[self.activeGroup]["courses"][coursecode]; 588 | 589 | // iterate through each class type 590 | for (var type in thiscourse["types"]) { 591 | var thistype = thiscourse["types"][type]; 592 | if (thistype == true) { 593 | // They don't have a specific selection, we'll have to generate it 594 | var html = '
    ' + self.typeExpand(type) + '
    '; 595 | element.append(html); 596 | } 597 | else if (thistype != false) { 598 | // this is a specific class 599 | 600 | // find the obj of the class 601 | var data = {"classes": []}; 602 | 603 | for (var classv in thiscourse["obj"]["classes"]) { 604 | var thisclass = thiscourse["obj"]["classes"][classv]; 605 | if (thisclass["id"] == thistype) { 606 | // we found the obj for this class 607 | data["classes"].push(thisclass); 608 | break; 609 | } 610 | } 611 | 612 | if (data["classes"].length > 0) { 613 | // generate the table 614 | var html = window.classList.generateClasses(data, element, false, false); 615 | 616 | // add the remove button 617 | var removebtn = $(''); 618 | 619 | // bind class removing button 620 | removebtn.find("button").click(function (event) { 621 | event.stopPropagation(); 622 | var type = $(this).attr("type"); 623 | var coursecode = $(this).attr("code"); 624 | 625 | // set to generic class 626 | self.courses[self.activeGroup]["courses"][coursecode]["types"][type] = true; 627 | 628 | // update the class list 629 | window.classList.updateRemovedClass($(this).attr("myclassid")); 630 | 631 | // update UI 632 | self.updateAccordion(coursecode); 633 | 634 | // update the generation 635 | self.startGeneration(); 636 | }) 637 | 638 | html.find("tr:first").append(removebtn); 639 | //
    ×
    640 | 641 | // edit the css 642 | html.css("padding-left", "50px"); 643 | html.css("padding-right", "15px"); 644 | } 645 | } 646 | } 647 | 648 | element.slideDown(); 649 | }); 650 | } 651 | 652 | /* 653 | Initiates schedule generation given the current chosen classes 654 | */ 655 | startGeneration() { 656 | // we want to terminate the previous generator if its still running 657 | if (this.generator != false) this.generator.stop(); 658 | 659 | // generate the schedules 660 | this.generator = new Generator(this.courses); 661 | 662 | // save the current state to localStorage 663 | this.saveState(); 664 | } 665 | 666 | /* 667 | Generates the course HTML 668 | */ 669 | generateCourseHTML(course, path) { 670 | var subject = path.split("\\"); 671 | var coursenum = subject[subject.length-1] 672 | var subject = subject[subject.length-2]; 673 | 674 | var title = subject + " " + coursenum; 675 | 676 | if (course["description"] != undefined && course["description"]["name"] != undefined) { 677 | title += " - " + course["description"]["name"]; 678 | } 679 | 680 | return this.generateAccordionHTML(title, subject + " " + coursenum); 681 | } 682 | 683 | /* 684 | Generates the course remove button HTML 685 | */ 686 | generateRemoveButton() { 687 | return ''; 688 | } 689 | 690 | /* 691 | Generates the general accordian structure HTML given a value 692 | */ 693 | generateAccordionHTML(value, path) { 694 | return '
    • '; 695 | } 696 | } -------------------------------------------------------------------------------- /js/core/Calendar.js: -------------------------------------------------------------------------------- 1 | class Calendar { 2 | // Handles the UI construction of the calendar 3 | constructor() { 4 | var self = this; 5 | 6 | this.weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]; 7 | 8 | this.resetCalendar(); 9 | 10 | this.removeTimes = false; 11 | 12 | this.isLoading = false; 13 | 14 | this.currentSchedule = []; 15 | 16 | this.bindNextPrev(); 17 | this.initializeTooltips(); 18 | 19 | // Bind left side buttons 20 | this.bindSchedulePhotoDL(); 21 | this.bindRemoveBlockedTimes(); 22 | this.bindCopyScheduleToClipboard(); 23 | this.bindFacebookSharing(); 24 | this.bindImgurUpload(); 25 | 26 | // Key binds 27 | this.keyBinds(); 28 | 29 | // Bind resize event 30 | this.bindResize(); 31 | 32 | this.eventcolours = { 33 | "#FF5E3A": false, 34 | "#099e12": false, 35 | "#1D62F0": false, 36 | "#FF2D55": false, 37 | "#8E8E93": false, 38 | "#0b498c": false, 39 | "#34AADC": false, 40 | "#5AD427": false 41 | } 42 | 43 | // We want to bind the mouse up handler for blocking times 44 | $(document).mouseup(function () { 45 | self.mouseDown = false; 46 | 47 | // Change each deep array to strings for comparison 48 | var blockedTimesString = JSON.stringify(self.blockedTimes); 49 | var prevBlockedTimesString = JSON.stringify(self.prevBlockedTimes); 50 | 51 | // Check if the blocked times changed, if so, restart generation 52 | if (blockedTimesString != prevBlockedTimesString) { 53 | window.mycourses.startGeneration(); 54 | } 55 | 56 | // Reset prev 57 | self.prevBlockedTimes = self.blockedTimes; 58 | }); 59 | } 60 | 61 | /* 62 | Initializes the tooltips associated with buttons on the calendar 63 | */ 64 | initializeTooltips() { 65 | // Initialize prev/next sched tooltips 66 | $("#prevSchedule").tooltip(); 67 | $("#nextSchedule").tooltip(); 68 | 69 | // Initialize left side button tooltips 70 | $("#dlSchedulePhoto").tooltip(); 71 | $("#removeBlockedTimes").tooltip(); 72 | $("#copySchedToClipboard").tooltip(); 73 | $("#shareToFacebook").tooltip(); 74 | $("#uploadToImgur").tooltip(); 75 | } 76 | 77 | /* 78 | Binds an event handler to redraw the current schedule when the window is resized (since the event sizes will change) 79 | 80 | Waits for 500ms since the latest resize event 81 | */ 82 | bindResize() { 83 | var self = this; 84 | var resizeTimer; 85 | 86 | $(window).resize(function () { 87 | clearTimeout(resizeTimer); 88 | resizeTimer = setTimeout(function () {self.redrawSchedule()}, 500); 89 | }); 90 | } 91 | 92 | /* 93 | Binds the Schedule Photo Download button and implements the DL functionality 94 | */ 95 | bindSchedulePhotoDL() { 96 | var self = this; 97 | 98 | // on click 99 | $("#dlSchedulePhoto").click(function () { 100 | // Take the screenshot 101 | self.takeCalendarHighResScreenshot(1.6, 2, function (canvas) { 102 | // Download the picture 103 | var a = document.createElement('a'); 104 | a.href = canvas.replace("image/png", "image/octet-stream"); 105 | 106 | // Set the name of the file 107 | if (window.uni != null && window.term != null) a.download = window.uni + '_' + window.term + '_ScheduleStorm.png'; 108 | else a.download = 'ScheduleStorm_Schedule.png'; 109 | 110 | // Append it to the body 111 | document.body.appendChild(a); 112 | 113 | a.click(); 114 | 115 | // Remove it from the body 116 | document.body.removeChild(a); 117 | }); 118 | }); 119 | } 120 | 121 | /* 122 | Binds the imgur button to upload a photo of the schedule to imgur and open it 123 | */ 124 | bindImgurUpload() { 125 | var self = this; 126 | 127 | $("#uploadToImgur").click(function () { 128 | /* 129 | Why do we make a separate window/tab now? 130 | 131 | If we simply open up a new window/tab after we already have the photo uploaded 132 | and the imgur link, we lose the "trusted" event that came from a user click. 133 | As a result, the window/tab would be blocked as a popup. If we create the window 134 | now while we have a trusted event and then change its location when we're ready, 135 | we can bypass this. 136 | */ 137 | var imgurwindow = window.open("http://schedulestorm.com/assets/imgurloading.png",'Uploading to Imgur...', "width=900,height=500"); 138 | 139 | // Upload the image to imgur and get the link 140 | self.uploadToImgur(1.6, function (link) { 141 | if (link != false) { 142 | imgurwindow.location.href = link + ".png"; 143 | } 144 | else { 145 | // There was an error, show the error screen 146 | imgurwindow.location.href = "http://schedulestorm.com/assets/imgurerror.png" 147 | } 148 | }); 149 | }) 150 | } 151 | 152 | /* 153 | Uploads the current schedule to imgur and returns the URL if successful 154 | If not, returns false 155 | */ 156 | uploadToImgur(ratio, cb) { 157 | var self = this; 158 | 159 | // Takes a screenshot of the calendar 160 | self.takeCalendarHighResScreenshot(ratio, 2, function (canvas) { 161 | // Send AJAX request to imgur with the photo to upload 162 | $.ajax({ 163 | url: 'https://api.imgur.com/3/image', 164 | type: 'POST', 165 | headers: { 166 | Authorization: 'Client-ID 9bdb3669a12eeb2' 167 | }, 168 | data: { 169 | type: 'base64', 170 | name: 'schedulestorm.png', 171 | title: 'Schedule Storm', 172 | description: "Made using ScheduleStorm.com for " + 173 | window.unis[window.uni]["name"] + " - " + 174 | window.unis[window.uni]["terms"][window.term], 175 | image: canvas.split(',')[1] 176 | }, 177 | dataType: 'json' 178 | }).success(function(data) { 179 | cb(data.data.link); 180 | }).error(function() { 181 | cb(false); 182 | }); 183 | 184 | }); 185 | } 186 | 187 | /* 188 | Binds the Facebook share button to actually share on click 189 | */ 190 | bindFacebookSharing() { 191 | var self = this; 192 | 193 | $("#shareToFacebook").click(function () { 194 | // We have to preserve this "trusted" event and thus have to make the window now 195 | var facebookwindow = window.open("http://schedulestorm.com/assets/facebookshare.png",'Sharing to Facebook...', "width=575,height=592"); 196 | 197 | self.uploadToImgur(1.91, function (link) { 198 | // Set the default image if no image 199 | if (link == false) { 200 | link = "https://camo.githubusercontent.com/ac09e7e7a60799733396a0f4d496d7be8116c542/687474703a2f2f692e696d6775722e636f6d2f5a425258656d342e706e67"; 201 | } 202 | 203 | var url = self.generateFacebookFeedURL(link); 204 | facebookwindow.location.href = url; 205 | }); 206 | }); 207 | } 208 | 209 | /* 210 | Generates the URL to use to share this schedule to Facebook 211 | */ 212 | generateFacebookFeedURL(picture) { 213 | 214 | var url = "https://www.facebook.com/v2.8/dialog/feed"; 215 | var parameters = { 216 | "app_id": "138997789901870", 217 | "caption": "University Student Schedule Generator", 218 | "display": "popup", 219 | "e2e": "{}", 220 | "link": "http://schedulestorm.com", 221 | "locale": "en_US", 222 | "name": "Schedule Storm", 223 | "domain": "schedulestorm.com", 224 | "relation": "opener", 225 | "result": '"xxRESULTTOKENxx"', 226 | "sdk": "joey", 227 | "version": "v2.8" 228 | } 229 | var index = 0; 230 | 231 | for (var parameter in parameters) { 232 | if (index > 0) url += "&"; 233 | else url += "?"; 234 | 235 | url += parameter + "=" + encodeURIComponent(parameters[parameter]); 236 | index += 1; 237 | } 238 | 239 | url += "&description=" + encodeURIComponent(this.generateFacebookDescription(this.currentSchedule)); 240 | url += "&picture=" + encodeURIComponent(picture); 241 | 242 | return url; 243 | } 244 | 245 | /* 246 | Generates the Facebook description text given a schedule 247 | */ 248 | generateFacebookDescription(schedule) { 249 | var returnText = window.unis[window.uni]["name"] + " - " + 250 | window.unis[window.uni]["terms"][window.term]; 251 | 252 | // Make sure we actully have a possible schedule 253 | if (schedule.length > 0) { 254 | returnText += " --- Classes: "; 255 | 256 | var coursesdict = {}; 257 | 258 | // Iterate through each class and populate the return Text 259 | for (var classv in schedule) { 260 | var thisclass = schedule[classv]; 261 | if (typeof thisclass == "object") { 262 | 263 | if (coursesdict[thisclass["name"]] == undefined) { 264 | coursesdict[thisclass["name"]] = []; 265 | } 266 | 267 | coursesdict[thisclass["name"]].push(thisclass["id"]); 268 | } 269 | } 270 | 271 | // Iterate through the dict keys and add the values to the returnText 272 | var keylength = Object.keys(coursesdict).length; 273 | var index = 0; 274 | for (var key in coursesdict) { 275 | index += 1; 276 | returnText += key + " (" + coursesdict[key] + ")"; 277 | 278 | if (index < keylength) { 279 | returnText += ", "; 280 | } 281 | } 282 | } 283 | 284 | return returnText; 285 | } 286 | 287 | /* 288 | Takes a high-res screenshot of the calendar with the specified aspect ratio and downloads it as a png to the system 289 | 290 | Thanks to: https://github.com/niklasvh/html2canvas/issues/241#issuecomment-247705673 291 | */ 292 | takeCalendarHighResScreenshot(aspectratio, scaleFactor, cb) { 293 | var self = this; 294 | 295 | var srcEl = document.getElementById("maincalendar"); 296 | 297 | var wrapdiv = $(srcEl).find('.wrap'); 298 | 299 | var beforeHeight = wrapdiv.height(); 300 | 301 | // Want to remove any scrollbars 302 | wrapdiv.removeClass('wrap'); 303 | 304 | // If removing the size caused the rows to be smaller, add the class again 305 | if (beforeHeight > wrapdiv.height()) { 306 | wrapdiv.addClass('wrap'); 307 | } 308 | 309 | // Save original size of element 310 | var originalWidth = srcEl.offsetWidth; 311 | var originalHeight = wrapdiv.height() + $(srcEl).find("table").eq(0).height(); 312 | 313 | // see if we can scale the width for it to look right for the aspect ratio 314 | if ((originalHeight * aspectratio) <= $(window).width()) { 315 | originalWidth = originalHeight * aspectratio; 316 | } 317 | 318 | // Force px size (no %, EMs, etc) 319 | srcEl.style.width = originalWidth + "px"; 320 | srcEl.style.height = originalHeight + "px"; 321 | 322 | // Position the element at the top left of the document because of bugs in html2canvas. 323 | // See html2canvas issues #790, #820, #893, #922 324 | srcEl.style.position = "fixed"; 325 | srcEl.style.top = "0"; 326 | srcEl.style.left = "0"; 327 | 328 | // Create scaled canvas 329 | var scaledCanvas = document.createElement("canvas"); 330 | scaledCanvas.width = originalWidth * scaleFactor; 331 | scaledCanvas.height = originalHeight * scaleFactor; 332 | scaledCanvas.style.width = originalWidth + "px"; 333 | scaledCanvas.style.height = originalHeight + "px"; 334 | var scaledContext = scaledCanvas.getContext("2d"); 335 | scaledContext.scale(scaleFactor, scaleFactor); 336 | 337 | // Force the schedule to be redrawn 338 | this.redrawSchedule(); 339 | 340 | html2canvas(srcEl, { canvas: scaledCanvas }) 341 | .then(function(canvas) { 342 | 343 | // Reset the styling of the source element 344 | srcEl.style.position = ""; 345 | srcEl.style.top = ""; 346 | srcEl.style.left = ""; 347 | srcEl.style.width = ""; 348 | srcEl.style.height = ""; 349 | 350 | wrapdiv.addClass('wrap'); 351 | 352 | self.redrawSchedule(); 353 | 354 | // return the data 355 | cb(canvas.toDataURL("image/png")); 356 | }); 357 | }; 358 | 359 | /* 360 | Binds button that allows you to remove all blocked times 361 | */ 362 | bindRemoveBlockedTimes() { 363 | var self = this; 364 | 365 | $("#removeBlockedTimes").click(function () { 366 | // Make sure there are actually blocked times before regenning 367 | if (JSON.stringify(self.blockedTimes) != "[]") { 368 | self.blockedTimes = []; 369 | self.prevBlockedTimes = []; 370 | 371 | // Visually remove all of the blocked times 372 | self.removeAllBlockedTimeUI(); 373 | 374 | window.mycourses.startGeneration(); 375 | } 376 | }) 377 | } 378 | 379 | /* 380 | Binds the copy schedule to clipboard button 381 | */ 382 | bindCopyScheduleToClipboard() { 383 | var self = this; 384 | 385 | self.copyschedclipboard = new Clipboard('#copySchedToClipboard', { 386 | text: function(trigger) { 387 | return self.generateScheduleText(self.currentSchedule); 388 | } 389 | }); 390 | } 391 | 392 | /* 393 | Visually removes all blocked times from the Schedule UI 394 | */ 395 | removeAllBlockedTimeUI() { 396 | $(".calendar").find(".blockedTime").toggleClass("blockedTime"); 397 | } 398 | 399 | /* 400 | Starts loading animation 401 | */ 402 | startLoading(message) { 403 | this.clearEvents(); 404 | 405 | // If it is already loading, don't add another loading sign 406 | if (this.isLoading == false) { 407 | this.loading = new Loading($("#schedule").find(".wrap:first"), message, "position: absolute; top: 20%; left: 40%;"); 408 | this.isLoading = true; 409 | } 410 | } 411 | 412 | /* 413 | If there is a loading animation, stops it 414 | */ 415 | doneLoading(cb) { 416 | var self = this; 417 | self.loadingcb = cb; 418 | 419 | if (self.isLoading) { 420 | self.loading.remove(function () { 421 | self.isLoading = false; 422 | self.loadingcb(); 423 | }); 424 | } 425 | else { 426 | self.isLoading = false; 427 | cb(); 428 | } 429 | } 430 | 431 | /* 432 | Sets loading status of the animation 433 | */ 434 | setLoadingStatus(message) { 435 | this.loading.setStatus(message); 436 | } 437 | 438 | /* 439 | Empties out the calendar 440 | */ 441 | emptyCalendar() { 442 | $("#schedule").find(".outer:first").empty(); 443 | } 444 | 445 | /* 446 | Sets the calendar status to the defined text 447 | */ 448 | setCalendarStatus(text) { 449 | $("#schedule").find("#calendarStatus").text(text); 450 | } 451 | 452 | /* 453 | Resets the calendar status to an empty string 454 | */ 455 | resetCalendarStatus() { 456 | this.setCalendarStatus(""); 457 | } 458 | 459 | /* 460 | Displays the given schedule 461 | */ 462 | displaySchedule(schedule) { 463 | var self = this; 464 | 465 | // set the score 466 | // make sure its a number 467 | if (typeof schedule[0] == "number") $("#scheduleScore").text(schedule[0].toFixed(2)); 468 | 469 | // Destroy all the tooltips from previous events 470 | self.destroyEventTooltips(); 471 | 472 | // Clear all the current events on the calendar 473 | self.clearEvents(); 474 | 475 | console.log("This schedule"); 476 | console.log(schedule); 477 | 478 | self.currentSchedule = schedule; 479 | 480 | self.setScheduleConstraints(schedule); 481 | 482 | 483 | for (var classv in schedule) { 484 | var thisclass = schedule[classv]; 485 | 486 | var text = thisclass["name"] + " - " + thisclass["type"] + " - " + thisclass["id"]; 487 | 488 | // for every time 489 | for (var time in thisclass["times"]) { 490 | var thistime = thisclass["times"][time]; 491 | 492 | // make sure there isn't a -1 in the days 493 | if (thistime[0].indexOf(-1) == -1) { 494 | this.addEvent(Generator.totalMinutesToTime(thistime[1][0]), Generator.totalMinutesToTime(thistime[1][1]), thistime[0], text, thisclass); 495 | } 496 | } 497 | } 498 | 499 | // reset the colour ids 500 | self.resetColours(); 501 | } 502 | 503 | /* 504 | Redraws the current schedule 505 | */ 506 | redrawSchedule() { 507 | if (this.currentSchedule.length > 0) { 508 | this.displaySchedule(this.currentSchedule); 509 | } 510 | } 511 | 512 | /* 513 | Destroys every currently displayed event tooltip 514 | */ 515 | destroyEventTooltips() { 516 | // Destroy the tooltips 517 | $("#schedule").find('.event').each(function (index) { 518 | $(this).tooltip('destroy'); 519 | }); 520 | 521 | // Remove any open tooltip div 522 | $('[role=tooltip]').each(function (index) { 523 | $(this).remove(); 524 | }) 525 | } 526 | 527 | /* 528 | Returns copy-paste schedule text 529 | */ 530 | generateScheduleText(schedule) { 531 | var returnText = "Generated by ScheduleStorm.com for " + 532 | window.unis[window.uni]["name"] + " " + 533 | window.unis[window.uni]["terms"][window.term] + "\n\n"; 534 | 535 | var allowedAttributes = ["id", "name", "type", "rooms", "teachers", "times", "section"]; 536 | 537 | if (schedule.length > 0) { 538 | // Iterate through each class and populate the return Text 539 | for (var classv in schedule) { 540 | var thisclass = schedule[classv]; 541 | 542 | var thisrow = ""; 543 | 544 | // Make sure this is a class object 545 | if (typeof thisclass != "number") { 546 | 547 | // Fill up the row with the correct formatting and order of attributes 548 | if (thisclass["id"] != undefined) thisrow += thisclass["id"] + " | "; 549 | 550 | if (thisclass["name"] != undefined) thisrow += thisclass["name"] + " | "; 551 | 552 | if (thisclass["section"] != undefined) { 553 | thisrow += thisclass["type"] + "-" + thisclass["section"] + " (" + thisclass["id"] + ")" + " | "; 554 | } 555 | else if (thisclass["group"] != undefined) { 556 | thisrow += thisclass["type"] + "-" + thisclass["group"] + " (" + thisclass["id"] + ")" + " | "; 557 | } 558 | 559 | thisrow += thisclass["teachers"] + " | "; 560 | thisrow += thisclass["rooms"] + " | "; 561 | thisrow += thisclass["oldtimes"] + " | " 562 | thisrow += thisclass["status"]; 563 | } 564 | 565 | // Add the row if it was actually populated 566 | if (thisrow != "") returnText += thisrow + "\n"; 567 | } 568 | } 569 | else { 570 | returnText += "There were no possible schedules generated :("; 571 | } 572 | 573 | return returnText; 574 | } 575 | 576 | /* 577 | Resets the allocation of colours to each class 578 | */ 579 | resetColours() { 580 | for (var colour in this.eventcolours) { 581 | this.eventcolours[colour] = false; 582 | } 583 | } 584 | 585 | /* 586 | Given a classname, returns the div bg colour 587 | */ 588 | getEventColour(classname) { 589 | // check if we already have a colour for this class 590 | for (var colour in this.eventcolours) { 591 | if (this.eventcolours[colour] == classname) { 592 | return colour; 593 | } 594 | } 595 | 596 | // add a new colour for this class 597 | for (var colour in this.eventcolours) { 598 | if (this.eventcolours[colour] == false) { 599 | this.eventcolours[colour] = classname; 600 | return colour; 601 | } 602 | } 603 | 604 | // there are no colours left, return a default colour 605 | return "#0275d8"; 606 | 607 | } 608 | 609 | /* 610 | Sets the time constraints of the calendar given a schedule 611 | */ 612 | setScheduleConstraints(schedule) { 613 | var maxDay = 4; // we always want to show Mon-Fri unless there are Sat or Sun classes 614 | var minDay = 0; 615 | var minHour = 24; 616 | var maxHour = 0; 617 | 618 | for (var classv in schedule) { 619 | var thisclass = schedule[classv]; 620 | 621 | // for every time 622 | for (var time in thisclass["times"]) { 623 | var thistime = thisclass["times"][time]; 624 | 625 | // make sure there isn't a -1 in the days 626 | if (thistime[0].indexOf(-1) == -1) { 627 | // check whether the date changes constraints 628 | var thisMaxDay = Math.max.apply(null, thistime[0]); 629 | 630 | if (thisMaxDay > maxDay) { 631 | maxDay = thisMaxDay; 632 | } 633 | 634 | // check whether these times change the constraints 635 | var startTime = Generator.totalMinutesToTime(thistime[1][0]); 636 | var startHour = parseInt(startTime.split(":")[0]) 637 | 638 | if (startHour < minHour) { 639 | minHour = startHour; 640 | } 641 | 642 | var endTime = Generator.totalMinutesToTime(thistime[1][1]); 643 | var endHour = parseInt(endTime.split(":")[0]) + 1; 644 | 645 | if (endHour > maxHour) { 646 | maxHour = endHour; 647 | } 648 | } 649 | } 650 | } 651 | 652 | if (maxDay == 4 && minDay == 0 && minHour == 24 && maxHour == 0) { 653 | // Just set a default scale 654 | this.resizeCalendarNoScroll(0, 4, 9, 17); 655 | } 656 | else { 657 | this.resizeCalendarNoScroll(minDay, maxDay, minHour, maxHour); 658 | } 659 | } 660 | 661 | /* 662 | Sets the current generated index 663 | */ 664 | setCurrentIndex(index) { 665 | var self = this; 666 | 667 | if (index > (self.totalGenerated-1)) { 668 | // go down to the start at 0 669 | index = 0; 670 | } 671 | if (index < 0) { 672 | // go to the max index 673 | index = self.totalGenerated-1; 674 | } 675 | 676 | self.curIndex = index; 677 | 678 | // show it on the UI 679 | self.updateIndexUI(self.curIndex+1); 680 | } 681 | 682 | /* 683 | Updates the UI with the passed in current schedule index 684 | */ 685 | updateIndexUI(index) { 686 | $("#curGenIndex").text(index); 687 | } 688 | 689 | /* 690 | Updates the UI with the passed in total generated schedules 691 | */ 692 | updateTotalUI(total) { 693 | $("#totalGen").text(total); 694 | } 695 | 696 | /* 697 | Sets the total amount of generated schedules for the UI 698 | */ 699 | setTotalGenerated(total) { 700 | var self = this; 701 | 702 | self.totalGenerated = total; 703 | 704 | self.updateTotalUI(self.totalGenerated); 705 | } 706 | 707 | /* 708 | Goes to the previous schedule 709 | */ 710 | goToPrev() { 711 | var self = this; 712 | 713 | if (self.totalGenerated > 0) { 714 | self.setCurrentIndex(self.curIndex-1); 715 | 716 | // get the schedule 717 | var newschedules = window.mycourses.generator.getSchedule(self.curIndex); 718 | 719 | if (newschedules != false) { 720 | // we got the schedule, now populate it 721 | self.displaySchedule(newschedules); 722 | } 723 | } 724 | } 725 | 726 | /* 727 | Goes to the next schedule 728 | */ 729 | goToNext() { 730 | var self = this; 731 | 732 | if (self.totalGenerated > 0) { 733 | self.setCurrentIndex(self.curIndex+1); 734 | 735 | // get the schedule 736 | var newschedules = window.mycourses.generator.getSchedule(self.curIndex); 737 | 738 | if (newschedules != false) { 739 | // we got the schedule, now populate it 740 | self.displaySchedule(newschedules); 741 | } 742 | } 743 | } 744 | 745 | /* 746 | Binds the buttons that let you go through each generated schedule 747 | */ 748 | bindNextPrev() { 749 | var self = this; 750 | // unbind any current binds 751 | $("#prevSchedule").unbind(); 752 | $("#nextSchedule").unbind(); 753 | 754 | $("#prevSchedule").click(function () { 755 | self.goToPrev(); 756 | }); 757 | 758 | $("#nextSchedule").click(function () { 759 | self.goToNext(); 760 | }); 761 | } 762 | 763 | /* 764 | Binds the arrow keys and Ctrl+C 765 | */ 766 | keyBinds() { 767 | var self = this; 768 | 769 | // Bind arrow keys 770 | $(document).on('keydown', function (e){ 771 | var tag = e.target.tagName.toLowerCase(); 772 | 773 | // We don't want to do anything if they have an input focused 774 | if (tag != "input" && !window.tourInProgress) { 775 | if (e.keyCode == 37) self.goToPrev(); 776 | else if (e.keyCode == 39) self.goToNext(); 777 | else if (e.keyCode == 67 && (e.metaKey || e.ctrlKey)) $("#copySchedToClipboard").click(); 778 | } 779 | }); 780 | } 781 | 782 | /* 783 | Visually clears all of the events on the calendar 784 | */ 785 | clearEvents() { 786 | $("#schedule").find(".event").each(function() { 787 | $(this).remove(); 788 | }); 789 | } 790 | 791 | /* 792 | Generates the HTML for a calendar event tooltip given a class object 793 | */ 794 | generateTooltip(classobj) { 795 | // Return html string 796 | var htmlString = ""; 797 | 798 | // Define the attributes and their names to add 799 | var allowedAttributes = [ 800 | { 801 | "id": "id", 802 | "name": "Class ID" 803 | }, 804 | { 805 | "id": "teachers", 806 | "name": "Teachers" 807 | }, 808 | { 809 | "id": "oldtimes", 810 | "name": "Times" 811 | }, 812 | { 813 | "id": "rooms", 814 | "name": "Rooms" 815 | }, 816 | { 817 | "id": "location", 818 | "name": "Location" 819 | }, 820 | { 821 | "id": "scheduletype", 822 | "name": "Type" 823 | }, 824 | { 825 | "id": "status", 826 | "name": "Status" 827 | } 828 | ]; 829 | 830 | // Iterate through every attribute 831 | for (var attribute in allowedAttributes) { 832 | attribute = allowedAttributes[attribute]; 833 | 834 | // Make sure its id is defined in the class 835 | if (classobj[attribute["id"]] != undefined) { 836 | if (typeof classobj[attribute["id"]] != "object") { 837 | htmlString += "" + attribute["name"] + ": " + classobj[attribute["id"]] + "
      "; 838 | } 839 | else { 840 | // Prevent dupes 841 | var alreadyAdded = []; 842 | 843 | // Iterate through the elements and add them 844 | htmlString += "" + attribute["name"] + ":
      "; 845 | for (var index in classobj[attribute["id"]]) { 846 | 847 | // Check if we've already added this element 848 | if (alreadyAdded.indexOf(classobj[attribute["id"]][index]) == -1) { 849 | // we haven't already added this element 850 | 851 | if (attribute["id"] == "teachers") { 852 | var thisteacher = classobj[attribute["id"]][index]; 853 | 854 | htmlString += thisteacher; 855 | 856 | // If this teacher has an RMP score, add it 857 | if (classList.rmpdata[thisteacher] != undefined && classList.rmpdata[thisteacher]["rating"] != undefined) { 858 | htmlString += " (" + classList.rmpdata[thisteacher]["rating"] + ")"; 859 | } 860 | 861 | htmlString += "
      "; 862 | } 863 | else { 864 | // Just add the element 865 | htmlString += classobj[attribute["id"]][index] + "
      "; 866 | } 867 | 868 | // push it to added elements 869 | alreadyAdded.push(classobj[attribute["id"]][index]); 870 | } 871 | } 872 | } 873 | } 874 | } 875 | 876 | return htmlString; 877 | } 878 | 879 | /* 880 | Add an event with start and end time (24 hours) 881 | 882 | Days is an array containing the integers that represent the days that this event is on 883 | */ 884 | addEvent(starttime, endtime, days, text, classobj) { 885 | 886 | var rowheight = $("#schedule").find("td:first").height() + 1; 887 | 888 | var starthour = parseInt(starttime.split(":")[0]); 889 | var startmin = parseInt(starttime.split(":")[1]); 890 | 891 | var endhour = parseInt(endtime.split(":")[0]); 892 | var endmin = parseInt(endtime.split(":")[1]); 893 | 894 | // round down to closest 30min or hour 895 | var roundedstartmin = Math.floor(startmin/30) * 30; 896 | 897 | // figure out how many minutes are in between the two times 898 | var totalstartmin = starthour*60 + startmin; 899 | var totalendmin = endhour*60 + endmin; 900 | 901 | var totalmin = totalendmin - totalstartmin; 902 | 903 | // Calculate the height of the box 904 | var totalheight = 0; 905 | 906 | // Every 30min is rowheight 907 | totalheight += (totalmin/30)*rowheight; 908 | 909 | // calculate how far from the top the element is 910 | var topoffset = ((startmin % 30)/30) * rowheight; 911 | 912 | // draw the events 913 | for (var day in days) { 914 | day = days[day]; 915 | 916 | // find the parent 917 | var tdelement = $("#schedule").find("#" + starthour + "-" + roundedstartmin); 918 | tdelement = tdelement.find("td:eq(" + (day+1) + ")"); 919 | 920 | // empty it 921 | tdelement.empty(); 922 | 923 | // create the element and append it 924 | var html = '
      '; 927 | 928 | html += text; 929 | 930 | html += '
      '; 931 | 932 | // Initialize the tooltip 933 | html = $(html).tooltip({container: 'body', html: true}); 934 | 935 | tdelement.append(html); 936 | } 937 | } 938 | 939 | /* 940 | Resizes the calendar to the specified constraints 941 | */ 942 | resizeCalendarNoScroll(startDay, endDay, startHour, endHour) { 943 | 944 | // If the difference between the start and end hours is less than 6, extend the end hour 945 | // This is to make sure the appearance of the calendar doesn't look weird and 946 | // that every row is 20px high 947 | 948 | var self = this; 949 | 950 | if ((endHour - startHour) < 6) { 951 | endHour += 6 - (endHour - startHour); 952 | } 953 | 954 | if (endHour > 24) { 955 | endHour = 24; 956 | } 957 | 958 | var windowheight = $(window).height(); 959 | var calendarheight = windowheight * 0.49; 960 | 961 | 962 | this.emptyCalendar(); 963 | 964 | // all parameters are inclusive 965 | 966 | // build header 967 | var header = ''; 968 | 969 | for (var x = startDay; x <= endDay; x++) { 970 | header += ""; 971 | } 972 | 973 | header += '
      " + this.weekdays[x] + "
      '; 974 | 975 | // append the header 976 | $("#schedule").find(".outer:first").append(header); 977 | 978 | 979 | var table = '
      '; 980 | 981 | // we start 30 min earlier than the specified start hour 982 | var min = 30; 983 | var hour = startHour-1; // 24 hour 984 | 985 | while (hour < endHour) { 986 | 987 | if (min >= 60) { 988 | min = 0; 989 | hour += 1; 990 | } 991 | 992 | // find 12 hour equivalent 993 | var hours12 = ((hour + 11) % 12 + 1); 994 | 995 | var hourtext = ""; 996 | if (min == 0) { 997 | // we want to ensure 2 0's 998 | hourtext += hours12 + ":00"; 999 | } 1000 | 1001 | // generate the text 1002 | table += ""; 1003 | 1004 | var iteratelength = endDay - startDay + 1; 1005 | 1006 | for (var x = 0; x < iteratelength; x++) { 1007 | table += ""; 1017 | } 1018 | 1019 | table += ""; 1020 | 1021 | min += 30; 1022 | } 1023 | 1024 | table += '
      " + hourtext + " -1) { 1012 | table += ' class="blockedTime"'; 1013 | } 1014 | } 1015 | 1016 | table += ">
      '; 1025 | 1026 | table = $(table); 1027 | 1028 | // bind the blocked times mouse events 1029 | table.find("td:not(.headcol)").mousedown(function () { 1030 | // If the first block you mouse down on causes a certain event, 1031 | // you can only cause that event when hovering over other blocks 1032 | 1033 | // Ex. If you start of removing a time block, you can only remove 1034 | // other timeblocks when you hover 1035 | 1036 | // Preserve the old copy of the blocked times for the mouseUp document event 1037 | self.prevBlockedTimes = jQuery.extend(true, [], self.blockedTimes); 1038 | 1039 | self.mouseDown = true; 1040 | 1041 | // check the event we're making 1042 | var thisday = parseInt($(this).attr("day")); 1043 | var thistime = $(this).parent().attr("id"); 1044 | 1045 | // we want to populate the index if it's undefined 1046 | if (self.blockedTimes[thisday] == undefined) { 1047 | self.blockedTimes[thisday] = []; 1048 | } 1049 | 1050 | // check whether we've already blocked this timeslot 1051 | if (self.blockedTimes[thisday].indexOf(thistime) > -1) { 1052 | // we want to remove it 1053 | self.removeTimes = true; 1054 | var thisindex = self.blockedTimes[thisday].indexOf(thistime); 1055 | 1056 | // modify the array 1057 | self.blockedTimes[thisday].splice(thisindex, 1); 1058 | } 1059 | else { 1060 | // we want to add blocked times 1061 | self.removeTimes = false; 1062 | self.blockedTimes[thisday].push(thistime); 1063 | } 1064 | 1065 | // Toggle the visual class 1066 | $(this).toggleClass("blockedTime"); 1067 | 1068 | }).mouseover(function () { 1069 | if (self.mouseDown) { 1070 | // get the data for this time block 1071 | var thisday = parseInt($(this).attr("day")); 1072 | var thistime = $(this).parent().attr("id"); 1073 | 1074 | if (self.blockedTimes[thisday] == undefined) { 1075 | self.blockedTimes[thisday] = []; 1076 | } 1077 | 1078 | if (self.removeTimes == true && self.blockedTimes[thisday].indexOf(thistime) > -1) { 1079 | // we want to remove this timeblock 1080 | var thisindex = self.blockedTimes[thisday].indexOf(thistime); 1081 | self.blockedTimes[thisday].splice(thisindex, 1); 1082 | 1083 | // toggle the class 1084 | $(this).toggleClass("blockedTime"); 1085 | } 1086 | else if (self.removeTimes == false && self.blockedTimes[thisday].indexOf(thistime) == -1) { 1087 | // we want to add blocked times 1088 | self.blockedTimes[thisday].push(thistime); 1089 | 1090 | // toggle the class 1091 | $(this).toggleClass("blockedTime"); 1092 | } 1093 | } 1094 | }); 1095 | 1096 | // append the table 1097 | $("#schedule").find(".outer:first").append(table); 1098 | } 1099 | 1100 | /* 1101 | If there are blocked times, fits the schedule to display them all 1102 | */ 1103 | displayBlockedTimes() { 1104 | var maxDay = -1; 1105 | var minDay = 7; 1106 | 1107 | var minTime = 1440; 1108 | var maxTime = 0; 1109 | 1110 | // Iterate through the blocked times 1111 | for (var day in this.blockedTimes) { 1112 | var thisDay = this.blockedTimes[day]; 1113 | 1114 | if (thisDay != undefined && thisDay.length > 0) { 1115 | // Check if it sets a new day range 1116 | if (day < minDay) minDay = day; 1117 | if (day > maxDay) maxDay = day; 1118 | 1119 | // Iterate times 1120 | for (var time in thisDay) { 1121 | var thistime = thisDay[time]; 1122 | 1123 | var totalMin = parseInt(thistime.split("-")[0])*60 + parseInt(thistime.split("-")[1]); 1124 | 1125 | // Check if it sets a new time range 1126 | if (totalMin > maxTime) maxTime = totalMin; 1127 | if (totalMin < minTime) minTime = totalMin; 1128 | } 1129 | } 1130 | } 1131 | 1132 | // Make sure there are actually some blocked times 1133 | if (maxDay > -1 && minDay < 7 && minTime < 1440 && maxTime > 0) { 1134 | // Make sure its atleast monday to friday 1135 | if (minDay != 0) minDay = 0; 1136 | if (maxDay < 4) maxDay = 4; 1137 | 1138 | // Draw it 1139 | this.resizeCalendarNoScroll(minDay, maxDay, Math.floor(minTime/60), Math.floor(maxTime/60)+1); 1140 | } 1141 | } 1142 | 1143 | /* 1144 | Resets the calendar (removes timeblocks and current schedules) 1145 | */ 1146 | resetCalendar() { 1147 | this.blockedTimes = []; 1148 | this.prevBlockedTimes = []; 1149 | this.currentSchedule = []; 1150 | 1151 | this.setTotalGenerated(0); 1152 | this.setCurrentIndex(-1); 1153 | 1154 | this.resizeCalendarNoScroll(0, 4, 9, 17); 1155 | } 1156 | } 1157 | --------------------------------------------------------------------------------