Reworked timeline grid (stage 1)

This commit is contained in:
PMKuipers 2023-07-15 22:00:17 +02:00
parent f8896ddfdb
commit 340b0eef6b
4 changed files with 147 additions and 126 deletions

View File

@ -1,5 +1,7 @@
/*eslint no-var: "error"*/ /*eslint no-var: "error"*/
/*eslint no-console: "off"*/ /*eslint no-console: "off"*/
/*eslint no-unused-vars: warn */
/*eslint max-len: ["error", { "code": 160 }] */
/*eslint-disable no-trailing-spaces */ /*eslint-disable no-trailing-spaces */
/*eslint-env es6*/ /*eslint-env es6*/
// Put this file in path/to/plugin/amd/src // Put this file in path/to/plugin/amd/src
@ -1131,6 +1133,26 @@ export default {
} }
}, },
methods: { 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;
},
slotsempty(slots) { slotsempty(slots) {
if(Array.isArray(slots)){ if(Array.isArray(slots)){
let count = 0; let count = 0;
@ -1353,32 +1375,36 @@ export default {
<!-- Line by line add the items --> <!-- Line by line add the items -->
<!-- The grid layout handles putting it in rows and columns --> <!-- The grid layout handles putting it in rows and columns -->
<template v-for="(line,lineindex) in value.studylines" <template v-for="(line,lineindex) in value.studylines"
><template v-for="(n,index) in (value.slots+1)" ><template v-for="layeridx in countLineLayers(line)+1"
><t-studyline-slot ><template v-for="(n,index) in (value.slots+1)"
v-if="index > 0"
type='gradable'
v-model="line.slots[index].competencies"
:key="'c-'+lineindex+'-'+index"
:slotindex="index"
:line="line"
:plan="value"
:class= "'t-studyline ' + (line.sequence==1?' first':'') +
(line.sequence==value.studylines.length?' last':'')"
> >
</t-studyline-slot <t-studyline-slot
><t-studyline-slot v-if="index > 0"
type='filter' type='gradable'
v-model="line.slots[index].filters" v-model="line.slots[index].competencies"
:key="'f-'+lineindex+'-'+index" :key="'c-'+lineindex+'-'+index+'-'+layeridx"
:slotindex="index" :slotindex="index"
:line="line" :line="line"
:plan="value" :plan="value"
:class=" 't-studyline ' :layer="layeridx-1"
+ (line.sequence==1?' first':'') :class= "'t-studyline ' + ((line.sequence==1 && layeridx==1)?' first':'') +
+ (line.sequence==value.studylines.length?' last':'') (line.sequence==value.studylines.length?' last':'')"
+ (index==value.slots?' end':'')" ></t-studyline-slot
> ><t-studyline-slot
</t-studyline-slot type='filter'
v-model="line.slots[index].filters"
:key="'f-'+lineindex+'-'+index+'-'+layeridx"
:slotindex="index"
:line="line"
:plan="value"
:layer="layeridx-1"
:class=" 't-studyline '
+ ((line.sequence==1 && layeridx==1)?' first':'')
+ (line.sequence==value.studylines.length?' last':'')
+ (index==value.slots?' end':'')"
>
</t-studyline-slot
></template
></template ></template
></template ></template
></div><div :id="'studyplan-linewrapper-'+value.id" class='l-leaderline-linewrapper'></div> ></div><div :id="'studyplan-linewrapper-'+value.id" class='l-leaderline-linewrapper'></div>
@ -1594,6 +1620,11 @@ export default {
`, `,
}); });
/*
* During a redisign it was decided to have the studyline still get the entire array as a value,
* even though it only shows one drop slot for the layer it is in. This is to make repainting easier,
* since we modify the array for the layer we handle
*/
Vue.component('t-studyline-slot', { Vue.component('t-studyline-slot', {
props: { props: {
type : { type : {
@ -1608,8 +1639,11 @@ export default {
type: Object, type: Object,
default(){ return null;}, default(){ return null;},
}, },
layer : {
type: Number,
},
value: { value: {
type: Array, type: Array, // dict with layer as index
default(){ return [];}, default(){ return [];},
}, },
plan: { plan: {
@ -1640,20 +1674,35 @@ export default {
} }
}, },
computed: { computed: {
item(){
for(const itm of this.value){
if(itm.layer == this.layer){
return itm;
}
}
return null;
},
listtype() { listtype() {
return this.type; return this.type;
}, },
dragacceptlist(){ dragacceptlist(){
if(this.type == "gradable"){ if(this.type == "gradable"){
return ["course","competency","gradable-item"]; return ["course", "gradable-item"];
} else { } else {
return ["filter", "filter-item"]; return ["filter", "filter-item"];
} }
}, },
courseHoverDummy(){
return {course: this.hover.component};
}
}, },
data() { data() {
return { return {
resizeListener: null, resizeListener: null,
hover: {
component:null,
type: null,
}
}; };
}, },
methods: { methods: {
@ -1666,51 +1715,34 @@ export default {
}, },
dragacceptcomponent(){ dragacceptcomponent(){
if(this.type == "gradable"){ if(this.type == "gradable"){
return ["course","competency",]; return ["course",];
} else { } else {
return ["filter",]; return ["filter",];
} }
}, },
onInsert(event) { onDrop(event) {
this.hover.component = null;
this.hover.type = null;
const self = this; const self = this;
if(self.dragacceptitem().includes(event.type)) { if(self.dragacceptitem().includes(event.type)) {
let item = event.data; let item = event.data;
self.value.splice( event.index,0, item); // Perform layer update - set this slot and layer here
self.afterReorder(self.value).done(function(){ self.relocateStudyItem(item).done(()=>{
item.layer = this.layer;
item.slot = this.slotindex;
self.value.push(item);
self.$emit("input",self.value); self.$emit("input",self.value);
}); });
} }
else if(self.dragacceptcomponent().includes(event.type) ){ else if(self.dragacceptcomponent().includes(event.type) ){
if(event.type == "competency"){ if(event.type == "course"){
call([{
methodname: 'local_treestudyplan_add_studyitem',
args: {
"line_id": self.line.id,
"slot" : self.slotindex,
"type": 'competency',
"details": {
"competency_id": event.data.id,
'conditions':'',
'course_id':null,
'badge_id':null,
'continuation_id':null,
}
}
}])[0].done((response) => {
console.info('Add item response:', response);
let item = response;
self.value.splice(event.index, 0, item);
self.afterReorder(self.value).done(function () {
self.$emit("input", self.value);
});
}).fail(notification.exception);
}
else if(event.type == "course"){
call([{ call([{
methodname: 'local_treestudyplan_add_studyitem', methodname: 'local_treestudyplan_add_studyitem',
args: { args: {
"line_id": self.line.id, "line_id": self.line.id,
"slot" : self.slotindex, "slot" : self.slotindex,
"layer" : self.layer,
"type": 'course', "type": 'course',
"details": { "details": {
"competency_id": null, "competency_id": null,
@ -1723,9 +1755,9 @@ export default {
}])[0].done((response) => { }])[0].done((response) => {
console.info('Add item response:', response); console.info('Add item response:', response);
let item = response; let item = response;
self.value.splice(event.index, 0, item); self.relocateStudyItem(item).done(()=>{
self.afterReorder(self.value).done(function () { self.value.push(item);
self.$emit("input", self.value); self.$emit("input",self.value);
}); });
}).fail(notification.exception); }).fail(notification.exception);
} }
@ -1743,9 +1775,9 @@ export default {
}])[0].done((response) => { }])[0].done((response) => {
console.info('Add item response:', response); console.info('Add item response:', response);
let item = response; let item = response;
self.value.splice(event.index, 0, item); self.relocateStudyItem(item).done(()=>{
self.afterReorder(self.value).done(function () { self.value.push(item);
self.$emit("input", self.value); self.$emit("input",self.value);
}); });
}).fail(notification.exception); }).fail(notification.exception);
} }
@ -1754,88 +1786,76 @@ export default {
onCut(event) { onCut(event) {
const self=this; const self=this;
let id = event.data.id; let id = event.data.id;
for(let i = 0; i < self.value.length; i++){ for(let i = 0; i < self.value.length; i++){
if(self.value[i].id == id){ if(self.value[i].id == id){
self.value.splice(i, 1); i--; self.value.splice(i, 1); i--;
break; // just remove one break; // just remove one
} }
} }
this.afterReorder(self.value); // Do something to signal that this item has been removed
this.$emit("input",this.value); this.$emit("input",this.value);
}, },
onReorder(event) { relocateStudyItem(item){
const self=this; const iteminfo = {'id': item.id, 'layer': this.layer, 'slot': this.slotindex, 'line_id': this.line.id};
// apply list first
event.apply(self.value);
this.afterReorder(self.value);
},
afterReorder() {
const self=this;
// send the new order to the server
let items = [];
for(let idx in self.value)
{
self.value[idx].layer = idx;
items.push({'id': self.value[idx].id,'layer': idx, 'slot': this.slotindex, 'line_id': this.line.id});
}
return call([{ return call([{
methodname: 'local_treestudyplan_reorder_studyitems', methodname: 'local_treestudyplan_reorder_studyitems',
args: { 'items': items } args: { 'items': [iteminfo] } // function was designed to relocate multiple items at once, hence the array
}])[0].fail(notification.exception); }])[0].fail(notification.exception);
}, },
feedbackDummy(type,data){ onDragEnter(event){
let item = {}; this.hover.component = event.data;
item[type] = data; this.hover.type = event.type;
return item; },
} onDragLeave(){
this.hover.component = null;
this.hover.type = null;
},
}, },
template: ` template: `
<div :class="'t-studyline-slot '+type + ' t-studyline-slot-'+slotindex + ((slotindex==0)?'t-studyline-firstcolumn':'') <div :class="'t-studyline-slot '+type + ' t-studyline-slot-'+slotindex + ((slotindex==0)?'t-studyline-firstcolumn':'')
+ ((line.sequence%2)?' odd':' even') " + ((line.sequence%2)?' odd':' even') "
:data-studyline="line.id" ref="sizeElement" :data-studyline="line.id" ref="sizeElement"
> ><drag v-if="item"
<drop-list
:items="value" :key="item.id"
class="t-slot-item"
:data="item"
:type="type+'-item'"
@cut="onCut"
><t-item v-model="item" :plan="plan"></t-item
></drag
><drop v-else
:class="'t-slot-droplist '+type" :class="'t-slot-droplist '+type"
:accepts-type="dragacceptlist" :accepts-type="dragacceptlist"
@insert="onInsert" @drop="onDrop"
@reorder="onReorder"
mode="cut" mode="cut"
row @dragenter="onDragEnter"
> @dragleave="onDragLeave"
<template v-slot:item="{item}"> ><template v-if="hover.component">
<drag <div v-if="hover.type == listtype+'-item'"
:key="item.id"
class="t-slot-item"
:data="item"
:type="type+'-item'"
@cut="onCut"><t-item v-model="item" :plan="plan"></t-item></drag>
</template>
<template v-slot:feedback="{data,type}">
<div v-if="type == listtype+'-item'"
class="t-slot-item feedback" class="t-slot-item feedback"
:key="data.id"><t-item v-model="data" dummy></t-item></div> :key="hover.component.id"
<div v-else-if="type == 'competency'" ><t-item v-model="hover.component" dummy></t-item
></div
><div v-else-if="hover.type == 'course'"
class="t-slot-item feedback" class="t-slot-item feedback"
:key="'competency-'+data.idnumber"><t-item-competency v-model="data"></t-item-competency></div> :key="'course-'+hover.component.id"
<div v-else-if="type == 'course'" ><t-item-course v-model="courseHoverDummy"></t-item-course></div
><div v-else-if="hover.type == 'filter'"
class="t-slot-item feedback" class="t-slot-item feedback"
:key="'course-;'+data.id"><t-item-course v-model="feedbackDummy('course',data)"></t-item-course></div> key="tooldrop"
<div v-else-if="type == 'filter'" ><t-item-junction v-if="hover.component.type == 'junction'" ></t-item-junction
class="t-slot-item feedback" ><t-item-start v-else-if="hover.component.type == 'start'" ></t-item-start
key="tooldrop"> ><t-item-finish v-else-if="hover.component.type == 'finish'" ></t-item-finish
<t-item-junction v-if="data.type == 'junction'" ></t-item-junction> ><t-item-badge v-else-if="hover.component.type == 'badge'" ></t-item-badge
<t-item-start v-else-if="data.type == 'start'" ></t-item-start> ></div
<t-item-finish v-else-if="data.type == 'finish'" ></t-item-finish> ><div v-else
<t-item-badge v-else-if="data.type == 'badge'" ></t-item-badge>
</div>
<div v-else
class="t-slot-item feedback" class="t-slot-item feedback"
:key="type">--{{ type }}--</div> :key="hover.type">--{{ hover.type }}--</div
</template> ></template
</drop-list> ></drop
</div> ></div>
`, `,
}); });

View File

@ -189,9 +189,8 @@ class studyitem {
public static function add($fields,$import=false) public static function add($fields,$import=false)
{ {
global $DB; global $DB;
$addable = ['line_id','type','conditions','slot','competency_id','course_id','badge_id','continuation_id']; $addable = ['line_id','type','layer','conditions','slot','competency_id','course_id','badge_id','continuation_id'];
if($import){ $addable[] = "layer";} $info = [ 'layer' => 0, ];
$info = [ 'layer' => -1, ];
foreach($addable as $f){ foreach($addable as $f){
if(array_key_exists($f,$fields)){ if(array_key_exists($f,$fields)){
$info[$f] = $fields[$f]; $info[$f] = $fields[$f];

View File

@ -417,6 +417,7 @@ class studyplanservice extends \external_api
"continuation_id" => new \external_value(PARAM_INT, 'id of continued item',VALUE_OPTIONAL), "continuation_id" => new \external_value(PARAM_INT, 'id of continued item',VALUE_OPTIONAL),
]), ]),
"slot" => new \external_value(PARAM_INT, 'slot in the study plan',VALUE_DEFAULT), "slot" => new \external_value(PARAM_INT, 'slot in the study plan',VALUE_DEFAULT),
"layer" => new \external_value(PARAM_INT, 'layer in the slot',VALUE_OPTIONAL),
] ); ] );
} }
@ -425,7 +426,7 @@ class studyplanservice extends \external_api
return studyitem::editor_structure(); return studyitem::editor_structure();
} }
public static function add_studyitem($line_id,$type,$details,$slot=-1) public static function add_studyitem($line_id,$type,$details,$slot=-1,$layer=0)
{ {
webservicehelper::require_capabilities(self::CAP_EDIT,studyline::findById($line_id)->context()); webservicehelper::require_capabilities(self::CAP_EDIT,studyline::findById($line_id)->context());
@ -434,6 +435,7 @@ class studyplanservice extends \external_api
'type' => $type, 'type' => $type,
//'conditions' => $conditions, //'conditions' => $conditions,
'slot' => $slot, 'slot' => $slot,
'layer' => $layer,
'competency_id' => isset($details['competency_id'])?$details['competency_id']:null, 'competency_id' => isset($details['competency_id'])?$details['competency_id']:null,
'course_id' => isset($details['course_id'])?$details['course_id']:null, 'course_id' => isset($details['course_id'])?$details['course_id']:null,
'badge_id' => isset($details['badge_id'])?$details['badge_id']:null, 'badge_id' => isset($details['badge_id'])?$details['badge_id']:null,

View File

@ -1,6 +1,6 @@
<?php <?php
$plugin->component = 'local_treestudyplan'; // Recommended since 2.0.2 (MDL-26035). Required since 3.0 (MDL-48494) $plugin->component = 'local_treestudyplan'; // Recommended since 2.0.2 (MDL-26035). Required since 3.0 (MDL-48494)
$plugin->version = 2023070400; // YYYYMMDDHH (year, month, day, iteration) $plugin->version = 2023071400; // YYYYMMDDHH (year, month, day, iteration)
$plugin->requires = 2021051700; // YYYYMMDDHH (This is the release version for Moodle 3.11) $plugin->requires = 2021051700; // YYYYMMDDHH (This is the release version for Moodle 3.11)
$plugin->dependencies = [ $plugin->dependencies = [