From 551d2de1771bfc6dac6b1c53c8cc97c26d04de75 Mon Sep 17 00:00:00 2001 From: Ron Lucke <lucke@elan-ev.de> Date: Tue, 26 Sep 2023 07:25:43 +0000 Subject: [PATCH] =?UTF-8?q?=C3=9Cberarbeitung=20der=20Verwaltungsseite:=20?= =?UTF-8?q?Werkzeuge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #3089 Merge request studip/studip!2070 --- .../components/ContentModulesEditTiles.vue | 240 +++++++++++++----- .../components/ContentmodulesEditTable.vue | 138 +++++++--- resources/vue/mixins/ContentModulesMixin.js | 15 +- 3 files changed, 286 insertions(+), 107 deletions(-) diff --git a/resources/vue/components/ContentModulesEditTiles.vue b/resources/vue/components/ContentModulesEditTiles.vue index 70f7da5f3bb..5aa5bf04a96 100644 --- a/resources/vue/components/ContentModulesEditTiles.vue +++ b/resources/vue/components/ContentModulesEditTiles.vue @@ -1,66 +1,149 @@ <template> - <draggable v-model="sortedModules" handle=".dragarea"> - <transition-group name="admin_contentmodules" - class="admin_contentmodules studip-grid" - tag="div" - role="listbox" + <div class="content-modules-wrapper"> + <draggable v-model="sortedModules" handle=".dragarea"> + <transition-group + name="admin_contentmodules" + class="admin_contentmodules studip-grid" + tag="div" + role="listbox" + > + <div + v-for="module in activeModules" + :key="module.id" + role="option" + class="studip-grid-element" + :class="getModuleCSSClasses(module, activated[module.id])" + v-cloak + > + <div> + <a class="upper_part dragarea" :href="getDescriptionURL(module)" data-dialog> + <div> + <img :src="module.icon" width="40" height="40" v-if="module.icon" /> + </div> + <div> + <h3>{{ module.displayname }}</h3> + {{ module.summary }} + </div> + </a> + <div class="down_part"> + <div> + <a + class="dragarea" + tabindex="0" + :aria-label=" + $gettextInterpolate( + $gettext( + 'Sortierelement für Werkzeug %{module}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.' + ), + { module: module.displayname } + ) + " + @keydown="keyboardHandler($event, module)" + v-if="filterCategory === null" + :ref="`draghandle-${module.id}`" + > + <span class="drag-handle"></span> + </a> + <label v-if="!module.mandatory"> + <input + type="checkbox" + :checked="activated[module.id]" + @click="toggleModule(module)" + :ref="'checkbox_' + module.id" + /> + {{ $gettext('Werkzeug ist aktiv') }} + </label> + </div> + + <div class="icons_right"> + <a + href="#" + class="toggle_visibility" + role="checkbox" + v-if="!module.mandatory" + :aria-checked="module.visibility !== 'tutor' ? 'true' : 'false'" + @click.prevent="toggleModuleVisibility(module)" + > + <studip-icon + :shape=" + module.visibility !== 'tutor' + ? 'visibility-visible' + : 'visibility-invisible' + " + class="text-bottom" + :title=" + $gettextInterpolate( + $gettext( + 'Inhaltsmoduls %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten' + ), + { name: module.displayname } + ) + " + ></studip-icon> + </a> + <a :href="getRenameURL(module)" data-dialog="size=medium"> + <studip-icon + shape="edit" + class="text-bottom" + :title=" + $gettextInterpolate( + $gettext( + 'Umbenennen des Inhaltsmoduls %{ name }' + ), + { name: module.displayname } + ) + " + ></studip-icon> + </a> + </div> + </div> + </div> + </div> + </transition-group> + </draggable> + <transition-group + name="admin_contentmodules" + class="admin_contentmodules studip-grid inactive-modules" + tag="div" + role="listbox" > - <div v-for="module in sortedModules" - :key="module.id" - role="option" - class="studip-grid-element" - :class="getModuleCSSClasses(module, activated[module.id])" - v-cloak + <div + v-for="module in inactiveModules" + :key="module.id" + role="option" + class="studip-grid-element" + :class="getModuleCSSClasses(module, activated[module.id])" + v-cloak > <div> - <a :class="'upper_part' + (module.active && filterCategory === null ? ' dragarea' : '')" :href="getDescriptionURL(module)" data-dialog> + <a class="upper_part" :href="getDescriptionURL(module)" data-dialog> <div> - <img :src="module.icon" width="40" height="40" v-if="module.icon"> + <img :src="module.icon" width="40" height="40" v-if="module.icon" /> </div> <div> - <h3>{{module.displayname}}</h3> - {{module.summary}} + <h3>{{ module.displayname }}</h3> + {{ module.summary }} </div> </a> <div class="down_part"> <div> - <a class="dragarea" - tabindex="0" - :title="$gettextInterpolate('Sortierelement für Module %{module}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.', {module: module.displayname})" - @keydown="keyboardHandler($event, module)" - v-if="module.active && filterCategory === null" - :ref="`draghandle-${module.id}`"> - <span class="drag-handle"></span> - </a> <label v-if="!module.mandatory"> - <input type="checkbox" :checked="activated[module.id]" @click="toggleModule(module)" :ref="'checkbox_' + module.id"> - {{ module.active ? $gettext('Werkzeug ist aktiv') : $gettext('Werkzeug ist inaktiv') }} + <input + type="checkbox" + :checked="activated[module.id]" + @click="toggleModule(module)" + :ref="'checkbox_' + module.id" + /> + {{ $gettext('Werkzeug ist inaktiv') }} </label> </div> - - <div class="icons_right"> - <a href="#" - class="toggle_visibility" - role="checkbox" - v-if="module.active && !module.mandatory" - :aria-checked="module.visibility !== 'tutor' ? 'true' : 'false'" - @click.prevent="toggleModuleVisibility(module)"> - <studip-icon :shape="module.visibility !== 'tutor' ? 'visibility-visible' : 'visibility-invisible'" - class="text-bottom" - :title="$gettextInterpolate($gettext('Inhaltsmoduls %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'), { name: module.displayname})"></studip-icon> - </a> - <a :href="getRenameURL(module)" data-dialog="size=medium" v-if="module.active"> - <studip-icon shape="edit" - class="text-bottom" - :title="$gettextInterpolate($gettext('Umbenennen des Inhaltsmoduls %{ name }'), { name: module.displayname })"></studip-icon> - </a> - </div> </div> </div> </div> </transition-group> - </draggable> + </div> </template> + <script> import Vue from 'vue'; import { mapState } from 'vuex'; @@ -74,42 +157,41 @@ export default { timeouts: {}, }), computed: { - ...mapState('contentmodules', [ - 'modules' - ]), + ...mapState('contentmodules', ['modules']), }, methods: { toggleModule(module) { Vue.set(this.activated, module.id, !this.activated[module.id]); - - if (this.timeouts[module.id] ?? null) { - clearTimeout(this.timeouts[module.id] ?? null); - this.timeouts[module.id] = null; - } else { - this.timeouts[module.id] = setTimeout(() => { - this.toggleModuleActivation(module); - this.timeouts[module.id] = null; - }, 700); - } + this.toggleModuleActivation(module); }, }, watch: { modules: { immediate: true, handler(current) { - current.forEach(module => Vue.set(this.activated, module.id, module.active)); - } - } - } -} + current.forEach((module) => Vue.set(this.activated, module.id, module.active)); + }, + }, + }, +}; </script> + <style lang="scss" scoped> +.content-modules-wrapper { + max-width: 1410px; +} +.inactive-modules { + margin-top: 1em; + border-top: solid thin var(--content-color-40); + padding-top: 1em; +} .studip-grid-element { display: flex; flex-direction: row; background-color: var(--white); border-left: 1px solid var(--dark-gray-color-60); - transition: all var(--transition-duration) ease; + margin: 2px 0; + &.visibility-visible { border-left-color: var(--green); > div { @@ -122,6 +204,38 @@ export default { border-left-color: var(--yellow); } } + + &.sortable-ghost { + border: dashed 2px var(--content-color-40); + margin: 0; + * { + opacity: 0; + } + } + &.pulse:not(.sortable-ghost) { + box-shadow: 0 0 0 0 rgb(255, 189, 51, 1); + animation: pulse 2s; + animation-iteration-count: 1; + } + + @keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(255, 189, 51, 1); + } + 25% { + box-shadow: 0 0 0 5px rgba(255, 189, 51, 0.8); + } + 50% { + box-shadow: 0 0 0 5px rgba(255, 189, 51, 0.6); + } + 75% { + box-shadow: 0 0 0 5px rgba(255, 189, 51, 0.4); + } + 100% { + box-shadow: 0 0 0 0 rgba(255, 189, 51, 0); + } + } + > div { display: flex; flex-direction: column; diff --git a/resources/vue/components/ContentmodulesEditTable.vue b/resources/vue/components/ContentmodulesEditTable.vue index 28e28435af5..a9ab50c2a70 100644 --- a/resources/vue/components/ContentmodulesEditTable.vue +++ b/resources/vue/components/ContentmodulesEditTable.vue @@ -1,69 +1,116 @@ <template> <table class="admin_contentmodules table default"> <colgroup> - <col style="width: 20px" v-if="filterCategory === null"> - <col style="width: 20px"> - <col> - <col style="width: 24px"> + <col style="width: 20px" v-if="filterCategory === null" /> + <col style="width: 20px" /> + <col /> + <col style="width: 24px" /> </colgroup> <thead> - <tr> - <th v-if="filterCategory === null"></th> - <th></th> - <th>{{ $gettext('Name') }}</th> - <th class="actions">{{ $gettext('Aktionen') }}</th> - </tr> + <tr> + <th v-if="filterCategory === null"></th> + <th></th> + <th>{{ $gettext('Name') }}</th> + <th class="actions">{{ $gettext('Aktionen') }}</th> + </tr> </thead> - <draggable v-model="sortedModules" handle=".dragarea" tag="tbody"> - <tr v-for="module in sortedModules" - :key="module.id" - :class="getModuleCSSClasses(module)" - v-cloak> + <tr v-for="module in activeModules" :key="module.id" :class="getModuleCSSClasses(module)" v-cloak> <td v-if="filterCategory === null"> - <a class="dragarea" - tabindex="0" - :title="$gettextInterpolate('Sortierelement für Module %{module}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.', {module: module.displayname})" - @keydown="keyboardHandler($event, module)" - v-if="module.active" - :ref="`draghandle-${module.id}`" + <a + class="dragarea" + tabindex="0" + :aria-label=" + $gettextInterpolate( + $gettext( + 'Sortierelement für Module %{module}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.' + ), + { module: module.displayname } + ) + " + @keydown="keyboardHandler($event, module)" + v-if="module.active" + :ref="`draghandle-${module.id}`" > <span class="drag-handle"></span> </a> </td> <td> - <input type="checkbox" - v-model="module.active" - @click="toggleModuleActivation(module)" - v-if="!module.mandatory" - :ref="'checkbox_' + module.id"> + <input + type="checkbox" + v-model="module.active" + @click="toggleModuleActivation(module)" + v-if="!module.mandatory" + :ref="'checkbox_' + module.id" + /> </td> <td> - <a class="upper_part" - :class="{ dragrea: module.active }" - :href="getDescriptionURL(module)" - data-dialog + <a + class="upper_part" + :class="{ dragrea: module.active }" + :href="getDescriptionURL(module)" + data-dialog > - <img :src="module.icon" width="20" height="20" v-if="module.icon" class="text-bottom"> + <img :src="module.icon" width="20" height="20" v-if="module.icon" class="text-bottom" /> {{ module.displayname }} </a> </td> <td class="actions"> - <a href="#" - v-if="module.active && !module.mandatory" - role="checkbox" - :aria-checked="module.visibility !== 'tutor' ? 'true' : 'false'" - @click.prevent="toggleModuleVisibility(module)"> - <studip-icon :shape="module.visibility !== 'tutor' ? 'visibility-visible' : 'visibility-invisible'" - class="text-bottom" - :title="$gettextInterpolate($gettext('Inhaltsmoduls %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'), { name: module.displayname })"></studip-icon> + <a + href="#" + v-if="module.active && !module.mandatory" + role="checkbox" + :aria-checked="module.visibility !== 'tutor' ? 'true' : 'false'" + @click.prevent="toggleModuleVisibility(module)" + > + <studip-icon + :shape="module.visibility !== 'tutor' ? 'visibility-visible' : 'visibility-invisible'" + class="text-bottom" + :title=" + $gettextInterpolate( + $gettext( + 'Inhaltsmoduls %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten' + ), + { name: module.displayname } + ) + " + ></studip-icon> </a> <a :href="getRenameURL(module)" data-dialog="size=auto" v-if="module.active"> - <studip-icon shape="edit" class="text-bottom" :title="$gettextInterpolate($gettext('Umbenennen des Inhaltsmoduls %{ name }'), { name: module.displayname })"></studip-icon> + <studip-icon + shape="edit" + class="text-bottom" + :title=" + $gettextInterpolate($gettext('Umbenennen des Inhaltsmoduls %{ name }'), { + name: module.displayname, + }) + " + ></studip-icon> </a> </td> </tr> </draggable> + <tbody> + <tr v-for="module in inactiveModules" :key="module.id" :class="getModuleCSSClasses(module)" v-cloak> + <td></td> + <td> + <input + type="checkbox" + v-model="module.active" + @click="toggleModuleActivation(module)" + v-if="!module.mandatory" + :ref="'checkbox_' + module.id" + /> + </td> + <td> + <a class="upper_part" :href="getDescriptionURL(module)" data-dialog> + <img :src="module.icon" width="20" height="20" v-if="module.icon" class="text-bottom" /> + {{ module.displayname }} + </a> + </td> + <td></td> + </tr> + </tbody> </table> </template> @@ -74,10 +121,17 @@ export default { name: 'contentmodules-edit-table', mixins: [ContentModulesMixin], -} +}; </script> <style lang="scss"> -table.admin_contentmodules > tbody > tr { +@use '../../assets/stylesheets/mixins/colors.scss'; + +table.admin_contentmodules > tbody > tr { + &.sortable-ghost { + * { + opacity: 0; + } + } > td:first-child { background-image: linear-gradient(var(--dark-gray-color-60), var(--dark-gray-color-60)); background-repeat: no-repeat; diff --git a/resources/vue/mixins/ContentModulesMixin.js b/resources/vue/mixins/ContentModulesMixin.js index 7df586dda28..86a29ffb52a 100644 --- a/resources/vue/mixins/ContentModulesMixin.js +++ b/resources/vue/mixins/ContentModulesMixin.js @@ -19,6 +19,9 @@ export default { activeModules() { return this.sortedModules.filter(module => module.active); }, + inactiveModules() { + return this.sortedModules.filter(module => !module.active); + }, sortedModules: { get() { return Object.values(this.modules) @@ -89,6 +92,7 @@ export default { }); }, toggleModuleActivation(module) { + module.pulse = true; this.setModuleActive({ moduleId: module.id, active: !module.active, @@ -96,6 +100,7 @@ export default { if (output.tabs) { $('.tabs_wrapper').replaceWith(output.tabs); } + module.pulse = false; }); }, toggleModuleVisibility(module) { @@ -115,11 +120,17 @@ export default { return STUDIP.URLHelper.getURL(`dispatch.php/course/contentmodules/info/${module.id}`); }, getModuleCSSClasses(module, active= null) { + let classes = []; + classes.push(module.pulse ? 'pulse' : ''); + if (!(active ?? module.active)) { - return 'inactive'; + classes.push('inactive'); + } else { + classes.push(module.visibility === 'tutor' ? 'visibility-invisible' : 'visibility-visible'); } - return module.visibility === 'tutor' ? 'visibility-invisible' : 'visibility-visible'; + + return classes.join(' '); }, }, }; -- GitLab