Skip to content
Snippets Groups Projects
Forked from Stud.IP / Stud.IP
2193 commits behind the upstream repository.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
StudipTreeTable.vue 14.91 KiB
<template>
    <div v-if="isLoading">
        <studip-progress-indicator></studip-progress-indicator>
    </div>
    <article v-else class="studip-tree-table">
        <header>
            <tree-breadcrumb v-if="currentNode.id !== 'root'" :node="currentNode"
                             :icon="breadcrumbIcon" :editable="editable" :edit-url="editUrl" :create-url="createUrl"
                             :delete-url="deleteUrl" :show-navigation="showStructureAsNavigation"
                             :num-children="children.length" :num-courses="courses.length"
                             :assignable="assignable" :visible-children-only="visibleChildrenOnly"></tree-breadcrumb>
        </header>
        <section v-if="withChildren && !currentNode.attributes['has-children']" class="studip-tree-node-no-children">
            {{ $gettext('Auf dieser Ebene existieren keine weiteren Unterebenen.')}}
        </section>

        <span aria-live="assertive" class="sr-only">{{ assistiveLive }}</span>

        <div v-if="currentNode.attributes.description?.trim() !== ''"
             v-html="currentNode.attributes['description-formatted']"></div>

        <section v-if="thisLevelCourses === 0" class="studip-tree-node-no-courses">
            {{ $gettext('Auf dieser Ebene sind keine Veranstaltungen zugeordnet.')}}
        </section>

        <section v-if="thisLevelCourses + subLevelsCourses > 0">
            <span v-if="withCourses && showingAllCourses">
                <button type="button" @click="showAllCourses(false)"
                        :title="$gettext('Veranstaltungen auf dieser Ebene anzeigen')">
                    {{ $gettext('Veranstaltungen auf dieser Ebene anzeigen') }}
                </button>
            </span>
            <template v-if="thisLevelCourses > 0 && subLevelsCourses > 0">
                |
            </template>
            <span v-if="withCourses && subLevelsCourses > 0 && !showingAllCourses">
                <button type="button" @click="showAllCourses(true)"
                        :title="$gettext('Veranstaltungen auf allen Unterebenen anzeigen')">
                    {{ $gettext('Veranstaltungen auf allen Unterebenen anzeigen') }}
                </button>
            </span>
        </section>

        <table v-if="currentNode.attributes['has-children'] || courses.length > 0" class="default">
            <caption class="studip-tree-node-info">
                <span v-if="withChildren && children.length > 0">
                    {{ $gettextInterpolate($gettext('%{ count } Unterebenen'), { count: children.length }) }}
                </span>
                <span v-if="withChildren && children.length > 0 && withCourses && courses.length > 0">
                    ,
                </span>
            </caption>
            <colgroup>
                <col style="width: 20px">
                <col style="width: 30px">
                <col>
                <col style="width: 40%">
            </colgroup>
            <thead>
                <tr>
                    <th></th>
                    <th>{{ $gettext('Typ') }}</th>
                    <th>{{ $gettext('Name') }}</th>
                    <th>{{ $gettext('Information') }}</th>
                </tr>
            </thead>
            <draggable v-model="children" handle=".drag-handle" :animation="300"
                       @end="dropChild" tag="tbody" role="listbox">
                <tr v-for="(child, index) in children" :key="index" class="studip-tree-child">
                    <td>
                        <a v-if="editable && children.length > 1" class="drag-link" role="option"
                           tabindex="0"
                           :title="$gettextInterpolate($gettext('Sortierelement für Element %{node}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {node: child.attributes.name})"
                           @keydown="keyHandler($event, index)"
                           :ref="'draghandle-' + index">
                            <span class="drag-handle"></span>
                        </a>
                    </td>
                    <td>
                        <studip-icon :shape="child.attributes['has-children'] ? 'folder-full' : 'folder-empty'"
                                     :size="26"></studip-icon>
                    </td>
                    <td>
                        <a :href="nodeUrl(child.id, semester !== 'all' ? semester : null)" tabindex="0"
                           @click.prevent="openNode(child)"
                           :title="$gettextInterpolate($gettext('Unterebene %{ node } öffnen'),
                                { node: node.attributes.name })">
                            {{ child.attributes.name }}
                        </a>
                    </td>
                    <td>
                        <tree-node-course-info :node="child" :semester="semester"
                                               :sem-class="semClass"></tree-node-course-info>
                    </td>
                </tr>
                <tr v-for="(course) in courses" :key="course.id" class="studip-tree-child studip-tree-course">
                    <td></td>
                    <td>
                        <studip-icon shape="seminar" :size="26"></studip-icon>
                    </td>
                    <td>
                        <a :href="courseUrl(course.id)" tabindex="0"
                           :title="$gettextInterpolate($gettext('Zur Veranstaltung %{ course }'),
                                { course: course.attributes.title })">
                            <template v-if="course.attributes['course-number']">
                                {{ course.attributes['course-number'] }}
                            </template>
                            {{ course.attributes.title }}
                        </a>
                        <div :id="'course-dates-' + course.id" class="course-dates"></div>
                    </td>
                    <td :colspan="editable ? 2 : null">
                        <tree-course-details :course="course.id"></tree-course-details>
                    </td>
                </tr>
            </draggable>
        </table>
        <MountingPortal v-if="withExport" mountTo="#export-widget" name="sidebar-export">
            <tree-export-widget v-if="courses.length > 0" :title="$gettext('Download des Ergebnisses')" :url="exportUrl()"
                                :export-data="courses"></tree-export-widget>
        </MountingPortal>
        <MountingPortal v-if="withCourseAssign" mountTo="#assign-widget" name="sidebar-assign-courses">
            <assign-link-widget v-if="courses.length > 0" :node="currentNode" :courses="courses"></assign-link-widget>
        </MountingPortal>
    </article>
</template>

<script>
import draggable from 'vuedraggable';
import { TreeMixin } from '../../mixins/TreeMixin';
import TreeExportWidget from './TreeExportWidget.vue';
import TreeBreadcrumb from './TreeBreadcrumb.vue';
import StudipProgressIndicator from '../StudipProgressIndicator.vue';
import StudipIcon from '../StudipIcon.vue';
import TreeNodeCourseInfo from './TreeNodeCourseInfo.vue';
import TreeCourseDetails from "./TreeCourseDetails.vue";
import AssignLinkWidget from "./AssignLinkWidget.vue";

export default {
    name: 'StudipTreeTable',
    components: {
        draggable, TreeExportWidget, TreeCourseDetails, StudipIcon, StudipProgressIndicator, TreeBreadcrumb,
        TreeNodeCourseInfo, AssignLinkWidget
    },
    mixins: [ TreeMixin ],
    props: {
        node: {
            type: Object,
            required: true
        },
        breadcrumbIcon: {
            type: String,
            default: 'literature'
        },
        editable: {
            type: Boolean,
            default: false
        },
        editUrl: {
            type: String,
            default: ''
        },
        createUrl: {
            type: String,
            default: ''
        },
        deleteUrl: {
            type: String,
            default: ''
        },
        withCourses: {
            type: Boolean,
            default: false
        },
        withExport: {
            type: Boolean,
            default: false
        },
        withChildren: {
            type: Boolean,
            default: true
        },
        visibleChildrenOnly: {
            type: Boolean,
            default: true
        },
        assignable: {
            type: Boolean,
            default: false
        },
        withCourseAssign: {
            type: Boolean,
            default: false
        },
        semester: {
            type: String,
            default: ''
        },
        semClass: {
            type: Number,
            default: 0
        },
        showStructureAsNavigation: {
            type: Boolean,
            default: false
        }
    },
    data() {
        return {
            currentNode: this.node,
            isLoading: false,
            isLoaded: false,
            children: [],
            courses: [],
            assistiveLive: '',
            subLevelsCourses: 0,
            thisLevelCourses: 0,
            showingAllCourses: false
        }
    },
    methods: {
        openNode(node, pushState = true) {
            this.currentNode = node;
            this.$emit('change-current-node', node);

            if (this.withChildren) {
                this.getNodeChildren(node, this.visibleChildrenOnly).then(response => {
                    this.children = response.data.data;
                });
            }

            this.getNodeCourseInfo(node, this.semester, this.semClass)
                .then(response => {
                    this.thisLevelCourses = response?.data.courses;
                    this.subLevelsCourses = response?.data.allCourses;
                });

            if (this.withCourses) {

                this.getNodeCourses(node, this.semester, this.semClass, '', false)
                    .then(response => {
                        this.courses = response.data.data;
                    });
            }

            // Update browser history.
            if (pushState) {
                const nodeId = node.id;
                const url = STUDIP.URLHelper.getURL('', {node_id: nodeId});
                window.history.pushState({nodeId}, '', url);
            }

            // Update node_id for semester selector.
            const semesterSelector = document.querySelector('#semester-selector-node-id');
            semesterSelector.value = node.id;
        },
        dropChild() {
            this.updateSorting(this.currentNode.id, this.children);
        },
        keyHandler(e, index) {
            switch (e.keyCode) {
                case 38: // up
                    e.preventDefault();
                    this.decreasePosition(index);
                    this.$nextTick(() => {
                        this.$refs['draghandle-' + (index - 1)][0].focus();
                        this.assistiveLive = this.$gettextInterpolate(
                            this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
                            { pos: index, listLength: this.children.length }
                        );
                    });
                    break;
                case 40: // down
                    e.preventDefault();
                    this.increasePosition(index);
                    this.$nextTick(function () {
                        this.$refs['draghandle-' + (index + 1)][0].focus();
                        this.assistiveLive = this.$gettextInterpolate(
                            this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
                            { pos: index + 2, listLength: this.children.length }
                        );
                    });
                    break;
            }
        },
        decreasePosition(index) {
            if (index > 0) {
                const temp = this.children[index - 1];
                this.children[index - 1] = this.children[index];
                this.children[index] = temp;
                this.updateSorting(this.currentNode.id, this.children);
            }
        },
        increasePosition(index) {
            if (index < this.children.length) {
                const temp = this.children[index + 1];
                this.children[index + 1] = this.children[index];
                this.children[index] = temp;
                this.updateSorting(this.currentNode.id, this.children);
            }
        },
        showAllCourses(state) {
            this.getNodeCourses(this.currentNode, this.semester, this.semClass, '', state)
                .then(courses => {
                    this.courses = courses.data.data;
                    this.showingAllCourses = state;
                });
        }
    },
    mounted() {
        if (this.withChildren) {
            this.getNodeChildren(this.node, this.visibleChildrenOnly).then(response => {
                this.children = response.data.data;
            });
        }

        this.getNodeCourseInfo(this.currentNode, this.semester, this.semClass)
            .then(response => {
                this.thisLevelCourses = response?.data.courses;
                this.subLevelsCourses = response?.data.allCourses;
            });

        if (this.withCourses) {
            this.getNodeCourses(this.currentNode, this.semester, this.semClass)
                .then(courses => {
                    this.courses = courses.data.data;
                });
        }

        this.globalOn('open-tree-node', node => {
            STUDIP.eventBus.emit('cancel-search');
            this.openNode(node);
        });

        this.globalOn('load-tree-node', id => {
            STUDIP.eventBus.emit('cancel-search');
            this.getNode(id).then(response => {
                this.openNode(response.data.data);
            });
        });

        this.globalOn('sort-tree-children', data => {
            if (this.currentNode.id === data.parent) {
                this.children = data.children;
            }
        });

        window.addEventListener('popstate', (event) => {
            if (event.state) {
                if ('nodeId' in event.state) {
                    this.getNode(event.state.nodeId).then(response => {
                        this.openNode(response.data.data, false);
                    });
                }
            } else {
                this.openNode(this.node, false);
            }
        });

        // Add current node to semester selector widget.
        this.$nextTick(() => {
            const semesterForm = document.querySelector('#semester-selector .sidebar-widget-content form');
            const nodeField = document.createElement('input');
            nodeField.id = 'semester-selector-node-id';
            nodeField.type = 'hidden';
            nodeField.name = 'node_id';
            nodeField.value = this.node.id;
            semesterForm.appendChild(nodeField);
        });
    },
    beforeDestroy() {
        STUDIP.eventBus.off('open-tree-node');
        STUDIP.eventBus.off('load-tree-node');
        STUDIP.eventBus.off('sort-tree-children');
    }
}
</script>