diff --git a/resources/assets/stylesheets/scss/courseware/layouts/companion.scss b/resources/assets/stylesheets/scss/courseware/layouts/companion.scss index cf48c8f2ec6823e15852e3684b6b3d4830523b6a..acd1abc98df5feef588ca20d6d82ab30e50e9242 100644 --- a/resources/assets/stylesheets/scss/courseware/layouts/companion.scss +++ b/resources/assets/stylesheets/scss/courseware/layouts/companion.scss @@ -84,7 +84,12 @@ $companion-types: ( margin-top: 8px; } - p { + &.borderless { + border: none; + } + + .cw-companion-message { margin: 0 1em 10px 0; + color: var(--black); } } \ No newline at end of file diff --git a/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue b/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue index c4a31e22d5576e8fdb60205d0c87f1b367152b7d..01c57e39cb667e5ec416a9370485da039675f18a 100644 --- a/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue +++ b/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue @@ -1,7 +1,7 @@ <template> - <div class="cw-companion-box" :class="[mood]"> + <div class="cw-companion-box" :class="[mood, border ? '' : 'borderless' ]"> <div> - <p v-html="msgCompanion"></p> + <p class="cw-companion-message" v-html="msgCompanion"></p> <slot name="companionActions"></slot> </div> </div> @@ -18,6 +18,10 @@ export default { validator: value => { return ['default','unsure', 'special', 'sad', 'pointing', 'curious'].includes(value); } + }, + border: { + type: Boolean, + default: true } } }; diff --git a/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue b/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue index 8dd5f45e4c7205a3d912cba2ed7ad0fb9011bf58..2a92579fdc0f749e01acfac679e296069cce98a8 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue @@ -21,6 +21,15 @@ id="cw-ribbon-tool-contents" /> </courseware-tab> + <courseware-tab + :name="$gettext('Lernmaterialien')" + :selected="showUnits" + alias="units" + ref="units" + :index="1" + > + <CoursewareToolsUnits /> + </courseware-tab> </courseware-tabs> <button :title="$gettext('schließen')" @@ -37,6 +46,7 @@ import CoursewareTabs from '../layouts/CoursewareTabs.vue'; import CoursewareTab from '../layouts/CoursewareTab.vue'; import CoursewareToolsContents from './CoursewareToolsContents.vue'; +import CoursewareToolsUnits from './CoursewareToolsUnits.vue'; import { FocusTrap } from 'focus-trap-vue'; import { mapActions, mapGetters } from 'vuex'; @@ -46,6 +56,7 @@ export default { CoursewareTabs, CoursewareTab, CoursewareToolsContents, + CoursewareToolsUnits, FocusTrap, }, props: { @@ -67,7 +78,7 @@ export default { data() { return { showContents: true, - showBlockAdder: false, + showUnits: false, trap: false, initialFocusElement: null }; @@ -94,7 +105,7 @@ export default { }, methods: { ...mapActions({ - coursewareContainerAdder: 'coursewareContainerAdder' + coursewareContainerAdder: 'coursewareContainerAdder', }), scrollToCurrent() { setTimeout(() => { diff --git a/resources/vue/components/courseware/structural-element/CoursewareToolsUnits.vue b/resources/vue/components/courseware/structural-element/CoursewareToolsUnits.vue new file mode 100644 index 0000000000000000000000000000000000000000..29ce79c90870725150f526a053897848c8b5760b --- /dev/null +++ b/resources/vue/components/courseware/structural-element/CoursewareToolsUnits.vue @@ -0,0 +1,103 @@ +<template> + <StudipProgressIndicator v-if="loadingUnits" :description="$gettext('Vorgang wird bearbeitet...')" /> + <ul v-else class="cw-ribbon-tools-units"> + <li v-for="unit in units" :key="unit.id"> + <CoursewareToolsUnitsItem :unit="unit" :element="getUnitElement(unit)" /> + </li> + <li v-if="emptyUnits"> + <CoursewareCompanionBox mood="sad" :msgCompanion="emptyUnitsMessage" :border="false"/> + </li> + </ul> +</template> + +<script> +import CoursewareToolsUnitsItem from './CoursewareToolsUnitsItem.vue'; +import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue'; +import StudipProgressIndicator from '../../StudipProgressIndicator.vue'; +import { mapActions, mapGetters } from 'vuex'; +export default { + name: 'CoursewareToolsUnits', + components: { + CoursewareToolsUnitsItem, + CoursewareCompanionBox, + StudipProgressIndicator, + }, + data() { + return { + loadingUnits: false, + }; + }, + computed: { + ...mapGetters({ + context: 'context', + coursewareUnits: 'courseware-units/all', + currentUnit: 'currentUnit', + elementById: 'courseware-structural-elements/byId', + userId: 'userId', + }), + units() { + return ( + this.coursewareUnits + .filter( + (unit) => + unit.relationships.range.data.id === this.context.id && unit.id !== this.currentUnit.id + ) + .sort((a, b) => a.attributes.position - b.attributes.position) ?? [] + ); + }, + inCourseContext() { + return this.context.type === 'courses'; + }, + inUserContext() { + return this.context.type === 'users'; + }, + emptyUnits() { + return this.units.length === 0; + }, + emptyUnitsMessage() { + if (this.inCourseContext) { + return this.$gettext('Es wurden keine weiteren Lernmaterialien in dieser Veranstaltung gefunden.'); + } + if (this.inUserContext) { + return this.$gettext('Es wurden keine weiteren Lernmaterialien gefunden.'); + } + + return ''; + } + }, + methods: { + ...mapActions({ + loadCourseUnits: 'loadCourseUnits', + loadUserUnits: 'loadUserUnits', + }), + getUnitElement(unit) { + const elementId = unit.relationships['structural-element'].data.id; + return this.elementById({ id: elementId }); + }, + }, + async beforeMount() { + if (this.coursewareUnits.length === 0) { + this.loadingUnits = true; + } + + if (this.inCourseContext) { + await this.loadCourseUnits(this.context.id); + } + if (this.inUserContext) { + await this.loadUserUnits(this.userId); + } + + this.loadingUnits = false; + }, +}; +</script> +<style lang="scss"> +.cw-ribbon-tools-units { + list-style: none; + padding: 0; + + li { + margin-bottom: 2em; + } +} +</style> diff --git a/resources/vue/components/courseware/structural-element/CoursewareToolsUnitsItem.vue b/resources/vue/components/courseware/structural-element/CoursewareToolsUnitsItem.vue new file mode 100644 index 0000000000000000000000000000000000000000..cbbc011b309fab115a699df1a50124a8f2d44d14 --- /dev/null +++ b/resources/vue/components/courseware/structural-element/CoursewareToolsUnitsItem.vue @@ -0,0 +1,98 @@ +<template> + <a v-if="element" class="cw-tools-units-item-header" :href="url"> + <studip-ident-image v-model="identimage" :baseColor="headerColor.hex" :pattern="element.attributes.title" /> + <div class="cw-tools-units-item-header-image" :style="headerImageStyle"></div> + <div class="cw-tools-units-item-header-details"> + <header>{{ element.attributes.title }}</header> + <p>{{ element.attributes.payload.description }}</p> + </div> + </a> +</template> + +<script> +import StudipIdentImage from '../../StudipIdentImage.vue'; +import colorMixin from '@/vue/mixins/courseware/colors.js'; +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'CoursewareToolsUnitsItem', + mixins: [colorMixin], + components: { + StudipIdentImage, + }, + props: { + unit: Object, + element: Object, + }, + data() { + return { + identimage: '', + }; + }, + computed: { + ...mapGetters({ + context: 'context', + }), + headerImageUrl() { + return this.element.relationships?.image?.meta?.['download-url']; + }, + headerImageStyle() { + if (this.headerImageUrl) { + return { 'background-image': 'url(' + this.headerImageUrl + ')' }; + } + return { 'background-image': 'url(' + this.identimage + ')' }; + }, + headerColor() { + const rootColor = this.element?.attributes?.payload?.color ?? 'studip-blue'; + return this.mixinColors.find((color) => color.class === rootColor); + }, + inCourseContext() { + return this.context.type === 'courses'; + }, + url() { + if (this.inCourseContext) { + return STUDIP.URLHelper.getURL('dispatch.php/course/courseware/courseware/' + this.unit.id, { + cid: this.context.id, + }); + } else { + return STUDIP.URLHelper.getURL('dispatch.php/contents/courseware/courseware/' + this.unit.id); + } + }, + } +}; +</script> +<style lang="scss"> +.cw-tools-units-item-header { + display: flex; + flex-direction: row; + height: 100px; + margin-top: 8px; + .cw-tools-units-item-header-image { + height: 100px; + width: 150px; + min-width: 150px; + background-size: 100% auto; + background-repeat: no-repeat; + background-position: center; + background-color: var(--content-color-20); + } + + .cw-tools-units-item-header-details { + margin: 0 8px; + display: -webkit-box; + overflow: hidden; + height: 100px; + -webkit-line-clamp: 5; + -webkit-box-orient: vertical; + header { + margin: 0 0 6px 0; + font-size: 16px; + line-height: 16px; + } + p { + margin: 0; + color: var(--black); + } + } +} +</style> \ No newline at end of file diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js index db55cdd6d1cbb639017a7ce512cd6e859a5edb51..7004b1628c3eda5d25d97d713f4b121d7423f455 100644 --- a/resources/vue/store/courseware/courseware.module.js +++ b/resources/vue/store/courseware/courseware.module.js @@ -103,6 +103,13 @@ const getters = { const id = getters.currentElement; return rootGetters['courseware-structural-elements/byId']({ id }); }, + currentUnit(state, getters, rootState, rootGetters) { + const id = getters.currentStructuralElement.relationships?.unit?.data?.id; + if (id) { + return rootGetters['courseware-units/byId']({ id }); + } + return null; + }, currentElementBlocked(state, getters, rootState, rootGetters) { const elemData = getters.currentStructuralElement?.relationships?.['edit-blocker']?.data; return elemData !== null && elemData !== '' && getters.currentStructuralElement;