diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss index 7a41ab33030780d084dcede52e86fcbe3429c3f3..2d7deaa71de4f2f6287724094f7baf46f77aaf19 100644 --- a/resources/assets/stylesheets/scss/courseware.scss +++ b/resources/assets/stylesheets/scss/courseware.scss @@ -761,17 +761,24 @@ ribbon end .cw-structural-element-image-preview { display: block; - max-width: 300px; - max-height: 256px; + max-height: 200px; + max-width: 400px; width: auto; height: auto; margin: 0 auto; + padding-bottom: 1em; } .cw-structural-element-image-preview-placeholder { - width: 400px; - height: 225px; - background-color: $black; + width: 356px; + height: 200px; + margin: 0 auto; + background-color: var(--content-color-20); + background-size: 100% auto; + background-repeat: no-repeat; + background-position: center; + @include background-icon(courseware, clickable, 128); + margin-bottom: 1em; } .cw-element-permissions { @@ -5384,9 +5391,9 @@ cw tiles .preview-image { height: 180px; width: 100%; - background-size: auto 180px; + background-size: 100% auto; background-repeat: no-repeat; - background-color: $content-color-20; + background-color: var(--content-color-20); background-position: center; &.default-image { @include background-icon(courseware, clickable, 128); diff --git a/resources/vue/components/courseware/CoursewareStructuralElement.vue b/resources/vue/components/courseware/CoursewareStructuralElement.vue index 94d6ca7943db86cee0f285a33c6c5f5c55d15b97..edf15aabea78ca1f62b24713be120af81aa2b6b8 100644 --- a/resources/vue/components/courseware/CoursewareStructuralElement.vue +++ b/resources/vue/components/courseware/CoursewareStructuralElement.vue @@ -333,8 +333,15 @@ {{ uploadFileError }} </div> <label v-if="!showPreviewImage"> - <translate>Bild hochladen</translate> - <input ref="upload_image" type="file" accept="image/*" @change="checkUploadFile" /> + <img + v-if="uploadImageURL" + :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> </courseware-tab> @@ -749,7 +756,8 @@ export default { deletingPreviewImage: false, processing: false, keyboardSelected: null, - assistiveLive: '' + assistiveLive: '', + uploadImageURL: null, }; }, @@ -1231,6 +1239,7 @@ export default { this.currentElement = _.cloneDeep(this.structuralElement); this.uploadFileError = ''; this.deletingPreviewImage = false; + this.uploadImageURL = null; }, async menuAction(action) { switch (action) { @@ -1313,7 +1322,12 @@ export default { this.showElementAddDialog(false); }, checkUploadFile() { + const file = this.$refs?.upload_image?.files[0]; + this.uploadImageURL = null; this.uploadFileError = this.checkUploadImageFile(this.$refs?.upload_image?.files[0]); + if (this.uploadFileError === '') { + this.uploadImageURL = window.URL.createObjectURL(file); + } }, deleteImage() { if (!this.deletingPreviewImage) { diff --git a/resources/vue/components/courseware/CoursewareUnitItem.vue b/resources/vue/components/courseware/CoursewareUnitItem.vue index c55a0f38cc981fa47fc2543dc5b79bac4dea9883..d5287fab23834b479229baaf8712123c98a09ca3 100644 --- a/resources/vue/components/courseware/CoursewareUnitItem.vue +++ b/resources/vue/components/courseware/CoursewareUnitItem.vue @@ -19,6 +19,7 @@ @showExport="openExportDialog" @showProgress="openProgressDialog" @showSettings="openSettingsDialog" + @showLayout="openLayoutDialog" @copyUnit="copy" /> </template> @@ -54,6 +55,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"/> </li> </template> @@ -61,6 +63,7 @@ import CoursewareTile from './CoursewareTile.vue'; import CoursewareUnitItemDialogExport from './CoursewareUnitItemDialogExport.vue'; import CoursewareUnitItemDialogSettings from './CoursewareUnitItemDialogSettings.vue'; +import CoursewareUnitItemDialogLayout from './CoursewareUnitItemDialogLayout.vue'; import CoursewareUnitProgress from './CoursewareUnitProgress.vue'; import { mapActions, mapGetters } from 'vuex'; @@ -70,6 +73,7 @@ export default { components: { CoursewareTile, CoursewareUnitItemDialogExport, + CoursewareUnitItemDialogLayout, CoursewareUnitItemDialogSettings, CoursewareUnitProgress, }, @@ -82,6 +86,7 @@ export default { showExportDialog: false, showSettingsDialog: false, showProgressDialog: false, + showLayoutDialog: false, progresses: null } }, @@ -100,9 +105,10 @@ export default { menu.push({ id: 2, label: this.$gettext('Einstellungen'), icon: 'settings', emit: 'showSettings' }); } if(this.userIsTeacher || !this.inCourseContext) { - menu.push({ id: 3, label: this.$gettext('Kopieren'), icon: 'copy', emit: 'copyUnit' }); - menu.push({ id: 4, label: this.$gettext('Exportieren'), icon: 'export', emit: 'showExport' }); - menu.push({ id: 5, label: this.$gettext('Löschen'), icon: 'trash', emit: 'showDelete' }); + menu.push({ id: 4, label: this.$gettext('Darstellung'), icon: 'colorpicker', emit: 'showLayout' }); + menu.push({ id: 4, label: this.$gettext('Kopieren'), icon: 'copy', emit: 'copyUnit' }); + menu.push({ id: 5, label: this.$gettext('Exportieren'), icon: 'export', emit: 'showExport' }); + menu.push({ id: 6, label: this.$gettext('Löschen'), icon: 'trash', emit: 'showDelete' }); } return menu; @@ -176,6 +182,12 @@ export default { closeSettingsDialog() { this.showSettingsDialog = false; }, + openLayoutDialog() { + this.showLayoutDialog = true; + }, + closeLayoutDialog() { + this.showLayoutDialog = false; + }, async copy() { await this.copyUnit({unitId: this.unit.id, modified: null}); this.companionSuccess({ info: this.$gettext('Lernmaterial kopiert.') }); } diff --git a/resources/vue/components/courseware/CoursewareUnitItemDialogLayout.vue b/resources/vue/components/courseware/CoursewareUnitItemDialogLayout.vue new file mode 100644 index 0000000000000000000000000000000000000000..4f49d968381751fed7f6bcdcd28ba48f4ee6eac6 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareUnitItemDialogLayout.vue @@ -0,0 +1,197 @@ +<template> + <studip-dialog + :title="$gettext('Darstellung')" + :confirmText="$gettext('Speichern')" + confirmClass="accept" + :closeText="$gettext('Schließen')" + closeClass="cancel" + height="720" + width="500" + @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"> + <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> + + <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> + </form> + </template> + </studip-dialog> +</template> + +<script> +import CoursewareCompanionBox from './CoursewareCompanionBox.vue'; + +import colorMixin from '@/vue/mixins/courseware/colors.js'; +import { mapActions, mapGetters } from 'vuex'; + + +export default { + name: 'courseware-unit-item-dialog-layout', + components: { + CoursewareCompanionBox + }, + props: { + unitElement: Object + }, + mixins: [colorMixin], + data() { + return { + currentElement: null, + deletingPreviewImage: false, + uploadFileError: '', + currentFile: null, + uploadImageURL: null, + } + }, + computed: { + ...mapGetters({ + userId: 'userId' + }), + colors() { + return this.mixinColors.filter(color => color.darkmode); + }, + image() { + return this.currentElement.relationships?.image?.meta?.['download-url'] ?? null; + }, + + showPreviewImage() { + return this.image !== null && this.deletingPreviewImage === false; + }, + }, + methods: { + ...mapActions({ + companionSuccess: 'companionSuccess', + companionWarning: 'companionWarning', + loadStructuralElement: 'loadStructuralElement', + lockObject: 'lockObject', + unlockObject: 'unlockObject', + updateStructuralElement: 'updateStructuralElement', + uploadImageForStructuralElement: 'uploadImageForStructuralElement', + deleteImageForStructuralElement: 'deleteImageForStructuralElement', + }), + initData() { + this.currentElement = _.cloneDeep(this.unitElement); + }, + checkUploadFile() { + const file = this.$refs?.upload_image?.files[0]; + if (file.size > 2097152) { + this.uploadFileError = this.$gettext('Diese Datei ist zu groß. Bitte wählen Sie eine kleinere Datei.'); + } else if (!file.type.includes('image')) { + this.uploadFileError = this.$gettext('Diese Datei ist kein Bild. Bitte wählen Sie ein Bild aus.'); + } else { + this.uploadFileError = ''; + this.currentFile = file; + this.uploadImageURL = window.URL.createObjectURL(file); + } + }, + deleteImage() { + if (!this.deletingPreviewImage) { + this.deletingPreviewImage = true; + } + }, + async storeLayout() { + this.$emit('close'); + await this.loadStructuralElement(this.currentElement.id); + if ( + this.unitElement.relationships['edit-blocker'].data !== null + && this.unitElement.relationships['edit-blocker'].data?.id !== this.userId + ) { + this.companionWarning({ + info: this.$gettext('Ihre Änderungen konnten nicht gespeichert werden, die Daten werden bereits von einem anderen Nutzer bearbeitet.') + }); + return false; + } else { + await this.lockObject({ id: this.currentElement.id, type: 'courseware-structural-elements' }); + } + + if (this.currentFile) { + this.uploadImageForStructuralElement({ + structuralElement: this.currentElement, + file: this.currentFile, + }).catch((error) => { + console.error(error); + this.companionWarning({ + info: this.$gettext('Beim Hochladen der Bilddatei ist ein Fehler aufgetretten.') + }); + }); + await this.loadStructuralElement(this.currentElement.id); + } else if (this.deletingPreviewImage) { + await this.deleteImageForStructuralElement(this.currentElement); + } + + await this.updateStructuralElement({ + element: this.currentElement, + id: this.currentElement.id, + }); + await this.unlockObject({ id: this.currentElement.id, type: 'courseware-structural-elements' }); + } + }, + async mounted() { + this.initData(); + } +} +</script> \ No newline at end of file diff --git a/resources/vue/store/courseware/courseware-shelf.module.js b/resources/vue/store/courseware/courseware-shelf.module.js index 8fab7a275c1ac91d9943d7613ce30f593c2d8e23..9d8e80f6b80a1879140cf63021f49b4a996fb5b9 100644 --- a/resources/vue/store/courseware/courseware-shelf.module.js +++ b/resources/vue/store/courseware/courseware-shelf.module.js @@ -565,6 +565,13 @@ export const actions = { }); }, + async deleteImageForStructuralElement({ dispatch, state }, structuralElement) { + const url = `courseware-structural-elements/${structuralElement.id}/image`; + await state.httpClient.delete(url); + + return dispatch('loadStructuralElement', structuralElement.id); + }, + setImportFilesState({ commit }, state) { commit('setImportFilesState', state); },