{"version":3,"file":"report-viewer-components.min.js","sources":["../src/report-viewer-components.js"],"sourcesContent":["/* eslint no-var: \"error\"*/\n/* eslint no-console: \"off\"*/\n/* eslint no-unused-vars: warn */\n/* eslint max-len: [\"error\", { \"code\": 160 }] */\n/* eslint max-depth: [\"error\", 6] */\n/* eslint no-trailing-spaces: warn */\n/* eslint-env es6*/\n\nimport {SimpleLine} from './simpleline/simpleline';\nimport {loadStrings} from './util/string-helper';\nimport {formatDate, formatDatetime, studyplanPageTiming, studyplanTiming} from './util/date-helper';\nimport {addBrowserButtonEvent} from './util/browserbuttonevents';\nimport {call} from 'core/ajax';\nimport notification from 'core/notification';\nimport {svgarcpath} from './util/svgarc';\nimport Debugger from './util/debugger';\nimport Config from 'core/config';\nimport {processStudyplan, objCopy} from './studyplan-processor';\nimport TSComponents from './treestudyplan-components';\nimport {premiumenabled} from \"./util/premium\";\nimport FitTextVue from './util/fittext-vue';\n\n\n// Make π available as a constant\nconst π = Math.PI;\n// Gravity value for arrow lines - determines how much a line is pulled in the direction of the start/end before changing direction\nconst LINE_GRAVITY = 1.3;\n\n/**\n * Studyline is not enrollable\n * @var int\n */\nconst ENROLLABLE_NONE = 0;\n\n/**\n * Studyline can be enrolled into by the student\n * @var int\n */\nconst ENROLLABLE_SELF = 1;\n\n/**\n * Studyline can be enrolled into by specific role(s)\n * @var int\n */\nconst ENROLLABLE_ROLE = 2;\n\n/**\n * Studyline can be enrolled by user and/or role\n * @var int\n */\nconst ENROLLABLE_SELF_ROLE = 3;\n\n\nexport default {\n install(Vue/* ,options */) {\n Vue.use(TSComponents);\n Vue.use(FitTextVue);\n let debug = new Debugger(\"treestudyplan-viewer\");\n\n let lastCaller = null;\n /**\n * Scroll current period into view\n * @param {*} handle A key to pass so subsequent calls with the same key won't trigger (always triggers when null or undefined)\n */\n function scrollCurrentIntoView(handle) {\n const elScrollContainer = document.querySelector(\".r-studyplan-scrollable\");\n const elCurrentHeader = elScrollContainer.querySelector(\".s-studyline-header-period.current\");\n\n if (elCurrentHeader && ((!handle) || (handle != lastCaller))) {\n lastCaller = handle;\n elCurrentHeader.scrollIntoView({\n behavior: \"smooth\",\n block: \"start\",\n inline: \"center\",\n });\n }\n }\n\n let strings = loadStrings({\n report: {\n loading: \"loadinghelp@core\",\n 'studyplan_past': \"studyplan_past\",\n 'studyplan_present': \"studyplan_present\",\n 'studyplan_future': \"studyplan_future\",\n back: \"back\",\n },\n invalid: {\n error: 'error',\n },\n grading: {\n ungraded: \"ungraded\",\n graded: \"graded\",\n allgraded: \"allgraded\",\n unsubmitted: \"unsubmitted\",\n nogrades: \"nogrades\",\n unknown: \"unknown\",\n },\n completion: {\n completed: \"completion_completed\",\n incomplete: \"completion_incomplete\",\n 'completed_pass': \"completion_passed\",\n 'completed_fail': \"completion_failed\",\n ungraded: \"ungraded\",\n 'aggregation_all': \"aggregation_all\",\n 'aggregation_any': \"aggregation_any\",\n 'aggregation_one': \"aggregation_one\",\n 'aggregation_overall_all': \"aggregation_overall_all\",\n 'aggregation_overall_any': \"aggregation_overall_any\",\n 'aggregation_overall_one': \"aggregation_overall_one\",\n 'completion_not_configured': \"completion_not_configured\",\n 'configure_completion': \"configure_completion\",\n 'view_completion_report': \"view_completion_report\",\n 'completion_incomplete': \"completion_incomplete\",\n 'completion_failed': \"completion_failed\",\n 'completion_pending': \"completion_pending\",\n 'completion_progress': \"completion_progress\",\n 'completion_completed': \"completion_completed\",\n 'completion_good': \"completion_good\",\n 'completion_excellent': \"completion_excellent\",\n 'view_feedback': \"view_feedback\",\n 'coursetiming_past': \"coursetiming_past\",\n 'coursetiming_present': \"coursetiming_present\",\n 'coursetiming_future': \"coursetiming_future\",\n 'required_goal': \"required_goal\",\n 'student_not_tracked': \"student_not_tracked\",\n 'completion_not_enabled': \"completion_not_enabled\",\n },\n badge: {\n 'share_badge': \"share_badge\",\n dateissued: \"dateissued\",\n dateexpire: \"dateexpire\",\n badgeinfo: \"badgeinfo\",\n badgeissuedstats: \"badgeissuedstats\",\n 'completion_incomplete': \"completion_incomplete_badge\",\n 'completion_completed': \"completion_completed_badge\",\n badgedisabled: \"badgedisabled\"\n },\n course: {\n 'completion_incomplete': \"completion_incomplete\",\n 'completion_failed': \"completion_failed\",\n 'completion_pending': \"completion_pending\",\n 'completion_progress': \"completion_progress\",\n 'completion_completed': \"completion_completed\",\n 'completion_good': \"completion_good\",\n 'completion_excellent': \"completion_excellent\",\n 'view_feedback': \"view_feedback\",\n 'coursetiming_past': \"coursetiming_past\",\n 'coursetiming_present': \"coursetiming_present\",\n 'coursetiming_future': \"coursetiming_future\",\n 'required_goal': \"required_goal\",\n 'student_not_tracked': \"student_not_tracked\",\n 'not_enrolled': \"not_enrolled\",\n noenddate: \"noenddate\",\n },\n teachercourse: {\n 'select_conditions': \"select_conditions\",\n 'select_grades': \"select_grades\",\n 'coursetiming_past': \"coursetiming_past\",\n 'coursetiming_present': \"coursetiming_present\",\n 'coursetiming_future': \"coursetiming_future\",\n 'grade_include': \"grade_include\",\n 'grade_require': \"grade_require\",\n 'required_goal': \"required_goal\",\n 'student_from_plan_enrolled': \"student_from_plan_enrolled\",\n 'students_from_plan_enrolled': \"students_from_plan_enrolled\",\n noenddate: \"noenddate\",\n },\n competency: {\n 'competency_not_configured': \"competency_not_configured\",\n 'configure_competency': \"configure_competency\",\n when: \"when\",\n required: \"required\",\n points: \"points@core_grades\",\n heading: \"competency_heading\",\n details: \"competency_details\",\n results: \"results\",\n unrated: \"unrated\",\n progress: \"completion_progress\",\n 'view_feedback': \"view_feedback\",\n },\n pageinfo: {\n edit: 'period_edit',\n fullname: 'studyplan_name',\n shortname: 'studyplan_shortname',\n startdate: 'studyplan_startdate',\n enddate: 'studyplan_enddate',\n description: 'studyplan_description',\n duration: 'studyplan_duration',\n details: 'studyplan_details',\n overview: 'overviewreport:all',\n oveviewperiod: 'overviewreport:period'\n },\n lineheader: {\n 'cannot_enrol': 'line_cannot_enrol',\n 'can_enrol': 'line_can_enrol',\n 'is_enrolled': 'line_is_enrolled',\n enrol: 'line_enrol',\n unenrol: 'line_unenrol',\n enrolled: 'line_enrolled',\n notenrolled: 'line_notenrolled',\n 'enrol_question': 'line_enrol_question',\n enrollments: 'line_enrollments',\n enrollment: 'line_enrollment',\n info: 'info@core',\n confirm: 'confirm@core',\n yes: 'yes@core',\n no: 'no@core',\n 'enrolled_in': 'line_enrolled_in',\n since: 'since@core',\n byname: 'byname@core',\n students: 'students@core',\n firstname: 'firstname@core',\n lastname: 'lastname@core',\n email: 'email@core',\n 'enrol_student_question': 'line_enrol_student_question',\n 'unenrol_student_question': 'line_unenrol_student_question',\n\n }\n\n });\n\n /* **********************************\n * *\n * Treestudyplan Viewer components *\n * *\n ************************************/\n\n /**\n * Check if element is visible\n * @param {Object} elem The element to check\n * @returns {boolean} True if visible\n */\n function isVisible(elem) {\n return !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length);\n }\n\n // Create new eventbus for interaction between item components\n const ItemEventBus = new Vue();\n\n /*\n // Add event listener for the edit mode event so we can react to it, or at the very least ignore it\n document.addEventListener(editSwEventTypes.editModeSet,(e) => {\n e.preventDefault();\n ItemEventBus.$emit('editModeSet',e.detail.editMode);\n });\n */\n\n Vue.component('r-progress-circle', {\n props: {\n value: {\n type: Number,\n },\n max: {\n type: Number,\n 'default': 100,\n },\n min: {\n type: Number,\n 'default': 0,\n },\n stroke: {\n type: Number,\n 'default': 0.2,\n },\n bgopacity: {\n type: Number,\n 'default': 0.2,\n },\n title: {\n type: String,\n 'default': \"\",\n },\n icon: {\n type: String,\n }\n\n },\n data() {\n return {\n selectedstudyplan: null,\n };\n },\n computed: {\n range() {\n return this.max - this.min;\n },\n fraction() {\n if (this.max - this.min == 0) {\n return 0;\n // 0 size is always empty :)\n } else {\n return (this.value - this.min) / (this.max - this.min);\n }\n },\n radius() {\n return 50 - (50 * this.stroke);\n },\n arcpath() {\n let fraction = 0;\n const r = 50 - (50 * this.stroke);\n if (this.max - this.min != 0) {\n fraction = (this.value - this.min) / (this.max - this.min);\n }\n\n const Δ = fraction * 2 * π;\n return svgarcpath([50, 50], [r, r], [0, Δ], 1.5 * π);\n },\n },\n methods: {\n },\n template: `\n
\n \n {{title}}\n = 1.0\" cx=\"50\" cy=\"50\" :r=\"radius\"\n :style=\"'opacity: 1; stroke-width: '+ (stroke*100)+'; stroke: currentcolor; fill: none;'\"/>\n \n \n \n \n \n \n
\n `,\n });\n\n\n Vue.component('r-report', {\n props: {\n invitekey: {\n type: String,\n default() {\n return null;\n },\n },\n userid: {\n type: Number,\n default() {\n return 0;\n },\n },\n type: {\n type: String,\n default() {\n return \"own\";\n },\n },\n },\n data() {\n return {\n text: strings.report,\n studyplans: {\n past: [],\n present: [],\n future: [],\n },\n\n selectedstudyplan: null,\n loadingstudyplan: false,\n loading: true,\n };\n },\n computed: {\n teachermode() {\n return (this.type == \"teaching\");\n },\n guestmode() {\n return (this.type == \"invited\");\n },\n verifiedType() {\n if (![\"invited\", \"other\", \"teaching\", \"own\"].includes(this.type)) {\n return \"own\";\n } else {\n return this.type;\n }\n },\n studyplancount() {\n return this.studyplans.past.length + this.studyplans.present.length + this.studyplans.future.length;\n }\n },\n\n mounted() {\n this.loadStudyplans();\n addBrowserButtonEvent(this.backPressed);\n },\n\n methods: {\n backPressed() {\n debug.log(\"Back button pressed\");\n if (this.selectedstudyplan) {\n debug.log(\"Closing studyplan\");\n this.deselectStudyplan();\n }\n },\n callArgs(o) {\n const args = {};\n if (typeof o == 'object' && !Array.isArray(o) && o !== null) {\n objCopy(args, o);\n }\n\n if (this.verifiedType == \"invited\") {\n args.invitekey = this.invitekey;\n } else if (this.verifiedType == \"other\") {\n args.userid = this.userid;\n }\n return args;\n },\n loadStudyplans() {\n const self = this;\n this.loading = true;\n\n call([{\n methodname: `local_treestudyplan_list_${this.verifiedType}_studyplans`,\n args: this.callArgs(),\n }])[0].then(function(response) {\n console.info(\"Loaded: plans\", response);\n const plans = {future: [], present: [], past: []};\n\n for (const ix in response) {\n const plan = response[ix];\n const timing = studyplanTiming(plan);\n plans[timing].push(plan);\n }\n\n for (const ix in plans) {\n plans[ix].sort((a, b) => {\n const t = new Date(b.startdate).getTime() - new Date(a.startdate).getTime();\n if (t == 0) {\n // Sort by name if timing is equal\n t = a.name.localeCompare(b.name);\n }\n return t;\n });\n }\n\n self.studyplans = plans;\n self.loading = false;\n\n // Load studyplan from hash if applicable\n const hash = window.location.hash.replace('#', '');\n const parts = hash.split(\"-\");\n\n if (!!parts && parts.length > 0) {\n for (const k in self.studyplans) {\n const list = self.studyplans[k];\n for (const idx in list) {\n const plan = list[idx];\n if (plan.id == parts[0] && !plan.suspended) {\n self.selectStudyplan(plan);\n return;\n }\n }\n }\n }\n\n // If there is but a single studyplan, select it anyway, even if it is not current...\n if (this.studyplancount == 1) {\n if (self.studyplans.present.length > 0) {\n // Directly show the current study plan if it's the only current one\n const plan = self.studyplans.present[0];\n if (!plan.suspended) {\n self.selectStudyplan(plan);\n }\n } else if (self.studyplans.future.lengh > 0) {\n const plan = self.studyplans.future[0];\n if (!plan.suspended) {\n self.selectStudyplan(plan);\n }\n } else {\n const plan = self.studyplans.past[0];\n if (!plan.suspended) {\n self.selectStudyplan(plan);\n }\n }\n }\n return;\n }).catch(notification.exception);\n },\n selectStudyplan(plan) {\n const self = this;\n this.loadingstudyplan = true;\n call([{\n methodname: `local_treestudyplan_get_${this.verifiedType}_studyplan`,\n args: this.callArgs({\n studyplanid: plan.id,\n }),\n }])[0].then(function(response) {\n self.selectedstudyplan = processStudyplan(response);\n self.loadingstudyplan = false;\n window.location.hash = self.selectedstudyplan.id;\n return;\n }).catch(notification.exception);\n },\n deselectStudyplan() {\n this.selectedstudyplan = null;\n this.loadStudyplans(); // Reload the list of studyplans.\n window.location.hash = '';\n }\n },\n template: `\n
\n
\n \n
\n `,\n });\n\n Vue.component('r-studyplan', {\n props: {\n value: {\n type: Object,\n },\n guestmode: {\n type: Boolean,\n 'default': false,\n },\n teachermode: {\n type: Boolean,\n 'default': false,\n },\n coaching: {\n type: Boolean,\n 'default': false,\n },\n },\n data() {\n return {\n selectedpageindex: -1,\n text: strings.pageinfo,\n };\n },\n computed: {\n\n selectedpage() {\n return this.value.pages[this.selectedpageindex];\n },\n startpageindex() {\n let startpageindex = 0;\n let firststart = null;\n for (const ix in this.value.pages) {\n const page = this.value.pages[ix];\n if (studyplanPageTiming(page) == \"present\") {\n const s = new Date(page.startdate);\n if ((!firststart) || firststart > s) {\n startpageindex = ix;\n firststart = s;\n }\n }\n }\n return startpageindex;\n },\n wwwroot() {\n return Config.wwwroot;\n },\n },\n methods: {\n pageduration(page) {\n return formatDate(page.startdate, false) + \" - \" + formatDate(page.enddate, false);\n },\n columns(page) {\n return 1 + (page.periods * 2);\n },\n columnsStylerule(page) {\n // Uses css variables, so width for slots and filters can be configured in css\n let s = \"grid-template-columns: var(--studyplan-filter-width)\"; // Ese css variable here\n for (let i = 0; i < page.periods; i++) {\n s += \" var(--studyplan-course-width) var(--studyplan-filter-width)\";\n }\n return s + \";\";\n },\n countLineLayers(line, page) {\n let maxLayer = -1;\n for (let i = 0; i <= page.periods; i++) {\n // Determine the amount of used layers in a studyline slot\n for (const ix in line.slots[i].courses) {\n const item = line.slots[i].courses[ix];\n if (item.layer > maxLayer) {\n maxLayer = item.layer;\n }\n }\n for (const ix in line.slots[i].filters) {\n const item = line.slots[i].filters[ix];\n if (item.layer > maxLayer) {\n maxLayer = item.layer;\n }\n }\n }\n return (maxLayer >= 0) ? (maxLayer + 1) : 1;\n },\n showslot(page, line, index, layeridx, type) {\n // Check if the slot should be hidden because a previous slot has an item with a span\n // so big that it hides this slot\n const forGradable = (type == 'gradable') ? true : false;\n const periods = page.periods;\n let show = true;\n for (let i = 0; i < periods; i++) {\n if (line.slots[index - i] && line.slots[index - i].courses) {\n const list = line.slots[index - i].courses;\n for (const ix in list) {\n const item = list[ix];\n if (item.layer == layeridx) {\n if (forGradable) {\n if (i > 0 && (item.span - i) > 0) {\n show = false;\n }\n } else {\n if ((item.span - i) > 1) {\n show = false;\n }\n }\n }\n }\n }\n }\n\n return show;\n },\n selectedpageChanged(/* Params: newTabIndex, prevTabIndex */) {\n\n ItemEventBus.$emit('redrawLines', null);\n scrollCurrentIntoView(this.selectedpage.id);\n },\n },\n mounted() {\n this.$root.$emit('redrawLines');\n },\n updated() {\n\n scrollCurrentIntoView(this.selectedpage.id);\n ItemEventBus.$emit('lineHeightChange', null);\n this.$root.$emit('redrawLines');\n ItemEventBus.$emit('redrawLines');\n },\n template: `\n
\n \n \n \n\n \n \n \n \n {{ text.shortname}}\n \n {{ page.shortname }}\n \n \n \n {{ text.duration}}\n \n {{ pageduration(page) }}\n \n \n \n {{ text.description}}\n \n \n \n \n \n \n \n \n
0\" class='r-studyplan-content'>\n \n \n \n
\n
\n \n \n\n \n \n