aboutsummaryrefslogtreecommitdiff
path: root/resources/vue/components/courseware
diff options
context:
space:
mode:
authorRon Lucke <lucke@elan-ev.de>2023-12-11 10:37:55 +0000
committerRon Lucke <lucke@elan-ev.de>2023-12-11 10:37:55 +0000
commitf2767008ffcf723bc73a4ea8781b31f23c2b68e5 (patch)
tree55d6c4b96b65f42545d9a624263089f5f5cb06a4 /resources/vue/components/courseware
parent47e23c2e78b4cc099dc6217fa0827a033ef0dc9f (diff)
TIC #3111
Closes #3111 Merge request studip/studip!2295
Diffstat (limited to 'resources/vue/components/courseware')
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue115
-rw-r--r--resources/vue/components/courseware/layouts/CoursewareTile.vue23
-rw-r--r--resources/vue/components/courseware/structural-element/CoursewareRootContent.vue246
-rw-r--r--resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue89
-rw-r--r--resources/vue/components/courseware/structural-element/CoursewareToolsContents.vue41
-rw-r--r--resources/vue/components/courseware/structural-element/CoursewareTreeItem.vue8
-rw-r--r--resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue15
-rw-r--r--resources/vue/components/courseware/unit/CoursewareUnitItem.vue2
-rw-r--r--resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue174
-rw-r--r--resources/vue/components/courseware/unit/CoursewareUnitItemDialogSettings.vue4
-rw-r--r--resources/vue/components/courseware/widgets/CoursewareViewWidget.vue4
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({