619 lines
No EOL
23 KiB
JavaScript
619 lines
No EOL
23 KiB
JavaScript
/* eslint no-var: "error"*/
|
|
/* eslint no-console: "off"*/
|
|
/* eslint camelcase: "off" */
|
|
/* eslint-disable no-trailing-spaces */
|
|
/* eslint-env es6*/
|
|
// Put this file in path/to/plugin/amd/src
|
|
|
|
import {loadStrings} from './util/string-helper';
|
|
import {formatDate, studyplanDates, studyplanTiming} from './util/date-helper';
|
|
import FitTextVue from './util/fittext-vue';
|
|
import {settings} from "./util/settings";
|
|
|
|
export default {
|
|
|
|
install(Vue /* ,options */) {
|
|
Vue.use(FitTextVue);
|
|
|
|
let strings = loadStrings({
|
|
studyplancard: {
|
|
open: "open",
|
|
noenddate: "noenddate",
|
|
idnumber: "studyplan_idnumber",
|
|
description: "studyplan_description",
|
|
completed: "completed",
|
|
details: "studyplan_details",
|
|
suspended: "suspended",
|
|
},
|
|
details: {
|
|
details: "studyplan_details",
|
|
},
|
|
extrafields: {
|
|
show: "show@core"
|
|
},
|
|
prevnext: {
|
|
prev: "prev@core",
|
|
previous: "previous@core",
|
|
next: "next@core",
|
|
select: "selectanoptions@core",
|
|
}
|
|
});
|
|
// Create new eventbus for interaction between item components
|
|
const ItemEventBus = new Vue();
|
|
|
|
Vue.component('s-studyplan-card', {
|
|
props: {
|
|
value: {
|
|
type: Object,
|
|
},
|
|
open: {
|
|
type: Boolean
|
|
},
|
|
ignoresuspend: {
|
|
type: Boolean,
|
|
'default': false,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
text: strings.studyplancard
|
|
};
|
|
},
|
|
computed: {
|
|
timeless() {
|
|
const plan = this.value;
|
|
if (!plan.pages || plan.pages.length == 0 || plan.pages[0].timeless) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
timing() {
|
|
return studyplanTiming(this.value);
|
|
},
|
|
dates() {
|
|
const dates = studyplanDates(this.value);
|
|
return {
|
|
start: formatDate(dates.start),
|
|
end: (dates.end) ? formatDate(dates.end) : this.text.noenddate,
|
|
};
|
|
},
|
|
suspended() {
|
|
return (this.value.suspended && !this.ignoresuspend);
|
|
}
|
|
|
|
},
|
|
methods: {
|
|
onOpenClick(e) {
|
|
this.$emit('open', e);
|
|
}
|
|
},
|
|
template: `
|
|
<b-card
|
|
:class="'s-studyplan-card timing-' + timing + (suspended?' s-suspended':'')"
|
|
>
|
|
<template #header></template>
|
|
|
|
<div class='s-studyplan-card-content'>
|
|
<div class='s-studyplan-card-icon'><img :src='value.icon'></div>
|
|
<div class='s-studyplan-card-info'>
|
|
<div class='s-studyplan-card-titlebar'>
|
|
<b-card-title>
|
|
<a class='title' v-if='open && !suspended'
|
|
href='#' @click.prevent='onOpenClick($event)'>{{value.name}}</a>
|
|
<template v-else>{{value.name}}</template>
|
|
<div v-if="suspended" class='text-danger'
|
|
><fittext maxsize="12pt">{{text.suspended}}</fittext></div>
|
|
</b-card-title>
|
|
<div class='s-studyplan-card-titleslot'><slot name='title'></slot></div>
|
|
</div>
|
|
<div class='s-studyplan-card-idnumber' v-if='value.idnumber'>
|
|
{{ text.idnumber }}: {{ value.idnumber }}
|
|
</div>
|
|
<s-progress-bar
|
|
v-if='!suspended && value.progress !== undefined && value.progress !== null'
|
|
v-model="value.progress"
|
|
></s-progress-bar>
|
|
</div>
|
|
|
|
</div>
|
|
<slot></slot>
|
|
<template #footer>
|
|
<span v-if="!timeless" :class="'t-timing-'+timing" v-html="dates.start + ' - '+ dates.end"></span>
|
|
<span class="s-studyplan-card-buttons">
|
|
<slot name='footer'></slot>
|
|
<s-studyplan-details
|
|
v-model="value"
|
|
v-if="value.description"
|
|
><i class='fa fa-info-circle'></i></s-studyplan-details>
|
|
<b-button style="float:right;" v-if='open && !suspended' variant='primary'
|
|
@click.prevent='onOpenClick($event)'>{{ text.open }}</b-button>
|
|
</span>
|
|
</template>
|
|
</b-card>
|
|
`,
|
|
});
|
|
|
|
Vue.component('s-progress-bar', {
|
|
props: {
|
|
value: {
|
|
type: Number,
|
|
},
|
|
min: {
|
|
type: Number,
|
|
default() {
|
|
return 0;
|
|
}
|
|
},
|
|
max: {
|
|
type: Number,
|
|
default() {
|
|
return 1;
|
|
}
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
text: strings.studyplancard
|
|
};
|
|
},
|
|
computed: {
|
|
width_completed() {
|
|
if (this.value) {
|
|
const v = ((this.value - this.min) / (this.max - this.min));
|
|
return v * 100;
|
|
} else {
|
|
return 0;
|
|
}
|
|
},
|
|
width_incomplete() {
|
|
if (this.value) {
|
|
const v = ((this.value - this.min) / (this.max - this.min));
|
|
return (1 - v) * 100;
|
|
} else {
|
|
return 100;
|
|
}
|
|
},
|
|
percentage_complete() {
|
|
if (this.value) {
|
|
const v = ((this.value - this.min) / (this.max - this.min));
|
|
return Math.round(v * 100) + "%";
|
|
} else {
|
|
return "0%";
|
|
}
|
|
}
|
|
},
|
|
template: `
|
|
<div class='s-studyplan-card-progress' >
|
|
<div class="s-studyplan-card-progressbar"
|
|
><span v-if="width_completed > 0"
|
|
:style="{width: width_completed+'%'}"
|
|
class='s-studyplan-card-progress-segment s-studyplan-card-progress-completed'
|
|
></span
|
|
><span :style="{width: width_incomplete+'%'}"
|
|
class='s-studyplan-card-progress-segment s-studyplan-card-progress-incomplete'
|
|
></span
|
|
></div>
|
|
<div class="s-studyplan-card-progresstext">
|
|
{{ percentage_complete}} {{ text.completed.toLowerCase() }}
|
|
</div>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
/*
|
|
* S-STUDYLINE-HEADER-HEADING
|
|
* The only reasing this is not a simple empty div, is the fact that the header height
|
|
* needs to match that of the period headers
|
|
*/
|
|
Vue.component('s-studyline-header-heading', {
|
|
props: {
|
|
identifier: {
|
|
type: Number, // Page reference.
|
|
default() {
|
|
return 0;
|
|
}
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
layerHeights: {}
|
|
};
|
|
},
|
|
created() {
|
|
// Listener for the signal that a new connection was made and needs to be drawn
|
|
// Sent by the incoming item - By convention, outgoing items are responsible for drawing the lines
|
|
ItemEventBus.$on('headerHeightChange', this.onHeaderHeightChange);
|
|
},
|
|
computed: {
|
|
|
|
},
|
|
methods: {
|
|
onHeaderHeightChange(newheight, identifier) {
|
|
if (this.identifier == identifier) {
|
|
if (this.$refs.main) {
|
|
this.$refs.main.style.height = `${newheight}px`;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
template: `
|
|
<div class="s-studyline-header-heading" ref="main" :data-id="identifier"><slot></slot></div>
|
|
`,
|
|
});
|
|
|
|
Vue.component('s-studyline-header-period', {
|
|
props: {
|
|
value: {
|
|
type: Object, // Dict with layer as index
|
|
},
|
|
identifier: {
|
|
type: Number, // Page reference.
|
|
default() {
|
|
return 0;
|
|
}
|
|
},
|
|
mode: {
|
|
type: String,
|
|
'default': "view",
|
|
}
|
|
},
|
|
mounted() {
|
|
const self = this;
|
|
if (self.value.period == 1) {
|
|
self.resizeListener = new ResizeObserver(() => {
|
|
if (self.$refs.main) {
|
|
const size = self.$refs.main.getBoundingClientRect();
|
|
ItemEventBus.$emit('headerHeightChange', size.height, self.identifier);
|
|
}
|
|
}).observe(self.$refs.main);
|
|
}
|
|
},
|
|
unmounted() {
|
|
if (this.resizeListener) {
|
|
this.resizeListener.disconnect();
|
|
}
|
|
},
|
|
computed: {
|
|
startdate() {
|
|
return formatDate(this.value.startdate);
|
|
},
|
|
enddate() {
|
|
return formatDate(this.value.enddate);
|
|
},
|
|
current() {
|
|
if (this.value && this.value.startdate && this.value.enddate) {
|
|
const now = new Date();
|
|
const pstart = new Date(this.value.startdate);
|
|
const pend = new Date(this.value.enddate);
|
|
return (now >= pstart && now < pend);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
},
|
|
data() {
|
|
return {
|
|
};
|
|
},
|
|
template: `
|
|
<div :class="'s-studyline-header-period '" ref="main" :data-id="identifier"
|
|
><p><abbr :id="'s-period-'+value.id" :title="value.fullname">{{ value.shortname }}</abbr>
|
|
<b-tooltip
|
|
:target="'s-period-'+value.id" triggers="hover"
|
|
>{{ value.fullname }}<br>
|
|
<span class="s-studyline-header-period-datespan">
|
|
<span class="date">{{ startdate }}</span> - <span class="date">{{ enddate }}</span>
|
|
</span>
|
|
</b-tooltip>
|
|
<slot></slot
|
|
><p v-if="value.startdate > 0"
|
|
class="s-studyline-header-period-datespan small">
|
|
<span class="date">{{ startdate }}</span>
|
|
- <span class="date" v-if="this.value.enddate > 0">{{ enddate }}</span><span class="date" v-else>∞</span>
|
|
</p>
|
|
<div v-if="current && mode == 'view'" class='s-studyline-period-highlight'></div>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
|
|
Vue.component('s-studyplan-details', {
|
|
props: {
|
|
value: {
|
|
type: Object,
|
|
},
|
|
variant: {
|
|
type: String,
|
|
default() {
|
|
return "info";
|
|
}
|
|
},
|
|
pill: {
|
|
type: Boolean,
|
|
default() {
|
|
return false;
|
|
}
|
|
},
|
|
size: {
|
|
type: String,
|
|
default() {
|
|
return "";
|
|
}
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
text: strings.details,
|
|
};
|
|
},
|
|
template: `
|
|
<span>
|
|
<b-button
|
|
:size="size"
|
|
:pill="pill"
|
|
:variant="variant"
|
|
v-b-modal="'modal-description-'+value.id"
|
|
><slot><i class='fa fa-info-circle'></i>
|
|
{{ text.details}}</slot>
|
|
</b-button>
|
|
<b-modal
|
|
:title="value.name"
|
|
scrollable
|
|
centered
|
|
ok-only
|
|
content-class="s-studyplan-details"
|
|
:id="'modal-description-'+value.id"
|
|
>
|
|
<b-container>
|
|
<b-row>
|
|
<b-col cols="4"><img :src='value.icon'></b-col>
|
|
<b-col cols="8" class="pl-1">
|
|
<span v-html="value.description"></span>
|
|
</b-col>
|
|
</b-row>
|
|
</b-container>
|
|
</b-modal>
|
|
</span>
|
|
`,
|
|
});
|
|
|
|
Vue.component('s-course-extrafields', {
|
|
props: {
|
|
value: {
|
|
type: Array,
|
|
},
|
|
variant: {
|
|
type: String,
|
|
default() {
|
|
return "info";
|
|
}
|
|
},
|
|
position: {
|
|
type: String,
|
|
default() {
|
|
return "below";
|
|
}
|
|
},
|
|
size: {
|
|
type: String,
|
|
default() {
|
|
return "";
|
|
}
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
text: strings.extrafields,
|
|
};
|
|
},
|
|
computed: {
|
|
fields() {
|
|
const fields = [];
|
|
for (const ix in this.value) {
|
|
const field = this.value[ix];
|
|
if (field.position == this.position && field.value && field.value.length > 0) {
|
|
fields.push(field);
|
|
}
|
|
}
|
|
return fields;
|
|
},
|
|
},
|
|
methods: {
|
|
displaydate(field) {
|
|
return formatDate(field.value, false);
|
|
},
|
|
},
|
|
template: `
|
|
<div :class="'s-course-extrafields ' + ((fields.length>0)?position:'empty')">
|
|
<table v-if="fields.length > 0">
|
|
<tr v-for="field in fields">
|
|
<td><span class='title' v-if='field.title'>{{ field.title}}</span></td>
|
|
<td>
|
|
<span v-if='field.type == "date"' class="value date">{{ displaydate(field) }}</span>
|
|
<span v-else-if='field.type == "checkbox"'
|
|
:class="'value ' + (field.checked?'true':'false')">{{ field.value }}</span>
|
|
<span v-else-if='field.type == "textarea"'>
|
|
<a class='text-info'
|
|
@click.prevent.stop=""
|
|
href='#'
|
|
v-b-modal="field.courseid+'_'+field.fieldname">{{text.show}}...</a>
|
|
<b-modal
|
|
:id="field.courseid+'_'+field.fieldname"
|
|
:title="field.title""
|
|
scrollable
|
|
centered
|
|
ok-only
|
|
><span v-html="field.value"></span
|
|
></b-modal>
|
|
</span>
|
|
<span v-else class="value"><span v-html="field.value"></span></span>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
Vue.component('s-prevnext-selector', {
|
|
props: {
|
|
value: {
|
|
type: Object,
|
|
default() {
|
|
return null;
|
|
}
|
|
},
|
|
options: {
|
|
type: Array,
|
|
},
|
|
grouped: {
|
|
type: Boolean,
|
|
'default': false,
|
|
},
|
|
titlefield: {
|
|
type: String,
|
|
'default': "title",
|
|
},
|
|
labelfield: {
|
|
type: String,
|
|
'default': "label",
|
|
},
|
|
optionsfield: {
|
|
type: String,
|
|
'default': "options",
|
|
},
|
|
defaultselectable: {
|
|
type: Boolean,
|
|
'default': false,
|
|
},
|
|
variant: {
|
|
type: String,
|
|
'default': "",
|
|
},
|
|
arrows: {
|
|
type: Boolean,
|
|
'default': false,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
showarrows: settings("showprevnextarrows"),
|
|
text: strings.prevnext,
|
|
};
|
|
},
|
|
computed: {
|
|
fields() {
|
|
const f = [];
|
|
if (this.defaultselectable) {
|
|
f.push(null);
|
|
}
|
|
if (this.grouped) {
|
|
for (const gix in this.options) {
|
|
const group = this.options[gix];
|
|
for (const ix in group[this.optionsfield]) {
|
|
const v = group[this.optionsfield][ix];
|
|
f.push(v);
|
|
}
|
|
}
|
|
} else {
|
|
for (const ix in this.options) {
|
|
const v = this.options[ix];
|
|
f.push(v);
|
|
}
|
|
}
|
|
return f;
|
|
},
|
|
bubblevalue: {
|
|
get() {
|
|
return (this.value) ? this.value : null;
|
|
},
|
|
set(v) {
|
|
this.$emit('input', (v) ? v : null);
|
|
},
|
|
}
|
|
},
|
|
methods: {
|
|
atFirst() {
|
|
if (this.fields.length > 0) {
|
|
return this.fields[0] == this.value;
|
|
}
|
|
return true; // Since it disables the button, do so if pressing it would make no sense.
|
|
},
|
|
atLast() {
|
|
if (this.fields.length > 0) {
|
|
const l = this.fields.length - 1;
|
|
return this.fields[l] == this.value;
|
|
}
|
|
return true; // Since it disables the button, do so if pressing it would make no sense.
|
|
},
|
|
prev() {
|
|
if (this.fields.length > 0) {
|
|
const index = this.fields.indexOf(this.value);
|
|
if (index > 0) {
|
|
this.$emit("input", this.fields[index - 1]);
|
|
this.$emit("change", this.fields[index - 1]);
|
|
}
|
|
}
|
|
},
|
|
next() {
|
|
if (this.fields.length > 0) {
|
|
const index = this.fields.indexOf(this.value);
|
|
const l = this.fields.length - 1;
|
|
if (index >= 0 && index < l) {
|
|
this.$emit("input", this.fields[index + 1]);
|
|
this.$emit("change", this.fields[index + 1]);
|
|
}
|
|
}
|
|
},
|
|
selectedchanged(value) {
|
|
this.$emit("change", value);
|
|
}
|
|
},
|
|
template: `
|
|
<div :class="'s-prevnext-selector '">
|
|
<template v-if="showarrows"
|
|
><b-button :variant='variant' @click="prev" :disabled="atFirst()"
|
|
><i v-if='arrows' class='fa fa-caret-left s-prevnext-arrow'></i
|
|
><template v-else>{{text.prev}}</template></b-button
|
|
> </template
|
|
><b-form-select v-model="bubblevalue"
|
|
@change='selectedchanged'
|
|
><b-form-select-option
|
|
:disabled="!defaultselectable"
|
|
:value="null"
|
|
:class="(defaultselectable)?'font-italic text-primary ':'font-italic'"
|
|
><slot name="defaultlabel">{{text.select}}</slot></b-form-select-option
|
|
><template v-if="grouped"
|
|
><template v-for="(g,gi) in this.options"
|
|
><b-form-select-option-group
|
|
v-if="g[optionsfield] && g[optionsfield].length > 0"
|
|
:label="g[labelfield]"
|
|
><b-form-select-option
|
|
v-for="(o,i) in g[optionsfield]"
|
|
:key="i"
|
|
:value="o"
|
|
><slot :value="o">{{o[titlefield]}}</slot></b-form-select-option>
|
|
</b-form-select-option-group
|
|
></template
|
|
></template
|
|
><template v-else>
|
|
<b-form-select-option
|
|
v-for="(o,i) in this.options"
|
|
:key="i"
|
|
:value="o"
|
|
><slot :value="o">{{o[titlefield]}}</slot></b-form-select-option
|
|
></template
|
|
></b-form-select
|
|
><template v-if="showarrows"
|
|
> <b-button :variant='variant' @click="next" :disabled="atLast()"
|
|
><i v-if='arrows' class='fa fa-caret-right s-prevnext-arrow'></i
|
|
><template v-else>{{text.next}}</template></b-button
|
|
></template
|
|
></div>
|
|
`,
|
|
});
|
|
|
|
|
|
}
|
|
}; |