diff options
Diffstat (limited to 'resources')
| -rw-r--r-- | resources/assets/stylesheets/scss/admin.scss | 5 | ||||
| -rw-r--r-- | resources/assets/stylesheets/scss/tables.scss | 10 | ||||
| -rw-r--r-- | resources/vue/apps/AdminCourses.vue | 67 | ||||
| -rw-r--r-- | resources/vue/components/SortableToggleElement.vue | 120 | ||||
| -rw-r--r-- | resources/vue/components/my-courses/TableView.vue | 26 |
5 files changed, 175 insertions, 53 deletions
diff --git a/resources/assets/stylesheets/scss/admin.scss b/resources/assets/stylesheets/scss/admin.scss index af9aa5e..63866ad 100644 --- a/resources/assets/stylesheets/scss/admin.scss +++ b/resources/assets/stylesheets/scss/admin.scss @@ -149,11 +149,6 @@ fieldset.attribute_table { display: block; } - th .course-completion { - @include icon(before, radiobutton-checked); - color: var(--color--highlight); - } - td .course-completion { @include icon(before, span-empty); color: var(--color--warning); diff --git a/resources/assets/stylesheets/scss/tables.scss b/resources/assets/stylesheets/scss/tables.scss index b2bc8c9..44740cb 100644 --- a/resources/assets/stylesheets/scss/tables.scss +++ b/resources/assets/stylesheets/scss/tables.scss @@ -269,11 +269,17 @@ tr.sortable { } } - th.sortasc { + th.sortasc:not(:has(button.as-link)) { @include icon('after', 'arr_1up', $size: $icon-size-inline, $align: top, $padding: 2px); } + th.sortdesc:not(:has(button.as-link)) { + @include icon('after', 'arr_1down', $size: $icon-size-inline, $align: top, $padding: 2px); + } - th.sortdesc { + th.sortasc button.as-link { + @include icon('after', 'arr_1up', $size: $icon-size-inline, $align: top, $padding: 2px); + } + th.sortdesc button.as-link { @include icon('after', 'arr_1down', $size: $icon-size-inline, $align: top, $padding: 2px); } diff --git a/resources/vue/apps/AdminCourses.vue b/resources/vue/apps/AdminCourses.vue index 41d84c2..c7c48e4 100644 --- a/resources/vue/apps/AdminCourses.vue +++ b/resources/vue/apps/AdminCourses.vue @@ -22,27 +22,23 @@ </colgroup> <thead> <tr class="sortable"> - <th v-if="showComplete" :class="sort.by === 'completion' ? 'sort' + sort.direction.toLowerCase() : ''"> - <a - @click.prevent="changeSort('completion')" - class="course-completion" - :title="$gettext('Bearbeitungsstatus')" - > - {{ $gettext('Bearbeitungsstatus') }} - </a> - </th> - <th v-for="activeField in sortedActivatedFields" :key="`field-${activeField}`" :class="sort.by === activeField ? 'sort' + sort.direction.toLowerCase() : ''"> - <a href="#" - @click.prevent="changeSort(activeField)" - :title="sort.by === activeField && sort.direction === 'ASC' ? $gettext('Sortiert aufsteigend nach %{field}', {field: fields[activeField]}, true) : (sort.by === activeField && sort.direction === 'DESC' ? $gettext('Sortiert absteigend nach %{ field } ', { field: fields[activeField]}, true) : $gettext('Sortieren nach %{ field }', { field: fields[activeField]}, true))" - v-if="!unsortableFields.includes(activeField)" - > - {{ fields[activeField] }} - </a> - <template v-else> - {{ fields[activeField] }} - </template> - </th> + <sortable-toggle-element v-if="showComplete" + column="completion" + v-model:sort-by="sort.by" + v-model:sort-dir="sort.direction" + :label="$gettext('Bearbeitungsstatus')" + > + <studip-icon shape="radiobutton-checked" /> + </sortable-toggle-element> + <sortable-toggle-element v-for="activeField in sortedActivatedFields" + :key="`field-${activeField}`" + :active="!unsortableFields.includes(activeField)" + :column="activeField" + v-model:sort-by="sort.by" + v-model:sort-dir="sort.direction" + > + {{ fields[activeField] }} + </sortable-toggle-element> <th class="actions"> {{ $gettext('Aktion') }} <studip-action-menu class="filter" :title="$gettext('Darstellungsfilter')" :items="availableFields" @toggleActiveField="toggleActiveField"></studip-action-menu> @@ -115,9 +111,11 @@ </template> <script> import { mapActions, mapGetters, mapState } from 'vuex'; +import SortableToggleElement from "../components/SortableToggleElement.vue"; export default { name: 'AdminCourses', + components: {SortableToggleElement}, props: { maxCourses: Number, showComplete: { @@ -244,20 +242,6 @@ export default { this.open_children = this.open_children.filter(cid => cid !== course_id); } }, - changeSort(column) { - if (this.sort.by === column) { - this.sort.direction = this.sort.direction === 'ASC' ? 'DESC' : 'ASC'; - } else { - this.currentLine = null; - this.sort.direction = 'ASC'; - } - this.sort.by = column; - - $.post(STUDIP.URLHelper.getURL('dispatch.php/admin/courses/sort'), { - sortby: column, - sortflag: this.sort.direction, - }); - }, sortArray (array) { const mappedFields = { last_activity: 'last_activity_raw', @@ -282,7 +266,7 @@ export default { let sortby = mappedFields[this.sort.by] ?? this.sort.by; // Define sort direction by this factor - const directionFactor = this.sort.direction === 'ASC' ? 1 : -1; + const directionFactor = this.sort.direction === 'asc' ? 1 : -1; // Default sort function by string comparison of field const collator = new Intl.Collator(String.locale, { @@ -323,5 +307,16 @@ export default { return STUDIP.URLHelper.getURL(url, params); }, }, + watch: { + sort: { + handler(current) { + $.post(STUDIP.URLHelper.getURL('dispatch.php/admin/courses/sort'), { + sortby: current.by, + sortflag: current.direction, + }); + }, + deep: true + } + } }; </script> diff --git a/resources/vue/components/SortableToggleElement.vue b/resources/vue/components/SortableToggleElement.vue new file mode 100644 index 0000000..5024dab --- /dev/null +++ b/resources/vue/components/SortableToggleElement.vue @@ -0,0 +1,120 @@ +<script setup lang="ts"> +import {computed, useSlots} from "vue"; +import {$gettextInterpolate} from "../../assets/javascripts/lib/gettext"; + +const props = defineProps({ + tag: { + type: String, + default: 'th' + }, + scope: { + type: String, + default: 'col', + validator(value: string | null): boolean { + return [null, 'row', 'col', 'rowgroup', 'colgroup'].includes(value); + } + }, + column: { + type: String, + required: true + }, + sortBy: { + type: String, + default : '' + }, + sortDir: { + type: String, + default: 'asc' + }, + active: { + type: Boolean, + default: true + }, + label: { + type: String, + default: null + } +}); + +const emit = defineEmits(['update:sortBy', 'update:sortDir']); +const slots = useSlots(); + +const isActive = computed(() => props.sortBy === props.column); + +const baseLabel = computed(() => { + if (props.label) { + return props.label; + } + + const vnode = slots.default?.()[0]; + return vnode?.children?.toString() ?? ''; +}); + +const ariaSort = computed(() => { + if (!props.active) { + return undefined; + } + + if (!isActive.value) { + return 'none'; + } + + return props.sortDir === 'asc' ? 'ascending' : 'descending'; +}); + +const ariaLabel = computed(() => { + if (!props.active) { + return undefined; + } + + if (!isActive.value || props.sortDir === 'desc') { + return $gettextInterpolate( + 'Sortieren nach %{label}, aufsteigend sortieren.', + {label: baseLabel.value} + ); + } + + return $gettextInterpolate( + 'Sortieren nach %{label}, absteigend sortieren.', + {label: baseLabel.value} + ); +}); + +const cssClasses = computed(() => { + if (!props.active || !isActive.value) { + return []; + } + + return props.sortDir === 'asc' ? ['sortasc'] : ['sortdesc']; +}); + +const toggleSort = () => { + let newDir = 'asc'; + if (isActive.value) { + newDir = props.sortDir === 'asc' ? 'desc' : 'asc'; + } + emit('update:sortBy', props.column); + emit('update:sortDir', newDir); +}; +</script> + +<template> + <component :is="tag" + :scope="scope" + :aria-sort="ariaSort" + :class="cssClasses" + > + <template v-if="!active"> + <slot name="default"></slot> + </template> + <button v-else + type="button" + class="as-link" + @click="toggleSort" + :title="label" + :aria-label="ariaLabel" + > + <slot name="default"></slot> + </button> + </component> +</template> diff --git a/resources/vue/components/my-courses/TableView.vue b/resources/vue/components/my-courses/TableView.vue index f8ffa85..ab4951f 100644 --- a/resources/vue/components/my-courses/TableView.vue +++ b/resources/vue/components/my-courses/TableView.vue @@ -18,16 +18,20 @@ </span> </th> <th></th> - <th v-if="displaySemNumber" :class="getOrderClasses('number')"> - <a href="#" @click.prevent="changeOrder('number')"> - {{ $gettext('Nr.') }} - </a> - </th> - <th :class="getOrderClasses('name')"> - <a href="#" @click.prevent="changeOrder('name')"> - {{ $gettext('Name') }} - </a> - </th> + <sortable-toggle-element v-if="displaySemNumber" + column="number" + v-model:sort-by="orderBy" + v-model:sort-dir="orderDir" + :label="$gettext('Veranstaltungsnummer')" + > + {{ $gettext('Nr.') }} + </sortable-toggle-element> + <sortable-toggle-element column="name" + v-model:sort-by="orderBy" + v-model:sort-dir="orderDir" + > + {{ $gettext('Name') }} + </sortable-toggle-element> <th v-if="!responsiveDisplay" >{{ $gettext('Inhalt') }}</th> <th v-if="!responsiveDisplay"></th> </tr> @@ -88,6 +92,7 @@ <script> import MyCoursesMixin from '../../mixins/MyCoursesMixin.js'; +import SortableToggleElement from "../SortableToggleElement.vue"; const defaultIconSize = parseInt( getComputedStyle(document.body).getPropertyValue('--icon-size-default'), @@ -96,6 +101,7 @@ const defaultIconSize = parseInt( export default { name: 'TableView', + components: {SortableToggleElement}, mixins: [MyCoursesMixin], props: { iconSize: { |
