<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>