aboutsummaryrefslogtreecommitdiff
path: root/resources
diff options
context:
space:
mode:
authorJan-Hendrik Willms <tleilax+studip@gmail.com>2025-11-14 16:30:13 +0100
committerJan-Hendrik Willms <tleilax+studip@gmail.com>2025-11-14 16:30:13 +0100
commit26fc100d1d84c55dee179049ccbf1b7c49b91564 (patch)
tree01f5a1a27e3c9ac5dadffeb92d70f9a91b5daa56 /resources
parenta1026373531f77890799ca5f306802544afb9510 (diff)
add vue component for sortable toggle elements and use it in some placesvue-sortable-element
Diffstat (limited to 'resources')
-rw-r--r--resources/assets/stylesheets/scss/admin.scss5
-rw-r--r--resources/assets/stylesheets/scss/tables.scss10
-rw-r--r--resources/vue/apps/AdminCourses.vue67
-rw-r--r--resources/vue/components/SortableToggleElement.vue120
-rw-r--r--resources/vue/components/my-courses/TableView.vue26
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: {