From 39c97593bf8297ee4f137c89870f31a56ef52559 Mon Sep 17 00:00:00 2001 From: Ron Lucke <lucke@elan-ev.de> Date: Thu, 22 Jun 2023 11:41:47 +0000 Subject: [PATCH] =?UTF-8?q?Courseware=20Wizards=20Zielauswahl=20intuitiver?= =?UTF-8?q?=20und=20=C3=BCbersichtlicher=20gestalten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1980 and #2599 Merge request studip/studip!1335 --- .../assets/stylesheets/scss/courseware.scss | 30 ++ .../vue/components/StudipWizardDialog.vue | 2 +- .../CoursewareStructuralElementDialogCopy.vue | 83 ++---- .../CoursewareStructuralElementDialogLink.vue | 63 ++-- .../CoursewareStructuralElementSelector.vue | 141 +++++++++ ...oursewareStructuralElementSelectorItem.vue | 272 ++++++++++++++++++ .../CoursewareTasksDialogDistribute.vue | 80 +----- 7 files changed, 501 insertions(+), 170 deletions(-) create mode 100644 resources/vue/components/courseware/CoursewareStructuralElementSelector.vue create mode 100644 resources/vue/components/courseware/CoursewareStructuralElementSelectorItem.vue diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss index a4508514d0c..7a41ab33030 100644 --- a/resources/assets/stylesheets/scss/courseware.scss +++ b/resources/assets/stylesheets/scss/courseware.scss @@ -5665,6 +5665,36 @@ w i z a r d e l e m e n t s } } } +form.default .courseware-structural-element-selector { + list-style: none; + padding-left: 0; + .courseware-structural-element-selector-item { + .radiobutton { + background: none; + border: none; + padding: 0; + } + a label { + cursor: pointer; + } + label { + display: inline-block; + margin-bottom: 0; + text-indent: 0; + vertical-align: middle; + } + img { + vertical-align: middle; + &.inactive { + opacity: 0.5; + } + } + ul { + list-style: none; + padding-left: 18px; + } + } +} /* * * * * * * * * * * * * * * * * * w i z a r d e l e m e n t s e n d * * * * * * * * * * * * * * * * * */ diff --git a/resources/vue/components/StudipWizardDialog.vue b/resources/vue/components/StudipWizardDialog.vue index 7f73c1e160b..5f0339bd7ce 100644 --- a/resources/vue/components/StudipWizardDialog.vue +++ b/resources/vue/components/StudipWizardDialog.vue @@ -129,7 +129,7 @@ export default { }, width: { type: String, - default: '880' + default: '900' }, slots: { type: Array, diff --git a/resources/vue/components/courseware/CoursewareStructuralElementDialogCopy.vue b/resources/vue/components/courseware/CoursewareStructuralElementDialogCopy.vue index 8eda4d32583..556d0085f50 100644 --- a/resources/vue/components/courseware/CoursewareStructuralElementDialogCopy.vue +++ b/resources/vue/components/courseware/CoursewareStructuralElementDialogCopy.vue @@ -21,10 +21,10 @@ :aria-description="text.sourceSelf" /> <label v-if="inCourseContext" @click="source = 'self'" for="cw-element-copy-source-self"> - <div class="icon"><studip-icon shape="seminar" size="32"/></div> + <div class="icon"><studip-icon shape="seminar" :size="32"/></div> <div class="text">{{ text.sourceSelf }}</div> - <studip-icon shape="radiobutton-unchecked" size="24" class="unchecked" /> - <studip-icon shape="check-circle" size="24" class="check" /> + <studip-icon shape="radiobutton-unchecked" :size="24" class="unchecked" /> + <studip-icon shape="check-circle" :size="24" class="check" /> </label> <input id="cw-element-copy-source-courses" @@ -34,10 +34,10 @@ :aria-description="text.sourceCourses" /> <label @click="source = 'courses'" for="cw-element-copy-source-courses"> - <div class="icon"><studip-icon shape="seminar" size="32"/></div> + <div class="icon"><studip-icon shape="seminar" :size="32"/></div> <div class="text">{{ text.sourceCourses }}</div> - <studip-icon shape="radiobutton-unchecked" size="24" class="unchecked" /> - <studip-icon shape="check-circle" size="24" class="check" /> + <studip-icon shape="radiobutton-unchecked" :size="24" class="unchecked" /> + <studip-icon shape="check-circle" :size="24" class="check" /> </label> <input id="cw-element-copy-source-users" @@ -47,10 +47,10 @@ :aria-description="text.sourceUsers" /> <label @click="source = 'users'" for="cw-element-copy-source-users"> - <div class="icon"><studip-icon shape="content" size="32"/></div> + <div class="icon"><studip-icon shape="content" :size="32"/></div> <div class="text">{{ text.sourceUsers }}</div> - <studip-icon shape="radiobutton-unchecked" size="24" class="unchecked" /> - <studip-icon shape="check-circle" size="24" class="check" /> + <studip-icon shape="radiobutton-unchecked" :size="24" class="unchecked" /> + <studip-icon shape="check-circle" :size="24" class="check" /> </label> </fieldset> <template v-if="source === 'courses'"> @@ -75,7 +75,7 @@ > <template #open-indicator="selectAttributes"> <span v-bind="selectAttributes" - ><studip-icon shape="arr_1down" size="10" + ><studip-icon shape="arr_1down" :size="10" /></span> </template> <template #no-options="{}"> @@ -113,10 +113,10 @@ :aria-description="unit.element.attributes.title" /> <label :key="'label-' + unit.id" :for="'cw-element-copy-unit-' + unit.id"> - <div class="icon"><studip-icon shape="courseware" size="32"/></div> + <div class="icon"><studip-icon shape="courseware" :size="32"/></div> <div class="text">{{ unit.element.attributes.title }}</div> - <studip-icon shape="radiobutton-unchecked" size="24" class="unchecked" /> - <studip-icon shape="check-circle" size="24" class="check" /> + <studip-icon shape="radiobutton-unchecked" :size="24" class="unchecked" /> + <studip-icon shape="check-circle" :size="24" class="check" /> </label> </template> </fieldset> @@ -129,48 +129,17 @@ </template> <template v-slot:element> <form v-if="selectedUnit" class="default" @submit.prevent=""> - <fieldset class="radiobutton-set"> - <input id="cw-element-copy-element" type="radio" checked :aria-description="selectedElementTitle" /> - <label for="cw-element-copy-element" @click="e => e.preventDefault()"> - <div class="icon"><studip-icon shape="content2" size="32"/></div> - <div class="text">{{ selectedElementTitle }}</div> - <studip-icon shape="check-circle" size="24" class="check" /> - </label> - </fieldset> - <button - v-if="selectedElementParent" - class="button" - @click="selectElement(selectedElementParent.id)" - > - {{ $gettextInterpolate( - $gettext('zurück zu %{ parentTitle }'), - { parentTitle: selectedElementParentTitle } - ) }} - </button> - <fieldset> - <legend>{{ $gettext('Unterseiten') }}</legend> - <ul class="cw-element-selector-list"> - <li - v-for="child in children" - :key="child.id" - > - <button - class="cw-element-selector-item" - @click="selectElement(child.id)" - > - {{ child.attributes.title }} - </button> - </li> - <li v-if="children.length === 0"> - {{ $gettext('Es wurden keine Unterseiten gefunden') }} - </li> - </ul> - </fieldset> + <courseware-structural-element-selector + v-model="selectedElement" + :rootId="selectedUnitRootId" + :validateAncestors="true" + :targetId="currentElement" + /> </form> <courseware-companion-box - v-else - mood="pointing" - :msgCompanion="$gettext('Bitte wählen Sie ein Lernmaterial aus.')" + v-else + mood="pointing" + :msgCompanion="$gettext('Bitte wählen Sie ein Lernmaterial aus.')" /> </template> <template v-slot:edit> @@ -190,7 +159,7 @@ > <template #open-indicator="selectAttributes"> <span v-bind="selectAttributes" - ><studip-icon shape="arr_1down" size="10" + ><studip-icon shape="arr_1down" :size="10" /></span> </template> <template #no-options> @@ -222,6 +191,7 @@ <script> import CoursewareCompanionBox from './CoursewareCompanionBox.vue'; +import CoursewareStructuralElementSelector from './CoursewareStructuralElementSelector.vue'; import colorMixin from '@/vue/mixins/courseware/colors.js'; import StudipSelect from './../StudipSelect.vue'; import StudipWizardDialog from './../StudipWizardDialog.vue'; @@ -233,6 +203,7 @@ export default { mixins: [colorMixin], components: { CoursewareCompanionBox, + CoursewareStructuralElementSelector, StudipWizardDialog, StudipSelect, }, @@ -244,7 +215,7 @@ export default { { id: 2, valid: false, name: 'unit', title: this.$gettext('Lernmaterial'), icon: 'courseware', description: this.$gettext('Wählen Sie das Lernmaterial aus, in dem sich der zu kopierende Lerninhalt befindet.') }, { id: 3, valid: false, name: 'element', title: this.$gettext('Seite'), icon: 'content2', - description: this.$gettext('Wählen Sie die zu kopierende Seite aus. Vorausgewählt ist die oberste Seite des ausgewählten Lernmaterials. Unterseiten erreichen Sie über die Schaltflächen im Bereich "Unterseiten". Sie können über die "zurück zu" Schaltfläche das übergeordnete Element anwählen. Die ausgewählte Seite ist mit einem Kontrollhaken markiert.') }, + description: this.$gettext('Wählen Sie die zu kopierende Seite aus. Um Unterseiten anzuzeigen, klicken Sie auf den Seitennamen. Mit einem weiteren Klick werden die Unterseiten wieder zugeklappt.') }, { id: 4, valid: true, name: 'edit', title: this.$gettext('Anpassen'), icon: 'edit', description: this.$gettext('Sie können hier die Daten der zu kopierenden Seite anpassen. Eine Anpassung ist optional, Sie können die Seite auch unverändert kopieren.') }, ], @@ -472,7 +443,7 @@ export default { if (newUnit !== null) { this.wizardSlots[1].valid = true; await this.loadStructuralElement({id: this.selectedUnitRootId, options: {include: 'children'}}); - this.selectedElement = this.structuralElementById({id: this.selectedUnitRootId}); + this.selectedElement = null; } else { this.wizardSlots[1].valid = false; } diff --git a/resources/vue/components/courseware/CoursewareStructuralElementDialogLink.vue b/resources/vue/components/courseware/CoursewareStructuralElementDialogLink.vue index 38d3953527b..78c6725255f 100644 --- a/resources/vue/components/courseware/CoursewareStructuralElementDialogLink.vue +++ b/resources/vue/components/courseware/CoursewareStructuralElementDialogLink.vue @@ -41,59 +41,24 @@ /> </template> <template v-slot:element> - <form class="default" @submit.prevent=""> - <template v-if="selectedUnit"> - <fieldset class="radiobutton-set"> - <input id="cw-element-link-element" type="radio" checked :aria-description="selectedElementTitle"/> - <label for="cw-element-link-element" @click="e => e.preventDefault()"> - <div class="icon"><studip-icon shape="content2" size="32"/></div> - <div class="text">{{ selectedElementTitle }}</div> - <studip-icon shape="check-circle" size="24" class="check" /> - </label> - </fieldset> - <button - v-if="selectedElementParent" - class="button" - @click="selectElement(selectedElementParent.id)" - > - {{ $gettextInterpolate( - $gettext('zurück zu %{ parentTitle }'), - { parentTitle: selectedElementParentTitle } - ) }} - </button> - <fieldset> - <legend>{{ $gettext('Unterseiten') }}</legend> - <ul class="cw-element-selector-list"> - <li - v-for="child in children" - :key="child.id" - > - <button - class="cw-element-selector-item" - @click="selectElement(child.id)" - > - {{ child.attributes.title }} - </button> - - </li> - <li v-if="children.length === 0"> - {{ $gettext('Es wurden keine Unterseiten gefunden.') }} - </li> - </ul> - </fieldset> - </template> - <courseware-companion-box - v-if="!selectedUnit" - mood="pointing" - :msgCompanion="$gettext('Bitte wählen Sie zuerst das Lernmaterial aus.')" + <form v-if="selectedUnit" class="default" @submit.prevent=""> + <courseware-structural-element-selector + v-model="selectedElement" + :rootId="selectedUnitRootId" /> </form> + <courseware-companion-box + v-else + mood="pointing" + :msgCompanion="$gettext('Bitte wählen Sie zuerst das Lernmaterial aus.')" + /> </template> </studip-wizard-dialog> </template> <script> import CoursewareCompanionBox from './CoursewareCompanionBox.vue'; +import CoursewareStructuralElementSelector from './CoursewareStructuralElementSelector.vue'; import StudipWizardDialog from './../StudipWizardDialog.vue'; import StudipProgressIndicator from '../StudipProgressIndicator.vue'; @@ -103,6 +68,7 @@ export default { name: 'courseware-structural-element-dialog-link', components: { CoursewareCompanionBox, + CoursewareStructuralElementSelector, StudipWizardDialog, StudipProgressIndicator }, @@ -112,7 +78,7 @@ export default { {id: 1, valid: false, name: 'unit', title: this.$gettext('Lernmaterial'), icon: 'courseware', description: this.$gettext('Wählen Sie das Lernmaterial aus, in dem sich der zu verknüpfende Lerninhalt befindet. Die Lerninhalte, die verknüpft werden können, müssen unter Arbeitsplatz/Courseware vorher erstellt werden.')}, {id: 2, valid: false, name: 'element', title: this.$gettext('Seite'), icon: 'content2', - description: this.$gettext('Wählen Sie die zu verknüpfende Seite aus. Vorausgewählt ist die oberste Seite des ausgewählten Lernmaterials. Unterseiten erreichen Sie über die Schaltflächen im Bereich "Unterseiten". Sie können über die "zurück zu" Schaltfläche das übergeordnete Element anwählen. Die ausgewählte Seite ist mit einem Kontrollhaken markiert.')}, ], + description: this.$gettext('Wählen Sie die zu verknüpfende Seite aus. Um Unterseiten anzuzeigen, klicken Sie auf den Seitennamen. Mit einem weiteren Klick werden die Unterseiten wieder zugeklappt.')}, ], loadingUnits: false, selectedUnit: null, selectedElement: null, @@ -237,6 +203,9 @@ export default { if (this.selectedUnit === null) { this.requirements.push({slot: this.wizardSlots[0], text: this.$gettext('Lernmaterial') }); } + if (this.selectedElement === null) { + this.requirements.push({slot: this.wizardSlots[1], text: this.$gettext('Seite') }); + } } }, watch: { @@ -253,7 +222,7 @@ export default { if (newUnit !== null) { this.wizardSlots[0].valid = true; await this.loadStructuralElement({id: this.selectedUnitRootId, options: {include: 'children'}}); - this.selectedElement = this.structuralElementById({id: this.selectedUnitRootId}); + this.selectedElement = null; } else { this.wizardSlots[0].valid = false; } diff --git a/resources/vue/components/courseware/CoursewareStructuralElementSelector.vue b/resources/vue/components/courseware/CoursewareStructuralElementSelector.vue new file mode 100644 index 00000000000..0e8acda3bbc --- /dev/null +++ b/resources/vue/components/courseware/CoursewareStructuralElementSelector.vue @@ -0,0 +1,141 @@ +<template> + <ul class="courseware-structural-element-selector" role="radiogroup"> + <courseware-structural-element-selector-item + v-if="rootElement !== null" + :element="rootElement" + :siblings="[]" + :selectedId="selectedId" + :focusedElementId="focusedElementId" + :rootId="rootId" + :validateAncestors="validateAncestors" + :targetId="targetId" + :targetAncestors="targetAncestors" + :selectablePurposes="selectablePurposes" + @input="handleInput" + @focus="handleFocus" + /> + </ul> +</template> + +<script> +import CoursewareStructuralElementSelectorItem from './CoursewareStructuralElementSelectorItem.vue'; +import { mapActions, mapGetters } from 'vuex' + +export default { + name: 'courseware-structural-element-selector', + components: { + CoursewareStructuralElementSelectorItem + }, + model: { + prop: 'element' + }, + props: { + element: { + type: Object + }, + rootId: { + type: String, + required: true + }, + validateAncestors: { + type: Boolean, + default: false + }, + targetId: { + type: String, + default: null + }, + selectablePurposes: { + type: Array, + default: () => [] + } + }, + data() { + return { + rootElement: null, + focusedElementId: '' + }; + }, + computed: { + ...mapGetters({ + userId: 'userId', + coursewareUnits: 'courseware-units/all', + structuralElementById: 'courseware-structural-elements/byId', + context: 'context', + childrenById: 'courseware-structure/children', + currentElement: 'currentElement' + }), + children() { + if (!this.rootElement) { + return []; + } + + return this.childrenById(this.rootElement.id) + .map((id) => this.structuralElementById({ id })) + .filter(Boolean); + }, + selectedId() { + return this.element?.id ?? ''; + }, + targetElement() { + return this.structuralElementById({ id: this.targetId }); + }, + targetAncestors() { + if (!this.targetElement || !this.validateAncestors) { + return []; + } + + const finder = (parent) => { + const parentId = parent.relationships?.parent?.data?.id; + if (!parentId) { + return null; + } + const element = this.structuralElementById({ id: parentId }); + if (!element) { + console.error(`CoursewareStructuralElement#ancestors: Could not find parent by ID: "${parentId}".`); + } + + return element; + }; + + const visitAncestors = function* (node) { + const parent = finder(node); + if (parent) { + yield parent; + yield* visitAncestors(parent); + } + }; + + return [...visitAncestors(this.targetElement)].reverse(); + }, + }, + methods: { + ...mapActions({ + loadStructuralElement: 'courseware-structural-elements/loadById', + companionError: 'companionError', + companionSuccess: 'companionSuccess', + }), + handleInput(id) { + this.$emit('input', this.structuralElementById({ id })); + this.focusedElementId = id; + }, + handleFocus(id) { + this.focusedElementId = id; + }, + async loadRootElement(rootId) { + this.rootElement = null; + await this.loadStructuralElement({id: rootId, options: {include: 'children'}}); + this.rootElement = this.structuralElementById({ id: rootId}); + } + }, + async mounted() { + await this.loadRootElement(this.rootId); + }, + watch: { + async rootId(newRootId) { + await this.loadRootElement(newRootId); + this.focusedElementId = ''; + } + } +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareStructuralElementSelectorItem.vue b/resources/vue/components/courseware/CoursewareStructuralElementSelectorItem.vue new file mode 100644 index 00000000000..f6b3cf6a66e --- /dev/null +++ b/resources/vue/components/courseware/CoursewareStructuralElementSelectorItem.vue @@ -0,0 +1,272 @@ +<template> + <li class="courseware-structural-element-selector-item"> + <span + class="radiobutton" + :tabindex="tabindex" + :ref="'radiobutton-' + element.id" + role="radio" + :aria-checked="selected ? 'true' : 'false'" + :aria-labelledby="labelId" + @click="handleClickInput(element.id)" + @keydown="handleKeyInput($event, element.id)" + > + <template v-if="selectable"> + <studip-icon v-if="selected" shape="radiobutton-checked" /> + <studip-icon v-else shape="radiobutton-unchecked" /> + </template> + <studip-icon v-else shape="decline" role="inactive" /> + </span> + <template v-if="hasChildren"> + <a href="#" :aria-expanded="isOpen ? 'true' : 'false'" @click.prevent="toggleChildrenVisibility"> + <studip-icon v-if="!isOpen" shape="arr_1right" /> + <studip-icon v-if="isOpen" shape="arr_1down" /> + <label :id="labelId"> + {{ element.attributes.title }} + <span v-if="!selectable" class="sr-only">{{ $gettext('nicht wählbar') }}</span> + </label> + </a> + <ul v-if="isOpen"> + <courseware-structural-element-selector-item + v-for="child in children" + :key="child.id" + :element="child" + :siblings="children" + :selectedId="selectedId" + :focusedElementId="focusedElementId" + :rootId="rootId" + :validateAncestors="validateAncestors" + :targetId="targetId" + :targetAncestors="targetAncestors" + :selectablePurposes="selectablePurposes" + @input="handleInput" + @focus="handleFocus" + @selectable="updateSelectable" + /> + </ul> + </template> + <template v-else> + <studip-icon shape="arr_1right" role="inactive" class="inactive"/> + <label :id="labelId"> + {{ element.attributes.title }} + <span v-if="!selectable" class="sr-only">{{ $gettext('nicht wählbar') }}</span> + </label> + </template> + </li> +</template> +<script> +import { mapActions, mapGetters } from 'vuex' + +export default { + name: 'courseware-structural-element-selector-item', + props: { + element: { + type: Object + }, + siblings: { + type: Array + }, + selectedId: { + type: String, + required: true + }, + focusedElementId: { + type: String + }, + rootId: { + type: String, + required: true + }, + validateAncestors: { + type: Boolean, + default: false + }, + targetId: { + type: String, + default: null + }, + targetAncestors: { + type: Array + }, + selectablePurposes: { + type: Array + } + }, + data() { + return { + isOpen: false, + selectable: true, + } + }, + computed: { + ...mapGetters({ + userId: 'userId', + coursewareUnits: 'courseware-units/all', + structuralElementById: 'courseware-structural-elements/byId', + context: 'context', + currentElement: 'currentElement' + }), + children() { + const children = this.element?.relationships?.children?.data?.map(child => child.id); + if (!children) { + return []; + } + + return children.map((id) => this.structuralElementById({ id })).filter(Boolean); + }, + hasChildren() { + return this.children.length > 0; + }, + selected() { + return this.selectedId === this.element?.id; + }, + focused() { + return this.focusedElementId === this.element?.id; + }, + labelId() { + return this.element.id + '_checkbox-label'; + }, + isRoot() { + return this.rootId === this.element.id; + }, + tabindex() { + if (this.focusedElementId !== '') { + return this.focused ? 0 : -1; + } + return this.isRoot ? 0 : -1; + }, + nextElementId() { + if (this.hasChildren && this.isOpen) { + return this.children[0].id; + } + + return this.nextSiblingId; + }, + nextSiblingId() { + if (this.isRoot) { + return null; + } + const index = this.siblings.findIndex(element => element.id === this.element.id) + 1; + if (this.siblings.length > index) { + return this.siblings[index].id; + } else { + return this.$parent.nextSiblingId; + } + }, + previousElementId() { + if (this.isRoot) { + return null; + } + const index = this.siblings.findIndex(element => element.id === this.element.id) - 1; + if (index > -1) { + const childrenCount = this.siblings[index].relationships.children.data.length; + const previousElement = this.$parent.$children.find(child => child.element?.id === this.siblings[index].id); + if (childrenCount > 0 && previousElement.isOpen) { + const element = this.structuralElementById({ id: this.siblings[index].relationships.children.data[childrenCount - 1].id }); + if ( + element.relationships.children.data.length > 0 + && previousElement.$children.find(child => child.element?.id === element.id).isOpen + ) { + return element.relationships.children.data[element.relationships.children.data.length -1].id; + } + return element.id; + } else { + return this.siblings[index].id; + } + } else { + return this.$parent.element.id; + } + }, + }, + methods: { + ...mapActions({ + loadStructuralElement: 'courseware-structural-elements/loadById', + companionError: 'companionError', + companionSuccess: 'companionSuccess', + }), + loadChildren() { + const children = this.element?.relationships?.children?.data?.map(child => child.id) ?? []; + children.forEach((id) => this.loadStructuralElement({id: id, options: {include: 'children'}})); + }, + toggleChildrenVisibility() { + if (!this.isOpen) { + this.loadChildren(); + } + this.isOpen = !this.isOpen; + }, + handleInput(id) { + this.$emit('input', id); + }, + handleFocus(id) { + this.$emit('focus', id); + }, + validate() { + if ( + this.element.id === this.targetId + || this.targetAncestors.find(ancestor => ancestor.id === this.element.id) + ) { + this.selectable = false; + this.$emit('selectable', false); + } + }, + updateSelectable() { + this.selectable = false; + this.$emit('selectable', false); + }, + filterSelectablePurposes() { + if (this.selectablePurposes.length === 0) { + return; + } + this.selectable = this.selectablePurposes.includes(this.element.attributes.purpose); + }, + handleClickInput(id) { + if (this.selectable) { + this.handleInput(id); + } + }, + handleKeyInput(event, id) { + switch(event.keyCode) { + case 37: // arrow left + case 38: // arrow up + event.preventDefault(); + if (this.previousElementId !== null) { + this.$emit('focus', this.previousElementId); + } + break; + case 39: // arrow right + case 40: // arrow down + event.preventDefault(); + if (this.nextElementId !== null) { + this.$emit('focus', this.nextElementId); + } + break; + } + }, + selectRoot() { + if (this.focusedElementId === '' && this.isRoot && this.selectable) { + this.handleInput(this.element.id); + } + } + }, + mounted() { + this.loadChildren(); + if (this.validateAncestors) { + this.validate(); + } + this.filterSelectablePurposes(); + this.selectRoot(); + }, + watch: { + focusedElementId(newId) { + if (this.focused) { + this.$refs['radiobutton-'+ this.element.id].focus(); + if (this.selectable) { + this.handleInput(newId); + } else { + this.handleInput(''); + } + } + this.selectRoot(); + } + } +} +</script> diff --git a/resources/vue/components/courseware/CoursewareTasksDialogDistribute.vue b/resources/vue/components/courseware/CoursewareTasksDialogDistribute.vue index 4a8f64b94b9..78d6d46d684 100644 --- a/resources/vue/components/courseware/CoursewareTasksDialogDistribute.vue +++ b/resources/vue/components/courseware/CoursewareTasksDialogDistribute.vue @@ -43,36 +43,11 @@ </template> <template v-slot:task> <form v-if="selectedSourceUnit" class="default" @submit.prevent=""> - <fieldset class="radiobutton-set"> - <input - id="cw-task-dist-task" - type="radio" - :checked="selectedTaskIsTask" - :aria-description="selectedTaskTitle" - /> - <label for="cw-task-dist-task" @click="(e) => e.preventDefault()"> - <div class="icon"><studip-icon shape="content2" size="32" /></div> - <div class="text">{{ selectedTaskTitle }}</div> - <studip-icon v-if="selectedTaskIsTask" shape="check-circle" size="24" class="check" /> - <studip-icon v-else shape="decline-circle" size="24" class="unchecked" /> - </label> - </fieldset> - <button v-if="selectedTaskParent" class="button" @click="selectTask(selectedTaskParent.id)"> - {{ $gettext('zurück zur übergeordneten Seite') }} - </button> - <fieldset> - <legend>{{ $gettext('Unterseiten') }}</legend> - <ul class="cw-element-selector-list"> - <li v-for="child in taskChildren" :key="child.id"> - <button class="cw-element-selector-item" @click="selectTask(child.id)"> - {{ child.attributes.title }} - </button> - </li> - <li v-if="taskChildren.length === 0"> - {{ $gettext('Es wurden keine Unterseiten gefunden.') }} - </li> - </ul> - </fieldset> + <courseware-structural-element-selector + v-model="selectedTask" + :rootId="selectedSourceUnitRootId" + :selectablePurposes="['template']" + /> </form> <courseware-companion-box v-else @@ -145,39 +120,10 @@ </template> <template v-slot:targetelement> <form v-if="selectedTargetUnit && selectedTaskIsTask" class="default" @submit.prevent=""> - <fieldset class="radiobutton-set"> - <input - id="cw-task-dist-target-element" - type="radio" - checked - :aria-description="selectedTargetElementTitle" - /> - <label for="cw-task-dist-target-element" @click="(e) => e.preventDefault()"> - <div class="icon"><studip-icon shape="content2" size="32" /></div> - <div class="text">{{ selectedTargetElementTitle }}</div> - <studip-icon shape="check-circle" size="24" class="check" /> - </label> - </fieldset> - <button - v-if="selectedTargetElementParent" - class="button" - @click="selectTargetElement(selectedTargetElementParent.id)" - > - {{ $gettext('zurück zur übergeordneten Seite') }} - </button> - <fieldset> - <legend>{{ $gettext('Unterseiten') }}</legend> - <ul class="cw-element-selector-list"> - <li v-for="child in targetChildren" :key="child.id"> - <button class="cw-element-selector-item" @click="selectTargetElement(child.id)"> - {{ child.attributes.title }} - </button> - </li> - <li v-if="targetChildren.length === 0"> - {{ $gettext('Es wurden keine Unterseiten gefunden.') }} - </li> - </ul> - </fieldset> + <courseware-structural-element-selector + v-model="selectedTargetElement" + :rootId="selectedTargetUnitRootId" + /> </form> <courseware-companion-box v-if="!selectedTaskIsTask" @@ -292,6 +238,7 @@ <script> import CoursewareCompanionBox from './CoursewareCompanionBox.vue'; +import CoursewareStructuralElementSelector from './CoursewareStructuralElementSelector.vue'; import StudipWizardDialog from './../StudipWizardDialog.vue'; import { mapActions, mapGetters } from 'vuex'; @@ -300,6 +247,7 @@ export default { name: 'courseware-tasks-dialog-distribute', components: { CoursewareCompanionBox, + CoursewareStructuralElementSelector, StudipWizardDialog, }, data() { @@ -322,7 +270,7 @@ export default { title: this.$gettext('Aufgabenvorlage'), icon: 'category-task', description: this.$gettext( - 'Wählen Sie die zu verteilende Aufgabenvorlage aus. Vorausgewählt ist die oberste Seite des ausgewählten Lernmaterials. Unterseiten erreichen Sie über die Schaltflächen im Bereich "Unterseiten". Sie können über die "zurück zu" Schaltfläche das übergeordnete Element anwählen. Die ausgewählte Aufgabenvorlage ist mit einem Kontrollhaken markiert. Nur Seiten der Kategorie "Aufgabenvorlage" können verteilt werden.' + 'Wählen Sie die zu verteilende Aufgabenvorlage aus. Vorausgewählt ist die oberste Seite des ausgewählten Lernmaterials. Um Unterseiten anzuzeigen, klicken Sie auf den Seitennamen. Mit einem weiteren Klick werden die Unterseiten wieder zugeklappt. Nur Seiten der Kategorie "Aufgabenvorlage" können verteilt werden.' ), }, { @@ -352,7 +300,7 @@ export default { title: this.$gettext('Zielseite'), icon: 'content2', description: this.$gettext( - 'Wählen Sie hier die Seite aus unterhalb der die Aufgabe verteilt werden soll. Zum bearbeiten der Aufgabe müssen Lernende Zugriff auf die Seite haben. Prüfen Sie ggf. die Leserechte und die Sichtbarkeit.' + 'Wählen Sie hier die Seite aus unterhalb der die Aufgabe verteilt werden soll. Um Unterseiten anzuzeigen, klicken Sie auf den Seitennamen. Mit einem weiteren Klick werden die Unterseiten wieder zugeklappt. Zum Bearbeiten der Aufgabe müssen Lernende Zugriff auf die Seite haben. Prüfen Sie ggf. die Leserechte und die Sichtbarkeit.' ), }, { @@ -679,7 +627,7 @@ export default { id: this.selectedTargetUnitRootId, options: { include: 'children' }, }); - this.selectedTargetElement = this.structuralElementById({ id: this.selectedTargetUnitRootId }); + this.selectedTargetElement = null; } else { this.wizardSlots[3].valid = false; } -- GitLab