Select Git revision
FileManager.php
Forked from
Stud.IP / Stud.IP
Source project has a limited visibility.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
CoursewareTreeItem.vue 17.62 KiB
<template>
<li v-if="showItem" :draggable="editMode ? true : null" :aria-selected="editMode ? keyboardSelected : null">
<div class="cw-tree-item-wrapper">
<span
v-if="editMode && depth > 0 && canEdit"
class="cw-sortable-handle"
:tabindex="0"
aria-describedby="operation"
ref="sortableHandle"
role="button"
@keydown="handleKeyEvent"
>
</span>
<courseware-tree-item-updater
v-if="editMode && editingItem"
:structuralElement="element"
@close="editingItem = false"
@childrenUpdated="$emit('childrenUpdated')"
/>
<router-link
v-else
:to="'/structural_element/' + element.id"
class="cw-tree-item-link"
:class="{
'cw-tree-item-link-current': isCurrent,
'cw-tree-item-link-edit': editMode,
'cw-tree-item-link-selected': keyboardSelected,
}"
>
{{ element.attributes?.title || '–' }}
<button v-if="editMode && canEdit" class="cw-tree-item-edit-button" @click.prevent="editingItem = true">
<studip-icon shape="edit" />
</button>
<span v-if="task">| {{ solverName }}</span>
<span
v-if="hasReleaseOrWithdrawDate"
class="cw-tree-item-flag-date"
:title="$gettext('Diese Seite hat eine zeitlich beschränkte Sichtbarkeit')"
></span>
<span
v-if="hasWriteApproval"
class="cw-tree-item-flag-write"
:title="$gettext('Diese Seite kann von Teilnehmenden bearbeitet werden')"
></span>
<span
v-if="hasNoReadApproval"
class="cw-tree-item-flag-cant-read"
:title="$gettext('Diese Seite kann von Teilnehmenden nicht gesehen werden')"
></span>
<template v-if="!userIsTeacher && inCourse">
<span
v-if="complete"
class="cw-tree-item-sequential cw-tree-item-sequential-complete"
:title="$gettext('Diese Seite wurde von Ihnen vollständig bearbeitet')"
>
</span>
<span
v-else
class="cw-tree-item-sequential cw-tree-item-sequential-percentage"
:title="$gettextInterpolate($gettext('Fortschritt: %{progress}%'), { progress: itemProgress })"
>
{{ itemProgress }} %
</span>
</template>
</router-link>
</div>
<ol
v-if="hasChildren && !editMode"
:class="{
'cw-tree-chapter-list': isRoot,
'cw-tree-subchapter-list': isFirstLevel,
}"
>
<courseware-tree-item
v-for="child in children"
:key="child.id"
:element="child"
:currentElement="currentElement"
:depth="depth + 1"
class="cw-tree-item"
/>
</ol>
<draggable
v-if="editMode"
:class="{ 'cw-tree-chapter-list-empty': nestedChildren.length === 0 }"
tag="ol"
:component-data="draggableData"
class="cw-tree-draggable-list"
handle=".cw-sortable-handle"
v-bind="dragOptions"
:elementId="element.id"
:list="nestedChildren"
:group="{ name: 'g1' }"
@end="endDrag"
>
<courseware-tree-item
v-for="el in nestedChildren"
:key="el.id"
:element="el"
:currentElement="currentElement"
:depth="depth + 1"
:newPos="el.newPos"
:newParentId="el.newParentId"
:siblingCount="nestedChildren.length"
class="cw-tree-item"
:elementid="el.id"
@sort="sort"
@moveItemUp="moveItemUp"
@moveItemDown="moveItemDown"
@moveItemPrevLevel="moveItemPrevLevel"
@moveItemNextLevel="moveItemNextLevel"
@childrenUpdated="$emit('childrenUpdated')"
/>
</draggable>
<ol
v-if="editMode && canEdit && isFirstLevel"
class="cw-tree-adder-list"
>
<courseware-tree-item-adder :parentId="element.id" />
</ol>
</li>
</template>
<script>
import CoursewareTreeItemAdder from './CoursewareTreeItemAdder.vue';
import CoursewareTreeItemUpdater from './CoursewareTreeItemUpdater.vue';
import draggable from 'vuedraggable';
import { mapGetters, mapActions } from 'vuex';
export default {
name: 'courseware-tree-item',
components: {
CoursewareTreeItemAdder,
CoursewareTreeItemUpdater,
draggable,
},
props: {
element: {
type: Object,
required: true,
},
currentElement: {
type: Object,
},
depth: {
type: Number,
default: 0,
},
keyboardSelectedProp: {
type: Boolean,
default: false,
},
newPos: {
type: Number,
},
newParentId: {
type: Number,
},
siblingCount: {
type: Number,
},
},
data() {
return {
keyboardSelected: false,
editingItem: false,
};
},
computed: {
...mapGetters({
childrenById: 'courseware-structure/children',
structuralElementById: 'courseware-structural-elements/byId',
context: 'context',
taskById: 'courseware-tasks/byId',
userById: 'users/byId',
groupById: 'status-groups/byId',
viewMode: 'viewMode',
courseware: 'courseware',
progressData: 'progresses',
userIsTeacher: 'userIsTeacher',
}),
draggableData() {
return {
attrs: {
role: 'listbox',
['aria-label']: this.$gettextInterpolate(this.$gettext('Unterseiten von %{elementName}'), {
elementName: this.element.attributes?.title,
}),
},
};
},
children() {
if (!this.element) {
return [];
}
return this.childrenById(this.element.id)
.map((id) => this.structuralElementById({ id }))
.filter(Boolean)
.sort((a, b) => a.attributes.position - b.attributes.position);
},
nestedChildren() {
return this.element.nestedChildren ?? [];
},
hasChildren() {
return this.childrenById(this.element.id).length;
},
isRoot() {
return this.depth === 0;
},
isFirstLevel() {
return this.depth === 1;
},
isCurrent() {
return this.element.id === this.currentElement?.id;
},
hasReleaseOrWithdrawDate() {
return (
this.element.attributes?.['release-date'] !== null ||
this.element.attributes?.['withdraw-date'] !== null
);
},
hasWriteApproval() {
const writeApproval = this.element.attributes?.['write-approval'];
if (!writeApproval || Object.keys(writeApproval).length === 0) {
return false;
}
return (
(writeApproval.all || writeApproval.groups.length > 0 || writeApproval.users.length > 0) &&
this.element.attributes?.['can-edit']
);
},
hasNoReadApproval() {
if (this.context.type === 'users') {
return false;
}
const readApproval = this.element.attributes?.['read-approval'];
if (!readApproval || Object.keys(readApproval).length === 0 || this.hasWriteApproval) {
return false;
}
return !readApproval.all && readApproval.groups.length === 0 && readApproval.users.length === 0;
},
hasPurposeClass() {
return this.purposeClass !== '';
},
purposeClass() {
if (
(this.isFirstLevel && this.context.type === 'users') ||
(this.context.type === 'courses' && this.element.attributes?.purpose === 'task')
) {
return this.element.attributes?.purpose;
}
return '';
},
task() {
if (this.element.relationships?.task?.data) {
return this.taskById({
id: this.element.relationships?.task?.data?.id,
});
}
return null;
},
taskProgress() {
return this.task ? this.task.attributes.progress + '%' : '';
},
solver() {
if (this.task) {
const solver = this.task.relationships.solver.data;
if (solver.type === 'users') {
return this.userById({ id: solver.id });
}
if (solver.type === 'status-groups') {
return this.groupById({ id: solver.id });
}
}
return null;
},
solverName() {
if (this.solver) {
if (this.solver.type === 'users') {
return this.solver.attributes['formatted-name'];
}
if (this.solver.type === 'status-groups') {
return this.solver.attributes.name;
}
}
return '';
},
isTask() {
return this.element.attributes?.purpose === 'task';
},
showItem() {
if (this.isTask) {
return this.task !== undefined;
}
return true;
},
editMode() {
return this.viewMode === 'edit';
},
dragOptions() {
return {
animation: 0,
disabled: false,
ghostClass: 'cw-tree-item-ghost',
};
},
canEdit() {
return this.element.attributes?.['can-edit'] ?? false;
},
inCourse() {
return this.context.type === 'courses';
},
progress() {
return this.progressData?.[this.element.id];
},
itemProgress() {
return this.progress?.progress?.self ?? 0;
},
complete() {
return this.itemProgress === 100;
},
},
methods: {
...mapActions({
loadTask: 'loadTask',
setAssistiveLiveContents: 'setAssistiveLiveContents',
}),
endDrag(e) {
let sortArray = [];
for (let child of e.to.childNodes) {
sortArray.push({ id: child.attributes.elementid.nodeValue, type: 'courseware-structural-elements' });
}
let data = {
id: e.item._underlying_vm_.id,
newPos: e.newIndex,
oldPos: e.oldIndex,
oldParent: e.item._underlying_vm_.relationships.parent.data.id,
newParent: e.to.__vue__.$attrs.elementId,
sortArray: sortArray,
};
if (data.oldParent === data.newParent && data.oldPos === data.newPos) {
return;
}
if (data.oldParent !== data.newParent) {
sortArray.splice(data.newPos, 0, { id: data.id, type: 'courseware-structural-elements' });
}
data.sortArray = sortArray;
this.$emit('sort', data);
},
sort(data) {
this.$emit('sort', data);
},
handleKeyEvent(e) {
switch (e.keyCode) {
case 13: // enter
e.preventDefault();
if (this.keyboardSelected) {
this.storeKeyboardSorting();
} else {
this.keyboardSelected = true;
const assistiveLive = this.$gettextInterpolate(
this.$gettext(
'%{elementTitle} ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen. Mit Pfeiltasten links und rechts kann die Position in der Hierarchie verändert werden.'
),
{
elementTitle: this.element.attributes.title,
pos: this.element.attributes.position + 1,
listLength: this.siblingCount,
}
);
this.setAssistiveLiveContents(assistiveLive);
}
break;
}
if (this.keyboardSelected) {
const data = {
element: this.element,
parents: [],
};
switch (e.keyCode) {
case 27: // esc
case 9: //tab
this.abortKeyboardSorting();
break;
case 37: // left
e.preventDefault();
this.$emit('moveItemPrevLevel', data);
break;
case 38: // up
e.preventDefault();
this.$emit('moveItemUp', data);
break;
case 39: // right
e.preventDefault();
this.$emit('moveItemNextLevel', data);
break;
case 40: // down
e.preventDefault();
this.$emit('moveItemDown', data);
break;
}
}
},
moveItemPrevLevel(data) {
data.parents.push(this.element.id);
this.$emit('moveItemPrevLevel', data);
},
moveItemUp(data) {
data.parents.push(this.element.id);
this.$emit('moveItemUp', data);
},
moveItemNextLevel(data) {
data.parents.push(this.element.id);
this.$emit('moveItemNextLevel', data);
},
moveItemDown(data) {
data.parents.push(this.element.id);
this.$emit('moveItemDown', data);
},
abortKeyboardSorting() {
this.$emit('childrenUpdated');
const assistiveLive = this.$gettextInterpolate(this.$gettext('%{elementTitle}. Neuordnung abgebrochen.'), {
elementTitle: this.element.attributes.title,
});
this.setAssistiveLiveContents(assistiveLive);
this.$nextTick(() => {
this.keyboardSelected = false;
});
},
storeKeyboardSorting() {
const data = {
id: this.element.id,
newPos: this.element.newPos,
oldPos: this.element.attributes.position,
oldParent: this.element.relationships.parent.data.id,
newParent: this.element.newParentId,
sortArray: this.element.sortArray,
};
this.keyboardSelected = false;
if (data.newParent === undefined || data.newPos === undefined) {
const assistiveLive = this.$gettextInterpolate(
this.$gettext('%{elementTitle}. Neuordnung nicht möglich.'),
{ elementTitle: this.element.attributes.title }
);
this.setAssistiveLiveContents(assistiveLive);
return;
}
if (data.oldParent === data.newParent && data.oldPos === data.newPos) {
const assistiveLive = this.$gettextInterpolate(
this.$gettext('%{elementTitle}. Neuordnung abgebrochen.'),
{ elementTitle: this.element.attributes.title }
);
this.setAssistiveLiveContents(assistiveLive);
return;
}
this.$emit('sort', data);
const assistiveLive = this.$gettextInterpolate(
this.$gettext('%{elementTitle}, abgelegt. Entgültige Position in der Liste: %{pos} von %{listLength}.'),
{ elementTitle: this.element.attributes.title, pos: data.newPos + 1, listLength: this.siblingCount }
);
this.setAssistiveLiveContents(assistiveLive);
},
},
mounted() {
if (this.element.relationships?.task?.data) {
this.loadTask({
taskId: this.element.relationships.task.data.id,
});
}
if (this.newPos || this.newParentId) {
this.keyboardSelected = true;
this.$refs.sortableHandle.focus();
}
},
watch: {
newPos() {
this.keyboardSelected = true;
this.$refs.sortableHandle.focus();
},
newParentId() {
this.keyboardSelected = true;
this.$refs.sortableHandle.focus();
},
},
};
</script>