├── manifest ├── chrome.json ├── firefox.json └── common.json ├── .gitignore ├── images ├── overview.png ├── icons │ ├── falcon-hd.png │ ├── falcon-16x16.png │ ├── falcon-32x32.png │ ├── falcon-48x48.png │ └── falcon-128x128.png └── banners │ ├── dark mode.png │ ├── Falcon editor.png │ ├── new resources.png │ ├── new announcements.png │ ├── custom course names.png │ ├── lightning fast pjax.png │ └── sort-and-search-tables.png ├── src ├── assets │ ├── scss │ │ ├── bootstrap │ │ │ ├── mixins.scss │ │ │ ├── shadows.scss │ │ │ ├── main.scss │ │ │ ├── mixins │ │ │ │ ├── _breakpoints.scss │ │ │ │ └── _badge.scss │ │ │ ├── main-dark.scss │ │ │ ├── card.scss │ │ │ ├── variables-dark.scss │ │ │ └── variables.scss │ │ ├── nightowl │ │ │ ├── custom.scss │ │ │ └── _falcon-dark.scss │ │ ├── alert.scss │ │ ├── vars-light.scss │ │ ├── falcon.scss │ │ ├── loading-animation.scss │ │ ├── _fonts.scss │ │ ├── auth.scss │ │ └── shared.scss │ ├── fonts │ │ ├── Feather.ttf │ │ ├── Feather.woff │ │ ├── dxicons.ttf │ │ ├── dxicons.woff │ │ ├── Inter-Black.ttf │ │ ├── Inter-Bold.ttf │ │ ├── Inter-Light.ttf │ │ ├── Inter-Thin.ttf │ │ ├── dxicons.woff2 │ │ ├── Inter-Medium.ttf │ │ ├── Inter-Regular.ttf │ │ ├── Inter-ExtraBold.ttf │ │ ├── Inter-ExtraLight.ttf │ │ ├── Inter-SemiBold.ttf │ │ ├── dxiconsmaterial.ttf │ │ ├── dxiconsmaterial.woff │ │ ├── dxiconsmaterial.woff2 │ │ └── feather.scss │ ├── css │ │ ├── dist │ │ │ ├── tablesorter-bs4.min.css │ │ │ └── dx-diagram.css │ │ └── owl │ │ │ └── lesson-builder-checklist.css │ └── js │ │ └── owl │ │ └── syllabus.js ├── services │ ├── redirect-handler.js │ ├── falcon-assignments.js │ ├── gradebook.js │ ├── storage.js │ ├── table-sorter.js │ ├── core.js │ ├── dark-mode.js │ ├── course-name-editor.js │ ├── falcon.js │ ├── announcement.js │ ├── falcon-editor.js │ └── file-manager.js ├── background.js ├── main.js ├── options.html ├── popup.js ├── popup.html └── ui │ ├── asset-injector.js │ └── ui-injector.js ├── package.json ├── webpack.mix.js ├── mix-manifest.json └── README.md /manifest/chrome.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .idea/** 3 | node_modules 4 | /dist 5 | 6 | -------------------------------------------------------------------------------- /images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/images/overview.png -------------------------------------------------------------------------------- /src/assets/scss/bootstrap/mixins.scss: -------------------------------------------------------------------------------- 1 | @import "mixins/breakpoints"; 2 | @import "mixins/badge"; 3 | -------------------------------------------------------------------------------- /images/icons/falcon-hd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/images/icons/falcon-hd.png -------------------------------------------------------------------------------- /images/banners/dark mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/images/banners/dark mode.png -------------------------------------------------------------------------------- /images/icons/falcon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/images/icons/falcon-16x16.png -------------------------------------------------------------------------------- /images/icons/falcon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/images/icons/falcon-32x32.png -------------------------------------------------------------------------------- /images/icons/falcon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/images/icons/falcon-48x48.png -------------------------------------------------------------------------------- /src/assets/fonts/Feather.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/src/assets/fonts/Feather.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Feather.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/src/assets/fonts/Feather.woff -------------------------------------------------------------------------------- /src/assets/fonts/dxicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/src/assets/fonts/dxicons.ttf -------------------------------------------------------------------------------- /src/assets/fonts/dxicons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/src/assets/fonts/dxicons.woff -------------------------------------------------------------------------------- /images/banners/Falcon editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/images/banners/Falcon editor.png -------------------------------------------------------------------------------- /images/banners/new resources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/images/banners/new resources.png -------------------------------------------------------------------------------- /images/icons/falcon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/images/icons/falcon-128x128.png -------------------------------------------------------------------------------- /src/assets/fonts/Inter-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/src/assets/fonts/Inter-Black.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/src/assets/fonts/Inter-Bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Inter-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/src/assets/fonts/Inter-Light.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Inter-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/src/assets/fonts/Inter-Thin.ttf -------------------------------------------------------------------------------- /src/assets/fonts/dxicons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/src/assets/fonts/dxicons.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Inter-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/src/assets/fonts/Inter-Medium.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/src/assets/fonts/Inter-Regular.ttf -------------------------------------------------------------------------------- /images/banners/new announcements.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/images/banners/new announcements.png -------------------------------------------------------------------------------- /src/assets/fonts/Inter-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/src/assets/fonts/Inter-ExtraBold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Inter-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/src/assets/fonts/Inter-ExtraLight.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Inter-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/src/assets/fonts/Inter-SemiBold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/dxiconsmaterial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/src/assets/fonts/dxiconsmaterial.ttf -------------------------------------------------------------------------------- /src/assets/fonts/dxiconsmaterial.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/src/assets/fonts/dxiconsmaterial.woff -------------------------------------------------------------------------------- /images/banners/custom course names.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/images/banners/custom course names.png -------------------------------------------------------------------------------- /images/banners/lightning fast pjax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/images/banners/lightning fast pjax.png -------------------------------------------------------------------------------- /src/assets/fonts/dxiconsmaterial.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/src/assets/fonts/dxiconsmaterial.woff2 -------------------------------------------------------------------------------- /images/banners/sort-and-search-tables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RishabSwift/Falcon/HEAD/images/banners/sort-and-search-tables.png -------------------------------------------------------------------------------- /manifest/firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "applications": { 3 | "gecko": { 4 | "id": "swift@hey.co", 5 | "strict_min_version": "53.0" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/assets/scss/nightowl/custom.scss: -------------------------------------------------------------------------------- 1 | .Mrphs-multipleTools .Mrphs-container { 2 | border-radius: 10px !important; 3 | box-shadow: 1px 1px 5px rgb(0 0 0 / 40%); 4 | } 5 | 6 | .owlAbout-pageContainer { 7 | background: $white !important; 8 | } 9 | -------------------------------------------------------------------------------- /src/assets/scss/bootstrap/shadows.scss: -------------------------------------------------------------------------------- 1 | .shadow-sm { box-shadow: $box-shadow-sm !important; } 2 | .shadow { box-shadow: $box-shadow !important; } 3 | .shadow-lg { box-shadow: $box-shadow-lg !important; } 4 | .shadow-none { box-shadow: none !important; } 5 | -------------------------------------------------------------------------------- /src/assets/scss/alert.scss: -------------------------------------------------------------------------------- 1 | .alert { 2 | 3 | &.alert-primary { 4 | background-color: $primary-light; 5 | padding: 1.75rem 1.25rem; 6 | border-radius: 10px; 7 | } 8 | 9 | .text-primary { 10 | color: $primary !important; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/services/redirect-handler.js: -------------------------------------------------------------------------------- 1 | // If a 301/302 redirect has occurred, force refresh the page since we cannot catch that via standard XHR request 2 | bodyExists = !!document.getElementsByTagName('body').length; 3 | if (!bodyExists) { 4 | window.location.reload(); 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/scss/bootstrap/main.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../node_modules/bootstrap/scss/functions"; 2 | @import "variables"; 3 | @import "../../../../node_modules/bootstrap/scss/mixins"; 4 | @import '../../../../node_modules/bootstrap/scss/card'; 5 | 6 | @import "mixins"; 7 | @import "card"; 8 | @import "shadows"; 9 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { 2 | if (changeInfo.status === 'complete') { 3 | if (/^https:\/\/owl\.uwo\.ca/.test(tab.url)) { 4 | chrome.tabs.executeScript(null, {file: './assets/redirect-handler.js'}) 5 | } 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /src/assets/scss/bootstrap/mixins/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | // 2 | // breakpoint.scss 3 | // Extended from Bootstrap 4 | // 5 | 6 | @function breakpoint-prev($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) { 7 | $n: index($breakpoint-names, $name); 8 | @return if($n != null and $n != 1, nth($breakpoint-names, $n - 1), null); 9 | } -------------------------------------------------------------------------------- /src/assets/scss/bootstrap/main-dark.scss: -------------------------------------------------------------------------------- 1 | //@import "variables"; 2 | @import "../../../../node_modules/bootstrap/scss/functions"; 3 | @import "variables"; 4 | @import "variables-dark"; 5 | @import "../../../../node_modules/bootstrap/scss/mixins"; 6 | @import '../../../../node_modules/bootstrap/scss/card'; 7 | 8 | @import "mixins"; 9 | @import "card"; 10 | @import "shadows"; 11 | 12 | -------------------------------------------------------------------------------- /src/assets/scss/bootstrap/mixins/_badge.scss: -------------------------------------------------------------------------------- 1 | // Badge Mixins 2 | // 3 | // This is a custom mixin for badge-#{color}-soft variant of Bootstrap's .badge class 4 | 5 | @mixin badge-variant-soft($bg, $color) { 6 | color: $color; 7 | background-color: $bg; 8 | 9 | &[href] { 10 | @include hover-focus { 11 | color: $color; 12 | text-decoration: none; 13 | background-color: darken($bg, 5%); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/assets/scss/vars-light.scss: -------------------------------------------------------------------------------- 1 | // This file contains all the variables for Falcon's LIGHT theme 2 | $primary: #6400ff; 3 | $nav-bg-color: $primary; 4 | $primary-darker: #4c00c1; 5 | $text-color: #222222; 6 | $white: #fff; 7 | $true-white: #fff; 8 | $light: #eef3f7; 9 | $tab-hover-color: #ccd5db; 10 | $sidebar-menu-active-bg-color: #ececec; 11 | $sidebar-menu-active-text-color: #3f4b5b; 12 | $link-hover-color: #4b00ba; 13 | $primary-light: #f3ebff; 14 | $primary-hover-color: $primary-darker; 15 | 16 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Falcon from "./services/falcon"; 2 | import FalconStorage from "./services/storage"; 3 | 4 | const START_KEY = 'falconEnabled'; 5 | 6 | // TODO -> this is called on every request... fix this.. esp if the user navigates every single page.. this is called 7 | // Resolve this by adding a last checked time stamp.. or even better yet, just getting this start key set up when the user installs it 8 | FalconStorage.sync().get(START_KEY).then(data => { 9 | if (Object.keys(data).length === 0) { 10 | FalconStorage.sync().set({ 11 | falconEnabled: true, 12 | }).then(() => { 13 | Falcon.start(); 14 | }) 15 | } 16 | 17 | if (data.falconEnabled) { 18 | Falcon.start(); 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /src/assets/scss/falcon.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the main file which contains ALL assets for Falcon 3 | * This is for both LIGHT and DARK mode 4 | */ 5 | 6 | $urlPath: "chrome-extension://__MSG_@@extension_id__"; 7 | 8 | @supports (-moz-appearance:none) { 9 | $urlPath: ""; 10 | } 11 | 12 | 13 | @import "../css/dist/animate.min.css"; 14 | @import "../css/dist/dx-diagram.css"; 15 | @import "../css/dist/dx.light"; 16 | @import "../css/dist/tablesorter-bs4.min.css"; 17 | @import "../css/owl/lesson-builder.css"; 18 | @import "../css/owl/lesson-builder-checklist.css"; 19 | 20 | @import "vars"; 21 | 22 | @import "bootstrap/main"; 23 | 24 | @import "helper"; 25 | @import "vars-light"; 26 | @import "fonts"; 27 | @import "shared"; 28 | @import "../fonts/feather"; 29 | @import "auth"; 30 | @import "loading-animation"; 31 | @import "alert"; 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/assets/scss/loading-animation.scss: -------------------------------------------------------------------------------- 1 | @-webkit-keyframes shimmer { 2 | 100% { 3 | transform: translateX(100%); 4 | } 5 | } 6 | 7 | @keyframes shimmer { 8 | 100% { 9 | transform: translateX(100%); 10 | } 11 | } 12 | 13 | .skeleton-box { 14 | display: inline-block; 15 | height: 1em; 16 | position: relative; 17 | overflow: hidden; 18 | background-color: $sidebar-menu-active-bg-color; 19 | color: transparent; 20 | border-radius: 3px; 21 | 22 | &::after { 23 | position: absolute; 24 | top: 0; 25 | right: 0; 26 | bottom: 0; 27 | left: 0; 28 | transform: translateX(-100%); 29 | background-image: linear-gradient(90deg, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 0.2) 20%, rgba(255, 255, 255, 0.5) 60%, rgba(255, 255, 255, 0)); 30 | -webkit-animation: shimmer 2s infinite; 31 | animation: shimmer 2s infinite; 32 | content: ""; 33 | } 34 | 35 | @keyframes shimmer { 36 | 100% { 37 | transform: translateX(100%); 38 | } 39 | } 40 | } 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/assets/scss/nightowl/_falcon-dark.scss: -------------------------------------------------------------------------------- 1 | // This file contains all the variables for Falcon's DARK (NightOWL) theme 2 | 3 | $primary: #e3a1ff; 4 | $nav-bg-color: #6400ff; 5 | //$nav-bg-color: #944eff; 6 | $primary-darker: #e3a1ff; 7 | $text-color: #12263f; 8 | $white: #191a1b; 9 | $true-white: #000; 10 | $fff: #fff; 11 | $light: $white; 12 | $tab-hover-color: #343435; 13 | $sidebar-menu-active-bg-color: #2d2d2d; 14 | $sidebar-menu-active-text-color: #c9c8c5; 15 | $link-hover-color: darken($primary, 10%); 16 | $primary-light: #261544; 17 | $primary-hover-color: #4c00c1; 18 | 19 | $border-color: #393d3d; 20 | 21 | 22 | $box-shadow: 0 1px 2px rgba(0,0,0,0.07), 23 | 0 2px 4px rgba(0,0,0,0.07), 24 | 0 4px 8px rgba(0,0,0,0.07), 25 | 0 8px 16px rgba(0,0,0,0.07), 26 | 0 16px 32px rgba(0,0,0,0.07), 27 | 0 32px 64px rgba(0,0,0,0.07); 28 | 29 | //@import "../../../../node_modules/bootstrap/scss/variables"; 30 | 31 | @import "../vars"; 32 | @import "../bootstrap/main-dark"; 33 | 34 | @import "../shared"; 35 | @import "falcon-dark-generated"; 36 | @import "custom"; 37 | @import "../auth"; 38 | @import "../alert"; 39 | -------------------------------------------------------------------------------- /src/assets/scss/_fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Inter'; 3 | font-style: normal; 4 | font-weight: 200; 5 | src: url('#{$urlPath}/fonts/Inter-ExtraLight.ttf'); 6 | } 7 | 8 | @font-face { 9 | font-family: 'Inter'; 10 | font-style: normal; 11 | font-weight: 300; 12 | src: url('#{$urlPath}/fonts/Inter-Light.ttf'); 13 | } 14 | 15 | @font-face { 16 | font-family: 'Inter'; 17 | font-style: normal; 18 | font-weight: 400; 19 | src: url('#{$urlPath}/fonts/Inter-Regular.ttf'); 20 | } 21 | 22 | @font-face { 23 | font-family: 'Inter'; 24 | font-style: normal; 25 | font-weight: 500; 26 | src: url('#{$urlPath}/fonts/Inter-Medium.ttf'); 27 | } 28 | 29 | @font-face { 30 | font-family: 'Inter'; 31 | font-style: normal; 32 | font-weight: 600; 33 | src: url('#{$urlPath}/fonts/Inter-SemiBold.ttf'); 34 | } 35 | 36 | @font-face { 37 | font-family: 'Inter'; 38 | font-style: normal; 39 | font-weight: 700; 40 | src: url('#{$urlPath}/fonts/Inter-Bold.ttf'); 41 | } 42 | 43 | @font-face { 44 | font-family: 'Inter'; 45 | font-style: normal; 46 | font-weight: 900; 47 | src: url('#{$urlPath}/fonts/Inter-Black.ttf'); 48 | } 49 | -------------------------------------------------------------------------------- /src/services/falcon-assignments.js: -------------------------------------------------------------------------------- 1 | import Alpine from 'alpinejs' 2 | window.Alpine = Alpine 3 | Alpine.start() 4 | 5 | class FalconAssignments { 6 | 7 | assignments = []; // all assignments. 8 | courseId; 9 | 10 | constructor(courseId) { 11 | this.courseId = courseId; 12 | this.setupAssignments(); 13 | } 14 | 15 | addEventListeners() { 16 | 17 | } 18 | 19 | 20 | async setupAssignments() { 21 | 22 | // fetch(`/direct/assignment/site/${this.courseId}.json`) 23 | // .then(response => response.json()) 24 | // .then(response => { 25 | // if (response.assignment_collection) { 26 | // this.assignments = response.assignment_collection; 27 | // } 28 | // }).then(() => { 29 | // $('#falcon-assignments').html(` 30 | //

31 | // ok. 32 | //
33 | // 36 | //
37 | // `); 38 | // }); 39 | 40 | } 41 | 42 | 43 | } 44 | 45 | export default FalconAssignments; 46 | -------------------------------------------------------------------------------- /manifest/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Falcon", 3 | "description": "Add various new functionalities and improvements to Western's OWL site", 4 | "version": "1.0.9", 5 | "manifest_version": 2, 6 | "icons": { 7 | "16": "./images/falcon-16x16.png", 8 | "32": "./images/falcon-32x32.png", 9 | "48": "./images/falcon-48x48.png", 10 | "128": "./images/falcon-128x128.png" 11 | }, 12 | "background": { 13 | "scripts": [ 14 | "./background.js" 15 | ], 16 | "persistent": false 17 | }, 18 | "options_page": "./options.html", 19 | "browser_action": { 20 | "default_popup": "./popup.html" 21 | }, 22 | "permissions": [ 23 | "storage", 24 | "tabs", 25 | "https://owl.uwo.ca/*/*", 26 | "https://www.owlfalcon.com/*" 27 | ], 28 | "content_scripts": [ 29 | { 30 | "matches": [ 31 | "https://owl.uwo.ca/*" 32 | ], 33 | "exclude_matches": [ 34 | "https://owl.uwo.ca/*.pdf" 35 | ], 36 | "css": [ 37 | "./assets/falcon.css" 38 | ], 39 | "js": [ 40 | "./assets/falcon.js" 41 | ], 42 | "run_at": "document_idle" 43 | } 44 | ], 45 | "web_accessible_resources": [ 46 | "/assets/*", 47 | "/fonts/*" 48 | ], 49 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'" 50 | } 51 | -------------------------------------------------------------------------------- /src/assets/scss/auth.scss: -------------------------------------------------------------------------------- 1 | #Mrphs-xlogin { 2 | 3 | border-radius: 15px !important; 4 | background-color: $true-white !important; 5 | 6 | fieldset { 7 | padding: 20px !important; 8 | 9 | #eid, #pw { 10 | padding: 0.55em 0.4em !important; 11 | border-radius: 5px !important; 12 | border: none; 13 | background: $light !important; 14 | } 15 | 16 | #eid { 17 | margin-bottom: 0.75em !important; 18 | } 19 | 20 | input[type=submit], input[name=cancel]{ 21 | width: 100% !important; 22 | padding: 0.70em 0 !important; 23 | margin-right: 0 !important; 24 | margin-top: 5px !important; 25 | } 26 | 27 | input[name=cancel] { 28 | margin-top: 0.35em !important; 29 | padding: 0.50em 0 !important; 30 | border-radius: 5px !important; 31 | } 32 | } 33 | 34 | .logo { 35 | background: black url(#{$logoUrl}) no-repeat center center !important; 36 | } 37 | } 38 | 39 | #loginForm { 40 | #eid, #pw { 41 | border-radius: 5px !important; 42 | padding: 0.05em .35em !important; 43 | } 44 | 45 | .Mrphs-loginForm__button { 46 | background: $nav-bg-color !important; 47 | border: $primary-darker !important; 48 | color: white !important; 49 | &:hover { 50 | background: $primary-hover-color !important; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/services/gradebook.js: -------------------------------------------------------------------------------- 1 | class FalconGradebook { 2 | constructor() { 3 | // parse the grades 4 | this.parseGrades(); 5 | } 6 | 7 | parseGrades() { 8 | let _grades = $('.gb-summary-grade-score'); 9 | for (let grades of _grades) { 10 | // $(grades).closest('.') 11 | let grade = $(grades).children('.gb-summary-grade-score-raw').text(); 12 | let outOf = $(grades).children('.gb-summary-grade-score-outof').text() 13 | outOf = outOf.replaceAll('/', '').replaceAll(',', ''); 14 | grade = grade.replaceAll(',', ''); 15 | 16 | let emoji = this.getEmojiForGrade(parseFloat(grade), parseFloat(outOf)); 17 | $(grades).children('.gb-summary-grade-score-outof').append(' ' + emoji); 18 | } 19 | } 20 | 21 | getEmojiForGrade(grade, outOf) { 22 | if (grade === outOf) { 23 | return '💯' 24 | } 25 | 26 | if (grade === 0){ 27 | return '😭'; 28 | } 29 | 30 | if (grade > outOf) { 31 | return '👀'; 32 | } 33 | 34 | if (grade === '69') { 35 | return '😉' 36 | } 37 | 38 | if (grade / outOf < 0.5) { 39 | return '😢'; 40 | } 41 | 42 | if (grade / outOf === 0.5) { 43 | return '😅'; 44 | } 45 | 46 | return ''; 47 | 48 | } 49 | 50 | } 51 | 52 | export default FalconGradebook; 53 | -------------------------------------------------------------------------------- /src/services/storage.js: -------------------------------------------------------------------------------- 1 | /** 2 | This storage file is responsible for saving data using the browser storage sync/local API 3 | */ 4 | 5 | const FalconStorage = { 6 | 7 | api: 'sync', // sync OR local 8 | 9 | setup: () => { 10 | 11 | }, 12 | 13 | sync: () => { 14 | FalconStorage.api = 'sync'; 15 | return FalconStorage; 16 | }, 17 | 18 | local: () => { 19 | FalconStorage.api = 'local'; 20 | return FalconStorage; 21 | }, 22 | 23 | get: (key) => { 24 | return new Promise((resolve, reject) => 25 | chrome.storage[FalconStorage.api].get(key, result => 26 | chrome.runtime.lastError 27 | ? reject(Error(chrome.runtime.lastError.message)) 28 | : resolve(result) 29 | ) 30 | ); 31 | }, 32 | 33 | set: (data) => { 34 | return new Promise((resolve, reject) => 35 | chrome.storage[FalconStorage.api].set(data, () => 36 | chrome.runtime.lastError 37 | ? reject(Error(chrome.runtime.lastError.message)) 38 | : resolve() 39 | ) 40 | ); 41 | }, 42 | 43 | // data = array returned from FalconStorage.get() 44 | existsInStorage: (data, index, matching) => { 45 | if (!data) { 46 | return false; 47 | } 48 | return data.findIndex(item => item[index] === matching) !== -1; 49 | } 50 | } 51 | 52 | export default FalconStorage; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "falcon", 3 | "version": "1.0.1", 4 | "description": "Add various new functionalities and improvements to Western's OWL site", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --config=node_modules/laravel-mix/setup/webpack.config.js", 8 | "watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --config=node_modules/laravel-mix/setup/webpack.config.js", 9 | "watch-poll": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --watch-poll --progress --config=node_modules/laravel-mix/setup/webpack.config.js", 10 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 11 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --config=node_modules/laravel-mix/setup/webpack.config.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/RishabSwift/Falcon.git" 16 | }, 17 | "keywords": [ 18 | "falcon" 19 | ], 20 | "author": "RishabSwift", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/RishabSwift/Falcon/issues" 24 | }, 25 | "homepage": "https://github.com/RishabSwift/Falcon#readme", 26 | "devDependencies": { 27 | "cross-env": "^7.0.3", 28 | "laravel-mix": "^6.0.31", 29 | "node-sass": "^6.0.1", 30 | "resolve-url-loader": "^4.0.0", 31 | "sass": "^1.42.1", 32 | "sass-loader": "^12.1.0" 33 | }, 34 | "dependencies": { 35 | "alpinejs": "^3.4.2", 36 | "bootstrap": "^4.6.0", 37 | "del": "^6.0.0", 38 | "devextreme": "^21.1.5", 39 | "jquery": "^3.6.0", 40 | "merge-jsons-webpack-plugin": "^2.0.0-alpha", 41 | "pjax": "^0.2.8", 42 | "popper.js": "^1.16.1", 43 | "topbar": "^1.0.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Falcon 12 | 19 | 20 | 21 | 22 |
23 | 24 |
25 | 26 |

Falcon for Owl

27 |
28 | 29 |
30 | 31 |

32 | Falcon is installed! 33 |

34 | 35 |
36 |
37 |
38 |
39 | 40 | 41 |
42 | Current Status 43 |
44 | 45 | 46 | Active 47 | 48 |
49 |
50 | 51 | 52 | 53 | 54 |
55 |
56 |
57 |
58 | 59 |
60 | 61 |
62 | 63 |
64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | let mix = require('laravel-mix'); 2 | let del = require('del'); 3 | 4 | 5 | let MergeJsonWebpackPlugin = require("merge-jsons-webpack-plugin"); 6 | 7 | mix.js(['src/main.js'], 'dist/falcon.js'); 8 | mix.sass('src/assets/scss/falcon.scss', 'dist/falcon.css') 9 | mix.sass('src/assets/scss/nightowl/_falcon-dark.scss', 'dist/falcon-dark.css') 10 | 11 | mix.copy('src/background.js', 'dist/chrome/background.js') 12 | .copy('src/options.html', 'dist/chrome/options.html') 13 | .copy('src/popup.html', 'dist/chrome/popup.html') 14 | .copy('src/popup.js', 'dist/chrome/popup.js') 15 | .copy('dist/falcon.js', 'dist/chrome/assets/falcon.js') 16 | .copy('dist/falcon.css', 'dist/chrome/assets/falcon.css') 17 | .copy('dist/falcon-dark.css', 'dist/chrome/assets/falcon-dark.css') 18 | .copy('src/assets/css/popup-app.css', 'dist/chrome/assets/popup-app.css') 19 | .copy('src/assets/js/dist/jquery-2.2.4.min.js', 'dist/chrome/assets/jquery-2.2.4.min.js') 20 | .copy('src/assets/js/dist/alpine-3.9.0.min.js', 'dist/chrome/assets/alpine-3.9.0.min.js') 21 | .copy('src/services/redirect-handler.js', 'dist/chrome/assets/redirect-handler.js') 22 | .copyDirectory('images/icons', 'dist/chrome/images') 23 | .copyDirectory('src/assets/fonts', 'dist/chrome/fonts') 24 | // .then(() => { 25 | // del('dist/falcon.css'); 26 | // del('dist/falcon.js'); 27 | // }); 28 | 29 | // now copy the chrome folder to firefox 30 | // mix.copyDirectory('dist/chrome', 'dist/firefox'); 31 | 32 | // join manifest files and copy to dist/chrome 33 | // copy background.js to dist/chrome/background.js 34 | // copy fonts to dist/chrome/fonts 35 | // options.html, popup.html, popup.js to dist/chrome/ 36 | mix.webpackConfig({ 37 | plugins: [ 38 | new MergeJsonWebpackPlugin({ 39 | "files": [ 40 | './manifest/common.json', 41 | './manifest/chrome.json', 42 | ], 43 | "output": { 44 | "fileName": 'dist/chrome/manifest.json' 45 | } 46 | }), 47 | // new MergeJsonWebpackPlugin({ 48 | // "files": [ 49 | // './manifest/common.json', 50 | // './manifest/firefox.json', 51 | // ], 52 | // "output": { 53 | // "fileName": 'dist/firefox/manifest.json' 54 | // } 55 | // }) 56 | ] 57 | }) 58 | 59 | -------------------------------------------------------------------------------- /src/services/table-sorter.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | window.$ = window.jQuery = $; 3 | import '../assets/js/dist/jquery.tablesorter.min'; 4 | import '../assets/js/dist/jquery.tablesorter.widgets.min.js'; 5 | 6 | class TableSorter { 7 | 8 | // all the tables that should be sorted/searched 9 | sortableTables =[ 10 | 'form[name="listAssignmentsForm"]', 11 | 'form[name="announcementListForm"]' 12 | ]; 13 | 14 | constructor() { 15 | this.makeSortable(); 16 | } 17 | 18 | 19 | makeSortable() { 20 | // Ensure that the page includes tables to be sortable 21 | if (!this.tablesExist()) { 22 | return; 23 | } 24 | 25 | // These tables do not have ... so convert the header to thead... 26 | let theadElement = $('table tbody tr')[0]; 27 | 28 | $('table').prepend(`${theadElement}`).removeClass('table-bordered'); 29 | $('table thead').html(theadElement); 30 | 31 | setTimeout(function () { 32 | $('.tablesorter-header-inner a').contents().unwrap().wrap(''); 33 | }, 10); 34 | 35 | $('td[headers="author"]').addClass('text-nowrap'); 36 | 37 | $("table").tablesorter({ 38 | theme: "bootstrap", 39 | widthFixed: true, 40 | widgets: [ "columns"], 41 | 42 | widgetOptions: { 43 | 44 | 45 | // class names added to columns when sorted 46 | columns: ["falcon-table-column"], 47 | 48 | // reset filters button TODO: add it 49 | filter_reset: ".reset", 50 | 51 | // extra css class name (string or array) added to the filter element (input or select) 52 | filter_cssFilter: [ 53 | 'form-control', 54 | 'form-control', 55 | 'form-control', // select needs custom class names :( 56 | 'form-control', 57 | 'form-control', 58 | 'form-control', 59 | 'form-control' 60 | ] 61 | 62 | } 63 | }); 64 | } 65 | 66 | tablesExist() { 67 | let exists = false; 68 | this.sortableTables.forEach(table => { 69 | if ($(table).length > 0) { 70 | exists = true; 71 | } 72 | }) 73 | return exists; 74 | } 75 | } 76 | 77 | export default TableSorter; 78 | -------------------------------------------------------------------------------- /src/assets/css/dist/tablesorter-bs4.min.css: -------------------------------------------------------------------------------- 1 | .tablesorter-bootstrap { 2 | width: 100% 3 | } 4 | 5 | .tablesorter-bootstrap tfoot td, .tablesorter-bootstrap tfoot th, .tablesorter-bootstrap thead td, .tablesorter-bootstrap thead th { 6 | font: 14px/20px Arial, Sans-serif; 7 | font-weight: 700; 8 | padding: 4px; 9 | margin: 0 0 18px 10 | } 11 | 12 | .tablesorter-bootstrap thead .tablesorter-header { 13 | background-position: right 5px center; 14 | background-repeat: no-repeat; 15 | cursor: pointer; 16 | white-space: normal 17 | } 18 | 19 | .tablesorter-bootstrap:not(.table-dark) tfoot td, .tablesorter-bootstrap:not(.table-dark) tfoot th, .tablesorter-bootstrap:not(.table-dark) thead:not(.thead-dark) .tablesorter-header { 20 | background-color: #eee 21 | } 22 | 23 | .tablesorter-bootstrap thead .sorter-false { 24 | cursor: default; 25 | background-image: none 26 | } 27 | 28 | .tablesorter-bootstrap .tablesorter-header-inner { 29 | position: relative; 30 | padding: 4px 18px 4px 4px 31 | } 32 | 33 | .tablesorter-bootstrap .sorter-false .tablesorter-header-inner { 34 | padding: 4px 35 | } 36 | 37 | .tablesorter-bootstrap thead .tablesorter-headerUnSorted:not(.sorter-false) { 38 | background-image: url(data:image/gif;base64,R0lGODlhFQAJAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAkAAAIXjI+AywnaYnhUMoqt3gZXPmVg94yJVQAAOw==); 39 | } 40 | 41 | .tablesorter-bootstrap thead .tablesorter-headerAsc { 42 | background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjI8Bya2wnINUMopZAQA7); 43 | } 44 | 45 | .tablesorter-bootstrap thead .tablesorter-headerDesc { 46 | background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjB+gC+jP2ptn0WskLQA7); 47 | } 48 | 49 | .tablesorter-bootstrap thead.thead-dark .tablesorter-headerUnSorted:not(.sorter-false), .tablesorter-bootstrap.table-dark thead .tablesorter-headerUnSorted:not(.sorter-false) { 50 | background-image: url(data:image/gif;base64,R0lGODlhFQAJAIAAAP///////yH5BAEAAAEALAAAAAAVAAkAAAIXjI+AywnaYnhUMoqt3gZXPmVg94yJVQAAOw==); 51 | } 52 | 53 | .tablesorter-bootstrap thead.thead-dark .tablesorter-headerAsc, .tablesorter-bootstrap.table-dark thead .tablesorter-headerAsc { 54 | background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAAP///////yH5BAEAAAEALAAAAAAVAAQAAAINjI8Bya2wnINUMopZAQA7); 55 | } 56 | 57 | .tablesorter-bootstrap thead.thead-dark .tablesorter-headerDesc, .tablesorter-bootstrap.table-dark thead .tablesorter-headerDesc { 58 | background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAAP///////yH5BAEAAAEALAAAAAAVAAQAAAINjB+gC+jP2ptn0WskLQA7); 59 | } 60 | -------------------------------------------------------------------------------- /src/services/core.js: -------------------------------------------------------------------------------- 1 | const Core = { 2 | 3 | formatDateTime: (date) => { 4 | const options = {year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'}; 5 | return date.toLocaleDateString('en-US', options); 6 | 7 | }, 8 | 9 | // https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site 10 | timeAgo: (time) => { 11 | 12 | switch (typeof time) { 13 | case 'number': 14 | break; 15 | case 'string': 16 | time = +new Date(time); 17 | break; 18 | case 'object': 19 | if (time.constructor === Date) time = time.getTime(); 20 | break; 21 | default: 22 | time = +new Date(); 23 | } 24 | var time_formats = [ 25 | [60, 'seconds', 1], // 60 26 | [120, '1 minute ago', '1 minute from now'], // 60*2 27 | [3600, 'minutes', 60], // 60*60, 60 28 | [7200, '1 hour ago', '1 hour from now'], // 60*60*2 29 | [86400, 'hours', 3600], // 60*60*24, 60*60 30 | [172800, 'Yesterday', 'Tomorrow'], // 60*60*24*2 31 | [604800, 'days', 86400], // 60*60*24*7, 60*60*24 32 | [1209600, 'Last week', 'Next week'], // 60*60*24*7*4*2 33 | [2419200, 'weeks', 604800], // 60*60*24*7*4, 60*60*24*7 34 | [4838400, 'Last month', 'Next month'], // 60*60*24*7*4*2 35 | [29030400, 'months', 2419200], // 60*60*24*7*4*12, 60*60*24*7*4 36 | [58060800, 'Last year', 'Next year'], // 60*60*24*7*4*12*2 37 | [2903040000, 'years', 29030400], // 60*60*24*7*4*12*100, 60*60*24*7*4*12 38 | [5806080000, 'Last century', 'Next century'], // 60*60*24*7*4*12*100*2 39 | [58060800000, 'centuries', 2903040000] // 60*60*24*7*4*12*100*20, 60*60*24*7*4*12*100 40 | ]; 41 | var seconds = (+new Date() - time) / 1000, 42 | token = 'ago', 43 | list_choice = 1; 44 | 45 | 46 | if (seconds === 0 || seconds < 0.05) { 47 | return 'just now' 48 | } 49 | if (seconds < 0) { 50 | seconds = Math.abs(seconds); 51 | token = 'from now'; 52 | list_choice = 2; 53 | } 54 | var i = 0, 55 | format; 56 | while (format = time_formats[i++]) 57 | if (seconds < format[0]) { 58 | if (typeof format[2] == 'string') 59 | return format[list_choice]; 60 | else 61 | return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token; 62 | } 63 | return time; 64 | } 65 | } 66 | 67 | export default Core; 68 | -------------------------------------------------------------------------------- /mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/dist/falcon.js": "/dist/falcon.js", 3 | "/dist/falcon.css": "/dist/falcon.css", 4 | "/dist/falcon-dark.css": "/dist/falcon-dark.css", 5 | "/dist/chrome/background.js": "/dist/chrome/background.js", 6 | "/dist/chrome/options.html": "/dist/chrome/options.html", 7 | "/dist/chrome/popup.html": "/dist/chrome/popup.html", 8 | "/dist/chrome/popup.js": "/dist/chrome/popup.js", 9 | "/dist/chrome/assets/falcon.js": "/dist/chrome/assets/falcon.js", 10 | "/dist/chrome/assets/falcon.css": "/dist/chrome/assets/falcon.css", 11 | "/dist/chrome/assets/falcon-dark.css": "/dist/chrome/assets/falcon-dark.css", 12 | "/dist/chrome/assets/popup-app.css": "/dist/chrome/assets/popup-app.css", 13 | "/dist/chrome/assets/jquery-2.2.4.min.js": "/dist/chrome/assets/jquery-2.2.4.min.js", 14 | "/dist/chrome/assets/alpine-3.9.0.min.js": "/dist/chrome/assets/alpine-3.9.0.min.js", 15 | "/dist/chrome/assets/redirect-handler.js": "/dist/chrome/assets/redirect-handler.js", 16 | "/dist/chrome/images/falcon-128x128.png": "/dist/chrome/images/falcon-128x128.png", 17 | "/dist/chrome/images/falcon-16x16.png": "/dist/chrome/images/falcon-16x16.png", 18 | "/dist/chrome/images/falcon-32x32.png": "/dist/chrome/images/falcon-32x32.png", 19 | "/dist/chrome/images/falcon-48x48.png": "/dist/chrome/images/falcon-48x48.png", 20 | "/dist/chrome/images/falcon-hd.png": "/dist/chrome/images/falcon-hd.png", 21 | "/dist/chrome/fonts/Feather.svg": "/dist/chrome/fonts/Feather.svg", 22 | "/dist/chrome/fonts/Feather.ttf": "/dist/chrome/fonts/Feather.ttf", 23 | "/dist/chrome/fonts/Feather.woff": "/dist/chrome/fonts/Feather.woff", 24 | "/dist/chrome/fonts/Inter-Black.ttf": "/dist/chrome/fonts/Inter-Black.ttf", 25 | "/dist/chrome/fonts/Inter-Bold.ttf": "/dist/chrome/fonts/Inter-Bold.ttf", 26 | "/dist/chrome/fonts/Inter-ExtraBold.ttf": "/dist/chrome/fonts/Inter-ExtraBold.ttf", 27 | "/dist/chrome/fonts/Inter-ExtraLight.ttf": "/dist/chrome/fonts/Inter-ExtraLight.ttf", 28 | "/dist/chrome/fonts/Inter-Light.ttf": "/dist/chrome/fonts/Inter-Light.ttf", 29 | "/dist/chrome/fonts/Inter-Medium.ttf": "/dist/chrome/fonts/Inter-Medium.ttf", 30 | "/dist/chrome/fonts/Inter-Regular.ttf": "/dist/chrome/fonts/Inter-Regular.ttf", 31 | "/dist/chrome/fonts/Inter-SemiBold.ttf": "/dist/chrome/fonts/Inter-SemiBold.ttf", 32 | "/dist/chrome/fonts/Inter-Thin.ttf": "/dist/chrome/fonts/Inter-Thin.ttf", 33 | "/dist/chrome/fonts/dxicons.ttf": "/dist/chrome/fonts/dxicons.ttf", 34 | "/dist/chrome/fonts/dxicons.woff": "/dist/chrome/fonts/dxicons.woff", 35 | "/dist/chrome/fonts/dxicons.woff2": "/dist/chrome/fonts/dxicons.woff2", 36 | "/dist/chrome/fonts/dxiconsmaterial.ttf": "/dist/chrome/fonts/dxiconsmaterial.ttf", 37 | "/dist/chrome/fonts/dxiconsmaterial.woff": "/dist/chrome/fonts/dxiconsmaterial.woff", 38 | "/dist/chrome/fonts/dxiconsmaterial.woff2": "/dist/chrome/fonts/dxiconsmaterial.woff2", 39 | "/dist/chrome/fonts/feather.scss": "/dist/chrome/fonts/feather.scss" 40 | } 41 | -------------------------------------------------------------------------------- /src/popup.js: -------------------------------------------------------------------------------- 1 | fetch('https://www.owlfalcon.com/popup-message') 2 | .then(response => response.text()) 3 | .then(response => { 4 | document.getElementById('message').innerHTML = response; 5 | }); 6 | 7 | 8 | var backgroundPage; 9 | let enableButton = $('#enable-button'); 10 | let disableButton = $('#disable-button'); 11 | let activeText = $('#status-active'); 12 | let inactiveText = $('#status-inactive'); 13 | let activeIcon = $('#status-active-icon'); 14 | let inactiveIcon = $('#status-inactive-icon'); 15 | 16 | 17 | (new Promise((resolve, reject) => 18 | chrome.storage['sync'].get('falconEnabled', result => 19 | chrome.runtime.lastError 20 | ? reject(Error(chrome.runtime.lastError.message)) 21 | : resolve(result) 22 | ) 23 | )).then(response => { 24 | updateButtonVisibility(response.falconEnabled); 25 | }); 26 | 27 | 28 | function updateButtonVisibility(isEnabled) { 29 | if (isEnabled) { 30 | disableButton.show(); 31 | enableButton.hide(); 32 | activeText.show(); 33 | inactiveText.hide(); 34 | activeIcon.show(); 35 | inactiveIcon.hide(); 36 | } else { 37 | enableButton.show(); 38 | disableButton.hide(); 39 | inactiveText.show(); 40 | activeText.hide(); 41 | inactiveIcon.show(); 42 | activeIcon.hide(); 43 | } 44 | } 45 | 46 | $('#reset-falcon-btn').on('click', function() { 47 | chrome.storage.sync.clear(); 48 | }) 49 | 50 | function toggleButton(enable) { 51 | backgroundPage.isExtensionOn = enable; 52 | 53 | updateButtonVisibility(backgroundPage.isExtensionOn); 54 | chrome.tabs.executeScript(null, {file: './assets/redirect-handler.js'}) 55 | } 56 | 57 | chrome.runtime.getBackgroundPage(function (bgPage) { 58 | backgroundPage = bgPage; 59 | 60 | enableButton.on('click', function() { 61 | $('#falcon-alert').html(`

Falcon is active.

Refresh your page to take effect.`) 62 | toggleButton(true); 63 | 64 | new Promise((resolve, reject) => 65 | chrome.storage['sync'].set({falconEnabled: true}, () => 66 | chrome.runtime.lastError 67 | ? reject(Error(chrome.runtime.lastError.message)) 68 | : resolve() 69 | ) 70 | ); 71 | }) 72 | disableButton.on('click', function() { 73 | $('#falcon-alert').html(`

Falcon has been disabled.

The only thing still enabled is the purple theme of OWL, which does not impact any functionality since it's just a theme. Refresh your page to take effect.`) 74 | toggleButton(false); 75 | 76 | new Promise((resolve, reject) => 77 | chrome.storage['sync'].set({falconEnabled: false}, () => 78 | chrome.runtime.lastError 79 | ? reject(Error(chrome.runtime.lastError.message)) 80 | : resolve() 81 | ) 82 | ); 83 | }) 84 | 85 | }); 86 | 87 | -------------------------------------------------------------------------------- /src/services/dark-mode.js: -------------------------------------------------------------------------------- 1 | import FalconStorage from "./storage"; 2 | import AssetInjector from "../ui/asset-injector"; 3 | import FalconInterfaceInjector from "../ui/ui-injector"; 4 | import FalconAnnouncement from "./announcement"; 5 | 6 | class FalconDarkMode { 7 | storageKey = 'darkMode' 8 | assetUrl = './assets/falcon-dark.css'; 9 | assetUrlLight = './assets/falcon.css'; 10 | className = 'falcon-dark-mode'; 11 | classNameLight = 'falcon-normal-mode'; 12 | isDarkModeEnabled; 13 | 14 | constructor() { 15 | FalconInterfaceInjector.darkModeButton(); 16 | this.setupEventListeners(); 17 | this.isEnabled().then(({darkMode}) => { 18 | this.isDarkModeEnabled = darkMode; 19 | this.isDarkModeEnabled ? this.injectDarkMode() : this.removeDarkMode(); 20 | }) 21 | } 22 | 23 | 24 | setupEventListeners() { 25 | let self = this; 26 | $('#dark-mode-toggle').on('click', function () { 27 | self.toggle(); 28 | }) 29 | } 30 | 31 | static isDarkModeEnabled() { 32 | return $("#dark-mode-toggle-icon").hasClass('fa-toggle-on'); 33 | } 34 | 35 | isEnabled() { 36 | return FalconStorage.sync().get(this.storageKey); 37 | } 38 | 39 | toggle() { 40 | if (this.isDarkModeEnabled) { 41 | this.removeDarkMode(); 42 | this.disable(); 43 | } else { 44 | this.injectDarkMode(); 45 | this.enable(); 46 | } 47 | } 48 | 49 | injectDarkMode() { 50 | AssetInjector.falconAssets().alsoToIframes().once().injectStyle(this.assetUrl, this.className) 51 | $("#dark-mode-toggle-icon").addClass('fa-toggle-on'); 52 | this.isDarkModeEnabled = true; 53 | this.fixPortalColors(); 54 | FalconAnnouncement.fixDarkModeAnnouncement(); 55 | } 56 | 57 | removeDarkMode() { 58 | // AssetInjector.falconAssets().alsoToIframes().once().injectStyle(this.assetUrlLight, this.classNameLight); 59 | AssetInjector.falconAssets().alsoToIframes().removeInjectionByClassName(this.className); 60 | $("#dark-mode-toggle-icon").removeClass('fa-toggle-on'); 61 | this.isDarkModeEnabled = false 62 | } 63 | 64 | fixPortalColors() { 65 | // sometimes instructors set certain colors to the portal (like black for text) which, when dark mode is enabled, cannot be overwritten as easily. 66 | // so remove those overwritten styles 67 | $('.portletBody.siteDescription [style*="background-color"]').css('background-color', ''); 68 | $('.portletBody.siteDescription [style*="background"]').css('background-color', ''); 69 | $('.portletBody.siteDescription [style*="color"]').css('color', ''); 70 | } 71 | 72 | 73 | 74 | // Enable and disable are separate from the injectDarkMode and removeDarkMode because we don't want to save the settings everytime from the constructor 75 | enable() { 76 | FalconStorage.sync().set({darkMode: true}) 77 | } 78 | 79 | disable() { 80 | FalconStorage.sync().set({darkMode: false}) 81 | } 82 | 83 | 84 | } 85 | 86 | export default FalconDarkMode; 87 | -------------------------------------------------------------------------------- /src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Falcon 12 | 20 | 21 | 22 | 23 |
24 | 25 |
26 | 27 |

Falcon for Owl

28 |
29 | 30 |
31 | 32 |
33 |
34 |
35 |
36 | 37 | 38 |
39 | Falcon is 40 |
41 | 42 | 43 | Active Disabled 44 | 45 |
46 |
47 | 48 | 49 | 50 | 51 |
52 |
53 |
54 |
55 | 56 |
57 | 58 | 61 | 64 |
65 | 66 |
67 | Reset Falcon 68 |
69 |
Reset Falcon?
70 |
Doing so will remove all your saved course names and clear your Falcon Editor. Are you sure?
71 | 72 | 73 |
74 | 75 |

Falcon has been reset.

Refresh your page to take effect.
76 | 77 |
78 | 79 |
80 | 81 |
82 | 83 |
84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/assets/scss/bootstrap/card.scss: -------------------------------------------------------------------------------- 1 | // 2 | // card.scss 3 | // Extended from Bootstrap 4 | // 5 | 6 | // 7 | // Bootstrap Overrides ===================================== 8 | // 9 | 10 | .card { 11 | display: block; 12 | margin-bottom: $spacer; 13 | border-color: $card-outline-color; 14 | box-shadow: $card-box-shadow; 15 | } 16 | 17 | .card-header { 18 | display: flex; 19 | flex-direction: row; 20 | flex-grow: 1; 21 | align-items: center; 22 | min-height: $card-header-min-height; 23 | padding-top: $card-spacer-y / 2; 24 | padding-bottom: $card-spacer-y / 2; 25 | border-bottom-color: $border-color !important; 26 | > * { 27 | flex: 1; 28 | } 29 | } 30 | 31 | .card-title { 32 | margin-bottom: .5rem; 33 | } 34 | 35 | 36 | // 37 | // Dashkit =================================== 38 | // 39 | 40 | // Card header 41 | // 42 | // Make sure the card header is always the same height with its content 43 | // centered vertically 44 | 45 | .card-header-title { 46 | margin-bottom: 0; 47 | } 48 | 49 | .card-header-tabs { 50 | margin-top: -$card-spacer-y / 2; 51 | margin-bottom: -$card-spacer-y / 2; 52 | margin-left: 0; 53 | margin-right: 0; 54 | } 55 | 56 | .card-header-tabs .nav-link { 57 | padding-top: calc((#{$card-header-min-height} - 1em * #{$line-height-base}) / 2) !important; 58 | padding-bottom: calc((#{$card-header-min-height} - 1em * #{$line-height-base}) / 2) !important; 59 | } 60 | 61 | 62 | // Card table 63 | // 64 | // Make sure the card table content is aligned with the rest of the 65 | // card content 66 | 67 | .card-table { 68 | margin-bottom:0; 69 | 70 | thead th { 71 | border-top-width: 0; 72 | } 73 | 74 | thead th, tbody td { 75 | 76 | &:first-child { 77 | padding-left: $card-spacer-x !important; 78 | } 79 | &:last-child { 80 | padding-right: $card-spacer-x !important; 81 | } 82 | } 83 | } 84 | 85 | .card > .table-responsive:first-child > .card-table, 86 | .card > .card-table:first-child { 87 | 88 | > thead, > tbody, > tfoot { 89 | 90 | &:first-child { 91 | 92 | > tr:first-child { 93 | 94 | > th, > td { 95 | 96 | &:first-child { 97 | border-top-left-radius: $card-border-radius; 98 | } 99 | &:last-child { 100 | border-top-right-radius: $card-border-radius; 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | 109 | // Card avatar 110 | // 111 | // Moves card avatar up by 50% 112 | 113 | .card-avatar { 114 | display: block !important; 115 | margin-left: auto; margin-right: auto; 116 | margin-bottom: $card-spacer-y; 117 | } 118 | .card-avatar-top { 119 | margin-top: -($card-spacer-x + $avatar-size-base / 2); 120 | } 121 | .card-avatar-top.avatar-xs { 122 | margin-top: -($card-spacer-x + $avatar-size-xs / 2); 123 | } 124 | .card-avatar-top.avatar-sm { 125 | margin-top: -($card-spacer-x + $avatar-size-sm / 2); 126 | } 127 | .card-avatar-top.avatar-lg { 128 | margin-top: -($card-spacer-x + $avatar-size-lg / 2); 129 | } 130 | .card-avatar-top.avatar-xl { 131 | margin-top: -($card-spacer-x + $avatar-size-xl / 2); 132 | } 133 | .card-avatar-top.avatar-xxl { 134 | margin-top: -($card-spacer-x + $avatar-size-xxl / 2); 135 | } 136 | 137 | 138 | // Card dropdown 139 | // 140 | // Places dropdowns in the top right corner 141 | 142 | .card-dropdown { 143 | position: absolute; 144 | top: $card-spacer-x; 145 | right: $card-spacer-x; 146 | } 147 | 148 | 149 | // Card inactive 150 | 151 | .card-inactive { 152 | border-color: $border-color; 153 | border-style: dashed; 154 | background-color: transparent; 155 | box-shadow: none; 156 | } 157 | 158 | 159 | // Card flush 160 | 161 | .card-flush { 162 | background: none; 163 | border: none; 164 | box-shadow: none; 165 | } 166 | 167 | 168 | // Card sizing 169 | 170 | .card-sm .card-body { 171 | padding: $card-spacer-x-sm; 172 | } 173 | 174 | 175 | // Card header flush 176 | 177 | .card-header-flush { 178 | border-bottom: 0; 179 | } 180 | 181 | .card-header-flush + .card-body { 182 | padding-top: 0; 183 | } 184 | -------------------------------------------------------------------------------- /src/services/course-name-editor.js: -------------------------------------------------------------------------------- 1 | import FalconStorage from "./storage"; 2 | import FalconInterfaceInjector from "../ui/ui-injector"; 3 | 4 | class FalconCourseNameEditor { 5 | 6 | STORAGE_KEY = 'customCourseNames'; 7 | customCourseNames = []; 8 | 9 | constructor() { 10 | FalconInterfaceInjector.courseEditElements(); 11 | this.addEventListeners(); 12 | this.getCourseNames() 13 | } 14 | 15 | 16 | 17 | addEventListeners() { 18 | 19 | let self = this; 20 | 21 | $('.edit-course-name-button').on('click', function () { 22 | $('#custom-course-name-text').val(''); 23 | let link = $(this).siblings('div.fav-title').find('a'); 24 | $('#old-course-name').html(link.attr('title')); 25 | self.autoPopulateCourseNameInModal() 26 | }) 27 | 28 | 29 | // once they save their custom course name 30 | $('#save-custom-course-title-button').on('click', function () { 31 | let customName = $('#custom-course-name-text').val(); 32 | let oldCourseName = $('#old-course-name').html(); 33 | 34 | self.saveCourseName(oldCourseName, customName).then(() => { 35 | $('#custom-course-name-text').val(''); 36 | $('#edit-course-title-modal').modal('hide'); 37 | }); 38 | }) 39 | 40 | $('#custom-course-name-text').on('keypress', function (e) { 41 | if (e.which === 13) { // enter pressed 42 | // Disable textbox to prevent multiple submit 43 | $(this).attr("disabled", "disabled"); 44 | $('#save-custom-course-title-button').click(); 45 | $(this).removeAttr("disabled"); 46 | } 47 | }); 48 | 49 | } 50 | 51 | // auto populate course name that's being edited in the text box 52 | autoPopulateCourseNameInModal() { 53 | 54 | let oldName = $('#old-course-name').html(); 55 | 56 | let exists = FalconStorage.existsInStorage(this.customCourseNames, 'oldName', oldName); 57 | // exists already! 58 | if (exists) { 59 | let course = this.customCourseNames.filter(item => { 60 | return item.oldName === oldName; 61 | })[0]; 62 | 63 | $('#custom-course-name-text').val(oldName === course.newName ? '' : course.newName); 64 | } 65 | 66 | } 67 | 68 | 69 | async saveCourseName(oldName, newName) { 70 | 71 | let customCourseNames = this.customCourseNames; 72 | 73 | if (!customCourseNames) { 74 | customCourseNames = []; 75 | } 76 | 77 | // no name exists... set the new name to old name 78 | if (!newName.trim()) { 79 | newName = oldName; 80 | } 81 | 82 | let exists = FalconStorage.existsInStorage(customCourseNames, 'oldName', oldName); 83 | 84 | if (exists) { 85 | customCourseNames.map(item => { 86 | if (item.oldName === oldName) { 87 | return item.newName = newName; 88 | } 89 | return item; 90 | }) 91 | } else { 92 | customCourseNames.push({oldName: oldName, newName: newName}) 93 | } 94 | 95 | await FalconStorage.sync().set({customCourseNames}); 96 | this.replaceDomCourseNamesWithNewNames(); 97 | } 98 | 99 | replaceDomCourseNamesWithNewNames() { 100 | 101 | let self = this; 102 | $('.link-container span').each(reflectCourseNameChanges); 103 | $('.fullTitle').each(reflectCourseNameChanges); 104 | 105 | function reflectCourseNameChanges() { 106 | let oldName = $(this).parent().attr('title'); 107 | 108 | let exists = FalconStorage.existsInStorage(self.customCourseNames, 'oldName', oldName); 109 | if (exists) { 110 | let course = self.customCourseNames.filter(item => { 111 | return item.oldName === oldName; 112 | })[0]; 113 | $(this).html(course.newName); 114 | } 115 | } 116 | } 117 | 118 | async getCourseNames() { 119 | FalconStorage.sync().get(this.STORAGE_KEY).then((data) => { 120 | let {customCourseNames} = data; 121 | if (customCourseNames && customCourseNames !== 'undefined') { 122 | this.customCourseNames = customCourseNames; 123 | this.replaceDomCourseNamesWithNewNames() 124 | } 125 | }); 126 | } 127 | 128 | } 129 | 130 | export default FalconCourseNameEditor; 131 | -------------------------------------------------------------------------------- /src/assets/scss/bootstrap/variables-dark.scss: -------------------------------------------------------------------------------- 1 | // 2 | // variables-dark.scss 3 | // Dashkit dark version 4 | // 5 | 6 | // 7 | // Bootstrap Overrides =================================== 8 | // 9 | 10 | 11 | // 12 | // Color system 13 | // 14 | 15 | $border-color: #393d3d; 16 | 17 | $gray-300: #E3EBF6 !default; 18 | $gray-600: #95AAC9 !default; 19 | $gray-700: #6E84A3 !default; 20 | $gray-900: #283E59 !default; 21 | $black: #12263F !default; 22 | 23 | $gray-600-dark: #244166 !default; 24 | $gray-700-dark: #1E3A5C !default; 25 | $gray-800-dark: #152E4D !default; 26 | $gray-900-dark: #132A46 !default; 27 | $black-dark: #12263F !default; 28 | 29 | $light: $gray-800-dark !default; 30 | $lighter: $gray-900-dark !default; 31 | 32 | 33 | // Body 34 | // 35 | // Settings for the `` element. 36 | 37 | $body-bg: $black-dark !default; 38 | $body-color: $white !default; 39 | 40 | 41 | // Components 42 | // 43 | // Define common padding and border radius sizes and more. 44 | 45 | // Fonts 46 | // 47 | // Font, line-height, and color for body text, headings, and more. 48 | 49 | $text-muted: $gray-700 !default; 50 | 51 | 52 | // Tables 53 | // 54 | // Customizes the `.table` component with basic values, each used across all table variations. 55 | 56 | $table-border-color: $border-color !default; 57 | 58 | $table-head-bg: $black-dark !default; 59 | 60 | 61 | // Forms 62 | 63 | $input-bg: $gray-700-dark !default; 64 | 65 | $input-color: $white !default; 66 | $input-border-color: $black-dark !default; 67 | 68 | $input-placeholder-color: $gray-600 !default; 69 | 70 | $custom-control-indicator-bg: $gray-600-dark !default; 71 | 72 | 73 | // Dropdowns 74 | // 75 | // Dropdown menu container and contents. 76 | 77 | $dropdown-bg: $gray-800-dark !default; 78 | $dropdown-border-color: $black !default; 79 | $dropdown-divider-bg: $black !default; 80 | 81 | $dropdown-link-color: $text-muted !default; 82 | $dropdown-link-hover-color: $white !default; 83 | 84 | 85 | // Navbar 86 | 87 | $navbar-dark-color: $gray-700 !default; 88 | $navbar-dark-hover-color: $black !default; 89 | $navbar-dark-active-color: $black !default; 90 | $navbar-dark-toggler-border-color: transparent !default; 91 | 92 | $navbar-dark-bg: $white !default; 93 | $navbar-dark-border-color: $white !default; 94 | $navbar-dark-heading-color: $text-muted !default; 95 | $navbar-dark-divider-color: $gray-300 !default; 96 | $navbar-dark-brand-filter: none; 97 | 98 | $navbar-dark-input-bg: $input-bg !default; 99 | $navbar-dark-input-border-color: $input-border-color !default; 100 | 101 | $navbar-light-hover-color: $white !default; 102 | $navbar-light-active-color: $white !default; 103 | $navbar-light-input-bg: $gray-700-dark !default; 104 | $navbar-light-input-border-color: $black-dark !default; 105 | 106 | 107 | // Pagination 108 | 109 | $pagination-color: $white !default; 110 | $pagination-bg: $gray-800-dark !default; 111 | $pagination-border-color: $gray-600-dark !default; 112 | 113 | $pagination-hover-color: $white !default; 114 | $pagination-hover-bg: $gray-900-dark !default; 115 | $pagination-hover-border-color: $gray-700-dark !default; 116 | 117 | $pagination-disabled-bg: $gray-900-dark !default; 118 | $pagination-disabled-border-color: $gray-700-dark !default; 119 | 120 | 121 | // Jumbotron 122 | 123 | $jumbotron-bg: $gray-800-dark !default; 124 | 125 | 126 | // Cards 127 | 128 | $card-bg: $gray-800-dark !default; 129 | $card-border-color: $border-color !default; 130 | 131 | 132 | // Tooltips 133 | 134 | $tooltip-bg: $gray-800-dark !default; 135 | $tooltip-color: $white !default; 136 | 137 | 138 | // Popovers 139 | 140 | $popover-bg: $gray-800-dark !default; 141 | $popover-border-color: $black !default; 142 | 143 | 144 | // Toasts 145 | 146 | $toast-background-color: $gray-800-dark !default;; 147 | 148 | 149 | // Modals 150 | 151 | $modal-content-bg: $gray-800-dark !default; 152 | $modal-content-border-color: $black !default; 153 | 154 | 155 | // Progress bars 156 | 157 | $progress-bg: $gray-600-dark !default; 158 | 159 | 160 | // 161 | // Dashkit ===================================== 162 | // 163 | 164 | // Auth 165 | 166 | $auth-bg: $body-bg !default; 167 | 168 | 169 | // Avatar 170 | 171 | $avatar-title-bg: $gray-600-dark !default; 172 | 173 | 174 | // Badges 175 | 176 | $badge-soft-bg-level: 10 !default; 177 | 178 | 179 | // Cards 180 | 181 | $card-outline-color: #393d3d !important; 182 | $card-box-shadow: 0 .75rem 1.5rem transparentize($black-dark, .5) !important; 183 | 184 | 185 | // Comment 186 | 187 | $comment-body-bg: $gray-700-dark; 188 | 189 | 190 | // Header 191 | 192 | $header-body-border-color-dark: $border-color !default; 193 | 194 | 195 | // Navbar 196 | 197 | $navbar-light-bg: $gray-800-dark !default; 198 | $navbar-light-border-color: $gray-800-dark !default; 199 | 200 | 201 | // Switch 202 | 203 | $custom-switch-indicator-bg: $gray-800-dark !default; 204 | $custom-switch-indicator-active-bg: $white !default; 205 | -------------------------------------------------------------------------------- /src/ui/asset-injector.js: -------------------------------------------------------------------------------- 1 | const AssetInjector = { 2 | 3 | getFalconAsset: false, 4 | injectToIframes: false, 5 | onlyOnce: false, 6 | appendToEndOfBody: false, 7 | 8 | falconAssets: function () { 9 | this.getFalconAsset = true; 10 | return this; 11 | }, 12 | 13 | owlAssets: function () { 14 | this.getFalconAsset = false; 15 | return this; 16 | }, 17 | 18 | getUrl: function (url) { 19 | if (this.getFalconAsset) { 20 | return chrome.runtime.getURL(url); 21 | } 22 | return url; 23 | }, 24 | 25 | // inject asset only once... MUST HAVE className to keep track of insertion 26 | once: function () { 27 | this.onlyOnce = true; 28 | return this; 29 | }, 30 | 31 | alsoToIframes: function () { 32 | this.injectToIframes = true; 33 | return this; 34 | }, 35 | 36 | injectScript: function (src, className = null) { 37 | let script = document.createElement('script'); 38 | script.src = this.getUrl(src); 39 | if (className) { 40 | script.className = className; 41 | } 42 | script.onload = function () { 43 | // this.remove(); 44 | }; 45 | 46 | // let parentElement = this.appendToEndOfBody ? document.body : (document.head || document.documentElement); 47 | 48 | if (this.onlyOnce) { 49 | if ($(`.${className}`).length === 0) { 50 | (document.head || document.documentElement).appendChild(script); 51 | } 52 | } else { 53 | (document.head || document.documentElement).appendChild(script); 54 | } 55 | 56 | return this; 57 | }, 58 | 59 | endOfBody() { 60 | this.appendToEndOfBody = true; 61 | return this; 62 | }, 63 | endOfHead() { 64 | this.appendToEndOfBody = false; 65 | return this; 66 | }, 67 | 68 | injectStyle: function (url, className = null) { 69 | 70 | let link = document.createElement('link'); 71 | link.setAttribute('rel', 'stylesheet'); 72 | link.href = this.getUrl(url); 73 | if (className) { 74 | link.className = className; 75 | } 76 | 77 | if (this.onlyOnce) { 78 | if ($(`.${className}`).length === 0) { 79 | document.body.appendChild(link.cloneNode(true)); 80 | } 81 | } else { 82 | document.body.appendChild(link.cloneNode(true)); 83 | } 84 | 85 | 86 | if (this.injectToIframes) { 87 | // $('.portletMainIframe').contents().find('body').append(link); 88 | // 89 | // if ($('.portletMainIframe').length > 0) { 90 | // $('iframe').on('load', function() { 91 | // $('.portletMainIframe').contents().find('body').append(link); 92 | // }) 93 | // } 94 | 95 | 96 | $('iframe').contents().find('body').append(link); 97 | 98 | if ( $('iframe').length > 0) { 99 | $('iframe').on('load', function() { 100 | $('iframe').contents().find('body').append(link); 101 | }) 102 | } 103 | 104 | 105 | // ensure that it has been injected... 106 | // this fixes the bug that doesn't enable dark mode after page switching from pjax 107 | // setTimeout(function () { 108 | // if ($('.portletMainIframe').length > 0) { 109 | // if ($('.portletMainIframe').contents().find('.falcon-dark-mode').length === 0) { 110 | // $('.portletMainIframe').contents().find('body').append(link); 111 | // } 112 | // } 113 | // }, 500) 114 | } 115 | 116 | 117 | return this; 118 | }, 119 | 120 | removeInjectionByUrl: function (url) { 121 | // find out if the URL is css or js 122 | let mimeType = url.split('.').pop(); 123 | 124 | if (mimeType === 'css') { 125 | $('link').find(`[href='${url}']`).remove(); 126 | } else if (mimeType === 'js') { 127 | $('script').find(`[src='${url}']`).remove(); 128 | } 129 | }, 130 | 131 | removeInjectionByClassName: function (className) { 132 | $(`.${className}`).remove(); 133 | 134 | if (this.injectToIframes) { 135 | $('.portletMainIframe').contents().find(`.${className}`).remove(); 136 | } 137 | return this; 138 | }, 139 | 140 | injectRawHtml: function (html, className = null) { 141 | if (this.onlyOnce) { 142 | if ($(`.${className}`).length === 0) { 143 | $(html).appendTo(document.head); 144 | return this; 145 | } 146 | } else { 147 | $(html).appendTo(document.head); 148 | return this; 149 | } 150 | 151 | return this; 152 | } 153 | 154 | 155 | } 156 | 157 | export default AssetInjector; 158 | -------------------------------------------------------------------------------- /src/assets/css/owl/lesson-builder-checklist.css: -------------------------------------------------------------------------------- 1 | .checklistForm { 2 | margin-left: 1em; 3 | } 4 | #checklistItems { 5 | display: inline-block; 6 | width: 90%; 7 | max-width: 500px; 8 | } 9 | #createdChecklistItems { 10 | margin-bottom: 1em; 11 | list-style: none; 12 | } 13 | #createdChecklistItems li { 14 | background: #eee; 15 | margin: 0 3px 3px 3px; 16 | padding: 0.2em 0.4em 0.2em 1.5em; 17 | font-size: 1.4em; 18 | } 19 | .handle { 20 | margin-left: -1em; 21 | } 22 | 23 | .checklist-icons { 24 | margin-top: .3em; 25 | } 26 | #createdChecklistItems li:hover { 27 | border-color: #0074D9; 28 | background-color: #ddd; 29 | } 30 | .checklist-item-name { 31 | line-height: 1.7em; 32 | width: 85%; 33 | max-width: 500px; 34 | } 35 | .checklistDescription { 36 | display: block; 37 | } 38 | #description:not(th) { 39 | height: 80px; 40 | width: 90%; 41 | max-width: 500px; 42 | } 43 | .actionContainer { 44 | float: right; 45 | } 46 | .deleteItemLink { 47 | margin-top: -3px; 48 | } 49 | 50 | ul#createdChecklistItems .handle, ul#createdChecklistItems .handle:hover { 51 | cursor: move; 52 | } 53 | 54 | #addChecklistItemButton { /** Have to use important because tool.css overrides **/ 55 | display: block !important; 56 | } 57 | #addChecklistItemButton:hover { /** Have to use important because tool.css overrides **/ 58 | display: block !important; 59 | } 60 | .titleBorder { 61 | border-bottom: 1px solid #ccc; 62 | } 63 | #name{ 64 | vertical-align: middle; 65 | } 66 | .checklistFormInput input, .checklistFormInput textarea { 67 | margin-left: 2em; 68 | margin-top: .5em; 69 | } 70 | .checklistFormInput label{ 71 | margin-left: 0; 72 | font-weight: bold; 73 | color: #555; 74 | font-size: 110%; 75 | } 76 | p.edit-group { 77 | margin-top: 2em; 78 | } 79 | .checklist-checkbox { 80 | /** Hide from sighted users **/ 81 | position: absolute; 82 | left:-10000px; 83 | top:auto; 84 | } 85 | .checklist-checkbox ~ span.checklist-checkbox-label { 86 | background-image: url("https://owl.uwo.ca/lessonbuilder-tool/images/gray_unchecked.png"); 87 | background-position: left center; 88 | background-repeat: no-repeat; 89 | } 90 | .checklist-checkbox:checked ~ span.checklist-checkbox-label { 91 | background-image: url("https://owl.uwo.ca/lessonbuilder-tool/images/green_checkmark.png"); 92 | background-position: left center; 93 | background-repeat: no-repeat; 94 | } 95 | .checklist-checkbox-label { 96 | padding: 0.5em 0.5em 0.5em 22px; 97 | } 98 | label.checklistLabel.disabled:hover input.checklist-checkbox ~ span.checklist-checkbox-label { 99 | background-image: none; 100 | cursor: not-allowed; 101 | } 102 | label.checklistLabel:hover .checklist-checkbox ~ span.checklist-checkbox-label { 103 | background-image: url("https://owl.uwo.ca/lessonbuilder-tool/images/gray_checkmark.png"); 104 | background-position: left center; 105 | background-repeat: no-repeat; 106 | cursor: pointer; 107 | } 108 | label.checklistLabel:hover .checklist-checkbox:checked ~ span.checklist-checkbox, label.checklistLabel:hover .checklist-checkbox:checked ~ span.checklist-checkbox-label { 109 | background-image: url("https://owl.uwo.ca/lessonbuilder-tool/images/green_checkmark.png"); 110 | } 111 | .checklistLabel { 112 | line-height: 2em; 113 | } 114 | .hideNameCheckbox { 115 | margin: 1em .5em 0 4em !important; 116 | } 117 | .hideNameCheckboxLabel { 118 | font-size: 100% !important; 119 | } 120 | h3.checklistTitle { 121 | display: inline-block; 122 | margin-top: 0; 123 | } 124 | legend.no-border { 125 | border: none; 126 | margin: 0; 127 | padding: 0; 128 | } 129 | #additionalSettings { 130 | margin: 1em 0; 131 | max-width: 600px; 132 | } 133 | /* Override the jquery ui optional settings stylings. */ 134 | .ui-accordion-header.ui-state-default.ui-state-active { 135 | border-color: #c5c5c5; 136 | background-color: #ededed; 137 | color: #212121; 138 | } 139 | 140 | .checklistList { 141 | display: inline; 142 | } 143 | 144 | .checklistList-item { 145 | display:flex; 146 | } 147 | 148 | .checklistList-radio { 149 | margin-right: 4px; 150 | } 151 | 152 | .external-link, .external-unlink { 153 | cursor: pointer; 154 | } 155 | 156 | .dialogs { 157 | display: none; 158 | } 159 | 160 | 161 | .checklistLabel.disabled:hover .externally-linked { 162 | margin-right: -19px; 163 | display: inline-block; 164 | } 165 | 166 | .tooltip-content { 167 | display: none; 168 | position: absolute; 169 | background-color: #fff; 170 | padding: .5em; 171 | border: 1px solid black; 172 | box-shadow: 2px 2px 1px rgba(0, 0, 0, 0.5); 173 | width: 20em; 174 | height: auto; 175 | line-height: normal; 176 | font-weight: normal; 177 | z-index: 900001; 178 | cursor: pointer; 179 | } 180 | 181 | span.externally-linked, label input.checklist-checkbox:checked + span.externally-linked { 182 | display: none; 183 | } 184 | 185 | h3.ui-accordion-header span:last-child { 186 | margin-left: 1.5em; 187 | } 188 | -------------------------------------------------------------------------------- /src/services/falcon.js: -------------------------------------------------------------------------------- 1 | import Pjax from "pjax"; 2 | import topbar from "topbar"; 3 | 4 | import TableSorter from "./table-sorter"; 5 | import $ from "jquery"; 6 | import FalconEditor from "./falcon-editor"; 7 | import 'bootstrap'; 8 | import FalconFileManager from "./file-manager"; 9 | import FalconInterfaceInjector from "../ui/ui-injector"; 10 | import FalconCourseNameEditor from "./course-name-editor"; 11 | import FalconDarkMode from "./dark-mode"; 12 | import AssetInjector from "../ui/asset-injector"; 13 | import FalconAssignments from "./falcon-assignments"; 14 | import FalconAnnouncement from "./announcement"; 15 | import FalconGradebook from "./gradebook"; 16 | 17 | let currentCourseId; // in format 39cbafa5-fa7b-4a18-8ca8-d7ae032c8de8 18 | let currentCourseName; 19 | let pjax; 20 | 21 | const Falcon = { 22 | 23 | start: () => { 24 | // loaded only once on page load (regular page load, no pjax) 25 | Falcon.onSuccess(false); 26 | 27 | pjax = new Pjax({ 28 | elements: "a[href]:not(.Mrphs-sitesNav__dropdown):not(#loginLink1):not(.nopjax):not([target=_blank]):not(.attachment), form[action]:not(#Mrphs-xlogin):not(#loginForm):not(#dfCompose):not(#takeAssessmentForm):not(#compose):not(#msgForum):not(#prefs_pvt_form):not(#selectIndexForm)", 29 | cacheBust: false, 30 | debug: false, 31 | selectors: [ 32 | "title", 33 | // "head", 34 | "meta[name=description]", 35 | ".Mrphs-pagebody", 36 | ".Mrphs-container--nav-tools", 37 | "#topnav_container", 38 | ], 39 | }); 40 | 41 | document.addEventListener("pjax:success", Falcon.onSuccess) 42 | document.addEventListener('pjax:send', Falcon.onSend) 43 | document.addEventListener('pjax:complete', Falcon.onComplete) 44 | 45 | Falcon.setupTopbar(); 46 | 47 | }, 48 | 49 | setupTopbar: () => { 50 | topbar.config({ 51 | autoRun: true, 52 | barThickness: 3, 53 | barColors: { 54 | '0': '#d900ff', 55 | '.3': '#7000ff', 56 | '1.0': '#d900ff' 57 | }, 58 | shadowBlur: 5, 59 | shadowColor: 'rgba(0, 0, 0, .5)', 60 | className: 'topbar', 61 | }); 62 | }, 63 | 64 | onSend: () => { 65 | topbar.show(); 66 | }, 67 | 68 | // @runOnInit = do not run it on init (i.e on a regular page load and NOT from pjax success page load) 69 | onSuccess: (runOnInit = true) => { 70 | 71 | if (runOnInit) { 72 | FalconInterfaceInjector.initAnimations(); 73 | } 74 | FalconInterfaceInjector.fixAssignmentLinks(); 75 | FalconInterfaceInjector.setActiveClassToNavigation(); 76 | FalconInterfaceInjector.replaceIcons(); 77 | FalconInterfaceInjector.announcementPaginationButtonsFix(); 78 | FalconInterfaceInjector.hideFavouritesBar(); 79 | 80 | // FalconInterfaceInjector.welcomeMessageAlert(); 81 | // FalconInterfaceInjector.falconAssignments(); 82 | 83 | Falcon.saveCourseId(); 84 | 85 | new TableSorter(); 86 | new FalconDarkMode(); 87 | new FalconCourseNameEditor(); 88 | new FalconEditor(currentCourseId); 89 | 90 | new FalconFileManager(currentCourseId, currentCourseName); 91 | new FalconAnnouncement(currentCourseId); 92 | new FalconGradebook(); 93 | 94 | new FalconAssignments(currentCourseId); 95 | Falcon.injectResources(); 96 | // Needed to initiate the matchParser 97 | 98 | 99 | }, 100 | 101 | onComplete: () => { 102 | topbar.hide(); 103 | }, 104 | 105 | saveCourseId() { 106 | let linkElement = $('.Mrphs-sitesNav__menuitem.is-selected a.link-container'); 107 | if (linkElement.length) { 108 | currentCourseId = linkElement.attr('href').split('/').pop(); 109 | currentCourseName = linkElement.attr('title'); 110 | } 111 | }, 112 | 113 | injectResources() { 114 | 115 | AssetInjector.once().owlAssets() 116 | .endOfHead() 117 | .injectScript('/library/js/spinner.js?version=20_2-owl1', 'owl-spinner-src') 118 | .injectScript('/sakai-assignment-tool/js/studentViewSubmission.js?version=20_2-owl1', 'owl-stdviewsubmission-src') 119 | .injectScript('/sakai-assignment-tool/js/assignments.js?version=20_2-owl1', 'owl-assignments-src') 120 | .injectScript('/library/js/mathjax/MathJax.js?config=default,Safe', 'falcon-mathjax') 121 | .injectScript('/gradebookng-tool/scripts/gradebook-grade-summary.js?version=20_2-owl1', 'falcon-gradebook-js') 122 | .injectStyle('/gradebookng-tool/styles/gradebook-grades.css?version=20_2-owl1', 'falcon-gradebook-css') 123 | .injectStyle('/gradebookng-tool/styles/gradebook-gbgrade-table.css?version=20_2-owl1', 'falcon-gradebook-table-css') 124 | .injectStyle('/messageforums-tool/css/msgcntr.css', 'falcon-forum-css') 125 | .injectRawHtml(``, 'falcon-math-jax-parser') 126 | .injectRawHtml(``, 'falcon-math-jax-config'); 127 | 128 | // MathJax Parser 129 | $(document).ready(function () { 130 | location.href = "javascript:parseMath(); void 0" 131 | }); 132 | } 133 | } 134 | 135 | export default Falcon; 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Falcon for OWL 2 | 3 | 4 | 5 | Falcon immensely enhances the user experience on OWL. With Falcon, OWL loads over **65% faster** on average. Falcon also improves the visual layout of OWL (including styling of buttons, forms, etc.) Falcon operates completely on the front-end and has no connection to the back-end. 6 | 7 | Falcon is available for install on Google Chrome, and is coming soon for Mozilla Firefox and Safari. 8 | 9 | 10 | 11 | 12 | # Features 13 | 14 | ### Dark Mode 15 | 16 | 17 | 18 | ----- 19 | 20 | ### Load pages without refreshing 21 | 22 | It will be much faster when you navigate OWL because a full refresh is not done every time you go to a new page. (This feature is disabled on Gradebook, Test/Quizzes, and Forum) 23 | 24 | 25 | 26 | ----- 27 | 28 | ### Revamped Resources page 29 | 30 | The new resources page is extremely fast and offers an instant file-browsing experience. You can also search for any files or folders instantly. No more full-page refresh just to browse folders. And if for any reason you need to access the old resources system, you are just one click away. 31 | 32 | 33 | 34 | ----- 35 | 36 | ### Edit course names 37 | 38 | Especially useful when you're just starting a new semester, and you're not used to the course codes yet. 39 | 40 | 41 | 42 | ----- 43 | 44 | ### New Announcements Experience 45 | 46 | Scroll through all your announcements at once like it's your personal feed. 47 | 48 | 49 | 50 | ----- 51 | 52 | ### Powerful New Editor 53 | 54 | Falcon includes a new editor which you can use to create flow charts, diagrams, and more. Each course gets its own editor, and changes are saved instantly. You can also export your diagrams as PNG, JPG or SVG. 55 | 56 | 57 | 58 | ----- 59 | 60 | ### Instant Search & Sorting of Announcements, Assignments, etc 61 | 62 | OWL offers sorting of announcements, assignments, etc. but it takes a *full* page refresh just to sort, making it an awful experience. Falcon sorts *instantly*. Falcon also allows you to search tables, too. 63 | 64 | 65 | 66 | ---- 67 | 68 | Of course, the features listed here are the main features you can visually interact with / see on OWL. There are many minor enhancements to OWL itself that Falcon makes that improves the user experience. One of which is a more modern and updated look without taking away from the OWL you are familiar with. 69 | 70 | # Installation 71 | Falcon is available on the Chrome Web Store for easy install. 72 | 73 | 1. Go to the releases page and download the latest version, and extract it 74 | 2. Go to `chrome://extensions` and toggle on `Developer Mode` on the top right 75 | 3. On the same page, you should now see three new buttons. Click on the `Load unpacked` button and select `dist/chrome` from the extracted directory 76 | 4. That's it! Falcon is now running! 77 | 78 | If there are any updates, then you will need to reinstall it manually. However, once it's available on the Chrome store, updating Falcon is just like updating any other Chrome extensions. 79 | 80 | # Security Notice 81 | Falcon was built with security in mind. Using Falcon does not compromise the security of OWL. In fact, it enhances certain security elements of OWL, particularly in the case of POST-based browsing. 82 | 83 | Falcon works on top of OWL, and it makes no external requests to any external endpoints (with the exception of asset files). Falcon also does not collect any inputs, form data, session data, or any other relevant information. You are free to inspect the source code! 84 | 85 | 86 | # Run into issues? 87 | If you encounter any issues that prevent you from using OWL normally, please disable or uninstall Falcon until it is fixed. 88 | 89 | If you run into any issues, then I'd really appreciate it if you could either create a new issue here or email me at swift@hey.com. 90 | 91 | 92 | # Contribution 93 | Feel free to fork Falcon and create a pull request! 94 | 95 | Contribution can be anything from fixing spelling mistakes to implementing new features. 96 | 97 | Some tips to help you get started: 98 | - Ensure you have node.js and NPM installed. 99 | - While developing, you will need to build it and load it in chrome manually. i.e. run `npm run dev` from the base directory. It will create a new `dist` directory with folders for `chrome` and `firefox`. 100 | - It may be cumbersome to type in `npm run dev` every time you make any changes. You can use the `npm run watch` command to automatically compile the source code upon any change. 101 | - Use Falcon's API for fluid development 102 | - `AssetInjector` for injecting resources to any page (including `iframes`) 103 | - `FalconStorage` for saving / querying user's data 104 | - `FalconInterfaceInjector` to save and inject elements on any page 105 | 106 | 107 | If you have any questions, ideas, or concerns, please send them over to swift@hey.com or create a new issue . 108 | 109 | 110 | # License 111 | 112 | See [LICENSE](/LICENSE.md) for more information. 113 | -------------------------------------------------------------------------------- /src/services/announcement.js: -------------------------------------------------------------------------------- 1 | import FalconInterfaceInjector from "../ui/ui-injector"; 2 | import Core from "./core"; 3 | import Falcon from "./falcon"; 4 | import FalconDarkMode from "./dark-mode"; 5 | 6 | class FalconAnnouncement { 7 | 8 | API_URL = 'https://owl.uwo.ca/direct/announcement/site/'; 9 | courseId; 10 | 11 | constructor(courseId) { 12 | this.courseId = courseId; 13 | 14 | // if currently on announcements page... and not on the user home page... 15 | if (FalconInterfaceInjector.pageContainsElement('form[name=announcementListForm]') && !FalconInterfaceInjector.urlContainsText('~')) { 16 | 17 | FalconInterfaceInjector.falconAnnouncementsSetup(); 18 | this.addEventListeners(); 19 | 20 | this.fetch(); 21 | } 22 | } 23 | 24 | // because we have set the announcement form to be hidden by default... fix it so it's not hidden in dark mode.. 25 | // and also fix the colors 26 | static fixDarkModeAnnouncement() { 27 | $('#falcon-announcements [style*="color"]').css('color', ''); 28 | $('#falcon-announcements [style*="background-color"]').css('background-color', ''); 29 | $('#falcon-announcements [style*="background"]').css('background-color', ''); 30 | $('#falcon-announcements [style*="border-bottom"]').css('border-color', '#393d3d'); 31 | $('#falcon-announcements [style*="border"]').css('border-color', '#393d3d'); 32 | } 33 | 34 | // both light and dark mode... 35 | static fixAnnouncement() { 36 | $('#falcon-announcements [style*="font-size"]').css('font-size', ''); 37 | $('#falcon-announcements [style*="font-family"]').css('font-family', ''); 38 | } 39 | 40 | addEventListeners() { 41 | 42 | let showOriginal = false; 43 | 44 | let ogAnnouncementsElem = $('form[name="announcementListForm"]'); 45 | 46 | $('#toggle-announcements-viewer').on('click', function () { 47 | if (showOriginal) { 48 | $('#falcon-announcements').fadeIn(); 49 | ogAnnouncementsElem.hide(); 50 | showOriginal = false; 51 | $(this).html('Classic Viewer'); 52 | 53 | } else { 54 | $('#falcon-announcements').hide(); 55 | ogAnnouncementsElem.fadeIn(); 56 | showOriginal = true; 57 | $(this).html('Falcon Viewer'); 58 | } 59 | }) 60 | } 61 | 62 | fetch() { 63 | let url = `${this.API_URL}/${this.courseId}.json?n=120`; 64 | 65 | fetch(url) 66 | .then(response => response.json()) 67 | .then(response => { 68 | this.setupAnnouncements(response.announcement_collection); 69 | }).then(() => { 70 | if (FalconDarkMode.isDarkModeEnabled()) { 71 | FalconAnnouncement.fixDarkModeAnnouncement(); 72 | } 73 | FalconAnnouncement.fixAnnouncement(); 74 | }) 75 | } 76 | 77 | setupAnnouncements(announcements) { 78 | let announcementElem = $('#falcon-announcements'); 79 | announcementElem.html(''); 80 | 81 | if (!announcements.length) { 82 | return; 83 | } 84 | for (let announcement of announcements) { 85 | this.insertAnnouncement(announcement); 86 | } 87 | 88 | 89 | // Fix the dark mode... 90 | } 91 | 92 | getAttachments(announcement) { 93 | 94 | let attachments = announcement.attachments; 95 | 96 | if (!attachments.length) { 97 | return []; 98 | } 99 | 100 | let allAttachments = []; 101 | for (let attachment of attachments) { 102 | allAttachments.push({ 103 | name: attachment.name, 104 | url: attachment.url, 105 | type: attachment.type, 106 | }); 107 | } 108 | 109 | return allAttachments; 110 | } 111 | 112 | renderAttachments(attachments) { 113 | if (!attachments.length) { 114 | return ""; 115 | } 116 | let html = '
'; 117 | for (let i = 0; i < attachments.length; i++) { 118 | let isImage = attachments[i].type.includes('image/'); 119 | let isPdf = attachments[i].type.includes('application/pdf'); 120 | let icon = isImage ? 'fe fe-image' : (isPdf ? 'fa fa-file-pdf-o' : 'fe fe-file'); 121 | html += ` ${attachments[i].name}` 122 | 123 | if (i !== attachments.length - 1) { 124 | html += `·` 125 | } 126 | } 127 | html += "
"; 128 | return html; 129 | } 130 | 131 | insertAnnouncement(announcement) { 132 | 133 | let attachments = this.getAttachments(announcement); 134 | let attachmentElem = this.renderAttachments(attachments); 135 | 136 | let time = new Date(announcement.createdOn); 137 | let announcementElem = $('#falcon-announcements'); 138 | $(` 139 | 140 |
141 |
142 |
143 |

${announcement.entityTitle}

144 | ${Core.timeAgo(time)} - ${Core.formatDateTime(time)} · Posted by ${announcement.createdByDisplayName} 145 |
146 |
147 |
148 | ${announcement.body} 149 | ${attachmentElem} 150 |
151 |
152 | `).appendTo(announcementElem) 153 | } 154 | 155 | } 156 | 157 | export default FalconAnnouncement; 158 | -------------------------------------------------------------------------------- /src/services/falcon-editor.js: -------------------------------------------------------------------------------- 1 | import FalconStorage from "./storage"; 2 | import FalconInterfaceInjector from "../ui/ui-injector"; 3 | import config from "devextreme/core/config"; 4 | import Diagram from "devextreme/ui/diagram"; 5 | 6 | class FalconEditor { 7 | 8 | STORAGE_KEY = 'falconEditor'; 9 | uiEditor; 10 | falconEditor = []; // array of all diagrams from all courses 11 | currentCourse; 12 | 13 | constructor(currentCourse) { 14 | this.currentCourse = currentCourse; 15 | FalconInterfaceInjector.falconEditorButton(); 16 | this.addEventListeners(); 17 | } 18 | 19 | addEventListeners() { 20 | let isOpen = false; 21 | let self = this; 22 | 23 | if (!isOpen) { 24 | cleanupEditor(); 25 | } 26 | 27 | $('#falcon-editor-button').on('click', function () { 28 | $('#falcon-editor-icon').addClass('fa-spin fe-loader').removeClass('fe-edit-2 fe-x'); 29 | if (isOpen) { 30 | $('#falcon-editor-title').html('Saving...'); 31 | self.saveFalconEdits(self.uiEditor.export()).then(item => { 32 | $('#falcon-editor-title').html('Saved!'); 33 | }) 34 | } 35 | 36 | // Async to prevent from the slight freeze up 37 | setTimeout(function () { 38 | isOpen = !isOpen; 39 | if (isOpen) { 40 | $('.Mrphs-pagebody').hide(); 41 | $('a.Mrphs-toolsNav__menuitem--link').not('#falcon-editor-button').parent().hide(); 42 | $('#falcon-editor-title').html('Close Editor'); 43 | $('#falcon-editor-icon').removeClass('fa-spin fe-loader fe-edit-2').addClass('fe-x') 44 | $('#pageBody').append(`
`) 45 | self.setupEditor(); 46 | self.getEditorDataFromStorage(); 47 | } else { 48 | cleanupEditor(); 49 | } 50 | }, 0) 51 | }) 52 | 53 | function cleanupEditor() { 54 | $('#falcon-editor-title').html('Falcon Editor'); 55 | $('a.Mrphs-toolsNav__menuitem--link').not('#falcon-editor-button').parent().show(); 56 | $('#falcon-editor-icon').removeClass('fa-spin fe-loader fe-x').addClass('fe-edit-2') 57 | $('#falcon-editor-diagram').remove(); 58 | $('.Mrphs-pagebody').show(); 59 | } 60 | } 61 | 62 | setupEditor() { 63 | let autoSaveIntervalMs = 2000; 64 | let autoSaveTimeout = -1; 65 | 66 | let self = this; 67 | 68 | this.uiEditor = new Diagram(document.getElementById("falcon-editor-diagram"), { 69 | 70 | "height": function () { 71 | return window.innerHeight / 1.1; 72 | }, 73 | "nodes": { 74 | "autoLayout": { 75 | "orientation": "horizontal" 76 | } 77 | }, 78 | propertiesPanel: { 79 | visibility: 'disabled', 80 | }, 81 | "simpleView": true, 82 | "toolbox": { 83 | "visible": true 84 | }, 85 | historyToolbar: { 86 | visible: false 87 | }, 88 | mainToolbar: { 89 | visible: true, 90 | }, 91 | 92 | onOptionChanged: function (e) { 93 | if (e.name === "hasChanges" && e.value && autoSaveTimeout === -1) { 94 | 95 | autoSaveTimeout = setTimeout(function () { 96 | let data = e.component.export(); 97 | console.log(data); 98 | autoSaveTimeout = -1; 99 | self.saveFalconEdits(data).then(function () { 100 | e.component.option("hasChanges", false); 101 | }); 102 | 103 | }, autoSaveIntervalMs); 104 | } 105 | }, 106 | 107 | "width": '100%' 108 | }); 109 | } 110 | 111 | 112 | saveFalconEdits(data) { 113 | let exists = FalconStorage.existsInStorage(this.falconEditor, 'courseName', this.currentCourse); 114 | if (exists) { 115 | this.falconEditor.map((item => { 116 | if (item.courseName === this.currentCourse) { 117 | return item.diagramData = data; 118 | } 119 | return item; 120 | })); 121 | } else { 122 | this.falconEditor.push({ 123 | courseName: this.currentCourse, 124 | diagramData: data 125 | }) 126 | } 127 | 128 | return FalconStorage.set({falconEditor: this.falconEditor}) 129 | } 130 | 131 | async getEditorDataFromStorage() { 132 | let {falconEditor} = await FalconStorage.local().get(this.STORAGE_KEY); 133 | this.falconEditor = falconEditor; 134 | 135 | // if there's no data... it means that it's a new user visiting the falcon editor 136 | // populate it with default data and show them the welcome page 137 | // It's NOT saved in storage (because it's dummy data) 138 | if (!falconEditor) { 139 | this.falconEditor = []; 140 | this.importDataToEditor(this.defaultData()); 141 | return; 142 | } 143 | 144 | let exists = FalconStorage.existsInStorage(falconEditor, 'courseName', this.currentCourse); 145 | if (exists) { 146 | let falconEdit = falconEditor.filter(item => { 147 | return item.courseName === this.currentCourse; 148 | })[0]; 149 | this.importDataToEditor(falconEdit.diagramData); 150 | } 151 | } 152 | 153 | importDataToEditor(data) { 154 | this.uiEditor.import(data); 155 | } 156 | 157 | // Default falcon editor data 158 | defaultData() { 159 | return `{"page":{"width":16782,"height":23812,"pageColor":-1,"pageWidth":8391,"pageHeight":11906,"pageLandscape":false},"connectors":[{"key":"20","locked":false,"zIndex":0,"points":[{"x":9540,"y":10620},{"x":10980,"y":10620}],"style":{"stroke":"#e86048"},"beginItemKey":"6","beginConnectionPointIndex":1,"endItemKey":"5","endConnectionPointIndex":3},{"key":"21","locked":false,"zIndex":0,"points":[{"x":12960,"y":10620},{"x":14040,"y":10620}],"style":{"stroke":"#2d47b8"},"beginItemKey":"5","beginConnectionPointIndex":1,"endItemKey":"14","endConnectionPointIndex":3},{"key":"23","locked":false,"zIndex":0,"points":[{"x":11610,"y":16200},{"x":11610,"y":17100}],"style":{"stroke":"#fd513a"},"beginItemKey":"13","beginConnectionPointIndex":-1,"endItemKey":"3","endConnectionPointIndex":-1}],"shapes":[{"key":"3","dataKey":"d054a3b3-9b51-45a9-96cb-5b31e958c513","locked":false,"zIndex":0,"type":"heart","text":"Good\\nLuck ;)","x":9540,"y":16560,"width":4140,"height":3600,"style":{"fill":"#ce8898","stroke":"#ffffff"},"styleText":{"fill":"#a6414b","font-family":"Helvetica","font-size":"22pt","font-weight":"bold"}},{"key":"4","dataKey":"c33421dc-650b-4ace-89de-b0c9048d4e02","locked":false,"zIndex":0,"type":"rectangle","text":"Falcon Editor","x":7062,"y":8486,"width":9000,"height":900,"style":{"fill":"#6400ff","stroke":"#ffffff"},"styleText":{"fill":"#ffffff","font-family":"Helvetica","font-size":"28pt","font-weight":"bold"}},{"key":"5","dataKey":"d97730d1-3f1a-45b5-9b75-651fa3c90327","locked":false,"zIndex":0,"type":"ellipse","text":"Draw diagrams","x":10980,"y":9720,"width":1980.0000000000002,"height":1800,"style":{"fill":"#2d47b8","stroke":"#ffffff"},"styleText":{"fill":"#9ceee0","font-family":"Helvetica","font-size":"14pt","font-weight":"bold"}},{"key":"6","dataKey":"0463af6f-c987-4f2d-8069-9318cec6efd5","locked":false,"zIndex":0,"type":"ellipse","text":"Draw flow-charts","x":7560,"y":9720,"width":1980,"height":1800,"style":{"fill":"#e86048","stroke":"#ffffff"},"styleText":{"fill":"#d2efcf","font-family":"Helvetica","font-size":"14pt","font-weight":"bold"}},{"key":"13","locked":false,"zIndex":0,"type":"rectangle","text":"","x":7560,"y":11880,"width":8100,"height":4320,"style":{"fill":"#fd513a","stroke":"#ffffff"}},{"key":"14","locked":false,"zIndex":0,"type":"ellipse","text":"Brainstorm ideas","x":14040,"y":9720,"width":1980.0000000000002,"height":1800,"style":{"fill":"#34db6a","stroke":"#ffffff"},"styleText":{"fill":"#126551","font-family":"Helvetica","font-size":"14pt","font-weight":"bold"}},{"key":"16","locked":false,"zIndex":0,"type":"rectangle","text":"Each course has its own editor","x":8100,"y":12240,"width":7020,"height":1080,"style":{"fill":"#c9f1cd","stroke":"#c9f1cd"},"styleText":{"fill":"#fd513a","font-family":"Helvetica","font-size":"18pt","font-weight":"bold"}},{"key":"17","locked":false,"zIndex":0,"type":"rectangle","text":"You can export your diagrams as PNG, JPG and SVG","x":8100,"y":13500,"width":7020,"height":1080,"style":{"fill":"#cef563","stroke":"#cef563"},"styleText":{"fill":"#4202f5","font-family":"Helvetica","font-size":"18pt","font-weight":"bold"}},{"key":"18","locked":false,"zIndex":0,"type":"rectangle","text":"To get started, select all these elements and hit delete!","x":8100,"y":14760,"width":7020,"height":1080,"style":{"fill":"#c6e3c5","stroke":"#c6e3c5"},"styleText":{"fill":"#126551","font-family":"Helvetica","font-size":"18pt","font-weight":"bold"}}]}`; 160 | } 161 | 162 | } 163 | 164 | export default FalconEditor; 165 | -------------------------------------------------------------------------------- /src/ui/ui-injector.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | 3 | const FalconInterfaceInjector = { 4 | 5 | darkModeButton: () => { 6 | let $dark_mode_button = ``; 7 | if ($('#dark-mode-toggle').length === 0) { 8 | $(".Mrphs-loginNav").prepend($dark_mode_button); 9 | } 10 | }, 11 | 12 | falconEditorButton: () => { 13 | $('.Mrphs-toolsNav__menu ul').append(`
  • Falcon Editor
  • `) 14 | }, 15 | 16 | welcomeMessageAlert: () => { 17 | $('.Mrphs-pagebody').prepend(` 18 |
    19 |

    Falcon is installed!

    20 | 21 |

    Thanks for installing Falcon!

    22 |

    Remember, Falcon does not know who you are, see your courses, or track how you use Falcon.

    23 |

    It is open-source , so you can provide feedback and even contribute!

    24 |
    25 |

    Default Settings

    26 |

    Falcon has many great features, all of which are enabled by default. Select the ones you'd like enabled.

    27 | 28 | 29 |

    30 | 31 |
    32 |
    33 | 34 |
    35 | 36 |
    37 | 38 |
    39 | 40 |

    41 | 42 |

    You can change these settings anytime from the top right corner: [Your Name] -> Falcon Settings

    43 | 44 |
    45 | `); 46 | }, 47 | 48 | 49 | courseEditElements: () => { 50 | // only add this if user is logged in... 51 | // if it's not logged in, this element doesn't exist. 52 | if ($('#loginUser').length !== 1) { 53 | return; 54 | } 55 | let $courseNameEditModal = `` 78 | if (!$('#edit-course-title-modal').length) { 79 | $(` Edit`).insertBefore('.toolMenus') 80 | $('body').append($courseNameEditModal); 81 | } 82 | }, 83 | 84 | 85 | falconResources: () => { 86 | if ($('.page-header h1').html() === 'Site Resources') { 87 | $(`
    `).insertBefore($('.page-header h1')); 88 | $(`
    `).insertAfter($('.page-header')); 89 | $('#showForm').hide(); 90 | $(`

    Loading resources...

    `).insertAfter('.page-header'); 91 | } 92 | }, 93 | 94 | // add is-selected to selected site... 95 | setActiveClassToNavigation: () => { 96 | $('.link-container').on('click', function () { 97 | $('.Mrphs-sitesNav__menuitem').removeClass('is-selected'); 98 | $(this).parent().addClass('is-selected'); 99 | }) 100 | }, 101 | 102 | falconAssignments: () => { 103 | // $('#listAssignmentsForm') 104 | let originalAssignmentForm = $('[name=listAssignmentsForm]'); 105 | 106 | if (!originalAssignmentForm) { 107 | return; 108 | } 109 | 110 | let assignmentToolbar = originalAssignmentForm.siblings('.sakai-table-toolBar'); 111 | 112 | if (!assignmentToolbar) { 113 | return; 114 | } 115 | 116 | $(`

    Loading assignments...

    `).insertBefore(assignmentToolbar); 117 | }, 118 | 119 | initAnimations: () => { 120 | $('.Mrphs-pagebody').addClass('animate__animated animate__fadeIn'); 121 | $('#selectSiteModal').addClass('animate__animated animate__fadeIn'); 122 | $('.Mrphs-userNav__subnav').addClass('animate__animated animate__fadeIn'); 123 | }, 124 | 125 | // replace all font-awesome icons with feather-icons 126 | replaceIcons() { 127 | let icons = { 128 | 'icon-sakai--sakai-iframe-site': 'fe fe-list', 129 | 'icon-sakai--sakai-syllabus': 'fe fe-map', 130 | 'icon-sakai--sakai-lessonbuildertool': 'fe fe-book', 131 | 'icon-sakai--sakai-samigo': 'fe fe-check-circle', 132 | 'fa fa-video-camera': 'fe fe-video', 133 | 'fa fa-play': 'fe fe-play', 134 | 'icon-sakai--sakai-siteinfo': 'fe fe-info', 135 | 'icon-sakai--sakai-gradebookng': 'fe fe-grid', 136 | 'icon-sakai--sakai-schedule': 'fe fe-calendar', 137 | 'icon-sakai--sakai-resources': 'fe fe-folder-plus', 138 | 'icon-sakai--sakai-web-168': 'fe fe-globe', 139 | 'icon-sakai--sakai-researchguideslti': 'fe fe-globe', 140 | 'icon-sakai--sakai-basiclti': 'fe fe-globe', 141 | 'icon-sakai--sakai-poll': 'fe fe-bar-chart-2', 142 | 'icon-sakai--sakai-messages': 'fe fe-inbox', 143 | 'icon-sakai--help': 'fe fe-help-circle', 144 | 'icon-sakai--sakai-assignment-grades': 'fe fe-file-text', 145 | 'icon-sakai--sakai-forums': 'fe fe-message-circle', 146 | 'fa fa-eye': 'fe fe-eye', 147 | 'fa fa-home': 'fe fe-home', 148 | 'icon-sakai--sakai-iframe': 'fe fe-globe', 149 | 'icon-sakai--sakai-signup': 'fe fe-user-plus', 150 | 'icon-sakai--sakai-dropbox': 'fe fe-upload-cloud', 151 | 'icon-sakai--sakai-singleuser': 'fe fe-user', 152 | 'icon-sakai--sakai-preferences': 'fe fe-settings', 153 | 'icon-sakai--sakai-sitesetup': 'fe fe-sliders', 154 | 'icon-sakai--sakai-membership': 'fe fe-users', 155 | 'icon-sakai--sakai-motd': 'fe fe-list', 156 | 'fa fa-print': 'fe fe-printer', 157 | 'fa fa-list-ul': 'fe fe-list', 158 | 'Mrphs-toolTitleNav__link--directurl': 'fe fe-link', 159 | 'Mrphs-toolTitleNav__link--help-popup': 'fe fe-help-circle', 160 | 'icon-sakai--sakai-resetpass': 'fe fe-more-horizontal', 161 | 'icon-sakai--sakai-sitebrowser': 'fe fe-globe' 162 | } 163 | 164 | // elements to check for and replace icons in 165 | let elements = [ 166 | '.Mrphs-toolsNav__menuitem--icon', 167 | '.Mrphs-breadcrumb--icon', 168 | '.toolMenuIcon', 169 | ] 170 | 171 | elements.forEach(element => { 172 | let all = $(element); 173 | if (all.length > 0) { 174 | all.each(function () { 175 | 176 | Object.keys(icons).forEach(oldIcon => { 177 | if ($(this).hasClass(oldIcon)) { 178 | $(this).removeClass(oldIcon); 179 | $(this).addClass(icons[oldIcon]); 180 | } 181 | }) 182 | }) 183 | 184 | } 185 | }) 186 | 187 | // share button 188 | $('.Mrphs-breadcrumb--reset-icon').removeClass('fa fa-share').addClass('fe fe-arrow-left'); 189 | }, 190 | 191 | announcementPaginationButtonsFix() { 192 | let order = [0, 1, 2]; 193 | // ANNOUNCEMENTS click on next, previous, or return to list button. 194 | $('input[name="eventSubmit_doPrev_message"]').on('click', function () { 195 | order = [0, 1, 2]; 196 | switchUp(); 197 | }) 198 | 199 | $('input[name="eventSubmit_doLinkcancel"]').on('click', function () { 200 | order = [1, 0, 2]; 201 | switchUp(); 202 | }) 203 | 204 | $('input[name="eventSubmit_doNext_message"]').on('click', function () { 205 | order = [2, 0, 1]; 206 | switchUp(); 207 | }) 208 | 209 | function switchUp() { 210 | var wrapper = $('.itemNav'), 211 | items = wrapper.children('input'); 212 | wrapper.append($.map(order, function (v) { 213 | return items[v] 214 | })); 215 | wrapper.hide(); 216 | } 217 | }, 218 | 219 | falconAnnouncementsSetup() { 220 | $(`

    Loading Announcements...

    `).insertAfter($('.page-header')); 221 | $(``).insertBefore($('.page-header h1')); 222 | this.removeToolbar(); 223 | this.hideElement($('form[name="announcementListForm"]')) 224 | }, 225 | 226 | removeToolbar() { 227 | this.removeElement($('.sakai-table-toolBar')); 228 | }, 229 | 230 | removeElement(element) { 231 | $(element).remove(); 232 | }, 233 | 234 | hideElement(element) { 235 | $(element).hide(); 236 | }, 237 | 238 | showElement(element) { 239 | $(element).hide(); 240 | }, 241 | 242 | // if favourites bar is open and user clicks on anything which navigates away from a page, it should close 243 | hideFavouritesBar() { 244 | if (!$('#selectSiteModal').hasClass('outscreen')) { 245 | $('.view-all-sites-btn').click(); 246 | } 247 | }, 248 | 249 | // Fix attachments from opening in the same page... 250 | fixAssignmentLinks() { 251 | if ($('ul.attachList li a').length === 0) { 252 | return; 253 | } 254 | 255 | $('ul.attachList li a').each(function() { 256 | $(this).addClass('nopjax'); 257 | }) 258 | }, 259 | 260 | // determine if the current page title in UI matches 261 | isCurrentPageTitle(title, elem = '.page-header h1') { 262 | return $(elem).text().includes(title); 263 | }, 264 | 265 | pageContainsElement(elem) { 266 | return !! $(elem).length; 267 | }, 268 | 269 | urlContainsText(text) { 270 | return window.location.href.indexOf(text) > -1; 271 | } 272 | 273 | 274 | } 275 | 276 | export default FalconInterfaceInjector; 277 | -------------------------------------------------------------------------------- /src/services/file-manager.js: -------------------------------------------------------------------------------- 1 | import FalconStorage from "./storage"; 2 | import FalconInterfaceInjector from "../ui/ui-injector"; 3 | import FileManager from "devextreme/ui/file_manager"; 4 | import TextBox from "devextreme/ui/text_box"; 5 | import PopUp from "devextreme/ui/popup"; 6 | 7 | class FalconFileManager { 8 | 9 | API_URL = 'https://owl.uwo.ca/direct/content/site/'; 10 | CACHE_TTL = 10 * 60 * 1000; // 10 minutes 11 | STORAGE_KEY = 'falconResources'; 12 | 13 | forceRecache = false; 14 | falconResources = []; // all resources from all courses 15 | 16 | searchString = ''; 17 | 18 | falconFileManager; 19 | 20 | constructor(courseId, courseName) { 21 | this.currentCourse = courseName; // The original Course Name 22 | this.courseId = courseId; // current course ID in format 39cbafa5-fa7b-4a18-8ca8-d7ae032c8de8 23 | 24 | FalconInterfaceInjector.falconResources(); 25 | this.addEventListeners(); 26 | } 27 | 28 | addEventListeners() { 29 | let showOriginal = false; 30 | let self = this; 31 | 32 | $('#toggle-original-resource').on('click', function () { 33 | if (showOriginal) { 34 | $('#showForm').hide(); 35 | $('#file-manager-container').fadeIn(); 36 | showOriginal = false; 37 | $(this).html('Classic Viewer'); 38 | 39 | } else { 40 | $('#showForm').fadeIn(); 41 | $('#file-manager-container').hide(); 42 | showOriginal = true; 43 | $(this).html('Falcon Viewer'); 44 | } 45 | }) 46 | 47 | setTimeout(function () { 48 | self.setupResources().then(() => { 49 | $('#loading-resources').remove(); 50 | }); 51 | }, 0) 52 | 53 | } 54 | 55 | forceRefresh() { 56 | this.forceRecache = true; 57 | return this; 58 | } 59 | 60 | 61 | async setupResources() { 62 | 63 | let result = await this.getResourcesForCourse(); 64 | 65 | function searchResources(searchText) { 66 | const localData = [...result]; 67 | 68 | function getValueLogic(result, searchText) { 69 | searchText = searchText.toLowerCase(); 70 | const arr = []; 71 | if (result && Array.isArray(result)) { 72 | for (let i = 0; i < result.length; i++) { 73 | const ele = result[i]; 74 | ele && ele.name.toLowerCase().includes(searchText) 75 | ? arr.push(ele) 76 | : arr.push(...getValueLogic(ele.items, searchText)); 77 | } 78 | } 79 | return arr; 80 | } 81 | 82 | return getValueLogic(localData, searchText); 83 | } 84 | 85 | // make sure we have the element 86 | if ($('#file-manager').length === 0) { 87 | return; 88 | } 89 | 90 | var popup = new PopUp('#photo-popup'); 91 | 92 | let self = this; 93 | 94 | let falconFileManager = new FileManager(document.getElementById('file-manager'), { 95 | name: "fileManager", 96 | fileSystemProvider: result, 97 | rootFolderName: "Falcon", 98 | selectionMode: "single", 99 | currentPath: this.currentCourse, 100 | contextMenu: { 101 | items: [ 102 | { 103 | text: 'Download', 104 | icon: 'download', 105 | }, 106 | { 107 | name: "refresh", 108 | beginGroup: true 109 | } 110 | ] 111 | }, 112 | height: function () { 113 | return window.innerHeight / 1.5; 114 | }, 115 | permissions: { 116 | download: false, 117 | }, 118 | onSelectedFileOpened: function (e) { 119 | if (isImage(e.file.dataItem.mimeType)) { 120 | popup.option({ 121 | "title": e.file.name, 122 | "contentTemplate": `
    Open in New Tab
    `, 123 | }); 124 | 125 | popup.show(); 126 | } else { 127 | openFileInNewTab(e.file.dataItem.url); 128 | } 129 | }, 130 | onContextMenuItemClick: onItemClick, 131 | toolbar: { 132 | items: [ 133 | { 134 | name: "showNavPane", 135 | visible: true 136 | }, 137 | 'refresh', 'separator', 138 | 'switchView', 139 | ], 140 | 141 | 142 | }, 143 | itemView: { 144 | details: { 145 | columns: [ 146 | "thumbnail", 147 | "name", 148 | { 149 | dataField: "size", 150 | caption: "Size", 151 | width: 'auto', 152 | }, 153 | { 154 | dataField: "modified_at", 155 | caption: "Last Modified", 156 | width: 'auto', 157 | dataType: 'date', 158 | 159 | }, 160 | { 161 | dataField: "created_by", 162 | caption: "Created By", 163 | width: 'auto', 164 | }, 165 | 166 | 167 | ] 168 | } 169 | }, 170 | }); 171 | 172 | new TextBox('#file-manager-search', { 173 | placeholder: 'Search...', 174 | width: '300px', 175 | showClearButton: true, 176 | valueChangeEvent: "keyup", 177 | onValueChanged: function (event) { 178 | 179 | 180 | let searchResult; 181 | if (event.value === '' || !event.value) { 182 | searchResult = result; 183 | } else { 184 | self.searchString = event.value; 185 | searchResult = searchResources(self.searchString); 186 | } 187 | setTimeout(function () { 188 | falconFileManager.option('fileSystemProvider', searchResult); 189 | }, 500); 190 | }, 191 | }) 192 | 193 | // Whenever an item is (double) clicked, download it 194 | function onItemClick(args) { 195 | let url = args.fileSystemItem.dataItem.url; 196 | // they wanna download 197 | if (args.itemData.text === 'Download') { 198 | if (url) { 199 | download(url); 200 | } 201 | } 202 | } 203 | 204 | function openFileInNewTab(url) { 205 | window.open(url, '_blank'); 206 | } 207 | 208 | function isImage(mimeType) { 209 | return mimeType.split('/')[0] === 'image'; //returns true or false 210 | } 211 | 212 | function download(url) { 213 | let a = document.createElement("a"); 214 | a.href = url; 215 | a.download = ''; 216 | a.click(); 217 | } 218 | 219 | // https://stackoverflow.com/questions/1909441/how-to-delay-the-keyup-handler-until-the-user-stops-typing 220 | function delay(callback, ms) { 221 | var timer = 0; 222 | return function () { 223 | var context = this, args = arguments; 224 | clearTimeout(timer); 225 | timer = setTimeout(function () { 226 | callback.apply(context, args); 227 | }, ms || 0); 228 | }; 229 | } 230 | 231 | } 232 | 233 | // Get resources 234 | async getResourcesForCourse() { 235 | 236 | let falconResources = await this.getResources(); 237 | 238 | let data; 239 | 240 | // if we don't have any resources saved... or we want to RE-CACHE it 241 | if (!falconResources || this.forceRecache) { 242 | this.forceRecache = false; 243 | data = await this.fetchResourcesForCourse(); 244 | return data; 245 | } 246 | 247 | let exists = FalconStorage.existsInStorage(falconResources, 'courseId', this.courseId); 248 | 249 | if (!exists) { 250 | data = await this.fetchResourcesForCourse(); 251 | return data; 252 | } 253 | 254 | // if exists 255 | let falconResource = falconResources.filter(item => { 256 | return item.courseId === this.courseId; 257 | })[0]; 258 | 259 | // make sure it's not older than TTL... otherwise re-fetch 260 | if (this.isExpired(falconResource)) { 261 | data = await this.fetchResourcesForCourse(); 262 | return data; 263 | } 264 | 265 | return falconResource.courses; 266 | 267 | } 268 | 269 | async getResources() { 270 | // first check the local storage... 271 | let {falconResources} = await FalconStorage.local().get(this.STORAGE_KEY) 272 | 273 | // if it doesn't exist at all... then we haven't set anything in the local storage 274 | if (!falconResources) { 275 | return false; 276 | } 277 | 278 | return falconResources; 279 | } 280 | 281 | async saveResources(data) { 282 | 283 | // let {falconResources} = await this.getResources(); 284 | // 285 | 286 | let currentTimeInMillis = (new Date()).getTime(); 287 | let exists = FalconStorage.existsInStorage(this.falconResources, 'courseId', this.courseId); 288 | 289 | // if it exists... update it.. 290 | if (exists) { 291 | this.falconResources.map(item => { 292 | if (item.courseId === this.courseId) { 293 | item.courses = data; 294 | item.lastFetched = currentTimeInMillis; 295 | return item; 296 | } 297 | return item; 298 | }) 299 | } else { 300 | // otherwise, add a new record 301 | this.falconResources.push({ 302 | courseName: this.currentCourse, 303 | courseId: this.courseId, 304 | courses: data, 305 | lastFetched: currentTimeInMillis 306 | }) 307 | } 308 | 309 | // and save it in storage 310 | return FalconStorage.local().set({falconResources: this.falconResources}); 311 | } 312 | 313 | isExpired(falconResource) { 314 | return new Date() - new Date(falconResource.lastFetched) > this.CACHE_TTL 315 | } 316 | 317 | async fetchResourcesForCourse() { 318 | let file; 319 | await fetch(this.API_URL + this.courseId + '.json') 320 | .then(response => response.json()) 321 | .then(json => { 322 | file = json; 323 | }); 324 | 325 | if (file) { 326 | let data = this.parseRawFileDataIntoTree(file.content_collection); 327 | await this.saveResources(data); 328 | return data; 329 | } 330 | } 331 | 332 | // save the cache of the file manager... 333 | saveCache() { 334 | 335 | } 336 | 337 | parseRawFileDataIntoTree(rawFileStructure) { 338 | 339 | // final file structure result 340 | let result = []; 341 | 342 | let level = {result}; 343 | 344 | rawFileStructure.forEach(file => { 345 | let rawFilePath; 346 | if (file.type === 'collection') { 347 | rawFilePath = decodeURIComponent(file.url.slice(0, -1)).split('/'); 348 | } else { 349 | rawFilePath = decodeURIComponent(file.url).split('/'); 350 | } 351 | rawFilePath = rawFilePath.splice(6); 352 | rawFilePath[0] = this.currentCourse; 353 | rawFilePath = rawFilePath.join('/'); 354 | 355 | let dateString = file.modifiedDate.substr(0, 8); 356 | let year = dateString.substring(0, 4); 357 | let month = dateString.substring(4, 6); 358 | let day = dateString.substring(6, 8); 359 | let date = new Date(year, month - 1, day).toLocaleString('en-us', {month: 'long', year: 'numeric', day: 'numeric'}); 360 | 361 | let path = rawFilePath; 362 | 363 | let sharedDataToShow = { //TODO Fix 364 | modified_at: date, 365 | created_by: file.author, 366 | } 367 | 368 | path.split('/').reduce((r, name, i, a) => { 369 | if (!r[name]) { 370 | r[name] = {result: []}; 371 | if (file.type === 'collection') { 372 | r.result.push({name, isDirectory: true, modified_at: date, size: file.size, created_by: file.author, items: r[name].result}) 373 | } else { 374 | r.result.push({name, url: file.url, mimeType: file.type, modified_at: date, created_by: file.author, size: file.size, items: r[name].result}) 375 | 376 | } 377 | } 378 | return r[name]; 379 | }, level) 380 | }) 381 | 382 | return result; 383 | } 384 | 385 | 386 | } 387 | 388 | export default FalconFileManager; 389 | -------------------------------------------------------------------------------- /src/assets/fonts/feather.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Feather'; 3 | src: 4 | url('#{$urlPath}/fonts/Feather.ttf?sdxovp') format('truetype'), 5 | url('#{$urlPath}/fonts/Feather.woff?sdxovp') format('woff'), 6 | url('#{$urlPath}/fonts/Feather.svg?sdxovp#Feather') format('svg'); 7 | font-weight: normal; 8 | font-style: normal; 9 | } 10 | 11 | .fe { 12 | /* use !important to prevent issues with browser extensions that change fonts */ 13 | font-family: 'Feather' !important; 14 | speak: none; 15 | font-style: normal; 16 | font-weight: normal; 17 | font-variant: normal; 18 | text-transform: none; 19 | line-height: 1; 20 | 21 | /* Better Font Rendering =========== */ 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | } 25 | 26 | .fe-activity:before { 27 | content: "\e900"; 28 | } 29 | .fe-airplay:before { 30 | content: "\e901"; 31 | } 32 | .fe-alert-circle:before { 33 | content: "\e902"; 34 | } 35 | .fe-alert-octagon:before { 36 | content: "\e903"; 37 | } 38 | .fe-alert-triangle:before { 39 | content: "\e904"; 40 | } 41 | .fe-align-center:before { 42 | content: "\e905"; 43 | } 44 | .fe-align-justify:before { 45 | content: "\e906"; 46 | } 47 | .fe-align-left:before { 48 | content: "\e907"; 49 | } 50 | .fe-align-right:before { 51 | content: "\e908"; 52 | } 53 | .fe-anchor:before { 54 | content: "\e909"; 55 | } 56 | .fe-aperture:before { 57 | content: "\e90a"; 58 | } 59 | .fe-archive:before { 60 | content: "\e90b"; 61 | } 62 | .fe-arrow-down:before { 63 | content: "\e90c"; 64 | } 65 | .fe-arrow-down-circle:before { 66 | content: "\e90d"; 67 | } 68 | .fe-arrow-down-left:before { 69 | content: "\e90e"; 70 | } 71 | .fe-arrow-down-right:before { 72 | content: "\e90f"; 73 | } 74 | .fe-arrow-left:before { 75 | content: "\e910"; 76 | } 77 | .fe-arrow-left-circle:before { 78 | content: "\e911"; 79 | } 80 | .fe-arrow-right:before { 81 | content: "\e912"; 82 | } 83 | .fe-arrow-right-circle:before { 84 | content: "\e913"; 85 | } 86 | .fe-arrow-up:before { 87 | content: "\e914"; 88 | } 89 | .fe-arrow-up-circle:before { 90 | content: "\e915"; 91 | } 92 | .fe-arrow-up-left:before { 93 | content: "\e916"; 94 | } 95 | .fe-arrow-up-right:before { 96 | content: "\e917"; 97 | } 98 | .fe-at-sign:before { 99 | content: "\e918"; 100 | } 101 | .fe-award:before { 102 | content: "\e919"; 103 | } 104 | .fe-bar-chart:before { 105 | content: "\e91a"; 106 | } 107 | .fe-bar-chart-2:before { 108 | content: "\e91b"; 109 | } 110 | .fe-battery:before { 111 | content: "\e91c"; 112 | } 113 | .fe-battery-charging:before { 114 | content: "\e91d"; 115 | } 116 | .fe-bell:before { 117 | content: "\e91e"; 118 | } 119 | .fe-bell-off:before { 120 | content: "\e91f"; 121 | } 122 | .fe-bluetooth:before { 123 | content: "\e920"; 124 | } 125 | .fe-bold:before { 126 | content: "\e921"; 127 | } 128 | .fe-book:before { 129 | content: "\e922"; 130 | } 131 | .fe-book-open:before { 132 | content: "\e923"; 133 | } 134 | .fe-bookmark:before { 135 | content: "\e924"; 136 | } 137 | .fe-box:before { 138 | content: "\e925"; 139 | } 140 | .fe-briefcase:before { 141 | content: "\e926"; 142 | } 143 | .fe-calendar:before { 144 | content: "\e927"; 145 | } 146 | .fe-camera:before { 147 | content: "\e928"; 148 | } 149 | .fe-camera-off:before { 150 | content: "\e929"; 151 | } 152 | .fe-cast:before { 153 | content: "\e92a"; 154 | } 155 | .fe-check:before { 156 | content: "\e92b"; 157 | } 158 | .fe-check-circle:before { 159 | content: "\e92c"; 160 | } 161 | .fe-check-square:before { 162 | content: "\e92d"; 163 | } 164 | .fe-chevron-down:before { 165 | content: "\e92e"; 166 | } 167 | .fe-chevron-left:before { 168 | content: "\e92f"; 169 | } 170 | .fe-chevron-right:before { 171 | content: "\e930"; 172 | } 173 | .fe-chevron-up:before { 174 | content: "\e931"; 175 | } 176 | .fe-chevrons-down:before { 177 | content: "\e932"; 178 | } 179 | .fe-chevrons-left:before { 180 | content: "\e933"; 181 | } 182 | .fe-chevrons-right:before { 183 | content: "\e934"; 184 | } 185 | .fe-chevrons-up:before { 186 | content: "\e935"; 187 | } 188 | .fe-chrome:before { 189 | content: "\e936"; 190 | } 191 | .fe-circle:before { 192 | content: "\e937"; 193 | } 194 | .fe-clipboard:before { 195 | content: "\e938"; 196 | } 197 | .fe-clock:before { 198 | content: "\e939"; 199 | } 200 | .fe-cloud:before { 201 | content: "\e93a"; 202 | } 203 | .fe-cloud-drizzle:before { 204 | content: "\e93b"; 205 | } 206 | .fe-cloud-lightning:before { 207 | content: "\e93c"; 208 | } 209 | .fe-cloud-off:before { 210 | content: "\e93d"; 211 | } 212 | .fe-cloud-rain:before { 213 | content: "\e93e"; 214 | } 215 | .fe-cloud-snow:before { 216 | content: "\e93f"; 217 | } 218 | .fe-code:before { 219 | content: "\e940"; 220 | } 221 | .fe-codepen:before { 222 | content: "\e941"; 223 | } 224 | .fe-command:before { 225 | content: "\e942"; 226 | } 227 | .fe-compass:before { 228 | content: "\e943"; 229 | } 230 | .fe-copy:before { 231 | content: "\e944"; 232 | } 233 | .fe-corner-down-left:before { 234 | content: "\e945"; 235 | } 236 | .fe-corner-down-right:before { 237 | content: "\e946"; 238 | } 239 | .fe-corner-left-down:before { 240 | content: "\e947"; 241 | } 242 | .fe-corner-left-up:before { 243 | content: "\e948"; 244 | } 245 | .fe-corner-right-down:before { 246 | content: "\e949"; 247 | } 248 | .fe-corner-right-up:before { 249 | content: "\e94a"; 250 | } 251 | .fe-corner-up-left:before { 252 | content: "\e94b"; 253 | } 254 | .fe-corner-up-right:before { 255 | content: "\e94c"; 256 | } 257 | .fe-cpu:before { 258 | content: "\e94d"; 259 | } 260 | .fe-credit-card:before { 261 | content: "\e94e"; 262 | } 263 | .fe-crop:before { 264 | content: "\e94f"; 265 | } 266 | .fe-crosshair:before { 267 | content: "\e950"; 268 | } 269 | .fe-database:before { 270 | content: "\e951"; 271 | } 272 | .fe-delete:before { 273 | content: "\e952"; 274 | } 275 | .fe-disc:before { 276 | content: "\e953"; 277 | } 278 | .fe-dollar-sign:before { 279 | content: "\e954"; 280 | } 281 | .fe-download:before { 282 | content: "\e955"; 283 | } 284 | .fe-download-cloud:before { 285 | content: "\e956"; 286 | } 287 | .fe-droplet:before { 288 | content: "\e957"; 289 | } 290 | .fe-edit:before { 291 | content: "\e958"; 292 | } 293 | .fe-edit-2:before { 294 | content: "\e959"; 295 | } 296 | .fe-edit-3:before { 297 | content: "\e95a"; 298 | } 299 | .fe-external-link:before { 300 | content: "\e95b"; 301 | } 302 | .fe-eye:before { 303 | content: "\e95c"; 304 | } 305 | .fe-eye-off:before { 306 | content: "\e95d"; 307 | } 308 | .fe-facebook:before { 309 | content: "\e95e"; 310 | } 311 | .fe-fast-forward:before { 312 | content: "\e95f"; 313 | } 314 | .fe-feather:before { 315 | content: "\e960"; 316 | } 317 | .fe-file:before { 318 | content: "\e961"; 319 | } 320 | .fe-file-minus:before { 321 | content: "\e962"; 322 | } 323 | .fe-file-plus:before { 324 | content: "\e963"; 325 | } 326 | .fe-file-text:before { 327 | content: "\e964"; 328 | } 329 | .fe-film:before { 330 | content: "\e965"; 331 | } 332 | .fe-filter:before { 333 | content: "\e966"; 334 | } 335 | .fe-flag:before { 336 | content: "\e967"; 337 | } 338 | .fe-folder:before { 339 | content: "\e968"; 340 | } 341 | .fe-folder-minus:before { 342 | content: "\e969"; 343 | } 344 | .fe-folder-plus:before { 345 | content: "\e96a"; 346 | } 347 | .fe-gift:before { 348 | content: "\e96b"; 349 | } 350 | .fe-git-branch:before { 351 | content: "\e96c"; 352 | } 353 | .fe-git-commit:before { 354 | content: "\e96d"; 355 | } 356 | .fe-git-merge:before { 357 | content: "\e96e"; 358 | } 359 | .fe-git-pull-request:before { 360 | content: "\e96f"; 361 | } 362 | .fe-github:before { 363 | content: "\e970"; 364 | } 365 | .fe-gitlab:before { 366 | content: "\e971"; 367 | } 368 | .fe-globe:before { 369 | content: "\e972"; 370 | } 371 | .fe-grid:before { 372 | content: "\e973"; 373 | } 374 | .fe-hard-drive:before { 375 | content: "\e974"; 376 | } 377 | .fe-hash:before { 378 | content: "\e975"; 379 | } 380 | .fe-headphones:before { 381 | content: "\e976"; 382 | } 383 | .fe-heart:before { 384 | content: "\e977"; 385 | } 386 | .fe-help-circle:before { 387 | content: "\e978"; 388 | } 389 | .fe-home:before { 390 | content: "\e979"; 391 | } 392 | .fe-image:before { 393 | content: "\e97a"; 394 | } 395 | .fe-inbox:before { 396 | content: "\e97b"; 397 | } 398 | .fe-info:before { 399 | content: "\e97c"; 400 | } 401 | .fe-instagram:before { 402 | content: "\e97d"; 403 | } 404 | .fe-italic:before { 405 | content: "\e97e"; 406 | } 407 | .fe-layers:before { 408 | content: "\e97f"; 409 | } 410 | .fe-layout:before { 411 | content: "\e980"; 412 | } 413 | .fe-life-buoy:before { 414 | content: "\e981"; 415 | } 416 | .fe-link:before { 417 | content: "\e982"; 418 | } 419 | .fe-link-2:before { 420 | content: "\e983"; 421 | } 422 | .fe-linkedin:before { 423 | content: "\e984"; 424 | } 425 | .fe-list:before { 426 | content: "\e985"; 427 | } 428 | .fe-loader:before { 429 | content: "\e986"; 430 | } 431 | .fe-lock:before { 432 | content: "\e987"; 433 | } 434 | .fe-log-in:before { 435 | content: "\e988"; 436 | } 437 | .fe-log-out:before { 438 | content: "\e989"; 439 | } 440 | .fe-mail:before { 441 | content: "\e98a"; 442 | } 443 | .fe-map:before { 444 | content: "\e98b"; 445 | } 446 | .fe-map-pin:before { 447 | content: "\e98c"; 448 | } 449 | .fe-maximize:before { 450 | content: "\e98d"; 451 | } 452 | .fe-maximize-2:before { 453 | content: "\e98e"; 454 | } 455 | .fe-menu:before { 456 | content: "\e98f"; 457 | } 458 | .fe-message-circle:before { 459 | content: "\e990"; 460 | } 461 | .fe-message-square:before { 462 | content: "\e991"; 463 | } 464 | .fe-mic:before { 465 | content: "\e992"; 466 | } 467 | .fe-mic-off:before { 468 | content: "\e993"; 469 | } 470 | .fe-minimize:before { 471 | content: "\e994"; 472 | } 473 | .fe-minimize-2:before { 474 | content: "\e995"; 475 | } 476 | .fe-minus:before { 477 | content: "\e996"; 478 | } 479 | .fe-minus-circle:before { 480 | content: "\e997"; 481 | } 482 | .fe-minus-square:before { 483 | content: "\e998"; 484 | } 485 | .fe-monitor:before { 486 | content: "\e999"; 487 | } 488 | .fe-moon:before { 489 | content: "\e99a"; 490 | } 491 | .fe-more-horizontal:before { 492 | content: "\e99b"; 493 | } 494 | .fe-more-vertical:before { 495 | content: "\e99c"; 496 | } 497 | .fe-move:before { 498 | content: "\e99d"; 499 | } 500 | .fe-music:before { 501 | content: "\e99e"; 502 | } 503 | .fe-navigation:before { 504 | content: "\e99f"; 505 | } 506 | .fe-navigation-2:before { 507 | content: "\e9a0"; 508 | } 509 | .fe-octagon:before { 510 | content: "\e9a1"; 511 | } 512 | .fe-package:before { 513 | content: "\e9a2"; 514 | } 515 | .fe-paperclip:before { 516 | content: "\e9a3"; 517 | } 518 | .fe-pause:before { 519 | content: "\e9a4"; 520 | } 521 | .fe-pause-circle:before { 522 | content: "\e9a5"; 523 | } 524 | .fe-percent:before { 525 | content: "\e9a6"; 526 | } 527 | .fe-phone:before { 528 | content: "\e9a7"; 529 | } 530 | .fe-phone-call:before { 531 | content: "\e9a8"; 532 | } 533 | .fe-phone-forwarded:before { 534 | content: "\e9a9"; 535 | } 536 | .fe-phone-incoming:before { 537 | content: "\e9aa"; 538 | } 539 | .fe-phone-missed:before { 540 | content: "\e9ab"; 541 | } 542 | .fe-phone-off:before { 543 | content: "\e9ac"; 544 | } 545 | .fe-phone-outgoing:before { 546 | content: "\e9ad"; 547 | } 548 | .fe-pie-chart:before { 549 | content: "\e9ae"; 550 | } 551 | .fe-play:before { 552 | content: "\e9af"; 553 | } 554 | .fe-play-circle:before { 555 | content: "\e9b0"; 556 | } 557 | .fe-plus:before { 558 | content: "\e9b1"; 559 | } 560 | .fe-plus-circle:before { 561 | content: "\e9b2"; 562 | } 563 | .fe-plus-square:before { 564 | content: "\e9b3"; 565 | } 566 | .fe-pocket:before { 567 | content: "\e9b4"; 568 | } 569 | .fe-power:before { 570 | content: "\e9b5"; 571 | } 572 | .fe-printer:before { 573 | content: "\e9b6"; 574 | } 575 | .fe-radio:before { 576 | content: "\e9b7"; 577 | } 578 | .fe-refresh-ccw:before { 579 | content: "\e9b8"; 580 | } 581 | .fe-refresh-cw:before { 582 | content: "\e9b9"; 583 | } 584 | .fe-repeat:before { 585 | content: "\e9ba"; 586 | } 587 | .fe-rewind:before { 588 | content: "\e9bb"; 589 | } 590 | .fe-rotate-ccw:before { 591 | content: "\e9bc"; 592 | } 593 | .fe-rotate-cw:before { 594 | content: "\e9bd"; 595 | } 596 | .fe-rss:before { 597 | content: "\e9be"; 598 | } 599 | .fe-save:before { 600 | content: "\e9bf"; 601 | } 602 | .fe-scissors:before { 603 | content: "\e9c0"; 604 | } 605 | .fe-search:before { 606 | content: "\e9c1"; 607 | } 608 | .fe-send:before { 609 | content: "\e9c2"; 610 | } 611 | .fe-server:before { 612 | content: "\e9c3"; 613 | } 614 | .fe-settings:before { 615 | content: "\e9c4"; 616 | } 617 | .fe-share:before { 618 | content: "\e9c5"; 619 | } 620 | .fe-share-2:before { 621 | content: "\e9c6"; 622 | } 623 | .fe-shield:before { 624 | content: "\e9c7"; 625 | } 626 | .fe-shield-off:before { 627 | content: "\e9c8"; 628 | } 629 | .fe-shopping-bag:before { 630 | content: "\e9c9"; 631 | } 632 | .fe-shopping-cart:before { 633 | content: "\e9ca"; 634 | } 635 | .fe-shuffle:before { 636 | content: "\e9cb"; 637 | } 638 | .fe-sidebar:before { 639 | content: "\e9cc"; 640 | } 641 | .fe-skip-back:before { 642 | content: "\e9cd"; 643 | } 644 | .fe-skip-forward:before { 645 | content: "\e9ce"; 646 | } 647 | .fe-slack:before { 648 | content: "\e9cf"; 649 | } 650 | .fe-slash:before { 651 | content: "\e9d0"; 652 | } 653 | .fe-sliders:before { 654 | content: "\e9d1"; 655 | } 656 | .fe-smartphone:before { 657 | content: "\e9d2"; 658 | } 659 | .fe-speaker:before { 660 | content: "\e9d3"; 661 | } 662 | .fe-square:before { 663 | content: "\e9d4"; 664 | } 665 | .fe-star:before { 666 | content: "\e9d5"; 667 | } 668 | .fe-stop-circle:before { 669 | content: "\e9d6"; 670 | } 671 | .fe-sun:before { 672 | content: "\e9d7"; 673 | } 674 | .fe-sunrise:before { 675 | content: "\e9d8"; 676 | } 677 | .fe-sunset:before { 678 | content: "\e9d9"; 679 | } 680 | .fe-tablet:before { 681 | content: "\e9da"; 682 | } 683 | .fe-tag:before { 684 | content: "\e9db"; 685 | } 686 | .fe-target:before { 687 | content: "\e9dc"; 688 | } 689 | .fe-terminal:before { 690 | content: "\e9dd"; 691 | } 692 | .fe-thermometer:before { 693 | content: "\e9de"; 694 | } 695 | .fe-thumbs-down:before { 696 | content: "\e9df"; 697 | } 698 | .fe-thumbs-up:before { 699 | content: "\e9e0"; 700 | } 701 | .fe-toggle-left:before { 702 | content: "\e9e1"; 703 | } 704 | .fe-toggle-right:before { 705 | content: "\e9e2"; 706 | } 707 | .fe-trash:before { 708 | content: "\e9e3"; 709 | } 710 | .fe-trash-2:before { 711 | content: "\e9e4"; 712 | } 713 | .fe-trending-down:before { 714 | content: "\e9e5"; 715 | } 716 | .fe-trending-up:before { 717 | content: "\e9e6"; 718 | } 719 | .fe-triangle:before { 720 | content: "\e9e7"; 721 | } 722 | .fe-truck:before { 723 | content: "\e9e8"; 724 | } 725 | .fe-tv:before { 726 | content: "\e9e9"; 727 | } 728 | .fe-twitter:before { 729 | content: "\e9ea"; 730 | } 731 | .fe-type:before { 732 | content: "\e9eb"; 733 | } 734 | .fe-umbrella:before { 735 | content: "\e9ec"; 736 | } 737 | .fe-underline:before { 738 | content: "\e9ed"; 739 | } 740 | .fe-unlock:before { 741 | content: "\e9ee"; 742 | } 743 | .fe-upload:before { 744 | content: "\e9ef"; 745 | } 746 | .fe-upload-cloud:before { 747 | content: "\e9f0"; 748 | } 749 | .fe-user:before { 750 | content: "\e9f1"; 751 | } 752 | .fe-user-check:before { 753 | content: "\e9f2"; 754 | } 755 | .fe-user-minus:before { 756 | content: "\e9f3"; 757 | } 758 | .fe-user-plus:before { 759 | content: "\e9f4"; 760 | } 761 | .fe-user-x:before { 762 | content: "\e9f5"; 763 | } 764 | .fe-users:before { 765 | content: "\e9f6"; 766 | } 767 | .fe-video:before { 768 | content: "\e9f7"; 769 | } 770 | .fe-video-off:before { 771 | content: "\e9f8"; 772 | } 773 | .fe-voicemail:before { 774 | content: "\e9f9"; 775 | } 776 | .fe-volume:before { 777 | content: "\e9fa"; 778 | } 779 | .fe-volume-1:before { 780 | content: "\e9fb"; 781 | } 782 | .fe-volume-2:before { 783 | content: "\e9fc"; 784 | } 785 | .fe-volume-x:before { 786 | content: "\e9fd"; 787 | } 788 | .fe-watch:before { 789 | content: "\e9fe"; 790 | } 791 | .fe-wifi:before { 792 | content: "\e9ff"; 793 | } 794 | .fe-wifi-off:before { 795 | content: "\ea00"; 796 | } 797 | .fe-wind:before { 798 | content: "\ea01"; 799 | } 800 | .fe-x:before { 801 | content: "\ea02"; 802 | } 803 | .fe-x-circle:before { 804 | content: "\ea03"; 805 | } 806 | .fe-x-square:before { 807 | content: "\ea04"; 808 | } 809 | .fe-youtube:before { 810 | content: "\ea05"; 811 | } 812 | .fe-zap:before { 813 | content: "\ea06"; 814 | } 815 | .fe-zap-off:before { 816 | content: "\ea07"; 817 | } 818 | .fe-zoom-in:before { 819 | content: "\ea08"; 820 | } 821 | .fe-zoom-out:before { 822 | content: "\ea09"; 823 | } 824 | -------------------------------------------------------------------------------- /src/assets/css/dist/dx-diagram.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * DevExpress Diagram (dx-diagram) 3 | * Version: 2.0.23 4 | * Build date: Mon Apr 19 2021 5 | * 6 | * Copyright (c) 2012 - 2021 Developer Express Inc. ALL RIGHTS RESERVED 7 | * Read about DevExpress licensing here: https://www.devexpress.com/Support/EULAs 8 | */ 9 | .dxdi-control { 10 | overflow: visible; 11 | box-sizing: border-box; 12 | position: relative; } 13 | .dxdi-control.dxdi-read-only .dxdi-canvas.dxdi-drag-scroll { 14 | cursor: grab !important; } 15 | .dxdi-control.dxdi-read-only .dxdi-canvas.dxdi-drag-scroll .shape, 16 | .dxdi-control.dxdi-read-only .dxdi-canvas.dxdi-drag-scroll .shape-expand-btn, 17 | .dxdi-control.dxdi-read-only .dxdi-canvas.dxdi-drag-scroll text { 18 | cursor: grab; } 19 | .dxdi-control.dxdi-read-only .dxdi-canvas .shape, 20 | .dxdi-control.dxdi-read-only .dxdi-canvas .shape-expand-btn, 21 | .dxdi-control.dxdi-read-only .dxdi-canvas text { 22 | cursor: default; } 23 | .dxdi-control .dxdi-canvas { 24 | display: block; 25 | background-color: #d9d9d9; 26 | transform-origin: 0 0; 27 | overflow: hidden; 28 | /* Fix excess scroll size in IE */ } 29 | 30 | .dxdi-canvas.dxdi-drag-scroll { 31 | cursor: grab !important; } 32 | .dxdi-canvas.dxdi-drag-scroll .shape, 33 | .dxdi-canvas.dxdi-drag-scroll .shape .shape-expand-btn, 34 | .dxdi-canvas.dxdi-drag-scroll .connector, 35 | .dxdi-canvas.dxdi-drag-scroll .connection-point, 36 | .dxdi-canvas.dxdi-drag-scroll .connector text, 37 | .dxdi-canvas.dxdi-drag-scroll .connector-side-mark.vertical, 38 | .dxdi-canvas.dxdi-drag-scroll .connector-side-mark.horizontal, 39 | .dxdi-canvas.dxdi-drag-scroll .selection-mark[data-type="9"][data-value="1"], 40 | .dxdi-canvas.dxdi-drag-scroll .selection-mark[data-type="9"][data-value="2"], 41 | .dxdi-canvas.dxdi-drag-scroll .selection-mark[data-type="9"][data-value="3"], 42 | .dxdi-canvas.dxdi-drag-scroll .selection-mark[data-type="9"][data-value="4"], 43 | .dxdi-canvas.dxdi-drag-scroll .selection-mark[data-type="9"][data-value="5"], 44 | .dxdi-canvas.dxdi-drag-scroll .selection-mark[data-type="9"][data-value="6"], 45 | .dxdi-canvas.dxdi-drag-scroll .selection-mark[data-type="9"][data-value="7"], 46 | .dxdi-canvas.dxdi-drag-scroll .selection-mark[data-type="9"][data-value="8"], 47 | .dxdi-canvas.dxdi-drag-scroll .selection-mark[data-type="4"], 48 | .dxdi-canvas.dxdi-drag-scroll .selection-mark[data-type="5"] { 49 | cursor: grab; } 50 | 51 | .dxdi-canvas * { 52 | user-select: none; } 53 | 54 | .dxdi-canvas text { 55 | font-family: arial, helvetica, sans-serif; 56 | font-size: 10pt; } 57 | 58 | .dxdi-canvas .page { 59 | fill: white; } 60 | 61 | .dxdi-canvas .pages-grid-line { 62 | fill: none; 63 | stroke: rgba(0, 0, 0, 0.15); 64 | stroke-dasharray: 8; 65 | stroke-width: 2; } 66 | 67 | .dxdi-canvas .grid-outer-line, 68 | .dxdi-canvas .grid-inner-line { 69 | fill: none; } 70 | 71 | .dxdi-canvas .grid-outer-line { 72 | stroke: rgba(0, 0, 0, 0.1); } 73 | 74 | .dxdi-canvas .grid-inner-line { 75 | stroke: rgba(0, 0, 0, 0.05); } 76 | 77 | .dxdi-canvas .shape, 78 | .dxdi-canvas .toolbox-item { 79 | pointer-events: bounding-box; } 80 | .dxdi-canvas .shape rect, 81 | .dxdi-canvas .shape path, 82 | .dxdi-canvas .shape line, 83 | .dxdi-canvas .shape ellipse, 84 | .dxdi-canvas .toolbox-item rect, 85 | .dxdi-canvas .toolbox-item path, 86 | .dxdi-canvas .toolbox-item line, 87 | .dxdi-canvas .toolbox-item ellipse { 88 | fill: white; 89 | stroke-width: 2; 90 | stroke: black; } 91 | .dxdi-canvas .shape text, 92 | .dxdi-canvas .toolbox-item text { 93 | fill: black; 94 | text-anchor: middle; } 95 | .dxdi-canvas .shape rect.selector, 96 | .dxdi-canvas .toolbox-item rect.selector { 97 | stroke-width: 48; 98 | stroke: transparent; 99 | fill: transparent; 100 | pointer-events: initial; } 101 | 102 | .dxdi-canvas .shape.not-valid rect, 103 | .dxdi-canvas .shape.not-valid path, 104 | .dxdi-canvas .shape.not-valid line, 105 | .dxdi-canvas .shape.not-valid ellipse { 106 | stroke: red; } 107 | 108 | .dxdi-canvas .shape { 109 | cursor: move; } 110 | .dxdi-canvas .shape.text-input > text { 111 | display: none; } 112 | .dxdi-canvas .shape.container > rect:first-child { 113 | fill: none; } 114 | .dxdi-canvas .shape .shape-expand-btn { 115 | cursor: pointer; } 116 | .dxdi-canvas .shape .shape-expand-btn > rect, 117 | .dxdi-canvas .shape .shape-expand-btn > path { 118 | stroke-dasharray: initial !important; } 119 | .dxdi-canvas .shape .shape-expand-btn > path { 120 | stroke-width: 2 !important; } 121 | .dxdi-canvas .shape .dxdi-image .dxdi-spinner { 122 | animation: loading-spinner 1s linear infinite; } 123 | .dxdi-canvas .shape .dxdi-image .dxdi-spinner ellipse { 124 | stroke: black; 125 | stroke-opacity: 0.2; } 126 | .dxdi-canvas .shape .dxdi-image .dxdi-spinner path { 127 | stroke: #fd7010; 128 | stroke-linecap: round; } 129 | .dxdi-canvas .shape .dxdi-image .dxdi-spinner ellipse, 130 | .dxdi-canvas .shape .dxdi-image .dxdi-spinner path { 131 | fill: none; 132 | stroke-width: 5; } 133 | 134 | @keyframes loading-spinner { 135 | from { 136 | transform: rotate(0deg); } 137 | to { 138 | transform: rotate(360deg); } } 139 | .dxdi-canvas .shape .dxdi-image .dxdi-user .dxdi-background { 140 | fill: black; 141 | opacity: 0.2; 142 | stroke: none; } 143 | .dxdi-canvas .shape .dxdi-image .dxdi-user ellipse, 144 | .dxdi-canvas .shape .dxdi-image .dxdi-user path { 145 | fill: white; 146 | stroke: none; } 147 | .dxdi-canvas .shape .dxdi-image .dxdi-warning ellipse { 148 | stroke: none; 149 | fill: #ee1616; } 150 | .dxdi-canvas .shape .dxdi-image .dxdi-warning rect { 151 | stroke: none; 152 | fill: white; } 153 | 154 | .dxdi-canvas .shape.locked, 155 | .dxdi-canvas .shape.locked + .container-children .shape { 156 | cursor: inherit !important; } 157 | .dxdi-canvas .shape.locked .shape-expand-btn, 158 | .dxdi-canvas .shape.locked + .container-children .shape .shape-expand-btn { 159 | cursor: inherit !important; } 160 | 161 | .dxdi-canvas .container-children .shape .selector { 162 | stroke-width: 8; } 163 | 164 | .dxdi-canvas .toolbox-item { 165 | cursor: pointer; } 166 | .dxdi-canvas .toolbox-item .selector { 167 | stroke-width: 0 !important; } 168 | 169 | .dxdi-canvas .connector path, 170 | .dxdi-canvas .connector line { 171 | stroke-width: 2; 172 | stroke: black; 173 | stroke-linejoin: round; 174 | pointer-events: stroke; } 175 | 176 | .dxdi-canvas .connector path:not(.outlined-line-ending):not(.filled-line-ending) { 177 | fill: none !important; } 178 | 179 | .dxdi-canvas .connector path.outlined-line-ending { 180 | fill: white; } 181 | 182 | .dxdi-canvas .connector path.filled-line-ending { 183 | fill: black; } 184 | 185 | .dxdi-canvas .connector path.selector, 186 | .dxdi-canvas .connector line.selector { 187 | stroke-width: 16; 188 | stroke: transparent; } 189 | 190 | .dxdi-canvas .connector text { 191 | cursor: move; 192 | fill: black; 193 | text-anchor: middle; } 194 | 195 | .dxdi-canvas .connector .text-filter-flood { 196 | flood-color: white; } 197 | 198 | .dxdi-canvas .connector.not-valid path, 199 | .dxdi-canvas .connector.not-valid line { 200 | stroke: red; } 201 | 202 | .dxdi-canvas .connector.can-move { 203 | cursor: move; } 204 | 205 | .dxdi-canvas .selection-mark, 206 | .dxdi-canvas .geometry-mark, 207 | .dxdi-canvas .connection-point, 208 | .dxdi-canvas .connection-mark, 209 | .dxdi-canvas .connector-point-mark, 210 | .dxdi-canvas .connector-side-mark { 211 | fill: white; 212 | stroke-width: 2; } 213 | 214 | .dxdi-canvas .selection-mark { 215 | stroke: dodgerblue; } 216 | 217 | .dxdi-canvas .selection-mark[data-type="9"][data-value="1"] { 218 | cursor: nw-resize; } 219 | 220 | .dxdi-canvas .selection-mark[data-type="9"][data-value="2"] { 221 | cursor: ne-resize; } 222 | 223 | .dxdi-canvas .selection-mark[data-type="9"][data-value="3"] { 224 | cursor: se-resize; } 225 | 226 | .dxdi-canvas .selection-mark[data-type="9"][data-value="4"] { 227 | cursor: sw-resize; } 228 | 229 | .dxdi-canvas .selection-mark[data-type="9"][data-value="5"] { 230 | cursor: n-resize; } 231 | 232 | .dxdi-canvas .selection-mark[data-type="9"][data-value="6"] { 233 | cursor: e-resize; } 234 | 235 | .dxdi-canvas .selection-mark[data-type="9"][data-value="7"] { 236 | cursor: s-resize; } 237 | 238 | .dxdi-canvas .selection-mark[data-type="9"][data-value="8"] { 239 | cursor: w-resize; } 240 | 241 | .dxdi-canvas .selection-mark[data-type="4"], 242 | .dxdi-canvas .selection-mark[data-type="5"] { 243 | cursor: move; } 244 | 245 | .dxdi-canvas .locked-selection-mark { 246 | fill: white; 247 | stroke-width: 1; 248 | stroke: #666; } 249 | 250 | .dxdi-canvas .geometry-mark { 251 | cursor: pointer; 252 | stroke: goldenrod; } 253 | 254 | .dxdi-canvas .container-target, 255 | .dxdi-canvas .connection-target { 256 | fill: transparent; 257 | stroke: orchid; 258 | stroke-width: 2; 259 | pointer-events: none; } 260 | 261 | .dxdi-canvas .connection-point { 262 | cursor: crosshair; 263 | stroke: orchid; } 264 | 265 | .dxdi-canvas .connection-point.not-valid { 266 | stroke: grey; 267 | display: none; } 268 | 269 | .dxdi-canvas .connection-mark { 270 | cursor: crosshair; 271 | stroke: orchid; } 272 | 273 | .dxdi-canvas .connection-point.selector, 274 | .dxdi-canvas .connection-mark.selector { 275 | stroke-width: 10px; 276 | stroke: transparent; 277 | fill: transparent; } 278 | 279 | .dxdi-canvas .connection-point.active { 280 | fill: orchid; } 281 | 282 | .dxdi-canvas .connection-mark.active { 283 | fill: orchid; } 284 | 285 | .dxdi-canvas .connector-point-mark, 286 | .dxdi-canvas .connector-side-mark { 287 | cursor: move; 288 | stroke: dodgerblue; } 289 | 290 | .dxdi-canvas .connector-point-mark.disabled { 291 | cursor: default; 292 | display: none; } 293 | 294 | .dxdi-canvas .connector-side-mark { 295 | fill: dodgerblue; } 296 | 297 | .dxdi-canvas .connector-side-mark.vertical { 298 | cursor: col-resize; } 299 | 300 | .dxdi-canvas .connector-side-mark.horizontal { 301 | cursor: row-resize; } 302 | 303 | .dxdi-canvas .item-selection-rect, 304 | .dxdi-canvas .items-selection-rect { 305 | fill: transparent; 306 | stroke-width: 1; 307 | stroke: dodgerblue; 308 | stroke-dasharray: 2px; 309 | pointer-events: none; } 310 | 311 | .dxdi-canvas .items-selection-rect { 312 | fill: rgba(30, 144, 255, 0.02); } 313 | 314 | .dxdi-canvas .item-multi-selection-rect { 315 | fill: rgba(30, 144, 255, 0.02); 316 | stroke-width: 1; 317 | stroke: dodgerblue; 318 | pointer-events: none; } 319 | 320 | .dxdi-canvas .selection-rect { 321 | fill: rgba(30, 144, 255, 0.2); 322 | stroke-width: 1; 323 | stroke: dodgerblue; 324 | pointer-events: none; } 325 | 326 | .dxdi-canvas .connector-selection, 327 | .dxdi-canvas .connector-multi-selection { 328 | fill: transparent; 329 | stroke-width: 1; 330 | stroke: dodgerblue; 331 | pointer-events: none; } 332 | 333 | .dxdi-canvas .connector-selection.text, 334 | .dxdi-canvas .connector-multi-selection.text { 335 | fill: transparent; 336 | stroke-width: 1; } 337 | 338 | .dxdi-canvas .connector-selection { 339 | stroke-dasharray: 2px; } 340 | 341 | .dxdi-canvas .connector-selection-mask rect { 342 | fill: black; } 343 | 344 | .dxdi-canvas .connector-selection-mask rect.background { 345 | fill: white; } 346 | 347 | .dxdi-canvas .connector-selection-mask path, 348 | .dxdi-canvas .connector-selection-mask line { 349 | fill: white; 350 | stroke: black; 351 | stroke-width: 4; } 352 | 353 | .dxdi-canvas .connector-selection-mask text { 354 | text-anchor: middle; } 355 | 356 | .dxdi-canvas .extension-line path { 357 | stroke: dodgerblue; 358 | stroke-width: 1; } 359 | 360 | .dxdi-canvas .extension-line path.size-line { 361 | stroke-dasharray: 4px; } 362 | 363 | .dxdi-canvas .extension-line text { 364 | fill: dodgerblue; 365 | font-size: 0.8em; 366 | text-anchor: middle; } 367 | 368 | .dxdi-canvas .extension-line.center > path.size-line, 369 | .dxdi-canvas .extension-line.page > path.size-line { 370 | stroke-dasharray: 0; } 371 | 372 | .dxdi-canvas .extension-line:not(.center) > path:not(:first-child) { 373 | display: none; } 374 | 375 | .dxdi-canvas .resize-info text { 376 | fill: rgba(0, 0, 0, 0.8); 377 | font-size: 0.8em; 378 | text-anchor: middle; } 379 | 380 | .dxdi-canvas .resize-info rect { 381 | fill: white; 382 | stroke: rgba(0, 0, 0, 0.3); 383 | stroke-width: 1; } 384 | 385 | .dxdi-canvas .dxdi-active-selection .shape { 386 | cursor: default; } 387 | 388 | .dxdi-control:not(.focused) .dxdi-canvas .selection-mark { 389 | stroke: #666; } 390 | 391 | .dxdi-control:not(.focused) .dxdi-canvas .geometry-mark { 392 | stroke: #666; } 393 | 394 | .dxdi-control:not(.focused) .dxdi-canvas .item-selection-rect, 395 | .dxdi-control:not(.focused) .dxdi-canvas .items-selection-rect { 396 | fill: transparent; 397 | stroke: #666; } 398 | 399 | .dxdi-control:not(.focused) .dxdi-canvas .items-selection-rect { 400 | fill: rgba(144, 144, 144, 0.02); } 401 | 402 | .dxdi-control:not(.focused) .dxdi-canvas .item-multi-selection-rect { 403 | fill: rgba(144, 144, 144, 0.02); 404 | stroke: #666; } 405 | 406 | .dxdi-control:not(.focused) .dxdi-canvas .connection-point, 407 | .dxdi-control:not(.focused) .dxdi-canvas .connection-point.selector, 408 | .dxdi-control:not(.focused) .dxdi-canvas .connection-mark, 409 | .dxdi-control:not(.focused) .dxdi-canvas .connection-mark.selector { 410 | display: none; } 411 | 412 | .dxdi-control:not(.focused) .dxdi-canvas .connector-selection, 413 | .dxdi-control:not(.focused) .dxdi-canvas .connector-multi-selection { 414 | stroke: #666; } 415 | 416 | .dxdi-control:not(.focused) .dxdi-canvas .connector-point-mark, 417 | .dxdi-control:not(.focused) .dxdi-canvas .connector-side-mark { 418 | stroke: #666; } 419 | 420 | .dxdi-control:not(.focused) .dxdi-canvas .connector-side-mark { 421 | fill: #666; } 422 | 423 | .dxdi-dragging, 424 | .dxdi-dragging * { 425 | user-select: none; } 426 | 427 | .dxdi-canvas.export * { 428 | cursor: inherit !important; 429 | pointer-events: all !important; } 430 | 431 | .dxdi-toolbox, 432 | .dxdi-toolbox svg { 433 | user-select: none; 434 | outline: none; } 435 | 436 | .dxdi-toolbox, 437 | .dxdi-toolbox .dxdi-canvas, 438 | .dxdi-toolbox-drag-item .dxdi-canvas { 439 | width: 100%; 440 | height: 100%; } 441 | 442 | .dxdi-toolbox .dxdi-canvas .toolbox-item rect, 443 | .dxdi-toolbox .dxdi-canvas .toolbox-item path, 444 | .dxdi-toolbox .dxdi-canvas .toolbox-item line, 445 | .dxdi-toolbox .dxdi-canvas .toolbox-item ellipse { 446 | fill: transparent; 447 | stroke: currentColor; } 448 | 449 | .dxdi-toolbox .dxdi-canvas .toolbox-item .dxdi-image-placeholder { 450 | opacity: 0.75; 451 | fill: currentColor; 452 | stroke: none; } 453 | 454 | .dxdi-toolbox .dxdi-canvas .toolbox-item .dxdi-shape-text { 455 | opacity: 0.25; } 456 | 457 | .dxdi-toolbox .dxdi-canvas .toolbox-item text, 458 | .dxdi-toolbox-drag-item .dxdi-canvas text { 459 | font-weight: bold; 460 | font-family: "Segoe UI", "Helvetica Neue", Helvetica, arial, sans-serif; 461 | fill: currentColor; } 462 | 463 | .dxdi-toolbox-drag-item .dxdi-canvas .dxdi-image-placeholder { 464 | opacity: 0.75; 465 | fill: currentColor; 466 | stroke: none; } 467 | 468 | .dxdi-toolbox-drag-item .dxdi-canvas .dxdi-shape-text { 469 | display: none; } 470 | 471 | .dxdi-toolbox .toolbox-text-item { 472 | cursor: pointer; 473 | user-select: none; 474 | margin: 0 0 0.6em; } 475 | 476 | .dxdi-toolbox-drag-item, 477 | .dxdi-toolbox-drag-text-item { 478 | font-family: arial, helvetica, sans-serif; 479 | font-size: 10pt; 480 | color: black; 481 | position: absolute; 482 | z-index: 10000; 483 | pointer-events: none !important; } 484 | .dxdi-toolbox-drag-item *, 485 | .dxdi-toolbox-drag-text-item * { 486 | pointer-events: none !important; } 487 | 488 | .dxdi-toolbox-drag-item text { 489 | pointer-events: none; } 490 | 491 | .dxdi-toolbox-drag-text-item { 492 | background-color: white; 493 | border: 2px solid black; 494 | padding: 0.5em; } 495 | 496 | .dxdi-tb-drag-captured { 497 | display: none; } 498 | 499 | .dxdi-focus-input, 500 | .dxdi-text-input-container, 501 | .dxdi-text-input { 502 | padding: 0; 503 | outline: none; 504 | border: none; 505 | resize: none; } 506 | 507 | .dxdi-clipboard-input, 508 | .dxdi-focus-input { 509 | position: fixed; 510 | overflow: hidden; 511 | left: -1000px !important; 512 | top: -1000px !important; } 513 | 514 | .dxdi-text-input-container { 515 | display: none; } 516 | 517 | .dxdi-text-input-container.shape-text, 518 | .dxdi-text-input-container.connector-text { 519 | display: inherit; 520 | position: absolute; 521 | overflow: hidden; 522 | background-color: transparent; 523 | transform-origin: 0 0; } 524 | 525 | .dxdi-text-input-container.shape-text .dxdi-text-input { 526 | display: table-cell; 527 | overflow: hidden; 528 | padding: 1px 0 0; 529 | outline: none; 530 | background-color: transparent; 531 | font-family: arial, helvetica, sans-serif; 532 | font-size: 10pt; 533 | color: black; 534 | line-height: 1.1em; 535 | text-align: center; 536 | vertical-align: middle; } 537 | 538 | .dxdi-text-input-container.connector-text { 539 | overflow: visible; } 540 | 541 | .dxdi-text-input-container.connector-text .dxdi-text-input { 542 | padding: 2px; 543 | outline: none; 544 | height: calc(1.1em + 6px); 545 | width: calc(8em + 6px); 546 | margin-top: calc(-0.55em - 3px); 547 | margin-left: calc(-4em - 3px); 548 | background-color: white; 549 | border: 1px solid dodgerblue; 550 | font-family: arial, helvetica, sans-serif; 551 | font-size: 10pt; 552 | color: black; 553 | line-height: 1.1em; 554 | text-align: center; 555 | vertical-align: middle; 556 | overflow: hidden; } 557 | 558 | .dxdi-page-shadow { 559 | fill: #808080; } 560 | 561 | -------------------------------------------------------------------------------- /src/assets/js/owl/syllabus.js: -------------------------------------------------------------------------------- 1 | var dragStartIndex; 2 | var editing = false; 3 | var editorIndex = 1; 4 | var bodiesLoaded = false; 5 | 6 | //https://gist.github.com/Reinmar/b9df3f30a05786511a42 7 | $.widget( 'ui.dialog', $.ui.dialog, { 8 | _allowInteraction: function( event ) { 9 | if ( this._super( event ) ) { 10 | return true; 11 | } 12 | 13 | // Address interaction issues with general iframes with the dialog. 14 | // Fixes errors thrown in IE when clicking CKEditor magicline's "Insert paragraph here" button. 15 | if ( event.target.ownerDocument != this.document[ 0 ] ) { 16 | return true; 17 | } 18 | 19 | // Address interaction issues with dialog window. 20 | if ( $( event.target ).closest( '.cke_dialog' ).length ) { 21 | return true; 22 | } 23 | 24 | // Address interaction issues with iframe based drop downs in IE. 25 | if ( $( event.target ).closest( '.cke' ).length ) { 26 | return true; 27 | } 28 | }, 29 | 30 | // Uncomment this code when using jQuery UI 1.10.*. 31 | // Addresses http://dev.ckeditor.com/ticket/10269 32 | _moveToTop: function ( event, silent ) { 33 | if ( !event || !this.options.modal ) { 34 | this._super( event, silent ); 35 | } 36 | } 37 | } ); 38 | 39 | function setupAccordion(iframId, isInstructor, msgs, openDataId){ 40 | var activeVar = false; 41 | if($( "#accordion .group" ).children("h3").size() <= 1){ 42 | //since there is only 1 option, might was well keep it open instead of collapsed 43 | activeVar = 0; 44 | //only one to expand, might as well hide the expand all link: 45 | $("#expandLink").closest("li").hide(); 46 | } 47 | $( "#accordion > span > div" ).accordion({ 48 | header: "> div > h3", 49 | active: activeVar, 50 | autoHeight: false, 51 | collapsible: true, 52 | heightStyle: "content", 53 | }); 54 | if(isInstructor){ 55 | $( "#accordion span" ).sortable({ 56 | axis: "y", 57 | handle: ".group", 58 | start: function(event, ui){ 59 | dragStartIndex = ui.item.index(); 60 | }, 61 | stop: function( event, ui ) { 62 | // IE doesn't register the blur when sorting 63 | // so trigger focusout handlers to remove .ui-state-focus 64 | ui.item.children( "h3" ).triggerHandler( "focusout" ); 65 | 66 | //find how much this item was dragged: 67 | var dragEndIndex = ui.item.index(); 68 | var moved = dragStartIndex - dragEndIndex; 69 | if(moved !== 0){ 70 | //update the position: 71 | postAjax($(ui.item).children(":first").attr("syllabusItem"), {"move": moved}, msgs); 72 | updatePositions(); 73 | } 74 | } 75 | }); 76 | var itemsOrder = []; 77 | function updatePositions() { 78 | itemsOrder = []; 79 | $('.reorder-element .group').each(function() { 80 | itemsOrder.push($(this).attr('syllabusitem')); 81 | }); 82 | } 83 | updatePositions() 84 | var saveTimeout; 85 | $('#lastItemMoved').change(function() { 86 | // Clear the enqueued positions save 87 | clearTimeout(saveTimeout); 88 | syllabusId = $(this).text(); 89 | syllabusItem = $('#' + syllabusId).parent().attr('syllabusitem'); 90 | saveTimeout = setTimeout(function(){ 91 | // Save the positions after 500ms with no more changes 92 | // First of all save the selected item 93 | var positionBefore = itemsOrder.indexOf(syllabusItem); 94 | var index = $('#' + syllabusId).parent().parent().index(); 95 | var move = positionBefore - index; 96 | if (move !== 0) { 97 | postAjax(syllabusItem, {"move": move}, msgs); 98 | itemsOrder.move(positionBefore, index); 99 | } 100 | var index = 0; 101 | // After this, check if other elements also should be saved (mulitple changes) 102 | $('.reorder-element .group').each(function() { 103 | var syllabusItem = $(this).attr('syllabusitem'); 104 | var positionBefore = itemsOrder.indexOf(syllabusItem); 105 | var move = positionBefore - index; 106 | if (move === 0) { 107 | index++; 108 | return; 109 | } 110 | // This request should send all the array of positions in order to make it asynchronous (this logic will need a change) 111 | postAjax(syllabusItem, {"move": move}, msgs); 112 | itemsOrder.move(positionBefore, index); 113 | index++; 114 | }); 115 | }, 500); 116 | }); 117 | } 118 | Array.prototype.move = function(from,to){ 119 | this.splice(to,0,this.splice(from,1)[0]); 120 | return this; 121 | }; 122 | if(activeVar === false && openDataId && openDataId !== ''){ 123 | //instructor is working on this data item, keep it open and focused on when refreshing 124 | $( "#accordion div[syllabusItem=" + openDataId + "].group .ui-accordion-header").click().focus(); 125 | 126 | } 127 | 128 | $( "#accordion div.group:first-child h3:first-child").focus(); 129 | 130 | //set hover over text for collapse/expand arrow: 131 | $(".ui-accordion-header-icon").attr("title", msgs.clickToExpandAndCollapse); 132 | } 133 | 134 | function expandAccordion(iframId){ 135 | $('#accordion > span > div.ui-accordion').each(function(){ 136 | if(!$(this).find(".ui-accordion-content:first").is(":visible")){ 137 | $(this).find(".ui-accordion-header:first").click(); 138 | } 139 | }); 140 | $("#collapseLink").show(); 141 | $("#expandLink").hide(); 142 | } 143 | 144 | function collapseAccordion(iframId){ 145 | $('#accordion > span > div.ui-accordion').each(function(){ 146 | if($(this).find(".ui-accordion-content:first").is(":visible")){ 147 | $(this).find(".ui-accordion-header:first").click(); 148 | } 149 | }); 150 | $("#collapseLink").hide(); 151 | $("#expandLink").show(); 152 | } 153 | 154 | function editorClick(event){ 155 | $("#textAreaWysiwyg" + (editorIndex - 1)).val($(event.target).closest(".control-group").find("iframe").contents().find('body').html()).change(); 156 | } 157 | 158 | function showMessage(message, success){ 159 | var spanItem; 160 | if(success){ 161 | spanItem = $("#successInfo"); 162 | }else{ 163 | spanItem = $("#warningInfo"); 164 | } 165 | $(spanItem).html(message); 166 | var topPos = 0; 167 | //set topPos to top of the scroll bar for this iFrame 168 | try{ 169 | topPos = $(window.parent.$("html,body")).scrollTop(); 170 | if(topPos === 0){ 171 | //try a different method to be sure 172 | topPos = $(window.parent.$("body")).scrollTop(); 173 | } 174 | }catch(e){} 175 | //if this is an iframe, adjust top by the offset of the iframe 176 | try{ 177 | if(topPos !== 0){ 178 | topPos = topPos - $(window.parent.$("iframe")).offset().top; 179 | } 180 | }catch(e){} 181 | //unless the users scrolls past the iframe, the position will be negative... just make it 0 182 | if(topPos < 0){ 183 | topPos = 0; 184 | } 185 | $(spanItem).css("top", topPos); 186 | $(spanItem).show(); 187 | 188 | setTimeout(function(){$(spanItem).fadeOut();}, 4000); 189 | } 190 | 191 | function postAjax(id, params, msgs){ 192 | var d = $.Deferred; 193 | $.ajax({ 194 | type: 'POST', 195 | url: "/direct/syllabus/" + id + ".json", 196 | data: params, 197 | async:false, 198 | error: function error(data){ 199 | showMessage(msgs.error, false); 200 | d().reject(); 201 | }, 202 | failure: function failure(data){ 203 | showMessage(msgs.error, false); 204 | d().reject(); 205 | }, 206 | success: function success(data){ 207 | var successText = msgs.saved; 208 | if (params.delete !== null && params.delete) { 209 | successText = msgs.deleted; 210 | } 211 | showMessage(successText, true); 212 | d().resolve(); 213 | } 214 | }); 215 | return d().promise(); 216 | } 217 | 218 | function getDateTime(dateTimeStr){ 219 | var split = dateTimeStr.split(" "); 220 | if(split.length === 3){ 221 | var dateStr = split[0]; 222 | var timeStr = split[1] + " " + split[2]; 223 | var date = new Date(Date.parse(dateStr)); 224 | //TODO, internationalize the "P" match? 225 | var time = timeStr.match(/(\d+)(?::(\d\d))?\s*(P?)/); 226 | date.setHours( parseInt(time[1]) + (time[3] ? 12 : 0) ); 227 | date.setMinutes( parseInt(time[2]) || 0 ); 228 | return date; 229 | }else{ 230 | return null; 231 | } 232 | } 233 | 234 | function setupToggleImages(action, imgClass, classOn, classOff, msgs){ 235 | $("." + imgClass).click(function(event){ 236 | var status; 237 | //custom action for calendar: 238 | if(action === "linkCalendar"){ 239 | //make sure at least one date is set 240 | if(!$(this).hasClass(classOn)){ 241 | //only warn user is they are turning on the calendar sync 242 | var startTime = $(this).parents('div.group').find(".startTimeInput").text(); 243 | var endTime = $(this).parents('div.group').find(".endTimeInput").text(); 244 | if((startTime === null || "" === $.trim(startTime) || $.trim(startTime) === $.trim(msgs.clickToAddStartDate)) 245 | && (endTime === null || "" === $.trim(endTime) || $.trim(endTime) === $.trim(msgs.clickToAddEndDate))){ 246 | showMessage(msgs.calendarDatesNeeded, false); 247 | event.stopPropagation(); 248 | return; 249 | } 250 | } 251 | } 252 | 253 | if($(this).hasClass(classOn)){ 254 | //need to toggle to false 255 | status = false; 256 | $(this).hide(); 257 | $(this).parents('div.group').find("." + classOff).fadeIn(); 258 | }else{ 259 | //need to toggle to true 260 | status = true; 261 | $(this).hide(); 262 | $(this).parents('div.group').find("." + classOn).fadeIn(); 263 | } 264 | //custom action for publish and unpublish: 265 | if(action === "publish"){ 266 | //toggle the draft class 267 | if(status){ 268 | $(this).parent().find(".editItemTitle").parent().removeClass("draft"); 269 | $(this).parent().find( ".draftTitlePrefix " ).remove(); 270 | }else{ 271 | $(this).parent().find(".editItemTitle").parent().addClass("draft"); 272 | var span = "" + msgs.draftTitlePrefix + ""; 273 | $(this).parent().find(".editItemTitle").parent().prepend( span ); 274 | } 275 | } 276 | 277 | var id = $(this).parents('div.group').attr("syllabusItem"); 278 | params = {"toggle" : action, 279 | "status": status}; 280 | postAjax(id, params, msgs); 281 | event.stopPropagation(); 282 | }); 283 | } 284 | function showConfirmDeleteAttachment(deleteButton, msgs, event){ 285 | var title = $(deleteButton).parent().find(".attachment").html(); 286 | $('
    ').appendTo('body') 287 | .html('
    ' + msgs.noUndoWarning + '
    ' + msgs.confirmDelete + " '" + title + "'?
    ") 288 | .dialog({ 289 | position: { my: 'left center', at: 'right center', of: $(deleteButton)}, 290 | modal: true, title: msgs.deleteAttachmentTitle, zIndex: 10000, autoOpen: true, 291 | width: 'auto', resizable: true, 292 | buttons: [ 293 | { 294 | text: msgs.bar_delete, 295 | click: function () { 296 | var id = $(deleteButton).parents('div.group').attr("syllabusItem"); 297 | params = {"deleteAttachment" : true, 298 | "attachmentId" : $(deleteButton).attr("attachmentId")}; 299 | postAjax(id, params, msgs); 300 | if($("#successInfo").is(":visible")){ 301 | $(deleteButton).parents('tr').remove(); 302 | } 303 | $(this).dialog("close"); 304 | } 305 | }, 306 | { 307 | text: msgs.bar_cancel, 308 | click: function () { 309 | $(this).dialog("close"); 310 | } 311 | } 312 | ], 313 | close: function (event, ui) { 314 | $(this).remove(); 315 | } 316 | }); 317 | event.stopPropagation(); 318 | } 319 | 320 | function showConfirmDelete(deleteButton, msgs, event){ 321 | var title = $(deleteButton).parent().parent().find(".syllabusItemTitle").html(); 322 | $('
    ').appendTo('body') 323 | .html('
    ' + msgs.noUndoWarning + '
    ' + msgs.confirmDelete + " '" + title + "'?
    ") 324 | .dialog({ 325 | position: { my: 'left center', at: 'right center', of: $(deleteButton)}, 326 | modal: true, title: msgs.deleteItemTitle, zIndex: 10000, autoOpen: true, 327 | width: 'auto', resizable: true, 328 | buttons: [ 329 | { 330 | text: msgs.bar_delete, 331 | click: function () { 332 | var id = $(deleteButton).parents('div.group').attr("syllabusItem"); 333 | params = {"delete" : true}; 334 | postAjax(id, params, msgs); 335 | if($("#successInfo").is(":visible")){ 336 | $(deleteButton).parents('div.group').remove(); 337 | } 338 | $(this).dialog("close"); 339 | } 340 | }, 341 | { 342 | text: msgs.bar_cancel, 343 | click: function () { 344 | $(this).dialog("close"); 345 | } 346 | } 347 | ], 348 | close: function (event, ui) { 349 | $(this).remove(); 350 | return false; 351 | } 352 | }); 353 | event.stopPropagation(); 354 | } 355 | 356 | function doAddItemButtonClick( msgs, published ) 357 | { 358 | var title = $( "#newTitle" ).val(); 359 | if( !title || "" === title.trim() ) 360 | { 361 | $( "#requiredTitle" ).show(); 362 | setTimeout( function() { $( "#requiredTitle" ).fadeOut(); }, 5000 ); 363 | } 364 | else 365 | { 366 | // Fetch the content from the new wysiwyg 367 | $("#newContentTextAreaWysiwyg").val($('#newContentDiv').find('iframe').contents().find('body').html()).change(); 368 | 369 | // ID doesn't exist since we're adding a new one 370 | var id = "0"; 371 | params = 372 | { 373 | "add" : true, 374 | "title": title, 375 | "siteId": $("#siteId").val(), 376 | "published": published, 377 | "content": $("#newContentTextAreaWysiwyg").val() 378 | }; 379 | 380 | postAjax( id, params, msgs ); 381 | if( $( "#successInfo" ).is( ":visible" ) ) 382 | { 383 | location.reload(); 384 | return true; 385 | } 386 | } 387 | 388 | return false; 389 | } 390 | 391 | function showConfirmAdd(msgs, mainframeId){ 392 | $('#container', this.top.document).append("
    "); 393 | $('
    ').appendTo('body') 394 | .html("
    * " + msgs.syllabus_title + "
    " + 395 | "
    " + msgs.syllabus_content + "