diff options
| author | Ron Lucke <lucke@elan-ev.de> | 2023-12-11 10:37:55 +0000 |
|---|---|---|
| committer | Ron Lucke <lucke@elan-ev.de> | 2023-12-11 10:37:55 +0000 |
| commit | f2767008ffcf723bc73a4ea8781b31f23c2b68e5 (patch) | |
| tree | 55d6c4b96b65f42545d9a624263089f5f5cb06a4 /resources/vue/components/courseware | |
| parent | 47e23c2e78b4cc099dc6217fa0827a033ef0dc9f (diff) | |
TIC #3111
Closes #3111
Merge request studip/studip!2295
Diffstat (limited to 'resources/vue/components/courseware')
11 files changed, 539 insertions, 182 deletions
diff --git a/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue b/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue index ef111bf..dd619dd 100644 --- a/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue @@ -1,29 +1,17 @@ <template> <div class="cw-block cw-block-table-of-contents"> - <courseware-default-block - :block="block" - :canEdit="canEdit" - :isTeacher="isTeacher" - :preview="true" - @showEdit="initCurrentData" - @storeEdit="storeText" - @closeEdit="initCurrentData" - > + <courseware-default-block :block="block" :canEdit="canEdit" :isTeacher="isTeacher" :preview="true" + @showEdit="initCurrentData" @storeEdit="storeText" @closeEdit="initCurrentData"> <template #content> <div v-if="childElementsWithTasks.length > 0"> <div v-if="currentStyle !== 'tiles' && currentTitle !== ''" class="cw-block-title"> {{ currentTitle }} </div> - <ul - v-if="currentStyle === 'list-details' || currentStyle === 'list'" - :class="['cw-block-table-of-contents-' + currentStyle]" - > + <ul v-if="currentStyle === 'list-details' || currentStyle === 'list'" + :class="['cw-block-table-of-contents-' + currentStyle]"> <li v-for="child in childElementsWithTasks" :key="child.id"> <router-link :to="'/structural_element/' + child.id"> - <div - class="cw-block-table-of-contents-title-box" - :class="[child.attributes.payload.color]" - > + <div class="cw-block-table-of-contents-title-box" :class="[child.attributes.payload.color]"> {{ child.attributes.title }} <span v-if="child.attributes.purpose === 'task'"> | {{ child.solverName }}</span> <p v-if="currentStyle === 'list-details'"> @@ -34,63 +22,37 @@ </li> </ul> <ul v-if="currentStyle === 'tiles'" class="cw-block-table-of-contents-tiles cw-tiles"> - <li - v-for="child in childElementsWithTasks" - :key="child.id" - class="tile" - :class="[child.attributes.payload.color]" - > - <router-link - :to="'/structural_element/' + child.id" - :title=" - child.attributes.purpose === 'task' - ? child.attributes.title + ' | ' + child.solverName - : child.attributes.title - " - > - <div - class="preview-image" - :class="[hasImage(child) ? '' : 'default-image']" - :style="getChildStyle(child)" - > - <div v-if="child.attributes.purpose === 'task'" class="overlay-text"> - {{ child.solverName }} - </div> - </div> - <div class="description"> - <header - :class="[ - child.attributes.purpose !== '' - ? 'description-icon-' + child.attributes.purpose - : '', - ]" - > - {{ child.attributes.title || '–' }} - </header> - <div class="description-text-wrapper"> - <p>{{ child.attributes.payload.description }}</p> - </div> - <footer> - {{ countChildChildren(child) }} - <translate :translate-n="countChildChildren(child)" translate-plural="Seiten"> - Seite - </translate> - </footer> - </div> + <li v-for="child in childElementsWithTasks" :key="child.id"> + <router-link :to="'/structural_element/' + child.id" :title="child.attributes.purpose === 'task' + ? child.attributes.title + ' | ' + child.solverName + : child.attributes.title + "> + <courseware-tile tag="div" :color="child.attributes.payload.color" + :title="child.attributes.title" :imageUrl="getChildImageUrl(child)"> + <template #description> + {{ child.attributes.payload.description }} + </template> + <template #footer> + {{ + $gettextInterpolate( + $ngettext( + '%{length} Seite', + '%{length} Seiten', + countChildChildren(child) + ), + { length: countChildChildren(child) }) + }} + </template> + </courseware-tile> </router-link> </li> </ul> </div> - <courseware-companion-box - v-if="viewMode === 'edit' && childElementsWithTasks.length === 0" - :msgCompanion=" - $gettext( - 'Es sind noch keine Unterseiten vorhanden. ' + - 'Sobald Sie weitere Unterseiten anlegen, erscheinen diese automatisch hier im Inhaltsverzeichnis.' - ) - " - mood="pointing" - /> + <courseware-companion-box v-if="viewMode === 'edit' && childElementsWithTasks.length === 0" :msgCompanion="$gettext( + 'Es sind noch keine Unterseiten vorhanden. ' + + 'Sobald Sie weitere Unterseiten anlegen, erscheinen diese automatisch hier im Inhaltsverzeichnis.' + ) + " mood="pointing" /> </template> <template v-if="canEdit" #edit> <form class="default" @submit.prevent=""> @@ -115,13 +77,14 @@ <script> import BlockComponents from './block-components.js'; +import CoursewareTile from '../layouts/CoursewareTile.vue'; import blockMixin from '@/vue/mixins/courseware/block.js'; import { mapActions, mapGetters } from 'vuex'; export default { name: 'courseware-table-of-contents-block', mixins: [blockMixin], - components: Object.assign(BlockComponents, {}), + components: Object.assign(BlockComponents, { CoursewareTile }), props: { block: Object, canEdit: Boolean, @@ -208,14 +171,8 @@ export default { containerId: this.block.relationships.container.data.id, }); }, - getChildStyle(child) { - let url = child.relationships?.image?.meta?.['download-url']; - - if (url) { - return { 'background-image': 'url(' + url + ')' }; - } else { - return {}; - } + getChildImageUrl(child) { + return child.relationships?.image?.meta?.['download-url']; }, countChildChildren(child) { return this.childrenById(child.id).length + 1; diff --git a/resources/vue/components/courseware/layouts/CoursewareTile.vue b/resources/vue/components/courseware/layouts/CoursewareTile.vue index dd7e173..1b37511 100644 --- a/resources/vue/components/courseware/layouts/CoursewareTile.vue +++ b/resources/vue/components/courseware/layouts/CoursewareTile.vue @@ -1,6 +1,7 @@ <template> <component :is="tag" class="cw-tile" :class="[color]"> - <div class="preview-image" :class="[hasImage ? '' : 'default-image']" :style="previewImageStyle"> + <studip-ident-image v-model="identimage" :baseColor="tileColor.hex" :pattern="title" /> + <div class="preview-image" :style="previewImageStyle"> <div v-if="handle" class="overlay-handle cw-tile-handle" @@ -40,10 +41,16 @@ </template> <script> +import colorMixin from '@/vue/mixins/courseware/colors.js'; +import StudipIdentImage from './../../StudipIdentImage.vue'; import { mapGetters } from 'vuex'; export default { name: 'courseware-tile', + mixins: [colorMixin], + components: { + StudipIdentImage + }, props: { tag: { type: String, @@ -111,6 +118,11 @@ export default { type: String } }, + data() { + return { + identimage: '', + }; + }, computed: { ...mapGetters({ userIsTeacher: 'userIsTeacher' @@ -127,9 +139,9 @@ export default { previewImageStyle() { if (this.hasImage) { return { 'background-image': 'url(' + this.imageUrl + ')' }; - } else { - return {}; - } + } + + return { 'background-image': 'url(' + this.identimage + ')' }; }, progressTitle() { if (this.userIsTeacher) { @@ -140,6 +152,9 @@ export default { hasDescriptionLink() { return this.descriptionLink !== ''; }, + tileColor() { + return this.mixinColors.find((color) => color.class === this.color); + }, }, methods: { showProgress(e) { diff --git a/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue b/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue new file mode 100644 index 0000000..1a739a6 --- /dev/null +++ b/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue @@ -0,0 +1,246 @@ +<template> + <div class="cw-root-content-hint" v-if="hideRoot"> + <courseware-companion-box + :msgCompanion=" + $gettext( + 'In diesem Lernmaterial wird die Startseite ausgeblendet. Dies können Sie in den Einstellungen des Lernmaterials ändern. Wenn Sie die Einstellung beibehalten wollen, legen Sie bitte eine Seite an.' + ) + " + > + <template v-slot:companionActions> + <button v-if="canEdit" class="button" @click="addPage"> + {{ $gettext('Eine Seite hinzufügen') }} + </button> + </template> + </courseware-companion-box> + </div> + <div v-else class="cw-root-content-wrapper"> + <div class="cw-root-content" :class="['cw-root-content-' + rootLayout]"> + <div class="cw-root-content-img" :style="image"> + <section class="cw-root-content-description" :style="bgColor"> + <img v-if="imageIsSet" class="cw-root-content-description-img" :src="imageURL" /> + <template v-else> + <studip-ident-image + class="cw-root-content-description-img" + v-model="identImageCanvas" + :showCanvas="true" + :baseColor="bgColorHex" + :pattern="structuralElement.attributes.title" + /> + <studip-ident-image + v-model="identImage" + :width="1095" + :height="withTOC ? 300 : 480" + :baseColor="bgColorHex" + :pattern="structuralElement.attributes.title" + /> + </template> + <div class="cw-root-content-description-text"> + <h1>{{ structuralElement.attributes.title }}</h1> + <p> + {{ structuralElement.attributes.payload.description }} + </p> + </div> + </section> + </div> + </div> + <div v-if="withTOC" class="cw-root-content-toc"> + <ul class="cw-tiles"> + <li + v-for="child in childElements" + :key="child.id" + class="tile" + :class="[child.attributes.payload.color]" + > + <router-link :to="'/structural_element/' + child.id" :title="child.attributes.title"> + <div + v-if="hasImage(child)" + class="preview-image" + :style="getChildStyle(child)" + ></div> + <studip-ident-image + v-else + :baseColor="getColor(child).hex" + :pattern="child.attributes.title" + :showCanvas="true" + /> + <div class="description"> + <header + :class="[ + child.attributes.purpose !== '' + ? 'description-icon-' + child.attributes.purpose + : '', + ]" + > + {{ child.attributes.title || '–' }} + </header> + <div class="description-text-wrapper"> + <p>{{ child.attributes.payload.description }}</p> + </div> + <footer> + {{ countChildChildren(child) }} + <translate :translate-n="countChildChildren(child)" translate-plural="Seiten"> + Seite + </translate> + </footer> + </div> + </router-link> + </li> + </ul> + </div> + </div> +</template> + +<script> +import CoursewareCompanionBox from './../layouts/CoursewareCompanionBox.vue'; +import StudipIdentImage from './../../StudipIdentImage.vue'; +import colorMixin from '@/vue/mixins/courseware/colors.js'; +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-root-content', + mixins: [colorMixin], + props: { + structuralElement: Object, + canEdit: Boolean, + }, + components: { + CoursewareCompanionBox, + StudipIdentImage, + }, + data() { + return { + identImage: '', + identImageCanvas: '', + }; + }, + computed: { + ...mapGetters({ + rootLayout: 'rootLayout', + childrenById: 'courseware-structure/children', + structuralElementById: 'courseware-structural-elements/byId', + context: 'context', + }), + imageURL() { + return this.structuralElement.relationships?.image?.meta?.['download-url']; + }, + imageIsSet() { + return this.imageURL !== undefined; + }, + image() { + let style = {}; + const backgroundURL = this.imageIsSet ? this.imageURL : this.identImage; + + style.backgroundImage = 'url(' + backgroundURL + ')'; + style.height = this.withTOC ? '300px' : '480px'; + + return style; + }, + bgColorHex() { + const elementColor = this.structuralElement?.attributes?.payload?.color ?? 'studip-blue'; + const color = this.mixinColors.find((c) => { + return c.class === elementColor; + }); + return color.hex; + }, + bgColor() { + return { 'background-color': this.bgColorHex }; + }, + withTOC() { + return this.rootLayout === 'toc'; + }, + hideRoot() { + return this.rootLayout === 'none'; + }, + childElements() { + return this.childrenById(this.structuralElement.id).map((id) => this.structuralElementById({ id })); + }, + }, + methods: { + ...mapActions({ + showElementAddDialog: 'showElementAddDialog', + }), + getChildStyle(child) { + let url = child.relationships?.image?.meta?.['download-url']; + + if (url) { + return { 'background-image': 'url(' + url + ')' }; + } else { + return {}; + } + }, + countChildChildren(child) { + return this.childrenById(child.id).length + 1; + }, + hasImage(child) { + return child.relationships?.image?.data !== null; + }, + getColor(child) { + return this.mixinColors.find((color) => color.class === child.attributes.payload.color); + }, + addPage() { + this.showElementAddDialog(true); + } + }, +}; +</script> +<style scoped lang="scss"> +.cw-root-content { + max-width: 1095px; + margin-bottom: 1em; + overflow: hidden; + .cw-root-content-img { + background-position: center; + background-size: cover; + background-repeat: no-repeat; + } + .cw-root-content-description { + display: flex; + flex-direction: row; + margin: 0 8em; + padding: 2em 4px 2em 2em; + position: relative; + top: 8em; + + .cw-root-content-description-img { + width: 240px; + height: fit-content; + margin-right: 2em; + } + .cw-root-content-description-text { + max-height: calc(480px - 18em); + overflow-y: auto; + &::-webkit-scrollbar { + width: 2px; + } + + &::-webkit-scrollbar-track { + box-shadow: inset 0 0 6px rgba(255, 255, 255, 0.3); + } + + &::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.4); + } + h1, + p { + color: #fff; + margin-right: 2em; + } + } + } +} +.cw-root-content-toc { + max-width: 1095px; + margin-bottom: 1em; + .cw-root-content-description { + margin: 0 8em; + top: 1.5em; + .cw-root-content-description-text { + max-height: calc(300px - 6em); + } + } +} +.cw-root-content-hint { + max-width: 1095px; +} +</style> diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue index 34d7fcb..03b2cec 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue @@ -100,15 +100,16 @@ </template> </courseware-companion-box> <courseware-empty-element-box - v-if="showEmptyElementBox" + v-if="empty && !showRootLayout" :canEdit="canEdit" :noContainers="noContainers" /> - <courseware-welcome-screen v-if="noContainers && isRoot && canEdit" /> </div> + <courseware-root-content v-if="showRootLayout" :structuralElement="currentElement" :canEdit="canEdit" /> + <div - v-if="canVisit && !editView && !isLink" + v-if="canVisit && !editView && !isLink && !hideRootContent" class="cw-container-wrapper" :class="{ 'cw-container-wrapper-consume': consumeMode, @@ -131,6 +132,7 @@ class="cw-container-item" /> </div> + <div v-if="isLink" class="cw-container-wrapper" @@ -161,7 +163,7 @@ class="cw-container-item" /> </div> - <div v-if="canVisit && canEdit && editView && !isLink" class="cw-container-wrapper cw-container-wrapper-edit"> + <div v-if="canVisit && canEdit && editView && !isLink && !hideRootContent" class="cw-container-wrapper cw-container-wrapper-edit"> <template v-if="!processing"> <span aria-live="assertive" class="assistive-text">{{ assistiveLive }}</span> <span id="operation" class="assistive-text"> @@ -207,7 +209,7 @@ <studip-progress-indicator v-if="processing" :description="$gettext('Vorgang wird bearbeitet...')" /> </div> </div> - <courseware-toolbar v-if="canVisit && canEdit && editView && !isLink" /> + <courseware-toolbar v-if="canVisit && canEdit && editView && !isLink" /> </div> </div> <studip-dialog @@ -588,6 +590,8 @@ import ContainerComponents from '../containers/container-components.js'; import StructuralElementComponents from './structural-element-components.js'; import CoursewarePluginComponents from '../plugin-components.js'; +import CoursewareRootContent from './CoursewareRootContent.vue'; + import CoursewareStructuralElementDialogAdd from './CoursewareStructuralElementDialogAdd.vue'; import CoursewareStructuralElementDialogCopy from './CoursewareStructuralElementDialogCopy.vue'; import CoursewareStructuralElementDialogImport from './CoursewareStructuralElementDialogImport.vue'; @@ -612,6 +616,7 @@ import { mapActions, mapGetters } from 'vuex'; export default { name: 'courseware-structural-element', components: Object.assign(StructuralElementComponents, { + CoursewareRootContent, CoursewareStructuralElementDialogAdd, CoursewareStructuralElementDialogCopy, CoursewareStructuralElementDialogImport, @@ -738,11 +743,23 @@ export default { templates: 'courseware-templates/all', progressData: 'progresses', + + showRootElement: 'showRootElement', + childrenById: 'courseware-structure/children', + + rootLayout: 'rootLayout' }), currentId() { return this.structuralElement?.id; }, + countSiblings() { + if (this.parent) { + return this.childrenById(this.parent.id).length; + } + + return 0; + }, textOer() { return { @@ -854,6 +871,9 @@ export default { return null; } const element = this.structuralElementById({ id: parentId }); + if (element.relationships.parent.data === null && !this.showRootElement) { + return null; + } if (!element) { console.error(`CoursewareStructuralElement#ancestors: Could not find parent by ID: "${parentId}".`); } @@ -879,6 +899,10 @@ export default { const previousId = this.orderedStructuralElements[currentIndex - 1]; const previous = this.structuralElementById({ id: previousId }); + if (previous.relationships.parent.data === null && !this.showRootElement) { + return null; + } + return previous; }, nextElement() { @@ -926,19 +950,46 @@ export default { return this.structuralElement.attributes['can-edit']; }, + parent() { + const parentId = this.structuralElement?.relationships?.parent?.data?.id; + if (!parentId) { + return null; + } + + return this.structuralElementById({ id: parentId }); + }, + canEditParent() { if (this.isRoot) { return false; } - const parentId = this.structuralElement.relationships.parent.data.id; - const parent = this.structuralElementById({ id: parentId }); + if (!parent) { + return false; + } - return parent.attributes['can-edit']; + return this.parent.attributes['can-edit']; }, isRoot() { return this.structuralElement.relationships.parent.data === null; }, + showRootLayout() { + return this.isRoot && this.rootLayout !== 'classic'; + }, + hideRootContent() { + return this.isRoot && this.rootLayout === 'none'; + }, + deletable() { + if (this.isRoot) { + return false; + } + + if (!this.showRootElement && this.countSiblings <= 1) { + return false; + } + + return true; + }, editor() { const editor = this.relatedUsers({ @@ -995,7 +1046,7 @@ export default { if (this.context.type === 'users') { menu.push({ id: 8, label: this.$gettext('Öffentlichen Link erzeugen'), icon: 'group', emit: 'linkElement' }); } - if (!this.isRoot && this.canEdit && !this.isTask && !this.blocked) { + if (this.deletable && this.canEdit && !this.isTask && !this.blocked) { menu.push({ id: 8, label: this.$gettext('Seite löschen'), @@ -1098,15 +1149,6 @@ export default { return taskGroup?.attributes['solver-may-add-blocks']; }, - showEmptyElementBox() { - if (!this.empty) { - return false; - } - - return ( - (!this.isRoot && this.canEdit) || !this.canEdit || (!this.noContainers && this.isRoot && this.canEdit) - ); - }, linkedElement() { if (this.isLink) { @@ -1415,6 +1457,13 @@ export default { }, async deleteCurrentElement() { await this.loadStructuralElement(this.currentElement.id); + if (!this.deletable) { + this.companionWarning({ + info: this.$gettext('Diese Seite darf nicht gelöscht werden') + }); + this.showElementDeleteDialog(false); + return false; + } if (this.blockedByAnotherUser) { this.companionWarning({ info: this.$gettextInterpolate( @@ -1425,7 +1474,7 @@ export default { this.showElementDeleteDialog(false); return false; } - let parent_id = this.structuralElement.relationships.parent.data.id; + const redirect_id = this.prevElement.id; this.showElementDeleteDialog(false); this.companionInfo({ info: this.$gettext('Lösche Seite und alle darunter liegenden Elemente.') }); this.deleteStructuralElement({ @@ -1433,7 +1482,7 @@ export default { parentId: this.structuralElement.relationships.parent.data.id, }) .then(response => { - this.$router.push(parent_id); + this.$router.push(redirect_id); this.companionInfo({ info: this.$gettext('Die Seite wurde gelöscht.') }); }) .catch(error => { diff --git a/resources/vue/components/courseware/structural-element/CoursewareToolsContents.vue b/resources/vue/components/courseware/structural-element/CoursewareToolsContents.vue index ce6ad62..234d9af 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareToolsContents.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareToolsContents.vue @@ -1,10 +1,10 @@ <template> <div class="cw-tools cw-tools-contents"> - <router-link :to="'/structural_element/' + rootElement.id" :class="{'root-is-current': rootIsCurrent}"> + <component :is="headerComponent" :to="'/structural_element/' + rootElement.id" :class="{'root-is-current': rootIsCurrent, 'root-is-hidden': hideRoot}"> <div v-if="rootElement" class="cw-tools-contents-header"> + <studip-ident-image v-model="identimage" :baseColor="headerColor.hex" :pattern="rootElement.attributes.title" /> <div class="cw-tools-contents-header-image" - :class="[headerImageUrl ? '' : 'default-image']" :style="headerImageStyle" ></div> <div class="cw-tools-contents-header-details"> @@ -12,25 +12,34 @@ <p>{{ rootElement.attributes.payload.description }}</p> </div> </div> - </router-link> + </component> <courseware-tree v-if="structuralElements.length" /> </div> </template> <script> import CoursewareTree from './CoursewareTree.vue'; +import colorMixin from '@/vue/mixins/courseware/colors.js'; +import StudipIdentImage from './../../StudipIdentImage.vue'; import { mapGetters } from 'vuex'; export default { name: 'courseware-tools-contents', + mixins: [colorMixin], components: { CoursewareTree, + StudipIdentImage, + }, + data() { + return { + identimage: '', + }; }, - computed: { ...mapGetters({ courseware: 'courseware', relatedStructuralElement: 'courseware-structural-elements/related', + rootLayout: 'rootLayout', structuralElements: 'courseware-structural-elements/all', structuralElementById: 'courseware-structural-elements/byId', }), @@ -49,13 +58,22 @@ export default { if (this.headerImageUrl) { return { 'background-image': 'url(' + this.headerImageUrl + ')' }; } - return {}; + return { 'background-image': 'url(' + this.identimage + ')' }; + }, + headerColor() { + const rootColor = this.rootElement?.attributes?.payload?.color ?? 'studip-blue'; + return this.mixinColors.find((color) => color.class === rootColor); }, - rootIsCurrent() { const id = this.$route?.params?.id; return this.rootElement.id === id; }, + hideRoot() { + return this.rootLayout === 'none'; + }, + headerComponent() { + return this.hideRoot ? 'span' : 'router-link'; + } }, }; </script> @@ -73,10 +91,6 @@ export default { background-repeat: no-repeat; background-position: center; background-color: var(--content-color-20); - &.default-image { - background-image: url("../images/icons/blue/courseware.svg"); - background-size: 64px; - } } .cw-tools-contents-header-details { @@ -105,4 +119,11 @@ export default { } } } +.root-is-hidden { + .cw-tools-contents-header-details { + header { + color: var(--black); + } + } +} </style> diff --git a/resources/vue/components/courseware/structural-element/CoursewareTreeItem.vue b/resources/vue/components/courseware/structural-element/CoursewareTreeItem.vue index a4bfa35..418d443 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareTreeItem.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareTreeItem.vue @@ -1,6 +1,9 @@ <template> - <li v-if="showItem" :draggable="editMode ? true : null" :aria-selected="editMode ? keyboardSelected : null"> - <div class="cw-tree-item-wrapper"> + <li v-if="showItem" + :draggable="editMode ? true : null" + :aria-selected="editMode ? keyboardSelected : null" + > + <div class="cw-tree-item-wrapper" v-if="showRootElement || depth > 0"> <span v-if="editMode && depth > 0 && canEdit" class="cw-sortable-handle" @@ -180,6 +183,7 @@ export default { courseware: 'courseware', progressData: 'progresses', userIsTeacher: 'userIsTeacher', + showRootElement: 'showRootElement' }), draggableData() { return { diff --git a/resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue b/resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue index 6eadc06..ea738a6 100644 --- a/resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue +++ b/resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue @@ -76,6 +76,15 @@ </template> </studip-select> </label> + <label> + <span>{{ $gettext('Titelseite') }}</span> + <select v-model="addWizardData.rootLayout"> + <option value="default">{{ $gettext('Automatisch') }}</option> + <option value="toc">{{ $gettext('Automatisch mit Inhaltsverzeichnis') }}</option> + <option value="classic">{{ $gettext('Frei bearbeitbar') }}</option> + <option value="none">{{ $gettext('Keine') }}</option> + </select> + </label> </form> </template> <template v-slot:advanced> @@ -157,7 +166,7 @@ export default { wizardSlots: [ { id: 1, valid: false, name: 'basic', title: this.$gettext('Grundeinstellungen'), icon: 'courseware', description: this.$gettext('Wählen Sie einen kurzen, prägnanten Titel und beschreiben Sie in einigen Worten den Inhalt des Lernmaterials. Eine Beschreibung erleichtert Lernenden die Auswahl des Lernmaterials.') }, - { id: 2, valid: true, name: 'layout', title: this.$gettext('Erscheinung'), icon: 'picture', + { id: 2, valid: true, name: 'layout', title: this.$gettext('Darstellung'), icon: 'picture', description: this.$gettext('Ein Vorschaubild motiviert Lernende das Lernmaterial zu erkunden. Die Kombination aus Bild und Farbe erleichtert das wiederfinden des Lernmaterials in der Übersicht.') }, { id: 3, valid: true, name: 'advanced', title: this.$gettext('Zusatzangaben'), icon: 'info-list', description: this.$gettext('Hier können Sie detaillierte Angaben zum Lernmaterial eintragen. Diese sind besonders interessant wenn das Lernmaterial als OER geteilt wird.') } @@ -210,6 +219,7 @@ export default { description: '', purpose: 'content', color: 'studip-blue', + rootLayout: 'default' } }, validateSlots() { @@ -252,6 +262,9 @@ export default { required_time: this.purposeIsOer ? this.addWizardData.required_time : '', difficulty_start: this.purposeIsOer ? this.addWizardData.difficulty_start : '', difficulty_end: this.purposeIsOer ? this.addWizardData.difficulty_end : '' + }, + settings: { + 'root-layout': this.addWizardData.rootLayout } }, relationships: { diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItem.vue b/resources/vue/components/courseware/unit/CoursewareUnitItem.vue index bf637ae..6c0ec6e 100644 --- a/resources/vue/components/courseware/unit/CoursewareUnitItem.vue +++ b/resources/vue/components/courseware/unit/CoursewareUnitItem.vue @@ -62,7 +62,7 @@ <courseware-unit-item-dialog-export v-if="showExportDialog" :unit="unit" @close="showExportDialog = false" /> <courseware-unit-item-dialog-settings v-if="showSettingsDialog" :unit="unit" @close="closeSettingsDialog"/> - <courseware-unit-item-dialog-layout v-if="showLayoutDialog" :unitElement="unitElement" @close="closeLayoutDialog"/> + <courseware-unit-item-dialog-layout v-if="showLayoutDialog" :unit="unit" :unitElement="unitElement" @close="closeLayoutDialog"/> </li> </template> diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue index f06f7fd..702a986 100644 --- a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue +++ b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue @@ -5,79 +5,91 @@ confirmClass="accept" :closeText="$gettext('Schließen')" closeClass="cancel" - height="720" - width="500" + height="470" + width="870" @close="$emit('close')" @confirm="storeLayout" > <template v-slot:dialogContent> - <form v-if="currentElement" class="default" @submit.prevent=""> - <label>{{ $gettext('Vorschaubild') }}</label> - <img - v-if="showPreviewImage" - :src="image" - class="cw-structural-element-image-preview" - :alt="$gettext('Vorschaubild')" - /> - <label v-if="showPreviewImage"> - <button class="button" @click="deleteImage">{{ $gettext('Bild löschen') }}</button> - </label> - <courseware-companion-box - v-if="uploadFileError" - :msgCompanion="uploadFileError" - mood="sad" - /> - <label v-if="!showPreviewImage"> + <div v-if="currentElement && !loadingInstance" class="cw-unit-item-dialog-layout-content"> + <form class="default cw-unit-item-dialog-layout-content-image" @submit.prevent=""> + <label>{{ $gettext('Vorschaubild') }}</label> <img - v-if="currentFile" - :src="uploadImageURL" + v-if="showPreviewImage" + :src="image" class="cw-structural-element-image-preview" :alt="$gettext('Vorschaubild')" /> - <div v-else class="cw-structural-element-image-preview-placeholder"></div> - {{ $gettext('Bild hochladen') }} - <input class="cw-file-input" ref="upload_image" type="file" accept="image/*" @change="checkUploadFile" /> - </label> - - <label> - {{ $gettext('Titel') }} - <input type="text" v-model="currentElement.attributes.title"/> - </label> - <label> - {{ $gettext('Beschreibung') }} - <textarea - v-model="currentElement.attributes.payload.description" - class="cw-structural-element-description" + <label v-if="showPreviewImage"> + <button class="button" @click="deleteImage">{{ $gettext('Bild löschen') }}</button> + </label> + <courseware-companion-box + v-if="uploadFileError" + :msgCompanion="uploadFileError" + mood="sad" /> - </label> - <label> - {{ $gettext('Farbe') }} - <studip-select - v-model="currentElement.attributes.payload.color" - :options="colors" - :reduce="(color) => color.class" - label="class" - class="cw-vs-select" - > - <template #open-indicator="selectAttributes"> - <span v-bind="selectAttributes" - ><studip-icon shape="arr_1down" :size="10" - /></span> - </template> - <template #no-options> - {{ $gettext('Es steht keine Auswahl zur Verfügung') }}. - </template> - <template #selected-option="{ name, hex }"> - <span class="vs__option-color" :style="{ 'background-color': hex }"></span - ><span>{{ name }}</span> - </template> - <template #option="{ name, hex }"> - <span class="vs__option-color" :style="{ 'background-color': hex }"></span - ><span>{{ name }}</span> - </template> - </studip-select> - </label> - </form> + <label v-if="!showPreviewImage"> + <img + v-if="currentFile" + :src="uploadImageURL" + class="cw-structural-element-image-preview" + :alt="$gettext('Vorschaubild')" + /> + <div v-else class="cw-structural-element-image-preview-placeholder"></div> + {{ $gettext('Bild hochladen') }} + <input class="cw-file-input" ref="upload_image" type="file" accept="image/*" @change="checkUploadFile" /> + </label> + </form> + <form class="default cw-unit-item-dialog-layout-content-settings" @submit.prevent=""> + <label> + {{ $gettext('Titel') }} + <input type="text" v-model="currentElement.attributes.title"/> + </label> + <label> + {{ $gettext('Beschreibung') }} + <textarea + v-model="currentElement.attributes.payload.description" + class="cw-structural-element-description" + /> + </label> + <label> + {{ $gettext('Farbe') }} + <studip-select + v-model="currentElement.attributes.payload.color" + :options="colors" + :reduce="(color) => color.class" + label="class" + class="cw-vs-select" + > + <template #open-indicator="selectAttributes"> + <span v-bind="selectAttributes" + ><studip-icon shape="arr_1down" :size="10" + /></span> + </template> + <template #no-options> + {{ $gettext('Es steht keine Auswahl zur Verfügung') }}. + </template> + <template #selected-option="{ name, hex }"> + <span class="vs__option-color" :style="{ 'background-color': hex }"></span + ><span>{{ name }}</span> + </template> + <template #option="{ name, hex }"> + <span class="vs__option-color" :style="{ 'background-color': hex }"></span + ><span>{{ name }}</span> + </template> + </studip-select> + </label> + <label> + {{ $gettext('Titelseite') }} + <select v-model="currentRootLayout"> + <option value="default">{{ $gettext('Automatisch') }}</option> + <option value="toc">{{ $gettext('Automatisch mit Inhaltsverzeichnis') }}</option> + <option value="classic">{{ $gettext('Frei bearbeitbar') }}</option> + <option value="none">{{ $gettext('Keine') }}</option> + </select> + </label> + </form> + </div> </template> </studip-dialog> </template> @@ -95,6 +107,7 @@ export default { CoursewareCompanionBox }, props: { + unit: Object, unitElement: Object }, mixins: [colorMixin], @@ -105,10 +118,14 @@ export default { uploadFileError: '', currentFile: null, uploadImageURL: null, + currentRootLayout: 'default', + loadingInstance: false, } }, computed: { ...mapGetters({ + context: 'context', + instanceById: 'courseware-instances/byId', userId: 'userId' }), colors() { @@ -121,9 +138,21 @@ export default { showPreviewImage() { return this.image !== null && this.deletingPreviewImage === false; }, + instance() { + if (this.inCourseContext) { + return this.instanceById({id: 'course_' + this.context.id + '_' + this.unit.id}); + } else { + return this.instanceById({id: 'user_' + this.context.id + '_' + this.unit.id}); + } + + }, + inCourseContext() { + return this.context.type === 'courses'; + } }, methods: { ...mapActions({ + loadInstance: 'loadInstance', companionSuccess: 'companionSuccess', companionWarning: 'companionWarning', loadStructuralElement: 'loadStructuralElement', @@ -132,9 +161,15 @@ export default { updateStructuralElement: 'updateStructuralElement', uploadImageForStructuralElement: 'uploadImageForStructuralElement', deleteImageForStructuralElement: 'deleteImageForStructuralElement', + storeCoursewareSettings: 'storeCoursewareSettings', }), + async loadUnitInstance() { + const context = {type: this.context.type, id: this.context.id, unit: this.unit.id}; + await this.loadInstance(context); + }, initData() { this.currentElement = _.cloneDeep(this.unitElement); + this.currentRootLayout = this.instance.attributes['root-layout']; }, checkUploadFile() { const file = this.$refs?.upload_image?.files[0]; @@ -188,9 +223,20 @@ export default { id: this.currentElement.id, }); await this.unlockObject({ id: this.currentElement.id, type: 'courseware-structural-elements' }); + + if (this.instance.attributes['root-layout'] !== this.currentRootLayout) { + let currentInstance = _.cloneDeep(this.instance); + currentInstance.attributes['root-layout'] = this.currentRootLayout; + this.storeCoursewareSettings({ + instance: currentInstance, + }); + } } }, async mounted() { + this.loadingInstance = true; + await this.loadUnitInstance(); + this.loadingInstance = false; this.initData(); } } diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogSettings.vue b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogSettings.vue index 399a87c..ba2eaf4 100644 --- a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogSettings.vue +++ b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogSettings.vue @@ -21,7 +21,6 @@ <option value="1">{{ $gettext('Sequentiell') }}</option> </select> </label> - <label> <span>{{ $gettext('Editierberechtigung für Tutor/-innen') }}</span> <select class="size-s" v-model="currentPermissionLevel"> @@ -209,6 +208,7 @@ export default { return { currentInstance: null, loadSettings: false, + currentRootLayout: 'default', currentPermissionLevel: '', currentProgression: 0, makeCert: false, @@ -260,6 +260,7 @@ export default { await this.loadInstance(context); }, initData() { + this.currentRootLayout = this.currentInstance.attributes['root-layout']; this.currentPermissionLevel = this.currentInstance.attributes['editing-permission-level']; this.currentProgression = this.currentInstance.attributes['sequential-progression'] ? '1' : '0'; this.certSettings = this.currentInstance.attributes['certificate-settings']; @@ -286,6 +287,7 @@ export default { }, storeSettings() { this.$emit('close'); + this.currentInstance.attributes['root-layout'] = this.currentRootLayout; this.currentInstance.attributes['editing-permission-level'] = this.currentPermissionLevel; this.currentInstance.attributes['sequential-progression'] = this.currentProgression; this.currentInstance.attributes['certificate-settings'] = this.generateCertificateSettings(); diff --git a/resources/vue/components/courseware/widgets/CoursewareViewWidget.vue b/resources/vue/components/courseware/widgets/CoursewareViewWidget.vue index 5202abd..d8a8563 100644 --- a/resources/vue/components/courseware/widgets/CoursewareViewWidget.vue +++ b/resources/vue/components/courseware/widgets/CoursewareViewWidget.vue @@ -42,6 +42,7 @@ export default { ...mapGetters({ viewMode: 'viewMode', context: 'context', + rootLayout: 'rootLayout' }), readView() { return this.viewMode === 'read'; @@ -58,6 +59,9 @@ export default { } return this.structuralElement.attributes['can-edit']; }, + isRoot() { + return this.structuralElement.relationships.parent.data === null; + } }, methods: { ...mapActions({ |
