New grid style and rudimentary page support added to report

This commit is contained in:
PMKuipers 2023-07-27 15:07:14 +02:00
parent 1c63ad83c2
commit 5d5ac2ce1c
4 changed files with 264 additions and 199 deletions

View File

@ -1,5 +1,7 @@
/*eslint no-var: "error"*/
/*eslint no-console: "off"*/
/*eslint no-unused-vars: warn */
/*eslint max-len: ["error", { "code": 160 }] */
/*eslint-disable no-trailing-spaces */
/*eslint-env es6*/
// Put this file in path/to/plugin/amd/src
@ -94,6 +96,9 @@ export default {
return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );
}
// Create new eventbus for interaction between item components
const ItemEventBus = new Vue();
Vue.component('r-progress-circle',{
props: {
value: {
@ -238,108 +243,192 @@ export default {
return {
};
},
updated(){
this.$root.$emit('redrawLines');
},
mounted(){
this.$root.$emit('redrawLines');
},
computed: {
columns() {
return 1+ (this.page.periods * 2);
},
columns_stylerule() {
// Uses css variables, so width for slots and filters can be configured in css
let s = "grid-template-columns: var(--studyplan-filter-width)"; // use css variable here
for(let i=0; i<this.page.periods;i++){
s+= " var(--studyplan-course-width) var(--studyplan-filter-width)";
}
return s+";";
},
page() {
//FIXME: Replace this when actual page management is implemented
return this.value.pages[0];
},
},
methods: {
countLineLayers(line){
let maxLayer = -1;
for(let i = 0; i <= this.value.slots; i++){
const slot = line.slots[i];
// Determine the amount of used layers in a studyline slit
for(const ix in line.slots[i].competencies){
const item = line.slots[i].competencies[ix];
if(item.layer > maxLayer){
maxLayer = item.layer;
}
}
for(const ix in line.slots[i].filters){
const item = line.slots[i].filters[ix];
if(item.layer > maxLayer){
maxLayer = item.layer;
}
}
}
return maxLayer+1;
},
},
template: `
<div class='r-studyplan-content'>
<template v-if="value && value.studylines">
<r-studyline v-for="(item,lineindex) in value.studylines"
:key="item.id"
:color='item.color'
:name='item.name'
:code='item.shortname'
:sequence='lineindex'
:numlines='value.studylines.length'
:guestmode='guestmode'
:teachermode='teachermode'
>
<template v-for="(n,index) in (value.slots+1)">
<r-studyline-slot
<!-- First paint the headings-->
<div class='r-studyplan-headings'
><r-studyline-heading v-for="(line,lineindex) in page.studylines"
:key="line.id"
v-model="page.studylines[lineindex]"
:layers='countLineLayers(line)+1'
:class=" 't-studyline' + ((lineindex%2==0)?' odd ' :' even ' )
+ ((lineindex==0)?' first ':' ')
+ ((lineindex==page.studylines.length-1)?' last ':' ')"
></r-studyline-heading
></div>
<!-- Next, paint all the cells in the scrollable -->
<div class="r-studyplan-scrollable" >
<div class="r-studyplan-timeline" :style="columns_stylerule">
<!-- Line by line add the items -->
<!-- The grid layout handles putting it in rows and columns -->
<template v-for="(line,lineindex) in page.studylines"
><template v-for="layeridx in countLineLayers(line)+1"
><template v-for="(n,index) in (page.periods+1)"
><r-studyline-slot
v-if="index > 0"
type='competency'
v-model="item.slots[index].competencies"
:key="'c-'+index"
type='gradable'
v-model="line.slots[index].competencies"
:key="'c-'+lineindex+'-'+index+'-'+layeridx"
:slotindex="index"
:lineid="item.id"
:line="line"
:plan="value"
:page="page"
:guestmode='guestmode'
:teachermode='teachermode'>
</r-studyline-slot>
<r-studyline-slot
type='filter'
v-model="item.slots[index].filters"
:key="'f-'+index"
:slotindex="index"
:lineid="item.id"
:plan="value"
:teachermode='teachermode'
:layer="layeridx-1"
:class="'t-studyline ' + ((lineindex%2==0)?' odd ':' even ')
+ ((lineindex==0 && layeridx==1)?' first ':' ')
+ ((lineindex==page.studylines.length-1)?' last ':' ')"
></r-studyline-slot
><r-studyline-slot
type='filter'
v-model="line.slots[index].filters"
:teachermode='teachermode'
:key="'f-'+lineindex+'-'+index+'-'+layeridx"
:slotindex="index"
:line="line"
:plan="value"
:page="page"
:layer="layeridx-1"
:class="'t-studyline ' + ((lineindex%2==0)?' odd ':' even ')
+ ((lineindex==0 && layeridx==1)?' first ':'')
+ ((lineindex==page.studylines.length-1)?' last ':' ')
+ ((index==page.periods)?' rightmost':'')"
>
</r-studyline-slot>
</template>
</r-studyline>
</template>
</div>
</r-studyline-slot
></template
></template
></template
></div
></div
></div>
`,
});
/*
* R-STUDYLINE
* R-STUDYLINE-HEADER
*/
Vue.component('r-studyline', {
props: ['color','name','code', 'slots','sequence','numlines','guestmode','teachermode'],
Vue.component('r-studyline-heading', {
props: {
value : {
type: Object, // Studyline
default: function(){ return {};},
},
layers: {
type: Number,
default: 1,
},
},
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('lineHeightChange', this.onLineHeightChange);
},
computed: {
},
methods: {
onLineHeightChange(lineid,layerid,newheight){
// All layers for this line have the first slot send an update message on layer height change.
// When one of those updates is received, record the height and recalculate the total height of the
// header
if(this.$refs.mainEl && lineid == this.value.id){
const items = document.querySelectorAll(
`.r-studyline-slot-0[data-studyline='${this.value.id}']`);
// determine the height of all the lines and add them up.
let heightSum = 0;
items.forEach((el) => {
// getBoundingClientRect() Gets the actual fractional height instead of rounded to integer pixels
const r = el.getBoundingClientRect();
const height = r.height;
heightSum += height;
});
const heightStyle=`${heightSum}px`;
this.$refs.mainEl.style.height = heightStyle;
}
}
},
template: `
<div :class="'r-studyline ' + ((sequence%2)?'odd':'even') +
(sequence==0?' first':'') + (sequence==numlines-1?' last':'')">
<div class="r-studyline-handle" :style="'background-color: ' + color"></div>
<div class="r-studyline-title" >
<abbr v-b-popover.hover.top :title="name">{{ code }}</abbr>
<div class="r-studyline r-studyline-heading "
:data-studyline="value.id" ref="mainEl"
><div class="r-studyline-handle" :style="'background-color: ' + value.color"></div>
<div class="r-studyline-title">
<abbr v-b-tooltip.hover :title="value.name">{{ value.shortname }}</abbr>
</div>
<slot></slot>
</div>
`,
});
Vue.component('r-studyline-slot', {
props: {
value: {
type: Array, // item to display
default(){ return [];},
},
type : {
type: String,
default: 'competency',
default: 'gradable',
},
slotindex : {
type: Number,
default: 0,
},
lineid : {
type: Number,
default: 0,
line : {
type: Object,
default(){ return null;},
},
value: {
type: Array,
default(){ return [];},
layer : {
type: Number,
},
plan: {
type: Object,
default(){ return null;}
default(){ return null;},
},
guestmode: {
type: Boolean,
@ -350,14 +439,27 @@ export default {
default: false,
}
},
computed: {
sorted(){
let copy = [...this.value];
copy.sort(function(a,b){
return a.layer - b.layer;
});
return copy;
mounted() {
const self=this;
if(self.type == "gradable" && self.slotindex == 1){
self.resizeListener = new ResizeObserver(() => {
if(self.$refs.sizeElement){
const height = self.$refs.sizeElement.getBoundingClientRect().height;
ItemEventBus.$emit('lineHeightChange', self.line.id, self.layer, height);
}
}).observe(self.$refs.sizeElement);
}
},
computed: {
item(){
for(const ix in this.value){
const itm = this.value[ix];
if(itm.layer == this.layer){
return itm;
}
}
return null;
},
},
data() {
return {
@ -367,13 +469,19 @@ export default {
},
template: `
<div :class="'r-studyline-slot '+type + ' r-studyline-slot-'+slotindex">
<r-item v-for="(item, index) in sorted"
:key="item.id" v-model="sorted[index]"
<div :class=" 'r-studyline-slot ' + type + ' '
+ 'r-studyline-slot-' + slotindex + ' '
+ ((slotindex==0)?'r-studyline-firstcolumn ':' ')"
:data-studyline="line.id" ref="sizeElement"
><div class="t-slot-item" v-if="item"
><r-item
v-model="item"
:plan="plan"
:guestmode='guestmode'
:teachermode='teachermode'></r-item></drag>
</div>
:teachermode='teachermode'></r-item
></div
></r-item
></div>
`,
});
@ -405,22 +513,22 @@ export default {
methods: {
lineColor(){
if(this.teachermode){
return "#aaa";
return "var(--gray)";
}
else{
switch(this.value.completion){
default: // "incomplete"
return "#777";
return "var(--gray)";
case "failed":
return "#933";
return "var(--danger)";
case "progress":
return "#da3";
return "var(--warning)";
case "completed":
return "#383";
return "var(--success)";
case "good":
return "#398";
return "var(--info)";
case "excellent":
return "#36f";
return "var(--blue)";
}
}
},
@ -458,11 +566,6 @@ export default {
elmWrapper.appendChild(elmLine);
lineinfo.lineElm = elmLine; // store line element so it can more easily be removed from the dom
}
setTimeout(function(){
if(lineinfo.line){
lineinfo.line.position();
}
},1);
}
},
redrawLines(){
@ -499,7 +602,6 @@ export default {
// Add resize event listener
window.addEventListener('resize',this.onWindowResize);
},
beforeDestroy(){
for(let i in this.value.connections.out){
@ -519,7 +621,6 @@ export default {
}
// Remove resize event listener
window.removeEventListener('resize',this.onWindowResize);
},
beforeUpdate(){
},

View File

@ -1524,7 +1524,7 @@ export default {
// All layers for this line have the first slot send an update message on layer height change.
// When one of those updates is received, record the height and recalculate the total height of the
// header
if(lineid == this.value.id){
if(this.$refs.mainEl && lineid == this.value.id){
const items = document.querySelectorAll(
`.t-studyline-slot-0[data-studyline='${this.value.id}']`);
@ -1672,7 +1672,8 @@ export default {
},
computed: {
item(){
for(const itm of this.value){
for(const ix in this.value){
const itm = this.value[ix];
if(itm.layer == this.layer){
return itm;
}

View File

@ -95,7 +95,7 @@ class studyplanpage {
public static function editor_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'id of studyplan'),
"name" => new \external_value(PARAM_TEXT, 'name of studyplan page'),
"fullname" => new \external_value(PARAM_TEXT, 'name of studyplan page'),
"shortname"=> new \external_value(PARAM_TEXT, 'shortname of studyplan page'),
"description"=> new \external_value(PARAM_TEXT, 'description of studyplan page'),
"periods" => new \external_value(PARAM_INT, 'number of periods in studyplan page'),
@ -110,7 +110,7 @@ class studyplanpage {
$model = [
'id' => $this->r->id,
'name' => $this->r->name,
'fullname' => $this->r->fullname,
'shortname' => $this->r->shortname,
'description' => $this->r->description,
'periods' => $this->r->periods,
@ -198,7 +198,7 @@ class studyplanpage {
$model = [
'id' => $this->r->id,
'fullname' => $this->r->name,
'fullname' => $this->r->fullname,
'shortname' => $this->r->shortname,
'description' => $this->r->description,
'periods' => $this->r->periods,

View File

@ -10,18 +10,23 @@
}
.t-studyplan-content {
.t-studyplan-content,
.r-studyplan-content {
display: flex;
}
.t-studyplan-headings {
.t-studyplan-headings,
.r-studyplan-headings {
display: block;
}
.t-studyplan-wrapper {
.t-studyplan-wrapper,
.r-studyplan-wrapper {
display: block;
}
.t-studyplan-timeline {
.t-studyplan-timeline,
.r-studyplan-timeline {
display: grid;
position: relative; /* make sure this grid is the offset for all arrows that are drawn by SimpleLine */
/* grid-template-columns will be set in the style attribute */
@ -30,27 +35,32 @@
--studyplan-course-width: auto; /* better leave this at auto for now*/
}
.t-studyplan-scrollable {
.t-studyplan-scrollable,
.r-studyplan-scrollable {
overflow-x: scroll;
scrollbar-color: var(--primary) color-mix(in srgb, var(--primary) 20%, white);
scrollbar-width: thin;
}
.t-studyplan-scrollable::-webkit-scrollbar {
.t-studyplan-scrollable::-webkit-scrollbar,
.r-studyplan-scrollable::-webkit-scrollbar {
width: 8px;
}
/* Track */
.t-studyplan-scrollable::-webkit-scrollbar-track {
.t-studyplan-scrollable::-webkit-scrollbar-track,
.r-studyplan-scrollable::-webkit-scrollbar-track {
background: color-mix(in srgb, var(--primary) 20%, white);
}
/* Handle */
.t-studyplan-scrollable::-webkit-scrollbar-thumb {
.t-studyplan-scrollable::-webkit-scrollbar-thumb,
.r-studyplan-scrollable::-webkit-scrollbar-thumb {
background:var(--primary);
}
.t-studyplan-column-heading {
.t-studyplan-column-heading,
.r-studyplan-column-heading {
color: inherit; /* placeholder */
}
@ -58,7 +68,8 @@ ul.dropdown-menu.show {
background-color: white;
}
.t-studyline {
.t-studyline,
.r-studyline {
display: grid;
grid-auto-flow: column;
/*border-bottom-style: solid;*/
@ -70,12 +81,13 @@ ul.dropdown-menu.show {
justify-content: start;
}
.t-studyline.t-studyline-heading {
.t-studyline.t-studyline-heading,
.r-studyline.r-studyline-heading {
border-right-style: none;
}
.t-studyline.end {
.t-studyline.end,
.r-studyline.end {
border-right-style: solid;
}
@ -124,7 +136,8 @@ ul.dropdown-menu.show {
margin-bottom: 1em;
}
.t-studyline-title {
.t-studyline-title,
.r-studyline-title {
padding-top: 5px;
padding-left: 10px;
width: 150px;
@ -138,7 +151,8 @@ ul.dropdown-menu.show {
}
.t-studyline-title abbr {
.t-studyline-title abbr,
.r-studyline-title abbr {
display: inline-block;
vertical-align: middle;
font-weight: bold;
@ -229,19 +243,26 @@ ul.t-competency-list li {
}
.t-studyline-slot {
.t-studyline-slot,
.r-studyline-slot{
width: 130px;
}
.t-studyline-slot.t-studyline-slot-0 {
.r-studyline-slot {
min-height: 32px;
}
.t-studyline-slot.t-studyline-slot-0,
.r-studyline-slot.r-studyline-slot-0 {
width: 75px;
}
.t-studyline-slot.t-studyline-slot-0 .t-slot-drop.filter .t-slot-item {
.t-studyline-slot.t-studyline-slot-0 .t-slot-drop.filter .t-slot-item,
.r-studyline-slot.r-studyline-slot-0 .r-item-base {
margin-left: 10px;
}
.t-slot-drop {
min-height: 32px;
height: 100%;
@ -318,7 +339,8 @@ ul.t-competency-list li {
max-width: 300px;
}
.gradable .t-slot-item {
.gradable .t-slot-item,
.gradable .r-slit-item {
width: 100%;
}
@ -422,29 +444,29 @@ ul.t-toolbox li {
}
.t-item-junction i {
color: #eebb00;
color: var(--warning);
}
.t-item-finish i {
color: #009900;
color: var(--success);
}
.t-item-start i {
color: #009900;
color: var(--success);
}
.t-item-badge svg {
color: #ddaa00;
color: var(--warning);
}
.t-slot-drop.type-allowed {
border-color: green;
border-color: var(--success);
border-style: dashed;
border-width: 1px;
}
.t-slot-drop.type-allowed.drop-forbidden {
border-color: red;
border-color: var(--danger);
}
.t-slot-drop.filter .t-item-base {
@ -528,11 +550,7 @@ a.t-item-course-config {
width: inherit;
}
.r-studyplan-content {
overflow-y: visible;
width: min-content;
position: relative;
}
.r-studyplan-tab,
.t-studyplan-tab {
@ -541,19 +559,11 @@ a.t-item-course-config {
}
.r-studyline {
width: min-content;
display: grid;
grid-auto-flow: column;
/*border-bottom-style: solid;*/
border-color: #cccccc;
border-width: 1px;
}
.t-studyline-drag:nth-child(odd) .t-studyline div,
.t-studyline-heading.odd,
.r-studyline-heading.odd,
.t-studyline-slot.odd{
.t-studyline-slot.odd,
.r-studyline-slot.odd {
background-color: #f0f0f0;
}
@ -561,7 +571,7 @@ a.t-item-course-config {
.t-studyline-heading.first,
.t-studyline-slot.first,
.r-studyline-heading.first,
.r-studyline.first {
.r-studyline-slot.first {
border-top-style: solid;
}
@ -569,12 +579,13 @@ a.t-item-course-config {
.t-studyline-heading.last,
.t-studyline-slot.last,
.r-studyline-heading.last,
.r-studyline.last {
.r-studyline-slot.last {
border-bottom-style: solid;
}
.t-studyline-slot.rightmost {
.t-studyline-slot.rightmost,
.r-studyline-slot.rightmost {
border-right-style: solid;
}
@ -588,55 +599,7 @@ a.t-item-course-config {
border-color: rgba(0, 0, 0, 0.125);
}
.r-studyline-title {
padding-top: 5px;
padding-left: 10px;
width: 130px;
flex-shrink: 0;
white-space: nowrap;
border-color: rgba(0, 0, 0, 0.125);
border-width: 1px;
border-left-style: solid;
border-right-style: solid;
display: flex;
flex-direction: column;
justify-content: center;
}
.r-studyline-title abbr {
display: inline-block;
vertical-align: middle;
font-weight: bold;
font-style: italic;
}
.r-studyline-slot {
width: 130px;
min-height: 32px;
min-width: 50px;
display: flex;
flex-shrink: 0;
flex-direction: column;
align-content: center;
justify-content: center;
}
.r-studyline-slot.r-studyline-slot-0 {
width: 75px;
}
.r-studyline-slot.r-studyline-slot-0 .r-item-base {
margin-left: 10px;
}
.r-studyline-slot.competency {
min-width: 100px;
}
.r-studyline-slot.filter {
min-width: 50px;
}
.r-item-base {
margin-top: 5px;
@ -646,7 +609,7 @@ a.t-item-course-config {
position: relative;
}
.competency .r-item-base {
.gradable .r-item-base {
width: 100%;
}
@ -674,11 +637,11 @@ a.t-item-course-config {
}
.r-item-start i {
color: #009900;
color: var(--success);
}
.r-item-badge i {
color: #ddaa00;
color: var(--warning);
}
.r-badges li {
@ -789,28 +752,28 @@ tr.r-completion-category-header {
.r-item-finish.completion-incomplete,
.r-item-junction.completion-incomplete {
color: rgb(127, 127, 127);
color: var(--gray);
}
.r-item-finish.completion-progress,
.r-item-junction.completion-progress {
color: rgb(139, 107, 0);
color: var(--warning);
}
.r-item-finish.completion-completed,
.r-item-junction.completion-completed {
color: rgb(0, 126, 0);
color: var(--success);
}
.r-item-finish.completion-good,
.r-item-junction.completion-good {
color: #398;
color: var(--info);
}
.r-item-finish.completion-excellent,
.r-item-junction.completion-excellent {
color: rgb(0, 103, 255);
color: var(--blue);
}
.r-item-finish.completion-failed,
.r-item-junction.completion-failed {
color: #933;
color: var(--danger);
}
.r-activity-icon {