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-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
@ -1131,6 +1133,26 @@ export default {
}
},
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) {
if(Array.isArray(slots)){
let count = 0;
@ -1353,32 +1375,36 @@ export default {
<!-- Line by line add the items -->
<!-- The grid layout handles putting it in rows and columns -->
<template v-for="(line,lineindex) in value.studylines"
><template v-for="(n,index) in (value.slots+1)"
><t-studyline-slot
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':'')"
><template v-for="layeridx in countLineLayers(line)+1"
><template v-for="(n,index) in (value.slots+1)"
>
</t-studyline-slot
><t-studyline-slot
type='filter'
v-model="line.slots[index].filters"
:key="'f-'+lineindex+'-'+index"
:slotindex="index"
:line="line"
:plan="value"
:class=" 't-studyline '
+ (line.sequence==1?' first':'')
+ (line.sequence==value.studylines.length?' last':'')
+ (index==value.slots?' end':'')"
>
</t-studyline-slot
<t-studyline-slot
v-if="index > 0"
type='gradable'
v-model="line.slots[index].competencies"
:key="'c-'+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':'')"
></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
></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', {
props: {
type : {
@ -1608,8 +1639,11 @@ export default {
type: Object,
default(){ return null;},
},
layer : {
type: Number,
},
value: {
type: Array,
type: Array, // dict with layer as index
default(){ return [];},
},
plan: {
@ -1640,20 +1674,35 @@ export default {
}
},
computed: {
item(){
for(const itm of this.value){
if(itm.layer == this.layer){
return itm;
}
}
return null;
},
listtype() {
return this.type;
},
dragacceptlist(){
if(this.type == "gradable"){
return ["course","competency","gradable-item"];
return ["course", "gradable-item"];
} else {
return ["filter", "filter-item"];
}
},
courseHoverDummy(){
return {course: this.hover.component};
}
},
data() {
return {
resizeListener: null,
hover: {
component:null,
type: null,
}
};
},
methods: {
@ -1666,51 +1715,34 @@ export default {
},
dragacceptcomponent(){
if(this.type == "gradable"){
return ["course","competency",];
return ["course",];
} else {
return ["filter",];
}
},
onInsert(event) {
onDrop(event) {
this.hover.component = null;
this.hover.type = null;
const self = this;
if(self.dragacceptitem().includes(event.type)) {
let item = event.data;
self.value.splice( event.index,0, item);
self.afterReorder(self.value).done(function(){
// Perform layer update - set this slot and layer here
self.relocateStudyItem(item).done(()=>{
item.layer = this.layer;
item.slot = this.slotindex;
self.value.push(item);
self.$emit("input",self.value);
});
}
else if(self.dragacceptcomponent().includes(event.type) ){
if(event.type == "competency"){
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"){
if(event.type == "course"){
call([{
methodname: 'local_treestudyplan_add_studyitem',
args: {
"line_id": self.line.id,
"slot" : self.slotindex,
"layer" : self.layer,
"type": 'course',
"details": {
"competency_id": null,
@ -1723,9 +1755,9 @@ export default {
}])[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);
self.relocateStudyItem(item).done(()=>{
self.value.push(item);
self.$emit("input",self.value);
});
}).fail(notification.exception);
}
@ -1743,9 +1775,9 @@ export default {
}])[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);
self.relocateStudyItem(item).done(()=>{
self.value.push(item);
self.$emit("input",self.value);
});
}).fail(notification.exception);
}
@ -1754,88 +1786,76 @@ export default {
onCut(event) {
const self=this;
let id = event.data.id;
for(let i = 0; i < self.value.length; i++){
if(self.value[i].id == id){
self.value.splice(i, 1); i--;
break; // just remove one
}
}
this.afterReorder(self.value);
// Do something to signal that this item has been removed
this.$emit("input",this.value);
},
onReorder(event) {
const self=this;
// 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});
}
relocateStudyItem(item){
const iteminfo = {'id': item.id, 'layer': this.layer, 'slot': this.slotindex, 'line_id': this.line.id};
return call([{
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);
},
feedbackDummy(type,data){
let item = {};
item[type] = data;
return item;
}
onDragEnter(event){
this.hover.component = event.data;
this.hover.type = event.type;
},
onDragLeave(){
this.hover.component = null;
this.hover.type = null;
},
},
template: `
<div :class="'t-studyline-slot '+type + ' t-studyline-slot-'+slotindex + ((slotindex==0)?'t-studyline-firstcolumn':'')
+ ((line.sequence%2)?' odd':' even') "
:data-studyline="line.id" ref="sizeElement"
>
<drop-list
:items="value"
><drag v-if="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
><drop v-else
:class="'t-slot-droplist '+type"
:accepts-type="dragacceptlist"
@insert="onInsert"
@reorder="onReorder"
@drop="onDrop"
mode="cut"
row
>
<template v-slot:item="{item}">
<drag
: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'"
@dragenter="onDragEnter"
@dragleave="onDragLeave"
><template v-if="hover.component">
<div v-if="hover.type == listtype+'-item'"
class="t-slot-item feedback"
:key="data.id"><t-item v-model="data" dummy></t-item></div>
<div v-else-if="type == 'competency'"
:key="hover.component.id"
><t-item v-model="hover.component" dummy></t-item
></div
><div v-else-if="hover.type == 'course'"
class="t-slot-item feedback"
:key="'competency-'+data.idnumber"><t-item-competency v-model="data"></t-item-competency></div>
<div v-else-if="type == 'course'"
:key="'course-'+hover.component.id"
><t-item-course v-model="courseHoverDummy"></t-item-course></div
><div v-else-if="hover.type == 'filter'"
class="t-slot-item feedback"
:key="'course-;'+data.id"><t-item-course v-model="feedbackDummy('course',data)"></t-item-course></div>
<div v-else-if="type == 'filter'"
key="tooldrop"
><t-item-junction v-if="hover.component.type == 'junction'" ></t-item-junction
><t-item-start v-else-if="hover.component.type == 'start'" ></t-item-start
><t-item-finish v-else-if="hover.component.type == 'finish'" ></t-item-finish
><t-item-badge v-else-if="hover.component.type == 'badge'" ></t-item-badge
></div
><div v-else
class="t-slot-item feedback"
key="tooldrop">
<t-item-junction v-if="data.type == 'junction'" ></t-item-junction>
<t-item-start v-else-if="data.type == 'start'" ></t-item-start>
<t-item-finish v-else-if="data.type == 'finish'" ></t-item-finish>
<t-item-badge v-else-if="data.type == 'badge'" ></t-item-badge>
</div>
<div v-else
class="t-slot-item feedback"
:key="type">--{{ type }}--</div>
</template>
</drop-list>
</div>
:key="hover.type">--{{ hover.type }}--</div
></template
></drop
></div>
`,
});

View file

@ -189,9 +189,8 @@ class studyitem {
public static function add($fields,$import=false)
{
global $DB;
$addable = ['line_id','type','conditions','slot','competency_id','course_id','badge_id','continuation_id'];
if($import){ $addable[] = "layer";}
$info = [ 'layer' => -1, ];
$addable = ['line_id','type','layer','conditions','slot','competency_id','course_id','badge_id','continuation_id'];
$info = [ 'layer' => 0, ];
foreach($addable as $f){
if(array_key_exists($f,$fields)){
$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),
]),
"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();
}
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());
@ -434,6 +435,7 @@ class studyplanservice extends \external_api
'type' => $type,
//'conditions' => $conditions,
'slot' => $slot,
'layer' => $layer,
'competency_id' => isset($details['competency_id'])?$details['competency_id']:null,
'course_id' => isset($details['course_id'])?$details['course_id']:null,
'badge_id' => isset($details['badge_id'])?$details['badge_id']:null,

View file

@ -1,6 +1,6 @@
<?php
$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->dependencies = [