diff --git a/app/controllers/courseware_controller.php b/app/controllers/courseware_controller.php index ddee584320519145816a89e626118647a142a1ca..91fc9c856bd9fcf158325bec3dc3afaf858b8ad9 100644 --- a/app/controllers/courseware_controller.php +++ b/app/controllers/courseware_controller.php @@ -35,10 +35,10 @@ abstract class CoursewareController extends AuthenticatedController if ($last_element) { $last_element_unit = $last_element->findUnit(); } - if (isset($last_element_unit) && $last_element_unit->id === $unit->id) { + if (isset($last_element_unit) && $last_element_unit->id === $unit->id && $last_element_unit->hasRootLayout()) { $this->entry_element_id = $last_element->id; } else { - $this->entry_element_id = $unit->structural_element_id; + $this->entry_element_id = $unit->findOrCreateFirstElement()->id; } if ($this->entry_element_id) { $last_element_item = $context === 'user' ? 'global' : $rangeId; diff --git a/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesUpdate.php b/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesUpdate.php index c5ef250ba5245c6a9e4a6a977c5af7777c4119b0..8bb01968b7f21ac70a36ba9f5997e3be22ee1001 100644 --- a/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesUpdate.php +++ b/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesUpdate.php @@ -74,6 +74,16 @@ class CoursewareInstancesUpdate extends JsonApiController } } + if (self::arrayHas($json, 'data.attributes.root-layout')) { + $rootLayout = self::arrayGet($json, 'data.attributes.root-layout'); + if (!is_string($rootLayout)) { + return 'Attribute `root-layout` must be a string.'; + } + if (!$data->isValidRootLayout($rootLayout)) { + return 'Attribute `root-layout` contains an invalid value.'; + } + } + if (self::arrayHas($json, 'data.attributes.editing-permission-level')) { $editingPermissionLevel = self::arrayGet($json, 'data.attributes.editing-permission-level'); if (!is_string($editingPermissionLevel)) { @@ -118,6 +128,9 @@ class CoursewareInstancesUpdate extends JsonApiController $favorites = $get('data.attributes.favorite-block-types'); $instance->setFavoriteBlockTypes($user, $favorites); + $rootLayout = $get('data.attributes.root-layout'); + $instance->setRootLayout($rootLayout); + $sequentialProgression = $get('data.attributes.sequential-progression'); $instance->setSequentialProgression($sequentialProgression); diff --git a/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php b/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php index 6f88bca97219c87ea883547a2430d69cac2ba466..f55fd6aa5468b653251a17ef638c88ca7839280f 100644 --- a/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php +++ b/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php @@ -111,6 +111,12 @@ class UnitsCreate extends JsonApiController 'withdraw_date' => self::arrayGet($json, 'data.attributes.withdraw-date'), ]); + $instance = new \Courseware\Instance($struct); + + $instance->setRootLayout(self::arrayGet($json, 'data.attributes.settings.root-layout')); + + $instance->getUnit()->store(); + return $unit; } diff --git a/lib/classes/JsonApi/Schemas/Courseware/Instance.php b/lib/classes/JsonApi/Schemas/Courseware/Instance.php index d9437592af3704258af59d6838fe4c2eb7b5e1a3..7df0cf6d719f1ca5fd33c2fac981bd85234b54d2 100644 --- a/lib/classes/JsonApi/Schemas/Courseware/Instance.php +++ b/lib/classes/JsonApi/Schemas/Courseware/Instance.php @@ -37,6 +37,7 @@ class Instance extends SchemaProvider 'block-types' => array_map([$this, 'mapBlockType'], $resource->getBlockTypes()), 'container-types' => array_map([$this, 'mapContainerType'], $resource->getContainerTypes()), 'favorite-block-types' => $resource->getFavoriteBlockTypes($user), + 'root-layout' => $resource->getRootLayout(), 'sequential-progression' => $resource->getSequentialProgression(), 'editing-permission-level' => $resource->getEditingPermissionLevel(), 'certificate-settings' => $resource->getCertificateSettings(), diff --git a/lib/models/Courseware/Instance.php b/lib/models/Courseware/Instance.php index b2308b93770ca993f5c55adc008ac7c42ceae646..1084ed55d6175ba67e75dde68a15b77885108b30 100644 --- a/lib/models/Courseware/Instance.php +++ b/lib/models/Courseware/Instance.php @@ -174,6 +174,45 @@ class Instance \UserConfig::get($user->id)->store('COURSEWARE_FAVORITE_BLOCK_TYPES', $favorites); } + /** + * Returns which layout is set for root node of this coursware instance + * + * @return string name of the layout + */ + public function getRootLayout(): string + { + $rootLayout = $this->unit->config['root_layout']; + if ($rootLayout) { + $this->validateRootLayout($rootLayout); + return $rootLayout; + } + + return 'default'; + } + + /** + * Sets layout of the root node page of this courseware + * + * @param string name of the layout + */ + public function setRootLayout(string $rootLayout): void + { + $this->validateRootLayout($rootLayout); + $this->unit->config['root_layout'] = $rootLayout; + } + + public function isValidRootLayout(string $rootLayout): bool + { + return in_array($rootLayout, ['default', 'toc', 'classic', 'none']); + } + + private function validateRootLayout(string $rootLayout): void + { + if (!$this->isValidRootLayout($rootLayout)) { + throw new \InvalidArgumentException('Invalid root layout for courseware.'); + } + } + /** * Returns whether this courseware instance uses a sequential progression through the structural elements. * diff --git a/lib/models/Courseware/Unit.php b/lib/models/Courseware/Unit.php index 6f3535d7351b9263ea827056b7bdb8c3735928cb..bf083281887865b7e9dea4aa43d4ba4518858bd8 100644 --- a/lib/models/Courseware/Unit.php +++ b/lib/models/Courseware/Unit.php @@ -172,4 +172,33 @@ class Unit extends \SimpleORMap implements \PrivacyObject $stmt = $db->prepare($query); $stmt->execute($args); } + + public function hasRootLayout() + { + return !isset($this->config['root_layout']) || $this->config['root_layout'] !== 'none'; + } + + public function findOrCreateFirstElement(): StructuralElement + { + if ($this->hasRootLayout()) { + return $this->structural_element; + } + + $children = $this->structural_element->children; + if (count($children) > 0) { + return $children[0]; + } + + $struct = StructuralElement::create([ + 'parent_id' => $this->structural_element->id, + 'range_id' => $this->range_id, + 'range_type' => $this->range_type, + 'owner_id' => $this->structural_element->owner_id, + 'editor_id' => $this->structural_element->editor_id, + 'title' => _('neue Seite'), + ]); + + + return $struct; + } } diff --git a/resources/assets/stylesheets/scss/courseware/layouts/tile.scss b/resources/assets/stylesheets/scss/courseware/layouts/tile.scss index c9cfd15c245af3c2102fe975c9b98a92325d0149..4fd93bda5bdc8fb135a6c74e809c2deda52cec4d 100644 --- a/resources/assets/stylesheets/scss/courseware/layouts/tile.scss +++ b/resources/assets/stylesheets/scss/courseware/layouts/tile.scss @@ -3,8 +3,8 @@ display: flex; flex-wrap: wrap; padding-left: 0; - row-gap: 5px; - column-gap: 5px; + gap: 5px; + overflow: hidden; } .cw-tiles .tile, .cw-tile { @@ -29,9 +29,6 @@ background-repeat: no-repeat; background-color: var(--content-color-20); background-position: center; - &.default-image { - @include background-icon(courseware, clickable, 128); - } .overlay-handle { @extend .drag-handle; diff --git a/resources/assets/stylesheets/scss/courseware/shelf.scss b/resources/assets/stylesheets/scss/courseware/shelf.scss index 0b6c93b3633c4a0e1463dd83c1ee7a4f74b0cebf..fb339b5b9b50044bee65b19a96e411b8ca4d8d72 100644 --- a/resources/assets/stylesheets/scss/courseware/shelf.scss +++ b/resources/assets/stylesheets/scss/courseware/shelf.scss @@ -62,3 +62,14 @@ height: 416px; } } + +.cw-unit-item-dialog-layout-content { + display: flex; + .cw-unit-item-dialog-layout-content-image { + padding-right: 2em; + } + .cw-unit-item-dialog-layout-content-settings { + width: 100%; + max-width: 500px; + } +} diff --git a/resources/assets/stylesheets/scss/courseware/structural-element.scss b/resources/assets/stylesheets/scss/courseware/structural-element.scss index 917684d110de034098a26b4ec093b3b5cd1e4878..ed1ba385749a31c97adca13ffb5b08e45c6c94de 100644 --- a/resources/assets/stylesheets/scss/courseware/structural-element.scss +++ b/resources/assets/stylesheets/scss/courseware/structural-element.scss @@ -157,7 +157,7 @@ .cw-structural-element-image-preview { display: block; max-height: 200px; - max-width: 400px; + max-width: 300px; width: auto; height: auto; margin: 0 auto; @@ -165,7 +165,7 @@ } .cw-structural-element-image-preview-placeholder { - width: 356px; + width: 300px; height: 200px; margin: 0 auto; background-color: var(--content-color-20); diff --git a/resources/vue/components/StudipIdentImage.vue b/resources/vue/components/StudipIdentImage.vue new file mode 100644 index 0000000000000000000000000000000000000000..80c1fa6d293e3596beb063b1d62d42c984939af8 --- /dev/null +++ b/resources/vue/components/StudipIdentImage.vue @@ -0,0 +1,247 @@ +<template> + <canvas v-show="showCanvas" ref="canvas"></canvas> +</template> + +<script> +export default { + name: 'studip-ident-image', + props: { + value: { + type: String, + }, + showCanvas: { + type: Boolean, + default: false, + }, + baseColor: { + type: String, // hex color + }, + pattern: { + type: String, + required: true, + }, + width: { + type: Number, + default: 270, + }, + height: { + type: Number, + default: 180, + }, + shapesMin: { + type: Number, + default: 5, + }, + shapesMax: { + type: Number, + default: 8, + }, + }, + data() { + return { + random: null, + ellipse: null, + }; + }, + methods: { + randint(min, max) { + return Math.floor(this.random() * (max - min) + min); + }, + renderIdentimage() { + let canvas = this.$refs.canvas; + canvas.width = this.width; + canvas.height = this.height; + + const minSize = Math.min(this.width, this.height) * 0.2; + const ctx = canvas.getContext('2d'); + const backgroundHSL = this.hexToHSL(this.baseColor); + const numShape = this.randint(this.shapesMin, this.shapesMax); + const shapeSizes = []; + + ctx.fillStyle = this.hexToRgbA(this.baseColor, 0.8); + ctx.fillRect(0, 0, canvas.width, canvas.height); + + const curveStart = this.randint(10, 70)/100 * this.height; + const curveEnd = this.randint(10, 70)/100 * this.height; + ctx.strokeStyle = `rgba(255, 255, 255, ${this.randint(30, 40) / 100})`; + const curvedistance = this.randint(10, 20); + const xFactor = this.randint(10, 45) / 100; + const yFactor = this.randint(10, 45) / 100; + for (let c = 0; c < numShape * 2; c++) { + ctx.beginPath(); + ctx.moveTo(0, curveStart + curvedistance * c); + ctx.bezierCurveTo(this.width * xFactor, this.height * yFactor, this.width * (xFactor + 0.5), this.height * (yFactor + 0.5), this.width, curveEnd + curvedistance * c); + ctx.stroke(); + } + + for (let i = 0; i < numShape; i++) { + shapeSizes.push(this.randint(minSize*0.2, minSize*2) + minSize); + } + + shapeSizes.sort((a, b) => { + return a < b ? 1 : a > b ? -1 : 0; + }); + + shapeSizes.forEach((shapeSizes, index) => { + const radius = shapeSizes / 2; + const [x, y] = this.createPointInEllipse(ctx); + const x_center = x * (this.width + radius / 2) - radius / 4; + const y_center = y * (this.height + radius / 2) - radius / 4; + + ctx.fillStyle = `rgba(255, 255, 255, ${this.randint(10, 80) / 100})`; + + ctx.beginPath(); + + if (index % 2 === 0) { + ctx.arc(x_center, y_center, radius, 0, 2 * Math.PI); + } else { + const size = radius; + ctx.moveTo(x_center + size * Math.cos(0), y_center + size * Math.sin(0)); + + for (let side = 0; side < 7; side++) { + ctx.lineTo( + x_center + size * Math.cos((side * 2 * Math.PI) / 6), + y_center + size * Math.sin((side * 2 * Math.PI) / 6) + ); + } + } + + ctx.fill(); + }); + + this.$emit('input', canvas.toDataURL()); + }, + createPointInEllipse(ctx) { + const x = this.random(); + const y = this.random(); + + if (ctx.isPointInPath(this.ellipse, x, y)) { + return [x, y]; + } + + return this.createPointInEllipse(...arguments); + }, + + cyrb128(value) { + let h1 = 1779033703, + h2 = 3144134277, + h3 = 1013904242, + h4 = 2773480762; + + for (let i = 0, k; i < value.length; i++) { + k = value.charCodeAt(i); + h1 = h2 ^ Math.imul(h1 ^ k, 597399067); + h2 = h3 ^ Math.imul(h2 ^ k, 2869860233); + h3 = h4 ^ Math.imul(h3 ^ k, 951274213); + h4 = h1 ^ Math.imul(h4 ^ k, 2716044179); + } + + h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067); + h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233); + h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213); + h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179); + + return [(h1 ^ h2 ^ h3 ^ h4) >>> 0, (h2 ^ h1) >>> 0, (h3 ^ h1) >>> 0, (h4 ^ h1) >>> 0]; + }, + sfc32(a, b, c, d) { + return function () { + a >>>= 0; + b >>>= 0; + c >>>= 0; + d >>>= 0; + var t = (a + b) | 0; + a = b ^ (b >>> 9); + b = (c + (c << 3)) | 0; + c = (c << 21) | (c >>> 11); + d = (d + 1) | 0; + t = (t + d) | 0; + c = (c + t) | 0; + + return (t >>> 0) / 4294967296; + }; + }, + + hexToRGB(color) { + color = color.slice(1); // remove # + let val = parseInt(color, 16); + let r = val >> 16; + let g = (val >> 8) & 0x00ff; + let b = val & 0x0000ff; + + if (g > 255) { + g = 255; + } else if (g < 0) { + g = 0; + } + if (b > 255) { + b = 255; + } else if (b < 0) { + b = 0; + } + + return { r: r, g: g, b: b }; + }, + RGBToHSL(r, g, b) { + r /= 255; + g /= 255; + b /= 255; + + let cmin = Math.min(r, g, b), + cmax = Math.max(r, g, b), + delta = cmax - cmin, + h = 0, + s = 0, + l = 0; + if (delta == 0) h = 0; + // Red is max + else if (cmax == r) h = ((g - b) / delta) % 6; + // Green is max + else if (cmax == g) h = (b - r) / delta + 2; + // Blue is max + else h = (r - g) / delta + 4; + + h = Math.round(h * 60); + + if (h < 0) h += 360; + l = (cmax + cmin) / 2; + + s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); + + s = +(s * 100).toFixed(1); + l = +(l * 100).toFixed(1); + + return { h: h, s: s, l: l }; + // return 'hsl(' + h + ',' + s + '%,' + l + '%)'; + }, + hexToHSL(color) { + const RGB = this.hexToRGB(color); + return this.RGBToHSL(RGB.r, RGB.g, RGB.b); + }, + hexToRgbA(hex, a){ + const RGB = this.hexToRGB(hex); + + return 'rgba(' + RGB.r + ',' + RGB.g + ',' + RGB.b + ',' + a +')'; + }, + init() { + const seed = this.cyrb128(this.pattern); + this.random = this.sfc32(...seed); + this.ellipse = new Path2D(); + this.ellipse.ellipse(0.5, 0.5, 0.5, 0.5, 0, 0, Math.PI * 2); + this.renderIdentimage(); + } + }, + mounted() { + this.init(); + }, + watch: { + baseColor() { + this.init(); + }, + }, +}; +</script> +<style scoped> + canvas { + background-color: #fff; + } +</style> \ No newline at end of file diff --git a/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue b/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue index ef111bfa4a0f26e72757449ee21a67853c539541..dd619dd1d8c2e03d64278fcb5c99d0b311c3844c 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 dd7e173bfb15e93b111ece3ed788d66db2edf86b..1b37511eb3d87a358a963e69c458536ca8fd9bab 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 0000000000000000000000000000000000000000..1a739a629f4da0250857fb019f9104e0e54b7974 --- /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 34d7fcbce2ce91d1641c2a519b65d2260b4303d1..03b2cec8c145c51e5e7dec2a3bab102afa79e377 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 ce6ad627e91a9b8a49b08744c80f55a2a16e4d79..234d9af13a8e9053e0fa1cbcf8383c873d4320f2 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 a4bfa35e48cc43cf2bdd3b55f230a7540be27996..418d443e88a5f36372cf3f6e08925363fd8d786d 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 6eadc06cb00d5e868d3db46a5fbf461583d0464f..ea738a6553f2564c2a0fc3ead742c3698dc0400b 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 bf637aedc82ec07b918d53562506a747ae5316dd..6c0ec6e7fbbd7e399c7859afc5ca14a5377dae93 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 f06f7fde0e601be77cbaecd38b76fedaa9ce75b7..702a98676d9cd26621b50f51d276ced92ee294ed 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 399a87c49a1136db2f16c180b3cec59998086621..ba2eaf4072805fd5893a597b8985cdde7922d0ad 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 5202abd7638e18cd9660acea9762f2f42b88c877..d8a8563ce1a855117229ce273e008b6af0fcd690 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({ diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js index 75ce62580ff525331f774df9cfc35f5ece763b73..1c969bec2644b36b31656a47c9b8c18d1f751b8c 100644 --- a/resources/vue/store/courseware/courseware.module.js +++ b/resources/vue/store/courseware/courseware.module.js @@ -77,6 +77,12 @@ const getters = { courseware(state) { return state.courseware; }, + rootLayout(state, getters) { + return getters.courseware.attributes['root-layout']; + }, + showRootElement(state, getters) { + return getters.rootLayout !== 'none'; + }, currentElement(state) { return state.currentElement; },