diff options
Diffstat (limited to 'resources/vue/components/courseware')
22 files changed, 465 insertions, 286 deletions
diff --git a/resources/vue/components/courseware/CoursewareContentPermissions.vue b/resources/vue/components/courseware/CoursewareContentPermissions.vue index d2412ef..60f0964 100644 --- a/resources/vue/components/courseware/CoursewareContentPermissions.vue +++ b/resources/vue/components/courseware/CoursewareContentPermissions.vue @@ -42,7 +42,7 @@ <td class="perm"> <input class="right" - :title="$gettextInterpolate($gettext('Leserechte für %{ userName }'), { userName: user_perm.username })" + :title="$gettextInterpolate($gettext('Leserechte für %{ userName }'), { userName: user_perm.username }, true)" type="radio" :name="`${user_perm.id}_right`" value="read" @@ -53,7 +53,7 @@ <td class="perm"> <input class="right" - :title="$gettextInterpolate($gettext('Lese- und Schreibrechte für %{ userName }'), { userName: user_perm.username })" + :title="$gettextInterpolate($gettext('Lese- und Schreibrechte für %{ userName }'), { userName: user_perm.username }, true)" type="radio" :name="`${user_perm.id}_right`" value="write" @@ -75,7 +75,7 @@ <td class="actions"> <button class="cw-permission-delete" - :title="$gettextInterpolate($gettext('Entfernen der Rechte von %{ userName }'), { userName: user_perm.username })" + :title="$gettextInterpolate($gettext('Entfernen der Rechte von %{ userName }'), { userName: user_perm.username }, true)" @click.prevent="confirmDeleteUserPerm(index)" > </button> diff --git a/resources/vue/components/courseware/blocks/CoursewareBiographyCareerBlock.vue b/resources/vue/components/courseware/blocks/CoursewareBiographyCareerBlock.vue index c366618..98fab71 100644 --- a/resources/vue/components/courseware/blocks/CoursewareBiographyCareerBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareBiographyCareerBlock.vue @@ -17,8 +17,8 @@ class="cw-timeline-item" > <div class="cw-timeline-item-icon cw-timeline-item-icon-color-studip-blue"> - <studip-icon v-if="item.type === 'school'" shape="doctoral-cap" role="clickable" size="32"/> - <studip-icon v-if="item.type === 'experience'" shape="tools" role="clickable" size="32"/> + <studip-icon v-if="item.type === 'school'" shape="doctoral-cap" role="clickable" :size="32"/> + <studip-icon v-if="item.type === 'experience'" shape="tools" role="clickable" :size="32"/> </div> <div class="cw-timeline-item-content cw-timeline-item-content-color-studip-blue" diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue b/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue index 89beb0a..eea76fe 100644 --- a/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue +++ b/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue @@ -3,6 +3,7 @@ <studip-action-menu :items="menuItems" :context="block.attributes.title" + :collapseAt="1" @editBlock="editBlock" @setVisibility="setVisibility" @showInfo="showInfo" diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue b/resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue index d1ed09f..82d1abc 100644 --- a/resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue +++ b/resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue @@ -2,7 +2,7 @@ <section class="cw-block-edit"> <header v-if="preview">{{ $gettext('Bearbeiten') }}</header> <div class="cw-block-features-content"> - <div @click="deactivateToolbar(); exitHandler = true;"> + <div @click="exitHandler = true;"> <slot name="edit" /> </div> <div class="cw-button-box"> @@ -31,16 +31,6 @@ export default { beforeMount() { this.originalBlock = this.block; }, - methods: { - ...mapActions({ - coursewareBlockAdder: 'coursewareBlockAdder', - coursewareShowToolbar: 'coursewareShowToolbar' - }), - deactivateToolbar() { - this.coursewareBlockAdder({}); - this.coursewareShowToolbar(false); - }, - }, beforeDestroy() { if (this.exitHandler) { this.$emit('store'); diff --git a/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue b/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue index 669a5b8..24a59f5 100644 --- a/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue @@ -127,7 +127,7 @@ <studip-file-chooser v-model="currentFileId" selectable="file" - :courseId="studipContext.id" + :courseId="context.id" :userId="userId" :isImage="true" :excludedCourseFolderTypes="excludedCourseFolderTypes" @@ -187,7 +187,7 @@ export default { currentUserView: 'own', currentFile: {}, - context: {}, + canvasContext: {}, paint: false, write: false, clickX: [], @@ -221,7 +221,6 @@ export default { }, computed: { ...mapGetters({ - studipContext: 'context', fileRefById: 'file-refs/byId', getUserDataById: 'courseware-user-data-fields/byId', relatedUserData: 'user-data-field/related', @@ -301,9 +300,7 @@ export default { return this.currentUploadFolderId !== ""; }, canSwitchView() { - // this feature is not something to offer in the Arbeitsplatz! - let context = this.$store.getters.context; - if (context.type !== 'courses') { + if (this.context.type !== 'courses') { return false; } if (this.currentShowUserData === 'off') { @@ -419,7 +416,7 @@ export default { } else { canvas.height = 500; } - this.context = canvas.getContext('2d'); + this.canvasContext = canvas.getContext('2d'); this.setColor('blue'); this.currentSize = this.sizes['normal']; this.currentTool = this.tools['pen']; @@ -427,7 +424,7 @@ export default { }, redraw() { let view = this; - let context = view.context; + let context = view.canvasContext; context.clearRect(0, 0, context.canvas.width, context.canvas.height); // Clears the canvas context.fillStyle = '#ffffff'; context.fillRect(0, 0, context.canvas.width, context.canvas.height); // set background @@ -658,7 +655,7 @@ export default { }, async store() { let user = this.usersById({id: this.userId}); - let imageBase64 = this.context.canvas.toDataURL("image/jpeg", 1.0); + let imageBase64 = this.canvasContext.canvas.toDataURL("image/jpeg", 1.0); let image = await fetch(imageBase64); let imageBlob = await image.blob(); let file = {}; diff --git a/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue index 72cfeb3..55e7b01 100644 --- a/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue @@ -131,8 +131,8 @@ </button> <select v-model="currentScale" :aria-label="$gettext('Zoom')" @change="updateZoom"> <option v-show="false" :value="currentScale">{{ formattedZoom }}%</option> - <option v-for="(value, index) in scaleValues" :key="index" :value="value"> - {{ value * 100 }}% + <option v-for="(value, index) in scaleValues" :key="index" :value="value.scale"> + {{ value.name }} </option> </select> </div> @@ -305,7 +305,7 @@ export default { pdfAnnotationLayer: null, pdfAnnotation: false, pdfRotate: 0, - PdfViewer: null, + pdfViewer: null, pdfEventBus: null, pdfLinkService: null, pdfFindController: null, @@ -322,8 +322,8 @@ export default { pageNum: 1, pageCount: 0, scale: 1, + baseScale: 1, currentScale: 1, - scaleValues: [0.5, 1, 1.5, 2, 3, 4], file: null, srMessage: '', @@ -361,10 +361,23 @@ export default { formattedZoom() { return Number.parseInt(this.scale * 100, 10); }, + scaleValues() { + const defaultValues = [ + { name: '25%', scale: 0.25 }, + { name: '50%', scale: 0.5 }, + { name: '75%', scale: 0.75 }, + { name: '100%', scale: 1.0 }, + { name: '150%', scale: 1.5 }, + { name: '200%', scale: 2.0 }, + { name: '300%', scale: 3.0 }, + ]; + + return defaultValues.concat([{ name: this.$gettext('volle Breite'), scale: this.baseScale }]); + }, }, watch: { scale(newValue) { - let overflow = newValue > 1 ? 'auto' : 'hidden'; + let overflow = newValue > this.baseScale ? 'auto' : 'hidden'; let container = this.$refs.container; container.style.overflow = overflow; this.currentScale = newValue; @@ -407,7 +420,7 @@ export default { if (this.currentUrl) { let view = this; view.pdfEventBus = new EventBus(); - view.pdfLoadingTask = getDocument(this.currentUrl).promise; + view.pdfLoadingTask = getDocument({ url: this.currentUrl, verbosity: 0 }).promise; view.pdfLoadingTask.__PDFDocumentLoadingTask = true; // Link Service view.pdfLinkService = new PDFLinkService({ @@ -473,9 +486,16 @@ export default { .getPage(parseInt(view.pageNum)) .then((pdfPage) => { view.pdfPage = pdfPage; + const width = outerContainer.offsetWidth; + let pdfWidth = pdfPage.view[2]; + if (pdfPage.rotate === 90 || pdfPage.rotate === 270) { + pdfWidth = pdfPage.view[3]; + } + view.baseScale = (width / pdfWidth / 1.33).toFixed(2); + view.scale = view.baseScale; // Creating the page view with default parameters. let defaultViewport = pdfPage.getViewport({ - scale: 1.35, + scale: 1.0, }); view.pdfBasePage = new PDFViewer({ @@ -512,8 +532,6 @@ export default { view.pdfViewer.setPdfPage(view.pdfPage); // Set LinkService viewer view.pdfLinkService.setViewer(view.pdfViewer); - // Set outer container height - outerContainer.style.height = container.offsetHeight + 'px'; view.renderPage(); }) .catch((err) => { @@ -610,12 +628,12 @@ export default { this.updateSrMessage(this.$gettext('gedreht')); }, zoomIn() { - this.scale = this.scale < 4 ? (this.scale * 10 + 1) / 10 : this.scale; + this.scale = this.scale < 4 ? ((this.scale * 10 + 1) / 10).toFixed(1) : this.scale; this.renderPage(); this.updateSrMessage(this.$gettext('vergrößert')); }, zoomOut() { - this.scale = this.scale > 0.1 ? (this.scale * 10 - 1) / 10 : this.scale; + this.scale = this.scale > 0.1 ? ((this.scale * 10 - 1) / 10).toFixed(1) : this.scale; this.renderPage(); this.updateSrMessage(this.$gettext('verkleinert')); }, diff --git a/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue b/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue index be56ab4..4ff7e45 100644 --- a/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue @@ -234,17 +234,16 @@ export default { this.currentUrlIsValid = this.isValidUrl(this.currentUrl); }, isValidUrl(urlString) { - const urlPattern = new RegExp( - '^(https?:\\/\\/)?' + // validate protocol - '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // validate domain name - '((\\d{1,3}\\.){3}\\d{1,3}))' + // validate OR ip (v4) address - '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // validate port and path - '(\\?[;&a-z\\d%_.~+=-]*)?' + // validate query string - '(\\#[-a-z\\d_]*)?$', - 'i' - ); // validate fragment locator + if (!urlString.startsWith('http')) { + urlString = `${location.protocol}//${urlString}`; + } - return !!urlPattern.test(urlString); + try { + const url = new URL(urlString); + return ['http:', 'https:'].includes(url.protocol); + } catch (e) { + return false; + } }, updateUrl() { diff --git a/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue b/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue index 6d12980..4f52d4b 100644 --- a/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue @@ -113,7 +113,7 @@ > <template #open-indicator="selectAttributes"> <span v-bind="selectAttributes" - ><studip-icon shape="arr_1down" size="10" + ><studip-icon shape="arr_1down" :size="10" /></span> </template> <template #no-options> @@ -141,7 +141,7 @@ > <template #open-indicator="selectAttributes"> <span v-bind="selectAttributes" - ><studip-icon shape="arr_1down" size="10" + ><studip-icon shape="arr_1down" :size="10" /></span> </template> <template #no-options> diff --git a/resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue b/resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue index a410740..e52b608 100644 --- a/resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue @@ -38,7 +38,7 @@ v-model="currentColor" > <template #open-indicator="selectAttributes"> - <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10" /></span> + <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10" /></span> </template> <template #no-options> {{ $gettext('Es steht keine Auswahl zur Verfügung.') }} @@ -57,7 +57,7 @@ {{ $gettext('Icon') }} <studip-select :options="icons" :clearable="false" v-model="currentIcon"> <template #open-indicator="selectAttributes"> - <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10" /></span> + <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10" /></span> </template> <template #no-options> {{ $gettext('Es steht keine Auswahl zur Verfügung.') }} diff --git a/resources/vue/components/courseware/containers/CoursewareContainerActions.vue b/resources/vue/components/courseware/containers/CoursewareContainerActions.vue index 79379bf..875ab1f 100644 --- a/resources/vue/components/courseware/containers/CoursewareContainerActions.vue +++ b/resources/vue/components/courseware/containers/CoursewareContainerActions.vue @@ -3,6 +3,7 @@ <studip-action-menu :items="menuItems" :context="container.attributes.title" + :collapseAt="1" @editContainer="editContainer" @changeContainer="changeContainer" @deleteContainer="deleteContainer" diff --git a/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue b/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue index f617e5a..f26c8d7 100644 --- a/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue +++ b/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue @@ -1,15 +1,9 @@ -<template> - <div class="cw-companion-box" :class="[mood]"> - <div> - <p v-html="msgCompanion"></p> - <slot name="companionActions"></slot> - </div> - </div> -</template> - <script> export default { name: 'courseware-companion-box', + render(createElement) { + return null; + }, props: { msgCompanion: String, mood: { @@ -20,5 +14,40 @@ export default { } } }, + computed: { + msgType() { + let type = 'info'; + switch (this.mood) { + case 'special': + case 'unsure': + type = 'warning'; + break; + case 'sad': + type = 'error'; + break; + case 'happy': + type = 'success'; + break + case 'pointing': + case 'curious': + } + return type; + } + }, + watch: { + msgCompanion: { + handler(current) { + if (current.trim().length === 0) { + return; + } + const notification = { + type: this.msgType, + message: current + }; + this.globalEmit('push-system-notification', notification); + }, + immediate: true + } + } }; -</script>
\ No newline at end of file +</script> diff --git a/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue b/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue index ff177f7..bfd0a93 100644 --- a/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue +++ b/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue @@ -1,28 +1,11 @@ -<template> - <div class="cw-companion-overlay-wrapper"> - <div - class="cw-companion-overlay" - :class="[showCompanion ? 'cw-companion-overlay-in' : '', showCompanion ? '' : 'cw-companion-overlay-out', styleCompanion]" - aria-hidden="true" - > - <div class="cw-companion-overlay-content" v-html="msgCompanion"></div> - <button class="cw-compantion-overlay-close" @click="hideCompanion"></button> - </div> - <div - class="sr-only" - aria-live="polite" - role="log" - > - <p>{{ msgCompanion }}</p> - </div> - </div> -</template> - <script> import { mapActions, mapGetters } from 'vuex'; export default { name: 'courseware-companion-overlay', + render(createElement) { + return null; + }, computed: { ...mapGetters({ showCompanion: 'showCompanionOverlay', @@ -30,6 +13,24 @@ export default { styleCompanion: 'styleCompanionOverlay', showToolbar: 'showToolbar', }), + msgType() { + let type = 'info'; + switch (this.styleCompanion) { + case 'special': + case 'unsure': + type = 'warning'; + break; + case 'sad': + type = 'error'; + break; + case 'happy': + type = 'success'; + break + case 'pointing': + case 'curious': + } + return type; + } }, methods: { ...mapActions({ @@ -49,11 +50,24 @@ export default { } }, showToolbar(newValue, oldValue) { - // hide companion when toolbar is closed + // hide companion when toolbar is closed if (oldValue === true && newValue === false) { this.hideCompanion(); } + }, + msgCompanion: { + handler(current) { + if (current.trim().length === 0) { + return; + } + const notification = { + type: this.msgType, + message: current + }; + this.globalEmit('push-system-notification', notification); + }, + immediate: true } - }, + } }; </script> diff --git a/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue b/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue index 1a739a6..1aa8802 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue @@ -16,21 +16,21 @@ </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"> + <div class="cw-root-content-img" :style="bgImage"> <section class="cw-root-content-description" :style="bgColor"> - <img v-if="imageIsSet" class="cw-root-content-description-img" :src="imageURL" /> - <template v-else> + <div class="cw-root-content-description-img" :src="imageURL" :style="image"></div> + <template v-if="!imageIsSet"> <studip-ident-image class="cw-root-content-description-img" - v-model="identImageCanvas" - :showCanvas="true" + v-model="identImage" :baseColor="bgColorHex" :pattern="structuralElement.attributes.title" /> <studip-ident-image - v-model="identImage" - :width="1095" - :height="withTOC ? 300 : 480" + v-model="identBgImage" + class="cw-root-content-description-background-img" + :width="4380" + :height="withTOC ? 1200 : 1920" :baseColor="bgColorHex" :pattern="structuralElement.attributes.title" /> @@ -46,44 +46,28 @@ </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]" - > + <li v-for="child in childElements" :key="child.id"> <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> + <courseware-tile + tag="div" + :color="child.attributes.payload.color" + :title="child.attributes.title || '–'" + :imageUrl="hasImage(child) ? child.relationships?.image?.meta?.['download-url'] : ''" + > + <template #description> + {{ child.attributes.payload.description }} + </template> + <template #footer> + <p class="cw-root-content-toc-tile-footer"> + {{ + $gettextInterpolate( + $ngettext('%{pages} Seite', '%{pages} Seiten', countChildChildren(child)), + { pages: countChildChildren(child) } + ) + }} + </p> + </template> + </courseware-tile> </router-link> </li> </ul> @@ -93,6 +77,7 @@ <script> import CoursewareCompanionBox from './../layouts/CoursewareCompanionBox.vue'; +import CoursewareTile from './../layouts/CoursewareTile.vue'; import StudipIdentImage from './../../StudipIdentImage.vue'; import colorMixin from '@/vue/mixins/courseware/colors.js'; import { mapActions, mapGetters } from 'vuex'; @@ -100,18 +85,19 @@ import { mapActions, mapGetters } from 'vuex'; export default { name: 'courseware-root-content', mixins: [colorMixin], - props: { - structuralElement: Object, - canEdit: Boolean, - }, components: { CoursewareCompanionBox, + CoursewareTile, StudipIdentImage, }, + props: { + structuralElement: Object, + canEdit: Boolean, + }, data() { return { identImage: '', - identImageCanvas: '', + identBgImage: '', }; }, computed: { @@ -130,6 +116,13 @@ export default { image() { let style = {}; const backgroundURL = this.imageIsSet ? this.imageURL : this.identImage; + style.backgroundImage = 'url(' + backgroundURL + ')'; + + return style; + }, + bgImage() { + let style = {}; + const backgroundURL = this.imageIsSet ? this.imageURL : this.identBgImage; style.backgroundImage = 'url(' + backgroundURL + ')'; style.height = this.withTOC ? '300px' : '480px'; @@ -180,7 +173,7 @@ export default { }, addPage() { this.showElementAddDialog(true); - } + }, }, }; </script> @@ -196,16 +189,20 @@ export default { } .cw-root-content-description { display: flex; - flex-direction: row; - margin: 0 8em; - padding: 2em 4px 2em 2em; position: relative; - top: 8em; + flex-direction: column; + margin: 0 1em; + padding: 1em 4px 1em 1em; + top: 1em; + gap: 10px; .cw-root-content-description-img { - width: 240px; - height: fit-content; - margin-right: 2em; + min-width: 135px; + height: 90px; + background-color: #fff; + background-size: cover; + background-position: center; + margin-right: 1em; } .cw-root-content-description-text { max-height: calc(480px - 18em); @@ -233,14 +230,68 @@ export default { max-width: 1095px; margin-bottom: 1em; .cw-root-content-description { - margin: 0 8em; - top: 1.5em; + height: calc(100% - 4em); .cw-root-content-description-text { max-height: calc(300px - 6em); } } + .cw-root-content-toc-tile-footer { + line-height: 4em; + } } .cw-root-content-hint { max-width: 1095px; } + +.size-small { + .cw-root-content-description { + flex-direction: row; + padding: 1em 4px 1em 1em; + + .cw-root-content-description-img { + min-width: 135px; + height: 90px; + } + } + + .cw-root-content-default { + .cw-root-content-description { + margin: 0 4em; + top: 8em; + } + } + .cw-root-content-toc { + .cw-root-content-description { + height: calc(100% - 6em); + margin: 0 4em; + top: 1.5em; + } + } +} + +.size-large { + .cw-root-content-description { + flex-direction: row; + padding: 2em 4px 2em 2em; + + .cw-root-content-description-img { + min-width: 270px; + height: 180px; + } + } + + .cw-root-content-default { + .cw-root-content-description { + margin: 0 8em; + top: 8em; + } + } + .cw-root-content-toc { + .cw-root-content-description { + height: calc(100% - 7em); + margin: 0 8em; + top: 1.5em; + } + } +} </style> diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogImport.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogImport.vue index 60b404e..019bbf2 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogImport.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogImport.vue @@ -130,7 +130,6 @@ export default { this.importZipFile = event.target.files[0]; this.setImportFilesProgress(0); this.setImportStructuresProgress(0); - this.setImportErrors([]); }, async importCoursewareArchiv() { this.importAborted = false; @@ -199,6 +198,9 @@ export default { await this.importCourseware(courseware, this.currentElement, files, this.importBehavior, null); } + }, + mounted() { + this.setImportErrors([]); } } </script> diff --git a/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue b/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue index 1e076c7..7680167 100644 --- a/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue +++ b/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue @@ -179,7 +179,7 @@ :aria-label=" $gettextInterpolate($gettext('%{userName} auswählen'), { userName: user.formattedname, - }) + }, true) " /> </td> @@ -217,7 +217,7 @@ :aria-label=" $gettextInterpolate($gettext('%{groupName} auswählen'), { groupName: group.name, - }) + }, true) " /> </td> diff --git a/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue b/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue index a843341..d7db6e1 100644 --- a/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue +++ b/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue @@ -44,7 +44,7 @@ :aria-label=" $gettextInterpolate($gettext('%{userName} auswählen'), { userName: user.formattedname, - }) + }, true) " /> </td> @@ -77,7 +77,7 @@ :aria-label=" $gettextInterpolate($gettext('%{groupName} auswählen'), { groupName: group.name, - }) + }, true) " /> </td> diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue index d31e963..dc70348 100644 --- a/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue +++ b/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue @@ -2,7 +2,7 @@ <div class="cw-toolbar-wrapper"> <div id="cw-toolbar" class="cw-toolbar" :style="toolbarStyle"> <div v-if="showTools" class="cw-toolbar-tools" :class="{ unfold: unfold, hd: isHd, wqhd: isWqhd }"> - <div class="cw-toolbar-button-wrapper"> + <div id="cw-toolbar-nav" class="cw-toolbar-button-wrapper"> <button class="cw-toolbar-button" :class="{ active: activeTool === 'blockAdder' }" @@ -35,9 +35,19 @@ <studip-icon shape="arr_2right" :size="24" /> </button> </div> - <courseware-toolbar-blocks v-if="activeTool === 'blockAdder'" /> - <courseware-toolbar-containers v-if="activeTool === 'containerAdder'" /> - <courseware-toolbar-clipboard v-if="activeTool === 'clipboard'" /> + <div class="cw-toolbar-tool-wrapper"> + <CoursewareToolbarBlocks + v-if="activeTool === 'blockAdder'" + :toolbarContentHeight="toolbarContentHeight" + /> + <CoursewareToolbarContainers + v-if="activeTool === 'containerAdder'" + /> + <CoursewareToolbarClipboard + v-if="activeTool === 'clipboard'" + :toolbarContentHeight="toolbarContentHeight" + /> + </div> </div> <div v-else class="cw-toolbar-folded-wrapper"> <button @@ -97,20 +107,26 @@ export default { toolbarActive: 'toolbarActive', hideEditLayout: 'hideEditLayout', }), - toolbarStyle() { - const scrollTopStyles = window.getComputedStyle(document.getElementById('scroll-to-top')); + scrollTopStyles() { + return window.getComputedStyle(document.getElementById('scroll-to-top')); + }, + toolbarHeight() { const scrollTopHeight = - parseInt(scrollTopStyles['height'], 10) + - parseInt(scrollTopStyles['padding-top'], 10) + - parseInt(scrollTopStyles['padding-bottom'], 10) + - parseInt(scrollTopStyles['margin-bottom'], 10); - let height = parseInt( + parseInt(this.scrollTopStyles['height'], 10) + + parseInt(this.scrollTopStyles['padding-top'], 10) + + parseInt(this.scrollTopStyles['padding-bottom'], 10) + + parseInt(this.scrollTopStyles['margin-bottom'], 10); + return parseInt( Math.min(this.windowInnerHeight * 0.9, this.windowInnerHeight - this.toolbarTop - scrollTopHeight) ); - + }, + toolbarContentHeight() { + return this.toolbarHeight - 55; + }, + toolbarStyle() { return { - height: height + 'px', - minHeight: height + 'px', + height: this.toolbarHeight + 'px', + minHeight: this.toolbarHeight + 'px', top: this.toolbarTop + 'px', }; }, @@ -183,8 +199,8 @@ export default { }, watch: { - containers(oldValue, newValue) { - if (oldValue && newValue && oldValue.length !== newValue.length) { + containers(newValue, oldValue) { + if (newValue) { this.resetAdderStorage(); } }, diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue index 2795e62..ceda0f0 100644 --- a/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue +++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue @@ -1,85 +1,88 @@ <template> <div class="cw-toolbar-blocks"> - <form @submit.prevent="loadSearch"> - <div class="input-group files-search search cw-block-search"> - <input - ref="searchBox" - type="text" - v-model="searchInput" - @click.stop - :label="$gettext('Geben Sie einen Suchbegriff mit mindestens 3 Zeichen ein.')" - /> - <span class="input-group-append" @click.stop> - <button - v-if="searchInput" - type="button" - class="button reset-search" - id="reset-search" - :title="$gettext('Suche zurücksetzen')" - @click="resetSearch" - > - <studip-icon shape="decline" :size="20"></studip-icon> - </button> - <button - type="submit" - class="button" - id="search-btn" - :title="$gettext('Suche starten')" - @click="loadSearch" - > - <studip-icon shape="search" :size="20"></studip-icon> - </button> - </span> - </div> - </form> + <div id="cw-toolbar-blocks-header" class="cw-toolbar-tool-header"> + <form @submit.prevent="loadSearch"> + <div class="input-group files-search search cw-block-search"> + <input + ref="searchBox" + type="text" + v-model="searchInput" + @click.stop + :label="$gettext('Geben Sie einen Suchbegriff mit mindestens 3 Zeichen ein.')" + /> + <span class="input-group-append" @click.stop> + <button + v-if="searchInput" + type="button" + class="button reset-search" + id="reset-search" + :title="$gettext('Suche zurücksetzen')" + @click="resetSearch" + > + <studip-icon shape="decline" :size="20"></studip-icon> + </button> + <button + type="submit" + class="button" + id="search-btn" + :title="$gettext('Suche starten')" + @click="loadSearch" + > + <studip-icon shape="search" :size="20"></studip-icon> + </button> + </span> + </div> + </form> - <div class="filterpanel"> - <span class="sr-only">{{ $gettext('Kategorien-Filter') }}</span> - <button - v-for="category in blockCategories" - :key="category.type" - class="button" - :class="{ 'button-active': category.type === currentFilterCategory }" - :aria-pressed="category.type === currentFilterCategory ? 'true' : 'false'" - @click="selectCategory(category.type)" - > - {{ category.title }} - </button> + <div class="filterpanel"> + <span class="sr-only">{{ $gettext('Kategorien-Filter') }}</span> + <button + v-for="category in blockCategories" + :key="category.type" + class="button" + :class="{ 'button-active': category.type === currentFilterCategory }" + :aria-pressed="category.type === currentFilterCategory ? 'true' : 'false'" + @click="selectCategory(category.type)" + > + {{ category.title }} + </button> + </div> </div> - - <div v-if="filteredBlockTypes.length > 0" class="cw-blockadder-item-list"> - <draggable - v-if="filteredBlockTypes.length > 0" - class="cw-blockadder-item-list" - tag="div" - role="listbox" - v-model="filteredBlockTypes" - handle=".cw-sortable-handle-blockadder" - :group="{ name: 'blocks', pull: 'clone', put: 'false' }" - :clone="cloneBlock" - :sort="false" - :emptyInsertThreshold="20" - @start="dragBlockStart($event)" - @end="dropNewBlock($event)" - ref="sortables" - sectionId="0" - > - <courseware-blockadder-item - v-for="(block, index) in filteredBlockTypes" - :key="index" - :title="block.title" - :type="block.type" - :data-blocktype="block.type" - :description="block.description" - @blockAdded="$emit('blockAdded')" - /> - </draggable> + <div class="cw-toolbar-tool-content" :style="toolContentStyle"> + <div v-if="filteredBlockTypes.length > 0" class="cw-blockadder-item-list"> + <draggable + v-if="filteredBlockTypes.length > 0" + class="cw-blockadder-item-list" + tag="div" + role="listbox" + v-model="filteredBlockTypes" + handle=".cw-sortable-handle-blockadder" + :group="{ name: 'blocks', pull: 'clone', put: 'false' }" + :clone="cloneBlock" + :sort="false" + :emptyInsertThreshold="20" + @start="dragBlockStart($event)" + @end="dropNewBlock($event)" + ref="sortables" + sectionId="0" + > + <courseware-blockadder-item + v-for="(block, index) in filteredBlockTypes" + :key="index" + :title="block.title" + :type="block.type" + :data-blocktype="block.type" + :description="block.description" + @blockAdded="$emit('blockAdded')" + /> + </draggable> + </div> + <courseware-companion-box + v-else + :msgCompanion="$gettext('Es wurden keine passenden Blöcke gefunden.')" + mood="pointing" + /> </div> - <courseware-companion-box - v-else - :msgCompanion="$gettext('Es wurden keine passenden Blöcke gefunden.')" - mood="pointing" - /> </div> </template> @@ -99,6 +102,12 @@ export default { CoursewareCompanionBox, draggable, }, + props: { + toolbarContentHeight: { + type: Number, + required: true, + }, + }, data() { return { searchInput: '', @@ -132,6 +141,13 @@ export default { { title: this.$gettext('Biografie'), type: 'biography' }, ]; }, + toolContentStyle() { + const height = this.toolbarContentHeight - 115; + + return { + height: height + 'px', + }; + }, }, methods: { ...mapActions({ diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue index 98cd573..c6f1e92 100644 --- a/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue +++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue @@ -1,5 +1,5 @@ <template> - <div class="cw-toolbar-clipboard"> + <div class="cw-toolbar-clipboard cw-toolbar-tool-content" :style="toolContentStyle"> <courseware-collapsible-box :title="$gettext('Blöcke')" :open="clipboardBlocks.length > 0"> <template v-if="clipboardBlocks.length > 0"> <div class="cw-element-inserter-wrapper"> @@ -101,7 +101,12 @@ export default { StudipDialog, draggable, }, - + props: { + toolbarContentHeight: { + type: Number, + required: true, + }, + }, data() { return { showDeleteClipboardDialog: false, @@ -143,6 +148,11 @@ export default { } return ''; }, + toolContentStyle() { + return { + height: this.toolbarContentHeight + 'px', + }; + }, }, methods: { ...mapActions({ diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItem.vue b/resources/vue/components/courseware/unit/CoursewareUnitItem.vue index f09601e..dc68225 100644 --- a/resources/vue/components/courseware/unit/CoursewareUnitItem.vue +++ b/resources/vue/components/courseware/unit/CoursewareUnitItem.vue @@ -63,7 +63,7 @@ :question=" $gettextInterpolate($gettext('Möchten Sie das Lernmaterial %{ unitTitle } wirklich löschen?'), { unitTitle: title, - }) + }, true) " height="200" @confirm="executeDelete" diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue index f956e47..572e171 100644 --- a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue +++ b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue @@ -1,11 +1,11 @@ <template> - <studip-dialog + <studip-dialog :title="$gettext('Darstellung')" :confirmText="$gettext('Speichern')" confirmClass="accept" :closeText="$gettext('Schließen')" closeClass="cancel" - height="470" + height="540" width="870" @close="$emit('close')" @confirm="storeLayout" @@ -23,12 +23,8 @@ <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"> + <courseware-companion-box v-if="uploadFileError" :msgCompanion="uploadFileError" mood="sad" /> + <template v-if="!showPreviewImage"> <img v-if="currentFile" :src="uploadImageURL" @@ -36,14 +32,32 @@ :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('Bild hochladen') }} + <input + class="cw-file-input" + ref="upload_image" + type="file" + accept="image/*" + @change="checkUploadFile" + /> + </label> + {{ $gettext('oder') }} + <br /> + <button class="button" type="button" @click="showStockImageSelector = true"> + {{ $gettext('Aus dem Bilderpool auswählen') }} + </button> + <StockImageSelector + v-if="showStockImageSelector" + @close="showStockImageSelector = false" + @select="onSelectStockImage" + /> + </template> </form> <form class="default cw-unit-item-dialog-layout-content-settings" @submit.prevent=""> <label> {{ $gettext('Titel') }} - <input type="text" v-model="currentElement.attributes.title"/> + <input type="text" v-model="currentElement.attributes.title" /> </label> <label> {{ $gettext('Beschreibung') }} @@ -62,13 +76,9 @@ 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') }}. + <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> @@ -96,19 +106,19 @@ <script> import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue'; - +import StockImageSelector from '../../stock-images/SelectorDialog.vue'; import colorMixin from '@/vue/mixins/courseware/colors.js'; import { mapActions, mapGetters } from 'vuex'; - export default { name: 'courseware-unit-item-dialog-layout', components: { - CoursewareCompanionBox + CoursewareCompanionBox, + StockImageSelector, }, props: { unit: Object, - unitElement: Object + unitElement: Object, }, mixins: [colorMixin], data() { @@ -120,18 +130,26 @@ export default { uploadImageURL: null, currentRootLayout: 'default', loadingInstance: false, - } + showStockImageSelector: false, + selectedStockImage: null, + }; }, computed: { ...mapGetters({ context: 'context', instanceById: 'courseware-instances/byId', - userId: 'userId' + userId: 'userId', }), colors() { - return this.mixinColors.filter(color => color.darkmode); + return this.mixinColors.filter((color) => color.darkmode); }, image() { + if (this.selectedStockImage) { + return this.selectedStockImage.attributes['download-urls'].small + } + if (this.uploadImageURL) { + return this.uploadImageURL; + } return this.currentElement.relationships?.image?.meta?.['download-url'] ?? null; }, @@ -140,15 +158,14 @@ export default { }, instance() { if (this.inCourseContext) { - return this.instanceById({id: 'course_' + this.context.id + '_' + this.unit.id}); + return this.instanceById({ id: 'course_' + this.context.id + '_' + this.unit.id }); } else { - return this.instanceById({id: 'user_' + this.context.id + '_' + this.unit.id}); + return this.instanceById({ id: 'user_' + this.context.id + '_' + this.unit.id }); } - }, inCourseContext() { return this.context.type === 'courses'; - } + }, }, methods: { ...mapActions({ @@ -162,9 +179,10 @@ export default { uploadImageForStructuralElement: 'uploadImageForStructuralElement', deleteImageForStructuralElement: 'deleteImageForStructuralElement', storeCoursewareSettings: 'storeCoursewareSettings', + setStockImageForStructuralElement: 'setStockImageForStructuralElement', }), async loadUnitInstance() { - const context = {type: this.context.type, id: this.context.id, unit: this.unit.id}; + const context = { type: this.context.type, id: this.context.id, unit: this.unit.id }; await this.loadInstance(context); }, initData() { @@ -192,11 +210,13 @@ export default { this.$emit('close'); await this.loadStructuralElement(this.currentElement.id); if ( - this.unitElement.relationships['edit-blocker'].data !== null - && this.unitElement.relationships['edit-blocker'].data?.id !== this.userId + this.unitElement.relationships['edit-blocker'].data !== null && + this.unitElement.relationships['edit-blocker'].data?.id !== this.userId ) { this.companionWarning({ - info: this.$gettext('Ihre Änderungen konnten nicht gespeichert werden, die Daten werden bereits von einem anderen Nutzer bearbeitet.') + info: this.$gettext( + 'Ihre Änderungen konnten nicht gespeichert werden, die Daten werden bereits von einem anderen Nutzer bearbeitet.' + ), }); return false; } else { @@ -207,13 +227,20 @@ export default { this.uploadImageForStructuralElement({ structuralElement: this.currentElement, file: this.currentFile, - }).then(() => { - this.loadStructuralElement(this.currentElement.id) - }).catch((error) => { - console.error(error); - this.companionWarning({ - info: this.$gettext('Beim Hochladen der Bilddatei ist ein Fehler aufgetretten.') + }) + .then(() => { + this.loadStructuralElement(this.currentElement.id); + }) + .catch((error) => { + console.error(error); + this.companionWarning({ + info: this.$gettext('Beim Hochladen der Bilddatei ist ein Fehler aufgetretten.'), + }); }); + } else if (this.selectedStockImage) { + await this.setStockImageForStructuralElement({ + structuralElement: this.currentElement, + stockImage: this.selectedStockImage, }); } else if (this.deletingPreviewImage) { await this.deleteImageForStructuralElement(this.currentElement); @@ -233,13 +260,21 @@ export default { instance: currentInstance, }); } - } + }, + onSelectStockImage(stockImage) { + if (this.$refs?.upload_image) { + this.$refs.upload_image.value = null; + } + this.selectedStockImage = stockImage; + this.showStockImageSelector = false; + this.deletingPreviewImage = false; + }, }, async mounted() { this.loadingInstance = true; await this.loadUnitInstance(); this.loadingInstance = false; this.initData(); - } -} + }, +}; </script> diff --git a/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue b/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue index 7c9de01..31c6c01 100644 --- a/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue +++ b/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue @@ -17,7 +17,7 @@ <h1> <a :href="chapterUrl" - :title="$gettextInterpolate('%{ pageTitle } öffnen', { pageTitle: selected.name })" + :title="$gettextInterpolate('%{ pageTitle } öffnen', { pageTitle: selected.name }, true)" > {{ selected.name }} </a> |
