Multi-period courses drag and drop
This commit is contained in:
parent
f97c4cb2ef
commit
235972e2f4
|
@ -171,7 +171,15 @@ export function init(contextid,categoryid) {
|
|||
},
|
||||
contextid(){
|
||||
return contextid;
|
||||
}
|
||||
},
|
||||
filterComponentType(){
|
||||
return {
|
||||
item: true,
|
||||
component: false,
|
||||
span: 1,
|
||||
type: 'filter',
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeStudyplan() {
|
||||
|
@ -250,7 +258,6 @@ export function init(contextid,categoryid) {
|
|||
download(plan.shortname+".json",response.content,response.format);
|
||||
}).fail(notification.exception);
|
||||
},
|
||||
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -169,7 +169,6 @@ export default {
|
|||
day: 'day$core',
|
||||
rememberchoice: 'course_timing_rememberchoice',
|
||||
hidewarning: 'course_timing_hidewarning,'
|
||||
|
||||
},
|
||||
studyplan_associate: {
|
||||
associations: 'associations',
|
||||
|
@ -1285,7 +1284,7 @@ export default {
|
|||
methods: {
|
||||
countLineLayers(line){
|
||||
let maxLayer = -1;
|
||||
for(let i = 0; i <= this.value.slots; i++){
|
||||
for(let i = 0; i <= this.page.periods; i++){
|
||||
const slot = line.slots[i];
|
||||
// Determine the amount of used layers in a studyline slit
|
||||
for(const ix in line.slots[i].competencies){
|
||||
|
@ -1447,6 +1446,34 @@ export default {
|
|||
}).fail(notification.exception);
|
||||
|
||||
},
|
||||
showslot(line,index, layeridx, type){
|
||||
// check if the slot should be hidden because a previous slot has an item with a span
|
||||
// so big that it hides this slot
|
||||
const forGradable = (type == 'gradable')?true:false;
|
||||
const periods = this.page.periods;
|
||||
let show = true;
|
||||
for(let i = 0; i < periods; i++){
|
||||
if(line.slots[index-i] && line.slots[index-i].competencies){
|
||||
const list = line.slots[index-i].competencies;
|
||||
for(const ix in list){ // Really wish that for ( .. of .. ) would work with the minifier
|
||||
const item = list[ix];
|
||||
if(item.layer == layeridx){
|
||||
if(forGradable){
|
||||
if(i > 0 && (item.span - i) > 0){
|
||||
show = false;
|
||||
}
|
||||
} else {
|
||||
if((item.span - i) > 1){
|
||||
show = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return show;
|
||||
}
|
||||
}
|
||||
,
|
||||
template:
|
||||
|
@ -1540,35 +1567,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 page.studylines"
|
||||
><template v-for="layeridx in countLineLayers(line)+1"
|
||||
><template v-for="(layernr,layeridx) in countLineLayers(line)+1"
|
||||
><template v-for="(n,index) in (page.periods+1)"
|
||||
>
|
||||
<t-studyline-slot
|
||||
v-if="index > 0"
|
||||
v-if="index > 0 && showslot(line, index, layeridx, 'gradable')"
|
||||
type='gradable'
|
||||
v-model="line.slots[index].competencies"
|
||||
:key="'c-'+lineindex+'-'+index+'-'+layeridx"
|
||||
:key="'c-'+lineindex+'-'+index+'-'+layernr"
|
||||
:slotindex="index"
|
||||
:line="line"
|
||||
:plan="value"
|
||||
:page="page"
|
||||
:period="page.perioddesc[index-1]"
|
||||
:layer="layeridx-1"
|
||||
:layer="layeridx"
|
||||
:class="'t-studyline ' + ((lineindex%2==0)?' odd ':' even ')
|
||||
+ ((lineindex==0 && layeridx==1)?' first ':' ')
|
||||
+ ((lineindex==0 && layernr==1)?' first ':' ')
|
||||
+ ((lineindex==page.studylines.length-1)?' last ':' ')"
|
||||
></t-studyline-slot
|
||||
><t-studyline-slot
|
||||
type='filter'
|
||||
v-if="showslot(line, index, layeridx, 'filter')"
|
||||
v-model="line.slots[index].filters"
|
||||
:key="'f-'+lineindex+'-'+index+'-'+layeridx"
|
||||
:key="'f-'+lineindex+'-'+index+'-'+layernr"
|
||||
:slotindex="index"
|
||||
:line="line"
|
||||
:plan="value"
|
||||
:page="page"
|
||||
:layer="layeridx-1"
|
||||
:layer="layeridx"
|
||||
:class="'t-studyline ' + ((lineindex%2==0)?' odd ':' even ')
|
||||
+ ((lineindex==0 && layeridx==1)?' first ':'')
|
||||
+ ((lineindex==0 && layernr==1)?' first ':'')
|
||||
+ ((lineindex==page.studylines.length-1)?' last ':' ')
|
||||
+ ((index==page.periods)?' rightmost':'')"
|
||||
>
|
||||
|
@ -1876,6 +1904,14 @@ export default {
|
|||
else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
spanCss(){
|
||||
if(this.item && this.item.span > 1){
|
||||
const span = (2 * this.item.span) - 1;
|
||||
return `width: 100%; grid-column: span ${span};`;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
@ -1896,40 +1932,33 @@ export default {
|
|||
};
|
||||
},
|
||||
methods: {
|
||||
dragacceptitem(){
|
||||
if(this.type == "gradable"){
|
||||
return ["gradable-item"];
|
||||
} else {
|
||||
return ["filter-item"];
|
||||
}
|
||||
},
|
||||
dragacceptcomponent(){
|
||||
if(this.type == "gradable"){
|
||||
return ["course",];
|
||||
} else {
|
||||
return ["filter",];
|
||||
}
|
||||
},
|
||||
onDrop(event) {
|
||||
this.hover.component = null;
|
||||
this.hover.type = null;
|
||||
|
||||
const self = this;
|
||||
if(self.dragacceptitem().includes(event.type)) {
|
||||
if(event.type.item) {
|
||||
let item = event.data;
|
||||
|
||||
// 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);
|
||||
self.validate_course_period();
|
||||
// To avoid weird visuals with the lines,
|
||||
// we add the item to the proper place in the front-end first
|
||||
item.layer = this.layer;
|
||||
item.slot = this.slotindex;
|
||||
self.value.push(item);
|
||||
self.$emit("input",self.value);
|
||||
|
||||
// then on the next tick, we inform the back end
|
||||
// Since moving things around has never been unsuccessful, unless you have other problems,
|
||||
// it's better to have nice visuals.
|
||||
this.$nextTick(() => {
|
||||
self.relocateStudyItem(item).done(()=>{
|
||||
self.validate_course_period();
|
||||
});
|
||||
});
|
||||
}
|
||||
else if(self.dragacceptcomponent().includes(event.type) ){
|
||||
else if(event.type.component){
|
||||
|
||||
if(event.type == "course"){
|
||||
if(event.type.type == "course"){
|
||||
call([{
|
||||
methodname: 'local_treestudyplan_add_studyitem',
|
||||
args: {
|
||||
|
@ -1951,11 +1980,13 @@ export default {
|
|||
self.relocateStudyItem(item).done(()=>{
|
||||
self.value.push(item);
|
||||
self.$emit("input",self.value);
|
||||
self.validate_course_period();
|
||||
// call the validate period function on next tick,
|
||||
// since it paints the item in the slot first
|
||||
this.$nextTick(self.validate_course_period);
|
||||
});
|
||||
}).fail(notification.exception);
|
||||
}
|
||||
else if(event.type == "filter") {
|
||||
else if(event.type.type == "filter") {
|
||||
call([{
|
||||
methodname: 'local_treestudyplan_add_studyitem',
|
||||
args: {
|
||||
|
@ -2055,39 +2086,86 @@ export default {
|
|||
else if(dsi.days > 1){ s += `${dsi.days} ${this.text.days}, `;}
|
||||
|
||||
return s.toLocaleLowerCase();
|
||||
},
|
||||
maxSpan(){
|
||||
// Determine the maximum span for components in this slot
|
||||
// Used for setting the max in the timing adjustment screen (s)
|
||||
// And for checking the filter types
|
||||
|
||||
// Assume this slot is first one available
|
||||
let freeIndex = this.slotindex;
|
||||
// Determine last free slot following this one in the layer
|
||||
for(let i = this.slotindex + 1; i <= this.page.periods; i++){
|
||||
if(this.line.slots && this.line.slots[i] && this.line.slots[i].competencies){
|
||||
const l = this.line.slots[i].competencies;
|
||||
if(l[this.layer]) {
|
||||
// slot is busy in this layer.
|
||||
break;
|
||||
} else {
|
||||
freeIndex = i;
|
||||
}
|
||||
} else {
|
||||
break; // stop processing
|
||||
}
|
||||
}
|
||||
// calculate span from that
|
||||
return freeIndex - this.slotindex + 1;
|
||||
|
||||
},
|
||||
makeType(item){
|
||||
return {
|
||||
item: true,
|
||||
component: false,
|
||||
span: item.span,
|
||||
type: this.type,
|
||||
};
|
||||
},
|
||||
checkType(type) {
|
||||
if(type.type == this.type){
|
||||
if(type == 'filter'){
|
||||
return true;
|
||||
} else if(type.span <= this.maxSpan()){
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div :class="'t-studyline-slot '+type + ' t-studyline-slot-'+slotindex + ' ' + ((slotindex==0)?' t-studyline-firstcolumn ':' ')
|
||||
+ (current?'current ':' ')"
|
||||
:data-studyline="line.id" ref="main"
|
||||
:style='spanCss'
|
||||
><drag v-if="item"
|
||||
|
||||
:key="item.id"
|
||||
class="t-slot-item"
|
||||
:data="item"
|
||||
:type="type+'-item'"
|
||||
:type="makeType(item)"
|
||||
@cut="onCut"
|
||||
><t-item v-model="item" :plan="plan"></t-item
|
||||
></drag
|
||||
><drop v-else
|
||||
:class="'t-slot-drop '+type + (layer > 0?' secondary':' primary')"
|
||||
:accepts-type="dragacceptlist"
|
||||
:accepts-type="checkType"
|
||||
@drop="onDrop"
|
||||
mode="cut"
|
||||
@dragenter="onDragEnter"
|
||||
@dragleave="onDragLeave"
|
||||
><template v-if="hover.component">
|
||||
<div v-if="hover.type == listtype+'-item'"
|
||||
<div v-if="hover.type.item"
|
||||
class="t-slot-item feedback"
|
||||
:key="hover.component.id"
|
||||
><t-item v-model="hover.component" dummy></t-item
|
||||
></div
|
||||
><div v-else-if="hover.type == 'course'"
|
||||
><div v-else-if="hover.type.type == 'gradable'"
|
||||
class="t-slot-item feedback"
|
||||
:key="'course-'+hover.component.id"
|
||||
><t-item-course v-model="courseHoverDummy"></t-item-course></div
|
||||
><div v-else-if="hover.type == 'filter'"
|
||||
><div v-else-if="hover.type.type == 'filter'"
|
||||
class="t-slot-item feedback"
|
||||
key="tooldrop"
|
||||
><t-item-junction v-if="hover.component.type == 'junction'" ></t-item-junction
|
||||
|
@ -2941,110 +3019,9 @@ export default {
|
|||
|
||||
/************************************
|
||||
* *
|
||||
* Competency map Vue components *
|
||||
* Toolbox list components *
|
||||
* *
|
||||
************************************/
|
||||
Vue.component('t-competency-heading', {
|
||||
props: {
|
||||
value : {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
inuse() {
|
||||
return (this.value.inuse !== undefined && !!this.value.inuse);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onCut(){
|
||||
// console.info('cutevent-competency',event);
|
||||
this.value.inuse=true;
|
||||
this.$emit('input',this.value);
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<span class="t-competency-heading">
|
||||
<i :class="'t-'+value.type+' fa fa-puzzle-piece'" v-if="value.type == 'module'"></i>
|
||||
<i :class="'t-'+value.type+' fa fa-map'" v-else-if="value.type == 'category'"></i>
|
||||
<i :class="'t-'+value.type+' fa fa-check-square'" v-else-if="value.type == 'goal'"></i>
|
||||
<i :class="'t-'+value.type+' fa fa-circle'" v-else ></i>
|
||||
<drag
|
||||
class="draggable-competency"
|
||||
v-if="value.type == 'module' && !inuse"
|
||||
:data="value"
|
||||
type="competency"
|
||||
@cut="onCut">
|
||||
{{ value.shortname }}
|
||||
</drag>
|
||||
<span v-else-if="value.type == 'module'" class="disabled-competency">{{ value.shortname }}</span>
|
||||
<span v-else class="competency-info">{{ value.shortname }}</span>
|
||||
</span>
|
||||
`,
|
||||
});
|
||||
|
||||
Vue.component('t-competency-display', {
|
||||
props: {
|
||||
value : {
|
||||
type: Object,
|
||||
default: function(){ return {};},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dragLine: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
computed: {
|
||||
haschildren() {
|
||||
return this.value.children && this.value.children.length > 0;
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<li>
|
||||
<span v-if="haschildren" v-b-toggle="'cmp-'+value.id">
|
||||
<i class="when-closed fa fa-caret-right"></i>
|
||||
<i class="when-open fa fa-caret-down"></i>
|
||||
<t-competency-heading v-model="value"></t-competency-heading>
|
||||
</span>
|
||||
<span v-else>
|
||||
<t-competency-heading v-model="value"></t-competency-heading>
|
||||
</span>
|
||||
<b-collapse v-if="haschildren" :id="'cmp-'+value.id">
|
||||
<t-competency-list v-model="value.children"></t-competency-list>
|
||||
</b-collapse>
|
||||
</li>
|
||||
`,
|
||||
});
|
||||
Vue.component('t-competency-list', {
|
||||
props: {
|
||||
value : {
|
||||
type: Array,
|
||||
default: function(){ return [];},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
template: `
|
||||
<ul class='t-competency-list'>
|
||||
<t-competency-display
|
||||
v-for="(child, index) in value"
|
||||
:key='child.idnumber'
|
||||
v-model="value[index]"
|
||||
></t-competency-display>
|
||||
</ul>
|
||||
`,
|
||||
});
|
||||
|
||||
Vue.component('t-item-junction',{
|
||||
props: {
|
||||
value : {
|
||||
|
@ -3270,6 +3247,14 @@ export default {
|
|||
};
|
||||
},
|
||||
methods: {
|
||||
makeType(){
|
||||
return {
|
||||
item: false,
|
||||
component: true,
|
||||
span: 1, //TODO: Detect longer courses and give them an appropriate span
|
||||
type: 'gradable',
|
||||
};
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<ul class="t-course-list">
|
||||
|
@ -3277,7 +3262,7 @@ export default {
|
|||
<drag
|
||||
class="draggable-course"
|
||||
:data="course"
|
||||
type="course"
|
||||
:type="makeType()"
|
||||
@cut=""
|
||||
>
|
||||
<i class="t-course-list-item fa fa-book"></i> {{ course.shortname }} - {{ course.fullname }}
|
||||
|
|
|
@ -124,7 +124,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
onHeaderHeightChange(newheight){
|
||||
console.info("Height change event to",newheight);
|
||||
//console.info("Height change event to",newheight);
|
||||
this.$refs.main.style.height = `${newheight}px`;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -84,6 +84,7 @@ class studyitem {
|
|||
"conditions"=> new \external_value(PARAM_TEXT, 'conditions for completion'),
|
||||
"slot" => new \external_value(PARAM_INT, 'slot in the study plan'),
|
||||
"layer" => new \external_value(PARAM_INT, 'layer in the slot'),
|
||||
"span" => new \external_value(PARAM_INT, 'how many periods the item spans'),
|
||||
"course" => courseinfo::editor_structure(VALUE_OPTIONAL),
|
||||
"badge" => badgeinfo::editor_structure(VALUE_OPTIONAL),
|
||||
"continuation_id" => new \external_value(PARAM_INT, 'id of continued item'),
|
||||
|
@ -110,6 +111,7 @@ class studyitem {
|
|||
'conditions' => $this->r->conditions,
|
||||
'slot' => $this->r->slot,
|
||||
'layer' => $this->r->layer,
|
||||
'span' => $this->r->span,
|
||||
'continuation_id' => $this->r->continuation_id,
|
||||
'connections' => [
|
||||
"in" => [],
|
||||
|
@ -332,6 +334,7 @@ class studyitem {
|
|||
"completion" => new \external_value(PARAM_TEXT, 'completion state (incomplete|progress|completed|excellent)'),
|
||||
"slot" => new \external_value(PARAM_INT, 'slot in the study plan'),
|
||||
"layer" => new \external_value(PARAM_INT, 'layer in the slot'),
|
||||
"span" => new \external_value(PARAM_INT, 'how many periods the item spans'),
|
||||
"course" => courseinfo::user_structure(VALUE_OPTIONAL),
|
||||
"badge" => badgeinfo::user_structure(VALUE_OPTIONAL),
|
||||
"continuation" => self::link_structure(VALUE_OPTIONAL),
|
||||
|
@ -352,6 +355,7 @@ class studyitem {
|
|||
'completion' => completion::label($this->completion($userid)),
|
||||
'slot' => $this->r->slot,
|
||||
'layer' => $this->r->layer,
|
||||
'span' => $this->r->span,
|
||||
'connections' => [
|
||||
"in" => [],
|
||||
"out" => [],
|
||||
|
|
|
@ -366,11 +366,11 @@ ul.t-competency-list li {
|
|||
}
|
||||
|
||||
.t-slot-drop.secondary {
|
||||
min-height: 0px;
|
||||
min-height: 3px;
|
||||
}
|
||||
|
||||
.t-slot-drop.secondary.drop-allowed {
|
||||
min-height: 12px;
|
||||
min-height: 3px;
|
||||
}
|
||||
|
||||
.t-slot-item.feedback {
|
||||
|
|
|
@ -147,21 +147,21 @@ print $OUTPUT->header();
|
|||
<b-tab title="<?php t('toolbox')?>">
|
||||
<ul class="t-toolbox">
|
||||
<li><drag
|
||||
type="filter"
|
||||
:type="filterComponentType"
|
||||
:data="{type: 'junction'}"
|
||||
@cut=""
|
||||
><t-item-junction></t-item-junction><?php t("tool-junction") ?>
|
||||
<template v-slot:drag-image="{data}"><t-item-junction></t-item-junction></template>
|
||||
</drag></li>
|
||||
<li><drag
|
||||
type="filter"
|
||||
:type="filterComponentType"
|
||||
:data="{type: 'finish'}"
|
||||
@cut=""
|
||||
><t-item-finish></t-item-finish><?php t("tool-finish") ?>
|
||||
<template v-slot:drag-image="{data}"><t-item-finish></t-item-finish></template>
|
||||
</drag></li>
|
||||
<li><drag
|
||||
type="filter"
|
||||
:type="filterComponentType"
|
||||
:data="{type: 'start'}"
|
||||
@cut=""
|
||||
><t-item-start></t-item-start><?php t("tool-start") ?>
|
||||
|
@ -173,7 +173,7 @@ print $OUTPUT->header();
|
|||
<ul class="t-badges">
|
||||
<li v-for="b in badges"><img :src="b.imageurl" :alt="b.name"><drag
|
||||
class="t-badge-drag"
|
||||
type="filter"
|
||||
:type="filterComponentType"
|
||||
:data="{type: 'badge', badge: b}"
|
||||
@cut=""
|
||||
>{{b.name}}
|
||||
|
|
Loading…
Reference in New Issue
Block a user