{"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-disable no-trailing-spaces */\n/*eslint-env es6*/\n// Put this file in path/to/plugin/amd/src\n\nimport {SimpleLine} from './simpleline/simpleline';\nimport {get_strings} from 'core/str';\nimport {load_strings} from './util/string-helper';\nimport {format_date, format_datetime, studyplanPageTiming,studyplanTiming} from './util/date-helper';\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, ProcessStudyplanPage, objCopy} from './studyplan-processor';\nimport TSComponents from './treestudyplan-components';\n/*import {eventTypes as editSwEventTypes} from 'core/edit_switch';*/\nimport { premiumenabled, premiumstatus } 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 = load_strings({\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 completioninfo: \"completioninfo\",\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() { return null;},\n },\n userid: {\n type: Number,\n default() { return 0;},\n },\n type: {\n type: String,\n default() { return \"own\";},\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 verified_type() {\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 updated() {\n\n },\n mounted() {\n this.loadStudyplans();\n },\n\n methods: {\n call_args(o) {\n const args = {};\n if (typeof o == 'object' && !Array.isArray(o) && o !== null) {\n objCopy(args,o);\n }\n\n if(this.verified_type == \"invited\") {\n args[\"invitekey\"] = this.invitekey;\n } else if(this.verified_type == \"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.verified_type}_studyplans`,\n args: this.call_args(),\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 }).catch(notification.exception);\n },\n selectStudyplan(plan) {\n const self = this;\n this.loadingstudyplan = true;\n call([{\n methodname: `local_treestudyplan_get_${this.verified_type}_studyplan`,\n args: this.call_args({\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 }).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 format_date(page.startdate,false) + \" - \" + format_date(page.enddate,false);\n },\n columns(page) {\n return 1+ (page.periods * 2);\n },\n columns_stylerule(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)\"; // use css variable here\n for(let i=0; i 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(newTabIndex,prevTabIndex) {\n \n ItemEventBus.$emit('redrawLines', null);\n scrollCurrentIntoView(this.selectedpage.id);\n },\n },\n mounted() {\n// scrollCurrentIntoView(this.selectedpage.id);\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